@platforma-sdk/block-tools 2.14.3 → 2.14.5
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 +258 -254
- package/dist/cli.mjs.map +1 -1
- package/package.json +11 -11
- package/src/structure/__tests__/kind-bootstrap.test.ts +136 -0
- package/src/structure/engine/discovery-fs.ts +13 -0
- package/src/structure/rules/block-package-json.ts +5 -2
- package/src/structure/rules/kind.ts +13 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platforma-sdk/block-tools",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.5",
|
|
4
4
|
"description": "Utility to manipulate Platforma Blocks and Block Registry",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
"undici": "~7.16.0",
|
|
35
35
|
"yaml": "^2.8.0",
|
|
36
36
|
"zod": "~3.25.76",
|
|
37
|
-
"@milaboratories/pl-model-backend": "1.4.21",
|
|
38
|
-
"@milaboratories/pl-model-common": "1.48.0",
|
|
39
37
|
"@milaboratories/pl-http": "1.2.4",
|
|
40
|
-
"@milaboratories/
|
|
41
|
-
"@platforma-sdk/blocks-deps-updater": "2.2.0",
|
|
38
|
+
"@milaboratories/pl-model-common": "1.48.0",
|
|
42
39
|
"@milaboratories/pl-model-middle-layer": "1.32.0",
|
|
40
|
+
"@milaboratories/ts-helpers": "1.8.6",
|
|
43
41
|
"@platforma-sdk/package-builder-lib": "1.3.0",
|
|
44
|
-
"@milaboratories/resolve-helper": "1.1.3"
|
|
42
|
+
"@milaboratories/resolve-helper": "1.1.3",
|
|
43
|
+
"@platforma-sdk/blocks-deps-updater": "2.2.0",
|
|
44
|
+
"@milaboratories/pl-model-backend": "1.4.22"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
@@ -52,13 +52,13 @@
|
|
|
52
52
|
"oxfmt": "0.35.0",
|
|
53
53
|
"rollup-plugin-node-externals": "^8.0.0",
|
|
54
54
|
"semver": "^7.7.2",
|
|
55
|
-
"typescript": "
|
|
55
|
+
"typescript": "7.0.2",
|
|
56
56
|
"vite": "^8.0.6",
|
|
57
|
-
"vite-plugin-dts": "^
|
|
57
|
+
"vite-plugin-dts": "^5.0.3",
|
|
58
58
|
"vitest": "^4.1.3",
|
|
59
|
-
"@milaboratories/build-configs": "2.0.
|
|
60
|
-
"@milaboratories/ts-
|
|
61
|
-
"@milaboratories/ts-
|
|
59
|
+
"@milaboratories/build-configs": "2.0.1",
|
|
60
|
+
"@milaboratories/ts-configs": "1.4.0",
|
|
61
|
+
"@milaboratories/ts-builder": "1.7.2"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"build": "vite build",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// A block written before kinds existed carries no `kind/` package, so
|
|
2
|
+
// DISCOVERY finds no kind module. `refresh` must BOOTSTRAP the package rather
|
|
3
|
+
// than fail: DISCOVERY synthesises the module (discovery-fs.ts) and the kind
|
|
4
|
+
// rules then write it. Before this, the facade rule threw
|
|
5
|
+
// `declares no kind` before a single file was touched, and the only way
|
|
6
|
+
// forward was to hand-craft `kind/package.json` so discovery could see it.
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect } from "vitest";
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
import type { BlockVars } from "../engine/api";
|
|
11
|
+
import { simulateInit, defaultTemplateProvider } from "../engine/testing";
|
|
12
|
+
import { discoverRunContext } from "../engine/discovery-fs";
|
|
13
|
+
import { run as engineRun } from "../engine/runner";
|
|
14
|
+
import { STRUCTURE } from "../structure-definition";
|
|
15
|
+
import type { MemoryFileSystem } from "../engine/fs/memory";
|
|
16
|
+
|
|
17
|
+
const VARS: BlockVars = {
|
|
18
|
+
facadeName: "@platforma-open/test-org.demo",
|
|
19
|
+
baseName: "test-org.demo",
|
|
20
|
+
npmOrg: "@platforma-open",
|
|
21
|
+
orgScope: "test-org",
|
|
22
|
+
shortName: "demo",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** The packages `init` wires to the kind: the model depends on it, the facade
|
|
26
|
+
* carries it as a devDependency. */
|
|
27
|
+
const KIND_DEPENDENTS = ["model/package.json", "block/package.json"];
|
|
28
|
+
|
|
29
|
+
type DepSections = {
|
|
30
|
+
dependencies?: Record<string, string>;
|
|
31
|
+
devDependencies?: Record<string, string>;
|
|
32
|
+
peerDependencies?: Record<string, string>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const DEP_SECTIONS = ["dependencies", "devDependencies", "peerDependencies"] as const;
|
|
36
|
+
|
|
37
|
+
/** Every `<facade>.kind` reference the file declares, in any dep section. */
|
|
38
|
+
function kindDepsOf(fs: MemoryFileSystem, path: string): string[] {
|
|
39
|
+
const pkg = JSON.parse(fs.read(path)) as DepSections;
|
|
40
|
+
return DEP_SECTIONS.flatMap((s) => Object.keys(pkg[s] ?? {})).filter((d) => d.endsWith(".kind"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Drop every `<facade>.kind` reference from one package.json. */
|
|
44
|
+
function stripKindDeps(fs: MemoryFileSystem, path: string): void {
|
|
45
|
+
const pkg = JSON.parse(fs.read(path)) as DepSections;
|
|
46
|
+
for (const section of DEP_SECTIONS) {
|
|
47
|
+
const deps = pkg[section];
|
|
48
|
+
if (!deps) continue;
|
|
49
|
+
for (const name of Object.keys(deps)) if (name.endsWith(".kind")) delete deps[name];
|
|
50
|
+
}
|
|
51
|
+
fs.write(path, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A canonical block with every trace of its kind stripped back off — the shape
|
|
55
|
+
* a pre-kind block has on disk. The package, the workspace entry AND the
|
|
56
|
+
* dependencies its siblings declare on it all have to go: leaving the deps
|
|
57
|
+
* behind would make the wiring assertions below pass without a bootstrap. */
|
|
58
|
+
function preKindBlock(): MemoryFileSystem {
|
|
59
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
60
|
+
for (const p of Object.keys(fs.snapshot())) {
|
|
61
|
+
if (p === "kind" || p.startsWith("kind/")) fs.delete(p);
|
|
62
|
+
}
|
|
63
|
+
const ws = parseYaml(fs.read("pnpm-workspace.yaml")) as { packages: string[] };
|
|
64
|
+
ws.packages = ws.packages.filter((p) => p !== "kind");
|
|
65
|
+
fs.write("pnpm-workspace.yaml", `packages:\n${ws.packages.map((p) => ` - ${p}\n`).join("")}`);
|
|
66
|
+
for (const p of KIND_DEPENDENTS) stripKindDeps(fs, p);
|
|
67
|
+
return fs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function refresh(fs: MemoryFileSystem) {
|
|
71
|
+
const ctx = discoverRunContext({ fs, isSdkInternal: false });
|
|
72
|
+
return engineRun(STRUCTURE, fs, ctx, { templates: defaultTemplateProvider() });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("refresh bootstraps a missing kind package", () => {
|
|
76
|
+
test("DISCOVERY synthesises the kind module", () => {
|
|
77
|
+
const ctx = discoverRunContext({ fs: preKindBlock(), isSdkInternal: false });
|
|
78
|
+
const kind = ctx.modules.filter((m) => m.scope === "kind");
|
|
79
|
+
expect(kind).toHaveLength(1);
|
|
80
|
+
expect(kind[0]).toMatchObject({
|
|
81
|
+
name: "@platforma-open/test-org.demo.kind",
|
|
82
|
+
path: "kind",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("refresh writes the package, its configs and the params sentinel", () => {
|
|
87
|
+
const fs = preKindBlock();
|
|
88
|
+
expect(fs.exists("kind/package.json")).toBe(false);
|
|
89
|
+
|
|
90
|
+
refresh(fs);
|
|
91
|
+
|
|
92
|
+
expect(fs.exists("kind/package.json")).toBe(true);
|
|
93
|
+
expect(fs.exists("kind/tsconfig.json")).toBe(true);
|
|
94
|
+
expect(fs.exists("kind/.oxlintrc.json")).toBe(true);
|
|
95
|
+
expect(fs.exists("kind/.oxfmtrc.json")).toBe(true);
|
|
96
|
+
|
|
97
|
+
const pkg = JSON.parse(fs.read("kind/package.json")) as {
|
|
98
|
+
name: string;
|
|
99
|
+
dependencies: Record<string, string>;
|
|
100
|
+
};
|
|
101
|
+
expect(pkg.name).toBe("@platforma-open/test-org.demo.kind");
|
|
102
|
+
expect(pkg.dependencies).toHaveProperty("@platforma-sdk/block-kind");
|
|
103
|
+
|
|
104
|
+
// The entry point is a `scaffold`, so it lands on refresh too — a `seed`
|
|
105
|
+
// would have left the package without one.
|
|
106
|
+
expect(fs.read("kind/src/index.ts")).toContain("NEEDS_BLOCK_PARAMS");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("the bootstrapped kind joins the workspace and its dependents", () => {
|
|
110
|
+
const fs = preKindBlock();
|
|
111
|
+
|
|
112
|
+
// The fixture really is kind-free: nothing declares the dep yet, so the
|
|
113
|
+
// assertions after the refresh can only pass by way of the bootstrap.
|
|
114
|
+
for (const p of KIND_DEPENDENTS) expect(kindDepsOf(fs, p)).toEqual([]);
|
|
115
|
+
|
|
116
|
+
refresh(fs);
|
|
117
|
+
|
|
118
|
+
const ws = parseYaml(fs.read("pnpm-workspace.yaml")) as { packages: string[] };
|
|
119
|
+
expect(ws.packages).toContain("kind");
|
|
120
|
+
|
|
121
|
+
for (const p of KIND_DEPENDENTS) {
|
|
122
|
+
expect(kindDepsOf(fs, p)).toEqual(["@platforma-open/test-org.demo.kind"]);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a second refresh is a fixpoint — the sentinel is never rewritten", () => {
|
|
127
|
+
const fs = preKindBlock();
|
|
128
|
+
refresh(fs);
|
|
129
|
+
fs.write("kind/src/index.ts", "// the author's own contract\n");
|
|
130
|
+
|
|
131
|
+
const changes = refresh(fs).changes;
|
|
132
|
+
|
|
133
|
+
expect(changes).toHaveLength(0);
|
|
134
|
+
expect(fs.read("kind/src/index.ts")).toBe("// the author's own contract\n");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -197,6 +197,19 @@ export function discoverRunContext(input: DiscoverInput): RunContext {
|
|
|
197
197
|
const facadeName = blockMod.name.replace(/\.block$/, "");
|
|
198
198
|
const blockVars = parseBlockVars(facadeName);
|
|
199
199
|
|
|
200
|
+
// A kind is a mandatory block component, but a block written before kinds
|
|
201
|
+
// existed has no `kind/` package on disk, so DISCOVERY finds no kind module
|
|
202
|
+
// and every kind rule fans out over nothing. Synthesise the module the same
|
|
203
|
+
// way `init` does (`modulesForInit`) so a plain `structure refresh`
|
|
204
|
+
// BOOTSTRAPS the package: the managed `package.json` and the fixed configs
|
|
205
|
+
// are written from their generators, `src/index.ts` lands as a scaffold, and
|
|
206
|
+
// `kind` joins `packages:` because `ensureWorkspaceModulePaths` reads
|
|
207
|
+
// `ctx.modules`. Without this a pre-kind block could only be migrated by
|
|
208
|
+
// hand-crafting the package first — `refresh` threw before touching a file.
|
|
209
|
+
if (!modules.some((m) => m.scope === "kind")) {
|
|
210
|
+
modules.push({ scope: "kind", name: `${facadeName}.kind`, path: "kind" });
|
|
211
|
+
}
|
|
212
|
+
|
|
200
213
|
return createRunContext({
|
|
201
214
|
isSdkInternal,
|
|
202
215
|
updateDepsOnly: input.updateDepsOnly ?? false,
|
|
@@ -200,8 +200,11 @@ export function blockPackageJsonRules(ctx: RunContext): void {
|
|
|
200
200
|
const v = ctx.blockVars;
|
|
201
201
|
|
|
202
202
|
// The kind is a MANDATORY block component: every block must declare exactly
|
|
203
|
-
// one sibling `kind/` package.
|
|
204
|
-
//
|
|
203
|
+
// one sibling `kind/` package. DISCOVERY synthesises the module when the
|
|
204
|
+
// package is absent, so `refresh` bootstraps it instead of failing here and
|
|
205
|
+
// the zero branch is an invariant guard rather than the migration signal it
|
|
206
|
+
// once was. Two or more kinds stay a hard error — nothing can pick between
|
|
207
|
+
// them.
|
|
205
208
|
const kindModules = findModules(ctx, "kind");
|
|
206
209
|
if (kindModules.length !== 1) {
|
|
207
210
|
throw new Error(
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Kind-scope rules. Static config files + managed package.json; the
|
|
2
|
-
// `src/index.ts`
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// `src/index.ts` scaffold is dropped once, on whichever run first finds it
|
|
3
|
+
// missing, and never touched again — the block author owns the BlockParams
|
|
4
|
+
// contract. What lands is a deliberate non-building sentinel: its BlockParams
|
|
5
|
+
// is undefined, so a scaffolded-but-unmigrated block fails to typecheck until
|
|
6
|
+
// the params contract is chosen on purpose.
|
|
6
7
|
|
|
7
|
-
import { scope, fixed, managed,
|
|
8
|
+
import { scope, fixed, managed, scaffold, file, generate } from "../engine/api";
|
|
8
9
|
import { kindPackageJsonInitial, kindPackageJsonRules } from "./kind-package-json";
|
|
9
10
|
|
|
10
11
|
export function kindRules(): void {
|
|
@@ -16,7 +17,13 @@ export function kindRules(): void {
|
|
|
16
17
|
// Minimal-but-real kind: identity imported from the package's own
|
|
17
18
|
// package.json, plus a non-building BlockParams sentinel the block author
|
|
18
19
|
// must resolve (via the migration recipe or by hand) before the block builds.
|
|
19
|
-
|
|
20
|
+
//
|
|
21
|
+
// `scaffold`, not `seed`: create-if-missing in EVERY mode, so `refresh`
|
|
22
|
+
// bootstrapping a kind for a pre-kind block deposits the sentinel too. A
|
|
23
|
+
// seed is init-only, which would have left such a block with a kind package
|
|
24
|
+
// that has no entry point. Once the file exists the engine never touches it
|
|
25
|
+
// — the params contract belongs to the block author.
|
|
26
|
+
scaffold("src/index.ts", file("kind/src/index.ts"));
|
|
20
27
|
|
|
21
28
|
managed(
|
|
22
29
|
"package.json",
|