UNPKG

tsdx

Version:

Zero-config TypeScript package development

463 lines (461 loc) 19.3 kB
#!/usr/bin/env node "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const sade_1 = tslib_1.__importDefault(require("sade")); const sync_1 = tslib_1.__importDefault(require("tiny-glob/sync")); const rollup_1 = require("rollup"); const asyncro_1 = tslib_1.__importDefault(require("asyncro")); const chalk_1 = tslib_1.__importDefault(require("chalk")); const fs = tslib_1.__importStar(require("fs-extra")); const jest = tslib_1.__importStar(require("jest")); const eslint_1 = require("eslint"); const logError_1 = tslib_1.__importDefault(require("./logError")); const path_1 = tslib_1.__importDefault(require("path")); const execa_1 = tslib_1.__importDefault(require("execa")); const shelljs_1 = tslib_1.__importDefault(require("shelljs")); const ora_1 = tslib_1.__importDefault(require("ora")); const semver_1 = tslib_1.__importDefault(require("semver")); const constants_1 = require("./constants"); const Messages = tslib_1.__importStar(require("./messages")); const createBuildConfigs_1 = require("./createBuildConfigs"); const createJestConfig_1 = require("./createJestConfig"); const createEslintConfig_1 = require("./createEslintConfig"); const utils_1 = require("./utils"); const jpjs_1 = require("jpjs"); const getInstallCmd_1 = tslib_1.__importDefault(require("./getInstallCmd")); const getInstallArgs_1 = tslib_1.__importDefault(require("./getInstallArgs")); const enquirer_1 = require("enquirer"); const createProgressEstimator_1 = require("./createProgressEstimator"); const templates_1 = require("./templates"); const utils_2 = require("./templates/utils"); const deprecated = tslib_1.__importStar(require("./deprecated")); const pkg = require('../package.json'); const prog = sade_1.default('tsdx'); let appPackageJson; try { appPackageJson = fs.readJSONSync(constants_1.paths.appPackageJson); } catch (e) { } exports.isDir = (name) => fs .stat(name) .then(stats => stats.isDirectory()) .catch(() => false); exports.isFile = (name) => fs .stat(name) .then(stats => stats.isFile()) .catch(() => false); async function jsOrTs(filename) { const extension = (await exports.isFile(utils_1.resolveApp(filename + '.ts'))) ? '.ts' : (await exports.isFile(utils_1.resolveApp(filename + '.tsx'))) ? '.tsx' : (await exports.isFile(utils_1.resolveApp(filename + '.jsx'))) ? '.jsx' : '.js'; return utils_1.resolveApp(`${filename}${extension}`); } async function getInputs(entries, source) { return jpjs_1.concatAllArray([] .concat(entries && entries.length ? entries : (source && utils_1.resolveApp(source)) || ((await exports.isDir(utils_1.resolveApp('src'))) && (await jsOrTs('src/index')))) .map(file => sync_1.default(file))); } prog .version(pkg.version) .command('create <pkg>') .describe('Create a new package with TSDX') .example('create mypackage') .option('--template', `Specify a template. Allowed choices: [${Object.keys(templates_1.templates).join(', ')}]`) .example('create --template react mypackage') .action(async (pkg, opts) => { console.log(chalk_1.default.blue(` ::::::::::: :::::::: ::::::::: ::: ::: :+: :+: :+: :+: :+: :+: :+: +:+ +:+ +:+ +:+ +:+ +:+ +#+ +#++:++#++ +#+ +:+ +#++:+ +#+ +#+ +#+ +#+ +#+ +#+ #+# #+# #+# #+# #+# #+# #+# ### ######## ######### ### ### `)); const bootSpinner = ora_1.default(`Creating ${chalk_1.default.bold.green(pkg)}...`); let template; // Helper fn to prompt the user for a different // folder name if one already exists async function getProjectPath(projectPath) { const exists = await fs.pathExists(projectPath); if (!exists) { return projectPath; } bootSpinner.fail(`Failed to create ${chalk_1.default.bold.red(pkg)}`); const prompt = new enquirer_1.Input({ message: `A folder named ${chalk_1.default.bold.red(pkg)} already exists! ${chalk_1.default.bold('Choose a different name')}`, initial: pkg + '-1', result: (v) => v.trim(), }); pkg = await prompt.run(); projectPath = (await fs.realpath(process.cwd())) + '/' + pkg; bootSpinner.start(`Creating ${chalk_1.default.bold.green(pkg)}...`); return await getProjectPath(projectPath); // recursion! } try { // get the project path const realPath = await fs.realpath(process.cwd()); let projectPath = await getProjectPath(realPath + '/' + pkg); const prompt = new enquirer_1.Select({ message: 'Choose a template', choices: Object.keys(templates_1.templates), }); if (opts.template) { template = opts.template.trim(); if (!prompt.choices.includes(template)) { bootSpinner.fail(`Invalid template ${chalk_1.default.bold.red(template)}`); template = await prompt.run(); } } else { template = await prompt.run(); } bootSpinner.start(); // copy the template await fs.copy(path_1.default.resolve(__dirname, `../templates/${template}`), projectPath, { overwrite: true, }); // fix gitignore await fs.move(path_1.default.resolve(projectPath, './gitignore'), path_1.default.resolve(projectPath, './.gitignore')); // update license year and author let license = await fs.readFile(path_1.default.resolve(projectPath, 'LICENSE'), { encoding: 'utf-8' }); license = license.replace(/<year>/, `${new Date().getFullYear()}`); // attempt to automatically derive author name let author = getAuthorName(); if (!author) { bootSpinner.stop(); const licenseInput = new enquirer_1.Input({ name: 'author', message: 'Who is the package author?', }); author = await licenseInput.run(); setAuthorName(author); bootSpinner.start(); } license = license.replace(/<author>/, author.trim()); await fs.writeFile(path_1.default.resolve(projectPath, 'LICENSE'), license, { encoding: 'utf-8', }); const templateConfig = templates_1.templates[template]; const generatePackageJson = utils_2.composePackageJson(templateConfig); // Install deps process.chdir(projectPath); const safeName = utils_1.safePackageName(pkg); const pkgJson = generatePackageJson({ name: safeName, author }); const nodeVersionReq = utils_1.getNodeEngineRequirement(pkgJson); if (nodeVersionReq && !semver_1.default.satisfies(process.version, nodeVersionReq)) { bootSpinner.fail(Messages.incorrectNodeVersion(nodeVersionReq)); process.exit(1); } await fs.outputJSON(path_1.default.resolve(projectPath, 'package.json'), pkgJson); bootSpinner.succeed(`Created ${chalk_1.default.bold.green(pkg)}`); await Messages.start(pkg); } catch (error) { bootSpinner.fail(`Failed to create ${chalk_1.default.bold.red(pkg)}`); logError_1.default(error); process.exit(1); } const templateConfig = templates_1.templates[template]; const { dependencies: deps } = templateConfig; const installSpinner = ora_1.default(Messages.installing(deps.sort())).start(); try { const cmd = await getInstallCmd_1.default(); await execa_1.default(cmd, getInstallArgs_1.default(cmd, deps)); installSpinner.succeed('Installed dependencies'); console.log(await Messages.start(pkg)); } catch (error) { installSpinner.fail('Failed to install dependencies'); logError_1.default(error); process.exit(1); } }); prog .command('watch') .describe('Rebuilds on any change') .option('--entry, -i', 'Entry module') .example('watch --entry src/foo.tsx') .option('--target', 'Specify your target environment', 'browser') .example('watch --target node') .option('--name', 'Specify name exposed in UMD builds') .example('watch --name Foo') .option('--format', 'Specify module format(s)', 'cjs,esm') .example('watch --format cjs,esm') .option('--verbose', 'Keep outdated console output in watch mode instead of clearing the screen') .example('watch --verbose') .option('--noClean', "Don't clean the dist folder") .example('watch --noClean') .option('--tsconfig', 'Specify custom tsconfig path') .example('watch --tsconfig ./tsconfig.foo.json') .option('--onFirstSuccess', 'Run a command on the first successful build') .example('watch --onFirstSuccess "echo The first successful build!"') .option('--onSuccess', 'Run a command on a successful build') .example('watch --onSuccess "echo Successful build!"') .option('--onFailure', 'Run a command on a failed build') .example('watch --onFailure "The build failed!"') .option('--transpileOnly', 'Skip type checking') .example('watch --transpileOnly') .option('--extractErrors', 'Extract invariant errors to ./errors/codes.json.') .example('watch --extractErrors') .action(async (dirtyOpts) => { const opts = await normalizeOpts(dirtyOpts); const buildConfigs = await createBuildConfigs_1.createBuildConfigs(opts); if (!opts.noClean) { await cleanDistFolder(); } if (opts.format.includes('cjs')) { await writeCjsEntryFile(opts.name); } let firstTime = true; let successKiller = null; let failureKiller = null; function run(command) { if (!command) { return null; } const [exec, ...args] = command.split(' '); return execa_1.default(exec, args, { stdio: 'inherit', }); } function killHooks() { return Promise.all([ successKiller ? successKiller.kill('SIGTERM') : null, failureKiller ? failureKiller.kill('SIGTERM') : null, ]); } const spinner = ora_1.default().start(); rollup_1.watch(buildConfigs.map(inputOptions => (Object.assign({ watch: { silent: true, include: ['src/**'], exclude: ['node_modules/**'], } }, inputOptions)))).on('event', async (event) => { // clear previous onSuccess/onFailure hook processes so they don't pile up await killHooks(); if (event.code === 'START') { if (!opts.verbose) { utils_1.clearConsole(); } spinner.start(chalk_1.default.bold.cyan('Compiling modules...')); } if (event.code === 'ERROR') { spinner.fail(chalk_1.default.bold.red('Failed to compile')); logError_1.default(event.error); failureKiller = run(opts.onFailure); } if (event.code === 'END') { spinner.succeed(chalk_1.default.bold.green('Compiled successfully')); console.log(` ${chalk_1.default.dim('Watching for changes')} `); try { await deprecated.moveTypes(); if (firstTime && opts.onFirstSuccess) { firstTime = false; run(opts.onFirstSuccess); } else { successKiller = run(opts.onSuccess); } } catch (_error) { } } }); }); prog .command('build') .describe('Build your project once and exit') .option('--entry, -i', 'Entry module') .example('build --entry src/foo.tsx') .option('--target', 'Specify your target environment', 'browser') .example('build --target node') .option('--name', 'Specify name exposed in UMD builds') .example('build --name Foo') .option('--format', 'Specify module format(s)', 'cjs,esm') .example('build --format cjs,esm') .option('--tsconfig', 'Specify custom tsconfig path') .example('build --tsconfig ./tsconfig.foo.json') .option('--transpileOnly', 'Skip type checking') .example('build --transpileOnly') .option('--extractErrors', 'Extract errors to ./errors/codes.json and provide a url for decoding.') .example('build --extractErrors=https://reactjs.org/docs/error-decoder.html?invariant=') .action(async (dirtyOpts) => { const opts = await normalizeOpts(dirtyOpts); const buildConfigs = await createBuildConfigs_1.createBuildConfigs(opts); await cleanDistFolder(); const logger = await createProgressEstimator_1.createProgressEstimator(); if (opts.format.includes('cjs')) { const promise = writeCjsEntryFile(opts.name).catch(logError_1.default); logger(promise, 'Creating entry file'); } try { const promise = asyncro_1.default .map(buildConfigs, async (inputOptions) => { let bundle = await rollup_1.rollup(inputOptions); await bundle.write(inputOptions.output); }) .catch((e) => { throw e; }) .then(async () => { await deprecated.moveTypes(); }); logger(promise, 'Building modules'); await promise; } catch (error) { logError_1.default(error); process.exit(1); } }); async function normalizeOpts(opts) { return Object.assign(Object.assign({}, opts), { name: opts.name || appPackageJson.name, input: await getInputs(opts.entry, appPackageJson.source), format: opts.format.split(',').map((format) => { if (format === 'es') { return 'esm'; } return format; }) }); } async function cleanDistFolder() { await fs.remove(constants_1.paths.appDist); } function writeCjsEntryFile(name) { const baseLine = `module.exports = require('./${utils_1.safePackageName(name)}`; const contents = ` 'use strict' if (process.env.NODE_ENV === 'production') { ${baseLine}.cjs.production.min.js') } else { ${baseLine}.cjs.development.js') } `; return fs.outputFile(path_1.default.join(constants_1.paths.appDist, 'index.js'), contents); } function getAuthorName() { let author = ''; author = shelljs_1.default .exec('npm config get init-author-name', { silent: true }) .stdout.trim(); if (author) return author; author = shelljs_1.default .exec('git config --global user.name', { silent: true }) .stdout.trim(); if (author) { setAuthorName(author); return author; } author = shelljs_1.default .exec('npm config get init-author-email', { silent: true }) .stdout.trim(); if (author) return author; author = shelljs_1.default .exec('git config --global user.email', { silent: true }) .stdout.trim(); if (author) return author; return author; } function setAuthorName(author) { shelljs_1.default.exec(`npm config set init-author-name "${author}"`, { silent: true }); } prog .command('test') .describe('Run jest test runner. Passes through all flags directly to Jest') .action(async (opts) => { // Do this as the first thing so that any code reading it knows the right env. process.env.BABEL_ENV = 'test'; process.env.NODE_ENV = 'test'; // Makes the script crash on unhandled rejections instead of silently // ignoring them. In the future, promise rejections that are not handled will // terminate the Node.js process with a non-zero exit code. process.on('unhandledRejection', err => { throw err; }); const argv = process.argv.slice(2); let jestConfig = Object.assign(Object.assign({}, createJestConfig_1.createJestConfig(relativePath => path_1.default.resolve(__dirname, '..', relativePath), opts.config ? path_1.default.dirname(opts.config) : constants_1.paths.appRoot)), appPackageJson.jest); // Allow overriding with jest.config const defaultPathExists = await fs.pathExists(constants_1.paths.jestConfig); if (opts.config || defaultPathExists) { const jestConfigPath = utils_1.resolveApp(opts.config || constants_1.paths.jestConfig); const jestConfigContents = require(jestConfigPath); jestConfig = Object.assign(Object.assign({}, jestConfig), jestConfigContents); } // if custom path, delete the arg as it's already been merged if (opts.config) { let configIndex = argv.indexOf('--config'); if (configIndex !== -1) { // case of "--config path", delete both args argv.splice(configIndex, 2); } else { // case of "--config=path", only one arg to delete const configRegex = /--config=.+/; configIndex = argv.findIndex(arg => arg.match(configRegex)); if (configIndex !== -1) { argv.splice(configIndex, 1); } } } argv.push('--config', JSON.stringify(Object.assign({}, jestConfig))); const [, ...argsToPassToJestCli] = argv; jest.run(argsToPassToJestCli); }); prog .command('lint') .describe('Run eslint with Prettier') .example('lint src test') .option('--fix', 'Fixes fixable errors and warnings') .example('lint src test --fix') .option('--ignore-pattern', 'Ignore a pattern') .example('lint src test --ignore-pattern test/foobar.ts') .option('--max-warnings', 'Exits with non-zero error code if number of warnings exceed this number', Infinity) .example('lint src test --max-warnings 10') .option('--write-file', 'Write the config file locally') .example('lint --write-file') .option('--report-file', 'Write JSON report to file locally') .example('lint --report-file eslint-report.json') .action(async (opts) => { if (opts['_'].length === 0 && !opts['write-file']) { const defaultInputs = ['src', 'test'].filter(fs.existsSync); opts['_'] = defaultInputs; console.log(chalk_1.default.yellow(`Defaulting to "tsdx lint ${defaultInputs.join(' ')}"`, '\nYou can override this in the package.json scripts, like "lint": "tsdx lint src otherDir"')); } const config = await createEslintConfig_1.createEslintConfig({ pkg: appPackageJson, rootDir: constants_1.paths.appRoot, writeFile: opts['write-file'], }); const cli = new eslint_1.CLIEngine({ baseConfig: Object.assign(Object.assign({}, config), appPackageJson.eslint), extensions: ['.ts', '.tsx', '.js', '.jsx'], fix: opts.fix, ignorePattern: opts['ignore-pattern'], }); const report = cli.executeOnFiles(opts['_']); if (opts.fix) { eslint_1.CLIEngine.outputFixes(report); } console.log(cli.getFormatter()(report.results)); if (opts['report-file']) { await fs.outputFile(opts['report-file'], cli.getFormatter('json')(report.results)); } if (report.errorCount) { process.exit(1); } if (report.warningCount > opts['max-warnings']) { process.exit(1); } }); prog.parse(process.argv);