@effected/workspaces 0.25.0 → 0.26.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/LayerPolicy.js +133 -0
- package/PackedInstall.js +282 -0
- package/README.md +195 -0
- package/SourceBoundary.js +540 -0
- package/WorkspaceLayering.js +199 -0
- package/internal/dependencyFields.js +17 -0
- package/internal/packedInstallPlan.js +174 -0
- package/internal/sourceText.js +333 -0
- package/package.json +6 -1
- package/testing.d.ts +1382 -0
- package/testing.js +10 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { WorkspaceDiscovery } from "./WorkspaceDiscovery.js";
|
|
2
|
+
import { ALL_DEPENDENCY_FIELDS } from "./internal/dependencyFields.js";
|
|
3
|
+
import { Effect, Graph, Option, Schema } from "effect";
|
|
4
|
+
import { GlobSet } from "@effected/glob";
|
|
5
|
+
import { DependencyField } from "@effected/npm";
|
|
6
|
+
|
|
7
|
+
//#region src/WorkspaceLayering.ts
|
|
8
|
+
/**
|
|
9
|
+
* One dependency edge between two workspace packages, in one field.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
var LayerEdge = class extends Schema.Class("LayerEdge")({
|
|
14
|
+
/** The dependent package. */
|
|
15
|
+
from: Schema.String,
|
|
16
|
+
/** The package depended on. */
|
|
17
|
+
to: Schema.String,
|
|
18
|
+
/** The manifest map that declares it. */
|
|
19
|
+
field: DependencyField
|
|
20
|
+
}) {
|
|
21
|
+
/** `from -> to (field)`. */
|
|
22
|
+
get label() {
|
|
23
|
+
return `${this.from} -> ${this.to} (${this.field})`;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* What a layering check found.
|
|
28
|
+
*
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
var LayeringReport = class extends Schema.Class("LayeringReport")({
|
|
32
|
+
/** Packages the policy declares more than once, or declares and also matches with an unconstrained glob. */
|
|
33
|
+
duplicates: Schema.Array(Schema.String),
|
|
34
|
+
/** Workspace packages the policy does not classify. */
|
|
35
|
+
unclassified: Schema.Array(Schema.String),
|
|
36
|
+
/** Edges that break the policy, each with why. */
|
|
37
|
+
offenders: Schema.Array(Schema.Struct({
|
|
38
|
+
edge: LayerEdge,
|
|
39
|
+
reason: Schema.Literals([
|
|
40
|
+
"upward",
|
|
41
|
+
"sameLayer",
|
|
42
|
+
"toolingReachesLayer",
|
|
43
|
+
"intoUnconstrained",
|
|
44
|
+
"intoUnclassified"
|
|
45
|
+
])
|
|
46
|
+
})),
|
|
47
|
+
/** The members of every dependency cycle in the checked fields, or none. */
|
|
48
|
+
cycle: Schema.Option(Schema.Array(Schema.String)),
|
|
49
|
+
/** Packages the policy names that the workspace does not contain. */
|
|
50
|
+
missingDeclared: Schema.Array(Schema.String),
|
|
51
|
+
/** Required edges absent from the checked fields. */
|
|
52
|
+
missingRequiredEdges: Schema.Array(Schema.String),
|
|
53
|
+
/** Edges in the checked fields; `0` is itself a violation. */
|
|
54
|
+
edgeCount: Schema.Number
|
|
55
|
+
}) {
|
|
56
|
+
/** One line per violation: `[]` means the graph honours the policy and the check was not vacuous. */
|
|
57
|
+
get violations() {
|
|
58
|
+
return [
|
|
59
|
+
...this.duplicates.map((name) => `classified more than once: ${name}`),
|
|
60
|
+
...this.unclassified.map((name) => `not classified by the policy: ${name}`),
|
|
61
|
+
...this.offenders.map(({ edge, reason }) => `${reason}: ${edge.label}`),
|
|
62
|
+
...Option.match(this.cycle, {
|
|
63
|
+
onNone: () => [],
|
|
64
|
+
onSome: (members) => [`dependency cycle among: ${members.join(", ")}`]
|
|
65
|
+
}),
|
|
66
|
+
...this.missingDeclared.map((name) => `declared but not in the workspace: ${name}`),
|
|
67
|
+
...this.missingRequiredEdges.map((edge) => `required edge missing: ${edge}`),
|
|
68
|
+
...this.edgeCount === 0 ? ["no workspace edges in the checked fields: the check is vacuous"] : []
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const offence = (from, to) => {
|
|
73
|
+
if (from.kind === "unconstrained" || from.kind === "unclassified") return void 0;
|
|
74
|
+
if (to.kind === "unconstrained") return "intoUnconstrained";
|
|
75
|
+
if (to.kind === "unclassified") return "intoUnclassified";
|
|
76
|
+
if (from.kind === "tooling") return to.kind === "layer" ? "toolingReachesLayer" : void 0;
|
|
77
|
+
if (to.kind === "tooling") return void 0;
|
|
78
|
+
return to.index > from.index ? void 0 : to.index === from.index ? "sameLayer" : "upward";
|
|
79
|
+
};
|
|
80
|
+
/** The sorted members of every strongly connected component larger than one (cf. `DependencyGraph.ts` `cycleMembers`). */
|
|
81
|
+
const cycleOf = (names, edges) => {
|
|
82
|
+
const sorted = [.../* @__PURE__ */ new Set([...names, ...edges.flatMap((e) => [e.from, e.to])])].sort();
|
|
83
|
+
const graph = Graph.directed((mutable) => {
|
|
84
|
+
const index = /* @__PURE__ */ new Map();
|
|
85
|
+
for (const name of sorted) index.set(name, Graph.addNode(mutable, name));
|
|
86
|
+
for (const e of edges) {
|
|
87
|
+
const from = index.get(e.from);
|
|
88
|
+
const to = index.get(e.to);
|
|
89
|
+
if (from !== void 0 && to !== void 0 && from !== to) Graph.addEdge(mutable, from, to, e.field);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
const members = /* @__PURE__ */ new Set();
|
|
93
|
+
for (const component of Graph.stronglyConnectedComponents(graph)) {
|
|
94
|
+
if (component.length < 2) continue;
|
|
95
|
+
for (const index of component) {
|
|
96
|
+
const name = sorted[index];
|
|
97
|
+
if (name !== void 0) members.add(name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return members.size === 0 ? Option.none() : Option.some([...members].sort());
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Holds a workspace's package graph to a committed {@link LayerPolicy}.
|
|
104
|
+
*
|
|
105
|
+
* @remarks
|
|
106
|
+
* `check` is pure, so positive-control fixture graphs need no filesystem.
|
|
107
|
+
* `edgesOf` recomputes one edge per declaring field, because
|
|
108
|
+
* `DependencyGraph` merges the four fields into one adjacency and a policy
|
|
109
|
+
* may check only some of them. An edge exists wherever a dependency NAME is
|
|
110
|
+
* a workspace package, whatever its specifier protocol.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* import { NodeServices } from "@effect/platform-node";
|
|
115
|
+
* import { Workspaces } from "@effected/workspaces";
|
|
116
|
+
* import { LayerPolicy, WorkspaceLayering } from "@effected/workspaces/testing";
|
|
117
|
+
* import { Effect, Layer } from "effect";
|
|
118
|
+
*
|
|
119
|
+
* const Live = Workspaces.layer({ cwd: "/repo" }).pipe(Layer.provideMerge(NodeServices.layer));
|
|
120
|
+
*
|
|
121
|
+
* const violations = Effect.gen(function* () {
|
|
122
|
+
* const policy = yield* LayerPolicy.load("/repo/layers.json");
|
|
123
|
+
* const report = yield* WorkspaceLayering.checkWorkspace(policy);
|
|
124
|
+
* return report.violations;
|
|
125
|
+
* }).pipe(Effect.provide(Live));
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* @public
|
|
129
|
+
*/
|
|
130
|
+
var WorkspaceLayering = class WorkspaceLayering {
|
|
131
|
+
constructor() {}
|
|
132
|
+
/** Check `graph` against `policy`, reading only the policy's fields. Pure. */
|
|
133
|
+
static check = (graph, policy) => {
|
|
134
|
+
const fields = new Set(policy.effectiveFields);
|
|
135
|
+
const edges = graph.edges.filter((e) => fields.has(e.field));
|
|
136
|
+
const unconstrained = GlobSet.make({ patterns: policy.unconstrained });
|
|
137
|
+
const layerOf = /* @__PURE__ */ new Map();
|
|
138
|
+
const declared = /* @__PURE__ */ new Map();
|
|
139
|
+
const count = (name) => {
|
|
140
|
+
declared.set(name, (declared.get(name) ?? 0) + 1);
|
|
141
|
+
};
|
|
142
|
+
policy.layers.forEach((members, index) => {
|
|
143
|
+
for (const name of members) {
|
|
144
|
+
count(name);
|
|
145
|
+
if (!layerOf.has(name)) layerOf.set(name, index);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
for (const name of policy.tooling) count(name);
|
|
149
|
+
const tooling = new Set(policy.tooling);
|
|
150
|
+
const place = (name) => {
|
|
151
|
+
const index = layerOf.get(name);
|
|
152
|
+
if (index !== void 0) return {
|
|
153
|
+
kind: "layer",
|
|
154
|
+
index
|
|
155
|
+
};
|
|
156
|
+
if (tooling.has(name)) return { kind: "tooling" };
|
|
157
|
+
return unconstrained.matches(name) ? { kind: "unconstrained" } : { kind: "unclassified" };
|
|
158
|
+
};
|
|
159
|
+
const offenders = [];
|
|
160
|
+
for (const e of edges) {
|
|
161
|
+
const reason = offence(place(e.from), place(e.to));
|
|
162
|
+
if (reason !== void 0) offenders.push({
|
|
163
|
+
edge: e,
|
|
164
|
+
reason
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const names = new Set(graph.names);
|
|
168
|
+
const present = new Set(edges.map((e) => `${e.from} -> ${e.to}`));
|
|
169
|
+
return LayeringReport.make({
|
|
170
|
+
duplicates: [...declared].filter(([name, times]) => times > 1 || unconstrained.matches(name)).map(([name]) => name).sort(),
|
|
171
|
+
unclassified: graph.names.filter((name) => place(name).kind === "unclassified").sort(),
|
|
172
|
+
offenders,
|
|
173
|
+
cycle: cycleOf(graph.names, edges),
|
|
174
|
+
missingDeclared: [...declared.keys()].filter((name) => !names.has(name)).sort(),
|
|
175
|
+
missingRequiredEdges: (policy.requiredEdges ?? []).filter((required) => !present.has(required)),
|
|
176
|
+
edgeCount: edges.length
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
/** One edge per declaring field between workspace packages; self-edges dropped. */
|
|
180
|
+
static edgesOf = (packages) => {
|
|
181
|
+
const names = new Set(packages.map((pkg) => pkg.name));
|
|
182
|
+
return packages.flatMap((pkg) => ALL_DEPENDENCY_FIELDS.flatMap((field) => Object.keys(pkg[field]).filter((name) => names.has(name) && name !== pkg.name).sort().map((to) => LayerEdge.make({
|
|
183
|
+
from: pkg.name,
|
|
184
|
+
to,
|
|
185
|
+
field
|
|
186
|
+
}))));
|
|
187
|
+
};
|
|
188
|
+
/** Discover the workspace and check it against `policy`. */
|
|
189
|
+
static checkWorkspace = Effect.fn("WorkspaceLayering.checkWorkspace")(function* (policy) {
|
|
190
|
+
const packages = yield* (yield* WorkspaceDiscovery).listPackages();
|
|
191
|
+
return WorkspaceLayering.check({
|
|
192
|
+
names: packages.map((pkg) => pkg.name),
|
|
193
|
+
edges: WorkspaceLayering.edgesOf(packages)
|
|
194
|
+
}, policy);
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
//#endregion
|
|
199
|
+
export { LayerEdge, LayeringReport, WorkspaceLayering };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/internal/dependencyFields.ts
|
|
2
|
+
/** Every dependency map a manifest declares, in manifest order. */
|
|
3
|
+
const ALL_DEPENDENCY_FIELDS = [
|
|
4
|
+
"dependencies",
|
|
5
|
+
"devDependencies",
|
|
6
|
+
"peerDependencies",
|
|
7
|
+
"optionalDependencies"
|
|
8
|
+
];
|
|
9
|
+
/** The maps an installed package's own dependencies come from — never devDependencies. */
|
|
10
|
+
const RUNTIME_DEPENDENCY_FIELDS = [
|
|
11
|
+
"dependencies",
|
|
12
|
+
"optionalDependencies",
|
|
13
|
+
"peerDependencies"
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
export { ALL_DEPENDENCY_FIELDS, RUNTIME_DEPENDENCY_FIELDS };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { RUNTIME_DEPENDENCY_FIELDS } from "./dependencyFields.js";
|
|
2
|
+
import { Result } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/packedInstallPlan.ts
|
|
5
|
+
/** Variables that carry the PARENT run's context into a child package manager. */
|
|
6
|
+
const TRAPS = /* @__PURE__ */ new Set([
|
|
7
|
+
"CI",
|
|
8
|
+
"INIT_CWD",
|
|
9
|
+
"NODE_V8_COVERAGE",
|
|
10
|
+
"PNPM_SCRIPT_SRC_DIR",
|
|
11
|
+
"PNPM_PACKAGE_NAME"
|
|
12
|
+
]);
|
|
13
|
+
/** Prefixes of whole families of parent-run context: npm's, pnpm's config, and Yarn's. */
|
|
14
|
+
const TRAP_PREFIXES = /^(npm_|pnpm_config_|yarn_)/i;
|
|
15
|
+
/**
|
|
16
|
+
* `env` without undefined values and without the parent's context: every
|
|
17
|
+
* `npm_*` variable (a pnpm-run vitest leaks `npm_config_user_agent`), every
|
|
18
|
+
* `pnpm_config_*` variable plus `PNPM_SCRIPT_SRC_DIR` and `PNPM_PACKAGE_NAME`
|
|
19
|
+
* (a `pnpm exec` child carries all three, `pnpm_config_verify_deps_before_run`
|
|
20
|
+
* among them), every `YARN_*` variable (Berry lets them override
|
|
21
|
+
* `.yarnrc.yml`, so `YARN_NODE_LINKER=pnp` would defeat the consumer's
|
|
22
|
+
* `nodeLinker`), `CI` (Yarn Berry turns on immutable installs), `INIT_CWD`,
|
|
23
|
+
* and `NODE_V8_COVERAGE` (a spawned manager writes coverage files that race
|
|
24
|
+
* vitest's V8 provider).
|
|
25
|
+
*
|
|
26
|
+
* Stripping `CI` does not make the child believe it runs locally: ci-info
|
|
27
|
+
* also reads provider variables such as `GITHUB_ACTIONS`, which stay. That is
|
|
28
|
+
* harmless today, because the only CI-conditional behaviour a consumer
|
|
29
|
+
* install trips on (Berry's immutable installs, pnpm's frozen lockfile) keys
|
|
30
|
+
* on a lockfile the fresh consumer does not have, or is switched off in the
|
|
31
|
+
* files `consumerFiles` writes.
|
|
32
|
+
*
|
|
33
|
+
* User-level configuration is inherited by design: `HOME` stays, so each
|
|
34
|
+
* manager still reads the user's `~/.npmrc`, `~/.yarnrc.yml` and friends, and
|
|
35
|
+
* with them the registry, auth and proxy a real install on this machine
|
|
36
|
+
* would use.
|
|
37
|
+
*/
|
|
38
|
+
const scrubEnv = (env) => {
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const [key, value] of Object.entries(env)) if (value !== void 0 && !TRAP_PREFIXES.test(key) && !TRAPS.has(key)) out[key] = value;
|
|
41
|
+
return out;
|
|
42
|
+
};
|
|
43
|
+
/** The last version-shaped line of `--version` output, without a leading `v`. */
|
|
44
|
+
const versionOf = (stdout) => {
|
|
45
|
+
for (const line of stdout.split(/\r?\n/).reverse()) {
|
|
46
|
+
const match = /^v?(\d+\.\d+\.\d+\S*)$/.exec(line.trim());
|
|
47
|
+
if (match?.[1] !== void 0) return match[1];
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/** The carrier first, then the rest of the closure; the failure names the first package the workspace lacks. */
|
|
51
|
+
const closureOf = (packages, carrier, closure) => {
|
|
52
|
+
const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
|
|
53
|
+
const root = byName.get(carrier);
|
|
54
|
+
if (root === void 0) return Result.fail(carrier);
|
|
55
|
+
if (closure !== "auto") {
|
|
56
|
+
const unknown = closure.find((name) => !byName.has(name));
|
|
57
|
+
if (unknown !== void 0) return Result.fail(unknown);
|
|
58
|
+
const rest = [...new Set(closure)].filter((name) => name !== carrier);
|
|
59
|
+
return Result.succeed([root, ...rest.flatMap((name) => byName.get(name) ?? [])]);
|
|
60
|
+
}
|
|
61
|
+
const ordered = [root];
|
|
62
|
+
const seen = /* @__PURE__ */ new Set([carrier]);
|
|
63
|
+
for (let head = 0; head < ordered.length; head++) {
|
|
64
|
+
const current = ordered[head];
|
|
65
|
+
if (current === void 0) continue;
|
|
66
|
+
for (const field of RUNTIME_DEPENDENCY_FIELDS) for (const name of Object.keys(current[field]).sort()) {
|
|
67
|
+
const dependency = byName.get(name);
|
|
68
|
+
if (dependency !== void 0 && !seen.has(name)) {
|
|
69
|
+
seen.add(name);
|
|
70
|
+
ordered.push(dependency);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return Result.succeed(ordered);
|
|
75
|
+
};
|
|
76
|
+
const major = (version) => Number.parseInt(version.split(".")[0] ?? "", 10);
|
|
77
|
+
const YARNRC = "nodeLinker: node-modules\nenableImmutableInstalls: false\nenableScripts: false\nenableTelemetry: false\n";
|
|
78
|
+
/** JSON strings are valid YAML double-quoted scalars. */
|
|
79
|
+
const pnpmWorkspaceYaml = (specs) => {
|
|
80
|
+
const entries = Object.entries(specs);
|
|
81
|
+
return entries.length === 0 ? "overrides: {}\n" : `overrides:\n${entries.map(([name, spec]) => ` ${JSON.stringify(name)}: ${JSON.stringify(spec)}`).join("\n")}\n`;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The files of one scratch consumer. The carrier and the caller's extra
|
|
85
|
+
* dependencies are its only direct dependencies; every other packed package
|
|
86
|
+
* is steered to its tarball through the field this manager reads:
|
|
87
|
+
* `overrides` (npm, bun), `resolutions` (yarn), or a settings-only
|
|
88
|
+
* `pnpm-workspace.yaml` (pnpm 10+ reads overrides there, and pnpm 11+ no
|
|
89
|
+
* longer reads `package.json#pnpm`).
|
|
90
|
+
*
|
|
91
|
+
* An extra dependency that names a packed package (the carrier or a closure
|
|
92
|
+
* member) is written as that package's `file:` tarball spec, whatever the
|
|
93
|
+
* caller passed: the packed tarball always wins. npm fails an install whose
|
|
94
|
+
* override differs from a direct spec for the same package (`EOVERRIDE`), and
|
|
95
|
+
* accepts one that is identical; and a caller's range must never silently
|
|
96
|
+
* replace the tarball the run exists to prove.
|
|
97
|
+
*/
|
|
98
|
+
const consumerFiles = (input) => {
|
|
99
|
+
const specs = Object.fromEntries(Object.entries(input.overrides).map(([name, tarball]) => [name, `file:${tarball}`]));
|
|
100
|
+
const carrierSpec = `file:${input.carrier.tarball}`;
|
|
101
|
+
const extra = Object.fromEntries(Object.entries(input.dependencies).filter(([name]) => name !== input.carrier.name).map(([name, spec]) => [name, Object.hasOwn(specs, name) ? specs[name] ?? spec : spec]));
|
|
102
|
+
const manifest = {
|
|
103
|
+
name: `packed-install-${input.manager}`,
|
|
104
|
+
version: "0.0.0",
|
|
105
|
+
private: true,
|
|
106
|
+
...input.manager === "pnpm" ? { devEngines: { packageManager: {
|
|
107
|
+
name: "pnpm",
|
|
108
|
+
version: input.version,
|
|
109
|
+
onFail: "ignore"
|
|
110
|
+
} } } : { packageManager: `${input.manager}@${input.version}` },
|
|
111
|
+
dependencies: {
|
|
112
|
+
[input.carrier.name]: carrierSpec,
|
|
113
|
+
...extra
|
|
114
|
+
},
|
|
115
|
+
...input.manager === "npm" || input.manager === "bun" ? { overrides: specs } : {},
|
|
116
|
+
...input.manager === "yarn" ? { resolutions: specs } : {}
|
|
117
|
+
};
|
|
118
|
+
const files = [{
|
|
119
|
+
file: "package.json",
|
|
120
|
+
content: `${JSON.stringify(manifest, null, 2)}\n`
|
|
121
|
+
}];
|
|
122
|
+
if (input.manager === "pnpm") files.push({
|
|
123
|
+
file: "pnpm-workspace.yaml",
|
|
124
|
+
content: pnpmWorkspaceYaml(specs)
|
|
125
|
+
});
|
|
126
|
+
if (input.manager === "yarn" && major(input.version) >= 2) files.push({
|
|
127
|
+
file: ".yarnrc.yml",
|
|
128
|
+
content: YARNRC
|
|
129
|
+
});
|
|
130
|
+
return files;
|
|
131
|
+
};
|
|
132
|
+
/** The install argv, lifecycle scripts skipped the way this manager spells it. */
|
|
133
|
+
const installArgs = (manager, version) => {
|
|
134
|
+
switch (manager) {
|
|
135
|
+
case "npm": return [
|
|
136
|
+
"install",
|
|
137
|
+
"--ignore-scripts",
|
|
138
|
+
"--no-audit",
|
|
139
|
+
"--no-fund"
|
|
140
|
+
];
|
|
141
|
+
case "pnpm": return ["install", "--config.ignore-scripts=true"];
|
|
142
|
+
case "yarn": return major(version) >= 2 ? ["install"] : [
|
|
143
|
+
"install",
|
|
144
|
+
"--ignore-scripts",
|
|
145
|
+
"--non-interactive"
|
|
146
|
+
];
|
|
147
|
+
case "bun": return ["install", "--ignore-scripts"];
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* A specifier no consumer outside the workspace can resolve: `workspace:`,
|
|
152
|
+
* `catalog:`, `link:`, or a relative `file:` path. An absolute `file:` path
|
|
153
|
+
* is left alone: it resolves wherever the file exists.
|
|
154
|
+
*/
|
|
155
|
+
const UNRESOLVABLE = /^(?:workspace:|catalog:|link:|file:(?!\/))/;
|
|
156
|
+
/** Every specifier in a packed manifest's runtime maps that only the workspace could resolve (see `UNRESOLVABLE`). */
|
|
157
|
+
const unresolvedSpecifiers = (manifestJson) => {
|
|
158
|
+
let manifest;
|
|
159
|
+
try {
|
|
160
|
+
manifest = JSON.parse(manifestJson);
|
|
161
|
+
} catch (cause) {
|
|
162
|
+
return Result.fail(cause);
|
|
163
|
+
}
|
|
164
|
+
if (typeof manifest !== "object" || manifest === null) return Result.fail(/* @__PURE__ */ new Error("package.json is not an object"));
|
|
165
|
+
const record = manifest;
|
|
166
|
+
return Result.succeed(RUNTIME_DEPENDENCY_FIELDS.flatMap((field) => {
|
|
167
|
+
const block = record[field];
|
|
168
|
+
if (typeof block !== "object" || block === null) return [];
|
|
169
|
+
return Object.entries(block).filter(([, spec]) => typeof spec === "string" && UNRESOLVABLE.test(spec)).map(([name, spec]) => `${field}.${name}: ${String(spec)}`);
|
|
170
|
+
}));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
//#endregion
|
|
174
|
+
export { closureOf, consumerFiles, installArgs, scrubEnv, unresolvedSpecifiers, versionOf };
|