@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.d.mts +97 -6
- package/dist/index.mjs +1088 -288
- package/package.json +12 -9
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,
|
|
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)
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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` → `
|
|
1449
|
+
* - Renames `dist-collection` → `collection`
|
|
1127
1450
|
* - Renames `dist-types` → `types`
|
|
1128
|
-
* - Extracts `collectionDir` from loader-bundle into separate `
|
|
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": "
|
|
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" ? "
|
|
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": "
|
|
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: '
|
|
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
|
|
1706
|
-
const defaultConfig = (sys) => sys.resolvePath(`${sys.homeDir()}/.ionic/${isTest
|
|
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
|
|
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
|
-
|
|
2777
|
+
options: [
|
|
2216
2778
|
{
|
|
2217
|
-
title: "Run migration",
|
|
2218
2779
|
value: "run",
|
|
2219
|
-
|
|
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
|
-
|
|
2785
|
+
label: "Dry run",
|
|
2786
|
+
hint: "Preview changes without modifying files"
|
|
2225
2787
|
},
|
|
2226
2788
|
{
|
|
2227
|
-
title: "Exit",
|
|
2228
2789
|
value: "exit",
|
|
2229
|
-
|
|
2790
|
+
label: "Exit",
|
|
2791
|
+
hint: "Exit without making changes"
|
|
2230
2792
|
}
|
|
2231
2793
|
]
|
|
2232
2794
|
});
|
|
2233
|
-
if (
|
|
2234
|
-
return
|
|
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/
|
|
2810
|
+
//#region src/wizard/clack.ts
|
|
2249
2811
|
/**
|
|
2250
|
-
*
|
|
2251
|
-
*
|
|
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
|
|
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
|
|
2265
|
-
if (!
|
|
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
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
const
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
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
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
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;
|