@twobirds/microcomponents 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -116,9 +116,9 @@ pnpm run cli -- create --namespace mc --dir mc-app
116
116
  # project "mc" created in mc-app/ — run: cd mc-app && pnpm install && pnpm dev
117
117
  ```
118
118
 
119
- `pnpm dev` runs `mc dev`: `tsc -w` plus a `node:http` server serving `build/`, so a saved component reaches the browser through `autoload()` with nothing bundled; companion assets are mirrored into `build/` next to the compiled component, and the library is resolved through an import map to the served `microcomponents.min.js`. `pnpm build` runs `tsc` then `mc bundle`, which emits one `bundle.js` into `public/` with every companion asset inlined by the CLI bundler, the concatenated `bundle.css`, and the production shell — a single script tag, no autoloader. `pnpm preview` serves `public/`. `pnpm typecheck` checks `src/` and `test/` in two contexts, and `pnpm test` runs the specs under vitest in jsdom, so component class code executes against a real DOM. `opencode.jsonc` registers the MCP server, so opencode picks it up when you open the project.
119
+ `pnpm dev` runs `mc dev`: `tsc -w` plus a `node:http` server serving `build/`, so a saved component reaches the browser through `autoload()` with nothing bundled; companion assets and plain `.js` components are mirrored into `build/` next to the compiled component, and the server rewrites the `@twobirds/microcomponents` specifier in the JS it serves to the `/microcomponents.min.js` it answers, so the page carries no import map. `pnpm build` runs `tsc` then `mc bundle`, which emits one `bundle.js` into `public/` with every companion asset inlined by the CLI bundler, the concatenated `bundle.css`, and the production shell — a single script tag, no autoloader. `pnpm preview` serves `public/`. `pnpm typecheck` checks `src/` and `test/` in two contexts, and `pnpm test` runs the specs under vitest in jsdom, so component class code executes against a real DOM. `opencode.jsonc` registers the MCP server, so opencode picks it up when you open the project.
120
120
 
121
- The namespace (1 to 4 lowercase letters) becomes the `src/<namespace>/` folder and the first segment of every component tag, so `mc` is the home of tags like `<mc-app>`. Because the dev shell is the autoload base, `import.meta.url` + the tag-to-path rule resolves `<mc-app>` to `build/mc/app.js` — the compiled starter. New components are served automatically by the autoloader, but the bundle walks one entry, so add each one to `src/<namespace>/bundle.ts`. `--dir` defaults to the namespace, `--dry-run` prints the plan without touching disk.
121
+ The namespace (lowercase letters and digits, starting with a letter, any length) becomes the `src/<namespace>/` folder and the first segment of every component tag, so `mc` is the home of tags like `<mc-app>`. The project owns its namespace, so length is allowed; past 5 characters `mc create` warns and asks for confirmation, and `--yes` skips the prompt for scripted runs. Because the dev shell is the autoload base, `import.meta.url` + the tag-to-path rule resolves `<mc-app>` to `build/mc/app.js` — the compiled starter. New components are served automatically by the autoloader, but the bundle walks one entry, so add each one to `src/<namespace>/bundle.ts`. `--dir` defaults to the namespace, `--dry-run` prints the plan without touching disk.
122
122
 
123
123
  The project is written into a folder of its own, so its files are reported under that folder (`mc-app/src/mc/app.ts`) — the path you can find from the directory you ran the command in, not relative to the project root.
124
124
 
@@ -4,8 +4,9 @@
4
4
 
5
5
  The chain is `src/` → `build/` → `public/`. The scaffolded `build` script runs
6
6
  `tsc -p tsconfig.json` (so `build/` holds one `.js` per component at its
7
- autoload path), then `mc bundle` mirrors the companion assets into `build/`,
8
- walks the barrel entry named by `src/bundle.json` through {@link bundleEntry}
7
+ autoload path), then `mc bundle` mirrors the files `tsc` does not compile —
8
+ companion assets and plain `.js` components — into `build/`, walks the barrel
9
+ entry named by `src/bundle.json` through {@link bundleEntry}
9
10
  (inlining every `static assets`), rewrites the `@twobirds/microcomponents`
10
11
  specifier to the self-contained `microcomponents.min.js` shipped next to the
11
12
  bundle, concatenates the plain `.css` token presets into `bundle.css`, and
@@ -28,29 +29,35 @@ export interface BundleConfig {
28
29
  outDir?: string;
29
30
  }
30
31
  /**
31
- * Whether a file is a companion asset to mirror into `build/`.
32
+ * Whether a file in `src/` is copied verbatim into `build/`. Companion assets
33
+ * (`.css`, `.html`, `.json`) sit next to their compiled `.js`, and a plain `.js`
34
+ * component (`mc new --js`) is copied as it is: `tsc` compiles only `.ts`, so plain
35
+ * JS reaches the autoloader by copying, not by compiling. The page shells are not
36
+ * mirrored by this rule — `mc dev` copies the dev shell itself.
32
37
  *
33
38
  * @example
34
39
  * ```ts
35
- * isCompanion('app.target.html'); // true
36
- * isCompanion('index.html'); // false
40
+ * isMirrored('app.target.html'); // true
41
+ * isMirrored('app.js'); // true — a plain JS component
42
+ * isMirrored('index.html'); // false
37
43
  * ```
38
44
  * @param name Basename of the file.
39
45
  */
40
- export declare function isCompanion(name: string): boolean;
46
+ export declare function isMirrored(name: string): boolean;
41
47
  /**
42
- * Mirror companion assets (`.css`, `.html`, `.json`) from `src/` into `build/`,
43
- * preserving the tree, so the bundler reads each asset next to its compiled `.js`.
48
+ * Mirror the files `tsc` does not compile (`.css`, `.html`, `.json`, `.js`) from
49
+ * `src/` into `build/`, preserving the tree, so the bundler reads each asset and
50
+ * each plain JS component next to its compiled `.js`.
44
51
  *
45
52
  * @example
46
53
  * ```ts
47
- * await mirrorCompanions('<root>/src', '<root>/build');
54
+ * await mirrorSources('<root>/src', '<root>/build');
48
55
  * ```
49
56
  * @param srcDir Source root to walk.
50
57
  * @param buildDir Folder to write into.
51
58
  * @param rel Path of the walked directory relative to `srcDir`.
52
59
  */
53
- export declare function mirrorCompanions(srcDir: string, buildDir: string, rel?: string): Promise<void>;
60
+ export declare function mirrorSources(srcDir: string, buildDir: string, rel?: string): Promise<void>;
54
61
  /**
55
62
  * Collect the plain `.css` token presets under `src/`, skipping the companion
56
63
  * `.target.css` / `.shadow.css` files, which the bundler inlines into the JS.
@@ -67,14 +74,17 @@ export declare function collectPlainCss(srcDir: string, rel?: string, found?: st
67
74
  /**
68
75
  * Rewrite the library specifier in a bundled module to the self-contained bundle
69
76
  * shipped next to it, so the browser can resolve `@twobirds/microcomponents`
70
- * without a bare-specifier import map.
77
+ * without a bare-specifier import map. Subpath imports resolve to the same module,
78
+ * so the library stays one instance. This is the single source of truth for the
79
+ * rule: the bundler uses the relative target, the dev server passes its served URL.
71
80
  *
72
- * @example Turns the package specifier into a relative path.
81
+ * @example Turns the package specifier into a relative path, leaves others alone.
73
82
  * ```ts
74
83
  * rewriteLibrarySpecifier("import { DC } from '@twobirds/microcomponents';");
84
+ * rewriteLibrarySpecifier("import { z } from 'zod';"); // unchanged
75
85
  * ```
76
86
  * @param code Bundled module source.
77
- * @param target Relative specifier to point the import at.
87
+ * @param target Specifier to point the import at.
78
88
  */
79
89
  export declare function rewriteLibrarySpecifier(code: string, target?: string): string;
80
90
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/cli/bundle.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;GAeG;AAMH;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGjD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhG;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACpC,MAAM,EAAE,MAAM,EACd,GAAG,SAAK,EACR,KAAK,GAAE,MAAM,EAAO,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CAUnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAA6B,GAAG,MAAM,CAEjG;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAClC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D,OAAO,CAAC,MAAM,EAAE,CAAC,CAkCnB;AAED;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAC9B,WAAW,GAAE,MAAsB,EACnC,KAAK,GAAE,MAAuE,GAC5E,OAAO,CAAC,MAAM,CAAC,CAIjB"}
1
+ {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/cli/bundle.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;GAgBG;AAMH;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGhD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAY7F;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACpC,MAAM,EAAE,MAAM,EACd,GAAG,SAAK,EACR,KAAK,GAAE,MAAM,EAAO,GAClB,OAAO,CAAC,MAAM,EAAE,CAAC,CAUnB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAA6B,GAAG,MAAM,CAEjG;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAClC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D,OAAO,CAAC,MAAM,EAAE,CAAC,CAmCnB;AAED;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAC9B,WAAW,GAAE,MAAsB,EACnC,KAAK,GAAE,MAAuE,GAC5E,OAAO,CAAC,MAAM,CAAC,CAIjB"}
@@ -8,8 +8,10 @@ runtime autoload loader uses, so the scaffold and the runtime can never disagree
8
8
  where a component lives.
9
9
 
10
10
  The namespace is the first segment of every component tag, so `xxx` makes `src/xxx/`
11
- the home of tags like `<xxx-app>` (tagPath `xxx-app` → `xxx/app`). The generated
12
- environment follows the three-path chain the library's own demos use: `src/` holds the
11
+ the home of tags like `<xxx-app>` (tagPath `xxx-app` → `xxx/app`). The project owns its
12
+ namespace, so it can be any length: `isLongNamespace` flags the ones past
13
+ `LONG_NAMESPACE` (5) for the CLI to warn about and confirm before anything is written.
14
+ The generated environment follows the three-path chain the library's own demos use: `src/` holds the
13
15
  authored sources, `build/` receives the per-file `tsc` output that the dev server
14
16
  serves through `autoload()`, and `public/` receives the bundled production site. Because
15
17
  the dev shell is the autoload base, `import.meta.url` + `tagPath('xxx-app')` resolves to
@@ -18,17 +20,35 @@ the dev shell is the autoload base, `import.meta.url` + `tagPath('xxx-app')` res
18
20
  @module CLI/Project
19
21
  */
20
22
  /**
21
- Whether a string is a valid project namespace: 1 to 4 lowercase letters.
23
+ Whether a string is a valid project namespace: lowercase letters and digits, starting
24
+ with a letter, any length. The project owns its namespace, so length is the user's
25
+ call — {@link isLongNamespace} flags the ones worth confirming before writing.
22
26
 
23
27
  @example
24
28
  ```ts
25
29
  console.log(isValidNamespace('mc'));
26
- console.log(isValidNamespace('mcxyz'));
27
- console.log(isValidNamespace('toolong'));
30
+ console.log(isValidNamespace('microcomponents'));
31
+ console.log(isValidNamespace('Shop'));
28
32
  ```
29
33
  @param ns - Candidate namespace.
30
34
  */
31
35
  export declare function isValidNamespace(ns: string): boolean;
36
+ /** A namespace longer than this is confirmed before the project is written. */
37
+ export declare const LONG_NAMESPACE = 5;
38
+ /**
39
+ Whether a namespace is long enough to warn about. The namespace is the project's own
40
+ and the first segment of every component tag, so a long one lands in every tag, every
41
+ folder path, and every bundle import.
42
+
43
+ @example
44
+ ```ts
45
+ console.log(isLongNamespace('mc')); // false
46
+ console.log(isLongNamespace('shop')); // false
47
+ console.log(isLongNamespace('microcomponents')); // true
48
+ ```
49
+ @param ns - Candidate namespace.
50
+ */
51
+ export declare function isLongNamespace(ns: string): boolean;
32
52
  /** Result of {@link scaffoldProject}. */
33
53
  export interface ProjectResult {
34
54
  /** Files to write, keyed by path relative to the project root. */
@@ -45,9 +65,9 @@ const { namespace, files } = scaffoldProject('mc');
45
65
  console.log(namespace);
46
66
  console.log(Object.keys(files));
47
67
  ```
48
- @param namespace - 1 to 4 lowercase letters; becomes the `src/<namespace>/` folder and
49
- the first segment of every component tag.
50
- @throws If `namespace` is not 1 to 4 lowercase letters.
68
+ @param namespace - lowercase letters and digits, starting with a letter, any length;
69
+ becomes the `src/<namespace>/` folder and the first segment of every component tag.
70
+ @throws If `namespace` is not lowercase letters and digits starting with a letter.
51
71
  */
52
72
  export declare function scaffoldProject(namespace: string): ProjectResult;
53
73
  //# sourceMappingURL=create.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/cli/create.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;EAkBE;AAUF;;;;;;;;;;EAUE;AACF,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,yCAAyC;AACzC,MAAM,WAAW,aAAa;IAC7B,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;EAYE;AACF,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CA6BhE"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/cli/create.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;EAoBE;AAUF;;;;;;;;;;;;EAYE;AACF,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,+EAA+E;AAC/E,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC;;;;;;;;;;;;EAYE;AACF,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,yCAAyC;AACzC,MAAM,WAAW,aAAa;IAC7B,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;EAYE;AACF,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CA6BhE"}
package/dist/cli/dev.d.ts CHANGED
@@ -3,12 +3,14 @@
3
3
 
4
4
  A minimal `node:http` static server rooted at the project's `build/` folder, so
5
5
  a saved component reaches the browser through `autoload()` with no bundling. It
6
- mirrors companion assets from `src/` into `build/` (on startup and on every
7
- change), runs `tsc -w` for the lifetime of the server, serves companion
6
+ mirrors the files `tsc` does not compile — companion assets and plain `.js`
7
+ components — from `src/` into `build/` (on startup and on every change), runs
8
+ `tsc -w` for the lifetime of the server, serves companion
8
9
  `.css` / `.html` / `.json` as raw text (so `loadAssets()` receives markup it
9
- can use), redirects `/` to the autoload shell (`index-dev.html`), and serves
10
- the self-contained library at `/microcomponents.min.js` — the target of the
11
- import map in the dev shell.
10
+ can use), redirects `/` to the autoload shell (`index-dev.html`), serves the
11
+ self-contained library at `/microcomponents.min.js`, and rewrites the
12
+ `@twobirds/microcomponents` specifier in served JS to that URL, so a compiled
13
+ component resolves the library with no import map in the page.
12
14
 
13
15
  There is no HMR: a page reload picks up the recompiled component.
14
16
 
@@ -45,8 +47,9 @@ export declare function mimeFor(name: string): string;
45
47
  */
46
48
  export declare function resolveTarget(url: string, buildRoot: string, minJs: string): string | undefined;
47
49
  /**
48
- * Mirror companion assets from `src/` into `build/`, preserving the tree, so the
49
- * served folder stays autoload-ready.
50
+ * Mirror the files `tsc` does not compile — companion assets and plain `.js`
51
+ * components — from `src/` into `build/`, preserving the tree, so the served
52
+ * folder stays autoload-ready.
50
53
  *
51
54
  * @example
52
55
  * ```ts
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AASjG;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO5C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO/F;AA8BD;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC9B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACtC,MAAM,CAKR;AAED;;;;;;;;;;GAUG;AAEH,wBAAsB,MAAM,CAC3B,WAAW,GAAE,MAAsB,EACnC,KAAK,GAAE,MAAuE,EAC9E,IAAI,GAAE,MAAa,EACnB,IAAI,GAAE,MAAoB,GACxB,OAAO,CAAC,MAAM,CAAC,CAiCjB"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAsC,KAAK,MAAM,EAAuB,MAAM,WAAW,CAAC;AASjG;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO5C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO/F;AAkCD;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC9B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACtC,MAAM,CAKR;AAED;;;;;;;;;;GAUG;AAEH,wBAAsB,MAAM,CAC3B,WAAW,GAAE,MAAsB,EACnC,KAAK,GAAE,MAAuE,EAC9E,IAAI,GAAE,MAAa,EACnB,IAAI,GAAE,MAAoB,GACxB,OAAO,CAAC,MAAM,CAAC,CAiCjB"}
@@ -15,6 +15,8 @@ export type Runner = (script: string, options?: {
15
15
  port?: number;
16
16
  host?: string;
17
17
  }) => Promise<number>;
18
+ /** Asks the user to confirm a namespace long enough to be worth a second look. */
19
+ export type Confirmer = (namespace: string) => Promise<boolean>;
18
20
  /**
19
21
  Runs a package.json script through pnpm and resolves with its exit code.
20
22
 
@@ -35,7 +37,8 @@ export declare function runScript(script: string, options?: {
35
37
  /**
36
38
  Parses `argv` and routes it to the matching command, returning a process exit
37
39
  code. `run` is injectable so the script-wrapping commands can be tested without
38
- spawning anything.
40
+ spawning anything, and `confirm` is injectable so the long-namespace prompt can be
41
+ tested without a terminal.
39
42
 
40
43
  @example
41
44
  ```ts
@@ -48,6 +51,9 @@ const code = await main(['clean'], async (script) => {
48
51
  ```ts
49
52
  const code = await main(['--version']);
50
53
  ```
54
+ @param argv - Arguments to route (usually `process.argv.slice(2)`).
55
+ @param run - Runs a package.json script (defaults to {@link runScript}).
56
+ @param confirm - Confirms a long namespace (defaults to the terminal prompt).
51
57
  */
52
- export declare function main(argv: string[], run?: Runner): Promise<number>;
58
+ export declare function main(argv: string[], run?: Runner, confirm?: Confirmer): Promise<number>;
53
59
  //# sourceMappingURL=dispatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/cli/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;EAYE;AAsEF,MAAM,MAAM,MAAM,GAAG,CACpB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,KACtC,OAAO,CAAC,MAAM,CAAC,CAAC;AAwHrB;;;;;;;;;;;;EAYE;AACF,wBAAsB,SAAS,CAC9B,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,MAAM,CAAC,CAajB;AAED;;;;;;;;;;;;;;;;EAgBE;AACF,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,GAAE,MAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8HnF"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/cli/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;EAYE;AAwEF,MAAM,MAAM,MAAM,GAAG,CACpB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,KACtC,OAAO,CAAC,MAAM,CAAC,CAAC;AAErB,kFAAkF;AAClF,MAAM,MAAM,SAAS,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAyIhE;;;;;;;;;;;;EAYE;AACF,wBAAsB,SAAS,CAC9B,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC,OAAO,CAAC,MAAM,CAAC,CAajB;AAED;;;;;;;;;;;;;;;;;;;;EAoBE;AACF,wBAAsB,IAAI,CACzB,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,GAAE,MAAkB,EACvB,OAAO,GAAE,SAA4B,GACnC,OAAO,CAAC,MAAM,CAAC,CA4IjB"}
package/dist/cli/index.js CHANGED
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
3
3
  import { parseArgs } from "node:util";
4
4
  import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
5
  import { existsSync, watch } from "node:fs";
6
+ import { createInterface } from "node:readline/promises";
6
7
  import path from "node:path";
7
8
  import { createServer } from "node:http";
8
9
  //#region src/utils/resolveComponentPath.ts
@@ -168,8 +169,10 @@ runtime autoload loader uses, so the scaffold and the runtime can never disagree
168
169
  where a component lives.
169
170
 
170
171
  The namespace is the first segment of every component tag, so `xxx` makes `src/xxx/`
171
- the home of tags like `<xxx-app>` (tagPath `xxx-app` → `xxx/app`). The generated
172
- environment follows the three-path chain the library's own demos use: `src/` holds the
172
+ the home of tags like `<xxx-app>` (tagPath `xxx-app` → `xxx/app`). The project owns its
173
+ namespace, so it can be any length: `isLongNamespace` flags the ones past
174
+ `LONG_NAMESPACE` (5) for the CLI to warn about and confirm before anything is written.
175
+ The generated environment follows the three-path chain the library's own demos use: `src/` holds the
173
176
  authored sources, `build/` receives the per-file `tsc` output that the dev server
174
177
  serves through `autoload()`, and `public/` receives the bundled production site. Because
175
178
  the dev shell is the autoload base, `import.meta.url` + `tagPath('xxx-app')` resolves to
@@ -182,18 +185,36 @@ function starterTag(namespace) {
182
185
  return `${namespace}-app`;
183
186
  }
184
187
  /**
185
- Whether a string is a valid project namespace: 1 to 4 lowercase letters.
188
+ Whether a string is a valid project namespace: lowercase letters and digits, starting
189
+ with a letter, any length. The project owns its namespace, so length is the user's
190
+ call — {@link isLongNamespace} flags the ones worth confirming before writing.
186
191
 
187
192
  @example
188
193
  ```ts
189
194
  console.log(isValidNamespace('mc'));
190
- console.log(isValidNamespace('mcxyz'));
191
- console.log(isValidNamespace('toolong'));
195
+ console.log(isValidNamespace('microcomponents'));
196
+ console.log(isValidNamespace('Shop'));
192
197
  ```
193
198
  @param ns - Candidate namespace.
194
199
  */
195
200
  function isValidNamespace(ns) {
196
- return /^[a-z]{1,4}$/.test(ns);
201
+ return /^[a-z][a-z0-9]*$/.test(ns);
202
+ }
203
+ /**
204
+ Whether a namespace is long enough to warn about. The namespace is the project's own
205
+ and the first segment of every component tag, so a long one lands in every tag, every
206
+ folder path, and every bundle import.
207
+
208
+ @example
209
+ ```ts
210
+ console.log(isLongNamespace('mc')); // false
211
+ console.log(isLongNamespace('shop')); // false
212
+ console.log(isLongNamespace('microcomponents')); // true
213
+ ```
214
+ @param ns - Candidate namespace.
215
+ */
216
+ function isLongNamespace(ns) {
217
+ return ns.length > 5;
197
218
  }
198
219
  /**
199
220
  Builds the files of a complete dev environment for a namespace.
@@ -204,12 +225,12 @@ const { namespace, files } = scaffoldProject('mc');
204
225
  console.log(namespace);
205
226
  console.log(Object.keys(files));
206
227
  ```
207
- @param namespace - 1 to 4 lowercase letters; becomes the `src/<namespace>/` folder and
208
- the first segment of every component tag.
209
- @throws If `namespace` is not 1 to 4 lowercase letters.
228
+ @param namespace - lowercase letters and digits, starting with a letter, any length;
229
+ becomes the `src/<namespace>/` folder and the first segment of every component tag.
230
+ @throws If `namespace` is not lowercase letters and digits starting with a letter.
210
231
  */
211
232
  function scaffoldProject(namespace) {
212
- if (!isValidNamespace(namespace)) throw new Error(`invalid namespace "${namespace}": must be 1 to 4 lowercase letters (e.g. "mc")`);
233
+ if (!isValidNamespace(namespace)) throw new Error(`invalid namespace "${namespace}": must be lowercase letters and digits, starting with a letter (e.g. "mc")`);
213
234
  const tag = starterTag(namespace);
214
235
  const base = tagPath(tag);
215
236
  const files = {};
@@ -246,7 +267,7 @@ function packageJson(namespace) {
246
267
  typecheck: "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
247
268
  test: "vitest run"
248
269
  },
249
- dependencies: { "@twobirds/microcomponents": "^0.12.1" },
270
+ dependencies: { "@twobirds/microcomponents": "^0.13.0" },
250
271
  devDependencies: {
251
272
  typescript: "^6.0.3",
252
273
  vitest: "^4.1.10",
@@ -354,7 +375,7 @@ function opencodeConfig() {
354
375
  function componentSkill(namespace, tag) {
355
376
  return `---
356
377
  name: components
357
- description: Use when adding, editing, naming, or wiring a micro component in this project, registering a component in the bundle, choosing an event dispatch target, running dev or build, or touching build/ or public/.
378
+ description: Use when adding, editing, naming, or wiring a micro component in this project, registering a component in the bundle, choosing an event dispatch target, binding state or form values in a component, running dev or build, or touching build/ or public/.
358
379
  ---
359
380
 
360
381
  # Micro Components project
@@ -366,11 +387,13 @@ Namespace \`${namespace}\`. Every tag starts with \`${namespace}-\`. Components
366
387
  \`src/\` → \`build/\` → \`public/\`
367
388
 
368
389
  - \`src/\` is the only folder to edit.
369
- - \`build/\` is the \`tsc\` output plus mirrored companion assets, served by the dev server. Generated.
390
+ - \`build/\` is the \`tsc\` output plus the files \`tsc\` does not compile — companion assets and plain \`.js\` components — served by the dev server. Generated.
370
391
  - \`public/\` is the production site: \`bundle.js\` with companion assets inlined, \`bundle.css\`, \`index.html\`. Generated.
371
392
 
372
393
  Never edit, rename, or delete anything under \`build/\` or \`public/\`. Change \`src/\` and rebuild.
373
394
 
395
+ Never write an import map. \`autoload()\` fetches a component by its tag (\`ns-name\` → \`./ns/name.js\`), and the dev server and \`mc bundle\` resolve the \`@twobirds/microcomponents\` specifier, so the page never lists imports.
396
+
374
397
  ## Add a component
375
398
 
376
399
  \`\`\`sh
@@ -379,6 +402,8 @@ npx mc new ${namespace}-name --target-html --target-css
379
402
 
380
403
  The tag must be kebab-case with at least one hyphen. \`ns-name\` becomes \`src/ns/name.ts\`: the first hyphen becomes the folder, which is the same rule the autoloader uses.
381
404
 
405
+ \`--js\` writes \`src/ns/name.js\` instead of \`.ts\`. \`tsc\` compiles only \`.ts\`, so the dev server and \`mc bundle\` copy a plain \`.js\` component to \`build/\` verbatim — it autoloads and bundles like a compiled component, with no type checking and no \`.d.ts\`.
406
+
382
407
  Register the component so the production bundle picks it up:
383
408
 
384
409
  \`\`\`ts
@@ -405,18 +430,26 @@ A camelCase descriptor maps to a dotted filename and is listed in \`static asset
405
430
 
406
431
  The loader fetches exactly the listed descriptors, so every entry in \`static assets\` needs its file.
407
432
 
433
+ ## State
434
+
435
+ Inside a component, use the plugin methods. \`observePlugin(DC)\` adds \`this.notify()\`, \`this.subscribe()\`, \`this.bind()\`; \`formValuePlugin(DC)\` adds \`this.formValues()\` and pulls in the observe methods. Call the plugin once at module level, then chain on \`this\` with the property *name*: \`this.subscribe('data', …)\`.
436
+
437
+ The pure \`observe()\` and \`formValues()\` functions are for code outside the MC system — vanilla JS, framework lifecycle code, Node. Never call them inside a \`DC\`: the plugin methods create the same wrapper, chain, and keep the \`keyof this & string\` typing contract.
438
+
408
439
  ## Events
409
440
 
410
441
  Prefer a direct target with the default local dispatch and a typed payload:
411
442
 
412
443
  \`\`\`ts
413
444
  target.dispatchEvent(
414
- new CustomEvent('${namespace}:action', { detail: { data: { id: 'x' } } })
445
+ new CustomEvent('addToCart', { detail: { data: { id: 'x' } } })
415
446
  );
416
447
  \`\`\`
417
448
 
418
449
  Bubble up or down the tree only when the receiver cannot be named.
419
450
 
451
+ Event names are plain inside the project — \`add\`, \`toggle\`, \`addToCart\`. Namespace a name (\`${namespace}:addToCart\`) only when a collision is likely: an event entering the project from outside (a framework, a page script, a third-party addon DC), or several DCs attached to one component dispatching the same name. One producer per name means no collision, so no namespace.
452
+
420
453
  ## Verify
421
454
 
422
455
  \`\`\`sh
@@ -537,11 +570,8 @@ function devShell(namespace, tag) {
537
570
  <meta charset="UTF-8" />
538
571
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
539
572
  <title>${namespace} App</title>
540
- <script type="importmap">
541
- { "imports": { "@twobirds/microcomponents": "/microcomponents.min.js" } }
542
- <\/script>
543
573
  <script type="module">
544
- import * as m from '@twobirds/microcomponents';
574
+ import * as m from '/microcomponents.min.js';
545
575
  m.autoload(true);
546
576
  Object.assign(window, m);
547
577
  <\/script>
@@ -700,12 +730,12 @@ namespace under a project root, creating nested folders as needed.
700
730
  const written = await writeProject('mc', 'mc-app');
701
731
  console.log(written);
702
732
  ```
703
- @param namespace - 1 to 4 lowercase letters.
733
+ @param namespace - lowercase letters and digits, starting with a letter, any length.
704
734
  @param root - Directory to create the project in (e.g. `mc-app`).
705
735
  @param options - Whether to overwrite existing files.
706
736
  @returns The written file paths, relative to `root`.
707
- @throws If `namespace` is not 1 to 4 lowercase letters, or a target file exists and
708
- `force` is false.
737
+ @throws If `namespace` is not lowercase letters and digits starting with a letter, or
738
+ a target file exists and `force` is false.
709
739
  */
710
740
  async function writeProject(namespace, root, options = {}) {
711
741
  const { files } = scaffoldProject(namespace);
@@ -1150,8 +1180,9 @@ the `tsc`-emitted `build/` tree, with no Vite.
1150
1180
 
1151
1181
  The chain is `src/` → `build/` → `public/`. The scaffolded `build` script runs
1152
1182
  `tsc -p tsconfig.json` (so `build/` holds one `.js` per component at its
1153
- autoload path), then `mc bundle` mirrors the companion assets into `build/`,
1154
- walks the barrel entry named by `src/bundle.json` through {@link bundleEntry}
1183
+ autoload path), then `mc bundle` mirrors the files `tsc` does not compile —
1184
+ companion assets and plain `.js` components — into `build/`, walks the barrel
1185
+ entry named by `src/bundle.json` through {@link bundleEntry}
1155
1186
  (inlining every `static assets`), rewrites the `@twobirds/microcomponents`
1156
1187
  specifier to the self-contained `microcomponents.min.js` shipped next to the
1157
1188
  bundle, concatenates the plain `.css` token presets into `bundle.css`, and
@@ -1160,45 +1191,52 @@ copies the production shell. The result is a complete static site: a single
1160
1191
 
1161
1192
  @module CLI/Bundle
1162
1193
  */
1163
- /** Companion asset extensions copied verbatim next to their compiled `.js`. */
1164
- var COMPANION_EXTENSIONS = [
1194
+ /** Files `tsc` does not compile, copied verbatim next to the compiled components. */
1195
+ var MIRROR_EXTENSIONS = [
1165
1196
  ".css",
1166
1197
  ".html",
1167
- ".json"
1198
+ ".json",
1199
+ ".js"
1168
1200
  ];
1169
1201
  /**
1170
- * Whether a file is a companion asset to mirror into `build/`.
1202
+ * Whether a file in `src/` is copied verbatim into `build/`. Companion assets
1203
+ * (`.css`, `.html`, `.json`) sit next to their compiled `.js`, and a plain `.js`
1204
+ * component (`mc new --js`) is copied as it is: `tsc` compiles only `.ts`, so plain
1205
+ * JS reaches the autoloader by copying, not by compiling. The page shells are not
1206
+ * mirrored by this rule — `mc dev` copies the dev shell itself.
1171
1207
  *
1172
1208
  * @example
1173
1209
  * ```ts
1174
- * isCompanion('app.target.html'); // true
1175
- * isCompanion('index.html'); // false
1210
+ * isMirrored('app.target.html'); // true
1211
+ * isMirrored('app.js'); // true — a plain JS component
1212
+ * isMirrored('index.html'); // false
1176
1213
  * ```
1177
1214
  * @param name Basename of the file.
1178
1215
  */
1179
- function isCompanion(name) {
1216
+ function isMirrored(name) {
1180
1217
  if (name === "index.html" || name === "index-dev.html") return false;
1181
- return COMPANION_EXTENSIONS.some((ext) => name.endsWith(ext));
1218
+ return MIRROR_EXTENSIONS.some((ext) => name.endsWith(ext));
1182
1219
  }
1183
1220
  /**
1184
- * Mirror companion assets (`.css`, `.html`, `.json`) from `src/` into `build/`,
1185
- * preserving the tree, so the bundler reads each asset next to its compiled `.js`.
1221
+ * Mirror the files `tsc` does not compile (`.css`, `.html`, `.json`, `.js`) from
1222
+ * `src/` into `build/`, preserving the tree, so the bundler reads each asset and
1223
+ * each plain JS component next to its compiled `.js`.
1186
1224
  *
1187
1225
  * @example
1188
1226
  * ```ts
1189
- * await mirrorCompanions('<root>/src', '<root>/build');
1227
+ * await mirrorSources('<root>/src', '<root>/build');
1190
1228
  * ```
1191
1229
  * @param srcDir Source root to walk.
1192
1230
  * @param buildDir Folder to write into.
1193
1231
  * @param rel Path of the walked directory relative to `srcDir`.
1194
1232
  */
1195
- async function mirrorCompanions(srcDir, buildDir, rel = "") {
1233
+ async function mirrorSources(srcDir, buildDir, rel = "") {
1196
1234
  for (const entry of await readdir(path.join(srcDir, rel), { withFileTypes: true })) {
1197
1235
  const next = rel ? path.join(rel, entry.name) : entry.name;
1198
1236
  if (entry.isDirectory()) {
1199
1237
  if (entry.name === "node_modules") continue;
1200
- await mirrorCompanions(srcDir, buildDir, next);
1201
- } else if (isCompanion(entry.name)) {
1238
+ await mirrorSources(srcDir, buildDir, next);
1239
+ } else if (isMirrored(entry.name)) {
1202
1240
  const dst = path.join(buildDir, next);
1203
1241
  await mkdir(path.dirname(dst), { recursive: true });
1204
1242
  await cp(path.join(srcDir, next), dst);
@@ -1229,17 +1267,20 @@ async function collectPlainCss(srcDir, rel = "", found = []) {
1229
1267
  /**
1230
1268
  * Rewrite the library specifier in a bundled module to the self-contained bundle
1231
1269
  * shipped next to it, so the browser can resolve `@twobirds/microcomponents`
1232
- * without a bare-specifier import map.
1270
+ * without a bare-specifier import map. Subpath imports resolve to the same module,
1271
+ * so the library stays one instance. This is the single source of truth for the
1272
+ * rule: the bundler uses the relative target, the dev server passes its served URL.
1233
1273
  *
1234
- * @example Turns the package specifier into a relative path.
1274
+ * @example Turns the package specifier into a relative path, leaves others alone.
1235
1275
  * ```ts
1236
1276
  * rewriteLibrarySpecifier("import { DC } from '@twobirds/microcomponents';");
1277
+ * rewriteLibrarySpecifier("import { z } from 'zod';"); // unchanged
1237
1278
  * ```
1238
1279
  * @param code Bundled module source.
1239
- * @param target Relative specifier to point the import at.
1280
+ * @param target Specifier to point the import at.
1240
1281
  */
1241
1282
  function rewriteLibrarySpecifier(code, target = "./microcomponents.min.js") {
1242
- return code.replace(/from\s*['"]@twobirds\/microcomponents['"]/g, `from '${target}'`);
1283
+ return code.replace(/(['"])@twobirds\/microcomponents(?:\/[^'"]*)?\1/g, `$1${target}$1`);
1243
1284
  }
1244
1285
  /**
1245
1286
  * Assemble the production site in `public/` from the compiled `build/` tree.
@@ -1259,7 +1300,7 @@ async function bundleProject(projectRoot, options) {
1259
1300
  const config = JSON.parse(await readFile(path.join(sourceRoot, "bundle.json"), "utf8"));
1260
1301
  const publicRoot = path.join(projectRoot, options.outDir ?? config.outDir ?? "public");
1261
1302
  const fileName = config.fileName ?? "bundle.js";
1262
- await mirrorCompanions(sourceRoot, buildRoot);
1303
+ await mirrorSources(sourceRoot, buildRoot);
1263
1304
  const rewritten = rewriteLibrarySpecifier(await bundleEntry((options.bundleEntry ?? config.entry).replace(/\.ts$/, ".js"), buildRoot, true));
1264
1305
  await mkdir(publicRoot, { recursive: true });
1265
1306
  await writeFile(path.join(publicRoot, fileName), rewritten, "utf8");
@@ -1295,12 +1336,14 @@ Vite-free dev server for `mc dev`.
1295
1336
 
1296
1337
  A minimal `node:http` static server rooted at the project's `build/` folder, so
1297
1338
  a saved component reaches the browser through `autoload()` with no bundling. It
1298
- mirrors companion assets from `src/` into `build/` (on startup and on every
1299
- change), runs `tsc -w` for the lifetime of the server, serves companion
1339
+ mirrors the files `tsc` does not compile — companion assets and plain `.js`
1340
+ components — from `src/` into `build/` (on startup and on every change), runs
1341
+ `tsc -w` for the lifetime of the server, serves companion
1300
1342
  `.css` / `.html` / `.json` as raw text (so `loadAssets()` receives markup it
1301
- can use), redirects `/` to the autoload shell (`index-dev.html`), and serves
1302
- the self-contained library at `/microcomponents.min.js` — the target of the
1303
- import map in the dev shell.
1343
+ can use), redirects `/` to the autoload shell (`index-dev.html`), serves the
1344
+ self-contained library at `/microcomponents.min.js`, and rewrites the
1345
+ `@twobirds/microcomponents` specifier in served JS to that URL, so a compiled
1346
+ component resolves the library with no import map in the page.
1304
1347
 
1305
1348
  There is no HMR: a page reload picks up the recompiled component.
1306
1349
 
@@ -1362,8 +1405,9 @@ async function serve(req, res, buildRoot, minJs) {
1362
1405
  }
1363
1406
  try {
1364
1407
  const text = await readFile(abs, "utf-8");
1408
+ const body = abs.endsWith(".js") ? rewriteLibrarySpecifier(text, "/microcomponents.min.js") : text;
1365
1409
  res.setHeader("Content-Type", mimeFor(path.basename(abs)));
1366
- res.end(text);
1410
+ res.end(body);
1367
1411
  } catch {
1368
1412
  /* v8 ignore start — resolveTarget already checked existence; a read failure is a race */
1369
1413
  res.statusCode = 404;
@@ -1371,8 +1415,9 @@ async function serve(req, res, buildRoot, minJs) {
1371
1415
  }
1372
1416
  }
1373
1417
  /**
1374
- * Mirror companion assets from `src/` into `build/`, preserving the tree, so the
1375
- * served folder stays autoload-ready.
1418
+ * Mirror the files `tsc` does not compile — companion assets and plain `.js`
1419
+ * components — from `src/` into `build/`, preserving the tree, so the served
1420
+ * folder stays autoload-ready.
1376
1421
  *
1377
1422
  * @example
1378
1423
  * ```ts
@@ -1387,7 +1432,7 @@ async function mirrorInto(sourceRoot, buildRoot, rel = "") {
1387
1432
  const next = rel ? path.join(rel, entry.name) : entry.name;
1388
1433
  if (entry.isDirectory()) {
1389
1434
  if (entry.name !== "node_modules") await mirrorInto(sourceRoot, buildRoot, next);
1390
- } else if (isCompanion(entry.name) || entry.name === "index-dev.html") {
1435
+ } else if (isMirrored(entry.name) || entry.name === "index-dev.html") {
1391
1436
  const dst = path.join(buildRoot, next);
1392
1437
  await mkdir(path.dirname(dst), { recursive: true });
1393
1438
  await cp(path.join(sourceRoot, next), dst);
@@ -1441,7 +1486,7 @@ async function runDev(projectRoot = process.cwd(), minJs = path.join(import.meta
1441
1486
  watch(sourceRoot, { recursive: true }, (_event, file) => {
1442
1487
  if (!file || file.endsWith(".ts")) return;
1443
1488
  const abs = path.join(sourceRoot, file);
1444
- if (isCompanion(path.basename(file)) || file.endsWith("index-dev.html")) {
1489
+ if (isMirrored(path.basename(file)) || file.endsWith("index-dev.html")) {
1445
1490
  const dst = path.join(buildRoot, file);
1446
1491
  mkdir(path.dirname(dst), { recursive: true }).then(() => cp(abs, dst));
1447
1492
  }
@@ -1568,7 +1613,7 @@ var USAGE = `mc — Micro Components build tools
1568
1613
 
1569
1614
  Usage:
1570
1615
  mc new <tag> [--css] [--html] [--target-html] [--shadow-html] [--target-css] [--shadow-css] [--js] [--dir <path>] [--force] [--dry-run]
1571
- mc create --namespace <ns> [--dir <path>] [--force] [--dry-run]
1616
+ mc create --namespace <ns> [--dir <path>] [--force] [--yes] [--dry-run]
1572
1617
  mc dev [--port <n>] [--host <addr>]
1573
1618
  mc bundle
1574
1619
  mc preview [--port <n>] [--host <addr>]
@@ -1601,6 +1646,7 @@ Other options:
1601
1646
  --host <addr> dev / preview bind address (default: 127.0.0.1 — loopback only; pass
1602
1647
  0.0.0.0 or a interface address to serve the LAN)
1603
1648
  --force overwrite files that already exist
1649
+ --yes skip the confirmation when a namespace is longer than 5 characters (-y)
1604
1650
  --dry-run print what would be written; touch nothing on disk
1605
1651
  --version print the version of the package the CLI ships in (-v)
1606
1652
  --help print this text (-h)
@@ -1618,6 +1664,25 @@ var ASSET_FLAGS = {
1618
1664
  "shadow-css": "shadowCss"
1619
1665
  };
1620
1666
  /**
1667
+ Asks on the terminal whether to continue with a long namespace. `y`/`yes`
1668
+ (case-insensitive) continue; anything else, including an unreadable stdin, aborts.
1669
+
1670
+ @example
1671
+ ```ts
1672
+ const ok = await confirmNamespace('microcomponents');
1673
+ ```
1674
+ @param namespace - The namespace to confirm.
1675
+ */
1676
+ async function confirmNamespace(namespace) {
1677
+ const rl = createInterface({
1678
+ input: process.stdin,
1679
+ output: process.stdout
1680
+ });
1681
+ const answer = await rl.question(`continue with "${namespace}"? [y/N] `);
1682
+ rl.close();
1683
+ return /^y(es)?$/i.test(answer.trim());
1684
+ }
1685
+ /**
1621
1686
  A scaffolded file as it appears from the current directory: the file `rel` under
1622
1687
  `root`, resolved and made readable. `mc create` writes a project into a folder of
1623
1688
  its own, so `src/mc/app.ts` under `mc-app` prints as `mc-app/src/mc/app.ts` — the
@@ -1764,7 +1829,8 @@ async function runScript(script, options) {
1764
1829
  /**
1765
1830
  Parses `argv` and routes it to the matching command, returning a process exit
1766
1831
  code. `run` is injectable so the script-wrapping commands can be tested without
1767
- spawning anything.
1832
+ spawning anything, and `confirm` is injectable so the long-namespace prompt can be
1833
+ tested without a terminal.
1768
1834
 
1769
1835
  @example
1770
1836
  ```ts
@@ -1777,8 +1843,11 @@ return 0;
1777
1843
  ```ts
1778
1844
  const code = await main(['--version']);
1779
1845
  ```
1846
+ @param argv - Arguments to route (usually `process.argv.slice(2)`).
1847
+ @param run - Runs a package.json script (defaults to {@link runScript}).
1848
+ @param confirm - Confirms a long namespace (defaults to the terminal prompt).
1780
1849
  */
1781
- async function main(argv, run = runScript) {
1850
+ async function main(argv, run = runScript, confirm = confirmNamespace) {
1782
1851
  const { values, positionals } = parseArgs({
1783
1852
  args: argv,
1784
1853
  allowPositionals: true,
@@ -1795,6 +1864,10 @@ async function main(argv, run = runScript) {
1795
1864
  port: { type: "string" },
1796
1865
  host: { type: "string" },
1797
1866
  force: { type: "boolean" },
1867
+ yes: {
1868
+ type: "boolean",
1869
+ short: "y"
1870
+ },
1798
1871
  "dry-run": { type: "boolean" },
1799
1872
  help: {
1800
1873
  type: "boolean",
@@ -1854,6 +1927,13 @@ async function main(argv, run = runScript) {
1854
1927
  console.error("mc create: missing --namespace (e.g. mc create --namespace mc)");
1855
1928
  return 1;
1856
1929
  }
1930
+ if (isLongNamespace(namespace)) {
1931
+ console.warn(`mc create: namespace "${namespace}" is ${namespace.length} characters — every component tag in the project starts with it.`);
1932
+ if (!values["dry-run"] && !values.yes && !await confirm(namespace)) {
1933
+ console.error("mc create: aborted");
1934
+ return 1;
1935
+ }
1936
+ }
1857
1937
  const root = values.dir ?? namespace;
1858
1938
  let files;
1859
1939
  try {
@@ -55,12 +55,12 @@ namespace under a project root, creating nested folders as needed.
55
55
  const written = await writeProject('mc', 'mc-app');
56
56
  console.log(written);
57
57
  ```
58
- @param namespace - 1 to 4 lowercase letters.
58
+ @param namespace - lowercase letters and digits, starting with a letter, any length.
59
59
  @param root - Directory to create the project in (e.g. `mc-app`).
60
60
  @param options - Whether to overwrite existing files.
61
61
  @returns The written file paths, relative to `root`.
62
- @throws If `namespace` is not 1 to 4 lowercase letters, or a target file exists and
63
- `force` is false.
62
+ @throws If `namespace` is not lowercase letters and digits starting with a letter, or
63
+ a target file exists and `force` is false.
64
64
  */
65
65
  export declare function writeProject(namespace: string, root: string, options?: WriteOptions): Promise<string[]>;
66
66
  //# sourceMappingURL=writeScaffold.d.ts.map
@@ -14,7 +14,9 @@ event delegation is untouched by that.
14
14
  ## Talking to micro components from vanilla JS
15
15
 
16
16
  `on()` and `trigger()` are the two ends of the wire. A custom event name is prefixed
17
- with `_mc` automatically, so the same name works on both sides:
17
+ with `_mc` automatically, so the same name works on both sides. A project-level prefix
18
+ (`shop:CartUpdate`) is optional here, and worth adding when the dispatching code is not
19
+ yours — it keeps an unrelated `CartUpdate` from the page from reaching your handler:
18
20
 
19
21
  ```js
20
22
  import { on, trigger } from '@twobirds/microcomponents';
@@ -83,11 +85,18 @@ form.subscribe('email', (v) => console.log('email is now', v));
83
85
  formValues(form, 'email', document.querySelector('input[name=email]'));
84
86
  ```
85
87
 
88
+ These are the standalone APIs, for code with no `this` to attach to. Inside a `DC` the
89
+ plugin methods are the API: `observePlugin(DC)` gives `this.subscribe()` / `this.bind()` /
90
+ `this.notify()`, and `formValuePlugin(DC)` gives `this.formValues()` — never call the pure
91
+ functions inside a component.
92
+
86
93
  ## No-build pages
87
94
 
88
95
  `autoload(true)` makes a page load its own components: an undefined tag in the markup
89
96
  resolves to a script URL by the tag-to-path rule (`<shop-header>` → `shop/header.js`)
90
- and is fetched on demand. A dev page needs only an import map and one call.
97
+ and is fetched on demand. A dev page needs one call and one import of the library URL
98
+ the dev server serves — no import map, because the toolchain resolves the
99
+ `@twobirds/microcomponents` specifier for it.
91
100
 
92
101
  ## Implementation priority
93
102
 
@@ -1 +1 @@
1
- {"version":3,"file":"api-external.d.ts","sourceRoot":"","sources":["../../src/docs/api-external.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmGE"}
1
+ {"version":3,"file":"api-external.d.ts","sourceRoot":"","sources":["../../src/docs/api-external.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4GE"}
@@ -101,7 +101,7 @@ mc check
101
101
  Reports the version of the installed library, not the version of the project it runs in.
102
102
 
103
103
  ```sh
104
- mc --version # 0.12.1
104
+ mc --version # 0.13.0
105
105
  ```
106
106
 
107
107
  ## mc --help
@@ -4,8 +4,11 @@
4
4
 
5
5
  `mc create` scaffolds a complete dev environment for a namespace, following the
6
6
  three-path chain the library's own demos use. Run it straight with `npx` — no install.
7
- The namespace (1 to 4 lowercase letters) becomes the `src/<ns>/` folder and the first
8
- segment of every component tag, so `shop` is the home of tags like `<shop-app>`.
7
+ The namespace (lowercase letters and digits, starting with a letter, any length)
8
+ becomes the `src/<ns>/` folder and the first segment of every component tag, so `shop`
9
+ is the home of tags like `<shop-app>`. The project owns its namespace, so length is
10
+ allowed; past 5 characters `mc create` warns and asks for confirmation (`--yes` skips
11
+ the prompt).
9
12
 
10
13
  ```sh
11
14
  npx @twobirds/microcomponents create --namespace shop
@@ -69,6 +72,8 @@ Companion asset flags:
69
72
  | `--html` | html | unvariant light-DOM template |
70
73
 
71
74
  `--js` emits plain JavaScript for rapid prototyping; `--dry-run` prints the plan without
72
- touching disk; `--force` overwrites existing files.
75
+ touching disk; `--force` overwrites existing files. A plain `.js` component is copied to
76
+ `build/` verbatim by `mc dev` and `mc bundle` — `tsc` compiles only `.ts`, so it gets no
77
+ type checking and no `.d.ts`, and it autoloads and bundles like a compiled component.
73
78
  */
74
79
  //# sourceMappingURL=installation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"installation.d.ts","sourceRoot":"","sources":["../../src/docs/installation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2BE;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4CE"}
1
+ {"version":3,"file":"installation.d.ts","sourceRoot":"","sources":["../../src/docs/installation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8BE;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8CE"}
package/dist/mcp/api.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "libraryVersion": "0.12.1",
2
+ "libraryVersion": "0.13.0",
3
3
  "exportedCount": 59,
4
4
  "entries": {
5
5
  "MC": {
@@ -45,6 +45,26 @@ Pass an object, not a bare string, so the receiver reads named fields (`ev.detai
45
45
  and the flow stays readable as the payload grows. Declare the events on the component's event
46
46
  map (`EM` on `DC`) so the payload type is inferred at both ends.
47
47
 
48
+ ## Naming events
49
+
50
+ Inside a project, event names are plain: `add`, `toggle`, `addToCart`. Nothing else in
51
+ the project dispatches them, so there is nothing to collide with and a namespace buys
52
+ nothing. The runtime's own `_mc` prefix (`getMcEventName`, `trueEventName`) is applied
53
+ for you — it marks the events the library dispatches (`_mcConnected`, `_mcFormReset`) so
54
+ they cannot collide with native names. It is not a project convention.
55
+
56
+ Namespace a name when a collision is likely:
57
+
58
+ - **From outside the project** — a framework, a page script, or a third-party addon DC
59
+ dispatching into your components. `shop:addToCart` keeps an unrelated `addToCart` from
60
+ landing on the wrong handler.
61
+ - **Several DCs in one component** — two DCs attached to the same host dispatching the
62
+ same name for different purposes. The namespace is the tie-breaker.
63
+
64
+ The decision is collision likelihood, not style. An i18n (DOM translation) addon DC in a
65
+ component is almost certainly the only translator there, so `translate` needs no
66
+ namespace; a generic `update` shared by three addons does.
67
+
48
68
  ## Payload & propagation
49
69
 
50
70
  MC-triggered custom events and lifecycle events are `McEvent<D>` — a `CustomEvent` whose `detail` is `{ data, control }`. The payload is always read from `ev.detail.data`, never `ev.data`.
@@ -0,0 +1,48 @@
1
+ Never write an import map in a Micro Components project. An import map is a table of
2
+ bare specifiers a page has to hand-list. In this library the system resolves imports
3
+ itself, in both delivery modes, so a page carrying an import map is doing the
4
+ system's job by hand.
5
+
6
+ ## Autoload mode
7
+
8
+ `autoload()` watches the DOM and fetches a component by its tag name. An undefined tag
9
+ in the markup becomes a script request: `<shop-header>` → `./shop/header.js`
10
+ (`resolveComponentPath`, the same rule `mc new` uses to place the file — see
11
+ [custom-element-registry](custom-element-registry.md) §Tag Name to File Path Mapping).
12
+ The page never imports a component, so an import map has nothing to describe: the tag
13
+ in the markup _is_ the import.
14
+
15
+ The library specifier is resolved by the toolchain, not by the page:
16
+
17
+ | Context | How `@twobirds/microcomponents` resolves |
18
+ | --------------------------- | ------------------------------------------------------------------------------------------------------ |
19
+ | Vite dev server (the demos) | `resolve.alias` to the library source, set in `demo/dev-server.ts` |
20
+ | `mc dev` | the server rewrites the specifier in the JS it serves to `/microcomponents.min.js` |
21
+ | `mc bundle` | `rewriteLibrarySpecifier` points the import at the `microcomponents.min.js` shipped next to the bundle |
22
+
23
+ The dev shell imports the library by URL (`/microcomponents.min.js`), so the page needs
24
+ no table of its own. `rewriteLibrarySpecifier` is the single source of truth for the
25
+ rewrite: the bundler passes the relative target, the dev server passes its served URL.
26
+
27
+ ## Bundled mode
28
+
29
+ `bundle.js` contains every component the barrel registers, with companion assets
30
+ inlined by `mc bundle`. Every tag is in the custom element registry before the page
31
+ renders, so a tag that appears in the DOM is upgraded immediately: its constructor
32
+ runs, its assets are already readable through `this.assets`, and `oneInit()` fires
33
+ synchronously. Nothing is fetched, so nothing is mapped.
34
+
35
+ A component missing from the barrel `src/<namespace>/bundle.ts` is the one gap: it
36
+ loads in dev through `autoload()` and is absent from `public/bundle.js`. Fix the
37
+ barrel, not the HTML.
38
+
39
+ ## The rule
40
+
41
+ - A component import is never written by hand. Autoload mode fetches it per tag;
42
+ bundled mode already contains it.
43
+ - The library specifier is resolved by the toolchain — Vite alias, dev-server rewrite,
44
+ bundler rewrite — never by an import map in the page.
45
+ - If a page needs an import map, a resolution step is missing. Fix the server, the
46
+ alias, or the bundle. Do not add a `<script type="importmap">`.
47
+
48
+ See [project-layout](project-layout.md) for the two shells that carry these modes.
@@ -8,8 +8,9 @@ src/ → (tsc) → build/ → (bundle) → public/
8
8
  `.target.html` / `.target.css` / `.shadow.css` assets, the dev shell
9
9
  (`index-dev.html`) and the production shell (`index.html`).
10
10
  - `build/` is the `tsc` output: one `.js` per component at its own autoload path,
11
- plus the mirrored companion assets and the dev shell. `mc dev` serves it, so
12
- the page loads components on demand through `autoload()` and nothing is bundled.
11
+ plus the files `tsc` does not compile — mirrored companion assets, plain `.js`
12
+ components, and the dev shell. `mc dev` serves it, so the page loads components
13
+ on demand through `autoload()` and nothing is bundled.
13
14
  - `public/` is the production site: one `bundle.js` with every companion asset
14
15
  inlined by `mc bundle`, the concatenated `bundle.css`, and the production
15
16
  shell. A single script tag loads it; the autoloader is not needed.
@@ -28,9 +29,12 @@ entry, so every component must be imported by the barrel `src/<namespace>/bundle
28
29
 
29
30
  The two shells are deliberately different:
30
31
 
31
- - `index-dev.html` imports the library through an import map (to the served
32
- `microcomponents.min.js`) and calls `autoload(true)`, so a tag in the markup
33
- pulls its own `.js` from `build/`.
32
+ - `index-dev.html` imports the library from the served `/microcomponents.min.js` and
33
+ calls `autoload(true)`, so a tag in the markup pulls its own `.js` from `build/`.
34
+ The page writes no import map: the dev server rewrites the
35
+ `@twobirds/microcomponents` specifier in the JS it serves to that URL, so a
36
+ compiled component resolves the library without the page listing any imports.
37
+ See [no-importmap](no-importmap.md).
34
38
  - `index.html` loads `bundle.js` and `bundle.css`, which only exist in `public/`.
35
39
 
36
40
  `mc dev` redirects `/` to `index-dev.html`; `mc bundle` copies `index.html` next to
@@ -1,14 +1,20 @@
1
- State management uses observable objects with reactive binding. Two usage patterns exist:
1
+ State management uses observable objects with reactive binding. Which API you use is
2
+ decided by where the code lives, and that is a rule, not a preference:
2
3
 
3
- **Standalone functions** (outside MC ecosystem):
4
+ **Inside a `DC`** — always the plugin equivalents:
4
5
 
5
- - observe(obj) — wraps properties in Observable wrappers; you manage subscribe/bind/notify manually
6
- - formValues(obj, prop, dom) — binds form controls to an observable property on the object
6
+ - `observePlugin(DC)` — adds `.notify()`, `.subscribe()`, `.bind()` instance methods that implicitly create observables on demand
7
+ - `formValuePlugin(DC)` — adds `.formValues()` instance method; also pulls in all `observePlugin` methods
7
8
 
8
- **Plugins** (inside DC custom elements):
9
+ **Outside the MC system** — the pure functions, for code with no `this` to attach to
10
+ (vanilla JS, framework lifecycle code, Node):
9
11
 
10
- - observePlugin(DC) — adds .notify(), .subscribe(), .bind() instance methods that implicitly create observables on demand
11
- - formValuePlugin(DC) — adds .formValues() instance method; also pulls in all observePlugin methods
12
+ - `observe(obj)` — wraps properties in Observable wrappers; you manage subscribe/bind/notify manually
13
+ - `formValues(obj, prop, dom)` — binds form controls to an observable property on the object
14
+
15
+ Never call `observe()` or `formValues()` inside a `DC`. The plugin methods create the same
16
+ wrapper, chain, and keep the `keyof this & string` typing contract; a pure call inside a
17
+ component puts the observable outside the class's typed surface.
12
18
 
13
19
  Key behaviors:
14
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twobirds/microcomponents",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "Micro Components",
5
5
  "keywords": [
6
6
  "PWA",