@platforma-sdk/block-tools 2.10.15 → 2.10.17
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 +11 -11
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +581 -583
- package/dist/cli.mjs.map +1 -1
- package/dist/structure/engine/builders.d.ts +9 -9
- package/dist/v2/registry/schema_public.d.ts +3600 -3600
- package/package.json +6 -6
- package/src/structure/__tests__/tsconfig-idempotency.test.ts +94 -0
- package/src/structure/engine/__tests__/content-rules/when-inner.test.ts +30 -0
- package/src/structure/engine/builders.ts +38 -23
- package/src/structure/rules/model.ts +11 -35
- package/src/structure/rules/ui-package-json.ts +7 -0
- package/src/structure/rules/ui.ts +10 -22
- package/src/structure/templates/static/model/tsconfig.node.json +7 -0
- package/src/structure/templates/static/ui/tsconfig.node.json +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platforma-sdk/block-tools",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.17",
|
|
4
4
|
"description": "Utility to manipulate Platforma Blocks and Block Registry",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
"undici": "~7.16.0",
|
|
34
34
|
"yaml": "^2.8.0",
|
|
35
35
|
"zod": "~3.25.76",
|
|
36
|
-
"@milaboratories/pl-http": "1.2.4",
|
|
37
36
|
"@milaboratories/pl-model-common": "1.46.1",
|
|
38
37
|
"@milaboratories/pl-model-backend": "1.4.7",
|
|
39
|
-
"@milaboratories/pl-model-middle-layer": "1.30.5",
|
|
40
|
-
"@milaboratories/ts-helpers": "1.8.3",
|
|
41
38
|
"@milaboratories/resolve-helper": "1.1.3",
|
|
39
|
+
"@milaboratories/pl-model-middle-layer": "1.30.6",
|
|
40
|
+
"@milaboratories/ts-helpers": "1.8.3",
|
|
41
|
+
"@milaboratories/pl-http": "1.2.4",
|
|
42
42
|
"@milaboratories/ts-helpers-oclif": "1.1.42",
|
|
43
43
|
"@platforma-sdk/blocks-deps-updater": "2.2.0"
|
|
44
44
|
},
|
|
@@ -56,10 +56,10 @@
|
|
|
56
56
|
"vite": "^8.0.6",
|
|
57
57
|
"vite-plugin-dts": "^4.5.3",
|
|
58
58
|
"vitest": "^4.1.3",
|
|
59
|
+
"@milaboratories/build-configs": "2.0.0",
|
|
59
60
|
"@milaboratories/oclif-index": "1.1.1",
|
|
60
61
|
"@milaboratories/ts-configs": "1.2.3",
|
|
61
|
-
"@milaboratories/
|
|
62
|
-
"@milaboratories/ts-builder": "1.5.0"
|
|
62
|
+
"@milaboratories/ts-builder": "1.5.1"
|
|
63
63
|
},
|
|
64
64
|
"oclif": {
|
|
65
65
|
"bin": "block-tools",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Regression: a test-bearing model/ui scope migrating from a LEGACY tsconfig
|
|
2
|
+
// must converge in a single pass.
|
|
3
|
+
//
|
|
4
|
+
// History: the tsconfig used to be a `managed` file whose body did
|
|
5
|
+
// `removeField("compilerOptions")`, conditionally re-added
|
|
6
|
+
// `compilerOptions.types: ["node"]`, then `ensureField("include", …)`. On a
|
|
7
|
+
// legacy tsconfig with no `include`, pass 1 appended `include` AFTER the
|
|
8
|
+
// re-added `compilerOptions` → `{extends, compilerOptions, include}`, while the
|
|
9
|
+
// body's fixpoint was `{extends, include, compilerOptions}`. The two differed
|
|
10
|
+
// only in key order, so the engine's post-run recheck saw a phantom diff and
|
|
11
|
+
// `refresh` aborted with a non-idempotency RecheckError on EVERY test-bearing
|
|
12
|
+
// block migrating off a legacy (vite-era) tsconfig.
|
|
13
|
+
//
|
|
14
|
+
// Now the tsconfig is a `fixed` file with two static end states selected by a
|
|
15
|
+
// `when`/else on co-located-test presence (`tsconfig.node.json` vs
|
|
16
|
+
// `tsconfig.json`). There is no imperative body and no insertion-order side
|
|
17
|
+
// effect, so single-pass idempotency holds by construction. This test guards
|
|
18
|
+
// that property.
|
|
19
|
+
|
|
20
|
+
import { describe, test, expect } from "vitest";
|
|
21
|
+
import { simulateInit, defaultTemplateProvider } from "../engine/testing";
|
|
22
|
+
import { run as engineRun } from "../engine/runner";
|
|
23
|
+
import { discoverRunContext } from "../engine/discovery-fs";
|
|
24
|
+
import { STRUCTURE } from "../structure-definition";
|
|
25
|
+
import { SDK_CATALOG_PACKAGES } from "../rules/root-pnpm-workspace";
|
|
26
|
+
import type { BlockVars } from "../engine/api";
|
|
27
|
+
|
|
28
|
+
const VARS: BlockVars = {
|
|
29
|
+
facadeName: "@platforma-open/test-org.demo",
|
|
30
|
+
baseName: "test-org.demo",
|
|
31
|
+
npmOrg: "@platforma-open",
|
|
32
|
+
orgScope: "test-org",
|
|
33
|
+
shortName: "demo",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const mockLookup = (name: string): string | undefined =>
|
|
37
|
+
SDK_CATALOG_PACKAGES.includes(name) ? "9.9.9" : undefined;
|
|
38
|
+
|
|
39
|
+
// `refresh` runs the full engine INCLUDING the post-run recheck (via
|
|
40
|
+
// `rediscover`); a non-idempotent rule set makes this throw.
|
|
41
|
+
function refresh(fs: ReturnType<typeof simulateInit>["fs"]) {
|
|
42
|
+
const ctx = discoverRunContext({ fs, isSdkInternal: false });
|
|
43
|
+
engineRun(STRUCTURE, fs, ctx, {
|
|
44
|
+
templates: defaultTemplateProvider(),
|
|
45
|
+
rediscover: () => discoverRunContext({ fs, isSdkInternal: false, dryRun: true }),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const CANONICAL_ORDER = ["extends", "compilerOptions", "include"];
|
|
50
|
+
|
|
51
|
+
describe("tsconfig first-pass idempotency on legacy (no-include) tsconfig", () => {
|
|
52
|
+
test("model + ui co-located tests, legacy tsconfig without `include` → refresh converges in one pass", () => {
|
|
53
|
+
const { fs } = simulateInit({ vars: VARS, registryLookup: mockLookup });
|
|
54
|
+
|
|
55
|
+
// Co-located unit tests in both scopes -> the conditional node-types re-add fires.
|
|
56
|
+
fs.write("model/src/m.test.ts", `import { test } from "vitest";\ntest("x", () => {});\n`);
|
|
57
|
+
fs.write("ui/src/u.test.ts", `import { test } from "vitest";\ntest("x", () => {});\n`);
|
|
58
|
+
|
|
59
|
+
// Legacy tsconfigs as a vite-era block carried them: a `compilerOptions`
|
|
60
|
+
// block and NO top-level `include`. This is what made pass 1 append
|
|
61
|
+
// `include` after the re-added `compilerOptions`.
|
|
62
|
+
fs.write(
|
|
63
|
+
"model/tsconfig.json",
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
extends: "@milaboratories/ts-configs/block/model",
|
|
66
|
+
compilerOptions: { strict: true },
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
fs.write(
|
|
70
|
+
"ui/tsconfig.json",
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
extends: "@milaboratories/ts-configs/block/ui",
|
|
73
|
+
compilerOptions: { strict: true },
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Without the fix this throws RecheckError (rule set not idempotent).
|
|
78
|
+
expect(() => refresh(fs)).not.toThrow();
|
|
79
|
+
|
|
80
|
+
for (const scope of ["model", "ui"]) {
|
|
81
|
+
const ts = JSON.parse(fs.read(`${scope}/tsconfig.json`)) as {
|
|
82
|
+
extends: string;
|
|
83
|
+
include: string[];
|
|
84
|
+
compilerOptions: { types: string[] };
|
|
85
|
+
};
|
|
86
|
+
// Canonical, deterministic key order.
|
|
87
|
+
expect(Object.keys(ts)).toEqual(CANONICAL_ORDER);
|
|
88
|
+
expect(ts.extends).toBe(`@milaboratories/ts-configs/block/${scope}`);
|
|
89
|
+
expect(ts.include).toEqual(["src/**/*"]);
|
|
90
|
+
// node types wired (co-located test present); legacy `strict` pruned.
|
|
91
|
+
expect(ts.compilerOptions).toEqual({ types: ["node"] });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -222,6 +222,36 @@ describe("when() — inner-mode dispatch inside managed bodies", () => {
|
|
|
222
222
|
).toThrow(/outside defineStructure.*outside any managed/);
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
+
test("else branch: predicate=true runs body, skips else", () => {
|
|
226
|
+
const out = withManagedBody(
|
|
227
|
+
{} as JsonObject,
|
|
228
|
+
() => {
|
|
229
|
+
when(
|
|
230
|
+
({ pathExists }) => pathExists("tests/"),
|
|
231
|
+
() => ensureField("branch", "if"),
|
|
232
|
+
() => ensureField("branch", "else"),
|
|
233
|
+
);
|
|
234
|
+
},
|
|
235
|
+
{ triggerContext: makeTctx({ paths: new Set(["tests/"]) }) },
|
|
236
|
+
);
|
|
237
|
+
expect(out.branch).toBe("if");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("else branch: predicate=false runs else, skips body", () => {
|
|
241
|
+
const out = withManagedBody(
|
|
242
|
+
{} as JsonObject,
|
|
243
|
+
() => {
|
|
244
|
+
when(
|
|
245
|
+
({ pathExists }) => pathExists("tests/"),
|
|
246
|
+
() => ensureField("branch", "if"),
|
|
247
|
+
() => ensureField("branch", "else"),
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
{ triggerContext: makeTctx({ paths: new Set() }) },
|
|
251
|
+
);
|
|
252
|
+
expect(out.branch).toBe("else");
|
|
253
|
+
});
|
|
254
|
+
|
|
225
255
|
test("idempotent — double-run yields the same parsed state", () => {
|
|
226
256
|
const body = () => {
|
|
227
257
|
ensureField("type", "module");
|
|
@@ -193,34 +193,49 @@ export function registerInnerWhenHandler(h: InnerWhenHandler): void {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
* Conditionally run `body
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* synchronously.
|
|
196
|
+
* Conditionally run `body`, with an optional `elseBody`. One `when` symbol for
|
|
197
|
+
* both layers — `dispatchWhen` picks the timing:
|
|
198
|
+
* - inside `defineStructure(...)` → register a `WhenFrame`; its trigger fires
|
|
199
|
+
* later, per module, at runner time.
|
|
200
|
+
* - inside a `managed(...)` body → evaluate the trigger against the live block
|
|
201
|
+
* now and run/skip synchronously.
|
|
202
202
|
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
203
|
+
* `else` is just the negated trigger dispatched the same way, so exactly one
|
|
204
|
+
* branch applies (in either layer).
|
|
205
205
|
*/
|
|
206
|
-
export function when(trigger: TriggerFn, body: () => void): void {
|
|
206
|
+
export function when(trigger: TriggerFn, body: () => void, elseBody?: () => void): void {
|
|
207
|
+
dispatchWhen(trigger, body);
|
|
208
|
+
if (elseBody) dispatchWhen((tctx) => !trigger(tctx), elseBody);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Single-branch dispatch shared by both `when` branches. In a managed body the
|
|
212
|
+
* handler runs `body` iff its trigger holds and returns whether a managed-body
|
|
213
|
+
* context existed; no context — and not tree-build either — means `when()` was
|
|
214
|
+
* called outside both layers, a misuse. */
|
|
215
|
+
function dispatchWhen(trigger: TriggerFn, body: () => void): void {
|
|
207
216
|
if (activeTree) {
|
|
208
|
-
|
|
209
|
-
const frame: WhenFrame = { kind: "when", trigger, children: [] };
|
|
210
|
-
t.stack[t.stack.length - 1]!.push(frame);
|
|
211
|
-
t.stack.push(frame.children);
|
|
212
|
-
try {
|
|
213
|
-
body();
|
|
214
|
-
} finally {
|
|
215
|
-
t.stack.pop();
|
|
216
|
-
}
|
|
217
|
+
pushWhenFrame(trigger, body);
|
|
217
218
|
return;
|
|
218
219
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
"
|
|
223
|
-
|
|
220
|
+
const handled = innerWhenHandler?.(trigger, body) ?? false;
|
|
221
|
+
if (!handled) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
"when() called outside defineStructure() and outside any managed(...) body. " +
|
|
224
|
+
"Place this call inside one of the two.",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function pushWhenFrame(trigger: TriggerFn, body: () => void): void {
|
|
230
|
+
const t = activeTree!;
|
|
231
|
+
const frame: WhenFrame = { kind: "when", trigger, children: [] };
|
|
232
|
+
t.stack[t.stack.length - 1]!.push(frame);
|
|
233
|
+
t.stack.push(frame.children);
|
|
234
|
+
try {
|
|
235
|
+
body();
|
|
236
|
+
} finally {
|
|
237
|
+
t.stack.pop();
|
|
238
|
+
}
|
|
224
239
|
}
|
|
225
240
|
|
|
226
241
|
/** Trigger factory: true when at least one file in the leaf's module
|
|
@@ -2,47 +2,23 @@
|
|
|
2
2
|
// `src/index.ts` seed is dropped by `init` and never touched again
|
|
3
3
|
// (block author owns it).
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
scope,
|
|
7
|
-
fixed,
|
|
8
|
-
managed,
|
|
9
|
-
seed,
|
|
10
|
-
file,
|
|
11
|
-
generate,
|
|
12
|
-
when,
|
|
13
|
-
whenFilesExist,
|
|
14
|
-
ensureField,
|
|
15
|
-
removeField,
|
|
16
|
-
pruneKeysMatching,
|
|
17
|
-
} from "../engine/api";
|
|
5
|
+
import { scope, fixed, managed, seed, file, generate, when, whenFilesExist } from "../engine/api";
|
|
18
6
|
import { getActiveRunContext } from "../engine/builders";
|
|
19
7
|
import { modelPackageJsonInitial, modelPackageJsonRules } from "./model-package-json";
|
|
20
8
|
import { COLOCATED_TEST_GLOB } from "./shared/colocated-tests";
|
|
21
9
|
|
|
22
10
|
export function modelRules(): void {
|
|
23
11
|
scope("model", () => {
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// `
|
|
27
|
-
//
|
|
28
|
-
// `
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
ensureField("extends", "@milaboratories/ts-configs/block/model");
|
|
35
|
-
// Canonical default carries no compilerOptions; drop any legacy block's
|
|
36
|
-
// before (conditionally) re-adding only `types`.
|
|
37
|
-
removeField("compilerOptions");
|
|
38
|
-
when(whenFilesExist(COLOCATED_TEST_GLOB), () => {
|
|
39
|
-
ensureField("compilerOptions.types", ["node"]);
|
|
40
|
-
});
|
|
41
|
-
ensureField("include", ["src/**/*"]);
|
|
42
|
-
// Strip anything else a legacy tsconfig carried (files, references, a
|
|
43
|
-
// different extends layout) — keep only the canonical keys.
|
|
44
|
-
pruneKeysMatching((k) => k !== "extends" && k !== "include" && k !== "compilerOptions");
|
|
45
|
-
});
|
|
12
|
+
// Two static end states by co-located-test presence: a test-bearing model
|
|
13
|
+
// gets node ambient types (`.node.json`) so the tests type-check —
|
|
14
|
+
// `@types/node` is wired alongside by the package.json rule; a test-less
|
|
15
|
+
// model stays bare. `fixed` (engine-owned, whole-file overwrite) not
|
|
16
|
+
// `managed`, so refresh is idempotent by construction — no key-order drift.
|
|
17
|
+
when(
|
|
18
|
+
whenFilesExist(COLOCATED_TEST_GLOB),
|
|
19
|
+
() => fixed("tsconfig.json", file("model/tsconfig.node.json")),
|
|
20
|
+
() => fixed("tsconfig.json", file("model/tsconfig.json")),
|
|
21
|
+
);
|
|
46
22
|
fixed(".oxlintrc.json", file("model/.oxlintrc.json"));
|
|
47
23
|
fixed(".oxfmtrc.json", file("model/.oxfmtrc.json"));
|
|
48
24
|
|
|
@@ -80,6 +80,13 @@ export function uiPackageJsonRules(): void {
|
|
|
80
80
|
vitest: "catalog:",
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
// `vue-tsc` is owned by `ts-builder` (which runs it internally for
|
|
84
|
+
// `--target block-ui`), so the ui must NOT declare its own. A direct
|
|
85
|
+
// `vue-tsc` dep is the vestigial vite/vue-tsc-era artefact: it resolves
|
|
86
|
+
// independently of the version ts-builder pins, which is how a block ended
|
|
87
|
+
// up type-checking under a different vue-tsc than the toolchain intends.
|
|
88
|
+
removeDep("vue-tsc");
|
|
89
|
+
|
|
83
90
|
// The ui builds with `--target block-ui` (types: []), so by default it
|
|
84
91
|
// carries no browser/vitest/node ambient types — only the `typescript`
|
|
85
92
|
// peer for IDE type resolution. Drop any stray `@types/node` a legacy
|
|
@@ -13,9 +13,6 @@ import {
|
|
|
13
13
|
blockVars,
|
|
14
14
|
when,
|
|
15
15
|
whenFilesExist,
|
|
16
|
-
ensureField,
|
|
17
|
-
removeField,
|
|
18
|
-
pruneKeysMatching,
|
|
19
16
|
} from "../engine/api";
|
|
20
17
|
import { getActiveRunContext } from "../engine/builders";
|
|
21
18
|
import { uiPackageJsonInitial, uiPackageJsonRules } from "./ui-package-json";
|
|
@@ -23,25 +20,16 @@ import { COLOCATED_TEST_GLOB } from "./shared/colocated-tests";
|
|
|
23
20
|
|
|
24
21
|
export function uiRules(): void {
|
|
25
22
|
scope("ui", () => {
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
managed("tsconfig.json", file("ui/tsconfig.json"), () => {
|
|
37
|
-
ensureField("extends", "@milaboratories/ts-configs/block/ui");
|
|
38
|
-
removeField("compilerOptions");
|
|
39
|
-
when(whenFilesExist(COLOCATED_TEST_GLOB), () => {
|
|
40
|
-
ensureField("compilerOptions.types", ["node"]);
|
|
41
|
-
});
|
|
42
|
-
ensureField("include", ["src/**/*"]);
|
|
43
|
-
pruneKeysMatching((k) => k !== "extends" && k !== "include" && k !== "compilerOptions");
|
|
44
|
-
});
|
|
23
|
+
// Two static end states by co-located-test presence: a test-bearing ui gets
|
|
24
|
+
// node ambient types (`.node.json`) so tests' `node:*` imports type-check
|
|
25
|
+
// under vue-tsc — `@types/node` is wired alongside by the package.json rule;
|
|
26
|
+
// a test-less ui stays bare. `fixed` (engine-owned, whole-file overwrite)
|
|
27
|
+
// not `managed`, so refresh is idempotent by construction — no key-order drift.
|
|
28
|
+
when(
|
|
29
|
+
whenFilesExist(COLOCATED_TEST_GLOB),
|
|
30
|
+
() => fixed("tsconfig.json", file("ui/tsconfig.node.json")),
|
|
31
|
+
() => fixed("tsconfig.json", file("ui/tsconfig.json")),
|
|
32
|
+
);
|
|
45
33
|
fixed(".oxlintrc.json", file("ui/.oxlintrc.json"));
|
|
46
34
|
// Block-local oxfmt config — the ui is oxfmt-checked (`ts-builder check
|
|
47
35
|
// --target block-ui`).
|