Skip to content

Commit f17dd1f

Browse files
feat: experimental binary source mapping (#5)
* Experimentally adds binary source mapping for built binaries - Syntax: `npx bin-path dist.js:::src.ts --foo bar` * Refactors helpers into their own file and adds separate tests
1 parent ba0c0db commit f17dd1f

15 files changed

+524
-149
lines changed

.xo-config.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"arrow-parens": "off",
44
"quotes": ["error", "double"],
55
"object-curly-spacing": ["error", "always"],
6-
"object-shorthand": ["error", "always", { "avoidExplicitReturnArrows": false }]
6+
"object-shorthand": ["error", "always", { "avoidExplicitReturnArrows": false }],
7+
"unicorn/no-process-exit": "off"
78
},
89
"overrides": [
910
{
@@ -17,6 +18,12 @@
1718
"n/file-extension-in-import": ["error", "always", { ".ts": "always" }],
1819
"@typescript-eslint/prefer-regexp-exec": "off"
1920
}
21+
},
22+
{
23+
"files": "test/fixtures/**/*.*",
24+
"rules": {
25+
"import/no-extraneous-dependencies": "off"
26+
}
2027
}
2128
]
2229
}

package.json

+12-5
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,9 @@
3434
],
3535
"scripts": {
3636
"prepare": "npm run build",
37-
"build": "tsc -p tsconfig.build.json && execify --pkg --fix-shebangs",
37+
"build": "tsc -p tsconfig.build.json && execify --pkg --fix-shebang",
3838
"lint": "xo",
39-
"test": "listr 'tsc --noEmit' 'c8 npm run ava'",
40-
"ava": "cross-env NODE_OPTIONS='--loader=tsx --no-warnings=ExperimentalWarning' ava"
39+
"test": "listr 'tsc --noEmit' 'c8 ava'"
4140
},
4241
"ava": {
4342
"files": [
@@ -46,8 +45,12 @@
4645
"extensions": {
4746
"ts": "module"
4847
},
48+
"environmentVariables": {
49+
"concurrency": "5"
50+
},
4951
"nodeArguments": [
50-
"--loader=tsx",
52+
"--loader=ts-node/esm",
53+
"--loader=esmock",
5154
"--no-warnings=ExperimentalWarning"
5255
]
5356
},
@@ -56,15 +59,19 @@
5659
"get-bin-path": "^10.0.0"
5760
},
5861
"devDependencies": {
62+
"@shopify/semaphore": "^3.0.2",
5963
"@tommy-mitchell/tsconfig": "^1.1.0",
6064
"@types/node": "^16.17",
6165
"ava": "^5.3.1",
6266
"c8": "^8.0.1",
6367
"cross-env": "^7.0.3",
68+
"esmock": "^2.3.6",
6469
"execify-cli": "^0.1.0",
70+
"is-executable": "^2.0.1",
6571
"listr-cli": "^0.3.0",
6672
"meow": "^11.0.0",
67-
"tsx": "^3.12.7",
73+
"testtriple": "^2.2.3",
74+
"ts-node": "^10.9.1",
6875
"typescript": "~5.1.6",
6976
"xo": "^0.54.2"
7077
}

readme.md

+43-6
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,10 @@ yarn global add bin-path-cli
2222
```
2323
</details>
2424

25-
*Uses top-level await. Requires Node 14.8 or higher.*
26-
2725
## Usage
2826

2927
```sh
30-
npx bin-path [binary-name] [arguments or flags…]
28+
npx bin-path [source-map] [binary-name] [arguments or flags…]
3129
```
3230

3331
### Curent Working Directory
@@ -54,10 +52,10 @@ $ npx bin-path --some-flag arg1 arg2
5452
```js
5553
// cli.js
5654
#!/usr/bin/env node
57-
import meow from "meow";
55+
import process from "node:process";
5856

59-
const {input} = meow({importMeta: import.meta});
60-
console.log(`Arguments: [${input.join(", ")}]`);
57+
const args = process.argv.slice(2);
58+
console.log(`Arguments: [${args.join(", ")}]`);
6159
```
6260

6361
```sh
@@ -114,6 +112,45 @@ $ npx bin-path --foo-flag
114112
```
115113
</details>
116114

115+
### Source Mapping
116+
117+
If you're writing your binary in a language that compiles to JavaScript (e.g. TypeScript) and would like to test your source binary, you can map the built file to the source file by using the following format as the first argument to `bin-path`:
118+
119+
```sh
120+
$ npx bin-path dist.js:::src.ts
121+
```
122+
123+
<details>
124+
<summary>Example</summary>
125+
126+
```
127+
\__ dist/
128+
\__ cli.js
129+
\__ src/
130+
\__ cli.ts
131+
\__ package.json
132+
```
133+
134+
</details>
135+
136+
#### Notice
137+
138+
This is an experimental feature, currently only available under the `beta` dist-tag. To use, install `bin-path-cli` with:
139+
140+
```sh
141+
npm install --global bin-path-cli@beta
142+
```
143+
144+
<details>
145+
<summary>Other Package Managers</summary>
146+
147+
```sh
148+
yarn global add bin-path-cli@beta
149+
```
150+
</details>
151+
152+
The feature is under-tested and the syntax is subject to change. If you have any problems or suggestings, please [file an issue](https://github.com/tommy-mitchell/bin-path-cli/issues/new).
153+
117154
## Related
118155

119156
- [get-bin-path](https://github.com/ehmicky/get-bin-path) - Get the current package's binary path.

src/cli.ts

+13-34
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,26 @@
1-
#!/usr/bin/env tsx
1+
#!/usr/bin/env ts-node-esm
22
import process from "node:process";
3-
import { getBinPath } from "get-bin-path";
43
import { execa, type ExecaError } from "execa";
5-
6-
type ExitOptions = {
7-
message?: string;
8-
exitCode?: number;
9-
};
10-
11-
/** Exit with an optional error message. */
12-
const exit = ({ message, exitCode = 1 }: ExitOptions = {}) => {
13-
if (message) {
14-
console.error(message);
15-
}
16-
17-
process.exit(exitCode);
18-
};
4+
import { exit, tryGetBinPath, tryMapBinPath } from "./helpers.js";
195

206
// First two arguments are Node binary and this binary
217
const args = process.argv.slice(2);
228

23-
/** Attempt to get a named binary from the first argument, or fallback to default binary. */
24-
const tryGetBinPath = async (binaryName?: string): ReturnType<typeof getBinPath> => {
25-
if (binaryName) {
26-
const binPath = await getBinPath({ name: binaryName });
27-
28-
if (binPath) {
29-
args.shift();
30-
return binPath;
31-
}
9+
const firstArg = args.at(0);
10+
const shouldMap = firstArg?.includes(":::");
3211

33-
return tryGetBinPath();
34-
}
35-
36-
return getBinPath();
37-
};
12+
if (shouldMap) {
13+
args.shift();
14+
}
3815

39-
// First argument could be a named binary to use
40-
const maybeBinaryName = args.at(0);
41-
const binPath = await tryGetBinPath(maybeBinaryName);
16+
let binPath = await tryGetBinPath({ args });
4217

4318
if (!binPath) {
44-
exit({ message: "No binary found. Usage: `$ npx bin-path [binary-name] [arguments or flags…]`" });
19+
exit({ message: "No binary found. Usage: `$ npx bin-path [source-map] [binary-name] [arguments or flags…]`" });
20+
}
21+
22+
if (shouldMap) {
23+
binPath = tryMapBinPath({ binPath: binPath!, map: firstArg! });
4524
}
4625

4726
try {

src/helpers.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import process from "node:process";
2+
import { getBinPath } from "get-bin-path";
3+
import type { Match } from "./types.js";
4+
5+
type ExitArgs = {
6+
message?: string;
7+
exitCode?: number;
8+
};
9+
10+
/** Exit with an optional error message. */
11+
export const exit = ({ message, exitCode = 1 }: ExitArgs = {}): never => {
12+
if (message) {
13+
console.error(message);
14+
}
15+
16+
process.exit(exitCode);
17+
};
18+
19+
type TryGetBinPathArgs = {
20+
args: typeof process.argv;
21+
};
22+
23+
/** Attempt to get a named binary from the first argument, or fallback to default binary. */
24+
export const tryGetBinPath = async ({ args }: TryGetBinPathArgs): ReturnType<typeof getBinPath> => {
25+
// First argument could be a named binary to use
26+
const binaryName = args.at(0);
27+
28+
if (binaryName) {
29+
const binPath = await getBinPath({ name: binaryName });
30+
31+
if (binPath) {
32+
args.shift();
33+
return binPath;
34+
}
35+
}
36+
37+
return getBinPath();
38+
};
39+
40+
/** https://regex101.com/r/OLVdtN/1 */
41+
const mapRegex = /(?<dPath>[^.]+)(?<dExt>\.\w+):::(?<sPath>[^.]+)(?<sExt>\.\w+)/;
42+
43+
type MapBinPathArgs = {
44+
binPath: string;
45+
map: string;
46+
};
47+
48+
/** Maps built binary to source binary if possible. */
49+
export const tryMapBinPath = ({ binPath, map }: MapBinPathArgs): string => {
50+
const match = map.match(mapRegex) as Match<"dPath" | "dExt" | "sPath" | "sExt">;
51+
52+
if (!match) {
53+
return binPath;
54+
}
55+
56+
const {
57+
dPath: distPath, dExt: distExtension,
58+
sPath: sourcePath, sExt: sourceExtension,
59+
} = match.groups;
60+
61+
return binPath.replace(distPath, sourcePath).replace(distExtension, sourceExtension);
62+
};

src/types.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Based on https://github.com/microsoft/TypeScript/issues/32098#issuecomment-1279645368
2+
type RegExpGroups<Groups extends string, Alternates extends string | undefined = undefined> = (
3+
RegExpMatchArray & {
4+
groups: (
5+
| { [name in Groups]: string }
6+
| { [name in NonNullable<Alternates>]?: string }
7+
| Record<string, string>
8+
);
9+
}
10+
);
11+
12+
export type Match<Groups extends string> = RegExpGroups<Groups> | null; // eslint-disable-line @typescript-eslint/ban-types
13+
14+
export type MatchAll<Groups extends string, Alternates extends string> = IterableIterator<RegExpGroups<Groups, Alternates>>;

0 commit comments

Comments
 (0)