@stencil/cli 5.0.0-alpha.6 → 5.0.0-alpha.7

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/dist/index.d.mts CHANGED
@@ -5,7 +5,7 @@ import { LogLevel } from "@stencil/core/compiler";
5
5
  /**
6
6
  * Supported CLI task commands
7
7
  */
8
- type TaskCommand = 'build' | 'docs' | 'generate' | 'g' | 'help' | 'info' | 'migrate' | 'prerender' | 'serve' | 'telemetry' | 'test' | 'version';
8
+ type TaskCommand = 'build' | 'docs' | 'generate' | 'g' | 'help' | 'info' | 'init' | 'migrate' | 'prerender' | 'serve' | 'telemetry' | 'test' | 'version';
9
9
  //#endregion
10
10
  //#region src/config-flags.d.ts
11
11
  /**
@@ -150,4 +150,95 @@ declare const run: (init: d.CliInitOptions) => Promise<any>;
150
150
  */
151
151
  declare const runTask: (coreCompiler: CoreCompiler, config: d.Config, task: TaskCommand, sys: d.CompilerSystem, flags?: ConfigFlags) => Promise<void>;
152
152
  //#endregion
153
- export { BOOLEAN_CLI_FLAGS, type ConfigFlags, type TaskCommand, createConfigFlags, parseFlags, run, runTask };
153
+ //#region src/wizard/types.d.ts
154
+ /**
155
+ * Context passed to wizard steps at runtime.
156
+ */
157
+ interface WizardContext {
158
+ /** Absolute path to the project root directory. */
159
+ rootDir: string;
160
+ /** True when `stencil.config.ts` did not previously exist (fresh scaffold). */
161
+ isNewProject: boolean;
162
+ }
163
+ /**
164
+ * A single file a plugin can offer during `stencil generate`.
165
+ */
166
+ interface WizardFileTemplate {
167
+ /** Label shown in the generate prompt checkbox, e.g. `"Spec Test (.spec.tsx)"`. */
168
+ label: string;
169
+ /**
170
+ * File extension used to derive the filename and deduplicate contributions,
171
+ * e.g. `"spec.tsx"` or `"e2e.ts"`.
172
+ */
173
+ extension: string;
174
+ /**
175
+ * Subdirectory within the component directory where the file is placed.
176
+ * e.g. `'test'` to place alongside other test files. Omit for the component root.
177
+ */
178
+ subdirectory?: string;
179
+ /**
180
+ * Returns the file content. `className` is the PascalCase form of `tagName`.
181
+ */
182
+ template: (tagName: string, className: string) => string;
183
+ /** Pre-selected in the generate prompt. Defaults to `true`. */
184
+ selectedByDefault?: boolean;
185
+ }
186
+ /**
187
+ * Contribution a package can make to `stencil generate`.
188
+ */
189
+ interface WizardGenerateContribution {
190
+ /**
191
+ * Files this plugin can generate alongside the component.
192
+ * Each entry appears as a checkbox in the generate prompt.
193
+ * A single plugin may contribute multiple entries (e.g. a vitest setup
194
+ * with several project configs, each producing a differently-scoped test file).
195
+ */
196
+ fileTemplates?: ReadonlyArray<WizardFileTemplate>;
197
+ /**
198
+ * Additional style extensions this package supports (e.g. `['sass', 'scss']`
199
+ * from `@stencil/sass`). The first entry is used as the default.
200
+ */
201
+ styleExtensions?: ReadonlyArray<string>;
202
+ }
203
+ /**
204
+ * Contribution a package can make to `stencil init`.
205
+ *
206
+ * The plugin owns its entire setup: prompts, peer dep installs, config file
207
+ * generation, example files, package.json script updates, etc.
208
+ */
209
+ interface WizardInitContribution {
210
+ /** Stable identifier used to deduplicate across re-runs. */
211
+ id: string;
212
+ /** Human-readable name shown in the prompt list. */
213
+ displayName: string;
214
+ /** One-line description shown alongside the name. */
215
+ description: string;
216
+ /**
217
+ * Called by the CLI after packages are installed. The plugin is responsible
218
+ * for all further setup: additional prompts, peer dep installs, config file
219
+ * writes, example tests, `.gitignore` and `package.json` script updates, etc.
220
+ */
221
+ run: (context: WizardContext) => Promise<void>;
222
+ }
223
+ /**
224
+ * Interface a package exports to participate in `stencil init` and/or
225
+ * `stencil generate`.
226
+ *
227
+ * Declare the entry point in `package.json`:
228
+ * ```json
229
+ * { "stencil": { "wizard": "./dist/wizard.js" } }
230
+ * ```
231
+ *
232
+ * Export a named `wizard` constant from that module:
233
+ * ```ts
234
+ * export const wizard: StencilWizardPlugin = { ... };
235
+ * ```
236
+ */
237
+ interface StencilWizardPlugin {
238
+ /** Contributions to `stencil generate`. */
239
+ generate?: WizardGenerateContribution;
240
+ /** Contributions to `stencil init`. */
241
+ init?: WizardInitContribution;
242
+ }
243
+ //#endregion
244
+ export { BOOLEAN_CLI_FLAGS, type ConfigFlags, type StencilWizardPlugin, type TaskCommand, type WizardContext, type WizardFileTemplate, type WizardGenerateContribution, type WizardInitContribution, createConfigFlags, parseFlags, run, runTask };
package/dist/index.mjs CHANGED
@@ -1,7 +1,16 @@
1
1
  import { LOG_LEVELS } from "@stencil/core/compiler";
2
2
  import { buildError, catchError, hasError, isFunction, isOutputTargetDocs, isOutputTargetSsr, isOutputTargetWww, isString, normalizePath, readOnlyArrayHasStringMember, result, shouldIgnoreError, toCamelCase, validateComponentTag } from "@stencil/core/compiler/utils";
3
- import { dirname, isAbsolute, join, parse, relative } from "path";
3
+ import { dirname, isAbsolute, join, relative } from "path";
4
+ import * as p from "@clack/prompts";
5
+ import { cancel, isCancel, select } from "@clack/prompts";
4
6
  import ts from "typescript";
7
+ import { basename, dirname as dirname$1, join as join$1, parse, relative as relative$1, resolve } from "node:path";
8
+ import { getComponentBoilerplate, getStyleBoilerplate, getTemplatePath, toPascalCase } from "@stencil/templates";
9
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
10
+ import { pathToFileURL } from "node:url";
11
+ import { existsSync } from "node:fs";
12
+ import { installDependencies } from "nypm";
13
+ import { isCI } from "std-env";
5
14
  import { start } from "@stencil/dev-server";
6
15
  //#region src/config-flags.ts
7
16
  /**
@@ -440,7 +449,7 @@ const findConfig = async (opts) => {
440
449
  else configPath = normalizePath(configPath);
441
450
  else configPath = rootDir;
442
451
  const results = {
443
- configPath,
452
+ configPath: null,
444
453
  rootDir: normalizePath(cwd)
445
454
  };
446
455
  const stat = await sys.stat(configPath);
@@ -455,12 +464,15 @@ const findConfig = async (opts) => {
455
464
  if (stat.isFile) {
456
465
  results.configPath = configPath;
457
466
  results.rootDir = sys.platformPath.dirname(configPath);
458
- } else if (stat.isDirectory) for (const configName of ["stencil.config.ts", "stencil.config.js"]) {
459
- const testConfigFilePath = sys.platformPath.join(configPath, configName);
460
- if ((await sys.stat(testConfigFilePath)).isFile) {
461
- results.configPath = testConfigFilePath;
462
- results.rootDir = sys.platformPath.dirname(testConfigFilePath);
463
- break;
467
+ } else if (stat.isDirectory) {
468
+ results.rootDir = configPath;
469
+ for (const configName of ["stencil.config.ts", "stencil.config.js"]) {
470
+ const testConfigFilePath = sys.platformPath.join(configPath, configName);
471
+ if ((await sys.stat(testConfigFilePath)).isFile) {
472
+ results.configPath = testConfigFilePath;
473
+ results.rootDir = sys.platformPath.dirname(testConfigFilePath);
474
+ break;
475
+ }
464
476
  }
465
477
  }
466
478
  return result.ok(results);
@@ -904,6 +916,53 @@ const externalRuntimeRule = {
904
916
  }
905
917
  };
906
918
  //#endregion
919
+ //#region src/migrations/rules/extras-to-compat.ts
920
+ /**
921
+ * Migration rule: rename the top-level `extras` config key to `compat`.
922
+ *
923
+ * In v5, `extras` is replaced by `compat` (framework/bundler compatibility flags).
924
+ */
925
+ const extrasToCompatRule = {
926
+ id: "extras-to-compat",
927
+ name: "Extras → Compat Rename",
928
+ description: "Rename top-level 'extras' config key to 'compat'",
929
+ fromVersion: "4.x",
930
+ toVersion: "5.x",
931
+ detect(sourceFile) {
932
+ const matches = [];
933
+ const visit = (node) => {
934
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
935
+ if (node.name.text === "extras") {
936
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
937
+ matches.push({
938
+ node,
939
+ message: "'extras' has been renamed to 'compat'",
940
+ line: line + 1,
941
+ column: character + 1
942
+ });
943
+ }
944
+ }
945
+ ts.forEachChild(node, visit);
946
+ };
947
+ visit(sourceFile);
948
+ return matches;
949
+ },
950
+ transform(sourceFile, matches) {
951
+ if (matches.length === 0) return sourceFile.getFullText();
952
+ let text = sourceFile.getFullText();
953
+ const sorted = [...matches].sort((a, b) => b.node.getStart() - a.node.getStart());
954
+ for (const match of sorted) {
955
+ const node = match.node;
956
+ if (node.name.text === "extras") {
957
+ const keyStart = node.name.getStart();
958
+ const keyEnd = node.name.getEnd();
959
+ text = text.slice(0, keyStart) + "compat" + text.slice(keyEnd);
960
+ }
961
+ }
962
+ return text;
963
+ }
964
+ };
965
+ //#endregion
907
966
  //#region src/migrations/rules/form-associated.ts
908
967
  /**
909
968
  * Migration rule for formAssociated → @AttachInternals.
@@ -1769,6 +1828,64 @@ const rolldownConfigRule = {
1769
1828
  }
1770
1829
  };
1771
1830
  //#endregion
1831
+ //#region src/migrations/rules/service-worker-default.ts
1832
+ /**
1833
+ * Migration rule for `serviceWorker: null` / `serviceWorker: false` on `www` output targets.
1834
+ *
1835
+ * In Stencil v5, `serviceWorker` defaults to `null` (disabled). Explicit `null` or `false`
1836
+ * values on `www` output targets are now redundant and can be removed.
1837
+ */
1838
+ const serviceWorkerDefaultRule = {
1839
+ id: "service-worker-default",
1840
+ name: "Service Worker Default Cleanup",
1841
+ description: "Remove redundant 'serviceWorker: null' / 'serviceWorker: false' from www output targets - null is now the default",
1842
+ fromVersion: "4.x",
1843
+ toVersion: "5.x",
1844
+ detect(sourceFile) {
1845
+ const matches = [];
1846
+ const visit = (node) => {
1847
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "serviceWorker") {
1848
+ const init = node.initializer;
1849
+ if (init.kind === ts.SyntaxKind.NullKeyword || init.kind === ts.SyntaxKind.FalseKeyword) {
1850
+ const parent = node.parent;
1851
+ if (ts.isObjectLiteralExpression(parent)) {
1852
+ if (parent.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type" && ts.isStringLiteral(p.initializer) && p.initializer.text === "www")) {
1853
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1854
+ matches.push({
1855
+ node,
1856
+ message: `'serviceWorker: ${init.kind === ts.SyntaxKind.NullKeyword ? "null" : "false"}' is now the default on www output targets and can be removed`,
1857
+ line: line + 1,
1858
+ column: character + 1
1859
+ });
1860
+ }
1861
+ }
1862
+ }
1863
+ }
1864
+ ts.forEachChild(node, visit);
1865
+ };
1866
+ visit(sourceFile);
1867
+ return matches;
1868
+ },
1869
+ transform(sourceFile, matches) {
1870
+ if (matches.length === 0) return sourceFile.getFullText();
1871
+ let text = sourceFile.getFullText();
1872
+ for (const match of [...matches].reverse()) {
1873
+ const node = match.node;
1874
+ let start = node.getFullStart();
1875
+ const end = node.getEnd();
1876
+ let removeEnd = end;
1877
+ const trailingComma = text.slice(end).match(/^\s*,/);
1878
+ if (trailingComma) removeEnd = end + trailingComma[0].length;
1879
+ else {
1880
+ const leadingComma = text.slice(0, start).match(/,\s*$/);
1881
+ if (leadingComma) start -= leadingComma[0].length;
1882
+ }
1883
+ text = text.slice(0, start) + text.slice(removeEnd);
1884
+ }
1885
+ return text;
1886
+ }
1887
+ };
1888
+ //#endregion
1772
1889
  //#region src/migrations/index.ts
1773
1890
  /**
1774
1891
  * Build a map of local import names to their original names from @stencil/core.
@@ -1814,9 +1931,11 @@ const migrationRules = [
1814
1931
  devModeRule,
1815
1932
  globalStyleInjectRule,
1816
1933
  lightDomPatchesRule,
1934
+ extrasToCompatRule,
1817
1935
  externalRuntimeRule,
1818
1936
  hashFileNamesRule,
1819
- rolldownConfigRule
1937
+ rolldownConfigRule,
1938
+ serviceWorkerDefaultRule
1820
1939
  ];
1821
1940
  /**
1822
1941
  * Get all migration rules for a specific version upgrade.
@@ -2126,7 +2245,7 @@ function uuidv4() {
2126
2245
  * @param path the path on the file system to read and parse
2127
2246
  * @returns the parsed JSON
2128
2247
  */
2129
- async function readJson(sys, path) {
2248
+ async function readJson$1(sys, path) {
2130
2249
  const file = await sys.readFile(path);
2131
2250
  return file ? JSON.parse(file) : null;
2132
2251
  }
@@ -2148,8 +2267,8 @@ function hasVerbose(flags) {
2148
2267
  }
2149
2268
  //#endregion
2150
2269
  //#region src/ionic-config.ts
2151
- const isTest$1 = () => process.env.JEST_WORKER_ID !== void 0;
2152
- const defaultConfig = (sys) => sys.resolvePath(`${sys.homeDir()}/.ionic/${isTest$1() ? "tmp-config.json" : "config.json"}`);
2270
+ const isTest = () => process.env.JEST_WORKER_ID !== void 0;
2271
+ const defaultConfig = (sys) => sys.resolvePath(`${sys.homeDir()}/.ionic/${isTest() ? "tmp-config.json" : "config.json"}`);
2153
2272
  const defaultConfigDirectory = (sys) => sys.resolvePath(`${sys.homeDir()}/.ionic`);
2154
2273
  /**
2155
2274
  * Reads an Ionic configuration file from disk, parses it, and performs any necessary corrections to it if certain
@@ -2158,7 +2277,7 @@ const defaultConfigDirectory = (sys) => sys.resolvePath(`${sys.homeDir()}/.ionic
2158
2277
  * @returns the config read from disk that has been potentially been updated
2159
2278
  */
2160
2279
  async function readConfig(sys) {
2161
- let config = await readJson(sys, defaultConfig(sys));
2280
+ let config = await readJson$1(sys, defaultConfig(sys));
2162
2281
  if (!config) {
2163
2282
  config = {
2164
2283
  "tokens.telemetry": uuidv4(),
@@ -2396,7 +2515,7 @@ async function getInstalledPackages(sys, flags) {
2396
2515
  const yarn = isUsingYarn(sys);
2397
2516
  try {
2398
2517
  const appRootDir = sys.getCurrentDirectory();
2399
- const packageJson = await tryFn(readJson, sys, sys.resolvePath(appRootDir + "/package.json"));
2518
+ const packageJson = await tryFn(readJson$1, sys, sys.resolvePath(appRootDir + "/package.json"));
2400
2519
  if (!packageJson) return {
2401
2520
  packages,
2402
2521
  packagesNoVersions
@@ -2431,7 +2550,7 @@ async function getInstalledPackages(sys, flags) {
2431
2550
  */
2432
2551
  async function npmPackages(sys, ionicPackages) {
2433
2552
  const appRootDir = sys.getCurrentDirectory();
2434
- const packageLockJson = await tryFn(readJson, sys, sys.resolvePath(appRootDir + "/package-lock.json"));
2553
+ const packageLockJson = await tryFn(readJson$1, sys, sys.resolvePath(appRootDir + "/package-lock.json"));
2435
2554
  return ionicPackages.map(([k, v]) => {
2436
2555
  let version = packageLockJson?.dependencies[k]?.version ?? packageLockJson?.devDependencies[k]?.version ?? v;
2437
2556
  version = version.includes("file:") ? sanitizeDeclaredVersion(v) : version;
@@ -2653,31 +2772,28 @@ async function promptForMigration(config, migrationResult, context) {
2653
2772
  logger.info("");
2654
2773
  if (context === "pre-build") logger.info("Your config contains deprecated options that must be migrated before building.");
2655
2774
  else logger.info("These migrations may help resolve the build errors above.");
2656
- const prompt = (await import("prompts")).default;
2657
- const response = await prompt({
2658
- name: "action",
2659
- type: "select",
2775
+ const action = await select({
2660
2776
  message: "What would you like to do?",
2661
- choices: [
2777
+ options: [
2662
2778
  {
2663
- title: "Run migration",
2664
2779
  value: "run",
2665
- description: "Apply migrations and re-run build"
2780
+ label: "Run migration",
2781
+ hint: "Apply migrations and re-run build"
2666
2782
  },
2667
2783
  {
2668
- title: "Dry run",
2669
2784
  value: "dry-run",
2670
- description: "Preview changes without modifying files"
2785
+ label: "Dry run",
2786
+ hint: "Preview changes without modifying files"
2671
2787
  },
2672
2788
  {
2673
- title: "Exit",
2674
2789
  value: "exit",
2675
- description: "Exit without making changes"
2790
+ label: "Exit",
2791
+ hint: "Exit without making changes"
2676
2792
  }
2677
2793
  ]
2678
2794
  });
2679
- if (response.action === void 0) return "exit";
2680
- return response.action;
2795
+ if (isCancel(action)) return "exit";
2796
+ return action;
2681
2797
  }
2682
2798
  //#endregion
2683
2799
  //#region src/task-docs.ts
@@ -2691,277 +2807,197 @@ const taskDocs = async (coreCompiler, config) => {
2691
2807
  await compiler.destroy();
2692
2808
  };
2693
2809
  //#endregion
2694
- //#region src/task-generate.ts
2810
+ //#region src/wizard/clack.ts
2695
2811
  /**
2696
- * Task to generate component boilerplate and write it to disk. This task can
2697
- * cause the program to exit with an error under various circumstances, such as
2698
- * being called in an inappropriate place, being asked to overwrite files that
2699
- * already exist, etc.
2812
+ * Exits cleanly if the user cancelled a prompt (Ctrl+C).
2813
+ * Narrows the type from `T | symbol` to `T` for callers.
2700
2814
  *
2701
- * @param config the user-supplied config, which we need here to access `.sys`.
2702
- * @param flags the CLI flags (owned by CLI, not part of core config)
2703
- * @returns a void promise
2815
+ * @param value - Return value from a `@clack/prompts` prompt call.
2704
2816
  */
2817
+ function cancelIfAborted(value) {
2818
+ if (isCancel(value)) {
2819
+ cancel("Cancelled.");
2820
+ process.exit(0);
2821
+ }
2822
+ }
2823
+ //#endregion
2824
+ //#region src/wizard/discover.ts
2825
+ function toStringRecord(val) {
2826
+ return val !== null && typeof val === "object" ? val : {};
2827
+ }
2828
+ async function readJson(filePath) {
2829
+ try {
2830
+ return JSON.parse(await readFile(filePath, "utf8"));
2831
+ } catch {
2832
+ return null;
2833
+ }
2834
+ }
2835
+ async function loadOne(rootDir, packageName, loader) {
2836
+ const wizardEntry = ((await readJson(join$1(rootDir, "node_modules", packageName, "package.json")))?.stencil)?.wizard;
2837
+ if (!wizardEntry) return null;
2838
+ const wizardPath = join$1(rootDir, "node_modules", packageName, wizardEntry);
2839
+ let mod;
2840
+ try {
2841
+ mod = await loader(pathToFileURL(wizardPath).href);
2842
+ } catch {
2843
+ console.warn(`[stencil] ${packageName} declares stencil.wizard but the module failed to load: ${wizardPath}`);
2844
+ return null;
2845
+ }
2846
+ const plugin = mod.wizard;
2847
+ if (!plugin || typeof plugin !== "object") {
2848
+ console.warn(`[stencil] ${packageName} declares stencil.wizard but does not export a 'wizard' object`);
2849
+ return null;
2850
+ }
2851
+ return {
2852
+ packageName,
2853
+ plugin
2854
+ };
2855
+ }
2856
+ /**
2857
+ * Scans the project's declared dependencies for packages that expose a
2858
+ * `stencil.wizard` entry in their `package.json` and dynamically imports
2859
+ * each matching module.
2860
+ *
2861
+ * @param rootDir - Absolute path to the project root (where `package.json` lives).
2862
+ * @param loader - Module loader; injectable for testing. Defaults to `import()`.
2863
+ * @returns Array of successfully loaded plugins, in dependency declaration order.
2864
+ */
2865
+ async function discoverPlugins(rootDir, loader = (url) => import(url)) {
2866
+ const pkg = await readJson(join$1(rootDir, "package.json"));
2867
+ if (!pkg) return [];
2868
+ const depNames = [...new Set([...Object.keys(toStringRecord(pkg.dependencies)), ...Object.keys(toStringRecord(pkg.devDependencies))])];
2869
+ const plugins = (await Promise.allSettled(depNames.map((name) => loadOne(rootDir, name, loader)))).filter((r) => r.status === "fulfilled" && r.value !== null).map((r) => r.value);
2870
+ const devPath = process.env.STENCIL_WIZARD_DEV;
2871
+ if (devPath) {
2872
+ const devPlugin = await loadDevPlugin(resolve(rootDir, devPath), loader);
2873
+ if (devPlugin) plugins.unshift(devPlugin);
2874
+ }
2875
+ return plugins;
2876
+ }
2877
+ async function findDevPackageName(wizardPath) {
2878
+ const dir = dirname$1(wizardPath);
2879
+ for (const candidate of [dir, resolve(dir, "..")]) {
2880
+ const pkg = await readJson(join$1(candidate, "package.json"));
2881
+ if (typeof pkg?.name === "string") return pkg.name;
2882
+ }
2883
+ return basename(dir);
2884
+ }
2885
+ async function loadDevPlugin(wizardPath, loader) {
2886
+ const packageName = await findDevPackageName(wizardPath);
2887
+ let mod;
2888
+ try {
2889
+ mod = await loader(pathToFileURL(wizardPath).href);
2890
+ } catch {
2891
+ console.warn(`[stencil] STENCIL_WIZARD_DEV: failed to load ${wizardPath}`);
2892
+ return null;
2893
+ }
2894
+ const plugin = mod.wizard;
2895
+ if (!plugin || typeof plugin !== "object") {
2896
+ console.warn(`[stencil] STENCIL_WIZARD_DEV: ${wizardPath} does not export a 'wizard' object`);
2897
+ return null;
2898
+ }
2899
+ return {
2900
+ packageName,
2901
+ plugin
2902
+ };
2903
+ }
2904
+ //#endregion
2905
+ //#region src/task-generate.ts
2705
2906
  const taskGenerate = async (config, flags) => {
2706
2907
  if (!config.configPath) {
2707
2908
  config.logger.error("Please run this command in your root directory (i. e. the one containing stencil.config.ts).");
2708
2909
  return config.sys.exit(1);
2709
2910
  }
2710
- const absoluteSrcDir = config.srcDir;
2711
- if (!absoluteSrcDir) {
2911
+ const srcDir = config.srcDir;
2912
+ if (!srcDir) {
2712
2913
  config.logger.error(`Stencil's srcDir was not specified.`);
2713
2914
  return config.sys.exit(1);
2714
2915
  }
2715
- const { prompt } = await import("prompts");
2716
- const input = flags.unknownArgs.find((arg) => !arg.startsWith("-")) || (await prompt({
2717
- name: "tagName",
2718
- type: "text",
2719
- message: "Component tag name (dash-case):"
2720
- })).tagName;
2721
- if (void 0 === input) return;
2916
+ const generateContribs = (await discoverPlugins(config.rootDir)).flatMap((d) => d.plugin.generate ? [d.plugin.generate] : []);
2917
+ p.intro("stencil generate");
2918
+ const rawInput = flags.unknownArgs.find((arg) => !arg.startsWith("-"));
2919
+ let input;
2920
+ if (rawInput) input = rawInput;
2921
+ else {
2922
+ const tagName = await p.text({
2923
+ message: "Component tag name (dash-case):",
2924
+ validate: (value) => validateComponentTag(value ?? "")
2925
+ });
2926
+ cancelIfAborted(tagName);
2927
+ input = tagName;
2928
+ }
2722
2929
  const { dir, base: componentName } = parse(input);
2723
2930
  const tagError = validateComponentTag(componentName);
2724
2931
  if (tagError) {
2725
2932
  config.logger.error(tagError);
2726
2933
  return config.sys.exit(1);
2727
2934
  }
2728
- let cssExtension = "css";
2729
- if (config.plugins?.find((plugin) => plugin.name === "sass")) cssExtension = await chooseSassExtension();
2730
- else if (config.plugins?.find((plugin) => plugin.name === "less")) cssExtension = "less";
2731
- const filesToGenerateExt = await chooseFilesToGenerate(cssExtension);
2732
- if (!filesToGenerateExt) return;
2733
- const extensionsToGenerate = ["tsx", ...filesToGenerateExt];
2734
- const testFolder = extensionsToGenerate.some(isTest) ? "test" : "";
2735
- const outDir = join(absoluteSrcDir, "components", dir, componentName);
2736
- await config.sys.createDir(normalizePath(join(outDir, testFolder)), { recursive: true });
2737
- const filesToGenerate = extensionsToGenerate.map((extension) => ({
2738
- extension,
2739
- path: getFilepathForFile(outDir, componentName, extension)
2740
- }));
2741
- await checkForOverwrite(filesToGenerate, config);
2742
- const writtenFiles = await Promise.all(filesToGenerate.map((file) => getBoilerplateAndWriteFile(config, componentName, extensionsToGenerate.includes("css") || extensionsToGenerate.includes("sass") || extensionsToGenerate.includes("scss") || extensionsToGenerate.includes("less"), file, cssExtension))).catch((error) => config.logger.error(error));
2743
- if (!writtenFiles) return config.sys.exit(1);
2744
- console.log();
2745
- console.log(`${config.logger.gray("$")} stencil generate ${input}`);
2746
- console.log();
2747
- console.log(config.logger.bold("The following files have been generated:"));
2748
- const absoluteRootDir = config.rootDir;
2749
- writtenFiles.map((file) => console.log(` - ${relative(absoluteRootDir, file)}`));
2750
- };
2751
- /**
2752
- * Show a checkbox prompt to select the files to be generated.
2753
- *
2754
- * @param cssExtension the extension of the CSS file to be generated
2755
- * @returns a read-only array of `GeneratableExtension`, the extensions that the user has decided
2756
- * to generate
2757
- */
2758
- const chooseFilesToGenerate = async (cssExtension) => {
2759
- const { prompt } = await import("prompts");
2760
- return (await prompt({
2761
- name: "filesToGenerate",
2762
- type: "multiselect",
2763
- message: "Which additional files do you want to generate?",
2764
- choices: [
2765
- {
2766
- value: cssExtension,
2767
- title: `Stylesheet (.${cssExtension})`,
2768
- selected: true
2769
- },
2770
- {
2771
- value: "spec.tsx",
2772
- title: "Spec Test (.spec.tsx)",
2773
- selected: true
2774
- },
2775
- {
2776
- value: "e2e.ts",
2777
- title: "E2E Test (.e2e.ts)",
2778
- selected: true
2779
- }
2780
- ]
2781
- })).filesToGenerate;
2782
- };
2783
- const chooseSassExtension = async () => {
2784
- const { prompt } = await import("prompts");
2785
- return (await prompt({
2786
- name: "sassFormat",
2787
- type: "select",
2788
- message: "Which Sass format would you like to use? (More info: https://sass-lang.com/documentation/syntax/#the-indented-syntax)",
2789
- choices: [{
2790
- value: "sass",
2791
- title: `*.sass Format`,
2792
- selected: true
2793
- }, {
2794
- value: "scss",
2795
- title: "*.scss Format"
2796
- }]
2797
- })).sassFormat;
2798
- };
2799
- /**
2800
- * Get a filepath for a file we want to generate!
2801
- *
2802
- * The filepath for a given file depends on the path, the user-supplied
2803
- * component name, the extension, and whether we're inside of a test directory.
2804
- *
2805
- * @param filePath path to where we're going to generate the component
2806
- * @param componentName the user-supplied name for the generated component
2807
- * @param extension the file extension
2808
- * @returns the full filepath to the component (with a possible `test` directory
2809
- * added)
2810
- */
2811
- const getFilepathForFile = (filePath, componentName, extension) => isTest(extension) ? normalizePath(join(filePath, "test", `${componentName}.${extension}`)) : normalizePath(join(filePath, `${componentName}.${extension}`));
2812
- /**
2813
- * Get the boilerplate for a file and write it to disk
2814
- *
2815
- * @param config the current config, needed for file operations
2816
- * @param componentName the component name (user-supplied)
2817
- * @param withCss are we generating CSS?
2818
- * @param file the file we want to write
2819
- * @param styleExtension extension used for styles
2820
- * @returns a `Promise<string>` which holds the full filepath we've written to,
2821
- * used to print out a little summary of our activity to the user.
2822
- */
2823
- const getBoilerplateAndWriteFile = async (config, componentName, withCss, file, styleExtension) => {
2824
- const boilerplate = getBoilerplateByExtension(componentName, file.extension, withCss, styleExtension);
2825
- await config.sys.writeFile(normalizePath(file.path), boilerplate);
2826
- return file.path;
2827
- };
2828
- /**
2829
- * Check to see if any of the files we plan to write already exist and would
2830
- * therefore be overwritten if we proceed, because we'd like to not overwrite
2831
- * people's code!
2832
- *
2833
- * This function will check all the filepaths and if it finds any files log an
2834
- * error and exit with an error code. If it doesn't find anything it will just
2835
- * peacefully return `Promise<void>`.
2836
- *
2837
- * @param files the files we want to check
2838
- * @param config the Config object, used here to get access to `sys.readFile`
2839
- */
2840
- const checkForOverwrite = async (files, config) => {
2841
- const alreadyPresent = [];
2842
- await Promise.all(files.map(async ({ path }) => {
2843
- if (await config.sys.readFile(path) !== void 0) alreadyPresent.push(path);
2844
- }));
2845
- if (alreadyPresent.length > 0) {
2846
- config.logger.error("Generating code would overwrite the following files:", ...alreadyPresent.map((path) => " " + normalizePath(path)));
2847
- await config.sys.exit(1);
2935
+ const styleOptions = [
2936
+ {
2937
+ value: "css",
2938
+ label: "CSS (.css)"
2939
+ },
2940
+ ...[...new Set(generateContribs.flatMap((c) => c.styleExtensions ?? []))].map((ext) => ({
2941
+ value: ext,
2942
+ label: `${ext.toUpperCase()} (.${ext})`
2943
+ })),
2944
+ {
2945
+ value: "",
2946
+ label: "None"
2947
+ }
2948
+ ];
2949
+ const stylePick = await p.select({
2950
+ message: "Stylesheet format:",
2951
+ options: styleOptions
2952
+ });
2953
+ cancelIfAborted(stylePick);
2954
+ const styleExtension = stylePick || void 0;
2955
+ const allFileTemplates = generateContribs.flatMap((c) => c.fileTemplates ?? []);
2956
+ let pickedExtensions = [];
2957
+ if (allFileTemplates.length > 0) {
2958
+ const filePick = await p.multiselect({
2959
+ message: "Additional files:",
2960
+ options: allFileTemplates.map((ft) => ({
2961
+ value: ft.extension,
2962
+ label: ft.label
2963
+ })),
2964
+ initialValues: allFileTemplates.filter((ft) => ft.selectedByDefault !== false).map((ft) => ft.extension),
2965
+ required: false
2966
+ });
2967
+ cancelIfAborted(filePick);
2968
+ pickedExtensions = filePick;
2848
2969
  }
2849
- };
2850
- /**
2851
- * Check if an extension is for a test
2852
- *
2853
- * @param extension the extension we want to check
2854
- * @returns a boolean indicating whether or not its a test
2855
- */
2856
- const isTest = (extension) => {
2857
- return extension === "e2e.ts" || extension === "spec.tsx";
2858
- };
2859
- /**
2860
- * Get the boilerplate for a file by its extension.
2861
- *
2862
- * @param tagName the name of the component we're generating
2863
- * @param extension the file extension we want boilerplate for (.css, tsx, etc)
2864
- * @param withCss a boolean indicating whether we're generating a CSS file
2865
- * @param styleExtension extension used for styles
2866
- * @returns a string container the file boilerplate for the supplied extension
2867
- */
2868
- const getBoilerplateByExtension = (tagName, extension, withCss, styleExtension) => {
2869
- switch (extension) {
2870
- case "tsx": return getComponentBoilerplate(tagName, withCss, styleExtension);
2871
- case "css":
2872
- case "less":
2873
- case "sass":
2874
- case "scss": return getStyleUrlBoilerplate(styleExtension);
2875
- case "spec.tsx": return getSpecTestBoilerplate(tagName);
2876
- case "e2e.ts": return getE2eTestBoilerplate(tagName);
2877
- default: throw new Error(`Unkown extension "${extension}".`);
2970
+ const outDir = join$1(srcDir, "components", dir, componentName);
2971
+ const className = toPascalCase(componentName);
2972
+ const filesToWrite = [];
2973
+ filesToWrite.push({
2974
+ absPath: normalizePath(join$1(outDir, `${componentName}.tsx`)),
2975
+ content: getComponentBoilerplate(componentName, styleExtension)
2976
+ });
2977
+ if (styleExtension) filesToWrite.push({
2978
+ absPath: normalizePath(join$1(outDir, `${componentName}.${styleExtension}`)),
2979
+ content: getStyleBoilerplate(styleExtension)
2980
+ });
2981
+ for (const ext of pickedExtensions) {
2982
+ const tmpl = allFileTemplates.find((ft) => ft.extension === ext);
2983
+ const absPath = normalizePath(join$1(outDir, tmpl.subdirectory ?? "", `${componentName}.${ext}`));
2984
+ filesToWrite.push({
2985
+ absPath,
2986
+ content: tmpl.template(componentName, className)
2987
+ });
2878
2988
  }
2989
+ const wouldOverwrite = (await Promise.all(filesToWrite.map(async ({ absPath }) => await config.sys.readFile(absPath) !== void 0 ? absPath : null))).filter((f) => f !== null);
2990
+ if (wouldOverwrite.length > 0) {
2991
+ config.logger.error("Generating code would overwrite the following files:", ...wouldOverwrite.map((path) => " " + normalizePath(path)));
2992
+ await config.sys.exit(1);
2993
+ return;
2994
+ }
2995
+ const dirs = [...new Set(filesToWrite.map(({ absPath }) => normalizePath(join$1(absPath, ".."))))];
2996
+ await Promise.all(dirs.map((d) => config.sys.createDir(d, { recursive: true })));
2997
+ await Promise.all(filesToWrite.map(({ absPath, content }) => config.sys.writeFile(absPath, content)));
2998
+ p.note(filesToWrite.map(({ absPath }) => relative$1(config.rootDir, absPath)).join("\n"), "Generated");
2999
+ p.outro(`stencil generate ${input}`);
2879
3000
  };
2880
- /**
2881
- * Get the boilerplate for a file containing the definition of a component
2882
- * @param tagName the name of the tag to give the component
2883
- * @param hasStyle designates if the component has an external stylesheet or not
2884
- * @param styleExtension extension used for styles
2885
- * @returns the contents of a file that defines a component
2886
- */
2887
- const getComponentBoilerplate = (tagName, hasStyle, styleExtension) => {
2888
- const decorator = [`{`];
2889
- decorator.push(` tag: '${tagName}',`);
2890
- if (hasStyle) decorator.push(` styleUrl: '${tagName}.${styleExtension}',`);
2891
- decorator.push(` shadow: true,`);
2892
- decorator.push(`}`);
2893
- return `import { Component, Host, h } from '@stencil/core';
2894
-
2895
- @Component(${decorator.join("\n")})
2896
- export class ${toPascalCase(tagName)} {
2897
- render() {
2898
- return (
2899
- <Host>
2900
- <slot></slot>
2901
- </Host>
2902
- );
2903
- }
2904
- }
2905
- `;
2906
- };
2907
- /**
2908
- * Get the boilerplate for style for a generated component
2909
- * @param ext extension used for styles
2910
- * @returns a boilerplate CSS block
2911
- */
2912
- const getStyleUrlBoilerplate = (ext) => ext === "sass" ? `:host
2913
- display: block
2914
- ` : `:host {
2915
- display: block;
2916
- }
2917
- `;
2918
- /**
2919
- * Get the boilerplate for a file containing a spec (unit) test for a component
2920
- * @param tagName the name of the tag associated with the component under test
2921
- * @returns the contents of a file that unit tests a component
2922
- */
2923
- const getSpecTestBoilerplate = (tagName) => `import { newSpecPage } from '@stencil/core/testing';
2924
- import { ${toPascalCase(tagName)} } from '../${tagName}';
2925
-
2926
- describe('${tagName}', () => {
2927
- it('renders', async () => {
2928
- const page = await newSpecPage({
2929
- components: [${toPascalCase(tagName)}],
2930
- html: \`<${tagName}></${tagName}>\`,
2931
- });
2932
- expect(page.root).toEqualHtml(\`
2933
- <${tagName}>
2934
- <mock:shadow-root>
2935
- <slot></slot>
2936
- </mock:shadow-root>
2937
- </${tagName}>
2938
- \`);
2939
- });
2940
- });
2941
- `;
2942
- /**
2943
- * Get the boilerplate for a file containing an end-to-end (E2E) test for a component
2944
- * @param tagName the name of the tag associated with the component under test
2945
- * @returns the contents of a file that E2E tests a component
2946
- */
2947
- const getE2eTestBoilerplate = (tagName) => `import { newE2EPage } from '@stencil/core/testing';
2948
-
2949
- describe('${tagName}', () => {
2950
- it('renders', async () => {
2951
- const page = await newE2EPage();
2952
- await page.setContent('<${tagName}></${tagName}>');
2953
-
2954
- const element = await page.find('${tagName}');
2955
- expect(element).toHaveClass('hydrated');
2956
- });
2957
- });
2958
- `;
2959
- /**
2960
- * Convert a dash case string to pascal case.
2961
- * @param str the string to convert
2962
- * @returns the converted input as pascal case
2963
- */
2964
- const toPascalCase = (str) => str.split("-").reduce((res, part) => res + part[0].toUpperCase() + part.slice(1), "");
2965
3001
  //#endregion
2966
3002
  //#region src/task-telemetry.ts
2967
3003
  /**
@@ -3075,6 +3111,317 @@ const taskInfo = (coreCompiler, sys, logger) => {
3075
3111
  console.log(``);
3076
3112
  };
3077
3113
  //#endregion
3114
+ //#region src/wizard/init/apply.ts
3115
+ /**
3116
+ * Copy component-starter template into rootDir, interpolating project name and namespace.
3117
+ *
3118
+ * @param rootDir - Destination directory (typically `process.cwd()`).
3119
+ * @param projectName - Value to substitute for `{{PROJECT_NAME}}` placeholders.
3120
+ * @param namespace - Value to substitute for `{{NAMESPACE}}` placeholders.
3121
+ */
3122
+ async function copyTemplate(rootDir, projectName, namespace) {
3123
+ const templateDir = getTemplatePath("component-starter");
3124
+ const entries = await readdir(templateDir, {
3125
+ recursive: true,
3126
+ withFileTypes: true
3127
+ });
3128
+ for (const entry of entries) {
3129
+ if (!entry.isFile()) continue;
3130
+ const srcPath = join$1(entry.parentPath, entry.name);
3131
+ const destPath = join$1(rootDir, relative$1(templateDir, srcPath));
3132
+ await mkdir(dirname$1(destPath), { recursive: true });
3133
+ await writeFile(destPath, (await readFile(srcPath, "utf8")).replace(/\{\{PROJECT_NAME\}\}/g, projectName).replace(/\{\{NAMESPACE\}\}/g, namespace), "utf8");
3134
+ }
3135
+ }
3136
+ /**
3137
+ * Inject integration package names into the project's package.json devDependencies.
3138
+ * Versions are set to 'latest' so the subsequent install resolves them from the registry.
3139
+ *
3140
+ * @param rootDir - Absolute path to the project root.
3141
+ * @param integrations - npm package names to add as devDependencies.
3142
+ */
3143
+ async function patchPackageJson(rootDir, integrations) {
3144
+ if (integrations.length === 0) return;
3145
+ const pkgPath = join$1(rootDir, "package.json");
3146
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
3147
+ const devDeps = pkg.devDependencies ?? {};
3148
+ for (const name of integrations) devDeps[name] = "latest";
3149
+ pkg.devDependencies = devDeps;
3150
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
3151
+ }
3152
+ //#endregion
3153
+ //#region src/wizard/init/steps.ts
3154
+ /** Well-known integrations the CLI can offer before any packages are installed. */
3155
+ const KNOWN_INTEGRATIONS = [
3156
+ {
3157
+ package: "@stencil/vitest",
3158
+ displayName: "Vitest",
3159
+ description: "Unit / Spec / Integration / Browser testing",
3160
+ group: "Testing"
3161
+ },
3162
+ {
3163
+ package: "@stencil/playwright",
3164
+ displayName: "Playwright",
3165
+ description: "E2E testing",
3166
+ group: "Testing"
3167
+ },
3168
+ {
3169
+ package: "@stencil/sass",
3170
+ displayName: "Sass",
3171
+ description: "Sass/SCSS styles",
3172
+ group: "Styling"
3173
+ },
3174
+ {
3175
+ package: "@stencil/eslint-plugin",
3176
+ displayName: "ESLint Plugin",
3177
+ description: "Stencil-aware lint rules (ESLint, oxlint, Biome)",
3178
+ group: "Linting"
3179
+ },
3180
+ {
3181
+ package: "@stencil/storybook-plugin",
3182
+ displayName: "Storybook",
3183
+ description: "Component development & documentation",
3184
+ group: "Tooling"
3185
+ },
3186
+ {
3187
+ package: "@stencil/types-output-target",
3188
+ displayName: "Types",
3189
+ description: "TypeScript types for React, Vue, Solid, Svelte, Preact",
3190
+ group: "Framework integrations"
3191
+ },
3192
+ {
3193
+ package: "@stencil/react-output-target",
3194
+ displayName: "React",
3195
+ description: "React component wrappers",
3196
+ group: "Framework integrations"
3197
+ },
3198
+ {
3199
+ package: "@stencil/angular-output-target",
3200
+ displayName: "Angular",
3201
+ description: "Angular component wrappers",
3202
+ group: "Framework integrations"
3203
+ },
3204
+ {
3205
+ package: "@stencil/vue-output-target",
3206
+ displayName: "Vue",
3207
+ description: "Vue component wrappers",
3208
+ group: "Framework integrations"
3209
+ }
3210
+ ];
3211
+ async function promptProjectName() {
3212
+ const name = await p.text({
3213
+ message: "Project name:",
3214
+ defaultValue: "my-stencil-library",
3215
+ validate: (v) => {
3216
+ if (!v?.trim()) return "Project name is required";
3217
+ }
3218
+ });
3219
+ cancelIfAborted(name);
3220
+ return name;
3221
+ }
3222
+ function buildGroupedOptions(integrations) {
3223
+ const groups = {};
3224
+ for (const i of integrations) (groups[i.group] ??= []).push({
3225
+ value: i.package,
3226
+ label: i.displayName,
3227
+ hint: i.description
3228
+ });
3229
+ return groups;
3230
+ }
3231
+ async function promptIntegrations() {
3232
+ const picks = await p.groupMultiselect({
3233
+ message: "Add integrations (optional):",
3234
+ options: buildGroupedOptions(KNOWN_INTEGRATIONS),
3235
+ required: false
3236
+ });
3237
+ cancelIfAborted(picks);
3238
+ return KNOWN_INTEGRATIONS.filter((i) => picks.includes(i.package));
3239
+ }
3240
+ /**
3241
+ * Prompt for actions on an existing project: install new integrations and/or
3242
+ * run init wizards for already-installed packages with wizard contributions.
3243
+ *
3244
+ * @param installable - KNOWN_INTEGRATIONS not yet present in the project.
3245
+ * @param configurable - Already-installed plugins that declare an `init` contribution.
3246
+ * @returns Selected integrations to install and plugins to configure.
3247
+ */
3248
+ async function promptAddCapabilities(installable, configurable) {
3249
+ const options = {};
3250
+ if (installable.length > 0) options["Install new integrations"] = installable.map((i) => ({
3251
+ value: `install:${i.package}`,
3252
+ label: i.displayName,
3253
+ hint: i.description
3254
+ }));
3255
+ if (configurable.length > 0) options["Configure existing integrations"] = configurable.map((d) => ({
3256
+ value: `configure:${d.packageName}`,
3257
+ label: d.plugin.init.displayName,
3258
+ hint: d.plugin.init.description
3259
+ }));
3260
+ const picks = await p.groupMultiselect({
3261
+ message: "What would you like to do?",
3262
+ options,
3263
+ required: false
3264
+ });
3265
+ cancelIfAborted(picks);
3266
+ const pickedSet = new Set(picks);
3267
+ return {
3268
+ toInstall: installable.filter((i) => pickedSet.has(`install:${i.package}`)),
3269
+ toConfigure: configurable.filter((d) => pickedSet.has(`configure:${d.packageName}`))
3270
+ };
3271
+ }
3272
+ //#endregion
3273
+ //#region src/wizard/splash.ts
3274
+ const noColor = !process.stdout.isTTY || "NO_COLOR" in process.env;
3275
+ const RESET = noColor ? "" : "\x1B[0m";
3276
+ const BG = noColor ? "" : "\x1B[38;2;60;44;255m";
3277
+ const RAW = `\
3278
+ .............
3279
+ ...................
3280
+ .........................
3281
+ ..............................
3282
+ .................................
3283
+ ..............████████████.........
3284
+ .............████████████............
3285
+ ............████████████...............
3286
+ ...........***************...............
3287
+ .........████████████████████████████....
3288
+ .......████████████████████████████......
3289
+ .....████████████████████████████........
3290
+ ...████████████████████████████..........
3291
+ ...............**************............
3292
+ ..............████████████.............
3293
+ ............████████████..............
3294
+ .........████████████...............
3295
+ .................................
3296
+ ..............................
3297
+ ..........................
3298
+ ......................
3299
+ ................`;
3300
+ function colorize(raw) {
3301
+ if (noColor) return raw;
3302
+ let out = "";
3303
+ let style = "";
3304
+ for (const ch of raw) {
3305
+ const next = ".*".includes(ch) ? BG : "";
3306
+ if (next !== style) {
3307
+ if (style) out += RESET;
3308
+ if (next) out += next;
3309
+ style = next;
3310
+ }
3311
+ out += ch;
3312
+ }
3313
+ return style ? out + RESET : out;
3314
+ }
3315
+ const SPLASH = colorize(RAW);
3316
+ function printSplash() {
3317
+ if (!process.stdout.isTTY) return;
3318
+ process.stdout.write("\n" + SPLASH + "\n\n");
3319
+ }
3320
+ //#endregion
3321
+ //#region src/task-init.ts
3322
+ async function taskInit() {
3323
+ const cwd = process.cwd();
3324
+ const isExistingProject = existsSync(join$1(cwd, "stencil.config.ts"));
3325
+ printSplash();
3326
+ p.intro("stencil init");
3327
+ if (process.env.STENCIL_WIZARD_DEV) p.log.warn(`Dev mode: loading wizard from ${process.env.STENCIL_WIZARD_DEV}`);
3328
+ if (isCI) {
3329
+ p.log.warn("Running in CI - non-interactive mode is not yet supported for `stencil init`.");
3330
+ process.exit(1);
3331
+ }
3332
+ if (isExistingProject) {
3333
+ await addCapabilities(cwd);
3334
+ return;
3335
+ }
3336
+ const projectName = await promptProjectName();
3337
+ const namespace = toNamespace(projectName);
3338
+ const selectedIntegrations = await promptIntegrations();
3339
+ const summaryLines = [
3340
+ `Template: component-starter`,
3341
+ `Name: ${projectName}`,
3342
+ `Namespace: ${namespace}`
3343
+ ];
3344
+ if (selectedIntegrations.length > 0) summaryLines.push(`Add: ${selectedIntegrations.map((i) => i.displayName).join(", ")}`);
3345
+ p.note(summaryLines.join("\n"), "Summary");
3346
+ const confirmed = await p.confirm({ message: "Scaffold project in current directory?" });
3347
+ cancelIfAborted(confirmed);
3348
+ if (!confirmed) {
3349
+ p.cancel("Cancelled.");
3350
+ process.exit(0);
3351
+ }
3352
+ const s1 = p.spinner();
3353
+ s1.start("Scaffolding project files");
3354
+ await copyTemplate(cwd, projectName, namespace);
3355
+ s1.stop("Project files created");
3356
+ if (selectedIntegrations.length > 0) await patchPackageJson(cwd, selectedIntegrations.map((i) => i.package));
3357
+ const s2 = p.spinner();
3358
+ s2.start("Installing dependencies");
3359
+ await installDependencies({
3360
+ cwd,
3361
+ silent: true
3362
+ });
3363
+ s2.stop("Dependencies installed");
3364
+ if (selectedIntegrations.length > 0) {
3365
+ const discovered = await discoverPlugins(cwd);
3366
+ const selectedPkgs = new Set(selectedIntegrations.map((i) => i.package));
3367
+ const context = {
3368
+ rootDir: cwd,
3369
+ isNewProject: true
3370
+ };
3371
+ for (const d of discovered) if (selectedPkgs.has(d.packageName) && d.plugin.init?.run) await d.plugin.init.run(context);
3372
+ }
3373
+ p.outro("Your project is ready! Run: pnpm run dev");
3374
+ }
3375
+ async function addCapabilities(cwd) {
3376
+ const raw = JSON.parse(await readFile(join$1(cwd, "package.json"), "utf8"));
3377
+ const installed = new Set([...Object.keys(raw.dependencies ?? {}), ...Object.keys(raw.devDependencies ?? {})]);
3378
+ const discovered = await discoverPlugins(cwd);
3379
+ const configurable = discovered.filter((d) => d.plugin.init);
3380
+ const installable = KNOWN_INTEGRATIONS.filter((i) => !installed.has(i.package));
3381
+ if (installable.length === 0 && configurable.length === 0) {
3382
+ p.log.info("All known integrations are already installed and configured.");
3383
+ p.outro("Nothing to do.");
3384
+ return;
3385
+ }
3386
+ const { toInstall, toConfigure } = await promptAddCapabilities(installable, configurable);
3387
+ if (toInstall.length === 0 && toConfigure.length === 0) {
3388
+ p.outro("No changes made.");
3389
+ return;
3390
+ }
3391
+ const summaryLines = [];
3392
+ for (const i of toInstall) summaryLines.push(`Install: ${i.displayName}`);
3393
+ for (const d of toConfigure) summaryLines.push(`Configure: ${d.plugin.init.displayName}`);
3394
+ p.note(summaryLines.join("\n"), "Summary");
3395
+ const confirmed = await p.confirm({ message: "Apply changes?" });
3396
+ cancelIfAborted(confirmed);
3397
+ if (!confirmed) {
3398
+ p.cancel("Cancelled.");
3399
+ process.exit(0);
3400
+ }
3401
+ if (toInstall.length > 0) {
3402
+ await patchPackageJson(cwd, toInstall.map((i) => i.package));
3403
+ const s = p.spinner();
3404
+ s.start("Installing dependencies");
3405
+ await installDependencies({
3406
+ cwd,
3407
+ silent: true
3408
+ });
3409
+ s.stop("Dependencies installed");
3410
+ }
3411
+ const allDiscovered = toInstall.length > 0 ? await discoverPlugins(cwd) : discovered;
3412
+ const newlyInstalledPkgs = new Set(toInstall.map((i) => i.package));
3413
+ const toRun = [...allDiscovered.filter((d) => newlyInstalledPkgs.has(d.packageName)), ...toConfigure].filter((d) => d.plugin.init?.run);
3414
+ const context = {
3415
+ rootDir: cwd,
3416
+ isNewProject: false
3417
+ };
3418
+ for (const d of toRun) await d.plugin.init.run(context);
3419
+ p.outro("Done! Run pnpm run dev to continue.");
3420
+ }
3421
+ function toNamespace(name) {
3422
+ return toPascalCase(name.replace(/^@[^/]+\//, "").replace(/[/_]/g, "-"));
3423
+ }
3424
+ //#endregion
3078
3425
  //#region src/task-serve.ts
3079
3426
  const taskServe = async (config, flags) => {
3080
3427
  config.suppressLogs = true;
@@ -3130,6 +3477,10 @@ const run = async (init) => {
3130
3477
  }), logger, sys);
3131
3478
  return;
3132
3479
  }
3480
+ if (task === "init") {
3481
+ await taskInit();
3482
+ return;
3483
+ }
3133
3484
  startupLog(logger, task);
3134
3485
  const findConfigResults = await findConfig({
3135
3486
  sys,
@@ -3150,7 +3501,7 @@ const run = async (init) => {
3150
3501
  const configWithFlags = mergeFlags({}, flags);
3151
3502
  const validated = await coreCompiler.loadConfig({
3152
3503
  config: configWithFlags,
3153
- configPath: foundConfig.configPath,
3504
+ configPath: foundConfig.configPath ?? void 0,
3154
3505
  logger,
3155
3506
  sys
3156
3507
  });
@@ -3200,6 +3551,9 @@ const runTask = async (coreCompiler, config, task, sys, flags) => {
3200
3551
  case "help":
3201
3552
  await taskHelp(resolvedFlags, strictConfig.logger, sys);
3202
3553
  break;
3554
+ case "init":
3555
+ await taskInit();
3556
+ break;
3203
3557
  case "migrate":
3204
3558
  await taskMigrate(coreCompiler, strictConfig, resolvedFlags);
3205
3559
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stencil/cli",
3
- "version": "5.0.0-alpha.6",
3
+ "version": "5.0.0-alpha.7",
4
4
  "description": "CLI for Stencil - Web component compiler",
5
5
  "keywords": [
6
6
  "components",
@@ -37,15 +37,17 @@
37
37
  "./cli": "./bin/stencil.mjs"
38
38
  },
39
39
  "dependencies": {
40
- "prompts": "^2.0.0",
41
- "@stencil/dev-server": "5.0.0-alpha.6"
40
+ "@clack/prompts": "^1.5.1",
41
+ "nypm": "^0.3.12",
42
+ "std-env": "^3.8.0",
43
+ "@stencil/templates": "5.0.0-alpha.7",
44
+ "@stencil/dev-server": "5.0.0-alpha.7"
42
45
  },
43
46
  "devDependencies": {
44
- "@types/prompts": "^2.4.9",
45
47
  "tsdown": ">=0.21.0 <1.0.0",
46
48
  "typescript": ">4.0.0 <7.0.0",
47
49
  "vitest": "^4.1.7",
48
- "@stencil/core": "5.0.0-alpha.6"
50
+ "@stencil/core": "5.0.0-alpha.7"
49
51
  },
50
52
  "peerDependencies": {
51
53
  "@stencil/core": "^5.0.0-0"
@@ -55,7 +57,8 @@
55
57
  },
56
58
  "scripts": {
57
59
  "build": "tsdown",
58
- "test": "vitest run",
60
+ "test": "vitest run && pnpm test:e2e",
61
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
59
62
  "typecheck": "tsc --noEmit"
60
63
  }
61
64
  }