diff --git a/package-lock.json b/package-lock.json index 543f882..96b8006 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "svelte": "3.37.0", "ts-essentials": "^7.0.1", "ts-jest": "^26.5.5", + "tsup": "^5.7.2", "typescript": "^4.2.4", "wait-for-expect": "^3.0.2" }, @@ -6733,6 +6734,39 @@ "node": ">=10" } }, + "node_modules/tsup": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-5.7.2.tgz", + "integrity": "sha512-YxryYjhB54h8YUNlN7Oze9PnqcuN++vwrFW6qtjC1ve9eQPvzCJw0rN0WPOuEQXburwo+gtDid9C4eQZ6x/Yog==", + "dev": true, + "dependencies": { + "cac": "^6.7.2", + "chalk": "^4.1.0", + "chokidar": "^3.5.1", + "debug": "^4.3.1", + "esbuild": "^0.13.4", + "execa": "^5.0.0", + "globby": "^11.0.3", + "joycon": "^3.0.1", + "postcss-load-config": "^3.0.1", + "resolve-from": "^5.0.0", + "rollup": "^2.56.1", + "sucrase": "^3.20.1", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "peerDependencies": { + "typescript": "^4.2.3" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -12380,6 +12414,27 @@ } } }, + "tsup": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-5.7.2.tgz", + "integrity": "sha512-YxryYjhB54h8YUNlN7Oze9PnqcuN++vwrFW6qtjC1ve9eQPvzCJw0rN0WPOuEQXburwo+gtDid9C4eQZ6x/Yog==", + "dev": true, + "requires": { + "cac": "^6.7.2", + "chalk": "^4.1.0", + "chokidar": "^3.5.1", + "debug": "^4.3.1", + "esbuild": "^0.13.4", + "execa": "^5.0.0", + "globby": "^11.0.3", + "joycon": "^3.0.1", + "postcss-load-config": "^3.0.1", + "resolve-from": "^5.0.0", + "rollup": "^2.56.1", + "sucrase": "^3.20.1", + "tree-kill": "^1.2.2" + } + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", diff --git a/package.json b/package.json index d03dbc4..019e4bc 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,11 @@ }, "scripts": { "dev": "npm run build:simple -- --watch", - "build": "npm run tsup -- src/cli-*.ts src/index.ts src/rollup.ts --clean --dts-resolve --splitting", + "build": "tsup src/cli-*.ts src/index.ts src/rollup.ts --clean --dts-resolve --splitting", "prepublishOnly": "npm run build", "test": "npm run build:simple && jest", "//": "Building without dts for speed", - "build:simple": "npm run tsup -- src/cli-*.ts src/index.ts src/rollup.ts --clean --splitting", - "tsup": "node -r sucrase/register src/cli-default.ts" + "build:simple": "tsup src/cli-*.ts src/index.ts src/rollup.ts --clean --splitting" }, "dependencies": { "cac": "^6.7.2", @@ -67,6 +66,7 @@ "svelte": "3.37.0", "ts-essentials": "^7.0.1", "ts-jest": "^26.5.5", + "tsup": "^5.7.2", "typescript": "^4.2.4", "wait-for-expect": "^3.0.2" }, diff --git a/src/esbuild/index.ts b/src/esbuild/index.ts new file mode 100644 index 0000000..c13447f --- /dev/null +++ b/src/esbuild/index.ts @@ -0,0 +1,257 @@ +import fs from 'fs' +import path from 'path' +import type { InputOption } from 'rollup' +import { transform as transformToEs5 } from 'buble' +import { + build as esbuild, + BuildOptions, + BuildResult, + Plugin as EsbuildPlugin, + formatMessages, +} from 'esbuild' +import { NormalizedOptions, Format } from '..' +import { getDeps, loadPkg } from '../load' +import { log } from '../log' +import { externalPlugin } from './external' +import { postcssPlugin } from './postcss' +import { sveltePlugin } from './svelte' +import consola from 'consola' +import { getBabel } from '../utils' +import { PrettyError } from '../errors' +import { transform } from 'sucrase' + +const getOutputExtensionMap = ( + pkgTypeField: string | undefined, + format: Format +) => { + const isModule = pkgTypeField === 'module' + const map: any = {} + if (isModule && format === 'cjs') { + map['.js'] = '.cjs' + } + if (!isModule && format === 'esm') { + map['.js'] = '.mjs' + } + if (format === 'iife') { + map['.js'] = '.global.js' + } + return map +} + +export async function runEsbuild( + options: NormalizedOptions, + { format, css }: { format: Format; css?: Map } +) { + const pkg = await loadPkg(process.cwd()) + const deps = await getDeps(process.cwd()) + const external = [ + // Exclude dependencies, e.g. `lodash`, `lodash/get` + ...deps.map((dep) => new RegExp(`^${dep}($|\\/|\\\\)`)), + ...(options.external || []), + ] + const outDir = options.outDir + + const outExtension = getOutputExtensionMap(pkg.type, format) + const env: { [k: string]: string } = { + ...options.env, + } + + if (options.replaceNodeEnv) { + env.NODE_ENV = + options.minify || options.minifyWhitespace ? 'production' : 'development' + } + + log(format, 'info', 'Build start') + + const startTime = Date.now() + + let result: BuildResult | undefined + + const splitting = + format === 'iife' + ? false + : typeof options.splitting === 'boolean' + ? options.splitting + : format === 'esm' + + const platform = options.platform || 'node' + + try { + result = await esbuild({ + entryPoints: options.entryPoints, + format: format === 'cjs' && splitting ? 'esm' : format, + bundle: typeof options.bundle === 'undefined' ? true : options.bundle, + platform, + globalName: options.globalName, + jsxFactory: options.jsxFactory, + jsxFragment: options.jsxFragment, + sourcemap: options.sourcemap, + target: options.target === 'es5' ? 'es2016' : options.target, + footer: options.footer, + banner: options.banner, + mainFields: + platform === 'node' + ? ['module', 'main'] + : ['browser', 'module', 'main'], + plugins: [ + { + name: 'modify-options', + setup(build) { + if (options.esbuildOptions) { + options.esbuildOptions(build.initialOptions, { format }) + } + }, + }, + // esbuild's `external` option doesn't support RegExp + // So here we use a custom plugin to implement it + externalPlugin({ + // everything should be bundled for iife format + disabled: format === 'iife', + patterns: external, + skipNodeModulesBundle: options.skipNodeModulesBundle, + }), + postcssPlugin({ css }), + sveltePlugin({ css }), + ...(options.esbuildPlugins || []), + ], + define: { + ...(format === 'cjs' + ? { + 'import.meta.url': 'importMetaUrlShim', + } + : {}), + ...options.define, + ...Object.keys(env).reduce((res, key) => { + const value = JSON.stringify(env[key]) + return { + ...res, + [`process.env.${key}`]: value, + [`import.meta.env.${key}`]: value, + } + }, {}), + }, + inject: [ + format === 'cjs' ? path.join(__dirname, '../assets/cjs_shims.js') : '', + ...(options.inject || []), + ].filter(Boolean), + outdir: + options.legacyOutput && format !== 'cjs' + ? path.join(outDir, format) + : outDir, + outExtension: options.legacyOutput ? undefined : outExtension, + write: false, + splitting, + logLevel: 'error', + minify: options.minify, + minifyWhitespace: options.minifyWhitespace, + minifyIdentifiers: options.minifyIdentifiers, + minifySyntax: options.minifySyntax, + keepNames: options.keepNames, + incremental: !!options.watch, + pure: typeof options.pure === 'string' ? [options.pure] : options.pure, + metafile: Boolean(options.metafile), + }) + } catch (error) { + log(format, 'error', 'Build failed') + throw error + } + + if (result && result.warnings) { + const messages = result.warnings.filter((warning) => { + if ( + warning.text.includes( + `This call to "require" will not be bundled because` + ) || + warning.text.includes(`Indirect calls to "require" will not be bundled`) + ) + return false + + return true + }) + const formatted = await formatMessages(messages, { + kind: 'warning', + color: true, + }) + formatted.forEach((message) => { + consola.warn(message) + }) + } + + // Manually write files + if (result && result.outputFiles) { + const timeInMs = Date.now() - startTime + log(format, 'success', `Build success in ${Math.floor(timeInMs)}ms`) + + await Promise.all( + result.outputFiles.map(async (file) => { + const dir = path.dirname(file.path) + const outPath = file.path + const ext = path.extname(outPath) + const comeFromSource = ext === '.js' || ext === outExtension['.js'] + await fs.promises.mkdir(dir, { recursive: true }) + let contents = file.text + let mode: number | undefined + if (contents[0] === '#' && contents[1] === '!') { + mode = 0o755 + } + if (comeFromSource) { + if (options.babel) { + const babel = getBabel() + if (babel) { + contents = await babel + .transformAsync(contents, { + filename: file.path, + }) + .then((res) => res?.code || contents) + } else { + throw new PrettyError( + `@babel/core is not found in ${process.cwd()}` + ) + } + } + if (options.target === 'es5') { + try { + contents = transformToEs5(contents, { + source: file.path, + file: file.path, + transforms: { + modules: false, + arrow: true, + dangerousTaggedTemplateString: true, + spreadRest: true, + }, + }).code + } catch (error: any) { + throw new PrettyError( + `Error compiling to es5 target:\n${error.snippet}` + ) + } + } + // Workaround to enable code splitting for cjs format + // Manually transform esm to cjs + // TODO: remove this once esbuild supports code splitting for cjs natively + if (splitting && format === 'cjs') { + contents = transform(contents, { + filePath: file.path, + transforms: ['imports'], + }).code + } + } + await fs.promises.writeFile(outPath, contents, { + encoding: 'utf8', + mode, + }) + }) + ) + } + + if (options.metafile && result?.metafile) { + const outPath = path.resolve(outDir, `metafile-${format}.json`) + await fs.promises.mkdir(path.dirname(outPath), { recursive: true }) + await fs.promises.writeFile( + outPath, + JSON.stringify(result.metafile), + 'utf8' + ) + } +} diff --git a/src/index.ts b/src/index.ts index b1fb493..14258e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,33 +1,20 @@ +import path from 'path' import fs from 'fs' -import path, { dirname, join, extname } from 'path' import { Worker } from 'worker_threads' -import type { InputOption } from 'rollup' -import { transform as transformToEs5 } from 'buble' -import { - build as esbuild, - BuildOptions, - BuildResult, - Plugin as EsbuildPlugin, - formatMessages, -} from 'esbuild' import type { MarkRequired, Buildable } from 'ts-essentials' -import { getBabel, removeFiles, debouncePromise } from './utils' -import { getDeps, loadTsConfig, loadPkg, loadTsupConfig } from './load' +import { removeFiles, debouncePromise } from './utils' +import { loadTsConfig, loadTsupConfig } from './load' import glob from 'globby' import { handleError, PrettyError } from './errors' -import { postcssPlugin } from './esbuild/postcss' -import { externalPlugin } from './esbuild/external' -import { sveltePlugin } from './esbuild/svelte' import resolveFrom from 'resolve-from' import { parseArgsStringToArgv } from 'string-argv' import type { ChildProcess } from 'child_process' import execa from 'execa' -import consola from 'consola' import kill from 'tree-kill' -import { transform } from 'sucrase' import { version } from '../package.json' import { log, setSilent } from './log' import { Format, Options } from './options' +import { runEsbuild } from './esbuild' export type { Format, Options } @@ -36,24 +23,6 @@ export type NormalizedOptions = MarkRequired< 'entryPoints' | 'format' | 'outDir' > -const getOutputExtensionMap = ( - pkgTypeField: string | undefined, - format: Format -) => { - const isModule = pkgTypeField === 'module' - const map: any = {} - if (isModule && format === 'cjs') { - map['.js'] = '.cjs' - } - if (!isModule && format === 'esm') { - map['.js'] = '.mjs' - } - if (format === 'iife') { - map['.js'] = '.global.js' - } - return map -} - export const defineConfig = ( options: | Options @@ -63,226 +32,6 @@ export const defineConfig = ( ) => Options) ) => options -export async function runEsbuild( - options: NormalizedOptions, - { format, css }: { format: Format; css?: Map } -): Promise { - const pkg = await loadPkg(process.cwd()) - const deps = await getDeps(process.cwd()) - const external = [ - // Exclude dependencies, e.g. `lodash`, `lodash/get` - ...deps.map((dep) => new RegExp(`^${dep}($|\\/|\\\\)`)), - ...(options.external || []), - ] - const outDir = options.outDir - - const outExtension = getOutputExtensionMap(pkg.type, format) - const env: { [k: string]: string } = { - ...options.env, - } - - if (options.replaceNodeEnv) { - env.NODE_ENV = - options.minify || options.minifyWhitespace ? 'production' : 'development' - } - - log(format, 'info', 'Build start') - - const startTime = Date.now() - - let result: BuildResult | undefined - - const splitting = - format === 'iife' - ? false - : typeof options.splitting === 'boolean' - ? options.splitting - : format === 'esm' - - const platform = options.platform || 'node' - - try { - result = await esbuild({ - entryPoints: options.entryPoints, - format: format === 'cjs' && splitting ? 'esm' : format, - bundle: typeof options.bundle === 'undefined' ? true : options.bundle, - platform, - globalName: options.globalName, - jsxFactory: options.jsxFactory, - jsxFragment: options.jsxFragment, - sourcemap: options.sourcemap, - target: options.target === 'es5' ? 'es2016' : options.target, - footer: options.footer, - banner: options.banner, - mainFields: - platform === 'node' - ? ['module', 'main'] - : ['browser', 'module', 'main'], - plugins: [ - { - name: 'modify-options', - setup(build) { - if (options.esbuildOptions) { - options.esbuildOptions(build.initialOptions, { format }) - } - }, - }, - // esbuild's `external` option doesn't support RegExp - // So here we use a custom plugin to implement it - externalPlugin({ - // everything should be bundled for iife format - disabled: format === 'iife', - patterns: external, - skipNodeModulesBundle: options.skipNodeModulesBundle, - }), - postcssPlugin({ css }), - sveltePlugin({ css }), - ...(options.esbuildPlugins || []), - ], - define: { - ...(format === 'cjs' - ? { - 'import.meta.url': 'importMetaUrlShim', - } - : {}), - ...options.define, - ...Object.keys(env).reduce((res, key) => { - const value = JSON.stringify(env[key]) - return { - ...res, - [`process.env.${key}`]: value, - [`import.meta.env.${key}`]: value, - } - }, {}), - }, - inject: [ - format === 'cjs' ? join(__dirname, '../assets/cjs_shims.js') : '', - ...(options.inject || []), - ].filter(Boolean), - outdir: - options.legacyOutput && format !== 'cjs' - ? join(outDir, format) - : outDir, - outExtension: options.legacyOutput ? undefined : outExtension, - write: false, - splitting, - logLevel: 'error', - minify: options.minify, - minifyWhitespace: options.minifyWhitespace, - minifyIdentifiers: options.minifyIdentifiers, - minifySyntax: options.minifySyntax, - keepNames: options.keepNames, - incremental: !!options.watch, - pure: typeof options.pure === 'string' ? [options.pure] : options.pure, - metafile: Boolean(options.metafile), - }) - } catch (error) { - log(format, 'error', 'Build failed') - throw error - } - - if (result && result.warnings) { - const messages = result.warnings.filter((warning) => { - if ( - warning.text.includes( - `This call to "require" will not be bundled because` - ) || - warning.text.includes(`Indirect calls to "require" will not be bundled`) - ) - return false - - return true - }) - const formatted = await formatMessages(messages, { - kind: 'warning', - color: true, - }) - formatted.forEach((message) => { - consola.warn(message) - }) - } - - // Manually write files - if (result && result.outputFiles) { - const timeInMs = Date.now() - startTime - log(format, 'success', `Build success in ${Math.floor(timeInMs)}ms`) - - await Promise.all( - result.outputFiles.map(async (file) => { - const dir = dirname(file.path) - const outPath = file.path - const ext = extname(outPath) - const comeFromSource = ext === '.js' || ext === outExtension['.js'] - await fs.promises.mkdir(dir, { recursive: true }) - let contents = file.text - let mode: number | undefined - if (contents[0] === '#' && contents[1] === '!') { - mode = 0o755 - } - if (comeFromSource) { - if (options.babel) { - const babel = getBabel() - if (babel) { - contents = await babel - .transformAsync(contents, { - filename: file.path, - }) - .then((res) => res?.code || contents) - } else { - throw new PrettyError( - `@babel/core is not found in ${process.cwd()}` - ) - } - } - if (options.target === 'es5') { - try { - contents = transformToEs5(contents, { - source: file.path, - file: file.path, - transforms: { - modules: false, - arrow: true, - dangerousTaggedTemplateString: true, - spreadRest: true, - }, - }).code - } catch (error: any) { - throw new PrettyError( - `Error compiling to es5 target:\n${error.snippet}` - ) - } - } - // Workaround to enable code splitting for cjs format - // Manually transform esm to cjs - // TODO: remove this once esbuild supports code splitting for cjs natively - if (splitting && format === 'cjs') { - contents = transform(contents, { - filePath: file.path, - transforms: ['imports'], - }).code - } - } - await fs.promises.writeFile(outPath, contents, { - encoding: 'utf8', - mode, - }) - }) - ) - } - - if (options.metafile && result?.metafile) { - const outPath = path.resolve(outDir, `metafile-${format}.json`) - await fs.promises.mkdir(path.dirname(outPath), { recursive: true }) - await fs.promises.writeFile( - outPath, - JSON.stringify(result.metafile), - 'utf8' - ) - } - - return result -} - const killProcess = ({ pid, signal = 'SIGTERM', @@ -484,10 +233,7 @@ export async function build(_options: Options) { throw new Error(`You need to install "typescript" in your project`) } - const isDev = __filename.endsWith('index.ts') - const worker = new Worker( - join(__dirname, isDev ? './rollup.dev.js' : './rollup.js') - ) + const worker = new Worker(path.join(__dirname, './rollup.js')) worker.postMessage({ options: { ...options, // functions cannot be cloned diff --git a/src/rollup.dev.js b/src/rollup.dev.js deleted file mode 100644 index d470259..0000000 --- a/src/rollup.dev.js +++ /dev/null @@ -1 +0,0 @@ -import './rollup'