@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.
- package/README.md +14 -4
- package/dist/cli/bundle.d.ts +23 -13
- package/dist/cli/bundle.d.ts.map +1 -1
- package/dist/cli/create.d.ts +28 -8
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/dev.d.ts +13 -8
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dispatch.d.ts +10 -2
- package/dist/cli/dispatch.d.ts.map +1 -1
- package/dist/cli/index.js +244 -72
- package/dist/cli/preview.d.ts +3 -1
- package/dist/cli/preview.d.ts.map +1 -1
- package/dist/cli/writeScaffold.d.ts +3 -3
- package/dist/docs/api-external.d.ts +11 -2
- package/dist/docs/api-external.d.ts.map +1 -1
- package/dist/docs/cli.d.ts +1 -1
- package/dist/docs/installation.d.ts +8 -3
- package/dist/docs/installation.d.ts.map +1 -1
- package/dist/helpers.min.js +1 -0
- package/dist/mcp/api.json +28 -2
- package/dist/mcp/concepts/event-system.md +20 -0
- package/dist/mcp/concepts/mc-types.md +15 -8
- package/dist/mcp/concepts/no-importmap.md +48 -0
- package/dist/mcp/concepts/project-layout.md +9 -5
- package/dist/mcp/concepts/state-management.md +13 -7
- package/dist/microcomponents.d.ts +1 -0
- package/dist/microcomponents.d.ts.map +1 -1
- package/dist/microcomponents.js +1 -1
- package/dist/microcomponents.min.js +1 -1
- package/dist/placeholders.min.js +1 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/placeholders.js +1980 -0
- package/dist/utils/autoload.js +1451 -0
- package/dist/utils/events.d.ts +1 -1
- package/dist/utils/events.d.ts.map +1 -1
- package/dist/utils/helpers.js +693 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- 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
|
|
172
|
-
|
|
172
|
+
the home of tags like `<xxx-app>` (tagPath `xxx-app` → `xxx/app`). The project owns its
|
|
173
|
+
namespace, so it can be any length: `isLongNamespace` flags the ones past
|
|
174
|
+
`LONG_NAMESPACE` (5) for the CLI to warn about and confirm before anything is written.
|
|
175
|
+
The generated environment follows the three-path chain the library's own demos use: `src/` holds the
|
|
173
176
|
authored sources, `build/` receives the per-file `tsc` output that the dev server
|
|
174
177
|
serves through `autoload()`, and `public/` receives the bundled production site. Because
|
|
175
178
|
the dev shell is the autoload base, `import.meta.url` + `tagPath('xxx-app')` resolves to
|
|
@@ -182,18 +185,36 @@ function starterTag(namespace) {
|
|
|
182
185
|
return `${namespace}-app`;
|
|
183
186
|
}
|
|
184
187
|
/**
|
|
185
|
-
Whether a string is a valid project namespace:
|
|
188
|
+
Whether a string is a valid project namespace: lowercase letters and digits, starting
|
|
189
|
+
with a letter, any length. The project owns its namespace, so length is the user's
|
|
190
|
+
call — {@link isLongNamespace} flags the ones worth confirming before writing.
|
|
186
191
|
|
|
187
192
|
@example
|
|
188
193
|
```ts
|
|
189
194
|
console.log(isValidNamespace('mc'));
|
|
190
|
-
console.log(isValidNamespace('
|
|
191
|
-
console.log(isValidNamespace('
|
|
195
|
+
console.log(isValidNamespace('microcomponents'));
|
|
196
|
+
console.log(isValidNamespace('Shop'));
|
|
192
197
|
```
|
|
193
198
|
@param ns - Candidate namespace.
|
|
194
199
|
*/
|
|
195
200
|
function isValidNamespace(ns) {
|
|
196
|
-
return /^[a-z]
|
|
201
|
+
return /^[a-z][a-z0-9]*$/.test(ns);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
Whether a namespace is long enough to warn about. The namespace is the project's own
|
|
205
|
+
and the first segment of every component tag, so a long one lands in every tag, every
|
|
206
|
+
folder path, and every bundle import.
|
|
207
|
+
|
|
208
|
+
@example
|
|
209
|
+
```ts
|
|
210
|
+
console.log(isLongNamespace('mc')); // false
|
|
211
|
+
console.log(isLongNamespace('shop')); // false
|
|
212
|
+
console.log(isLongNamespace('microcomponents')); // true
|
|
213
|
+
```
|
|
214
|
+
@param ns - Candidate namespace.
|
|
215
|
+
*/
|
|
216
|
+
function isLongNamespace(ns) {
|
|
217
|
+
return ns.length > 5;
|
|
197
218
|
}
|
|
198
219
|
/**
|
|
199
220
|
Builds the files of a complete dev environment for a namespace.
|
|
@@ -204,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 -
|
|
208
|
-
the first segment of every component tag.
|
|
209
|
-
@throws If `namespace` is not
|
|
228
|
+
@param namespace - lowercase letters and digits, starting with a letter, any length;
|
|
229
|
+
becomes the `src/<namespace>/` folder and the first segment of every component tag.
|
|
230
|
+
@throws If `namespace` is not lowercase letters and digits starting with a letter.
|
|
210
231
|
*/
|
|
211
232
|
function scaffoldProject(namespace) {
|
|
212
|
-
if (!isValidNamespace(namespace)) throw new Error(`invalid namespace "${namespace}": must be
|
|
233
|
+
if (!isValidNamespace(namespace)) throw new Error(`invalid namespace "${namespace}": must be lowercase letters and digits, starting with a letter (e.g. "mc")`);
|
|
213
234
|
const tag = starterTag(namespace);
|
|
214
235
|
const base = tagPath(tag);
|
|
215
236
|
const files = {};
|
|
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.
|
|
246
|
-
devDependencies: {
|
|
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
|
|
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('
|
|
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 '
|
|
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 -
|
|
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
|
|
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
|
|
1075
|
-
|
|
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
|
-
/**
|
|
1085
|
-
var
|
|
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
|
|
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
|
-
*
|
|
1096
|
-
*
|
|
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
|
|
1216
|
+
function isMirrored(name) {
|
|
1101
1217
|
if (name === "index.html" || name === "index-dev.html") return false;
|
|
1102
|
-
return
|
|
1218
|
+
return MIRROR_EXTENSIONS.some((ext) => name.endsWith(ext));
|
|
1103
1219
|
}
|
|
1104
1220
|
/**
|
|
1105
|
-
* Mirror
|
|
1106
|
-
* preserving the tree, so the bundler reads each asset
|
|
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
|
|
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
|
|
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
|
|
1122
|
-
} else if (
|
|
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
|
|
1280
|
+
* @param target Specifier to point the import at.
|
|
1161
1281
|
*/
|
|
1162
1282
|
function rewriteLibrarySpecifier(code, target = "./microcomponents.min.js") {
|
|
1163
|
-
return code.replace(/
|
|
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
|
|
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
|
|
1220
|
-
|
|
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`),
|
|
1223
|
-
|
|
1224
|
-
|
|
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(
|
|
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
|
|
1296
|
-
*
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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", {
|
|
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", {
|
|
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}`);
|