Commit 086b500

Anton Golub <antongolub@antongolub.com>
2024-05-04 14:51:17
feat: provide cjs pkg entrypoints (#793)
* feat: provide cjs pkg entrypoints relates #340 #399 #282 #197 * chore: linting * build: extract esbuild helpers * chore: up some dev deps * test: check licenses once * test: ignore esblib.cjs coverage * test: omit annotations coverage * style: linting
1 parent bc2a08c
.github/workflows/test.yml
@@ -78,3 +78,24 @@ jobs:
         timeout-minutes: 1
         env:
           FORCE_COLOR: 3
+
+  smoke-node:
+    runs-on: ubuntu-latest
+    needs: build
+    strategy:
+      matrix:
+        node-version: [12, 14, 16, 18, 20]
+    steps:
+      - uses: actions/checkout@v4
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v4
+        with:
+          node-version: ${{ matrix.node-version }}
+      - uses: actions/download-artifact@v4
+        with:
+          name: build
+      - name: cjs smoke test
+        run: npm run test:smoke:cjs
+      - name: mjs smoke test
+        if: matrix.node-version != '12'
+        run: npm run test:smoke:mjs
scripts/build-js.mjs
@@ -18,6 +18,9 @@ import path from 'node:path'
 import esbuild from 'esbuild'
 import { nodeExternalsPlugin } from 'esbuild-node-externals'
 import { entryChunksPlugin } from 'esbuild-plugin-entry-chunks'
+import { hybridExportPlugin } from 'esbuild-plugin-hybrid-export'
+import { transformHookPlugin } from 'esbuild-plugin-transform-hook'
+import { extractHelpersPlugin } from 'esbuild-plugin-extract-helpers'
 import minimist from 'minimist'
 import glob from 'fast-glob'
 
@@ -33,7 +36,7 @@ const argv = minimist(process.argv.slice(2), {
     target: 'node12',
     cwd: process.cwd(),
   },
-  boolean: ['minify', 'sourcemap', 'banner'],
+  boolean: ['minify', 'sourcemap', 'banner', 'hybrid'],
   string: ['entry', 'external', 'bundle', 'license', 'format', 'map', 'cwd'],
 })
 const {
@@ -44,6 +47,7 @@ const {
   sourcemap,
   license,
   format,
+  hybrid,
   cwd: _cwd,
 } = argv
 
@@ -54,9 +58,6 @@ const entryPoints = entry.includes('*')
   ? await glob(entries, { absolute: false, onlyFiles: true, cwd, root: cwd })
   : entries.map((p) => path.relative(cwd, path.resolve(cwd, p)))
 
-console.log('cwd=', cwd)
-console.log('entryPoints=', entryPoints)
-
 const _bundle = bundle !== 'none' && !process.argv.includes('--no-bundle')
 const _external = _bundle ? external.split(',') : undefined // https://github.com/evanw/esbuild/issues/1466
 
@@ -70,6 +71,50 @@ if (bundle === 'src') {
   plugins.push(nodeExternalsPlugin())
 }
 
+if (hybrid) {
+  plugins.push(
+    hybridExportPlugin({
+      loader: 'import',
+      to: 'build',
+      toExt: '.js',
+    })
+  )
+}
+
+plugins.push(
+  transformHookPlugin({
+    hooks: [
+      {
+        on: 'end',
+        pattern: new RegExp(
+          '(' +
+            entryPoints.map((e) => path.parse(e).name).join('|') +
+            ')\\.cjs$'
+        ),
+        transform(contents) {
+          return contents
+            .toString()
+            .replaceAll('"node:', '"')
+            .replaceAll(
+              'require("stream/promises")',
+              'require("stream").promises'
+            )
+            .replaceAll('require("fs/promises")', 'require("fs").promises')
+            .replaceAll('}).prototype', '}).prototype || {}')
+            .replace(
+              /\/\/ Annotate the CommonJS export names for ESM import in node:/,
+              ($0) => `/* c8 ignore next 100 */\n${$0}`
+            )
+        },
+      },
+    ],
+  }),
+  extractHelpersPlugin({
+    cwd: 'build',
+    include: /\.cjs/,
+  })
+)
+
 const formats = format.split(',')
 const banner =
   argv.banner && bundle === 'all'
@@ -95,7 +140,7 @@ const esmConfig = {
   target: 'esnext',
   format: 'esm',
   outExtension: {
-    // '.js': '.mjs'
+    '.js': '.mjs',
   },
   plugins,
   legalComments: license,
@@ -111,12 +156,18 @@ const cjsConfig = {
   format: 'cjs',
   banner: {},
   outExtension: {
-    // '.js': '.cjs'
+    '.js': '.cjs',
+  },
+  // https://github.com/evanw/esbuild/issues/1633
+  define: {
+    'import.meta.url': 'import_meta_url',
   },
+  inject: ['./scripts/import.meta.url-polyfill.js'],
 }
 
 for (const format of formats) {
   const config = format === 'cjs' ? cjsConfig : esmConfig
+  console.log('config=', config)
 
   await esbuild.build(config).catch(() => process.exit(1))
 }
scripts/import.meta.url-polyfill.js
@@ -0,0 +1,5 @@
+export const import_meta_url =
+  typeof document === 'undefined'
+    ? new (require('url'.replace('', '')).URL)('file:' + __filename).href
+    : (document.currentScript && document.currentScript.src) ||
+      new URL('main.js', document.baseURI).href
src/cli.ts
@@ -14,7 +14,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { createRequire } from 'node:module'
 import { basename, dirname, extname, join, resolve } from 'node:path'
 import url from 'node:url'
 import {
@@ -26,8 +25,9 @@ import {
   minimist,
   fs,
 } from './index.js'
-import { randomId } from './util.js'
 import { installDeps, parseDeps } from './deps.js'
+import { randomId } from './util.js'
+import { createRequire } from './vendor.js'
 
 function printUsage() {
   // language=txt
@@ -58,7 +58,7 @@ const argv = minimist(process.argv.slice(2), {
   stopEarly: true,
 })
 
-await (async function main() {
+;(async function main() {
   const globals = './globals.js'
   await import(globals)
   if (argv.verbose) $.verbose = true
src/vendor.ts
@@ -28,9 +28,19 @@ import {
 import * as yaml from 'yaml'
 import * as _fs from 'fs-extra'
 import type { fetch } from 'node-fetch-native'
+import { AbortController } from 'node-abort-controller'
 
 export { exec, buildCmd } from 'zurk/spawn'
 
+import _createRequire from 'create-require'
+import { URL } from 'url'
+
+export const createRequire = _createRequire as unknown as (
+  filename: string | URL
+) => NodeRequire
+
+global.AbortController = global.AbortController || AbortController
+
 export { fetch as nodeFetch } from 'node-fetch-native'
 export type RequestInfo = Parameters<typeof fetch>[0]
 export type RequestInit = Parameters<typeof fetch>[1]
test/fixtures/js-project/package-lock.json
@@ -23,26 +23,31 @@
         "@webpod/ps": "^0.0.0-beta.3",
         "c8": "^9.1.0",
         "chalk": "^5.3.0",
+        "create-require": "^1.1.1",
         "depseek": "^0.4.1",
-        "dts-bundle-generator": "^9.3.1",
+        "dts-bundle-generator": "^9.5.1",
         "esbuild": "^0.20.2",
         "esbuild-node-externals": "^1.13.0",
         "esbuild-plugin-entry-chunks": "^0.1.12",
+        "esbuild-plugin-extract-helpers": "^0.0.3",
+        "esbuild-plugin-hybrid-export": "^0.2.1",
+        "esbuild-plugin-transform-hook": "^0.0.1",
         "fs-extra": "^11.2.0",
         "fx": "*",
         "globby": "^14.0.1",
         "madge": "^6.1.0",
         "minimist": "^1.2.8",
+        "node-abort-controller": "^3.1.1",
         "node-fetch-native": "^1.6.4",
         "prettier": "^3.2.5",
         "tsd": "^0.31.0",
         "typescript": "^5.4.4",
         "which": "^4.0.0",
         "yaml": "^2.4.1",
-        "zurk": "^0.1.2"
+        "zurk": "^0.1.4"
       },
       "engines": {
-        "node": ">= 16.0.0"
+        "node": ">= 12.0.0"
       },
       "optionalDependencies": {
         "@types/fs-extra": "^11.0.4",
test/smoke/node.test.cjs
@@ -0,0 +1,31 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const assert = require('assert')
+require('zx/globals')
+;(async () => {
+  // smoke test
+  {
+    const p = await $`echo foo`
+    assert.match(p.stdout, /foo/)
+  }
+
+  // captures err stack
+  {
+    const p = await $({ nothrow: true })`echo foo; exit 3`
+    assert.match(p.message, /exit code: 3/)
+  }
+})()
+
+console.log('smoke cjs: ok')
test/smoke/node-esm.test.js → test/smoke/node.test.mjs
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import assert from 'assert'
-import '../../build/globals.js'
+import 'zx/globals'
 ;(async () => {
   // smoke test
   {
@@ -28,4 +28,4 @@ import '../../build/globals.js'
   }
 })()
 
-console.log('smoke: ok')
+console.log('smoke mjs: ok')
test/all.test.js
@@ -15,7 +15,6 @@
 import './cli.test.js'
 import './core.test.js'
 import './deps.test.js'
-import './extra.test.js'
 import './global.test.js'
 import './goods.test.js'
 import './index.test.js'
test/extra.test.js
@@ -15,11 +15,11 @@
 import assert from 'node:assert'
 import fs from 'node:fs/promises'
 import { test, describe } from 'node:test'
-import { globby } from 'globby'
+import { globby } from '../build/index.js'
 
 describe('extra', () => {
   test('every file should have a license', async () => {
-    const files = await globby(['**/*.{js,mjs,ts}'], {
+    const files = await globby(['**/*.{js,mjs,ts}', '!**/*-polyfill.js'], {
       gitignore: true,
       onlyFiles: true,
       cwd: process.cwd(),
package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "zx",
-  "version": "8.0.0",
+  "version": "8.0.2",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "zx",
-      "version": "8.0.0",
+      "version": "8.0.2",
       "license": "Apache-2.0",
       "bin": {
         "zx": "build/cli.js"
@@ -20,26 +20,31 @@
         "@webpod/ps": "^0.0.0-beta.3",
         "c8": "^9.1.0",
         "chalk": "^5.3.0",
+        "create-require": "^1.1.1",
         "depseek": "^0.4.1",
-        "dts-bundle-generator": "^9.3.1",
+        "dts-bundle-generator": "^9.5.1",
         "esbuild": "^0.20.2",
         "esbuild-node-externals": "^1.13.0",
         "esbuild-plugin-entry-chunks": "^0.1.12",
+        "esbuild-plugin-extract-helpers": "^0.0.3",
+        "esbuild-plugin-hybrid-export": "^0.2.1",
+        "esbuild-plugin-transform-hook": "^0.0.1",
         "fs-extra": "^11.2.0",
         "fx": "*",
         "globby": "^14.0.1",
         "madge": "^6.1.0",
         "minimist": "^1.2.8",
+        "node-abort-controller": "^3.1.1",
         "node-fetch-native": "^1.6.4",
         "prettier": "^3.2.5",
         "tsd": "^0.31.0",
         "typescript": "^5.4.4",
         "which": "^4.0.0",
         "yaml": "^2.4.1",
-        "zurk": "^0.1.2"
+        "zurk": "^0.1.4"
       },
       "engines": {
-        "node": ">= 16.0.0"
+        "node": ">= 12.0.0"
       },
       "optionalDependencies": {
         "@types/fs-extra": "^11.0.4",
@@ -835,6 +840,12 @@
         "safe-buffer": "~5.1.1"
       }
     },
+    "node_modules/create-require": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+      "dev": true
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -1199,9 +1210,9 @@
       }
     },
     "node_modules/dts-bundle-generator": {
-      "version": "9.3.1",
-      "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-9.3.1.tgz",
-      "integrity": "sha512-1/nMT7LFOkXbrL1ZvLpzrjNbfX090LZ64nLIXVmet557mshFCGP/oTiQiZenafJZ6GsmRQLTYKSlQnkxK8tsTw==",
+      "version": "9.5.1",
+      "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-9.5.1.tgz",
+      "integrity": "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA==",
       "dev": true,
       "dependencies": {
         "typescript": ">=5.0.2",
@@ -1302,6 +1313,36 @@
         "esbuild": ">=0.19.0"
       }
     },
+    "node_modules/esbuild-plugin-extract-helpers": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/esbuild-plugin-extract-helpers/-/esbuild-plugin-extract-helpers-0.0.3.tgz",
+      "integrity": "sha512-pD9p+WHsrN8iyRRFuI8on1grE0KQhiOmueYQV/PLHWzydY1fliNfydiRl5yemE+0S7keJ/ga2F9SCLUzS4H4tw==",
+      "dev": true,
+      "dependencies": {
+        "esbuild-plugin-transform-hook": "0.0.1"
+      },
+      "peerDependencies": {
+        "esbuild": ">=0.19.0"
+      }
+    },
+    "node_modules/esbuild-plugin-hybrid-export": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/esbuild-plugin-hybrid-export/-/esbuild-plugin-hybrid-export-0.2.1.tgz",
+      "integrity": "sha512-vdFLmsekj+lDihz7LAr61ApDsEQtVdUvwlGc+asnBlZbSG5DtCSlBkFR0FlJ+L4fjsQ62Supo3tAEd7K/oq48w==",
+      "dev": true,
+      "peerDependencies": {
+        "esbuild": ">=0.19.0"
+      }
+    },
+    "node_modules/esbuild-plugin-transform-hook": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/esbuild-plugin-transform-hook/-/esbuild-plugin-transform-hook-0.0.1.tgz",
+      "integrity": "sha512-OurhRFH6FYJcMKEw73/aAyOTSH+weEoHNuAuRnITR13u6BnSeXbuVdMS+Zuq1Mc/oGuk9hFIjm6MMsHNb+42kw==",
+      "dev": true,
+      "peerDependencies": {
+        "esbuild": ">=0.19.0"
+      }
+    },
     "node_modules/escalade": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@@ -2743,6 +2784,12 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
+    "node_modules/node-abort-controller": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
+      "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
+      "dev": true
+    },
     "node_modules/node-fetch-native": {
       "version": "1.6.4",
       "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz",
@@ -4384,9 +4431,9 @@
       }
     },
     "node_modules/zurk": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/zurk/-/zurk-0.1.2.tgz",
-      "integrity": "sha512-iulDU6SgS+YvvPqLaHrBzDMXxPj72JKCHzbx3oo+38zcyMcF/kwK4vClnHwRZ8UqtQB35Lj1cRXRqoXKvRUdqg==",
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/zurk/-/zurk-0.1.4.tgz",
+      "integrity": "sha512-sAUhRgP6vqTxVhcjSMlukAZ9l2TJSK3HWOnzAIUYInkuToS07LdAb878xtNc/Dgj7sve43chedsXc/k5ft/WyQ==",
       "dev": true
     }
   }
package.json
@@ -22,32 +22,55 @@
     }
   },
   "exports": {
-    ".": "./build/index.js",
-    "./globals": "./build/globals.js",
-    "./cli": "./build/cli.js",
-    "./core": "./build/core.js",
+    ".": {
+      "import": "./build/index.js",
+      "require": "./build/index.cjs",
+      "types": "./build/index.d.ts",
+      "default": "./build/index.js"
+    },
+    "./globals": {
+      "import": "./build/globals.js",
+      "require": "./build/globals.cjs",
+      "types": "./build/globals.d.ts",
+      "default": "./build/globals.js"
+    },
+    "./cli": {
+      "import": "./build/cli.js",
+      "require": "./build/cli.cjs",
+      "types": "./build/cli.d.ts",
+      "default": "./build/cli.js"
+    },
+    "./core": {
+      "import": "./build/core.js",
+      "require": "./build/core.cjs",
+      "types": "./build/core.d.ts",
+      "default": "./build/core.js"
+    },
     "./package.json": "./package.json"
   },
   "bin": {
     "zx": "./build/cli.js"
   },
   "engines": {
-    "node": ">= 16.0.0"
+    "node": ">= 12.0.0"
   },
   "scripts": {
     "fmt": "prettier --write .",
     "fmt:check": "prettier --check .",
     "build": "npm run build:js && npm run build:dts",
     "build:check": "tsc",
-    "build:js": "node scripts/build-js.mjs --format=esm --entry=src/*.ts && npm run build:vendor",
-    "build:vendor": "node scripts/build-js.mjs --format=esm --entry=src/vendor.ts --bundle=all --banner",
+    "build:js": "node scripts/build-js.mjs --format=cjs --hybrid --entry=src/*.ts && npm run build:vendor",
+    "build:vendor": "node scripts/build-js.mjs --format=cjs --entry=src/vendor.ts --bundle=all",
     "build:dts": "tsc --project tsconfig.prod.json && node scripts/build-dts.mjs",
-    "test": "npm run build && npm run test:unit && npm run test:types",
+    "test": "npm run build && npm run test:unit && npm run test:types && npm run test:license",
     "test:unit": "node ./test/all.test.js",
     "test:types": "tsd",
+    "test:license": "node ./test/extra.test.js",
     "test:smoke:bun": "bun test ./test/smoke/bun.test.js",
     "test:smoke:win32": "node ./test/smoke/win32.test.js",
-    "coverage": "c8 -x build/vendor.js -x 'test/**' -x scripts --check-coverage npm test",
+    "test:smoke:cjs": "node ./test/smoke/node.test.cjs",
+    "test:smoke:mjs": "node ./test/smoke/node.test.mjs",
+    "coverage": "c8 -x build/vendor.cjs -x build/esblib.cjs -x 'test/**' -x scripts --check-coverage npm test",
     "circular": "madge --circular src/*",
     "version": "cat package.json | fx .version"
   },
@@ -60,27 +83,32 @@
     "@types/minimist": "^1.2.5",
     "@types/node": ">=20.11.30",
     "@types/which": "^3.0.3",
-    "@webpod/ps": "^0.0.0-beta.3",
     "@webpod/ingrid": "^0.0.0-beta.3",
+    "@webpod/ps": "^0.0.0-beta.3",
     "c8": "^9.1.0",
     "chalk": "^5.3.0",
+    "create-require": "^1.1.1",
     "depseek": "^0.4.1",
-    "dts-bundle-generator": "^9.3.1",
+    "dts-bundle-generator": "^9.5.1",
     "esbuild": "^0.20.2",
     "esbuild-node-externals": "^1.13.0",
     "esbuild-plugin-entry-chunks": "^0.1.12",
+    "esbuild-plugin-hybrid-export": "^0.2.1",
+    "esbuild-plugin-transform-hook": "^0.0.1",
+    "esbuild-plugin-extract-helpers": "^0.0.3",
     "fs-extra": "^11.2.0",
     "fx": "*",
     "globby": "^14.0.1",
     "madge": "^6.1.0",
     "minimist": "^1.2.8",
+    "node-abort-controller": "^3.1.1",
     "node-fetch-native": "^1.6.4",
     "prettier": "^3.2.5",
     "tsd": "^0.31.0",
     "typescript": "^5.4.4",
     "which": "^4.0.0",
     "yaml": "^2.4.1",
-    "zurk": "^0.1.2"
+    "zurk": "^0.1.4"
   },
   "publishConfig": {
     "registry": "https://wombat-dressing-room.appspot.com"