@twobirds/microcomponents 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +14 -4
  2. package/dist/cli/bundle.d.ts +23 -13
  3. package/dist/cli/bundle.d.ts.map +1 -1
  4. package/dist/cli/create.d.ts +28 -8
  5. package/dist/cli/create.d.ts.map +1 -1
  6. package/dist/cli/dev.d.ts +13 -8
  7. package/dist/cli/dev.d.ts.map +1 -1
  8. package/dist/cli/dispatch.d.ts +10 -2
  9. package/dist/cli/dispatch.d.ts.map +1 -1
  10. package/dist/cli/index.js +244 -72
  11. package/dist/cli/preview.d.ts +3 -1
  12. package/dist/cli/preview.d.ts.map +1 -1
  13. package/dist/cli/writeScaffold.d.ts +3 -3
  14. package/dist/docs/api-external.d.ts +11 -2
  15. package/dist/docs/api-external.d.ts.map +1 -1
  16. package/dist/docs/cli.d.ts +1 -1
  17. package/dist/docs/installation.d.ts +8 -3
  18. package/dist/docs/installation.d.ts.map +1 -1
  19. package/dist/helpers.min.js +1 -0
  20. package/dist/mcp/api.json +28 -2
  21. package/dist/mcp/concepts/event-system.md +20 -0
  22. package/dist/mcp/concepts/mc-types.md +15 -8
  23. package/dist/mcp/concepts/no-importmap.md +48 -0
  24. package/dist/mcp/concepts/project-layout.md +9 -5
  25. package/dist/mcp/concepts/state-management.md +13 -7
  26. package/dist/microcomponents.d.ts +1 -0
  27. package/dist/microcomponents.d.ts.map +1 -1
  28. package/dist/microcomponents.js +1 -1
  29. package/dist/microcomponents.min.js +1 -1
  30. package/dist/placeholders.min.js +1 -0
  31. package/dist/state/index.d.ts +2 -0
  32. package/dist/state/index.d.ts.map +1 -1
  33. package/dist/state/placeholders.js +1980 -0
  34. package/dist/utils/autoload.js +1451 -0
  35. package/dist/utils/events.d.ts +1 -1
  36. package/dist/utils/events.d.ts.map +1 -1
  37. package/dist/utils/helpers.js +693 -0
  38. package/dist/utils/index.d.ts +1 -1
  39. package/dist/utils/index.d.ts.map +1 -1
  40. package/package.json +21 -5
package/dist/cli/index.js CHANGED
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
3
3
  import { parseArgs } from "node:util";
4
4
  import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
5
  import { existsSync, watch } from "node:fs";
6
+ import { createInterface } from "node:readline/promises";
6
7
  import path from "node:path";
7
8
  import { createServer } from "node:http";
8
9
  //#region src/utils/resolveComponentPath.ts
@@ -168,8 +169,10 @@ runtime autoload loader uses, so the scaffold and the runtime can never disagree
168
169
  where a component lives.
169
170
 
170
171
  The namespace is the first segment of every component tag, so `xxx` makes `src/xxx/`
171
- the home of tags like `<xxx-app>` (tagPath `xxx-app` → `xxx/app`). The generated
172
- environment follows the three-path chain the library's own demos use: `src/` holds the
172
+ the home of tags like `<xxx-app>` (tagPath `xxx-app` → `xxx/app`). The project owns its
173
+ namespace, so it can be any length: `isLongNamespace` flags the ones past
174
+ `LONG_NAMESPACE` (5) for the CLI to warn about and confirm before anything is written.
175
+ The generated environment follows the three-path chain the library's own demos use: `src/` holds the
173
176
  authored sources, `build/` receives the per-file `tsc` output that the dev server
174
177
  serves through `autoload()`, and `public/` receives the bundled production site. Because
175
178
  the dev shell is the autoload base, `import.meta.url` + `tagPath('xxx-app')` resolves to
@@ -182,18 +185,36 @@ function starterTag(namespace) {
182
185
  return `${namespace}-app`;
183
186
  }
184
187
  /**
185
- Whether a string is a valid project namespace: 1 to 4 lowercase letters.
188
+ Whether a string is a valid project namespace: lowercase letters and digits, starting
189
+ with a letter, any length. The project owns its namespace, so length is the user's
190
+ call — {@link isLongNamespace} flags the ones worth confirming before writing.
186
191
 
187
192
  @example
188
193
  ```ts
189
194
  console.log(isValidNamespace('mc'));
190
- console.log(isValidNamespace('mcxyz'));
191
- console.log(isValidNamespace('toolong'));
195
+ console.log(isValidNamespace('microcomponents'));
196
+ console.log(isValidNamespace('Shop'));
192
197
  ```
193
198
  @param ns - Candidate namespace.
194
199
  */
195
200
  function isValidNamespace(ns) {
196
- return /^[a-z]{1,4}$/.test(ns);
201
+ return /^[a-z][a-z0-9]*$/.test(ns);
202
+ }
203
+ /**
204
+ Whether a namespace is long enough to warn about. The namespace is the project's own
205
+ and the first segment of every component tag, so a long one lands in every tag, every
206
+ folder path, and every bundle import.
207
+
208
+ @example
209
+ ```ts
210
+ console.log(isLongNamespace('mc')); // false
211
+ console.log(isLongNamespace('shop')); // false
212
+ console.log(isLongNamespace('microcomponents')); // true
213
+ ```
214
+ @param ns - Candidate namespace.
215
+ */
216
+ function isLongNamespace(ns) {
217
+ return ns.length > 5;
197
218
  }
198
219
  /**
199
220
  Builds the files of a complete dev environment for a namespace.
@@ -204,17 +225,19 @@ const { namespace, files } = scaffoldProject('mc');
204
225
  console.log(namespace);
205
226
  console.log(Object.keys(files));
206
227
  ```
207
- @param namespace - 1 to 4 lowercase letters; becomes the `src/<namespace>/` folder and
208
- the first segment of every component tag.
209
- @throws If `namespace` is not 1 to 4 lowercase letters.
228
+ @param namespace - lowercase letters and digits, starting with a letter, any length;
229
+ becomes the `src/<namespace>/` folder and the first segment of every component tag.
230
+ @throws If `namespace` is not lowercase letters and digits starting with a letter.
210
231
  */
211
232
  function scaffoldProject(namespace) {
212
- if (!isValidNamespace(namespace)) throw new Error(`invalid namespace "${namespace}": must be 1 to 4 lowercase letters (e.g. "mc")`);
233
+ if (!isValidNamespace(namespace)) throw new Error(`invalid namespace "${namespace}": must be lowercase letters and digits, starting with a letter (e.g. "mc")`);
213
234
  const tag = starterTag(namespace);
214
235
  const base = tagPath(tag);
215
236
  const files = {};
216
237
  files["package.json"] = packageJson(namespace);
217
238
  files["tsconfig.json"] = tsconfig();
239
+ files["tsconfig.test.json"] = tsconfigTest();
240
+ files["vitest.config.ts"] = vitestConfig();
218
241
  files[".gitignore"] = gitignore();
219
242
  files["opencode.jsonc"] = opencodeConfig();
220
243
  files[".opencode/skills/components/SKILL.md"] = componentSkill(namespace, tag);
@@ -226,6 +249,7 @@ function scaffoldProject(namespace) {
226
249
  files[`src/${base}.target.css`] = appCss(tag);
227
250
  files[`src/${namespace}/bundle.ts`] = bundle();
228
251
  files["src/bundle.json"] = bundleConfig(namespace);
252
+ files[`test/${base}.spec.ts`] = appSpec(tag);
229
253
  return {
230
254
  files,
231
255
  namespace
@@ -240,10 +264,15 @@ function packageJson(namespace) {
240
264
  dev: "mc dev",
241
265
  build: "tsc -p tsconfig.json && mc bundle",
242
266
  preview: "mc preview",
243
- typecheck: "tsc --noEmit -p tsconfig.json"
267
+ typecheck: "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
268
+ test: "vitest run"
244
269
  },
245
- dependencies: { "@twobirds/microcomponents": "^0.11.0" },
246
- devDependencies: { typescript: "^6.0.3" }
270
+ dependencies: { "@twobirds/microcomponents": "^0.13.0" },
271
+ devDependencies: {
272
+ typescript: "^6.0.3",
273
+ vitest: "^4.1.10",
274
+ jsdom: "^29.1.1"
275
+ }
247
276
  }, null, " ");
248
277
  }
249
278
  function tsconfig() {
@@ -267,6 +296,60 @@ function tsconfig() {
267
296
  include: ["src/**/*.ts"]
268
297
  }, null, " ");
269
298
  }
299
+ function tsconfigTest() {
300
+ return JSON.stringify({
301
+ compilerOptions: {
302
+ target: "ESNext",
303
+ module: "ESNext",
304
+ moduleResolution: "Bundler",
305
+ lib: [
306
+ "ESNext",
307
+ "DOM",
308
+ "DOM.Iterable"
309
+ ],
310
+ strict: true,
311
+ esModuleInterop: true,
312
+ skipLibCheck: true,
313
+ noEmit: true
314
+ },
315
+ include: ["test/**/*.ts"]
316
+ }, null, " ");
317
+ }
318
+ function vitestConfig() {
319
+ return `import { defineConfig } from 'vitest/config';
320
+
321
+ export default defineConfig({
322
+ test: {
323
+ // components need a DOM: jsdom gives \`customElements\`, \`document\`, and the
324
+ // \`_mc\` contract the library writes, so a spec runs the real class code
325
+ environment: 'jsdom',
326
+ include: ['test/**/*.spec.ts'],
327
+ },
328
+ });
329
+ `;
330
+ }
331
+ function appSpec(tag) {
332
+ const className = kebabToPascal(tag);
333
+ return `import { describe, expect, it } from 'vitest';
334
+ import { DC, parse } from '@twobirds/microcomponents';
335
+ import { ${className} } from '../../src/${tagPath(tag)}.js';
336
+
337
+ describe('${tag}', () => {
338
+ it('registers the starter tag in the custom element registry', () => {
339
+ // importing the module runs defineCE, so the tag is in the registry, and the
340
+ // exported class is the DC subclass behind it
341
+ expect(customElements.get('${tag}')).toBeDefined();
342
+ expect(${className}.prototype instanceof DC).toBe(true);
343
+ });
344
+
345
+ it('renders the title from data through parse()', () => {
346
+ const html = parse('<h1>{title}</h1>', { title: 'Micro Components' });
347
+
348
+ expect(html).toContain('<h1>Micro Components</h1>');
349
+ });
350
+ });
351
+ `;
352
+ }
270
353
  function gitignore() {
271
354
  return `node_modules/
272
355
  build/
@@ -292,7 +375,7 @@ function opencodeConfig() {
292
375
  function componentSkill(namespace, tag) {
293
376
  return `---
294
377
  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/.
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/.
296
379
  ---
297
380
 
298
381
  # Micro Components project
@@ -304,11 +387,13 @@ Namespace \`${namespace}\`. Every tag starts with \`${namespace}-\`. Components
304
387
  \`src/\` → \`build/\` → \`public/\`
305
388
 
306
389
  - \`src/\` is the only folder to edit.
307
- - \`build/\` is the \`tsc\` output plus mirrored companion assets, served by the dev server. Generated.
390
+ - \`build/\` is the \`tsc\` output plus the files \`tsc\` does not compile — companion assets and plain \`.js\` components — served by the dev server. Generated.
308
391
  - \`public/\` is the production site: \`bundle.js\` with companion assets inlined, \`bundle.css\`, \`index.html\`. Generated.
309
392
 
310
393
  Never edit, rename, or delete anything under \`build/\` or \`public/\`. Change \`src/\` and rebuild.
311
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
+
312
397
  ## Add a component
313
398
 
314
399
  \`\`\`sh
@@ -317,6 +402,8 @@ npx mc new ${namespace}-name --target-html --target-css
317
402
 
318
403
  The tag must be kebab-case with at least one hyphen. \`ns-name\` becomes \`src/ns/name.ts\`: the first hyphen becomes the folder, which is the same rule the autoloader uses.
319
404
 
405
+ \`--js\` writes \`src/ns/name.js\` instead of \`.ts\`. \`tsc\` compiles only \`.ts\`, so the dev server and \`mc bundle\` copy a plain \`.js\` component to \`build/\` verbatim — it autoloads and bundles like a compiled component, with no type checking and no \`.d.ts\`.
406
+
320
407
  Register the component so the production bundle picks it up:
321
408
 
322
409
  \`\`\`ts
@@ -343,27 +430,38 @@ A camelCase descriptor maps to a dotted filename and is listed in \`static asset
343
430
 
344
431
  The loader fetches exactly the listed descriptors, so every entry in \`static assets\` needs its file.
345
432
 
433
+ ## State
434
+
435
+ Inside a component, use the plugin methods. \`observePlugin(DC)\` adds \`this.notify()\`, \`this.subscribe()\`, \`this.bind()\`; \`formValuePlugin(DC)\` adds \`this.formValues()\` and pulls in the observe methods. Call the plugin once at module level, then chain on \`this\` with the property *name*: \`this.subscribe('data', …)\`.
436
+
437
+ The pure \`observe()\` and \`formValues()\` functions are for code outside the MC system — vanilla JS, framework lifecycle code, Node. Never call them inside a \`DC\`: the plugin methods create the same wrapper, chain, and keep the \`keyof this & string\` typing contract.
438
+
346
439
  ## Events
347
440
 
348
441
  Prefer a direct target with the default local dispatch and a typed payload:
349
442
 
350
443
  \`\`\`ts
351
444
  target.dispatchEvent(
352
- new CustomEvent('${namespace}:action', { detail: { data: { id: 'x' } } })
445
+ new CustomEvent('addToCart', { detail: { data: { id: 'x' } } })
353
446
  );
354
447
  \`\`\`
355
448
 
356
449
  Bubble up or down the tree only when the receiver cannot be named.
357
450
 
451
+ Event names are plain inside the project — \`add\`, \`toggle\`, \`addToCart\`. Namespace a name (\`${namespace}:addToCart\`) only when a collision is likely: an event entering the project from outside (a framework, a page script, a third-party addon DC), or several DCs attached to one component dispatching the same name. One producer per name means no collision, so no namespace.
452
+
358
453
  ## Verify
359
454
 
360
455
  \`\`\`sh
361
456
  pnpm typecheck
457
+ pnpm test
362
458
  pnpm dev
363
459
  pnpm build
364
460
  pnpm preview
365
461
  \`\`\`
366
462
 
463
+ Specs live in \`test/\` and run under vitest in jsdom, so component class code executes against a real DOM. Import the component from \`src/\` with a \`.js\` specifier; vitest resolves it to the \`.ts\` source.
464
+
367
465
  \`mc clean\` removes \`build/\`, \`public/\` and \`coverage/\`.
368
466
 
369
467
  ## API reference
@@ -391,7 +489,8 @@ pnpm dev
391
489
  The dev server runs \`tsc -w\` and serves \`build/\`, so a saved component reaches the
392
490
  browser through \`autoload()\` with no bundling. Companion \`.css\`, \`.html\` and \`.json\`
393
491
  files are mirrored into \`build/\` next to the compiled component, which is where the
394
- autoloader looks for them.
492
+ autoloader looks for them. The server binds loopback, so only this machine reaches it;
493
+ \`pnpm dev -- --host 0.0.0.0\` widens it to the LAN.
395
494
 
396
495
  ## Build
397
496
 
@@ -410,13 +509,26 @@ The bundle is driven by \`src/bundle.json\`: \`entry\` names the barrel to bundl
410
509
  pnpm preview
411
510
  \`\`\`
412
511
 
512
+ ## Test
513
+
514
+ \`\`\`sh
515
+ pnpm test
516
+ \`\`\`
517
+
518
+ Specs live in \`test/\` and run under vitest in jsdom, so component class code executes
519
+ against a real DOM: \`customElements\`, \`document\`, and the \`_mc\` property the library
520
+ writes. \`test/${namespace}/app.spec.ts\` is the starter spec — it asserts the tag is
521
+ registered and that \`parse()\` renders the title from \`data\`.
522
+
413
523
  ## Type-check
414
524
 
415
525
  \`\`\`sh
416
526
  pnpm typecheck
417
527
  \`\`\`
418
528
 
419
- \`tsconfig.json\` compiles \`src/\` into \`build/\`.
529
+ \`tsconfig.json\` compiles \`src/\` into \`build/\`; \`tsconfig.test.json\` type-checks
530
+ \`test/\` without emitting, so a spec that imports a component is checked against the
531
+ component's real types.
420
532
 
421
533
  ## Components
422
534
 
@@ -458,11 +570,8 @@ function devShell(namespace, tag) {
458
570
  <meta charset="UTF-8" />
459
571
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
460
572
  <title>${namespace} App</title>
461
- <script type="importmap">
462
- { "imports": { "@twobirds/microcomponents": "/microcomponents.min.js" } }
463
- <\/script>
464
573
  <script type="module">
465
- import * as m from '@twobirds/microcomponents';
574
+ import * as m from '/microcomponents.min.js';
466
575
  m.autoload(true);
467
576
  Object.assign(window, m);
468
577
  <\/script>
@@ -499,7 +608,7 @@ function appSource(namespace, tag) {
499
608
  <${tag}></${tag}>
500
609
  \`\`\`
501
610
  */
502
- class ${className} extends DC {
611
+ export class ${className} extends DC {
503
612
  static assets = ['html', 'targetCss'];
504
613
 
505
614
  data = { title: 'Micro Components' };
@@ -621,12 +730,12 @@ namespace under a project root, creating nested folders as needed.
621
730
  const written = await writeProject('mc', 'mc-app');
622
731
  console.log(written);
623
732
  ```
624
- @param namespace - 1 to 4 lowercase letters.
733
+ @param namespace - lowercase letters and digits, starting with a letter, any length.
625
734
  @param root - Directory to create the project in (e.g. `mc-app`).
626
735
  @param options - Whether to overwrite existing files.
627
736
  @returns The written file paths, relative to `root`.
628
- @throws If `namespace` is not 1 to 4 lowercase letters, or a target file exists and
629
- `force` is false.
737
+ @throws If `namespace` is not lowercase letters and digits starting with a letter, or
738
+ a target file exists and `force` is false.
630
739
  */
631
740
  async function writeProject(namespace, root, options = {}) {
632
741
  const { files } = scaffoldProject(namespace);
@@ -1071,8 +1180,9 @@ the `tsc`-emitted `build/` tree, with no Vite.
1071
1180
 
1072
1181
  The chain is `src/` → `build/` → `public/`. The scaffolded `build` script runs
1073
1182
  `tsc -p tsconfig.json` (so `build/` holds one `.js` per component at its
1074
- autoload path), then `mc bundle` mirrors the companion assets into `build/`,
1075
- walks the barrel entry named by `src/bundle.json` through {@link bundleEntry}
1183
+ autoload path), then `mc bundle` mirrors the files `tsc` does not compile —
1184
+ companion assets and plain `.js` components — into `build/`, walks the barrel
1185
+ entry named by `src/bundle.json` through {@link bundleEntry}
1076
1186
  (inlining every `static assets`), rewrites the `@twobirds/microcomponents`
1077
1187
  specifier to the self-contained `microcomponents.min.js` shipped next to the
1078
1188
  bundle, concatenates the plain `.css` token presets into `bundle.css`, and
@@ -1081,45 +1191,52 @@ copies the production shell. The result is a complete static site: a single
1081
1191
 
1082
1192
  @module CLI/Bundle
1083
1193
  */
1084
- /** Companion asset extensions copied verbatim next to their compiled `.js`. */
1085
- var COMPANION_EXTENSIONS = [
1194
+ /** Files `tsc` does not compile, copied verbatim next to the compiled components. */
1195
+ var MIRROR_EXTENSIONS = [
1086
1196
  ".css",
1087
1197
  ".html",
1088
- ".json"
1198
+ ".json",
1199
+ ".js"
1089
1200
  ];
1090
1201
  /**
1091
- * Whether a file is a companion asset to mirror into `build/`.
1202
+ * Whether a file in `src/` is copied verbatim into `build/`. Companion assets
1203
+ * (`.css`, `.html`, `.json`) sit next to their compiled `.js`, and a plain `.js`
1204
+ * component (`mc new --js`) is copied as it is: `tsc` compiles only `.ts`, so plain
1205
+ * JS reaches the autoloader by copying, not by compiling. The page shells are not
1206
+ * mirrored by this rule — `mc dev` copies the dev shell itself.
1092
1207
  *
1093
1208
  * @example
1094
1209
  * ```ts
1095
- * isCompanion('app.target.html'); // true
1096
- * isCompanion('index.html'); // false
1210
+ * isMirrored('app.target.html'); // true
1211
+ * isMirrored('app.js'); // true — a plain JS component
1212
+ * isMirrored('index.html'); // false
1097
1213
  * ```
1098
1214
  * @param name Basename of the file.
1099
1215
  */
1100
- function isCompanion(name) {
1216
+ function isMirrored(name) {
1101
1217
  if (name === "index.html" || name === "index-dev.html") return false;
1102
- return COMPANION_EXTENSIONS.some((ext) => name.endsWith(ext));
1218
+ return MIRROR_EXTENSIONS.some((ext) => name.endsWith(ext));
1103
1219
  }
1104
1220
  /**
1105
- * Mirror companion assets (`.css`, `.html`, `.json`) from `src/` into `build/`,
1106
- * preserving the tree, so the bundler reads each asset next to its compiled `.js`.
1221
+ * Mirror the files `tsc` does not compile (`.css`, `.html`, `.json`, `.js`) from
1222
+ * `src/` into `build/`, preserving the tree, so the bundler reads each asset and
1223
+ * each plain JS component next to its compiled `.js`.
1107
1224
  *
1108
1225
  * @example
1109
1226
  * ```ts
1110
- * await mirrorCompanions('<root>/src', '<root>/build');
1227
+ * await mirrorSources('<root>/src', '<root>/build');
1111
1228
  * ```
1112
1229
  * @param srcDir Source root to walk.
1113
1230
  * @param buildDir Folder to write into.
1114
1231
  * @param rel Path of the walked directory relative to `srcDir`.
1115
1232
  */
1116
- async function mirrorCompanions(srcDir, buildDir, rel = "") {
1233
+ async function mirrorSources(srcDir, buildDir, rel = "") {
1117
1234
  for (const entry of await readdir(path.join(srcDir, rel), { withFileTypes: true })) {
1118
1235
  const next = rel ? path.join(rel, entry.name) : entry.name;
1119
1236
  if (entry.isDirectory()) {
1120
1237
  if (entry.name === "node_modules") continue;
1121
- await mirrorCompanions(srcDir, buildDir, next);
1122
- } else if (isCompanion(entry.name)) {
1238
+ await mirrorSources(srcDir, buildDir, next);
1239
+ } else if (isMirrored(entry.name)) {
1123
1240
  const dst = path.join(buildDir, next);
1124
1241
  await mkdir(path.dirname(dst), { recursive: true });
1125
1242
  await cp(path.join(srcDir, next), dst);
@@ -1150,17 +1267,20 @@ async function collectPlainCss(srcDir, rel = "", found = []) {
1150
1267
  /**
1151
1268
  * Rewrite the library specifier in a bundled module to the self-contained bundle
1152
1269
  * shipped next to it, so the browser can resolve `@twobirds/microcomponents`
1153
- * without a bare-specifier import map.
1270
+ * without a bare-specifier import map. Subpath imports resolve to the same module,
1271
+ * so the library stays one instance. This is the single source of truth for the
1272
+ * rule: the bundler uses the relative target, the dev server passes its served URL.
1154
1273
  *
1155
- * @example Turns the package specifier into a relative path.
1274
+ * @example Turns the package specifier into a relative path, leaves others alone.
1156
1275
  * ```ts
1157
1276
  * rewriteLibrarySpecifier("import { DC } from '@twobirds/microcomponents';");
1277
+ * rewriteLibrarySpecifier("import { z } from 'zod';"); // unchanged
1158
1278
  * ```
1159
1279
  * @param code Bundled module source.
1160
- * @param target Relative specifier to point the import at.
1280
+ * @param target Specifier to point the import at.
1161
1281
  */
1162
1282
  function rewriteLibrarySpecifier(code, target = "./microcomponents.min.js") {
1163
- return code.replace(/from\s*['"]@twobirds\/microcomponents['"]/g, `from '${target}'`);
1283
+ return code.replace(/(['"])@twobirds\/microcomponents(?:\/[^'"]*)?\1/g, `$1${target}$1`);
1164
1284
  }
1165
1285
  /**
1166
1286
  * Assemble the production site in `public/` from the compiled `build/` tree.
@@ -1180,7 +1300,7 @@ async function bundleProject(projectRoot, options) {
1180
1300
  const config = JSON.parse(await readFile(path.join(sourceRoot, "bundle.json"), "utf8"));
1181
1301
  const publicRoot = path.join(projectRoot, options.outDir ?? config.outDir ?? "public");
1182
1302
  const fileName = config.fileName ?? "bundle.js";
1183
- await mirrorCompanions(sourceRoot, buildRoot);
1303
+ await mirrorSources(sourceRoot, buildRoot);
1184
1304
  const rewritten = rewriteLibrarySpecifier(await bundleEntry((options.bundleEntry ?? config.entry).replace(/\.ts$/, ".js"), buildRoot, true));
1185
1305
  await mkdir(publicRoot, { recursive: true });
1186
1306
  await writeFile(path.join(publicRoot, fileName), rewritten, "utf8");
@@ -1216,12 +1336,14 @@ Vite-free dev server for `mc dev`.
1216
1336
 
1217
1337
  A minimal `node:http` static server rooted at the project's `build/` folder, so
1218
1338
  a saved component reaches the browser through `autoload()` with no bundling. It
1219
- mirrors companion assets from `src/` into `build/` (on startup and on every
1220
- change), runs `tsc -w` for the lifetime of the server, serves companion
1339
+ mirrors the files `tsc` does not compile — companion assets and plain `.js`
1340
+ components — from `src/` into `build/` (on startup and on every change), runs
1341
+ `tsc -w` for the lifetime of the server, serves companion
1221
1342
  `.css` / `.html` / `.json` as raw text (so `loadAssets()` receives markup it
1222
- can use), redirects `/` to the autoload shell (`index-dev.html`), and serves
1223
- the self-contained library at `/microcomponents.min.js` — the target of the
1224
- import map in the dev shell.
1343
+ can use), redirects `/` to the autoload shell (`index-dev.html`), serves the
1344
+ self-contained library at `/microcomponents.min.js`, and rewrites the
1345
+ `@twobirds/microcomponents` specifier in served JS to that URL, so a compiled
1346
+ component resolves the library with no import map in the page.
1225
1347
 
1226
1348
  There is no HMR: a page reload picks up the recompiled component.
1227
1349
 
@@ -1283,8 +1405,9 @@ async function serve(req, res, buildRoot, minJs) {
1283
1405
  }
1284
1406
  try {
1285
1407
  const text = await readFile(abs, "utf-8");
1408
+ const body = abs.endsWith(".js") ? rewriteLibrarySpecifier(text, "/microcomponents.min.js") : text;
1286
1409
  res.setHeader("Content-Type", mimeFor(path.basename(abs)));
1287
- res.end(text);
1410
+ res.end(body);
1288
1411
  } catch {
1289
1412
  /* v8 ignore start — resolveTarget already checked existence; a read failure is a race */
1290
1413
  res.statusCode = 404;
@@ -1292,8 +1415,9 @@ async function serve(req, res, buildRoot, minJs) {
1292
1415
  }
1293
1416
  }
1294
1417
  /**
1295
- * Mirror companion assets from `src/` into `build/`, preserving the tree, so the
1296
- * served folder stays autoload-ready.
1418
+ * Mirror the files `tsc` does not compile — companion assets and plain `.js`
1419
+ * components — from `src/` into `build/`, preserving the tree, so the served
1420
+ * folder stays autoload-ready.
1297
1421
  *
1298
1422
  * @example
1299
1423
  * ```ts
@@ -1308,7 +1432,7 @@ async function mirrorInto(sourceRoot, buildRoot, rel = "") {
1308
1432
  const next = rel ? path.join(rel, entry.name) : entry.name;
1309
1433
  if (entry.isDirectory()) {
1310
1434
  if (entry.name !== "node_modules") await mirrorInto(sourceRoot, buildRoot, next);
1311
- } else if (isCompanion(entry.name) || entry.name === "index-dev.html") {
1435
+ } else if (isMirrored(entry.name) || entry.name === "index-dev.html") {
1312
1436
  const dst = path.join(buildRoot, next);
1313
1437
  await mkdir(path.dirname(dst), { recursive: true });
1314
1438
  await cp(path.join(sourceRoot, next), dst);
@@ -1341,10 +1465,12 @@ function createDevServer(projectRoot, options) {
1341
1465
  * @param projectRoot Project root (defaults to the current directory).
1342
1466
  * @param minJs Path of `microcomponents.min.js` (defaults to this package's `dist/`).
1343
1467
  * @param port Dev-server port.
1468
+ * @param host Bind address. Defaults to loopback, so a dev server serving source files is
1469
+ * not reachable off the machine. Pass an interface address to serve the LAN.
1344
1470
  * @returns The process exit code (the server keeps running until killed).
1345
1471
  */
1346
1472
  /* v8 ignore start — the long-running server + tsc watch is exercised by hand, not in the suite */
1347
- async function runDev(projectRoot = process.cwd(), minJs = path.join(import.meta.dirname, "..", "microcomponents.min.js"), port = 5173) {
1473
+ async function runDev(projectRoot = process.cwd(), minJs = path.join(import.meta.dirname, "..", "microcomponents.min.js"), port = 5173, host = "127.0.0.1") {
1348
1474
  const sourceRoot = path.join(projectRoot, "src");
1349
1475
  const buildRoot = path.join(projectRoot, "build");
1350
1476
  await mirrorInto(sourceRoot, buildRoot);
@@ -1360,7 +1486,7 @@ async function runDev(projectRoot = process.cwd(), minJs = path.join(import.meta
1360
1486
  watch(sourceRoot, { recursive: true }, (_event, file) => {
1361
1487
  if (!file || file.endsWith(".ts")) return;
1362
1488
  const abs = path.join(sourceRoot, file);
1363
- if (isCompanion(path.basename(file)) || file.endsWith("index-dev.html")) {
1489
+ if (isMirrored(path.basename(file)) || file.endsWith("index-dev.html")) {
1364
1490
  const dst = path.join(buildRoot, file);
1365
1491
  mkdir(path.dirname(dst), { recursive: true }).then(() => cp(abs, dst));
1366
1492
  }
@@ -1369,8 +1495,8 @@ async function runDev(projectRoot = process.cwd(), minJs = path.join(import.meta
1369
1495
  minJs,
1370
1496
  port
1371
1497
  });
1372
- server.listen(port);
1373
- console.log(`dev server on http://localhost:${port}/ (serving build/, tsc -w running)`);
1498
+ server.listen(port, host);
1499
+ console.log(`dev server on http://${host === "0.0.0.0" ? "localhost" : host}:${port}/ (serving build/, tsc -w running)`);
1374
1500
  return new Promise((resolve) => {
1375
1501
  tsc.on("exit", () => {
1376
1502
  server.close();
@@ -1454,13 +1580,15 @@ function createPreviewServer(projectRoot, port, outDir = "public") {
1454
1580
  *
1455
1581
  * @param projectRoot Project root (defaults to the current directory).
1456
1582
  * @param port Preview-server port.
1583
+ * @param host Bind address. Defaults to loopback, so a preview server is not reachable off
1584
+ * the machine. Pass an interface address to serve the LAN.
1457
1585
  * @returns The process exit code (the server keeps running until killed).
1458
1586
  */
1459
1587
  /* v8 ignore start — the long-running server is exercised by hand, not in the suite */
1460
- async function runPreview(projectRoot = process.cwd(), port = 5174) {
1588
+ async function runPreview(projectRoot = process.cwd(), port = 5174, host = "127.0.0.1") {
1461
1589
  const server = createPreviewServer(projectRoot, port);
1462
- server.listen(port);
1463
- console.log(`preview server on http://localhost:${port}/ (serving public/)`);
1590
+ server.listen(port, host);
1591
+ console.log(`preview server on http://${host === "0.0.0.0" ? "localhost" : host}:${port}/ (serving public/)`);
1464
1592
  return new Promise((resolve) => {
1465
1593
  server.on("close", () => resolve(0));
1466
1594
  });
@@ -1485,10 +1613,10 @@ var USAGE = `mc — Micro Components build tools
1485
1613
 
1486
1614
  Usage:
1487
1615
  mc new <tag> [--css] [--html] [--target-html] [--shadow-html] [--target-css] [--shadow-css] [--js] [--dir <path>] [--force] [--dry-run]
1488
- mc create --namespace <ns> [--dir <path>] [--force] [--dry-run]
1489
- mc dev [--port <n>]
1616
+ mc create --namespace <ns> [--dir <path>] [--force] [--yes] [--dry-run]
1617
+ mc dev [--port <n>] [--host <addr>]
1490
1618
  mc bundle
1491
- mc preview [--port <n>]
1619
+ mc preview [--port <n>] [--host <addr>]
1492
1620
  mc clean
1493
1621
  mc check
1494
1622
  mc --version
@@ -1515,7 +1643,10 @@ Other options:
1515
1643
  --js emit plain JavaScript instead of TypeScript
1516
1644
  --dir <path> scaffold root (default: src)
1517
1645
  --port <n> dev / preview server port (default: 5173 / 5174)
1646
+ --host <addr> dev / preview bind address (default: 127.0.0.1 — loopback only; pass
1647
+ 0.0.0.0 or a interface address to serve the LAN)
1518
1648
  --force overwrite files that already exist
1649
+ --yes skip the confirmation when a namespace is longer than 5 characters (-y)
1519
1650
  --dry-run print what would be written; touch nothing on disk
1520
1651
  --version print the version of the package the CLI ships in (-v)
1521
1652
  --help print this text (-h)
@@ -1533,6 +1664,25 @@ var ASSET_FLAGS = {
1533
1664
  "shadow-css": "shadowCss"
1534
1665
  };
1535
1666
  /**
1667
+ Asks on the terminal whether to continue with a long namespace. `y`/`yes`
1668
+ (case-insensitive) continue; anything else, including an unreadable stdin, aborts.
1669
+
1670
+ @example
1671
+ ```ts
1672
+ const ok = await confirmNamespace('microcomponents');
1673
+ ```
1674
+ @param namespace - The namespace to confirm.
1675
+ */
1676
+ async function confirmNamespace(namespace) {
1677
+ const rl = createInterface({
1678
+ input: process.stdin,
1679
+ output: process.stdout
1680
+ });
1681
+ const answer = await rl.question(`continue with "${namespace}"? [y/N] `);
1682
+ rl.close();
1683
+ return /^y(es)?$/i.test(answer.trim());
1684
+ }
1685
+ /**
1536
1686
  A scaffolded file as it appears from the current directory: the file `rel` under
1537
1687
  `root`, resolved and made readable. `mc create` writes a project into a folder of
1538
1688
  its own, so `src/mc/app.ts` under `mc-app` prints as `mc-app/src/mc/app.ts` — the
@@ -1670,16 +1820,17 @@ async function runScript(script, options) {
1670
1820
  if (await hasScript(script)) return spawnProcess("pnpm", ["run", script]);
1671
1821
  if (script === "clean") return cleanSelf();
1672
1822
  if (script === "check") return checkSelf();
1673
- if (script === "dev") return runDev(process.cwd(), void 0, options?.port ?? 5173);
1823
+ if (script === "dev") return runDev(process.cwd(), void 0, options?.port ?? 5173, options?.host ?? "127.0.0.1");
1674
1824
  if (script === "bundle") return runBundle();
1675
- if (script === "preview") return runPreview(process.cwd(), options?.port ?? 5174);
1825
+ if (script === "preview") return runPreview(process.cwd(), options?.port ?? 5174, options?.host ?? "127.0.0.1");
1676
1826
  console.error(`mc: no "${script}" script in the current project`);
1677
1827
  return 1;
1678
1828
  }
1679
1829
  /**
1680
1830
  Parses `argv` and routes it to the matching command, returning a process exit
1681
1831
  code. `run` is injectable so the script-wrapping commands can be tested without
1682
- spawning anything.
1832
+ spawning anything, and `confirm` is injectable so the long-namespace prompt can be
1833
+ tested without a terminal.
1683
1834
 
1684
1835
  @example
1685
1836
  ```ts
@@ -1692,8 +1843,11 @@ return 0;
1692
1843
  ```ts
1693
1844
  const code = await main(['--version']);
1694
1845
  ```
1846
+ @param argv - Arguments to route (usually `process.argv.slice(2)`).
1847
+ @param run - Runs a package.json script (defaults to {@link runScript}).
1848
+ @param confirm - Confirms a long namespace (defaults to the terminal prompt).
1695
1849
  */
1696
- async function main(argv, run = runScript) {
1850
+ async function main(argv, run = runScript, confirm = confirmNamespace) {
1697
1851
  const { values, positionals } = parseArgs({
1698
1852
  args: argv,
1699
1853
  allowPositionals: true,
@@ -1708,7 +1862,12 @@ async function main(argv, run = runScript) {
1708
1862
  dir: { type: "string" },
1709
1863
  namespace: { type: "string" },
1710
1864
  port: { type: "string" },
1865
+ host: { type: "string" },
1711
1866
  force: { type: "boolean" },
1867
+ yes: {
1868
+ type: "boolean",
1869
+ short: "y"
1870
+ },
1712
1871
  "dry-run": { type: "boolean" },
1713
1872
  help: {
1714
1873
  type: "boolean",
@@ -1768,6 +1927,13 @@ async function main(argv, run = runScript) {
1768
1927
  console.error("mc create: missing --namespace (e.g. mc create --namespace mc)");
1769
1928
  return 1;
1770
1929
  }
1930
+ if (isLongNamespace(namespace)) {
1931
+ console.warn(`mc create: namespace "${namespace}" is ${namespace.length} characters — every component tag in the project starts with it.`);
1932
+ if (!values["dry-run"] && !values.yes && !await confirm(namespace)) {
1933
+ console.error("mc create: aborted");
1934
+ return 1;
1935
+ }
1936
+ }
1771
1937
  const root = values.dir ?? namespace;
1772
1938
  let files;
1773
1939
  try {
@@ -1791,9 +1957,15 @@ async function main(argv, run = runScript) {
1791
1957
  console.log(`\nproject "${namespace}" created in ${root}/ — run: cd ${root} && pnpm install && pnpm dev`);
1792
1958
  return 0;
1793
1959
  }
1794
- if (command === "dev") return run("dev", { port: values.port ? Number(values.port) : 5173 });
1960
+ if (command === "dev") return run("dev", {
1961
+ port: values.port ? Number(values.port) : 5173,
1962
+ host: values.host
1963
+ });
1795
1964
  if (command === "bundle") return run("bundle");
1796
- if (command === "preview") return run("preview", { port: values.port ? Number(values.port) : 5174 });
1965
+ if (command === "preview") return run("preview", {
1966
+ port: values.port ? Number(values.port) : 5174,
1967
+ host: values.host
1968
+ });
1797
1969
  if (command === "clean") return run("clean");
1798
1970
  if (command === "check") return run("check");
1799
1971
  console.error(`mc: unknown command "${command}"\n\n${USAGE}`);