Commit c9d6511

Anton Golub <antongolub@antongolub.com>
2025-07-30 19:25:55
feat: expose internal deps versions (#1298)
* feat: expose internal deps version fix: export zx own version as static value fixes #1295 * refactor: move `versions` map to goods.ts * test: check npm artifact is buildable with esbuild iife * build: simplify `build:js` task params
1 parent 2a9e46f
build/goods.d.ts
@@ -4,6 +4,7 @@ import { type Mode } from 'node:fs';
 import { type ProcessPromise } from './core.js';
 import { type Duration } from './util.js';
 import { type RequestInfo, type RequestInit, minimist } from './vendor.js';
+export { versions } from './versions.js';
 export declare function tempdir(prefix?: string, mode?: Mode): string;
 export declare function tempfile(name?: string, data?: string | Buffer, mode?: Mode): string;
 export { tempdir as tmpdir, tempfile as tmpfile };
build/index.cjs
@@ -9,27 +9,21 @@ const {
   __forAwait
 } = require('./esblib.cjs');
 
-const import_meta_url =
-  typeof document === 'undefined'
-    ? new (require('url').URL)('file:' + __filename).href
-    : (document.currentScript && document.currentScript.src) ||
-      new URL('main.js', document.baseURI).href
-
 
 // src/index.ts
 var index_exports = {};
 __export(index_exports, {
   VERSION: () => VERSION,
-  YAML: () => import_vendor3.YAML,
+  YAML: () => import_vendor2.YAML,
   argv: () => argv,
-  dotenv: () => import_vendor3.dotenv,
+  dotenv: () => import_vendor2.dotenv,
   echo: () => echo,
   expBackoff: () => expBackoff,
   fetch: () => fetch,
-  fs: () => import_vendor3.fs,
-  glob: () => import_vendor3.glob,
-  globby: () => import_vendor3.glob,
-  minimist: () => import_vendor3.minimist,
+  fs: () => import_vendor2.fs,
+  glob: () => import_vendor2.glob,
+  globby: () => import_vendor2.glob,
+  minimist: () => import_vendor2.minimist,
   nothrow: () => nothrow,
   parseArgv: () => parseArgv,
   question: () => question,
@@ -43,11 +37,10 @@ __export(index_exports, {
   tmpdir: () => tempdir,
   tmpfile: () => tempfile,
   updateArgv: () => updateArgv,
-  version: () => version
+  version: () => version,
+  versions: () => versions
 });
 module.exports = __toCommonJS(index_exports);
-var import_vendor2 = require("./vendor.cjs");
-__reExport(index_exports, require("./core.cjs"), module.exports);
 
 // src/goods.ts
 var import_node_buffer = require("buffer");
@@ -57,6 +50,23 @@ var import_node_stream = require("stream");
 var import_core = require("./core.cjs");
 var import_util = require("./util.cjs");
 var import_vendor = require("./vendor.cjs");
+
+// src/versions.ts
+var versions = {
+  zx: "8.7.2",
+  chalk: "5.4.1",
+  depseek: "0.4.1",
+  dotenv: "0.2.3",
+  fetch: "1.6.6",
+  fs: "11.3.0",
+  glob: "14.1.0",
+  minimist: "1.2.8",
+  ps: "0.1.4",
+  which: "5.0.0",
+  yaml: "2.8.0"
+};
+
+// src/goods.ts
 function tempdir(prefix = `zx-${(0, import_util.randomId)()}`, mode) {
   const dirpath = import_core.path.join(import_core.os.tmpdir(), prefix);
   import_vendor.fs.mkdirSync(dirpath, { recursive: true, mode });
@@ -89,8 +99,8 @@ function sleep(duration) {
   });
 }
 var responseToReadable = (response, rs) => {
-  var _a2;
-  const reader = (_a2 = response.body) == null ? void 0 : _a2.getReader();
+  var _a;
+  const reader = (_a = response.body) == null ? void 0 : _a.getReader();
   if (!reader) {
     rs.push(null);
     return rs;
@@ -113,12 +123,12 @@ function fetch(url, init) {
       })(dest, ...args) : dest;
       p.then(
         (r) => {
-          var _a2;
-          return responseToReadable(r, rs).pipe((_a2 = _dest.run) == null ? void 0 : _a2.call(_dest));
+          var _a;
+          return responseToReadable(r, rs).pipe((_a = _dest.run) == null ? void 0 : _a.call(_dest));
         },
         (err) => {
-          var _a2;
-          return (_a2 = _dest.abort) == null ? void 0 : _a2.call(_dest, err);
+          var _a;
+          return (_a = _dest.abort) == null ? void 0 : _a.call(_dest, err);
         }
       );
       return _dest;
@@ -239,12 +249,9 @@ function spinner(title, callback) {
 }
 
 // src/index.ts
-var import_vendor3 = require("./vendor.cjs");
-var import_meta = {};
-var _a;
-var VERSION = ((_a = import_vendor2.fs.readJsonSync(new URL("../package.json", import_meta_url), {
-  throws: false
-})) == null ? void 0 : _a.version) || URL.parse(import_meta_url).pathname.split("/")[3];
+__reExport(index_exports, require("./core.cjs"), module.exports);
+var import_vendor2 = require("./vendor.cjs");
+var VERSION = versions.zx || "0.0.0";
 var version = VERSION;
 function nothrow(promise) {
   return promise.nothrow();
@@ -280,5 +287,6 @@ function quiet(promise) {
   tmpfile,
   updateArgv,
   version,
+  versions,
   ...require("./core.cjs")
 });
\ No newline at end of file
build/index.d.ts
@@ -1,7 +1,7 @@
 /// <reference types="node" />
 /// <reference types="fs-extra" />
 
-import { ProcessPromise } from './core.js';
+import { type ProcessPromise } from './core.js';
 export * from './core.js';
 export * from './goods.js';
 export { minimist, dotenv, fs, YAML, glob, glob as globby } from './vendor.js';
build/index.js
@@ -27,6 +27,7 @@ const {
   tmpfile,
   updateArgv,
   version,
+  versions,
   $,
   Fail,
   ProcessOutput,
@@ -75,6 +76,7 @@ export {
   tmpfile,
   updateArgv,
   version,
+  versions,
   $,
   Fail,
   ProcessOutput,
build/versions.d.ts
@@ -0,0 +1,1 @@
+export declare const versions: Record<string, string>;
docs/api.md
@@ -425,6 +425,16 @@ dotenv.config('.env')
 process.env.FOO // BAR
 ```
 
+## `versions`
+Exports versions of the zx dependencies.
+
+```ts
+import { versions } from 'zx'
+
+versions.zx     // 8.7.2
+versions.chalk  // 5.4.1
+```
+
 ## `quote()`
 
 Default bash quoting function.
scripts/build-js.mjs
@@ -59,10 +59,10 @@ const {
 const formats = format.split(',')
 const cwd = [_cwd].flat().pop()
 const entries = entry.split(/:\s?/)
-const entryPoints = entry.includes('*')
-  ? await glob(entries, { absolute: false, onlyFiles: true, cwd, root: cwd })
-  : entries.map((p) => path.relative(cwd, path.resolve(cwd, p)))
-
+const entryPoints =
+  entry.includes('*') || entry.includes('{')
+    ? await glob(entries, { absolute: false, onlyFiles: true, cwd, root: cwd })
+    : entries.map((p) => path.relative(cwd, path.resolve(cwd, p)))
 const _bundle = bundle && bundle !== 'none'
 const _external = ['zx/globals', ...(_bundle ? external.split(',') : [])] // https://github.com/evanw/esbuild/issues/1466
 
scripts/build-versions.mjs
@@ -0,0 +1,78 @@
+#!/usr/bin/env node
+
+// Copyright 2025 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.
+
+import fs from 'fs-extra'
+import path from 'node:path'
+import minimist from 'minimist'
+
+const root = path.resolve(new URL(import.meta.url).pathname, '../..')
+const copyright = await fs.readFileSync(
+  path.resolve(root, 'test/fixtures/copyright.txt'),
+  'utf8'
+)
+const deps = [
+  'chalk',
+  'depseek',
+  'dotenv',
+  'fetch',
+  'fs',
+  'glob',
+  'minimist',
+  'ps',
+  'which',
+  'yaml',
+]
+const namemap = {
+  dotenv: 'envapi',
+  fs: 'fs-extra',
+  fetch: 'node-fetch-native',
+  glob: 'globby',
+  ps: '@webpod/ps',
+}
+const versions = deps.reduce(
+  (m, name) => {
+    m[name] = fs.readJsonSync(
+      path.resolve(root, 'node_modules', namemap[name] || name, 'package.json')
+    ).version
+    return m
+  },
+  {
+    zx: fs.readJsonSync(path.join(root, 'package.json')).version,
+  }
+)
+
+const argv = minimist(process.argv.slice(2), {
+  default: versions,
+  string: ['zx', ...deps],
+})
+
+delete argv._
+
+const list = JSON.stringify(argv, null, 2)
+  .replaceAll('  "', '  ')
+  .replaceAll('": ', ': ')
+  .replaceAll('"', "'")
+  .replace(/\n}$/, ',\n}')
+
+const versionsTs = `${copyright.replace('YEAR', new Date().getFullYear())}
+export const versions: Record<string, string> = ${list}
+`
+const versionsCjs = `${copyright.replace('YEAR', new Date().getFullYear())}
+module.exports = { versions: ${list}
+`
+
+fs.writeFileSync(path.join(root, 'src/versions.ts'), versionsTs, 'utf8')
+// fs.writeFileSync(path.join(root, 'build/versions.cjs'), versionsCjs, 'utf8')
scripts/prepublish-lite.mjs
@@ -48,7 +48,9 @@ const pkgJson = {
     'build/core.js',
     'build/core.d.ts',
     'build/deno.js',
+    'build/error.d.ts',
     'build/esblib.cjs',
+    'build/log.d.ts',
     'build/util.cjs',
     'build/util.js',
     'build/util.d.ts',
src/goods.ts
@@ -44,6 +44,8 @@ import {
   fs,
 } from './vendor.ts'
 
+export { versions } from './versions.ts'
+
 export function tempdir(
   prefix: string = `zx-${randomId()}`,
   mode?: Mode
src/index.ts
@@ -12,18 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { ProcessPromise } from './core.ts'
-import { fs } from './vendor.ts'
+import { type ProcessPromise } from './core.ts'
+import { versions } from './goods.ts'
 
 export * from './core.ts'
 export * from './goods.ts'
 export { minimist, dotenv, fs, YAML, glob, glob as globby } from './vendor.ts'
 
-export const VERSION: string =
-  fs.readJsonSync(new URL('../package.json', import.meta.url), {
-    throws: false,
-  })?.version || URL.parse(import.meta.url)!.pathname.split('/')[3] // extracts version from JSR url
-
+export const VERSION: string = versions.zx || '0.0.0'
 export const version: string = VERSION
 
 /**
src/versions.ts
@@ -0,0 +1,27 @@
+// Copyright 2025 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.
+
+export const versions: Record<string, string> = {
+  zx: '8.7.2',
+  chalk: '5.4.1',
+  depseek: '0.4.1',
+  dotenv: '0.2.3',
+  fetch: '1.6.6',
+  fs: '11.3.0',
+  glob: '14.1.0',
+  minimist: '1.2.8',
+  ps: '0.1.4',
+  which: '5.0.0',
+  yaml: '2.8.0',
+}
test/it/build-npm.test.js
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { tempdir, $, path, fs } from '../../build/index.js'
+import { tempdir, $, path, fs, version } from '../../build/index.js'
 import assert from 'node:assert'
 import { describe, before, after, it } from 'node:test'
 
@@ -28,6 +28,7 @@ describe('npm artifact', () => {
     name: 'zx-test',
     dependencies: {
       typescript: '^5',
+      esbuild: '^0.25.8',
       '@types/node': '*',
       '@types/fs-extra': '*',
     },
@@ -50,17 +51,12 @@ describe('npm artifact', () => {
 
   after(() => fs.remove(tmp))
 
-  it('looks buildable with tsc', async () => {
-    const indexTs = `import {$} from 'zx'
-(async () => {
-  await $({verbose: true, stdio: 'pipe'})\`echo hello\`
-})()
-`
+  it('buildable with tsc', async () => {
     const tsconfig = {
       compilerOptions: {
         module: 'commonjs',
         target: 'esnext',
-        outDir: 'build-tsc',
+        outDir: 'bundle',
         rootDir: 'src',
         declaration: true,
         declarationMap: false,
@@ -68,12 +64,29 @@ describe('npm artifact', () => {
       },
       include: ['src'],
     }
-
+    const indexTs = `import {$} from 'zx'
+(async () => {
+  await $({verbose: true})\`echo hello\`
+})()
+`
     await fs.outputJSON(path.resolve(tmp, 'tsconfig.json'), tsconfig)
     await fs.outputFile(path.resolve(tmp, 'src/index.ts'), indexTs)
+
     await t$`tsc`
-    const result = await t$`node build-tsc/index.js`.text()
+    const out = await t$`node bundle/index.js`.text()
+    assert.strictEqual(out, '$ echo hello\nhello\n')
+  })
+
+  it('compilable with esbuild (iife)', async () => {
+    const verJs = `import {version, $} from 'zx'
+(async () => {
+  await $({verbose: true})\`echo \${version}\`
+})()
+`
+    await fs.outputFile(path.resolve(tmp, 'src/ver.js'), verJs)
 
-    assert.strictEqual(result, '$ echo hello\nhello\n')
+    await t$`npx esbuild src/ver.js --bundle --format=iife --platform=node > bundle/ver.js`
+    const out = await t$`node bundle/ver.js`.text()
+    assert.strictEqual(out, `$ echo ${version}\n${version}\n`)
   })
 })
test/export.test.js
@@ -472,6 +472,18 @@ describe('index', () => {
     assert.equal(typeof index.usePowerShell, 'function', 'index.usePowerShell')
     assert.equal(typeof index.usePwsh, 'function', 'index.usePwsh')
     assert.equal(typeof index.version, 'string', 'index.version')
+    assert.equal(typeof index.versions, 'object', 'index.versions')
+    assert.equal(typeof index.versions.chalk, 'string', 'index.versions.chalk')
+    assert.equal(typeof index.versions.depseek, 'string', 'index.versions.depseek')
+    assert.equal(typeof index.versions.dotenv, 'string', 'index.versions.dotenv')
+    assert.equal(typeof index.versions.fetch, 'string', 'index.versions.fetch')
+    assert.equal(typeof index.versions.fs, 'string', 'index.versions.fs')
+    assert.equal(typeof index.versions.glob, 'string', 'index.versions.glob')
+    assert.equal(typeof index.versions.minimist, 'string', 'index.versions.minimist')
+    assert.equal(typeof index.versions.ps, 'string', 'index.versions.ps')
+    assert.equal(typeof index.versions.which, 'string', 'index.versions.which')
+    assert.equal(typeof index.versions.yaml, 'string', 'index.versions.yaml')
+    assert.equal(typeof index.versions.zx, 'string', 'index.versions.zx')
     assert.equal(typeof index.which, 'function', 'index.which')
     assert.equal(typeof index.which.sync, 'function', 'index.which.sync')
     assert.equal(typeof index.within, 'function', 'index.within')
test/goods.test.ts
@@ -32,6 +32,7 @@ import {
   tempdir,
   tmpdir,
   tmpfile,
+  versions,
 } from '../src/goods.ts'
 import { Writable } from 'node:stream'
 import process from 'node:process'
@@ -464,4 +465,25 @@ ENV5=v5 # comment
       assert.equal(fs.readFileSync(tf, 'utf-8'), 'bar')
     })
   })
+
+  describe('versions', () => {
+    test('exports deps versions', () => {
+      assert.deepEqual(
+        Object.keys(versions).sort(),
+        [
+          'chalk',
+          'depseek',
+          'dotenv',
+          'fetch',
+          'fs',
+          'glob',
+          'minimist',
+          'ps',
+          'which',
+          'yaml',
+          'zx',
+        ].sort()
+      )
+    })
+  })
 })
test/index.test.js
@@ -17,6 +17,7 @@ import { describe, test } from 'node:test'
 import {
   nothrow,
   quiet,
+  versions,
   version,
   VERSION,
   $,
@@ -67,6 +68,7 @@ describe('index', () => {
     assert(nothrow)
     assert(quiet)
     assert(version)
+    assert(versions)
     assert.equal(version, VERSION)
 
     // core
test/package.test.js
@@ -66,6 +66,7 @@ describe('package', () => {
         'build/md.d.ts',
         'build/util.cjs',
         'build/util.d.ts',
+        'build/versions.d.ts',
         'build/vendor-core.cjs',
         'build/vendor-core.d.ts',
         'build/vendor-extra.cjs',
.size-limit.json
@@ -29,14 +29,14 @@
       "build/globals.js",
       "build/deno.js"
     ],
-    "limit": "813.75 kB",
+    "limit": "813.60 kB",
     "brotli": false,
     "gzip": false
   },
   {
     "name": "libdefs",
     "path": "build/*.d.ts",
-    "limit": "40.25 kB",
+    "limit": "40.35 kB",
     "brotli": false,
     "gzip": false
   },
@@ -62,7 +62,7 @@
       "README.md",
       "LICENSE"
     ],
-    "limit": "870.85 kB",
+    "limit": "870.80 kB",
     "brotli": false,
     "gzip": false
   }
package.json
@@ -70,9 +70,10 @@
     "fmt": "prettier --write .",
     "fmt:check": "prettier --check .",
     "prebuild": "rm -rf build",
-    "build": "npm run build:js && npm run build:dts && npm run build:tests",
-    "build:js": "node scripts/build-js.mjs --format=cjs --hybrid --entry=src/*.ts:!src/error.ts:!src/repl.ts:!src/md.ts:!src/log.ts:!src/globals-jsr.ts:!src/goods.ts && npm run build:vendor",
+    "build": "npm run build:versions && npm run build:js && npm run build:dts && npm run build:tests",
+    "build:js": "node scripts/build-js.mjs --format=cjs --hybrid --entry='src/{cli,core,deps,globals,index,internals,util,vendor*}.ts' && npm run build:vendor",
     "build:vendor": "node scripts/build-js.mjs --format=cjs --entry=src/vendor-*.ts --bundle=all --external='./internals.ts'",
+    "build:versions": "node scripts/build-versions.mjs",
     "build:tests": "node scripts/build-tests.mjs",
     "build:dts": "tsc --project tsconfig.json && rm build/repl.d.ts build/globals-jsr.d.ts && node scripts/build-dts.mjs",
     "build:dcr": "docker build -f ./dcr/Dockerfile . -t zx",
tsconfig.json
@@ -10,6 +10,7 @@
     "declaration": true,
     "emitDeclarationOnly": true,
     "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
     "types": ["node", "fs-extra"]
   },
   "include": ["./src/**/*"],