From e2a456396e0d273c1260110e5ae7c19d61d3a36b Mon Sep 17 00:00:00 2001 From: Nad Alaba <37968805+nadalaba@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:51:26 +0300 Subject: [PATCH] fix!: support creating shims of shims fix pnpm/cmd-shim#10 --- README.md | 3 ++- src/index.ts | 16 +++++++++++++++- test/e2e.test.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ca45ea4..d3590c7 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,8 @@ The same as above, but will just continue if the file does not exist. - `opts.nodeExecPath` - _String_ - sets the path to the Node.js executable. - `opts.createCmdFile` - _Boolean_ - is `true` on Windows by default. If true, creates a cmd file. - `opts.createPwshFile` - _Boolean_ - is `true` by default. If true, creates a powershell file. -- `opts.progArgs` - String - optional arguments that will be prepend to any CLI arguments +- `opts.progArgs` - _String_ - optional arguments that will be prepended to any CLI arguments. +- `opts.stripShellExtensionFromShim` - _Boolean_ - is `true` by default. If true, strips common shell extensions from the name of the `to` argument before creating the shims. ```javascript const cmdShim = require('@zkochan/cmd-shim') diff --git a/src/index.ts b/src/index.ts index 92e2a59..ab77478 100644 --- a/src/index.ts +++ b/src/index.ts @@ -83,6 +83,13 @@ export interface Options { nodeExecPath?: string prependToPath?: string + + /** + * If common shell extensions should be stripped from target name. + * + * @default true + */ + stripShellExtensionFromShim?: boolean } /** @@ -119,6 +126,7 @@ const DEFAULT_OPTIONS = { // Create PowerShell file by default if the option hasn't been specified createPwshFile: true, createCmdFile: isWindows, + stripShellExtensionFromShim: true } /** * Map from extensions of files that this module is frequently used for to their runtime. @@ -152,7 +160,13 @@ function ingestOptions (opts?: Options): InternalOptions { */ export async function cmdShim (src: string, to: string, opts?: Options): Promise { const opts_ = ingestOptions(opts) - await cmdShim_(src, to, opts_) + // strip shell extensions that might cause confusion + // e.g., the sh shim of the powershell script.ps1 will be named script.ps1 + // but it will be an sh script with a ps1 extension causing confusion for powershell + const target = opts_.stripShellExtensionFromShim + ? to.replace(/\.(cmd|bat|ps1|sh)$/i, "") + : to + await cmdShim_(src, target, opts_) } /** diff --git a/test/e2e.test.js b/test/e2e.test.js index 514fdbb..38dbf68 100644 --- a/test/e2e.test.js +++ b/test/e2e.test.js @@ -3,6 +3,7 @@ import fs from 'node:fs' import path from 'node:path' import { describe, test, snapshot } from 'node:test' import assert from 'node:assert/strict' +import cmdExtension from 'cmd-extension' snapshot.setDefaultSnapshotSerializers([ (value) => typeof value === 'string' ? `\n${value.replaceAll('\r', '')}` : JSON.stringify(value), @@ -108,3 +109,32 @@ describeOnPosix('sh shim uses POSIX runtime from PATH', () => { assert.doesNotMatch(r.stderr, /node\.exe/) }) }) + +describe('create a shim of a pwsh shim with the same name in another location', () => { + // https://github.com/pnpm/cmd-shim/issues/10#issuecomment-4681988320 + // If the option `stripShellExtensionFromShim` is true, it should strip common shell + // extensions (e.g., `.ps1`) from the end of the `to` argument name. + // Otherwise, it should leave `to` name as it is. + test('generate shims with opts.stripShellExtensionFromShim true/false', async (t) => { + const tempDir = tempy.directory() + const srcDir = path.join(tempDir, 'src') + const toDir_strip = path.join(tempDir, 'to', 'strip') + const toDir_noSrip = path.join(tempDir, 'to', 'noStrip') + fs.mkdirSync(srcDir, { recursive: true }) + fs.mkdirSync(toDir_strip, { recursive: true }) + fs.mkdirSync(toDir_noSrip, { recursive: true }) + + const src = path.join(srcDir, 'script.ps1') + fs.writeFileSync(src, 'Write-Output "Hello from ps1 script!"\r\n', 'utf8') + + const to_strip = path.join(toDir_strip, 'script.ps1') + await cmdShim(src, to_strip, { createCmdFile: true, stripShellExtensionFromShim: true }) + const files_strip = fs.readdirSync(toDir_strip).sort() + assert.deepStrictEqual(files_strip, ['script', `script${cmdExtension}`, 'script.ps1']) + + const to_noStrip = path.join(toDir_noSrip, 'script.ps1') + await cmdShim(src, to_noStrip, { createCmdFile: true, stripShellExtensionFromShim: false }) + const files_noStrip = fs.readdirSync(toDir_noSrip).sort() + assert.deepStrictEqual(files_noStrip, ['script.ps1', `script.ps1${cmdExtension}`, 'script.ps1.ps1']) + }) +})