From 0f9d9642f8de650fd4baa218713ab00eda37044a Mon Sep 17 00:00:00 2001 From: Daniel K Date: Tue, 18 Jun 2019 15:43:06 +0200 Subject: [PATCH] No env specific bundle for ESM (#142) * No env specific bundles (except for CJS) * CJS does have both builds * Revert readme * Enable terser toplevel for CJS * Output format condition * es -> esm * Use backticks to format entry file --- README.md | 12 +-- src/createRollupConfig.ts | 35 +++++--- src/index.ts | 108 +++++++++++------------ test/fixtures/build-default/package.json | 2 +- test/tests/tsdx-build.test.js | 8 +- 5 files changed, 86 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 400e10915..dfec5ae0d 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,8 @@ exports.sum = (s, t) => s + t; AS you can see, TSDX stripped out the development check from the production code. **This allows you to safely add development-only behavior (like more useful error messages) without any production bundle size impact.** +For ESM build, it's up to end-user to build environment specific build with NODE_ENV replace (done by Webpack 4 automatically). + #### Rollup Treeshaking TSDX's rollup config [removes getters and setters on objects](https://github.com/palmerhq/tsdx/blob/1f6a1b6819bb17678aa417f0df5349bec12f59ac/src/createRollupConfig.ts#L73) so that property access has no side effects. Don't do it. @@ -246,7 +248,7 @@ For brevity let's look at the ES module output. ```js import o from"lodash-es/kebabCase";const e=e=>{console.log(o(e))};export{e as KebabLogger}; -//# sourceMappingURL=test-react-tsdx.es.production.js.map +//# sourceMappingURL=test-react-tsdx.esm.production.js.map ``` TSDX will rewrite your `import kebabCase from 'lodash/kebabCase'` to `import o from 'lodash-es/kebabCase'`. This allows your library to be treeshakable to end consumers while allowing to you to use `@types/lodash` for free. @@ -279,14 +281,14 @@ Options -i, --entry Entry module(s) --target Specify your target environment (default web) --name Specify name exposed in UMD builds - --format Specify module format(s) (default cjs,es) + --format Specify module format(s) (default cjs,esm) -h, --help Displays this message Examples $ tsdx watch --entry src/foo.tsx $ tsdx watch --target node $ tsdx watch --name Foo - $ tsdx watch --format cjs,es,umd + $ tsdx watch --format cjs,esm,umd ``` ### `tsdx build` @@ -302,14 +304,14 @@ Options -i, --entry Entry module(s) --target Specify your target environment (default web) --name Specify name exposed in UMD builds - --format Specify module format(s) (default cjs,es) + --format Specify module format(s) (default cjs,esm) -h, --help Displays this message Examples $ tsdx build --entry src/foo.tsx $ tsdx build --target node $ tsdx build --name Foo - $ tsdx build --format cjs,es,umd + $ tsdx build --format cjs,esm,umd ``` ### `tsdx test` diff --git a/src/createRollupConfig.ts b/src/createRollupConfig.ts index 16e9f248f..df2dd7ee0 100644 --- a/src/createRollupConfig.ts +++ b/src/createRollupConfig.ts @@ -14,7 +14,7 @@ import shebangPlugin from '@jaredpalmer/rollup-plugin-preserve-shebang'; const replacements = [{ original: 'lodash', replacement: 'lodash-es' }]; const babelOptions = ( - format: 'cjs' | 'es' | 'umd', + format: 'cjs' | 'esm' | 'umd', target: 'node' | 'browser' ) => ({ exclude: 'node_modules/**', @@ -52,15 +52,29 @@ const babelOptions = ( // shebang cache map thing because the transform only gets run once let shebang: any = {}; export function createRollupConfig( - format: 'cjs' | 'umd' | 'es', - env: 'development' | 'production', + format: 'cjs' | 'umd' | 'esm', opts: { + env?: 'development' | 'production'; + minify?: boolean; input: string; name: string; target: 'node' | 'browser'; tsconfig?: string; } ) { + const shouldMinify = + opts.minify !== undefined ? opts.minify : opts.env === 'production'; + + const outputName = [ + `${paths.appDist}/${safePackageName(opts.name)}`, + format, + opts.env, + shouldMinify ? 'min' : '', + 'js', + ] + .filter(Boolean) + .join('.'); + return { // Tell Rollup the entry point to the package input: opts.input, @@ -74,9 +88,7 @@ export function createRollupConfig( // Establish Rollup output output: { // Set filenames of the consumer's package - file: `${paths.appDist}/${safePackageName( - opts.name - )}.${format}.${env}.js`, + file: outputName, // Pass through the file format format, // Do not let Rollup call Object.freeze() on namespace import objects @@ -161,14 +173,15 @@ export function createRollupConfig( }, }), babel(babelOptions(format, opts.target)), - replace({ - 'process.env.NODE_ENV': JSON.stringify(env), - }), + opts.env !== undefined && + replace({ + 'process.env.NODE_ENV': JSON.stringify(opts.env), + }), sourceMaps(), // sizeSnapshot({ // printInfo: false, // }), - env === 'production' && + shouldMinify && terser({ sourcemap: true, output: { comments: false }, @@ -178,7 +191,7 @@ export function createRollupConfig( passes: 10, }, ecma: 5, - toplevel: format === 'es' || format === 'cjs', + toplevel: format === 'cjs', warnings: true, }), ], diff --git a/src/index.ts b/src/index.ts index 8bfb66824..60cfc21c2 100755 --- a/src/index.ts +++ b/src/index.ts @@ -92,15 +92,15 @@ function createBuildConfigs( return concatAllArray( opts.input.map((input: string) => [ opts.format.includes('cjs') && - createRollupConfig('cjs', 'development', { ...opts, input }), + createRollupConfig('cjs', { env: 'development', ...opts, input }), opts.format.includes('cjs') && - createRollupConfig('cjs', 'production', { ...opts, input }), - opts.format.includes('es') && - createRollupConfig('es', 'production', { ...opts, input }), + createRollupConfig('cjs', { env: 'production', ...opts, input }), + opts.format.includes('esm') && + createRollupConfig('esm', { ...opts, input }), opts.format.includes('umd') && - createRollupConfig('umd', 'development', { ...opts, input }), + createRollupConfig('umd', { env: 'development', ...opts, input }), opts.format.includes('umd') && - createRollupConfig('umd', 'production', { ...opts, input }), + createRollupConfig('umd', { env: 'production', ...opts, input }), ]) ).filter(Boolean); } @@ -259,31 +259,16 @@ prog .example('watch --target node') .option('--name', 'Specify name exposed in UMD builds') .example('watch --name Foo') - .option('--format', 'Specify module format(s)', 'cjs,es,umd') - .example('watch --format cjs,es') + .option('--format', 'Specify module format(s)', 'cjs,esm') + .example('watch --format cjs,esm') .option('--tsconfig', 'Specify custom tsconfig path') .example('build --tsconfig ./tsconfig.foo.json') - .action(async (opts: any) => { - opts.name = opts.name || appPackageJson.name; - opts.input = await getInputs(opts.entry, appPackageJson.source); + .action(async (dirtyOpts: any) => { + const opts = await normalizeOpts(dirtyOpts); const buildConfigs = createBuildConfigs(opts); + await ensureDistFolder(); if (opts.format.includes('cjs')) { - await util.promisify(mkdirp)(resolveApp('dist')); - await fs.writeFile( - resolveApp('dist/index.js'), - ` - 'use strict' - - if (process.env.NODE_ENV === 'production') { - module.exports = require('./${safePackageName( - opts.name - )}.cjs.production.js') - } else { - module.exports = require('./${safePackageName( - opts.name - )}.cjs.development.js') - }` - ); + await writeCjsEntryFile(opts.name); } const spinner = ora().start(); await watch( @@ -329,40 +314,17 @@ prog .example('build --target node') .option('--name', 'Specify name exposed in UMD builds') .example('build --name Foo') - .option('--format', 'Specify module format(s)', 'cjs,es') - .example('build --format cjs,es') + .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') - .action(async (opts: any) => { - opts.name = opts.name || appPackageJson.name; - opts.input = await getInputs(opts.entry, appPackageJson.source); + .action(async (dirtyOpts: any) => { + const opts = await normalizeOpts(dirtyOpts); const buildConfigs = createBuildConfigs(opts); + await ensureDistFolder(); if (opts.format.includes('cjs')) { - try { - await util.promisify(mkdirp)(resolveApp('./dist')); - const promise = fs - .writeFile( - resolveApp('./dist/index.js'), - ` - 'use strict' - - if (process.env.NODE_ENV === 'production') { - module.exports = require('./${safePackageName( - opts.name - )}.cjs.production.js') - } else { - module.exports = require('./${safePackageName( - opts.name - )}.cjs.development.js') - }` - ) - .catch(e => { - throw e; - }); - logger(promise, 'Creating entry file'); - } catch (e) { - logError(e); - } + const promise = writeCjsEntryFile(opts.name).catch(logError); + logger(promise, 'Creating entry file'); } try { const promise = asyncro @@ -383,6 +345,38 @@ prog } }); +async function normalizeOpts(opts: any) { + return { + ...opts, + name: opts.name || appPackageJson.name, + input: await getInputs(opts.entry, appPackageJson.source), + format: opts.format.split(',').map((format: string) => { + if (format === 'es') { + return 'esm'; + } + return format; + }), + }; +} + +function ensureDistFolder() { + return util.promisify(mkdirp)(resolveApp('dist')); +} + +function writeCjsEntryFile(name: string) { + const baseLine = `module.exports = require('./${safePackageName(name)}`; + const contents = ` +'use strict' + +if (process.env.NODE_ENV === 'production') { + ${baseLine}.cjs.production.min.js') +} else { + ${baseLine}.cjs.development.js') +} +`; + return fs.writeFile(resolveApp(`./dist/index.js`), contents); +} + prog .command('test') .describe( diff --git a/test/fixtures/build-default/package.json b/test/fixtures/build-default/package.json index 45de7c92f..8d2bef063 100644 --- a/test/fixtures/build-default/package.json +++ b/test/fixtures/build-default/package.json @@ -4,4 +4,4 @@ }, "name": "build-default", "license": "MIT" -} \ No newline at end of file +} diff --git a/test/tests/tsdx-build.test.js b/test/tests/tsdx-build.test.js index 3f56da165..61d460f2a 100644 --- a/test/tests/tsdx-build.test.js +++ b/test/tests/tsdx-build.test.js @@ -18,18 +18,16 @@ describe('tsdx build', () => { it('should compile files into a dist directory', () => { util.setupStageWithFixture(stageName, 'build-default'); - const output = shell.exec('node ../dist/index.js build'); + const output = shell.exec('node ../dist/index.js build --format esm,cjs'); expect(shell.test('-f', 'dist/index.js')).toBeTruthy(); expect( shell.test('-f', 'dist/build-default.cjs.development.js') ).toBeTruthy(); expect( - shell.test('-f', 'dist/build-default.cjs.production.js') - ).toBeTruthy(); - expect( - shell.test('-f', 'dist/build-default.es.production.js') + shell.test('-f', 'dist/build-default.cjs.production.min.js') ).toBeTruthy(); + expect(shell.test('-f', 'dist/build-default.esm.js')).toBeTruthy(); expect(shell.test('-f', 'dist/index.d.ts')).toBeTruthy();