@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/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
- function xImpl() {
4
- console.log("Hello, world!");
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
- xImpl();
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
- "version": "0.0.1",
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.0-beta.3",
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": "echo \"No linting configured\"",
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
+ });