@twobirds/microcomponents 0.3.0 → 0.4.1

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 +91 -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 +287 -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 +66 -0
  17. package/dist/cli/writeScaffold.d.ts.map +1 -0
  18. package/dist/cli/writeScaffold.js +114 -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,287 @@
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
+
30
+ Commands:
31
+ new <tag> Scaffold a micro component (e.g. mc new my-card --target-html --target-css)
32
+ create Create a dev environment (e.g. mc create --namespace mc --dir mc-app)
33
+ clean Remove build artifacts (dist, public, build, coverage)
34
+ check Type-check every project (lib, cli, test, demo, node)
35
+
36
+ Companion assets:
37
+ --css document-level stylesheet (CSS custom properties / design tokens)
38
+ --html unvariant light-DOM template
39
+ --target-html template applied to target.innerHTML
40
+ --shadow-html template applied to shadowRoot.innerHTML
41
+ --target-css document-level styles scoped to the host tag
42
+ --shadow-css styles injected into the shadow root
43
+
44
+ Other options:
45
+ --js emit plain JavaScript instead of TypeScript
46
+ --dir <path> scaffold root (default: src)
47
+ --force overwrite files that already exist
48
+ --dry-run print what would be written; touch nothing on disk
49
+
50
+ Created paths are printed as they appear from the current directory. mc create writes
51
+ a whole project into a folder of its own (default: the namespace), so its files show
52
+ up under that folder, e.g. mc-app/src/mc/app.ts.
53
+ `;
54
+ // Map CLI flags to the runtime's camelCase asset descriptors.
55
+ const ASSET_FLAGS = {
56
+ css: 'css',
57
+ html: 'html',
58
+ 'target-html': 'targetHtml',
59
+ 'shadow-html': 'shadowHtml',
60
+ 'target-css': 'targetCss',
61
+ 'shadow-css': 'shadowCss',
62
+ };
63
+ /**
64
+ A scaffolded file as it appears from the current directory: the file `rel` under
65
+ `root`, resolved and made readable. `mc create` writes a project into a folder of
66
+ its own, so `src/mc/app.ts` under `mc-app` prints as `mc-app/src/mc/app.ts` — the
67
+ path the user can find with `ls`.
68
+
69
+ @example
70
+ ```ts
71
+ createdPath('mc-app', 'src/mc/app.ts');
72
+ createdPath('src', 'my/card.ts');
73
+ ```
74
+ @param root - Scaffold root the file is written under.
75
+ @param rel - Path of the file relative to `root`.
76
+ */
77
+ function createdPath(root, rel) {
78
+ return displayPath(path.resolve(root, rel));
79
+ }
80
+ /**
81
+ Spawns a command and resolves with its exit code.
82
+
83
+ @example
84
+ ```ts
85
+ const code = await spawnProcess('pnpm', ['run', 'clean']);
86
+ ```
87
+ @param command - Executable to spawn.
88
+ @param args - Arguments to pass.
89
+ @param cwd - Working directory (defaults to the current one).
90
+ */
91
+ function spawnProcess(command, args, cwd) {
92
+ return new Promise((resolve) => {
93
+ const child = spawn(command, args, { stdio: 'inherit', cwd });
94
+ child.on('exit', (code) => resolve(code ?? 1));
95
+ });
96
+ }
97
+ /**
98
+ Whether the package.json in the current directory declares `script` in its
99
+ "scripts". Consumers get this check so `mc clean`/`mc check` can fall back to
100
+ the CLI's own implementation when the project defines no such script.
101
+
102
+ @example
103
+ ```ts
104
+ const ok = await hasScript('clean');
105
+ ```
106
+ @param script - Script name to look up.
107
+ */
108
+ async function hasScript(script) {
109
+ try {
110
+ const pkg = JSON.parse(await readFile(path.join(process.cwd(), 'package.json'), 'utf8'));
111
+ return typeof pkg?.scripts?.[script] === 'string';
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
117
+ /** Build artifacts to remove, relative to the project root. */
118
+ const CLEAN_DIRS = ['dist', 'public', 'build', 'coverage'];
119
+ /**
120
+ Removes build artifacts in the current project.
121
+
122
+ @example
123
+ ```ts
124
+ const code = await cleanSelf();
125
+ ```
126
+ @param cwd - Project root (defaults to the current directory).
127
+ */
128
+ async function cleanSelf(cwd = process.cwd()) {
129
+ await Promise.all(CLEAN_DIRS.map(async (dir) => {
130
+ const abs = path.join(cwd, dir);
131
+ if (existsSync(abs)) {
132
+ await rm(abs, { recursive: true, force: true });
133
+ console.log(`removed ${dir}/`);
134
+ }
135
+ }));
136
+ return 0;
137
+ }
138
+ /**
139
+ Type-checks the current project with its own `tsconfig.json`.
140
+
141
+ @example
142
+ ```ts
143
+ const code = await checkSelf();
144
+ ```
145
+ @param cwd - Project root (defaults to the current directory).
146
+ */
147
+ async function checkSelf(cwd = process.cwd()) {
148
+ const config = path.join(cwd, 'tsconfig.json');
149
+ if (!existsSync(config)) {
150
+ console.error('mc check: no tsconfig.json in the current project');
151
+ return 1;
152
+ }
153
+ return spawnProcess('pnpm', ['exec', 'tsc', '--noEmit', '-p', 'tsconfig.json'], cwd);
154
+ }
155
+ /**
156
+ Runs a package.json script through pnpm and resolves with its exit code.
157
+
158
+ Thin wrappers like `clean` and `check` delegate here so the script — not the
159
+ CLI — remains the single source of truth for what each command does. If the
160
+ current project defines no such script, the CLI runs its own implementation
161
+ against the current project, so the command works in any consumer project.
162
+
163
+ @example
164
+ ```ts
165
+ const code = await runScript('check');
166
+ ```
167
+ */
168
+ export async function runScript(script) {
169
+ if (await hasScript(script)) {
170
+ return spawnProcess('pnpm', ['run', script]);
171
+ }
172
+ if (script === 'clean')
173
+ return cleanSelf();
174
+ if (script === 'check')
175
+ return checkSelf();
176
+ console.error(`mc: no "${script}" script in the current project`);
177
+ return 1;
178
+ }
179
+ /**
180
+ Parses `argv` and routes it to the matching command, returning a process exit
181
+ code. `run` is injectable so the script-wrapping commands can be tested without
182
+ spawning anything.
183
+
184
+ @example
185
+ ```ts
186
+ const code = await main(['clean'], async (script) => {
187
+ console.log(`would run ${script}`);
188
+ return 0;
189
+ });
190
+ ```
191
+ */
192
+ export async function main(argv, run = runScript) {
193
+ const { values, positionals } = parseArgs({
194
+ args: argv,
195
+ allowPositionals: true,
196
+ options: {
197
+ css: { type: 'boolean' },
198
+ html: { type: 'boolean' },
199
+ 'target-html': { type: 'boolean' },
200
+ 'shadow-html': { type: 'boolean' },
201
+ 'target-css': { type: 'boolean' },
202
+ 'shadow-css': { type: 'boolean' },
203
+ js: { type: 'boolean' },
204
+ dir: { type: 'string' },
205
+ namespace: { type: 'string' },
206
+ force: { type: 'boolean' },
207
+ 'dry-run': { type: 'boolean' },
208
+ help: { type: 'boolean', short: 'h' },
209
+ },
210
+ });
211
+ const command = positionals[0];
212
+ if (values.help || !command) {
213
+ console.log(USAGE);
214
+ return 0;
215
+ }
216
+ if (command === 'new') {
217
+ const tag = positionals[1];
218
+ if (!tag) {
219
+ console.error('mc new: missing component tag (e.g. mc new my-card)');
220
+ return 1;
221
+ }
222
+ const assets = [];
223
+ const flags = values;
224
+ for (const [flag, kind] of Object.entries(ASSET_FLAGS)) {
225
+ if (flags[flag])
226
+ assets.push(kind);
227
+ }
228
+ if (values['dry-run']) {
229
+ const { files } = scaffoldComponent(tag, { assets, js: values.js });
230
+ const root = values.dir ?? 'src';
231
+ for (const rel of Object.keys(files).sort())
232
+ console.log(`would create ${createdPath(root, rel)}`);
233
+ return 0;
234
+ }
235
+ const root = values.dir ?? 'src';
236
+ let written;
237
+ try {
238
+ written = await writeScaffold(tag, root, { assets, js: values.js, force: values.force });
239
+ }
240
+ catch (e) {
241
+ console.error(e.message);
242
+ return 1;
243
+ }
244
+ for (const rel of written)
245
+ console.log(`created ${createdPath(root, rel)}`);
246
+ return 0;
247
+ }
248
+ if (command === 'create') {
249
+ const namespace = values.namespace;
250
+ if (!namespace) {
251
+ console.error('mc create: missing --namespace (e.g. mc create --namespace mc)');
252
+ return 1;
253
+ }
254
+ const root = values.dir ?? namespace;
255
+ let files;
256
+ try {
257
+ ({ files } = scaffoldProject(namespace));
258
+ }
259
+ catch (e) {
260
+ console.error(e.message);
261
+ return 1;
262
+ }
263
+ if (values['dry-run']) {
264
+ for (const rel of Object.keys(files).sort())
265
+ console.log(`would create ${createdPath(root, rel)}`);
266
+ return 0;
267
+ }
268
+ let written;
269
+ try {
270
+ written = await writeProject(namespace, root, { force: values.force });
271
+ }
272
+ catch (e) {
273
+ console.error(e.message);
274
+ return 1;
275
+ }
276
+ for (const rel of written)
277
+ console.log(`created ${createdPath(root, rel)}`);
278
+ console.log(`\nproject "${namespace}" created in ${root}/ — run: cd ${root} && pnpm install && pnpm dev`);
279
+ return 0;
280
+ }
281
+ if (command === 'clean')
282
+ return run('clean');
283
+ if (command === 'check')
284
+ return run('check');
285
+ console.error(`mc: unknown command "${command}"\n\n${USAGE}`);
286
+ return 1;
287
+ }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ Executable entry point of the `mc` CLI (package.json "bin").
4
+
5
+ Deliberately thin: all parsing and routing lives in {@link main} so it can be
6
+ unit-tested without spawning a process. This module only feeds `process.argv`
7
+ in and pushes the resulting exit code out.
8
+
9
+ @module CLI/Index
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA;;;;;;;;EAQE"}
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ Executable entry point of the `mc` CLI (package.json "bin").
4
+
5
+ Deliberately thin: all parsing and routing lives in {@link main} so it can be
6
+ unit-tested without spawning a process. This module only feeds `process.argv`
7
+ in and pushes the resulting exit code out.
8
+
9
+ @module CLI/Index
10
+ */
11
+ import { main } from './dispatch.js';
12
+ process.exitCode = await main(process.argv.slice(2));
@@ -0,0 +1,68 @@
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
+ /**
18
+ Companion asset kinds, mirroring the camelCase descriptors the runtime loads.
19
+
20
+ - `css` — document-level global stylesheet (CSS custom properties / design tokens).
21
+ - `targetCss` — document-level styles scoped to the host tag.
22
+ - `shadowCss` — styles injected into the shadow root.
23
+ - `html` — unvariant light-DOM template.
24
+ - `targetHtml` — light-DOM template applied to `target.innerHTML`.
25
+ - `shadowHtml` — template applied to `shadowRoot.innerHTML`.
26
+ */
27
+ export type AssetKind = 'css' | 'targetCss' | 'shadowCss' | 'html' | 'targetHtml' | 'shadowHtml';
28
+ /** Options controlling which files a scaffold emits. */
29
+ export interface ScaffoldOptions {
30
+ /** Companion assets to emit and wire into `static assets`. */
31
+ assets?: AssetKind[];
32
+ /** Emit a plain `.js` file instead of `.ts` (rapid-prototyping path). */
33
+ js?: boolean;
34
+ }
35
+ /** Result of {@link scaffoldComponent}. */
36
+ export interface ScaffoldResult {
37
+ /** Files to write, keyed by path relative to the scaffold root (e.g. `my/card.ts`). */
38
+ files: Record<string, string>;
39
+ /** The PascalCase class name derived from the tag. */
40
+ className: string;
41
+ }
42
+ /**
43
+ Whether a string is a valid micro component tag name.
44
+
45
+ @example
46
+ ```ts
47
+ console.log(isValidComponentTag('my-card'));
48
+ console.log(isValidComponentTag('mycard'));
49
+ ```
50
+ */
51
+ export declare function isValidComponentTag(tag: string): boolean;
52
+ /**
53
+ Builds the files for a new micro component from its tag name.
54
+
55
+ @example
56
+ ```ts
57
+ const { className, files } = scaffoldComponent('my-card', {
58
+ assets: ['targetHtml', 'targetCss'],
59
+ });
60
+ console.log(className);
61
+ console.log(Object.keys(files));
62
+ ```
63
+ @param tag - Kebab-case component tag, e.g. `my-card`.
64
+ @param options - Which companion assets to emit and whether to use `.js`.
65
+ @throws If `tag` is not a valid custom element name.
66
+ */
67
+ export declare function scaffoldComponent(tag: string, options?: ScaffoldOptions): ScaffoldResult;
68
+ //# sourceMappingURL=scaffold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/cli/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;EAeE;AAIF;;;;;;;;;EASE;AACF,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,WAAW,GAAG,WAAW,GAAG,MAAM,GAAG,YAAY,GAAG,YAAY,CAAC;AAEjG,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC/B,8DAA8D;IAC9D,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,yEAAyE;IACzE,EAAE,CAAC,EAAE,OAAO,CAAC;CACb;AAED,2CAA2C;AAC3C,MAAM,WAAW,cAAc;IAC9B,uFAAuF;IACvF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;CAClB;AAMD;;;;;;;;EAQE;AACF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;;;;;;;;;;EAcE;AACF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,cAAc,CAuB5F"}
@@ -0,0 +1,109 @@
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
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ Filesystem side of {@link scaffoldComponent} and {@link scaffoldProject}: writes the
3
+ generated files under a root directory, creating nested folders as needed.
4
+
5
+ Existing files are never overwritten silently: every target is checked before the
6
+ first write, so a rerun in a populated directory reports the conflicts and touches
7
+ nothing. `force` opts into overwriting.
8
+
9
+ @module CLI/Scaffold
10
+ */
11
+ import { type ScaffoldOptions } from './scaffold.js';
12
+ /** Whether files that already exist may be overwritten. */
13
+ export interface WriteOptions {
14
+ force?: boolean;
15
+ }
16
+ /**
17
+ The path of a target file as it appears from the current working directory.
18
+ `mc create` writes a project into a folder of its own, so a path relative to the
19
+ project root cannot be found from the directory the command ran in: this turns
20
+ `src/mc/app.ts` under `mc-app/` into `mc-app/src/mc/app.ts`. Falls back to the
21
+ absolute path when the target lies outside the current directory, so the log never
22
+ prints `../../..` chains.
23
+
24
+ @example
25
+ ```ts
26
+ // in a directory containing mc-app/src/mc/app.ts
27
+ displayPath(path.resolve('mc-app', 'src/mc/app.ts'));
28
+ displayPath('/tmp/mc-app/package.json');
29
+ ```
30
+ @param abs - Absolute path of the target file.
31
+ @group CLI
32
+ */
33
+ export declare function displayPath(abs: string): string;
34
+ /**
35
+ Scaffolds a component onto disk under `root`.
36
+
37
+ @example
38
+ ```ts
39
+ const written = await writeScaffold('my-card', 'src', { assets: ['css'] });
40
+ console.log(written);
41
+ ```
42
+ @param tag - Kebab-case component tag.
43
+ @param root - Directory to scaffold under (e.g. `src`).
44
+ @param options - Which companion files to emit, and whether to overwrite existing ones.
45
+ @returns The written file paths, relative to `root`.
46
+ @throws If a target file exists and `force` is false.
47
+ */
48
+ export declare function writeScaffold(tag: string, root: string, options?: ScaffoldOptions & WriteOptions): Promise<string[]>;
49
+ /**
50
+ Filesystem side of {@link scaffoldProject}: writes a complete dev environment for a
51
+ namespace under a project root, creating nested folders as needed.
52
+
53
+ @example
54
+ ```ts
55
+ const written = await writeProject('mc', 'mc-app');
56
+ console.log(written);
57
+ ```
58
+ @param namespace - 1 to 4 lowercase letters.
59
+ @param root - Directory to create the project in (e.g. `mc-app`).
60
+ @param options - Whether to overwrite existing files.
61
+ @returns The written file paths, relative to `root`.
62
+ @throws If `namespace` is not 1 to 4 lowercase letters, or a target file exists and
63
+ `force` is false.
64
+ */
65
+ export declare function writeProject(namespace: string, root: string, options?: WriteOptions): Promise<string[]>;
66
+ //# sourceMappingURL=writeScaffold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writeScaffold.d.ts","sourceRoot":"","sources":["../../src/cli/writeScaffold.ts"],"names":[],"mappings":"AACA;;;;;;;;;EASE;AAKF,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGxE,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAE5B,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;EAgBE;AACF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG/C;AAmDD;;;;;;;;;;;;;EAaE;AACF,wBAAsB,aAAa,CAClC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,eAAe,GAAG,YAAiB,GAC1C,OAAO,CAAC,MAAM,EAAE,CAAC,CAGnB;AAED;;;;;;;;;;;;;;;EAeE;AACF,wBAAsB,YAAY,CACjC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,YAAiB,GACxB,OAAO,CAAC,MAAM,EAAE,CAAC,CAGnB"}
@@ -0,0 +1,114 @@
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
+ }