@twobirds/microcomponents 0.9.0 → 0.10.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.
Files changed (45) hide show
  1. package/README.md +2 -16
  2. package/dist/cli/bundle.d.ts +106 -0
  3. package/dist/cli/bundle.d.ts.map +1 -0
  4. package/dist/cli/bundler.d.ts +44 -0
  5. package/dist/cli/bundler.d.ts.map +1 -0
  6. package/dist/cli/create.d.ts.map +1 -1
  7. package/dist/cli/dev.d.ts +87 -0
  8. package/dist/cli/dev.d.ts.map +1 -0
  9. package/dist/cli/dispatch.d.ts +6 -2
  10. package/dist/cli/dispatch.d.ts.map +1 -1
  11. package/dist/cli/index.js +1807 -1
  12. package/dist/cli/inliner.d.ts +42 -0
  13. package/dist/cli/inliner.d.ts.map +1 -0
  14. package/dist/cli/preview.d.ts +48 -0
  15. package/dist/cli/preview.d.ts.map +1 -0
  16. package/dist/core/elements.js +1294 -75
  17. package/dist/core/mcGuards.js +5 -8
  18. package/dist/mcp/api.json +2 -17
  19. package/dist/mcp/concepts/project-layout.md +9 -8
  20. package/dist/mcp/index.js +450 -230
  21. package/dist/microcomponents.js +2882 -9
  22. package/dist/state/formValues.js +2236 -281
  23. package/dist/state/observables.js +1794 -342
  24. package/dist/utils/events.js +171 -197
  25. package/package.json +3 -7
  26. package/dist/cli/create.js +0 -653
  27. package/dist/cli/dispatch.js +0 -316
  28. package/dist/cli/scaffold.js +0 -109
  29. package/dist/cli/writeScaffold.js +0 -114
  30. package/dist/core/MC.js +0 -837
  31. package/dist/core/index.js +0 -72
  32. package/dist/state/index.js +0 -5
  33. package/dist/state/placeholders.js +0 -187
  34. package/dist/types/index.js +0 -1
  35. package/dist/types/mcTypes.js +0 -9
  36. package/dist/utils/autoload.js +0 -160
  37. package/dist/utils/helpers.js +0 -532
  38. package/dist/utils/index.js +0 -12
  39. package/dist/utils/resolveComponentPath.js +0 -39
  40. package/dist/vite/index.d.ts +0 -7
  41. package/dist/vite/index.d.ts.map +0 -1
  42. package/dist/vite/index.js +0 -6
  43. package/dist/vite/loadAssets.d.ts +0 -36
  44. package/dist/vite/loadAssets.d.ts.map +0 -1
  45. package/dist/vite/loadAssets.js +0 -221
package/dist/cli/index.js CHANGED
@@ -1,4 +1,1810 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { parseArgs } from "node:util";
4
+ import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
+ import { existsSync, watch } from "node:fs";
6
+ import path from "node:path";
7
+ import { createServer } from "node:http";
8
+ //#region src/utils/resolveComponentPath.ts
9
+ /**
10
+ @module Autoload
11
+
12
+ Single source of truth for the tag-to-file convention: hyphens become path
13
+ separators, so `my-card` resolves to `./my/card.js`. Imported by the runtime
14
+ autoload loader and by build-time tooling so both agree on the path without
15
+ either duplicating the rule.
16
+ */
17
+ /**
18
+ The tag-to-path primitive: hyphens become path separators, with no prefix or
19
+ extension. `my-card` → `my/card`. The single source of truth for the
20
+ convention; {@link resolveComponentPath} and build-time tooling both derive
21
+ their paths from it so neither duplicates the rule.
22
+
23
+ @example Returns `'my/card'` for `my-card`, and `'app/header'` for `app-header`.
24
+ ```ts
25
+ tagPath('my-card');
26
+ tagPath('app-header');
27
+ ```
28
+ @hidden
29
+ @group Autoload
30
+ */
31
+ function tagPath(tag) {
32
+ return tag.split("-").join("/");
33
+ }
34
+ //#endregion
35
+ //#region src/cli/scaffold.ts
36
+ /**
37
+ Scaffolding for new micro components.
38
+
39
+ Pure, filesystem-free logic: given a tag name it returns the files to write and
40
+ their contents. The tag-to-path rule is not re-derived here — it comes from
41
+ {@link tagPath}, the same primitive the runtime autoload loader uses, so the
42
+ scaffold and the runtime can never disagree about where a component lives.
43
+
44
+ Companion assets follow the runtime's variant convention (see `loadAssets` /
45
+ `processAssets`): a camelCase descriptor maps to a dotted filename, e.g.
46
+ `'targetHtml'` → `{path}.target.html`, `'shadowCss'` → `{path}.shadow.css`, and
47
+ the unvariant `'css'` → `{path}.css` (the document-level stylesheet where CSS
48
+ custom properties / design tokens live).
49
+
50
+ @module CLI/Scaffold
51
+ */
52
+ var TAG_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/;
53
+ /**
54
+ Whether a string is a valid micro component tag name.
55
+
56
+ @example
57
+ ```ts
58
+ console.log(isValidComponentTag('my-card'));
59
+ console.log(isValidComponentTag('mycard'));
60
+ ```
61
+ */
62
+ function isValidComponentTag(tag) {
63
+ return TAG_RE.test(tag);
64
+ }
65
+ /**
66
+ Builds the files for a new micro component from its tag name.
67
+
68
+ @example
69
+ ```ts
70
+ const { className, files } = scaffoldComponent('my-card', {
71
+ assets: ['targetHtml', 'targetCss'],
72
+ });
73
+ console.log(className);
74
+ console.log(Object.keys(files));
75
+ ```
76
+ @param tag - Kebab-case component tag, e.g. `my-card`.
77
+ @param options - Which companion assets to emit and whether to use `.js`.
78
+ @throws If `tag` is not a valid custom element name.
79
+ */
80
+ function scaffoldComponent(tag, options = {}) {
81
+ if (!isValidComponentTag(tag)) throw new Error(`invalid component tag "${tag}": must be kebab-case with at least one hyphen (e.g. "my-card")`);
82
+ const className = tag.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
83
+ const ext = options.js ? "js" : "ts";
84
+ const ctorParam = options.js ? "target" : "target: HTMLElement";
85
+ const base = tagPath(tag);
86
+ const assets = [...new Set(options.assets ?? [])];
87
+ const files = {};
88
+ files[`${base}.${ext}`] = componentSource(tag, ctorParam, assets);
89
+ for (const kind of assets) files[assetFile(tag, kind)] = assetSource(kind, tag);
90
+ return {
91
+ files,
92
+ className
93
+ };
94
+ }
95
+ function assetFile(tag, kind) {
96
+ const m = kind.match(/^([a-z]+)([A-Z][a-z]*)$/);
97
+ const suffix = m ? `.${m[1]}.${m[2].toLowerCase()}` : `.${kind.toLowerCase()}`;
98
+ return `${tagPath(tag)}${suffix}`;
99
+ }
100
+ function componentSource(tag, ctorParam, assets) {
101
+ const lines = [
102
+ `import { DC, defineCE } from '@twobirds/microcomponents';`,
103
+ ``,
104
+ `/**`,
105
+ `\`${tag}\` — describe what this micro component does.`,
106
+ ``,
107
+ `@example`,
108
+ "```html",
109
+ `<${tag}></${tag}>`,
110
+ "```",
111
+ `*/`,
112
+ `defineCE(`,
113
+ `\t'${tag}',`,
114
+ `\tclass extends DC {`
115
+ ];
116
+ if (assets.length) lines.push(`\t\tstatic assets = [${assets.map((a) => `'${a}'`).join(", ")}];`);
117
+ lines.push(`\t\tconstructor(${ctorParam}) {`, `\t\t\tsuper(target);`, `\t\t}`, `\t}`, `);`, ``);
118
+ return lines.join("\n");
119
+ }
120
+ function assetSource(kind, tag) {
121
+ switch (kind) {
122
+ case "css": return [
123
+ ":root {",
124
+ " /* design tokens (CSS custom properties) */",
125
+ "}",
126
+ ""
127
+ ].join("\n");
128
+ case "targetCss": return [
129
+ `${tag} {`,
130
+ " display: block;",
131
+ "}",
132
+ ""
133
+ ].join("\n");
134
+ case "shadowCss": return [
135
+ ":host {",
136
+ " display: block;",
137
+ "}",
138
+ ""
139
+ ].join("\n");
140
+ default: return "<slot></slot>\n";
141
+ }
142
+ }
143
+ //#endregion
144
+ //#region src/utils/helpers.ts
145
+ /**
146
+ * Converts a kebab-case string to PascalCase.
147
+ * Example: 'my-component-name' -> 'MyComponentName'
148
+ * @param str - The kebab-case string to convert.
149
+ * @returns The converted PascalCase string.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * console.log(kebabToPascal('my-element'));
154
+ * ```
155
+ */
156
+ function kebabToPascal(str) {
157
+ return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
158
+ }
159
+ //#endregion
160
+ //#region src/cli/create.ts
161
+ /**
162
+ Project scaffolding for `mc create --namespace xxx`.
163
+
164
+ Pure, filesystem-free logic: given a namespace it returns the files of a complete,
165
+ working Micro Components dev environment and their contents. The starter component's
166
+ path is not re-derived here — it comes from {@link tagPath}, the same primitive the
167
+ runtime autoload loader uses, so the scaffold and the runtime can never disagree about
168
+ where a component lives.
169
+
170
+ 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
173
+ authored sources, `build/` receives the per-file `tsc` output that the dev server
174
+ serves through `autoload()`, and `public/` receives the bundled production site. Because
175
+ the dev shell is the autoload base, `import.meta.url` + `tagPath('xxx-app')` resolves to
176
+ `build/xxx/app.js` — the compiled starter.
177
+
178
+ @module CLI/Project
179
+ */
180
+ /** The starter component tag for a namespace: `xxx` → `xxx-app`. */
181
+ function starterTag(namespace) {
182
+ return `${namespace}-app`;
183
+ }
184
+ /**
185
+ Whether a string is a valid project namespace: 1 to 4 lowercase letters.
186
+
187
+ @example
188
+ ```ts
189
+ console.log(isValidNamespace('mc'));
190
+ console.log(isValidNamespace('mcxyz'));
191
+ console.log(isValidNamespace('toolong'));
192
+ ```
193
+ @param ns - Candidate namespace.
194
+ */
195
+ function isValidNamespace(ns) {
196
+ return /^[a-z]{1,4}$/.test(ns);
197
+ }
198
+ /**
199
+ Builds the files of a complete dev environment for a namespace.
200
+
201
+ @example
202
+ ```ts
203
+ const { namespace, files } = scaffoldProject('mc');
204
+ console.log(namespace);
205
+ console.log(Object.keys(files));
206
+ ```
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.
210
+ */
211
+ function scaffoldProject(namespace) {
212
+ if (!isValidNamespace(namespace)) throw new Error(`invalid namespace "${namespace}": must be 1 to 4 lowercase letters (e.g. "mc")`);
213
+ const tag = starterTag(namespace);
214
+ const base = tagPath(tag);
215
+ const files = {};
216
+ files["package.json"] = packageJson(namespace);
217
+ files["tsconfig.json"] = tsconfig();
218
+ files[".gitignore"] = gitignore();
219
+ files["opencode.jsonc"] = opencodeConfig();
220
+ files[".opencode/skills/components/SKILL.md"] = componentSkill(namespace, tag);
221
+ files["README.md"] = readme(namespace, tag);
222
+ files["src/index.html"] = prodShell(tag);
223
+ files["src/index-dev.html"] = devShell(namespace, tag);
224
+ files[`src/${base}.ts`] = appSource(namespace, tag);
225
+ files[`src/${base}.target.html`] = appHtml(namespace);
226
+ files[`src/${base}.target.css`] = appCss(tag);
227
+ files[`src/${namespace}/bundle.ts`] = bundle();
228
+ files["src/bundle.json"] = bundleConfig(namespace);
229
+ return {
230
+ files,
231
+ namespace
232
+ };
233
+ }
234
+ function packageJson(namespace) {
235
+ return JSON.stringify({
236
+ name: `${namespace}-app`,
237
+ private: true,
238
+ type: "module",
239
+ scripts: {
240
+ dev: "mc dev",
241
+ build: "tsc -p tsconfig.json && mc bundle",
242
+ preview: "mc preview",
243
+ typecheck: "tsc --noEmit -p tsconfig.json"
244
+ },
245
+ dependencies: { "@twobirds/microcomponents": "^0.10.0" },
246
+ devDependencies: { typescript: "^6.0.3" }
247
+ }, null, " ");
248
+ }
249
+ function tsconfig() {
250
+ return JSON.stringify({
251
+ compilerOptions: {
252
+ target: "ESNext",
253
+ module: "ESNext",
254
+ moduleResolution: "Bundler",
255
+ lib: [
256
+ "ESNext",
257
+ "DOM",
258
+ "DOM.Iterable"
259
+ ],
260
+ strict: true,
261
+ esModuleInterop: true,
262
+ skipLibCheck: true,
263
+ rootDir: "src",
264
+ outDir: "build",
265
+ removeComments: true
266
+ },
267
+ include: ["src/**/*.ts"]
268
+ }, null, " ");
269
+ }
270
+ function gitignore() {
271
+ return `node_modules/
272
+ build/
273
+ public/
274
+ `;
275
+ }
276
+ function opencodeConfig() {
277
+ return JSON.stringify({
278
+ $schema: "https://opencode.ai/config.json",
279
+ lsp: true,
280
+ mcp: { microcomponents: {
281
+ type: "local",
282
+ command: [
283
+ "npx",
284
+ "-p",
285
+ "@twobirds/microcomponents",
286
+ "microcomponents-mcp"
287
+ ],
288
+ enabled: true
289
+ } }
290
+ }, null, " ");
291
+ }
292
+ function componentSkill(namespace, tag) {
293
+ return `---
294
+ name: components
295
+ 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/.
296
+ ---
297
+
298
+ # Micro Components project
299
+
300
+ Namespace \`${namespace}\`. Every tag starts with \`${namespace}-\`. Components live in \`src/${namespace}/\` and the starter is \`<${tag}>\`.
301
+
302
+ ## The chain
303
+
304
+ \`src/\` → \`build/\` → \`public/\`
305
+
306
+ - \`src/\` is the only folder to edit.
307
+ - \`build/\` is the \`tsc\` output plus mirrored companion assets, served by the dev server. Generated.
308
+ - \`public/\` is the production site: \`bundle.js\` with companion assets inlined, \`bundle.css\`, \`index.html\`. Generated.
309
+
310
+ Never edit, rename, or delete anything under \`build/\` or \`public/\`. Change \`src/\` and rebuild.
311
+
312
+ ## Add a component
313
+
314
+ \`\`\`sh
315
+ npx mc new ${namespace}-name --target-html --target-css
316
+ \`\`\`
317
+
318
+ 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.
319
+
320
+ Register the component so the production bundle picks it up:
321
+
322
+ \`\`\`ts
323
+ // src/${namespace}/bundle.ts
324
+ import './name.js';
325
+ \`\`\`
326
+
327
+ The bundler reads \`src/bundle.json\`; its \`entry\` points at this barrel, so the barrel is what \`public/bundle.js\` is built from.
328
+
329
+ A component missing from the barrel loads in dev through \`autoload()\` but is absent from \`public/bundle.js\`.
330
+
331
+ ## Companion assets
332
+
333
+ A camelCase descriptor maps to a dotted filename and is listed in \`static assets\`:
334
+
335
+ | descriptor | file | role |
336
+ | --- | --- | --- |
337
+ | \`css\` | \`name.css\` | document-level tokens under \`:root\` |
338
+ | \`targetCss\` | \`name.target.css\` | styles scoped to the host tag |
339
+ | \`shadowCss\` | \`name.shadow.css\` | styles inside the shadow root |
340
+ | \`html\` | \`name.html\` | light-DOM template |
341
+ | \`targetHtml\` | \`name.target.html\` | template for \`target.innerHTML\`, \`{key}\` tokens filled by \`parse()\` |
342
+ | \`shadowHtml\` | \`name.shadow.html\` | template for \`shadowRoot.innerHTML\` |
343
+
344
+ The loader fetches exactly the listed descriptors, so every entry in \`static assets\` needs its file.
345
+
346
+ ## Events
347
+
348
+ Prefer a direct target with the default local dispatch and a typed payload:
349
+
350
+ \`\`\`ts
351
+ target.dispatchEvent(
352
+ new CustomEvent('${namespace}:action', { detail: { data: { id: 'x' } } })
353
+ );
354
+ \`\`\`
355
+
356
+ Bubble up or down the tree only when the receiver cannot be named.
357
+
358
+ ## Verify
359
+
360
+ \`\`\`sh
361
+ pnpm typecheck
362
+ pnpm dev
363
+ pnpm build
364
+ pnpm preview
365
+ \`\`\`
366
+
367
+ \`mc clean\` removes \`build/\`, \`public/\` and \`coverage/\`.
368
+
369
+ ## API reference
370
+
371
+ The \`microcomponents\` MCP server registered in \`opencode.jsonc\` documents every export, the event system, and the concept essays. Query it instead of guessing an API.
372
+ `;
373
+ }
374
+ function readme(namespace, tag) {
375
+ return `# ${namespace} App
376
+
377
+ A Micro Components project scaffolded by \`mc create --namespace ${namespace}\`.
378
+
379
+ ## Install
380
+
381
+ \`\`\`sh
382
+ pnpm install
383
+ \`\`\`
384
+
385
+ ## Dev
386
+
387
+ \`\`\`sh
388
+ pnpm dev
389
+ \`\`\`
390
+
391
+ The dev server runs \`tsc -w\` and serves \`build/\`, so a saved component reaches the
392
+ browser through \`autoload()\` with no bundling. Companion \`.css\`, \`.html\` and \`.json\`
393
+ files are mirrored into \`build/\` next to the compiled component, which is where the
394
+ autoloader looks for them.
395
+
396
+ ## Build
397
+
398
+ \`\`\`sh
399
+ pnpm build
400
+ \`\`\`
401
+
402
+ \`public/\` is the production site: one \`bundle.js\` with every companion asset
403
+ inlined by \`mc bundle\`, the concatenated \`bundle.css\`, and the page shell. A
404
+ single script tag loads it, no autoloader needed.
405
+
406
+ The bundle is driven by \`src/bundle.json\`: \`entry\` names the barrel to bundle,
407
+ \`fileName\` the output, and \`outDir\` the folder.
408
+
409
+ \`\`\`sh
410
+ pnpm preview
411
+ \`\`\`
412
+
413
+ ## Type-check
414
+
415
+ \`\`\`sh
416
+ pnpm typecheck
417
+ \`\`\`
418
+
419
+ \`tsconfig.json\` compiles \`src/\` into \`build/\`.
420
+
421
+ ## Components
422
+
423
+ Components live in \`src/${namespace}/\`. The starter is \`<${tag}>\` (\`src/${namespace}/app.ts\`).
424
+
425
+ Add a component with:
426
+
427
+ \`\`\`sh
428
+ npx mc new ${namespace}-name --target-html --target-css
429
+ \`\`\`
430
+
431
+ Then add its import to \`src/${namespace}/bundle.ts\` so the production bundle picks it up.
432
+
433
+ \`.opencode/skills/components/SKILL.md\` carries the same rules for opencode: the
434
+ \`src/\` → \`build/\` → \`public/\` chain, the asset descriptors, the bundle registration,
435
+ and the event dispatch preference.
436
+ `;
437
+ }
438
+ function prodShell(tag) {
439
+ return `<!doctype html>
440
+ <html lang="en">
441
+ <head>
442
+ <meta charset="UTF-8" />
443
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
444
+ <title>${tag.replace("-app", " App")}</title>
445
+ <script type="module" src="bundle.js"><\/script>
446
+ <link rel="stylesheet" href="./bundle.css" />
447
+ </head>
448
+ <body>
449
+ <${tag}></${tag}>
450
+ </body>
451
+ </html>
452
+ `;
453
+ }
454
+ function devShell(namespace, tag) {
455
+ return `<!doctype html>
456
+ <html lang="en">
457
+ <head>
458
+ <meta charset="UTF-8" />
459
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
460
+ <title>${namespace} App</title>
461
+ <script type="importmap">
462
+ { "imports": { "@twobirds/microcomponents": "/microcomponents.min.js" } }
463
+ <\/script>
464
+ <script type="module">
465
+ import * as m from '@twobirds/microcomponents';
466
+ m.autoload(true);
467
+ Object.assign(window, m);
468
+ <\/script>
469
+ </head>
470
+ <body>
471
+ <${tag}></${tag}>
472
+ </body>
473
+ </html>
474
+ `;
475
+ }
476
+ function bundle() {
477
+ return `// Barrel entry for the production bundle: imports every component so the bundler
478
+ // registers them all in one file. Add a line here when a component is created.
479
+
480
+ import './app.js';
481
+ `;
482
+ }
483
+ function bundleConfig(namespace) {
484
+ return JSON.stringify({
485
+ entry: `${namespace}/bundle.ts`,
486
+ fileName: "bundle.js",
487
+ outDir: "public"
488
+ }, null, " ");
489
+ }
490
+ function appSource(namespace, tag) {
491
+ const className = kebabToPascal(tag);
492
+ return `import { DC, defineCE, parse, processAssets, type McEvent } from '@twobirds/microcomponents';
493
+
494
+ /**
495
+ \`${tag}\` — the starter micro component.
496
+
497
+ @example
498
+ \`\`\`html
499
+ <${tag}></${tag}>
500
+ \`\`\`
501
+ */
502
+ class ${className} extends DC {
503
+ static assets = ['targetHtml', 'targetCss'];
504
+
505
+ data = { title: 'Micro Components' };
506
+
507
+ // Fill the template \`{key}\` tokens once, instead of binding reactive state.
508
+ oneAssets(ev: McEvent<Record<string, string>>): void {
509
+ processAssets(this, {
510
+ targetHtml: parse(ev.detail.data.targetHtml, this.data),
511
+ });
512
+ }
513
+ }
514
+
515
+ defineCE('${tag}', ${className});
516
+ `;
517
+ }
518
+ function appHtml(namespace) {
519
+ return `<div class="app">
520
+ <h1>{title}</h1>
521
+ <p>Namespace scaffold ready. Edit <code>src/${namespace}/app.ts</code>.</p>
522
+ </div>
523
+ `;
524
+ }
525
+ function appCss(tag) {
526
+ return `${tag} {
527
+ display: block;
528
+ padding: 2rem;
529
+ font-family: system-ui, sans-serif;
530
+ }
531
+ `;
532
+ }
533
+ //#endregion
534
+ //#region src/cli/writeScaffold.ts
535
+ /**
536
+ Filesystem side of {@link scaffoldComponent} and {@link scaffoldProject}: writes the
537
+ generated files under a root directory, creating nested folders as needed.
538
+
539
+ Existing files are never overwritten silently: every target is checked before the
540
+ first write, so a rerun in a populated directory reports the conflicts and touches
541
+ nothing. `force` opts into overwriting.
542
+
543
+ @module CLI/Scaffold
544
+ */
545
+ /**
546
+ The path of a target file as it appears from the current working directory.
547
+ `mc create` writes a project into a folder of its own, so a path relative to the
548
+ project root cannot be found from the directory the command ran in: this turns
549
+ `src/mc/app.ts` under `mc-app/` into `mc-app/src/mc/app.ts`. Falls back to the
550
+ absolute path when the target lies outside the current directory, so the log never
551
+ prints `../../..` chains.
552
+
553
+ @example
554
+ ```ts
555
+ // in a directory containing mc-app/src/mc/app.ts
556
+ displayPath(path.resolve('mc-app', 'src/mc/app.ts'));
557
+ displayPath('/tmp/mc-app/package.json');
558
+ ```
559
+ @param abs - Absolute path of the target file.
560
+ @group CLI
561
+ */
562
+ function displayPath(abs) {
563
+ const rel = path.relative(process.cwd(), abs);
564
+ return rel.startsWith("..") ? abs : rel;
565
+ }
566
+ /**
567
+ Writes generated files under `root`, creating nested folders as needed, and reports
568
+ the written paths relative to `root`.
569
+
570
+ @example
571
+ ```ts
572
+ const written = await writeGenerated({ 'my/card.ts': 'source' }, 'src', false);
573
+ console.log(written);
574
+ ```
575
+ @param files - Files to write, keyed by path relative to `root`.
576
+ @param root - Directory to write under (`src` for a component, `mc-app` for a project).
577
+ @param force - Overwrite files that already exist.
578
+ @returns The written file paths, relative to `root`.
579
+ @throws If any target file exists and `force` is false. Nothing is written in that case.
580
+ */
581
+ async function writeGenerated(files, root, force) {
582
+ const targets = Object.entries(files).map(([rel, contents]) => ({
583
+ rel,
584
+ abs: path.resolve(root, rel),
585
+ contents
586
+ }));
587
+ if (!force) {
588
+ const conflicts = targets.filter((target) => existsSync(target.abs)).map((t) => displayPath(t.abs));
589
+ if (conflicts.length) throw new Error(`refusing to overwrite ${conflicts.length} existing file(s):\n${conflicts.map((file) => `\t${file}`).join("\n")}\n\nrerun with --force to overwrite`);
590
+ }
591
+ const written = [];
592
+ for (const { rel, abs, contents } of targets) {
593
+ await mkdir(path.dirname(abs), { recursive: true });
594
+ await writeFile(abs, contents, "utf8");
595
+ written.push(rel);
596
+ }
597
+ return written;
598
+ }
599
+ /**
600
+ Scaffolds a component onto disk under `root`.
601
+
602
+ @example
603
+ ```ts
604
+ const written = await writeScaffold('my-card', 'src', { assets: ['css'] });
605
+ console.log(written);
606
+ ```
607
+ @param tag - Kebab-case component tag.
608
+ @param root - Directory to scaffold under (e.g. `src`).
609
+ @param options - Which companion files to emit, and whether to overwrite existing ones.
610
+ @returns The written file paths, relative to `root`.
611
+ @throws If a target file exists and `force` is false.
612
+ */
613
+ async function writeScaffold(tag, root, options = {}) {
614
+ const { files } = scaffoldComponent(tag, options);
615
+ return writeGenerated(files, root, options.force ?? false);
616
+ }
617
+ /**
618
+ Filesystem side of {@link scaffoldProject}: writes a complete dev environment for a
619
+ namespace under a project root, creating nested folders as needed.
620
+
621
+ @example
622
+ ```ts
623
+ const written = await writeProject('mc', 'mc-app');
624
+ console.log(written);
625
+ ```
626
+ @param namespace - 1 to 4 lowercase letters.
627
+ @param root - Directory to create the project in (e.g. `mc-app`).
628
+ @param options - Whether to overwrite existing files.
629
+ @returns The written file paths, relative to `root`.
630
+ @throws If `namespace` is not 1 to 4 lowercase letters, or a target file exists and
631
+ `force` is false.
632
+ */
633
+ async function writeProject(namespace, root, options = {}) {
634
+ const { files } = scaffoldProject(namespace);
635
+ return writeGenerated(files, root, options.force ?? false);
636
+ }
637
+ //#endregion
638
+ //#region src/cli/inliner.ts
639
+ /**
640
+ Asset inlining for the vite-free CLI bundler.
641
+
642
+ Companion `.css` / `.html` files declared by a DC class through
643
+ `static assets = [...]` are read at build time and embedded as a
644
+ `static _assets = { ... }` property inserted as the first line inside the
645
+ class body, so `loadAssets()` never fetches them at runtime. The original
646
+ `static assets` line is preserved for debugging purposes.
647
+
648
+ This is the inlining the shipped Vite plugin used, ported to run inside
649
+ {@link bundleEntry} without Vite: the bundler calls {@link inlineAssets} on
650
+ each module body before wrapping it, reading the companion files from the
651
+ same folder as the compiled `.js` (the mirrored `build/` tree).
652
+
653
+ @module CLI/Inliner
654
+ */
655
+ /**
656
+ * Escape a raw string so it can be safely embedded inside a JS template literal.
657
+ * Backticks and `${` sequences are escaped to prevent breaking the enclosing literal.
658
+ *
659
+ * @example Returns the input with backticks and `${` sequences escaped.
660
+ * ```ts
661
+ * escapeTemplateLiteral('price is ${cost}`done`');
662
+ * ```
663
+ */
664
+ function escapeTemplateLiteral(raw) {
665
+ return raw.replace(/\\/g, "\\\\").replace(/\$/g, "\\$").replace(/`/g, "\\`");
666
+ }
667
+ /**
668
+ * Resolve the relative file path for a given asset descriptor and component tag.
669
+ * Only the last segment of the tag is used as the basename since the `.js` source
670
+ * already lives in the correct directory (e.g. `cart-container-checkout.js` is
671
+ * inside `cart/container/`, so we just need `checkout.target.html`).
672
+ *
673
+ * @example Returns `./header.shadow.css`, `./card.target.html` and `./summary.css` for the calls below.
674
+ * ```ts
675
+ * resolveAssetPath('shop-header', 'shadowCss');
676
+ * resolveAssetPath('product-card', 'targetHtml');
677
+ * resolveAssetPath('cart-summary', 'css');
678
+ * ```
679
+ */
680
+ function resolveAssetPath(tagName, asset) {
681
+ const basename = tagName.split("-").pop() ?? tagName;
682
+ const m = asset.match(/^([a-z]+)([A-Z][a-z]*)$/);
683
+ if (m) return `./${basename}.${m[1]}.${m[2].toLowerCase()}`;
684
+ return `./${basename}.${asset.toLowerCase()}`;
685
+ }
686
+ /**
687
+ * Extract the tag name from a `defineCE('tag-name', ...)` call in source code.
688
+ * Returns the first found tag or undefined.
689
+ *
690
+ * @example Returns `'shop-header'` for the code below.
691
+ * ```ts
692
+ * extractDefineCETag("defineCE('shop-header', ShopHeader);");
693
+ * ```
694
+ */
695
+ function extractDefineCETag(code) {
696
+ const m = code.match(/defineCE\s*\(\s*['"]([^'"]+)['"]/);
697
+ return m ? m[1] : void 0;
698
+ }
699
+ /**
700
+ * Strip all line and block comments from source code, replacing
701
+ * their content with equivalent whitespace so character positions stay valid for
702
+ * later string slicing.
703
+ *
704
+ * @example Blanks out the `// note` segment while keeping every character
705
+ * position and line break, so later string slicing stays aligned.
706
+ * ```ts
707
+ * stripComments('const a = 1; // note\nconst b = 2;');
708
+ * ```
709
+ */
710
+ function stripComments(code) {
711
+ let result = "";
712
+ let i = 0;
713
+ while (i < code.length) if (code[i] === "/" && code[i + 1] === "/") {
714
+ const end = code.indexOf("\n", i);
715
+ if (end === -1) {
716
+ result += " ".repeat(code.length - i);
717
+ break;
718
+ }
719
+ result += " ".repeat(end - i) + "\n";
720
+ i = end + 1;
721
+ } else if (code[i] === "/" && code[i + 1] === "*") {
722
+ const end = code.indexOf("*/", i + 2);
723
+ if (end === -1) {
724
+ result += " ".repeat(code.length - i);
725
+ break;
726
+ }
727
+ let j = i;
728
+ while (j <= end + 1) {
729
+ result += code[j] === "\n" ? "\n" : " ";
730
+ j++;
731
+ }
732
+ i = end + 2;
733
+ } else {
734
+ result += code[i];
735
+ i++;
736
+ }
737
+ return result;
738
+ }
739
+ /**
740
+ * Read and inline all assets for a given tag, returning an array of
741
+ * `{ key, literal }` entries suitable for building object literals.
742
+ * Plain `.css` files hold CSS custom-property presets and are inlined with
743
+ * their real content so the component injects the default variables at
744
+ * runtime. They are never left empty — an empty string would make the
745
+ * runtime treat the asset as not-yet-loaded and re-fetch it.
746
+ *
747
+ * @example Returns one escaped template literal per descriptor, ready to be joined into an object.
748
+ * ```ts
749
+ * readAssetsInline('/build/shop/header', 'shop-header', ['shadowCss', 'targetHtml']);
750
+ * ```
751
+ * @throws If a companion file for a descriptor does not exist.
752
+ */
753
+ async function readAssetsInline(dir, tagName, assets) {
754
+ const entries = [];
755
+ for (const asset of assets) {
756
+ const relPath = resolveAssetPath(tagName, asset);
757
+ const filePath = path.resolve(dir, relPath);
758
+ try {
759
+ const content = await readFile(filePath, "utf8");
760
+ entries.push({
761
+ key: asset,
762
+ literal: "`" + escapeTemplateLiteral(content).replace(/[\n\t\r]/g, " ").replace(/\s\s/g, " ") + "`"
763
+ });
764
+ } catch (_err) {
765
+ throw new Error(`[load-assets] Cannot resolve asset "${asset}" for component "${tagName}": file not found at ${filePath}`, { cause: _err });
766
+ }
767
+ }
768
+ return entries;
769
+ }
770
+ /**
771
+ * Inline every `static assets = [...]` declaration in a module body as a
772
+ * `static _assets` property, reading the companion files from `dir`.
773
+ *
774
+ * @example Returns the class with `static _assets` injected after its opening brace.
775
+ * ```ts
776
+ * await inlineAssets("class A extends DC { static assets = ['targetHtml']; }", '/build/mc');
777
+ * ```
778
+ * @param code Module source (the compiled `.js`).
779
+ * @param dir Folder holding the companion files next to the module.
780
+ * @returns The transformed code, or the original when there is nothing to inline.
781
+ */
782
+ async function inlineAssets(code, dir) {
783
+ if (!code.includes("static assets")) return code;
784
+ const stripped = stripComments(code);
785
+ const tagName = extractDefineCETag(stripped);
786
+ if (!tagName) return code;
787
+ const saMatches = [...stripped.matchAll(/static\s+assets\s*=\s*\[(.*?)\]\s*;?/gs)];
788
+ if (saMatches.length === 0) return code;
789
+ let transformed = code;
790
+ for (let i = saMatches.length - 1; i >= 0; i--) {
791
+ const match = saMatches[i];
792
+ const assets = match[1].split(",").map((a) => a.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
793
+ if (assets.length === 0) continue;
794
+ const assetsObj = `{ ${(await readAssetsInline(dir, tagName, assets)).map((e) => `${e.key}: ${e.literal}`).join(", ")} }`;
795
+ const beforeMatch = stripped.substring(0, match.index);
796
+ const classOpenMatch = /class\s+\w+[^{]*\{/.exec(beforeMatch);
797
+ if (!classOpenMatch) continue;
798
+ const classBraceIndex = classOpenMatch.index + classOpenMatch[0].length - 1;
799
+ const injectCode = `\n static _assets = ${assetsObj};\n`;
800
+ transformed = transformed.substring(0, classBraceIndex + 1) + injectCode + transformed.substring(classBraceIndex + 1);
801
+ }
802
+ return transformed;
803
+ }
804
+ //#endregion
805
+ //#region src/cli/bundler.ts
806
+ /**
807
+ Pure-TypeScript global bundler — no Vite, no Rolldown, no oxc.
808
+
809
+ Reads the `tsc`-emitted ESM files in a project's `build/` and flattens the
810
+ barrel entry's dependency graph into one self-contained ES module. Each source
811
+ module is wrapped in a function scope keyed by its path (relative to the root),
812
+ its `import`/`export` statements are rewritten to a tiny `require` registry,
813
+ and the entry's re-exported names are emitted as the bundle's public exports.
814
+
815
+ When `inline` is set, every module body is first run through
816
+ {@link inlineAssets}, so a DC class declaring `static assets = [...]` gets its
817
+ companion `.css` / `.html` embedded as `static _assets` and `loadAssets()`
818
+ never fetches them at runtime.
819
+
820
+ The rewrite is comment-aware: `import`/`export` lines inside JSDoc `@example`
821
+ blocks are left untouched, so the self-referential `@twobirds/microcomponents`
822
+ specifiers in documentation never break resolution.
823
+
824
+ @module CLI/Bundler
825
+ */
826
+ /**
827
+ * Resolve a relative specifier to a module id (path relative to `root`),
828
+ * given the id of the importing module. Bare specifiers (`node:`, `@twobirds/`)
829
+ * are external and return `undefined`.
830
+ *
831
+ * @example Resolves a sibling and a parent-relative import.
832
+ * ```ts
833
+ * resolveId('./events.js', 'utils/helpers.js'); // 'utils/events.js'
834
+ * resolveId('../utils/helpers.js', 'core/MC.js'); // 'utils/helpers.js'
835
+ * ```
836
+ */
837
+ function resolveId(spec, currentId) {
838
+ if (!spec.startsWith(".")) return void 0;
839
+ return path.posix.normalize(path.posix.join(path.posix.dirname(currentId), spec));
840
+ }
841
+ /** Parse the binding list inside `{ a, b as c }` into local/exported pairs. */
842
+ function parseBindings(inner) {
843
+ return inner.split(",").map((b) => b.trim()).filter(Boolean).map((b) => {
844
+ const m = b.match(/^(\w+)(?:\s+as\s+(\w+))?$/);
845
+ if (!m) return {
846
+ local: b,
847
+ exported: b
848
+ };
849
+ return {
850
+ local: m[1],
851
+ exported: m[2] ?? m[1]
852
+ };
853
+ });
854
+ }
855
+ /**
856
+ * Rewrite a single top-level `import` line into a `require` destructure.
857
+ * Returns the replacement line and the resolved dependency id.
858
+ *
859
+ * @example Turns a named import into a registry lookup.
860
+ * ```ts
861
+ * rewriteImport("import { on, trigger } from './events.js'", 'utils/helpers.js');
862
+ * ```
863
+ */
864
+ function rewriteImport(line, currentId) {
865
+ const sideEffect = line.match(/^import\s*['"]([^'"]+)['"];?\s*$/);
866
+ if (sideEffect) {
867
+ const id = resolveId(sideEffect[1], currentId);
868
+ return {
869
+ line: id ? `__r(${JSON.stringify(id)});` : line,
870
+ spec: id,
871
+ names: [],
872
+ pairs: []
873
+ };
874
+ }
875
+ const m = line.match(/^import\s+(?:\*\s+as\s+(\w+)|\{([^}]*)\}|(\w+))\s*from\s*['"]([^'"]+)['"];?\s*$/);
876
+ if (!m) return {
877
+ line,
878
+ names: [],
879
+ pairs: []
880
+ };
881
+ const id = resolveId(m[4], currentId);
882
+ if (!id) return {
883
+ line,
884
+ names: [],
885
+ pairs: []
886
+ };
887
+ const lookup = `__r(${JSON.stringify(id)})`;
888
+ if (m[1]) return {
889
+ line: `const ${m[1]} = ${lookup};`,
890
+ spec: id,
891
+ names: [],
892
+ pairs: []
893
+ };
894
+ if (m[2]) return {
895
+ line: `const { ${parseBindings(m[2]).map((b) => b.local === b.exported ? b.local : `${b.local}: ${b.exported}`).join(", ")} } = ${lookup};`,
896
+ spec: id,
897
+ names: [],
898
+ pairs: []
899
+ };
900
+ return {
901
+ line: `const ${m[3]} = ${lookup}.default;`,
902
+ spec: id,
903
+ names: [],
904
+ pairs: []
905
+ };
906
+ }
907
+ /**
908
+ * Rewrite a single top-level `export` line. `export *` / `export { } from`
909
+ * become registry merges; declarations and bare `export { }` record the local
910
+ * bindings that get assigned to `exports` at the end of the module body.
911
+ *
912
+ * @example Turns a star re-export into an Object.assign merge.
913
+ * ```ts
914
+ * rewriteExport("export * from './helpers.js'", 'microcomponents.js');
915
+ * ```
916
+ */
917
+ function rewriteExport(line, currentId) {
918
+ const star = line.match(/^export\s+\*\s+from\s*['"]([^'"]+)['"];?\s*$/);
919
+ if (star) {
920
+ const id = resolveId(star[1], currentId);
921
+ return {
922
+ line: id ? `Object.assign(exports, ${`__r(${JSON.stringify(id)})`});` : line,
923
+ spec: id,
924
+ star: true,
925
+ names: [],
926
+ pairs: []
927
+ };
928
+ }
929
+ const namedFrom = line.match(/^export\s+\{([^}]*)\}\s*from\s*['"]([^'"]+)['"];?\s*$/);
930
+ if (namedFrom) {
931
+ const id = resolveId(namedFrom[2], currentId);
932
+ const bindings = parseBindings(namedFrom[1]);
933
+ const destructure = bindings.map((b) => `${b.local}: __x_${b.exported}`).join(", ");
934
+ const assigns = bindings.map((b) => `exports.${b.exported} = __x_${b.exported};`).join(" ");
935
+ return {
936
+ line: id ? `const { ${destructure} } = __r(${JSON.stringify(id)}); ${assigns}` : line,
937
+ spec: id,
938
+ names: bindings.map((b) => b.exported),
939
+ pairs: []
940
+ };
941
+ }
942
+ const named = line.match(/^export\s+\{([^}]*)\};?\s*$/);
943
+ if (named) {
944
+ const bindings = parseBindings(named[1]);
945
+ return {
946
+ line: "",
947
+ names: bindings.map((b) => b.exported),
948
+ pairs: bindings
949
+ };
950
+ }
951
+ const decl = line.match(/^export\s+(function|class|const|let|var)\s+(\w+)/);
952
+ if (decl) return {
953
+ line: line.replace(/^export\s+/, ""),
954
+ names: [decl[2]],
955
+ pairs: [{
956
+ local: decl[2],
957
+ exported: decl[2]
958
+ }]
959
+ };
960
+ if (line.match(/^export\s+default\s+/)) return {
961
+ line: "const __default = " + line.replace(/^export\s+default\s+/, ""),
962
+ names: ["default"],
963
+ pairs: [{
964
+ local: "__default",
965
+ exported: "default"
966
+ }]
967
+ };
968
+ return {
969
+ line,
970
+ names: [],
971
+ pairs: []
972
+ };
973
+ }
974
+ /**
975
+ * Depth-first, post-order walk of one entry's dependency graph. Each visited
976
+ * module is rewritten once; `export *` merges the dependency's export names
977
+ * into the current module's public set.
978
+ *
979
+ * @example Walks the whole barrel graph from the entry.
980
+ * ```ts
981
+ * const mods = await collectModules('mc/bundle.js', 'build');
982
+ * ```
983
+ */
984
+ async function collectModules(entryId, root, inline) {
985
+ const modules = /* @__PURE__ */ new Map();
986
+ /** Modules currently being processed. Real cycles exist in the graph
987
+ * (e.g. `state/observables.js` <-> `state/placeholders.js`), so a
988
+ * re-entrant visit must return early instead of recursing forever. The
989
+ * `__r` registry mirrors ESM live-binding semantics, so the cycle resolves
990
+ * at runtime once both modules have run. */
991
+ const visiting = /* @__PURE__ */ new Set();
992
+ async function visit(id) {
993
+ if (modules.has(id) || visiting.has(id)) return;
994
+ visiting.add(id);
995
+ let code = await readFile(path.join(root, id), "utf8");
996
+ if (inline) code = await inlineAssets(code, path.join(root, path.dirname(id)));
997
+ const stripped = stripComments(code);
998
+ const lines = code.split("\n");
999
+ const strippedLines = stripped.split("\n");
1000
+ const exportNames = /* @__PURE__ */ new Set();
1001
+ const pairs = [];
1002
+ const out = [];
1003
+ const deps = [];
1004
+ const starSpecs = [];
1005
+ for (let i = 0; i < lines.length; i++) {
1006
+ const t = strippedLines[i].trim();
1007
+ if (t.startsWith("import ")) {
1008
+ const r = rewriteImport(lines[i], id);
1009
+ out.push(r.line);
1010
+ if (r.spec) deps.push(r.spec);
1011
+ } else if (t.startsWith("export ")) {
1012
+ const r = rewriteExport(lines[i], id);
1013
+ out.push(r.line);
1014
+ if (r.spec) deps.push(r.spec);
1015
+ if (r.star && r.spec) starSpecs.push(r.spec);
1016
+ for (const n of r.names) exportNames.add(n);
1017
+ for (const p of r.pairs) pairs.push(p);
1018
+ } else out.push(lines[i]);
1019
+ }
1020
+ for (const d of deps) await visit(d);
1021
+ for (const s of starSpecs) for (const n of modules.get(s).exportNames) exportNames.add(n);
1022
+ visiting.delete(id);
1023
+ modules.set(id, {
1024
+ id,
1025
+ body: out.join("\n"),
1026
+ exportNames,
1027
+ pairs
1028
+ });
1029
+ }
1030
+ await visit(entryId);
1031
+ return modules;
1032
+ }
1033
+ /**
1034
+ * Assemble one entry's flattened ES module string from its collected graph.
1035
+ *
1036
+ * @example Produces a self-contained bundle for the barrel entry.
1037
+ * ```ts
1038
+ * const code = await bundleEntry('mc/bundle.js', 'build', true);
1039
+ * ```
1040
+ * @param entryId Entry module id, relative to `root` (e.g. `mc/bundle.js`).
1041
+ * @param root Folder holding the compiled `.js` files.
1042
+ * @param inline Inline companion assets into DC classes before wrapping.
1043
+ */
1044
+ async function bundleEntry(entryId, root, inline = true) {
1045
+ const modules = await collectModules(entryId, root, inline);
1046
+ const wrappers = [];
1047
+ for (const m of modules.values()) {
1048
+ const assigns = m.pairs.map((p) => `exports.${p.exported} = ${p.local};`).join("\n");
1049
+ wrappers.push(`__m[${JSON.stringify(m.id)}] = function (exports, __r) {\n${m.body}\n${assigns}\n};`);
1050
+ }
1051
+ const names = [...modules.get(entryId).exportNames];
1052
+ const bindings = names.map((n, i) => `const __ex${i} = __e[${JSON.stringify(n)}];`).join("\n");
1053
+ const publicExports = names.map((n, i) => `__ex${i} as ${n}`).join(", ");
1054
+ return [
1055
+ "const __m = {};",
1056
+ "const __c = {};",
1057
+ "function __r(id) {",
1058
+ " if (__c[id]) return __c[id];",
1059
+ " const exp = {};",
1060
+ " __c[id] = exp;",
1061
+ " __m[id](exp, __r);",
1062
+ " return exp;",
1063
+ "}",
1064
+ wrappers.join("\n"),
1065
+ `const __e = __r(${JSON.stringify(entryId)});`,
1066
+ bindings,
1067
+ `export { ${publicExports} };`
1068
+ ].join("\n");
1069
+ }
1070
+ //#endregion
1071
+ //#region src/cli/bundle.ts
1072
+ /**
1073
+ Filesystem side of `mc bundle`: assembles the production site in `public/` from
1074
+ the `tsc`-emitted `build/` tree, with no Vite.
1075
+
1076
+ The chain is `src/` → `build/` → `public/`. The scaffolded `build` script runs
1077
+ `tsc -p tsconfig.json` (so `build/` holds one `.js` per component at its
1078
+ autoload path), then `mc bundle` mirrors the companion assets into `build/`,
1079
+ walks the barrel entry named by `src/bundle.json` through {@link bundleEntry}
1080
+ (inlining every `static assets`), rewrites the `@twobirds/microcomponents`
1081
+ specifier to the self-contained `microcomponents.min.js` shipped next to the
1082
+ bundle, concatenates the plain `.css` token presets into `bundle.css`, and
1083
+ copies the production shell. The result is a complete static site: a single
1084
+ `bundle.js` loads every component, no autoloader needed.
1085
+
1086
+ @module CLI/Bundle
1087
+ */
1088
+ /** Companion asset extensions copied verbatim next to their compiled `.js`. */
1089
+ var COMPANION_EXTENSIONS = [
1090
+ ".css",
1091
+ ".html",
1092
+ ".json"
1093
+ ];
1094
+ /**
1095
+ * Whether a file is a companion asset to mirror into `build/`.
1096
+ *
1097
+ * @example
1098
+ * ```ts
1099
+ * isCompanion('app.target.html'); // true
1100
+ * isCompanion('index.html'); // false
1101
+ * ```
1102
+ * @param name Basename of the file.
1103
+ */
1104
+ function isCompanion(name) {
1105
+ if (name === "index.html" || name === "index-dev.html") return false;
1106
+ return COMPANION_EXTENSIONS.some((ext) => name.endsWith(ext));
1107
+ }
1108
+ /**
1109
+ * Mirror companion assets (`.css`, `.html`, `.json`) from `src/` into `build/`,
1110
+ * preserving the tree, so the bundler reads each asset next to its compiled `.js`.
1111
+ *
1112
+ * @example
1113
+ * ```ts
1114
+ * await mirrorCompanions('<root>/src', '<root>/build');
1115
+ * ```
1116
+ * @param srcDir Source root to walk.
1117
+ * @param buildDir Folder to write into.
1118
+ * @param rel Path of the walked directory relative to `srcDir`.
1119
+ */
1120
+ async function mirrorCompanions(srcDir, buildDir, rel = "") {
1121
+ for (const entry of await readdir(path.join(srcDir, rel), { withFileTypes: true })) {
1122
+ const next = rel ? path.join(rel, entry.name) : entry.name;
1123
+ if (entry.isDirectory()) {
1124
+ if (entry.name === "node_modules") continue;
1125
+ await mirrorCompanions(srcDir, buildDir, next);
1126
+ } else if (isCompanion(entry.name)) {
1127
+ const dst = path.join(buildDir, next);
1128
+ await mkdir(path.dirname(dst), { recursive: true });
1129
+ await cp(path.join(srcDir, next), dst);
1130
+ }
1131
+ }
1132
+ }
1133
+ /**
1134
+ * Collect the plain `.css` token presets under `src/`, skipping the companion
1135
+ * `.target.css` / `.shadow.css` files, which the bundler inlines into the JS.
1136
+ *
1137
+ * @example
1138
+ * ```ts
1139
+ * const css = await collectPlainCss('<root>/src');
1140
+ * ```
1141
+ * @param srcDir Source root to walk.
1142
+ * @param rel Path of the walked directory relative to `srcDir`.
1143
+ * @param found Accumulated absolute paths.
1144
+ */
1145
+ async function collectPlainCss(srcDir, rel = "", found = []) {
1146
+ for (const entry of await readdir(path.join(srcDir, rel), { withFileTypes: true })) {
1147
+ const next = rel ? path.join(rel, entry.name) : entry.name;
1148
+ if (entry.isDirectory()) {
1149
+ if (entry.name !== "node_modules") await collectPlainCss(srcDir, next, found);
1150
+ } else if (entry.name.endsWith(".css") && !/\.(target|shadow)\.css$/.test(entry.name)) found.push(path.join(srcDir, next));
1151
+ }
1152
+ return found;
1153
+ }
1154
+ /**
1155
+ * Rewrite the library specifier in a bundled module to the self-contained bundle
1156
+ * shipped next to it, so the browser can resolve `@twobirds/microcomponents`
1157
+ * without a bare-specifier import map.
1158
+ *
1159
+ * @example Turns the package specifier into a relative path.
1160
+ * ```ts
1161
+ * rewriteLibrarySpecifier("import { DC } from '@twobirds/microcomponents';");
1162
+ * ```
1163
+ * @param code Bundled module source.
1164
+ * @param target Relative specifier to point the import at.
1165
+ */
1166
+ function rewriteLibrarySpecifier(code, target = "./microcomponents.min.js") {
1167
+ return code.replace(/from\s*['"]@twobirds\/microcomponents['"]/g, `from '${target}'`);
1168
+ }
1169
+ /**
1170
+ * Assemble the production site in `public/` from the compiled `build/` tree.
1171
+ *
1172
+ * @example
1173
+ * ```ts
1174
+ * await bundleProject('<root>', { minJs: '<pkg>/dist/microcomponents.min.js' });
1175
+ * ```
1176
+ * @param projectRoot Project root (the folder holding `src/`, `build/`, `public/`).
1177
+ * @param options.minJs Absolute path of the self-contained `microcomponents.min.js` to ship next to the bundle.
1178
+ * @param options.bundleEntry Optional override of the barrel entry (defaults to `src/bundle.json`).
1179
+ * @returns The names of the files written into `public/`.
1180
+ */
1181
+ async function bundleProject(projectRoot, options) {
1182
+ const sourceRoot = path.join(projectRoot, "src");
1183
+ const buildRoot = path.join(projectRoot, "build");
1184
+ const config = JSON.parse(await readFile(path.join(sourceRoot, "bundle.json"), "utf8"));
1185
+ const publicRoot = path.join(projectRoot, options.outDir ?? config.outDir ?? "public");
1186
+ const fileName = config.fileName ?? "bundle.js";
1187
+ await mirrorCompanions(sourceRoot, buildRoot);
1188
+ const rewritten = rewriteLibrarySpecifier(await bundleEntry((options.bundleEntry ?? config.entry).replace(/\.ts$/, ".js"), buildRoot, true));
1189
+ await mkdir(publicRoot, { recursive: true });
1190
+ await writeFile(path.join(publicRoot, fileName), rewritten, "utf8");
1191
+ await cp(options.minJs, path.join(publicRoot, "microcomponents.min.js"));
1192
+ const cssFiles = await collectPlainCss(sourceRoot);
1193
+ const parts = await Promise.all(cssFiles.map((f) => readFile(f, "utf8")));
1194
+ await writeFile(path.join(publicRoot, "bundle.css"), parts.join("\n"), "utf8");
1195
+ await cp(path.join(sourceRoot, "index.html"), path.join(publicRoot, "index.html"));
1196
+ return [
1197
+ fileName,
1198
+ "microcomponents.min.js",
1199
+ "bundle.css",
1200
+ "index.html"
1201
+ ];
1202
+ }
1203
+ /**
1204
+ * CLI entry for `mc bundle`: runs {@link bundleProject} against the current
1205
+ * project, reading the self-contained library from this package's own `dist/`.
1206
+ *
1207
+ * @param projectRoot Project root (defaults to the current directory).
1208
+ * @param minJs Path of `microcomponents.min.js` (defaults to this package's `dist/`).
1209
+ * @returns The process exit code.
1210
+ */
1211
+ async function runBundle(projectRoot = process.cwd(), minJs = path.join(import.meta.dirname, "..", "microcomponents.min.js")) {
1212
+ const written = await bundleProject(projectRoot, { minJs });
1213
+ console.log(`bundled ${written.join(", ")} into public/`);
1214
+ return 0;
1215
+ }
1216
+ //#endregion
1217
+ //#region src/cli/dev.ts
1218
+ /**
1219
+ Vite-free dev server for `mc dev`.
1220
+
1221
+ A minimal `node:http` static server rooted at the project's `build/` folder, so
1222
+ a saved component reaches the browser through `autoload()` with no bundling. It
1223
+ mirrors companion assets from `src/` into `build/` (on startup and on every
1224
+ change), runs `tsc -w` for the lifetime of the server, serves companion
1225
+ `.css` / `.html` / `.json` as raw text (so `loadAssets()` receives markup it
1226
+ can use), redirects `/` to the autoload shell (`index-dev.html`), and serves
1227
+ the self-contained library at `/microcomponents.min.js` — the target of the
1228
+ import map in the dev shell.
1229
+
1230
+ There is no HMR: a page reload picks up the recompiled component.
1231
+
1232
+ @module CLI/Dev
1233
+ */
1234
+ /** Page shells Vite/the server serves as HTML; everything else stays raw text. */
1235
+ var SHELLS = /* @__PURE__ */ new Set(["index.html", "index-dev.html"]);
1236
+ /**
1237
+ * Content type for a served file. Companion `.html` / `.json` stay raw text so
1238
+ * `loadAssets()` gets usable markup; the page shells are HTML.
1239
+ *
1240
+ * @example
1241
+ * ```ts
1242
+ * mimeFor('app.target.html'); // 'text/plain'
1243
+ * mimeFor('index-dev.html'); // 'text/html'
1244
+ * mimeFor('app.js'); // 'text/javascript'
1245
+ * ```
1246
+ * @param name Basename of the file.
1247
+ */
1248
+ function mimeFor(name) {
1249
+ if (name.endsWith(".js")) return "text/javascript";
1250
+ if (name.endsWith(".css")) return "text/css";
1251
+ if (name.endsWith(".svg")) return "image/svg+xml";
1252
+ if (name.endsWith(".html")) return SHELLS.has(name) ? "text/html" : "text/plain";
1253
+ if (name.endsWith(".json")) return "text/plain";
1254
+ return "application/octet-stream";
1255
+ }
1256
+ /**
1257
+ * Resolve a request URL to a file to serve, given the served `build/` root and
1258
+ * the library bundle. `/` and `/index.html` map to the autoload shell;
1259
+ * `/microcomponents.min.js` maps to the library; everything else to `build/`.
1260
+ *
1261
+ * @example
1262
+ * ```ts
1263
+ * resolveTarget('/mc/app.target.html', buildRoot, minJs);
1264
+ * resolveTarget('/', buildRoot, minJs);
1265
+ * ```
1266
+ * @param url Request URL (path only, no query).
1267
+ * @param buildRoot Served folder.
1268
+ * @param minJs Absolute path of the self-contained library.
1269
+ * @returns Absolute file path, or `undefined` when nothing matches.
1270
+ */
1271
+ function resolveTarget(url, buildRoot, minJs) {
1272
+ const file = decodeURIComponent(url.split("?")[0]);
1273
+ if (url === "/" || url === "/index.html") return path.join(buildRoot, "index-dev.html");
1274
+ if (url === "/microcomponents.min.js") return minJs;
1275
+ const abs = path.join(buildRoot, file);
1276
+ return abs.startsWith(buildRoot) && existsSync(abs) ? abs : void 0;
1277
+ }
1278
+ /**
1279
+ * Serve one request from the `build/` tree.
1280
+ */
1281
+ async function serve(req, res, buildRoot, minJs) {
1282
+ const abs = resolveTarget(req.url?.split("?")[0] ?? "/", buildRoot, minJs);
1283
+ if (!abs) {
1284
+ res.statusCode = 404;
1285
+ res.end("not found");
1286
+ return;
1287
+ }
1288
+ try {
1289
+ const text = await readFile(abs, "utf-8");
1290
+ res.setHeader("Content-Type", mimeFor(path.basename(abs)));
1291
+ res.end(text);
1292
+ } catch {
1293
+ /* v8 ignore start — resolveTarget already checked existence; a read failure is a race */
1294
+ res.statusCode = 404;
1295
+ res.end("not found");
1296
+ }
1297
+ }
1298
+ /**
1299
+ * Mirror companion assets from `src/` into `build/`, preserving the tree, so the
1300
+ * served folder stays autoload-ready.
1301
+ *
1302
+ * @example
1303
+ * ```ts
1304
+ * await mirrorInto(sourceRoot, buildRoot);
1305
+ * ```
1306
+ * @param sourceRoot Source root to walk.
1307
+ * @param buildRoot Folder to write into.
1308
+ * @param rel Path of the walked directory relative to `sourceRoot`.
1309
+ */
1310
+ async function mirrorInto(sourceRoot, buildRoot, rel = "") {
1311
+ for (const entry of await readdir(path.join(sourceRoot, rel), { withFileTypes: true })) {
1312
+ const next = rel ? path.join(rel, entry.name) : entry.name;
1313
+ if (entry.isDirectory()) {
1314
+ if (entry.name !== "node_modules") await mirrorInto(sourceRoot, buildRoot, next);
1315
+ } else if (isCompanion(entry.name) || entry.name === "index-dev.html") {
1316
+ const dst = path.join(buildRoot, next);
1317
+ await mkdir(path.dirname(dst), { recursive: true });
1318
+ await cp(path.join(sourceRoot, next), dst);
1319
+ }
1320
+ }
1321
+ }
1322
+ /**
1323
+ * Build the dev server for a project: an `node:http` server rooted at `build/`.
1324
+ *
1325
+ * @example
1326
+ * ```ts
1327
+ * const server = createDevServer('<root>', { minJs: '<pkg>/dist/microcomponents.min.js', port: 5173 });
1328
+ * server.listen(port);
1329
+ * ```
1330
+ * @param projectRoot Project root (the folder holding `src/`, `build/`).
1331
+ * @param options.minJs Absolute path of the self-contained library.
1332
+ * @param options.port Dev-server port.
1333
+ * @returns The HTTP server (not yet listening).
1334
+ */
1335
+ function createDevServer(projectRoot, options) {
1336
+ const buildRoot = path.join(projectRoot, "build");
1337
+ return createServer((req, res) => {
1338
+ serve(req, res, buildRoot, options.minJs);
1339
+ });
1340
+ }
1341
+ /**
1342
+ * CLI entry for `mc dev`: mirror companions, start the server, and run `tsc -w`
1343
+ * for the lifetime of the process.
1344
+ *
1345
+ * @param projectRoot Project root (defaults to the current directory).
1346
+ * @param minJs Path of `microcomponents.min.js` (defaults to this package's `dist/`).
1347
+ * @param port Dev-server port.
1348
+ * @returns The process exit code (the server keeps running until killed).
1349
+ */
1350
+ /* v8 ignore start — the long-running server + tsc watch is exercised by hand, not in the suite */
1351
+ async function runDev(projectRoot = process.cwd(), minJs = path.join(import.meta.dirname, "..", "microcomponents.min.js"), port = 5173) {
1352
+ const sourceRoot = path.join(projectRoot, "src");
1353
+ const buildRoot = path.join(projectRoot, "build");
1354
+ await mirrorInto(sourceRoot, buildRoot);
1355
+ const tsc = spawn("npx", [
1356
+ "tsc",
1357
+ "-w",
1358
+ "-p",
1359
+ "tsconfig.json"
1360
+ ], {
1361
+ cwd: projectRoot,
1362
+ stdio: "inherit"
1363
+ });
1364
+ watch(sourceRoot, { recursive: true }, (_event, file) => {
1365
+ if (!file || file.endsWith(".ts")) return;
1366
+ const abs = path.join(sourceRoot, file);
1367
+ if (isCompanion(path.basename(file)) || file.endsWith("index-dev.html")) {
1368
+ const dst = path.join(buildRoot, file);
1369
+ mkdir(path.dirname(dst), { recursive: true }).then(() => cp(abs, dst));
1370
+ }
1371
+ });
1372
+ const server = createDevServer(projectRoot, {
1373
+ minJs,
1374
+ port
1375
+ });
1376
+ server.listen(port);
1377
+ console.log(`dev server on http://localhost:${port}/ (serving build/, tsc -w running)`);
1378
+ return new Promise((resolve) => {
1379
+ tsc.on("exit", () => {
1380
+ server.close();
1381
+ resolve(0);
1382
+ });
1383
+ });
1384
+ }
1385
+ /* v8 ignore end */
1386
+ //#endregion
1387
+ //#region src/cli/preview.ts
1388
+ /**
1389
+ Vite-free preview server for `mc preview`.
1390
+
1391
+ A minimal `node:http` static server rooted at the project's `public/` folder —
1392
+ the production site assembled by `mc bundle` (one `bundle.js`, the
1393
+ `microcomponents.min.js` it imports, `bundle.css`, and the page shell). `/`
1394
+ serves `index.html`; every file is served raw with its content type.
1395
+
1396
+ @module CLI/Preview
1397
+ */
1398
+ /**
1399
+ * Resolve a request URL to a file in `public/`. `/` and `/index.html` map to the
1400
+ * production shell; everything else to `public/<path>`.
1401
+ *
1402
+ * @example
1403
+ * ```ts
1404
+ * resolvePublicTarget('/bundle.js', publicRoot);
1405
+ * resolvePublicTarget('/', publicRoot);
1406
+ * ```
1407
+ * @param url Request URL (path only, no query).
1408
+ * @param publicRoot Served folder.
1409
+ * @returns Absolute file path, or `undefined` when nothing matches.
1410
+ */
1411
+ function resolvePublicTarget(url, publicRoot) {
1412
+ const file = decodeURIComponent(url.split("?")[0]);
1413
+ if (url === "/" || url === "/index.html") return path.join(publicRoot, "index.html");
1414
+ const abs = path.join(publicRoot, file);
1415
+ return abs.startsWith(publicRoot) && existsSync(abs) ? abs : void 0;
1416
+ }
1417
+ /**
1418
+ * Serve one request from the `public/` tree.
1419
+ */
1420
+ async function servePublic(req, res, publicRoot) {
1421
+ const abs = resolvePublicTarget(req.url?.split("?")[0] ?? "/", publicRoot);
1422
+ if (!abs) {
1423
+ res.statusCode = 404;
1424
+ res.end("not found");
1425
+ return;
1426
+ }
1427
+ try {
1428
+ const text = await readFile(abs, "utf-8");
1429
+ res.setHeader("Content-Type", mimeFor(path.basename(abs)));
1430
+ res.end(text);
1431
+ } catch {
1432
+ /* v8 ignore start — resolvePublicTarget already checked existence; a read failure is a race */
1433
+ res.statusCode = 404;
1434
+ res.end("not found");
1435
+ }
1436
+ }
1437
+ /**
1438
+ * Build the preview server for a project: an `node:http` server rooted at `public/`.
1439
+ *
1440
+ * @example
1441
+ * ```ts
1442
+ * const server = createPreviewServer('<root>', 5174);
1443
+ * server.listen(5174);
1444
+ * ```
1445
+ * @param projectRoot Project root (the folder holding `public/`).
1446
+ * @param port Preview-server port.
1447
+ * @param outDir Output folder relative to the root (default `public`).
1448
+ * @returns The HTTP server (not yet listening).
1449
+ */
1450
+ function createPreviewServer(projectRoot, port, outDir = "public") {
1451
+ const publicRoot = path.join(projectRoot, outDir);
1452
+ return createServer((req, res) => {
1453
+ servePublic(req, res, publicRoot);
1454
+ });
1455
+ }
1456
+ /**
1457
+ * CLI entry for `mc preview`: serve the production site in `public/`.
1458
+ *
1459
+ * @param projectRoot Project root (defaults to the current directory).
1460
+ * @param port Preview-server port.
1461
+ * @returns The process exit code (the server keeps running until killed).
1462
+ */
1463
+ /* v8 ignore start — the long-running server is exercised by hand, not in the suite */
1464
+ async function runPreview(projectRoot = process.cwd(), port = 5174) {
1465
+ const server = createPreviewServer(projectRoot, port);
1466
+ server.listen(port);
1467
+ console.log(`preview server on http://localhost:${port}/ (serving public/)`);
1468
+ return new Promise((resolve) => {
1469
+ server.on("close", () => resolve(0));
1470
+ });
1471
+ }
1472
+ /* v8 ignore end */
1473
+ //#endregion
1474
+ //#region src/cli/dispatch.ts
1475
+ /**
1476
+ Command dispatcher for the `mc` CLI.
1477
+
1478
+ Kept free of any top-level side effects so the routing can be unit-tested:
1479
+ `index.ts` is the thin executable entry that calls {@link main}, while this
1480
+ module only exports the logic. Commands that wrap a package.json script
1481
+ (`clean`, `check`) are deliberately thin — they delegate to the script so the
1482
+ script stays the single source of truth. When the current project defines no
1483
+ such script, the CLI runs its own implementation against the current project,
1484
+ so the command works in any consumer project.
1485
+
1486
+ @module CLI/Dispatch
1487
+ */
1488
+ var USAGE = `mc — Micro Components build tools
1489
+
1490
+ Usage:
1491
+ mc new <tag> [--css] [--html] [--target-html] [--shadow-html] [--target-css] [--shadow-css] [--js] [--dir <path>] [--force] [--dry-run]
1492
+ mc create --namespace <ns> [--dir <path>] [--force] [--dry-run]
1493
+ mc dev [--port <n>]
1494
+ mc bundle
1495
+ mc preview [--port <n>]
1496
+ mc clean
1497
+ mc check
1498
+ mc --version
1499
+ mc --help
1500
+
1501
+ Commands:
1502
+ new <tag> Scaffold a micro component (e.g. mc new my-card --target-html --target-css)
1503
+ create Create a dev environment (e.g. mc create --namespace mc --dir mc-app)
1504
+ dev Serve build/ through autoload() with tsc -w (no bundling)
1505
+ bundle Assemble public/ from build/: bundle.js with assets inlined, bundle.css, shell
1506
+ preview Serve the production site in public/
1507
+ clean Remove build artifacts (dist, public, build, coverage)
1508
+ check Type-check every project (lib, cli, test, demo, node)
1509
+
1510
+ Companion assets:
1511
+ --css document-level stylesheet (CSS custom properties / design tokens)
1512
+ --html unvariant light-DOM template
1513
+ --target-html template applied to target.innerHTML
1514
+ --shadow-html template applied to shadowRoot.innerHTML
1515
+ --target-css document-level styles scoped to the host tag
1516
+ --shadow-css styles injected into the shadow root
1517
+
1518
+ Other options:
1519
+ --js emit plain JavaScript instead of TypeScript
1520
+ --dir <path> scaffold root (default: src)
1521
+ --port <n> dev / preview server port (default: 5173 / 5174)
1522
+ --force overwrite files that already exist
1523
+ --dry-run print what would be written; touch nothing on disk
1524
+ --version print the version of the package the CLI ships in (-v)
1525
+ --help print this text (-h)
1526
+
1527
+ Created paths are printed as they appear from the current directory. mc create writes
1528
+ a whole project into a folder of its own (default: the namespace), so its files show
1529
+ up under that folder, e.g. mc-app/src/mc/app.ts.
1530
+ `;
1531
+ var ASSET_FLAGS = {
1532
+ css: "css",
1533
+ html: "html",
1534
+ "target-html": "targetHtml",
1535
+ "shadow-html": "shadowHtml",
1536
+ "target-css": "targetCss",
1537
+ "shadow-css": "shadowCss"
1538
+ };
1539
+ /**
1540
+ A scaffolded file as it appears from the current directory: the file `rel` under
1541
+ `root`, resolved and made readable. `mc create` writes a project into a folder of
1542
+ its own, so `src/mc/app.ts` under `mc-app` prints as `mc-app/src/mc/app.ts` — the
1543
+ path the user can find with `ls`.
1544
+
1545
+ @example
1546
+ ```ts
1547
+ createdPath('mc-app', 'src/mc/app.ts');
1548
+ createdPath('src', 'my/card.ts');
1549
+ ```
1550
+ @param root - Scaffold root the file is written under.
1551
+ @param rel - Path of the file relative to `root`.
1552
+ */
1553
+ function createdPath(root, rel) {
1554
+ return displayPath(path.resolve(root, rel));
1555
+ }
1556
+ /**
1557
+ Spawns a command and resolves with its exit code.
1558
+
1559
+ @example
1560
+ ```ts
1561
+ const code = await spawnProcess('pnpm', ['run', 'clean']);
1562
+ ```
1563
+ @param command - Executable to spawn.
1564
+ @param args - Arguments to pass.
1565
+ @param cwd - Working directory (defaults to the current one).
1566
+ */
1567
+ function spawnProcess(command, args, cwd) {
1568
+ return new Promise((resolve) => {
1569
+ spawn(command, args, {
1570
+ stdio: "inherit",
1571
+ cwd
1572
+ }).on("exit", (code) => resolve(code ?? 1));
1573
+ });
1574
+ }
1575
+ /**
1576
+ Whether the package.json in the current directory declares `script` in its
1577
+ "scripts". Consumers get this check so `mc clean`/`mc check` can fall back to
1578
+ the CLI's own implementation when the project defines no such script.
1579
+
1580
+ @example
1581
+ ```ts
1582
+ const ok = await hasScript('clean');
1583
+ ```
1584
+ @param script - Script name to look up.
1585
+ */
1586
+ async function hasScript(script) {
1587
+ try {
1588
+ return typeof JSON.parse(await readFile(path.join(process.cwd(), "package.json"), "utf8"))?.scripts?.[script] === "string";
1589
+ } catch {
1590
+ return false;
1591
+ }
1592
+ }
1593
+ /** Build artifacts to remove, relative to the project root. */
1594
+ var CLEAN_DIRS = [
1595
+ "dist",
1596
+ "public",
1597
+ "build",
1598
+ "coverage"
1599
+ ];
1600
+ /**
1601
+ Removes build artifacts in the current project.
1602
+
1603
+ @example
1604
+ ```ts
1605
+ const code = await cleanSelf();
1606
+ ```
1607
+ @param cwd - Project root (defaults to the current directory).
1608
+ */
1609
+ async function cleanSelf(cwd = process.cwd()) {
1610
+ await Promise.all(CLEAN_DIRS.map(async (dir) => {
1611
+ const abs = path.join(cwd, dir);
1612
+ if (existsSync(abs)) {
1613
+ await rm(abs, {
1614
+ recursive: true,
1615
+ force: true
1616
+ });
1617
+ console.log(`removed ${dir}/`);
1618
+ }
1619
+ }));
1620
+ return 0;
1621
+ }
1622
+ /**
1623
+ Type-checks the current project with its own `tsconfig.json`.
1624
+
1625
+ @example
1626
+ ```ts
1627
+ const code = await checkSelf();
1628
+ ```
1629
+ @param cwd - Project root (defaults to the current directory).
1630
+ */
1631
+ async function checkSelf(cwd = process.cwd()) {
1632
+ if (!existsSync(path.join(cwd, "tsconfig.json"))) {
1633
+ console.error("mc check: no tsconfig.json in the current project");
1634
+ return 1;
1635
+ }
1636
+ return spawnProcess("pnpm", [
1637
+ "exec",
1638
+ "tsc",
1639
+ "--noEmit",
1640
+ "-p",
1641
+ "tsconfig.json"
1642
+ ], cwd);
1643
+ }
1644
+ /**
1645
+ The version of the package the CLI ships in, read from its package.json. Two
1646
+ levels above `dist/cli` (and above `src/cli` when tsx runs the source) is the
1647
+ package root, so the CLI reports the version it was installed with, not the
1648
+ version of the project it is run in.
1649
+
1650
+ @example
1651
+ ```ts
1652
+ const version = await readVersion();
1653
+ ```
1654
+ @param pkgPath - package.json to read (defaults to the package root of this CLI).
1655
+ */
1656
+ async function readVersion(pkgPath = path.join(import.meta.dirname, "..", "..", "package.json")) {
1657
+ const { version } = JSON.parse(await readFile(pkgPath, "utf8"));
1658
+ return version;
1659
+ }
1660
+ /**
1661
+ Runs a package.json script through pnpm and resolves with its exit code.
1662
+
1663
+ Thin wrappers like `clean` and `check` delegate here so the script — not the
1664
+ CLI — remains the single source of truth for what each command does. If the
1665
+ current project defines no such script, the CLI runs its own implementation
1666
+ against the current project, so the command works in any consumer project.
1667
+
1668
+ @example
1669
+ ```ts
1670
+ const code = await runScript('check');
1671
+ ```
1672
+ */
1673
+ async function runScript(script, options) {
1674
+ if (await hasScript(script)) return spawnProcess("pnpm", ["run", script]);
1675
+ if (script === "clean") return cleanSelf();
1676
+ if (script === "check") return checkSelf();
1677
+ if (script === "dev") return runDev(process.cwd(), void 0, options?.port ?? 5173);
1678
+ if (script === "bundle") return runBundle();
1679
+ if (script === "preview") return runPreview(process.cwd(), options?.port ?? 5174);
1680
+ console.error(`mc: no "${script}" script in the current project`);
1681
+ return 1;
1682
+ }
1683
+ /**
1684
+ Parses `argv` and routes it to the matching command, returning a process exit
1685
+ code. `run` is injectable so the script-wrapping commands can be tested without
1686
+ spawning anything.
1687
+
1688
+ @example
1689
+ ```ts
1690
+ const code = await main(['clean'], async (script) => {
1691
+ console.log(`would run ${script}`);
1692
+ return 0;
1693
+ });
1694
+ ```
1695
+ @example
1696
+ ```ts
1697
+ const code = await main(['--version']);
1698
+ ```
1699
+ */
1700
+ async function main(argv, run = runScript) {
1701
+ const { values, positionals } = parseArgs({
1702
+ args: argv,
1703
+ allowPositionals: true,
1704
+ options: {
1705
+ css: { type: "boolean" },
1706
+ html: { type: "boolean" },
1707
+ "target-html": { type: "boolean" },
1708
+ "shadow-html": { type: "boolean" },
1709
+ "target-css": { type: "boolean" },
1710
+ "shadow-css": { type: "boolean" },
1711
+ js: { type: "boolean" },
1712
+ dir: { type: "string" },
1713
+ namespace: { type: "string" },
1714
+ port: { type: "string" },
1715
+ force: { type: "boolean" },
1716
+ "dry-run": { type: "boolean" },
1717
+ help: {
1718
+ type: "boolean",
1719
+ short: "h"
1720
+ },
1721
+ version: {
1722
+ type: "boolean",
1723
+ short: "v"
1724
+ }
1725
+ }
1726
+ });
1727
+ const command = positionals[0];
1728
+ if (values.version) {
1729
+ console.log(await readVersion());
1730
+ return 0;
1731
+ }
1732
+ if (values.help || !command) {
1733
+ console.log(USAGE);
1734
+ return 0;
1735
+ }
1736
+ if (command === "new") {
1737
+ const tag = positionals[1];
1738
+ if (!tag) {
1739
+ console.error("mc new: missing component tag (e.g. mc new my-card)");
1740
+ return 1;
1741
+ }
1742
+ const assets = [];
1743
+ const flags = values;
1744
+ for (const [flag, kind] of Object.entries(ASSET_FLAGS)) if (flags[flag]) assets.push(kind);
1745
+ if (values["dry-run"]) {
1746
+ const { files } = scaffoldComponent(tag, {
1747
+ assets,
1748
+ js: values.js
1749
+ });
1750
+ const root = values.dir ?? "src";
1751
+ for (const rel of Object.keys(files).sort()) console.log(`would create ${createdPath(root, rel)}`);
1752
+ return 0;
1753
+ }
1754
+ const root = values.dir ?? "src";
1755
+ let written;
1756
+ try {
1757
+ written = await writeScaffold(tag, root, {
1758
+ assets,
1759
+ js: values.js,
1760
+ force: values.force
1761
+ });
1762
+ } catch (e) {
1763
+ console.error(e.message);
1764
+ return 1;
1765
+ }
1766
+ for (const rel of written) console.log(`created ${createdPath(root, rel)}`);
1767
+ return 0;
1768
+ }
1769
+ if (command === "create") {
1770
+ const namespace = values.namespace;
1771
+ if (!namespace) {
1772
+ console.error("mc create: missing --namespace (e.g. mc create --namespace mc)");
1773
+ return 1;
1774
+ }
1775
+ const root = values.dir ?? namespace;
1776
+ let files;
1777
+ try {
1778
+ ({files} = scaffoldProject(namespace));
1779
+ } catch (e) {
1780
+ console.error(e.message);
1781
+ return 1;
1782
+ }
1783
+ if (values["dry-run"]) {
1784
+ for (const rel of Object.keys(files).sort()) console.log(`would create ${createdPath(root, rel)}`);
1785
+ return 0;
1786
+ }
1787
+ let written;
1788
+ try {
1789
+ written = await writeProject(namespace, root, { force: values.force });
1790
+ } catch (e) {
1791
+ console.error(e.message);
1792
+ return 1;
1793
+ }
1794
+ for (const rel of written) console.log(`created ${createdPath(root, rel)}`);
1795
+ console.log(`\nproject "${namespace}" created in ${root}/ — run: cd ${root} && pnpm install && pnpm dev`);
1796
+ return 0;
1797
+ }
1798
+ if (command === "dev") return run("dev", { port: values.port ? Number(values.port) : 5173 });
1799
+ if (command === "bundle") return run("bundle");
1800
+ if (command === "preview") return run("preview", { port: values.port ? Number(values.port) : 5174 });
1801
+ if (command === "clean") return run("clean");
1802
+ if (command === "check") return run("check");
1803
+ console.error(`mc: unknown command "${command}"\n\n${USAGE}`);
1804
+ return 1;
1805
+ }
1806
+ //#endregion
1807
+ //#region src/cli/index.ts
2
1808
  /**
3
1809
  Executable entry point of the `mc` CLI (package.json "bin").
4
1810
 
@@ -8,5 +1814,5 @@ in and pushes the resulting exit code out.
8
1814
 
9
1815
  @module CLI/Index
10
1816
  */
11
- import { main } from './dispatch.js';
12
1817
  process.exitCode = await main(process.argv.slice(2));
1818
+ //#endregion