@stencil/core
Version:
A Compiler for Web Components and Progressive Web Apps
1,367 lines (1,349 loc) • 95.8 kB
JavaScript
/*!
Stencil CLI v4.3.0 | MIT Licensed | https://stenciljs.com
*/
import { parse, join, relative } from 'path';
/**
* Default style mode id
*/
/**
* Constant for the 'dist-hydrate-script' output target
*/
const DIST_HYDRATE_SCRIPT = 'dist-hydrate-script';
/**
* Constant for the 'docs-custom' output target
*/
const DOCS_CUSTOM = 'docs-custom';
/**
* Constant for the 'docs-json' output target
*/
const DOCS_JSON = 'docs-json';
/**
* Constant for the 'docs-readme' output target
*/
const DOCS_README = 'docs-readme';
/**
* Constant for the 'docs-vscode' output target
*/
const DOCS_VSCODE = 'docs-vscode';
/**
* Constant for the 'www' output target
*/
const WWW = 'www';
/**
* Convert a string from dash-case / kebab-case to PascalCase (or CamelCase,
* or whatever you call it!)
*
* @param str a string to convert
* @returns a converted string
*/
const dashToPascalCase = (str) => str
.toLowerCase()
.split('-')
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
.join('');
/**
* Convert a string to 'camelCase'
*
* @param str the string to convert
* @returns the converted string
*/
const toCamelCase = (str) => {
const pascalCase = dashToPascalCase(str);
return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
};
const isFunction = (v) => typeof v === 'function';
const isString = (v) => typeof v === 'string';
/**
* Builds a template `Diagnostic` entity for a build error. The created `Diagnostic` is returned, and have little
* detail attached to it regarding the specifics of the error - it is the responsibility of the caller of this method
* to attach the specifics of the error message.
*
* The created `Diagnostic` is pushed to the `diagnostics` argument as a side effect of calling this method.
*
* @param diagnostics the existing diagnostics that the created template `Diagnostic` should be added to
* @returns the created `Diagnostic`
*/
const buildError = (diagnostics) => {
const diagnostic = {
level: 'error',
type: 'build',
header: 'Build Error',
messageText: 'build error',
relFilePath: undefined,
absFilePath: undefined,
lines: [],
};
if (diagnostics) {
diagnostics.push(diagnostic);
}
return diagnostic;
};
/**
* Builds a diagnostic from an `Error`, appends it to the `diagnostics` parameter, and returns the created diagnostic
* @param diagnostics the series of diagnostics the newly created diagnostics should be added to
* @param err the error to derive information from in generating the diagnostic
* @param msg an optional message to use in place of `err` to generate the diagnostic
* @returns the generated diagnostic
*/
const catchError = (diagnostics, err, msg) => {
const diagnostic = {
level: 'error',
type: 'build',
header: 'Build Error',
messageText: 'build error',
lines: [],
};
if (isString(msg)) {
diagnostic.messageText = msg.length ? msg : 'UNKNOWN ERROR';
}
else if (err != null) {
if (err.stack != null) {
diagnostic.messageText = err.stack.toString();
}
else {
if (err.message != null) {
diagnostic.messageText = err.message.length ? err.message : 'UNKNOWN ERROR';
}
else {
diagnostic.messageText = err.toString();
}
}
}
if (diagnostics != null && !shouldIgnoreError(diagnostic.messageText)) {
diagnostics.push(diagnostic);
}
return diagnostic;
};
/**
* Determine if the provided diagnostics have any build errors
* @param diagnostics the diagnostics to inspect
* @returns true if any of the diagnostics in the list provided are errors that did not occur at runtime. false
* otherwise.
*/
const hasError = (diagnostics) => {
if (diagnostics == null || diagnostics.length === 0) {
return false;
}
return diagnostics.some((d) => d.level === 'error' && d.type !== 'runtime');
};
const shouldIgnoreError = (msg) => {
return msg === TASK_CANCELED_MSG;
};
const TASK_CANCELED_MSG = `task canceled`;
/**
* Convert Windows backslash paths to slash paths: foo\\bar ➔ foo/bar
* Forward-slash paths can be used in Windows as long as they're not
* extended-length paths and don't contain any non-ascii characters.
* This was created since the path methods in Node.js outputs \\ paths on Windows.
* @param path the Windows-based path to convert
* @param relativize whether or not a relative path should have `./` prepended
* @returns the converted path
*/
const normalizePath = (path, relativize = true) => {
if (typeof path !== 'string') {
throw new Error(`invalid path to normalize`);
}
path = normalizeSlashes(path.trim());
const components = pathComponents(path, getRootLength(path));
const reducedComponents = reducePathComponents(components);
const rootPart = reducedComponents[0];
const secondPart = reducedComponents[1];
const normalized = rootPart + reducedComponents.slice(1).join('/');
if (normalized === '') {
return '.';
}
if (rootPart === '' &&
secondPart &&
path.includes('/') &&
!secondPart.startsWith('.') &&
!secondPart.startsWith('@') &&
relativize) {
return './' + normalized;
}
return normalized;
};
const normalizeSlashes = (path) => path.replace(backslashRegExp, '/');
const altDirectorySeparator = '\\';
const urlSchemeSeparator = '://';
const backslashRegExp = /\\/g;
const reducePathComponents = (components) => {
if (!Array.isArray(components) || components.length === 0) {
return [];
}
const reduced = [components[0]];
for (let i = 1; i < components.length; i++) {
const component = components[i];
if (!component)
continue;
if (component === '.')
continue;
if (component === '..') {
if (reduced.length > 1) {
if (reduced[reduced.length - 1] !== '..') {
reduced.pop();
continue;
}
}
else if (reduced[0])
continue;
}
reduced.push(component);
}
return reduced;
};
const getRootLength = (path) => {
const rootLength = getEncodedRootLength(path);
return rootLength < 0 ? ~rootLength : rootLength;
};
const getEncodedRootLength = (path) => {
if (!path)
return 0;
const ch0 = path.charCodeAt(0);
// POSIX or UNC
if (ch0 === 47 /* CharacterCodes.slash */ || ch0 === 92 /* CharacterCodes.backslash */) {
if (path.charCodeAt(1) !== ch0)
return 1; // POSIX: "/" (or non-normalized "\")
const p1 = path.indexOf(ch0 === 47 /* CharacterCodes.slash */ ? '/' : altDirectorySeparator, 2);
if (p1 < 0)
return path.length; // UNC: "//server" or "\\server"
return p1 + 1; // UNC: "//server/" or "\\server\"
}
// DOS
if (isVolumeCharacter(ch0) && path.charCodeAt(1) === 58 /* CharacterCodes.colon */) {
const ch2 = path.charCodeAt(2);
if (ch2 === 47 /* CharacterCodes.slash */ || ch2 === 92 /* CharacterCodes.backslash */)
return 3; // DOS: "c:/" or "c:\"
if (path.length === 2)
return 2; // DOS: "c:" (but not "c:d")
}
// URL
const schemeEnd = path.indexOf(urlSchemeSeparator);
if (schemeEnd !== -1) {
const authorityStart = schemeEnd + urlSchemeSeparator.length;
const authorityEnd = path.indexOf('/', authorityStart);
if (authorityEnd !== -1) {
// URL: "file:///", "file://server/", "file://server/path"
// For local "file" URLs, include the leading DOS volume (if present).
// Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a
// special case interpreted as "the machine from which the URL is being interpreted".
const scheme = path.slice(0, schemeEnd);
const authority = path.slice(authorityStart, authorityEnd);
if (scheme === 'file' &&
(authority === '' || authority === 'localhost') &&
isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) {
const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2);
if (volumeSeparatorEnd !== -1) {
if (path.charCodeAt(volumeSeparatorEnd) === 47 /* CharacterCodes.slash */) {
// URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/"
return ~(volumeSeparatorEnd + 1);
}
if (volumeSeparatorEnd === path.length) {
// URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a"
// but not "file:///c:d" or "file:///c%3ad"
return ~volumeSeparatorEnd;
}
}
}
return ~(authorityEnd + 1); // URL: "file://server/", "http://server/"
}
return ~path.length; // URL: "file://server", "http://server"
}
// relative
return 0;
};
const isVolumeCharacter = (charCode) => (charCode >= 97 /* CharacterCodes.a */ && charCode <= 122 /* CharacterCodes.z */) ||
(charCode >= 65 /* CharacterCodes.A */ && charCode <= 90 /* CharacterCodes.Z */);
const getFileUrlVolumeSeparatorEnd = (url, start) => {
const ch0 = url.charCodeAt(start);
if (ch0 === 58 /* CharacterCodes.colon */)
return start + 1;
if (ch0 === 37 /* CharacterCodes.percent */ && url.charCodeAt(start + 1) === 51 /* CharacterCodes._3 */) {
const ch2 = url.charCodeAt(start + 2);
if (ch2 === 97 /* CharacterCodes.a */ || ch2 === 65 /* CharacterCodes.A */)
return start + 3;
}
return -1;
};
const pathComponents = (path, rootLength) => {
const root = path.substring(0, rootLength);
const rest = path.substring(rootLength).split('/');
const restLen = rest.length;
if (restLen > 0 && !rest[restLen - 1]) {
rest.pop();
}
return [root, ...rest];
};
const isOutputTargetHydrate = (o) => o.type === DIST_HYDRATE_SCRIPT;
const isOutputTargetDocs = (o) => o.type === DOCS_README || o.type === DOCS_JSON || o.type === DOCS_CUSTOM || o.type === DOCS_VSCODE;
/**
* Create an `Ok` given a value. This doesn't do any checking that the value is
* 'ok-ish' since doing so would make an undue assumption about what is 'ok'.
* Instead, this trusts the user to determine, at the call site, whether
* something is `ok()` or `err()`.
*
* @param value the value to wrap up in an `Ok`
* @returns an Ok wrapping the value
*/
const ok = (value) => ({
isOk: true,
isErr: false,
value,
});
/**
* Create an `Err` given a value.
*
* @param value the value to wrap up in an `Err`
* @returns an Ok wrapping the value
*/
const err = (value) => ({
isOk: false,
isErr: true,
value,
});
/**
* Unwrap a {@link Result}, return the value inside if it is an `Ok` and
* throw with the wrapped value if it is an `Err`.
*
* @throws with the wrapped value if it is an `Err`.
* @param result a result to peer inside of
* @returns the wrapped value, if `Ok`
*/
const unwrap = (result) => {
if (result.isOk) {
return result.value;
}
else {
throw result.value;
}
};
/**
* Check whether a string is a member of a ReadonlyArray<string>
*
* We need a little helper for this because unfortunately `includes` is typed
* on `ReadonlyArray<T>` as `(el: T): boolean` so a `string` cannot be passed
* to `includes` on a `ReadonlyArray` 😢 thus we have a little helper function
* where we do the type coercion just once.
*
* see microsoft/TypeScript#31018 for some discussion of this
*
* @param readOnlyArray the array we're checking
* @param maybeMember a value which is possibly a member of the array
* @returns whether the array contains the member or not
*/
const readOnlyArrayHasStringMember = (readOnlyArray, maybeMember) => readOnlyArray.includes(maybeMember);
/**
* Validates that a component tag meets required naming conventions to be used for a web component
* @param tag the tag to validate
* @returns an error message if the tag has an invalid name, undefined if the tag name passes all checks
*/
const validateComponentTag = (tag) => {
// we want to check this first since we call some String.prototype methods below
if (typeof tag !== 'string') {
return `Tag "${tag}" must be a string type`;
}
if (tag !== tag.trim()) {
return `Tag can not contain white spaces`;
}
if (tag !== tag.toLowerCase()) {
return `Tag can not contain upper case characters`;
}
if (tag.length === 0) {
return `Received empty tag value`;
}
if (tag.indexOf(' ') > -1) {
return `"${tag}" tag cannot contain a space`;
}
if (tag.indexOf(',') > -1) {
return `"${tag}" tag cannot be used for multiple tags`;
}
const invalidChars = tag.replace(/\w|-/g, '');
if (invalidChars !== '') {
return `"${tag}" tag contains invalid characters: ${invalidChars}`;
}
if (tag.indexOf('-') === -1) {
return `"${tag}" tag must contain a dash (-) to work as a valid web component`;
}
if (tag.indexOf('--') > -1) {
return `"${tag}" tag cannot contain multiple dashes (--) next to each other`;
}
if (tag.indexOf('-') === 0) {
return `"${tag}" tag cannot start with a dash (-)`;
}
if (tag.lastIndexOf('-') === tag.length - 1) {
return `"${tag}" tag cannot end with a dash (-)`;
}
return undefined;
};
/**
* This sets the log level hierarchy for our terminal logger, ranging from
* most to least verbose.
*
* Ordering the levels like this lets us easily check whether we should log a
* message at a given time. For instance, if the log level is set to `'warn'`,
* then anything passed to the logger with level `'warn'` or `'error'` should
* be logged, but we should _not_ log anything with level `'info'` or `'debug'`.
*
* If we have a current log level `currentLevel` and a message with level
* `msgLevel` is passed to the logger, we can determine whether or not we should
* log it by checking if the log level on the message is further up or at the
* same level in the hierarchy than `currentLevel`, like so:
*
* ```ts
* LOG_LEVELS.indexOf(msgLevel) >= LOG_LEVELS.indexOf(currentLevel)
* ```
*
* NOTE: for the reasons described above, do not change the order of the entries
* in this array without good reason!
*/
const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
/**
* All the Boolean options supported by the Stencil CLI
*/
const BOOLEAN_CLI_FLAGS = [
'build',
'cache',
'checkVersion',
'ci',
'compare',
'debug',
'dev',
'devtools',
'docs',
'e2e',
'es5',
'esm',
'help',
'log',
'open',
'prerender',
'prerenderExternal',
'prod',
'profile',
'serviceWorker',
'screenshot',
'serve',
'skipNodeCheck',
'spec',
'ssr',
'stats',
'updateScreenshot',
'verbose',
'version',
'watch',
// JEST CLI OPTIONS
'all',
'automock',
'bail',
// 'cache', Stencil already supports this argument
'changedFilesWithAncestor',
// 'ci', Stencil already supports this argument
'clearCache',
'clearMocks',
'collectCoverage',
'color',
'colors',
'coverage',
// 'debug', Stencil already supports this argument
'detectLeaks',
'detectOpenHandles',
'errorOnDeprecated',
'expand',
'findRelatedTests',
'forceExit',
'init',
'injectGlobals',
'json',
'lastCommit',
'listTests',
'logHeapUsage',
'noStackTrace',
'notify',
'onlyChanged',
'onlyFailures',
'passWithNoTests',
'resetMocks',
'resetModules',
'restoreMocks',
'runInBand',
'runTestsByPath',
'showConfig',
'silent',
'skipFilter',
'testLocationInResults',
'updateSnapshot',
'useStderr',
// 'verbose', Stencil already supports this argument
// 'version', Stencil already supports this argument
// 'watch', Stencil already supports this argument
'watchAll',
'watchman',
];
/**
* All the Number options supported by the Stencil CLI
*/
const NUMBER_CLI_FLAGS = [
'port',
// JEST CLI ARGS
'maxConcurrency',
'testTimeout',
];
/**
* All the String options supported by the Stencil CLI
*/
const STRING_CLI_FLAGS = [
'address',
'config',
'docsApi',
'docsJson',
'emulate',
'root',
'screenshotConnector',
// JEST CLI ARGS
'cacheDirectory',
'changedSince',
'collectCoverageFrom',
// 'config', Stencil already supports this argument
'coverageDirectory',
'coverageThreshold',
'env',
'filter',
'globalSetup',
'globalTeardown',
'globals',
'haste',
'moduleNameMapper',
'notifyMode',
'outputFile',
'preset',
'prettierPath',
'resolver',
'rootDir',
'runner',
'testEnvironment',
'testEnvironmentOptions',
'testFailureExitCode',
'testNamePattern',
'testResultsProcessor',
'testRunner',
'testSequencer',
'testURL',
'timers',
'transform',
];
const STRING_ARRAY_CLI_FLAGS = [
'collectCoverageOnlyFrom',
'coveragePathIgnorePatterns',
'coverageReporters',
'moduleDirectories',
'moduleFileExtensions',
'modulePathIgnorePatterns',
'modulePaths',
'projects',
'reporters',
'roots',
'selectProjects',
'setupFiles',
'setupFilesAfterEnv',
'snapshotSerializers',
'testMatch',
'testPathIgnorePatterns',
'testPathPattern',
'testRegex',
'transformIgnorePatterns',
'unmockedModulePathPatterns',
'watchPathIgnorePatterns',
];
/**
* All the CLI arguments which may have string or number values
*
* `maxWorkers` is an argument which is used both by Stencil _and_ by Jest,
* which means that we need to support parsing both string and number values.
*/
const STRING_NUMBER_CLI_FLAGS = ['maxWorkers'];
/**
* All the CLI arguments which may have boolean or string values.
*/
const BOOLEAN_STRING_CLI_FLAGS = [
/**
* `headless` is an argument passed through to Puppeteer (which is passed to Chrome) for end-to-end testing.
* Prior to Chrome v112, `headless` was treated like a boolean flag. Starting with Chrome v112, 'new' is an accepted
* option to support Chrome's new headless mode. In order to support this option in Stencil, both the boolean and
* string versions of the flag must be accepted.
*
* {@see https://developer.chrome.com/articles/new-headless/}
*/
'headless',
];
/**
* All the LogLevel-type options supported by the Stencil CLI
*
* This is a bit silly since there's only one such argument atm,
* but this approach lets us make sure that we're handling all
* our arguments in a type-safe way.
*/
const LOG_LEVEL_CLI_FLAGS = ['logLevel'];
/**
* For a small subset of CLI options we support a short alias e.g. `'h'` for `'help'`
*/
const CLI_FLAG_ALIASES = {
c: 'config',
h: 'help',
p: 'port',
v: 'version',
// JEST SPECIFIC CLI FLAGS
// these are defined in
// https://github.com/facebook/jest/blob/4156f86/packages/jest-cli/src/args.ts
b: 'bail',
e: 'expand',
f: 'onlyFailures',
i: 'runInBand',
o: 'onlyChanged',
t: 'testNamePattern',
u: 'updateSnapshot',
w: 'maxWorkers',
};
/**
* A regular expression which can be used to match a CLI flag for one of our
* short aliases.
*/
const CLI_FLAG_REGEX = new RegExp(`^-[chpvbewofitu]{1}$`);
/**
* Helper function for initializing a `ConfigFlags` object. Provide any overrides
* for default values and off you go!
*
* @param init an object with any overrides for default values
* @returns a complete CLI flag object
*/
const createConfigFlags = (init = {}) => {
const flags = {
task: null,
args: [],
knownArgs: [],
unknownArgs: [],
...init,
};
return flags;
};
/**
* Parse command line arguments into a structured `ConfigFlags` object
*
* @param args an array of CLI flags
* @returns a structured ConfigFlags object
*/
const parseFlags = (args) => {
const flags = createConfigFlags();
// cmd line has more priority over npm scripts cmd
flags.args = Array.isArray(args) ? args.slice() : [];
if (flags.args.length > 0 && flags.args[0] && !flags.args[0].startsWith('-')) {
flags.task = flags.args[0];
// if the first argument was a "task" (like `build`, `test`, etc) then
// we go on to parse the _rest_ of the CLI args
parseArgs(flags, args.slice(1));
}
else {
// we didn't find a leading flag, so we should just parse them all
parseArgs(flags, flags.args);
}
if (flags.task != null) {
const i = flags.args.indexOf(flags.task);
if (i > -1) {
flags.args.splice(i, 1);
}
}
return flags;
};
/**
* Parse the supported command line flags which are enumerated in the
* `config-flags` module. Handles leading dashes on arguments, aliases that are
* defined for a small number of arguments, and parsing values for non-boolean
* arguments (e.g. port number for the dev server).
*
* This parses the following grammar:
*
* CLIArguments → ""
* | CLITerm ( " " CLITerm )* ;
* CLITerm → EqualsArg
* | AliasEqualsArg
* | AliasArg
* | NegativeDashArg
* | NegativeArg
* | SimpleArg ;
* EqualsArg → "--" ArgName "=" CLIValue ;
* AliasEqualsArg → "-" AliasName "=" CLIValue ;
* AliasArg → "-" AliasName ( " " CLIValue )? ;
* NegativeDashArg → "--no-" ArgName ;
* NegativeArg → "--no" ArgName ;
* SimpleArg → "--" ArgName ( " " CLIValue )? ;
* ArgName → /^[a-zA-Z-]+$/ ;
* AliasName → /^[a-z]{1}$/ ;
* CLIValue → '"' /^[a-zA-Z0-9]+$/ '"'
* | /^[a-zA-Z0-9]+$/ ;
*
* There are additional constraints (not shown in the grammar for brevity's sake)
* on the type of `CLIValue` which will be associated with a particular argument.
* We enforce this by declaring lists of boolean, string, etc arguments and
* checking the types of values before setting them.
*
* We don't need to turn the list of CLI arg tokens into any kind of
* intermediate representation since we aren't concerned with doing anything
* other than setting the correct values on our ConfigFlags object. So we just
* parse the array of string arguments using a recursive-descent approach
* (which is not very deep since our grammar is pretty simple) and make the
* modifications we need to make to the {@link ConfigFlags} object as we go.
*
* @param flags a ConfigFlags object to which parsed arguments will be added
* @param args an array of command-line arguments to parse
*/
const parseArgs = (flags, args) => {
const argsCopy = args.concat();
while (argsCopy.length > 0) {
// there are still unprocessed args to deal with
parseCLITerm(flags, argsCopy);
}
};
/**
* Given an array of CLI arguments, parse it and perform a series of side
* effects (setting values on the provided `ConfigFlags` object).
*
* @param flags a {@link ConfigFlags} object which is updated as we parse the CLI
* arguments
* @param args a list of args to work through. This function (and some functions
* it calls) calls `Array.prototype.shift` to get the next argument to look at,
* so this parameter will be modified.
*/
const parseCLITerm = (flags, args) => {
// pull off the first arg from the argument array
const arg = args.shift();
// array is empty, we're done!
if (arg === undefined)
return;
// capture whether this is a special case of a negated boolean or boolean-string before we start to test each case
const isNegatedBoolean = !readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizeFlagName(arg)) &&
readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizeNegativeFlagName(arg));
const isNegatedBooleanOrString = !readOnlyArrayHasStringMember(BOOLEAN_STRING_CLI_FLAGS, normalizeFlagName(arg)) &&
readOnlyArrayHasStringMember(BOOLEAN_STRING_CLI_FLAGS, normalizeNegativeFlagName(arg));
// EqualsArg → "--" ArgName "=" CLIValue ;
if (arg.startsWith('--') && arg.includes('=')) {
// we're dealing with an EqualsArg, we have a special helper for that
const [originalArg, value] = parseEqualsArg(arg);
setCLIArg(flags, arg.split('=')[0], normalizeFlagName(originalArg), value);
}
// AliasEqualsArg → "-" AliasName "=" CLIValue ;
else if (arg.startsWith('-') && arg.includes('=')) {
// we're dealing with an AliasEqualsArg, we have a special helper for that
const [originalArg, value] = parseEqualsArg(arg);
setCLIArg(flags, desugarRawAlias(originalArg), normalizeFlagName(originalArg), value);
}
// AliasArg → "-" AliasName ( " " CLIValue )? ;
else if (CLI_FLAG_REGEX.test(arg)) {
// this is a short alias, like `-c` for Config
setCLIArg(flags, desugarRawAlias(arg), normalizeFlagName(arg), parseCLIValue(args));
}
// NegativeDashArg → "--no-" ArgName ;
else if (arg.startsWith('--no-') && arg.length > '--no-'.length) {
// this is a `NegativeDashArg` term, so we need to normalize the negative
// flag name and then set an appropriate value
const normalized = normalizeNegativeFlagName(arg);
setCLIArg(flags, arg, normalized, '');
}
// NegativeArg → "--no" ArgName ;
else if (arg.startsWith('--no') && (isNegatedBoolean || isNegatedBooleanOrString)) {
// possibly dealing with a `NegativeArg` here. There is a little ambiguity
// here because we have arguments that already begin with `no` like
// `notify`, so we need to test if a normalized form of the raw argument is
// a valid and supported boolean flag.
setCLIArg(flags, arg, normalizeNegativeFlagName(arg), '');
}
// SimpleArg → "--" ArgName ( " " CLIValue )? ;
else if (arg.startsWith('--') && arg.length > '--'.length) {
setCLIArg(flags, arg, normalizeFlagName(arg), parseCLIValue(args));
}
else {
// if we get here then `arg` is not an argument in our list of supported
// arguments. This doesn't necessarily mean we want to report an error or
// anything though! Instead, with unknown / unrecognized arguments we want
// to stick them into the `unknownArgs` array, which is used when we pass
// CLI args to Jest, for instance.
flags.unknownArgs.push(arg);
}
};
/**
* Normalize a 'negative' flag name, just to do a little pre-processing before
* we pass it to `setCLIArg`.
*
* @param flagName the flag name to normalize
* @returns a normalized flag name
*/
const normalizeNegativeFlagName = (flagName) => {
const trimmed = flagName.replace(/^--no[-]?/, '');
return normalizeFlagName(trimmed.charAt(0).toLowerCase() + trimmed.slice(1));
};
/**
* Normalize a flag name by:
*
* - replacing any leading dashes (`--foo` -> `foo`)
* - converting `dash-case` to camelCase (if necessary)
*
* Normalizing in this context basically means converting the various
* supported flag spelling variants to the variant defined in our lists of
* supported arguments (e.g. BOOLEAN_CLI_FLAGS, etc). So, for instance,
* `--log-level` should be converted to `logLevel`.
*
* @param flagName the flag name to normalize
* @returns a normalized flag name
*
*/
const normalizeFlagName = (flagName) => {
const trimmed = flagName.replace(/^-+/, '');
return trimmed.includes('-') ? toCamelCase(trimmed) : trimmed;
};
/**
* Set a value on a provided {@link ConfigFlags} object, given an argument
* name and a raw string value. This function dispatches to other functions
* which make sure that the string value can be properly parsed into a JS
* runtime value of the right type (e.g. number, string, etc).
*
* @throws if a value cannot be parsed to the right type for a given flag
* @param flags a {@link ConfigFlags} object
* @param rawArg the raw argument name matched by the parser
* @param normalizedArg an argument with leading control characters (`--`,
* `--no-`, etc) removed
* @param value the raw value to be set onto the config flags object
*/
const setCLIArg = (flags, rawArg, normalizedArg, value) => {
normalizedArg = desugarAlias(normalizedArg);
// We're setting a boolean!
if (readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizedArg)) {
flags[normalizedArg] =
typeof value === 'string'
? Boolean(value)
: // no value was supplied, default to true
true;
flags.knownArgs.push(rawArg);
}
// We're setting a string!
else if (readOnlyArrayHasStringMember(STRING_CLI_FLAGS, normalizedArg)) {
if (typeof value === 'string') {
flags[normalizedArg] = value;
flags.knownArgs.push(rawArg);
flags.knownArgs.push(value);
}
else {
throwCLIParsingError(rawArg, 'expected a string argument but received nothing');
}
}
// We're setting a string, but it's one where the user can pass multiple values,
// like `--reporters="default" --reporters="jest-junit"`
else if (readOnlyArrayHasStringMember(STRING_ARRAY_CLI_FLAGS, normalizedArg)) {
if (typeof value === 'string') {
if (!Array.isArray(flags[normalizedArg])) {
flags[normalizedArg] = [];
}
const targetArray = flags[normalizedArg];
// this is irritating, but TS doesn't know that the `!Array.isArray`
// check above guarantees we have an array to work with here, and it
// doesn't want to narrow the type of `flags[normalizedArg]`, so we need
// to grab a reference to that array and then `Array.isArray` that. Bah!
if (Array.isArray(targetArray)) {
targetArray.push(value);
flags.knownArgs.push(rawArg);
flags.knownArgs.push(value);
}
}
else {
throwCLIParsingError(rawArg, 'expected a string argument but received nothing');
}
}
// We're setting a number!
else if (readOnlyArrayHasStringMember(NUMBER_CLI_FLAGS, normalizedArg)) {
if (typeof value === 'string') {
const parsed = parseInt(value, 10);
if (isNaN(parsed)) {
throwNumberParsingError(rawArg, value);
}
else {
flags[normalizedArg] = parsed;
flags.knownArgs.push(rawArg);
flags.knownArgs.push(value);
}
}
else {
throwCLIParsingError(rawArg, 'expected a number argument but received nothing');
}
}
// We're setting a value which could be either a string _or_ a number
else if (readOnlyArrayHasStringMember(STRING_NUMBER_CLI_FLAGS, normalizedArg)) {
if (typeof value === 'string') {
if (CLI_ARG_STRING_REGEX.test(value)) {
// if it matches the regex we treat it like a string
flags[normalizedArg] = value;
}
else {
const parsed = Number(value);
if (isNaN(parsed)) {
// parsing didn't go so well, we gotta get out of here
// this is unlikely given our regex guard above
// but hey, this is ultimately JS so let's be safe
throwNumberParsingError(rawArg, value);
}
else {
flags[normalizedArg] = parsed;
}
}
flags.knownArgs.push(rawArg);
flags.knownArgs.push(value);
}
else {
throwCLIParsingError(rawArg, 'expected a string or a number but received nothing');
}
}
// We're setting a value which could be either a boolean _or_ a string
else if (readOnlyArrayHasStringMember(BOOLEAN_STRING_CLI_FLAGS, normalizedArg)) {
const derivedValue = typeof value === 'string'
? value
? value // use the supplied value if it's a non-empty string
: false // otherwise, default to false for the empty string
: true; // no value was supplied, default to true
flags[normalizedArg] = derivedValue;
flags.knownArgs.push(rawArg);
if (typeof derivedValue === 'string' && derivedValue) {
flags.knownArgs.push(derivedValue);
}
}
// We're setting the log level, which can only be a set of specific string values
else if (readOnlyArrayHasStringMember(LOG_LEVEL_CLI_FLAGS, normalizedArg)) {
if (typeof value === 'string') {
if (isLogLevel(value)) {
flags[normalizedArg] = value;
flags.knownArgs.push(rawArg);
flags.knownArgs.push(value);
}
else {
throwCLIParsingError(rawArg, `expected to receive a valid log level but received "${String(value)}"`);
}
}
else {
throwCLIParsingError(rawArg, 'expected to receive a valid log level but received nothing');
}
}
else {
// we haven't found this flag in any of our lists of arguments, so we
// should put it in our list of unknown arguments
flags.unknownArgs.push(rawArg);
if (typeof value === 'string') {
flags.unknownArgs.push(value);
}
}
};
/**
* We use this regular expression to detect CLI parameters which
* should be parsed as string values (as opposed to numbers) for
* the argument types for which we support both a string and a
* number value.
*
* The regex tests for the presence of at least one character which is
* _not_ a digit (`\d`), a period (`\.`), or one of the characters `"e"`,
* `"E"`, `"+"`, or `"-"` (the latter four characters are necessary to
* support the admittedly unlikely use of scientific notation, like `"4e+0"`
* for `4`).
*
* Thus we'll match a string like `"50%"`, but not a string like `"50"` or
* `"5.0"`. If it matches a given string we conclude that the string should
* be parsed as a string literal, rather than using `Number` to convert it
* to a number.
*/
const CLI_ARG_STRING_REGEX = /[^\d\.Ee\+\-]+/g;
const Empty = Symbol('Empty');
/**
* A little helper which tries to parse a CLI value (as opposed to a flag) off
* of the argument array.
*
* We support a variety of different argument formats for flags (as opposed to
* values), but all of them start with `-`, so we can check the first character
* to test whether the next token in our array of CLI arguments is a flag name
* or a value.
*
* @param args an array of CLI args
* @returns either a string result or an Empty sentinel
*/
const parseCLIValue = (args) => {
// it's possible the arguments array is empty, if so, return empty
if (args[0] === undefined) {
return Empty;
}
// all we're concerned with here is that it does not start with `"-"`,
// which would indicate it should be parsed as a CLI flag and not a value.
if (!args[0].startsWith('-')) {
// It's not a flag, so we return the value and defer any specific parsing
// until later on.
const value = args.shift();
if (typeof value === 'string') {
return value;
}
}
return Empty;
};
/**
* Parse an 'equals' argument, which is a CLI argument-value pair in the
* format `--foobar=12` (as opposed to a space-separated format like
* `--foobar 12`).
*
* To parse this we split on the `=`, returning the first part as the argument
* name and the second part as the value. We join the value on `"="` in case
* there is another `"="` in the argument.
*
* This function is safe to call with any arg, and can therefore be used as
* an argument 'normalizer'. If CLI argument is not an 'equals' argument then
* the return value will be a tuple of the original argument and an empty
* string `""` for the value.
*
* In code terms, if you do:
*
* ```ts
* const [arg, value] = parseEqualsArg("--myArgument")
* ```
*
* Then `arg` will be `"--myArgument"` and `value` will be `""`, whereas if
* you do:
*
*
* ```ts
* const [arg, value] = parseEqualsArg("--myArgument=myValue")
* ```
*
* Then `arg` will be `"--myArgument"` and `value` will be `"myValue"`.
*
* @param arg the arg in question
* @returns a tuple containing the arg name and the value (if present)
*/
const parseEqualsArg = (arg) => {
const [originalArg, ...splitSections] = arg.split('=');
const value = splitSections.join('=');
return [originalArg, value === '' ? Empty : value];
};
/**
* Small helper for getting type-system-level assurance that a `string` can be
* narrowed to a `LogLevel`
*
* @param maybeLogLevel the string to check
* @returns whether this is a `LogLevel`
*/
const isLogLevel = (maybeLogLevel) => readOnlyArrayHasStringMember(LOG_LEVELS, maybeLogLevel);
/**
* A little helper for constructing and throwing an error message with info
* about what went wrong
*
* @param flag the flag which encountered the error
* @param message a message specific to the error which was encountered
*/
const throwCLIParsingError = (flag, message) => {
throw new Error(`when parsing CLI flag "${flag}": ${message}`);
};
/**
* Throw a specific error for the situation where we ran into an issue parsing
* a number.
*
* @param flag the flag for which we encountered the issue
* @param value what we were trying to parse
*/
const throwNumberParsingError = (flag, value) => {
throwCLIParsingError(flag, `expected a number but received "${value}"`);
};
/**
* A little helper to 'desugar' a flag alias, meaning expand it to its full
* name. For instance, the alias `"c"` will desugar to `"config"`.
*
* If no expansion is found for the possible alias we just return the passed
* string unmodified.
*
* @param maybeAlias a string which _could_ be an alias to a full flag name
* @returns the full aliased flag name, if found, or the passed string if not
*/
const desugarAlias = (maybeAlias) => {
const possiblyDesugared = CLI_FLAG_ALIASES[maybeAlias];
if (typeof possiblyDesugared === 'string') {
return possiblyDesugared;
}
return maybeAlias;
};
/**
* Desugar a 'raw' alias (with a leading dash) and return an equivalent,
* desugared argument.
*
* For instance, passing `"-c` will return `"--config"`.
*
* The reason we'd like to do this is not so much for our own code, but so that
* we can transform an alias like `"-u"` to `"--updateSnapshot"` in order to
* pass it along to Jest.
*
* @param rawAlias a CLI flag alias as found on the command line (like `"-c"`)
* @returns an equivalent full command (like `"--config"`)
*/
const desugarRawAlias = (rawAlias) => '--' + desugarAlias(normalizeFlagName(rawAlias));
/**
* Attempt to find a Stencil configuration file on the file system
* @param opts the options needed to find the configuration file
* @returns the results of attempting to find a configuration file on disk
*/
const findConfig = async (opts) => {
const sys = opts.sys;
const cwd = sys.getCurrentDirectory();
const rootDir = normalizePath(cwd);
let configPath = opts.configPath;
if (isString(configPath)) {
if (!sys.platformPath.isAbsolute(configPath)) {
// passed in a custom stencil config location,
// but it's relative, so prefix the cwd
configPath = normalizePath(sys.platformPath.join(cwd, configPath));
}
else {
// config path already an absolute path, we're good here
configPath = normalizePath(opts.configPath);
}
}
else {
// nothing was passed in, use the current working directory
configPath = rootDir;
}
const results = {
configPath,
rootDir: normalizePath(cwd),
};
const stat = await sys.stat(configPath);
if (stat.error) {
const diagnostics = [];
const diagnostic = buildError(diagnostics);
diagnostic.absFilePath = configPath;
diagnostic.header = `Invalid config path`;
diagnostic.messageText = `Config path "${configPath}" not found`;
return err(diagnostics);
}
if (stat.isFile) {
results.configPath = configPath;
results.rootDir = sys.platformPath.dirname(configPath);
}
else if (stat.isDirectory) {
// this is only a directory, so let's make some assumptions
for (const configName of ['stencil.config.ts', 'stencil.config.js']) {
const testConfigFilePath = sys.platformPath.join(configPath, configName);
const stat = await sys.stat(testConfigFilePath);
if (stat.isFile) {
results.configPath = testConfigFilePath;
results.rootDir = sys.platformPath.dirname(testConfigFilePath);
break;
}
}
}
return ok(results);
};
const loadCoreCompiler = async (sys) => {
await sys.dynamicImport(sys.getCompilerExecutingPath());
return globalThis.stencil;
};
/**
* Log the name of this package (`@stencil/core`) to an output stream
*
* The output stream is determined by the {@link Logger} instance that is provided as an argument to this function
*
* The name of the package may not be logged, by design, for certain `task` types and logging levels
*
* @param logger the logging entity to use to output the name of the package
* @param task the current task
*/
const startupLog = (logger, task) => {
if (task === 'info' || task === 'serve' || task === 'version') {
return;
}
logger.info(logger.cyan(`@stencil/core`));
};
/**
* Log this package's version to an output stream
*
* The output stream is determined by the {@link Logger} instance that is provided as an argument to this function
*
* The package version may not be logged, by design, for certain `task` types and logging levels
*
* @param logger the logging entity to use for output
* @param task the current task
* @param coreCompiler the compiler instance to derive version information from
*/
const startupLogVersion = (logger, task, coreCompiler) => {
if (task === 'info' || task === 'serve' || task === 'version') {
return;
}
const isDevBuild = coreCompiler.version.includes('-dev.');
let startupMsg;
if (isDevBuild) {
startupMsg = logger.yellow(`[LOCAL DEV] v${coreCompiler.version}`);
}
else {
startupMsg = logger.cyan(`v${coreCompiler.version}`);
}
startupMsg += logger.emoji(' ' + coreCompiler.vermoji);
logger.info(startupMsg);
};
/**
* Log details from a {@link CompilerSystem} used by Stencil to an output stream
*
* The output stream is determined by the {@link Logger} instance that is provided as an argument to this function
*
* @param sys the `CompilerSystem` to report details on
* @param logger the logging entity to use for output
* @param flags user set flags for the current invocation of Stencil
* @param coreCompiler the compiler instance being used for this invocation of Stencil
*/
const loadedCompilerLog = (sys, logger, flags, coreCompiler) => {
const sysDetails = sys.details;
const runtimeInfo = `${sys.name} ${sys.version}`;
const platformInfo = sysDetails
? `${sysDetails.platform}, ${sysDetails.cpuModel}`
: `Unknown Platform, Unknown CPU Model`;
const statsInfo = sysDetails
? `cpus: ${sys.hardwareConcurrency}, freemem: ${Math.round(sysDetails.freemem() / 1000000)}MB, totalmem: ${Math.round(sysDetails.totalmem / 1000000)}MB`
: 'Unknown CPU Core Count, Unknown Memory';
if (logger.getLevel() === 'debug') {
logger.debug(runtimeInfo);
logger.debug(platformInfo);
logger.debug(statsInfo);
logger.debug(`compiler: ${sys.getCompilerExecutingPath()}`);
logger.debug(`build: ${coreCompiler.buildId}`);
}
else if (flags.ci) {
logger.info(runtimeInfo);
logger.info(platformInfo);
logger.info(statsInfo);
}
};
/**
* Log various warnings to an output stream
*
* The output stream is determined by the {@link Logger} instance attached to the `config` argument to this function
*
* @param coreCompiler the compiler instance being used for this invocation of Stencil
* @param config a validated configuration object to be used for this run of Stencil
*/
const startupCompilerLog = (coreCompiler, config) => {
if (config.suppressLogs === true) {
return;
}
const { logger } = config;
const isDebug = logger.getLevel() === 'debug';
const isPrerelease = coreCompiler.version.includes('-');
const isDevBuild = coreCompiler.version.includes('-dev.');
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.`));
}
if (config.devMode && !isDebug) {
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.`);
}
if (!config.enableCache) {
logger.warn(`Disabling cache during development will slow down incremental builds.`);
}
}
};
/**
* Retrieve a reference to the active `CompilerSystem`'s `checkVersion` function
* @param config the Stencil configuration associated with the currently compiled project
* @param currentVersion the Stencil compiler's version string
* @returns a reference to `checkVersion`, or `null` if one does not exist on the current `CompilerSystem`
*/
const startCheckVersion = async (config, currentVersion) => {
if (config.devMode && !config.flags.ci && !currentVersion.includes('-dev.') && isFunction(config.sys.checkVersion)) {
return config.sys.checkVersion(config.logger, currentVersion);
}
return null;
};
/**
* Print the results of running the provided `versionChecker`.
*
* Does not print if no `versionChecker` is provided.
*
* @param versionChecker the function to invoke.
*/
const printCheckVersionResults = async (versionChecker) => {
if (versionChecker) {
const checkVersionResults = await versionChecker;
if (isFunction(checkVersionResults)) {
checkVersionResults();
}
}
};
const taskPrerender = async (coreCompiler, config) => {
startupCompilerLog(coreCompiler, config);
const hydrateAppFilePath = config.flags.unknownArgs[0];
if (typeof hydrateAppFilePath !== 'string') {
config.logger.error(`Missing hydrate app script path`);
return config.sys.exit(1);
}
const srcIndexHtmlPath = config.srcIndexHtml;
const diagnostics = await runPrerenderTask(coreCompiler, config, hydrateAppFilePath, null, srcIndexHtmlPath);
config.logger.printDiagnostics(diagnostics);
if (diagnostics.some((d) => d.level === 'error')) {
return config.sys.exit(1);
}
};
const runPrerenderTask = async (coreCompiler, config, hydrateAppFilePath, componentGraph, srcIndexHtmlPath) => {
const diagnostics = [];
try {
const prerenderer = await coreCompiler.createPrerenderer(config);
const results = await prerenderer.start({
hydrateAppFilePath,
componentGraph,
srcIndexHtmlPath,
});
diagnostics.push(...results.diagnostics);
}
catch (e) {
catchError(diagnostics, e);
}
return diagnostics;
};
const taskWatch = async (coreCompiler, config) => {
let devServer = null;
let exitCode = 0;
try {
startupCompilerLog(coreCompiler, config);
const versionChecker = startCheckVersion(config, coreCompiler.version);
const compiler = await coreCompiler.createCompiler(config);
const watcher = await compiler.createWatcher();
if (config.flags.serve) {
const devServerPath = config.sys.getDevServerExecutingPath();
const { start } = await config.sys.dynamicImport(devServerPath);
devServer = await start(config.devServer, config.logger, watcher);
}
config.sys.onProcessInterrupt(() => {
config.logger.debug(`close watch`);
compiler && compiler.destroy();
});
const rmVersionCheckerLog = watcher.on('buildFinish', async () => {
// log the version check one time
rmVersionCheckerLog();
printCheckVersionResults(versionChecker);
});
if (devServer) {
const rmDevServerLog = watcher.on('buildFinish', () => {
var _a;
// log the dev server url one time
rmDevServerLog();
const url = (_a = devServer === null || devServer === void 0 ? void 0 : devServer.browserUrl) !== null && _a !== void 0 ? _a : 'UNKNOWN URL';
config.logger.info(`${config.logger.cyan(url)}\n`);
});
}
const closeResults = await watcher.start();
if (closeResults.exitCode > 0) {
exitCode = closeResults.exitCode;
}
}
catch (e) {
exitCode = 1;
config.logger.error(e);
}
if (devServer) {
await devServer.close();
}
if (exitCode > 0) {
return config.sys.exit(exitCode);
}
};
const tryFn = asyn