bun-workspaces 1.9.0 → 1.11.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/AGENTS.md +537 -0
- package/README.md +4 -2
- package/package.json +1 -1
- package/src/2392.mjs +10 -1
- package/src/5166.mjs +1 -0
- package/src/affected/fileAffectedWorkspaces.mjs +7 -1
- package/src/affected/gitAffectedFiles.mjs +26 -1
- package/src/ai/mcp/bwMcpServer.mjs +5 -1
- package/src/ai/mcp/serverState.mjs +10 -1
- package/src/ai/mcp/tools.mjs +1 -1
- package/src/cli/commands/commandHandlerUtils.mjs +11 -10
- package/src/cli/commands/commands.mjs +1 -1
- package/src/cli/commands/handleSimpleCommands.mjs +7 -6
- package/src/cli/commands/listAffected.mjs +17 -13
- package/src/cli/commands/mcp.mjs +11 -1
- package/src/cli/commands/runScript/output/renderGroupedOutput.mjs +3 -2
- package/src/cli/commands/runScript/output/renderPlainOutput.mjs +4 -1
- package/src/cli/commands/runScript/scriptRunFlow.mjs +8 -3
- package/src/cli/createCli.mjs +8 -2
- package/src/cli/globalOptions/globalOptions.mjs +5 -4
- package/src/cli/index.d.ts +35 -2
- package/src/config/rootConfig/loadRootConfig.mjs +2 -1
- package/src/config/rootConfig/rootConfig.mjs +5 -9
- package/src/config/userEnvVars/userEnvVars.mjs +12 -1
- package/src/config/util/loadConfig.mjs +23 -2
- package/src/config/workspaceConfig/loadWorkspaceConfig.mjs +2 -1
- package/src/index.d.ts +11 -0
- package/src/internal/core/language/string/index.mjs +1 -0
- package/src/internal/core/language/string/sanitizeOutput.mjs +15 -0
- package/src/internal/core/runtime/tempFile.mjs +20 -2
- package/src/internal/generated/aiDocs/docs.mjs +19 -8
- package/src/project/implementations/fileSystemProject/affectedWorkspaces.mjs +2 -0
- package/src/project/implementations/fileSystemProject/fileSystemProject.mjs +39 -14
- package/src/project/implementations/projectBase.mjs +11 -17
- package/src/runScript/scriptExecution.mjs +1 -1
- package/src/runScript/workspaceScriptMetadata.mjs +24 -1
- package/src/workspaces/applyWorkspacePatternConfigs.mjs +6 -1
- package/src/workspaces/dependencyGraph/validateDependencyRules.mjs +14 -7
- package/src/workspaces/findWorkspaces.mjs +4 -0
- package/src/workspaces/workspacePattern.mjs +134 -46
|
@@ -5,13 +5,14 @@ import {
|
|
|
5
5
|
} from "./workspaceConfig.mjs";
|
|
6
6
|
import { WORKSPACE_CONFIG_FILE_NAME } from "../../8529.mjs";
|
|
7
7
|
|
|
8
|
-
const loadWorkspaceConfig = (workspacePath) => {
|
|
8
|
+
const loadWorkspaceConfig = (workspacePath, loadOptions = {}) => {
|
|
9
9
|
const config = loadConfig(
|
|
10
10
|
"workspace",
|
|
11
11
|
workspacePath,
|
|
12
12
|
WORKSPACE_CONFIG_FILE_NAME,
|
|
13
13
|
/* inlined export .WORKSPACE_CONFIG_PACKAGE_JSON_KEY */ "bw",
|
|
14
14
|
(content) => resolveWorkspaceConfig(content),
|
|
15
|
+
loadOptions,
|
|
15
16
|
);
|
|
16
17
|
return config ?? createDefaultWorkspaceConfig();
|
|
17
18
|
};
|
package/src/index.d.ts
CHANGED
|
@@ -600,6 +600,17 @@ export type CreateFileSystemProjectOptions = {
|
|
|
600
600
|
name?: string;
|
|
601
601
|
/** Whether to include the root workspace as a normal workspace. This overrides any config or env var settings. */
|
|
602
602
|
includeRootWorkspace?: boolean;
|
|
603
|
+
/**
|
|
604
|
+
* When true, skip discovery of `.ts`/`.js` config files (`bw.root.{ts,js}`,
|
|
605
|
+
* `bw.workspace.{ts,js}`) so no executable code is loaded from the project,
|
|
606
|
+
* for untrusted contexts.
|
|
607
|
+
*
|
|
608
|
+
* `.jsonc`/`.json` configs and the `package.json` `bw` key still resolve.
|
|
609
|
+
*
|
|
610
|
+
* When omitted, the `BW_DISABLE_EXECUTABLE_CONFIGS_DEFAULT` user env var is
|
|
611
|
+
* consulted (`"true"` or `"false"`). If neither is set, defaults to false.
|
|
612
|
+
*/
|
|
613
|
+
disableExecutableConfigs?: boolean;
|
|
603
614
|
};
|
|
604
615
|
export type InlineScriptOptions = {
|
|
605
616
|
/** A name to act as a label for the inline script */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip ANSI escape sequences and other terminal-disruptive control
|
|
3
|
+
* characters from a string before rendering it. Use this for values
|
|
4
|
+
* sourced from package.json, config files, or other untrusted inputs to
|
|
5
|
+
* prevent terminal-escape injection in bw's CLI output (e.g., a workspace
|
|
6
|
+
* name containing `\x1b[2J` clearing the user's screen during `bw info`).
|
|
7
|
+
*
|
|
8
|
+
* Preserves `\n` and `\t`. Strips ANSI sequences plus C0/C1 controls
|
|
9
|
+
* other than newline and tab.
|
|
10
|
+
*/ // eslint-disable-next-line no-control-regex
|
|
11
|
+
const DISRUPTIVE_CONTROLS_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g;
|
|
12
|
+
const sanitizeOutput = (value) =>
|
|
13
|
+
Bun.stripANSI(value).replace(DISRUPTIVE_CONTROLS_REGEX, "");
|
|
14
|
+
|
|
15
|
+
export { sanitizeOutput };
|
|
@@ -7,7 +7,21 @@ import { BUN_WORKSPACES_VERSION } from "../../version.mjs";
|
|
|
7
7
|
import { createShortId } from "../language/string/id.mjs";
|
|
8
8
|
import { runOnExit } from "./onExit.mjs";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Per-user suffix on the temp base dir prevents a different local user from
|
|
12
|
+
* pre-creating or symlink-squatting `/tmp/bun-workspaces` to steer our writes.
|
|
13
|
+
* On platforms without a numeric uid (Windows), `os.tmpdir()` is already
|
|
14
|
+
* per-user so the suffix becomes inert.
|
|
15
|
+
*/ const getUserSuffix = () => {
|
|
16
|
+
try {
|
|
17
|
+
const { uid } = os.userInfo();
|
|
18
|
+
return uid >= 0 ? `-${uid}` : "";
|
|
19
|
+
} catch {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const getTempBasePackageDir = () =>
|
|
24
|
+
path.join(os.tmpdir(), `bun-workspaces${getUserSuffix()}`);
|
|
11
25
|
const getTempParentDir = () =>
|
|
12
26
|
path.join(getTempBasePackageDir(), BUN_WORKSPACES_VERSION);
|
|
13
27
|
class TempDir {
|
|
@@ -18,10 +32,14 @@ class TempDir {
|
|
|
18
32
|
}
|
|
19
33
|
initialize(clean = false) {
|
|
20
34
|
if (fs.existsSync(this.dir)) return;
|
|
35
|
+
// Pass mode at creation time so the dir is never briefly readable by
|
|
36
|
+
// other local users between mkdir and a subsequent chmod (closes the
|
|
37
|
+
// TOCTOU window where another process could enter the dir before the
|
|
38
|
+
// mode is tightened).
|
|
21
39
|
fs.mkdirSync(this.dir, {
|
|
22
40
|
recursive: true,
|
|
41
|
+
mode: 448,
|
|
23
42
|
});
|
|
24
|
-
fs.chmodSync(this.dir, 448);
|
|
25
43
|
if (clean) {
|
|
26
44
|
for (const dir of fs.readdirSync(path.resolve(getTempBasePackageDir()))) {
|
|
27
45
|
if (dir !== BUN_WORKSPACES_VERSION) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// This file is generated by scripts/
|
|
1
|
+
// This file is generated by scripts/createPublicAgentDocs.ts. Do not edit manually.
|
|
2
2
|
const DOC_OVERVIEW = `## Project Overview
|
|
3
3
|
|
|
4
4
|
bun-workspaces is a CLI and TypeScript API to help manage Bun monorepos. It reads \`bun.lock\` to find all workspaces in the project. It is referred to as "bw" for short, which is also the recommended CLI alias. The overall goal is a monorepo tool that is more lightweight than others, with still powerful comparable features, requiring no special config to get started, only a standard Bun repo using workspaces.
|
|
@@ -9,12 +9,15 @@ Three main domain terms to know:
|
|
|
9
9
|
- Workspace: a nested package within a project. The root package.json can count as a workspace as well, but by default, only nested packages are considered workspaces.
|
|
10
10
|
- Script: an entry in the \`scripts\` field of a workspace's \`package.json\` file. bw can also run one-off commands known as "inline scripts," which can use the Bun shell or system shell (\`sh -c\` or \`cmd /d /s /c\` for windows).
|
|
11
11
|
|
|
12
|
-
bw also supports **affected workspace** detection: given a set of changed files (from a git diff or an explicit list), it determines which workspaces are meaningfully changed. This drives \`bw list-affected\`/\`bw run-affected\` for orchestrating builds, tests, etc. across only the workspaces that need them
|
|
12
|
+
bw also supports **affected workspace** detection: given a set of changed files (from a git diff or an explicit list), it determines which workspaces are meaningfully changed. This drives \`bw list-affected\`/\`bw run-affected\` for orchestrating builds, tests, etc. across only the workspaces that need them.
|
|
13
|
+
`;
|
|
13
14
|
const DOC_CONCEPTS = `## Concepts
|
|
14
15
|
|
|
15
16
|
### Workspace patterns
|
|
16
17
|
|
|
17
|
-
Many features accept a list of workspace patterns to match a subset of workspaces
|
|
18
|
+
Many features accept a list of workspace patterns to match a subset of workspaces:
|
|
19
|
+
|
|
20
|
+
\`[not:][(name|alias|path|tag):][re:]<value>\`
|
|
18
21
|
|
|
19
22
|
By default, a pattern matches the workspace name or alias: \`my-workspace-name\` or \`my-alias-name\`. Aliases are defined in config explained below.
|
|
20
23
|
|
|
@@ -24,8 +27,12 @@ Patterns can include a wildcard to match only by workspace name: \`my-workspace-
|
|
|
24
27
|
- Path pattern specifier (supports glob): \`path:packages/**/*\`.
|
|
25
28
|
- Name pattern specifier: \`name:my-workspace-*\`.
|
|
26
29
|
- Tag pattern specifier: \`tag:my-tag\`.
|
|
27
|
-
- Special root workspace selector: \`@root\`.
|
|
28
30
|
- Any pattern can start with \`not:\` to negate the pattern. (e.g. "not:my-workspace-name", "not:tag:my-tag-\\*") This excludes workspaces that match any other present patterns from a result.
|
|
31
|
+
- Regex pattern modifier can be applied before the pattern value: \`re:\` (e.g. "re:^my-workspace-.+" or "not:alias:re:^my-alias-.+")
|
|
32
|
+
|
|
33
|
+
#### Special selectors
|
|
34
|
+
|
|
35
|
+
- Special root workspace selector: \`@root\`. This is a reference to the root workspace, whether it's included in a Project's workspace list or not.
|
|
29
36
|
|
|
30
37
|
### Workspace Script Metadata
|
|
31
38
|
|
|
@@ -82,7 +89,8 @@ There are two diff sources:
|
|
|
82
89
|
- **git** (default): diff \`HEAD\` against the configured base ref (default \`main\`, configurable via \`affectedBaseRef\` in the root config or \`BW_AFFECTED_BASE_REF_DEFAULT\` env var). Uncommitted changes (staged, unstaged, untracked) are included by default. Gitignored files never participate.
|
|
83
90
|
- **fileList**: pass changed files explicitly (paths, dirs, or globs) — bypasses git entirely.
|
|
84
91
|
|
|
85
|
-
Use \`--explain\` for a per-workspace summary of changed inputs and dep cascade reasons, and \`--explain --detailed\` for full per-file/edge breakdowns including the affected-dep chain
|
|
92
|
+
Use \`--explain\` for a per-workspace summary of changed inputs and dep cascade reasons, and \`--explain --detailed\` for full per-file/edge breakdowns including the affected-dep chain.
|
|
93
|
+
`;
|
|
86
94
|
const DOC_CLI = `### CLI examples:
|
|
87
95
|
|
|
88
96
|
\`\`\`bash
|
|
@@ -208,7 +216,8 @@ bw --no-include-root ls # override config/env var setting
|
|
|
208
216
|
# Log level (debug|info|warn|error|silent, default info)
|
|
209
217
|
bw --log-level=silent ls
|
|
210
218
|
bw -l silent ls
|
|
211
|
-
|
|
219
|
+
\`\`\`
|
|
220
|
+
`;
|
|
212
221
|
const DOC_API = `### API examples:
|
|
213
222
|
|
|
214
223
|
The API is held in close parity with the CLI. It is developed first so that the CLI is a thin wrapper around the API.
|
|
@@ -337,7 +346,8 @@ project.runAffectedWorkspaceScript({
|
|
|
337
346
|
},
|
|
338
347
|
],
|
|
339
348
|
}
|
|
340
|
-
|
|
349
|
+
\`\`\`
|
|
350
|
+
`;
|
|
341
351
|
const DOC_CONFIG = `## Root config
|
|
342
352
|
|
|
343
353
|
Optional project config can be placed in \`bw.root.ts\`/\`bw.root.js\`/\`bw.root.jsonc\`/\`bw.root.json\` in the root directory, or in the \`"bw"\` key of \`package.json\`.
|
|
@@ -522,6 +532,7 @@ export default defineRootConfig({
|
|
|
522
532
|
parallelMax: 5,
|
|
523
533
|
},
|
|
524
534
|
});
|
|
525
|
-
|
|
535
|
+
\`\`\`
|
|
536
|
+
`;
|
|
526
537
|
|
|
527
538
|
export { DOC_API, DOC_CLI, DOC_CONCEPTS, DOC_CONFIG, DOC_OVERVIEW };
|
|
@@ -167,6 +167,7 @@ const determineAffectedWorkspaces = async (project, options) => {
|
|
|
167
167
|
workspacesOptions: {
|
|
168
168
|
workspaceInputs,
|
|
169
169
|
workspaces: project.workspaces,
|
|
170
|
+
rootWorkspace: project.rootWorkspace,
|
|
170
171
|
ignoreWorkspaceDependencies,
|
|
171
172
|
ignoreExternalDependencies,
|
|
172
173
|
},
|
|
@@ -209,6 +210,7 @@ const determineAffectedWorkspaces = async (project, options) => {
|
|
|
209
210
|
rootDirectory: project.rootDirectory,
|
|
210
211
|
workspaceInputs,
|
|
211
212
|
changedFilePaths: expandedChangedFilePaths,
|
|
213
|
+
rootWorkspace: project.rootWorkspace,
|
|
212
214
|
externalDepChangesByWorkspace,
|
|
213
215
|
ignoreWorkspaceDependencies,
|
|
214
216
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { loadRootConfig } from "../../../config/index.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getUserBoolEnvVar,
|
|
6
|
+
getUserEnvVar,
|
|
7
|
+
} from "../../../config/userEnvVars/index.mjs";
|
|
5
8
|
import { parse, quote } from "../../../internal/bundledDeps/shellQuote.mjs";
|
|
6
9
|
import {
|
|
7
10
|
DEFAULT_TEMP_DIR,
|
|
@@ -70,17 +73,22 @@ const serializeArgs = (args, metadata, shell) => {
|
|
|
70
73
|
return args
|
|
71
74
|
.map((arg) =>
|
|
72
75
|
quoteArg(
|
|
73
|
-
interpolateWorkspaceScriptMetadata(
|
|
76
|
+
interpolateWorkspaceScriptMetadata({
|
|
77
|
+
text: arg,
|
|
78
|
+
metadata,
|
|
79
|
+
shell,
|
|
80
|
+
}),
|
|
74
81
|
shell,
|
|
75
82
|
),
|
|
76
83
|
)
|
|
77
84
|
.join(" ");
|
|
78
85
|
}
|
|
79
|
-
const interpolated = interpolateWorkspaceScriptMetadata(
|
|
80
|
-
args,
|
|
86
|
+
const interpolated = interpolateWorkspaceScriptMetadata({
|
|
87
|
+
text: args,
|
|
81
88
|
metadata,
|
|
82
89
|
shell,
|
|
83
|
-
|
|
90
|
+
quoteValues: true,
|
|
91
|
+
});
|
|
84
92
|
// Escape backslashes in interpolated values before POSIX parse on Windows,
|
|
85
93
|
// so that path separators survive parse's escape processing (\\→\)
|
|
86
94
|
const parseInput =
|
|
@@ -128,6 +136,11 @@ class _FileSystemProject extends ProjectBase {
|
|
|
128
136
|
typeofName: "boolean",
|
|
129
137
|
optional: true,
|
|
130
138
|
},
|
|
139
|
+
"disableExecutableConfigs option": {
|
|
140
|
+
value: options.disableExecutableConfigs,
|
|
141
|
+
typeofName: "boolean",
|
|
142
|
+
optional: true,
|
|
143
|
+
},
|
|
131
144
|
},
|
|
132
145
|
{
|
|
133
146
|
throw: true,
|
|
@@ -141,7 +154,16 @@ class _FileSystemProject extends ProjectBase {
|
|
|
141
154
|
process.cwd(),
|
|
142
155
|
expandHomePath(options.rootDirectory ?? ""),
|
|
143
156
|
);
|
|
144
|
-
|
|
157
|
+
// Root config can't supply a default for this — the config file itself
|
|
158
|
+
// is what we're deciding whether to evaluate. Precedence is therefore
|
|
159
|
+
// option > BW_DISABLE_EXECUTABLE_CONFIGS_DEFAULT env var > false.
|
|
160
|
+
const loadConfigOptions = {
|
|
161
|
+
disableExecutableConfigs:
|
|
162
|
+
options.disableExecutableConfigs ??
|
|
163
|
+
getUserBoolEnvVar("disableExecutableConfigsDefault") ??
|
|
164
|
+
false,
|
|
165
|
+
};
|
|
166
|
+
const rootConfig = loadRootConfig(this.rootDirectory, loadConfigOptions);
|
|
145
167
|
const { workspaces, workspaceMap, rootWorkspace } = findWorkspaces({
|
|
146
168
|
rootDirectory: this.rootDirectory,
|
|
147
169
|
includeRootWorkspace:
|
|
@@ -149,6 +171,7 @@ class _FileSystemProject extends ProjectBase {
|
|
|
149
171
|
rootConfig.defaults.includeRootWorkspace ??
|
|
150
172
|
getUserEnvVar("includeRootWorkspaceDefault") === "true",
|
|
151
173
|
workspacePatternConfigs: rootConfig.workspacePatternConfigs,
|
|
174
|
+
loadConfigOptions,
|
|
152
175
|
});
|
|
153
176
|
this.rootWorkspace = rootWorkspace;
|
|
154
177
|
this.workspaces = workspaces;
|
|
@@ -262,11 +285,12 @@ class _FileSystemProject extends ProjectBase {
|
|
|
262
285
|
};
|
|
263
286
|
const args = serializeArgs(options.args, workspaceScriptMetadata, shell);
|
|
264
287
|
const script = options.inline
|
|
265
|
-
? interpolateWorkspaceScriptMetadata(
|
|
266
|
-
options.script,
|
|
267
|
-
workspaceScriptMetadata,
|
|
288
|
+
? interpolateWorkspaceScriptMetadata({
|
|
289
|
+
text: options.script,
|
|
290
|
+
metadata: workspaceScriptMetadata,
|
|
268
291
|
shell,
|
|
269
|
-
|
|
292
|
+
quoteValues: true,
|
|
293
|
+
}) + (args ? " " + args : "")
|
|
270
294
|
: options.script;
|
|
271
295
|
if (!options.inline && checkIsRecursiveScript(workspace.name, script)) {
|
|
272
296
|
throw new PROJECT_ERRORS.RecursiveWorkspaceScript(
|
|
@@ -473,11 +497,12 @@ class _FileSystemProject extends ProjectBase {
|
|
|
473
497
|
shell,
|
|
474
498
|
);
|
|
475
499
|
const script = options.inline
|
|
476
|
-
? interpolateWorkspaceScriptMetadata(
|
|
477
|
-
options.script,
|
|
478
|
-
workspaceScriptMetadata,
|
|
500
|
+
? interpolateWorkspaceScriptMetadata({
|
|
501
|
+
text: options.script,
|
|
502
|
+
metadata: workspaceScriptMetadata,
|
|
479
503
|
shell,
|
|
480
|
-
|
|
504
|
+
quoteValues: true,
|
|
505
|
+
}) + (args ? " " + args : "")
|
|
481
506
|
: options.script;
|
|
482
507
|
const scriptCommand = options.inline
|
|
483
508
|
? {
|
|
@@ -130,24 +130,18 @@ class ProjectBase {
|
|
|
130
130
|
return this.workspaces.filter((workspace) => workspace.tags.includes(tag));
|
|
131
131
|
}
|
|
132
132
|
findWorkspacesByPattern(...workspacePatterns) {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
workspaces.push(
|
|
146
|
-
...sortWorkspaces(
|
|
147
|
-
matchWorkspacesByPatterns(workspacePatterns, this.workspaces),
|
|
148
|
-
),
|
|
133
|
+
const matched = matchWorkspacesByPatterns(
|
|
134
|
+
workspacePatterns,
|
|
135
|
+
this.workspaces,
|
|
136
|
+
this.rootWorkspace,
|
|
137
|
+
);
|
|
138
|
+
// Preserve historical ordering: root workspace first, then sorted others.
|
|
139
|
+
const rootName = this.rootWorkspace.name;
|
|
140
|
+
const rootMatch = matched.find((workspace) => workspace.name === rootName);
|
|
141
|
+
const rest = sortWorkspaces(
|
|
142
|
+
matched.filter((workspace) => workspace.name !== rootName),
|
|
149
143
|
);
|
|
150
|
-
return
|
|
144
|
+
return rootMatch ? [rootMatch, ...rest] : rest;
|
|
151
145
|
}
|
|
152
146
|
createScriptCommand(options) {
|
|
153
147
|
validateJSTypes(
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import { quote } from "../internal/bundledDeps/shellQuote.mjs";
|
|
1
2
|
import { BunWorkspacesError, IS_WINDOWS } from "../internal/core/index.mjs";
|
|
2
3
|
import { getWorkspaceScriptMetadataConfig } from "../3725.mjs";
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Wrap a value so that, when it is concatenated into a shell command for
|
|
7
|
+
* `shell`, the receiving shell parses it as a single literal token. Used
|
|
8
|
+
* when substituted metadata values land in a shell-interpretable context
|
|
9
|
+
* (inline command bodies, string-form `--args` before POSIX parse).
|
|
10
|
+
*/ const quoteShellValue = (value, shell) =>
|
|
11
|
+
IS_WINDOWS && shell === "system"
|
|
12
|
+
? `"${value.replace(/"/g, '""')}"`
|
|
13
|
+
: quote([value]);
|
|
4
14
|
const createScriptRuntimeEnvVars = (metadata) => {
|
|
5
15
|
const keys = [
|
|
6
16
|
"projectPath",
|
|
@@ -16,7 +26,12 @@ const createScriptRuntimeEnvVars = (metadata) => {
|
|
|
16
26
|
return acc;
|
|
17
27
|
}, {});
|
|
18
28
|
};
|
|
19
|
-
const interpolateWorkspaceScriptMetadata = (
|
|
29
|
+
const interpolateWorkspaceScriptMetadata = ({
|
|
30
|
+
text,
|
|
31
|
+
metadata,
|
|
32
|
+
shell,
|
|
33
|
+
quoteValues = false,
|
|
34
|
+
}) => {
|
|
20
35
|
const keys = [
|
|
21
36
|
"projectPath",
|
|
22
37
|
"projectName",
|
|
@@ -33,6 +48,13 @@ const interpolateWorkspaceScriptMetadata = (text, metadata, shell) => {
|
|
|
33
48
|
(k) => getWorkspaceScriptMetadataConfig(k).inlineName === match,
|
|
34
49
|
);
|
|
35
50
|
const value = metadata[key];
|
|
51
|
+
if (quoteValues) {
|
|
52
|
+
// Preserve "empty substitution is invisible" — quoting an empty value
|
|
53
|
+
// would inject a literal `''` shell token (an empty positional arg),
|
|
54
|
+
// which changes argv length for commands like `echo <scriptName>`
|
|
55
|
+
// when no inline scriptName is set.
|
|
56
|
+
return value === "" ? "" : quoteShellValue(value, shell);
|
|
57
|
+
}
|
|
36
58
|
if (IS_WINDOWS && shell === "bun") {
|
|
37
59
|
return value.replace(/\\/g, "\\\\");
|
|
38
60
|
}
|
|
@@ -57,4 +79,5 @@ export {
|
|
|
57
79
|
createScriptRuntimeEnvVars,
|
|
58
80
|
getWorkspaceScriptMetadata,
|
|
59
81
|
interpolateWorkspaceScriptMetadata,
|
|
82
|
+
quoteShellValue,
|
|
60
83
|
};
|
|
@@ -33,9 +33,14 @@ const applyWorkspacePatternConfigs = (
|
|
|
33
33
|
workspaceMap,
|
|
34
34
|
workspaceAliases,
|
|
35
35
|
patternConfigs,
|
|
36
|
+
rootWorkspace,
|
|
36
37
|
) => {
|
|
37
38
|
for (const entry of patternConfigs) {
|
|
38
|
-
const matched = matchWorkspacesByPatterns(
|
|
39
|
+
const matched = matchWorkspacesByPatterns(
|
|
40
|
+
entry.patterns,
|
|
41
|
+
workspaces,
|
|
42
|
+
rootWorkspace,
|
|
43
|
+
);
|
|
39
44
|
for (const workspace of matched) {
|
|
40
45
|
const mapEntry = workspaceMap[workspace.name];
|
|
41
46
|
const prevConfig = mapEntry.config;
|
|
@@ -17,7 +17,7 @@ const getTransitiveDeps = (workspaceName, workspaceMap, chain, visited) => {
|
|
|
17
17
|
}
|
|
18
18
|
return result;
|
|
19
19
|
};
|
|
20
|
-
const validateWorkspaceDependencyRules = ({ workspaceMap }) => {
|
|
20
|
+
const validateWorkspaceDependencyRules = ({ workspaceMap, rootWorkspace }) => {
|
|
21
21
|
const violations = [];
|
|
22
22
|
for (const [workspaceName, { config }] of Object.entries(workspaceMap)) {
|
|
23
23
|
const rule = config.rules?.workspaceDependencies;
|
|
@@ -32,10 +32,15 @@ const validateWorkspaceDependencyRules = ({ workspaceMap }) => {
|
|
|
32
32
|
const depWorkspace = workspaceMap[depName]?.workspace;
|
|
33
33
|
if (!depWorkspace) continue;
|
|
34
34
|
const chainStr = chain.join(" -> ");
|
|
35
|
+
// matchWorkspacesByPatterns can inject the root workspace when an
|
|
36
|
+
// "@root" pattern is present, even if it isn't in the input universe.
|
|
37
|
+
// We're only asking "does the single dep match?" so confirm by name.
|
|
35
38
|
if (rule.allowPatterns) {
|
|
36
|
-
const isAllowed =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
const isAllowed = matchWorkspacesByPatterns(
|
|
40
|
+
rule.allowPatterns,
|
|
41
|
+
[depWorkspace],
|
|
42
|
+
rootWorkspace,
|
|
43
|
+
).some((matched) => matched.name === depWorkspace.name);
|
|
39
44
|
if (!isAllowed) {
|
|
40
45
|
violations.push(
|
|
41
46
|
`"${workspaceName}" violates workspaceDependencies rule: workspace "${depName}" is not permitted by allowPatterns (dependency chain: ${chainStr})`,
|
|
@@ -44,9 +49,11 @@ const validateWorkspaceDependencyRules = ({ workspaceMap }) => {
|
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
if (rule.denyPatterns) {
|
|
47
|
-
const isDenied =
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
const isDenied = matchWorkspacesByPatterns(
|
|
53
|
+
rule.denyPatterns,
|
|
54
|
+
[depWorkspace],
|
|
55
|
+
rootWorkspace,
|
|
56
|
+
).some((matched) => matched.name === depWorkspace.name);
|
|
50
57
|
if (isDenied) {
|
|
51
58
|
violations.push(
|
|
52
59
|
`"${workspaceName}" violates workspaceDependencies rule: workspace "${depName}" is denied by denyPatterns (dependency chain: ${chainStr})`,
|
|
@@ -67,6 +67,7 @@ const findWorkspaces = ({
|
|
|
67
67
|
workspaceGlobs: _workspaceGlobs,
|
|
68
68
|
includeRootWorkspace = false,
|
|
69
69
|
workspacePatternConfigs,
|
|
70
|
+
loadConfigOptions,
|
|
70
71
|
}) => {
|
|
71
72
|
rootDirectory = path.resolve(rootDirectory);
|
|
72
73
|
logger.debug(`Finding workspaces in ${rootDirectory}`);
|
|
@@ -109,6 +110,7 @@ const findWorkspaces = ({
|
|
|
109
110
|
);
|
|
110
111
|
const workspaceConfig = loadWorkspaceConfig(
|
|
111
112
|
path.dirname(packageJsonPath),
|
|
113
|
+
loadConfigOptions,
|
|
112
114
|
);
|
|
113
115
|
if (workspaceConfig) {
|
|
114
116
|
for (const alias of workspaceConfig.aliases) {
|
|
@@ -178,10 +180,12 @@ const findWorkspaces = ({
|
|
|
178
180
|
workspaceMap,
|
|
179
181
|
workspaceAliases,
|
|
180
182
|
workspacePatternConfigs,
|
|
183
|
+
rootWorkspace,
|
|
181
184
|
);
|
|
182
185
|
}
|
|
183
186
|
validateWorkspaceDependencyRules({
|
|
184
187
|
workspaceMap,
|
|
188
|
+
rootWorkspace,
|
|
185
189
|
});
|
|
186
190
|
validateWorkspaceAliases(workspaces, workspaceAliases, rootWorkspace.name);
|
|
187
191
|
logger.debug(
|