refactor: move cmd-shim into the pnpm monorepo (#14893)
Import cmd-shim source, tests, and snapshots from pnpm/cmd-shim at 83f9b9449c2adeec26e1cc2d03ef0fe82d2e41ca. Maintaining the shim alongside its consumers removes the need for coordinated changes across repositories. Publish the package as `@pnpm/bins.cmd-shim` and replace external dependencies with workspace links. Preserve the original BSD-2-Clause license, including its copyright notice, and keep the manifest updater from replacing it with MIT. Reuse `@pnpm/fs.graceful-fs` and integrate the existing Node.js tests with the workspace test scripts. Retain the existing snapshots and align the version with the TypeScript workspace's required 1100.x band.
This commit is contained in:
1 parent
a8ade4902f
commit
224aed744b
26 files changed
+3246
-43
No files matched your search
@@ -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`.
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -370,6 +370,7 @@ async function updateManifest (workspaceDir: string, manifest: ProjectManifest,
|
||||
let scripts: Record<string, string>
|
||||
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: {
|
||||
|
||||
+8
-1
@@ -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",
|
||||
|
||||
Generated
+44
-18
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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<void>`
|
||||
|
||||
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<void>`
|
||||
|
||||
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)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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<Pick<Options, keyof typeof DEFAULT_OPTIONS>> & {
|
||||
fs_: FsPromises
|
||||
}
|
||||
|
||||
type FsPromises = Pick<typeof fs.promises, 'chmod' | 'mkdir' | 'readFile' | 'stat' | 'unlink' | 'writeFile'>
|
||||
|
||||
/**
|
||||
* 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<string, string>}
|
||||
*/
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<RuntimeInfo> {
|
||||
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('/')}`
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
`;
|
||||
@@ -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)
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
Executable
+284
@@ -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' ? /$(?<!\r)\n/ugm : /$\r\n/ugm
|
||||
let content = await fs.promises.readFile(fileName, 'utf8')
|
||||
|
||||
assert.equal(invalidLineEnding.test(content), false, 'unexpected line ending')
|
||||
// Normalize cmd extension casing for cross-platform snapshot consistency
|
||||
if (cmdExtension !== '.cmd') {
|
||||
content = content.replaceAll(cmdExtension, '.cmd')
|
||||
}
|
||||
// Strip the target marker line — it contains a platform-dependent absolute path.
|
||||
// The marker is tested directly by the isShimPointingAt tests.
|
||||
content = content.replace(/# cmd-shim-target=.*\n/, '')
|
||||
t.assert.snapshot(content)
|
||||
})
|
||||
}
|
||||
|
||||
describe('isShimPointingAt', () => {
|
||||
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`)
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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": ".."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../fs/graceful-fs"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../__typings__/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -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:",
|
||||
|
||||
@@ -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<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
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<DependencyManifest | null> {
|
||||
try {
|
||||
return await readPackageJsonFromDir(pkgDir) as DependencyManifest
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
{
|
||||
"path": "../../workspace/project-manifest-reader"
|
||||
},
|
||||
{
|
||||
"path": "../cmd-shim"
|
||||
},
|
||||
{
|
||||
"path": "../resolver"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:',
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user