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