@somewhatabstract/x 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +28 -0
- package/.github/workflows/codeql-analysis.yml +29 -29
- package/.github/workflows/dependabot-pr-approval.yml +36 -0
- package/.github/workflows/nodejs.yml +84 -86
- package/.github/workflows/release.yml +4 -7
- package/.vscode/settings.json +19 -0
- package/CHANGELOG.md +23 -0
- package/CONTRIBUTING.md +3 -3
- package/README.md +132 -1
- package/biome.json +39 -0
- package/dist/x.mjs +278 -3
- package/package.json +14 -4
- package/src/__tests__/build-environment.test.ts +285 -0
- package/src/__tests__/discover-packages.test.ts +196 -0
- package/src/__tests__/errors.test.ts +59 -0
- package/src/__tests__/execute-script.test.ts +1042 -0
- package/src/__tests__/find-matching-bins.test.ts +506 -0
- package/src/__tests__/find-workspace-root.test.ts +73 -0
- package/src/__tests__/is-node-executable.test.ts +125 -0
- package/src/__tests__/resolve-bin-path.test.ts +344 -0
- package/src/__tests__/x-impl.test.ts +306 -7
- package/src/__tests__/x.test.ts +236 -0
- package/src/bin/x.ts +55 -1
- package/src/build-environment.ts +98 -0
- package/src/discover-packages.ts +35 -0
- package/src/errors.ts +10 -0
- package/src/execute-script.ts +56 -0
- package/src/find-matching-bins.ts +72 -0
- package/src/find-workspace-root.ts +24 -0
- package/src/is-node-executable.ts +16 -0
- package/src/resolve-bin-path.ts +48 -0
- package/src/x-impl.ts +95 -4
- package/tsconfig-types.json +2 -4
- package/tsconfig.json +5 -13
- package/tsdown.config.ts +1 -1
- package/vitest.config.ts +1 -0
package/dist/x.mjs
CHANGED
|
@@ -1,12 +1,287 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import yargs from "yargs";
|
|
3
|
+
import { hideBin } from "yargs/helpers";
|
|
4
|
+
import { getPackages } from "@manypkg/get-packages";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import * as fs from "node:fs/promises";
|
|
7
|
+
import * as path$1 from "node:path";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { findRoot } from "@manypkg/find-root";
|
|
10
|
+
|
|
11
|
+
//#region src/errors.ts
|
|
12
|
+
/**
|
|
13
|
+
* Error class for known/expected errors that should be displayed to the user
|
|
14
|
+
* without a stack trace.
|
|
15
|
+
*/
|
|
16
|
+
var HandledError = class extends Error {
|
|
17
|
+
constructor(message, options) {
|
|
18
|
+
super(message, options);
|
|
19
|
+
this.name = "HandledError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/discover-packages.ts
|
|
25
|
+
/**
|
|
26
|
+
* Discover all packages in the workspace using @manypkg/get-packages.
|
|
27
|
+
* Supports multiple package managers: npm, yarn, pnpm, lerna, bun, rush.
|
|
28
|
+
*
|
|
29
|
+
* @param workspaceRoot - The absolute path to the workspace root
|
|
30
|
+
* @returns Array of package information
|
|
31
|
+
* @throws {HandledError} If package discovery fails
|
|
32
|
+
*/
|
|
33
|
+
async function discoverPackages(workspaceRoot) {
|
|
34
|
+
try {
|
|
35
|
+
return (await getPackages(workspaceRoot)).packages.map((pkg) => ({
|
|
36
|
+
name: pkg.packageJson.name,
|
|
37
|
+
path: pkg.dir,
|
|
38
|
+
version: pkg.packageJson.version || "unknown"
|
|
39
|
+
}));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof HandledError) throw error;
|
|
42
|
+
throw new HandledError(`Failed to discover packages`, { cause: error });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/build-environment.ts
|
|
48
|
+
/**
|
|
49
|
+
* Build environment variables for script execution that mimics npm/pnpm behavior.
|
|
50
|
+
* The environment is set as if the package were installed at the workspace root.
|
|
51
|
+
*
|
|
52
|
+
* @param workspaceRoot - Path to the workspace root
|
|
53
|
+
* @param currentEnv - Current environment variables (usually process.env)
|
|
54
|
+
* @returns Environment object to pass to child_process.spawn
|
|
55
|
+
*/
|
|
56
|
+
async function buildEnvironment(workspaceRoot, currentEnv) {
|
|
57
|
+
let workspacePackageJson = {};
|
|
58
|
+
try {
|
|
59
|
+
const packageJsonPath = path$1.join(workspaceRoot, "package.json");
|
|
60
|
+
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
61
|
+
workspacePackageJson = JSON.parse(content);
|
|
62
|
+
} catch {}
|
|
63
|
+
let ourPackageVersion = "0.0.0-development";
|
|
64
|
+
try {
|
|
65
|
+
const ownPackageJsonPath = path$1.join(__dirname, "..", "package.json");
|
|
66
|
+
const ownPackageJsonContent = await fs.readFile(ownPackageJsonPath, "utf-8");
|
|
67
|
+
const ownPackageJson = JSON.parse(ownPackageJsonContent);
|
|
68
|
+
if (typeof ownPackageJson.version === "string") ourPackageVersion = ownPackageJson.version;
|
|
69
|
+
} catch {}
|
|
70
|
+
const env = {
|
|
71
|
+
...currentEnv,
|
|
72
|
+
PATH: [path$1.join(workspaceRoot, "node_modules", ".bin"), currentEnv.PATH].filter(Boolean).join(path$1.delimiter),
|
|
73
|
+
npm_command: "exec",
|
|
74
|
+
npm_execpath: process.execPath,
|
|
75
|
+
npm_node_execpath: process.execPath,
|
|
76
|
+
NODE: process.execPath,
|
|
77
|
+
INIT_CWD: process.cwd(),
|
|
78
|
+
npm_config_user_agent: `x/${ourPackageVersion} node/${process.version} ${process.platform} ${process.arch}`
|
|
79
|
+
};
|
|
80
|
+
if (workspacePackageJson.name) env.npm_package_name = workspacePackageJson.name;
|
|
81
|
+
if (workspacePackageJson.version) env.npm_package_version = workspacePackageJson.version;
|
|
82
|
+
if (workspacePackageJson.description) env.npm_package_description = workspacePackageJson.description;
|
|
83
|
+
for (const field of [
|
|
84
|
+
"author",
|
|
85
|
+
"license",
|
|
86
|
+
"homepage",
|
|
87
|
+
"repository",
|
|
88
|
+
"bugs",
|
|
89
|
+
"keywords"
|
|
90
|
+
]) if (workspacePackageJson[field]) {
|
|
91
|
+
const value = workspacePackageJson[field];
|
|
92
|
+
env[`npm_package_${field}`] = typeof value === "string" ? value : JSON.stringify(value);
|
|
93
|
+
}
|
|
94
|
+
return env;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/is-node-executable.ts
|
|
99
|
+
/**
|
|
100
|
+
* Determine if a bin file should be invoked via the Node executable,
|
|
101
|
+
* based on its file extension (case-insensitive).
|
|
102
|
+
* Matches npm's behavior for .js, .mjs, and .cjs files.
|
|
103
|
+
*
|
|
104
|
+
* @param binPath - The path to the bin file
|
|
105
|
+
* @returns True if the file should be invoked via node
|
|
106
|
+
*/
|
|
107
|
+
function isNodeExecutable(binPath) {
|
|
108
|
+
const lower = binPath.toLowerCase();
|
|
109
|
+
return lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/execute-script.ts
|
|
114
|
+
/**
|
|
115
|
+
* Execute a bin script with the given arguments.
|
|
116
|
+
* Executes the script as if the package were installed at the workspace root,
|
|
117
|
+
* with npm/pnpm-style environment variables.
|
|
118
|
+
*
|
|
119
|
+
* @param bin - The bin info to execute
|
|
120
|
+
* @param args - Arguments to pass to the script
|
|
121
|
+
* @param workspaceRoot - Path to the workspace root
|
|
122
|
+
* @returns A promise that resolves with the exit code
|
|
123
|
+
*/
|
|
124
|
+
async function executeScript(bin, args, workspaceRoot) {
|
|
125
|
+
const env = await buildEnvironment(workspaceRoot, process.env);
|
|
126
|
+
const [executable, spawnArgs] = isNodeExecutable(bin.binPath) ? [process.execPath, [bin.binPath, ...args]] : [bin.binPath, args];
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const child = spawn(executable, spawnArgs, {
|
|
129
|
+
stdio: "inherit",
|
|
130
|
+
env
|
|
131
|
+
});
|
|
132
|
+
child.on("error", (_) => {
|
|
133
|
+
resolve(1);
|
|
134
|
+
});
|
|
135
|
+
child.on("exit", (code, signal) => {
|
|
136
|
+
if (signal) resolve(1);
|
|
137
|
+
else resolve(code ?? 1);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/resolve-bin-path.ts
|
|
144
|
+
/**
|
|
145
|
+
* Resolve a bin script to the actual file path.
|
|
146
|
+
*
|
|
147
|
+
* This function ensures that the resolved bin path is within the package
|
|
148
|
+
* directory to prevent path traversal issues.
|
|
149
|
+
*
|
|
150
|
+
* @param pkg - The package information containing the path to the package
|
|
151
|
+
* @param bin - The bin entry from package.json, which can be a string or an
|
|
152
|
+
* object
|
|
153
|
+
* @param binName - The name of the bin as specified in package.json
|
|
154
|
+
* @returns The resolved absolute path to the bin script, or null if invalid
|
|
155
|
+
|
|
156
|
+
*/
|
|
157
|
+
function resolveBinPath(pkg, bin, binName) {
|
|
158
|
+
const binPath = !bin ? null : typeof bin === "string" && pkg.name === binName ? bin : typeof bin === "object" && bin[binName] ? bin[binName] : null;
|
|
159
|
+
if (!binPath) return null;
|
|
160
|
+
const packageDir = path.resolve(pkg.path);
|
|
161
|
+
const resolvedBinPath = path.resolve(pkg.path, binPath);
|
|
162
|
+
if (resolvedBinPath !== packageDir && !resolvedBinPath.startsWith(packageDir + path.sep)) return null;
|
|
163
|
+
return resolvedBinPath;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/find-matching-bins.ts
|
|
168
|
+
/**
|
|
169
|
+
* Find all packages that have a bin script matching the given name.
|
|
170
|
+
*
|
|
171
|
+
* @param packages - List of packages from discoverPackages
|
|
172
|
+
* @param binName - Name of the bin script to find
|
|
173
|
+
* @returns Array of matching bin information
|
|
174
|
+
*/
|
|
175
|
+
async function findMatchingBins(packages, binName) {
|
|
176
|
+
const matches = [];
|
|
177
|
+
for (const pkg of packages) try {
|
|
178
|
+
const packageJsonPath = path$1.join(pkg.path, "package.json");
|
|
179
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
|
|
180
|
+
const bin = JSON.parse(packageJsonContent).bin;
|
|
181
|
+
const resolvedBinPath = resolveBinPath(pkg, bin, binName);
|
|
182
|
+
if (!resolvedBinPath) continue;
|
|
183
|
+
matches.push({
|
|
184
|
+
packageName: pkg.name,
|
|
185
|
+
packagePath: pkg.path,
|
|
186
|
+
binName,
|
|
187
|
+
binPath: resolvedBinPath
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error instanceof SyntaxError) console.warn(`Warning: Failed to parse package.json for package "${pkg.name}" at "${pkg.path}": invalid JSON.`);
|
|
191
|
+
else if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {} else console.warn(`Warning: Could not read package.json for package "${pkg.name}" at "${pkg.path}":`, error);
|
|
192
|
+
}
|
|
193
|
+
return matches;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/find-workspace-root.ts
|
|
198
|
+
/**
|
|
199
|
+
* Find the workspace root using @manypkg/find-root.
|
|
200
|
+
* Supports multiple package managers: npm, yarn, pnpm, lerna, bun, rush.
|
|
201
|
+
*
|
|
202
|
+
* @param startDir - Directory to start searching from (defaults to cwd)
|
|
203
|
+
* @returns The absolute path to the workspace root
|
|
204
|
+
* @throws {HandledError} If workspace root cannot be found
|
|
205
|
+
*/
|
|
206
|
+
async function findWorkspaceRoot(startDir = process.cwd()) {
|
|
207
|
+
try {
|
|
208
|
+
return (await findRoot(startDir)).rootDir;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw new HandledError("Could not find workspace root. Make sure you're in a monorepo workspace.", { cause: error });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
2
215
|
//#region src/x-impl.ts
|
|
3
|
-
|
|
4
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Main implementation of the x command.
|
|
218
|
+
* Finds and executes a bin script from any package in the workspace.
|
|
219
|
+
*
|
|
220
|
+
* @param scriptName - Name of the bin script to execute
|
|
221
|
+
* @param args - Arguments to pass to the script
|
|
222
|
+
* @param options - Additional options
|
|
223
|
+
* @returns Result object with exit code
|
|
224
|
+
*/
|
|
225
|
+
async function xImpl(scriptName, args = [], options = {}) {
|
|
226
|
+
try {
|
|
227
|
+
if (!scriptName || !scriptName.trim()) throw new HandledError("Script name cannot be empty");
|
|
228
|
+
if (scriptName.includes("/") || scriptName.includes("\\")) throw new HandledError("Script name cannot contain path separators");
|
|
229
|
+
const workspaceRoot = await findWorkspaceRoot();
|
|
230
|
+
const packages = await discoverPackages(workspaceRoot);
|
|
231
|
+
if (packages.length === 0) throw new HandledError("No packages found in workspace. Is this a valid monorepo?");
|
|
232
|
+
const matchingBins = await findMatchingBins(packages, scriptName);
|
|
233
|
+
if (matchingBins.length === 0) throw new HandledError(`No bin script named "${scriptName}" found in any workspace package.`);
|
|
234
|
+
if (matchingBins.length > 1) {
|
|
235
|
+
console.error(`Multiple packages provide bin "${scriptName}". Please be more specific.`);
|
|
236
|
+
console.error("\nMatching packages:");
|
|
237
|
+
matchingBins.forEach((bin) => {
|
|
238
|
+
console.error(` - ${bin.packageName} (${bin.packagePath})`);
|
|
239
|
+
});
|
|
240
|
+
throw new HandledError(`Ambiguous bin name "${scriptName}". Found ${matchingBins.length} matches.`);
|
|
241
|
+
}
|
|
242
|
+
const bin = matchingBins[0];
|
|
243
|
+
if (options.dryRun) {
|
|
244
|
+
console.log(`Would execute: ${bin.binName} from ${bin.packageName}`);
|
|
245
|
+
console.log(` Binary: ${bin.binPath}`);
|
|
246
|
+
console.log(` Arguments: ${args.join(" ")}`);
|
|
247
|
+
return { exitCode: 0 };
|
|
248
|
+
}
|
|
249
|
+
return { exitCode: await executeScript(bin, args, workspaceRoot) };
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error instanceof HandledError) {
|
|
252
|
+
console.error(`Error: ${error.message}`);
|
|
253
|
+
return { exitCode: 1 };
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
5
257
|
}
|
|
6
258
|
|
|
7
259
|
//#endregion
|
|
8
260
|
//#region src/bin/x.ts
|
|
9
|
-
|
|
261
|
+
const argv = yargs(hideBin(process.argv)).usage("Usage: $0 <script-name> [...args]").command("$0 <script-name> [args..]", "Execute a bin script from any package in the workspace", (yargs) => {
|
|
262
|
+
return yargs.positional("script-name", {
|
|
263
|
+
describe: "Name of the bin script to execute",
|
|
264
|
+
type: "string",
|
|
265
|
+
demandOption: true
|
|
266
|
+
}).positional("args", {
|
|
267
|
+
describe: "Arguments to pass to the script",
|
|
268
|
+
type: "string",
|
|
269
|
+
array: true,
|
|
270
|
+
default: []
|
|
271
|
+
});
|
|
272
|
+
}).option("dry-run", {
|
|
273
|
+
alias: "d",
|
|
274
|
+
describe: "Show what would be executed without running it",
|
|
275
|
+
type: "boolean",
|
|
276
|
+
default: false
|
|
277
|
+
}).help().alias("help", "h").version().alias("version", "v").example("$0 tsc --noEmit", "Run TypeScript compiler from any package").example("$0 eslint src/", "Run ESLint from any package that provides it").example("$0 --dry-run jest", "Preview which jest would be executed").strict().parseSync();
|
|
278
|
+
const scriptName = argv["script-name"];
|
|
279
|
+
xImpl(scriptName, argv._ || [], { dryRun: argv["dry-run"] }).then((result) => {
|
|
280
|
+
process.exit(result.exitCode);
|
|
281
|
+
}).catch((error) => {
|
|
282
|
+
console.error("Unexpected error:", error);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
});
|
|
10
285
|
|
|
11
286
|
//#endregion
|
|
12
287
|
export { };
|
package/package.json
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
"name": "@somewhatabstract/x",
|
|
3
3
|
"private": false,
|
|
4
4
|
"publishConfig": {
|
|
5
|
-
"access": "public"
|
|
5
|
+
"access": "public",
|
|
6
|
+
"provenance": true
|
|
6
7
|
},
|
|
7
8
|
"bin": {
|
|
8
9
|
"x": "./dist/x.mjs"
|
|
9
10
|
},
|
|
10
|
-
"
|
|
11
|
+
"type": "module",
|
|
12
|
+
"version": "0.1.0",
|
|
11
13
|
"description": "Execute any bin defined by any package in a monorepo without needing to install that package",
|
|
12
14
|
"bugs": {
|
|
13
15
|
"url": "https://github.com/somewhatabstract/x/issues"
|
|
@@ -22,18 +24,26 @@
|
|
|
22
24
|
"yarn": "please-use-pnpm"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
27
|
+
"@biomejs/biome": "^2.4.3",
|
|
25
28
|
"@changesets/cli": "^2.29.8",
|
|
26
29
|
"@codecov/rollup-plugin": "^1.9.1",
|
|
27
30
|
"@types/node": "^20.19.29",
|
|
31
|
+
"@types/yargs": "^17.0.35",
|
|
28
32
|
"@vitest/coverage-v8": "^4.0.17",
|
|
29
|
-
"tsdown": "0.20.
|
|
33
|
+
"tsdown": "0.20.3",
|
|
30
34
|
"typescript": "^5.9.3",
|
|
31
35
|
"vitest": "^4.0.17"
|
|
32
36
|
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@manypkg/find-root": "^3.1.0",
|
|
39
|
+
"@manypkg/get-packages": "^3.1.0",
|
|
40
|
+
"yargs": "^18.0.0"
|
|
41
|
+
},
|
|
33
42
|
"scripts": {
|
|
34
43
|
"build": "tsdown",
|
|
35
44
|
"dev": "tsdown --watch",
|
|
36
|
-
"lint": "
|
|
45
|
+
"lint": "biome check",
|
|
46
|
+
"lint:fix": "biome check --write --unsafe",
|
|
37
47
|
"typecheck": "tsc --noEmit --project tsconfig.json",
|
|
38
48
|
"typewatch": "pnpm typecheck --watch",
|
|
39
49
|
"test": "vitest",
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import {beforeEach, describe, expect, it, vi} from "vitest";
|
|
4
|
+
import {buildEnvironment} from "../build-environment";
|
|
5
|
+
|
|
6
|
+
// Mock the fs module
|
|
7
|
+
vi.mock("node:fs/promises", () => ({
|
|
8
|
+
readFile: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe("buildEnvironment", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should preserve existing environment variables", async () => {
|
|
17
|
+
// Arrange
|
|
18
|
+
const workspaceRoot = "/test/workspace";
|
|
19
|
+
const currentEnv = {
|
|
20
|
+
EXISTING_VAR: "existing-value",
|
|
21
|
+
PATH: "/usr/bin:/bin",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
name: "test-workspace",
|
|
27
|
+
version: "1.0.0",
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
33
|
+
|
|
34
|
+
// Assert
|
|
35
|
+
expect(env.EXISTING_VAR).toBe("existing-value");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should prepend workspace node_modules/.bin to PATH", async () => {
|
|
39
|
+
// Arrange
|
|
40
|
+
const workspaceRoot = "/test/workspace";
|
|
41
|
+
const currentEnv = {
|
|
42
|
+
PATH: "/usr/bin:/bin",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
name: "test-workspace",
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Act
|
|
52
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
53
|
+
|
|
54
|
+
// Assert
|
|
55
|
+
const expectedPath = path.join(workspaceRoot, "node_modules", ".bin");
|
|
56
|
+
expect(env.PATH).toContain(expectedPath);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should set npm_command to exec", async () => {
|
|
60
|
+
// Arrange
|
|
61
|
+
const workspaceRoot = "/test/workspace";
|
|
62
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
63
|
+
|
|
64
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({}));
|
|
65
|
+
|
|
66
|
+
// Act
|
|
67
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
68
|
+
|
|
69
|
+
// Assert
|
|
70
|
+
expect(env.npm_command).toBe("exec");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should set INIT_CWD to current working directory", async () => {
|
|
74
|
+
// Arrange
|
|
75
|
+
const workspaceRoot = "/test/workspace";
|
|
76
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
77
|
+
const expectedCwd = process.cwd();
|
|
78
|
+
|
|
79
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({}));
|
|
80
|
+
|
|
81
|
+
// Act
|
|
82
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
83
|
+
|
|
84
|
+
// Assert
|
|
85
|
+
expect(env.INIT_CWD).toBe(expectedCwd);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should set npm_package_name from workspace package.json", async () => {
|
|
89
|
+
// Arrange
|
|
90
|
+
const workspaceRoot = "/test/workspace";
|
|
91
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
92
|
+
|
|
93
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
name: "my-workspace",
|
|
96
|
+
version: "2.5.0",
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Act
|
|
101
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
102
|
+
|
|
103
|
+
// Assert
|
|
104
|
+
expect(env.npm_package_name).toBe("my-workspace");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should set npm_package_version from workspace package.json", async () => {
|
|
108
|
+
// Arrange
|
|
109
|
+
const workspaceRoot = "/test/workspace";
|
|
110
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
111
|
+
|
|
112
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
113
|
+
JSON.stringify({
|
|
114
|
+
name: "my-workspace",
|
|
115
|
+
version: "2.5.0",
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Act
|
|
120
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
121
|
+
|
|
122
|
+
// Assert
|
|
123
|
+
expect(env.npm_package_version).toBe("2.5.0");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should set npm_execpath to node executable path", async () => {
|
|
127
|
+
// Arrange
|
|
128
|
+
const workspaceRoot = "/test/workspace";
|
|
129
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
130
|
+
|
|
131
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({}));
|
|
132
|
+
|
|
133
|
+
// Act
|
|
134
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
135
|
+
|
|
136
|
+
// Assert
|
|
137
|
+
expect(env.npm_execpath).toBe(process.execPath);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should include package name in npm_config_user_agent", async () => {
|
|
141
|
+
// Arrange
|
|
142
|
+
const workspaceRoot = "/test/workspace";
|
|
143
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
144
|
+
|
|
145
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({}));
|
|
146
|
+
|
|
147
|
+
// Act
|
|
148
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
149
|
+
|
|
150
|
+
// Assert
|
|
151
|
+
expect(env.npm_config_user_agent).toContain("x/");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should include node version in npm_config_user_agent", async () => {
|
|
155
|
+
// Arrange
|
|
156
|
+
const workspaceRoot = "/test/workspace";
|
|
157
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
158
|
+
|
|
159
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({}));
|
|
160
|
+
|
|
161
|
+
// Act
|
|
162
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
163
|
+
|
|
164
|
+
// Assert
|
|
165
|
+
expect(env.npm_config_user_agent).toContain("node/");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should set npm_command to exec when package.json is missing", async () => {
|
|
169
|
+
// Arrange
|
|
170
|
+
const workspaceRoot = "/test/workspace";
|
|
171
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
172
|
+
|
|
173
|
+
vi.mocked(fs.readFile).mockRejectedValue(
|
|
174
|
+
new Error("ENOENT: no such file"),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Act
|
|
178
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
179
|
+
|
|
180
|
+
// Assert
|
|
181
|
+
expect(env.npm_command).toBe("exec");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should set INIT_CWD when package.json is missing", async () => {
|
|
185
|
+
// Arrange
|
|
186
|
+
const workspaceRoot = "/test/workspace";
|
|
187
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
188
|
+
|
|
189
|
+
vi.mocked(fs.readFile).mockRejectedValue(
|
|
190
|
+
new Error("ENOENT: no such file"),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Act
|
|
194
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
195
|
+
|
|
196
|
+
// Assert
|
|
197
|
+
expect(env.INIT_CWD).toBe(process.cwd());
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should include npm_package_description if present", async () => {
|
|
201
|
+
// Arrange
|
|
202
|
+
const workspaceRoot = "/test/workspace";
|
|
203
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
204
|
+
|
|
205
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
206
|
+
JSON.stringify({
|
|
207
|
+
name: "my-workspace",
|
|
208
|
+
description: "A test workspace",
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Act
|
|
213
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
214
|
+
|
|
215
|
+
// Assert
|
|
216
|
+
expect(env.npm_package_description).toBe("A test workspace");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should convert object fields to JSON strings", async () => {
|
|
220
|
+
// Arrange
|
|
221
|
+
const workspaceRoot = "/test/workspace";
|
|
222
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
223
|
+
|
|
224
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
225
|
+
JSON.stringify({
|
|
226
|
+
name: "my-workspace",
|
|
227
|
+
repository: {
|
|
228
|
+
type: "git",
|
|
229
|
+
url: "https://github.com/test/repo",
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Act
|
|
235
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
236
|
+
|
|
237
|
+
// Assert
|
|
238
|
+
expect(env.npm_package_repository).toBe(
|
|
239
|
+
JSON.stringify({
|
|
240
|
+
type: "git",
|
|
241
|
+
url: "https://github.com/test/repo",
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should set npm_package_license from workspace package.json", async () => {
|
|
247
|
+
// Arrange
|
|
248
|
+
const workspaceRoot = "/test/workspace";
|
|
249
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
250
|
+
|
|
251
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
252
|
+
JSON.stringify({
|
|
253
|
+
name: "my-workspace",
|
|
254
|
+
license: "MIT",
|
|
255
|
+
author: "John Doe",
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Act
|
|
260
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
261
|
+
|
|
262
|
+
// Assert
|
|
263
|
+
expect(env.npm_package_license).toBe("MIT");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should set npm_package_author from workspace package.json", async () => {
|
|
267
|
+
// Arrange
|
|
268
|
+
const workspaceRoot = "/test/workspace";
|
|
269
|
+
const currentEnv = {PATH: "/usr/bin"};
|
|
270
|
+
|
|
271
|
+
vi.mocked(fs.readFile).mockResolvedValue(
|
|
272
|
+
JSON.stringify({
|
|
273
|
+
name: "my-workspace",
|
|
274
|
+
license: "MIT",
|
|
275
|
+
author: "John Doe",
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Act
|
|
280
|
+
const env = await buildEnvironment(workspaceRoot, currentEnv);
|
|
281
|
+
|
|
282
|
+
// Assert
|
|
283
|
+
expect(env.npm_package_author).toBe("John Doe");
|
|
284
|
+
});
|
|
285
|
+
});
|