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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import * as _$_stencil_core_compiler0 from "@stencil/core/compiler";
1
+ import * as d from "@stencil/core/compiler";
2
2
  import { LogLevel } from "@stencil/core/compiler";
3
3
 
4
4
  //#region src/types.d.ts
@@ -11,7 +11,7 @@ type TaskCommand = 'build' | 'docs' | 'generate' | 'g' | 'help' | 'info' | 'migr
11
11
  /**
12
12
  * All the Boolean options supported by the Stencil CLI
13
13
  */
14
- declare const BOOLEAN_CLI_FLAGS: readonly ["build", "cache", "checkVersion", "ci", "compare", "debug", "dev", "devtools", "docs", "dryRun", "esm", "help", "log", "open", "prerender", "prerenderExternal", "prod", "profile", "serviceWorker", "serve", "skipNodeCheck", "ssr", "verbose", "version", "watch"];
14
+ declare const BOOLEAN_CLI_FLAGS: readonly ["build", "cache", "checkVersion", "ci", "compare", "debug", "dev", "devtools", "docs", "dryRun", "help", "log", "open", "prerender", "prerenderExternal", "profile", "serviceWorker", "serve", "skipNodeCheck", "ssr", "verbose", "version", "watch"];
15
15
  /**
16
16
  * All the Number options supported by the Stencil CLI
17
17
  */
@@ -123,7 +123,7 @@ declare const createConfigFlags: (init?: Partial<ConfigFlags>) => ConfigFlags;
123
123
  declare const parseFlags: (args: string[]) => ConfigFlags;
124
124
  //#endregion
125
125
  //#region src/load-compiler.d.ts
126
- type CoreCompiler = typeof _$_stencil_core_compiler0;
126
+ type CoreCompiler = typeof import('@stencil/core/compiler');
127
127
  //#endregion
128
128
  //#region src/run.d.ts
129
129
  /**
@@ -136,7 +136,7 @@ type CoreCompiler = typeof _$_stencil_core_compiler0;
136
136
  * @param init initial CLI options
137
137
  * @returns an empty promise
138
138
  */
139
- declare const run: (init: _$_stencil_core_compiler0.CliInitOptions) => Promise<any>;
139
+ declare const run: (init: d.CliInitOptions) => Promise<any>;
140
140
  /**
141
141
  * Run a specified task
142
142
  *
@@ -148,6 +148,6 @@ declare const run: (init: _$_stencil_core_compiler0.CliInitOptions) => Promise<a
148
148
  * @public
149
149
  * @returns a void promise
150
150
  */
151
- declare const runTask: (coreCompiler: CoreCompiler, config: _$_stencil_core_compiler0.Config, task: TaskCommand, sys: _$_stencil_core_compiler0.CompilerSystem, flags?: ConfigFlags) => Promise<void>;
151
+ declare const runTask: (coreCompiler: CoreCompiler, config: d.Config, task: TaskCommand, sys: d.CompilerSystem, flags?: ConfigFlags) => Promise<void>;
152
152
  //#endregion
153
153
  export { BOOLEAN_CLI_FLAGS, type ConfigFlags, type TaskCommand, createConfigFlags, parseFlags, run, runTask };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { LOG_LEVELS } from "@stencil/core/compiler";
2
- import { buildError, catchError, hasError, isFunction, isOutputTargetDocs, isOutputTargetHydrate, isOutputTargetWww, isString, normalizePath, readOnlyArrayHasStringMember, result, shouldIgnoreError, toCamelCase, validateComponentTag } from "@stencil/core/compiler/utils";
3
- import { isAbsolute, join, parse, relative } from "path";
2
+ import { buildError, catchError, hasError, isFunction, isOutputTargetDocs, isOutputTargetSsr, isOutputTargetWww, isString, normalizePath, readOnlyArrayHasStringMember, result, shouldIgnoreError, toCamelCase, validateComponentTag } from "@stencil/core/compiler/utils";
3
+ import { dirname, isAbsolute, join, parse, relative } from "path";
4
4
  import ts from "typescript";
5
5
  import { start } from "@stencil/dev-server";
6
6
  //#region src/config-flags.ts
@@ -18,13 +18,11 @@ const BOOLEAN_CLI_FLAGS = [
18
18
  "devtools",
19
19
  "docs",
20
20
  "dryRun",
21
- "esm",
22
21
  "help",
23
22
  "log",
24
23
  "open",
25
24
  "prerender",
26
25
  "prerenderExternal",
27
- "prod",
28
26
  "profile",
29
27
  "serviceWorker",
30
28
  "serve",
@@ -561,7 +559,7 @@ const startupCompilerLog = (coreCompiler, config) => {
561
559
  *
562
560
  * This function applies command-line flags to the config, with CLI flags
563
561
  * taking precedence over config file values. This is the canonical place
564
- * where flag values are translated into config properties.?
562
+ * where flag values are translated into config properties.
565
563
  *
566
564
  * @param config The config object (from stencil.config.ts or empty)
567
565
  * @param flags The parsed CLI flags
@@ -569,13 +567,11 @@ const startupCompilerLog = (coreCompiler, config) => {
569
567
  */
570
568
  const mergeFlags = (config, flags) => {
571
569
  const merged = { ...config };
572
- if (flags.prod === true) merged.devMode = false;
573
- else if (flags.dev === true) merged.devMode = true;
570
+ if (flags.dev === true) merged.devMode = true;
574
571
  if (flags.debug === true || flags.verbose === true) merged.logLevel = "debug";
575
572
  else if (flags.logLevel) merged.logLevel = flags.logLevel;
576
573
  if (typeof flags.watch === "boolean") merged.watch = flags.watch;
577
- if (typeof flags.docs === "boolean") merged.buildDocs = flags.docs;
578
- if (typeof flags.esm === "boolean") merged.buildDist = flags.esm;
574
+ if (flags.docs === true) merged._docsFlag = true;
579
575
  if (typeof flags.profile === "boolean") merged.profile = flags.profile;
580
576
  if (typeof flags.log === "boolean") merged.writeLog = flags.log;
581
577
  if (typeof flags.cache === "boolean") merged.enableCache = flags.cache;
@@ -618,6 +614,164 @@ const printCheckVersionResults = async (versionChecker) => {
618
614
  }
619
615
  };
620
616
  //#endregion
617
+ //#region src/migrations/rules/build-dist-docs.ts
618
+ /**
619
+ * Migration rule for buildDist and buildDocs removal.
620
+ *
621
+ * In Stencil v5, these global config options are replaced with per-output-target
622
+ * `skipInDev` property. This migration:
623
+ * - Detects `buildDist` or `buildDocs` in stencil.config.ts
624
+ * - Removes the property
625
+ * - For `buildDist: true` or `buildDocs: true`, adds `skipInDev: false` to relevant output targets
626
+ */
627
+ const buildDistDocsRule = {
628
+ id: "build-dist-docs",
629
+ name: "buildDist/buildDocs Removal",
630
+ description: "Remove deprecated buildDist and buildDocs config options",
631
+ fromVersion: "4.x",
632
+ toVersion: "5.x",
633
+ detect(sourceFile) {
634
+ const matches = [];
635
+ const visit = (node) => {
636
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
637
+ const propName = node.name.text;
638
+ if (propName === "buildDist" || propName === "buildDocs") {
639
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
640
+ const isTrue = node.initializer.kind === ts.SyntaxKind.TrueKeyword;
641
+ matches.push({
642
+ node,
643
+ message: `Deprecated '${propName}' found${isTrue ? " (set to true)" : ""} - will be removed and skipInDev added to output targets`,
644
+ line: line + 1,
645
+ column: character + 1
646
+ });
647
+ }
648
+ }
649
+ ts.forEachChild(node, visit);
650
+ };
651
+ visit(sourceFile);
652
+ return matches;
653
+ },
654
+ transform(sourceFile, matches) {
655
+ if (matches.length === 0) return sourceFile.getFullText();
656
+ let text = sourceFile.getFullText();
657
+ const targetTypesNeedingSkipInDev = [];
658
+ for (const match of matches) {
659
+ const prop = match.node;
660
+ const propName = prop.name.text;
661
+ if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
662
+ if (propName === "buildDist") targetTypesNeedingSkipInDev.push("dist", "dist-custom-elements", "dist-hydrate-script", "loader-bundle", "standalone", "ssr");
663
+ else if (propName === "buildDocs") targetTypesNeedingSkipInDev.push("docs-readme", "docs-json", "docs-custom", "docs-vscode", "docs-custom-elements-manifest");
664
+ }
665
+ }
666
+ const outputTargetsToModify = [];
667
+ const findOutputTargets = (node) => {
668
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "outputTargets" && ts.isArrayLiteralExpression(node.initializer)) {
669
+ for (const element of node.initializer.elements) if (ts.isObjectLiteralExpression(element)) {
670
+ const typeProp = element.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type");
671
+ if (typeProp && ts.isStringLiteral(typeProp.initializer)) {
672
+ const targetType = typeProp.initializer.text;
673
+ if (targetTypesNeedingSkipInDev.includes(targetType)) {
674
+ if (!element.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "skipInDev")) outputTargetsToModify.push(element);
675
+ }
676
+ }
677
+ }
678
+ }
679
+ ts.forEachChild(node, findOutputTargets);
680
+ };
681
+ findOutputTargets(sourceFile);
682
+ const allModifications = [];
683
+ for (const match of matches) {
684
+ const prop = match.node;
685
+ const start = prop.getStart();
686
+ let end = prop.getEnd();
687
+ const afterProp = text.slice(end).match(/^\s*,/);
688
+ if (afterProp) end = end + afterProp[0].length;
689
+ allModifications.push({
690
+ type: "remove",
691
+ start,
692
+ end,
693
+ node: prop
694
+ });
695
+ }
696
+ for (const target of outputTargetsToModify) {
697
+ const lastProp = target.properties[target.properties.length - 1];
698
+ if (lastProp) allModifications.push({
699
+ type: "addSkipInDev",
700
+ start: lastProp.getEnd(),
701
+ end: lastProp.getEnd(),
702
+ node: target
703
+ });
704
+ }
705
+ allModifications.sort((a, b) => b.start - a.start);
706
+ for (const mod of allModifications) if (mod.type === "remove") text = text.slice(0, mod.start) + text.slice(mod.end);
707
+ else if (mod.type === "addSkipInDev") {
708
+ const firstProp = mod.node.properties[0];
709
+ let indent = " ";
710
+ if (firstProp) {
711
+ const propStart = firstProp.getStart();
712
+ const lineStart = text.lastIndexOf("\n", propStart) + 1;
713
+ const leadingWhitespace = text.slice(lineStart, propStart);
714
+ if (/^\s+$/.test(leadingWhitespace)) indent = leadingWhitespace;
715
+ }
716
+ const afterLastProp = text.slice(mod.start).match(/^(\s*,)?/);
717
+ const skipLength = afterLastProp && afterLastProp[1] ? afterLastProp[0].length : 0;
718
+ text = text.slice(0, mod.start) + `,\n${indent}skipInDev: false,` + text.slice(mod.start + skipLength);
719
+ }
720
+ text = text.replace(/,\s*\n\s*\n/g, ",\n");
721
+ text = text.replace(/{\s*\n\s*\n/g, "{\n");
722
+ return text;
723
+ }
724
+ };
725
+ //#endregion
726
+ //#region src/migrations/rules/dev-mode.ts
727
+ /**
728
+ * Migration rule for `devMode` config option removal.
729
+ *
730
+ * In Stencil v5, `devMode` is no longer a user-settable config option in `stencil.config.ts`.
731
+ * Build mode is controlled exclusively by the `--dev` CLI flag (default is production).
732
+ * This migration removes `devMode` from the config object.
733
+ */
734
+ const devModeRule = {
735
+ id: "dev-mode",
736
+ name: "devMode Config Removal",
737
+ description: "Remove deprecated devMode config option - use --dev CLI flag instead",
738
+ fromVersion: "4.x",
739
+ toVersion: "5.x",
740
+ detect(sourceFile) {
741
+ const matches = [];
742
+ const visit = (node) => {
743
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
744
+ if (node.name.text === "devMode") {
745
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
746
+ matches.push({
747
+ node,
748
+ message: `Deprecated 'devMode' config option found - build mode is now set via the --dev CLI flag (production is the default)`,
749
+ line: line + 1,
750
+ column: character + 1
751
+ });
752
+ }
753
+ }
754
+ ts.forEachChild(node, visit);
755
+ };
756
+ visit(sourceFile);
757
+ return matches;
758
+ },
759
+ transform(sourceFile, matches) {
760
+ if (matches.length === 0) return sourceFile.getFullText();
761
+ let text = sourceFile.getFullText();
762
+ for (const match of [...matches].reverse()) {
763
+ const node = match.node;
764
+ const start = node.getFullStart();
765
+ const end = node.getEnd();
766
+ let removeEnd = end;
767
+ const trailingComma = text.slice(end).match(/^\s*,/);
768
+ if (trailingComma) removeEnd = end + trailingComma[0].length;
769
+ text = text.slice(0, start) + text.slice(removeEnd);
770
+ }
771
+ return text;
772
+ }
773
+ };
774
+ //#endregion
621
775
  //#region src/migrations/rules/encapsulation-api.ts
622
776
  /**
623
777
  * Migration rule for the @Component encapsulation API change.
@@ -699,6 +853,57 @@ const encapsulationApiRule = {
699
853
  }
700
854
  };
701
855
  //#endregion
856
+ //#region src/migrations/rules/external-runtime.ts
857
+ /**
858
+ * Migration rule for `externalRuntime` on `standalone` output targets.
859
+ *
860
+ * In Stencil v5, `externalRuntime` defaults to `false` (was `true` in v4).
861
+ * Explicit `externalRuntime: false` is now redundant and can be removed.
862
+ */
863
+ const externalRuntimeRule = {
864
+ id: "external-runtime",
865
+ name: "externalRuntime Default Change",
866
+ description: "Remove redundant 'externalRuntime: false' from standalone output targets - false is now the default",
867
+ fromVersion: "4.x",
868
+ toVersion: "5.x",
869
+ detect(sourceFile) {
870
+ const matches = [];
871
+ const visit = (node) => {
872
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "externalRuntime" && node.initializer.kind === ts.SyntaxKind.FalseKeyword) {
873
+ const parent = node.parent;
874
+ if (ts.isObjectLiteralExpression(parent)) {
875
+ if (parent.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type" && ts.isStringLiteral(p.initializer) && p.initializer.text === "standalone")) {
876
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
877
+ matches.push({
878
+ node,
879
+ message: "'externalRuntime: false' is now the default - this property can be removed",
880
+ line: line + 1,
881
+ column: character + 1
882
+ });
883
+ }
884
+ }
885
+ }
886
+ ts.forEachChild(node, visit);
887
+ };
888
+ visit(sourceFile);
889
+ return matches;
890
+ },
891
+ transform(sourceFile, matches) {
892
+ if (matches.length === 0) return sourceFile.getFullText();
893
+ let text = sourceFile.getFullText();
894
+ for (const match of [...matches].reverse()) {
895
+ const node = match.node;
896
+ const start = node.getFullStart();
897
+ const end = node.getEnd();
898
+ let removeEnd = end;
899
+ const trailingComma = text.slice(end).match(/^\s*,/);
900
+ if (trailingComma) removeEnd = end + trailingComma[0].length;
901
+ text = text.slice(0, start) + text.slice(removeEnd);
902
+ }
903
+ return text;
904
+ }
905
+ };
906
+ //#endregion
702
907
  //#region src/migrations/rules/form-associated.ts
703
908
  /**
704
909
  * Migration rule for formAssociated → @AttachInternals.
@@ -820,6 +1025,750 @@ const formAssociatedRule = {
820
1025
  }
821
1026
  };
822
1027
  //#endregion
1028
+ //#region src/migrations/rules/global-style-inject.ts
1029
+ /**
1030
+ * Migration rule for `extras.addGlobalStyleToComponents` → `global-style` output target `inject`.
1031
+ *
1032
+ * In v5, the `addGlobalStyleToComponents` extras option is removed. Instead, configure
1033
+ * the `inject` option on the `global-style` output target.
1034
+ *
1035
+ * Migration mapping:
1036
+ * - `addGlobalStyleToComponents: true` → `inject: 'all'`
1037
+ * - `addGlobalStyleToComponents: 'client'` → `inject: 'client'`
1038
+ * - `addGlobalStyleToComponents: false` → omit (default is 'none')
1039
+ *
1040
+ * Note: This migration only acts on explicitly set `addGlobalStyleToComponents`.
1041
+ * Users who relied on the implicit v4 default ('client') will need to manually
1042
+ * add `inject: 'client'` to their global-style output target if desired.
1043
+ */
1044
+ const globalStyleInjectRule = {
1045
+ id: "global-style-inject",
1046
+ name: "Global Style Inject Migration",
1047
+ description: "Migrate extras.addGlobalStyleToComponents to global-style output target inject option",
1048
+ fromVersion: "4.x",
1049
+ toVersion: "5.x",
1050
+ detect(sourceFile) {
1051
+ const matches = [];
1052
+ const visit = (node) => {
1053
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "addGlobalStyleToComponents") {
1054
+ const parent = node.parent;
1055
+ if (ts.isObjectLiteralExpression(parent)) {
1056
+ const grandparent = parent.parent;
1057
+ if (ts.isPropertyAssignment(grandparent) && ts.isIdentifier(grandparent.name) && grandparent.name.text === "extras") {
1058
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1059
+ matches.push({
1060
+ node,
1061
+ message: `extras.addGlobalStyleToComponents is removed. Use 'inject' on global-style output target instead.`,
1062
+ line: line + 1,
1063
+ column: character + 1
1064
+ });
1065
+ }
1066
+ }
1067
+ }
1068
+ ts.forEachChild(node, visit);
1069
+ };
1070
+ visit(sourceFile);
1071
+ return matches;
1072
+ },
1073
+ transform(sourceFile, matches) {
1074
+ if (matches.length === 0) return sourceFile.getFullText();
1075
+ let text = sourceFile.getFullText();
1076
+ const matchNode = matches[0].node;
1077
+ if (!ts.isPropertyAssignment(matchNode)) return text;
1078
+ let injectValue = null;
1079
+ if (matchNode.initializer.kind === ts.SyntaxKind.TrueKeyword) injectValue = "all";
1080
+ else if (ts.isStringLiteral(matchNode.initializer) && matchNode.initializer.text === "client") injectValue = "client";
1081
+ let insertionOffset = 0;
1082
+ if (injectValue) {
1083
+ const result = addGlobalStyleOutputTarget(text, sourceFile, injectValue);
1084
+ insertionOffset = result.length - text.length;
1085
+ text = result;
1086
+ }
1087
+ let start = matchNode.getStart();
1088
+ let end = matchNode.getEnd();
1089
+ const outputTargetsEnd = findOutputTargetsEnd(sourceFile);
1090
+ if (insertionOffset !== 0 && outputTargetsEnd !== null && matchNode.getStart() > outputTargetsEnd) {
1091
+ start += insertionOffset;
1092
+ end += insertionOffset;
1093
+ }
1094
+ const afterProp = text.slice(end).match(/^\s*,/);
1095
+ if (afterProp) end = end + afterProp[0].length;
1096
+ else {
1097
+ const beforeProp = text.slice(0, start).match(/,\s*$/);
1098
+ if (beforeProp) {
1099
+ text = text.slice(0, start - beforeProp[0].length) + text.slice(end);
1100
+ end = -1;
1101
+ }
1102
+ }
1103
+ if (end !== -1) text = text.slice(0, start) + text.slice(end);
1104
+ text = cleanupEmptyExtras$1(text);
1105
+ return text;
1106
+ }
1107
+ };
1108
+ /**
1109
+ * Find the end position of the outputTargets array in the source file.
1110
+ * @param sourceFile The source file to search
1111
+ * @returns The end position of the outputTargets array, or null if not found
1112
+ */
1113
+ function findOutputTargetsEnd(sourceFile) {
1114
+ let endPos = null;
1115
+ const visit = (node) => {
1116
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "outputTargets") endPos = node.getEnd();
1117
+ ts.forEachChild(node, visit);
1118
+ };
1119
+ visit(sourceFile);
1120
+ return endPos;
1121
+ }
1122
+ /**
1123
+ * Clean up empty extras object after removing addGlobalStyleToComponents.
1124
+ * @param text The source text to clean up
1125
+ * @returns The cleaned up text with empty extras removed
1126
+ */
1127
+ function cleanupEmptyExtras$1(text) {
1128
+ return text.replace(/,?\s*extras\s*:\s*\{\s*\},?/g, "");
1129
+ }
1130
+ /**
1131
+ * Add a global-style output target with the specified inject value.
1132
+ * @param text The source text to modify
1133
+ * @param sourceFile The source file for position info
1134
+ * @param injectValue The inject value to set on the new global-style output target
1135
+ * @returns The modified text with the new global-style output target added
1136
+ */
1137
+ function addGlobalStyleOutputTarget(text, sourceFile, injectValue) {
1138
+ if (text.includes("type: 'global-style'")) return text;
1139
+ let outputTargetsArray = null;
1140
+ const findOutputTargets = (node) => {
1141
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "outputTargets" && ts.isArrayLiteralExpression(node.initializer)) outputTargetsArray = node.initializer;
1142
+ ts.forEachChild(node, findOutputTargets);
1143
+ };
1144
+ findOutputTargets(sourceFile);
1145
+ if (!outputTargetsArray) return text;
1146
+ const array = outputTargetsArray;
1147
+ let indent = " ";
1148
+ if (array.elements.length > 0) {
1149
+ const elemStart = array.elements[0].getStart();
1150
+ const lineStart = text.lastIndexOf("\n", elemStart) + 1;
1151
+ const leadingWhitespace = text.slice(lineStart, elemStart);
1152
+ if (/^\s+$/.test(leadingWhitespace)) indent = leadingWhitespace;
1153
+ }
1154
+ const lastElement = array.elements[array.elements.length - 1];
1155
+ if (!lastElement) {
1156
+ const arrayStart = array.getStart() + 1;
1157
+ const newTarget = `\n${indent}{\n${indent} type: 'global-style',\n${indent} inject: '${injectValue}',\n${indent}}\n${indent.slice(2)}`;
1158
+ return text.slice(0, arrayStart) + newTarget + text.slice(arrayStart);
1159
+ }
1160
+ let insertPos = lastElement.getEnd();
1161
+ const afterLastElement = text.slice(insertPos).match(/^(\s*,)?/);
1162
+ const hasTrailingComma = afterLastElement && afterLastElement[1];
1163
+ if (hasTrailingComma) insertPos += afterLastElement[0].length;
1164
+ const newTarget = `{\n${indent} type: 'global-style',\n${indent} inject: '${injectValue}',\n${indent}}`;
1165
+ const insertion = (hasTrailingComma ? "" : ",") + "\n" + indent + newTarget;
1166
+ return text.slice(0, insertPos) + insertion + text.slice(insertPos);
1167
+ }
1168
+ //#endregion
1169
+ //#region src/migrations/rules/hash-file-names.ts
1170
+ /**
1171
+ * Migration rule for `hashFileNames` and `hashedFileNameLength` config options.
1172
+ *
1173
+ * In Stencil v5, these are no longer top-level config options. They belong on
1174
+ * the `loader-bundle` and `www` output targets, which are the only outputs that
1175
+ * are served directly in the browser and benefit from content-hash caching.
1176
+ *
1177
+ * This migration:
1178
+ * 1. Removes them from the top-level config
1179
+ * 2. Injects them into any `loader-bundle` and `www` output targets found in the same file
1180
+ */
1181
+ const hashFileNamesRule = {
1182
+ id: "hash-file-names",
1183
+ name: "Hash File Names Config Move",
1184
+ description: "Move hashFileNames and hashedFileNameLength from top-level config to loader-bundle and www output targets",
1185
+ fromVersion: "4.x",
1186
+ toVersion: "5.x",
1187
+ detect(sourceFile) {
1188
+ const matches = [];
1189
+ const visit = (node) => {
1190
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && (node.name.text === "hashFileNames" || node.name.text === "hashedFileNameLength")) {
1191
+ const parent = node.parent;
1192
+ if (ts.isObjectLiteralExpression(parent)) {
1193
+ if (!parent.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type")) {
1194
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1195
+ matches.push({
1196
+ node,
1197
+ message: `'${node.name.text}' is no longer a top-level config option. Move it to your 'loader-bundle' and/or 'www' output targets.`,
1198
+ line: line + 1,
1199
+ column: character + 1
1200
+ });
1201
+ }
1202
+ }
1203
+ }
1204
+ ts.forEachChild(node, visit);
1205
+ };
1206
+ visit(sourceFile);
1207
+ return matches;
1208
+ },
1209
+ transform(sourceFile, matches) {
1210
+ if (matches.length === 0) return sourceFile.getFullText();
1211
+ const fullText = sourceFile.getFullText();
1212
+ const propsToInject = matches.map((m) => {
1213
+ const node = m.node;
1214
+ return {
1215
+ name: node.name.text,
1216
+ value: node.initializer.getText(sourceFile)
1217
+ };
1218
+ });
1219
+ const TARGET_TYPES = new Set(["loader-bundle", "www"]);
1220
+ const insertionsByPos = /* @__PURE__ */ new Map();
1221
+ const visit = (node) => {
1222
+ if (ts.isObjectLiteralExpression(node)) {
1223
+ 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))) {
1224
+ const existingPropNames = new Set(node.properties.filter((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)).map((p) => p.name.text));
1225
+ const objStart = node.getStart(sourceFile);
1226
+ const objLineStart = fullText.lastIndexOf("\n", objStart) + 1;
1227
+ const propIndent = (fullText.slice(objLineStart, objStart).match(/^(\s*)/)?.[1] ?? "") + " ";
1228
+ const closingBracePos = node.getEnd() - 1;
1229
+ const isMultiLine = fullText.slice(objStart, node.getEnd()).includes("\n");
1230
+ let insertPos;
1231
+ if (isMultiLine) insertPos = fullText.lastIndexOf("\n", closingBracePos - 1);
1232
+ else {
1233
+ insertPos = closingBracePos;
1234
+ while (insertPos > 0 && fullText[insertPos - 1] === " ") insertPos--;
1235
+ }
1236
+ const existing = insertionsByPos.get(insertPos) ?? [];
1237
+ for (const prop of propsToInject) if (!existingPropNames.has(prop.name)) existing.push(isMultiLine ? `\n${propIndent}${prop.name}: ${prop.value},` : `, ${prop.name}: ${prop.value}`);
1238
+ if (existing.length > 0) insertionsByPos.set(insertPos, existing);
1239
+ }
1240
+ }
1241
+ ts.forEachChild(node, visit);
1242
+ };
1243
+ visit(sourceFile);
1244
+ const edits = [];
1245
+ for (const match of matches) {
1246
+ const node = match.node;
1247
+ const start = node.getFullStart();
1248
+ const end = node.getEnd();
1249
+ let removeEnd = end;
1250
+ const trailingComma = fullText.slice(end).match(/^\s*,/);
1251
+ if (trailingComma) removeEnd = end + trailingComma[0].length;
1252
+ edits.push({
1253
+ start,
1254
+ end: removeEnd,
1255
+ replacement: ""
1256
+ });
1257
+ }
1258
+ for (const [pos, insertions] of insertionsByPos) edits.push({
1259
+ start: pos,
1260
+ end: pos,
1261
+ replacement: insertions.join("")
1262
+ });
1263
+ edits.sort((a, b) => b.start - a.start);
1264
+ let text = fullText;
1265
+ for (const edit of edits) text = text.slice(0, edit.start) + edit.replacement + text.slice(edit.end);
1266
+ return text;
1267
+ }
1268
+ };
1269
+ //#endregion
1270
+ //#region src/migrations/rules/light-dom-patches.ts
1271
+ /**
1272
+ * Migration rule for slot-fix extras → `lightDomPatches`.
1273
+ *
1274
+ * In v5, the individual slot-fix flags and `experimentalSlotFixes` umbrella are replaced
1275
+ * by a single `lightDomPatches` option which defaults to `true`.
1276
+ *
1277
+ * Migration mapping:
1278
+ * - `experimentalSlotFixes: true` → remove (new default is `true`)
1279
+ * - `experimentalSlotFixes: false` → `lightDomPatches: false`
1280
+ * - Individual flags only → `lightDomPatches: { <new names> }`
1281
+ *
1282
+ * Old → new individual key names:
1283
+ * appendChildSlotFix → domMutations
1284
+ * cloneNodeFix → cloneNode
1285
+ * scopedSlotTextContentFix → textContent
1286
+ * slotChildNodesFix → childNodes
1287
+ */
1288
+ const lightDomPatchesRule = {
1289
+ id: "light-dom-patches",
1290
+ name: "Light DOM Patches Migration",
1291
+ description: "Migrate experimentalSlotFixes / individual slot-fix flags to lightDomPatches",
1292
+ fromVersion: "4.x",
1293
+ toVersion: "5.x",
1294
+ detect(sourceFile) {
1295
+ const matches = [];
1296
+ const oldKeys = new Set([
1297
+ "experimentalSlotFixes",
1298
+ "appendChildSlotFix",
1299
+ "cloneNodeFix",
1300
+ "scopedSlotTextContentFix",
1301
+ "slotChildNodesFix"
1302
+ ]);
1303
+ const visit = (node) => {
1304
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && oldKeys.has(node.name.text)) {
1305
+ const parent = node.parent;
1306
+ if (ts.isObjectLiteralExpression(parent)) {
1307
+ const grandparent = parent.parent;
1308
+ if (ts.isPropertyAssignment(grandparent) && ts.isIdentifier(grandparent.name) && grandparent.name.text === "extras") {
1309
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1310
+ matches.push({
1311
+ node,
1312
+ message: `extras.${node.name.text} is removed. Use 'extras.lightDomPatches' instead.`,
1313
+ line: line + 1,
1314
+ column: character + 1
1315
+ });
1316
+ }
1317
+ }
1318
+ }
1319
+ ts.forEachChild(node, visit);
1320
+ };
1321
+ visit(sourceFile);
1322
+ return matches;
1323
+ },
1324
+ transform(sourceFile, matches) {
1325
+ if (matches.length === 0) return sourceFile.getFullText();
1326
+ const oldKeyMap = {};
1327
+ for (const match of matches) {
1328
+ const node = match.node;
1329
+ const key = node.name.text;
1330
+ const init = node.initializer;
1331
+ oldKeyMap[key] = init.kind === ts.SyntaxKind.TrueKeyword ? true : init.kind === ts.SyntaxKind.FalseKeyword ? false : void 0;
1332
+ }
1333
+ const keyRename = {
1334
+ appendChildSlotFix: "domMutations",
1335
+ cloneNodeFix: "cloneNode",
1336
+ scopedSlotTextContentFix: "textContent",
1337
+ slotChildNodesFix: "childNodes"
1338
+ };
1339
+ const experimentalValue = oldKeyMap["experimentalSlotFixes"];
1340
+ const hasIndividualKeys = matches.some((m) => {
1341
+ return m.node.name.text !== "experimentalSlotFixes";
1342
+ });
1343
+ let replacement = null;
1344
+ if (experimentalValue === true && !hasIndividualKeys) replacement = null;
1345
+ else if (experimentalValue === false && !hasIndividualKeys) replacement = "lightDomPatches: false";
1346
+ else if (experimentalValue === true && hasIndividualKeys) replacement = null;
1347
+ else {
1348
+ const parts = [];
1349
+ for (const [oldKey, newKey] of Object.entries(keyRename)) {
1350
+ const val = oldKeyMap[oldKey];
1351
+ if (val === true) parts.push(`${newKey}: true`);
1352
+ else if (val === false) parts.push(`${newKey}: false`);
1353
+ }
1354
+ if (parts.length > 0) replacement = `lightDomPatches: { ${parts.join(", ")} }`;
1355
+ }
1356
+ const sorted = [...matches].sort((a, b) => b.node.getStart() - a.node.getStart());
1357
+ let text = sourceFile.getFullText();
1358
+ let replacementInserted = false;
1359
+ for (const match of sorted) {
1360
+ const node = match.node;
1361
+ let start = node.getStart();
1362
+ let end = node.getEnd();
1363
+ const trailingComma = text.slice(end).match(/^(\s*,)/);
1364
+ if (trailingComma) end += trailingComma[1].length;
1365
+ else {
1366
+ const leadingComma = text.slice(0, start).match(/,\s*$/);
1367
+ if (leadingComma) start -= leadingComma[0].length;
1368
+ }
1369
+ if (!replacementInserted && replacement !== null) {
1370
+ text = text.slice(0, start) + replacement + text.slice(end);
1371
+ replacementInserted = true;
1372
+ } else text = text.slice(0, start) + text.slice(end);
1373
+ }
1374
+ text = cleanupEmptyExtras(text);
1375
+ return text;
1376
+ }
1377
+ };
1378
+ function cleanupEmptyExtras(text) {
1379
+ return text.replace(/,?\s*extras\s*:\s*\{\s*\},?/g, "");
1380
+ }
1381
+ //#endregion
1382
+ //#region src/migrations/rules/output-target-renames.ts
1383
+ /**
1384
+ * Migration rule for output target renames in Stencil v5.
1385
+ *
1386
+ * This migration:
1387
+ * - Renames `dist` → `loader-bundle`
1388
+ * - Renames `dist-custom-elements` → `standalone`
1389
+ * - Renames `dist-hydrate-script` → `ssr`
1390
+ * - Renames `dist-collection` → `collection`
1391
+ * - Renames `dist-types` → `types`
1392
+ * - Extracts `collectionDir` from loader-bundle into separate `collection` output
1393
+ * - Extracts `typesDir` from loader-bundle into separate `types` output
1394
+ * - Renames `esmLoaderPath` → `loaderPath` (applies to all module formats, not just ESM)
1395
+ * - Removes `isPrimaryPackageOutputTarget` (no longer needed, package.json validation auto-detects)
1396
+ * - Removes `generateTypeDeclarations` (types are now auto-generated via the `types` output target)
1397
+ */
1398
+ const outputTargetRenamesRule = {
1399
+ id: "output-target-renames",
1400
+ name: "Output Target Renames",
1401
+ description: "Rename output targets to new v5 names",
1402
+ fromVersion: "4.x",
1403
+ toVersion: "5.x",
1404
+ detect(sourceFile) {
1405
+ const matches = [];
1406
+ const oldToNewNames = {
1407
+ dist: "loader-bundle",
1408
+ "dist-custom-elements": "standalone",
1409
+ "dist-hydrate-script": "ssr",
1410
+ "dist-collection": "collection",
1411
+ "dist-types": "types"
1412
+ };
1413
+ const visit = (node) => {
1414
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "type" && ts.isStringLiteral(node.initializer)) {
1415
+ const typeValue = node.initializer.text;
1416
+ if (typeValue in oldToNewNames) {
1417
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1418
+ matches.push({
1419
+ node,
1420
+ message: `Output target type '${typeValue}' → '${oldToNewNames[typeValue]}'`,
1421
+ line: line + 1,
1422
+ column: character + 1
1423
+ });
1424
+ }
1425
+ }
1426
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && (node.name.text === "collectionDir" || node.name.text === "typesDir")) {
1427
+ const parent = node.parent;
1428
+ if (ts.isObjectLiteralExpression(parent)) {
1429
+ if (parent.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type")) {
1430
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1431
+ const propName = node.name.text;
1432
+ const newType = propName === "collectionDir" ? "collection" : "types";
1433
+ matches.push({
1434
+ node,
1435
+ message: `Property '${propName}' will be extracted to separate '${newType}' output target`,
1436
+ line: line + 1,
1437
+ column: character + 1
1438
+ });
1439
+ }
1440
+ }
1441
+ }
1442
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "esmLoaderPath") {
1443
+ const parent = node.parent;
1444
+ if (ts.isObjectLiteralExpression(parent)) {
1445
+ if (parent.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type")) {
1446
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1447
+ matches.push({
1448
+ node,
1449
+ message: `Property 'esmLoaderPath' renamed to 'loaderPath' (applies to all module formats)`,
1450
+ line: line + 1,
1451
+ column: character + 1
1452
+ });
1453
+ }
1454
+ }
1455
+ }
1456
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && (node.name.text === "isPrimaryPackageOutputTarget" || node.name.text === "generateTypeDeclarations")) {
1457
+ const parent = node.parent;
1458
+ if (ts.isObjectLiteralExpression(parent)) {
1459
+ if (parent.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type")) {
1460
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1461
+ const propName = node.name.text;
1462
+ const reason = propName === "isPrimaryPackageOutputTarget" ? "Package.json validation now auto-detects based on configured outputs" : "Types are now auto-generated via the 'types' output target";
1463
+ matches.push({
1464
+ node,
1465
+ message: `Property '${propName}' is removed in v5. ${reason}`,
1466
+ line: line + 1,
1467
+ column: character + 1
1468
+ });
1469
+ }
1470
+ }
1471
+ }
1472
+ ts.forEachChild(node, visit);
1473
+ };
1474
+ visit(sourceFile);
1475
+ return matches;
1476
+ },
1477
+ transform(sourceFile, matches) {
1478
+ if (matches.length === 0) return sourceFile.getFullText();
1479
+ let text = sourceFile.getFullText();
1480
+ const oldToNewNames = {
1481
+ dist: "loader-bundle",
1482
+ "dist-custom-elements": "standalone",
1483
+ "dist-hydrate-script": "ssr",
1484
+ "dist-collection": "collection",
1485
+ "dist-types": "types"
1486
+ };
1487
+ const outputTargetsToExtract = [];
1488
+ const findOutputTargetsToExtract = (node) => {
1489
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "outputTargets" && ts.isArrayLiteralExpression(node.initializer)) {
1490
+ for (const element of node.initializer.elements) if (ts.isObjectLiteralExpression(element)) {
1491
+ let collectionDir;
1492
+ let typesDir;
1493
+ for (const prop of element.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
1494
+ if (prop.name.text === "collectionDir" && ts.isStringLiteral(prop.initializer)) collectionDir = prop.initializer.text;
1495
+ else if (prop.name.text === "typesDir" && ts.isStringLiteral(prop.initializer)) typesDir = prop.initializer.text;
1496
+ }
1497
+ if (collectionDir || typesDir) outputTargetsToExtract.push({
1498
+ targetNode: element,
1499
+ collectionDir,
1500
+ typesDir
1501
+ });
1502
+ }
1503
+ }
1504
+ ts.forEachChild(node, findOutputTargetsToExtract);
1505
+ };
1506
+ findOutputTargetsToExtract(sourceFile);
1507
+ const modifications = [];
1508
+ for (const match of matches) {
1509
+ const prop = match.node;
1510
+ const propName = prop.name.text;
1511
+ if (propName === "type" && ts.isStringLiteral(prop.initializer)) {
1512
+ const newType = oldToNewNames[prop.initializer.text];
1513
+ if (newType) {
1514
+ const start = prop.initializer.getStart() + 1;
1515
+ const end = prop.initializer.getEnd() - 1;
1516
+ modifications.push({
1517
+ type: "replace",
1518
+ start,
1519
+ end,
1520
+ replacement: newType
1521
+ });
1522
+ }
1523
+ } else if (propName === "esmLoaderPath") {
1524
+ const nameNode = prop.name;
1525
+ modifications.push({
1526
+ type: "replace",
1527
+ start: nameNode.getStart(),
1528
+ end: nameNode.getEnd(),
1529
+ replacement: "loaderPath"
1530
+ });
1531
+ if (ts.isStringLiteral(prop.initializer)) {
1532
+ const newPath = "../" + prop.initializer.text;
1533
+ modifications.push({
1534
+ type: "replace",
1535
+ start: prop.initializer.getStart() + 1,
1536
+ end: prop.initializer.getEnd() - 1,
1537
+ replacement: newPath
1538
+ });
1539
+ }
1540
+ } else if (propName === "collectionDir" || propName === "typesDir" || propName === "isPrimaryPackageOutputTarget" || propName === "generateTypeDeclarations") {
1541
+ const start = prop.getStart();
1542
+ let end = prop.getEnd();
1543
+ const beforeProp = text.slice(0, start).match(/,\s*$/);
1544
+ const afterProp = text.slice(end).match(/^\s*,/);
1545
+ if (afterProp) end = end + afterProp[0].length;
1546
+ else if (beforeProp) {
1547
+ const commaStart = start - beforeProp[0].length;
1548
+ modifications.push({
1549
+ type: "remove",
1550
+ start: commaStart,
1551
+ end
1552
+ });
1553
+ continue;
1554
+ }
1555
+ modifications.push({
1556
+ type: "remove",
1557
+ start,
1558
+ end
1559
+ });
1560
+ }
1561
+ }
1562
+ modifications.sort((a, b) => b.start - a.start);
1563
+ for (const mod of modifications) if (mod.type === "replace") text = text.slice(0, mod.start) + mod.replacement + text.slice(mod.end);
1564
+ else if (mod.type === "remove") text = text.slice(0, mod.start) + text.slice(mod.end);
1565
+ if (outputTargetsToExtract.length > 0) text = addExtractedOutputTargets(text, sourceFile, outputTargetsToExtract);
1566
+ text = text.replace(/,\s*\n\s*\n/g, ",\n");
1567
+ text = text.replace(/{\s*\n\s*\n/g, "{\n");
1568
+ text = text.replace(/,\s*,/g, ",");
1569
+ return text;
1570
+ }
1571
+ };
1572
+ /**
1573
+ * Add new output targets for extracted collectionDir and typesDir.
1574
+ * @param text The original source text
1575
+ * @param sourceFile The TypeScript source file object
1576
+ * @param toExtract An array of objects containing the output target nodes and their collectionDir/typesDir values to extract
1577
+ * @returns The modified source text with new output targets added
1578
+ */
1579
+ function addExtractedOutputTargets(text, sourceFile, toExtract) {
1580
+ let outputTargetsArray = null;
1581
+ const findOutputTargets = (node) => {
1582
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "outputTargets" && ts.isArrayLiteralExpression(node.initializer)) outputTargetsArray = node.initializer;
1583
+ ts.forEachChild(node, findOutputTargets);
1584
+ };
1585
+ findOutputTargets(sourceFile);
1586
+ if (!outputTargetsArray || toExtract.length === 0) return text;
1587
+ const array = outputTargetsArray;
1588
+ let indent = " ";
1589
+ if (array.elements.length > 0) {
1590
+ const elemStart = array.elements[0].getStart();
1591
+ const lineStart = text.lastIndexOf("\n", elemStart) + 1;
1592
+ const leadingWhitespace = text.slice(lineStart, elemStart);
1593
+ if (/^\s+$/.test(leadingWhitespace)) indent = leadingWhitespace;
1594
+ }
1595
+ const lastElement = array.elements[array.elements.length - 1];
1596
+ if (!lastElement) return text;
1597
+ let insertPos = lastElement.getEnd();
1598
+ const afterLastElement = text.slice(insertPos).match(/^(\s*,)?/);
1599
+ if (afterLastElement && afterLastElement[1]) insertPos += afterLastElement[0].length;
1600
+ const newTargets = [];
1601
+ for (const extracted of toExtract) {
1602
+ if (extracted.collectionDir) newTargets.push(`{\n${indent} type: 'collection',\n${indent} dir: '${extracted.collectionDir}',\n${indent}}`);
1603
+ if (extracted.typesDir) newTargets.push(`{\n${indent} type: 'types',\n${indent} dir: '${extracted.typesDir}',\n${indent}}`);
1604
+ }
1605
+ if (newTargets.length === 0) return text;
1606
+ const insertion = ",\n" + newTargets.map((t) => `${indent}${t}`).join(",\n");
1607
+ text = text.slice(0, insertPos) + insertion + text.slice(insertPos);
1608
+ return text;
1609
+ }
1610
+ //#endregion
1611
+ //#region src/migrations/rules/rolldown-config.ts
1612
+ /**
1613
+ * NodeResolve fields that were valid in @rollup/plugin-node-resolve but don't exist
1614
+ * in rolldown's native resolver and should be removed.
1615
+ */
1616
+ const REMOVED_NODE_RESOLVE_FIELDS = new Set([
1617
+ "browser",
1618
+ "modulePaths",
1619
+ "dedupe",
1620
+ "jail",
1621
+ "modulesOnly",
1622
+ "preferBuiltins",
1623
+ "resolveOnly",
1624
+ "rootDir",
1625
+ "allowExportsFolderMapping"
1626
+ ]);
1627
+ /** Fields renamed between @rollup/plugin-node-resolve and rolldown's native resolver. */
1628
+ const RENAMED_NODE_RESOLVE_FIELDS = {
1629
+ exportConditions: "conditionNames",
1630
+ moduleDirectories: "modules"
1631
+ };
1632
+ function isInsideNodeResolve(node) {
1633
+ const parent = node.parent;
1634
+ if (!ts.isObjectLiteralExpression(parent)) return false;
1635
+ const grandParent = parent.parent;
1636
+ return ts.isPropertyAssignment(grandParent) && ts.isIdentifier(grandParent.name) && grandParent.name.text === "nodeResolve";
1637
+ }
1638
+ /**
1639
+ * Migration rule for rolldown-related config changes in Stencil v5.
1640
+ *
1641
+ * Handles:
1642
+ * - `rollupConfig` → `rolldownConfig` (key rename + flatten `inputOptions`)
1643
+ * - `rolldownConfig: { inputOptions: { ... } }` → `rolldownConfig: { ... }` (flatten)
1644
+ * - `rollupPlugins` → `rolldownPlugins`
1645
+ * - `nodeResolve.exportConditions` → `nodeResolve.conditionNames`
1646
+ * - `nodeResolve.moduleDirectories` → `nodeResolve.modules`
1647
+ * - Removes unsupported `nodeResolve` fields (`browser`, `dedupe`, `jail`, etc.)
1648
+ */
1649
+ const rolldownConfigRule = {
1650
+ id: "rolldown-config",
1651
+ name: "Rolldown Config Migration",
1652
+ description: "Migrate rollup/rolldown config options to v5 rolldown-native API",
1653
+ fromVersion: "4.x",
1654
+ toVersion: "5.x",
1655
+ detect(sourceFile) {
1656
+ const matches = [];
1657
+ const visit = (node) => {
1658
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
1659
+ const name = node.name.text;
1660
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
1661
+ if (name === "rollupPlugins") matches.push({
1662
+ node,
1663
+ message: `'rollupPlugins' renamed to 'rolldownPlugins'`,
1664
+ line: line + 1,
1665
+ column: character + 1
1666
+ });
1667
+ else if (name === "rollupConfig" || name === "rolldownConfig") {
1668
+ const value = node.initializer;
1669
+ if (name === "rollupConfig" || ts.isObjectLiteralExpression(value) && value.properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "inputOptions")) matches.push({
1670
+ node,
1671
+ message: name === "rollupConfig" ? `'rollupConfig' renamed to 'rolldownConfig'; 'inputOptions' wrapper removed` : `'rolldownConfig.inputOptions' wrapper removed - options are now top-level`,
1672
+ line: line + 1,
1673
+ column: character + 1
1674
+ });
1675
+ } else if (name in RENAMED_NODE_RESOLVE_FIELDS && isInsideNodeResolve(node)) matches.push({
1676
+ node,
1677
+ message: `'nodeResolve.${name}' renamed to 'nodeResolve.${RENAMED_NODE_RESOLVE_FIELDS[name]}'`,
1678
+ line: line + 1,
1679
+ column: character + 1
1680
+ });
1681
+ else if (REMOVED_NODE_RESOLVE_FIELDS.has(name) && isInsideNodeResolve(node)) matches.push({
1682
+ node,
1683
+ message: `'nodeResolve.${name}' is not supported by rolldown's native resolver and will be removed`,
1684
+ line: line + 1,
1685
+ column: character + 1
1686
+ });
1687
+ }
1688
+ ts.forEachChild(node, visit);
1689
+ };
1690
+ visit(sourceFile);
1691
+ return matches;
1692
+ },
1693
+ transform(sourceFile, matches) {
1694
+ if (matches.length === 0) return sourceFile.getFullText();
1695
+ const edits = [];
1696
+ const fullText = sourceFile.getFullText();
1697
+ for (const match of matches) {
1698
+ const node = match.node;
1699
+ const name = node.name.text;
1700
+ if (name === "rollupPlugins") {
1701
+ const nameStart = node.name.getStart(sourceFile);
1702
+ const nameEnd = node.name.getEnd();
1703
+ edits.push({
1704
+ start: nameStart,
1705
+ end: nameEnd,
1706
+ replacement: "rolldownPlugins"
1707
+ });
1708
+ continue;
1709
+ }
1710
+ if (name in RENAMED_NODE_RESOLVE_FIELDS && isInsideNodeResolve(node)) {
1711
+ const nameStart = node.name.getStart(sourceFile);
1712
+ const nameEnd = node.name.getEnd();
1713
+ edits.push({
1714
+ start: nameStart,
1715
+ end: nameEnd,
1716
+ replacement: RENAMED_NODE_RESOLVE_FIELDS[name]
1717
+ });
1718
+ continue;
1719
+ }
1720
+ if (REMOVED_NODE_RESOLVE_FIELDS.has(name) && isInsideNodeResolve(node)) {
1721
+ const start = node.getFullStart();
1722
+ const end = node.getEnd();
1723
+ const trailingComma = fullText.slice(end).match(/^\s*,/);
1724
+ edits.push({
1725
+ start,
1726
+ end: trailingComma ? end + trailingComma[0].length : end,
1727
+ replacement: ""
1728
+ });
1729
+ continue;
1730
+ }
1731
+ if (name === "rollupConfig" || name === "rolldownConfig") {
1732
+ const value = node.initializer;
1733
+ if (!ts.isObjectLiteralExpression(value)) {
1734
+ if (name === "rollupConfig") {
1735
+ const nameStart = node.name.getStart(sourceFile);
1736
+ edits.push({
1737
+ start: nameStart,
1738
+ end: node.name.getEnd(),
1739
+ replacement: "rolldownConfig"
1740
+ });
1741
+ }
1742
+ continue;
1743
+ }
1744
+ const inputOptionsProp = value.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "inputOptions");
1745
+ if (!inputOptionsProp) {
1746
+ if (name === "rollupConfig") {
1747
+ const nameStart = node.name.getStart(sourceFile);
1748
+ edits.push({
1749
+ start: nameStart,
1750
+ end: node.name.getEnd(),
1751
+ replacement: "rolldownConfig"
1752
+ });
1753
+ }
1754
+ continue;
1755
+ }
1756
+ const newValueText = inputOptionsProp.initializer.getText(sourceFile);
1757
+ const keyStart = node.name.getStart(sourceFile);
1758
+ edits.push({
1759
+ start: keyStart,
1760
+ end: node.getEnd(),
1761
+ replacement: `rolldownConfig: ${newValueText}`
1762
+ });
1763
+ }
1764
+ }
1765
+ edits.sort((a, b) => b.start - a.start);
1766
+ let text = fullText;
1767
+ for (const edit of edits) text = text.slice(0, edit.start) + edit.replacement + text.slice(edit.end);
1768
+ return text;
1769
+ }
1770
+ };
1771
+ //#endregion
823
1772
  //#region src/migrations/index.ts
824
1773
  /**
825
1774
  * Build a map of local import names to their original names from @stencil/core.
@@ -857,7 +1806,18 @@ const isStencilDecorator = (decoratorName, expectedOriginalName, importMap) => {
857
1806
  * Registry of all available migration rules.
858
1807
  * Rules are applied in order, so add new rules at the end.
859
1808
  */
860
- const migrationRules = [encapsulationApiRule, formAssociatedRule];
1809
+ const migrationRules = [
1810
+ encapsulationApiRule,
1811
+ formAssociatedRule,
1812
+ buildDistDocsRule,
1813
+ outputTargetRenamesRule,
1814
+ devModeRule,
1815
+ globalStyleInjectRule,
1816
+ lightDomPatchesRule,
1817
+ externalRuntimeRule,
1818
+ hashFileNamesRule,
1819
+ rolldownConfigRule
1820
+ ];
861
1821
  /**
862
1822
  * Get all migration rules for a specific version upgrade.
863
1823
  * @param fromVersion Source version (e.g., '4')
@@ -1041,27 +2001,49 @@ async function getTypeScriptFiles(config, sys, logger) {
1041
2001
  return [];
1042
2002
  }
1043
2003
  if (results.errors && results.errors.length > 0) for (const err of results.errors) logger.warn(ts.flattenDiagnosticMessageText(err.messageText, "\n"));
1044
- return results.fileNames.filter((f) => (f.endsWith(".ts") || f.endsWith(".tsx")) && !f.endsWith(".d.ts"));
2004
+ const files = results.fileNames.filter((f) => (f.endsWith(".ts") || f.endsWith(".tsx")) && !f.endsWith(".d.ts"));
2005
+ const configFile = config.configPath;
2006
+ if (configFile && (configFile.endsWith(".ts") || configFile.endsWith(".mts"))) {
2007
+ if (!files.includes(configFile)) files.push(configFile);
2008
+ const configContent = await sys.readFile(configFile);
2009
+ if (configContent) {
2010
+ const configSourceFile = ts.createSourceFile(configFile, configContent, ts.ScriptTarget.Latest, true);
2011
+ const configDir = dirname(configFile);
2012
+ for (const statement of configSourceFile.statements) if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
2013
+ const specifier = statement.moduleSpecifier.text;
2014
+ if (!specifier.startsWith("./") && !specifier.startsWith("../")) continue;
2015
+ const basePath = join(configDir, specifier);
2016
+ const candidates = basePath.endsWith(".ts") || basePath.endsWith(".tsx") ? [basePath] : [`${basePath}.ts`, `${basePath}.tsx`];
2017
+ for (const candidate of candidates) if (!candidate.endsWith(".d.ts") && !files.includes(candidate) && candidate.startsWith(config.rootDir)) {
2018
+ if (await sys.readFile(candidate)) {
2019
+ files.push(candidate);
2020
+ break;
2021
+ }
2022
+ }
2023
+ }
2024
+ }
2025
+ }
2026
+ return files;
1045
2027
  }
1046
2028
  //#endregion
1047
2029
  //#region src/task-prerender.ts
1048
2030
  const taskPrerender = async (coreCompiler, config, flags) => {
1049
2031
  startupCompilerLog(coreCompiler, config);
1050
- const hydrateAppFilePath = flags.unknownArgs[0];
1051
- if (typeof hydrateAppFilePath !== "string") {
2032
+ const ssrAppFilePath = flags.unknownArgs[0];
2033
+ if (typeof ssrAppFilePath !== "string") {
1052
2034
  config.logger.error(`Missing hydrate app script path`);
1053
2035
  return config.sys.exit(1);
1054
2036
  }
1055
2037
  const srcIndexHtmlPath = config.srcIndexHtml;
1056
- const diagnostics = await runPrerenderTask(coreCompiler, config, hydrateAppFilePath, void 0, srcIndexHtmlPath);
2038
+ const diagnostics = await runPrerenderTask(coreCompiler, config, ssrAppFilePath, void 0, srcIndexHtmlPath);
1057
2039
  config.logger.printDiagnostics(diagnostics);
1058
2040
  if (diagnostics.some((d) => d.level === "error")) return config.sys.exit(1);
1059
2041
  };
1060
- const runPrerenderTask = async (coreCompiler, config, hydrateAppFilePath, componentGraph, srcIndexHtmlPath) => {
2042
+ const runPrerenderTask = async (coreCompiler, config, ssrAppFilePath, componentGraph, srcIndexHtmlPath) => {
1061
2043
  const diagnostics = [];
1062
2044
  try {
1063
2045
  const results = await (await coreCompiler.createPrerenderer(config)).start({
1064
- hydrateAppFilePath,
2046
+ ssrAppFilePath,
1065
2047
  componentGraph,
1066
2048
  srcIndexHtmlPath
1067
2049
  });
@@ -1393,7 +2375,7 @@ const anonymizeConfigForTelemetry = (config) => {
1393
2375
  if (OUTPUT_TARGET_KEYS_TO_KEEP.includes(key)) return value;
1394
2376
  return "omitted";
1395
2377
  }));
1396
- if (isOutputTargetHydrate(target) && target.external) anonymizedOT["external"] = target.external.concat();
2378
+ if (isOutputTargetSsr(target) && target.external) anonymizedOT["external"] = target.external.concat();
1397
2379
  return anonymizedOT;
1398
2380
  });
1399
2381
  for (const prop of CONFIG_PROPS_TO_DELETE) delete anonymizedConfig[prop];
@@ -1587,6 +2569,23 @@ const taskBuild = async (coreCompiler, config, flags) => {
1587
2569
  let exitCode = 0;
1588
2570
  try {
1589
2571
  startupCompilerLog(coreCompiler, config);
2572
+ const preBuildMigrationResult = await detectMigrations(coreCompiler, config);
2573
+ if (preBuildMigrationResult.hasMigrations) {
2574
+ const action = await promptForMigration(config, preBuildMigrationResult, "pre-build");
2575
+ if (action === "run") {
2576
+ await taskMigrate(coreCompiler, config, {
2577
+ ...flags,
2578
+ dryRun: false
2579
+ });
2580
+ config.logger.info("\nMigrations applied. Starting build...\n");
2581
+ } else if (action === "dry-run") {
2582
+ await taskMigrate(coreCompiler, config, {
2583
+ ...flags,
2584
+ dryRun: true
2585
+ });
2586
+ return config.sys.exit(1);
2587
+ } else return config.sys.exit(1);
2588
+ }
1590
2589
  const versionChecker = startCheckVersion(config, coreCompiler.version, flags);
1591
2590
  const compiler = await coreCompiler.createCompiler(config);
1592
2591
  const results = await compiler.build();
@@ -1595,7 +2594,7 @@ const taskBuild = async (coreCompiler, config, flags) => {
1595
2594
  if (results.hasError) {
1596
2595
  const migrationResult = await detectMigrations(coreCompiler, config);
1597
2596
  if (migrationResult.hasMigrations) {
1598
- const action = await promptForMigrationOnBuildError(config, migrationResult);
2597
+ const action = await promptForMigration(config, migrationResult, "post-error");
1599
2598
  if (action === "run") {
1600
2599
  await taskMigrate(coreCompiler, config, {
1601
2600
  ...flags,
@@ -1608,7 +2607,7 @@ const taskBuild = async (coreCompiler, config, flags) => {
1608
2607
  if (!newResults.hasError) {
1609
2608
  exitCode = 0;
1610
2609
  if (flags.prerender) {
1611
- const prerenderDiagnostics = await runPrerenderTask(coreCompiler, config, newResults.hydrateAppFilePath, newResults.componentGraph, void 0);
2610
+ const prerenderDiagnostics = await runPrerenderTask(coreCompiler, config, newResults.ssrAppFilePath, newResults.componentGraph, void 0);
1612
2611
  config.logger.printDiagnostics(prerenderDiagnostics);
1613
2612
  if (prerenderDiagnostics.some((d) => d.level === "error")) exitCode = 1;
1614
2613
  }
@@ -1622,7 +2621,7 @@ const taskBuild = async (coreCompiler, config, flags) => {
1622
2621
  } else exitCode = 1;
1623
2622
  } else exitCode = 1;
1624
2623
  } else if (flags.prerender) {
1625
- const prerenderDiagnostics = await runPrerenderTask(coreCompiler, config, results.hydrateAppFilePath, results.componentGraph, void 0);
2624
+ const prerenderDiagnostics = await runPrerenderTask(coreCompiler, config, results.ssrAppFilePath, results.componentGraph, void 0);
1626
2625
  config.logger.printDiagnostics(prerenderDiagnostics);
1627
2626
  if (prerenderDiagnostics.some((d) => d.level === "error")) exitCode = 1;
1628
2627
  }
@@ -1634,24 +2633,26 @@ const taskBuild = async (coreCompiler, config, flags) => {
1634
2633
  if (exitCode > 0) return config.sys.exit(exitCode);
1635
2634
  };
1636
2635
  /**
1637
- * Prompt the user about available migrations when a build fails.
2636
+ * Prompt the user about available migrations.
1638
2637
  * Shows what migrations are available and lets them choose to run them.
1639
2638
  * @param config the Stencil config
1640
2639
  * @param migrationResult the result of migration detection with available migrations
2640
+ * @param context whether this is a pre-build check or post-error check
1641
2641
  * @returns the user's chosen action for handling migrations
1642
2642
  */
1643
- async function promptForMigrationOnBuildError(config, migrationResult) {
2643
+ async function promptForMigration(config, migrationResult, context) {
1644
2644
  const logger = config.logger;
1645
2645
  logger.info("");
1646
- logger.info(logger.bold(logger.yellow("Migrations Available")));
2646
+ logger.info(logger.bold(logger.yellow("Migrations Required")));
1647
2647
  logger.info("─".repeat(40));
1648
- logger.info(`Found ${migrationResult.totalMatches} item(s) in ${migrationResult.filesAffected} file(s) that can be automatically migrated.`);
2648
+ logger.info(`Found ${migrationResult.totalMatches} item(s) in ${migrationResult.filesAffected} file(s) that need to be migrated for Stencil v5.`);
1649
2649
  for (const migration of migrationResult.migrations) {
1650
2650
  const relPath = relative(config.rootDir, migration.filePath);
1651
2651
  logger.info(` ${logger.cyan(relPath)}: ${migration.matches.length} item(s)`);
1652
2652
  }
1653
2653
  logger.info("");
1654
- logger.info("These migrations may help resolve the build errors above.");
2654
+ if (context === "pre-build") logger.info("Your config contains deprecated options that must be migrated before building.");
2655
+ else logger.info("These migrations may help resolve the build errors above.");
1655
2656
  const prompt = (await import("prompts")).default;
1656
2657
  const response = await prompt({
1657
2658
  name: "action",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stencil/cli",
3
- "version": "5.0.0-alpha.4",
3
+ "version": "5.0.0-alpha.6",
4
4
  "description": "CLI for Stencil - Web component compiler",
5
5
  "keywords": [
6
6
  "components",
@@ -37,15 +37,15 @@
37
37
  "./cli": "./bin/stencil.mjs"
38
38
  },
39
39
  "dependencies": {
40
- "prompts": "^2.4.2",
41
- "@stencil/dev-server": "5.0.0-alpha.4"
40
+ "prompts": "^2.0.0",
41
+ "@stencil/dev-server": "5.0.0-alpha.6"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/prompts": "^2.4.9",
45
- "tsdown": "^0.21.6",
46
- "typescript": "~6.0.2",
47
- "vitest": "^4.1.1",
48
- "@stencil/core": "5.0.0-alpha.4"
45
+ "tsdown": ">=0.21.0 <1.0.0",
46
+ "typescript": ">4.0.0 <7.0.0",
47
+ "vitest": "^4.1.7",
48
+ "@stencil/core": "5.0.0-alpha.6"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "@stencil/core": "^5.0.0-0"