@twobirds/microcomponents 0.8.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 (44) 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/dev.d.ts +87 -0
  7. package/dist/cli/dev.d.ts.map +1 -0
  8. package/dist/cli/dispatch.d.ts +6 -2
  9. package/dist/cli/dispatch.d.ts.map +1 -1
  10. package/dist/cli/index.js +1807 -1
  11. package/dist/cli/inliner.d.ts +42 -0
  12. package/dist/cli/inliner.d.ts.map +1 -0
  13. package/dist/cli/preview.d.ts +48 -0
  14. package/dist/cli/preview.d.ts.map +1 -0
  15. package/dist/core/elements.js +1294 -75
  16. package/dist/core/mcGuards.js +5 -8
  17. package/dist/mcp/api.json +2 -17
  18. package/dist/mcp/concepts/project-layout.md +9 -8
  19. package/dist/mcp/index.js +450 -230
  20. package/dist/microcomponents.js +2882 -9
  21. package/dist/state/formValues.js +2236 -281
  22. package/dist/state/observables.js +1794 -342
  23. package/dist/utils/events.js +171 -197
  24. package/package.json +3 -7
  25. package/dist/cli/create.js +0 -568
  26. package/dist/cli/dispatch.js +0 -316
  27. package/dist/cli/scaffold.js +0 -109
  28. package/dist/cli/writeScaffold.js +0 -114
  29. package/dist/core/MC.js +0 -837
  30. package/dist/core/index.js +0 -72
  31. package/dist/state/index.js +0 -5
  32. package/dist/state/placeholders.js +0 -187
  33. package/dist/types/index.js +0 -1
  34. package/dist/types/mcTypes.js +0 -9
  35. package/dist/utils/autoload.js +0 -160
  36. package/dist/utils/helpers.js +0 -532
  37. package/dist/utils/index.js +0 -12
  38. package/dist/utils/resolveComponentPath.js +0 -39
  39. package/dist/vite/index.d.ts +0 -7
  40. package/dist/vite/index.d.ts.map +0 -1
  41. package/dist/vite/index.js +0 -6
  42. package/dist/vite/loadAssets.d.ts +0 -36
  43. package/dist/vite/loadAssets.d.ts.map +0 -1
  44. package/dist/vite/loadAssets.js +0 -221
@@ -1,568 +0,0 @@
1
- /**
2
- Project scaffolding for `mc create --namespace xxx`.
3
-
4
- Pure, filesystem-free logic: given a namespace it returns the files of a complete,
5
- working Micro Components dev environment and their contents. The starter component's
6
- path is not re-derived here — it comes from {@link tagPath}, the same primitive the
7
- runtime autoload loader uses, so the scaffold and the runtime can never disagree about
8
- where a component lives.
9
-
10
- The namespace is the first segment of every component tag, so `xxx` makes `src/xxx/`
11
- the home of tags like `<xxx-app>` (tagPath `xxx-app` → `xxx/app`). The generated
12
- environment follows the three-path chain the library's own demos use: `src/` holds the
13
- authored sources, `build/` receives the per-file `tsc` output that the dev server
14
- serves through `autoload()`, and `public/` receives the bundled production site. Because
15
- the dev shell is the autoload base, `import.meta.url` + `tagPath('xxx-app')` resolves to
16
- `build/xxx/app.js` — the compiled starter.
17
-
18
- @module CLI/Project
19
- */
20
- import { tagPath } from '../utils/resolveComponentPath.js';
21
- import { kebabToPascal } from '../utils/helpers.js';
22
- /** The starter component tag for a namespace: `xxx` → `xxx-app`. */
23
- function starterTag(namespace) {
24
- return `${namespace}-app`;
25
- }
26
- /**
27
- Whether a string is a valid project namespace: 1 to 4 lowercase letters.
28
-
29
- @example
30
- ```ts
31
- console.log(isValidNamespace('mc'));
32
- console.log(isValidNamespace('mcxyz'));
33
- console.log(isValidNamespace('toolong'));
34
- ```
35
- @param ns - Candidate namespace.
36
- */
37
- export function isValidNamespace(ns) {
38
- return /^[a-z]{1,4}$/.test(ns);
39
- }
40
- /**
41
- Builds the files of a complete dev environment for a namespace.
42
-
43
- @example
44
- ```ts
45
- const { namespace, files } = scaffoldProject('mc');
46
- console.log(namespace);
47
- console.log(Object.keys(files));
48
- ```
49
- @param namespace - 1 to 4 lowercase letters; becomes the `src/<namespace>/` folder and
50
- the first segment of every component tag.
51
- @throws If `namespace` is not 1 to 4 lowercase letters.
52
- */
53
- export function scaffoldProject(namespace) {
54
- if (!isValidNamespace(namespace)) {
55
- throw new Error(`invalid namespace "${namespace}": must be 1 to 4 lowercase letters (e.g. "mc")`);
56
- }
57
- const tag = starterTag(namespace);
58
- const base = tagPath(tag);
59
- const files = {};
60
- files['package.json'] = packageJson(namespace);
61
- files['tsconfig.json'] = tsconfig();
62
- files['tsconfig.vc.json'] = tsconfigVc();
63
- files['vite.config.ts'] = viteConfig(namespace);
64
- files['.gitignore'] = gitignore();
65
- files['opencode.jsonc'] = opencodeConfig();
66
- files['README.md'] = readme(namespace, tag);
67
- files['src/index.html'] = prodShell(tag);
68
- files['src/index-dev.html'] = devShell(namespace, tag);
69
- files[`src/${base}.ts`] = appSource(namespace, tag);
70
- files[`src/${base}.target.html`] = appHtml(namespace);
71
- files[`src/${base}.target.css`] = appCss(tag);
72
- files[`src/${namespace}/bundle.ts`] = bundle();
73
- return { files, namespace };
74
- }
75
- function packageJson(namespace) {
76
- return JSON.stringify({
77
- name: `${namespace}-app`,
78
- private: true,
79
- type: 'module',
80
- scripts: {
81
- dev: 'vite',
82
- build: 'vite build',
83
- preview: 'vite preview --outDir public --port 5174',
84
- typecheck: 'tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.vc.json',
85
- },
86
- dependencies: {
87
- '@twobirds/microcomponents': '^0.8.0',
88
- },
89
- devDependencies: {
90
- vite: '^8.1.5',
91
- typescript: '^6.0.3',
92
- '@types/node': '^26.6.5',
93
- },
94
- }, null, '\t');
95
- }
96
- function tsconfig() {
97
- return JSON.stringify({
98
- compilerOptions: {
99
- target: 'ESNext',
100
- module: 'ESNext',
101
- moduleResolution: 'Bundler',
102
- lib: ['ESNext', 'DOM', 'DOM.Iterable'],
103
- strict: true,
104
- esModuleInterop: true,
105
- skipLibCheck: true,
106
- rootDir: 'src',
107
- outDir: 'build',
108
- removeComments: true,
109
- },
110
- include: ['src/**/*.ts'],
111
- }, null, '\t');
112
- }
113
- function tsconfigVc() {
114
- return JSON.stringify({
115
- compilerOptions: {
116
- target: 'ESNext',
117
- module: 'ESNext',
118
- moduleResolution: 'Bundler',
119
- lib: ['ESNext', 'DOM'],
120
- types: ['node'],
121
- strict: true,
122
- esModuleInterop: true,
123
- skipLibCheck: true,
124
- noEmit: true,
125
- },
126
- files: ['vite.config.ts'],
127
- }, null, '\t');
128
- }
129
- function gitignore() {
130
- return `node_modules/
131
- build/
132
- public/
133
- `;
134
- }
135
- function opencodeConfig() {
136
- return JSON.stringify({
137
- $schema: 'https://opencode.ai/config.json',
138
- mcp: {
139
- microcomponents: {
140
- type: 'local',
141
- command: ['npx', '-p', '@twobirds/microcomponents', 'microcomponents-mcp'],
142
- enabled: true,
143
- },
144
- },
145
- }, null, '\t');
146
- }
147
- function readme(namespace, tag) {
148
- return `# ${namespace} App
149
-
150
- A Micro Components project scaffolded by \`mc create --namespace ${namespace}\`.
151
-
152
- ## Install
153
-
154
- \`\`\`sh
155
- pnpm install
156
- \`\`\`
157
-
158
- ## Dev
159
-
160
- \`\`\`sh
161
- pnpm dev
162
- \`\`\`
163
-
164
- The dev server runs \`tsc -w\` and serves \`build/\`, so a saved component reaches the
165
- browser through \`autoload()\` with no bundling. Companion \`.css\`, \`.html\` and \`.json\`
166
- files are mirrored into \`build/\` next to the compiled component, which is where the
167
- autoloader looks for them.
168
-
169
- ## Build
170
-
171
- \`\`\`sh
172
- pnpm build
173
- \`\`\`
174
-
175
- \`public/\` is the production site: one minified \`bundle.js\` with every companion asset
176
- inlined by \`loadAssetsPlugin\`, the concatenated \`bundle.css\`, and the page shell. A
177
- single script tag loads it, no autoloader needed.
178
-
179
- \`\`\`sh
180
- pnpm preview
181
- \`\`\`
182
-
183
- ## Type-check
184
-
185
- \`\`\`sh
186
- pnpm typecheck
187
- \`\`\`
188
-
189
- \`tsconfig.json\` compiles \`src/\` into \`build/\`; \`tsconfig.vc.json\` checks
190
- \`vite.config.ts\`, which lives outside \`src/\` and needs node types for its \`node:\`
191
- imports.
192
-
193
- ## Components
194
-
195
- Components live in \`src/${namespace}/\`. The starter is \`<${tag}>\` (\`src/${namespace}/app.ts\`).
196
-
197
- Add a component with:
198
-
199
- \`\`\`sh
200
- npx mc new ${namespace}-name --target-html --target-css
201
- \`\`\`
202
-
203
- Then add its import to \`src/${namespace}/bundle.ts\` so the production bundle picks it up.
204
- `;
205
- }
206
- function viteConfig(namespace) {
207
- return `/**
208
- * Dev and build config for a Micro Components project.
209
- *
210
- * The chain is \`src/\` → \`build/\` → \`public/\`:
211
- *
212
- * - \`build/\` is the \`tsc\` output: one \`.js\` per component at its own autoload path,
213
- * plus the copied companion assets and the dev shell. The dev server serves it, so
214
- * the page loads components on demand through \`autoload()\` and nothing is bundled.
215
- * - \`public/\` is the production output: one minified \`bundle.js\` with every companion
216
- * asset inlined by \`loadAssetsPlugin\`, the concatenated \`bundle.css\`, and the
217
- * production shell. A single script tag loads it, no autoloader needed.
218
- *
219
- * @module ViteConfig
220
- */
221
-
222
- import { defineConfig, type Plugin, type ResolvedConfig } from 'vite';
223
- import { loadAssetsPlugin } from '@twobirds/microcomponents/vite';
224
- import { spawn } from 'node:child_process';
225
- import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
226
- import path from 'node:path';
227
-
228
- const projectRoot = import.meta.dirname;
229
- const sourceRoot = path.join(projectRoot, 'src');
230
- const buildRoot = path.join(projectRoot, 'build');
231
- const publicRoot = path.join(projectRoot, 'public');
232
- const namespace = '${namespace}';
233
-
234
- const pageShells = new Set(['index.html', 'index-dev.html']);
235
-
236
- /**
237
- * Whether a served file is a companion asset that must stay raw text, rather
238
- * than one of the page shells Vite is allowed to transform.
239
- *
240
- * @example
241
- * \`\`\`ts
242
- * isCompanionAsset('app.target.html');
243
- * isCompanionAsset('index.html');
244
- * \`\`\`
245
- *
246
- * @param name Basename of the requested file.
247
- */
248
- function isCompanionAsset(name: string): boolean {
249
- if (pageShells.has(name)) return false;
250
- return name.endsWith('.css') || name.endsWith('.json') || name.endsWith('.html');
251
- }
252
-
253
- /**
254
- * Mirrors companion assets and the dev shell from \`src/\` into the served folder,
255
- * preserving the tree. Autoload resolves \`<ns-app>\` to \`<ns>/app.js\` relative to the
256
- * page shell, so the served folder needs the compiled component *and* its companion
257
- * files at the same relative path.
258
- *
259
- * @example
260
- * \`\`\`ts
261
- * await mirrorAssets(sourceRoot, buildRoot);
262
- * \`\`\`
263
- *
264
- * @param root Source root to walk.
265
- * @param out Folder to write into.
266
- * @param rel Path of the walked directory relative to \`root\`.
267
- */
268
- async function mirrorAssets(root: string, out: string, rel = ''): Promise<void> {
269
- for (const entry of await readdir(path.join(root, rel), { withFileTypes: true })) {
270
- const next = rel ? path.join(rel, entry.name) : entry.name;
271
- if (entry.isDirectory()) {
272
- await mirrorAssets(root, out, next);
273
- } else if (isCompanionAsset(entry.name) || entry.name === 'index-dev.html') {
274
- const dst = path.join(out, next);
275
- await mkdir(path.dirname(dst), { recursive: true });
276
- await cp(path.join(root, next), dst);
277
- }
278
- }
279
- }
280
-
281
- /**
282
- * Runs \`tsc -w\` for the lifetime of the dev server, so a saved component reaches
283
- * \`build/\` without a second command.
284
- *
285
- * @example
286
- * \`\`\`ts
287
- * plugins: [tscWatch()],
288
- * \`\`\`
289
- */
290
- function tscWatch(): Plugin {
291
- let child: ReturnType<typeof spawn> | undefined;
292
- return {
293
- name: 'tsc-watch',
294
- configResolved() {
295
- child = spawn('npx', ['tsc', '-w', '-p', 'tsconfig.json'], { cwd: projectRoot, stdio: 'inherit' });
296
- },
297
- closeServer() {
298
- child?.kill();
299
- },
300
- };
301
- }
302
-
303
- /**
304
- * Mirrors companion assets into \`build/\` before the dev server starts, and again on
305
- * every change, so the served folder stays autoload-ready.
306
- *
307
- * @example
308
- * \`\`\`ts
309
- * plugins: [syncBuild()],
310
- * \`\`\`
311
- */
312
- function syncBuild(): Plugin {
313
- return {
314
- name: 'sync-build',
315
- enforce: 'pre',
316
- async configureServer(server) {
317
- await mirrorAssets(sourceRoot, buildRoot);
318
- server.watcher.on('all', async (event, file) => {
319
- if (!file.startsWith(sourceRoot) || file.endsWith('.ts')) return;
320
- const rel = path.relative(sourceRoot, file);
321
- if (isCompanionAsset(path.basename(file)) || file.endsWith('index-dev.html')) {
322
- const dst = path.join(buildRoot, rel);
323
- await mkdir(path.dirname(dst), { recursive: true });
324
- await cp(file, dst);
325
- }
326
- });
327
- },
328
- };
329
- }
330
-
331
- /**
332
- * Serves companion \`.css\`, \`.html\` and \`.json\` files as raw text, ahead of the
333
- * Vite transform middleware. Without it a \`.html\` request returns the transformed
334
- * page shell and a \`.css\` request returns a JS module, so \`loadAssets()\` receives
335
- * markup it cannot use.
336
- *
337
- * @example
338
- * \`\`\`ts
339
- * plugins: [rawAssets(buildRoot)],
340
- * \`\`\`
341
- *
342
- * @param root Source root that mirrors the served tree.
343
- */
344
- function rawAssets(root: string): Plugin {
345
- return {
346
- name: 'raw-assets',
347
- enforce: 'pre',
348
- configureServer(server) {
349
- server.middlewares.use(async (req, res, next) => {
350
- const url = req.url?.split('?')[0];
351
- if (!url) return next();
352
- const file = decodeURIComponent(url);
353
- const name = file.split('/').pop() ?? '';
354
- if (!isCompanionAsset(name)) return next();
355
- try {
356
- const text = await readFile(path.join(root, file), 'utf-8');
357
- res.setHeader('Content-Type', name.endsWith('.css') ? 'text/css' : 'text/plain');
358
- res.end(text);
359
- } catch {
360
- next();
361
- }
362
- });
363
- },
364
- };
365
- }
366
-
367
- /**
368
- * Serves the autoload shell (\`index-dev.html\`) at \`/\`. Vite answers \`/\` with
369
- * \`index.html\`, which is the production shell referencing \`bundle.js\` — that file
370
- * only exists in \`public/\`, so the dev server has to redirect to the dev shell.
371
- *
372
- * @example
373
- * \`\`\`ts
374
- * plugins: [devShell()],
375
- * \`\`\`
376
- */
377
- function devShell(): Plugin {
378
- return {
379
- name: 'dev-shell',
380
- enforce: 'pre',
381
- configureServer(server) {
382
- server.middlewares.use((req, res, next) => {
383
- const url = req.url?.split('?')[0];
384
- if (url !== '/' && url !== '/index.html') return next();
385
- req.url = '/index-dev.html';
386
- next();
387
- });
388
- },
389
- };
390
- }
391
-
392
- /**
393
- * Concatenates the plain \`.css\` token presets under \`src/\` into \`bundle.css\`,
394
- * skipping the companion \`.target.css\` and \`.shadow.css\` files, which
395
- * \`loadAssetsPlugin\` inlines into the JS bundle.
396
- *
397
- * @example
398
- * \`\`\`ts
399
- * plugins: [bundleCss()],
400
- * \`\`\`
401
- */
402
- function bundleCss(): Plugin {
403
- let outDir = '';
404
- return {
405
- name: 'bundle-css',
406
- apply: 'build',
407
- configResolved(config: ResolvedConfig) {
408
- outDir = path.resolve(config.root, config.build.outDir);
409
- },
410
- async closeBundle() {
411
- async function collect(dir: string, rel: string, found: string[]): Promise<void> {
412
- for (const entry of await readdir(path.join(dir, rel), { withFileTypes: true })) {
413
- const next = rel ? path.join(rel, entry.name) : entry.name;
414
- if (entry.isDirectory()) {
415
- await collect(path.join(dir, rel), next, found);
416
- } else if (entry.name.endsWith('.css') && !/\\.(target|shadow)\\.css$/.test(entry.name)) {
417
- found.push(path.join(dir, next));
418
- }
419
- }
420
- }
421
- const files: string[] = [];
422
- await collect(sourceRoot, '', files);
423
- const parts = await Promise.all(files.map((f) => readFile(f, 'utf-8')));
424
- await mkdir(outDir, { recursive: true });
425
- await writeFile(path.join(outDir, 'bundle.css'), parts.join('\\n'), 'utf-8');
426
- },
427
- };
428
- }
429
-
430
- /**
431
- * Copies the production shell into \`public/\` next to the bundle, so the served
432
- * folder is a complete static site.
433
- *
434
- * @example
435
- * \`\`\`ts
436
- * plugins: [copyShell()],
437
- * \`\`\`
438
- */
439
- function copyShell(): Plugin {
440
- let outDir = '';
441
- return {
442
- name: 'copy-shell',
443
- apply: 'build',
444
- configResolved(config: ResolvedConfig) {
445
- outDir = path.resolve(config.root, config.build.outDir);
446
- },
447
- async closeBundle() {
448
- await cp(path.join(sourceRoot, 'index.html'), path.join(outDir, 'index.html'));
449
- },
450
- };
451
- }
452
-
453
- export default defineConfig(({ command }) => {
454
- if (command === 'serve') {
455
- return {
456
- root: buildRoot,
457
- server: { port: 5173, open: false },
458
- plugins: [syncBuild(), tscWatch(), rawAssets(buildRoot), devShell()],
459
- };
460
- }
461
- return {
462
- root: sourceRoot,
463
- build: {
464
- outDir: publicRoot,
465
- emptyOutDir: true,
466
- minify: true,
467
- target: 'esnext',
468
- sourcemap: false,
469
- lib: {
470
- entry: path.join(sourceRoot, namespace, 'bundle.ts'),
471
- formats: ['es'],
472
- fileName: () => 'bundle.js',
473
- },
474
- rollupOptions: {
475
- treeshake: false,
476
- output: { comments: false, keepNames: true },
477
- },
478
- },
479
- plugins: [loadAssetsPlugin(sourceRoot), bundleCss(), copyShell()],
480
- };
481
- });
482
- `;
483
- }
484
- function prodShell(tag) {
485
- return `<!doctype html>
486
- <html lang="en">
487
- <head>
488
- <meta charset="UTF-8" />
489
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
490
- <title>${tag.replace('-app', ' App')}</title>
491
- <script type="module" src="bundle.js"></script>
492
- <link rel="stylesheet" href="./bundle.css" />
493
- </head>
494
- <body>
495
- <${tag}></${tag}>
496
- </body>
497
- </html>
498
- `;
499
- }
500
- function devShell(namespace, tag) {
501
- return `<!doctype html>
502
- <html lang="en">
503
- <head>
504
- <meta charset="UTF-8" />
505
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
506
- <title>${namespace} App</title>
507
- <script type="module">
508
- import * as m from '@twobirds/microcomponents';
509
- m.autoload(true);
510
- Object.assign(window, m);
511
- </script>
512
- </head>
513
- <body>
514
- <${tag}></${tag}>
515
- </body>
516
- </html>
517
- `;
518
- }
519
- function bundle() {
520
- return `// Barrel entry for the production bundle: imports every component so the bundler
521
- // registers them all in one file. Add a line here when a component is created.
522
-
523
- import './app.js';
524
- `;
525
- }
526
- function appSource(namespace, tag) {
527
- const className = kebabToPascal(tag);
528
- return `import { DC, defineCE, parse, processAssets, type McEvent } from '@twobirds/microcomponents';
529
-
530
- /**
531
- \`${tag}\` — the starter micro component.
532
-
533
- @example
534
- \`\`\`html
535
- <${tag}></${tag}>
536
- \`\`\`
537
- */
538
- class ${className} extends DC {
539
- static assets = ['targetHtml', 'targetCss'];
540
-
541
- data = { title: 'Micro Components' };
542
-
543
- // Fill the template \`{key}\` tokens once, instead of binding reactive state.
544
- oneAssets(ev: McEvent<Record<string, string>>): void {
545
- processAssets(this, {
546
- targetHtml: parse(ev.detail.data.targetHtml, this.data),
547
- });
548
- }
549
- }
550
-
551
- defineCE('${tag}', ${className});
552
- `;
553
- }
554
- function appHtml(namespace) {
555
- return `<div class="app">
556
- <h1>{title}</h1>
557
- <p>Namespace scaffold ready. Edit <code>src/${namespace}/app.ts</code>.</p>
558
- </div>
559
- `;
560
- }
561
- function appCss(tag) {
562
- return `${tag} {
563
- display: block;
564
- padding: 2rem;
565
- font-family: system-ui, sans-serif;
566
- }
567
- `;
568
- }