UNPKG

@lume/cli

Version:

A CLI for managing JavaScript/TypeScript packages.

409 lines (314 loc) 14 kB
// @ts-check const path = require('path') const fs = require('fs') const {showName} = require('../scripts/name.js') const config = require('../config/getUserConfig.js') const {exec} = require('../scripts/exec.js') const {useBabelForTypeScript = false, tsProjectReferenceMode = false} = require('../config/getUserConfig.js') // TODO read CLI options from a project's lume.config too. let cli module.exports.setCli = function (_cli) { cli = _cli } let opts module.exports.setOpts = function (options) { opts = options } exports.showName = showName // We use these so that no matter if we are running in a Yarn or Npm env (which // set PATH differently), we'll always be able to run the needed bins by relying // on Node.js module lookup algo (calling them directly might fail if they are // not in PATH) const tscBin = path.resolve(require.resolve('typescript'), '..', '..', 'bin', 'tsc') const babelBin = path.resolve(require.resolve('@babel/cli'), '..', 'bin', 'babel.js') const prettierBin = path.resolve(require.resolve('prettier'), '..', 'bin', 'prettier.cjs') const playwrightBin = path.resolve(require.resolve('playwright'), '..', 'cli.js') exports.build = build async function build({clean: shouldClean = false, noFail = opts.noFail} = {}) { if (opts.verbose) console.log(`===> Running the "build" command.\n`) await showName() if (shouldClean) await clean() await Promise.all([ copyAssets(), (async function () { const builtTs = await buildTs({noFail}) if (!builtTs) { console.log('No sources to build.') return } })(), ]) if (opts.verbose) console.log(`===> Done running the "build" command.\n`) } exports.clean = clean async function clean() { if (opts.verbose) console.log(`===> Running the "clean" command.\n`) const rmrf = require('rimraf') const {promisify} = require('util') await Promise.all([promisify(rmrf)('dist'), promisify(rmrf)('tsconfig.tsbuildinfo')]) if (opts.verbose) console.log(`===> Done running the "clean" command.\n`) } exports.dev = dev async function dev() { if (opts.verbose) console.log(`===> Running the "dev" command.\n`) await watchTs() if (opts.verbose) console.log(`===> Done running the "dev" command.\n`) } const srcDir = path.join(process.cwd(), 'src') const distDir = path.join(process.cwd(), 'dist') exports.copyAssets = copyAssets async function copyAssets() { if (opts.verbose) console.log(`===> Running the "copyAssets" command.\n`) await fs.promises.cp(srcDir, distDir, { recursive: true, filter(src) { if (src.endsWith('.tsx') || (src.endsWith('.ts') && !src.endsWith('.d.ts'))) return false return true }, }) if (opts.verbose) console.log(`===> Done running the "copyAssets" command.\n`) } async function mkDist() { try { await fs.promises.mkdir(distDir) } catch (e) { if (e.code !== 'EEXIST') throw e } } exports.buildTs = buildTs async function buildTs({noFail = opts.noFail} = {}) { if (opts.verbose) console.log(`===> Running the "buildTs" command.\n`) const start = performance.now() const fs = require('fs') // If no tsconfig, don't build. // TODO If there's no user tsconfig fall back to cli's tsconfig. We // might need to temporarily write it to the project root. try { await fs.promises.access(path.resolve(process.cwd(), 'tsconfig.json')) } catch (e) { console.log('No tsconfig file found. Skipping TypeScript build.') if (opts.verbose) console.log(`===> Done running the "buildTs" command.\n`) // Don't try to run TypeScript build if no tsconfig.json file is present. return false } let promises = [] if (useBabelForTypeScript) { // Build with Babel decorators stage 3 const {configPath} = require('../config/getUserBabelConfig.js') const command = `node ${babelBin} --source-maps --config-file ${configPath} --extensions .ts,.tsx src --out-dir ./dist` if (opts.verbose) console.log(`=====> Running \`${command}\`.\n`) promises.push(exec(command)) } // Build with TypeScript (or only emit declaration files if building with Babel) const tsCliOptions = cli.rawArgs.join(' ').split(' -- ')[1] // TODO not sure if the --emitDeclarationOnly flag is in the correct place when tsProjectReferenceMode is enabled. const command = `node ${tscBin} ${tsProjectReferenceMode ? '--build --incremental' : '-p'} ./tsconfig.json ${ useBabelForTypeScript ? '--emitDeclarationOnly' : '' } ${tsCliOptions ?? ''} ${noFail ? '|| echo ""' : ''}` if (opts.verbose) console.log(`=====> Running \`${command}\`.\n`) promises.push(exec(command)) await Promise.all(promises) const end = performance.now() if (opts.verbose) console.log(`===> Done running the "buildTs" command. (${Math.round(end - start)}ms) \n`) return true } // TODO Project Reference mode for watch mode? exports.watchTs = watchTs async function watchTs() { if (opts.verbose) console.log(`===> Running the "watchTs" command.\n`) try { // TODO If there's no user tsconfig fall back to cli's tsconfig. We // might need to temporarily write it to the project root. await fs.promises.access(path.resolve(process.cwd(), 'tsconfig.json')) } catch (e) { console.log('No tsconfig file found. Skipping TypeScript watch mode.') if (opts.verbose) console.log(`===> Done running the "watchTs" command.\n`) // Don't try to run TypeScript build if no tsconfig.json file is present. return false } // TODO: watch both TS and Babel at the same time. if (useBabelForTypeScript) { // Build with Babel decorators stage 3 const {configPath} = require('../config/getUserBabelConfig.js') const command = `node ${babelBin} --source-maps --watch --config-file ${configPath} --extensions .ts,.tsx src --out-dir ./dist` if (opts.verbose) console.log(`=====> Running \`${command}\`.\n`) await exec(command) } // TODO does tsProjectReferenceMode for watch mode too? const tsCliOptions = cli.rawArgs.join(' ').split(' -- ')[1] const command = `node ${tscBin} ${tsProjectReferenceMode ? '--build --incremental' : '-p'} ./tsconfig.json ${ useBabelForTypeScript ? '--emitDeclarationOnly' : '' } --watch ${tsCliOptions ?? ''}` if (opts.verbose) console.log(`=====> Running \`${command}\`.\n`) await exec(command) if (opts.verbose) console.log(`===> Done running the "watchTs" command.\n`) } // TODO Project Reference mode while type checking? exports.typecheck = typecheck async function typecheck() { const tsCliOptions = cli.rawArgs.join(' ').split(' -- ')[1] // TODO tsProjectReferenceMode for typecheck too? const command = `node ${tscBin} -p ./tsconfig.json --noEmit ${tsCliOptions ?? ''}` if (opts.verbose) { console.log(`===> Running the "typecheck" command.\n`) console.log(`=====> Running \`${command}\`\n`) } await exec(command) if (opts.verbose) console.log(`===> Done running the "typecheck" command.\n`) } // TODO Project Reference mode while type checking in watch mode? exports.typecheckWatch = typecheckWatch async function typecheckWatch() { const tsCliOptions = cli.rawArgs.join(' ').split(' -- ')[1] // TODO tsProjectReferenceMode for typecheck watch mode too? const command = `node ${tscBin} -p ./tsconfig.json --noEmit --watch ${tsCliOptions ?? ''}` if (opts.verbose) { console.log(`===> Running the "typecheckWatch" command.\n`) console.log(`=====> Running \`${command}\`\n`) } await exec(command) if (opts.verbose) console.log(`===> Done running the "typecheckWatch" command.\n`) } exports.installBrowsers = installBrowsers async function installBrowsers() { exec(`node ${playwrightBin} install`) } exports.test = test async function test(subOpts) { const watch = subOpts.watch ?? false if (opts.verbose) console.log(`===> Running the "test" command.\n`) const webTestRunnerCommand = `node ${path.resolve(__dirname, '..', 'scripts', 'run-web-test-runner.js')}` await exec(webTestRunnerCommand, {env: {WATCH_TESTS: String(watch)}}) if (opts.verbose) console.log(`===> Done running the "test" command.\n`) } exports.testDebug = testDebug async function testDebug() { console.error('The `lume testDebug` command has been removed. Instead use `lume test --watch`.') process.exit(1) } exports.releasePre = releasePre async function releasePre() { if (opts.verbose) console.log(`===> Running the "releasePre" command.\n`) const command = path.resolve(__dirname, '..', 'scripts', 'release-pre.sh') if (opts.verbose) console.log(`=====> Running \`${command}\`.\n`) await exec(command) if (opts.verbose) console.log(`===> Done running the "releasePre" command.\n`) } exports.releasePatch = releasePatch async function releasePatch() { if (opts.verbose) console.log(`===> Running the "releasePatch" command.\n`) await releasePre() await exec('npm version --no-workspaces patch -m v%s') if (opts.verbose) console.log(`===> Done running the "releasePatch" command.\n`) } exports.releaseMinor = releaseMinor async function releaseMinor() { if (opts.verbose) console.log(`===> Running the "releaseMinor" command.\n`) await releasePre() await exec('npm version --no-workspaces minor -m v%s') if (opts.verbose) console.log(`===> Done running the "releaseMinor" command.\n`) } exports.releaseMajor = releaseMajor async function releaseMajor() { if (opts.verbose) console.log(`===> Running the "releaseMajor" command.\n`) await releasePre() await exec('npm version --no-workspaces major -m v%s') if (opts.verbose) console.log(`===> Done running the "releaseMajor" command.\n`) } exports.releaseAlphaMajor = releaseAlphaMajor async function releaseAlphaMajor() { if (opts.verbose) console.log(`===> Running the "releaseAlphaMajor" command.\n`) await releasePre() await exec('npm version --no-workspaces premajor --preid alpha -m v%s') if (opts.verbose) console.log(`===> Done running the "releaseAlphaMajor" command.\n`) } exports.releaseAlphaMinor = releaseAlphaMinor async function releaseAlphaMinor() { if (opts.verbose) console.log(`===> Running the "releaseAlphaMinor" command.\n`) await releasePre() await exec('npm version --no-workspaces preminor --preid alpha -m v%s') if (opts.verbose) console.log(`===> Done running the "releaseAlphaMinor" command.\n`) } exports.releaseAlphaPatch = releaseAlphaPatch async function releaseAlphaPatch() { if (opts.verbose) console.log(`===> Running the "releaseAlphaPatch" command.\n`) await releasePre() await exec('npm version --no-workspaces prepatch --preid alpha -m v%s') if (opts.verbose) console.log(`===> Done running the "releaseAlphaPatch" command.\n`) } exports.releaseBetaMajor = releaseBetaMajor async function releaseBetaMajor() { if (opts.verbose) console.log(`===> Running the "releaseBetaMajor" command.\n`) await releasePre() await exec('npm version --no-workspaces premajor --preid beta -m v%s') if (opts.verbose) console.log(`===> Done running the "releaseBetaMajor" command.\n`) } exports.releaseBetaMinor = releaseBetaMinor async function releaseBetaMinor() { if (opts.verbose) console.log(`===> Running the "releaseBetaMinor" command.\n`) await releasePre() await exec('npm version --no-workspaces preminor --preid beta -m v%s') if (opts.verbose) console.log(`===> Done running the "releaseBetaMinor" command.\n`) } exports.releaseBetaPatch = releaseBetaPatch async function releaseBetaPatch() { if (opts.verbose) console.log(`===> Running the "releaseBetaPatch" command.\n`) await releasePre() await exec('npm version --no-workspaces prepatch --preid beta -m v%s') if (opts.verbose) console.log(`===> Done running the "releaseBetaPatch" command.\n`) } exports.releaseAlpha = releaseAlpha async function releaseAlpha() { if (opts.verbose) console.log(`===> Running the "releaseAlpha" command.\n`) await releasePre() await exec('npm version --no-workspaces prerelease --preid alpha -m v%s') if (opts.verbose) console.log(`===> Done running the "releaseAlpha" command.\n`) } exports.releaseBeta = releaseBeta async function releaseBeta() { if (opts.verbose) console.log(`===> Running the "releaseBeta" command.\n`) await releasePre() await exec('npm version --no-workspaces prerelease --preid beta -m v%s') if (opts.verbose) console.log(`===> Done running the "releaseBeta" command.\n`) } exports.versionHook = versionHook async function versionHook() { if (opts.verbose) console.log(`===> Running the "versionHook" command.\n`) await exec('./node_modules/@lume/cli/scripts/version.sh') if (opts.verbose) console.log(`===> Done running the "versionHook" command.\n`) } exports.postVersionHook = postVersionHook async function postVersionHook() { if (opts.verbose) console.log(`===> Running the "postVersionHook" command.\n`) await exec('./node_modules/@lume/cli/scripts/postversion.sh') if (opts.verbose) console.log(`===> Done running the "postVersionHook" command.\n`) } const prettierConfig = '--config ' + (fs.existsSync('.prettierrc.js') ? '.prettierrc.js' : fs.existsSync('.prettierrc.cjs') ? '.prettierrc.cjs' : './node_modules/@lume/cli/.prettierrc.js') const prettierIgnore = '--ignore-path ' + (config.prettierIgnorePath ?? (fs.existsSync('.prettierignore') ? '.prettierignore' : './node_modules/@lume/cli/.prettierignore')) // Check formatting of all supported file types in the project. const prettierFiles = process.cwd() exports.prettier = prettier async function prettier() { if (opts.verbose) console.log(`===> Running the "prettier" command.\n`) const command = `node ${prettierBin} ${prettierConfig} ${prettierIgnore} --write ${prettierFiles}` if (opts.verbose) console.log(`=====> Running \`${command}\`\n`) await exec(command) if (opts.verbose) console.log(`===> Done running the "prettier" command.\n`) } exports.prettierCheck = prettierCheck async function prettierCheck() { if (opts.verbose) console.log(`===> Running the "prettierCheck" command.\n`) const command = `node ${prettierBin} ${prettierConfig} ${prettierIgnore} --check ${prettierFiles}` if (opts.verbose) console.log(`=====> Running \`${command}\`\n`) await exec(command) if (opts.verbose) console.log(`===> Done running the "prettierCheck" command.\n`) }