@wc-toolkit/cem-generator-cli 0.1.1 → 0.1.2

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 (3) hide show
  1. package/README.md +22 -4
  2. package/dist/cli.js +247 -26
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -11,7 +11,7 @@ pnpm add -w @wc-toolkit/cem-generator-cli
11
11
  ## Usage
12
12
 
13
13
  ```bash
14
- # Create a config and choose framework plugins interactively
14
+ # Create a config and choose parser plugins and integrations interactively
15
15
  cem init
16
16
 
17
17
  # Create a config with selected plugins without prompting
@@ -35,15 +35,33 @@ cem generate --no-inheritance
35
35
 
36
36
  ## Options
37
37
 
38
- `cem init` writes `cem-generator.config.mjs` and supports `--plugin <names...>`,
38
+ `cem init` writes `cem-generator.config.mjs`. In interactive mode it first asks
39
+ which parser plugins to use, then presents a multi-select list of integrations:
40
+ `react-wrappers`, `jsx-types`, `vuejs-types`, and `svelte-types`. The type
41
+ integrations use `./types` and strongly typed events by default. The command
42
+ also supports `--plugin <names...>`,
39
43
  `--yes`, `--force`, and `--config <path>`.
40
44
 
45
+ `cem generate` uses source-oriented include/exclude defaults when those flags
46
+ are omitted. Supplying either flag replaces its defaults. The TypeScript
47
+ configuration still controls which files enter the program.
48
+
49
+ When `cem init` installs dependencies, CLI mode installs
50
+ `@wc-toolkit/cem-generator-cli`, while code mode installs
51
+ `@wc-toolkit/cem-generator` and `tsx`. Selected parser and integration
52
+ packages are installed alongside the workflow package. The command prints the
53
+ `package.json` script to add and the package-manager command to run it.
54
+
55
+ In an interactive terminal, use the arrow keys to move through lists, Space to
56
+ toggle checkbox selections, and Enter to confirm. Non-interactive input falls
57
+ back to comma-separated selection numbers.
58
+
41
59
  | Option | Description | Default |
42
60
  |--------|-------------|---------|
43
61
  | `-c, --config <path>` | Path to tsconfig.json | `./tsconfig.json` |
44
62
  | `-o, --output <path>` | Output file path | `./custom-elements.json` |
45
- | `--include <patterns...>` | Glob patterns to include | - |
46
- | `--exclude <patterns...>` | Glob patterns to exclude | - |
63
+ | `--include <patterns...>` | Glob patterns to include | `src/**/*.{ts,tsx,js,jsx}` when `src/` exists |
64
+ | `--exclude <patterns...>` | Glob patterns to exclude | Tests, specs, stories, `dist/`, and `node_modules/` |
47
65
  | `--no-inheritance` | Disable inheritance materialization | - |
48
66
  | `--plugin <paths...>` | Additional plugin paths to load | - |
49
67
  | `--conflict-policy <policy>` | Detector conflict policy: `throw` \| `last-wins` | `last-wins` |
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import fs from "node:fs";
6
6
  import readline from "node:readline/promises";
7
7
  import { stdin as input, stdout as output } from "node:process";
8
8
  import { spawnSync } from "node:child_process";
9
+ import inquirer from "inquirer";
9
10
  import { generateCem, loadConfig, mergeConfig } from "@wc-toolkit/cem-generator";
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = path.dirname(__filename);
@@ -18,6 +19,40 @@ const PLUGIN_CHOICES = [
18
19
  { name: "solid", packageName: "@wc-toolkit/cem-generator-solid", factory: "solidPlugin" },
19
20
  { name: "svelte", packageName: "@wc-toolkit/cem-generator-svelte", factory: "sveltePlugin" },
20
21
  ];
22
+ const INTEGRATION_CHOICES = [
23
+ {
24
+ name: "react-wrappers",
25
+ packageName: "@wc-toolkit/react-wrappers",
26
+ factory: "reactWrapperGeneratorPlugin",
27
+ options: { stronglyTypedEvents: true },
28
+ },
29
+ {
30
+ name: "jsx-types",
31
+ packageName: "@wc-toolkit/jsx-types",
32
+ factory: "jsxTypesGeneratorPlugin",
33
+ options: { outdir: "./types", stronglyTypedEvents: true },
34
+ },
35
+ {
36
+ name: "vuejs-types",
37
+ packageName: "@wc-toolkit/vuejs-types",
38
+ factory: "vuejsTypesGeneratorPlugin",
39
+ options: { outdir: "./types", stronglyTypedEvents: true },
40
+ },
41
+ {
42
+ name: "svelte-types",
43
+ packageName: "@wc-toolkit/svelte-types",
44
+ factory: "svelteTypesGeneratorPlugin",
45
+ options: { outdir: "./types", stronglyTypedEvents: true },
46
+ },
47
+ ];
48
+ const DEFAULT_SOURCE_INCLUDE = ["src/**/*.{ts,tsx,js,jsx}"];
49
+ const DEFAULT_SOURCE_EXCLUDE = [
50
+ "**/*.test.*",
51
+ "**/*.spec.*",
52
+ "**/*.stories.*",
53
+ "**/dist/**",
54
+ "**/node_modules/**",
55
+ ];
21
56
  const program = new Command();
22
57
  program
23
58
  .name("cem")
@@ -47,8 +82,8 @@ program
47
82
  });
48
83
  const cliOptions = {
49
84
  tsConfigPath: path.resolve(options.tsconfig),
50
- include: options.include,
51
- exclude: options.exclude,
85
+ include: options.include ?? getDefaultInclude(cwd),
86
+ exclude: options.exclude ?? DEFAULT_SOURCE_EXCLUDE,
52
87
  sort: options.sort !== false,
53
88
  };
54
89
  if (options.deprecatedLast) {
@@ -111,28 +146,47 @@ program
111
146
  .option("-c, --config <path>", "Config file path", "./cem-generator.config.mjs")
112
147
  .option("--mode <mode>", "How to run the generator: cli | code")
113
148
  .option("--output <path>", "Generated code file path", "./generate-cem.ts")
114
- .option("--plugin <names...>", "Plugins to include (lit, fast, stencil, preact, vue, solid, svelte)")
149
+ .option("--plugin <names...>", "Parser plugins to include (lit, fast, stencil, preact, vue, solid, svelte)")
115
150
  .option("--install", "Install selected plugin packages")
116
151
  .option("--yes", "Create a config without prompting for plugins")
117
152
  .option("--force", "Overwrite existing output files")
118
153
  .action(async (options) => {
119
154
  try {
155
+ const cwd = process.cwd();
120
156
  let mode;
121
157
  let selectedNames;
158
+ let selectedIntegrationNames;
122
159
  if (options.yes) {
123
160
  mode = options.mode ? validateInitMode(options.mode) : "cli";
124
161
  selectedNames = [];
162
+ selectedIntegrationNames = [];
125
163
  }
126
164
  else if (options.mode) {
127
165
  mode = validateInitMode(options.mode);
128
- selectedNames = options.plugin ? validatePluginNames(options.plugin) : await promptForPlugins();
166
+ if (options.plugin) {
167
+ selectedNames = validatePluginNames(options.plugin);
168
+ selectedIntegrationNames = [];
169
+ }
170
+ else {
171
+ ({ selectedNames, selectedIntegrationNames } = await promptForPluginsAndIntegrations());
172
+ }
129
173
  }
130
174
  else {
131
- ({ mode, selectedNames } = options.plugin
175
+ ({ mode, selectedNames, selectedIntegrationNames } = options.plugin
132
176
  ? await promptForModeAndPlugins(options.plugin)
133
177
  : await promptForModeAndPlugins());
134
178
  }
135
179
  const selectedPlugins = PLUGIN_CHOICES.filter((plugin) => selectedNames.includes(plugin.name));
180
+ const selectedIntegrations = INTEGRATION_CHOICES.filter((integration) => selectedIntegrationNames.includes(integration.name));
181
+ const generatorPackage = mode === "cli"
182
+ ? "@wc-toolkit/cem-generator-cli"
183
+ : "@wc-toolkit/cem-generator";
184
+ const packagesToInstall = [
185
+ generatorPackage,
186
+ ...(mode === "code" ? ["tsx"] : []),
187
+ ...selectedPlugins.map((plugin) => plugin.packageName),
188
+ ...selectedIntegrations.map((integration) => integration.packageName),
189
+ ];
136
190
  const configPath = path.resolve(options.config);
137
191
  const outputPath = path.resolve(options.output);
138
192
  const targetPaths = mode === "cli" ? [configPath] : [configPath, outputPath];
@@ -140,18 +194,19 @@ program
140
194
  if (existingPath && !options.force) {
141
195
  throw new Error(`Output file already exists: ${existingPath}. Use --force to overwrite it.`);
142
196
  }
143
- const shouldInstall = options.install ?? (!options.yes && !options.mode && selectedPlugins.length > 0
197
+ const shouldInstall = options.install ?? (!options.yes && !options.mode
144
198
  ? await promptForInstall()
145
199
  : false);
146
- if (shouldInstall && selectedPlugins.length > 0) {
147
- installPluginDependencies(selectedPlugins.map((plugin) => plugin.packageName));
200
+ if (shouldInstall) {
201
+ installPluginDependencies(packagesToInstall);
148
202
  }
149
- fs.writeFileSync(configPath, createConfigSource(selectedPlugins), "utf-8");
203
+ fs.writeFileSync(configPath, createConfigSource(selectedPlugins, selectedIntegrations, getDefaultInclude(cwd), DEFAULT_SOURCE_EXCLUDE), "utf-8");
150
204
  console.log(`Created config file at ${configPath}`);
151
205
  if (mode === "code") {
152
206
  fs.writeFileSync(outputPath, createCodeSource(configPath, outputPath), "utf-8");
153
207
  console.log(`Created code file at ${outputPath}`);
154
208
  }
209
+ printNextSteps(mode, configPath, outputPath);
155
210
  }
156
211
  catch (error) {
157
212
  console.error("Error initializing project:", error instanceof Error ? error.message : error);
@@ -164,6 +219,9 @@ function validateInitMode(mode) {
164
219
  }
165
220
  return mode;
166
221
  }
222
+ function getDefaultInclude(cwd) {
223
+ return fs.existsSync(path.join(cwd, "src")) ? DEFAULT_SOURCE_INCLUDE : undefined;
224
+ }
167
225
  async function promptForMode(prompt) {
168
226
  const answer = await prompt.question("How will you run the generator? (1) CLI (2) code: ");
169
227
  if (answer.trim() === "1" || answer.trim().toLowerCase() === "cli")
@@ -174,7 +232,14 @@ async function promptForMode(prompt) {
174
232
  }
175
233
  async function promptForModeAndPlugins(pluginNames) {
176
234
  if (pluginNames) {
177
- return { mode: await promptForModeWithInput(), selectedNames: validatePluginNames(pluginNames) };
235
+ return {
236
+ mode: await promptForModeWithInput(),
237
+ selectedNames: validatePluginNames(pluginNames),
238
+ selectedIntegrationNames: [],
239
+ };
240
+ }
241
+ if (isInteractiveTerminal()) {
242
+ return promptForInteractiveSelections();
178
243
  }
179
244
  const prompt = readline.createInterface({ input, output });
180
245
  const lines = prompt[Symbol.asyncIterator]();
@@ -191,13 +256,37 @@ async function promptForModeAndPlugins(pluginNames) {
191
256
  const pluginsLine = await lines.next();
192
257
  if (pluginsLine.done)
193
258
  throw new Error("No plugin selection provided.");
194
- return { mode, selectedNames: parsePluginSelection(pluginsLine.value) };
259
+ const selectedNames = parsePluginSelection(pluginsLine.value);
260
+ console.log("Which project integrations should be included? (multi-select)");
261
+ INTEGRATION_CHOICES.forEach((integration, index) => console.log(` ${index + 1}. ${integration.name}`));
262
+ console.log("Enter numbers separated by commas, or press Enter for no integrations.");
263
+ output.write("Integrations: ");
264
+ const integrationsLine = await lines.next();
265
+ if (integrationsLine.done)
266
+ throw new Error("No integration selection provided.");
267
+ return {
268
+ mode,
269
+ selectedNames,
270
+ selectedIntegrationNames: parseIntegrationSelection(integrationsLine.value),
271
+ };
195
272
  }
196
273
  finally {
197
274
  prompt.close();
198
275
  }
199
276
  }
200
277
  async function promptForModeWithInput() {
278
+ if (isInteractiveTerminal()) {
279
+ const { mode } = await inquirer.prompt({
280
+ type: "select",
281
+ name: "mode",
282
+ message: "How will you run the generator?",
283
+ choices: [
284
+ { name: "CLI", value: "cli" },
285
+ { name: "Code", value: "code" },
286
+ ],
287
+ });
288
+ return mode;
289
+ }
201
290
  const prompt = readline.createInterface({ input, output });
202
291
  try {
203
292
  return await promptForMode(prompt);
@@ -207,6 +296,15 @@ async function promptForModeWithInput() {
207
296
  }
208
297
  }
209
298
  async function promptForInstall() {
299
+ if (isInteractiveTerminal()) {
300
+ const { install } = await inquirer.prompt({
301
+ type: "confirm",
302
+ name: "install",
303
+ message: "Install selected plugin packages now?",
304
+ default: true,
305
+ });
306
+ return install;
307
+ }
210
308
  const prompt = readline.createInterface({ input, output });
211
309
  try {
212
310
  const answer = await prompt.question("Install selected plugin packages now? (Y/n): ");
@@ -231,6 +329,35 @@ function installPluginDependencies(packages) {
231
329
  throw new Error(`Failed to install plugin packages with ${packageManager}.`);
232
330
  }
233
331
  }
332
+ function printNextSteps(mode, configPath, outputPath) {
333
+ console.log("\nNext steps:");
334
+ const packageManager = detectPackageManager();
335
+ if (mode === "cli") {
336
+ const defaultConfigPath = path.resolve(process.cwd(), "cem-generator.config.mjs");
337
+ const configArgument = configPath === defaultConfigPath ? "" : ` --config ${toCommandPath(configPath)}`;
338
+ printPackageScript(`cem generate${configArgument}`, packageManager);
339
+ }
340
+ else {
341
+ printPackageScript(`tsx ${toCommandPath(outputPath)}`, packageManager);
342
+ }
343
+ }
344
+ function printPackageScript(command, packageManager) {
345
+ const runCommand = packageManager === "npm"
346
+ ? "npm run cem"
347
+ : packageManager === "yarn"
348
+ ? "yarn cem"
349
+ : packageManager === "bun"
350
+ ? "bun run cem"
351
+ : "pnpm run cem";
352
+ console.log(" Add this script to package.json:");
353
+ console.log(` \"cem\": \"${command}\"`);
354
+ console.log(` Run it with: ${runCommand}`);
355
+ }
356
+ function toCommandPath(filePath) {
357
+ const relativePath = path.relative(process.cwd(), filePath).replaceAll(path.sep, "/");
358
+ const normalizedPath = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
359
+ return /\s/.test(normalizedPath) ? JSON.stringify(normalizedPath) : normalizedPath;
360
+ }
234
361
  function detectPackageManager() {
235
362
  if (fs.existsSync(path.resolve("pnpm-lock.yaml")))
236
363
  return "pnpm";
@@ -248,41 +375,135 @@ function validatePluginNames(names) {
248
375
  }
249
376
  return [...new Set(names)];
250
377
  }
251
- async function promptForPlugins(prompt) {
252
- console.log("Which framework plugins should be included?");
253
- PLUGIN_CHOICES.forEach((plugin, index) => console.log(` ${index + 1}. ${plugin.name}`));
254
- console.log("Enter numbers separated by commas, or press Enter for vanilla only.");
255
- const ownPrompt = prompt ? undefined : readline.createInterface({ input, output });
256
- const answer = await (prompt ?? ownPrompt).question("Plugins: ");
257
- ownPrompt?.close();
258
- return parsePluginSelection(answer);
378
+ function validateIntegrationNames(names) {
379
+ const validNames = new Set(INTEGRATION_CHOICES.map((integration) => integration.name));
380
+ const invalidNames = names.filter((name) => !validNames.has(name));
381
+ if (invalidNames.length > 0) {
382
+ throw new Error(`Unknown integration(s): ${invalidNames.join(", ")}. Choose from: ${[...validNames].join(", ")}`);
383
+ }
384
+ return [...new Set(names)];
385
+ }
386
+ async function promptForPluginsAndIntegrations() {
387
+ if (isInteractiveTerminal()) {
388
+ const { plugin, integrations } = await inquirer.prompt([
389
+ {
390
+ type: "select",
391
+ name: "plugin",
392
+ message: "Which framework plugins should be included?",
393
+ choices: PLUGIN_CHOICES.map((plugin) => ({ name: plugin.name, value: plugin.name })),
394
+ },
395
+ {
396
+ type: "checkbox",
397
+ name: "integrations",
398
+ message: "Which project integrations should be included?",
399
+ choices: INTEGRATION_CHOICES.map((integration) => ({
400
+ name: integration.name,
401
+ value: integration.name,
402
+ })),
403
+ },
404
+ ]);
405
+ return { selectedNames: [plugin], selectedIntegrationNames: integrations };
406
+ }
407
+ const prompt = readline.createInterface({ input, output });
408
+ const lines = prompt[Symbol.asyncIterator]();
409
+ try {
410
+ console.log("Which framework plugins should be included?");
411
+ PLUGIN_CHOICES.forEach((plugin, index) => console.log(` ${index + 1}. ${plugin.name}`));
412
+ console.log("Enter numbers separated by commas, or press Enter for vanilla only.");
413
+ output.write("Plugins: ");
414
+ const pluginsLine = await lines.next();
415
+ if (pluginsLine.done)
416
+ throw new Error("No plugin selection provided.");
417
+ console.log("Which project integrations should be included? (multi-select)");
418
+ INTEGRATION_CHOICES.forEach((integration, index) => console.log(` ${index + 1}. ${integration.name}`));
419
+ console.log("Enter numbers separated by commas, or press Enter for no integrations.");
420
+ output.write("Integrations: ");
421
+ const integrationsLine = await lines.next();
422
+ if (integrationsLine.done)
423
+ throw new Error("No integration selection provided.");
424
+ return {
425
+ selectedNames: parsePluginSelection(pluginsLine.value),
426
+ selectedIntegrationNames: parseIntegrationSelection(integrationsLine.value),
427
+ };
428
+ }
429
+ finally {
430
+ prompt.close();
431
+ }
432
+ }
433
+ async function promptForInteractiveSelections() {
434
+ const { mode, plugin, integrations } = await inquirer.prompt([
435
+ {
436
+ type: "select",
437
+ name: "mode",
438
+ message: "How will you run the generator?",
439
+ choices: [
440
+ { name: "CLI", value: "cli" },
441
+ { name: "Code", value: "code" },
442
+ ],
443
+ },
444
+ {
445
+ type: "select",
446
+ name: "plugin",
447
+ message: "Which framework plugins should be included?",
448
+ choices: PLUGIN_CHOICES.map((plugin) => ({ name: plugin.name, value: plugin.name })),
449
+ },
450
+ {
451
+ type: "checkbox",
452
+ name: "integrations",
453
+ message: "Which project integrations should be included?",
454
+ choices: INTEGRATION_CHOICES.map((integration) => ({
455
+ name: integration.name,
456
+ value: integration.name,
457
+ })),
458
+ },
459
+ ]);
460
+ return { mode, selectedNames: [plugin], selectedIntegrationNames: integrations };
461
+ }
462
+ function isInteractiveTerminal() {
463
+ return Boolean(input.isTTY && output.isTTY);
259
464
  }
260
465
  function parsePluginSelection(answer) {
466
+ return parseSelection(answer, PLUGIN_CHOICES, "plugin");
467
+ }
468
+ function parseIntegrationSelection(answer) {
469
+ return validateIntegrationNames(parseSelection(answer, INTEGRATION_CHOICES, "integration"));
470
+ }
471
+ function parseSelection(answer, choices, label) {
261
472
  const values = answer
262
473
  .split(",")
263
474
  .map((value) => value.trim())
264
475
  .filter(Boolean);
265
476
  const names = values.map((value) => {
266
477
  const index = Number(value) - 1;
267
- if (!Number.isInteger(index) || index < 0 || index >= PLUGIN_CHOICES.length) {
268
- throw new Error(`Invalid plugin selection: ${value}`);
478
+ if (!Number.isInteger(index) || index < 0 || index >= choices.length) {
479
+ throw new Error(`Invalid ${label} selection: ${value}`);
269
480
  }
270
- return PLUGIN_CHOICES[index].name;
481
+ return choices[index].name;
271
482
  });
272
- return validatePluginNames(names);
483
+ return names;
273
484
  }
274
- function createConfigSource(plugins) {
275
- const imports = plugins.map((plugin) => `import { ${plugin.factory} } from "${plugin.packageName}";`);
276
- const factories = plugins.map((plugin) => ` ${plugin.factory}(),`);
485
+ function createConfigSource(plugins, integrations = [], include, exclude = DEFAULT_SOURCE_EXCLUDE) {
486
+ const choices = [...plugins, ...integrations];
487
+ const imports = choices.map((plugin) => `import { ${plugin.factory} } from "${plugin.packageName}";`);
488
+ const factories = choices.map((plugin) => ` ${plugin.factory}(${getPluginOptionsSource(plugin)}),`);
277
489
  return [
278
490
  ...imports,
279
491
  imports.length ? "" : undefined,
280
492
  "export default {",
493
+ ...(include ? [` include: ${JSON.stringify(include)},`] : []),
494
+ ` exclude: ${JSON.stringify(exclude)},`,
281
495
  ...(factories.length ? [" plugins: [", ...factories, " ],"] : []),
282
496
  "};",
283
497
  "",
284
498
  ].filter((line) => line !== undefined).join("\n");
285
499
  }
500
+ function getPluginOptionsSource(plugin) {
501
+ if (!("options" in plugin) || !plugin.options)
502
+ return "";
503
+ return `{ ${Object.entries(plugin.options)
504
+ .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
505
+ .join(", ")} }`;
506
+ }
286
507
  function createCodeSource(configPath, outputPath) {
287
508
  let configImport = path.relative(path.dirname(outputPath), configPath).replaceAll(path.sep, "/");
288
509
  if (!configImport.startsWith("."))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wc-toolkit/cem-generator-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "CLI for generating Custom Elements Manifests.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,8 +34,9 @@
34
34
  "dependencies": {
35
35
  "commander": "^12.1.0",
36
36
  "custom-elements-manifest": "^2.1.0",
37
- "@wc-toolkit/cem-generator": "0.1.1",
38
- "@wc-toolkit/cem-generator-utils": "0.1.0"
37
+ "inquirer": "^14.2.2",
38
+ "@wc-toolkit/cem-generator": "0.1.2",
39
+ "@wc-toolkit/cem-generator-utils": "0.1.1"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@types/node": "^26.4.1",