@twobirds/microcomponents 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +2 -16
  2. package/dist/cli/bundle.d.ts +106 -0
  3. package/dist/cli/bundle.d.ts.map +1 -0
  4. package/dist/cli/bundler.d.ts +44 -0
  5. package/dist/cli/bundler.d.ts.map +1 -0
  6. package/dist/cli/create.d.ts.map +1 -1
  7. package/dist/cli/dev.d.ts +87 -0
  8. package/dist/cli/dev.d.ts.map +1 -0
  9. package/dist/cli/dispatch.d.ts +6 -2
  10. package/dist/cli/dispatch.d.ts.map +1 -1
  11. package/dist/cli/index.js +1807 -1
  12. package/dist/cli/inliner.d.ts +42 -0
  13. package/dist/cli/inliner.d.ts.map +1 -0
  14. package/dist/cli/preview.d.ts +48 -0
  15. package/dist/cli/preview.d.ts.map +1 -0
  16. package/dist/core/elements.js +1294 -75
  17. package/dist/core/mcGuards.js +5 -8
  18. package/dist/mcp/api.json +2 -17
  19. package/dist/mcp/concepts/project-layout.md +9 -8
  20. package/dist/mcp/index.js +450 -230
  21. package/dist/microcomponents.js +2882 -9
  22. package/dist/state/formValues.js +2236 -281
  23. package/dist/state/observables.js +1794 -342
  24. package/dist/utils/events.js +171 -197
  25. package/package.json +3 -7
  26. package/dist/cli/create.js +0 -653
  27. package/dist/cli/dispatch.js +0 -316
  28. package/dist/cli/scaffold.js +0 -109
  29. package/dist/cli/writeScaffold.js +0 -114
  30. package/dist/core/MC.js +0 -837
  31. package/dist/core/index.js +0 -72
  32. package/dist/state/index.js +0 -5
  33. package/dist/state/placeholders.js +0 -187
  34. package/dist/types/index.js +0 -1
  35. package/dist/types/mcTypes.js +0 -9
  36. package/dist/utils/autoload.js +0 -160
  37. package/dist/utils/helpers.js +0 -532
  38. package/dist/utils/index.js +0 -12
  39. package/dist/utils/resolveComponentPath.js +0 -39
  40. package/dist/vite/index.d.ts +0 -7
  41. package/dist/vite/index.d.ts.map +0 -1
  42. package/dist/vite/index.js +0 -6
  43. package/dist/vite/loadAssets.d.ts +0 -36
  44. package/dist/vite/loadAssets.d.ts.map +0 -1
  45. package/dist/vite/loadAssets.js +0 -221
@@ -1,316 +0,0 @@
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
- import { spawn } from 'node:child_process';
15
- import { parseArgs } from 'node:util';
16
- import { readFile, rm } from 'node:fs/promises';
17
- import { existsSync } from 'node:fs';
18
- import path from 'node:path';
19
- import { writeScaffold, writeProject, displayPath } from './writeScaffold.js';
20
- import { scaffoldComponent } from './scaffold.js';
21
- import { scaffoldProject } from './create.js';
22
- const USAGE = `mc — Micro Components build tools
23
-
24
- Usage:
25
- mc new <tag> [--css] [--html] [--target-html] [--shadow-html] [--target-css] [--shadow-css] [--js] [--dir <path>] [--force] [--dry-run]
26
- mc create --namespace <ns> [--dir <path>] [--force] [--dry-run]
27
- mc clean
28
- mc check
29
- mc --version
30
- mc --help
31
-
32
- Commands:
33
- new <tag> Scaffold a micro component (e.g. mc new my-card --target-html --target-css)
34
- create Create a dev environment (e.g. mc create --namespace mc --dir mc-app)
35
- clean Remove build artifacts (dist, public, build, coverage)
36
- check Type-check every project (lib, cli, test, demo, node)
37
-
38
- Companion assets:
39
- --css document-level stylesheet (CSS custom properties / design tokens)
40
- --html unvariant light-DOM template
41
- --target-html template applied to target.innerHTML
42
- --shadow-html template applied to shadowRoot.innerHTML
43
- --target-css document-level styles scoped to the host tag
44
- --shadow-css styles injected into the shadow root
45
-
46
- Other options:
47
- --js emit plain JavaScript instead of TypeScript
48
- --dir <path> scaffold root (default: src)
49
- --force overwrite files that already exist
50
- --dry-run print what would be written; touch nothing on disk
51
- --version print the version of the package the CLI ships in (-v)
52
- --help print this text (-h)
53
-
54
- Created paths are printed as they appear from the current directory. mc create writes
55
- a whole project into a folder of its own (default: the namespace), so its files show
56
- up under that folder, e.g. mc-app/src/mc/app.ts.
57
- `;
58
- // Map CLI flags to the runtime's camelCase asset descriptors.
59
- const ASSET_FLAGS = {
60
- css: 'css',
61
- html: 'html',
62
- 'target-html': 'targetHtml',
63
- 'shadow-html': 'shadowHtml',
64
- 'target-css': 'targetCss',
65
- 'shadow-css': 'shadowCss',
66
- };
67
- /**
68
- A scaffolded file as it appears from the current directory: the file `rel` under
69
- `root`, resolved and made readable. `mc create` writes a project into a folder of
70
- its own, so `src/mc/app.ts` under `mc-app` prints as `mc-app/src/mc/app.ts` — the
71
- path the user can find with `ls`.
72
-
73
- @example
74
- ```ts
75
- createdPath('mc-app', 'src/mc/app.ts');
76
- createdPath('src', 'my/card.ts');
77
- ```
78
- @param root - Scaffold root the file is written under.
79
- @param rel - Path of the file relative to `root`.
80
- */
81
- function createdPath(root, rel) {
82
- return displayPath(path.resolve(root, rel));
83
- }
84
- /**
85
- Spawns a command and resolves with its exit code.
86
-
87
- @example
88
- ```ts
89
- const code = await spawnProcess('pnpm', ['run', 'clean']);
90
- ```
91
- @param command - Executable to spawn.
92
- @param args - Arguments to pass.
93
- @param cwd - Working directory (defaults to the current one).
94
- */
95
- function spawnProcess(command, args, cwd) {
96
- return new Promise((resolve) => {
97
- const child = spawn(command, args, { stdio: 'inherit', cwd });
98
- child.on('exit', (code) => resolve(code ?? 1));
99
- });
100
- }
101
- /**
102
- Whether the package.json in the current directory declares `script` in its
103
- "scripts". Consumers get this check so `mc clean`/`mc check` can fall back to
104
- the CLI's own implementation when the project defines no such script.
105
-
106
- @example
107
- ```ts
108
- const ok = await hasScript('clean');
109
- ```
110
- @param script - Script name to look up.
111
- */
112
- async function hasScript(script) {
113
- try {
114
- const pkg = JSON.parse(await readFile(path.join(process.cwd(), 'package.json'), 'utf8'));
115
- return typeof pkg?.scripts?.[script] === 'string';
116
- }
117
- catch {
118
- return false;
119
- }
120
- }
121
- /** Build artifacts to remove, relative to the project root. */
122
- const CLEAN_DIRS = ['dist', 'public', 'build', 'coverage'];
123
- /**
124
- Removes build artifacts in the current project.
125
-
126
- @example
127
- ```ts
128
- const code = await cleanSelf();
129
- ```
130
- @param cwd - Project root (defaults to the current directory).
131
- */
132
- async function cleanSelf(cwd = process.cwd()) {
133
- await Promise.all(CLEAN_DIRS.map(async (dir) => {
134
- const abs = path.join(cwd, dir);
135
- if (existsSync(abs)) {
136
- await rm(abs, { recursive: true, force: true });
137
- console.log(`removed ${dir}/`);
138
- }
139
- }));
140
- return 0;
141
- }
142
- /**
143
- Type-checks the current project with its own `tsconfig.json`.
144
-
145
- @example
146
- ```ts
147
- const code = await checkSelf();
148
- ```
149
- @param cwd - Project root (defaults to the current directory).
150
- */
151
- async function checkSelf(cwd = process.cwd()) {
152
- const config = path.join(cwd, 'tsconfig.json');
153
- if (!existsSync(config)) {
154
- console.error('mc check: no tsconfig.json in the current project');
155
- return 1;
156
- }
157
- return spawnProcess('pnpm', ['exec', 'tsc', '--noEmit', '-p', 'tsconfig.json'], cwd);
158
- }
159
- /**
160
- The version of the package the CLI ships in, read from its package.json. Two
161
- levels above `dist/cli` (and above `src/cli` when tsx runs the source) is the
162
- package root, so the CLI reports the version it was installed with, not the
163
- version of the project it is run in.
164
-
165
- @example
166
- ```ts
167
- const version = await readVersion();
168
- ```
169
- @param pkgPath - package.json to read (defaults to the package root of this CLI).
170
- */
171
- async function readVersion(pkgPath = path.join(import.meta.dirname, '..', '..', 'package.json')) {
172
- const { version } = JSON.parse(await readFile(pkgPath, 'utf8'));
173
- return version;
174
- }
175
- /**
176
- Runs a package.json script through pnpm and resolves with its exit code.
177
-
178
- Thin wrappers like `clean` and `check` delegate here so the script — not the
179
- CLI — remains the single source of truth for what each command does. If the
180
- current project defines no such script, the CLI runs its own implementation
181
- against the current project, so the command works in any consumer project.
182
-
183
- @example
184
- ```ts
185
- const code = await runScript('check');
186
- ```
187
- */
188
- export async function runScript(script) {
189
- if (await hasScript(script)) {
190
- return spawnProcess('pnpm', ['run', script]);
191
- }
192
- if (script === 'clean')
193
- return cleanSelf();
194
- if (script === 'check')
195
- return checkSelf();
196
- console.error(`mc: no "${script}" script in the current project`);
197
- return 1;
198
- }
199
- /**
200
- Parses `argv` and routes it to the matching command, returning a process exit
201
- code. `run` is injectable so the script-wrapping commands can be tested without
202
- spawning anything.
203
-
204
- @example
205
- ```ts
206
- const code = await main(['clean'], async (script) => {
207
- console.log(`would run ${script}`);
208
- return 0;
209
- });
210
- ```
211
- @example
212
- ```ts
213
- const code = await main(['--version']);
214
- ```
215
- */
216
- export async function main(argv, run = runScript) {
217
- const { values, positionals } = parseArgs({
218
- args: argv,
219
- allowPositionals: true,
220
- options: {
221
- css: { type: 'boolean' },
222
- html: { type: 'boolean' },
223
- 'target-html': { type: 'boolean' },
224
- 'shadow-html': { type: 'boolean' },
225
- 'target-css': { type: 'boolean' },
226
- 'shadow-css': { type: 'boolean' },
227
- js: { type: 'boolean' },
228
- dir: { type: 'string' },
229
- namespace: { type: 'string' },
230
- force: { type: 'boolean' },
231
- 'dry-run': { type: 'boolean' },
232
- help: { type: 'boolean', short: 'h' },
233
- version: { type: 'boolean', short: 'v' },
234
- },
235
- });
236
- const command = positionals[0];
237
- if (values.version) {
238
- console.log(await readVersion());
239
- return 0;
240
- }
241
- if (values.help || !command) {
242
- console.log(USAGE);
243
- return 0;
244
- }
245
- if (command === 'new') {
246
- const tag = positionals[1];
247
- if (!tag) {
248
- console.error('mc new: missing component tag (e.g. mc new my-card)');
249
- return 1;
250
- }
251
- const assets = [];
252
- const flags = values;
253
- for (const [flag, kind] of Object.entries(ASSET_FLAGS)) {
254
- if (flags[flag])
255
- assets.push(kind);
256
- }
257
- if (values['dry-run']) {
258
- const { files } = scaffoldComponent(tag, { assets, js: values.js });
259
- const root = values.dir ?? 'src';
260
- for (const rel of Object.keys(files).sort())
261
- console.log(`would create ${createdPath(root, rel)}`);
262
- return 0;
263
- }
264
- const root = values.dir ?? 'src';
265
- let written;
266
- try {
267
- written = await writeScaffold(tag, root, { assets, js: values.js, force: values.force });
268
- }
269
- catch (e) {
270
- console.error(e.message);
271
- return 1;
272
- }
273
- for (const rel of written)
274
- console.log(`created ${createdPath(root, rel)}`);
275
- return 0;
276
- }
277
- if (command === 'create') {
278
- const namespace = values.namespace;
279
- if (!namespace) {
280
- console.error('mc create: missing --namespace (e.g. mc create --namespace mc)');
281
- return 1;
282
- }
283
- const root = values.dir ?? namespace;
284
- let files;
285
- try {
286
- ({ files } = scaffoldProject(namespace));
287
- }
288
- catch (e) {
289
- console.error(e.message);
290
- return 1;
291
- }
292
- if (values['dry-run']) {
293
- for (const rel of Object.keys(files).sort())
294
- console.log(`would create ${createdPath(root, rel)}`);
295
- return 0;
296
- }
297
- let written;
298
- try {
299
- written = await writeProject(namespace, root, { force: values.force });
300
- }
301
- catch (e) {
302
- console.error(e.message);
303
- return 1;
304
- }
305
- for (const rel of written)
306
- console.log(`created ${createdPath(root, rel)}`);
307
- console.log(`\nproject "${namespace}" created in ${root}/ — run: cd ${root} && pnpm install && pnpm dev`);
308
- return 0;
309
- }
310
- if (command === 'clean')
311
- return run('clean');
312
- if (command === 'check')
313
- return run('check');
314
- console.error(`mc: unknown command "${command}"\n\n${USAGE}`);
315
- return 1;
316
- }
@@ -1,109 +0,0 @@
1
- /**
2
- Scaffolding for new micro components.
3
-
4
- Pure, filesystem-free logic: given a tag name it returns the files to write and
5
- their contents. The tag-to-path rule is not re-derived here — it comes from
6
- {@link tagPath}, the same primitive the runtime autoload loader uses, so the
7
- scaffold and the runtime can never disagree about where a component lives.
8
-
9
- Companion assets follow the runtime's variant convention (see `loadAssets` /
10
- `processAssets`): a camelCase descriptor maps to a dotted filename, e.g.
11
- `'targetHtml'` → `{path}.target.html`, `'shadowCss'` → `{path}.shadow.css`, and
12
- the unvariant `'css'` → `{path}.css` (the document-level stylesheet where CSS
13
- custom properties / design tokens live).
14
-
15
- @module CLI/Scaffold
16
- */
17
- import { tagPath } from '../utils/resolveComponentPath.js';
18
- // Custom element names must be lowercase kebab-case and contain at least one
19
- // hyphen (defineCE rejects hyphenless tags), with no empty segments.
20
- const TAG_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/;
21
- /**
22
- Whether a string is a valid micro component tag name.
23
-
24
- @example
25
- ```ts
26
- console.log(isValidComponentTag('my-card'));
27
- console.log(isValidComponentTag('mycard'));
28
- ```
29
- */
30
- export function isValidComponentTag(tag) {
31
- return TAG_RE.test(tag);
32
- }
33
- /**
34
- Builds the files for a new micro component from its tag name.
35
-
36
- @example
37
- ```ts
38
- const { className, files } = scaffoldComponent('my-card', {
39
- assets: ['targetHtml', 'targetCss'],
40
- });
41
- console.log(className);
42
- console.log(Object.keys(files));
43
- ```
44
- @param tag - Kebab-case component tag, e.g. `my-card`.
45
- @param options - Which companion assets to emit and whether to use `.js`.
46
- @throws If `tag` is not a valid custom element name.
47
- */
48
- export function scaffoldComponent(tag, options = {}) {
49
- if (!isValidComponentTag(tag)) {
50
- throw new Error(`invalid component tag "${tag}": must be kebab-case with at least one hyphen (e.g. "my-card")`);
51
- }
52
- const className = tag
53
- .split('-')
54
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
55
- .join('');
56
- const ext = options.js ? 'js' : 'ts';
57
- const ctorParam = options.js ? 'target' : 'target: HTMLElement';
58
- const base = tagPath(tag);
59
- const assets = [...new Set(options.assets ?? [])];
60
- const files = {};
61
- files[`${base}.${ext}`] = componentSource(tag, ctorParam, assets);
62
- for (const kind of assets) {
63
- files[assetFile(tag, kind)] = assetSource(kind, tag);
64
- }
65
- return { files, className };
66
- }
67
- // Mirrors loadAssets' parseAsset: camelCase variant+ext → dotted filename,
68
- // plain ext → single extension. Reuses tagPath so scaffold and runtime agree.
69
- function assetFile(tag, kind) {
70
- const m = kind.match(/^([a-z]+)([A-Z][a-z]*)$/);
71
- const suffix = m ? `.${m[1]}.${m[2].toLowerCase()}` : `.${kind.toLowerCase()}`;
72
- return `${tagPath(tag)}${suffix}`;
73
- }
74
- function componentSource(tag, ctorParam, assets) {
75
- const lines = [
76
- `import { DC, defineCE } from '@twobirds/microcomponents';`,
77
- ``,
78
- `/**`,
79
- `\`${tag}\` — describe what this micro component does.`,
80
- ``,
81
- `@example`,
82
- '```html',
83
- `<${tag}></${tag}>`,
84
- '```',
85
- `*/`,
86
- `defineCE(`,
87
- `\t'${tag}',`,
88
- `\tclass extends DC {`,
89
- ];
90
- if (assets.length) {
91
- lines.push(`\t\tstatic assets = [${assets.map((a) => `'${a}'`).join(', ')}];`);
92
- }
93
- lines.push(`\t\tconstructor(${ctorParam}) {`, `\t\t\tsuper(target);`, `\t\t}`, `\t}`, `);`, ``);
94
- return lines.join('\n');
95
- }
96
- function assetSource(kind, tag) {
97
- switch (kind) {
98
- case 'css':
99
- // Document-level global stylesheet — the home of CSS custom properties.
100
- return [':root {', '\t/* design tokens (CSS custom properties) */', '}', ''].join('\n');
101
- case 'targetCss':
102
- return [`${tag} {`, '\tdisplay: block;', '}', ''].join('\n');
103
- case 'shadowCss':
104
- return [':host {', '\tdisplay: block;', '}', ''].join('\n');
105
- default:
106
- // html / targetHtml / shadowHtml
107
- return '<slot></slot>\n';
108
- }
109
- }
@@ -1,114 +0,0 @@
1
- // oxlint-disable no-await-in-loop
2
- /**
3
- Filesystem side of {@link scaffoldComponent} and {@link scaffoldProject}: writes the
4
- generated files under a root directory, creating nested folders as needed.
5
-
6
- Existing files are never overwritten silently: every target is checked before the
7
- first write, so a rerun in a populated directory reports the conflicts and touches
8
- nothing. `force` opts into overwriting.
9
-
10
- @module CLI/Scaffold
11
- */
12
- import { existsSync } from 'node:fs';
13
- import { mkdir, writeFile } from 'node:fs/promises';
14
- import path from 'node:path';
15
- import { scaffoldComponent } from './scaffold.js';
16
- import { scaffoldProject } from './create.js';
17
- /**
18
- The path of a target file as it appears from the current working directory.
19
- `mc create` writes a project into a folder of its own, so a path relative to the
20
- project root cannot be found from the directory the command ran in: this turns
21
- `src/mc/app.ts` under `mc-app/` into `mc-app/src/mc/app.ts`. Falls back to the
22
- absolute path when the target lies outside the current directory, so the log never
23
- prints `../../..` chains.
24
-
25
- @example
26
- ```ts
27
- // in a directory containing mc-app/src/mc/app.ts
28
- displayPath(path.resolve('mc-app', 'src/mc/app.ts'));
29
- displayPath('/tmp/mc-app/package.json');
30
- ```
31
- @param abs - Absolute path of the target file.
32
- @group CLI
33
- */
34
- export function displayPath(abs) {
35
- const rel = path.relative(process.cwd(), abs);
36
- return rel.startsWith('..') ? abs : rel;
37
- }
38
- /**
39
- Writes generated files under `root`, creating nested folders as needed, and reports
40
- the written paths relative to `root`.
41
-
42
- @example
43
- ```ts
44
- const written = await writeGenerated({ 'my/card.ts': 'source' }, 'src', false);
45
- console.log(written);
46
- ```
47
- @param files - Files to write, keyed by path relative to `root`.
48
- @param root - Directory to write under (`src` for a component, `mc-app` for a project).
49
- @param force - Overwrite files that already exist.
50
- @returns The written file paths, relative to `root`.
51
- @throws If any target file exists and `force` is false. Nothing is written in that case.
52
- */
53
- async function writeGenerated(files, root, force) {
54
- const targets = Object.entries(files).map(([rel, contents]) => ({
55
- rel,
56
- abs: path.resolve(root, rel),
57
- contents,
58
- }));
59
- if (!force) {
60
- const conflicts = targets
61
- .filter((target) => existsSync(target.abs))
62
- .map((t) => displayPath(t.abs));
63
- if (conflicts.length) {
64
- throw new Error(`refusing to overwrite ${conflicts.length} existing file(s):\n${conflicts
65
- .map((file) => `\t${file}`)
66
- .join('\n')}\n\nrerun with --force to overwrite`);
67
- }
68
- }
69
- const written = [];
70
- for (const { rel, abs, contents } of targets) {
71
- await mkdir(path.dirname(abs), { recursive: true });
72
- await writeFile(abs, contents, 'utf8');
73
- written.push(rel);
74
- }
75
- return written;
76
- }
77
- /**
78
- Scaffolds a component onto disk under `root`.
79
-
80
- @example
81
- ```ts
82
- const written = await writeScaffold('my-card', 'src', { assets: ['css'] });
83
- console.log(written);
84
- ```
85
- @param tag - Kebab-case component tag.
86
- @param root - Directory to scaffold under (e.g. `src`).
87
- @param options - Which companion files to emit, and whether to overwrite existing ones.
88
- @returns The written file paths, relative to `root`.
89
- @throws If a target file exists and `force` is false.
90
- */
91
- export async function writeScaffold(tag, root, options = {}) {
92
- const { files } = scaffoldComponent(tag, options);
93
- return writeGenerated(files, root, options.force ?? false);
94
- }
95
- /**
96
- Filesystem side of {@link scaffoldProject}: writes a complete dev environment for a
97
- namespace under a project root, creating nested folders as needed.
98
-
99
- @example
100
- ```ts
101
- const written = await writeProject('mc', 'mc-app');
102
- console.log(written);
103
- ```
104
- @param namespace - 1 to 4 lowercase letters.
105
- @param root - Directory to create the project in (e.g. `mc-app`).
106
- @param options - Whether to overwrite existing files.
107
- @returns The written file paths, relative to `root`.
108
- @throws If `namespace` is not 1 to 4 lowercase letters, or a target file exists and
109
- `force` is false.
110
- */
111
- export async function writeProject(namespace, root, options = {}) {
112
- const { files } = scaffoldProject(namespace);
113
- return writeGenerated(files, root, options.force ?? false);
114
- }