diff --git a/.changeset/cmd-shim-monorepo.md b/.changeset/cmd-shim-monorepo.md new file mode 100644 index 0000000000..5f60719b59 --- /dev/null +++ b/.changeset/cmd-shim-monorepo.md @@ -0,0 +1,7 @@ +--- +"@pnpm/bins.cmd-shim": patch +"@pnpm/bins.linker": patch +"pnpm": patch +--- + +The `@zkochan/cmd-shim` package is now available as `@pnpm/bins.cmd-shim`. diff --git a/.github/scripts/run-ts-tests-chunk.mjs b/.github/scripts/run-ts-tests-chunk.mjs index 5fa1d0fcf0..13046f27b3 100644 --- a/.github/scripts/run-ts-tests-chunk.mjs +++ b/.github/scripts/run-ts-tests-chunk.mjs @@ -178,10 +178,13 @@ async function runScriptTask (pkg) { try { if (pkg.manifest.name === 'pd') { await runCommand('node', ['pd.js', '--version'], { cwd: pkg.path }) + } else if (pkg.manifest.name === '@pnpm/bins.cmd-shim') { + await runCommand('node', ['--test', 'test/test.js', 'test/e2e.test.js'], { cwd: pkg.path }) } else { throw new Error(`Unsupported non-Jest .test script in ${relDir}: ${pkg.manifest.scripts['.test']}`) } } catch (err) { + console.error(err) status = 'failure' exitCode = err.exitCode ?? 1 } diff --git a/.meta-updater/src/index.ts b/.meta-updater/src/index.ts index 4404af7f6b..33bac985c9 100644 --- a/.meta-updater/src/index.ts +++ b/.meta-updater/src/index.ts @@ -370,6 +370,7 @@ async function updateManifest (workspaceDir: string, manifest: ProjectManifest, let scripts: Record let preset = '@pnpm/jest-config' switch (manifest.name) { + case '@pnpm/bins.cmd-shim': case '@pnpm/lockfile.types': scripts = { ...manifest.scripts } break @@ -486,7 +487,7 @@ async function updateManifest (workspaceDir: string, manifest: ProjectManifest, repository = `https://github.com/pnpm/pnpm/tree/main/${relative}` } if (scripts.lint) { - if (fs.existsSync(path.join(dir, 'test'))) { + if (fs.existsSync(path.join(dir, 'test')) && manifest.name !== '@pnpm/bins.cmd-shim') { scripts.lint = 'eslint "src/**/*.ts" "test/**/*.ts"' } else { scripts.lint = 'eslint "src/**/*.ts"' @@ -513,7 +514,7 @@ async function updateManifest (workspaceDir: string, manifest: ProjectManifest, } delete manifest.dependencies['@types/ramda'] } - if (scripts.test) { + if (scripts.test && manifest.name !== '@pnpm/bins.cmd-shim') { Object.assign(manifest, { jest: { ...(manifest as any).jest, // eslint-disable-line @@ -531,7 +532,7 @@ async function updateManifest (workspaceDir: string, manifest: ProjectManifest, files, funding: 'https://opencollective.com/pnpm', homepage, - license: 'MIT', + license: manifest.license ?? 'MIT', repository, scripts, exports: { diff --git a/cspell.json b/cspell.json index e72ecb41d0..540076611c 100644 --- a/cspell.json +++ b/cspell.json @@ -70,6 +70,7 @@ "cves", "cwsay", "cyclonedx", + "cygdrive", "deburr", "dedup", "denoland", @@ -184,6 +185,7 @@ "killcb", "kochan", "koorchik", + "LASTEXITCODE", "ldid", "ldni", "ledgered", @@ -227,6 +229,7 @@ "msgpackr", "msvc", "msys", + "MSYSTEM", "mtimes", "musleabihf", "mycomp", @@ -241,6 +244,7 @@ "newversion", "nistp", "NOASSERTION", + "nodepath", "nodetouch", "noent", "nonexec", @@ -278,6 +282,7 @@ "parseable", "partialmatch", "pathext", + "pathsep", "pegjs", "pids", "pidtree", @@ -293,7 +298,7 @@ "pnpmtest", "pnpr", "polyfilling", - "portugu\u00eas", + "português", "posix", "postbuild", "postfoo", @@ -383,6 +388,7 @@ "semistrict", "serp", "serverjs", + "SETLOCAL", "shasums", "sheetjs", "shlex", @@ -481,6 +487,7 @@ "worktree", "worktrees", "wrappy", + "wslpath", "xattr", "xattrs", "xmarw", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aea25b7547..1bc0ef1798 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -383,9 +383,6 @@ catalogs: '@yarnpkg/pnp': specifier: ^4.1.7 version: 4.1.7 - '@zkochan/cmd-shim': - specifier: ^9.0.8 - version: 9.0.8 '@zkochan/retry': specifier: ^0.2.0 version: 0.2.0 @@ -617,6 +614,9 @@ catalogs: mdast-util-to-string: specifier: ^4.0.0 version: 4.0.0 + memfs: + specifier: ^3.5.3 + version: 3.5.3 memoize: specifier: ^11.0.0 version: 11.0.0 @@ -1473,11 +1473,36 @@ importers: specifier: workspace:* version: link:../../core/logger + pnpm11/bins/cmd-shim: + dependencies: + '@pnpm/fs.graceful-fs': + specifier: workspace:* + version: link:../../fs/graceful-fs + cmd-extension: + specifier: 'catalog:' + version: 2.0.0 + devDependencies: + '@pnpm/bins.cmd-shim': + specifier: workspace:* + version: 'link:' + '@types/node': + specifier: 'catalog:' + version: 22.19.19 + memfs: + specifier: 'catalog:' + version: 3.5.3 + tempy: + specifier: 'catalog:' + version: 3.0.0 + pnpm11/bins/linker: dependencies: '@pnpm/bins.resolver': specifier: workspace:* version: link:../resolver + '@pnpm/bins.cmd-shim': + specifier: workspace:* + version: link:../cmd-shim '@pnpm/error': specifier: workspace:* version: link:../../core/error @@ -1496,9 +1521,6 @@ importers: '@pnpm/workspace.project-manifest-reader': specifier: workspace:* version: link:../../workspace/project-manifest-reader - '@zkochan/cmd-shim': - specifier: 'catalog:' - version: 9.0.8 '@zkochan/rimraf': specifier: 'catalog:' version: 4.0.0 @@ -8232,15 +8254,15 @@ importers: '@jest/globals': specifier: 'catalog:' version: 30.4.1(supports-color@10.2.2) + '@pnpm/bins.cmd-shim': + specifier: workspace:* + version: link:../../../bins/cmd-shim '@pnpm/exe': specifier: workspace:* version: 'link:' '@pnpm/jest-config': specifier: workspace:* version: link:../../../__utils__/jest-config - '@zkochan/cmd-shim': - specifier: 'catalog:' - version: 9.0.8 execa: specifier: 'catalog:' version: safe-execa@0.3.0 @@ -12928,10 +12950,6 @@ packages: resolution: {integrity: sha512-E5mgrRS8Kk80n19Xxmrx5qO9UG03FyZd8Me5gxYi++VPZsOv8+OsclA+0Fth4KTDCrQ/FkJryNFKJ6/642lo4g==} engines: {node: '>=18.12'} - '@zkochan/cmd-shim@9.0.8': - resolution: {integrity: sha512-PfbYtsZwUMs7YleBJ1XD+BaKQ99MGHxAw0XZCDiUId9Yok4zNg60N3hCALnh8KsjBI/ocMstjDDTFThNweIJAw==} - engines: {node: '>=22.13'} - '@zkochan/diable@1.0.2': resolution: {integrity: sha512-LvXkwkWyrsRulnXVfp0BfuEQqV6I2j0l3kQwvBHKFMI6Sg5j2GrUCLsEKqYB3jlxM+0ofScvlE4vB6knevdBmg==} @@ -14297,6 +14315,9 @@ packages: resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -15399,6 +15420,10 @@ packages: resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} engines: {node: '>=10'} + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + memoize@11.0.0: resolution: {integrity: sha512-cjsfZaC9b1clqPeIVMbb5dLHSXgdgGWGxdAU3oTUUkHiwWTKTBNnSmcqWJncNjYtBi3S8Rp0c5GIiyGztR8TRA==} engines: {node: '>=22'} @@ -20609,11 +20634,6 @@ snapshots: graceful-fs: 4.2.11(patch_hash=68ebc232025360cb3dcd3081f4067f4e9fc022ab6b6f71a3230e86c7a5b337d1) is-windows: 1.0.2 - '@zkochan/cmd-shim@9.0.8': - dependencies: - cmd-extension: 1.0.2 - graceful-fs: 4.2.11(patch_hash=68ebc232025360cb3dcd3081f4067f4e9fc022ab6b6f71a3230e86c7a5b337d1) - '@zkochan/diable@1.0.2': dependencies: spawno: 2.1.2 @@ -22168,6 +22188,8 @@ snapshots: dependencies: minipass: 7.1.3 + fs-monkey@1.1.0: {} + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -23535,6 +23557,10 @@ snapshots: map-age-cleaner: 0.1.3 mimic-fn: 3.1.0 + memfs@3.5.3: + dependencies: + fs-monkey: 1.1.0 + memoize@11.0.0: dependencies: mimic-function: 5.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1dd97d8460..7b640b4117 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -225,7 +225,6 @@ catalog: '@yarnpkg/lockfile': ^1.1.0 '@yarnpkg/nm': 4.1.1 '@yarnpkg/pnp': ^4.1.7 - '@zkochan/cmd-shim': ^9.0.8 '@zkochan/retry': ^0.2.0 '@zkochan/rimraf': ^4.0.0 '@zkochan/table': ^2.0.1 @@ -318,6 +317,7 @@ catalog: lru-cache: ^11.5.2 make-empty-dir: ^4.0.0 mdast-util-to-string: ^4.0.0 + memfs: ^3.5.3 memoize: ^11.0.0 micromatch: ^4.0.8 msgpackr: 2.1.0 diff --git a/pnpm11/bins/cmd-shim/LICENSE b/pnpm11/bins/cmd-shim/LICENSE new file mode 100644 index 0000000000..0c44ae716d --- /dev/null +++ b/pnpm11/bins/cmd-shim/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) Isaac Z. Schlueter ("Author") +All rights reserved. + +The BSD License + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pnpm11/bins/cmd-shim/README.md b/pnpm11/bins/cmd-shim/README.md new file mode 100644 index 0000000000..f42dad008b --- /dev/null +++ b/pnpm11/bins/cmd-shim/README.md @@ -0,0 +1,48 @@ +# @pnpm/bins.cmd-shim + +> Used in pnpm for command line application support + +The cmd-shim used in [pnpm](https://github.com/pnpm/pnpm) to create executable scripts. + +## Installation + +```sh +npm install --save @pnpm/bins.cmd-shim +``` + +## API + +### `cmdShim(src, to, opts?): Promise` + +Create a cmd shim at `to` for the command line program at `src`. +e.g. + +```javascript +import { cmdShim } from '@pnpm/bins.cmd-shim' +cmdShim('/path/to/cli.js', '/usr/bin/command-name') + .catch(err => console.error(err)) +``` + +### `cmdShimIfExists(src, to, opts?): Promise` + +Creates shims like `cmdShim`, but ignores all creation errors, including a missing source file. + +#### Arguments: + +- `opts.preserveSymlinks` - _Boolean_ - if true, `--preserve-symlinks` is added to the options passed to NodeJS. +- `opts.nodePath` - `string | string[]` - sets the [NODE_PATH](https://nodejs.org/api/cli.html#cli_node_path_path) env variable. +- `opts.prependToPath` - _String_ - prepends the passed path to PATH before executing the Node.js program. +- `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 prepended to the CLI arguments + +```javascript +import { cmdShim } from '@pnpm/bins.cmd-shim' +cmdShim('/path/to/cli.js', '/usr/bin/command-name', { preserveSymlinks: true }) + .catch(err => console.error(err)) +``` + +## License + +[BSD-2-Clause](./LICENSE) © [Zoltan Kochan](http://kochan.io) diff --git a/pnpm11/bins/cmd-shim/package.json b/pnpm11/bins/cmd-shim/package.json new file mode 100644 index 0000000000..a5f7a8f75e --- /dev/null +++ b/pnpm11/bins/cmd-shim/package.json @@ -0,0 +1,46 @@ +{ + "name": "@pnpm/bins.cmd-shim", + "version": "1100.0.0", + "description": "Used in pnpm for command line application support", + "keywords": [ + "pnpm", + "pnpm11" + ], + "license": "BSD-2-Clause", + "funding": "https://opencollective.com/pnpm", + "repository": "https://github.com/pnpm/pnpm/tree/main/pnpm11/bins/cmd-shim", + "homepage": "https://github.com/pnpm/pnpm/tree/main/pnpm11/bins/cmd-shim#readme", + "bugs": { + "url": "https://github.com/pnpm/pnpm/issues" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "exports": { + ".": "./lib/index.js" + }, + "files": [ + "lib", + "!*.map" + ], + "scripts": { + "lint": "eslint \"src/**/*.ts\"", + "compile": "tsgo --build && pn lint --fix", + "test": "pn compile && pn .test", + ".test": "node --test test/test.js test/e2e.test.js", + "prepublishOnly": "tsgo --build" + }, + "dependencies": { + "@pnpm/fs.graceful-fs": "workspace:*", + "cmd-extension": "catalog:" + }, + "devDependencies": { + "@pnpm/bins.cmd-shim": "workspace:*", + "@types/node": "catalog:", + "memfs": "catalog:", + "tempy": "catalog:" + }, + "engines": { + "node": ">=22.13" + } +} diff --git a/pnpm11/bins/cmd-shim/src/index.ts b/pnpm11/bins/cmd-shim/src/index.ts new file mode 100644 index 0000000000..49c9b62aa9 --- /dev/null +++ b/pnpm11/bins/cmd-shim/src/index.ts @@ -0,0 +1,663 @@ +import fs from 'node:fs' +import path from 'node:path' +import util from 'node:util' + +import gfsPromises from '@pnpm/fs.graceful-fs' +import { cmdExtension as CMD_EXTENSION } from 'cmd-extension' + +export interface Options { + /** + * If a PowerShell script should be created. + * + * @default true + */ + createPwshFile?: boolean + + /** + * If a Windows Command Prompt script should be created. + * + * @default true on Windows, false on other platforms + */ + createCmdFile?: boolean + + /** + * If symbolic links should be preserved. + * + * @default false + */ + preserveSymlinks?: boolean + + /** + * The path to the executable file. + */ + prog?: string + + /** + * The arguments to initialize the `node` process with. + */ + args?: string + + /** + * The arguments to initialize the target process with, before the actual CLI arguments + */ + progArgs?: string[] + + /** + * The value of the $NODE_PATH environment variable. + * + * The single `string` format is only kept for legacy compatibility, + * and the array form should be preferred. + */ + nodePath?: string | string[] + + /** + * fs implementation to use. Must implement node's `fs` module interface. + */ + fs?: typeof import('fs') + + /* + * Path to the Node.js executable + */ + nodeExecPath?: string + + prependToPath?: string +} + +/** + * @internal + */ +type InternalOptions = Options & Required> & { + fs_: FsPromises +} + +type FsPromises = Pick + +/** + * Callback functions to generate scripts for shims. + * @param src Path to the executable or script. + * @param to Path to the shim(s) that is going to be created. + * @param opts Options. + * @return Generated script for shim. + */ +type ShimGenerator = (src: string, to: string, opts: InternalOptions) => string + +interface ShimGenExtTuple { + generator: ShimGenerator + extension: string +} + +const isWindows = process.platform === 'win32' +const isCygwin = () => isWindows && (process.env.TERM === 'CYGWIN' || process.env.MSYSTEM !== undefined) + + +// Interpreter paths may contain whitespace other than spaces and tabs. +// eslint-disable-next-line regexp/no-super-linear-backtracking +const shebangExpr = /^#!\s*(?:\/usr\/bin\/env(?:\s+-S)?\s*)?([^ \t]+)(.*)$/ +const DEFAULT_OPTIONS = { + createPwshFile: true, + createCmdFile: isWindows, +} +/** + * Map from extensions of files that this module is frequently used for to their runtime. + * @type {Map} + */ +const extensionToProgramMap = new Map([ + ['.js', 'node'], + ['.cjs', 'node'], + ['.mjs', 'node'], + ['.cmd', 'cmd'], + ['.bat', 'cmd'], + ['.ps1', 'pwsh'], // not 'powershell' + ['.sh', 'sh'], +]) + +function ingestOptions (opts?: Options): InternalOptions { + const opts_ = {...DEFAULT_OPTIONS, ...opts} as InternalOptions + opts_.fs_ = opts_.fs ? opts_.fs.promises : (gfsPromises as unknown as FsPromises) + return opts_ +} + +/** + * Try to create shims. + * + * @param src Path to program (executable or script). + * @param to Path to shims. + * Don't add an extension if you will create multiple types of shims. + * @param opts Options. + * @throws If `src` is missing. + */ +export async function cmdShim (src: string, to: string, opts?: Options): Promise { + const opts_ = ingestOptions(opts) + await cmdShim_(src, to, opts_) +} + +/** + * Try to create shims. + * + * Resolves even when shim creation fails, including when `src` is missing. + * + * @param src Path to program (executable or script). + * @param to Path to shims. + * Don't add an extension if you will create multiple types of shims. + * @param opts Options. + */ +export function cmdShimIfExists (src: string, to: string, opts?: Options): Promise { + return cmdShim(src, to, opts).catch(() => {}) +} + +/** + * Check whether a shim's content points at the given source path. + * + * @param shimContent The text content of the shim file. + * @param src The expected source path (the executable the shim should point to). + * @return `true` if the shim contains a matching target marker. + */ +export function isShimPointingAt (shimContent: string, src: string): boolean { + return shimContent.includes(`# ${shimTarget(src)}\n`) +} + +/** + * Try to unlink, but ignore errors. + * Any problems will surface later. + * + * @param path File to be removed. + */ +function rm (path: string, opts: InternalOptions): Promise { + return opts.fs_.unlink(path).catch(() => {}) +} + +async function cmdShim_ (src: string, to: string, opts: InternalOptions) { + const srcRuntimeInfo = await searchScriptRuntime(src, opts) + await writeShimsPreCommon(to, opts) + return writeAllShims(src, to, srcRuntimeInfo, opts) +} + +function writeShimsPreCommon (target: string, opts: InternalOptions) { + return opts.fs_.mkdir(path.dirname(target), { recursive: true }) +} + +/** + * Replaces the shell shim and the enabled CMD/PowerShell siblings with executable files. + * Resolves when all writes and permission changes finish; rejects if any fail. + */ +function writeAllShims (src: string, to: string, srcRuntimeInfo: RuntimeInfo, opts: Options) { + const opts_ = ingestOptions(opts) + const generatorAndExts: ShimGenExtTuple[] = [{ generator: generateShShim, extension: '' }] + if (opts_.createCmdFile) { + generatorAndExts.push({ generator: generateCmdShim, extension: CMD_EXTENSION }) + } + if (opts_.createPwshFile) { + generatorAndExts.push({ generator: generatePwshShim, extension: '.ps1' }) + } + return Promise.all( + generatorAndExts.map((generatorAndExt) => writeShim(src, to + generatorAndExt.extension, srcRuntimeInfo, generatorAndExt.generator, opts_)) + ) +} + +function writeShimPre (target: string, opts: InternalOptions) { + return rm(target, opts) +} + +function writeShimPost (target: string, opts: InternalOptions) { + return chmodShim(target, opts) +} + +interface RuntimeInfo { + program: string | null + additionalArgs: string +} + +async function searchScriptRuntime (target: string, opts: InternalOptions): Promise { + try { + const data = await opts.fs_.readFile(target, 'utf8') + + // First, check if the bin is a #! of some sort. + const firstLine = (data as string).trim().split(/\r*\n/)[0] + const shebang = firstLine.match(shebangExpr) + if (!shebang) { + // If not, infer script type from its extension. + // If the inference fails, it's something that'll be compiled, or some other + // sort of script, and just call it directly. + const targetExtension = path.extname(target).toLowerCase() + // undefined if extension is unknown but it's converted to null. + const program = extensionToProgramMap.get(targetExtension) || null + // CMD requires executing batch files with the `/C` flag + const additionalArgs = program === 'cmd' ? '/C' : '' + return { + program, + additionalArgs, + } + } + return { + program: shebang[1], + additionalArgs: shebang[2], + } + } catch (err) { + if (!isWindows || !util.types.isNativeError(err) || !('code' in err) || err.code !== 'ENOENT') throw err + if (await opts.fs_.stat(`${target}${getExeExtension()}`)) { + return { + program: null, + additionalArgs: '', + } + } + throw err + } +} + +export function getExeExtension (): string { + let cmdExtension + + if (process.env.PATHEXT) { + cmdExtension = process.env.PATHEXT + .split(path.delimiter) + .find(ext => ext.toLowerCase() === '.exe') + } + + return cmdExtension || '.exe' +} + +/** + * Prefix bare `/C` / `/K` switches with an extra `/` so MSYS / Git Bash + * passes them through to cmd.exe unchanged instead of converting them to + * `C:\` / `K:\` paths. See {@link generateShShim} for context. + */ +function escapeMsysCmdSwitches (args: string): string { + return args.replace(/(^|\s)\/([CK])(\s|$)/gi, '$1//$2$3') +} + +/** + * Replaces `to` with an executable shim. Rejects if writing or setting permissions fails. + */ +async function writeShim (src: string, to: string, srcRuntimeInfo: RuntimeInfo, generateShimScript: ShimGenerator, opts: InternalOptions) { + const defaultArgs = opts.preserveSymlinks ? '--preserve-symlinks' : '' + const args = [srcRuntimeInfo.additionalArgs, defaultArgs].filter(arg => arg).join(' ') + opts = Object.assign({}, opts, { + prog: srcRuntimeInfo.program, + args: args, + }) + + await writeShimPre(to, opts) + await opts.fs_.writeFile(to, generateShimScript(src, to, opts), 'utf8') + return writeShimPost(to, opts) +} + +/** + * Generate the content of a shim for CMD. + * + * @param src Path to the executable or script. + * @param to Path to the shim to be created. + * It is highly recommended to end with `.cmd` (or `.bat`). + * @param opts Options. + * @return The content of shim. + */ +function generateCmdShim (src: string, to: string, opts: InternalOptions): string { + const shTarget = path.relative(path.dirname(to), src) + let target = shTarget.split('/').join('\\') + const quotedPathToTarget = path.isAbsolute(target) ? `"${target}"` : `"%~dp0\\${target}"` + let longProg + let prog = opts.prog + let args = opts.args || '' + const nodePath = normalizePathEnvVar(opts.nodePath).win32 + const prependToPath = normalizePathEnvVar(opts.prependToPath).win32 + if (!prog) { + prog = quotedPathToTarget + args = '' + target = '' + } else if (prog === 'node' && opts.nodeExecPath) { + prog = `"${opts.nodeExecPath}"` + target = quotedPathToTarget + } else { + longProg = `"%~dp0\\${prog}.exe"` + target = quotedPathToTarget + } + + let progArgs = opts.progArgs ? `${opts.progArgs.join(' ')} ` : '' + + let cmd = '@SETLOCAL\r\n' + if (prependToPath) { + cmd += `@SET "PATH=${prependToPath}:%PATH%"\r\n` + } + if (nodePath) { + cmd += `\ +@IF NOT DEFINED NODE_PATH (\r + @SET "NODE_PATH=${nodePath}"\r +) ELSE (\r + @SET "NODE_PATH=${nodePath};%NODE_PATH%"\r +)\r +` + } + if (longProg) { + cmd += `\ +@IF EXIST ${longProg} (\r + ${longProg} ${args} ${target} ${progArgs}%*\r +) ELSE (\r + @SET PATHEXT=%PATHEXT:;.JS;=;%\r + ${prog} ${args} ${target} ${progArgs}%*\r +)\r +` + } else { + cmd += `@${prog} ${args} ${target} ${progArgs}%*\r\n` + } + + return cmd +} + +/** + * Generate the content of a shim for (Ba)sh in, for example, Cygwin and MSYS(2). + * + * @param src Path to the executable or script. + * @param to Path to the shim to be created. + * It is highly recommended to end with `.sh` or to contain no extension. + * @param opts Options. + * @return The content of shim. + */ +function generateShShim (src: string, to: string, opts: InternalOptions): string { + let shTarget = path.relative(path.dirname(to), src) + let shProg = opts.prog && opts.prog.split('\\').join('/') + let shLongProg: string | undefined + let shLongProgExe = '' + let shProgExe = '' + let shProgHasExe = false + shTarget = shTarget.split('\\').join('/') + const quotedPathToTarget = path.isAbsolute(shTarget) ? `"${shTarget}"` : `"$basedir/${shTarget}"` + const quotedPathToTargetWin = path.isAbsolute(shTarget) ? `"${shTarget}"` : `"$basedir_win/${shTarget}"` + let shTargetWin = '' + let args = opts.args || '' + const isCmdRuntime = opts.prog === 'cmd' || opts.prog === 'cmd.exe' + const shNodePath = normalizePathEnvVar(opts.nodePath).posix + if (!shProg) { + shProg = quotedPathToTarget + args = '' + shTarget = '' + } else if (opts.prog === 'node' && opts.nodeExecPath) { + shProg = `"${opts.nodeExecPath}"` + shTarget = /\.exe$/.test(opts.nodeExecPath) ? quotedPathToTargetWin : quotedPathToTarget + } else { + shProgHasExe = /\.exe$/i.test(shProg) + shProgExe = shProgHasExe ? shProg : `${shProg}.exe` + shLongProg = `"$basedir/${shProg}"` + shLongProgExe = `"$basedir/${shProgExe}"` + shTarget = quotedPathToTarget + shTargetWin = quotedPathToTargetWin + } + + let progArgs = opts.progArgs ? `${opts.progArgs.join(' ')} ` : '' + + let sh = `\ +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +` + if (opts.prependToPath) { + sh += `\ +export PATH="${opts.prependToPath}:$PATH" +` + } + if (shNodePath) { + sh += `\ +if [ -z "$NODE_PATH" ]; then + export NODE_PATH="${shNodePath}" +else + export NODE_PATH="${shNodePath}:$NODE_PATH" +fi +` + } + + const generateExecBlock = (execArgs: string) => { + if (shLongProg) { + if (shProgHasExe) { + return `\ +if [ -x ${shLongProgExe} ]; then + exec ${shLongProgExe} ${execArgs} ${shTargetWin} ${progArgs}"$@" +else + exec ${shProgExe} ${execArgs} ${shTargetWin} ${progArgs}"$@" +fi +` + } else { + return `\ +if [ -n "$exe" ] && [ -x ${shLongProgExe} ]; then + exec ${shLongProgExe} ${execArgs} ${shTargetWin} ${progArgs}"$@" +elif [ -x ${shLongProg} ]; then + exec ${shLongProg} ${execArgs} ${shTarget} ${progArgs}"$@" +elif command -v ${shProg} >/dev/null 2>&1; then + exec ${shProg} ${execArgs} ${shTarget} ${progArgs}"$@" +elif [ -n "$exe" ] && command -v ${shProgExe} >/dev/null 2>&1; then + exec ${shProgExe} ${execArgs} ${shTargetWin} ${progArgs}"$@" +else + exec ${shProg} ${execArgs} ${shTarget} ${progArgs}"$@" +fi +` + } + } else { + return `\ +exec ${shProg} ${execArgs} ${shTarget} ${progArgs}"$@" +exit $? +` + } + } + + const msysArgs = isCmdRuntime ? escapeMsysCmdSwitches(args) : args + if (msysArgs !== args) { + sh += `\ +if [ -n "$msys" ]; then +${indentShellBlock(generateExecBlock(msysArgs))} +else +${indentShellBlock(generateExecBlock(args))} +fi +` + } else { + sh += generateExecBlock(args) + } + + // Marker used by consumers to detect whether the shim is up-to-date + // without parsing the script content. + sh += `# ${shimTarget(src)}\n` + + return sh +} + +function indentShellBlock (script: string): string { + return script.split('\n').map(line => line ? ` ${line}` : line).join('\n') +} + +/** + * Generate the content of a shim for PowerShell. + * + * @param src Path to the executable or script. + * @param to Path to the shim to be created. + * It is highly recommended to end with `.ps1`. + * @param opts Options. + * @return The content of shim. + */ +function generatePwshShim (src: string, to: string, opts: InternalOptions): string { + let shTarget = path.relative(path.dirname(to), src) + const shProg = opts.prog && opts.prog.split('\\').join('/') + let pwshProg = shProg && `"${shProg}$exe"` + let pwshLongProg + shTarget = shTarget.split('\\').join('/') + const quotedPathToTarget = path.isAbsolute(shTarget) ? `"${shTarget}"` : `"$basedir/${shTarget}"` + let args = opts.args || '' + let normalizedNodePathEnvVar = normalizePathEnvVar(opts.nodePath) + const nodePath = normalizedNodePathEnvVar.win32 + const shNodePath = normalizedNodePathEnvVar.posix + let normalizedPrependPathEnvVar = normalizePathEnvVar(opts.prependToPath) + const prependPath = normalizedPrependPathEnvVar.win32 + const shPrependPath = normalizedPrependPathEnvVar.posix + if (!pwshProg) { + pwshProg = quotedPathToTarget + args = '' + shTarget = '' + } else if (opts.prog === 'node' && opts.nodeExecPath) { + pwshProg = `"${opts.nodeExecPath}"` + shTarget = quotedPathToTarget + } else { + pwshLongProg = `"$basedir/${opts.prog}$exe"` + shTarget = quotedPathToTarget + } + + let progArgs = opts.progArgs ? `${opts.progArgs.join(' ')} ` : '' + + let pwsh = `\ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +${(nodePath || prependPath) ? '$pathsep=":"\n' : ''}\ +${nodePath ? `\ +$env_node_path=$env:NODE_PATH +$new_node_path="${nodePath}" +` : ''}\ +${prependPath ? `\ +$env_path=$env:PATH +$prepend_path="${prependPath}" +` : ''}\ +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +${(nodePath || prependPath) ? ' $pathsep=";"\n' : ''}\ +}` + if (shNodePath || shPrependPath) { + pwsh += `\ + else { +${shNodePath ? ` $new_node_path="${shNodePath}"\n` : ''}\ +${shPrependPath ? ` $prepend_path="${shPrependPath}"\n` : ''}\ +} +` + } + if (shNodePath) { + pwsh += `\ +if ([string]::IsNullOrEmpty($env_node_path)) { + $env:NODE_PATH=$new_node_path +} else { + $env:NODE_PATH="$new_node_path$pathsep$env_node_path" +} +` + } + if (opts.prependToPath) { + pwsh += ` +$env:PATH="$prepend_path$pathsep$env:PATH" +` + } + if (pwshLongProg) { + pwsh += ` +$ret=0 +if (Test-Path ${pwshLongProg}) { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & ${pwshLongProg} ${args} ${shTarget} ${progArgs}$args + } else { + & ${pwshLongProg} ${args} ${shTarget} ${progArgs}$args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & ${pwshProg} ${args} ${shTarget} ${progArgs}$args + } else { + & ${pwshProg} ${args} ${shTarget} ${progArgs}$args + } + $ret=$LASTEXITCODE +} +${nodePath ? '$env:NODE_PATH=$env_node_path\n' : ''}\ +${prependPath ? '$env:PATH=$env_path\n' : ''}\ +exit $ret +` + } else { + pwsh += ` +# Support pipeline input +if ($MyInvocation.ExpectingInput) { + $input | & ${pwshProg} ${args} ${shTarget} ${progArgs}$args +} else { + & ${pwshProg} ${args} ${shTarget} ${progArgs}$args +} +${nodePath ? '$env:NODE_PATH=$env_node_path\n' : ''}\ +${prependPath ? '$env:PATH=$env_path\n' : ''}\ +exit $LASTEXITCODE +` + } + + return pwsh +} + +function chmodShim (to: string, opts: InternalOptions) { + return opts.fs_.chmod(to, 0o755) +} + +interface NormalizedPathEnvVar { + win32: string + posix: string + [index: number]: {win32: string, posix: string} +} +function normalizePathEnvVar (nodePath: undefined | string | string[]): NormalizedPathEnvVar { + if (!nodePath || !nodePath.length) { + return { + win32: '', + posix: '', + } + } + let split = (typeof nodePath === 'string' ? nodePath.split(path.delimiter) : Array.from(nodePath)) + let result = {} as NormalizedPathEnvVar + for (let i = 0; i < split.length; i++) { + const win32 = split[i].split('/').join('\\') + const posix = isWindows ? split[i].split('\\').join('/').replace(/^([^:\\/]*):/, (_, $1) => `${isCygwin() ? '/proc/cygdrive' : '/mnt'}/${$1.toLowerCase()}`) : split[i] + + result.win32 = result.win32 ? `${result.win32};${win32}` : win32 + result.posix = result.posix ? `${result.posix}:${posix}` : posix + + result[i] = {win32, posix} + } + return result +} + +function shimTarget (src: string): string { + return `cmd-shim-target=${src.split('\\').join('/')}` +} diff --git a/pnpm11/bins/cmd-shim/test/e2e.test.js b/pnpm11/bins/cmd-shim/test/e2e.test.js new file mode 100644 index 0000000000..100510b13a --- /dev/null +++ b/pnpm11/bins/cmd-shim/test/e2e.test.js @@ -0,0 +1,284 @@ +import { spawn, spawnSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import { describe, test, snapshot } from 'node:test' +import assert from 'node:assert/strict' + +snapshot.setDefaultSnapshotSerializers([ + (value) => typeof value === 'string' ? `\n${value.replaceAll('\r', '')}` : JSON.stringify(value), +]) +import { temporaryDirectory } from 'tempy' +import { cmdShim } from '@pnpm/bins.cmd-shim' + +const describeOnWindows = process.platform === 'win32' ? describe : describe.skip +const describeOnPosix = process.platform === 'win32' ? describe.skip : describe + +describeOnWindows('create a command shim for a .exe file', () => { + test('shim files', async (t) => { + const tempDir = temporaryDirectory() + fs.writeFileSync(path.join(tempDir, 'foo.exe'), '', 'utf8') + await cmdShim(path.join(tempDir, 'foo'), path.join(tempDir, 'dest')) + const stripMarker = (s) => s.replace(/# cmd-shim-target=.*\n/, '') + t.assert.snapshot(stripMarker(fs.readFileSync(path.join(tempDir, 'dest'), 'utf-8'))) + t.assert.snapshot(fs.readFileSync(path.join(tempDir, 'dest.cmd'), 'utf-8')) + t.assert.snapshot(fs.readFileSync(path.join(tempDir, 'dest.ps1'), 'utf-8')) + }) +}) + +describeOnWindows('sh shim wrapping a .cmd target invoked from Git Bash', () => { + // Regression for the MSYS path-translation bug: Git Bash rewrites a bare + // `/C` argument to `C:\` before launching cmd.exe, dropping the switch + // and leaving cmd.exe interactive. The fix escapes it as `//C` so MSYS + // passes it through. + test('runs the wrapped batch script and captures its output', async () => { + const tempDir = temporaryDirectory() + const target = path.join(tempDir, 'src.cmd') + fs.writeFileSync(target, '@echo HELLO_FROM_CMD\r\n', 'utf8') + const shim = path.join(tempDir, 'shim') + await cmdShim(target, shim) + + // Invoke the sh shim via Git Bash. If `/C` survives MSYS translation, + // cmd.exe runs src.cmd and prints HELLO_FROM_CMD; if it gets rewritten + // to a drive path, cmd.exe starts interactively and dumps its banner + // (`Microsoft Windows [Version ...]`) instead. + const bash = process.env.PROGRAMFILES + ? path.join(process.env.PROGRAMFILES, 'Git', 'bin', 'bash.exe') + : 'bash' + const r = spawnSync(bash, ['--noprofile', '--norc', shim], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }) + + assert.equal(r.status, 0, `bash exited ${r.status}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`) + assert.match(r.stdout, /HELLO_FROM_CMD/, `expected script output, got:\n${r.stdout}`) + assert.doesNotMatch( + r.stdout, + /Microsoft Windows \[Version/, + 'cmd.exe started interactively — the /C switch was dropped' + ) + }) +}) + +describeOnPosix('sh shim binstub uses exec', () => { + // Regression for the binstub bug: without `exec`, the shell process + // wraps the wrapped binary, so signals sent to the shim do not reach + // the wrapped process and the binary's PID differs from the shim's + // spawn PID. With `exec`, the shell process is replaced in place and + // the wrapped binary inherits the shim's PID. + test('wrapped binary inherits the shim\'s PID (proves exec replaced the shell)', async () => { + const tempDir = temporaryDirectory() + // process.execPath is a no-shebang native binary, so cmdShim hits the + // non-shLongProg branch of generateShShim — the one that needs `exec`. + const shim = path.join(tempDir, 'shim') + await cmdShim(process.execPath, shim) + + const proc = spawn('/bin/sh', [shim, '-e', 'process.stdout.write(String(process.pid))'], { + stdio: ['ignore', 'pipe', 'pipe'], + }) + const shimPid = proc.pid + let stdout = '' + proc.stdout.on('data', (chunk) => { stdout += chunk }) + // `exit` may fire before the stdio streams have been drained. Wait for + // `close` so stdout contains the complete PID before asserting on it. + const exitCode = await new Promise((resolve) => proc.on('close', resolve)) + + assert.equal(exitCode, 0, `shim exited ${exitCode}; stdout=${stdout}`) + const reportedPid = Number(stdout.trim()) + assert.equal( + reportedPid, + shimPid, + `expected child to inherit shim PID via exec — got reported=${reportedPid}, shim=${shimPid}` + ) + }) +}) + +describeOnPosix('sh shim uses POSIX runtime from PATH', () => { + test('does not fall through to node.exe when node is only on PATH', async () => { + const tempDir = temporaryDirectory() + const target = path.join(tempDir, 'tool.js') + fs.writeFileSync(target, '#!/usr/bin/env node\nconsole.log("SHIM_OK")\n', 'utf8') + const shim = path.join(tempDir, 'tool') + await cmdShim(target, shim, { createCmdFile: false }) + + const r = spawnSync('/bin/sh', [shim], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }) + + assert.equal(r.status, 0, `shim exited ${r.status}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`) + assert.equal(r.stdout.trim(), 'SHIM_OK') + assert.doesNotMatch(r.stderr, /node\.exe/) + }) +}) + +describeOnPosix('sh shim invoked through a chain of external symlinks', () => { + // POSIX shells set $0 to the invoked symlink, not the shim it points at, + // so the shim must follow the chain before deriving basedir + // (https://github.com/pnpm/pnpm/issues/13405). + const makeShimmedTool = async (tempDir) => { + const binDir = path.join(tempDir, 'node_modules', '.bin') + const targetDir = path.join(tempDir, 'node_modules', 'typescript', 'bin') + fs.mkdirSync(binDir, { recursive: true }) + fs.mkdirSync(targetDir, { recursive: true }) + + const target = path.join(targetDir, 'tsc') + fs.writeFileSync(target, '#!/bin/sh\necho "tsc-output"\n', 'utf8') + fs.chmodSync(target, 0o755) + + const shim = path.join(binDir, 'tsc') + await cmdShim(target, shim) + return shim + } + + const runShim = (cmd) => { + const r = spawnSync(cmd, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }) + assert.equal(r.status, 0, `shim exited ${r.status}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`) + return r.stdout.trim() + } + + test('follows a chain of absolute symlinks', async () => { + const tempDir = temporaryDirectory() + const shim = await makeShimmedTool(tempDir) + + const hop1 = path.join(tempDir, 'symlink_hop_1') + const hop2 = path.join(tempDir, 'symlink_hop_2') + fs.symlinkSync(shim, hop1) + fs.symlinkSync(hop1, hop2) + + assert.equal(runShim(hop2), 'tsc-output') + }) + + test('follows relative symlinks from another directory', async () => { + const tempDir = temporaryDirectory() + await makeShimmedTool(tempDir) + + // Both hops use relative targets, exercising the shim's + // dirname-composition branch rather than the absolute one. + const localBin = path.join(tempDir, 'usr', 'local', 'bin') + fs.mkdirSync(localBin, { recursive: true }) + const hop1 = path.join(tempDir, 'usr', 'tsc') + fs.symlinkSync(path.join('..', 'node_modules', '.bin', 'tsc'), hop1) + const hop2 = path.join(localBin, 'tsc') + fs.symlinkSync(path.join('..', '..', 'tsc'), hop2) + + assert.equal(runShim(hop2), 'tsc-output') + }) + + test('resolves symlinks when directories contain spaces', async () => { + const tempDir = path.join(temporaryDirectory(), 'dir with spaces') + fs.mkdirSync(tempDir, { recursive: true }) + await makeShimmedTool(tempDir) + + const hopDir = path.join(tempDir, 'link dir') + fs.mkdirSync(hopDir) + const hop = path.join(hopDir, 'tsc') + fs.symlinkSync(path.join('..', 'node_modules', '.bin', 'tsc'), hop) + + assert.equal(runShim(hop), 'tsc-output') + }) + + test('runs a node-shebang bin through a symlink chain', async () => { + const tempDir = temporaryDirectory() + const binDir = path.join(tempDir, 'node_modules', '.bin') + const pkgDir = path.join(tempDir, 'node_modules', 'tool') + fs.mkdirSync(binDir, { recursive: true }) + fs.mkdirSync(pkgDir, { recursive: true }) + + const target = path.join(pkgDir, 'cli.js') + fs.writeFileSync(target, '#!/usr/bin/env node\nconsole.log("NODE_BIN_OK")\n', 'utf8') + const shim = path.join(binDir, 'tool') + await cmdShim(target, shim, { createCmdFile: false }) + + const hop = path.join(tempDir, 'tool') + fs.symlinkSync(path.join('node_modules', '.bin', 'tool'), hop) + + assert.equal(runShim(hop), 'NODE_BIN_OK') + }) +}) + +describeOnPosix('sh shim resolves its helpers off the caller\'s PATH', () => { + // A shim runs with node_modules/.bin at the front of PATH, which is where a + // dependency's own bins live, so a helper taken from there could report any + // directory it liked and redirect what the shim finally execs + // (https://github.com/pnpm/pnpm/issues/14837). + const writeExecutable = (file, body) => { + fs.writeFileSync(file, body, 'utf8') + fs.chmodSync(file, 0o755) + } + + // A shimmed tool plus a relative symlink to it in the same directory, so the + // walk composes a directory with the link target instead of taking one + // straight from readlink. + const makeShimmedTool = async (tempDir) => { + const binDir = path.join(tempDir, 'node_modules', '.bin') + const target = path.join(tempDir, 'node_modules', 'typescript', 'bin', 'tsc.js') + fs.mkdirSync(binDir, { recursive: true }) + fs.mkdirSync(path.dirname(target), { recursive: true }) + fs.writeFileSync(target, 'console.log("tsc-output")\n', 'utf8') + // A dependency can declare a bin named node.exe, and the shim's basedir is + // the directory those bins land in. Only a lying uname reaches it. + writeExecutable(path.join(binDir, 'node.exe'), '#!/bin/sh\necho hijacked\n') + + const shim = path.join(binDir, 'tsc') + await cmdShim(target, shim, { createCmdFile: false }) + fs.symlinkSync('tsc', path.join(binDir, 'tsc-link')) + return binDir + } + + // Write the tree the decoys point at, and the decoys, returning the directory + // to put at the front of PATH. Each decoy answers with what its real + // counterpart would be asked for, so any one of them alone is enough to + // redirect the shim. + const plantHijackTreeAndDecoys = (tempDir) => { + const hijack = path.join(tempDir, 'hijack', 'node_modules') + const hijackBin = path.join(hijack, '.bin') + const hijackTarget = path.join(hijack, 'typescript', 'bin', 'tsc.js') + fs.mkdirSync(hijackBin, { recursive: true }) + fs.mkdirSync(path.dirname(hijackTarget), { recursive: true }) + fs.writeFileSync(hijackTarget, 'console.log("hijacked")\n', 'utf8') + + const decoyDir = path.join(tempDir, 'decoy') + fs.mkdirSync(decoyDir) + const answer = (p) => `#!/bin/sh\necho '${p}'\n` + for (const helper of ['readlink', 'sed']) { + writeExecutable(path.join(decoyDir, helper), answer(path.join(hijackBin, 'tsc'))) + } + writeExecutable(path.join(decoyDir, 'dirname'), answer(hijackBin)) + writeExecutable(path.join(decoyDir, 'uname'), '#!/bin/sh\necho MINGW64_NT-10.0\n') + return decoyDir + } + + const runWithDecoys = (tempDir, cmd, args, cwd) => { + const decoyDir = plantHijackTreeAndDecoys(tempDir) + const r = spawnSync(cmd, args, { + cwd, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + env: { + ...process.env, + PATH: [decoyDir, path.dirname(process.execPath), process.env.PATH].join(path.delimiter), + }, + }) + assert.equal(r.status, 0, `shim exited ${r.status}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`) + assert.equal(r.stdout.trim(), 'tsc-output', 'the shim took a helper from the caller\'s PATH') + } + + test('reaches its target with decoy readlink, dirname, sed, and uname first on PATH', async () => { + const tempDir = temporaryDirectory() + const binDir = await makeShimmedTool(tempDir) + + runWithDecoys(tempDir, path.join(binDir, 'tsc-link'), []) + }) + + // The kernel and execvp hand the interpreter the path they resolved, so $0 is + // bare only when a shell is given the name itself. + test('reaches its target when sh receives a bare name', async () => { + const tempDir = temporaryDirectory() + const binDir = await makeShimmedTool(tempDir) + + runWithDecoys(tempDir, 'sh', ['tsc-link'], binDir) + }) +}) diff --git a/pnpm11/bins/cmd-shim/test/e2e.test.js.snapshot b/pnpm11/bins/cmd-shim/test/e2e.test.js.snapshot new file mode 100644 index 0000000000..9719dd1eee --- /dev/null +++ b/pnpm11/bins/cmd-shim/test/e2e.test.js.snapshot @@ -0,0 +1,85 @@ +exports[`create a command shim for a .exe file > shim files 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +exec "$basedir/foo" "$@" +exit $? + +`; + +exports[`create a command shim for a .exe file > shim files 2`] = ` + +@SETLOCAL +@"%~dp0\\foo" %* + +`; + +exports[`create a command shim for a .exe file > shim files 3`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +# Support pipeline input +if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/foo" $args +} else { + & "$basedir/foo" $args +} +exit $LASTEXITCODE + +`; diff --git a/pnpm11/bins/cmd-shim/test/setup.js b/pnpm11/bins/cmd-shim/test/setup.js new file mode 100644 index 0000000000..4a330b95e5 --- /dev/null +++ b/pnpm11/bins/cmd-shim/test/setup.js @@ -0,0 +1,42 @@ +import { fs as memfs } from 'memfs' +import path from 'node:path' + +const { fixtures, fixtures2 } = process.platform === 'win32' ? { + fixtures: 'I:\\cmd-shim\\fixtures', + fixtures2: 'J:\\cmd-shim\\fixtures' +} : { + fixtures: '/foo/cmd-shim/fixtures', + fixtures2: '/bar/cmd-shim/fixtures' +} + +export { fixtures, fixtures2, memfs as fs } + +/** @type {{ [dir: string]: { [filename: string]: string } }} */ +const fixtureFiles = { + [fixtures]: { + 'src.exe': 'exe', + 'src.bat': 'bat', + 'src.env': '#!/usr/bin/env node\nconsole.log(/hi/)\n', + 'src.env.args': '#!/usr/bin/env node --expose_gc\ngc()\n', + 'src.sh': '#!/usr/bin/sh\necho hi\n', + 'src.sh.args': '#!/usr/bin/sh -x\necho hi\n', + 'from.env.S': '#!/usr/bin/env -S node --expose_gc\ngc()\n' + }, + [fixtures2]: { + 'src.sh.args': '#!/usr/bin/sh -x\necho hi\n' + } +} + +export async function setupFixtures () { + const fsPromises = memfs.promises + await Promise.all( + Object.entries(fixtureFiles).map(async ([dir, files]) => { + await fsPromises.mkdir(dir, { recursive: true }) + return Promise.all( + Object.entries(files).map(([filename, contents]) => { + return fsPromises.writeFile(path.join(dir, filename), contents) + }) + ) + }) + ) +} diff --git a/pnpm11/bins/cmd-shim/test/test.js b/pnpm11/bins/cmd-shim/test/test.js new file mode 100755 index 0000000000..e9e44be855 --- /dev/null +++ b/pnpm11/bins/cmd-shim/test/test.js @@ -0,0 +1,284 @@ +import { describe, test, before, snapshot } from 'node:test' +import assert from 'node:assert/strict' + +snapshot.setDefaultSnapshotSerializers([ + (value) => typeof value === 'string' ? `\n${value.replaceAll('\r', '')}` : JSON.stringify(value), +]) +import path from 'node:path' +import { cmdExtension } from 'cmd-extension' +import { fixtures, fixtures2, fs, setupFixtures } from './setup.js' +import { cmdShim, isShimPointingAt } from '@pnpm/bins.cmd-shim' + +/** + * @param {import('node:test').TestContext} t + * @param {string} fileName + * @param {'\n' | '\r\n'} lineEnding + */ +async function testFile (t, fileName, lineEnding = '\n') { + await t.test(path.basename(fileName).toLowerCase(), async (t) => { + const invalidLineEnding = lineEnding === '\r\n' ? /$(? { + const src = path.resolve(fixtures, 'src.exe') + const to = path.resolve(fixtures, 'exe.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { createCmdFile: false, fs }) + }) + + test('returns true for the correct target', async () => { + const content = await fs.promises.readFile(to, 'utf8') + assert.equal(isShimPointingAt(content, src), true) + }) + + test('returns false for a different target', async () => { + const content = await fs.promises.readFile(to, 'utf8') + assert.equal(isShimPointingAt(content, '/wrong/path.exe'), false) + }) + + test('returns false for a subdirectory prefix of the target', async () => { + const content = await fs.promises.readFile(to, 'utf8') + // src without the last path segment — must not match + assert.equal(isShimPointingAt(content, path.dirname(src)), false) + }) +}) + +describe('no cmd file', () => { + const src = path.resolve(fixtures, 'src.exe') + const to = path.resolve(fixtures, 'exe.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { createCmdFile: false, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + assert.throws(() => fs.readFileSync(`${to}.cmd`, 'utf8'), /no such file or directory/) + await testFile(t, `${to}.ps1`) + }) +}) + +describe('no shebang', () => { + const src = path.resolve(fixtures, 'src.exe') + const to = path.resolve(fixtures, 'exe.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { createCmdFile: true, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +describe('env shebang', () => { + const src = path.resolve(fixtures, 'src.env') + const to = path.resolve(fixtures, 'env.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { createCmdFile: true, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +describe('env shebang with PATH extending', () => { + const src = path.resolve(fixtures, 'src.env') + const to = path.resolve(fixtures, 'env.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { prependToPath: '/add-to-path', createCmdFile: true, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +describe('env shebang with NODE_PATH', () => { + const src = path.resolve(fixtures, 'src.env') + const to = path.resolve(fixtures, 'env.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { nodePath: ['/john/src/node_modules', '/bin/node/node_modules'], createCmdFile: true, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +describe('env shebang with no NODE_PATH', () => { + const src = path.resolve(fixtures, 'src.env') + const to = path.resolve(fixtures, 'env.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { nodePath: [], createCmdFile: true, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +describe('env shebang with default args', () => { + const src = path.resolve(fixtures, 'src.env') + const to = path.resolve(fixtures, 'env.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { preserveSymlinks: true, createCmdFile: true, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +describe('env shebang with args', () => { + const src = path.resolve(fixtures, 'src.env.args') + const to = path.resolve(fixtures, 'env.args.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { createCmdFile: true, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +describe('explicit shebang', () => { + const src = path.resolve(fixtures, 'src.sh') + const to = path.resolve(fixtures, 'sh.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { createCmdFile: true, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +describe('explicit shebang with args', () => { + const src = path.resolve(fixtures, 'src.sh.args') + const to = path.resolve(fixtures, 'sh.args.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { createCmdFile: true, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +describe('explicit shebang with prog args', () => { + const src = path.resolve(fixtures, 'src.sh.args') + const to = path.resolve(fixtures, 'sh.args.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { createCmdFile: true, progArgs: ['hello'], fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +describe('custom node executable', () => { + const src = path.resolve(fixtures, 'src.env') + const to = path.resolve(fixtures, 'env.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { createCmdFile: true, nodeExecPath: '/.pnpm/nodejs/16.0.0/node', fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +const describeOnWindows = process.platform === 'win32' ? describe : describe.skip + +describeOnWindows('explicit shebang with args, linking to another drive on Windows', () => { + const src = path.resolve(fixtures2, 'src.sh.args') + const to = path.resolve(fixtures, 'sh.args.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { createCmdFile: true, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +describe('shebang with -S', () => { + const src = path.resolve(fixtures, 'from.env.S') + const to = path.resolve(fixtures, 'from.env.S.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { createCmdFile: true, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) + +describe('batch script', () => { + const src = path.resolve(fixtures, 'src.bat') + const to = path.resolve(fixtures, 'bat.shim') + before(async () => { + await setupFixtures() + return cmdShim(src, to, { createCmdFile: true, fs }) + }) + + test('shim files', async (t) => { + await testFile(t, to) + await testFile(t, `${to}${cmdExtension}`, '\r\n') + await testFile(t, `${to}.ps1`) + }) +}) diff --git a/pnpm11/bins/cmd-shim/test/test.js.snapshot b/pnpm11/bins/cmd-shim/test/test.js.snapshot new file mode 100644 index 0000000000..d88ffa86ce --- /dev/null +++ b/pnpm11/bins/cmd-shim/test/test.js.snapshot @@ -0,0 +1,1645 @@ +exports[`batch script > shim files > bat.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +if [ -n "$msys" ]; then + if [ -n "$exe" ] && [ -x "$basedir/cmd.exe" ]; then + exec "$basedir/cmd.exe" //C "$basedir_win/src.bat" "$@" + elif [ -x "$basedir/cmd" ]; then + exec "$basedir/cmd" //C "$basedir/src.bat" "$@" + elif command -v cmd >/dev/null 2>&1; then + exec cmd //C "$basedir/src.bat" "$@" + elif [ -n "$exe" ] && command -v cmd.exe >/dev/null 2>&1; then + exec cmd.exe //C "$basedir_win/src.bat" "$@" + else + exec cmd //C "$basedir/src.bat" "$@" + fi + +else + if [ -n "$exe" ] && [ -x "$basedir/cmd.exe" ]; then + exec "$basedir/cmd.exe" /C "$basedir_win/src.bat" "$@" + elif [ -x "$basedir/cmd" ]; then + exec "$basedir/cmd" /C "$basedir/src.bat" "$@" + elif command -v cmd >/dev/null 2>&1; then + exec cmd /C "$basedir/src.bat" "$@" + elif [ -n "$exe" ] && command -v cmd.exe >/dev/null 2>&1; then + exec cmd.exe /C "$basedir_win/src.bat" "$@" + else + exec cmd /C "$basedir/src.bat" "$@" + fi + +fi + +`; + +exports[`batch script > shim files > bat.shim.cmd 1`] = ` + +@SETLOCAL +@IF EXIST "%~dp0\\cmd.exe" ( + "%~dp0\\cmd.exe" /C "%~dp0\\src.bat" %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + cmd /C "%~dp0\\src.bat" %* +) + +`; + +exports[`batch script > shim files > bat.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/cmd$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/cmd$exe" /C "$basedir/src.bat" $args + } else { + & "$basedir/cmd$exe" /C "$basedir/src.bat" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "cmd$exe" /C "$basedir/src.bat" $args + } else { + & "cmd$exe" /C "$basedir/src.bat" $args + } + $ret=$LASTEXITCODE +} +exit $ret + +`; + +exports[`custom node executable > shim files > env.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +exec "/.pnpm/nodejs/16.0.0/node" "$basedir/src.env" "$@" +exit $? + +`; + +exports[`custom node executable > shim files > env.shim.cmd 1`] = ` + +@SETLOCAL +@"/.pnpm/nodejs/16.0.0/node" "%~dp0\\src.env" %* + +`; + +exports[`custom node executable > shim files > env.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +# Support pipeline input +if ($MyInvocation.ExpectingInput) { + $input | & "/.pnpm/nodejs/16.0.0/node" "$basedir/src.env" $args +} else { + & "/.pnpm/nodejs/16.0.0/node" "$basedir/src.env" $args +} +exit $LASTEXITCODE + +`; + +exports[`env shebang > shim files > env.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then + exec "$basedir/node.exe" "$basedir_win/src.env" "$@" +elif [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/src.env" "$@" +elif command -v node >/dev/null 2>&1; then + exec node "$basedir/src.env" "$@" +elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then + exec node.exe "$basedir_win/src.env" "$@" +else + exec node "$basedir/src.env" "$@" +fi + +`; + +exports[`env shebang > shim files > env.shim.cmd 1`] = ` + +@SETLOCAL +@IF EXIST "%~dp0\\node.exe" ( + "%~dp0\\node.exe" "%~dp0\\src.env" %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + node "%~dp0\\src.env" %* +) + +`; + +exports[`env shebang > shim files > env.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/src.env" $args + } else { + & "$basedir/node$exe" "$basedir/src.env" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/src.env" $args + } else { + & "node$exe" "$basedir/src.env" $args + } + $ret=$LASTEXITCODE +} +exit $ret + +`; + +exports[`env shebang with NODE_PATH > shim files > env.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +if [ -z "$NODE_PATH" ]; then + export NODE_PATH="/john/src/node_modules:/bin/node/node_modules" +else + export NODE_PATH="/john/src/node_modules:/bin/node/node_modules:$NODE_PATH" +fi +if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then + exec "$basedir/node.exe" "$basedir_win/src.env" "$@" +elif [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/src.env" "$@" +elif command -v node >/dev/null 2>&1; then + exec node "$basedir/src.env" "$@" +elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then + exec node.exe "$basedir_win/src.env" "$@" +else + exec node "$basedir/src.env" "$@" +fi + +`; + +exports[`env shebang with NODE_PATH > shim files > env.shim.cmd 1`] = ` + +@SETLOCAL +@IF NOT DEFINED NODE_PATH ( + @SET "NODE_PATH=\\john\\src\\node_modules;\\bin\\node\\node_modules" +) ELSE ( + @SET "NODE_PATH=\\john\\src\\node_modules;\\bin\\node\\node_modules;%NODE_PATH%" +) +@IF EXIST "%~dp0\\node.exe" ( + "%~dp0\\node.exe" "%~dp0\\src.env" %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + node "%~dp0\\src.env" %* +) + +`; + +exports[`env shebang with NODE_PATH > shim files > env.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +$pathsep=":" +$env_node_path=$env:NODE_PATH +$new_node_path="\\john\\src\\node_modules;\\bin\\node\\node_modules" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" + $pathsep=";" +} else { + $new_node_path="/john/src/node_modules:/bin/node/node_modules" +} +if ([string]::IsNullOrEmpty($env_node_path)) { + $env:NODE_PATH=$new_node_path +} else { + $env:NODE_PATH="$new_node_path$pathsep$env_node_path" +} + +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/src.env" $args + } else { + & "$basedir/node$exe" "$basedir/src.env" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/src.env" $args + } else { + & "node$exe" "$basedir/src.env" $args + } + $ret=$LASTEXITCODE +} +$env:NODE_PATH=$env_node_path +exit $ret + +`; + +exports[`env shebang with PATH extending > shim files > env.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +export PATH="/add-to-path:$PATH" +if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then + exec "$basedir/node.exe" "$basedir_win/src.env" "$@" +elif [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/src.env" "$@" +elif command -v node >/dev/null 2>&1; then + exec node "$basedir/src.env" "$@" +elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then + exec node.exe "$basedir_win/src.env" "$@" +else + exec node "$basedir/src.env" "$@" +fi + +`; + +exports[`env shebang with PATH extending > shim files > env.shim.cmd 1`] = ` + +@SETLOCAL +@SET "PATH=\\add-to-path:%PATH%" +@IF EXIST "%~dp0\\node.exe" ( + "%~dp0\\node.exe" "%~dp0\\src.env" %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + node "%~dp0\\src.env" %* +) + +`; + +exports[`env shebang with PATH extending > shim files > env.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +$pathsep=":" +$env_path=$env:PATH +$prepend_path="\\add-to-path" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" + $pathsep=";" +} else { + $prepend_path="/add-to-path" +} + +$env:PATH="$prepend_path$pathsep$env:PATH" + +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/src.env" $args + } else { + & "$basedir/node$exe" "$basedir/src.env" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/src.env" $args + } else { + & "node$exe" "$basedir/src.env" $args + } + $ret=$LASTEXITCODE +} +$env:PATH=$env_path +exit $ret + +`; + +exports[`env shebang with args > shim files > env.args.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then + exec "$basedir/node.exe" --expose_gc "$basedir_win/src.env.args" "$@" +elif [ -x "$basedir/node" ]; then + exec "$basedir/node" --expose_gc "$basedir/src.env.args" "$@" +elif command -v node >/dev/null 2>&1; then + exec node --expose_gc "$basedir/src.env.args" "$@" +elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then + exec node.exe --expose_gc "$basedir_win/src.env.args" "$@" +else + exec node --expose_gc "$basedir/src.env.args" "$@" +fi + +`; + +exports[`env shebang with args > shim files > env.args.shim.cmd 1`] = ` + +@SETLOCAL +@IF EXIST "%~dp0\\node.exe" ( + "%~dp0\\node.exe" --expose_gc "%~dp0\\src.env.args" %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + node --expose_gc "%~dp0\\src.env.args" %* +) + +`; + +exports[`env shebang with args > shim files > env.args.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" --expose_gc "$basedir/src.env.args" $args + } else { + & "$basedir/node$exe" --expose_gc "$basedir/src.env.args" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" --expose_gc "$basedir/src.env.args" $args + } else { + & "node$exe" --expose_gc "$basedir/src.env.args" $args + } + $ret=$LASTEXITCODE +} +exit $ret + +`; + +exports[`env shebang with default args > shim files > env.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then + exec "$basedir/node.exe" --preserve-symlinks "$basedir_win/src.env" "$@" +elif [ -x "$basedir/node" ]; then + exec "$basedir/node" --preserve-symlinks "$basedir/src.env" "$@" +elif command -v node >/dev/null 2>&1; then + exec node --preserve-symlinks "$basedir/src.env" "$@" +elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then + exec node.exe --preserve-symlinks "$basedir_win/src.env" "$@" +else + exec node --preserve-symlinks "$basedir/src.env" "$@" +fi + +`; + +exports[`env shebang with default args > shim files > env.shim.cmd 1`] = ` + +@SETLOCAL +@IF EXIST "%~dp0\\node.exe" ( + "%~dp0\\node.exe" --preserve-symlinks "%~dp0\\src.env" %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + node --preserve-symlinks "%~dp0\\src.env" %* +) + +`; + +exports[`env shebang with default args > shim files > env.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" --preserve-symlinks "$basedir/src.env" $args + } else { + & "$basedir/node$exe" --preserve-symlinks "$basedir/src.env" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" --preserve-symlinks "$basedir/src.env" $args + } else { + & "node$exe" --preserve-symlinks "$basedir/src.env" $args + } + $ret=$LASTEXITCODE +} +exit $ret + +`; + +exports[`env shebang with no NODE_PATH > shim files > env.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then + exec "$basedir/node.exe" "$basedir_win/src.env" "$@" +elif [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/src.env" "$@" +elif command -v node >/dev/null 2>&1; then + exec node "$basedir/src.env" "$@" +elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then + exec node.exe "$basedir_win/src.env" "$@" +else + exec node "$basedir/src.env" "$@" +fi + +`; + +exports[`env shebang with no NODE_PATH > shim files > env.shim.cmd 1`] = ` + +@SETLOCAL +@IF EXIST "%~dp0\\node.exe" ( + "%~dp0\\node.exe" "%~dp0\\src.env" %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + node "%~dp0\\src.env" %* +) + +`; + +exports[`env shebang with no NODE_PATH > shim files > env.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/src.env" $args + } else { + & "$basedir/node$exe" "$basedir/src.env" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/src.env" $args + } else { + & "node$exe" "$basedir/src.env" $args + } + $ret=$LASTEXITCODE +} +exit $ret + +`; + +exports[`explicit shebang > shim files > sh.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +if [ -n "$exe" ] && [ -x "$basedir//usr/bin/sh.exe" ]; then + exec "$basedir//usr/bin/sh.exe" "$basedir_win/src.sh" "$@" +elif [ -x "$basedir//usr/bin/sh" ]; then + exec "$basedir//usr/bin/sh" "$basedir/src.sh" "$@" +elif command -v /usr/bin/sh >/dev/null 2>&1; then + exec /usr/bin/sh "$basedir/src.sh" "$@" +elif [ -n "$exe" ] && command -v /usr/bin/sh.exe >/dev/null 2>&1; then + exec /usr/bin/sh.exe "$basedir_win/src.sh" "$@" +else + exec /usr/bin/sh "$basedir/src.sh" "$@" +fi + +`; + +exports[`explicit shebang > shim files > sh.shim.cmd 1`] = ` + +@SETLOCAL +@IF EXIST "%~dp0\\/usr/bin/sh.exe" ( + "%~dp0\\/usr/bin/sh.exe" "%~dp0\\src.sh" %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + /usr/bin/sh "%~dp0\\src.sh" %* +) + +`; + +exports[`explicit shebang > shim files > sh.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir//usr/bin/sh$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir//usr/bin/sh$exe" "$basedir/src.sh" $args + } else { + & "$basedir//usr/bin/sh$exe" "$basedir/src.sh" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "/usr/bin/sh$exe" "$basedir/src.sh" $args + } else { + & "/usr/bin/sh$exe" "$basedir/src.sh" $args + } + $ret=$LASTEXITCODE +} +exit $ret + +`; + +exports[`explicit shebang with args > shim files > sh.args.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +if [ -n "$exe" ] && [ -x "$basedir//usr/bin/sh.exe" ]; then + exec "$basedir//usr/bin/sh.exe" -x "$basedir_win/src.sh.args" "$@" +elif [ -x "$basedir//usr/bin/sh" ]; then + exec "$basedir//usr/bin/sh" -x "$basedir/src.sh.args" "$@" +elif command -v /usr/bin/sh >/dev/null 2>&1; then + exec /usr/bin/sh -x "$basedir/src.sh.args" "$@" +elif [ -n "$exe" ] && command -v /usr/bin/sh.exe >/dev/null 2>&1; then + exec /usr/bin/sh.exe -x "$basedir_win/src.sh.args" "$@" +else + exec /usr/bin/sh -x "$basedir/src.sh.args" "$@" +fi + +`; + +exports[`explicit shebang with args > shim files > sh.args.shim.cmd 1`] = ` + +@SETLOCAL +@IF EXIST "%~dp0\\/usr/bin/sh.exe" ( + "%~dp0\\/usr/bin/sh.exe" -x "%~dp0\\src.sh.args" %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + /usr/bin/sh -x "%~dp0\\src.sh.args" %* +) + +`; + +exports[`explicit shebang with args > shim files > sh.args.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir//usr/bin/sh$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir//usr/bin/sh$exe" -x "$basedir/src.sh.args" $args + } else { + & "$basedir//usr/bin/sh$exe" -x "$basedir/src.sh.args" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "/usr/bin/sh$exe" -x "$basedir/src.sh.args" $args + } else { + & "/usr/bin/sh$exe" -x "$basedir/src.sh.args" $args + } + $ret=$LASTEXITCODE +} +exit $ret + +`; + +exports[`explicit shebang with args, linking to another drive on Windows > shim files > sh.args.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +if [ -n "$exe" ] && [ -x "$basedir//usr/bin/sh.exe" ]; then + exec "$basedir//usr/bin/sh.exe" -x "J:/cmd-shim/fixtures/src.sh.args" "$@" +elif [ -x "$basedir//usr/bin/sh" ]; then + exec "$basedir//usr/bin/sh" -x "J:/cmd-shim/fixtures/src.sh.args" "$@" +elif command -v /usr/bin/sh >/dev/null 2>&1; then + exec /usr/bin/sh -x "J:/cmd-shim/fixtures/src.sh.args" "$@" +elif [ -n "$exe" ] && command -v /usr/bin/sh.exe >/dev/null 2>&1; then + exec /usr/bin/sh.exe -x "J:/cmd-shim/fixtures/src.sh.args" "$@" +else + exec /usr/bin/sh -x "J:/cmd-shim/fixtures/src.sh.args" "$@" +fi + +`; + +exports[`explicit shebang with args, linking to another drive on Windows > shim files > sh.args.shim.cmd 1`] = ` + +@SETLOCAL +@IF EXIST "%~dp0\\/usr/bin/sh.exe" ( + "%~dp0\\/usr/bin/sh.exe" -x "J:\\cmd-shim\\fixtures\\src.sh.args" %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + /usr/bin/sh -x "J:\\cmd-shim\\fixtures\\src.sh.args" %* +) + +`; + +exports[`explicit shebang with args, linking to another drive on Windows > shim files > sh.args.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir//usr/bin/sh$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir//usr/bin/sh$exe" -x "J:/cmd-shim/fixtures/src.sh.args" $args + } else { + & "$basedir//usr/bin/sh$exe" -x "J:/cmd-shim/fixtures/src.sh.args" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "/usr/bin/sh$exe" -x "J:/cmd-shim/fixtures/src.sh.args" $args + } else { + & "/usr/bin/sh$exe" -x "J:/cmd-shim/fixtures/src.sh.args" $args + } + $ret=$LASTEXITCODE +} +exit $ret + +`; + +exports[`explicit shebang with prog args > shim files > sh.args.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +if [ -n "$exe" ] && [ -x "$basedir//usr/bin/sh.exe" ]; then + exec "$basedir//usr/bin/sh.exe" -x "$basedir_win/src.sh.args" hello "$@" +elif [ -x "$basedir//usr/bin/sh" ]; then + exec "$basedir//usr/bin/sh" -x "$basedir/src.sh.args" hello "$@" +elif command -v /usr/bin/sh >/dev/null 2>&1; then + exec /usr/bin/sh -x "$basedir/src.sh.args" hello "$@" +elif [ -n "$exe" ] && command -v /usr/bin/sh.exe >/dev/null 2>&1; then + exec /usr/bin/sh.exe -x "$basedir_win/src.sh.args" hello "$@" +else + exec /usr/bin/sh -x "$basedir/src.sh.args" hello "$@" +fi + +`; + +exports[`explicit shebang with prog args > shim files > sh.args.shim.cmd 1`] = ` + +@SETLOCAL +@IF EXIST "%~dp0\\/usr/bin/sh.exe" ( + "%~dp0\\/usr/bin/sh.exe" -x "%~dp0\\src.sh.args" hello %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + /usr/bin/sh -x "%~dp0\\src.sh.args" hello %* +) + +`; + +exports[`explicit shebang with prog args > shim files > sh.args.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir//usr/bin/sh$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir//usr/bin/sh$exe" -x "$basedir/src.sh.args" hello $args + } else { + & "$basedir//usr/bin/sh$exe" -x "$basedir/src.sh.args" hello $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "/usr/bin/sh$exe" -x "$basedir/src.sh.args" hello $args + } else { + & "/usr/bin/sh$exe" -x "$basedir/src.sh.args" hello $args + } + $ret=$LASTEXITCODE +} +exit $ret + +`; + +exports[`no cmd file > shim files > exe.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +exec "$basedir/src.exe" "$@" +exit $? + +`; + +exports[`no cmd file > shim files > exe.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +# Support pipeline input +if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/src.exe" $args +} else { + & "$basedir/src.exe" $args +} +exit $LASTEXITCODE + +`; + +exports[`no shebang > shim files > exe.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +exec "$basedir/src.exe" "$@" +exit $? + +`; + +exports[`no shebang > shim files > exe.shim.cmd 1`] = ` + +@SETLOCAL +@"%~dp0\\src.exe" %* + +`; + +exports[`no shebang > shim files > exe.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +# Support pipeline input +if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/src.exe" $args +} else { + & "$basedir/src.exe" $args +} +exit $LASTEXITCODE + +`; + +exports[`shebang with -S > shim files > from.env.s.shim 1`] = ` + +#!/bin/sh +# Resolve $0 through symlinks so basedir is the shim's real directory. +# Cap hops at the kernel's ELOOP limit so a cycle cannot hang the shim. +# +# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, and +# uname go through \`command -p\`, which searches the system default path instead. +# A dependency's bin cannot stand in for one of them and take over the shim +# before it reaches its target. Directories come from \`\${link%/*}\`, which needs +# no helper at all. +link="$0" +# \`\${link%/*}\` needs a separator to strip. A bare name came from a PATH lookup +# and stands for a file in the current directory. +case "$link" in + */*|*\\\\*) ;; + *) link="./$link" ;; +esac +hops=0 +while [ -L "$link" ] && [ "$hops" -lt 40 ]; do + hops=$((hops+1)) + target=$(command -p readlink "$link") + case "$target" in + /*) link="$target" ;; + *) link="\${link%/*}/$target" ;; + esac +done +basedir=$(echo "$link" | command -p sed -e 's,\\\\,/,g') +basedir="\${basedir%/*}" +basedir_win="$basedir" +exe="" +msys="" + +case \`command -p uname -a\` in + *CYGWIN*|*MINGW*|*MSYS*) + if command -v cygpath > /dev/null 2>&1; then + basedir_win=\`cygpath -w "$basedir"\` + fi + exe=".exe" + msys="true" + ;; + *WSL2*) + if command -v wslpath > /dev/null 2>&1; then + basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" + if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then + basedir_win="$basedir" + else + exe=".exe" + fi + fi + ;; +esac + +if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then + exec "$basedir/node.exe" --expose_gc "$basedir_win/from.env.S" "$@" +elif [ -x "$basedir/node" ]; then + exec "$basedir/node" --expose_gc "$basedir/from.env.S" "$@" +elif command -v node >/dev/null 2>&1; then + exec node --expose_gc "$basedir/from.env.S" "$@" +elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then + exec node.exe --expose_gc "$basedir_win/from.env.S" "$@" +else + exec node --expose_gc "$basedir/from.env.S" "$@" +fi + +`; + +exports[`shebang with -S > shim files > from.env.s.shim.cmd 1`] = ` + +@SETLOCAL +@IF EXIST "%~dp0\\node.exe" ( + "%~dp0\\node.exe" --expose_gc "%~dp0\\from.env.S" %* +) ELSE ( + @SET PATHEXT=%PATHEXT:;.JS;=;% + node --expose_gc "%~dp0\\from.env.S" %* +) + +`; + +exports[`shebang with -S > shim files > from.env.s.shim.ps1 1`] = ` + +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" --expose_gc "$basedir/from.env.S" $args + } else { + & "$basedir/node$exe" --expose_gc "$basedir/from.env.S" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" --expose_gc "$basedir/from.env.S" $args + } else { + & "node$exe" --expose_gc "$basedir/from.env.S" $args + } + $ret=$LASTEXITCODE +} +exit $ret + +`; diff --git a/pnpm11/bins/cmd-shim/test/tsconfig.json b/pnpm11/bins/cmd-shim/test/tsconfig.json new file mode 100644 index 0000000000..67ce5e1d0e --- /dev/null +++ b/pnpm11/bins/cmd-shim/test/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "../node_modules/.test.lib", + "rootDir": "..", + "isolatedModules": true + }, + "include": [ + "**/*.ts", + "../../../__typings__/**/*.d.ts" + ], + "references": [ + { + "path": ".." + } + ] +} diff --git a/pnpm11/bins/cmd-shim/tsconfig.json b/pnpm11/bins/cmd-shim/tsconfig.json new file mode 100644 index 0000000000..29411fc675 --- /dev/null +++ b/pnpm11/bins/cmd-shim/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@pnpm/tsconfig", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts", + "../../__typings__/**/*.d.ts" + ], + "references": [ + { + "path": "../../fs/graceful-fs" + } + ] +} diff --git a/pnpm11/bins/cmd-shim/tsconfig.lint.json b/pnpm11/bins/cmd-shim/tsconfig.lint.json new file mode 100644 index 0000000000..1bbe711971 --- /dev/null +++ b/pnpm11/bins/cmd-shim/tsconfig.lint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src/**/*.ts", + "test/**/*.ts", + "../../__typings__/**/*.d.ts" + ] +} diff --git a/pnpm11/bins/linker/package.json b/pnpm11/bins/linker/package.json index a5f302bc7d..d98802bdf2 100644 --- a/pnpm11/bins/linker/package.json +++ b/pnpm11/bins/linker/package.json @@ -33,6 +33,7 @@ ".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest" }, "dependencies": { + "@pnpm/bins.cmd-shim": "workspace:*", "@pnpm/bins.resolver": "workspace:*", "@pnpm/error": "workspace:*", "@pnpm/fs.read-modules-dir": "workspace:*", @@ -40,7 +41,6 @@ "@pnpm/pkg-manifest.utils": "workspace:*", "@pnpm/types": "workspace:*", "@pnpm/workspace.project-manifest-reader": "workspace:*", - "@zkochan/cmd-shim": "catalog:", "@zkochan/rimraf": "catalog:", "bin-links": "catalog:", "is-subdir": "catalog:", diff --git a/pnpm11/bins/linker/src/index.ts b/pnpm11/bins/linker/src/index.ts index 9bc64d07b2..f6133358d0 100644 --- a/pnpm11/bins/linker/src/index.ts +++ b/pnpm11/bins/linker/src/index.ts @@ -2,6 +2,7 @@ import { existsSync, promises as fs } from 'node:fs' import { createRequire } from 'node:module' import path from 'node:path' +import { cmdShim, getExeExtension, isShimPointingAt } from '@pnpm/bins.cmd-shim' import { type Command, getBinsFromPackageManifest, pkgOwnsBin } from '@pnpm/bins.resolver' import { PnpmError } from '@pnpm/error' import { readModulesDir } from '@pnpm/fs.read-modules-dir' @@ -10,7 +11,6 @@ import { readPackageJsonFromDir } from '@pnpm/pkg-manifest.reader' import { getAllDependenciesFromManifest } from '@pnpm/pkg-manifest.utils' import type { DependencyManifest, EngineDependency, ProjectManifest } from '@pnpm/types' import { safeReadProjectManifestOnly } from '@pnpm/workspace.project-manifest-reader' -import { cmdShim, isShimPointingAt } from '@zkochan/cmd-shim' import { rimraf } from '@zkochan/rimraf' import fixBin from 'bin-links/lib/fix-bin.js' import { isSubdir } from 'is-subdir' @@ -511,18 +511,6 @@ async function hasWindowsShebang (file: string): Promise { } } -function getExeExtension (): string { - let cmdExtension - - if (process.env.PATHEXT) { - cmdExtension = process.env.PATHEXT - .split(path.delimiter) - .find(ext => ext.toUpperCase() === '.EXE') - } - - return cmdExtension ?? '.exe' -} - async function safeReadPkgJson (pkgDir: string): Promise { try { return await readPackageJsonFromDir(pkgDir) as DependencyManifest diff --git a/pnpm11/bins/linker/test/index.ts b/pnpm11/bins/linker/test/index.ts index 066523303c..1c03f5504d 100644 --- a/pnpm11/bins/linker/test/index.ts +++ b/pnpm11/bins/linker/test/index.ts @@ -4,8 +4,8 @@ import fs from 'node:fs' import path from 'node:path' import { beforeEach, describe, expect, jest, test } from '@jest/globals' +import { cmdShim } from '@pnpm/bins.cmd-shim' import { fixtures } from '@pnpm/test-fixtures' -import { cmdShim } from '@zkochan/cmd-shim' import { cmdExtension as CMD_EXTENSION } from 'cmd-extension' import isWindows from 'is-windows' import normalizePath from 'normalize-path' diff --git a/pnpm11/bins/linker/tsconfig.json b/pnpm11/bins/linker/tsconfig.json index f3bcefa983..ed5710b156 100644 --- a/pnpm11/bins/linker/tsconfig.json +++ b/pnpm11/bins/linker/tsconfig.json @@ -33,6 +33,9 @@ { "path": "../../workspace/project-manifest-reader" }, + { + "path": "../cmd-shim" + }, { "path": "../resolver" } diff --git a/pnpm11/engine/pm/commands/test/self-updater/selfUpdate.test.ts b/pnpm11/engine/pm/commands/test/self-updater/selfUpdate.test.ts index 6cc0f73a66..4c26e5cf19 100644 --- a/pnpm11/engine/pm/commands/test/self-updater/selfUpdate.test.ts +++ b/pnpm11/engine/pm/commands/test/self-updater/selfUpdate.test.ts @@ -1516,7 +1516,7 @@ describe('linkExePlatformBinary', () => { // Regression coverage for https://github.com/pnpm/pnpm/issues/11486 — the // `pn` / `pnpx` / `pnx` aliases were broken in MSYS2 / Git Bash on Windows. // Root cause: linkExePlatformBinary pointed those bin entries at .cmd files, - // and @zkochan/cmd-shim's Bash shim for a .cmd source bounces through + // and @pnpm/bins.cmd-shim's Bash shim for a .cmd source bounces through // `exec cmd /C "...target.cmd" "$@"`. MSYS2's argument-conversion runtime // mangles the lone `/C` switch into a Windows path before cmd.exe sees it, // so cmd.exe finds no /C or /K and falls into interactive mode (printing its diff --git a/pnpm11/installing/commands/test/saveCatalog.ts b/pnpm11/installing/commands/test/saveCatalog.ts index 12ac4c2875..9d33bd029b 100644 --- a/pnpm11/installing/commands/test/saveCatalog.ts +++ b/pnpm11/installing/commands/test/saveCatalog.ts @@ -84,13 +84,15 @@ test('saveCatalogName works with different protocols', async () => { .intercept({ path: '/kevva/is-positive', method: 'HEAD' }) .reply(200) - const options = createOptions() + // Override the TLS and socket defaults to keep requests on the global mock dispatcher. + const options = { ...createOptions(), strictSsl: true, maxSockets: 0 } options.registriesByScope['@jsr'] = 'https://npm.jsr.io/' await add.handler(options, [ '@pnpm.e2e/foo@100.1.0', 'jsr:@rus/greet@0.0.3', 'github:kevva/is-positive#97edff6', ]) + getMockAgent().assertNoPendingInterceptors() expect(loadJsonFileSync('package.json')).toHaveProperty(['dependencies'], { '@pnpm.e2e/foo': 'catalog:', diff --git a/pnpm11/pnpm/artifacts/exe/package.json b/pnpm11/pnpm/artifacts/exe/package.json index 9ee17200ba..7cb0f9ea53 100644 --- a/pnpm11/pnpm/artifacts/exe/package.json +++ b/pnpm11/pnpm/artifacts/exe/package.json @@ -49,9 +49,9 @@ }, "devDependencies": { "@jest/globals": "catalog:", + "@pnpm/bins.cmd-shim": "workspace:*", "@pnpm/exe": "workspace:*", "@pnpm/jest-config": "workspace:*", - "@zkochan/cmd-shim": "catalog:", "execa": "catalog:" }, "jest": { diff --git a/pnpm11/pnpm/artifacts/exe/test/setup.test.ts b/pnpm11/pnpm/artifacts/exe/test/setup.test.ts index db8cd74018..32813b8593 100644 --- a/pnpm11/pnpm/artifacts/exe/test/setup.test.ts +++ b/pnpm11/pnpm/artifacts/exe/test/setup.test.ts @@ -4,7 +4,7 @@ import os from 'node:os' import path from 'node:path' import { describe, expect, test } from '@jest/globals' -import { cmdShim } from '@zkochan/cmd-shim' +import { cmdShim } from '@pnpm/bins.cmd-shim' import { familySync } from 'detect-libc' // @ts-expect-error — JS helper without type declarations @@ -252,7 +252,7 @@ winBashTest('aliases run from Bash (Git Bash / MSYS2) without dropping into inte const pkg = JSON.parse(fs.readFileSync(path.join(sandbox, 'package.json'), 'utf8')) // Mirror what `pnpm self-update` does in the global bin: feed each bin - // entry into @zkochan/cmd-shim and let it write the Bash / cmd / pwsh + // entry into @pnpm/bins.cmd-shim and let it write the Bash / cmd / pwsh // shims. Using cmd-shim here (the same lib pnpm's bin linker uses) is what // lets this repro the real-world chain rather than just asserting the // package.json shape.