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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -5,33 +5,33 @@ import { LogLevel } from "@stencil/core/compiler";
5
5
  /**
6
6
  * Supported CLI task commands
7
7
  */
8
- type TaskCommand = 'build' | 'docs' | 'generate' | 'g' | 'help' | 'info' | 'prerender' | 'serve' | 'telemetry' | 'test' | 'version';
8
+ type TaskCommand = 'build' | 'docs' | 'generate' | 'g' | 'help' | 'info' | 'migrate' | 'prerender' | 'serve' | 'telemetry' | 'test' | 'version';
9
9
  //#endregion
10
10
  //#region src/config-flags.d.ts
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", "e2e", "es5", "esm", "help", "log", "open", "prerender", "prerenderExternal", "prod", "profile", "serviceWorker", "screenshot", "serve", "skipNodeCheck", "spec", "ssr", "updateScreenshot", "verbose", "version", "watch", "all", "automock", "bail", "changedFilesWithAncestor", "clearCache", "clearMocks", "collectCoverage", "color", "colors", "coverage", "detectLeaks", "detectOpenHandles", "errorOnDeprecated", "expand", "findRelatedTests", "forceExit", "init", "injectGlobals", "json", "lastCommit", "listTests", "logHeapUsage", "noStackTrace", "notify", "onlyChanged", "onlyFailures", "passWithNoTests", "resetMocks", "resetModules", "restoreMocks", "runInBand", "runTestsByPath", "showConfig", "silent", "skipFilter", "testLocationInResults", "updateSnapshot", "useStderr", "watchAll", "watchman"];
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"];
15
15
  /**
16
16
  * All the Number options supported by the Stencil CLI
17
17
  */
18
- declare const NUMBER_CLI_FLAGS: readonly ["port", "maxConcurrency", "testTimeout"];
18
+ declare const NUMBER_CLI_FLAGS: readonly ["port"];
19
19
  /**
20
20
  * All the String options supported by the Stencil CLI
21
21
  */
22
- declare const STRING_CLI_FLAGS: readonly ["address", "config", "docsApi", "docsJson", "emulate", "root", "screenshotConnector", "cacheDirectory", "changedSince", "collectCoverageFrom", "coverageDirectory", "coverageThreshold", "env", "filter", "globalSetup", "globalTeardown", "globals", "haste", "moduleNameMapper", "notifyMode", "outputFile", "preset", "prettierPath", "resolver", "rootDir", "runner", "testEnvironment", "testEnvironmentOptions", "testFailureExitCode", "testNamePattern", "testResultsProcessor", "testRunner", "testSequencer", "testURL", "timers", "transform"];
23
- declare const STRING_ARRAY_CLI_FLAGS: readonly ["collectCoverageOnlyFrom", "coveragePathIgnorePatterns", "coverageReporters", "moduleDirectories", "moduleFileExtensions", "modulePathIgnorePatterns", "modulePaths", "projects", "reporters", "roots", "selectProjects", "setupFiles", "setupFilesAfterEnv", "snapshotSerializers", "testMatch", "testPathIgnorePatterns", "testPathPattern", "testRegex", "transformIgnorePatterns", "unmockedModulePathPatterns", "watchPathIgnorePatterns"];
22
+ declare const STRING_CLI_FLAGS: readonly ["address", "config", "docsApi", "docsJson", "emulate", "root"];
23
+ declare const STRING_ARRAY_CLI_FLAGS: readonly [];
24
24
  /**
25
25
  * All the CLI arguments which may have string or number values
26
26
  *
27
- * `maxWorkers` is an argument which is used both by Stencil _and_ by Jest,
28
- * which means that we need to support parsing both string and number values.
27
+ * `maxWorkers` controls the number of concurrent workers for Stencil builds.
28
+ * Supports both string (e.g., "50%") and number values.
29
29
  */
30
30
  declare const STRING_NUMBER_CLI_FLAGS: readonly ["maxWorkers"];
31
31
  /**
32
32
  * All the CLI arguments which may have boolean or string values.
33
33
  */
34
- declare const BOOLEAN_STRING_CLI_FLAGS: readonly ["headless", "stats"];
34
+ declare const BOOLEAN_STRING_CLI_FLAGS: readonly ["stats"];
35
35
  /**
36
36
  * All the LogLevel-type options supported by the Stencil CLI
37
37
  *
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { LOG_LEVELS } from "@stencil/core/compiler";
2
2
  import { buildError, catchError, hasError, isFunction, isOutputTargetDocs, isOutputTargetHydrate, isOutputTargetWww, isString, normalizePath, readOnlyArrayHasStringMember, result, shouldIgnoreError, toCamelCase, validateComponentTag } from "@stencil/core/compiler/utils";
3
- import { join, parse, relative } from "path";
3
+ import { isAbsolute, join, parse, relative } from "path";
4
+ import ts from "typescript";
4
5
  import { start } from "@stencil/dev-server";
5
6
  //#region src/config-flags.ts
6
7
  /**
@@ -16,8 +17,7 @@ const BOOLEAN_CLI_FLAGS = [
16
17
  "dev",
17
18
  "devtools",
18
19
  "docs",
19
- "e2e",
20
- "es5",
20
+ "dryRun",
21
21
  "esm",
22
22
  "help",
23
23
  "log",
@@ -27,64 +27,17 @@ const BOOLEAN_CLI_FLAGS = [
27
27
  "prod",
28
28
  "profile",
29
29
  "serviceWorker",
30
- "screenshot",
31
30
  "serve",
32
31
  "skipNodeCheck",
33
- "spec",
34
32
  "ssr",
35
- "updateScreenshot",
36
33
  "verbose",
37
34
  "version",
38
- "watch",
39
- "all",
40
- "automock",
41
- "bail",
42
- "changedFilesWithAncestor",
43
- "clearCache",
44
- "clearMocks",
45
- "collectCoverage",
46
- "color",
47
- "colors",
48
- "coverage",
49
- "detectLeaks",
50
- "detectOpenHandles",
51
- "errorOnDeprecated",
52
- "expand",
53
- "findRelatedTests",
54
- "forceExit",
55
- "init",
56
- "injectGlobals",
57
- "json",
58
- "lastCommit",
59
- "listTests",
60
- "logHeapUsage",
61
- "noStackTrace",
62
- "notify",
63
- "onlyChanged",
64
- "onlyFailures",
65
- "passWithNoTests",
66
- "resetMocks",
67
- "resetModules",
68
- "restoreMocks",
69
- "runInBand",
70
- "runTestsByPath",
71
- "showConfig",
72
- "silent",
73
- "skipFilter",
74
- "testLocationInResults",
75
- "updateSnapshot",
76
- "useStderr",
77
- "watchAll",
78
- "watchman"
35
+ "watch"
79
36
  ];
80
37
  /**
81
38
  * All the Number options supported by the Stencil CLI
82
39
  */
83
- const NUMBER_CLI_FLAGS = [
84
- "port",
85
- "maxConcurrency",
86
- "testTimeout"
87
- ];
40
+ const NUMBER_CLI_FLAGS = ["port"];
88
41
  /**
89
42
  * All the String options supported by the Stencil CLI
90
43
  */
@@ -94,72 +47,20 @@ const STRING_CLI_FLAGS = [
94
47
  "docsApi",
95
48
  "docsJson",
96
49
  "emulate",
97
- "root",
98
- "screenshotConnector",
99
- "cacheDirectory",
100
- "changedSince",
101
- "collectCoverageFrom",
102
- "coverageDirectory",
103
- "coverageThreshold",
104
- "env",
105
- "filter",
106
- "globalSetup",
107
- "globalTeardown",
108
- "globals",
109
- "haste",
110
- "moduleNameMapper",
111
- "notifyMode",
112
- "outputFile",
113
- "preset",
114
- "prettierPath",
115
- "resolver",
116
- "rootDir",
117
- "runner",
118
- "testEnvironment",
119
- "testEnvironmentOptions",
120
- "testFailureExitCode",
121
- "testNamePattern",
122
- "testResultsProcessor",
123
- "testRunner",
124
- "testSequencer",
125
- "testURL",
126
- "timers",
127
- "transform"
128
- ];
129
- const STRING_ARRAY_CLI_FLAGS = [
130
- "collectCoverageOnlyFrom",
131
- "coveragePathIgnorePatterns",
132
- "coverageReporters",
133
- "moduleDirectories",
134
- "moduleFileExtensions",
135
- "modulePathIgnorePatterns",
136
- "modulePaths",
137
- "projects",
138
- "reporters",
139
- "roots",
140
- "selectProjects",
141
- "setupFiles",
142
- "setupFilesAfterEnv",
143
- "snapshotSerializers",
144
- "testMatch",
145
- "testPathIgnorePatterns",
146
- "testPathPattern",
147
- "testRegex",
148
- "transformIgnorePatterns",
149
- "unmockedModulePathPatterns",
150
- "watchPathIgnorePatterns"
50
+ "root"
151
51
  ];
52
+ const STRING_ARRAY_CLI_FLAGS = [];
152
53
  /**
153
54
  * All the CLI arguments which may have string or number values
154
55
  *
155
- * `maxWorkers` is an argument which is used both by Stencil _and_ by Jest,
156
- * which means that we need to support parsing both string and number values.
56
+ * `maxWorkers` controls the number of concurrent workers for Stencil builds.
57
+ * Supports both string (e.g., "50%") and number values.
157
58
  */
158
59
  const STRING_NUMBER_CLI_FLAGS = ["maxWorkers"];
159
60
  /**
160
61
  * All the CLI arguments which may have boolean or string values.
161
62
  */
162
- const BOOLEAN_STRING_CLI_FLAGS = ["headless", "stats"];
63
+ const BOOLEAN_STRING_CLI_FLAGS = ["stats"];
163
64
  /**
164
65
  * All the LogLevel-type options supported by the Stencil CLI
165
66
  *
@@ -175,21 +76,13 @@ const CLI_FLAG_ALIASES = {
175
76
  c: "config",
176
77
  h: "help",
177
78
  p: "port",
178
- v: "version",
179
- b: "bail",
180
- e: "expand",
181
- f: "onlyFailures",
182
- i: "runInBand",
183
- o: "onlyChanged",
184
- t: "testNamePattern",
185
- u: "updateSnapshot",
186
- w: "maxWorkers"
79
+ v: "version"
187
80
  };
188
81
  /**
189
82
  * A regular expression which can be used to match a CLI flag for one of our
190
83
  * short aliases.
191
84
  */
192
- const CLI_FLAG_REGEX = new RegExp(`^-[chpvbewofitu]{1}$`);
85
+ const CLI_FLAG_REGEX = new RegExp(`^-[chpv]{1}$`);
193
86
  /**
194
87
  * Helper function for initializing a `ConfigFlags` object. Provide any overrides
195
88
  * for default values and off you go!
@@ -354,9 +247,10 @@ const setCLIArg = (flags, rawArg, normalizedArg, value) => {
354
247
  flags.knownArgs.push(rawArg);
355
248
  flags.knownArgs.push(value);
356
249
  } else throwCLIParsingError(rawArg, "expected a string argument but received nothing");
357
- else if (readOnlyArrayHasStringMember(STRING_ARRAY_CLI_FLAGS, normalizedArg)) if (typeof value === "string") {
358
- if (!Array.isArray(flags[normalizedArg])) flags[normalizedArg] = [];
359
- const targetArray = flags[normalizedArg];
250
+ else if (STRING_ARRAY_CLI_FLAGS.length > 0 && readOnlyArrayHasStringMember(STRING_ARRAY_CLI_FLAGS, normalizedArg)) if (typeof value === "string") {
251
+ const flagsRecord = flags;
252
+ if (!Array.isArray(flagsRecord[normalizedArg])) flagsRecord[normalizedArg] = [];
253
+ const targetArray = flagsRecord[normalizedArg];
360
254
  if (Array.isArray(targetArray)) {
361
255
  targetArray.push(value);
362
256
  flags.knownArgs.push(rawArg);
@@ -657,7 +551,6 @@ const startupCompilerLog = (coreCompiler, config) => {
657
551
  const isDevBuild = coreCompiler.version.includes("-dev.");
658
552
  if (isPrerelease && !isDevBuild) logger.warn(logger.yellow(`This is a prerelease build, undocumented changes might happen at any time. Technical support is not available for prereleases, but any assistance testing is appreciated.`));
659
553
  if (config.devMode && !isDebug) {
660
- if (config.buildEs5) logger.warn(`Generating ES5 during development is a very task expensive, initial and incremental builds will be much slower. Drop the '--es5' flag and use a modern browser for development.`);
661
554
  if (!config.enableCache) logger.warn(`Disabling cache during development will slow down incremental builds.`);
662
555
  }
663
556
  };
@@ -692,7 +585,6 @@ const mergeFlags = (config, flags) => {
692
585
  if (typeof flags.docsJson === "string") merged.docsJsonPath = flags.docsJson;
693
586
  if (flags.stats) merged.statsJsonPath = flags.stats;
694
587
  if (typeof flags.serviceWorker === "boolean") merged.generateServiceWorker = flags.serviceWorker;
695
- if (typeof flags.e2e === "boolean") merged.e2eTests = flags.e2e;
696
588
  if (typeof flags.maxWorkers === "number") merged.maxConcurrentWorkers = flags.maxWorkers;
697
589
  if (typeof flags.address === "string") merged.devServerAddress = flags.address;
698
590
  if (typeof flags.port === "number") merged.devServerPort = flags.port;
@@ -726,6 +618,432 @@ const printCheckVersionResults = async (versionChecker) => {
726
618
  }
727
619
  };
728
620
  //#endregion
621
+ //#region src/migrations/rules/encapsulation-api.ts
622
+ /**
623
+ * Migration rule for the @Component encapsulation API change.
624
+ *
625
+ * Migrates:
626
+ * - `shadow: true` → `encapsulation: { type: 'shadow' }`
627
+ * - `shadow: { delegatesFocus: true }` → `encapsulation: { type: 'shadow', delegatesFocus: true }`
628
+ * - `scoped: true` → `encapsulation: { type: 'scoped' }`
629
+ */
630
+ const encapsulationApiRule = {
631
+ id: "encapsulation-api",
632
+ name: "Encapsulation API",
633
+ description: "Migrate shadow/scoped properties to new encapsulation API",
634
+ fromVersion: "4.x",
635
+ toVersion: "5.x",
636
+ detect(sourceFile) {
637
+ const matches = [];
638
+ const importMap = getStencilCoreImportMap(sourceFile);
639
+ const visit = (node) => {
640
+ if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) {
641
+ const decoratorName = node.expression.expression;
642
+ if (ts.isIdentifier(decoratorName) && isStencilDecorator(decoratorName.text, "Component", importMap)) {
643
+ const [arg] = node.expression.arguments;
644
+ if (arg && ts.isObjectLiteralExpression(arg)) {
645
+ for (const prop of arg.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
646
+ const propName = prop.name.text;
647
+ if (propName === "shadow" || propName === "scoped") {
648
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(prop.getStart());
649
+ matches.push({
650
+ node: prop,
651
+ message: `Deprecated '${propName}' property found - migrate to 'encapsulation' API`,
652
+ line: line + 1,
653
+ column: character + 1
654
+ });
655
+ }
656
+ }
657
+ }
658
+ }
659
+ }
660
+ ts.forEachChild(node, visit);
661
+ };
662
+ visit(sourceFile);
663
+ return matches;
664
+ },
665
+ transform(sourceFile, matches) {
666
+ if (matches.length === 0) return sourceFile.getFullText();
667
+ let text = sourceFile.getFullText();
668
+ const sortedMatches = [...matches].sort((a, b) => {
669
+ const posA = a.node.getStart();
670
+ return b.node.getStart() - posA;
671
+ });
672
+ for (const match of sortedMatches) {
673
+ const prop = match.node;
674
+ const propName = prop.name.text;
675
+ const start = prop.getStart();
676
+ const end = prop.getEnd();
677
+ let replacement;
678
+ if (propName === "shadow") if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) replacement = "encapsulation: { type: 'shadow' }";
679
+ else if (ts.isObjectLiteralExpression(prop.initializer)) {
680
+ const options = [];
681
+ for (const innerProp of prop.initializer.properties) if (ts.isPropertyAssignment(innerProp) && ts.isIdentifier(innerProp.name)) {
682
+ const optName = innerProp.name.text;
683
+ const optValue = innerProp.initializer.getText();
684
+ options.push(`${optName}: ${optValue}`);
685
+ }
686
+ if (options.length > 0) replacement = `encapsulation: { type: 'shadow', ${options.join(", ")} }`;
687
+ else replacement = "encapsulation: { type: 'shadow' }";
688
+ } else replacement = "";
689
+ else if (propName === "scoped") if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) replacement = "encapsulation: { type: 'scoped' }";
690
+ else replacement = "";
691
+ else continue;
692
+ let endPos = end;
693
+ const afterProp = text.slice(end).match(/^\s*,/);
694
+ if (afterProp && replacement === "") endPos = end + afterProp[0].length;
695
+ else if (afterProp && replacement !== "") {} else if (!afterProp && replacement !== "") {}
696
+ text = text.slice(0, start) + replacement + text.slice(endPos);
697
+ }
698
+ return text;
699
+ }
700
+ };
701
+ //#endregion
702
+ //#region src/migrations/rules/form-associated.ts
703
+ /**
704
+ * Migration rule for formAssociated → @AttachInternals.
705
+ *
706
+ * Migrates:
707
+ * - `formAssociated: true` in @Component → Adds `@AttachInternals() internals: ElementInternals;`
708
+ */
709
+ const formAssociatedRule = {
710
+ id: "form-associated",
711
+ name: "Form Associated",
712
+ description: "Migrate formAssociated to @AttachInternals decorator",
713
+ fromVersion: "4.x",
714
+ toVersion: "5.x",
715
+ detect(sourceFile) {
716
+ const matches = [];
717
+ const importMap = getStencilCoreImportMap(sourceFile);
718
+ let hasAttachInternalsImport = false;
719
+ let stencilImportEnd = 0;
720
+ for (const statement of sourceFile.statements) if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "@stencil/core") {
721
+ stencilImportEnd = statement.getEnd();
722
+ for (const [, originalName] of importMap) if (originalName === "AttachInternals") {
723
+ hasAttachInternalsImport = true;
724
+ break;
725
+ }
726
+ break;
727
+ }
728
+ const visit = (node) => {
729
+ if (ts.isClassDeclaration(node)) {
730
+ const decorators = ts.getDecorators(node);
731
+ if (!decorators) {
732
+ ts.forEachChild(node, visit);
733
+ return;
734
+ }
735
+ let componentDecorator;
736
+ let formAssociatedProp;
737
+ for (const decorator of decorators) if (ts.isCallExpression(decorator.expression) && ts.isIdentifier(decorator.expression.expression) && isStencilDecorator(decorator.expression.expression.text, "Component", importMap)) {
738
+ componentDecorator = decorator;
739
+ const [arg] = decorator.expression.arguments;
740
+ if (arg && ts.isObjectLiteralExpression(arg)) {
741
+ for (const prop of arg.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === "formAssociated") {
742
+ formAssociatedProp = prop;
743
+ break;
744
+ }
745
+ }
746
+ break;
747
+ }
748
+ if (componentDecorator && formAssociatedProp) {
749
+ let hasAttachInternals = false;
750
+ for (const member of node.members) if (ts.isPropertyDeclaration(member) || ts.isMethodDeclaration(member)) {
751
+ const memberDecorators = ts.getDecorators(member);
752
+ if (memberDecorators) {
753
+ for (const d of memberDecorators) if (ts.isCallExpression(d.expression) && ts.isIdentifier(d.expression.expression) && isStencilDecorator(d.expression.expression.text, "AttachInternals", importMap)) {
754
+ hasAttachInternals = true;
755
+ break;
756
+ }
757
+ }
758
+ }
759
+ let searchStart = node.name ? node.name.getEnd() : node.getStart(sourceFile);
760
+ if (node.heritageClauses) for (const clause of node.heritageClauses) searchStart = Math.max(searchStart, clause.getEnd());
761
+ const braceMatch = sourceFile.getFullText().slice(searchStart).match(/\s*\{/);
762
+ if (!braceMatch) {
763
+ ts.forEachChild(node, visit);
764
+ return;
765
+ }
766
+ const classBodyStart = searchStart + braceMatch[0].length;
767
+ let indent = " ";
768
+ if (node.members.length > 0) {
769
+ const memberStart = node.members[0].getStart(sourceFile);
770
+ const indentMatch = sourceFile.getFullText().slice(classBodyStart, memberStart).match(/\n(\s+)/);
771
+ if (indentMatch) indent = indentMatch[1];
772
+ }
773
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(formAssociatedProp.getStart());
774
+ matches.push({
775
+ node: formAssociatedProp,
776
+ message: hasAttachInternals ? "Remove 'formAssociated' (already has @AttachInternals)" : "Migrate 'formAssociated' to @AttachInternals decorator",
777
+ line: line + 1,
778
+ column: character + 1,
779
+ classBodyStart,
780
+ hasAttachInternals,
781
+ indent,
782
+ needsImport: !hasAttachInternalsImport && !hasAttachInternals,
783
+ stencilImportEnd
784
+ });
785
+ }
786
+ }
787
+ ts.forEachChild(node, visit);
788
+ };
789
+ visit(sourceFile);
790
+ return matches;
791
+ },
792
+ transform(sourceFile, matches) {
793
+ if (matches.length === 0) return sourceFile.getFullText();
794
+ let text = sourceFile.getFullText();
795
+ const typedMatches = matches;
796
+ const sortedMatches = [...typedMatches].sort((a, b) => {
797
+ return b.classBodyStart - a.classBodyStart;
798
+ });
799
+ for (const match of sortedMatches) {
800
+ if (!match.hasAttachInternals) {
801
+ const newMember = `\n${match.indent}@AttachInternals() internals: ElementInternals;\n`;
802
+ text = text.slice(0, match.classBodyStart) + newMember + text.slice(match.classBodyStart);
803
+ }
804
+ const prop = match.node;
805
+ const start = prop.getStart();
806
+ let end = prop.getEnd();
807
+ const afterProp = text.slice(end).match(/^\s*,/);
808
+ if (afterProp) end = end + afterProp[0].length;
809
+ text = text.slice(0, start) + text.slice(end);
810
+ }
811
+ const firstMatch = typedMatches[0];
812
+ if (firstMatch?.needsImport && firstMatch.stencilImportEnd > 0) {
813
+ const importMatch = text.match(/import\s*\{([^}]*)\}\s*from\s*['"]@stencil\/core['"]/);
814
+ if (importMatch) {
815
+ const newImports = importMatch[1].trimEnd() + ", AttachInternals";
816
+ text = text.replace(importMatch[0], `import {${newImports}} from '@stencil/core'`);
817
+ }
818
+ }
819
+ return text;
820
+ }
821
+ };
822
+ //#endregion
823
+ //#region src/migrations/index.ts
824
+ /**
825
+ * Build a map of local import names to their original names from @stencil/core.
826
+ * Handles aliased imports like `import { Component as Cmp } from '@stencil/core'`.
827
+ * Also handles multiple imports from @stencil/core (e.g., separate type and value imports).
828
+ *
829
+ * @param sourceFile The TypeScript source file to analyze
830
+ * @returns Map where keys are local names and values are original imported names
831
+ */
832
+ const getStencilCoreImportMap = (sourceFile) => {
833
+ const importMap = /* @__PURE__ */ new Map();
834
+ for (const statement of sourceFile.statements) if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "@stencil/core") {
835
+ const namedBindings = statement.importClause?.namedBindings;
836
+ if (namedBindings && ts.isNamedImports(namedBindings)) for (const element of namedBindings.elements) {
837
+ const localName = element.name.text;
838
+ const originalName = element.propertyName?.text ?? element.name.text;
839
+ importMap.set(localName, originalName);
840
+ }
841
+ }
842
+ return importMap;
843
+ };
844
+ /**
845
+ * Check if a decorator identifier refers to a specific @stencil/core export.
846
+ * Handles aliased imports like `import { Component as Cmp } from '@stencil/core'`.
847
+ *
848
+ * @param decoratorName The identifier used in the decorator
849
+ * @param expectedOriginalName The original export name (e.g., 'Component')
850
+ * @param importMap The import map from getStencilCoreImportMap
851
+ * @returns True if the decorator refers to the expected export
852
+ */
853
+ const isStencilDecorator = (decoratorName, expectedOriginalName, importMap) => {
854
+ return importMap.get(decoratorName) === expectedOriginalName;
855
+ };
856
+ /**
857
+ * Registry of all available migration rules.
858
+ * Rules are applied in order, so add new rules at the end.
859
+ */
860
+ const migrationRules = [encapsulationApiRule, formAssociatedRule];
861
+ /**
862
+ * Get all migration rules for a specific version upgrade.
863
+ * @param fromVersion Source version (e.g., '4')
864
+ * @param toVersion Target version (e.g., '5')
865
+ * @returns Filtered list of applicable rules
866
+ */
867
+ const getRulesForVersionUpgrade = (fromVersion, toVersion) => {
868
+ return migrationRules.filter((rule) => rule.fromVersion.startsWith(fromVersion) && rule.toVersion.startsWith(toVersion));
869
+ };
870
+ //#endregion
871
+ //#region src/task-migrate.ts
872
+ /**
873
+ * Run the migration task to update Stencil components from v4 to v5 API.
874
+ *
875
+ * @param coreCompiler the Stencil compiler instance
876
+ * @param config the validated Stencil config
877
+ * @param flags CLI flags (includes dryRun option)
878
+ */
879
+ const taskMigrate = async (coreCompiler, config, flags) => {
880
+ const logger = config.logger;
881
+ const sys = config.sys;
882
+ const dryRun = flags.dryRun ?? false;
883
+ const currentMajor = coreCompiler.version.split(".")[0];
884
+ const fromVersion = String(Number(currentMajor) - 1);
885
+ const toVersion = currentMajor;
886
+ const rules = getRulesForVersionUpgrade(fromVersion, toVersion);
887
+ if (rules.length === 0) {
888
+ logger.info(`No migration rules found for ${fromVersion}.x → ${toVersion}.x upgrade.`);
889
+ return;
890
+ }
891
+ logger.info(`${logger.emoji("🔄 ")}Stencil Migration Tool (v${fromVersion} → v${toVersion})`);
892
+ logger.info(`Scanning for components that need migration...`);
893
+ if (dryRun) logger.info(logger.cyan("Dry run mode - no files will be modified"));
894
+ const tsFiles = await getTypeScriptFiles(config, sys, logger);
895
+ if (tsFiles.length === 0) {
896
+ logger.info(`No TypeScript files found. Check your tsconfig.json configuration.`);
897
+ return;
898
+ }
899
+ logger.info(`Found ${tsFiles.length} TypeScript files to scan`);
900
+ const results = [];
901
+ for (const filePath of tsFiles) {
902
+ let content = await sys.readFile(filePath);
903
+ if (!content) continue;
904
+ for (const rule of rules) {
905
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
906
+ const matches = rule.detect(sourceFile);
907
+ if (matches.length > 0) {
908
+ const relPath = relative(config.rootDir, filePath);
909
+ logger.info(`\n${logger.cyan(relPath)}`);
910
+ logger.info(` ${logger.yellow(`[${rule.id}]`)} ${rule.name}`);
911
+ for (const match of matches) logger.info(` Line ${match.line}: ${match.message}`);
912
+ if (!dryRun) {
913
+ const transformed = rule.transform(sourceFile, matches);
914
+ await sys.writeFile(filePath, transformed);
915
+ content = transformed;
916
+ results.push({
917
+ filePath,
918
+ rule,
919
+ matches,
920
+ transformed: true
921
+ });
922
+ logger.info(` ${logger.green("✓")} Migrated`);
923
+ } else results.push({
924
+ filePath,
925
+ rule,
926
+ matches,
927
+ transformed: false
928
+ });
929
+ }
930
+ }
931
+ }
932
+ logger.info("\n" + logger.bold("Migration Summary"));
933
+ logger.info("─".repeat(40));
934
+ const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
935
+ const filesAffected = new Set(results.map((r) => r.filePath)).size;
936
+ if (totalMatches === 0) logger.info(logger.green("No migrations needed - your code is up to date!"));
937
+ else {
938
+ logger.info(`Found ${totalMatches} item(s) to migrate in ${filesAffected} file(s)`);
939
+ if (dryRun) logger.info(logger.yellow("\nRun without --dry-run to apply the migrations"));
940
+ else logger.info(logger.green(`\n✓ Successfully migrated ${totalMatches} item(s)`));
941
+ }
942
+ const byRule = /* @__PURE__ */ new Map();
943
+ for (const result of results) {
944
+ const existing = byRule.get(result.rule.id) || [];
945
+ existing.push(result);
946
+ byRule.set(result.rule.id, existing);
947
+ }
948
+ if (byRule.size > 0) {
949
+ logger.info("\nBy migration rule:");
950
+ for (const [ruleId, ruleResults] of byRule) {
951
+ const rule = rules.find((r) => r.id === ruleId);
952
+ const count = ruleResults.reduce((sum, r) => sum + r.matches.length, 0);
953
+ logger.info(` ${rule?.name || ruleId}: ${count} item(s)`);
954
+ }
955
+ }
956
+ };
957
+ /**
958
+ * Detect available migrations without applying them.
959
+ * Used by the build task to check if migrations might help fix build errors.
960
+ *
961
+ * @param coreCompiler the Stencil compiler instance
962
+ * @param config the validated Stencil config
963
+ * @returns detection result with migration information
964
+ */
965
+ const detectMigrations = async (coreCompiler, config) => {
966
+ const sys = config.sys;
967
+ const logger = config.logger;
968
+ const currentMajor = coreCompiler.version.split(".")[0];
969
+ const rules = getRulesForVersionUpgrade(String(Number(currentMajor) - 1), currentMajor);
970
+ if (rules.length === 0) return {
971
+ hasMigrations: false,
972
+ totalMatches: 0,
973
+ filesAffected: 0,
974
+ migrations: [],
975
+ rules: []
976
+ };
977
+ const tsFiles = await getTypeScriptFiles(config, sys, logger);
978
+ if (tsFiles.length === 0) return {
979
+ hasMigrations: false,
980
+ totalMatches: 0,
981
+ filesAffected: 0,
982
+ migrations: [],
983
+ rules
984
+ };
985
+ const migrations = [];
986
+ for (const filePath of tsFiles) {
987
+ const content = await sys.readFile(filePath);
988
+ if (!content) continue;
989
+ for (const rule of rules) {
990
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
991
+ const matches = rule.detect(sourceFile);
992
+ if (matches.length > 0) migrations.push({
993
+ filePath,
994
+ rule,
995
+ matches
996
+ });
997
+ }
998
+ }
999
+ const totalMatches = migrations.reduce((sum, m) => sum + m.matches.length, 0);
1000
+ const filesAffected = new Set(migrations.map((m) => m.filePath)).size;
1001
+ return {
1002
+ hasMigrations: migrations.length > 0,
1003
+ totalMatches,
1004
+ filesAffected,
1005
+ migrations,
1006
+ rules
1007
+ };
1008
+ };
1009
+ /**
1010
+ * Get TypeScript files using the project's tsconfig.json.
1011
+ * Uses the same approach as the Stencil compiler.
1012
+ *
1013
+ * @param config the validated Stencil config
1014
+ * @param sys the compiler system for file operations
1015
+ * @param logger the logger for output
1016
+ * @returns array of absolute paths to TypeScript files
1017
+ */
1018
+ async function getTypeScriptFiles(config, sys, logger) {
1019
+ let tsconfigPath;
1020
+ if (config.tsconfig) tsconfigPath = isAbsolute(config.tsconfig) ? config.tsconfig : join(config.rootDir, config.tsconfig);
1021
+ else tsconfigPath = join(config.rootDir, "tsconfig.json");
1022
+ logger.debug(`Using tsconfig: ${tsconfigPath}`);
1023
+ const tsconfigContent = await sys.readFile(tsconfigPath);
1024
+ if (!tsconfigContent) {
1025
+ logger.error(`tsconfig not found: ${tsconfigPath}`);
1026
+ return [];
1027
+ }
1028
+ const host = {
1029
+ ...ts.sys,
1030
+ readFile: (p) => {
1031
+ if (p === tsconfigPath) return tsconfigContent;
1032
+ return ts.sys.readFile(p);
1033
+ },
1034
+ onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
1035
+ logger.error(ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
1036
+ }
1037
+ };
1038
+ const results = ts.getParsedCommandLineOfConfigFile(tsconfigPath, {}, host);
1039
+ if (!results) {
1040
+ logger.error(`Failed to parse tsconfig: ${tsconfigPath}`);
1041
+ return [];
1042
+ }
1043
+ 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"));
1045
+ }
1046
+ //#endregion
729
1047
  //#region src/task-prerender.ts
730
1048
  const taskPrerender = async (coreCompiler, config, flags) => {
731
1049
  startupCompilerLog(coreCompiler, config);
@@ -1000,9 +1318,9 @@ function getActiveTargets(config) {
1000
1318
  * @returns a Promise wrapping data for the telemetry endpoint
1001
1319
  */
1002
1320
  const prepareData = async (coreCompiler, config, sys, flags, duration_ms, component_count = void 0) => {
1003
- const { typescript, rollup } = coreCompiler.versions || {
1321
+ const { typescript, rolldown } = coreCompiler.versions || {
1004
1322
  typescript: "unknown",
1005
- rollup: "unknown"
1323
+ rolldown: "unknown"
1006
1324
  };
1007
1325
  const { packages, packagesNoVersions } = await getInstalledPackages(sys, flags);
1008
1326
  const targets = getActiveTargets(config);
@@ -1027,7 +1345,7 @@ const prepareData = async (coreCompiler, config, sys, flags, duration_ms, compon
1027
1345
  os_version,
1028
1346
  packages,
1029
1347
  packages_no_versions: packagesNoVersions,
1030
- rollup,
1348
+ rolldown,
1031
1349
  stencil,
1032
1350
  system,
1033
1351
  system_major: getMajorVersion(system),
@@ -1051,13 +1369,11 @@ const CONFIG_PROPS_TO_ANONYMIZE = [
1051
1369
  "tsconfig"
1052
1370
  ];
1053
1371
  const CONFIG_PROPS_TO_DELETE = [
1054
- "commonjs",
1055
1372
  "devServer",
1056
1373
  "env",
1057
1374
  "logger",
1058
- "rollupConfig",
1375
+ "rolldownConfig",
1059
1376
  "sys",
1060
- "testing",
1061
1377
  "tsCompilerOptions"
1062
1378
  ];
1063
1379
  /**
@@ -1276,8 +1592,36 @@ const taskBuild = async (coreCompiler, config, flags) => {
1276
1592
  const results = await compiler.build();
1277
1593
  await telemetryBuildFinishedAction(config.sys, config, coreCompiler, results, flags);
1278
1594
  await compiler.destroy();
1279
- if (results.hasError) exitCode = 1;
1280
- else if (flags.prerender) {
1595
+ if (results.hasError) {
1596
+ const migrationResult = await detectMigrations(coreCompiler, config);
1597
+ if (migrationResult.hasMigrations) {
1598
+ const action = await promptForMigrationOnBuildError(config, migrationResult);
1599
+ if (action === "run") {
1600
+ await taskMigrate(coreCompiler, config, {
1601
+ ...flags,
1602
+ dryRun: false
1603
+ });
1604
+ config.logger.info("\nRe-running build after migrations...\n");
1605
+ const newCompiler = await coreCompiler.createCompiler(config);
1606
+ const newResults = await newCompiler.build();
1607
+ await newCompiler.destroy();
1608
+ if (!newResults.hasError) {
1609
+ exitCode = 0;
1610
+ if (flags.prerender) {
1611
+ const prerenderDiagnostics = await runPrerenderTask(coreCompiler, config, newResults.hydrateAppFilePath, newResults.componentGraph, void 0);
1612
+ config.logger.printDiagnostics(prerenderDiagnostics);
1613
+ if (prerenderDiagnostics.some((d) => d.level === "error")) exitCode = 1;
1614
+ }
1615
+ } else exitCode = 1;
1616
+ } else if (action === "dry-run") {
1617
+ await taskMigrate(coreCompiler, config, {
1618
+ ...flags,
1619
+ dryRun: true
1620
+ });
1621
+ exitCode = 1;
1622
+ } else exitCode = 1;
1623
+ } else exitCode = 1;
1624
+ } else if (flags.prerender) {
1281
1625
  const prerenderDiagnostics = await runPrerenderTask(coreCompiler, config, results.hydrateAppFilePath, results.componentGraph, void 0);
1282
1626
  config.logger.printDiagnostics(prerenderDiagnostics);
1283
1627
  if (prerenderDiagnostics.some((d) => d.level === "error")) exitCode = 1;
@@ -1289,6 +1633,51 @@ const taskBuild = async (coreCompiler, config, flags) => {
1289
1633
  }
1290
1634
  if (exitCode > 0) return config.sys.exit(exitCode);
1291
1635
  };
1636
+ /**
1637
+ * Prompt the user about available migrations when a build fails.
1638
+ * Shows what migrations are available and lets them choose to run them.
1639
+ * @param config the Stencil config
1640
+ * @param migrationResult the result of migration detection with available migrations
1641
+ * @returns the user's chosen action for handling migrations
1642
+ */
1643
+ async function promptForMigrationOnBuildError(config, migrationResult) {
1644
+ const logger = config.logger;
1645
+ logger.info("");
1646
+ logger.info(logger.bold(logger.yellow("Migrations Available")));
1647
+ logger.info("─".repeat(40));
1648
+ logger.info(`Found ${migrationResult.totalMatches} item(s) in ${migrationResult.filesAffected} file(s) that can be automatically migrated.`);
1649
+ for (const migration of migrationResult.migrations) {
1650
+ const relPath = relative(config.rootDir, migration.filePath);
1651
+ logger.info(` ${logger.cyan(relPath)}: ${migration.matches.length} item(s)`);
1652
+ }
1653
+ logger.info("");
1654
+ logger.info("These migrations may help resolve the build errors above.");
1655
+ const prompt = (await import("prompts")).default;
1656
+ const response = await prompt({
1657
+ name: "action",
1658
+ type: "select",
1659
+ message: "What would you like to do?",
1660
+ choices: [
1661
+ {
1662
+ title: "Run migration",
1663
+ value: "run",
1664
+ description: "Apply migrations and re-run build"
1665
+ },
1666
+ {
1667
+ title: "Dry run",
1668
+ value: "dry-run",
1669
+ description: "Preview changes without modifying files"
1670
+ },
1671
+ {
1672
+ title: "Exit",
1673
+ value: "exit",
1674
+ description: "Exit without making changes"
1675
+ }
1676
+ ]
1677
+ });
1678
+ if (response.action === void 0) return "exit";
1679
+ return response.action;
1680
+ }
1292
1681
  //#endregion
1293
1682
  //#region src/task-docs.ts
1294
1683
  const taskDocs = async (coreCompiler, config) => {
@@ -1680,7 +2069,7 @@ const taskInfo = (coreCompiler, sys, logger) => {
1680
2069
  console.log(`${logger.cyan(" Build:")} ${coreCompiler.buildId}`);
1681
2070
  console.log(`${logger.cyan(" Stencil:")} ${coreCompiler.version}${logger.emoji(" " + coreCompiler.vermoji)}`);
1682
2071
  console.log(`${logger.cyan(" TypeScript:")} ${versions.typescript}`);
1683
- console.log(`${logger.cyan(" Rollup:")} ${versions.rollup}`);
2072
+ console.log(`${logger.cyan(" Rolldown:")} ${versions.rolldown}`);
1684
2073
  console.log(`${logger.cyan(" Terser:")} ${versions.terser}`);
1685
2074
  console.log(``);
1686
2075
  };
@@ -1810,6 +2199,9 @@ const runTask = async (coreCompiler, config, task, sys, flags) => {
1810
2199
  case "help":
1811
2200
  await taskHelp(resolvedFlags, strictConfig.logger, sys);
1812
2201
  break;
2202
+ case "migrate":
2203
+ await taskMigrate(coreCompiler, strictConfig, resolvedFlags);
2204
+ break;
1813
2205
  case "prerender":
1814
2206
  await taskPrerender(coreCompiler, strictConfig, resolvedFlags);
1815
2207
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stencil/cli",
3
- "version": "5.0.0-alpha.2",
3
+ "version": "5.0.0-alpha.4",
4
4
  "description": "CLI for Stencil - Web component compiler",
5
5
  "keywords": [
6
6
  "components",
@@ -38,13 +38,14 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "prompts": "^2.4.2",
41
- "@stencil/dev-server": "5.0.0-alpha.2"
41
+ "@stencil/dev-server": "5.0.0-alpha.4"
42
42
  },
43
43
  "devDependencies": {
44
+ "@types/prompts": "^2.4.9",
44
45
  "tsdown": "^0.21.6",
45
46
  "typescript": "~6.0.2",
46
47
  "vitest": "^4.1.1",
47
- "@stencil/core": "5.0.0-alpha.2"
48
+ "@stencil/core": "5.0.0-alpha.4"
48
49
  },
49
50
  "peerDependencies": {
50
51
  "@stencil/core": "^5.0.0-0"