Commit be0990f

Anton Golub <antongolub@antongolub.com>
2025-02-22 14:26:43
feat(cli): override non-js-like extensions via `--ext` (#1105)
* feat: introduce `--ext-override` option to process non-std (non js/ts like) file extensions resolves #1104 * refactor(cli): use `ext` option to override non-js-like script extensions
1 parent 82a5e0b
Changed files (5)
docs/cli.md
@@ -13,7 +13,16 @@ assumes that it is
 an [ESM](https://nodejs.org/api/modules.html#modules_module_createrequire_filename)
 module unless the `--ext` option is specified.
 
+## Non-standard extension
+`zx` internally loads scripts via `import` API, so you can use any extension supported by the runtime (nodejs, deno, bun) or apply a [custom loader](https://nodejs.org/api/cli.html#--experimental-loadermodule).
+However, if the script has a non-js-like extension (`/^\.[mc]?[jt]sx?$/`) and the `--ext` is specified, it will be used.
 
+```bash
+zx script.zx           # Unknown file extension "\.zx"
+zx --ext=mjs script.zx # OK
+```
+
+## Markdown
 ```bash
 zx docs/markdown.md
 ```
@@ -89,10 +98,10 @@ Enable verbose mode.
 
 ## `--shell`
 
-Specify a custom shell binary.
+Specify a custom shell binary path. By default, zx refers to `bash`.
 
 ```bash
-zx --shell=/bin/bash script.mjs
+zx --shell=/bin/another/sh script.mjs
 ```
 
 ## `--prefer-local, -l`
@@ -131,7 +140,7 @@ When `cwd` option is specified, it will be used as base path:
 
 ## `--ext`
 
-Override the default (temp) script extension. Default is `.mjs`.
+Overrides the default script extension (`.mjs`).
 
 ## `--version, -v`
 
man/zx.1
@@ -24,7 +24,7 @@ prefer locally installed packages bins
 .SS --eval=<js>, -e
 evaluate script
 .SS --ext=<.mjs>
-default extension
+script extension
 .SS --install, -i
 install dependencies
 .SS --registry=<URL>
src/cli.ts
@@ -35,6 +35,7 @@ import { randomId, bufToString } from './util.js'
 import { createRequire, type minimist } from './vendor.js'
 
 const EXT = '.mjs'
+const EXT_RE = /^\.[mc]?[jt]sx?$/
 
 isMain() &&
   main().catch((err) => {
@@ -64,7 +65,7 @@ export function printUsage() {
    --prefer-local, -l   prefer locally installed packages bins
    --cwd=<path>         set current directory
    --eval=<js>, -e      evaluate script
-   --ext=<.mjs>         default extension
+   --ext=<.mjs>         script extension
    --install, -i        install dependencies
    --registry=<URL>     npm registry, defaults to https://registry.npmjs.org/
    --version, -v        print current zx version
@@ -180,7 +181,7 @@ async function readScript() {
   }
 
   const { ext, base, dir } = path.parse(tempPath || scriptPath)
-  if (ext === '') {
+  if (ext === '' || (argv.ext && !EXT_RE.test(ext))) {
     tempPath = getFilepath(dir, base)
   }
   if (ext === '.md') {
test/fixtures/non-std-ext.zx
@@ -0,0 +1,19 @@
+#!/usr/bin/env zx
+
+// 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.
+
+console.log(chalk.yellowBright`If file has non-std ext and 'ext-override' option specified, zx assumes it's ESM.`)
+await $`pwd`
+console.log('__filename =', __filename)
test/cli.test.js
@@ -223,6 +223,17 @@ describe('cli', () => {
     )
   })
 
+  test('scripts with non standard extension', async () => {
+    const o =
+      await $`node build/cli.js --ext='.mjs' test/fixtures/non-std-ext.zx`
+    assert.ok(o.stdout.trim().endsWith('zx/test/fixtures/non-std-ext.zx.mjs'))
+
+    await assert.rejects(
+      $`node build/cli.js test/fixtures/non-std-ext.zx`,
+      /Unknown file extension "\.zx"/
+    )
+  })
+
   test22('scripts from stdin with explicit extension', async () => {
     const out =
       await $`node --experimental-strip-types build/cli.js --ext='.ts' <<< 'const foo: string = "bar"; console.log(foo)'`