@twobirds/microcomponents 0.4.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.
package/README.md CHANGED
@@ -58,40 +58,54 @@ A small command-line tool scaffolds new components so the tag-to-file convention
58
58
 
59
59
  ```bash
60
60
  pnpm run cli -- new my-card --target-html --target-css
61
- # created my/card.ts
62
- # created my/card.target.html
63
- # created my/card.target.css
61
+ # created src/my/card.ts
62
+ # created src/my/card.target.html
63
+ # created src/my/card.target.css
64
64
  ```
65
65
 
66
- `--dry-run` prints the plan without touching disk, so you can check the derived paths first:
66
+ Paths are printed as they appear from the current directory, so every line is a file you can open with `ls`. `--dry-run` prints the plan without touching disk, so you can check the derived paths first:
67
67
 
68
68
  ```bash
69
69
  pnpm run cli -- new my-card --target-html --target-css --dry-run
70
- # would create my/card.ts
71
- # would create my/card.target.html
72
- # would create my/card.target.css
70
+ # would create src/my/card.ts
71
+ # would create src/my/card.target.html
72
+ # would create src/my/card.target.css
73
73
  ```
74
74
 
75
75
  `mc create` scaffolds a complete dev environment for a namespace — a Vite dev server serving `src/` straight, a page shell that turns on `autoload(true)`, and a starter component with companion assets:
76
76
 
77
77
  ```bash
78
78
  pnpm run cli -- create --namespace mc --dir mc-app
79
- # created package.json
80
- # created src/index.html
81
- # created src/mc/app.ts
82
- # created src/mc/app.target.html
83
- # created src/mc/app.target.css
79
+ # created mc-app/package.json
80
+ # created mc-app/src/index.html
81
+ # created mc-app/src/mc/app.ts
82
+ # created mc-app/src/mc/app.target.html
83
+ # created mc-app/src/mc/app.target.css
84
84
  # ...
85
85
  # project "mc" created in mc-app/ — run: cd mc-app && pnpm install && pnpm dev
86
86
  ```
87
87
 
88
88
  The namespace (1 to 4 lowercase letters) becomes the `src/<namespace>/` folder and the first segment of every component tag, so `mc` is the home of tags like `<mc-app>`. Because the page shell is the autoload base, `import.meta.url` + the tag-to-path rule resolves `<mc-app>` to `src/mc/app.js` — the compiled starter. `--dir` defaults to the namespace, `--dry-run` prints the plan without touching disk.
89
89
 
90
+ The project is written into a folder of its own, so its files are reported under that folder (`mc-app/src/mc/app.ts`) — the path you can find from the directory you ran the command in, not relative to the project root.
91
+
92
+ Existing files are never overwritten silently: every target is checked before the first write, so a rerun in a populated directory lists the conflicts and touches nothing. `--force` opts into overwriting:
93
+
94
+ ```bash
95
+ pnpm run cli -- create --namespace mc
96
+ # refusing to overwrite 10 existing file(s):
97
+ # mc/package.json
98
+ # mc/src/mc/app.ts
99
+ # ...
100
+ #
101
+ # rerun with --force to overwrite
102
+ ```
103
+
90
104
  Once installed from npm the same command is available as `mc`:
91
105
 
92
106
  ```bash
93
- mc new my-card [--css] [--html] [--target-html] [--shadow-html] [--target-css] [--shadow-css] [--js] [--dir <path>] [--dry-run]
94
- mc create --namespace <ns> [--dir <path>] [--dry-run]
107
+ mc new my-card [--css] [--html] [--target-html] [--shadow-html] [--target-css] [--shadow-css] [--js] [--dir <path>] [--force] [--dry-run]
108
+ mc create --namespace <ns> [--dir <path>] [--force] [--dry-run]
95
109
  mc clean
96
110
  mc check
97
111
  ```
@@ -81,7 +81,7 @@ function packageJson(namespace) {
81
81
  typecheck: 'tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.vc.json',
82
82
  },
83
83
  dependencies: {
84
- '@twobirds/microcomponents': '^0.4.0',
84
+ '@twobirds/microcomponents': '^0.4.1',
85
85
  },
86
86
  devDependencies: {
87
87
  vite: '^8.1.5',
@@ -1 +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"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/cli/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;EAYE;AAsDF,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAqGzD;;;;;;;;;;;;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,CAwGnF"}
@@ -16,14 +16,14 @@ import { parseArgs } from 'node:util';
16
16
  import { readFile, rm } from 'node:fs/promises';
17
17
  import { existsSync } from 'node:fs';
18
18
  import path from 'node:path';
19
- import { writeScaffold, writeProject } from './writeScaffold.js';
19
+ import { writeScaffold, writeProject, displayPath } from './writeScaffold.js';
20
20
  import { scaffoldComponent } from './scaffold.js';
21
21
  import { scaffoldProject } from './create.js';
22
22
  const USAGE = `mc — Micro Components build tools
23
23
 
24
24
  Usage:
25
- mc new <tag> [--css] [--html] [--target-html] [--shadow-html] [--target-css] [--shadow-css] [--js] [--dir <path>] [--dry-run]
26
- mc create --namespace <ns> [--dir <path>] [--dry-run]
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
27
  mc clean
28
28
  mc check
29
29
 
@@ -44,7 +44,12 @@ Companion assets:
44
44
  Other options:
45
45
  --js emit plain JavaScript instead of TypeScript
46
46
  --dir <path> scaffold root (default: src)
47
+ --force overwrite files that already exist
47
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.
48
53
  `;
49
54
  // Map CLI flags to the runtime's camelCase asset descriptors.
50
55
  const ASSET_FLAGS = {
@@ -56,6 +61,23 @@ const ASSET_FLAGS = {
56
61
  'shadow-css': 'shadowCss',
57
62
  };
58
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
+ /**
59
81
  Spawns a command and resolves with its exit code.
60
82
 
61
83
  @example
@@ -181,6 +203,7 @@ export async function main(argv, run = runScript) {
181
203
  js: { type: 'boolean' },
182
204
  dir: { type: 'string' },
183
205
  namespace: { type: 'string' },
206
+ force: { type: 'boolean' },
184
207
  'dry-run': { type: 'boolean' },
185
208
  help: { type: 'boolean', short: 'h' },
186
209
  },
@@ -204,16 +227,22 @@ export async function main(argv, run = runScript) {
204
227
  }
205
228
  if (values['dry-run']) {
206
229
  const { files } = scaffoldComponent(tag, { assets, js: values.js });
207
- for (const file of Object.keys(files).sort())
208
- console.log(`would create ${file}`);
230
+ const root = values.dir ?? 'src';
231
+ for (const rel of Object.keys(files).sort())
232
+ console.log(`would create ${createdPath(root, rel)}`);
209
233
  return 0;
210
234
  }
211
- const written = await writeScaffold(tag, values.dir ?? 'src', {
212
- assets,
213
- js: values.js,
214
- });
215
- for (const file of written)
216
- console.log(`created ${file}`);
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)}`);
217
246
  return 0;
218
247
  }
219
248
  if (command === 'create') {
@@ -232,13 +261,20 @@ export async function main(argv, run = runScript) {
232
261
  return 1;
233
262
  }
234
263
  if (values['dry-run']) {
235
- for (const file of Object.keys(files).sort())
236
- console.log(`would create ${file}`);
264
+ for (const rel of Object.keys(files).sort())
265
+ console.log(`would create ${createdPath(root, rel)}`);
237
266
  return 0;
238
267
  }
239
- const written = await writeProject(namespace, root);
240
- for (const file of written)
241
- console.log(`created ${file}`);
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)}`);
242
278
  console.log(`\nproject "${namespace}" created in ${root}/ — run: cd ${root} && pnpm install && pnpm dev`);
243
279
  return 0;
244
280
  }
@@ -1,10 +1,36 @@
1
1
  /**
2
- Filesystem side of {@link scaffoldComponent}: writes the generated files under a
3
- root directory, creating nested folders as needed.
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.
4
8
 
5
9
  @module CLI/Scaffold
6
10
  */
7
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;
8
34
  /**
9
35
  Scaffolds a component onto disk under `root`.
10
36
 
@@ -15,10 +41,11 @@ console.log(written);
15
41
  ```
16
42
  @param tag - Kebab-case component tag.
17
43
  @param root - Directory to scaffold under (e.g. `src`).
18
- @param options - Which companion files to emit.
44
+ @param options - Which companion files to emit, and whether to overwrite existing ones.
19
45
  @returns The written file paths, relative to `root`.
46
+ @throws If a target file exists and `force` is false.
20
47
  */
21
- export declare function writeScaffold(tag: string, root: string, options?: ScaffoldOptions): Promise<string[]>;
48
+ export declare function writeScaffold(tag: string, root: string, options?: ScaffoldOptions & WriteOptions): Promise<string[]>;
22
49
  /**
23
50
  Filesystem side of {@link scaffoldProject}: writes a complete dev environment for a
24
51
  namespace under a project root, creating nested folders as needed.
@@ -30,8 +57,10 @@ console.log(written);
30
57
  ```
31
58
  @param namespace - 1 to 4 lowercase letters.
32
59
  @param root - Directory to create the project in (e.g. `mc-app`).
60
+ @param options - Whether to overwrite existing files.
33
61
  @returns The written file paths, relative to `root`.
34
- @throws If `namespace` is not 1 to 4 lowercase letters.
62
+ @throws If `namespace` is not 1 to 4 lowercase letters, or a target file exists and
63
+ `force` is false.
35
64
  */
36
- export declare function writeProject(namespace: string, root: string): Promise<string[]>;
65
+ export declare function writeProject(namespace: string, root: string, options?: WriteOptions): Promise<string[]>;
37
66
  //# sourceMappingURL=writeScaffold.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"writeScaffold.d.ts","sourceRoot":"","sources":["../../src/cli/writeScaffold.ts"],"names":[],"mappings":"AACA;;;;;EAKE;AAIF,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGxE;;;;;;;;;;;;EAYE;AACF,wBAAsB,aAAa,CAClC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,eAAoB,GAC3B,OAAO,CAAC,MAAM,EAAE,CAAC,CAYnB;AAED;;;;;;;;;;;;;EAaE;AACF,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAYrF"}
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"}
@@ -1,32 +1,73 @@
1
1
  // oxlint-disable no-await-in-loop
2
2
  /**
3
- Filesystem side of {@link scaffoldComponent}: writes the generated files under a
4
- root directory, creating nested folders as needed.
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.
5
9
 
6
10
  @module CLI/Scaffold
7
11
  */
12
+ import { existsSync } from 'node:fs';
8
13
  import { mkdir, writeFile } from 'node:fs/promises';
9
14
  import path from 'node:path';
10
15
  import { scaffoldComponent } from './scaffold.js';
11
16
  import { scaffoldProject } from './create.js';
12
17
  /**
13
- Scaffolds a component onto disk under `root`.
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.
14
24
 
15
25
  @example
16
26
  ```ts
17
- const written = await writeScaffold('my-card', 'src', { assets: ['css'] });
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);
18
45
  console.log(written);
19
46
  ```
20
- @param tag - Kebab-case component tag.
21
- @param root - Directory to scaffold under (e.g. `src`).
22
- @param options - Which companion files to emit.
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.
23
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.
24
52
  */
25
- export async function writeScaffold(tag, root, options = {}) {
26
- const { files } = scaffoldComponent(tag, options);
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
+ }
27
69
  const written = [];
28
- for (const [rel, contents] of Object.entries(files)) {
29
- const abs = path.join(root, rel);
70
+ for (const { rel, abs, contents } of targets) {
30
71
  await mkdir(path.dirname(abs), { recursive: true });
31
72
  await writeFile(abs, contents, 'utf8');
32
73
  written.push(rel);
@@ -34,6 +75,24 @@ export async function writeScaffold(tag, root, options = {}) {
34
75
  return written;
35
76
  }
36
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
+ /**
37
96
  Filesystem side of {@link scaffoldProject}: writes a complete dev environment for a
38
97
  namespace under a project root, creating nested folders as needed.
39
98
 
@@ -44,17 +103,12 @@ console.log(written);
44
103
  ```
45
104
  @param namespace - 1 to 4 lowercase letters.
46
105
  @param root - Directory to create the project in (e.g. `mc-app`).
106
+ @param options - Whether to overwrite existing files.
47
107
  @returns The written file paths, relative to `root`.
48
- @throws If `namespace` is not 1 to 4 lowercase letters.
108
+ @throws If `namespace` is not 1 to 4 lowercase letters, or a target file exists and
109
+ `force` is false.
49
110
  */
50
- export async function writeProject(namespace, root) {
111
+ export async function writeProject(namespace, root, options = {}) {
51
112
  const { files } = scaffoldProject(namespace);
52
- const written = [];
53
- for (const [rel, contents] of Object.entries(files)) {
54
- const abs = path.join(root, rel);
55
- await mkdir(path.dirname(abs), { recursive: true });
56
- await writeFile(abs, contents, 'utf8');
57
- written.push(rel);
58
- }
59
- return written;
113
+ return writeGenerated(files, root, options.force ?? false);
60
114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twobirds/microcomponents",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Micro Components",
5
5
  "keywords": [
6
6
  "PWA",