refactor: move esbuild related code to its own file

This commit is contained in:
EGOIST 2021-11-18 23:38:12 +08:00
parent 2ebd70bf48
commit 981a575a38
5 changed files with 320 additions and 263 deletions

55
package-lock.json generated
View File

@ -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",

View File

@ -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"
},

257
src/esbuild/index.ts Normal file
View File

@ -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<string, string> }
) {
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'
)
}
}

View File

@ -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<string, string> }
): Promise<BuildResult | undefined> {
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

View File

@ -1 +0,0 @@
import './rollup'