@twobirds/microcomponents 0.12.1 → 0.13.1
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 +5 -5
- package/dist/cli/bundle.d.ts +23 -13
- package/dist/cli/bundle.d.ts.map +1 -1
- package/dist/cli/create.d.ts +28 -8
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/dev.d.ts +10 -7
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dispatch.d.ts +8 -2
- package/dist/cli/dispatch.d.ts.map +1 -1
- package/dist/cli/index.js +138 -56
- package/dist/cli/writeScaffold.d.ts +3 -3
- package/dist/docs/api-external.d.ts +11 -2
- package/dist/docs/api-external.d.ts.map +1 -1
- package/dist/docs/cli.d.ts +16 -7
- package/dist/docs/cli.d.ts.map +1 -1
- package/dist/docs/installation.d.ts +15 -5
- package/dist/docs/installation.d.ts.map +1 -1
- package/dist/mcp/api.json +1 -1
- package/dist/mcp/concepts/custom-element-registry.md +8 -0
- package/dist/mcp/concepts/event-system.md +20 -0
- package/dist/mcp/concepts/mc-types.md +5 -0
- package/dist/mcp/concepts/microcomponent-architecture.md +21 -1
- package/dist/mcp/concepts/no-importmap.md +48 -0
- package/dist/mcp/concepts/project-layout.md +16 -9
- package/dist/mcp/concepts/state-management.md +13 -7
- package/package.json +1 -1
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
|
|
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 (
|
|
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
|
|
|
@@ -138,7 +138,7 @@ Once installed from npm the same command is available as `mc` (and as `microcomp
|
|
|
138
138
|
|
|
139
139
|
```bash
|
|
140
140
|
mc new my-card [--css] [--html] [--target-html] [--shadow-html] [--target-css] [--shadow-css] [--js] [--dir <path>] [--force] [--dry-run]
|
|
141
|
-
mc create --namespace <ns> [--dir <path>] [--force] [--dry-run]
|
|
141
|
+
mc create --namespace <ns> [--dir <path>] [--force] [--yes] [--dry-run]
|
|
142
142
|
mc dev [--port <n>] [--host <addr>]
|
|
143
143
|
mc bundle
|
|
144
144
|
mc preview [--port <n>] [--host <addr>]
|
|
@@ -165,7 +165,7 @@ npx -p @twobirds/microcomponents mc new my-card --target-html --target-css
|
|
|
165
165
|
npx -p @twobirds/microcomponents microcomponents-mcp
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
The scaffold derives the file path from the tag using the same rule the runtime autoload loader uses (`my-card` → `my/card.ts`), names companion assets after the runtime's asset descriptors (`targetHtml` → `card.target.html`), and wires them into a `static assets` declaration. `--js` emits a plain JavaScript file for the rapid-prototyping path.
|
|
168
|
+
The scaffold derives the file path from the tag using the same rule the runtime autoload loader uses (`my-card` → `my/card.ts`), names companion assets after the runtime's asset descriptors (`targetHtml` → `card.target.html`), and wires them into a `static assets` declaration. Every hyphen becomes a folder, so the tag's segments are the DOM containment chain — `<cart-container-articles>` sits inside `<cart-container>`, so it is `src/cart/container/articles.ts` — and the last segment names what the component does, not what shape it is: `panel`, `box`, `wrapper` are superfluous, because a folder level costs a segment that has to say something. `--js` emits a plain JavaScript file for the rapid-prototyping path.
|
|
169
169
|
|
|
170
170
|
## Examples
|
|
171
171
|
|
|
@@ -414,7 +414,7 @@ Feel free to contact me for any questions or inquiries:
|
|
|
414
414
|
|
|
415
415
|
email [Frank Thürigen](mailto:fthuerigen@gmail.com?subject=Micro%20Components)
|
|
416
416
|
|
|
417
|
-
You can [fund this](https://www.
|
|
417
|
+
You can [fund this](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=fthuerigen@gmail.com&item_name=Micro+Components¤cy_code=EUR).
|
|
418
418
|
|
|
419
419
|
## Disclaimer
|
|
420
420
|
|
package/dist/cli/bundle.d.ts
CHANGED
|
@@ -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
|
|
8
|
-
|
|
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
|
|
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
|
-
*
|
|
36
|
-
*
|
|
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
|
|
46
|
+
export declare function isMirrored(name: string): boolean;
|
|
41
47
|
/**
|
|
42
|
-
* Mirror
|
|
43
|
-
* preserving the tree, so the bundler reads each asset
|
|
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
|
|
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
|
|
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
|
|
87
|
+
* @param target Specifier to point the import at.
|
|
78
88
|
*/
|
|
79
89
|
export declare function rewriteLibrarySpecifier(code: string, target?: string): string;
|
|
80
90
|
/**
|
package/dist/cli/bundle.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/cli/bundle.ts"],"names":[],"mappings":"AACA
|
|
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"}
|
package/dist/cli/create.d.ts
CHANGED
|
@@ -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
|
|
12
|
-
|
|
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:
|
|
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('
|
|
27
|
-
console.log(isValidNamespace('
|
|
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 -
|
|
49
|
-
the first segment of every component tag.
|
|
50
|
-
@throws If `namespace` is not
|
|
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
|
package/dist/cli/create.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/cli/create.ts"],"names":[],"mappings":"AAAA
|
|
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
|
|
7
|
-
|
|
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`),
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
49
|
-
*
|
|
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
|
package/dist/cli/dev.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"AACA
|
|
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"}
|
package/dist/cli/dispatch.d.ts
CHANGED
|
@@ -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;
|
|
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
|
|
172
|
-
|
|
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:
|
|
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('
|
|
191
|
-
console.log(isValidNamespace('
|
|
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]
|
|
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 -
|
|
208
|
-
the first segment of every component tag.
|
|
209
|
-
@throws If `namespace` is not
|
|
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
|
|
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.
|
|
270
|
+
dependencies: { "@twobirds/microcomponents": "^0.13.1" },
|
|
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,18 +387,24 @@ 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
|
|
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
|
|
377
400
|
npx mc new ${namespace}-name --target-html --target-css
|
|
378
401
|
\`\`\`
|
|
379
402
|
|
|
380
|
-
The tag must be kebab-case with at least one hyphen. \`ns-name\` becomes \`src/ns/name.ts
|
|
403
|
+
The tag must be kebab-case with at least one hyphen. Every hyphen becomes a folder, so \`ns-parent-name\` becomes \`src/ns/parent/name.ts\` — the same rule the autoloader uses, so the tag is the file path.
|
|
404
|
+
|
|
405
|
+
The segments are the DOM containment chain: the namespace, then the components the element sits inside, then the component's own name. \`<cart-container-articles>\` sits inside \`<cart-container>\`, so it is \`src/cart/container/articles.ts\`. The last segment names what the component does, not what shape it is: \`<${namespace}-header-search>\` → \`src/${namespace}/header/search.ts\`. Generic structural words (\`panel\`, \`box\`, \`wrapper\`, \`widget\`, \`view\`) are superfluous — \`<${namespace}-header-panel>\` → \`src/${namespace}/header/panel.js\` buys a folder level that says nothing.
|
|
406
|
+
|
|
407
|
+
\`--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\`.
|
|
381
408
|
|
|
382
409
|
Register the component so the production bundle picks it up:
|
|
383
410
|
|
|
@@ -405,18 +432,26 @@ A camelCase descriptor maps to a dotted filename and is listed in \`static asset
|
|
|
405
432
|
|
|
406
433
|
The loader fetches exactly the listed descriptors, so every entry in \`static assets\` needs its file.
|
|
407
434
|
|
|
435
|
+
## State
|
|
436
|
+
|
|
437
|
+
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', …)\`.
|
|
438
|
+
|
|
439
|
+
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.
|
|
440
|
+
|
|
408
441
|
## Events
|
|
409
442
|
|
|
410
443
|
Prefer a direct target with the default local dispatch and a typed payload:
|
|
411
444
|
|
|
412
445
|
\`\`\`ts
|
|
413
446
|
target.dispatchEvent(
|
|
414
|
-
new CustomEvent('
|
|
447
|
+
new CustomEvent('addToCart', { detail: { data: { id: 'x' } } })
|
|
415
448
|
);
|
|
416
449
|
\`\`\`
|
|
417
450
|
|
|
418
451
|
Bubble up or down the tree only when the receiver cannot be named.
|
|
419
452
|
|
|
453
|
+
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.
|
|
454
|
+
|
|
420
455
|
## Verify
|
|
421
456
|
|
|
422
457
|
\`\`\`sh
|
|
@@ -537,11 +572,8 @@ function devShell(namespace, tag) {
|
|
|
537
572
|
<meta charset="UTF-8" />
|
|
538
573
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
539
574
|
<title>${namespace} App</title>
|
|
540
|
-
<script type="importmap">
|
|
541
|
-
{ "imports": { "@twobirds/microcomponents": "/microcomponents.min.js" } }
|
|
542
|
-
<\/script>
|
|
543
575
|
<script type="module">
|
|
544
|
-
import * as m from '
|
|
576
|
+
import * as m from '/microcomponents.min.js';
|
|
545
577
|
m.autoload(true);
|
|
546
578
|
Object.assign(window, m);
|
|
547
579
|
<\/script>
|
|
@@ -700,12 +732,12 @@ namespace under a project root, creating nested folders as needed.
|
|
|
700
732
|
const written = await writeProject('mc', 'mc-app');
|
|
701
733
|
console.log(written);
|
|
702
734
|
```
|
|
703
|
-
@param namespace -
|
|
735
|
+
@param namespace - lowercase letters and digits, starting with a letter, any length.
|
|
704
736
|
@param root - Directory to create the project in (e.g. `mc-app`).
|
|
705
737
|
@param options - Whether to overwrite existing files.
|
|
706
738
|
@returns The written file paths, relative to `root`.
|
|
707
|
-
@throws If `namespace` is not
|
|
708
|
-
`force` is false.
|
|
739
|
+
@throws If `namespace` is not lowercase letters and digits starting with a letter, or
|
|
740
|
+
a target file exists and `force` is false.
|
|
709
741
|
*/
|
|
710
742
|
async function writeProject(namespace, root, options = {}) {
|
|
711
743
|
const { files } = scaffoldProject(namespace);
|
|
@@ -1150,8 +1182,9 @@ the `tsc`-emitted `build/` tree, with no Vite.
|
|
|
1150
1182
|
|
|
1151
1183
|
The chain is `src/` → `build/` → `public/`. The scaffolded `build` script runs
|
|
1152
1184
|
`tsc -p tsconfig.json` (so `build/` holds one `.js` per component at its
|
|
1153
|
-
autoload path), then `mc bundle` mirrors the
|
|
1154
|
-
|
|
1185
|
+
autoload path), then `mc bundle` mirrors the files `tsc` does not compile —
|
|
1186
|
+
companion assets and plain `.js` components — into `build/`, walks the barrel
|
|
1187
|
+
entry named by `src/bundle.json` through {@link bundleEntry}
|
|
1155
1188
|
(inlining every `static assets`), rewrites the `@twobirds/microcomponents`
|
|
1156
1189
|
specifier to the self-contained `microcomponents.min.js` shipped next to the
|
|
1157
1190
|
bundle, concatenates the plain `.css` token presets into `bundle.css`, and
|
|
@@ -1160,45 +1193,52 @@ copies the production shell. The result is a complete static site: a single
|
|
|
1160
1193
|
|
|
1161
1194
|
@module CLI/Bundle
|
|
1162
1195
|
*/
|
|
1163
|
-
/**
|
|
1164
|
-
var
|
|
1196
|
+
/** Files `tsc` does not compile, copied verbatim next to the compiled components. */
|
|
1197
|
+
var MIRROR_EXTENSIONS = [
|
|
1165
1198
|
".css",
|
|
1166
1199
|
".html",
|
|
1167
|
-
".json"
|
|
1200
|
+
".json",
|
|
1201
|
+
".js"
|
|
1168
1202
|
];
|
|
1169
1203
|
/**
|
|
1170
|
-
* Whether a file
|
|
1204
|
+
* Whether a file in `src/` is copied verbatim into `build/`. Companion assets
|
|
1205
|
+
* (`.css`, `.html`, `.json`) sit next to their compiled `.js`, and a plain `.js`
|
|
1206
|
+
* component (`mc new --js`) is copied as it is: `tsc` compiles only `.ts`, so plain
|
|
1207
|
+
* JS reaches the autoloader by copying, not by compiling. The page shells are not
|
|
1208
|
+
* mirrored by this rule — `mc dev` copies the dev shell itself.
|
|
1171
1209
|
*
|
|
1172
1210
|
* @example
|
|
1173
1211
|
* ```ts
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1212
|
+
* isMirrored('app.target.html'); // true
|
|
1213
|
+
* isMirrored('app.js'); // true — a plain JS component
|
|
1214
|
+
* isMirrored('index.html'); // false
|
|
1176
1215
|
* ```
|
|
1177
1216
|
* @param name Basename of the file.
|
|
1178
1217
|
*/
|
|
1179
|
-
function
|
|
1218
|
+
function isMirrored(name) {
|
|
1180
1219
|
if (name === "index.html" || name === "index-dev.html") return false;
|
|
1181
|
-
return
|
|
1220
|
+
return MIRROR_EXTENSIONS.some((ext) => name.endsWith(ext));
|
|
1182
1221
|
}
|
|
1183
1222
|
/**
|
|
1184
|
-
* Mirror
|
|
1185
|
-
* preserving the tree, so the bundler reads each asset
|
|
1223
|
+
* Mirror the files `tsc` does not compile (`.css`, `.html`, `.json`, `.js`) from
|
|
1224
|
+
* `src/` into `build/`, preserving the tree, so the bundler reads each asset and
|
|
1225
|
+
* each plain JS component next to its compiled `.js`.
|
|
1186
1226
|
*
|
|
1187
1227
|
* @example
|
|
1188
1228
|
* ```ts
|
|
1189
|
-
* await
|
|
1229
|
+
* await mirrorSources('<root>/src', '<root>/build');
|
|
1190
1230
|
* ```
|
|
1191
1231
|
* @param srcDir Source root to walk.
|
|
1192
1232
|
* @param buildDir Folder to write into.
|
|
1193
1233
|
* @param rel Path of the walked directory relative to `srcDir`.
|
|
1194
1234
|
*/
|
|
1195
|
-
async function
|
|
1235
|
+
async function mirrorSources(srcDir, buildDir, rel = "") {
|
|
1196
1236
|
for (const entry of await readdir(path.join(srcDir, rel), { withFileTypes: true })) {
|
|
1197
1237
|
const next = rel ? path.join(rel, entry.name) : entry.name;
|
|
1198
1238
|
if (entry.isDirectory()) {
|
|
1199
1239
|
if (entry.name === "node_modules") continue;
|
|
1200
|
-
await
|
|
1201
|
-
} else if (
|
|
1240
|
+
await mirrorSources(srcDir, buildDir, next);
|
|
1241
|
+
} else if (isMirrored(entry.name)) {
|
|
1202
1242
|
const dst = path.join(buildDir, next);
|
|
1203
1243
|
await mkdir(path.dirname(dst), { recursive: true });
|
|
1204
1244
|
await cp(path.join(srcDir, next), dst);
|
|
@@ -1229,17 +1269,20 @@ async function collectPlainCss(srcDir, rel = "", found = []) {
|
|
|
1229
1269
|
/**
|
|
1230
1270
|
* Rewrite the library specifier in a bundled module to the self-contained bundle
|
|
1231
1271
|
* shipped next to it, so the browser can resolve `@twobirds/microcomponents`
|
|
1232
|
-
* without a bare-specifier import map.
|
|
1272
|
+
* without a bare-specifier import map. Subpath imports resolve to the same module,
|
|
1273
|
+
* so the library stays one instance. This is the single source of truth for the
|
|
1274
|
+
* rule: the bundler uses the relative target, the dev server passes its served URL.
|
|
1233
1275
|
*
|
|
1234
|
-
* @example Turns the package specifier into a relative path.
|
|
1276
|
+
* @example Turns the package specifier into a relative path, leaves others alone.
|
|
1235
1277
|
* ```ts
|
|
1236
1278
|
* rewriteLibrarySpecifier("import { DC } from '@twobirds/microcomponents';");
|
|
1279
|
+
* rewriteLibrarySpecifier("import { z } from 'zod';"); // unchanged
|
|
1237
1280
|
* ```
|
|
1238
1281
|
* @param code Bundled module source.
|
|
1239
|
-
* @param target
|
|
1282
|
+
* @param target Specifier to point the import at.
|
|
1240
1283
|
*/
|
|
1241
1284
|
function rewriteLibrarySpecifier(code, target = "./microcomponents.min.js") {
|
|
1242
|
-
return code.replace(/
|
|
1285
|
+
return code.replace(/(['"])@twobirds\/microcomponents(?:\/[^'"]*)?\1/g, `$1${target}$1`);
|
|
1243
1286
|
}
|
|
1244
1287
|
/**
|
|
1245
1288
|
* Assemble the production site in `public/` from the compiled `build/` tree.
|
|
@@ -1259,7 +1302,7 @@ async function bundleProject(projectRoot, options) {
|
|
|
1259
1302
|
const config = JSON.parse(await readFile(path.join(sourceRoot, "bundle.json"), "utf8"));
|
|
1260
1303
|
const publicRoot = path.join(projectRoot, options.outDir ?? config.outDir ?? "public");
|
|
1261
1304
|
const fileName = config.fileName ?? "bundle.js";
|
|
1262
|
-
await
|
|
1305
|
+
await mirrorSources(sourceRoot, buildRoot);
|
|
1263
1306
|
const rewritten = rewriteLibrarySpecifier(await bundleEntry((options.bundleEntry ?? config.entry).replace(/\.ts$/, ".js"), buildRoot, true));
|
|
1264
1307
|
await mkdir(publicRoot, { recursive: true });
|
|
1265
1308
|
await writeFile(path.join(publicRoot, fileName), rewritten, "utf8");
|
|
@@ -1295,12 +1338,14 @@ Vite-free dev server for `mc dev`.
|
|
|
1295
1338
|
|
|
1296
1339
|
A minimal `node:http` static server rooted at the project's `build/` folder, so
|
|
1297
1340
|
a saved component reaches the browser through `autoload()` with no bundling. It
|
|
1298
|
-
mirrors
|
|
1299
|
-
|
|
1341
|
+
mirrors the files `tsc` does not compile — companion assets and plain `.js`
|
|
1342
|
+
components — from `src/` into `build/` (on startup and on every change), runs
|
|
1343
|
+
`tsc -w` for the lifetime of the server, serves companion
|
|
1300
1344
|
`.css` / `.html` / `.json` as raw text (so `loadAssets()` receives markup it
|
|
1301
|
-
can use), redirects `/` to the autoload shell (`index-dev.html`),
|
|
1302
|
-
|
|
1303
|
-
|
|
1345
|
+
can use), redirects `/` to the autoload shell (`index-dev.html`), serves the
|
|
1346
|
+
self-contained library at `/microcomponents.min.js`, and rewrites the
|
|
1347
|
+
`@twobirds/microcomponents` specifier in served JS to that URL, so a compiled
|
|
1348
|
+
component resolves the library with no import map in the page.
|
|
1304
1349
|
|
|
1305
1350
|
There is no HMR: a page reload picks up the recompiled component.
|
|
1306
1351
|
|
|
@@ -1362,8 +1407,9 @@ async function serve(req, res, buildRoot, minJs) {
|
|
|
1362
1407
|
}
|
|
1363
1408
|
try {
|
|
1364
1409
|
const text = await readFile(abs, "utf-8");
|
|
1410
|
+
const body = abs.endsWith(".js") ? rewriteLibrarySpecifier(text, "/microcomponents.min.js") : text;
|
|
1365
1411
|
res.setHeader("Content-Type", mimeFor(path.basename(abs)));
|
|
1366
|
-
res.end(
|
|
1412
|
+
res.end(body);
|
|
1367
1413
|
} catch {
|
|
1368
1414
|
/* v8 ignore start — resolveTarget already checked existence; a read failure is a race */
|
|
1369
1415
|
res.statusCode = 404;
|
|
@@ -1371,8 +1417,9 @@ async function serve(req, res, buildRoot, minJs) {
|
|
|
1371
1417
|
}
|
|
1372
1418
|
}
|
|
1373
1419
|
/**
|
|
1374
|
-
* Mirror
|
|
1375
|
-
*
|
|
1420
|
+
* Mirror the files `tsc` does not compile — companion assets and plain `.js`
|
|
1421
|
+
* components — from `src/` into `build/`, preserving the tree, so the served
|
|
1422
|
+
* folder stays autoload-ready.
|
|
1376
1423
|
*
|
|
1377
1424
|
* @example
|
|
1378
1425
|
* ```ts
|
|
@@ -1387,7 +1434,7 @@ async function mirrorInto(sourceRoot, buildRoot, rel = "") {
|
|
|
1387
1434
|
const next = rel ? path.join(rel, entry.name) : entry.name;
|
|
1388
1435
|
if (entry.isDirectory()) {
|
|
1389
1436
|
if (entry.name !== "node_modules") await mirrorInto(sourceRoot, buildRoot, next);
|
|
1390
|
-
} else if (
|
|
1437
|
+
} else if (isMirrored(entry.name) || entry.name === "index-dev.html") {
|
|
1391
1438
|
const dst = path.join(buildRoot, next);
|
|
1392
1439
|
await mkdir(path.dirname(dst), { recursive: true });
|
|
1393
1440
|
await cp(path.join(sourceRoot, next), dst);
|
|
@@ -1441,7 +1488,7 @@ async function runDev(projectRoot = process.cwd(), minJs = path.join(import.meta
|
|
|
1441
1488
|
watch(sourceRoot, { recursive: true }, (_event, file) => {
|
|
1442
1489
|
if (!file || file.endsWith(".ts")) return;
|
|
1443
1490
|
const abs = path.join(sourceRoot, file);
|
|
1444
|
-
if (
|
|
1491
|
+
if (isMirrored(path.basename(file)) || file.endsWith("index-dev.html")) {
|
|
1445
1492
|
const dst = path.join(buildRoot, file);
|
|
1446
1493
|
mkdir(path.dirname(dst), { recursive: true }).then(() => cp(abs, dst));
|
|
1447
1494
|
}
|
|
@@ -1568,7 +1615,7 @@ var USAGE = `mc — Micro Components build tools
|
|
|
1568
1615
|
|
|
1569
1616
|
Usage:
|
|
1570
1617
|
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]
|
|
1618
|
+
mc create --namespace <ns> [--dir <path>] [--force] [--yes] [--dry-run]
|
|
1572
1619
|
mc dev [--port <n>] [--host <addr>]
|
|
1573
1620
|
mc bundle
|
|
1574
1621
|
mc preview [--port <n>] [--host <addr>]
|
|
@@ -1601,6 +1648,7 @@ Other options:
|
|
|
1601
1648
|
--host <addr> dev / preview bind address (default: 127.0.0.1 — loopback only; pass
|
|
1602
1649
|
0.0.0.0 or a interface address to serve the LAN)
|
|
1603
1650
|
--force overwrite files that already exist
|
|
1651
|
+
--yes skip the confirmation when a namespace is longer than 5 characters (-y)
|
|
1604
1652
|
--dry-run print what would be written; touch nothing on disk
|
|
1605
1653
|
--version print the version of the package the CLI ships in (-v)
|
|
1606
1654
|
--help print this text (-h)
|
|
@@ -1618,6 +1666,25 @@ var ASSET_FLAGS = {
|
|
|
1618
1666
|
"shadow-css": "shadowCss"
|
|
1619
1667
|
};
|
|
1620
1668
|
/**
|
|
1669
|
+
Asks on the terminal whether to continue with a long namespace. `y`/`yes`
|
|
1670
|
+
(case-insensitive) continue; anything else, including an unreadable stdin, aborts.
|
|
1671
|
+
|
|
1672
|
+
@example
|
|
1673
|
+
```ts
|
|
1674
|
+
const ok = await confirmNamespace('microcomponents');
|
|
1675
|
+
```
|
|
1676
|
+
@param namespace - The namespace to confirm.
|
|
1677
|
+
*/
|
|
1678
|
+
async function confirmNamespace(namespace) {
|
|
1679
|
+
const rl = createInterface({
|
|
1680
|
+
input: process.stdin,
|
|
1681
|
+
output: process.stdout
|
|
1682
|
+
});
|
|
1683
|
+
const answer = await rl.question(`continue with "${namespace}"? [y/N] `);
|
|
1684
|
+
rl.close();
|
|
1685
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1621
1688
|
A scaffolded file as it appears from the current directory: the file `rel` under
|
|
1622
1689
|
`root`, resolved and made readable. `mc create` writes a project into a folder of
|
|
1623
1690
|
its own, so `src/mc/app.ts` under `mc-app` prints as `mc-app/src/mc/app.ts` — the
|
|
@@ -1764,7 +1831,8 @@ async function runScript(script, options) {
|
|
|
1764
1831
|
/**
|
|
1765
1832
|
Parses `argv` and routes it to the matching command, returning a process exit
|
|
1766
1833
|
code. `run` is injectable so the script-wrapping commands can be tested without
|
|
1767
|
-
spawning anything
|
|
1834
|
+
spawning anything, and `confirm` is injectable so the long-namespace prompt can be
|
|
1835
|
+
tested without a terminal.
|
|
1768
1836
|
|
|
1769
1837
|
@example
|
|
1770
1838
|
```ts
|
|
@@ -1777,8 +1845,11 @@ return 0;
|
|
|
1777
1845
|
```ts
|
|
1778
1846
|
const code = await main(['--version']);
|
|
1779
1847
|
```
|
|
1848
|
+
@param argv - Arguments to route (usually `process.argv.slice(2)`).
|
|
1849
|
+
@param run - Runs a package.json script (defaults to {@link runScript}).
|
|
1850
|
+
@param confirm - Confirms a long namespace (defaults to the terminal prompt).
|
|
1780
1851
|
*/
|
|
1781
|
-
async function main(argv, run = runScript) {
|
|
1852
|
+
async function main(argv, run = runScript, confirm = confirmNamespace) {
|
|
1782
1853
|
const { values, positionals } = parseArgs({
|
|
1783
1854
|
args: argv,
|
|
1784
1855
|
allowPositionals: true,
|
|
@@ -1795,6 +1866,10 @@ async function main(argv, run = runScript) {
|
|
|
1795
1866
|
port: { type: "string" },
|
|
1796
1867
|
host: { type: "string" },
|
|
1797
1868
|
force: { type: "boolean" },
|
|
1869
|
+
yes: {
|
|
1870
|
+
type: "boolean",
|
|
1871
|
+
short: "y"
|
|
1872
|
+
},
|
|
1798
1873
|
"dry-run": { type: "boolean" },
|
|
1799
1874
|
help: {
|
|
1800
1875
|
type: "boolean",
|
|
@@ -1854,6 +1929,13 @@ async function main(argv, run = runScript) {
|
|
|
1854
1929
|
console.error("mc create: missing --namespace (e.g. mc create --namespace mc)");
|
|
1855
1930
|
return 1;
|
|
1856
1931
|
}
|
|
1932
|
+
if (isLongNamespace(namespace)) {
|
|
1933
|
+
console.warn(`mc create: namespace "${namespace}" is ${namespace.length} characters — every component tag in the project starts with it.`);
|
|
1934
|
+
if (!values["dry-run"] && !values.yes && !await confirm(namespace)) {
|
|
1935
|
+
console.error("mc create: aborted");
|
|
1936
|
+
return 1;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1857
1939
|
const root = values.dir ?? namespace;
|
|
1858
1940
|
let files;
|
|
1859
1941
|
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 -
|
|
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
|
|
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
|
|
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
|
|
1
|
+
{"version":3,"file":"api-external.d.ts","sourceRoot":"","sources":["../../src/docs/api-external.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4GE"}
|
package/dist/docs/cli.d.ts
CHANGED
|
@@ -44,18 +44,25 @@ mc create --namespace shop
|
|
|
44
44
|
# project "shop" created in shop/ — run: cd shop && pnpm install && pnpm dev
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Flags: `--namespace <ns>` (
|
|
48
|
-
the namespace), `--force`, `--
|
|
47
|
+
Flags: `--namespace <ns>` (lowercase letters and digits, starting with a letter, any
|
|
48
|
+
length, required), `--dir <path>` (default: the namespace), `--force`, `--yes` (`-y`,
|
|
49
|
+
skip the confirmation), `--dry-run`. The project owns its namespace, so length is the
|
|
50
|
+
user's call; past 5 characters `mc create` warns and asks the terminal to confirm before
|
|
51
|
+
anything is written, and `--dry-run` warns without prompting.
|
|
49
52
|
|
|
50
53
|
## mc dev
|
|
51
54
|
|
|
52
55
|
Runs `tsc -w` plus a `node:http` server that serves `build/`. The page loads components
|
|
53
56
|
on demand through `autoload()` — nothing is bundled, and a new component is picked up
|
|
54
|
-
without restarting.
|
|
57
|
+
without restarting. The server rewrites the `@twobirds/microcomponents` specifier in the
|
|
58
|
+
JS it serves to the `/microcomponents.min.js` it answers, so the page carries no import
|
|
59
|
+
map, and it mirrors the files `tsc` does not compile — companion assets and plain `.js`
|
|
60
|
+
components — into `build/` next to the compiled component.
|
|
55
61
|
|
|
56
62
|
```sh
|
|
57
|
-
mc dev
|
|
63
|
+
mc dev # dev server on port 5173, bound to 127.0.0.1
|
|
58
64
|
mc dev --port 3000
|
|
65
|
+
mc dev --host 0.0.0.0 # widen the bind to the LAN
|
|
59
66
|
```
|
|
60
67
|
|
|
61
68
|
## mc bundle
|
|
@@ -70,11 +77,13 @@ mc bundle
|
|
|
70
77
|
|
|
71
78
|
## mc preview
|
|
72
79
|
|
|
73
|
-
Serves the production site in `public/`.
|
|
80
|
+
Serves the production site in `public/`. Like `mc dev`, it binds to `127.0.0.1` by
|
|
81
|
+
default, so the served files are not reachable off the machine; `--host` widens the bind.
|
|
74
82
|
|
|
75
83
|
```sh
|
|
76
|
-
mc preview
|
|
84
|
+
mc preview # preview server on port 5174, bound to 127.0.0.1
|
|
77
85
|
mc preview --port 8080
|
|
86
|
+
mc preview --host 0.0.0.0 # widen the bind to the LAN
|
|
78
87
|
```
|
|
79
88
|
|
|
80
89
|
## mc clean
|
|
@@ -101,7 +110,7 @@ mc check
|
|
|
101
110
|
Reports the version of the installed library, not the version of the project it runs in.
|
|
102
111
|
|
|
103
112
|
```sh
|
|
104
|
-
mc --version # 0.
|
|
113
|
+
mc --version # 0.13.1
|
|
105
114
|
```
|
|
106
115
|
|
|
107
116
|
## mc --help
|
package/dist/docs/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/docs/cli.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/docs/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0HE"}
|
|
@@ -4,17 +4,21 @@
|
|
|
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 (
|
|
8
|
-
segment of every component tag, so `shop`
|
|
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
|
|
12
15
|
# created shop/package.json
|
|
13
16
|
# created shop/opencode.jsonc
|
|
17
|
+
# created shop/src/bundle.json
|
|
14
18
|
# created shop/src/index.html
|
|
15
19
|
# created shop/src/index-dev.html
|
|
16
20
|
# created shop/src/shop/app.ts
|
|
17
|
-
# created shop/src/shop/app.
|
|
21
|
+
# created shop/src/shop/app.html
|
|
18
22
|
# created shop/src/shop/app.target.css
|
|
19
23
|
# created shop/src/shop/bundle.ts
|
|
20
24
|
# ...
|
|
@@ -33,7 +37,11 @@ the project.
|
|
|
33
37
|
`mc new` derives the file path from the tag using the same rule the runtime autoloader
|
|
34
38
|
uses (`shop-app` → `shop/app.ts`), names companion assets after the runtime's asset
|
|
35
39
|
descriptors (`targetHtml` → `app.target.html`), and wires them into a `static assets`
|
|
36
|
-
declaration.
|
|
40
|
+
declaration. Every hyphen becomes a folder, so the segments are the DOM containment
|
|
41
|
+
chain: a child of `<cart-container>` is `<cart-container-articles>` →
|
|
42
|
+
`src/cart/container/articles.ts`. The last segment names what the component does, not
|
|
43
|
+
what shape it is — `panel`, `box`, `wrapper` are superfluous, because a folder level
|
|
44
|
+
costs a segment that has to say something.
|
|
37
45
|
|
|
38
46
|
```sh
|
|
39
47
|
mc new shop-app --target-html --target-css
|
|
@@ -69,6 +77,8 @@ Companion asset flags:
|
|
|
69
77
|
| `--html` | html | unvariant light-DOM template |
|
|
70
78
|
|
|
71
79
|
`--js` emits plain JavaScript for rapid prototyping; `--dry-run` prints the plan without
|
|
72
|
-
touching disk; `--force` overwrites existing files.
|
|
80
|
+
touching disk; `--force` overwrites existing files. A plain `.js` component is copied to
|
|
81
|
+
`build/` verbatim by `mc dev` and `mc bundle` — `tsc` compiles only `.ts`, so it gets no
|
|
82
|
+
type checking and no `.d.ts`, and it autoloads and bundles like a compiled component.
|
|
73
83
|
*/
|
|
74
84
|
//# sourceMappingURL=installation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installation.d.ts","sourceRoot":"","sources":["../../src/docs/installation.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"installation.d.ts","sourceRoot":"","sources":["../../src/docs/installation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+BE;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkDE"}
|
package/dist/mcp/api.json
CHANGED
|
@@ -47,6 +47,14 @@ src/
|
|
|
47
47
|
|
|
48
48
|
The DOM hierarchy is therefore immediately visible in the file system.
|
|
49
49
|
|
|
50
|
+
The name is the shortest one that says what the component does. The leaf segment is the
|
|
51
|
+
component's function, not its shape: `<shop-header-search>` → `src/shop/header/search.js`
|
|
52
|
+
says what the child of the header does. `<shop-header-panel>` → `src/shop/header/panel.js`
|
|
53
|
+
is wrong — `panel` describes a shape, so the file carries no information about the
|
|
54
|
+
component, and the folder level the tag creates is paid for by a word that says nothing.
|
|
55
|
+
Generic structural words (`panel`, `box`, `wrapper`, `widget`, `view`) are superfluous:
|
|
56
|
+
the nesting a tag encodes costs a directory, so the segment must be worth it.
|
|
57
|
+
|
|
50
58
|
---
|
|
51
59
|
|
|
52
60
|
## General-Purpose Components in Side Folders
|
|
@@ -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`.
|
|
@@ -111,6 +111,11 @@ External design of the composed unit is driven by namespaced CSS custom properti
|
|
|
111
111
|
</cart-container>
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
A child's tag carries its parent's prefix, so its file lands in the parent's folder
|
|
115
|
+
(`<cart-container-articles>` → `src/cart/container/articles.ts`), and the last segment
|
|
116
|
+
names what the child does — `articles`, `checkout`, `pay`. A generic structural word
|
|
117
|
+
(`panel`, `box`, `wrapper`) makes the folder level it creates say nothing.
|
|
118
|
+
|
|
114
119
|
### 1d. Leaf Components
|
|
115
120
|
|
|
116
121
|
Self-contained visual units that project no child custom elements — a button, a badge,
|
|
@@ -16,7 +16,7 @@ Every `.ts` or `.js` file contains exactly **one class**, regardless of which ty
|
|
|
16
16
|
visual, logic-only, style-only, or pure logic. This applies to `DC` subclasses, `MC`
|
|
17
17
|
subclasses, and any other class defined in `src/`.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
For the classes the toolchain registers, three mechanisms make the rule loadable:
|
|
20
20
|
|
|
21
21
|
- **Autoload resolution** — `autoload()` maps a tag name to a single script path
|
|
22
22
|
(`<shop-header>` → `./shop/header.js`). One class per file keeps that mapping
|
|
@@ -27,5 +27,25 @@ Why:
|
|
|
27
27
|
- **Registry clarity** — `defineCE()` is called once per file, so file, class, and
|
|
28
28
|
registered tag stay in a 1:1 relationship.
|
|
29
29
|
|
|
30
|
+
The three logic types are where the rule is easiest to lose, because none of those three
|
|
31
|
+
mechanisms can catch a violation:
|
|
32
|
+
|
|
33
|
+
- **Logic-only `DC`** (`static tag = ''`) — the class is registered, so the 1:1 contract
|
|
34
|
+
holds, and the asset name is the host's tag name, which for a removed host is the
|
|
35
|
+
registered tag (`app-router` → `./app/router.css`). The class runs setup logic on a
|
|
36
|
+
host it is attached to with `DC.add(host, key, instance)`: one instance under one key,
|
|
37
|
+
so a second class in the file is a second attachable unit with no key of its own. Its
|
|
38
|
+
companions are `.css` and `.json` — the constructor removes the host after the assets
|
|
39
|
+
are applied, so a markup companion has nothing to inject into.
|
|
40
|
+
- **Style-only `DC`** (`static tag = ''`, `static assets = ['css']`) — the class is a
|
|
41
|
+
delivery vehicle for one stylesheet. File, class, registered tag, and the `.css`
|
|
42
|
+
companion are one thing under four names: `page-cockpit` in `src/page/cockpit.ts`
|
|
43
|
+
fetches `./page/cockpit.css`. A second class in the file would ship a second
|
|
44
|
+
stylesheet under a basename the first one owns.
|
|
45
|
+
- **Pure logic `MC`** — no tag, no `defineCE`, no autoload path, so nothing in the
|
|
46
|
+
toolchain sees the file at all. The rule holds for the reason a reusable unit gets its
|
|
47
|
+
own file: the import is by path, so one file is one capability and the file name is
|
|
48
|
+
the capability. A helper that is not a class is a utility function, not an `MC`.
|
|
49
|
+
|
|
30
50
|
Non-class entities that belong to the class — interfaces, types, constants, plain
|
|
31
51
|
functions — may live alongside it. A class that is reusable on its own gets its own file.
|
|
@@ -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.
|
|
@@ -4,12 +4,15 @@ A Micro Components project keeps one folder per stage of the chain:
|
|
|
4
4
|
src/ → (tsc) → build/ → (bundle) → public/
|
|
5
5
|
```
|
|
6
6
|
|
|
7
|
-
- `src/` holds the authored sources: component `.ts` files,
|
|
8
|
-
`.target.html` / `.target.css` /
|
|
9
|
-
|
|
7
|
+
- `src/` holds the authored sources: component `.ts` files, plain `.js` components
|
|
8
|
+
written with `mc new --js`, the companion `.target.html` / `.target.css` /
|
|
9
|
+
`.shadow.css` assets, the bundle barrel `src/<namespace>/bundle.ts` with the
|
|
10
|
+
`src/bundle.json` that names it, the dev shell (`index-dev.html`) and the production
|
|
11
|
+
shell (`index.html`).
|
|
10
12
|
- `build/` is the `tsc` output: one `.js` per component at its own autoload path,
|
|
11
|
-
plus the
|
|
12
|
-
the
|
|
13
|
+
plus the files `tsc` does not compile — mirrored companion assets, plain `.js`
|
|
14
|
+
components, and the dev shell. `mc dev` serves it, so the page loads components
|
|
15
|
+
on demand through `autoload()` and nothing is bundled.
|
|
13
16
|
- `public/` is the production site: one `bundle.js` with every companion asset
|
|
14
17
|
inlined by `mc bundle`, the concatenated `bundle.css`, and the production
|
|
15
18
|
shell. A single script tag loads it; the autoloader is not needed.
|
|
@@ -22,15 +25,19 @@ served from `public/` in production. Do not add a fourth tree for it.
|
|
|
22
25
|
|
|
23
26
|
A new component is picked up by the dev server automatically, because `autoload()`
|
|
24
27
|
resolves its tag to a file under `build/`. The bundle is not: the bundler walks a single
|
|
25
|
-
entry, so every component must be imported by the barrel
|
|
28
|
+
entry, the one `src/bundle.json` names, so every component must be imported by the barrel
|
|
29
|
+
`src/<namespace>/bundle.ts` that file points at.
|
|
26
30
|
|
|
27
31
|
## Shells
|
|
28
32
|
|
|
29
33
|
The two shells are deliberately different:
|
|
30
34
|
|
|
31
|
-
- `index-dev.html` imports the library
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
- `index-dev.html` imports the library from the served `/microcomponents.min.js` and
|
|
36
|
+
calls `autoload(true)`, so a tag in the markup pulls its own `.js` from `build/`.
|
|
37
|
+
The page writes no import map: the dev server rewrites the
|
|
38
|
+
`@twobirds/microcomponents` specifier in the JS it serves to that URL, so a
|
|
39
|
+
compiled component resolves the library without the page listing any imports.
|
|
40
|
+
See [no-importmap](no-importmap.md).
|
|
34
41
|
- `index.html` loads `bundle.js` and `bundle.css`, which only exist in `public/`.
|
|
35
42
|
|
|
36
43
|
`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.
|
|
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
|
-
**
|
|
4
|
+
**Inside a `DC`** — always the plugin equivalents:
|
|
4
5
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
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
|
-
**
|
|
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
|
-
-
|
|
11
|
-
-
|
|
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
|
|