@twobirds/microcomponents 0.3.0 → 0.4.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 (55) hide show
  1. package/README.md +77 -34
  2. package/dist/MC.min.js +1 -1
  3. package/dist/autoload.min.js +1 -1
  4. package/dist/cli/create.d.ts +53 -0
  5. package/dist/cli/create.d.ts.map +1 -0
  6. package/dist/cli/create.js +419 -0
  7. package/dist/cli/dispatch.d.ts +43 -0
  8. package/dist/cli/dispatch.d.ts.map +1 -0
  9. package/dist/cli/dispatch.js +251 -0
  10. package/dist/cli/index.d.ts +12 -0
  11. package/dist/cli/index.d.ts.map +1 -0
  12. package/dist/cli/index.js +12 -0
  13. package/dist/cli/scaffold.d.ts +68 -0
  14. package/dist/cli/scaffold.d.ts.map +1 -0
  15. package/dist/cli/scaffold.js +109 -0
  16. package/dist/cli/writeScaffold.d.ts +37 -0
  17. package/dist/cli/writeScaffold.d.ts.map +1 -0
  18. package/dist/cli/writeScaffold.js +60 -0
  19. package/dist/core/MC.d.ts +50 -9
  20. package/dist/core/MC.d.ts.map +1 -1
  21. package/dist/core/MC.js +78 -33
  22. package/dist/core/elements.d.ts +11 -8
  23. package/dist/core/elements.d.ts.map +1 -1
  24. package/dist/core/elements.js +12 -7
  25. package/dist/core/mcGuards.d.ts +2 -2
  26. package/dist/elements.min.js +1 -1
  27. package/dist/formValues.min.js +1 -1
  28. package/dist/microcomponents.min.js +1 -1
  29. package/dist/observables.min.js +1 -1
  30. package/dist/state/formValues.d.ts +11 -5
  31. package/dist/state/formValues.d.ts.map +1 -1
  32. package/dist/state/formValues.js +18 -11
  33. package/dist/state/observables.d.ts +30 -13
  34. package/dist/state/observables.d.ts.map +1 -1
  35. package/dist/state/observables.js +64 -35
  36. package/dist/state/placeholders.d.ts.map +1 -1
  37. package/dist/state/placeholders.js +16 -11
  38. package/dist/types/mcTypes.d.ts +16 -7
  39. package/dist/types/mcTypes.d.ts.map +1 -1
  40. package/dist/utils/autoload.d.ts +6 -3
  41. package/dist/utils/autoload.d.ts.map +1 -1
  42. package/dist/utils/autoload.js +2 -2
  43. package/dist/utils/events.d.ts +21 -28
  44. package/dist/utils/events.d.ts.map +1 -1
  45. package/dist/utils/events.js +23 -29
  46. package/dist/utils/helpers.d.ts +19 -19
  47. package/dist/utils/helpers.d.ts.map +1 -1
  48. package/dist/utils/helpers.js +19 -19
  49. package/dist/utils/index.d.ts +2 -2
  50. package/dist/utils/index.d.ts.map +1 -1
  51. package/dist/utils/index.js +2 -2
  52. package/dist/utils/resolveComponentPath.d.ts +36 -0
  53. package/dist/utils/resolveComponentPath.d.ts.map +1 -0
  54. package/dist/utils/resolveComponentPath.js +39 -0
  55. package/package.json +19 -13
@@ -0,0 +1,419 @@
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 mirrors the repo's own demos: a Vite dev server serving `src/` straight,
13
+ a page shell at that root that turns on `autoload(true)`, and a starter component with
14
+ companion `targetHtml` / `targetCss` assets. Because the page shell is the autoload
15
+ base, `import.meta.url` + `tagPath('xxx-app')` resolves to `src/xxx/app.js` — the
16
+ 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['README.md'] = readme(namespace, tag);
66
+ files['src/index.html'] = shell(namespace, tag);
67
+ files[`src/${base}.ts`] = appSource(namespace, tag);
68
+ files[`src/${base}.target.html`] = appHtml(namespace);
69
+ files[`src/${base}.target.css`] = appCss(tag);
70
+ return { files, namespace };
71
+ }
72
+ function packageJson(namespace) {
73
+ return JSON.stringify({
74
+ name: `${namespace}-app`,
75
+ private: true,
76
+ type: 'module',
77
+ scripts: {
78
+ dev: 'vite',
79
+ build: 'vite build',
80
+ preview: 'vite preview',
81
+ typecheck: 'tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.vc.json',
82
+ },
83
+ dependencies: {
84
+ '@twobirds/microcomponents': '^0.4.0',
85
+ },
86
+ devDependencies: {
87
+ vite: '^8.1.5',
88
+ typescript: '^6.0.3',
89
+ '@types/node': '^26.6.5',
90
+ },
91
+ }, null, '\t');
92
+ }
93
+ function tsconfig() {
94
+ return JSON.stringify({
95
+ compilerOptions: {
96
+ target: 'ESNext',
97
+ module: 'ESNext',
98
+ moduleResolution: 'Bundler',
99
+ lib: ['ESNext', 'DOM', 'DOM.Iterable'],
100
+ strict: true,
101
+ esModuleInterop: true,
102
+ skipLibCheck: true,
103
+ noEmit: true,
104
+ },
105
+ include: ['src/**/*.ts'],
106
+ }, null, '\t');
107
+ }
108
+ function tsconfigVc() {
109
+ return JSON.stringify({
110
+ compilerOptions: {
111
+ target: 'ESNext',
112
+ module: 'ESNext',
113
+ moduleResolution: 'Bundler',
114
+ lib: ['ESNext', 'DOM'],
115
+ types: ['node'],
116
+ strict: true,
117
+ esModuleInterop: true,
118
+ skipLibCheck: true,
119
+ noEmit: true,
120
+ },
121
+ files: ['vite.config.ts'],
122
+ }, null, '\t');
123
+ }
124
+ function gitignore() {
125
+ return `node_modules/
126
+ dist/
127
+ `;
128
+ }
129
+ function readme(namespace, tag) {
130
+ return `# ${namespace} App
131
+
132
+ A Micro Components project scaffolded by \`mc create --namespace ${namespace}\`.
133
+
134
+ ## Install
135
+
136
+ \`\`\`sh
137
+ pnpm install
138
+ \`\`\`
139
+
140
+ ## Dev
141
+
142
+ \`\`\`sh
143
+ pnpm dev
144
+ \`\`\`
145
+
146
+ ## Build
147
+
148
+ \`\`\`sh
149
+ pnpm build
150
+ \`\`\`
151
+
152
+ Components compile to \`dist/\` at their own autoload path, and companion \`.css\`, \`.html\` and
153
+ \`.json\` files are copied next to them, so the built output loads like the dev server.
154
+
155
+ ## Type-check
156
+
157
+ \`\`\`sh
158
+ pnpm typecheck
159
+ \`\`\`
160
+
161
+ \`tsconfig.json\` checks \`src/\`; \`tsconfig.vc.json\` checks \`vite.config.ts\`, which lives outside
162
+ \`src/\` and needs node types for its \`node:\` imports.
163
+
164
+ ## Components
165
+
166
+ Components live in \`src/${namespace}/\`. The starter is \`<${tag}>\` (\`src/${namespace}/app.ts\`).
167
+
168
+ Add a component with:
169
+
170
+ \`\`\`sh
171
+ npx mc new ${namespace}-name --target-html --target-css
172
+ \`\`\`
173
+ `;
174
+ }
175
+ function viteConfig(namespace) {
176
+ return `/**
177
+ * Dev and build config for a Micro Components project.
178
+ *
179
+ * \`src/\` is the Vite root, so the page shell and every component source are
180
+ * served straight from source with no build step. Companion \`.css\`, \`.html\` and
181
+ * \`.json\` files are served as raw text — Vite would otherwise answer a \`.html\`
182
+ * request with its own transformed page (injecting \`/@vite/client\` into the
183
+ * template) and a \`.css\` request with a JS module, neither of which
184
+ * \`loadAssets()\` can consume. \`copyCompanions\` mirrors the same files into
185
+ * \`dist/\` so the built output stays autoload-ready, and every component under
186
+ * \`src/<namespace>/\` is a build entry, emitted at its own autoload path
187
+ * (\`mc/app.ts\` → \`dist/mc/app.js\`).
188
+ *
189
+ * @module ViteConfig
190
+ */
191
+
192
+ import { defineConfig, type Plugin, type ResolvedConfig } from 'vite';
193
+ import { cp, mkdir, readdir, readFile } from 'node:fs/promises';
194
+ import path from 'node:path';
195
+
196
+ const projectRoot = import.meta.dirname;
197
+ const sourceRoot = path.join(projectRoot, 'src');
198
+ const namespace = '${namespace}';
199
+
200
+ const pageShells = new Set(['index.html', 'index-dev.html']);
201
+
202
+ /**
203
+ * Whether a served file is a companion asset that must stay raw text, rather
204
+ * than one of the page shells Vite is allowed to transform.
205
+ *
206
+ * @example
207
+ * \`\`\`ts
208
+ * isCompanionAsset('app.target.html');
209
+ * isCompanionAsset('index.html');
210
+ * \`\`\`
211
+ *
212
+ * @param name Basename of the requested file.
213
+ */
214
+ function isCompanionAsset(name: string): boolean {
215
+ if (pageShells.has(name)) return false;
216
+ return name.endsWith('.css') || name.endsWith('.json') || name.endsWith('.html');
217
+ }
218
+
219
+ /**
220
+ * Serves companion \`.css\`, \`.html\` and \`.json\` files as raw text, ahead of the
221
+ * Vite transform middleware. Without it a \`.html\` request returns the transformed
222
+ * page shell and a \`.css\` request returns a JS module, so \`loadAssets()\` receives
223
+ * markup it cannot use.
224
+ *
225
+ * @example
226
+ * \`\`\`ts
227
+ * plugins: [rawAssets(sourceRoot)],
228
+ * \`\`\`
229
+ *
230
+ * @param root Source root that mirrors the \`src/\` tree.
231
+ */
232
+ function rawAssets(root: string): Plugin {
233
+ return {
234
+ name: 'raw-assets',
235
+ enforce: 'pre',
236
+ configureServer(server) {
237
+ server.middlewares.use(async (req, res, next) => {
238
+ const url = req.url?.split('?')[0];
239
+ if (!url) return next();
240
+ const file = decodeURIComponent(url);
241
+ const name = file.split('/').pop() ?? '';
242
+ if (!isCompanionAsset(name)) return next();
243
+ try {
244
+ const text = await readFile(path.join(root, file), 'utf-8');
245
+ res.setHeader('Content-Type', name.endsWith('.css') ? 'text/css' : 'text/plain');
246
+ res.end(text);
247
+ } catch {
248
+ next();
249
+ }
250
+ });
251
+ },
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Copies every companion asset from \`src/\` into the build output, preserving the
257
+ * directory tree. Autoload resolves \`<ns-app>\` to \`<ns>/app.js\` relative to the
258
+ * page shell, so \`dist/\` needs the compiled component *and* its companion files
259
+ * at the same relative path.
260
+ *
261
+ * @example
262
+ * \`\`\`ts
263
+ * plugins: [copyCompanions(sourceRoot)],
264
+ * \`\`\`
265
+ *
266
+ * @param root Source root to walk.
267
+ */
268
+ function copyCompanions(root: string): Plugin {
269
+ let outDir = '';
270
+ return {
271
+ name: 'copy-companions',
272
+ apply: 'build',
273
+ configResolved(config: ResolvedConfig) {
274
+ outDir = path.resolve(config.root, config.build.outDir);
275
+ },
276
+ async closeBundle() {
277
+ async function walk(dir: string, rel: string): Promise<void> {
278
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
279
+ const src = path.join(dir, entry.name);
280
+ if (entry.isDirectory()) {
281
+ await walk(src, path.join(rel, entry.name));
282
+ } else if (isCompanionAsset(entry.name)) {
283
+ const dst = path.join(outDir, rel, entry.name);
284
+ await mkdir(path.dirname(dst), { recursive: true });
285
+ await cp(src, dst);
286
+ }
287
+ }
288
+ }
289
+ await walk(root, '');
290
+ },
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Names a build chunk after its source path, so a component lands at the autoload
296
+ * path (\`src/mc/app.ts\` → \`dist/mc/app.js\`). Rollup truncates chunk names at the
297
+ * last \`/\`, so \`info.name\` alone cannot carry a folder — the facade module id is
298
+ * the reliable source.
299
+ *
300
+ * @example
301
+ * \`\`\`ts
302
+ * entryName({ name: 'mc/app', facadeModuleId: '/project/src/mc/app.ts' });
303
+ * entryName({ name: 'index', facadeModuleId: '/project/src/index.html' });
304
+ * \`\`\`
305
+ *
306
+ * @param info Rollup chunk info.
307
+ */
308
+ function entryName(info: { name: string; facadeModuleId: string | null }): string {
309
+ const facade = info.facadeModuleId;
310
+ if (!facade || facade.endsWith('.html')) return \`\${info.name}.js\`;
311
+ return path.relative(sourceRoot, facade).replace(/\\\\/g, '/').replace(/\\.(ts|js)$/, '.js');
312
+ }
313
+
314
+ /**
315
+ * Collects every component source as a build entry, keyed by its path relative to
316
+ * \`src/\`. Plain \`.js\` files count too, so the rapid-prototyping path stays
317
+ * buildable. New components are picked up automatically — no config edit needed.
318
+ *
319
+ * @example
320
+ * \`\`\`ts
321
+ * const entries: Record<string, string> = {};
322
+ * await collectEntries(path.join(sourceRoot, 'mc'), 'mc', entries);
323
+ * \`\`\`
324
+ *
325
+ * @param dir Directory to walk.
326
+ * @param rel Path of that directory relative to \`src/\`.
327
+ * @param entries Accumulated entries, mutated in place.
328
+ */
329
+ async function collectEntries(dir: string, rel: string, entries: Record<string, string>): Promise<void> {
330
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
331
+ const full = path.join(dir, entry.name);
332
+ if (entry.isDirectory()) {
333
+ await collectEntries(full, path.join(rel, entry.name), entries);
334
+ } else if (entry.name.endsWith('.ts') || entry.name.endsWith('.js')) {
335
+ entries[path.join(rel, entry.name).replace(/\\.(ts|js)$/, '')] = full;
336
+ }
337
+ }
338
+ }
339
+
340
+ export default defineConfig(async () => {
341
+ const entries: Record<string, string> = {};
342
+ await collectEntries(path.join(sourceRoot, namespace), namespace, entries);
343
+ return {
344
+ root: sourceRoot,
345
+ build: {
346
+ outDir: path.join(projectRoot, 'dist'),
347
+ emptyOutDir: true,
348
+ rollupOptions: {
349
+ input: { index: path.join(sourceRoot, 'index.html'), ...entries },
350
+ output: { entryFileNames: (info: { name: string; facadeModuleId: string | null }) => entryName(info) },
351
+ },
352
+ },
353
+ plugins: [rawAssets(sourceRoot), copyCompanions(sourceRoot)],
354
+ };
355
+ });
356
+ `;
357
+ }
358
+ function shell(namespace, tag) {
359
+ return `<!doctype html>
360
+ <html lang="en">
361
+ <head>
362
+ <meta charset="UTF-8" />
363
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
364
+ <title>${namespace} App</title>
365
+ <script type="module">
366
+ import * as m from '@twobirds/microcomponents';
367
+ m.autoload(true);
368
+ Object.assign(window, m);
369
+ </script>
370
+ </head>
371
+ <body>
372
+ <${tag}></${tag}>
373
+ </body>
374
+ </html>
375
+ `;
376
+ }
377
+ function appSource(namespace, tag) {
378
+ const className = kebabToPascal(tag);
379
+ return `import { DC, defineCE, parse, processAssets, type McEvent } from '@twobirds/microcomponents';
380
+
381
+ /**
382
+ \`${tag}\` — the starter micro component.
383
+
384
+ @example
385
+ \`\`\`html
386
+ <${tag}></${tag}>
387
+ \`\`\`
388
+ */
389
+ class ${className} extends DC {
390
+ static assets = ['targetHtml', 'targetCss'];
391
+
392
+ data = { title: 'Micro Components' };
393
+
394
+ // Fill the template \`{key}\` tokens once, instead of binding reactive state.
395
+ oneAssets(ev: McEvent<Record<string, string>>): void {
396
+ processAssets(this, {
397
+ targetHtml: parse(ev.detail.data.targetHtml, this.data),
398
+ });
399
+ }
400
+ }
401
+
402
+ defineCE('${tag}', ${className});
403
+ `;
404
+ }
405
+ function appHtml(namespace) {
406
+ return `<div class="app">
407
+ <h1>{title}</h1>
408
+ <p>Namespace scaffold ready. Edit <code>src/${namespace}/app.ts</code>.</p>
409
+ </div>
410
+ `;
411
+ }
412
+ function appCss(tag) {
413
+ return `${tag} {
414
+ display: block;
415
+ padding: 2rem;
416
+ font-family: system-ui, sans-serif;
417
+ }
418
+ `;
419
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ Command dispatcher for the `mc` CLI.
3
+
4
+ Kept free of any top-level side effects so the routing can be unit-tested:
5
+ `index.ts` is the thin executable entry that calls {@link main}, while this
6
+ module only exports the logic. Commands that wrap a package.json script
7
+ (`clean`, `check`) are deliberately thin — they delegate to the script so the
8
+ script stays the single source of truth. When the current project defines no
9
+ such script, the CLI runs its own implementation against the current project,
10
+ so the command works in any consumer project.
11
+
12
+ @module CLI/Dispatch
13
+ */
14
+ export type Runner = (script: string) => Promise<number>;
15
+ /**
16
+ Runs a package.json script through pnpm and resolves with its exit code.
17
+
18
+ Thin wrappers like `clean` and `check` delegate here so the script — not the
19
+ CLI — remains the single source of truth for what each command does. If the
20
+ current project defines no such script, the CLI runs its own implementation
21
+ against the current project, so the command works in any consumer project.
22
+
23
+ @example
24
+ ```ts
25
+ const code = await runScript('check');
26
+ ```
27
+ */
28
+ export declare function runScript(script: string): Promise<number>;
29
+ /**
30
+ Parses `argv` and routes it to the matching command, returning a process exit
31
+ code. `run` is injectable so the script-wrapping commands can be tested without
32
+ spawning anything.
33
+
34
+ @example
35
+ ```ts
36
+ const code = await main(['clean'], async (script) => {
37
+ console.log(`would run ${script}`);
38
+ return 0;
39
+ });
40
+ ```
41
+ */
42
+ export declare function main(argv: string[], run?: Runner): Promise<number>;
43
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/cli/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;EAYE;AAiDF,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAmFzD;;;;;;;;;;;;EAYE;AACF,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ/D;AAED;;;;;;;;;;;;EAYE;AACF,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,GAAE,MAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0FnF"}