@stencil/cli 5.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1832 @@
1
+ import { LOG_LEVELS } from "@stencil/core/compiler";
2
+ import { buildError, catchError, hasError, isFunction, isOutputTargetDocs, isOutputTargetHydrate, isOutputTargetWww, isString, normalizePath, readOnlyArrayHasStringMember, result, shouldIgnoreError, toCamelCase, validateComponentTag } from "@stencil/core/compiler/utils";
3
+ import { join, parse, relative } from "path";
4
+ import { start } from "@stencil/dev-server";
5
+ //#region src/config-flags.ts
6
+ /**
7
+ * All the Boolean options supported by the Stencil CLI
8
+ */
9
+ const BOOLEAN_CLI_FLAGS = [
10
+ "build",
11
+ "cache",
12
+ "checkVersion",
13
+ "ci",
14
+ "compare",
15
+ "debug",
16
+ "dev",
17
+ "devtools",
18
+ "docs",
19
+ "e2e",
20
+ "es5",
21
+ "esm",
22
+ "help",
23
+ "log",
24
+ "open",
25
+ "prerender",
26
+ "prerenderExternal",
27
+ "prod",
28
+ "profile",
29
+ "serviceWorker",
30
+ "screenshot",
31
+ "serve",
32
+ "skipNodeCheck",
33
+ "spec",
34
+ "ssr",
35
+ "updateScreenshot",
36
+ "verbose",
37
+ "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"
79
+ ];
80
+ /**
81
+ * All the Number options supported by the Stencil CLI
82
+ */
83
+ const NUMBER_CLI_FLAGS = [
84
+ "port",
85
+ "maxConcurrency",
86
+ "testTimeout"
87
+ ];
88
+ /**
89
+ * All the String options supported by the Stencil CLI
90
+ */
91
+ const STRING_CLI_FLAGS = [
92
+ "address",
93
+ "config",
94
+ "docsApi",
95
+ "docsJson",
96
+ "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"
151
+ ];
152
+ /**
153
+ * All the CLI arguments which may have string or number values
154
+ *
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.
157
+ */
158
+ const STRING_NUMBER_CLI_FLAGS = ["maxWorkers"];
159
+ /**
160
+ * All the CLI arguments which may have boolean or string values.
161
+ */
162
+ const BOOLEAN_STRING_CLI_FLAGS = ["headless", "stats"];
163
+ /**
164
+ * All the LogLevel-type options supported by the Stencil CLI
165
+ *
166
+ * This is a bit silly since there's only one such argument atm,
167
+ * but this approach lets us make sure that we're handling all
168
+ * our arguments in a type-safe way.
169
+ */
170
+ const LOG_LEVEL_CLI_FLAGS = ["logLevel"];
171
+ /**
172
+ * For a small subset of CLI options we support a short alias e.g. `'h'` for `'help'`
173
+ */
174
+ const CLI_FLAG_ALIASES = {
175
+ c: "config",
176
+ h: "help",
177
+ 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"
187
+ };
188
+ /**
189
+ * A regular expression which can be used to match a CLI flag for one of our
190
+ * short aliases.
191
+ */
192
+ const CLI_FLAG_REGEX = new RegExp(`^-[chpvbewofitu]{1}$`);
193
+ /**
194
+ * Helper function for initializing a `ConfigFlags` object. Provide any overrides
195
+ * for default values and off you go!
196
+ *
197
+ * @param init an object with any overrides for default values
198
+ * @returns a complete CLI flag object
199
+ */
200
+ const createConfigFlags = (init = {}) => {
201
+ return {
202
+ task: null,
203
+ args: [],
204
+ knownArgs: [],
205
+ unknownArgs: [],
206
+ ...init
207
+ };
208
+ };
209
+ //#endregion
210
+ //#region src/parse-flags.ts
211
+ /**
212
+ * Parse command line arguments into a structured `ConfigFlags` object
213
+ *
214
+ * @param args an array of CLI flags
215
+ * @returns a structured ConfigFlags object
216
+ */
217
+ const parseFlags = (args) => {
218
+ const flags = createConfigFlags();
219
+ flags.args = Array.isArray(args) ? args.slice() : [];
220
+ if (flags.args.length > 0 && flags.args[0] && !flags.args[0].startsWith("-")) {
221
+ flags.task = flags.args[0];
222
+ parseArgs(flags, args.slice(1));
223
+ } else parseArgs(flags, flags.args);
224
+ if (flags.task != null) {
225
+ const i = flags.args.indexOf(flags.task);
226
+ if (i > -1) flags.args.splice(i, 1);
227
+ }
228
+ return flags;
229
+ };
230
+ /**
231
+ * Parse the supported command line flags which are enumerated in the
232
+ * `config-flags` module. Handles leading dashes on arguments, aliases that are
233
+ * defined for a small number of arguments, and parsing values for non-boolean
234
+ * arguments (e.g. port number for the dev server).
235
+ *
236
+ * This parses the following grammar:
237
+ *
238
+ * CLIArguments → ""
239
+ * | CLITerm ( " " CLITerm )* ;
240
+ * CLITerm → EqualsArg
241
+ * | AliasEqualsArg
242
+ * | AliasArg
243
+ * | NegativeDashArg
244
+ * | NegativeArg
245
+ * | SimpleArg ;
246
+ * EqualsArg → "--" ArgName "=" CLIValue ;
247
+ * AliasEqualsArg → "-" AliasName "=" CLIValue ;
248
+ * AliasArg → "-" AliasName ( " " CLIValue )? ;
249
+ * NegativeDashArg → "--no-" ArgName ;
250
+ * NegativeArg → "--no" ArgName ;
251
+ * SimpleArg → "--" ArgName ( " " CLIValue )? ;
252
+ * ArgName → /^[a-zA-Z-]+$/ ;
253
+ * AliasName → /^[a-z]{1}$/ ;
254
+ * CLIValue → '"' /^[a-zA-Z0-9]+$/ '"'
255
+ * | /^[a-zA-Z0-9]+$/ ;
256
+ *
257
+ * There are additional constraints (not shown in the grammar for brevity's sake)
258
+ * on the type of `CLIValue` which will be associated with a particular argument.
259
+ * We enforce this by declaring lists of boolean, string, etc arguments and
260
+ * checking the types of values before setting them.
261
+ *
262
+ * We don't need to turn the list of CLI arg tokens into any kind of
263
+ * intermediate representation since we aren't concerned with doing anything
264
+ * other than setting the correct values on our ConfigFlags object. So we just
265
+ * parse the array of string arguments using a recursive-descent approach
266
+ * (which is not very deep since our grammar is pretty simple) and make the
267
+ * modifications we need to make to the {@link ConfigFlags} object as we go.
268
+ *
269
+ * @param flags a ConfigFlags object to which parsed arguments will be added
270
+ * @param args an array of command-line arguments to parse
271
+ */
272
+ const parseArgs = (flags, args) => {
273
+ const argsCopy = args.concat();
274
+ while (argsCopy.length > 0) parseCLITerm(flags, argsCopy);
275
+ };
276
+ /**
277
+ * Given an array of CLI arguments, parse it and perform a series of side
278
+ * effects (setting values on the provided `ConfigFlags` object).
279
+ *
280
+ * @param flags a {@link ConfigFlags} object which is updated as we parse the CLI
281
+ * arguments
282
+ * @param args a list of args to work through. This function (and some functions
283
+ * it calls) calls `Array.prototype.shift` to get the next argument to look at,
284
+ * so this parameter will be modified.
285
+ */
286
+ const parseCLITerm = (flags, args) => {
287
+ const arg = args.shift();
288
+ if (arg === void 0) return;
289
+ const isNegatedBoolean = !readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizeFlagName(arg)) && readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizeNegativeFlagName(arg));
290
+ const isNegatedBooleanOrString = !readOnlyArrayHasStringMember(BOOLEAN_STRING_CLI_FLAGS, normalizeFlagName(arg)) && readOnlyArrayHasStringMember(BOOLEAN_STRING_CLI_FLAGS, normalizeNegativeFlagName(arg));
291
+ if (arg.startsWith("--") && arg.includes("=")) {
292
+ const [originalArg, value] = parseEqualsArg(arg);
293
+ setCLIArg(flags, arg.split("=")[0], normalizeFlagName(originalArg), value);
294
+ } else if (arg.startsWith("-") && arg.includes("=")) {
295
+ const [originalArg, value] = parseEqualsArg(arg);
296
+ setCLIArg(flags, desugarRawAlias(originalArg), normalizeFlagName(originalArg), value);
297
+ } else if (CLI_FLAG_REGEX.test(arg)) setCLIArg(flags, desugarRawAlias(arg), normalizeFlagName(arg), parseCLIValue(args));
298
+ else if (arg.startsWith("--no-") && arg.length > 5) setCLIArg(flags, arg, normalizeNegativeFlagName(arg), "");
299
+ else if (arg.startsWith("--no") && (isNegatedBoolean || isNegatedBooleanOrString)) setCLIArg(flags, arg, normalizeNegativeFlagName(arg), "");
300
+ else if (arg.startsWith("--") && arg.length > 2) setCLIArg(flags, arg, normalizeFlagName(arg), parseCLIValue(args));
301
+ else flags.unknownArgs.push(arg);
302
+ };
303
+ /**
304
+ * Normalize a 'negative' flag name, just to do a little pre-processing before
305
+ * we pass it to {@link setCLIArg}.
306
+ *
307
+ * @param flagName the flag name to normalize
308
+ * @returns a normalized flag name
309
+ */
310
+ const normalizeNegativeFlagName = (flagName) => {
311
+ const trimmed = flagName.replace(/^--no[-]?/, "");
312
+ return normalizeFlagName(trimmed.charAt(0).toLowerCase() + trimmed.slice(1));
313
+ };
314
+ /**
315
+ * Normalize a flag name by:
316
+ *
317
+ * - replacing any leading dashes (`--foo` -> `foo`)
318
+ * - converting `dash-case` to camelCase (if necessary)
319
+ *
320
+ * Normalizing in this context basically means converting the various
321
+ * supported flag spelling variants to the variant defined in our lists of
322
+ * supported arguments (e.g. BOOLEAN_CLI_FLAGS, etc). So, for instance,
323
+ * `--log-level` should be converted to `logLevel`.
324
+ *
325
+ * @param flagName the flag name to normalize
326
+ * @returns a normalized flag name
327
+ *
328
+ */
329
+ const normalizeFlagName = (flagName) => {
330
+ const trimmed = flagName.replace(/^-+/, "");
331
+ return trimmed.includes("-") ? toCamelCase(trimmed) : trimmed;
332
+ };
333
+ /**
334
+ * Set a value on a provided {@link ConfigFlags} object, given an argument
335
+ * name and a raw string value. This function dispatches to other functions
336
+ * which make sure that the string value can be properly parsed into a JS
337
+ * runtime value of the right type (e.g. number, string, etc).
338
+ *
339
+ * @throws if a value cannot be parsed to the right type for a given flag
340
+ * @param flags a {@link ConfigFlags} object
341
+ * @param rawArg the raw argument name matched by the parser
342
+ * @param normalizedArg an argument with leading control characters (`--`,
343
+ * `--no-`, etc) removed
344
+ * @param value the raw value to be set onto the config flags object
345
+ */
346
+ const setCLIArg = (flags, rawArg, normalizedArg, value) => {
347
+ normalizedArg = desugarAlias(normalizedArg);
348
+ if (readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizedArg)) {
349
+ flags[normalizedArg] = typeof value === "string" ? value === "true" : true;
350
+ flags.knownArgs.push(rawArg);
351
+ if (typeof value === "string" && value !== "") flags.knownArgs.push(value);
352
+ } else if (readOnlyArrayHasStringMember(STRING_CLI_FLAGS, normalizedArg)) if (typeof value === "string") {
353
+ flags[normalizedArg] = value;
354
+ flags.knownArgs.push(rawArg);
355
+ flags.knownArgs.push(value);
356
+ } 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];
360
+ if (Array.isArray(targetArray)) {
361
+ targetArray.push(value);
362
+ flags.knownArgs.push(rawArg);
363
+ flags.knownArgs.push(value);
364
+ }
365
+ } else throwCLIParsingError(rawArg, "expected a string argument but received nothing");
366
+ else if (readOnlyArrayHasStringMember(NUMBER_CLI_FLAGS, normalizedArg)) if (typeof value === "string") {
367
+ const parsed = parseInt(value, 10);
368
+ if (isNaN(parsed)) throwNumberParsingError(rawArg, value);
369
+ else {
370
+ flags[normalizedArg] = parsed;
371
+ flags.knownArgs.push(rawArg);
372
+ flags.knownArgs.push(value);
373
+ }
374
+ } else throwCLIParsingError(rawArg, "expected a number argument but received nothing");
375
+ else if (readOnlyArrayHasStringMember(STRING_NUMBER_CLI_FLAGS, normalizedArg)) if (typeof value === "string") {
376
+ if (CLI_ARG_STRING_REGEX.test(value)) flags[normalizedArg] = value;
377
+ else {
378
+ const parsed = Number(value);
379
+ if (isNaN(parsed)) throwNumberParsingError(rawArg, value);
380
+ else flags[normalizedArg] = parsed;
381
+ }
382
+ flags.knownArgs.push(rawArg);
383
+ flags.knownArgs.push(value);
384
+ } else throwCLIParsingError(rawArg, "expected a string or a number but received nothing");
385
+ else if (readOnlyArrayHasStringMember(BOOLEAN_STRING_CLI_FLAGS, normalizedArg)) {
386
+ const derivedValue = typeof value === "string" ? value ? value : false : true;
387
+ flags[normalizedArg] = derivedValue;
388
+ flags.knownArgs.push(rawArg);
389
+ if (typeof derivedValue === "string" && derivedValue) flags.knownArgs.push(derivedValue);
390
+ } else if (readOnlyArrayHasStringMember(LOG_LEVEL_CLI_FLAGS, normalizedArg)) if (typeof value === "string") if (isLogLevel(value)) {
391
+ flags[normalizedArg] = value;
392
+ flags.knownArgs.push(rawArg);
393
+ flags.knownArgs.push(value);
394
+ } else throwCLIParsingError(rawArg, `expected to receive a valid log level but received "${String(value)}"`);
395
+ else throwCLIParsingError(rawArg, "expected to receive a valid log level but received nothing");
396
+ else {
397
+ flags.unknownArgs.push(rawArg);
398
+ if (typeof value === "string") flags.unknownArgs.push(value);
399
+ }
400
+ };
401
+ /**
402
+ * We use this regular expression to detect CLI parameters which
403
+ * should be parsed as string values (as opposed to numbers) for
404
+ * the argument types for which we support both a string and a
405
+ * number value.
406
+ *
407
+ * The regex tests for the presence of at least one character which is
408
+ * _not_ a digit (`\d`), a period (`\.`), or one of the characters `"e"`,
409
+ * `"E"`, `"+"`, or `"-"` (the latter four characters are necessary to
410
+ * support the admittedly unlikely use of scientific notation, like `"4e+0"`
411
+ * for `4`).
412
+ *
413
+ * Thus we'll match a string like `"50%"`, but not a string like `"50"` or
414
+ * `"5.0"`. If it matches a given string we conclude that the string should
415
+ * be parsed as a string literal, rather than using `Number` to convert it
416
+ * to a number.
417
+ */
418
+ const CLI_ARG_STRING_REGEX = /[^\d.Ee+-]+/g;
419
+ const Empty = Symbol("Empty");
420
+ /**
421
+ * A little helper which tries to parse a CLI value (as opposed to a flag) off
422
+ * of the argument array.
423
+ *
424
+ * We support a variety of different argument formats for flags (as opposed to
425
+ * values), but all of them start with `-`, so we can check the first character
426
+ * to test whether the next token in our array of CLI arguments is a flag name
427
+ * or a value.
428
+ *
429
+ * @param args an array of CLI args
430
+ * @returns either a string result or an Empty sentinel
431
+ */
432
+ const parseCLIValue = (args) => {
433
+ if (args[0] === void 0) return Empty;
434
+ if (!args[0].startsWith("-")) {
435
+ const value = args.shift();
436
+ if (typeof value === "string") return value;
437
+ }
438
+ return Empty;
439
+ };
440
+ /**
441
+ * Parse an 'equals' argument, which is a CLI argument-value pair in the
442
+ * format `--foobar=12` (as opposed to a space-separated format like
443
+ * `--foobar 12`).
444
+ *
445
+ * To parse this we split on the `=`, returning the first part as the argument
446
+ * name and the second part as the value. We join the value on `"="` in case
447
+ * there is another `"="` in the argument.
448
+ *
449
+ * This function is safe to call with any arg, and can therefore be used as
450
+ * an argument 'normalizer'. If CLI argument is not an 'equals' argument then
451
+ * the return value will be a tuple of the original argument and an empty
452
+ * string `""` for the value.
453
+ *
454
+ * In code terms, if you do:
455
+ *
456
+ * ```ts
457
+ * const [arg, value] = parseEqualsArg("--myArgument")
458
+ * ```
459
+ *
460
+ * Then `arg` will be `"--myArgument"` and `value` will be `""`, whereas if
461
+ * you do:
462
+ *
463
+ *
464
+ * ```ts
465
+ * const [arg, value] = parseEqualsArg("--myArgument=myValue")
466
+ * ```
467
+ *
468
+ * Then `arg` will be `"--myArgument"` and `value` will be `"myValue"`.
469
+ *
470
+ * @param arg the arg in question
471
+ * @returns a tuple containing the arg name and the value (if present)
472
+ */
473
+ const parseEqualsArg = (arg) => {
474
+ const [originalArg, ...splitSections] = arg.split("=");
475
+ const value = splitSections.join("=");
476
+ return [originalArg, value === "" ? Empty : value];
477
+ };
478
+ /**
479
+ * Small helper for getting type-system-level assurance that a `string` can be
480
+ * narrowed to a `LogLevel`
481
+ *
482
+ * @param maybeLogLevel the string to check
483
+ * @returns whether this is a `LogLevel`
484
+ */
485
+ const isLogLevel = (maybeLogLevel) => readOnlyArrayHasStringMember(LOG_LEVELS, maybeLogLevel);
486
+ /**
487
+ * A little helper for constructing and throwing an error message with info
488
+ * about what went wrong
489
+ *
490
+ * @param flag the flag which encountered the error
491
+ * @param message a message specific to the error which was encountered
492
+ */
493
+ const throwCLIParsingError = (flag, message) => {
494
+ throw new Error(`when parsing CLI flag "${flag}": ${message}`);
495
+ };
496
+ /**
497
+ * Throw a specific error for the situation where we ran into an issue parsing
498
+ * a number.
499
+ *
500
+ * @param flag the flag for which we encountered the issue
501
+ * @param value what we were trying to parse
502
+ */
503
+ const throwNumberParsingError = (flag, value) => {
504
+ throwCLIParsingError(flag, `expected a number but received "${value}"`);
505
+ };
506
+ /**
507
+ * A little helper to 'desugar' a flag alias, meaning expand it to its full
508
+ * name. For instance, the alias `"c"` will desugar to `"config"`.
509
+ *
510
+ * If no expansion is found for the possible alias we just return the passed
511
+ * string unmodified.
512
+ *
513
+ * @param maybeAlias a string which _could_ be an alias to a full flag name
514
+ * @returns the full aliased flag name, if found, or the passed string if not
515
+ */
516
+ const desugarAlias = (maybeAlias) => {
517
+ const possiblyDesugared = CLI_FLAG_ALIASES[maybeAlias];
518
+ if (typeof possiblyDesugared === "string") return possiblyDesugared;
519
+ return maybeAlias;
520
+ };
521
+ /**
522
+ * Desugar a 'raw' alias (with a leading dash) and return an equivalent,
523
+ * desugared argument.
524
+ *
525
+ * For instance, passing `"-c` will return `"--config"`.
526
+ *
527
+ * The reason we'd like to do this is not so much for our own code, but so that
528
+ * we can transform an alias like `"-u"` to `"--updateSnapshot"` in order to
529
+ * pass it along to Jest.
530
+ *
531
+ * @param rawAlias a CLI flag alias as found on the command line (like `"-c"`)
532
+ * @returns an equivalent full command (like `"--config"`)
533
+ */
534
+ const desugarRawAlias = (rawAlias) => "--" + desugarAlias(normalizeFlagName(rawAlias));
535
+ //#endregion
536
+ //#region src/find-config.ts
537
+ /**
538
+ * Attempt to find a Stencil configuration file on the file system
539
+ * @param opts the options needed to find the configuration file
540
+ * @returns the results of attempting to find a configuration file on disk
541
+ */
542
+ const findConfig = async (opts) => {
543
+ const sys = opts.sys;
544
+ const cwd = sys.getCurrentDirectory();
545
+ const rootDir = normalizePath(cwd);
546
+ let configPath = opts.configPath;
547
+ if (isString(configPath)) if (!sys.platformPath.isAbsolute(configPath)) configPath = normalizePath(sys.platformPath.join(cwd, configPath));
548
+ else configPath = normalizePath(configPath);
549
+ else configPath = rootDir;
550
+ const results = {
551
+ configPath,
552
+ rootDir: normalizePath(cwd)
553
+ };
554
+ const stat = await sys.stat(configPath);
555
+ if (stat.error) {
556
+ const diagnostics = [];
557
+ const diagnostic = buildError(diagnostics);
558
+ diagnostic.absFilePath = configPath;
559
+ diagnostic.header = `Invalid config path`;
560
+ diagnostic.messageText = `Config path "${configPath}" not found`;
561
+ return result.err(diagnostics);
562
+ }
563
+ if (stat.isFile) {
564
+ results.configPath = configPath;
565
+ results.rootDir = sys.platformPath.dirname(configPath);
566
+ } else if (stat.isDirectory) for (const configName of ["stencil.config.ts", "stencil.config.js"]) {
567
+ const testConfigFilePath = sys.platformPath.join(configPath, configName);
568
+ if ((await sys.stat(testConfigFilePath)).isFile) {
569
+ results.configPath = testConfigFilePath;
570
+ results.rootDir = sys.platformPath.dirname(testConfigFilePath);
571
+ break;
572
+ }
573
+ }
574
+ return result.ok(results);
575
+ };
576
+ //#endregion
577
+ //#region src/load-compiler.ts
578
+ const loadCoreCompiler = async (sys) => {
579
+ return await sys.dynamicImport(sys.getCompilerExecutingPath());
580
+ };
581
+ //#endregion
582
+ //#region src/logs.ts
583
+ /**
584
+ * Log the name of this package (`@stencil/core`) to an output stream
585
+ *
586
+ * The output stream is determined by the {@link Logger} instance that is provided as an argument to this function
587
+ *
588
+ * The name of the package may not be logged, by design, for certain `task` types and logging levels
589
+ *
590
+ * @param logger the logging entity to use to output the name of the package
591
+ * @param task the current task
592
+ */
593
+ const startupLog = (logger, task) => {
594
+ if (task === "info" || task === "serve" || task === "version") return;
595
+ logger.info(logger.cyan(`@stencil/core`));
596
+ };
597
+ /**
598
+ * Log this package's version to an output stream
599
+ *
600
+ * The output stream is determined by the {@link Logger} instance that is provided as an argument to this function
601
+ *
602
+ * The package version may not be logged, by design, for certain `task` types and logging levels
603
+ *
604
+ * @param logger the logging entity to use for output
605
+ * @param task the current task
606
+ * @param coreCompiler the compiler instance to derive version information from
607
+ */
608
+ const startupLogVersion = (logger, task, coreCompiler) => {
609
+ if (task === "info" || task === "serve" || task === "version") return;
610
+ const isDevBuild = coreCompiler.version.includes("-dev.");
611
+ let startupMsg;
612
+ if (isDevBuild) startupMsg = logger.yellow(`[LOCAL DEV] v${coreCompiler.version}`);
613
+ else startupMsg = logger.cyan(`v${coreCompiler.version}`);
614
+ startupMsg += logger.emoji(" " + coreCompiler.vermoji);
615
+ logger.info(startupMsg);
616
+ };
617
+ /**
618
+ * Log details from a {@link CompilerSystem} used by Stencil to an output stream
619
+ *
620
+ * The output stream is determined by the {@link Logger} instance that is provided as an argument to this function
621
+ *
622
+ * @param sys the `CompilerSystem` to report details on
623
+ * @param logger the logging entity to use for output
624
+ * @param flags user set flags for the current invocation of Stencil
625
+ * @param coreCompiler the compiler instance being used for this invocation of Stencil
626
+ */
627
+ const loadedCompilerLog = (sys, logger, flags, coreCompiler) => {
628
+ const sysDetails = sys.details;
629
+ const runtimeInfo = `${sys.name} ${sys.version}`;
630
+ const platformInfo = sysDetails ? `${sysDetails.platform}, ${sysDetails.cpuModel}` : `Unknown Platform, Unknown CPU Model`;
631
+ const statsInfo = sysDetails ? `cpus: ${sys.hardwareConcurrency}, freemem: ${Math.round(sysDetails.freemem() / 1e6)}MB, totalmem: ${Math.round(sysDetails.totalmem / 1e6)}MB` : "Unknown CPU Core Count, Unknown Memory";
632
+ if (logger.getLevel() === "debug") {
633
+ logger.debug(runtimeInfo);
634
+ logger.debug(platformInfo);
635
+ logger.debug(statsInfo);
636
+ logger.debug(`compiler: ${sys.getCompilerExecutingPath()}`);
637
+ logger.debug(`build: ${coreCompiler.buildId}`);
638
+ } else if (flags.ci) {
639
+ logger.info(runtimeInfo);
640
+ logger.info(platformInfo);
641
+ logger.info(statsInfo);
642
+ }
643
+ };
644
+ /**
645
+ * Log various warnings to an output stream
646
+ *
647
+ * The output stream is determined by the {@link Logger} instance attached to the `config` argument to this function
648
+ *
649
+ * @param coreCompiler the compiler instance being used for this invocation of Stencil
650
+ * @param config a validated configuration object to be used for this run of Stencil
651
+ */
652
+ const startupCompilerLog = (coreCompiler, config) => {
653
+ if (config.suppressLogs === true) return;
654
+ const { logger } = config;
655
+ const isDebug = logger.getLevel() === "debug";
656
+ const isPrerelease = coreCompiler.version.includes("-");
657
+ const isDevBuild = coreCompiler.version.includes("-dev.");
658
+ 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
+ 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
+ if (!config.enableCache) logger.warn(`Disabling cache during development will slow down incremental builds.`);
662
+ }
663
+ };
664
+ //#endregion
665
+ //#region src/merge-flags.ts
666
+ /**
667
+ * Merge CLI flags into a Stencil configuration object.
668
+ *
669
+ * This function applies command-line flags to the config, with CLI flags
670
+ * taking precedence over config file values. This is the canonical place
671
+ * where flag values are translated into config properties.?
672
+ *
673
+ * @param config The config object (from stencil.config.ts or empty)
674
+ * @param flags The parsed CLI flags
675
+ * @returns The config with flags merged in
676
+ */
677
+ const mergeFlags = (config, flags) => {
678
+ const merged = { ...config };
679
+ if (flags.prod === true) merged.devMode = false;
680
+ else if (flags.dev === true) merged.devMode = true;
681
+ if (flags.debug === true || flags.verbose === true) merged.logLevel = "debug";
682
+ else if (flags.logLevel) merged.logLevel = flags.logLevel;
683
+ if (typeof flags.watch === "boolean") merged.watch = flags.watch;
684
+ if (typeof flags.docs === "boolean") merged.buildDocs = flags.docs;
685
+ if (typeof flags.esm === "boolean") merged.buildDist = flags.esm;
686
+ if (typeof flags.profile === "boolean") merged.profile = flags.profile;
687
+ if (typeof flags.log === "boolean") merged.writeLog = flags.log;
688
+ if (typeof flags.cache === "boolean") merged.enableCache = flags.cache;
689
+ if (typeof flags.ci === "boolean") merged.ci = flags.ci;
690
+ if (typeof flags.ssr === "boolean") merged.ssr = flags.ssr;
691
+ if (typeof flags.prerender === "boolean") merged.prerender = flags.prerender;
692
+ if (typeof flags.docsJson === "string") merged.docsJsonPath = flags.docsJson;
693
+ if (flags.stats) merged.statsJsonPath = flags.stats;
694
+ if (typeof flags.serviceWorker === "boolean") merged.generateServiceWorker = flags.serviceWorker;
695
+ if (typeof flags.e2e === "boolean") merged.e2eTests = flags.e2e;
696
+ if (typeof flags.maxWorkers === "number") merged.maxConcurrentWorkers = flags.maxWorkers;
697
+ if (typeof flags.address === "string") merged.devServerAddress = flags.address;
698
+ if (typeof flags.port === "number") merged.devServerPort = flags.port;
699
+ if (typeof flags.open === "boolean") merged.devServerOpen = flags.open;
700
+ return merged;
701
+ };
702
+ //#endregion
703
+ //#region src/check-version.ts
704
+ /**
705
+ * Retrieve a reference to the active `CompilerSystem`'s `checkVersion` function
706
+ * @param config the Stencil configuration associated with the currently compiled project
707
+ * @param currentVersion the Stencil compiler's version string
708
+ * @param flags the CLI flags (owned by CLI, not part of core config)
709
+ * @returns a reference to `checkVersion`, or `null` if one does not exist on the current `CompilerSystem`
710
+ */
711
+ const startCheckVersion = async (config, currentVersion, flags) => {
712
+ if (config.devMode && !flags.ci && !currentVersion.includes("-dev.") && isFunction(config.sys.checkVersion)) return config.sys.checkVersion(config.logger, currentVersion);
713
+ return null;
714
+ };
715
+ /**
716
+ * Print the results of running the provided `versionChecker`.
717
+ *
718
+ * Does not print if no `versionChecker` is provided.
719
+ *
720
+ * @param versionChecker the function to invoke.
721
+ */
722
+ const printCheckVersionResults = async (versionChecker) => {
723
+ if (versionChecker) {
724
+ const checkVersionResults = await versionChecker;
725
+ if (isFunction(checkVersionResults)) checkVersionResults();
726
+ }
727
+ };
728
+ //#endregion
729
+ //#region src/task-prerender.ts
730
+ const taskPrerender = async (coreCompiler, config, flags) => {
731
+ startupCompilerLog(coreCompiler, config);
732
+ const hydrateAppFilePath = flags.unknownArgs[0];
733
+ if (typeof hydrateAppFilePath !== "string") {
734
+ config.logger.error(`Missing hydrate app script path`);
735
+ return config.sys.exit(1);
736
+ }
737
+ const srcIndexHtmlPath = config.srcIndexHtml;
738
+ const diagnostics = await runPrerenderTask(coreCompiler, config, hydrateAppFilePath, void 0, srcIndexHtmlPath);
739
+ config.logger.printDiagnostics(diagnostics);
740
+ if (diagnostics.some((d) => d.level === "error")) return config.sys.exit(1);
741
+ };
742
+ const runPrerenderTask = async (coreCompiler, config, hydrateAppFilePath, componentGraph, srcIndexHtmlPath) => {
743
+ const diagnostics = [];
744
+ try {
745
+ const results = await (await coreCompiler.createPrerenderer(config)).start({
746
+ hydrateAppFilePath,
747
+ componentGraph,
748
+ srcIndexHtmlPath
749
+ });
750
+ diagnostics.push(...results.diagnostics);
751
+ } catch (e) {
752
+ catchError(diagnostics, e);
753
+ }
754
+ return diagnostics;
755
+ };
756
+ //#endregion
757
+ //#region src/task-watch.ts
758
+ const taskWatch = async (coreCompiler, config, flags) => {
759
+ let devServer = null;
760
+ let exitCode = 0;
761
+ try {
762
+ startupCompilerLog(coreCompiler, config);
763
+ const versionChecker = startCheckVersion(config, coreCompiler.version, flags);
764
+ const compiler = await coreCompiler.createCompiler(config);
765
+ const watcher = await compiler.createWatcher();
766
+ if (!config.sys.onProcessInterrupt) throw new Error(`Environment doesn't provide required function: onProcessInterrupt`);
767
+ if (flags.serve) {
768
+ const { start } = await import("@stencil/dev-server");
769
+ devServer = await start(config.devServer, config.logger, watcher);
770
+ }
771
+ config.sys.onProcessInterrupt(() => {
772
+ config.logger.debug(`close watch`);
773
+ if (compiler) compiler.destroy();
774
+ });
775
+ const rmVersionCheckerLog = watcher.on("buildFinish", async () => {
776
+ rmVersionCheckerLog();
777
+ printCheckVersionResults(versionChecker);
778
+ });
779
+ if (devServer) {
780
+ const rmDevServerLog = watcher.on("buildFinish", () => {
781
+ rmDevServerLog();
782
+ const url = devServer?.browserUrl ?? "UNKNOWN URL";
783
+ config.logger.info(`${config.logger.cyan(url)}\n`);
784
+ });
785
+ }
786
+ const closeResults = await watcher.start();
787
+ if (closeResults.exitCode > 0) exitCode = closeResults.exitCode;
788
+ } catch (e) {
789
+ exitCode = 1;
790
+ config.logger.error(e);
791
+ }
792
+ if (devServer) await devServer.close();
793
+ if (exitCode > 0) return config.sys.exit(exitCode);
794
+ };
795
+ //#endregion
796
+ //#region src/telemetry/helpers.ts
797
+ const tryFn = async (fn, ...args) => {
798
+ try {
799
+ return await fn(...args);
800
+ } catch {}
801
+ return null;
802
+ };
803
+ const isInteractive = (sys, flags, object) => {
804
+ const terminalInfo = object || Object.freeze({
805
+ tty: sys.isTTY(),
806
+ ci: [
807
+ "CI",
808
+ "BUILD_ID",
809
+ "BUILD_NUMBER",
810
+ "BITBUCKET_COMMIT",
811
+ "CODEBUILD_BUILD_ARN"
812
+ ].filter((v) => !!sys.getEnvironmentVar?.(v)).length > 0 || !!flags.ci
813
+ });
814
+ return terminalInfo.tty && !terminalInfo.ci;
815
+ };
816
+ const UUID_REGEX = /* @__PURE__ */ new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i);
817
+ function uuidv4() {
818
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
819
+ const r = Math.random() * 16 | 0;
820
+ return (c == "x" ? r : r & 3 | 8).toString(16);
821
+ });
822
+ }
823
+ /**
824
+ * Reads and parses a JSON file from the given `path`
825
+ * @param sys The system where the command is invoked
826
+ * @param path the path on the file system to read and parse
827
+ * @returns the parsed JSON
828
+ */
829
+ async function readJson(sys, path) {
830
+ const file = await sys.readFile(path);
831
+ return file ? JSON.parse(file) : null;
832
+ }
833
+ /**
834
+ * Does the command have the debug flag?
835
+ * @param flags The configuration flags passed into the Stencil command
836
+ * @returns true if --debug has been passed, otherwise false
837
+ */
838
+ function hasDebug(flags) {
839
+ return !!flags.debug;
840
+ }
841
+ /**
842
+ * Does the command have the verbose and debug flags?
843
+ * @param flags The configuration flags passed into the Stencil command
844
+ * @returns true if both --debug and --verbose have been passed, otherwise false
845
+ */
846
+ function hasVerbose(flags) {
847
+ return !!flags.verbose && hasDebug(flags);
848
+ }
849
+ //#endregion
850
+ //#region src/ionic-config.ts
851
+ const isTest$1 = () => process.env.JEST_WORKER_ID !== void 0;
852
+ const defaultConfig = (sys) => sys.resolvePath(`${sys.homeDir()}/.ionic/${isTest$1() ? "tmp-config.json" : "config.json"}`);
853
+ const defaultConfigDirectory = (sys) => sys.resolvePath(`${sys.homeDir()}/.ionic`);
854
+ /**
855
+ * Reads an Ionic configuration file from disk, parses it, and performs any necessary corrections to it if certain
856
+ * values are deemed to be malformed
857
+ * @param sys The system where the command is invoked
858
+ * @returns the config read from disk that has been potentially been updated
859
+ */
860
+ async function readConfig(sys) {
861
+ let config = await readJson(sys, defaultConfig(sys));
862
+ if (!config) {
863
+ config = {
864
+ "tokens.telemetry": uuidv4(),
865
+ "telemetry.stencil": true
866
+ };
867
+ await writeConfig(sys, config);
868
+ } else if (!config["tokens.telemetry"] || !UUID_REGEX.test(config["tokens.telemetry"])) {
869
+ const newUuid = uuidv4();
870
+ await writeConfig(sys, {
871
+ ...config,
872
+ "tokens.telemetry": newUuid
873
+ });
874
+ config["tokens.telemetry"] = newUuid;
875
+ }
876
+ return config;
877
+ }
878
+ /**
879
+ * Writes an Ionic configuration file to disk.
880
+ * @param sys The system where the command is invoked
881
+ * @param config The config passed into the Stencil command
882
+ * @returns boolean If the command was successful
883
+ */
884
+ async function writeConfig(sys, config) {
885
+ let result = false;
886
+ try {
887
+ await sys.createDir(defaultConfigDirectory(sys), { recursive: true });
888
+ await sys.writeFile(defaultConfig(sys), JSON.stringify(config, null, 2));
889
+ result = true;
890
+ } catch (error) {
891
+ console.error(`Stencil Telemetry: couldn't write configuration file to ${defaultConfig(sys)} - ${error}.`);
892
+ }
893
+ return result;
894
+ }
895
+ /**
896
+ * Update a subset of the Ionic config.
897
+ * @param sys The system where the command is invoked
898
+ * @param newOptions The new options to save
899
+ * @returns boolean If the command was successful
900
+ */
901
+ async function updateConfig(sys, newOptions) {
902
+ const config = await readConfig(sys);
903
+ return await writeConfig(sys, Object.assign(config, newOptions));
904
+ }
905
+ //#endregion
906
+ //#region src/telemetry/shouldTrack.ts
907
+ /**
908
+ * Used to determine if tracking should occur.
909
+ * @param sys The system where the command is invoked
910
+ * @param flags The CLI flags (owned by CLI, not part of core config)
911
+ * @param ci whether or not the process is running in a Continuous Integration (CI) environment
912
+ * @returns true if telemetry should be sent, false otherwise
913
+ */
914
+ async function shouldTrack(sys, flags, ci) {
915
+ return !ci && isInteractive(sys, flags) && await checkTelemetry(sys);
916
+ }
917
+ //#endregion
918
+ //#region src/telemetry/telemetry.ts
919
+ /**
920
+ * Used to within taskBuild to provide the component_count property.
921
+ *
922
+ * @param sys The system where the command is invoked
923
+ * @param config The config passed into the Stencil command
924
+ * @param coreCompiler The compiler used to do builds
925
+ * @param result The results of a compiler build.
926
+ * @param flags The CLI flags (owned by CLI, not part of core config)
927
+ */
928
+ async function telemetryBuildFinishedAction(sys, config, coreCompiler, result, flags) {
929
+ if (!await shouldTrack(sys, flags, !!flags.ci)) return;
930
+ const component_count = result.componentGraph ? Object.keys(result.componentGraph).length : void 0;
931
+ const data = await prepareData(coreCompiler, config, sys, flags, result.duration, component_count);
932
+ await sendMetric(sys, flags, "stencil_cli_command", data);
933
+ config.logger.debug(`${config.logger.blue("Telemetry")}: ${config.logger.gray(JSON.stringify(data))}`);
934
+ }
935
+ /**
936
+ * A function to wrap a compiler task function around. Will send telemetry if, and only if, the machine allows.
937
+ *
938
+ * @param sys The system where the command is invoked
939
+ * @param config The config passed into the Stencil command
940
+ * @param coreCompiler The compiler used to do builds
941
+ * @param flags The CLI flags (owned by CLI, not part of core config)
942
+ * @param action A Promise-based function to call in order to get the duration of any given command.
943
+ * @returns void
944
+ */
945
+ async function telemetryAction(sys, config, coreCompiler, flags, action) {
946
+ const tracking = await shouldTrack(sys, flags, !!flags.ci);
947
+ let duration = void 0;
948
+ let error;
949
+ if (action) {
950
+ const start = /* @__PURE__ */ new Date();
951
+ try {
952
+ await action();
953
+ } catch (e) {
954
+ error = e;
955
+ }
956
+ duration = (/* @__PURE__ */ new Date()).getTime() - start.getTime();
957
+ }
958
+ if (!tracking || flags.task == "build" && !flags.args.includes("--watch")) return;
959
+ const data = await prepareData(coreCompiler, config, sys, flags, duration);
960
+ await sendMetric(sys, flags, "stencil_cli_command", data);
961
+ config.logger.debug(`${config.logger.blue("Telemetry")}: ${config.logger.gray(JSON.stringify(data))}`);
962
+ if (error) throw error;
963
+ }
964
+ /**
965
+ * Helper function to determine if a Stencil configuration builds an application.
966
+ *
967
+ * This function is a rough approximation whether an application is generated as a part of a Stencil build, based on
968
+ * contents of the project's `stencil.config.ts` file.
969
+ *
970
+ * @param config the configuration used by the Stencil project
971
+ * @returns true if we believe the project generates an application, false otherwise
972
+ */
973
+ function hasAppTarget(config) {
974
+ return config.outputTargets.some((target) => isOutputTargetWww(target) && (!!target.serviceWorker || !!target.baseUrl && target.baseUrl !== "/"));
975
+ }
976
+ function isUsingYarn(sys) {
977
+ return sys.getEnvironmentVar?.("npm_execpath")?.includes("yarn") || false;
978
+ }
979
+ /**
980
+ * Build a list of the different types of output targets used in a Stencil configuration.
981
+ *
982
+ * Duplicate entries will not be returned from the list
983
+ *
984
+ * @param config the configuration used by the Stencil project
985
+ * @returns a unique list of output target types found in the Stencil configuration
986
+ */
987
+ function getActiveTargets(config) {
988
+ const result = config.outputTargets.map((t) => t.type);
989
+ return Array.from(new Set(result));
990
+ }
991
+ /**
992
+ * Prepare data for telemetry
993
+ *
994
+ * @param coreCompiler the core compiler
995
+ * @param config the current Stencil config
996
+ * @param sys the compiler system instance in use
997
+ * @param flags the CLI flags (owned by CLI, not part of core config)
998
+ * @param duration_ms the duration of the action being tracked
999
+ * @param component_count the number of components being built (optional)
1000
+ * @returns a Promise wrapping data for the telemetry endpoint
1001
+ */
1002
+ const prepareData = async (coreCompiler, config, sys, flags, duration_ms, component_count = void 0) => {
1003
+ const { typescript, rollup } = coreCompiler.versions || {
1004
+ typescript: "unknown",
1005
+ rollup: "unknown"
1006
+ };
1007
+ const { packages, packagesNoVersions } = await getInstalledPackages(sys, flags);
1008
+ const targets = getActiveTargets(config);
1009
+ const yarn = isUsingYarn(sys);
1010
+ const stencil = coreCompiler.version || "unknown";
1011
+ const system = `${sys.name} ${sys.version}`;
1012
+ const os_name = sys.details?.platform;
1013
+ const os_version = sys.details?.release;
1014
+ const cpu_model = sys.details?.cpuModel;
1015
+ const build = coreCompiler.buildId || "unknown";
1016
+ const has_app_pwa_config = hasAppTarget(config);
1017
+ const anonymizedConfig = anonymizeConfigForTelemetry(config);
1018
+ return {
1019
+ arguments: flags.args,
1020
+ build,
1021
+ component_count,
1022
+ config: anonymizedConfig,
1023
+ cpu_model,
1024
+ duration_ms,
1025
+ has_app_pwa_config,
1026
+ os_name,
1027
+ os_version,
1028
+ packages,
1029
+ packages_no_versions: packagesNoVersions,
1030
+ rollup,
1031
+ stencil,
1032
+ system,
1033
+ system_major: getMajorVersion(system),
1034
+ targets,
1035
+ task: flags.task,
1036
+ typescript,
1037
+ yarn
1038
+ };
1039
+ };
1040
+ const OUTPUT_TARGET_KEYS_TO_KEEP = ["type"];
1041
+ const CONFIG_PROPS_TO_ANONYMIZE = [
1042
+ "rootDir",
1043
+ "fsNamespace",
1044
+ "packageJsonFilePath",
1045
+ "namespace",
1046
+ "srcDir",
1047
+ "srcIndexHtml",
1048
+ "buildLogFilePath",
1049
+ "cacheDir",
1050
+ "configPath",
1051
+ "tsconfig"
1052
+ ];
1053
+ const CONFIG_PROPS_TO_DELETE = [
1054
+ "commonjs",
1055
+ "devServer",
1056
+ "env",
1057
+ "logger",
1058
+ "rollupConfig",
1059
+ "sys",
1060
+ "testing",
1061
+ "tsCompilerOptions"
1062
+ ];
1063
+ /**
1064
+ * Anonymize the config for telemetry, replacing potentially revealing config props
1065
+ * with a placeholder string if they are present (this lets us still track how frequently
1066
+ * these config options are being used)
1067
+ *
1068
+ * @param config the config to anonymize
1069
+ * @returns an anonymized copy of the same config
1070
+ */
1071
+ const anonymizeConfigForTelemetry = (config) => {
1072
+ const anonymizedConfig = { ...config };
1073
+ for (const prop of CONFIG_PROPS_TO_ANONYMIZE) if (anonymizedConfig[prop] !== void 0) anonymizedConfig[prop] = "omitted";
1074
+ anonymizedConfig.outputTargets = config.outputTargets.map((target) => {
1075
+ const anonymizedOT = JSON.parse(JSON.stringify(target, (key, value) => {
1076
+ if (!(typeof value === "string")) return value;
1077
+ if (OUTPUT_TARGET_KEYS_TO_KEEP.includes(key)) return value;
1078
+ return "omitted";
1079
+ }));
1080
+ if (isOutputTargetHydrate(target) && target.external) anonymizedOT["external"] = target.external.concat();
1081
+ return anonymizedOT;
1082
+ });
1083
+ for (const prop of CONFIG_PROPS_TO_DELETE) delete anonymizedConfig[prop];
1084
+ return anonymizedConfig;
1085
+ };
1086
+ /**
1087
+ * Reads package-lock.json, yarn.lock, and package.json files in order to cross-reference
1088
+ * the dependencies and devDependencies properties. Pulls up the current installed version
1089
+ * of each package under the @stencil, @ionic, and @capacitor scopes.
1090
+ *
1091
+ * @param sys the system instance where telemetry is invoked
1092
+ * @param flags the CLI flags (owned by CLI, not part of core config)
1093
+ * @returns an object listing all dev and production dependencies under the aforementioned scopes
1094
+ */
1095
+ async function getInstalledPackages(sys, flags) {
1096
+ let packages = [];
1097
+ let packagesNoVersions = [];
1098
+ const yarn = isUsingYarn(sys);
1099
+ try {
1100
+ const appRootDir = sys.getCurrentDirectory();
1101
+ const packageJson = await tryFn(readJson, sys, sys.resolvePath(appRootDir + "/package.json"));
1102
+ if (!packageJson) return {
1103
+ packages,
1104
+ packagesNoVersions
1105
+ };
1106
+ const ionicPackages = Object.entries({
1107
+ ...packageJson.devDependencies,
1108
+ ...packageJson.dependencies
1109
+ }).filter(([k]) => k.startsWith("@stencil/") || k.startsWith("@ionic/") || k.startsWith("@capacitor/"));
1110
+ try {
1111
+ packages = yarn ? await yarnPackages(sys, ionicPackages) : await npmPackages(sys, ionicPackages);
1112
+ } catch {
1113
+ packages = ionicPackages.map(([k, v]) => `${k}@${v.replace("^", "")}`);
1114
+ }
1115
+ packagesNoVersions = ionicPackages.map(([k]) => `${k}`);
1116
+ return {
1117
+ packages,
1118
+ packagesNoVersions
1119
+ };
1120
+ } catch (err) {
1121
+ if (hasDebug(flags)) console.error(err);
1122
+ return {
1123
+ packages,
1124
+ packagesNoVersions
1125
+ };
1126
+ }
1127
+ }
1128
+ /**
1129
+ * Visits the npm lock file to find the exact versions that are installed
1130
+ * @param sys The system where the command is invoked
1131
+ * @param ionicPackages a list of the found packages matching `@stencil`, `@capacitor`, or `@ionic` from the package.json file.
1132
+ * @returns an array of strings of all the packages and their versions.
1133
+ */
1134
+ async function npmPackages(sys, ionicPackages) {
1135
+ const appRootDir = sys.getCurrentDirectory();
1136
+ const packageLockJson = await tryFn(readJson, sys, sys.resolvePath(appRootDir + "/package-lock.json"));
1137
+ return ionicPackages.map(([k, v]) => {
1138
+ let version = packageLockJson?.dependencies[k]?.version ?? packageLockJson?.devDependencies[k]?.version ?? v;
1139
+ version = version.includes("file:") ? sanitizeDeclaredVersion(v) : version;
1140
+ return `${k}@${version}`;
1141
+ });
1142
+ }
1143
+ /**
1144
+ * Visits the yarn lock file to find the exact versions that are installed
1145
+ * @param sys The system where the command is invoked
1146
+ * @param ionicPackages a list of the found packages matching `@stencil`, `@capacitor`, or `@ionic` from the package.json file.
1147
+ * @returns an array of strings of all the packages and their versions.
1148
+ */
1149
+ async function yarnPackages(sys, ionicPackages) {
1150
+ const appRootDir = sys.getCurrentDirectory();
1151
+ const yarnLock = sys.readFileSync(sys.resolvePath(appRootDir + "/yarn.lock"));
1152
+ const yarnLockYml = sys.parseYarnLockFile?.(yarnLock);
1153
+ return ionicPackages.map(([k, v]) => {
1154
+ const identifiedVersion = `${k}@${v}`;
1155
+ let version = yarnLockYml?.object[identifiedVersion]?.version;
1156
+ version = version && version.includes("undefined") ? sanitizeDeclaredVersion(identifiedVersion) : version;
1157
+ return `${k}@${version}`;
1158
+ });
1159
+ }
1160
+ /**
1161
+ * This function is used for fallback purposes, where an npm or yarn lock file doesn't exist in the consumers directory.
1162
+ * This will strip away '*', '^' and '~' from the declared package versions in a package.json.
1163
+ * @param version the raw semver pattern identifier version string
1164
+ * @returns a cleaned up representation without any qualifiers
1165
+ */
1166
+ function sanitizeDeclaredVersion(version) {
1167
+ return version.replace(/[*^~]/g, "");
1168
+ }
1169
+ /**
1170
+ * If telemetry is enabled, send a metric to an external data store
1171
+ *
1172
+ * @param sys the system instance where telemetry is invoked
1173
+ * @param flags the CLI flags (owned by CLI, not part of core config)
1174
+ * @param name the name of a trackable metric. Note this name is not necessarily a scalar value to track, like
1175
+ * "Stencil Version". For example, "stencil_cli_command" is a name that is used to track all CLI command information.
1176
+ * @param value the data to send to the external data store under the provided name argument
1177
+ */
1178
+ async function sendMetric(sys, flags, name, value) {
1179
+ const session_id = await getTelemetryToken(sys);
1180
+ await sendTelemetry(sys, flags, {
1181
+ name,
1182
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1183
+ source: "stencil_cli",
1184
+ value,
1185
+ session_id
1186
+ });
1187
+ }
1188
+ /**
1189
+ * Used to read the config file's tokens.telemetry property.
1190
+ *
1191
+ * @param sys The system where the command is invoked
1192
+ * @returns string
1193
+ */
1194
+ async function getTelemetryToken(sys) {
1195
+ const config = await readConfig(sys);
1196
+ if (config["tokens.telemetry"] === void 0) {
1197
+ config["tokens.telemetry"] = uuidv4();
1198
+ await writeConfig(sys, config);
1199
+ }
1200
+ return config["tokens.telemetry"];
1201
+ }
1202
+ /**
1203
+ * Issues a request to the telemetry server.
1204
+ * @param sys The system where the command is invoked
1205
+ * @param flags The CLI flags (owned by CLI, not part of core config)
1206
+ * @param data Data to be tracked
1207
+ */
1208
+ async function sendTelemetry(sys, flags, data) {
1209
+ try {
1210
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1211
+ const body = {
1212
+ metrics: [data],
1213
+ sent_at: now
1214
+ };
1215
+ if (!sys.fetch) throw new Error("No fetch implementation available");
1216
+ const response = await sys.fetch("https://api.ionicjs.com/events/metrics", {
1217
+ method: "POST",
1218
+ headers: { "Content-Type": "application/json" },
1219
+ body: JSON.stringify(body)
1220
+ });
1221
+ if (hasVerbose(flags)) console.debug("\nSent %O metric to events service (status: %O)", data.name, response.status, "\n");
1222
+ if (response.status !== 204 && hasVerbose(flags)) console.debug("\nBad response from events service. Request body: %O", response.body.toString(), "\n");
1223
+ } catch (e) {
1224
+ if (hasVerbose(flags)) console.debug("Telemetry request failed:", e);
1225
+ }
1226
+ }
1227
+ /**
1228
+ * Checks if telemetry is enabled on this machine
1229
+ * @param sys The system where the command is invoked
1230
+ * @returns true if telemetry is enabled, false otherwise
1231
+ */
1232
+ async function checkTelemetry(sys) {
1233
+ const config = await readConfig(sys);
1234
+ if (config["telemetry.stencil"] === void 0) {
1235
+ config["telemetry.stencil"] = true;
1236
+ await writeConfig(sys, config);
1237
+ }
1238
+ return config["telemetry.stencil"];
1239
+ }
1240
+ /**
1241
+ * Writes to the config file, enabling telemetry for this machine.
1242
+ * @param sys The system where the command is invoked
1243
+ * @returns true if writing the file was successful, false otherwise
1244
+ */
1245
+ async function enableTelemetry(sys) {
1246
+ return await updateConfig(sys, { "telemetry.stencil": true });
1247
+ }
1248
+ /**
1249
+ * Writes to the config file, disabling telemetry for this machine.
1250
+ * @param sys The system where the command is invoked
1251
+ * @returns true if writing the file was successful, false otherwise
1252
+ */
1253
+ async function disableTelemetry(sys) {
1254
+ return await updateConfig(sys, { "telemetry.stencil": false });
1255
+ }
1256
+ /**
1257
+ * Takes in a semver string in order to return the major version.
1258
+ * @param version The fully qualified semver version
1259
+ * @returns a string of the major version
1260
+ */
1261
+ function getMajorVersion(version) {
1262
+ return version.split(".")[0];
1263
+ }
1264
+ //#endregion
1265
+ //#region src/task-build.ts
1266
+ const taskBuild = async (coreCompiler, config, flags) => {
1267
+ if (flags.watch) {
1268
+ await taskWatch(coreCompiler, config, flags);
1269
+ return;
1270
+ }
1271
+ let exitCode = 0;
1272
+ try {
1273
+ startupCompilerLog(coreCompiler, config);
1274
+ const versionChecker = startCheckVersion(config, coreCompiler.version, flags);
1275
+ const compiler = await coreCompiler.createCompiler(config);
1276
+ const results = await compiler.build();
1277
+ await telemetryBuildFinishedAction(config.sys, config, coreCompiler, results, flags);
1278
+ await compiler.destroy();
1279
+ if (results.hasError) exitCode = 1;
1280
+ else if (flags.prerender) {
1281
+ const prerenderDiagnostics = await runPrerenderTask(coreCompiler, config, results.hydrateAppFilePath, results.componentGraph, void 0);
1282
+ config.logger.printDiagnostics(prerenderDiagnostics);
1283
+ if (prerenderDiagnostics.some((d) => d.level === "error")) exitCode = 1;
1284
+ }
1285
+ await printCheckVersionResults(versionChecker);
1286
+ } catch (e) {
1287
+ exitCode = 1;
1288
+ config.logger.error(e);
1289
+ }
1290
+ if (exitCode > 0) return config.sys.exit(exitCode);
1291
+ };
1292
+ //#endregion
1293
+ //#region src/task-docs.ts
1294
+ const taskDocs = async (coreCompiler, config) => {
1295
+ config.devServer = {};
1296
+ config.outputTargets = config.outputTargets.filter(isOutputTargetDocs);
1297
+ config.devMode = true;
1298
+ startupCompilerLog(coreCompiler, config);
1299
+ const compiler = await coreCompiler.createCompiler(config);
1300
+ await compiler.build();
1301
+ await compiler.destroy();
1302
+ };
1303
+ //#endregion
1304
+ //#region src/task-generate.ts
1305
+ /**
1306
+ * Task to generate component boilerplate and write it to disk. This task can
1307
+ * cause the program to exit with an error under various circumstances, such as
1308
+ * being called in an inappropriate place, being asked to overwrite files that
1309
+ * already exist, etc.
1310
+ *
1311
+ * @param config the user-supplied config, which we need here to access `.sys`.
1312
+ * @param flags the CLI flags (owned by CLI, not part of core config)
1313
+ * @returns a void promise
1314
+ */
1315
+ const taskGenerate = async (config, flags) => {
1316
+ if (!config.configPath) {
1317
+ config.logger.error("Please run this command in your root directory (i. e. the one containing stencil.config.ts).");
1318
+ return config.sys.exit(1);
1319
+ }
1320
+ const absoluteSrcDir = config.srcDir;
1321
+ if (!absoluteSrcDir) {
1322
+ config.logger.error(`Stencil's srcDir was not specified.`);
1323
+ return config.sys.exit(1);
1324
+ }
1325
+ const { prompt } = await import("prompts");
1326
+ const input = flags.unknownArgs.find((arg) => !arg.startsWith("-")) || (await prompt({
1327
+ name: "tagName",
1328
+ type: "text",
1329
+ message: "Component tag name (dash-case):"
1330
+ })).tagName;
1331
+ if (void 0 === input) return;
1332
+ const { dir, base: componentName } = parse(input);
1333
+ const tagError = validateComponentTag(componentName);
1334
+ if (tagError) {
1335
+ config.logger.error(tagError);
1336
+ return config.sys.exit(1);
1337
+ }
1338
+ let cssExtension = "css";
1339
+ if (config.plugins?.find((plugin) => plugin.name === "sass")) cssExtension = await chooseSassExtension();
1340
+ else if (config.plugins?.find((plugin) => plugin.name === "less")) cssExtension = "less";
1341
+ const filesToGenerateExt = await chooseFilesToGenerate(cssExtension);
1342
+ if (!filesToGenerateExt) return;
1343
+ const extensionsToGenerate = ["tsx", ...filesToGenerateExt];
1344
+ const testFolder = extensionsToGenerate.some(isTest) ? "test" : "";
1345
+ const outDir = join(absoluteSrcDir, "components", dir, componentName);
1346
+ await config.sys.createDir(normalizePath(join(outDir, testFolder)), { recursive: true });
1347
+ const filesToGenerate = extensionsToGenerate.map((extension) => ({
1348
+ extension,
1349
+ path: getFilepathForFile(outDir, componentName, extension)
1350
+ }));
1351
+ await checkForOverwrite(filesToGenerate, config);
1352
+ const writtenFiles = await Promise.all(filesToGenerate.map((file) => getBoilerplateAndWriteFile(config, componentName, extensionsToGenerate.includes("css") || extensionsToGenerate.includes("sass") || extensionsToGenerate.includes("scss") || extensionsToGenerate.includes("less"), file, cssExtension))).catch((error) => config.logger.error(error));
1353
+ if (!writtenFiles) return config.sys.exit(1);
1354
+ console.log();
1355
+ console.log(`${config.logger.gray("$")} stencil generate ${input}`);
1356
+ console.log();
1357
+ console.log(config.logger.bold("The following files have been generated:"));
1358
+ const absoluteRootDir = config.rootDir;
1359
+ writtenFiles.map((file) => console.log(` - ${relative(absoluteRootDir, file)}`));
1360
+ };
1361
+ /**
1362
+ * Show a checkbox prompt to select the files to be generated.
1363
+ *
1364
+ * @param cssExtension the extension of the CSS file to be generated
1365
+ * @returns a read-only array of `GeneratableExtension`, the extensions that the user has decided
1366
+ * to generate
1367
+ */
1368
+ const chooseFilesToGenerate = async (cssExtension) => {
1369
+ const { prompt } = await import("prompts");
1370
+ return (await prompt({
1371
+ name: "filesToGenerate",
1372
+ type: "multiselect",
1373
+ message: "Which additional files do you want to generate?",
1374
+ choices: [
1375
+ {
1376
+ value: cssExtension,
1377
+ title: `Stylesheet (.${cssExtension})`,
1378
+ selected: true
1379
+ },
1380
+ {
1381
+ value: "spec.tsx",
1382
+ title: "Spec Test (.spec.tsx)",
1383
+ selected: true
1384
+ },
1385
+ {
1386
+ value: "e2e.ts",
1387
+ title: "E2E Test (.e2e.ts)",
1388
+ selected: true
1389
+ }
1390
+ ]
1391
+ })).filesToGenerate;
1392
+ };
1393
+ const chooseSassExtension = async () => {
1394
+ const { prompt } = await import("prompts");
1395
+ return (await prompt({
1396
+ name: "sassFormat",
1397
+ type: "select",
1398
+ message: "Which Sass format would you like to use? (More info: https://sass-lang.com/documentation/syntax/#the-indented-syntax)",
1399
+ choices: [{
1400
+ value: "sass",
1401
+ title: `*.sass Format`,
1402
+ selected: true
1403
+ }, {
1404
+ value: "scss",
1405
+ title: "*.scss Format"
1406
+ }]
1407
+ })).sassFormat;
1408
+ };
1409
+ /**
1410
+ * Get a filepath for a file we want to generate!
1411
+ *
1412
+ * The filepath for a given file depends on the path, the user-supplied
1413
+ * component name, the extension, and whether we're inside of a test directory.
1414
+ *
1415
+ * @param filePath path to where we're going to generate the component
1416
+ * @param componentName the user-supplied name for the generated component
1417
+ * @param extension the file extension
1418
+ * @returns the full filepath to the component (with a possible `test` directory
1419
+ * added)
1420
+ */
1421
+ const getFilepathForFile = (filePath, componentName, extension) => isTest(extension) ? normalizePath(join(filePath, "test", `${componentName}.${extension}`)) : normalizePath(join(filePath, `${componentName}.${extension}`));
1422
+ /**
1423
+ * Get the boilerplate for a file and write it to disk
1424
+ *
1425
+ * @param config the current config, needed for file operations
1426
+ * @param componentName the component name (user-supplied)
1427
+ * @param withCss are we generating CSS?
1428
+ * @param file the file we want to write
1429
+ * @param styleExtension extension used for styles
1430
+ * @returns a `Promise<string>` which holds the full filepath we've written to,
1431
+ * used to print out a little summary of our activity to the user.
1432
+ */
1433
+ const getBoilerplateAndWriteFile = async (config, componentName, withCss, file, styleExtension) => {
1434
+ const boilerplate = getBoilerplateByExtension(componentName, file.extension, withCss, styleExtension);
1435
+ await config.sys.writeFile(normalizePath(file.path), boilerplate);
1436
+ return file.path;
1437
+ };
1438
+ /**
1439
+ * Check to see if any of the files we plan to write already exist and would
1440
+ * therefore be overwritten if we proceed, because we'd like to not overwrite
1441
+ * people's code!
1442
+ *
1443
+ * This function will check all the filepaths and if it finds any files log an
1444
+ * error and exit with an error code. If it doesn't find anything it will just
1445
+ * peacefully return `Promise<void>`.
1446
+ *
1447
+ * @param files the files we want to check
1448
+ * @param config the Config object, used here to get access to `sys.readFile`
1449
+ */
1450
+ const checkForOverwrite = async (files, config) => {
1451
+ const alreadyPresent = [];
1452
+ await Promise.all(files.map(async ({ path }) => {
1453
+ if (await config.sys.readFile(path) !== void 0) alreadyPresent.push(path);
1454
+ }));
1455
+ if (alreadyPresent.length > 0) {
1456
+ config.logger.error("Generating code would overwrite the following files:", ...alreadyPresent.map((path) => " " + normalizePath(path)));
1457
+ await config.sys.exit(1);
1458
+ }
1459
+ };
1460
+ /**
1461
+ * Check if an extension is for a test
1462
+ *
1463
+ * @param extension the extension we want to check
1464
+ * @returns a boolean indicating whether or not its a test
1465
+ */
1466
+ const isTest = (extension) => {
1467
+ return extension === "e2e.ts" || extension === "spec.tsx";
1468
+ };
1469
+ /**
1470
+ * Get the boilerplate for a file by its extension.
1471
+ *
1472
+ * @param tagName the name of the component we're generating
1473
+ * @param extension the file extension we want boilerplate for (.css, tsx, etc)
1474
+ * @param withCss a boolean indicating whether we're generating a CSS file
1475
+ * @param styleExtension extension used for styles
1476
+ * @returns a string container the file boilerplate for the supplied extension
1477
+ */
1478
+ const getBoilerplateByExtension = (tagName, extension, withCss, styleExtension) => {
1479
+ switch (extension) {
1480
+ case "tsx": return getComponentBoilerplate(tagName, withCss, styleExtension);
1481
+ case "css":
1482
+ case "less":
1483
+ case "sass":
1484
+ case "scss": return getStyleUrlBoilerplate(styleExtension);
1485
+ case "spec.tsx": return getSpecTestBoilerplate(tagName);
1486
+ case "e2e.ts": return getE2eTestBoilerplate(tagName);
1487
+ default: throw new Error(`Unkown extension "${extension}".`);
1488
+ }
1489
+ };
1490
+ /**
1491
+ * Get the boilerplate for a file containing the definition of a component
1492
+ * @param tagName the name of the tag to give the component
1493
+ * @param hasStyle designates if the component has an external stylesheet or not
1494
+ * @param styleExtension extension used for styles
1495
+ * @returns the contents of a file that defines a component
1496
+ */
1497
+ const getComponentBoilerplate = (tagName, hasStyle, styleExtension) => {
1498
+ const decorator = [`{`];
1499
+ decorator.push(` tag: '${tagName}',`);
1500
+ if (hasStyle) decorator.push(` styleUrl: '${tagName}.${styleExtension}',`);
1501
+ decorator.push(` shadow: true,`);
1502
+ decorator.push(`}`);
1503
+ return `import { Component, Host, h } from '@stencil/core';
1504
+
1505
+ @Component(${decorator.join("\n")})
1506
+ export class ${toPascalCase(tagName)} {
1507
+ render() {
1508
+ return (
1509
+ <Host>
1510
+ <slot></slot>
1511
+ </Host>
1512
+ );
1513
+ }
1514
+ }
1515
+ `;
1516
+ };
1517
+ /**
1518
+ * Get the boilerplate for style for a generated component
1519
+ * @param ext extension used for styles
1520
+ * @returns a boilerplate CSS block
1521
+ */
1522
+ const getStyleUrlBoilerplate = (ext) => ext === "sass" ? `:host
1523
+ display: block
1524
+ ` : `:host {
1525
+ display: block;
1526
+ }
1527
+ `;
1528
+ /**
1529
+ * Get the boilerplate for a file containing a spec (unit) test for a component
1530
+ * @param tagName the name of the tag associated with the component under test
1531
+ * @returns the contents of a file that unit tests a component
1532
+ */
1533
+ const getSpecTestBoilerplate = (tagName) => `import { newSpecPage } from '@stencil/core/testing';
1534
+ import { ${toPascalCase(tagName)} } from '../${tagName}';
1535
+
1536
+ describe('${tagName}', () => {
1537
+ it('renders', async () => {
1538
+ const page = await newSpecPage({
1539
+ components: [${toPascalCase(tagName)}],
1540
+ html: \`<${tagName}></${tagName}>\`,
1541
+ });
1542
+ expect(page.root).toEqualHtml(\`
1543
+ <${tagName}>
1544
+ <mock:shadow-root>
1545
+ <slot></slot>
1546
+ </mock:shadow-root>
1547
+ </${tagName}>
1548
+ \`);
1549
+ });
1550
+ });
1551
+ `;
1552
+ /**
1553
+ * Get the boilerplate for a file containing an end-to-end (E2E) test for a component
1554
+ * @param tagName the name of the tag associated with the component under test
1555
+ * @returns the contents of a file that E2E tests a component
1556
+ */
1557
+ const getE2eTestBoilerplate = (tagName) => `import { newE2EPage } from '@stencil/core/testing';
1558
+
1559
+ describe('${tagName}', () => {
1560
+ it('renders', async () => {
1561
+ const page = await newE2EPage();
1562
+ await page.setContent('<${tagName}></${tagName}>');
1563
+
1564
+ const element = await page.find('${tagName}');
1565
+ expect(element).toHaveClass('hydrated');
1566
+ });
1567
+ });
1568
+ `;
1569
+ /**
1570
+ * Convert a dash case string to pascal case.
1571
+ * @param str the string to convert
1572
+ * @returns the converted input as pascal case
1573
+ */
1574
+ const toPascalCase = (str) => str.split("-").reduce((res, part) => res + part[0].toUpperCase() + part.slice(1), "");
1575
+ //#endregion
1576
+ //#region src/task-telemetry.ts
1577
+ /**
1578
+ * Entrypoint for the Telemetry task
1579
+ * @param flags configuration flags provided to Stencil when a task was called (either this task or a task that invokes
1580
+ * telemetry)
1581
+ * @param sys the abstraction for interfacing with the operating system
1582
+ * @param logger a logging implementation to log the results out to the user
1583
+ */
1584
+ const taskTelemetry = async (flags, sys, logger) => {
1585
+ const prompt = logger.dim(sys.details?.platform === "windows" ? ">" : "$");
1586
+ const isEnabling = flags.args.includes("on");
1587
+ const isDisabling = flags.args.includes("off");
1588
+ const INFORMATION = `Opt in or out of telemetry. Information about the data we collect is available on our website: ${logger.bold("https://stenciljs.com/telemetry")}`;
1589
+ const ENABLED_MESSAGE = `${logger.green("Enabled")}. Thank you for helping to make Stencil better! 💖\n\n`;
1590
+ const DISABLED_MESSAGE = `${logger.red("Disabled")}\n\n`;
1591
+ const hasTelemetry = await checkTelemetry(sys);
1592
+ if (isEnabling) {
1593
+ if (await enableTelemetry(sys)) console.log(`\n ${logger.bold("Telemetry is now ") + ENABLED_MESSAGE}`);
1594
+ else console.log(`Something went wrong when enabling Telemetry.`);
1595
+ return;
1596
+ }
1597
+ if (isDisabling) {
1598
+ if (await disableTelemetry(sys)) console.log(`\n ${logger.bold("Telemetry is now ") + DISABLED_MESSAGE}`);
1599
+ else console.log(`Something went wrong when disabling Telemetry.`);
1600
+ return;
1601
+ }
1602
+ console.log(` ${logger.bold("Telemetry:")} ${logger.dim(INFORMATION)}`);
1603
+ console.log(`\n ${logger.bold("Status")}: ${hasTelemetry ? ENABLED_MESSAGE : DISABLED_MESSAGE}`);
1604
+ console.log(` ${prompt} ${logger.green("stencil telemetry [off|on]")}
1605
+
1606
+ ${logger.cyan("off")} ${logger.dim(".............")} Disable sharing anonymous usage data
1607
+ ${logger.cyan("on")} ${logger.dim("..............")} Enable sharing anonymous usage data
1608
+ `);
1609
+ };
1610
+ //#endregion
1611
+ //#region src/task-help.ts
1612
+ /**
1613
+ * Entrypoint for the Help task, providing Stencil usage context to the user
1614
+ * @param flags configuration flags provided to Stencil when a task was call (either this task or a task that invokes
1615
+ * telemetry)
1616
+ * @param logger a logging implementation to log the results out to the user
1617
+ * @param sys the abstraction for interfacing with the operating system
1618
+ */
1619
+ const taskHelp = async (flags, logger, sys) => {
1620
+ const prompt = logger.dim(sys.details?.platform === "windows" ? ">" : "$");
1621
+ console.log(`
1622
+ ${logger.bold("Build:")} ${logger.dim("Build components for development or production.")}
1623
+
1624
+ ${prompt} ${logger.green("stencil build [--dev] [--watch] [--prerender] [--debug]")}
1625
+
1626
+ ${logger.cyan("--dev")} ${logger.dim(".............")} Development build
1627
+ ${logger.cyan("--watch")} ${logger.dim("...........")} Rebuild when files update
1628
+ ${logger.cyan("--serve")} ${logger.dim("...........")} Start the dev-server
1629
+ ${logger.cyan("--prerender")} ${logger.dim(".......")} Prerender the application
1630
+ ${logger.cyan("--docs")} ${logger.dim("............")} Generate component readme.md docs
1631
+ ${logger.cyan("--config")} ${logger.dim("..........")} Set stencil config file
1632
+ ${logger.cyan("--stats")} ${logger.dim("...........")} Write stats, optional file path (default: stencil-stats.json)
1633
+ ${logger.cyan("--log")} ${logger.dim(".............")} Write stencil-build.log file
1634
+ ${logger.cyan("--debug")} ${logger.dim("...........")} Set the log level to debug
1635
+
1636
+
1637
+ ${logger.bold("Test:")} ${logger.dim("Run unit and end-to-end tests.")}
1638
+
1639
+ ${prompt} ${logger.green("stencil test [--spec] [--e2e]")}
1640
+
1641
+ ${logger.cyan("--spec")} ${logger.dim("............")} Run unit tests with Jest
1642
+ ${logger.cyan("--e2e")} ${logger.dim(".............")} Run e2e tests with Puppeteer
1643
+
1644
+
1645
+ ${logger.bold("Generate:")} ${logger.dim("Bootstrap components.")}
1646
+
1647
+ ${prompt} ${logger.green("stencil generate")} or ${logger.green("stencil g")}
1648
+
1649
+ `);
1650
+ await taskTelemetry(flags, sys, logger);
1651
+ console.log(`
1652
+ ${logger.bold("Examples:")}
1653
+
1654
+ ${prompt} ${logger.green("stencil build --dev --watch --serve")}
1655
+ ${prompt} ${logger.green("stencil build --prerender")}
1656
+ ${prompt} ${logger.green("stencil test --spec --e2e")}
1657
+ ${prompt} ${logger.green("stencil telemetry on")}
1658
+ ${prompt} ${logger.green("stencil generate")}
1659
+ ${prompt} ${logger.green("stencil g my-component")}
1660
+ `);
1661
+ };
1662
+ //#endregion
1663
+ //#region src/task-info.ts
1664
+ /**
1665
+ * Generate the output for Stencils 'info' task, and log that output - `npx stencil info`
1666
+ * @param coreCompiler the compiler instance to derive certain version information from
1667
+ * @param sys the compiler system instance that provides details about the system Stencil is running on
1668
+ * @param logger the logger instance to use to log information out to
1669
+ */
1670
+ const taskInfo = (coreCompiler, sys, logger) => {
1671
+ const details = sys.details;
1672
+ const versions = coreCompiler.versions;
1673
+ console.log(``);
1674
+ console.log(`${logger.cyan(" System:")} ${sys.name} ${sys.version}`);
1675
+ if (details) {
1676
+ console.log(`${logger.cyan(" Platform:")} ${details.platform} (${details.release})`);
1677
+ console.log(`${logger.cyan(" CPU Model:")} ${details.cpuModel} (${sys.hardwareConcurrency} cpu${sys.hardwareConcurrency !== 1 ? "s" : ""})`);
1678
+ }
1679
+ console.log(`${logger.cyan(" Compiler:")} ${sys.getCompilerExecutingPath()}`);
1680
+ console.log(`${logger.cyan(" Build:")} ${coreCompiler.buildId}`);
1681
+ console.log(`${logger.cyan(" Stencil:")} ${coreCompiler.version}${logger.emoji(" " + coreCompiler.vermoji)}`);
1682
+ console.log(`${logger.cyan(" TypeScript:")} ${versions.typescript}`);
1683
+ console.log(`${logger.cyan(" Rollup:")} ${versions.rollup}`);
1684
+ console.log(`${logger.cyan(" Terser:")} ${versions.terser}`);
1685
+ console.log(``);
1686
+ };
1687
+ //#endregion
1688
+ //#region src/task-serve.ts
1689
+ const taskServe = async (config, flags) => {
1690
+ config.suppressLogs = true;
1691
+ if (typeof flags.open === "boolean") config.devServer.openBrowser = flags.open;
1692
+ config.devServer.reloadStrategy = null;
1693
+ config.devServer.initialLoadUrl = "/";
1694
+ config.devServer.websocket = false;
1695
+ config.maxConcurrentWorkers = 1;
1696
+ config.devServer.root = isString(flags.root) ? flags.root : config.sys.getCurrentDirectory();
1697
+ if (!config.sys.onProcessInterrupt) throw new Error(`Environment doesn't provide required function: onProcessInterrupt`);
1698
+ const devServer = await start(config.devServer, config.logger);
1699
+ console.log(`${config.logger.cyan(" Root:")} ${devServer.root}`);
1700
+ console.log(`${config.logger.cyan(" Address:")} ${devServer.address}`);
1701
+ console.log(`${config.logger.cyan(" Port:")} ${devServer.port}`);
1702
+ console.log(`${config.logger.cyan(" Server:")} ${devServer.browserUrl}`);
1703
+ console.log(``);
1704
+ config.sys.onProcessInterrupt(() => {
1705
+ if (devServer) {
1706
+ config.logger.debug(`dev server close: ${devServer.browserUrl}`);
1707
+ devServer.close();
1708
+ }
1709
+ });
1710
+ };
1711
+ //#endregion
1712
+ //#region src/run.ts
1713
+ /**
1714
+ * Main entry point for the Stencil CLI
1715
+ *
1716
+ * Take care of parsing CLI arguments, initializing various components needed
1717
+ * by the rest of the program, and kicking off the correct task (build, test,
1718
+ * etc).
1719
+ *
1720
+ * @param init initial CLI options
1721
+ * @returns an empty promise
1722
+ */
1723
+ const run = async (init) => {
1724
+ const { args, logger, sys } = init;
1725
+ try {
1726
+ const flags = parseFlags(args);
1727
+ const task = flags.task;
1728
+ if (flags.debug || flags.verbose) logger.setLevel("debug");
1729
+ if (flags.ci) logger.enableColors(false);
1730
+ if (isFunction(sys.applyGlobalPatch)) sys.applyGlobalPatch(sys.getCurrentDirectory());
1731
+ if (task && task === "version" || flags.version) {
1732
+ const coreCompiler = await loadCoreCompiler(sys);
1733
+ console.log(coreCompiler.version);
1734
+ return;
1735
+ }
1736
+ if (!task || task === "help" || flags.help) {
1737
+ await taskHelp(createConfigFlags({
1738
+ task: "help",
1739
+ args
1740
+ }), logger, sys);
1741
+ return;
1742
+ }
1743
+ startupLog(logger, task);
1744
+ const findConfigResults = await findConfig({
1745
+ sys,
1746
+ configPath: flags.config
1747
+ });
1748
+ if (findConfigResults.isErr) {
1749
+ logger.printDiagnostics(findConfigResults.value);
1750
+ return sys.exit(1);
1751
+ }
1752
+ const coreCompiler = await loadCoreCompiler(sys);
1753
+ startupLogVersion(logger, task, coreCompiler);
1754
+ loadedCompilerLog(sys, logger, flags, coreCompiler);
1755
+ if (task === "info") {
1756
+ taskInfo(coreCompiler, sys, logger);
1757
+ return;
1758
+ }
1759
+ const foundConfig = result.unwrap(findConfigResults);
1760
+ const configWithFlags = mergeFlags({}, flags);
1761
+ const validated = await coreCompiler.loadConfig({
1762
+ config: configWithFlags,
1763
+ configPath: foundConfig.configPath,
1764
+ logger,
1765
+ sys
1766
+ });
1767
+ if (validated.diagnostics.length > 0) {
1768
+ logger.printDiagnostics(validated.diagnostics);
1769
+ if (hasError(validated.diagnostics)) return sys.exit(1);
1770
+ }
1771
+ if (isFunction(sys.applyGlobalPatch)) sys.applyGlobalPatch(validated.config.rootDir);
1772
+ await telemetryAction(sys, validated.config, coreCompiler, flags, async () => {
1773
+ await runTask(coreCompiler, validated.config, task, sys, flags);
1774
+ });
1775
+ } catch (e) {
1776
+ if (!shouldIgnoreError(e)) {
1777
+ const details = `${logger.getLevel() === "debug" && e instanceof Error ? e.stack : ""}`;
1778
+ logger.error(`uncaught cli error: ${e}${details}`);
1779
+ return sys.exit(1);
1780
+ }
1781
+ }
1782
+ };
1783
+ /**
1784
+ * Run a specified task
1785
+ *
1786
+ * @param coreCompiler an instance of a minimal, bootstrap compiler for running the specified task
1787
+ * @param config a configuration for the Stencil project to apply to the task run
1788
+ * @param task the task to run
1789
+ * @param sys the {@link d.CompilerSystem} for interacting with the operating system
1790
+ * @param flags the parsed CLI flags (owned by CLI, not passed to Core)
1791
+ * @public
1792
+ * @returns a void promise
1793
+ */
1794
+ const runTask = async (coreCompiler, config, task, sys, flags) => {
1795
+ const resolvedFlags = flags ?? createConfigFlags({ task });
1796
+ const configWithFlags = mergeFlags(config, resolvedFlags);
1797
+ if (!configWithFlags.sys) configWithFlags.sys = sys;
1798
+ const strictConfig = coreCompiler.validateConfig(configWithFlags, {}).config;
1799
+ switch (task) {
1800
+ case "build":
1801
+ await taskBuild(coreCompiler, strictConfig, resolvedFlags);
1802
+ break;
1803
+ case "docs":
1804
+ await taskDocs(coreCompiler, strictConfig);
1805
+ break;
1806
+ case "generate":
1807
+ case "g":
1808
+ await taskGenerate(strictConfig, resolvedFlags);
1809
+ break;
1810
+ case "help":
1811
+ await taskHelp(resolvedFlags, strictConfig.logger, sys);
1812
+ break;
1813
+ case "prerender":
1814
+ await taskPrerender(coreCompiler, strictConfig, resolvedFlags);
1815
+ break;
1816
+ case "serve":
1817
+ await taskServe(strictConfig, resolvedFlags);
1818
+ break;
1819
+ case "telemetry":
1820
+ await taskTelemetry(resolvedFlags, sys, strictConfig.logger);
1821
+ break;
1822
+ case "version":
1823
+ console.log(coreCompiler.version);
1824
+ break;
1825
+ default:
1826
+ strictConfig.logger.error(`${strictConfig.logger.emoji("❌ ")}Invalid stencil command, please see the options below:`);
1827
+ await taskHelp(resolvedFlags, strictConfig.logger, sys);
1828
+ return configWithFlags.sys.exit(1);
1829
+ }
1830
+ };
1831
+ //#endregion
1832
+ export { BOOLEAN_CLI_FLAGS, createConfigFlags, parseFlags, run, runTask };