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)