main
1#!/usr/bin/env node
2
3// Copyright 2024 Google LLC
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// https://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17import path from 'node:path'
18import fs from 'node:fs'
19import esbuild from 'esbuild'
20import { injectCode, injectFile } from 'esbuild-plugin-utils'
21import { nodeExternalsPlugin } from 'esbuild-node-externals'
22import { entryChunksPlugin } from 'esbuild-plugin-entry-chunks'
23import { hybridExportPlugin } from 'esbuild-plugin-hybrid-export'
24import { transformHookPlugin } from 'esbuild-plugin-transform-hook'
25import { extractHelpersPlugin } from 'esbuild-plugin-extract-helpers'
26import esbuildResolvePlugin from 'esbuild-plugin-resolve'
27import minimist from 'minimist'
28import glob from 'fast-glob'
29
30const __dirname = path.dirname(new URL(import.meta.url).pathname)
31
32const argv = minimist(process.argv.slice(2), {
33 default: {
34 entry: './src/index.ts',
35 external: 'node:*',
36 bundle: 'src', // 'all' | 'none'
37 license: 'none', // see digestLicenses below // 'eof',
38 minify: false,
39 sourcemap: false,
40 format: 'cjs,esm',
41 target: 'node12',
42 cwd: process.cwd(),
43 },
44 boolean: ['minify', 'sourcemap', 'hybrid'],
45 string: ['entry', 'external', 'bundle', 'license', 'format', 'map', 'cwd'],
46})
47const {
48 entry,
49 external,
50 bundle,
51 minify,
52 sourcemap,
53 license,
54 format,
55 hybrid,
56 cwd: _cwd,
57} = argv
58
59const formats = format.split(',')
60const cwd = [_cwd].flat().pop()
61const entries = entry.split(/:\s?/)
62const entryPoints =
63 entry.includes('*') || entry.includes('{')
64 ? await glob(entries, { absolute: false, onlyFiles: true, cwd, root: cwd })
65 : entries.map((p) => path.relative(cwd, path.resolve(cwd, p)))
66const _bundle = bundle && bundle !== 'none'
67const _external = ['zx/globals', ...(_bundle ? external.split(',') : [])] // https://github.com/evanw/esbuild/issues/1466
68
69const plugins = [
70 esbuildResolvePlugin({
71 yaml: path.resolve(__dirname, '../node_modules/yaml/browser'),
72 }),
73]
74
75const thirdPartyModules = new Set()
76
77if (_bundle && entryPoints.length > 1) {
78 plugins.push(entryChunksPlugin())
79}
80
81if (bundle === 'src') {
82 // https://github.com/evanw/esbuild/issues/619
83 // https://github.com/pradel/esbuild-node-externals/pull/52
84 plugins.push(nodeExternalsPlugin())
85}
86
87if (hybrid) {
88 plugins.push(
89 hybridExportPlugin({
90 loader: 'reexport',
91 to: 'build',
92 toExt: '.js',
93 })
94 )
95}
96
97plugins.push(
98 {
99 name: 'get-3rd-party-modules',
100 setup: (build) => {
101 build.onResolve({ filter: /./, namespace: 'file' }, async (args) => {
102 thirdPartyModules.add(args.resolveDir)
103 })
104 },
105 },
106 transformHookPlugin({
107 hooks: [
108 {
109 on: 'end',
110 if: !hybrid,
111 pattern: /\.js$/,
112 transform(contents, file) {
113 const { name } = path.parse(file)
114 const _contents = contents
115 .toString()
116 .replace(
117 '} = __module__',
118 `} = globalThis.Deno ? globalThis.require("./${name}.cjs") : __module__`
119 )
120 return injectCode(_contents, `import "./deno.js"`)
121 },
122 },
123 {
124 on: 'end',
125 pattern: entryPointsToRegexp(entryPoints),
126 transform(contents) {
127 const extras = [
128 // https://github.com/evanw/esbuild/issues/1633
129 contents.includes('import_meta')
130 ? './scripts/import-meta-url.polyfill.js'
131 : '',
132
133 //https://github.com/evanw/esbuild/issues/1921
134 // p.includes('vendor') ? './scripts/require.polyfill.js' : '',
135 ].filter(Boolean)
136 return injectFile(contents, ...extras)
137 },
138 },
139 {
140 on: 'end',
141 pattern: entryPointsToRegexp(entryPoints),
142 transform(contents) {
143 return contents
144 .toString()
145 .replaceAll('import.meta.url', 'import_meta_url')
146 .replaceAll('import_meta.url', 'import_meta_url')
147 .replaceAll('"node:', '"')
148 .replaceAll(
149 'require("stream/promises")',
150 'require("stream").promises'
151 )
152 .replaceAll('require("fs/promises")', 'require("fs").promises')
153 .replaceAll('}).prototype', '}).prototype || {}')
154 .replace(/DISABLE_NODE_FETCH_NATIVE_WARN/, ($0) => `${$0} || true`)
155 .replace(
156 /\/\/ Annotate the CommonJS export names for ESM import in node:/,
157 ($0) => `/* c8 ignore next 100 */\n${$0}`
158 )
159 .replace(
160 'yield import("zx/globals")',
161 'yield require("./globals.cjs")'
162 )
163 .replace('require("./internals.ts")', 'require("./internals.cjs")')
164 },
165 },
166 ],
167 }),
168 extractHelpersPlugin({
169 cwd: 'build',
170 include: /\.cjs/,
171 }),
172 {
173 name: 'deno',
174 setup(build) {
175 build.onEnd(() => {
176 fs.copyFileSync('./scripts/deno.polyfill.js', './build/deno.js')
177 fs.writeFileSync(
178 './build/3rd-party-licenses',
179 digestLicenses(thirdPartyModules)
180 )
181 })
182 },
183 }
184)
185
186// prettier-ignore
187function digestLicenses(dirs) {
188 const digest = [...[...dirs]
189 .reduce((m, d) => {
190 const chunks = d.split('/')
191 const i = chunks.lastIndexOf('node_modules')
192 const name = chunks[i + 1]
193 const shift = i + 1 + (name.startsWith('@') ? 2 : 1)
194 const root = chunks.slice(0, shift).join('/')
195 m.add(root)
196 return m
197 }, new Set())]
198 .map(d => {
199 const extractName = (entry) => entry?.name ? `${entry.name} <${entry.email}>` : entry
200 const pkg = path.join(d, 'package.json')
201 const pkgJson = JSON.parse(fs.readFileSync(pkg, 'utf-8'))
202 const author = extractName(pkgJson.author)
203 const contributors = (pkgJson.contributors || pkgJson.maintainers || []).map(extractName).join(', ')
204 const by = author || contributors || '<unknown>'
205 const repository = pkgJson.repository?.url || pkgJson.repository || ''
206 const license = pkgJson.license || '<unknown>'
207
208 if (pkgJson.name === 'zx') return
209
210 return `${pkgJson.name}@${pkgJson.version}
211 ${by}
212 ${repository}
213 ${license}`
214 })
215 .filter(Boolean)
216 .sort()
217 .join('\n\n')
218
219 return `THIRD PARTY LICENSES
220
221${digest}
222`
223}
224
225function entryPointsToRegexp(entryPoints) {
226 return new RegExp(
227 '(' +
228 entryPoints.map((e) => escapeRegExp(path.parse(e).name)).join('|') +
229 ')\\.cjs$'
230 )
231}
232
233function escapeRegExp(str) {
234 return str.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
235}
236
237const esmConfig = {
238 absWorkingDir: cwd,
239 entryPoints,
240 outdir: './build',
241 bundle: _bundle,
242 external: _external,
243 minify,
244 sourcemap,
245 sourcesContent: false,
246 platform: 'node',
247 target: 'esnext',
248 format: 'esm',
249 outExtension: {
250 '.js': '.mjs',
251 },
252 plugins,
253 legalComments: license,
254 tsconfig: './tsconfig.json',
255}
256
257const cjsConfig = {
258 ...esmConfig,
259 outdir: './build',
260 target: 'es6',
261 format: 'cjs',
262 outExtension: {
263 '.js': '.cjs',
264 },
265}
266
267for (const format of formats) {
268 const config = format === 'cjs' ? cjsConfig : esmConfig
269 console.log('esbuild config:', config)
270
271 await esbuild.build(config).catch(() => process.exit(1))
272}
273
274process.exit(0)