@stencil/cli 5.0.0-alpha.5 → 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.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 { 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);
@@ -853,6 +865,104 @@ const encapsulationApiRule = {
853
865
  }
854
866
  };
855
867
  //#endregion
868
+ //#region src/migrations/rules/external-runtime.ts
869
+ /**
870
+ * Migration rule for `externalRuntime` on `standalone` output targets.
871
+ *
872
+ * In Stencil v5, `externalRuntime` defaults to `false` (was `true` in v4).
873
+ * Explicit `externalRuntime: false` is now redundant and can be removed.
874
+ */
875
+ const externalRuntimeRule = {
876
+ id: "external-runtime",
877
+ name: "externalRuntime Default Change",
878
+ description: "Remove redundant 'externalRuntime: false' from standalone output targets - false is now the default",
879
+ fromVersion: "4.x",
880
+ toVersion: "5.x",
881
+ detect(sourceFile) {
882
+ const matches = [];
883
+ const visit = (node) => {
884
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "externalRuntime" && node.initializer.kind === ts.SyntaxKind.FalseKeyword) {
885
+ const parent = node.parent;
886
+ if (ts.isObjectLiteralExpression(parent)) {
887
+ if (parent.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type" && ts.isStringLiteral(p.initializer) && p.initializer.text === "standalone")) {
888
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
889
+ matches.push({
890
+ node,
891
+ message: "'externalRuntime: false' is now the default - this property can be removed",
892
+ line: line + 1,
893
+ column: character + 1
894
+ });
895
+ }
896
+ }
897
+ }
898
+ ts.forEachChild(node, visit);
899
+ };
900
+ visit(sourceFile);
901
+ return matches;
902
+ },
903
+ transform(sourceFile, matches) {
904
+ if (matches.length === 0) return sourceFile.getFullText();
905
+ let text = sourceFile.getFullText();
906
+ for (const match of [...matches].reverse()) {
907
+ const node = match.node;
908
+ const start = node.getFullStart();
909
+ const end = node.getEnd();
910
+ let removeEnd = end;
911
+ const trailingComma = text.slice(end).match(/^\s*,/);
912
+ if (trailingComma) removeEnd = end + trailingComma[0].length;
913
+ text = text.slice(0, start) + text.slice(removeEnd);
914
+ }
915
+ return text;
916
+ }
917
+ };
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
856
966
  //#region src/migrations/rules/form-associated.ts
857
967
  /**
858
968
  * Migration rule for formAssociated → @AttachInternals.
@@ -1050,7 +1160,7 @@ const globalStyleInjectRule = {
1050
1160
  }
1051
1161
  }
1052
1162
  if (end !== -1) text = text.slice(0, start) + text.slice(end);
1053
- text = cleanupEmptyExtras(text);
1163
+ text = cleanupEmptyExtras$1(text);
1054
1164
  return text;
1055
1165
  }
1056
1166
  };
@@ -1073,7 +1183,7 @@ function findOutputTargetsEnd(sourceFile) {
1073
1183
  * @param text The source text to clean up
1074
1184
  * @returns The cleaned up text with empty extras removed
1075
1185
  */
1076
- function cleanupEmptyExtras(text) {
1186
+ function cleanupEmptyExtras$1(text) {
1077
1187
  return text.replace(/,?\s*extras\s*:\s*\{\s*\},?/g, "");
1078
1188
  }
1079
1189
  /**
@@ -1115,6 +1225,219 @@ function addGlobalStyleOutputTarget(text, sourceFile, injectValue) {
1115
1225
  return text.slice(0, insertPos) + insertion + text.slice(insertPos);
1116
1226
  }
1117
1227
  //#endregion
1228
+ //#region src/migrations/rules/hash-file-names.ts
1229
+ /**
1230
+ * Migration rule for `hashFileNames` and `hashedFileNameLength` config options.
1231
+ *
1232
+ * In Stencil v5, these are no longer top-level config options. They belong on
1233
+ * the `loader-bundle` and `www` output targets, which are the only outputs that
1234
+ * are served directly in the browser and benefit from content-hash caching.
1235
+ *
1236
+ * This migration:
1237
+ * 1. Removes them from the top-level config
1238
+ * 2. Injects them into any `loader-bundle` and `www` output targets found in the same file
1239
+ */
1240
+ const hashFileNamesRule = {
1241
+ id: "hash-file-names",
1242
+ name: "Hash File Names Config Move",
1243
+ description: "Move hashFileNames and hashedFileNameLength from top-level config to loader-bundle and www output targets",
1244
+ fromVersion: "4.x",
1245
+ toVersion: "5.x",
1246
+ detect(sourceFile) {
1247
+ const matches = [];
1248
+ const visit = (node) => {
1249
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && (node.name.text === "hashFileNames" || node.name.text === "hashedFileNameLength")) {
1250
+ const parent = node.parent;
1251
+ if (ts.isObjectLiteralExpression(parent)) {
1252
+ if (!parent.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type")) {
1253
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1254
+ matches.push({
1255
+ node,
1256
+ message: `'${node.name.text}' is no longer a top-level config option. Move it to your 'loader-bundle' and/or 'www' output targets.`,
1257
+ line: line + 1,
1258
+ column: character + 1
1259
+ });
1260
+ }
1261
+ }
1262
+ }
1263
+ ts.forEachChild(node, visit);
1264
+ };
1265
+ visit(sourceFile);
1266
+ return matches;
1267
+ },
1268
+ transform(sourceFile, matches) {
1269
+ if (matches.length === 0) return sourceFile.getFullText();
1270
+ const fullText = sourceFile.getFullText();
1271
+ const propsToInject = matches.map((m) => {
1272
+ const node = m.node;
1273
+ return {
1274
+ name: node.name.text,
1275
+ value: node.initializer.getText(sourceFile)
1276
+ };
1277
+ });
1278
+ const TARGET_TYPES = new Set(["loader-bundle", "www"]);
1279
+ const insertionsByPos = /* @__PURE__ */ new Map();
1280
+ const visit = (node) => {
1281
+ if (ts.isObjectLiteralExpression(node)) {
1282
+ if (node.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type" && ts.isStringLiteral(p.initializer) && TARGET_TYPES.has(p.initializer.text))) {
1283
+ const existingPropNames = new Set(node.properties.filter((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)).map((p) => p.name.text));
1284
+ const objStart = node.getStart(sourceFile);
1285
+ const objLineStart = fullText.lastIndexOf("\n", objStart) + 1;
1286
+ const propIndent = (fullText.slice(objLineStart, objStart).match(/^(\s*)/)?.[1] ?? "") + " ";
1287
+ const closingBracePos = node.getEnd() - 1;
1288
+ const isMultiLine = fullText.slice(objStart, node.getEnd()).includes("\n");
1289
+ let insertPos;
1290
+ if (isMultiLine) insertPos = fullText.lastIndexOf("\n", closingBracePos - 1);
1291
+ else {
1292
+ insertPos = closingBracePos;
1293
+ while (insertPos > 0 && fullText[insertPos - 1] === " ") insertPos--;
1294
+ }
1295
+ const existing = insertionsByPos.get(insertPos) ?? [];
1296
+ for (const prop of propsToInject) if (!existingPropNames.has(prop.name)) existing.push(isMultiLine ? `\n${propIndent}${prop.name}: ${prop.value},` : `, ${prop.name}: ${prop.value}`);
1297
+ if (existing.length > 0) insertionsByPos.set(insertPos, existing);
1298
+ }
1299
+ }
1300
+ ts.forEachChild(node, visit);
1301
+ };
1302
+ visit(sourceFile);
1303
+ const edits = [];
1304
+ for (const match of matches) {
1305
+ const node = match.node;
1306
+ const start = node.getFullStart();
1307
+ const end = node.getEnd();
1308
+ let removeEnd = end;
1309
+ const trailingComma = fullText.slice(end).match(/^\s*,/);
1310
+ if (trailingComma) removeEnd = end + trailingComma[0].length;
1311
+ edits.push({
1312
+ start,
1313
+ end: removeEnd,
1314
+ replacement: ""
1315
+ });
1316
+ }
1317
+ for (const [pos, insertions] of insertionsByPos) edits.push({
1318
+ start: pos,
1319
+ end: pos,
1320
+ replacement: insertions.join("")
1321
+ });
1322
+ edits.sort((a, b) => b.start - a.start);
1323
+ let text = fullText;
1324
+ for (const edit of edits) text = text.slice(0, edit.start) + edit.replacement + text.slice(edit.end);
1325
+ return text;
1326
+ }
1327
+ };
1328
+ //#endregion
1329
+ //#region src/migrations/rules/light-dom-patches.ts
1330
+ /**
1331
+ * Migration rule for slot-fix extras → `lightDomPatches`.
1332
+ *
1333
+ * In v5, the individual slot-fix flags and `experimentalSlotFixes` umbrella are replaced
1334
+ * by a single `lightDomPatches` option which defaults to `true`.
1335
+ *
1336
+ * Migration mapping:
1337
+ * - `experimentalSlotFixes: true` → remove (new default is `true`)
1338
+ * - `experimentalSlotFixes: false` → `lightDomPatches: false`
1339
+ * - Individual flags only → `lightDomPatches: { <new names> }`
1340
+ *
1341
+ * Old → new individual key names:
1342
+ * appendChildSlotFix → domMutations
1343
+ * cloneNodeFix → cloneNode
1344
+ * scopedSlotTextContentFix → textContent
1345
+ * slotChildNodesFix → childNodes
1346
+ */
1347
+ const lightDomPatchesRule = {
1348
+ id: "light-dom-patches",
1349
+ name: "Light DOM Patches Migration",
1350
+ description: "Migrate experimentalSlotFixes / individual slot-fix flags to lightDomPatches",
1351
+ fromVersion: "4.x",
1352
+ toVersion: "5.x",
1353
+ detect(sourceFile) {
1354
+ const matches = [];
1355
+ const oldKeys = new Set([
1356
+ "experimentalSlotFixes",
1357
+ "appendChildSlotFix",
1358
+ "cloneNodeFix",
1359
+ "scopedSlotTextContentFix",
1360
+ "slotChildNodesFix"
1361
+ ]);
1362
+ const visit = (node) => {
1363
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && oldKeys.has(node.name.text)) {
1364
+ const parent = node.parent;
1365
+ if (ts.isObjectLiteralExpression(parent)) {
1366
+ const grandparent = parent.parent;
1367
+ if (ts.isPropertyAssignment(grandparent) && ts.isIdentifier(grandparent.name) && grandparent.name.text === "extras") {
1368
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1369
+ matches.push({
1370
+ node,
1371
+ message: `extras.${node.name.text} is removed. Use 'extras.lightDomPatches' instead.`,
1372
+ line: line + 1,
1373
+ column: character + 1
1374
+ });
1375
+ }
1376
+ }
1377
+ }
1378
+ ts.forEachChild(node, visit);
1379
+ };
1380
+ visit(sourceFile);
1381
+ return matches;
1382
+ },
1383
+ transform(sourceFile, matches) {
1384
+ if (matches.length === 0) return sourceFile.getFullText();
1385
+ const oldKeyMap = {};
1386
+ for (const match of matches) {
1387
+ const node = match.node;
1388
+ const key = node.name.text;
1389
+ const init = node.initializer;
1390
+ oldKeyMap[key] = init.kind === ts.SyntaxKind.TrueKeyword ? true : init.kind === ts.SyntaxKind.FalseKeyword ? false : void 0;
1391
+ }
1392
+ const keyRename = {
1393
+ appendChildSlotFix: "domMutations",
1394
+ cloneNodeFix: "cloneNode",
1395
+ scopedSlotTextContentFix: "textContent",
1396
+ slotChildNodesFix: "childNodes"
1397
+ };
1398
+ const experimentalValue = oldKeyMap["experimentalSlotFixes"];
1399
+ const hasIndividualKeys = matches.some((m) => {
1400
+ return m.node.name.text !== "experimentalSlotFixes";
1401
+ });
1402
+ let replacement = null;
1403
+ if (experimentalValue === true && !hasIndividualKeys) replacement = null;
1404
+ else if (experimentalValue === false && !hasIndividualKeys) replacement = "lightDomPatches: false";
1405
+ else if (experimentalValue === true && hasIndividualKeys) replacement = null;
1406
+ else {
1407
+ const parts = [];
1408
+ for (const [oldKey, newKey] of Object.entries(keyRename)) {
1409
+ const val = oldKeyMap[oldKey];
1410
+ if (val === true) parts.push(`${newKey}: true`);
1411
+ else if (val === false) parts.push(`${newKey}: false`);
1412
+ }
1413
+ if (parts.length > 0) replacement = `lightDomPatches: { ${parts.join(", ")} }`;
1414
+ }
1415
+ const sorted = [...matches].sort((a, b) => b.node.getStart() - a.node.getStart());
1416
+ let text = sourceFile.getFullText();
1417
+ let replacementInserted = false;
1418
+ for (const match of sorted) {
1419
+ const node = match.node;
1420
+ let start = node.getStart();
1421
+ let end = node.getEnd();
1422
+ const trailingComma = text.slice(end).match(/^(\s*,)/);
1423
+ if (trailingComma) end += trailingComma[1].length;
1424
+ else {
1425
+ const leadingComma = text.slice(0, start).match(/,\s*$/);
1426
+ if (leadingComma) start -= leadingComma[0].length;
1427
+ }
1428
+ if (!replacementInserted && replacement !== null) {
1429
+ text = text.slice(0, start) + replacement + text.slice(end);
1430
+ replacementInserted = true;
1431
+ } else text = text.slice(0, start) + text.slice(end);
1432
+ }
1433
+ text = cleanupEmptyExtras(text);
1434
+ return text;
1435
+ }
1436
+ };
1437
+ function cleanupEmptyExtras(text) {
1438
+ return text.replace(/,?\s*extras\s*:\s*\{\s*\},?/g, "");
1439
+ }
1440
+ //#endregion
1118
1441
  //#region src/migrations/rules/output-target-renames.ts
1119
1442
  /**
1120
1443
  * Migration rule for output target renames in Stencil v5.
@@ -1123,9 +1446,9 @@ function addGlobalStyleOutputTarget(text, sourceFile, injectValue) {
1123
1446
  * - Renames `dist` → `loader-bundle`
1124
1447
  * - Renames `dist-custom-elements` → `standalone`
1125
1448
  * - Renames `dist-hydrate-script` → `ssr`
1126
- * - Renames `dist-collection` → `stencil-rebundle`
1449
+ * - Renames `dist-collection` → `collection`
1127
1450
  * - Renames `dist-types` → `types`
1128
- * - Extracts `collectionDir` from loader-bundle into separate `stencil-rebundle` output
1451
+ * - Extracts `collectionDir` from loader-bundle into separate `collection` output
1129
1452
  * - Extracts `typesDir` from loader-bundle into separate `types` output
1130
1453
  * - Renames `esmLoaderPath` → `loaderPath` (applies to all module formats, not just ESM)
1131
1454
  * - Removes `isPrimaryPackageOutputTarget` (no longer needed, package.json validation auto-detects)
@@ -1143,7 +1466,7 @@ const outputTargetRenamesRule = {
1143
1466
  dist: "loader-bundle",
1144
1467
  "dist-custom-elements": "standalone",
1145
1468
  "dist-hydrate-script": "ssr",
1146
- "dist-collection": "stencil-rebundle",
1469
+ "dist-collection": "collection",
1147
1470
  "dist-types": "types"
1148
1471
  };
1149
1472
  const visit = (node) => {
@@ -1165,7 +1488,7 @@ const outputTargetRenamesRule = {
1165
1488
  if (parent.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type")) {
1166
1489
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1167
1490
  const propName = node.name.text;
1168
- const newType = propName === "collectionDir" ? "stencil-rebundle" : "types";
1491
+ const newType = propName === "collectionDir" ? "collection" : "types";
1169
1492
  matches.push({
1170
1493
  node,
1171
1494
  message: `Property '${propName}' will be extracted to separate '${newType}' output target`,
@@ -1217,7 +1540,7 @@ const outputTargetRenamesRule = {
1217
1540
  dist: "loader-bundle",
1218
1541
  "dist-custom-elements": "standalone",
1219
1542
  "dist-hydrate-script": "ssr",
1220
- "dist-collection": "stencil-rebundle",
1543
+ "dist-collection": "collection",
1221
1544
  "dist-types": "types"
1222
1545
  };
1223
1546
  const outputTargetsToExtract = [];
@@ -1335,7 +1658,7 @@ function addExtractedOutputTargets(text, sourceFile, toExtract) {
1335
1658
  if (afterLastElement && afterLastElement[1]) insertPos += afterLastElement[0].length;
1336
1659
  const newTargets = [];
1337
1660
  for (const extracted of toExtract) {
1338
- if (extracted.collectionDir) newTargets.push(`{\n${indent} type: 'stencil-rebundle',\n${indent} dir: '${extracted.collectionDir}',\n${indent}}`);
1661
+ if (extracted.collectionDir) newTargets.push(`{\n${indent} type: 'collection',\n${indent} dir: '${extracted.collectionDir}',\n${indent}}`);
1339
1662
  if (extracted.typesDir) newTargets.push(`{\n${indent} type: 'types',\n${indent} dir: '${extracted.typesDir}',\n${indent}}`);
1340
1663
  }
1341
1664
  if (newTargets.length === 0) return text;
@@ -1344,6 +1667,225 @@ function addExtractedOutputTargets(text, sourceFile, toExtract) {
1344
1667
  return text;
1345
1668
  }
1346
1669
  //#endregion
1670
+ //#region src/migrations/rules/rolldown-config.ts
1671
+ /**
1672
+ * NodeResolve fields that were valid in @rollup/plugin-node-resolve but don't exist
1673
+ * in rolldown's native resolver and should be removed.
1674
+ */
1675
+ const REMOVED_NODE_RESOLVE_FIELDS = new Set([
1676
+ "browser",
1677
+ "modulePaths",
1678
+ "dedupe",
1679
+ "jail",
1680
+ "modulesOnly",
1681
+ "preferBuiltins",
1682
+ "resolveOnly",
1683
+ "rootDir",
1684
+ "allowExportsFolderMapping"
1685
+ ]);
1686
+ /** Fields renamed between @rollup/plugin-node-resolve and rolldown's native resolver. */
1687
+ const RENAMED_NODE_RESOLVE_FIELDS = {
1688
+ exportConditions: "conditionNames",
1689
+ moduleDirectories: "modules"
1690
+ };
1691
+ function isInsideNodeResolve(node) {
1692
+ const parent = node.parent;
1693
+ if (!ts.isObjectLiteralExpression(parent)) return false;
1694
+ const grandParent = parent.parent;
1695
+ return ts.isPropertyAssignment(grandParent) && ts.isIdentifier(grandParent.name) && grandParent.name.text === "nodeResolve";
1696
+ }
1697
+ /**
1698
+ * Migration rule for rolldown-related config changes in Stencil v5.
1699
+ *
1700
+ * Handles:
1701
+ * - `rollupConfig` → `rolldownConfig` (key rename + flatten `inputOptions`)
1702
+ * - `rolldownConfig: { inputOptions: { ... } }` → `rolldownConfig: { ... }` (flatten)
1703
+ * - `rollupPlugins` → `rolldownPlugins`
1704
+ * - `nodeResolve.exportConditions` → `nodeResolve.conditionNames`
1705
+ * - `nodeResolve.moduleDirectories` → `nodeResolve.modules`
1706
+ * - Removes unsupported `nodeResolve` fields (`browser`, `dedupe`, `jail`, etc.)
1707
+ */
1708
+ const rolldownConfigRule = {
1709
+ id: "rolldown-config",
1710
+ name: "Rolldown Config Migration",
1711
+ description: "Migrate rollup/rolldown config options to v5 rolldown-native API",
1712
+ fromVersion: "4.x",
1713
+ toVersion: "5.x",
1714
+ detect(sourceFile) {
1715
+ const matches = [];
1716
+ const visit = (node) => {
1717
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
1718
+ const name = node.name.text;
1719
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1720
+ if (name === "rollupPlugins") matches.push({
1721
+ node,
1722
+ message: `'rollupPlugins' renamed to 'rolldownPlugins'`,
1723
+ line: line + 1,
1724
+ column: character + 1
1725
+ });
1726
+ else if (name === "rollupConfig" || name === "rolldownConfig") {
1727
+ const value = node.initializer;
1728
+ if (name === "rollupConfig" || ts.isObjectLiteralExpression(value) && value.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "inputOptions")) matches.push({
1729
+ node,
1730
+ message: name === "rollupConfig" ? `'rollupConfig' renamed to 'rolldownConfig'; 'inputOptions' wrapper removed` : `'rolldownConfig.inputOptions' wrapper removed - options are now top-level`,
1731
+ line: line + 1,
1732
+ column: character + 1
1733
+ });
1734
+ } else if (name in RENAMED_NODE_RESOLVE_FIELDS && isInsideNodeResolve(node)) matches.push({
1735
+ node,
1736
+ message: `'nodeResolve.${name}' renamed to 'nodeResolve.${RENAMED_NODE_RESOLVE_FIELDS[name]}'`,
1737
+ line: line + 1,
1738
+ column: character + 1
1739
+ });
1740
+ else if (REMOVED_NODE_RESOLVE_FIELDS.has(name) && isInsideNodeResolve(node)) matches.push({
1741
+ node,
1742
+ message: `'nodeResolve.${name}' is not supported by rolldown's native resolver and will be removed`,
1743
+ line: line + 1,
1744
+ column: character + 1
1745
+ });
1746
+ }
1747
+ ts.forEachChild(node, visit);
1748
+ };
1749
+ visit(sourceFile);
1750
+ return matches;
1751
+ },
1752
+ transform(sourceFile, matches) {
1753
+ if (matches.length === 0) return sourceFile.getFullText();
1754
+ const edits = [];
1755
+ const fullText = sourceFile.getFullText();
1756
+ for (const match of matches) {
1757
+ const node = match.node;
1758
+ const name = node.name.text;
1759
+ if (name === "rollupPlugins") {
1760
+ const nameStart = node.name.getStart(sourceFile);
1761
+ const nameEnd = node.name.getEnd();
1762
+ edits.push({
1763
+ start: nameStart,
1764
+ end: nameEnd,
1765
+ replacement: "rolldownPlugins"
1766
+ });
1767
+ continue;
1768
+ }
1769
+ if (name in RENAMED_NODE_RESOLVE_FIELDS && isInsideNodeResolve(node)) {
1770
+ const nameStart = node.name.getStart(sourceFile);
1771
+ const nameEnd = node.name.getEnd();
1772
+ edits.push({
1773
+ start: nameStart,
1774
+ end: nameEnd,
1775
+ replacement: RENAMED_NODE_RESOLVE_FIELDS[name]
1776
+ });
1777
+ continue;
1778
+ }
1779
+ if (REMOVED_NODE_RESOLVE_FIELDS.has(name) && isInsideNodeResolve(node)) {
1780
+ const start = node.getFullStart();
1781
+ const end = node.getEnd();
1782
+ const trailingComma = fullText.slice(end).match(/^\s*,/);
1783
+ edits.push({
1784
+ start,
1785
+ end: trailingComma ? end + trailingComma[0].length : end,
1786
+ replacement: ""
1787
+ });
1788
+ continue;
1789
+ }
1790
+ if (name === "rollupConfig" || name === "rolldownConfig") {
1791
+ const value = node.initializer;
1792
+ if (!ts.isObjectLiteralExpression(value)) {
1793
+ if (name === "rollupConfig") {
1794
+ const nameStart = node.name.getStart(sourceFile);
1795
+ edits.push({
1796
+ start: nameStart,
1797
+ end: node.name.getEnd(),
1798
+ replacement: "rolldownConfig"
1799
+ });
1800
+ }
1801
+ continue;
1802
+ }
1803
+ const inputOptionsProp = value.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "inputOptions");
1804
+ if (!inputOptionsProp) {
1805
+ if (name === "rollupConfig") {
1806
+ const nameStart = node.name.getStart(sourceFile);
1807
+ edits.push({
1808
+ start: nameStart,
1809
+ end: node.name.getEnd(),
1810
+ replacement: "rolldownConfig"
1811
+ });
1812
+ }
1813
+ continue;
1814
+ }
1815
+ const newValueText = inputOptionsProp.initializer.getText(sourceFile);
1816
+ const keyStart = node.name.getStart(sourceFile);
1817
+ edits.push({
1818
+ start: keyStart,
1819
+ end: node.getEnd(),
1820
+ replacement: `rolldownConfig: ${newValueText}`
1821
+ });
1822
+ }
1823
+ }
1824
+ edits.sort((a, b) => b.start - a.start);
1825
+ let text = fullText;
1826
+ for (const edit of edits) text = text.slice(0, edit.start) + edit.replacement + text.slice(edit.end);
1827
+ return text;
1828
+ }
1829
+ };
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
1347
1889
  //#region src/migrations/index.ts
1348
1890
  /**
1349
1891
  * Build a map of local import names to their original names from @stencil/core.
@@ -1387,7 +1929,13 @@ const migrationRules = [
1387
1929
  buildDistDocsRule,
1388
1930
  outputTargetRenamesRule,
1389
1931
  devModeRule,
1390
- globalStyleInjectRule
1932
+ globalStyleInjectRule,
1933
+ lightDomPatchesRule,
1934
+ extrasToCompatRule,
1935
+ externalRuntimeRule,
1936
+ hashFileNamesRule,
1937
+ rolldownConfigRule,
1938
+ serviceWorkerDefaultRule
1391
1939
  ];
1392
1940
  /**
1393
1941
  * Get all migration rules for a specific version upgrade.
@@ -1576,6 +2124,23 @@ async function getTypeScriptFiles(config, sys, logger) {
1576
2124
  const configFile = config.configPath;
1577
2125
  if (configFile && (configFile.endsWith(".ts") || configFile.endsWith(".mts"))) {
1578
2126
  if (!files.includes(configFile)) files.push(configFile);
2127
+ const configContent = await sys.readFile(configFile);
2128
+ if (configContent) {
2129
+ const configSourceFile = ts.createSourceFile(configFile, configContent, ts.ScriptTarget.Latest, true);
2130
+ const configDir = dirname(configFile);
2131
+ for (const statement of configSourceFile.statements) if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
2132
+ const specifier = statement.moduleSpecifier.text;
2133
+ if (!specifier.startsWith("./") && !specifier.startsWith("../")) continue;
2134
+ const basePath = join(configDir, specifier);
2135
+ const candidates = basePath.endsWith(".ts") || basePath.endsWith(".tsx") ? [basePath] : [`${basePath}.ts`, `${basePath}.tsx`];
2136
+ for (const candidate of candidates) if (!candidate.endsWith(".d.ts") && !files.includes(candidate) && candidate.startsWith(config.rootDir)) {
2137
+ if (await sys.readFile(candidate)) {
2138
+ files.push(candidate);
2139
+ break;
2140
+ }
2141
+ }
2142
+ }
2143
+ }
1579
2144
  }
1580
2145
  return files;
1581
2146
  }
@@ -1680,7 +2245,7 @@ function uuidv4() {
1680
2245
  * @param path the path on the file system to read and parse
1681
2246
  * @returns the parsed JSON
1682
2247
  */
1683
- async function readJson(sys, path) {
2248
+ async function readJson$1(sys, path) {
1684
2249
  const file = await sys.readFile(path);
1685
2250
  return file ? JSON.parse(file) : null;
1686
2251
  }
@@ -1702,8 +2267,8 @@ function hasVerbose(flags) {
1702
2267
  }
1703
2268
  //#endregion
1704
2269
  //#region src/ionic-config.ts
1705
- const isTest$1 = () => process.env.JEST_WORKER_ID !== void 0;
1706
- 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"}`);
1707
2272
  const defaultConfigDirectory = (sys) => sys.resolvePath(`${sys.homeDir()}/.ionic`);
1708
2273
  /**
1709
2274
  * Reads an Ionic configuration file from disk, parses it, and performs any necessary corrections to it if certain
@@ -1712,7 +2277,7 @@ const defaultConfigDirectory = (sys) => sys.resolvePath(`${sys.homeDir()}/.ionic
1712
2277
  * @returns the config read from disk that has been potentially been updated
1713
2278
  */
1714
2279
  async function readConfig(sys) {
1715
- let config = await readJson(sys, defaultConfig(sys));
2280
+ let config = await readJson$1(sys, defaultConfig(sys));
1716
2281
  if (!config) {
1717
2282
  config = {
1718
2283
  "tokens.telemetry": uuidv4(),
@@ -1950,7 +2515,7 @@ async function getInstalledPackages(sys, flags) {
1950
2515
  const yarn = isUsingYarn(sys);
1951
2516
  try {
1952
2517
  const appRootDir = sys.getCurrentDirectory();
1953
- const packageJson = await tryFn(readJson, sys, sys.resolvePath(appRootDir + "/package.json"));
2518
+ const packageJson = await tryFn(readJson$1, sys, sys.resolvePath(appRootDir + "/package.json"));
1954
2519
  if (!packageJson) return {
1955
2520
  packages,
1956
2521
  packagesNoVersions
@@ -1985,7 +2550,7 @@ async function getInstalledPackages(sys, flags) {
1985
2550
  */
1986
2551
  async function npmPackages(sys, ionicPackages) {
1987
2552
  const appRootDir = sys.getCurrentDirectory();
1988
- 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"));
1989
2554
  return ionicPackages.map(([k, v]) => {
1990
2555
  let version = packageLockJson?.dependencies[k]?.version ?? packageLockJson?.devDependencies[k]?.version ?? v;
1991
2556
  version = version.includes("file:") ? sanitizeDeclaredVersion(v) : version;
@@ -2207,31 +2772,28 @@ async function promptForMigration(config, migrationResult, context) {
2207
2772
  logger.info("");
2208
2773
  if (context === "pre-build") logger.info("Your config contains deprecated options that must be migrated before building.");
2209
2774
  else logger.info("These migrations may help resolve the build errors above.");
2210
- const prompt = (await import("prompts")).default;
2211
- const response = await prompt({
2212
- name: "action",
2213
- type: "select",
2775
+ const action = await select({
2214
2776
  message: "What would you like to do?",
2215
- choices: [
2777
+ options: [
2216
2778
  {
2217
- title: "Run migration",
2218
2779
  value: "run",
2219
- description: "Apply migrations and re-run build"
2780
+ label: "Run migration",
2781
+ hint: "Apply migrations and re-run build"
2220
2782
  },
2221
2783
  {
2222
- title: "Dry run",
2223
2784
  value: "dry-run",
2224
- description: "Preview changes without modifying files"
2785
+ label: "Dry run",
2786
+ hint: "Preview changes without modifying files"
2225
2787
  },
2226
2788
  {
2227
- title: "Exit",
2228
2789
  value: "exit",
2229
- description: "Exit without making changes"
2790
+ label: "Exit",
2791
+ hint: "Exit without making changes"
2230
2792
  }
2231
2793
  ]
2232
2794
  });
2233
- if (response.action === void 0) return "exit";
2234
- return response.action;
2795
+ if (isCancel(action)) return "exit";
2796
+ return action;
2235
2797
  }
2236
2798
  //#endregion
2237
2799
  //#region src/task-docs.ts
@@ -2245,277 +2807,197 @@ const taskDocs = async (coreCompiler, config) => {
2245
2807
  await compiler.destroy();
2246
2808
  };
2247
2809
  //#endregion
2248
- //#region src/task-generate.ts
2810
+ //#region src/wizard/clack.ts
2249
2811
  /**
2250
- * Task to generate component boilerplate and write it to disk. This task can
2251
- * cause the program to exit with an error under various circumstances, such as
2252
- * being called in an inappropriate place, being asked to overwrite files that
2253
- * 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.
2254
2814
  *
2255
- * @param config the user-supplied config, which we need here to access `.sys`.
2256
- * @param flags the CLI flags (owned by CLI, not part of core config)
2257
- * @returns a void promise
2815
+ * @param value - Return value from a `@clack/prompts` prompt call.
2258
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
2259
2906
  const taskGenerate = async (config, flags) => {
2260
2907
  if (!config.configPath) {
2261
2908
  config.logger.error("Please run this command in your root directory (i. e. the one containing stencil.config.ts).");
2262
2909
  return config.sys.exit(1);
2263
2910
  }
2264
- const absoluteSrcDir = config.srcDir;
2265
- if (!absoluteSrcDir) {
2911
+ const srcDir = config.srcDir;
2912
+ if (!srcDir) {
2266
2913
  config.logger.error(`Stencil's srcDir was not specified.`);
2267
2914
  return config.sys.exit(1);
2268
2915
  }
2269
- const { prompt } = await import("prompts");
2270
- const input = flags.unknownArgs.find((arg) => !arg.startsWith("-")) || (await prompt({
2271
- name: "tagName",
2272
- type: "text",
2273
- message: "Component tag name (dash-case):"
2274
- })).tagName;
2275
- 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
+ }
2276
2929
  const { dir, base: componentName } = parse(input);
2277
2930
  const tagError = validateComponentTag(componentName);
2278
2931
  if (tagError) {
2279
2932
  config.logger.error(tagError);
2280
2933
  return config.sys.exit(1);
2281
2934
  }
2282
- let cssExtension = "css";
2283
- if (config.plugins?.find((plugin) => plugin.name === "sass")) cssExtension = await chooseSassExtension();
2284
- else if (config.plugins?.find((plugin) => plugin.name === "less")) cssExtension = "less";
2285
- const filesToGenerateExt = await chooseFilesToGenerate(cssExtension);
2286
- if (!filesToGenerateExt) return;
2287
- const extensionsToGenerate = ["tsx", ...filesToGenerateExt];
2288
- const testFolder = extensionsToGenerate.some(isTest) ? "test" : "";
2289
- const outDir = join(absoluteSrcDir, "components", dir, componentName);
2290
- await config.sys.createDir(normalizePath(join(outDir, testFolder)), { recursive: true });
2291
- const filesToGenerate = extensionsToGenerate.map((extension) => ({
2292
- extension,
2293
- path: getFilepathForFile(outDir, componentName, extension)
2294
- }));
2295
- await checkForOverwrite(filesToGenerate, config);
2296
- 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));
2297
- if (!writtenFiles) return config.sys.exit(1);
2298
- console.log();
2299
- console.log(`${config.logger.gray("$")} stencil generate ${input}`);
2300
- console.log();
2301
- console.log(config.logger.bold("The following files have been generated:"));
2302
- const absoluteRootDir = config.rootDir;
2303
- writtenFiles.map((file) => console.log(` - ${relative(absoluteRootDir, file)}`));
2304
- };
2305
- /**
2306
- * Show a checkbox prompt to select the files to be generated.
2307
- *
2308
- * @param cssExtension the extension of the CSS file to be generated
2309
- * @returns a read-only array of `GeneratableExtension`, the extensions that the user has decided
2310
- * to generate
2311
- */
2312
- const chooseFilesToGenerate = async (cssExtension) => {
2313
- const { prompt } = await import("prompts");
2314
- return (await prompt({
2315
- name: "filesToGenerate",
2316
- type: "multiselect",
2317
- message: "Which additional files do you want to generate?",
2318
- choices: [
2319
- {
2320
- value: cssExtension,
2321
- title: `Stylesheet (.${cssExtension})`,
2322
- selected: true
2323
- },
2324
- {
2325
- value: "spec.tsx",
2326
- title: "Spec Test (.spec.tsx)",
2327
- selected: true
2328
- },
2329
- {
2330
- value: "e2e.ts",
2331
- title: "E2E Test (.e2e.ts)",
2332
- selected: true
2333
- }
2334
- ]
2335
- })).filesToGenerate;
2336
- };
2337
- const chooseSassExtension = async () => {
2338
- const { prompt } = await import("prompts");
2339
- return (await prompt({
2340
- name: "sassFormat",
2341
- type: "select",
2342
- message: "Which Sass format would you like to use? (More info: https://sass-lang.com/documentation/syntax/#the-indented-syntax)",
2343
- choices: [{
2344
- value: "sass",
2345
- title: `*.sass Format`,
2346
- selected: true
2347
- }, {
2348
- value: "scss",
2349
- title: "*.scss Format"
2350
- }]
2351
- })).sassFormat;
2352
- };
2353
- /**
2354
- * Get a filepath for a file we want to generate!
2355
- *
2356
- * The filepath for a given file depends on the path, the user-supplied
2357
- * component name, the extension, and whether we're inside of a test directory.
2358
- *
2359
- * @param filePath path to where we're going to generate the component
2360
- * @param componentName the user-supplied name for the generated component
2361
- * @param extension the file extension
2362
- * @returns the full filepath to the component (with a possible `test` directory
2363
- * added)
2364
- */
2365
- const getFilepathForFile = (filePath, componentName, extension) => isTest(extension) ? normalizePath(join(filePath, "test", `${componentName}.${extension}`)) : normalizePath(join(filePath, `${componentName}.${extension}`));
2366
- /**
2367
- * Get the boilerplate for a file and write it to disk
2368
- *
2369
- * @param config the current config, needed for file operations
2370
- * @param componentName the component name (user-supplied)
2371
- * @param withCss are we generating CSS?
2372
- * @param file the file we want to write
2373
- * @param styleExtension extension used for styles
2374
- * @returns a `Promise<string>` which holds the full filepath we've written to,
2375
- * used to print out a little summary of our activity to the user.
2376
- */
2377
- const getBoilerplateAndWriteFile = async (config, componentName, withCss, file, styleExtension) => {
2378
- const boilerplate = getBoilerplateByExtension(componentName, file.extension, withCss, styleExtension);
2379
- await config.sys.writeFile(normalizePath(file.path), boilerplate);
2380
- return file.path;
2381
- };
2382
- /**
2383
- * Check to see if any of the files we plan to write already exist and would
2384
- * therefore be overwritten if we proceed, because we'd like to not overwrite
2385
- * people's code!
2386
- *
2387
- * This function will check all the filepaths and if it finds any files log an
2388
- * error and exit with an error code. If it doesn't find anything it will just
2389
- * peacefully return `Promise<void>`.
2390
- *
2391
- * @param files the files we want to check
2392
- * @param config the Config object, used here to get access to `sys.readFile`
2393
- */
2394
- const checkForOverwrite = async (files, config) => {
2395
- const alreadyPresent = [];
2396
- await Promise.all(files.map(async ({ path }) => {
2397
- if (await config.sys.readFile(path) !== void 0) alreadyPresent.push(path);
2398
- }));
2399
- if (alreadyPresent.length > 0) {
2400
- config.logger.error("Generating code would overwrite the following files:", ...alreadyPresent.map((path) => " " + normalizePath(path)));
2401
- 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;
2402
2969
  }
2403
- };
2404
- /**
2405
- * Check if an extension is for a test
2406
- *
2407
- * @param extension the extension we want to check
2408
- * @returns a boolean indicating whether or not its a test
2409
- */
2410
- const isTest = (extension) => {
2411
- return extension === "e2e.ts" || extension === "spec.tsx";
2412
- };
2413
- /**
2414
- * Get the boilerplate for a file by its extension.
2415
- *
2416
- * @param tagName the name of the component we're generating
2417
- * @param extension the file extension we want boilerplate for (.css, tsx, etc)
2418
- * @param withCss a boolean indicating whether we're generating a CSS file
2419
- * @param styleExtension extension used for styles
2420
- * @returns a string container the file boilerplate for the supplied extension
2421
- */
2422
- const getBoilerplateByExtension = (tagName, extension, withCss, styleExtension) => {
2423
- switch (extension) {
2424
- case "tsx": return getComponentBoilerplate(tagName, withCss, styleExtension);
2425
- case "css":
2426
- case "less":
2427
- case "sass":
2428
- case "scss": return getStyleUrlBoilerplate(styleExtension);
2429
- case "spec.tsx": return getSpecTestBoilerplate(tagName);
2430
- case "e2e.ts": return getE2eTestBoilerplate(tagName);
2431
- 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
+ });
2432
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}`);
2433
3000
  };
2434
- /**
2435
- * Get the boilerplate for a file containing the definition of a component
2436
- * @param tagName the name of the tag to give the component
2437
- * @param hasStyle designates if the component has an external stylesheet or not
2438
- * @param styleExtension extension used for styles
2439
- * @returns the contents of a file that defines a component
2440
- */
2441
- const getComponentBoilerplate = (tagName, hasStyle, styleExtension) => {
2442
- const decorator = [`{`];
2443
- decorator.push(` tag: '${tagName}',`);
2444
- if (hasStyle) decorator.push(` styleUrl: '${tagName}.${styleExtension}',`);
2445
- decorator.push(` shadow: true,`);
2446
- decorator.push(`}`);
2447
- return `import { Component, Host, h } from '@stencil/core';
2448
-
2449
- @Component(${decorator.join("\n")})
2450
- export class ${toPascalCase(tagName)} {
2451
- render() {
2452
- return (
2453
- <Host>
2454
- <slot></slot>
2455
- </Host>
2456
- );
2457
- }
2458
- }
2459
- `;
2460
- };
2461
- /**
2462
- * Get the boilerplate for style for a generated component
2463
- * @param ext extension used for styles
2464
- * @returns a boilerplate CSS block
2465
- */
2466
- const getStyleUrlBoilerplate = (ext) => ext === "sass" ? `:host
2467
- display: block
2468
- ` : `:host {
2469
- display: block;
2470
- }
2471
- `;
2472
- /**
2473
- * Get the boilerplate for a file containing a spec (unit) test for a component
2474
- * @param tagName the name of the tag associated with the component under test
2475
- * @returns the contents of a file that unit tests a component
2476
- */
2477
- const getSpecTestBoilerplate = (tagName) => `import { newSpecPage } from '@stencil/core/testing';
2478
- import { ${toPascalCase(tagName)} } from '../${tagName}';
2479
-
2480
- describe('${tagName}', () => {
2481
- it('renders', async () => {
2482
- const page = await newSpecPage({
2483
- components: [${toPascalCase(tagName)}],
2484
- html: \`<${tagName}></${tagName}>\`,
2485
- });
2486
- expect(page.root).toEqualHtml(\`
2487
- <${tagName}>
2488
- <mock:shadow-root>
2489
- <slot></slot>
2490
- </mock:shadow-root>
2491
- </${tagName}>
2492
- \`);
2493
- });
2494
- });
2495
- `;
2496
- /**
2497
- * Get the boilerplate for a file containing an end-to-end (E2E) test for a component
2498
- * @param tagName the name of the tag associated with the component under test
2499
- * @returns the contents of a file that E2E tests a component
2500
- */
2501
- const getE2eTestBoilerplate = (tagName) => `import { newE2EPage } from '@stencil/core/testing';
2502
-
2503
- describe('${tagName}', () => {
2504
- it('renders', async () => {
2505
- const page = await newE2EPage();
2506
- await page.setContent('<${tagName}></${tagName}>');
2507
-
2508
- const element = await page.find('${tagName}');
2509
- expect(element).toHaveClass('hydrated');
2510
- });
2511
- });
2512
- `;
2513
- /**
2514
- * Convert a dash case string to pascal case.
2515
- * @param str the string to convert
2516
- * @returns the converted input as pascal case
2517
- */
2518
- const toPascalCase = (str) => str.split("-").reduce((res, part) => res + part[0].toUpperCase() + part.slice(1), "");
2519
3001
  //#endregion
2520
3002
  //#region src/task-telemetry.ts
2521
3003
  /**
@@ -2629,6 +3111,317 @@ const taskInfo = (coreCompiler, sys, logger) => {
2629
3111
  console.log(``);
2630
3112
  };
2631
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
2632
3425
  //#region src/task-serve.ts
2633
3426
  const taskServe = async (config, flags) => {
2634
3427
  config.suppressLogs = true;
@@ -2684,6 +3477,10 @@ const run = async (init) => {
2684
3477
  }), logger, sys);
2685
3478
  return;
2686
3479
  }
3480
+ if (task === "init") {
3481
+ await taskInit();
3482
+ return;
3483
+ }
2687
3484
  startupLog(logger, task);
2688
3485
  const findConfigResults = await findConfig({
2689
3486
  sys,
@@ -2704,7 +3501,7 @@ const run = async (init) => {
2704
3501
  const configWithFlags = mergeFlags({}, flags);
2705
3502
  const validated = await coreCompiler.loadConfig({
2706
3503
  config: configWithFlags,
2707
- configPath: foundConfig.configPath,
3504
+ configPath: foundConfig.configPath ?? void 0,
2708
3505
  logger,
2709
3506
  sys
2710
3507
  });
@@ -2754,6 +3551,9 @@ const runTask = async (coreCompiler, config, task, sys, flags) => {
2754
3551
  case "help":
2755
3552
  await taskHelp(resolvedFlags, strictConfig.logger, sys);
2756
3553
  break;
3554
+ case "init":
3555
+ await taskInit();
3556
+ break;
2757
3557
  case "migrate":
2758
3558
  await taskMigrate(coreCompiler, strictConfig, resolvedFlags);
2759
3559
  break;