@platforma-sdk/block-tools 2.10.13 → 2.10.15
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/cli.js +23 -2
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1944 -100
- package/dist/cli.mjs.map +1 -1
- package/dist/cmd/index.d.ts +10 -4
- package/dist/cmd/structure/check.d.ts +10 -0
- package/dist/cmd/structure/init.d.ts +18 -0
- package/dist/cmd/structure/refresh.d.ts +11 -0
- package/dist/cmd/update-deps.d.ts +7 -0
- package/dist/structure/cli/run-structure.d.ts +19 -0
- package/dist/structure/engine/api.d.ts +55 -0
- package/dist/structure/engine/builders.d.ts +95 -0
- package/dist/structure/engine/content-rules.d.ts +148 -0
- package/dist/structure/engine/ctx.d.ts +34 -0
- package/dist/structure/engine/discovery-fs.d.ts +22 -0
- package/dist/structure/engine/discovery.d.ts +39 -0
- package/dist/structure/engine/flatten.d.ts +3 -0
- package/dist/structure/engine/fs/api.d.ts +27 -0
- package/dist/structure/engine/fs/memory.d.ts +16 -0
- package/dist/structure/engine/fs/node.d.ts +14 -0
- package/dist/structure/engine/glob.d.ts +4 -0
- package/dist/structure/engine/ir.d.ts +124 -0
- package/dist/structure/engine/parsers/json.d.ts +37 -0
- package/dist/structure/engine/parsers/lines.d.ts +9 -0
- package/dist/structure/engine/parsers/yaml.d.ts +18 -0
- package/dist/structure/engine/registry-client.d.ts +34 -0
- package/dist/structure/engine/runner.d.ts +45 -0
- package/dist/structure/engine/templates.d.ts +30 -0
- package/dist/structure/engine/testing.d.ts +34 -0
- package/dist/structure/engine/version.d.ts +18 -0
- package/dist/structure/init-block-constructor.d.ts +32 -0
- package/dist/structure/rules/block-package-json.d.ts +9 -0
- package/dist/structure/rules/block.d.ts +1 -0
- package/dist/structure/rules/migrations.d.ts +2 -0
- package/dist/structure/rules/model-package-json.d.ts +3 -0
- package/dist/structure/rules/model.d.ts +1 -0
- package/dist/structure/rules/root-catalog-bump.d.ts +1 -0
- package/dist/structure/rules/root-gitignore.d.ts +2 -0
- package/dist/structure/rules/root-package-json.d.ts +3 -0
- package/dist/structure/rules/root-pnpm-workspace.d.ts +15 -0
- package/dist/structure/rules/root.d.ts +1 -0
- package/dist/structure/rules/shared/colocated-tests.d.ts +2 -0
- package/dist/structure/rules/shared/key-order.d.ts +1 -0
- package/dist/structure/rules/software-package-json.d.ts +3 -0
- package/dist/structure/rules/software.d.ts +1 -0
- package/dist/structure/rules/test-package-json.d.ts +3 -0
- package/dist/structure/rules/test.d.ts +1 -0
- package/dist/structure/rules/ui-package-json.d.ts +3 -0
- package/dist/structure/rules/ui.d.ts +1 -0
- package/dist/structure/rules/workflow-package-json.d.ts +3 -0
- package/dist/structure/rules/workflow.d.ts +1 -0
- package/dist/structure/structure-definition.d.ts +1 -0
- package/package.json +8 -6
- package/src/cmd/index.ts +10 -4
- package/src/cmd/structure/check.ts +51 -0
- package/src/cmd/structure/init.ts +65 -0
- package/src/cmd/structure/refresh.ts +44 -0
- package/src/cmd/update-deps.ts +15 -3
- package/src/structure/__tests__/colocated-test-wiring.test.ts +107 -0
- package/src/structure/__tests__/floor-version.test.ts +52 -0
- package/src/structure/__tests__/init-catalog-resolution.test.ts +82 -0
- package/src/structure/__tests__/init-check-parity.test.ts +59 -0
- package/src/structure/__tests__/model-package-json.snapshot.test.ts +60 -0
- package/src/structure/__tests__/oxfmt-clean-init.test.ts +64 -0
- package/src/structure/__tests__/root-catalog-bump.test.ts +110 -0
- package/src/structure/__tests__/seed-disjointness.test.ts +79 -0
- package/src/structure/cli/run-structure.ts +89 -0
- package/src/structure/engine/__tests__/content-rules/active-state-guards.test.ts +61 -0
- package/src/structure/engine/__tests__/content-rules/catalog-pin.test.ts +46 -0
- package/src/structure/engine/__tests__/content-rules/enforce-alphabetical-order.test.ts +55 -0
- package/src/structure/engine/__tests__/content-rules/enforce-field-order.test.ts +73 -0
- package/src/structure/engine/__tests__/content-rules/ensure-catalog-absent.test.ts +29 -0
- package/src/structure/engine/__tests__/content-rules/ensure-catalog-latest.test.ts +70 -0
- package/src/structure/engine/__tests__/content-rules/ensure-dep.test.ts +63 -0
- package/src/structure/engine/__tests__/content-rules/ensure-deps-bulk.test.ts +46 -0
- package/src/structure/engine/__tests__/content-rules/ensure-field-entries.test.ts +35 -0
- package/src/structure/engine/__tests__/content-rules/ensure-field.test.ts +56 -0
- package/src/structure/engine/__tests__/content-rules/ensure-script.test.ts +39 -0
- package/src/structure/engine/__tests__/content-rules/ensure-workspace-scope-deps.test.ts +92 -0
- package/src/structure/engine/__tests__/content-rules/gitignore.test.ts +50 -0
- package/src/structure/engine/__tests__/content-rules/pin-catalog-to.test.ts +64 -0
- package/src/structure/engine/__tests__/content-rules/prune-keys.test.ts +59 -0
- package/src/structure/engine/__tests__/content-rules/registry-client.test.ts +116 -0
- package/src/structure/engine/__tests__/content-rules/remove-field.test.ts +49 -0
- package/src/structure/engine/__tests__/content-rules/require-field.test.ts +40 -0
- package/src/structure/engine/__tests__/content-rules/transform-at.test.ts +33 -0
- package/src/structure/engine/__tests__/content-rules/when-inner.test.ts +244 -0
- package/src/structure/engine/__tests__/content-rules/workspace-module-paths.test.ts +53 -0
- package/src/structure/engine/__tests__/discovery.test.ts +160 -0
- package/src/structure/engine/__tests__/fs-memory.test.ts +59 -0
- package/src/structure/engine/__tests__/glob.test.ts +43 -0
- package/src/structure/engine/__tests__/parsers-json.test.ts +189 -0
- package/src/structure/engine/__tests__/parsers-lines.test.ts +44 -0
- package/src/structure/engine/__tests__/parsers-yaml.test.ts +90 -0
- package/src/structure/engine/__tests__/recheck.test.ts +99 -0
- package/src/structure/engine/__tests__/registry-lookup.test.ts +49 -0
- package/src/structure/engine/__tests__/runner-when-inside-managed.test.ts +136 -0
- package/src/structure/engine/__tests__/runner.test.ts +136 -0
- package/src/structure/engine/__tests__/synthetic.test.ts +153 -0
- package/src/structure/engine/__tests__/version.test.ts +44 -0
- package/src/structure/engine/__tests__/when-files-exist.test.ts +89 -0
- package/src/structure/engine/api.ts +131 -0
- package/src/structure/engine/builders.ts +341 -0
- package/src/structure/engine/content-rules.ts +616 -0
- package/src/structure/engine/ctx.ts +94 -0
- package/src/structure/engine/discovery-fs.ts +209 -0
- package/src/structure/engine/discovery.ts +168 -0
- package/src/structure/engine/flatten.ts +117 -0
- package/src/structure/engine/fs/api.ts +40 -0
- package/src/structure/engine/fs/memory.ts +132 -0
- package/src/structure/engine/fs/node.ts +89 -0
- package/src/structure/engine/glob.ts +42 -0
- package/src/structure/engine/ir.ts +149 -0
- package/src/structure/engine/parsers/json.ts +212 -0
- package/src/structure/engine/parsers/lines.ts +42 -0
- package/src/structure/engine/parsers/yaml.ts +72 -0
- package/src/structure/engine/registry-client.ts +140 -0
- package/src/structure/engine/runner.ts +519 -0
- package/src/structure/engine/templates.ts +74 -0
- package/src/structure/engine/testing.ts +93 -0
- package/src/structure/engine/version.ts +60 -0
- package/src/structure/init-block-constructor.ts +164 -0
- package/src/structure/rules/block-package-json.ts +156 -0
- package/src/structure/rules/block.ts +29 -0
- package/src/structure/rules/migrations.ts +63 -0
- package/src/structure/rules/model-package-json.ts +123 -0
- package/src/structure/rules/model.ts +65 -0
- package/src/structure/rules/root-catalog-bump.ts +77 -0
- package/src/structure/rules/root-gitignore.ts +27 -0
- package/src/structure/rules/root-package-json.ts +119 -0
- package/src/structure/rules/root-pnpm-workspace.ts +103 -0
- package/src/structure/rules/root.ts +43 -0
- package/src/structure/rules/shared/colocated-tests.ts +11 -0
- package/src/structure/rules/shared/key-order.ts +34 -0
- package/src/structure/rules/software-package-json.ts +86 -0
- package/src/structure/rules/software.ts +33 -0
- package/src/structure/rules/test-package-json.ts +70 -0
- package/src/structure/rules/test.ts +40 -0
- package/src/structure/rules/ui-package-json.ts +108 -0
- package/src/structure/rules/ui.ts +75 -0
- package/src/structure/rules/workflow-package-json.ts +92 -0
- package/src/structure/rules/workflow.ts +51 -0
- package/src/structure/structure-definition.ts +28 -0
- package/src/structure/templates/static/block/index.d.ts +6 -0
- package/src/structure/templates/static/block/index.js +8 -0
- package/src/structure/templates/static/block/logos/block-logo.png +0 -0
- package/src/structure/templates/static/block/logos/organization-logo.png +0 -0
- package/src/structure/templates/static/model/.oxfmtrc.json +4 -0
- package/src/structure/templates/static/model/.oxlintrc.json +3 -0
- package/src/structure/templates/static/model/src/index.ts +13 -0
- package/src/structure/templates/static/model/tsconfig.json +4 -0
- package/src/structure/templates/static/root/.gitignore +13 -0
- package/src/structure/templates/static/root/.vscode/settings.json +3 -0
- package/src/structure/templates/static/root/turbo.json +35 -0
- package/src/structure/templates/static/test/tsconfig.json +5 -0
- package/src/structure/templates/static/test/vitest.config.mts +8 -0
- package/src/structure/templates/static/ui/.oxfmtrc.json +3 -0
- package/src/structure/templates/static/ui/.oxlintrc.json +3 -0
- package/src/structure/templates/static/ui/index.html +12 -0
- package/src/structure/templates/static/ui/src/main.ts +5 -0
- package/src/structure/templates/static/ui/tsconfig.json +4 -0
- package/src/structure/templates/static/workflow/format.el +42 -0
- package/src/structure/templates/static/workflow/tsconfig.json +16 -0
- package/src/structure/templates/static/workflow/vitest.config.mts +9 -0
- package/src/structure/templates/text/README.md +5 -0
- package/src/structure/templates/text/docs/description.md +3 -0
- package/src/structure/templates/text/ui/src/MainPage.tpl.vue +9 -0
- package/src/structure/templates/text/ui/src/app.tpl.ts +11 -0
- package/src/structure/templates/text/workflow/src/main.tpl.tengo +10 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
// Content-rules DSL — builders that mutate the parsed representation
|
|
2
|
+
// of a `managed(path, initial, body)` file.
|
|
3
|
+
//
|
|
4
|
+
// Three active-state kinds correspond to three file flavours:
|
|
5
|
+
// - JSON object (package.json) → withManagedBody(obj, body)
|
|
6
|
+
// - YAML Document (pnpm-workspace.yaml) → withManagedYaml(doc, body)
|
|
7
|
+
// - line list (.gitignore) → withManagedLines(lines, body)
|
|
8
|
+
//
|
|
9
|
+
// Each builder verifies it is invoked under the matching active state
|
|
10
|
+
// and throws otherwise. The full inventory: JSON
|
|
11
|
+
// ensure*/remove*/require*/prune/enforce*/transform; YAML catalog
|
|
12
|
+
// management + workspace module paths; gitignore line management.
|
|
13
|
+
|
|
14
|
+
import { registerInnerWhenHandler, tryGetActiveRunContext } from "./builders";
|
|
15
|
+
import type { RunContext, Scope } from "./api";
|
|
16
|
+
import type { TriggerContext, TriggerFn } from "./ir";
|
|
17
|
+
import {
|
|
18
|
+
deleteAtPath,
|
|
19
|
+
getAtPath,
|
|
20
|
+
hasAtPath,
|
|
21
|
+
reorderTopLevel,
|
|
22
|
+
setAtPath,
|
|
23
|
+
type JsonObject,
|
|
24
|
+
} from "./parsers/json";
|
|
25
|
+
import { containsEntry, normaliseLine } from "./parsers/lines";
|
|
26
|
+
import type { YamlDocument } from "./parsers/yaml";
|
|
27
|
+
|
|
28
|
+
export type { JsonObject };
|
|
29
|
+
|
|
30
|
+
/** Allowed dep version strings. Specific versions live in the catalog.
|
|
31
|
+
* `"sdk:"` is an authoring abstraction, not an on-disk value: the engine
|
|
32
|
+
* resolves it at write time (`workspace:*` when `ctx.isSdkInternal`, else
|
|
33
|
+
* `"catalog:"`) and the resolved literal is what serializes. Use it for
|
|
34
|
+
* every dep that is a member of the platforma monorepo's pnpm workspace
|
|
35
|
+
* (the `@platforma-sdk/*` packages + `@milaboratories/ts-builder` /
|
|
36
|
+
* `ts-configs`). */
|
|
37
|
+
export type DepVersion = "catalog:" | "sdk:" | `workspace:${string}` | "*";
|
|
38
|
+
|
|
39
|
+
/** Resolve the `"sdk:"` sentinel to its on-disk literal. All other
|
|
40
|
+
* `DepVersion` forms pass through unchanged — `"sdk:"` never serializes. */
|
|
41
|
+
export function resolveDepVersion(
|
|
42
|
+
version: DepVersion,
|
|
43
|
+
isSdkInternal: boolean,
|
|
44
|
+
): Exclude<DepVersion, "sdk:"> {
|
|
45
|
+
if (version === "sdk:") return isSdkInternal ? "workspace:*" : "catalog:";
|
|
46
|
+
return version;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const DEP_SECTION_KEYS = [
|
|
50
|
+
"dependencies",
|
|
51
|
+
"devDependencies",
|
|
52
|
+
"peerDependencies",
|
|
53
|
+
"optionalDependencies",
|
|
54
|
+
] as const;
|
|
55
|
+
|
|
56
|
+
/** Resolve any `"sdk:"` sentinels in the dep sections of a generated
|
|
57
|
+
* package.json object, in place. Mirrors `resolveDepVersion` for the
|
|
58
|
+
* generator (`initial` content) path — the runner calls this on every
|
|
59
|
+
* `generate()`-produced `.json` value so the sentinel never reaches disk
|
|
60
|
+
* via a generator. */
|
|
61
|
+
export function resolveSdkSentinelsInJson(value: unknown, isSdkInternal: boolean): void {
|
|
62
|
+
if (!isObject(value)) return;
|
|
63
|
+
for (const section of DEP_SECTION_KEYS) {
|
|
64
|
+
const sec = value[section];
|
|
65
|
+
if (!isObject(sec)) continue;
|
|
66
|
+
for (const k of Object.keys(sec)) {
|
|
67
|
+
if (sec[k] === "sdk:") sec[k] = isSdkInternal ? "workspace:*" : "catalog:";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type JsonState = {
|
|
73
|
+
kind: "json";
|
|
74
|
+
obj: JsonObject;
|
|
75
|
+
ctx?: RunContext;
|
|
76
|
+
triggerContext?: TriggerContext;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type YamlState = {
|
|
80
|
+
kind: "yaml";
|
|
81
|
+
doc: YamlDocument;
|
|
82
|
+
ctx?: RunContext;
|
|
83
|
+
triggerContext?: TriggerContext;
|
|
84
|
+
/** Synchronous accessor for prefetched npm latest versions. */
|
|
85
|
+
getLatestVersion?: (packageName: string) => string | undefined;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type LinesState = {
|
|
89
|
+
kind: "lines";
|
|
90
|
+
lines: string[];
|
|
91
|
+
ctx?: RunContext;
|
|
92
|
+
triggerContext?: TriggerContext;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type ActiveState = JsonState | YamlState | LinesState;
|
|
96
|
+
|
|
97
|
+
let active: ActiveState | undefined;
|
|
98
|
+
|
|
99
|
+
function requireActive(builder: string): ActiveState {
|
|
100
|
+
if (!active) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`${builder}() only valid inside a managed(...) body. ` +
|
|
103
|
+
`Move this call inside managed(path, initial, () => {...}).`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return active;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function requireJson(builder: string): JsonState {
|
|
110
|
+
const a = requireActive(builder);
|
|
111
|
+
if (a.kind !== "json") {
|
|
112
|
+
throw new Error(`${builder}() requires a JSON-managed body; got '${a.kind}'.`);
|
|
113
|
+
}
|
|
114
|
+
return a;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function requireYaml(builder: string): YamlState {
|
|
118
|
+
const a = requireActive(builder);
|
|
119
|
+
if (a.kind !== "yaml") {
|
|
120
|
+
throw new Error(`${builder}() requires a YAML-managed body; got '${a.kind}'.`);
|
|
121
|
+
}
|
|
122
|
+
return a;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function requireLines(builder: string): LinesState {
|
|
126
|
+
const a = requireActive(builder);
|
|
127
|
+
if (a.kind !== "lines") {
|
|
128
|
+
throw new Error(`${builder}() requires a lines-managed body; got '${a.kind}'.`);
|
|
129
|
+
}
|
|
130
|
+
return a;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ctxOrThrow(state: ActiveState, builder: string): RunContext {
|
|
134
|
+
const c = state.ctx ?? tryGetActiveRunContext();
|
|
135
|
+
if (!c) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`${builder}() needs a RunContext but none is active. ` +
|
|
138
|
+
`Pass { ctx } via the with-managed-* helper or run inside engine.run(...).`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return c;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isObject(v: unknown): v is JsonObject {
|
|
145
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Active-context wrappers (used by the runner + Layer-1 helpers) ---
|
|
149
|
+
|
|
150
|
+
export type WithManagedOpts = {
|
|
151
|
+
ctx?: RunContext;
|
|
152
|
+
/** Trigger context for `when(...)` calls inside the body. The runner
|
|
153
|
+
* builds this from the post-structural-pass FS snapshot + run ctx.
|
|
154
|
+
* Without it, `when(...)` inside the body throws. */
|
|
155
|
+
triggerContext?: TriggerContext;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** Engine-internal: run `body` with `obj` as the active JSON state so the
|
|
159
|
+
* JSON builders mutate it; returns the mutated object. Exported for tests —
|
|
160
|
+
* the runner calls this for managed `.json` files. */
|
|
161
|
+
export function withManagedBody(
|
|
162
|
+
obj: JsonObject,
|
|
163
|
+
body: () => void,
|
|
164
|
+
opts: WithManagedOpts = {},
|
|
165
|
+
): JsonObject {
|
|
166
|
+
if (active) throw new Error("Nested managed(...) body — engine bug.");
|
|
167
|
+
active = { kind: "json", obj, ctx: opts.ctx, triggerContext: opts.triggerContext };
|
|
168
|
+
try {
|
|
169
|
+
body();
|
|
170
|
+
return obj;
|
|
171
|
+
} finally {
|
|
172
|
+
active = undefined;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type WithManagedYamlOpts = WithManagedOpts & {
|
|
177
|
+
/** Sync accessor for prefetched latest versions. Required for
|
|
178
|
+
* `ensureCatalogLatest` to take effect. */
|
|
179
|
+
getLatestVersion?: (packageName: string) => string | undefined;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** Engine-internal: run `body` with `doc` as the active YAML state (for
|
|
183
|
+
* `pnpm-workspace.yaml`); returns the mutated document. Exported for tests. */
|
|
184
|
+
export function withManagedYaml(
|
|
185
|
+
doc: YamlDocument,
|
|
186
|
+
body: () => void,
|
|
187
|
+
opts: WithManagedYamlOpts = {},
|
|
188
|
+
): YamlDocument {
|
|
189
|
+
if (active) throw new Error("Nested managed(...) body — engine bug.");
|
|
190
|
+
active = {
|
|
191
|
+
kind: "yaml",
|
|
192
|
+
doc,
|
|
193
|
+
ctx: opts.ctx,
|
|
194
|
+
triggerContext: opts.triggerContext,
|
|
195
|
+
getLatestVersion: opts.getLatestVersion,
|
|
196
|
+
};
|
|
197
|
+
try {
|
|
198
|
+
body();
|
|
199
|
+
return doc;
|
|
200
|
+
} finally {
|
|
201
|
+
active = undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Engine-internal: run `body` with `lines` as the active line-based state
|
|
206
|
+
* (for `.gitignore`); returns the mutated lines. Exported for tests. */
|
|
207
|
+
export function withManagedLines(
|
|
208
|
+
lines: string[],
|
|
209
|
+
body: () => void,
|
|
210
|
+
opts: WithManagedOpts = {},
|
|
211
|
+
): string[] {
|
|
212
|
+
if (active) throw new Error("Nested managed(...) body — engine bug.");
|
|
213
|
+
active = {
|
|
214
|
+
kind: "lines",
|
|
215
|
+
lines,
|
|
216
|
+
ctx: opts.ctx,
|
|
217
|
+
triggerContext: opts.triggerContext,
|
|
218
|
+
};
|
|
219
|
+
try {
|
|
220
|
+
body();
|
|
221
|
+
return lines;
|
|
222
|
+
} finally {
|
|
223
|
+
active = undefined;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- Inner-when handler registration ---
|
|
228
|
+
//
|
|
229
|
+
// `when(...)` is exported from `builders.ts`. When the active tree is
|
|
230
|
+
// absent (we are inside a managed body, not inside `defineStructure`),
|
|
231
|
+
// it delegates here. The handler returns true iff there is an inner
|
|
232
|
+
// active state to dispatch against; the caller in `builders.ts` then
|
|
233
|
+
// treats the call as handled. Outer/inner dispatch lives in one
|
|
234
|
+
// exported `when` symbol — no separate inner-when builder.
|
|
235
|
+
|
|
236
|
+
registerInnerWhenHandler((trigger: TriggerFn, body: () => void): boolean => {
|
|
237
|
+
if (!active) return false;
|
|
238
|
+
const tctx = active.triggerContext;
|
|
239
|
+
if (!tctx) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
"when() inside a managed(...) body requires a triggerContext. " +
|
|
242
|
+
"The runner supplies it via the `triggerContext` opt to with-managed-*; " +
|
|
243
|
+
"ensure your test/test helper passes one.",
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (trigger(tctx)) body();
|
|
247
|
+
return true;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ============================================================
|
|
251
|
+
// JSON builders — `package.json` (and other JSON managed files)
|
|
252
|
+
// ============================================================
|
|
253
|
+
|
|
254
|
+
/** Set the value at `jsonPath` in the active managed JSON object. */
|
|
255
|
+
export function ensureField(jsonPath: string, value: unknown): void {
|
|
256
|
+
setAtPath(requireJson("ensureField").obj, jsonPath, value);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Remove the field at `jsonPath`. With `predicate`, only removes when
|
|
260
|
+
* the predicate returns true against the current value. */
|
|
261
|
+
export function removeField(
|
|
262
|
+
jsonPath: string,
|
|
263
|
+
predicate?: (currentValue: unknown) => boolean,
|
|
264
|
+
): void {
|
|
265
|
+
const obj = requireJson("removeField").obj;
|
|
266
|
+
if (!hasAtPath(obj, jsonPath)) return;
|
|
267
|
+
if (predicate) {
|
|
268
|
+
const current = getAtPath(obj, jsonPath);
|
|
269
|
+
if (!predicate(current)) return;
|
|
270
|
+
}
|
|
271
|
+
deleteAtPath(obj, jsonPath);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Assert a field exists; throw if absent. No mutation. */
|
|
275
|
+
export function requireField(jsonPath: string, message?: string): void {
|
|
276
|
+
const obj = requireJson("requireField").obj;
|
|
277
|
+
if (!hasAtPath(obj, jsonPath)) {
|
|
278
|
+
throw new Error(message ?? `requireField: missing field '${jsonPath}'`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Merge `entries` into the object at `jsonPath`. Auto-creates the
|
|
283
|
+
* object if missing. Existing other entries preserved. */
|
|
284
|
+
export function ensureFieldEntries(jsonPath: string, entries: Record<string, unknown>): void {
|
|
285
|
+
const obj = requireJson("ensureFieldEntries").obj;
|
|
286
|
+
const current = hasAtPath(obj, jsonPath) ? getAtPath(obj, jsonPath) : undefined;
|
|
287
|
+
const base: JsonObject = isObject(current) ? current : {};
|
|
288
|
+
for (const [k, v] of Object.entries(entries)) base[k] = v;
|
|
289
|
+
if (!isObject(current)) setAtPath(obj, jsonPath, base);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Set `scripts.<name>` to `command`. */
|
|
293
|
+
export function ensureScript(name: string, command: string): void {
|
|
294
|
+
ensureField(`scripts.${name}`, command);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Remove `scripts.<name>` if present. */
|
|
298
|
+
export function removeScript(name: string): void {
|
|
299
|
+
removeField(`scripts.${name}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// --- Dependency builders ---
|
|
303
|
+
|
|
304
|
+
const DEP_SECTIONS = [
|
|
305
|
+
"dependencies",
|
|
306
|
+
"devDependencies",
|
|
307
|
+
"peerDependencies",
|
|
308
|
+
"optionalDependencies",
|
|
309
|
+
] as const;
|
|
310
|
+
type DepSection = (typeof DEP_SECTIONS)[number];
|
|
311
|
+
|
|
312
|
+
function ensureInSection(
|
|
313
|
+
builder: string,
|
|
314
|
+
name: string,
|
|
315
|
+
version: DepVersion,
|
|
316
|
+
section: DepSection,
|
|
317
|
+
): void {
|
|
318
|
+
const state = requireJson(builder);
|
|
319
|
+
const obj = state.obj;
|
|
320
|
+
// Resolve the `sdk:` sentinel against the run mode before writing — it
|
|
321
|
+
// must never reach disk.
|
|
322
|
+
const resolved =
|
|
323
|
+
version === "sdk:"
|
|
324
|
+
? resolveDepVersion(version, ctxOrThrow(state, builder).isSdkInternal)
|
|
325
|
+
: version;
|
|
326
|
+
// Single-section invariant: remove from any other section first.
|
|
327
|
+
for (const s of DEP_SECTIONS) {
|
|
328
|
+
if (s === section) continue;
|
|
329
|
+
const sec = obj[s];
|
|
330
|
+
if (isObject(sec) && name in sec) delete sec[name];
|
|
331
|
+
}
|
|
332
|
+
const existing = obj[section];
|
|
333
|
+
const target: JsonObject = isObject(existing) ? existing : {};
|
|
334
|
+
target[name] = resolved;
|
|
335
|
+
if (!isObject(existing)) obj[section] = target;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Pin `name` to `version` in `dependencies`. Enforces the single-section
|
|
339
|
+
* invariant — `name` may live in only one dependency section, so it is
|
|
340
|
+
* removed from the others first. A `sdk:` sentinel version is resolved
|
|
341
|
+
* against the run's `--sdk-internal` flag before it reaches disk. */
|
|
342
|
+
export function ensureDep(name: string, version: DepVersion): void {
|
|
343
|
+
ensureInSection("ensureDep", name, version, "dependencies");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Pin `name` to `version` in `devDependencies` (see {@link ensureDep} for the
|
|
347
|
+
* single-section + `sdk:` semantics). */
|
|
348
|
+
export function ensureDevDep(name: string, version: DepVersion): void {
|
|
349
|
+
ensureInSection("ensureDevDep", name, version, "devDependencies");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Pin `name` to `version` in `peerDependencies` (see {@link ensureDep}). */
|
|
353
|
+
export function ensurePeerDep(name: string, version: DepVersion): void {
|
|
354
|
+
ensureInSection("ensurePeerDep", name, version, "peerDependencies");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Pin `name` to `version` in `optionalDependencies` (see {@link ensureDep}). */
|
|
358
|
+
export function ensureOptionalDep(name: string, version: DepVersion): void {
|
|
359
|
+
ensureInSection("ensureOptionalDep", name, version, "optionalDependencies");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Remove `name` from every dependency section. */
|
|
363
|
+
export function removeDep(name: string): void {
|
|
364
|
+
const obj = requireJson("removeDep").obj;
|
|
365
|
+
for (const s of DEP_SECTIONS) {
|
|
366
|
+
const sec = obj[s];
|
|
367
|
+
if (isObject(sec) && name in sec) delete sec[name];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Bulk {@link ensureDep} for each `name → version` entry. */
|
|
372
|
+
export function ensureDeps(entries: Record<string, DepVersion>): void {
|
|
373
|
+
for (const [n, v] of Object.entries(entries)) ensureDep(n, v);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Bulk {@link ensureDevDep} for each `name → version` entry. */
|
|
377
|
+
export function ensureDevDeps(entries: Record<string, DepVersion>): void {
|
|
378
|
+
for (const [n, v] of Object.entries(entries)) ensureDevDep(n, v);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Bulk {@link ensurePeerDep} for each `name → version` entry. */
|
|
382
|
+
export function ensurePeerDeps(entries: Record<string, DepVersion>): void {
|
|
383
|
+
for (const [n, v] of Object.entries(entries)) ensurePeerDep(n, v);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Bulk {@link ensureOptionalDep} for each `name → version` entry. */
|
|
387
|
+
export function ensureOptionalDeps(entries: Record<string, DepVersion>): void {
|
|
388
|
+
for (const [n, v] of Object.entries(entries)) ensureOptionalDep(n, v);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function ensureWorkspaceScopeIn(builder: string, scope: Scope, section: DepSection): void {
|
|
392
|
+
const state = requireJson(builder);
|
|
393
|
+
const ctx = ctxOrThrow(state, builder);
|
|
394
|
+
for (const m of ctx.modules) {
|
|
395
|
+
if (m.scope === scope) {
|
|
396
|
+
ensureInSection(builder, m.name, "workspace:*", section);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Add every discovered module of `scope` as a `workspace:*` entry in
|
|
402
|
+
* `dependencies` — wires a block's intra-workspace links (e.g. the test
|
|
403
|
+
* scope depending on the other scopes). Single-section invariant per
|
|
404
|
+
* {@link ensureDep}. */
|
|
405
|
+
export function ensureWorkspaceScopeDeps(scope: Scope): void {
|
|
406
|
+
ensureWorkspaceScopeIn("ensureWorkspaceScopeDeps", scope, "dependencies");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Like {@link ensureWorkspaceScopeDeps}, into `devDependencies`. */
|
|
410
|
+
export function ensureWorkspaceScopeDevDeps(scope: Scope): void {
|
|
411
|
+
ensureWorkspaceScopeIn("ensureWorkspaceScopeDevDeps", scope, "devDependencies");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Like {@link ensureWorkspaceScopeDeps}, into `peerDependencies`. */
|
|
415
|
+
export function ensureWorkspaceScopePeerDeps(scope: Scope): void {
|
|
416
|
+
ensureWorkspaceScopeIn("ensureWorkspaceScopePeerDeps", scope, "peerDependencies");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// --- Prune ---
|
|
420
|
+
|
|
421
|
+
/** Delete every top-level key for which `predicate(key, value)` is true. */
|
|
422
|
+
export function pruneKeysMatching(predicate: (key: string, value: unknown) => boolean): void {
|
|
423
|
+
const obj = requireJson("pruneKeysMatching").obj;
|
|
424
|
+
for (const k of Object.keys(obj)) {
|
|
425
|
+
if (predicate(k, obj[k])) delete obj[k];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Delete every key of the object at `jsonPath` for which
|
|
430
|
+
* `predicate(key, value)` is true. No-op if the path is not an object. */
|
|
431
|
+
export function pruneKeysMatchingAt(
|
|
432
|
+
jsonPath: string,
|
|
433
|
+
predicate: (key: string, value: unknown) => boolean,
|
|
434
|
+
): void {
|
|
435
|
+
const obj = requireJson("pruneKeysMatchingAt").obj;
|
|
436
|
+
const sub = getAtPath(obj, jsonPath);
|
|
437
|
+
if (!isObject(sub)) return;
|
|
438
|
+
for (const k of Object.keys(sub)) {
|
|
439
|
+
if (predicate(k, sub[k])) delete sub[k];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// --- Field order ---
|
|
444
|
+
|
|
445
|
+
function applyOrderInPlace(o: JsonObject, order: readonly string[]): void {
|
|
446
|
+
const reordered = reorderTopLevel(o, order);
|
|
447
|
+
for (const k of Object.keys(o)) delete o[k];
|
|
448
|
+
for (const k of Object.keys(reordered)) o[k] = reordered[k];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Reorder top-level keys to `orderedKeys`; keys not listed keep their
|
|
452
|
+
* position relative to the preceding listed key. Typically the LAST call in
|
|
453
|
+
* a managed body, projecting the canonical field order. */
|
|
454
|
+
export function enforceFieldOrder(orderedKeys: string[]): void {
|
|
455
|
+
applyOrderInPlace(requireJson("enforceFieldOrder").obj, orderedKeys);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Like {@link enforceFieldOrder}, applied to the object at `jsonPath`. No-op
|
|
459
|
+
* if the path is not an object. */
|
|
460
|
+
export function enforceFieldOrderAt(jsonPath: string, orderedKeys: string[]): void {
|
|
461
|
+
const obj = requireJson("enforceFieldOrderAt").obj;
|
|
462
|
+
const sub = getAtPath(obj, jsonPath);
|
|
463
|
+
if (!isObject(sub)) return;
|
|
464
|
+
applyOrderInPlace(sub, orderedKeys);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function sortAlphabeticalInPlace(o: JsonObject, recursive: boolean): void {
|
|
468
|
+
const sorted = Object.keys(o).sort();
|
|
469
|
+
const buf: JsonObject = {};
|
|
470
|
+
for (const k of sorted) buf[k] = o[k];
|
|
471
|
+
for (const k of Object.keys(o)) delete o[k];
|
|
472
|
+
for (const k of sorted) {
|
|
473
|
+
o[k] = buf[k];
|
|
474
|
+
if (recursive && isObject(o[k])) sortAlphabeticalInPlace(o[k] as JsonObject, true);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Sort keys alphabetically. Called with no args it is a deliberate no-op
|
|
479
|
+
* (guards against accidental whole-file sorts); pass a `jsonPath` to sort one
|
|
480
|
+
* object, or `{ recursive: true }` to sort the whole tree. Matches oxfmt's
|
|
481
|
+
* dependency-section ordering. */
|
|
482
|
+
export function enforceAlphabeticalOrder(
|
|
483
|
+
jsonPath: string = "",
|
|
484
|
+
opts: { recursive?: boolean } = {},
|
|
485
|
+
): void {
|
|
486
|
+
const recursive = opts.recursive ?? false;
|
|
487
|
+
// Default jsonPath="" with recursive:false is a no-op (prevents accidents).
|
|
488
|
+
if (jsonPath === "" && !recursive) return;
|
|
489
|
+
const obj = requireJson("enforceAlphabeticalOrder").obj;
|
|
490
|
+
if (jsonPath === "") {
|
|
491
|
+
sortAlphabeticalInPlace(obj, true);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const sub = getAtPath(obj, jsonPath);
|
|
495
|
+
if (!isObject(sub)) return;
|
|
496
|
+
sortAlphabeticalInPlace(sub, recursive);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// --- Generic transform escape hatch ---
|
|
500
|
+
|
|
501
|
+
/** Escape hatch: replace the value at `jsonPath` with `transform(current)`,
|
|
502
|
+
* for shapes the typed builders above do not cover. */
|
|
503
|
+
export function transformAt<T = unknown>(jsonPath: string, transform: (current: T) => T): void {
|
|
504
|
+
const obj = requireJson("transformAt").obj;
|
|
505
|
+
const current = getAtPath(obj, jsonPath) as T;
|
|
506
|
+
const next = transform(current);
|
|
507
|
+
setAtPath(obj, jsonPath, next);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ============================================================
|
|
511
|
+
// YAML builders — `pnpm-workspace.yaml`
|
|
512
|
+
// ============================================================
|
|
513
|
+
|
|
514
|
+
/** Set the workspace `packages:` list to all discovered NON-root module
|
|
515
|
+
* paths (sorted lex). The block root ("") is discovered implicitly
|
|
516
|
+
* and is NEVER written to `packages:` — listing "." there breaks turbo's
|
|
517
|
+
* task graph. Equality-guarded — skips the rewrite when the existing
|
|
518
|
+
* list already matches, so a YAML round-trip on an already-canonical
|
|
519
|
+
* file produces zero churn. */
|
|
520
|
+
export function ensureWorkspaceModulePaths(): void {
|
|
521
|
+
const state = requireYaml("ensureWorkspaceModulePaths");
|
|
522
|
+
const ctx = ctxOrThrow(state, "ensureWorkspaceModulePaths");
|
|
523
|
+
const paths = ctx.modules
|
|
524
|
+
.filter((m) => m.path !== "")
|
|
525
|
+
.map((m) => m.path)
|
|
526
|
+
.sort();
|
|
527
|
+
const json = state.doc.toJSON() as { packages?: unknown } | null;
|
|
528
|
+
const cur = json?.packages;
|
|
529
|
+
if (Array.isArray(cur) && cur.length === paths.length && cur.every((v, i) => v === paths[i])) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
state.doc.setIn(["packages"], paths);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function readCatalogString(state: YamlState, name: string): string | undefined {
|
|
536
|
+
const node = state.doc.getIn(["catalog", name]);
|
|
537
|
+
return typeof node === "string" ? node : undefined;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Add `catalog.<name>` at `version` ONLY if the key is absent. An entry
|
|
541
|
+
* the block already carries is left untouched — no overwrite, no
|
|
542
|
+
* downgrade. This is the seeding primitive for curated fixed versions
|
|
543
|
+
* (`INFRA_CATALOG_FLOOR`, the python runenv pin): on
|
|
544
|
+
* `refresh --update-deps-only` a missing standard catalog key is added,
|
|
545
|
+
* while a key the block already pins keeps its own version. To force a
|
|
546
|
+
* specific version regardless of what is present, use `pinCatalogTo`. */
|
|
547
|
+
export function ensureCatalogVersion(name: string, version: string): void {
|
|
548
|
+
const state = requireYaml("ensureCatalogVersion");
|
|
549
|
+
if (readCatalogString(state, name) !== undefined) return;
|
|
550
|
+
state.doc.setIn(["catalog", name], version);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Set `catalog.<name>` to `version` EXACTLY — creates the entry if
|
|
554
|
+
* missing, overwrites it (including a downgrade) if present. Use to force
|
|
555
|
+
* a specific version; with top-to-bottom declaration order a later
|
|
556
|
+
* `pinCatalogTo` overrides an earlier `ensureCatalogLatest`. Distinct
|
|
557
|
+
* from `ensureCatalogVersion`, which never touches a present key. */
|
|
558
|
+
export function pinCatalogTo(name: string, version: string): void {
|
|
559
|
+
const state = requireYaml("pinCatalogTo");
|
|
560
|
+
if (readCatalogString(state, name) === version) return;
|
|
561
|
+
state.doc.setIn(["catalog", name], version);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Resolve `catalog.<name>` to npm "latest" from the active state's
|
|
565
|
+
* prefetched `getLatestVersion` accessor (the caller pre-resolves; no
|
|
566
|
+
* network here), creating the key if absent. No-op when no latest is
|
|
567
|
+
* available — default refresh passes no lookup, and a name that was not
|
|
568
|
+
* prefetched resolves to `undefined`; in both cases any existing entry is
|
|
569
|
+
* left untouched. This is the SDK-family resolver: on
|
|
570
|
+
* `refresh --update-deps-only` it both SEEDS a missing standard SDK key
|
|
571
|
+
* and refreshes a present one, and on `init` it overwrites the
|
|
572
|
+
* placeholder with the real latest. */
|
|
573
|
+
export function ensureCatalogLatest(name: string): void {
|
|
574
|
+
const state = requireYaml("ensureCatalogLatest");
|
|
575
|
+
if (!state.getLatestVersion) return;
|
|
576
|
+
const latest = state.getLatestVersion(name);
|
|
577
|
+
if (latest === undefined) return;
|
|
578
|
+
if (readCatalogString(state, name) === latest) return;
|
|
579
|
+
state.doc.setIn(["catalog", name], latest);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** Remove `catalog.<name>` if present; no-op when absent. */
|
|
583
|
+
export function ensureCatalogAbsent(name: string): void {
|
|
584
|
+
const state = requireYaml("ensureCatalogAbsent");
|
|
585
|
+
if (!state.doc.hasIn(["catalog", name])) return;
|
|
586
|
+
state.doc.deleteIn(["catalog", name]);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ============================================================
|
|
590
|
+
// Lines builders — `.gitignore`
|
|
591
|
+
// ============================================================
|
|
592
|
+
|
|
593
|
+
/** Append each `entry` if not already present (comment-aware equality). */
|
|
594
|
+
export function ensureGitignoreEntries(entries: string[]): void {
|
|
595
|
+
const state = requireLines("ensureGitignoreEntries");
|
|
596
|
+
for (const e of entries) {
|
|
597
|
+
if (containsEntry(state.lines, e)) continue;
|
|
598
|
+
state.lines.push(e);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Drop any line whose normalised form matches any of `patterns`. */
|
|
603
|
+
export function removeGitignoreEntries(patterns: RegExp[]): void {
|
|
604
|
+
const state = requireLines("removeGitignoreEntries");
|
|
605
|
+
const kept: string[] = [];
|
|
606
|
+
for (const line of state.lines) {
|
|
607
|
+
const n = normaliseLine(line);
|
|
608
|
+
if (n === "") {
|
|
609
|
+
kept.push(line);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (patterns.some((p) => p.test(n))) continue;
|
|
613
|
+
kept.push(line);
|
|
614
|
+
}
|
|
615
|
+
state.lines.splice(0, state.lines.length, ...kept);
|
|
616
|
+
}
|