diff --git a/.gitignore b/.gitignore index c5408586e..2b41c0cf9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules *.log dist -.cache \ No newline at end of file +.cache +playground \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index dcc730e96..5646c54d5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -409,6 +409,16 @@ You can also minify the output, resulting into lower bundle sizes by using the ` tsup src/index.ts --minify ``` +To use [Terser](https://github.com/terser/terser) instead of esbuild for minification, pass terser as argument value + +```bash +tsup src/index.ts --minify terser +``` + +> NOTE: You must have terser installed. Install it with `npm install -D terser` + +In `tsup.config.js`, you can pass `terserOptions` which will be passed to `terser.minify` as it is. + ### Custom loader Esbuild loader list: diff --git a/package.json b/package.json index 0deff6308..d63742352 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "rollup-plugin-hashbang": "2.2.2", "strip-json-comments": "4.0.0", "svelte": "3.46.4", + "terser": "^5.16.0", "ts-essentials": "9.1.2", "tsconfig-paths": "3.12.0", "tsup": "6.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a86bb624..30e5e8a2c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,7 @@ specifiers: strip-json-comments: 4.0.0 sucrase: ^3.20.3 svelte: 3.46.4 + terser: ^5.16.0 tree-kill: ^1.2.2 ts-essentials: 9.1.2 tsconfig-paths: 3.12.0 @@ -77,11 +78,12 @@ devDependencies: rollup-plugin-hashbang: 2.2.2 strip-json-comments: 4.0.0 svelte: 3.46.4 + terser: 5.16.0 ts-essentials: 9.1.2_typescript@4.6.3 tsconfig-paths: 3.12.0 tsup: 6.4.0_ien5tfzdggmpmrmtxysw6xj5lu typescript: 4.6.3 - vitest: 0.21.1 + vitest: 0.21.1_terser@5.16.0 wait-for-expect: 3.0.2 packages: @@ -119,6 +121,43 @@ packages: requiresBuild: true optional: true + /@jridgewell/gen-mapping/0.3.2: + resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/trace-mapping': 0.3.17 + dev: true + + /@jridgewell/resolve-uri/3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array/1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/source-map/0.3.2: + resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==} + dependencies: + '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.17 + dev: true + + /@jridgewell/sourcemap-codec/1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + dev: true + + /@jridgewell/trace-mapping/0.3.17: + resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + /@nodelib/fs.scandir/2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -353,6 +392,12 @@ packages: resolution: {integrity: sha512-Ku5+GPFa12S3W26Uwtw+xyrtIpaZsGYHH6zxNbZlstmlvMYSZRzOwzwsXbxlVUbHyUucctSyuFtu6bNxwYomIw==} dev: true + /acorn/8.8.1: + resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /ansi-styles/3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -398,6 +443,10 @@ packages: dependencies: fill-range: 7.0.1 + /buffer-from/1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + /bundle-require/3.1.2_esbuild@0.15.1: resolution: {integrity: sha512-Of6l6JBAxiyQ5axFxUM6dYeP/W7X2Sozeo/4EYB9sJhL+dqL7TKjg+shwxp6jlu/6ZSERfsYtIpSJ1/x3XkAEA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -468,6 +517,10 @@ packages: resolution: {integrity: sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==} dev: true + /commander/2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + /commander/4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1438,6 +1491,18 @@ packages: engines: {node: '>=0.10.0'} dev: true + /source-map-support/0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map/0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + /source-map/0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} @@ -1492,6 +1557,17 @@ packages: engines: {node: '>= 8'} dev: true + /terser/5.16.0: + resolution: {integrity: sha512-KjTV81QKStSfwbNiwlBXfcgMcOloyuRdb62/iLFPGBcVNF4EXjhdYBhYHmbJpiBrVxZhDvltE11j+LBQUxEEJg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.2 + acorn: 8.8.1 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + /thenify-all/1.6.0: resolution: {integrity: sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=} engines: {node: '>=0.8'} @@ -1602,7 +1678,7 @@ packages: engines: {node: '>= 10.0.0'} dev: true - /vite/3.0.3: + /vite/3.0.3_terser@5.16.0: resolution: {integrity: sha512-sDIpIcl3mv1NUaSzZwiXGEy1ZoWwwC2vkxUHY6yiDacR6zf//ZFuBJrozO62gedpE43pmxnLATNR5IYUdAEkMQ==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -1625,11 +1701,12 @@ packages: postcss: 8.4.14 resolve: 1.22.1 rollup: 2.77.0 + terser: 5.16.0 optionalDependencies: fsevents: 2.3.2 dev: true - /vitest/0.21.1: + /vitest/0.21.1_terser@5.16.0: resolution: {integrity: sha512-WBIxuFmIDPuK47GO6Lu9eNeRMqHj/FWL3dk73OHH3eyPPWPiu+UB3QHLkLK2PEggCqJW4FaWoWg8R68S7p9+9Q==} engines: {node: '>=v14.16.0'} hasBin: true @@ -1662,7 +1739,7 @@ packages: local-pkg: 0.4.2 tinypool: 0.2.4 tinyspy: 1.0.0 - vite: 3.0.3 + vite: 3.0.3_terser@5.16.0 transitivePeerDependencies: - less - sass diff --git a/src/cli-main.ts b/src/cli-main.ts index ad3284be6..ae6788433 100644 --- a/src/cli-main.ts +++ b/src/cli-main.ts @@ -20,7 +20,7 @@ export async function main(options: Options = {}) { .option('--format ', 'Bundle format, "cjs", "iife", "esm"', { default: 'cjs', }) - .option('--minify', 'Minify bundle') + .option('--minify [terser]', 'Minify bundle') .option('--minify-whitespace', 'Minify whitespace') .option('--minify-identifiers', 'Minify identifiers') .option('--minify-syntax', 'Minify syntax') diff --git a/src/esbuild/index.ts b/src/esbuild/index.ts index e0f14ea82..4513a7594 100644 --- a/src/esbuild/index.ts +++ b/src/esbuild/index.ts @@ -257,7 +257,7 @@ export async function runEsbuild( write: false, splitting, logLevel: 'error', - minify: options.minify, + minify: options.minify === 'terser' ? false : options.minify, minifyWhitespace: options.minifyWhitespace, minifyIdentifiers: options.minifyIdentifiers, minifySyntax: options.minifySyntax, diff --git a/src/index.ts b/src/index.ts index b549a5a92..64e062d20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import { es5 } from './plugins/es5' import { sizeReporter } from './plugins/size-reporter' import { treeShakingPlugin } from './plugins/tree-shaking' import { copyPublicDir, isInPublicDir } from './lib/public-dir' +import { terserPlugin } from './plugins/terser' export type { Format, Options, NormalizedOptions } @@ -260,6 +261,11 @@ export async function build(_options: Options) { cjsSplitting(), es5(), sizeReporter(), + terserPlugin({ + minifyOptions: options.minify, + format, + terserOptions: options.terserOptions, + }), ]) await runEsbuild(options, { pluginContainer, diff --git a/src/options.ts b/src/options.ts index d99a1bb17..b4c3ee2ae 100644 --- a/src/options.ts +++ b/src/options.ts @@ -3,6 +3,7 @@ import type { InputOption } from 'rollup' import { MarkRequired } from 'ts-essentials' import type { Plugin } from './plugin' import type { TreeshakingStrategy } from './plugins/tree-shaking' +import type { MinifyOptions } from 'terser' export type Format = 'cjs' | 'esm' | 'iife' @@ -65,7 +66,8 @@ export type Options = { * default to `node14` */ target?: string | string[] - minify?: boolean + minify?: boolean | 'terser' + terserOptions?: MinifyOptions minifyWhitespace?: boolean minifyIdentifiers?: boolean minifySyntax?: boolean diff --git a/src/plugins/terser.ts b/src/plugins/terser.ts new file mode 100644 index 000000000..78168ace5 --- /dev/null +++ b/src/plugins/terser.ts @@ -0,0 +1,67 @@ +import { MinifyOptions } from 'terser' +import { PrettyError } from '../errors' +import { createLogger } from '../log' +import { Format, Options } from '../options' +import { Plugin } from '../plugin' +import { localRequire } from '../utils' + +const logger = createLogger() + +export const terserPlugin = ({ + minifyOptions, + format, + terserOptions = {}, +}: { + minifyOptions: Options['minify'] + format: Format + terserOptions?: MinifyOptions +}): Plugin => { + return { + name: 'terser', + + async renderChunk(code, info) { + if (minifyOptions !== 'terser' || !/\.(cjs|js|mjs)$/.test(info.path)) + return + + const terser: typeof import('terser') | undefined = localRequire('terser') + + if (!terser) { + throw new PrettyError( + 'terser is required for terser minification. Please install it with `npm install terser -D`' + ) + } + + const { minify } = terser + + const defaultOptions: MinifyOptions = {} + + if (format === 'esm') { + defaultOptions.module = true + } else { + defaultOptions.toplevel = true + } + + try { + const minifiedOutput = await minify( + { [info.path]: code }, + { ...defaultOptions, ...terserOptions } + ) + + logger.info('TERSER', 'Minifying with Terser') + + if (!minifiedOutput.code) { + logger.error('TERSER', 'Failed to minify with terser') + } + + logger.success('TERSER', 'Terser Minification success') + + return { code: minifiedOutput.code!, map: minifiedOutput.map } + } catch (e) { + logger.error('TERSER', 'Failed to minify with terser') + logger.error('TERSER', e) + } + + return { code, map: info.map } + }, + } +}