@platforma-sdk/block-tools 2.14.4 → 2.14.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platforma-sdk/block-tools",
3
- "version": "2.14.4",
3
+ "version": "2.14.6",
4
4
  "description": "Utility to manipulate Platforma Blocks and Block Registry",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
@@ -35,13 +35,13 @@
35
35
  "yaml": "^2.8.0",
36
36
  "zod": "~3.25.76",
37
37
  "@milaboratories/pl-http": "1.2.4",
38
- "@milaboratories/pl-model-common": "1.48.0",
38
+ "@milaboratories/pl-model-backend": "1.4.23",
39
39
  "@milaboratories/pl-model-middle-layer": "1.32.0",
40
+ "@milaboratories/pl-model-common": "1.48.0",
40
41
  "@milaboratories/resolve-helper": "1.1.3",
41
- "@milaboratories/ts-helpers": "1.8.6",
42
42
  "@platforma-sdk/blocks-deps-updater": "2.2.0",
43
- "@milaboratories/pl-model-backend": "1.4.22",
44
- "@platforma-sdk/package-builder-lib": "1.3.0"
43
+ "@platforma-sdk/package-builder-lib": "1.3.0",
44
+ "@milaboratories/ts-helpers": "1.8.6"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@rollup/plugin-node-resolve": "^16.0.1",
@@ -57,8 +57,8 @@
57
57
  "vite-plugin-dts": "^5.0.3",
58
58
  "vitest": "^4.1.3",
59
59
  "@milaboratories/ts-builder": "1.7.2",
60
- "@milaboratories/ts-configs": "1.4.0",
61
- "@milaboratories/build-configs": "2.0.1"
60
+ "@milaboratories/build-configs": "2.0.1",
61
+ "@milaboratories/ts-configs": "1.4.0"
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. Fail loudly otherwise — a kind-less block is
204
- // invalid, and this is what surfaces the not-yet-migrated blocks.
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` seed is dropped by `init` and never touched again (the block
3
- // author owns the BlockParams contract). The seed is a deliberate non-building
4
- // sentinel — its BlockParams is undefined, so a scaffolded-but-unmigrated block
5
- // fails to typecheck until the params contract is chosen on purpose.
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, seed, file, generate } from "../engine/api";
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
- seed("src/index.ts", file("kind/src/index.ts"));
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",