@plumeria/init

View as Markdown

@plumeria/init sets Plumeria up in a project that already exists. There are eight packages and a plugin per bundler; the choice between them is not a judgement, it is a detection, so the command makes it.

Terminal
npx @plumeria/init

It reads the project, asks what it cannot read, shows the plan, and only then writes. Nothing is installed or patched before you answer.

Terminal
 detected  next  pnpm  typescript

  ➡︎ install  @plumeria/core @plumeria/next-plugin @plumeria/eslint-plugin oxlint
  + write    plumeria.d.ts     classStyle
  ~ patch    next.config.ts    withPlumeria(...)
  ~ patch    eslint.config.ts  plumeria.configs.recommended
  ~ patch    package.json      build: plumerialint -- next build

Apply? (Y/n)

Commit before running, so the writes can be reverted with git checkout.

What it detects

The package manager comes from packageManager, or from the lockfile when that field is absent. Both are looked for upwards from the directory init runs in, so a package inside a workspace is installed with the manager the workspace root uses rather than with npm.

The bundler comes from the dependencies, and from the config files when nothing is installed yet — next, vite, astro, webpack, rspack, rollup, rolldown, esbuild, farm and bun. Next.js is read before the bundler Next.js carries, so a Next project gets @plumeria/next-plugin; everything else gets @plumeria/unplugin with the factory that matches. Pass --bundler <name> to decide it yourself.

What it asks

Which spelling of a two-named property does this project write?

Terminal
  1: logical   marginBlockStart, insetInlineStart
  2: physical  marginTop, left
  3: both      no policy

One answer reaches both levels. The bundler plugin receives withoutPhysicalProperties or withoutLogicalProperties, so the counterpart cannot compile, and ESLint receives @plumeria/no-physical-properties or @plumeria/no-logical-properties, so it is reported while you type. They are the same choice made twice, and setting them from one answer is what keeps their directions from disagreeing.

--sizes extends both to the size axis, where width and inlineSize are the pair.

On a codebase that already has styles, answer 3 and follow Optional rules instead: the rule offers a rename on every occurrence, which is what a migration needs, and the build option belongs afterwards.

Expand a border shorthand into the three declarations it sets?

Turns on @plumeria/expand-border-shorthands. A border shorthand crosses the axis shorthands without either containing the other, so specificity cannot rank the pair and the outcome depends on the order it was written in. Expanding removes the shape. On by default.

Which JSX prop carries styles?

classStyle unless you name another. A renamed prop is written into the plugin as styleProp and declared in plumeria.d.ts at the same time, so the two cannot disagree — a mismatch type-checks but is never compiled away.

Set up @plumeria/eslint-plugin and the plumerialint build guard?

Adds the plugin and oxlint, writes or extends the flat config, and puts plumerialint -- in front of the build script, so a style error stops the build before the bundler runs.

A renamed prop is rejected unless it is an identifier TypeScript can declare, and unless React leaves the name free — className, style, key, ref and children are refused, from the prompt and from --style-prop alike.

What it writes

FileWhat lands in it
plumeria.d.tsthe styling prop TypeScript reads
the bundler configthe plugin, and the options the answers chose
eslint.config.tsplumeria.configs.recommended and the rules that were asked for
package.jsonplumerialint -- in front of the build script, and rimraf .next before dev and build on Next.js

An existing file is patched, not replaced. The import lands after the last import, the plugin joins the plugins array, and a Next config has its default export wrapped:

next.config.ts
import type { NextConfig } from 'next';
import { withPlumeria } from '@plumeria/next-plugin';

const nextConfig: NextConfig = {
  reactStrictMode: true,
};

export default nextConfig;
export default withPlumeria(nextConfig, { withoutLogicalProperties: true });
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import plumeria from '@plumeria/unplugin';

export default defineConfig({
  plugins: [react(), plumeria.vite({ withoutLogicalProperties: true })],
});

On Next.js a pre script clears the build cache, which is what keeps a version change from being read as a compile error. One is added for each of dev and build that the project actually has, so a project with no scripts gets none — and rimraf is only installed when one is added:

package.json
  "scripts": {
    "predev": "rimraf .next",
    "prebuild": "rimraf .next",
    "dev": "next dev",
    "build": "plumerialint -- next build"
  }

A predev or prebuild the project already wrote is left as it is. Clearing the cache is a Next.js concern rather than a lint one, so --no-eslint leaves it in place and drops only plumerialint.

What counts as already set up is the plugin being called, not merely imported. A bare plumeria.version, and a call inside a comment, a string or a regex literal, all read as not set up, because none of them registers anything; an import nothing calls is completed rather than skipped, and the binding the file already named is the one that gets called. An import spanning several lines is read as one statement, so it is never duplicated. A second run writes nothing, and a half-finished one is finished.

The plugins array it extends is the one the config object owns. A plugins belonging to a nested key — test for Vitest, say — is never written into. Where the plugin cannot be placed safely — a config whose own object has no plugins array, a build script rather than a config, a legacy .eslintrc — nothing is touched, the snippet is printed, and the command exits with 1.

Options

Terminal
npx @plumeria/init --dry-run            # show the plan, write nothing
npx @plumeria/init --yes                # take every default, ask nothing
npx @plumeria/init --physical --sizes   # answer the spelling question up front
npx @plumeria/init --style-prop sx
npx @plumeria/init --bundler rollup
npx @plumeria/init --no-eslint          # leave ESLint and the plumerialint guard out
npx @plumeria/init --no-install         # write the configs, print the command
npx @plumeria/init --cwd packages/app

Without a TTY it never asks — every unanswered question takes its default, which is what makes it usable in CI. Colour comes from node:util's styleText, which leaves the text alone when the output is not a terminal, so a piped or logged run carries no escape codes.

Installation

None. Run it with npx and it is gone again afterwards. Install it only if you want it pinned in the repo, where the command is plumeria-init:

Terminal
npm i --save-dev @plumeria/init
Terminal
yarn add -D @plumeria/init
Terminal
pnpm i --save-dev @plumeria/init

On this page