@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,519 @@
|
|
|
1
|
+
// Engine runner — structural pass, managed pass, post-run recheck.
|
|
2
|
+
//
|
|
3
|
+
// `run(structure, fs, ctx, opts)` drives one block through the structural
|
|
4
|
+
// pass, managed pass, and post-run recheck (DISCOVERY happens in the
|
|
5
|
+
// caller, which constructs `ctx.modules`). The function is shared by
|
|
6
|
+
// `check`, `refresh`,
|
|
7
|
+
// `refresh --update-deps-only`, and `init`. The mode (see `currentMode`)
|
|
8
|
+
// selects which leaves fire — a leaf fires iff its `modes` include the
|
|
9
|
+
// current one:
|
|
10
|
+
// - dryRun=true → check, mode "default" (no
|
|
11
|
+
// writes)
|
|
12
|
+
// - dryRun=false, updateDepsOnly=false → refresh, mode "default"
|
|
13
|
+
// (writes + .structure bump +
|
|
14
|
+
// post-run recheck)
|
|
15
|
+
// - dryRun=false, updateDepsOnly=true → refresh --update-deps-only,
|
|
16
|
+
// mode "updateDeps" (only
|
|
17
|
+
// onUpdateDeps/onInitOrUpdate
|
|
18
|
+
// leaves fire; no version
|
|
19
|
+
// bump; no recheck)
|
|
20
|
+
// - dryRun=false, opts.initMode=true → init, mode "init" (default
|
|
21
|
+
// + onInitOrUpdate leaves +
|
|
22
|
+
// seeds; no recheck — the
|
|
23
|
+
// init→check test runs an
|
|
24
|
+
// explicit dry-run afterwards)
|
|
25
|
+
//
|
|
26
|
+
// Idempotency invariant is enforced by the post-run recheck: after a
|
|
27
|
+
// successful refresh, the runner reruns the lifecycle in dry-run mode (fresh
|
|
28
|
+
// rediscover when supplied) and throws `RecheckError` on any non-empty
|
|
29
|
+
// change list. The error carries the failing item's
|
|
30
|
+
// (scope, path, primitive, action) tuple for diagnostics.
|
|
31
|
+
|
|
32
|
+
import { stringify as yamlStringify } from "yaml";
|
|
33
|
+
import type { ContentForm, FlatItem, RunMode, Structure, TriggerContext, ManagedBody } from "./ir";
|
|
34
|
+
import type { RunContext, Scope } from "./api";
|
|
35
|
+
import type { FileSystem } from "./fs/api";
|
|
36
|
+
import type { TemplateProvider } from "./templates";
|
|
37
|
+
import { substituteVars } from "./templates";
|
|
38
|
+
import { flatten } from "./flatten";
|
|
39
|
+
import { matchesGlob } from "./glob";
|
|
40
|
+
import { withRunContext } from "./builders";
|
|
41
|
+
import {
|
|
42
|
+
withManagedBody,
|
|
43
|
+
withManagedYaml,
|
|
44
|
+
withManagedLines,
|
|
45
|
+
resolveSdkSentinelsInJson,
|
|
46
|
+
} from "./content-rules";
|
|
47
|
+
import { parseJson, stringifyJson } from "./parsers/json";
|
|
48
|
+
import type { JsonObject } from "./parsers/json";
|
|
49
|
+
import { parseYaml, stringifyYaml } from "./parsers/yaml";
|
|
50
|
+
import { parseLines, serializeLines } from "./parsers/lines";
|
|
51
|
+
import { writeStructureVersion, STRUCTURE_META_FILE } from "./version";
|
|
52
|
+
|
|
53
|
+
export type ChangeAction = "create" | "update" | "delete" | "rename" | "noop";
|
|
54
|
+
|
|
55
|
+
export type Change = {
|
|
56
|
+
scope: Scope;
|
|
57
|
+
/** Resolved (block-relative) path. For rename, the source path. */
|
|
58
|
+
path: string;
|
|
59
|
+
/** "fixed" | "managed" | "scaffold" | "seed" | "remove" | "rename" */
|
|
60
|
+
primitive: string;
|
|
61
|
+
action: ChangeAction;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type RunResult = {
|
|
65
|
+
changes: Change[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type RunOptions = {
|
|
69
|
+
/** Called before the post-run recheck to obtain a fresh RunContext
|
|
70
|
+
* reflecting the just-written FS state (re-reads `pnpm-workspace.yaml`).
|
|
71
|
+
* If absent, recheck reuses the original ctx with `dryRun: true`. */
|
|
72
|
+
rediscover?: (fs: FileSystem) => RunContext;
|
|
73
|
+
/** Template provider used by `file(...)` and `tpl(...)` content forms.
|
|
74
|
+
* Required whenever the structure references such forms. */
|
|
75
|
+
templates?: TemplateProvider;
|
|
76
|
+
/** Init mode: write seed leaves (only-if-missing semantics) and skip
|
|
77
|
+
* the post-run recheck. The caller is expected to follow with an
|
|
78
|
+
* explicit dry-run check (Layer-2 invariant). */
|
|
79
|
+
initMode?: boolean;
|
|
80
|
+
/** Sync accessor for prefetched npm "latest" versions, consumed by
|
|
81
|
+
* `ensureCatalogLatest` inside `onUpdateDeps` managed bodies. The CLI
|
|
82
|
+
* prefetches the catalog's package versions before the run (network)
|
|
83
|
+
* and passes the resulting sync map here; unit tests pass a mock.
|
|
84
|
+
* Absent → `ensureCatalogLatest` is a no-op (default refresh). */
|
|
85
|
+
registryLookup?: (packageName: string) => string | undefined;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export class RecheckError extends Error {
|
|
89
|
+
constructor(public readonly failing: Change) {
|
|
90
|
+
super(
|
|
91
|
+
`Post-run recheck: rule set not idempotent. ` +
|
|
92
|
+
`Failing item: ${failing.primitive} '${failing.path}' ` +
|
|
93
|
+
`(scope=${failing.scope}, action=${failing.action}). Engine bug.`,
|
|
94
|
+
);
|
|
95
|
+
this.name = "RecheckError";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Resolve a `ContentForm` to its on-disk string for the given file
|
|
100
|
+
* path. `file` / `tpl` resolution goes through the configured
|
|
101
|
+
* `TemplateProvider`; `generate` returns parsed structure → JSON/YAML
|
|
102
|
+
* or a string. */
|
|
103
|
+
function resolveContent(
|
|
104
|
+
form: ContentForm,
|
|
105
|
+
filePath: string,
|
|
106
|
+
templates: TemplateProvider | undefined,
|
|
107
|
+
isSdkInternal: boolean,
|
|
108
|
+
): string {
|
|
109
|
+
switch (form.kind) {
|
|
110
|
+
case "text":
|
|
111
|
+
return form.value;
|
|
112
|
+
case "file": {
|
|
113
|
+
if (!templates) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`file('${form.path}'): no TemplateProvider configured. ` +
|
|
116
|
+
`Pass opts.templates to engine.run(...).`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return templates.staticRead(form.path);
|
|
120
|
+
}
|
|
121
|
+
case "tpl": {
|
|
122
|
+
if (!templates) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`tpl('${form.path}'): no TemplateProvider configured. ` +
|
|
125
|
+
`Pass opts.templates to engine.run(...).`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const raw = templates.textRead(form.path);
|
|
129
|
+
const vars = typeof form.vars === "function" ? form.vars() : form.vars;
|
|
130
|
+
return substituteVars(raw, vars);
|
|
131
|
+
}
|
|
132
|
+
case "generate": {
|
|
133
|
+
const value = form.fn();
|
|
134
|
+
if (filePath.endsWith(".json")) {
|
|
135
|
+
// Resolve `sdk:` sentinels in the generated dep sections so the
|
|
136
|
+
// initial content matches what the body rules re-assert.
|
|
137
|
+
resolveSdkSentinelsInJson(value, isSdkInternal);
|
|
138
|
+
// package.json stays fully expanded (oxfmt's package.json shape);
|
|
139
|
+
// every other .json collapses to oxfmt's generic-file shape.
|
|
140
|
+
return stringifyJson(value, { collapse: !filePath.endsWith("package.json") });
|
|
141
|
+
}
|
|
142
|
+
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
|
|
143
|
+
return yamlStringify(value);
|
|
144
|
+
}
|
|
145
|
+
if (typeof value !== "string") {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`generate() for '${filePath}' must return a string for non-JSON/YAML files; got ${typeof value}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
case "binary":
|
|
153
|
+
throw new Error(
|
|
154
|
+
`binary('${form.path}') cannot be resolved to a string; it is only valid ` +
|
|
155
|
+
`inside seed()/scaffold(), which copy it verbatim via fs.writeBinary.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Write a leaf's content to disk, dispatching binary assets (copied
|
|
161
|
+
* verbatim, never utf-8 decoded) from string content. */
|
|
162
|
+
function writeLeafContent(
|
|
163
|
+
fs: FileSystem,
|
|
164
|
+
resolvedPath: string,
|
|
165
|
+
form: ContentForm,
|
|
166
|
+
templates: TemplateProvider | undefined,
|
|
167
|
+
isSdkInternal: boolean,
|
|
168
|
+
): void {
|
|
169
|
+
if (form.kind === "binary") {
|
|
170
|
+
if (!templates) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`binary('${form.path}'): no TemplateProvider configured. ` +
|
|
173
|
+
`Pass opts.templates to engine.run(...).`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
fs.writeBinary(resolvedPath, templates.staticReadBinary(form.path));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
fs.write(resolvedPath, resolveContent(form, resolvedPath, templates, isSdkInternal));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Dispatch managed-body parse → mutate → serialize by file extension /
|
|
183
|
+
* basename. JSON, YAML, and lines all share the same `(initial, body,
|
|
184
|
+
* triggerContext) → newRaw` shape via the active-state wrappers in
|
|
185
|
+
* `content-rules.ts`. */
|
|
186
|
+
function runManagedBody(
|
|
187
|
+
path: string,
|
|
188
|
+
raw: string,
|
|
189
|
+
body: ManagedBody,
|
|
190
|
+
tctx: TriggerContext,
|
|
191
|
+
registryLookup: ((packageName: string) => string | undefined) | undefined,
|
|
192
|
+
): string {
|
|
193
|
+
const base = path.split("/").pop() ?? path;
|
|
194
|
+
if (path.endsWith(".json")) {
|
|
195
|
+
const parsed = parseJson(raw) as JsonObject;
|
|
196
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
197
|
+
throw new Error(`managed: parsed value at '${path}' is not a JSON object`);
|
|
198
|
+
}
|
|
199
|
+
withManagedBody(parsed, body, { triggerContext: tctx });
|
|
200
|
+
// package.json stays fully expanded; other .json (tsconfig, …) collapses
|
|
201
|
+
// to oxfmt's generic-file shape.
|
|
202
|
+
return stringifyJson(parsed, { collapse: base !== "package.json" });
|
|
203
|
+
}
|
|
204
|
+
if (path.endsWith(".yaml") || path.endsWith(".yml")) {
|
|
205
|
+
const doc = parseYaml(raw);
|
|
206
|
+
withManagedYaml(doc, body, { triggerContext: tctx, getLatestVersion: registryLookup });
|
|
207
|
+
return stringifyYaml(doc);
|
|
208
|
+
}
|
|
209
|
+
if (base === ".gitignore" || base.endsWith(".gitignore")) {
|
|
210
|
+
const lines = parseLines(raw);
|
|
211
|
+
withManagedLines(lines, body, { triggerContext: tctx });
|
|
212
|
+
return serializeLines(lines);
|
|
213
|
+
}
|
|
214
|
+
throw new Error(
|
|
215
|
+
`managed('${path}'): unsupported file kind. Only .json, .yaml, .yml, .gitignore are dispatched.`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildSnapshot(fs: FileSystem): Set<string> {
|
|
220
|
+
const files = fs.list("");
|
|
221
|
+
const out = new Set<string>();
|
|
222
|
+
for (const f of files) {
|
|
223
|
+
out.add(f);
|
|
224
|
+
const parts = f.split("/");
|
|
225
|
+
for (let i = 1; i < parts.length; i++) {
|
|
226
|
+
out.add(parts.slice(0, i).join("/"));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function snapshotHas(snap: Set<string>, p: string): boolean {
|
|
233
|
+
const n = p === "" ? p : p.endsWith("/") ? p.slice(0, -1) : p;
|
|
234
|
+
return snap.has(n);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Build the trigger context for one leaf. `pathExists`/`pathMissing` are
|
|
238
|
+
* block-relative; `filesMatch` resolves its glob MODULE-RELATIVE to
|
|
239
|
+
* `modulePath` so the same predicate (`whenFilesExist("src/*.test.ts")`)
|
|
240
|
+
* reads the right module when a scope fans out. The snapshot is shared
|
|
241
|
+
* across leaves; only the resolution prefix differs per item. */
|
|
242
|
+
function buildTriggerCtx(snap: Set<string>, ctx: RunContext, modulePath: string): TriggerContext {
|
|
243
|
+
return {
|
|
244
|
+
ctx,
|
|
245
|
+
version: ctx.version,
|
|
246
|
+
pathExists: (p: string) => snapshotHas(snap, p),
|
|
247
|
+
pathMissing: (p: string) => !snapshotHas(snap, p),
|
|
248
|
+
filesMatch: (glob: string) => {
|
|
249
|
+
const resolved = modulePath ? `${modulePath}/${glob}` : glob;
|
|
250
|
+
for (const entry of snap) {
|
|
251
|
+
if (matchesGlob(resolved, entry)) return true;
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function passesTriggers(item: FlatItem, tctx: TriggerContext): boolean {
|
|
259
|
+
for (const t of item.triggers) {
|
|
260
|
+
if (!t(tctx)) return false;
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Derive the active run mode from the context + init flag. `check`
|
|
266
|
+
* (dryRun) and `refresh` share `"default"` — same leaves, the `dryRun`
|
|
267
|
+
* flag alone decides whether writes land. */
|
|
268
|
+
function currentMode(ctx: RunContext, initMode: boolean): RunMode {
|
|
269
|
+
if (initMode) return "init";
|
|
270
|
+
if (ctx.updateDepsOnly) return "updateDeps";
|
|
271
|
+
return "default";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function filterByMode(items: FlatItem[], mode: RunMode): FlatItem[] {
|
|
275
|
+
return items.filter((i) => i.modes.includes(mode));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function applyStructural(
|
|
279
|
+
item: FlatItem,
|
|
280
|
+
fs: FileSystem,
|
|
281
|
+
dryRun: boolean,
|
|
282
|
+
templates: TemplateProvider | undefined,
|
|
283
|
+
initMode: boolean,
|
|
284
|
+
isSdkInternal: boolean,
|
|
285
|
+
): Change | undefined {
|
|
286
|
+
const leaf = item.leaf;
|
|
287
|
+
if (leaf.kind === "fixed") {
|
|
288
|
+
const desired = resolveContent(leaf.content, item.resolvedPath, templates, isSdkInternal);
|
|
289
|
+
const exists = fs.exists(item.resolvedPath);
|
|
290
|
+
if (!exists) {
|
|
291
|
+
if (!dryRun) fs.write(item.resolvedPath, desired);
|
|
292
|
+
return {
|
|
293
|
+
scope: item.scope,
|
|
294
|
+
path: item.resolvedPath,
|
|
295
|
+
primitive: "fixed",
|
|
296
|
+
action: "create",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const current = fs.read(item.resolvedPath);
|
|
300
|
+
if (current === desired) return undefined;
|
|
301
|
+
if (!dryRun) fs.write(item.resolvedPath, desired);
|
|
302
|
+
return {
|
|
303
|
+
scope: item.scope,
|
|
304
|
+
path: item.resolvedPath,
|
|
305
|
+
primitive: "fixed",
|
|
306
|
+
action: "update",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (leaf.kind === "scaffold") {
|
|
310
|
+
if (fs.exists(item.resolvedPath)) return undefined;
|
|
311
|
+
if (!dryRun) {
|
|
312
|
+
writeLeafContent(fs, item.resolvedPath, leaf.initial, templates, isSdkInternal);
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
scope: item.scope,
|
|
316
|
+
path: item.resolvedPath,
|
|
317
|
+
primitive: "scaffold",
|
|
318
|
+
action: "create",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (leaf.kind === "seed") {
|
|
322
|
+
// Init mode: write seeds once if missing (same shape as scaffold).
|
|
323
|
+
// Refresh / check: seeds are off-limits to the engine — they belong
|
|
324
|
+
// to the block author. Layer-2 init→check parity relies on the
|
|
325
|
+
// init action depositing canonical seed content, then the check
|
|
326
|
+
// pass simply skipping seeds (vacuously fine).
|
|
327
|
+
if (!initMode) return undefined;
|
|
328
|
+
if (fs.exists(item.resolvedPath)) return undefined;
|
|
329
|
+
if (!dryRun) {
|
|
330
|
+
writeLeafContent(fs, item.resolvedPath, leaf.initial, templates, isSdkInternal);
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
scope: item.scope,
|
|
334
|
+
path: item.resolvedPath,
|
|
335
|
+
primitive: "seed",
|
|
336
|
+
action: "create",
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (leaf.kind === "remove") {
|
|
340
|
+
if (!fs.exists(item.resolvedPath)) return undefined;
|
|
341
|
+
if (!dryRun) fs.delete(item.resolvedPath);
|
|
342
|
+
return {
|
|
343
|
+
scope: item.scope,
|
|
344
|
+
path: item.resolvedPath,
|
|
345
|
+
primitive: "remove",
|
|
346
|
+
action: "delete",
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (leaf.kind === "rename") {
|
|
350
|
+
if (!fs.exists(item.resolvedPath)) return undefined;
|
|
351
|
+
const to = item.resolvedTo!;
|
|
352
|
+
if (fs.exists(to)) {
|
|
353
|
+
// dest exists — treat as already-renamed; no change.
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
if (!dryRun) fs.move(item.resolvedPath, to);
|
|
357
|
+
return {
|
|
358
|
+
scope: item.scope,
|
|
359
|
+
path: item.resolvedPath,
|
|
360
|
+
primitive: "rename",
|
|
361
|
+
action: "rename",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function applyManaged(
|
|
368
|
+
item: FlatItem,
|
|
369
|
+
fs: FileSystem,
|
|
370
|
+
dryRun: boolean,
|
|
371
|
+
tctx: TriggerContext,
|
|
372
|
+
templates: TemplateProvider | undefined,
|
|
373
|
+
registryLookup: ((packageName: string) => string | undefined) | undefined,
|
|
374
|
+
isSdkInternal: boolean,
|
|
375
|
+
): Change | undefined {
|
|
376
|
+
if (item.leaf.kind !== "managed") return undefined;
|
|
377
|
+
const leaf = item.leaf;
|
|
378
|
+
const path = item.resolvedPath;
|
|
379
|
+
const exists = fs.exists(path);
|
|
380
|
+
|
|
381
|
+
let beforeRaw: string;
|
|
382
|
+
let created = false;
|
|
383
|
+
|
|
384
|
+
if (!exists) {
|
|
385
|
+
beforeRaw = resolveContent(leaf.initial, path, templates, isSdkInternal);
|
|
386
|
+
created = true;
|
|
387
|
+
} else {
|
|
388
|
+
beforeRaw = fs.read(path);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const after = runManagedBody(path, beforeRaw, leaf.body, tctx, registryLookup);
|
|
392
|
+
|
|
393
|
+
if (created) {
|
|
394
|
+
if (!dryRun) fs.write(path, after);
|
|
395
|
+
return {
|
|
396
|
+
scope: item.scope,
|
|
397
|
+
path,
|
|
398
|
+
primitive: "managed",
|
|
399
|
+
action: "create",
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (after === beforeRaw) return undefined;
|
|
403
|
+
if (!dryRun) fs.write(path, after);
|
|
404
|
+
return {
|
|
405
|
+
scope: item.scope,
|
|
406
|
+
path,
|
|
407
|
+
primitive: "managed",
|
|
408
|
+
action: "update",
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Execute one pass over the flattened items: structural items first
|
|
413
|
+
* (declaration order), then managed items. Returns the change list. */
|
|
414
|
+
function executePass(
|
|
415
|
+
flat: FlatItem[],
|
|
416
|
+
fs: FileSystem,
|
|
417
|
+
ctx: RunContext,
|
|
418
|
+
dryRun: boolean,
|
|
419
|
+
templates: TemplateProvider | undefined,
|
|
420
|
+
initMode: boolean,
|
|
421
|
+
registryLookup: ((packageName: string) => string | undefined) | undefined,
|
|
422
|
+
): Change[] {
|
|
423
|
+
const items = filterByMode(flat, currentMode(ctx, initMode));
|
|
424
|
+
const changes: Change[] = [];
|
|
425
|
+
|
|
426
|
+
// Structural pass. The trigger context is per-item: `filesMatch` resolves
|
|
427
|
+
// module-relative, so it must carry the leaf's bound module path.
|
|
428
|
+
const snap1 = buildSnapshot(fs);
|
|
429
|
+
for (const item of items) {
|
|
430
|
+
if (item.leaf.kind === "managed") continue;
|
|
431
|
+
const tctx1 = buildTriggerCtx(snap1, ctx, item.modulePath);
|
|
432
|
+
if (!passesTriggers(item, tctx1)) continue;
|
|
433
|
+
const change = applyStructural(item, fs, dryRun, templates, initMode, ctx.isSdkInternal);
|
|
434
|
+
if (change) changes.push(change);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Re-snapshot after structural pass — managed-body triggers see the
|
|
438
|
+
// post-rename / post-delete world.
|
|
439
|
+
const snap2 = buildSnapshot(fs);
|
|
440
|
+
for (const item of items) {
|
|
441
|
+
if (item.leaf.kind !== "managed") continue;
|
|
442
|
+
const tctx2 = buildTriggerCtx(snap2, ctx, item.modulePath);
|
|
443
|
+
if (!passesTriggers(item, tctx2)) continue;
|
|
444
|
+
const change = applyManaged(
|
|
445
|
+
item,
|
|
446
|
+
fs,
|
|
447
|
+
dryRun,
|
|
448
|
+
tctx2,
|
|
449
|
+
templates,
|
|
450
|
+
registryLookup,
|
|
451
|
+
ctx.isSdkInternal,
|
|
452
|
+
);
|
|
453
|
+
if (change) changes.push(change);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return changes;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Run the structurer engine end-to-end. See module header for mode
|
|
461
|
+
* semantics. Throws `RecheckError` if the post-run dry-run finds any
|
|
462
|
+
* leftover diff after a successful refresh.
|
|
463
|
+
*/
|
|
464
|
+
export function run(
|
|
465
|
+
structure: Structure,
|
|
466
|
+
fs: FileSystem,
|
|
467
|
+
ctx: RunContext,
|
|
468
|
+
opts: RunOptions = {},
|
|
469
|
+
): RunResult {
|
|
470
|
+
return withRunContext(ctx, () => {
|
|
471
|
+
const flat = flatten(structure, ctx);
|
|
472
|
+
const initMode = !!opts.initMode;
|
|
473
|
+
const changes = executePass(
|
|
474
|
+
flat,
|
|
475
|
+
fs,
|
|
476
|
+
ctx,
|
|
477
|
+
ctx.dryRun,
|
|
478
|
+
opts.templates,
|
|
479
|
+
initMode,
|
|
480
|
+
opts.registryLookup,
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const isRefreshDefault = !ctx.dryRun && !ctx.updateDepsOnly && !initMode;
|
|
484
|
+
|
|
485
|
+
if (isRefreshDefault) {
|
|
486
|
+
// version bump
|
|
487
|
+
writeStructureVersion(fs);
|
|
488
|
+
|
|
489
|
+
// post-run recheck. Fresh discovery → fresh flatten →
|
|
490
|
+
// dry-run. Any change list emits RecheckError on the first item.
|
|
491
|
+
const recheckCtxBase = opts.rediscover ? opts.rediscover(fs) : ctx;
|
|
492
|
+
const recheckCtx: RunContext = { ...recheckCtxBase, dryRun: true };
|
|
493
|
+
const recheckFlat = flatten(structure, recheckCtx);
|
|
494
|
+
const recheckChanges = executePass(
|
|
495
|
+
recheckFlat,
|
|
496
|
+
fs,
|
|
497
|
+
recheckCtx,
|
|
498
|
+
true,
|
|
499
|
+
opts.templates,
|
|
500
|
+
false,
|
|
501
|
+
opts.registryLookup,
|
|
502
|
+
);
|
|
503
|
+
// Ignore .structure self-write (already up to date after
|
|
504
|
+
// writeStructureVersion above).
|
|
505
|
+
const filtered = recheckChanges.filter((c) => c.path !== STRUCTURE_META_FILE);
|
|
506
|
+
if (filtered.length > 0) {
|
|
507
|
+
throw new RecheckError(filtered[0]!);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Init mode: write .structure so the resulting tree carries the
|
|
512
|
+
// current version (matches what `init` ships to disk).
|
|
513
|
+
if (initMode && !ctx.dryRun) {
|
|
514
|
+
writeStructureVersion(fs);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return { changes };
|
|
518
|
+
});
|
|
519
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Template-content provider for the runner.
|
|
2
|
+
//
|
|
3
|
+
// `file(path)` and `tpl(path, vars)` content forms resolve through this
|
|
4
|
+
// provider. Static templates live at `<root>/static/`; templated text
|
|
5
|
+
// lives at `<root>/text/`. The runner accepts a TemplateProvider via
|
|
6
|
+
// `RunOptions.templates`; the production CLI wires a NodeTemplateProvider
|
|
7
|
+
// rooted at the bundled `templates/` directory, and tests can use
|
|
8
|
+
// `MemoryTemplateProvider` or point at the on-disk templates directory
|
|
9
|
+
// during the Layer-2 zero-diff invariant.
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
export interface TemplateProvider {
|
|
15
|
+
/** Read a static template file (under `<root>/static/`). */
|
|
16
|
+
staticRead(p: string): string;
|
|
17
|
+
/** Read a static template file as raw bytes (under `<root>/static/`).
|
|
18
|
+
* For binary assets (logos, images) that must not be utf-8 decoded. */
|
|
19
|
+
staticReadBinary(p: string): Uint8Array;
|
|
20
|
+
/** Read a text template file (under `<root>/text/`). */
|
|
21
|
+
textRead(p: string): string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Provider backed by an on-disk template tree. Reads are synchronous
|
|
25
|
+
* so the runner can stay sync inside content-form resolution. */
|
|
26
|
+
export class NodeTemplateProvider implements TemplateProvider {
|
|
27
|
+
constructor(private root: string) {}
|
|
28
|
+
|
|
29
|
+
staticRead(p: string): string {
|
|
30
|
+
return fs.readFileSync(path.resolve(this.root, "static", p), "utf-8");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
staticReadBinary(p: string): Uint8Array {
|
|
34
|
+
return fs.readFileSync(path.resolve(this.root, "static", p));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
textRead(p: string): string {
|
|
38
|
+
return fs.readFileSync(path.resolve(this.root, "text", p), "utf-8");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** In-memory provider for tests. Keyed by the same `static/...` /
|
|
43
|
+
* `text/...` shape as on disk. */
|
|
44
|
+
export class MemoryTemplateProvider implements TemplateProvider {
|
|
45
|
+
constructor(private files: Record<string, string>) {}
|
|
46
|
+
|
|
47
|
+
staticRead(p: string): string {
|
|
48
|
+
const k = `static/${p}`;
|
|
49
|
+
const v = this.files[k];
|
|
50
|
+
if (v === undefined) throw new Error(`MemoryTemplateProvider: missing '${k}'`);
|
|
51
|
+
return v;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
staticReadBinary(p: string): Uint8Array {
|
|
55
|
+
// In-memory templates are stored as strings; expose their bytes. Real
|
|
56
|
+
// binary assets are exercised through NodeTemplateProvider on disk.
|
|
57
|
+
return new TextEncoder().encode(this.staticRead(p));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
textRead(p: string): string {
|
|
61
|
+
const k = `text/${p}`;
|
|
62
|
+
const v = this.files[k];
|
|
63
|
+
if (v === undefined) throw new Error(`MemoryTemplateProvider: missing '${k}'`);
|
|
64
|
+
return v;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Shell-style `${name}` substitution. Unknown names left as-is so
|
|
69
|
+
* template authors notice. Backslash escape is not supported. */
|
|
70
|
+
export function substituteVars(text: string, vars: Record<string, string>): string {
|
|
71
|
+
return text.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name: string) => {
|
|
72
|
+
return vars[name] ?? `\${${name}}`;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Test helpers for the engine.
|
|
2
|
+
//
|
|
3
|
+
// `runRulesAgainst`: unit helper — install `input` as the active
|
|
4
|
+
// managed-body object, invoke the body lambda, return the mutated object.
|
|
5
|
+
//
|
|
6
|
+
// `simulateInit`: produce an in-memory FS populated as if `block-tools
|
|
7
|
+
// structure init` had just run for the given BlockVars, then RE-DISCOVER
|
|
8
|
+
// the module set from that written FS (the same DISCOVERY the real `check`
|
|
9
|
+
// runs) rather than reusing init's module set. The returned `ctx` is the
|
|
10
|
+
// rediscovered one — running `engine.run(STRUCTURE, fs, {...ctx, dryRun:
|
|
11
|
+
// true})` against the same fs and asserting no changes is then a genuine
|
|
12
|
+
// init→check round-trip: a discovery / classification regression surfaces
|
|
13
|
+
// instead of being masked.
|
|
14
|
+
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import type { BlockVars, Module, RunContext, Structure } from "./api";
|
|
18
|
+
import type { JsonObject } from "./content-rules";
|
|
19
|
+
import { withManagedBody } from "./content-rules";
|
|
20
|
+
import { MemoryFileSystem } from "./fs/memory";
|
|
21
|
+
import { createRunContext, modulesForInit } from "./ctx";
|
|
22
|
+
import { NodeTemplateProvider, type TemplateProvider } from "./templates";
|
|
23
|
+
import { run as engineRun } from "./runner";
|
|
24
|
+
import { discoverRunContext } from "./discovery-fs";
|
|
25
|
+
import { STRUCTURE_VERSION } from "./version";
|
|
26
|
+
import { STRUCTURE } from "../structure-definition";
|
|
27
|
+
|
|
28
|
+
/** Layer-1 helper. */
|
|
29
|
+
export function runRulesAgainst(input: JsonObject, body: () => void): JsonObject {
|
|
30
|
+
return withManagedBody(input, body);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type SimulateInitInput = {
|
|
34
|
+
vars: BlockVars;
|
|
35
|
+
/** Override the discovered module set. Defaults to `defaultModulesFor`. */
|
|
36
|
+
modules?: Module[];
|
|
37
|
+
/** Override the structure under test (defaults to the canonical one). */
|
|
38
|
+
structure?: Structure;
|
|
39
|
+
/** Override the template provider (defaults to NodeTemplateProvider
|
|
40
|
+
* pointing at the bundled `templates/` directory). */
|
|
41
|
+
templates?: TemplateProvider;
|
|
42
|
+
/** SDK-internal flag. Default `false` (standalone block). */
|
|
43
|
+
isSdkInternal?: boolean;
|
|
44
|
+
/** Sync npm-latest lookup for the `onInitOrUpdate` catalog resolution.
|
|
45
|
+
* Absent → the SDK catalog keeps its seed versions (no bump), matching
|
|
46
|
+
* an offline scaffold; tests that assert init writes latest pass a mock. */
|
|
47
|
+
registryLookup?: (packageName: string) => string | undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type SimulatedInit = {
|
|
51
|
+
fs: MemoryFileSystem;
|
|
52
|
+
ctx: RunContext;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Module set produced by `init` for a given BlockVars. Delegates to the
|
|
56
|
+
* shared `modulesForInit` so the test helper and the real `init`
|
|
57
|
+
* constructor never drift. */
|
|
58
|
+
export function defaultModulesFor(vars: BlockVars): Module[] {
|
|
59
|
+
return modulesForInit(vars);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Default template provider — reads from the bundled templates tree
|
|
63
|
+
* next to this file. */
|
|
64
|
+
export function defaultTemplateProvider(): TemplateProvider {
|
|
65
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
66
|
+
// engine/testing.ts → ../templates
|
|
67
|
+
const root = path.resolve(here, "..", "templates");
|
|
68
|
+
return new NodeTemplateProvider(root);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function simulateInit(input: SimulateInitInput): SimulatedInit {
|
|
72
|
+
const fs = new MemoryFileSystem();
|
|
73
|
+
const isSdkInternal = input.isSdkInternal ?? false;
|
|
74
|
+
const modules = input.modules ?? defaultModulesFor(input.vars);
|
|
75
|
+
const initCtx = createRunContext({
|
|
76
|
+
blockVars: input.vars,
|
|
77
|
+
modules,
|
|
78
|
+
isSdkInternal,
|
|
79
|
+
version: STRUCTURE_VERSION,
|
|
80
|
+
});
|
|
81
|
+
const structure = input.structure ?? STRUCTURE;
|
|
82
|
+
const templates = input.templates ?? defaultTemplateProvider();
|
|
83
|
+
engineRun(structure, fs, initCtx, {
|
|
84
|
+
templates,
|
|
85
|
+
initMode: true,
|
|
86
|
+
registryLookup: input.registryLookup,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Re-discover from the written FS — the returned ctx is what `check`
|
|
90
|
+
// would build, not what `init` assembled.
|
|
91
|
+
const ctx = discoverRunContext({ fs, isSdkInternal });
|
|
92
|
+
return { fs, ctx };
|
|
93
|
+
}
|