UNPKG

@stencil/core

Version:

A Compiler for Web Components and Progressive Web Apps

620 lines (594 loc) 20.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var utils = require('../utils'); function getConfigFilePath(process, sys, configArg) { if (configArg) { if (!sys.path.isAbsolute(configArg)) { // passed in a custom stencil config location // but it's relative, so prefix the cwd return utils.normalizePath(sys.path.join(process.cwd(), configArg)); } // config path already an absolute path, we're good here return utils.normalizePath(configArg); } // nothing was passed in, use the current working directory return utils.normalizePath(process.cwd()); } function parseFlags(process) { const flags = { task: null, args: [], knownArgs: [], unknownArgs: null }; // cmd line has more priority over npm scripts cmd flags.args = process.argv.slice(2); if (flags.args.length > 0 && flags.args[0] && !flags.args[0].startsWith('-')) { flags.task = flags.args[0]; } parseArgs(flags, flags.args, flags.knownArgs); const npmScriptCmdArgs = getNpmScriptArgs(process); parseArgs(flags, npmScriptCmdArgs, flags.knownArgs); npmScriptCmdArgs.forEach(npmArg => { if (!flags.args.includes(npmArg)) { flags.args.push(npmArg); } }); if (flags.task != null) { const i = flags.args.indexOf(flags.task); if (i > -1) { flags.args.splice(i, 1); } } flags.unknownArgs = flags.args.filter((arg) => { return !flags.knownArgs.includes(arg); }); return flags; } function parseArgs(flags, args, knownArgs) { ARG_OPTS.boolean.forEach(booleanName => { const alias = ARG_OPTS.alias[booleanName]; const flagKey = configCase(booleanName); if (typeof flags[flagKey] !== 'boolean') { flags[flagKey] = null; } args.forEach(cmdArg => { if (cmdArg === `--${booleanName}`) { flags[flagKey] = true; knownArgs.push(cmdArg); } else if (cmdArg === `--${flagKey}`) { flags[flagKey] = true; knownArgs.push(cmdArg); } else if (cmdArg === `--no-${booleanName}`) { flags[flagKey] = false; knownArgs.push(cmdArg); } else if (cmdArg === `--no${utils.dashToPascalCase(booleanName)}`) { flags[flagKey] = false; knownArgs.push(cmdArg); } else if (alias && cmdArg === `-${alias}`) { flags[flagKey] = true; knownArgs.push(cmdArg); } }); }); ARG_OPTS.string.forEach(stringName => { const alias = ARG_OPTS.alias[stringName]; const flagKey = configCase(stringName); if (typeof flags[flagKey] !== 'string') { flags[flagKey] = null; } for (let i = 0; i < args.length; i++) { const cmdArg = args[i]; if (cmdArg.startsWith(`--${stringName}=`)) { const values = cmdArg.split('='); values.shift(); flags[flagKey] = values.join('='); knownArgs.push(cmdArg); } else if (cmdArg === `--${stringName}`) { flags[flagKey] = args[i + 1]; knownArgs.push(cmdArg); knownArgs.push(args[i + 1]); } else if (cmdArg === `--${flagKey}`) { flags[flagKey] = args[i + 1]; knownArgs.push(cmdArg); knownArgs.push(args[i + 1]); } else if (cmdArg.startsWith(`--${flagKey}=`)) { const values = cmdArg.split('='); values.shift(); flags[flagKey] = values.join('='); knownArgs.push(cmdArg); } else if (alias) { if (cmdArg.startsWith(`-${alias}=`)) { const values = cmdArg.split('='); values.shift(); flags[flagKey] = values.join('='); knownArgs.push(cmdArg); } else if (cmdArg === `-${alias}`) { flags[flagKey] = args[i + 1]; knownArgs.push(args[i + 1]); } } } }); ARG_OPTS.number.forEach(numberName => { const alias = ARG_OPTS.alias[numberName]; const flagKey = configCase(numberName); if (typeof flags[flagKey] !== 'number') { flags[flagKey] = null; } for (let i = 0; i < args.length; i++) { const cmdArg = args[i]; if (cmdArg.startsWith(`--${numberName}=`)) { const values = cmdArg.split('='); values.shift(); flags[flagKey] = parseInt(values.join(''), 10); knownArgs.push(cmdArg); } else if (cmdArg === `--${numberName}`) { flags[flagKey] = parseInt(args[i + 1], 10); knownArgs.push(args[i + 1]); } else if (cmdArg.startsWith(`--${flagKey}=`)) { const values = cmdArg.split('='); values.shift(); flags[flagKey] = parseInt(values.join(''), 10); knownArgs.push(cmdArg); } else if (cmdArg === `--${flagKey}`) { flags[flagKey] = parseInt(args[i + 1], 10); knownArgs.push(args[i + 1]); } else if (alias) { if (cmdArg.startsWith(`-${alias}=`)) { const values = cmdArg.split('='); values.shift(); flags[flagKey] = parseInt(values.join(''), 10); knownArgs.push(cmdArg); } else if (cmdArg === `-${alias}`) { flags[flagKey] = parseInt(args[i + 1], 10); knownArgs.push(args[i + 1]); } } } }); return flags; } function configCase(prop) { prop = utils.dashToPascalCase(prop); return prop.charAt(0).toLowerCase() + prop.substr(1); } const ARG_OPTS = { boolean: [ 'build', 'cache', 'check-version', 'ci', 'compare', 'debug', 'dev', 'docs', 'e2e', 'es5', 'esm', 'verbose', 'headless', 'devtools', 'help', 'log', 'open', 'prerender', 'prerender-external', 'prod', 'profile', 'service-worker', 'screenshot', 'serve', 'skip-node-check', 'spec', 'stats', 'update-screenshot', 'version', 'watch' ], number: [ 'max-workers', 'port' ], string: [ 'address', 'config', 'docs-json', 'emulate', 'log-level', 'root', 'screenshot-connector' ], alias: { 'config': 'c', 'help': 'h', 'port': 'p', 'version': 'v' } }; function getNpmScriptArgs(process) { // process.env.npm_config_argv // {"remain":["4444"],"cooked":["run","serve","--port","4444"],"original":["run","serve","--port","4444"]} let args = []; try { if (process.env) { const npmConfigArgs = process.env.npm_config_argv; if (npmConfigArgs) { args = JSON.parse(npmConfigArgs).original; if (args[0] === 'run') { args = args.slice(2); } } } } catch (e) { } return args; } /* * exit * https://github.com/cowboy/node-exit * * Copyright (c) 2013 "Cowboy" Ben Alman * Licensed under the MIT license. */ var exit = function exit(exitCode, streams) { if (!streams) { streams = [process.stdout, process.stderr]; } var drainCount = 0; // Actually exit if all streams are drained. function tryToExit() { if (drainCount === streams.length) { process.exit(exitCode); } } streams.forEach(function(stream) { // Count drained streams now, but monitor non-drained streams. if (stream.bufferSize === 0) { drainCount++; } else { stream.write('', 'utf-8', function() { drainCount++; tryToExit(); }); } // Prevent further writing. stream.write = function() {}; }); // If all streams were already drained, exit now. tryToExit(); // In Windows, when run as a Node.js child process, a script utilizing // this library might just exit with a 0 exit code, regardless. This code, // despite the fact that it looks a bit crazy, appears to fix that. process.on('exit', function() { process.exit(exitCode); }); }; function taskVersion(config) { console.log(config.sys.compiler.version); } async function taskCheckVersion(config) { try { const currentVersion = config.sys.compiler.version; const latestVersion = await config.sys.getLatestCompilerVersion(config.logger, true); if (config.sys.semver.lt(currentVersion, latestVersion)) { printUpdateMessage(config.logger, currentVersion, latestVersion); } else { console.log(`${config.logger.cyan(config.sys.compiler.name)} version ${config.logger.green(config.sys.compiler.version)} is the latest version`); } } catch (e) { config.logger.error(`unable to load latest compiler version: ${e}`); exit(1); } } async function validateCompilerVersion(sys, logger, latestVersionPromise) { const latestVersion = await latestVersionPromise; if (latestVersion == null) { return; } const currentVersion = sys.compiler.version; if (sys.semver.lt(currentVersion, latestVersion)) { printUpdateMessage(logger, currentVersion, latestVersion); } } function printUpdateMessage(logger, currentVersion, latestVersion) { const msg = [ `Update available: ${currentVersion} ${ARROW} ${latestVersion}`, `To get the latest, please run:`, NPM_INSTALL ]; const lineLength = msg[0].length; const o = []; let top = BOX_TOP_LEFT; while (top.length <= lineLength + (PADDING * 2)) { top += BOX_HORIZONTAL; } top += BOX_TOP_RIGHT; o.push(top); msg.forEach(m => { let line = BOX_VERTICAL; for (let i = 0; i < PADDING; i++) { line += ` `; } line += m; while (line.length <= lineLength + (PADDING * 2)) { line += ` `; } line += BOX_VERTICAL; o.push(line); }); let bottom = BOX_BOTTOM_LEFT; while (bottom.length <= lineLength + (PADDING * 2)) { bottom += BOX_HORIZONTAL; } bottom += BOX_BOTTOM_RIGHT; o.push(bottom); let output = `\n${INDENT}${o.join(`\n${INDENT}`)}\n`; output = output.replace(currentVersion, logger.red(currentVersion)); output = output.replace(latestVersion, logger.green(latestVersion)); output = output.replace(NPM_INSTALL, logger.cyan(NPM_INSTALL)); console.log(output); } const NPM_INSTALL = `npm install @stencil/core`; const ARROW = `→`; const BOX_TOP_LEFT = `╭`; const BOX_TOP_RIGHT = `╮`; const BOX_BOTTOM_LEFT = `╰`; const BOX_BOTTOM_RIGHT = `╯`; const BOX_VERTICAL = `│`; const BOX_HORIZONTAL = `─`; const PADDING = 2; const INDENT = ` `; async function taskBuild(process, config, flags) { const { Compiler } = require('../compiler/index.js'); const compiler = new Compiler(config); if (!compiler.isValid) { exit(1); } let devServerStart = null; if (shouldStartDevServer(config, flags)) { try { devServerStart = compiler.startDevServer(); } catch (e) { config.logger.error(e); exit(1); } } let latestVersionPromise; if (config.devMode && config.watch) { latestVersionPromise = config.sys.getLatestCompilerVersion(config.logger, false); } let devServer = null; if (devServerStart) { devServer = await devServerStart; } const results = await compiler.build(); if (!config.watch) { if (devServer != null) { await devServer.close(); devServer = null; } if (results != null && Array.isArray(results.diagnostics)) { const hasError = results.diagnostics.some(d => d.level === 'error' || d.type === 'runtime'); if (hasError) { config.sys.destroy(); exit(1); } } } if (config.watch || devServerStart) { process.once('SIGINT', () => { config.sys.destroy(); if (devServer != null) { devServer.close(); } }); } if (latestVersionPromise != null) { await validateCompilerVersion(config.sys, config.logger, latestVersionPromise); } return results; } function shouldStartDevServer(config, flags) { return (config.devServer && (flags.serve || flags.prerender)); } function taskDocs(config) { const { Compiler } = require('../compiler/index.js'); const compiler = new Compiler(config); if (!compiler.isValid) { exit(1); } return compiler.docs(); } function taskHelp(process, logger) { const p = logger.dim((process.platform === 'win32') ? '>' : '$'); console.log(` ${logger.bold('Build:')} ${logger.dim('Build components for development or production.')} ${p} ${logger.green('stencil build [--dev] [--watch] [--prerender] [--debug]')} ${logger.cyan('--dev')} ${logger.dim('.............')} Development build ${logger.cyan('--watch')} ${logger.dim('...........')} Rebuild when files update ${logger.cyan('--serve')} ${logger.dim('...........')} Start the dev-server ${logger.cyan('--prerender')} ${logger.dim('.......')} Prerender the application ${logger.cyan('--docs')} ${logger.dim('............')} Generate component readme.md docs ${logger.cyan('--config')} ${logger.dim('..........')} Set stencil config file ${logger.cyan('--stats')} ${logger.dim('...........')} Write stencil-stats.json file ${logger.cyan('--log')} ${logger.dim('.............')} Write stencil-build.log file ${logger.cyan('--debug')} ${logger.dim('...........')} Set the log level to debug ${logger.bold('Test:')} ${logger.dim('Run unit and end-to-end tests.')} ${p} ${logger.green('stencil test [--spec] [--e2e]')} ${logger.cyan('--spec')} ${logger.dim('............')} Run unit tests with Jest ${logger.cyan('--e2e')} ${logger.dim('.............')} Run e2e tests with Puppeteer ${logger.bold('Examples:')} ${p} ${logger.green('stencil build --dev --watch --serve')} ${p} ${logger.green('stencil build --prerender')} ${p} ${logger.green('stencil test --spec --e2e')} `); } async function taskServe(process, config, flags) { const { Compiler } = require('../compiler/index.js'); config.suppressLogs = true; const compiler = new Compiler(config); if (!compiler.isValid) { exit(1); } config.flags.serve = true; config.devServer.openBrowser = flags.open; config.devServer.reloadStrategy = null; config.devServer.initialLoadUrl = '/'; config.devServer.websocket = false; config.maxConcurrentWorkers = 1; config.devServer.root = process.cwd(); if (typeof flags.root === 'string') { if (!config.sys.path.isAbsolute(config.flags.root)) { config.devServer.root = config.sys.path.relative(process.cwd(), flags.root); } } config.devServer.root = utils.normalizePath(config.devServer.root); const devServer = await compiler.startDevServer(); if (devServer) { config.logger.info(`dev server: ${devServer.browserUrl}`); } process.once('SIGINT', () => { config.sys.destroy(); devServer && devServer.close(); exit(0); }); } async function taskTest(config) { try { const { Testing } = require('../testing/index.js'); const testing = new Testing(config); if (!testing.isValid) { exit(1); } // always ensure we have jest modules installed const ensureModuleIds = [ '@types/jest', 'jest', 'jest-cli' ]; if (config.flags.e2e) { // if it's an e2e test, also make sure we're got // puppeteer modules installed and if browserExecutablePath is provided don't download Chromium use only puppeteer-core instead const puppeteer = config.testing.browserExecutablePath ? 'puppeteer-core' : 'puppeteer'; ensureModuleIds.push('@types/puppeteer', puppeteer); if (config.flags.screenshot) { // ensure we've got pixelmatch for screenshots config.logger.warn(config.logger.yellow(`EXPERIMENTAL: screenshot visual diff testing is currently under heavy development and has not reached a stable status. However, any assistance testing would be appreciated.`)); } } // ensure we've got the required modules installed // jest and puppeteer are quite large, so this // is an experiment to lazy install these // modules only when you need them await config.sys.lazyRequire.ensure(config.logger, config.rootDir, ensureModuleIds); const passed = await testing.runTests(); await testing.destroy(); if (!passed) { exit(1); } } catch (e) { config.logger.error(e); exit(1); } } async function runTask(process, config, flags) { if (flags.help || flags.task === `help`) { taskHelp(process, config.logger); } else if (flags.version) { taskVersion(config); } else if (flags.checkVersion) { await taskCheckVersion(config); } else { switch (flags.task) { case 'build': await taskBuild(process, config, flags); break; case 'docs': await taskDocs(config); break; case 'serve': await taskServe(process, config, flags); break; case 'test': await taskTest(config); break; default: config.logger.error(`Invalid stencil command, please see the options below:`); taskHelp(process, config.logger); exit(1); } } } async function run(process, sys, logger) { process.on(`unhandledRejection`, (e) => { if (!utils.shouldIgnoreError(e)) { let msg = 'unhandledRejection'; if (e != null) { if (e.stack) { msg += ': ' + e.stack; } else if (e.message) { msg += ': ' + e.message; } else { msg += ': ' + e; } } logger.error(msg); } }); process.title = `Stencil`; const flags = parseFlags(process); // load the config file let config; try { const configPath = getConfigFilePath(process, sys, flags.config); // if --config is provided we need to check if it exists if (flags.config && !sys.fs.existsSync(configPath)) { throw new Error(`Stencil configuration file cannot be found at: "${flags.config}"`); } config = sys.loadConfigFile(configPath, process); config.sys = (config.sys || sys); config.logger = (config.logger || logger); } catch (e) { logger.error(e); exit(1); } try { if (typeof config.logLevel === 'string') { config.logger.level = config.logLevel; } config.flags = flags; process.title = `Stencil: ${config.namespace}`; await runTask(process, config, flags); } catch (e) { if (!utils.shouldIgnoreError(e)) { config.logger.error(`uncaught cli error: ${e}${config.logger.level === 'debug' ? e.stack : ''}`); exit(1); } } } exports.parseFlags = parseFlags; exports.run = run;