@platforma-sdk/block-tools 2.10.14 → 2.10.16

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.
Files changed (169) hide show
  1. package/dist/cli.js +23 -2
  2. package/dist/cli.js.map +1 -1
  3. package/dist/cli.mjs +1944 -100
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/cmd/index.d.ts +10 -4
  6. package/dist/cmd/structure/check.d.ts +10 -0
  7. package/dist/cmd/structure/init.d.ts +18 -0
  8. package/dist/cmd/structure/refresh.d.ts +11 -0
  9. package/dist/cmd/update-deps.d.ts +7 -0
  10. package/dist/structure/cli/run-structure.d.ts +19 -0
  11. package/dist/structure/engine/api.d.ts +55 -0
  12. package/dist/structure/engine/builders.d.ts +95 -0
  13. package/dist/structure/engine/content-rules.d.ts +148 -0
  14. package/dist/structure/engine/ctx.d.ts +34 -0
  15. package/dist/structure/engine/discovery-fs.d.ts +22 -0
  16. package/dist/structure/engine/discovery.d.ts +39 -0
  17. package/dist/structure/engine/flatten.d.ts +3 -0
  18. package/dist/structure/engine/fs/api.d.ts +27 -0
  19. package/dist/structure/engine/fs/memory.d.ts +16 -0
  20. package/dist/structure/engine/fs/node.d.ts +14 -0
  21. package/dist/structure/engine/glob.d.ts +4 -0
  22. package/dist/structure/engine/ir.d.ts +124 -0
  23. package/dist/structure/engine/parsers/json.d.ts +37 -0
  24. package/dist/structure/engine/parsers/lines.d.ts +9 -0
  25. package/dist/structure/engine/parsers/yaml.d.ts +18 -0
  26. package/dist/structure/engine/registry-client.d.ts +34 -0
  27. package/dist/structure/engine/runner.d.ts +45 -0
  28. package/dist/structure/engine/templates.d.ts +30 -0
  29. package/dist/structure/engine/testing.d.ts +34 -0
  30. package/dist/structure/engine/version.d.ts +18 -0
  31. package/dist/structure/init-block-constructor.d.ts +32 -0
  32. package/dist/structure/rules/block-package-json.d.ts +9 -0
  33. package/dist/structure/rules/block.d.ts +1 -0
  34. package/dist/structure/rules/migrations.d.ts +2 -0
  35. package/dist/structure/rules/model-package-json.d.ts +3 -0
  36. package/dist/structure/rules/model.d.ts +1 -0
  37. package/dist/structure/rules/root-catalog-bump.d.ts +1 -0
  38. package/dist/structure/rules/root-gitignore.d.ts +2 -0
  39. package/dist/structure/rules/root-package-json.d.ts +3 -0
  40. package/dist/structure/rules/root-pnpm-workspace.d.ts +15 -0
  41. package/dist/structure/rules/root.d.ts +1 -0
  42. package/dist/structure/rules/shared/colocated-tests.d.ts +2 -0
  43. package/dist/structure/rules/shared/key-order.d.ts +1 -0
  44. package/dist/structure/rules/software-package-json.d.ts +3 -0
  45. package/dist/structure/rules/software.d.ts +1 -0
  46. package/dist/structure/rules/test-package-json.d.ts +3 -0
  47. package/dist/structure/rules/test.d.ts +1 -0
  48. package/dist/structure/rules/ui-package-json.d.ts +3 -0
  49. package/dist/structure/rules/ui.d.ts +1 -0
  50. package/dist/structure/rules/workflow-package-json.d.ts +3 -0
  51. package/dist/structure/rules/workflow.d.ts +1 -0
  52. package/dist/structure/structure-definition.d.ts +1 -0
  53. package/package.json +9 -7
  54. package/src/cmd/index.ts +10 -4
  55. package/src/cmd/structure/check.ts +51 -0
  56. package/src/cmd/structure/init.ts +65 -0
  57. package/src/cmd/structure/refresh.ts +44 -0
  58. package/src/cmd/update-deps.ts +15 -3
  59. package/src/structure/__tests__/colocated-test-wiring.test.ts +107 -0
  60. package/src/structure/__tests__/floor-version.test.ts +52 -0
  61. package/src/structure/__tests__/init-catalog-resolution.test.ts +82 -0
  62. package/src/structure/__tests__/init-check-parity.test.ts +59 -0
  63. package/src/structure/__tests__/model-package-json.snapshot.test.ts +60 -0
  64. package/src/structure/__tests__/oxfmt-clean-init.test.ts +64 -0
  65. package/src/structure/__tests__/root-catalog-bump.test.ts +110 -0
  66. package/src/structure/__tests__/seed-disjointness.test.ts +79 -0
  67. package/src/structure/cli/run-structure.ts +89 -0
  68. package/src/structure/engine/__tests__/content-rules/active-state-guards.test.ts +61 -0
  69. package/src/structure/engine/__tests__/content-rules/catalog-pin.test.ts +46 -0
  70. package/src/structure/engine/__tests__/content-rules/enforce-alphabetical-order.test.ts +55 -0
  71. package/src/structure/engine/__tests__/content-rules/enforce-field-order.test.ts +73 -0
  72. package/src/structure/engine/__tests__/content-rules/ensure-catalog-absent.test.ts +29 -0
  73. package/src/structure/engine/__tests__/content-rules/ensure-catalog-latest.test.ts +70 -0
  74. package/src/structure/engine/__tests__/content-rules/ensure-dep.test.ts +63 -0
  75. package/src/structure/engine/__tests__/content-rules/ensure-deps-bulk.test.ts +46 -0
  76. package/src/structure/engine/__tests__/content-rules/ensure-field-entries.test.ts +35 -0
  77. package/src/structure/engine/__tests__/content-rules/ensure-field.test.ts +56 -0
  78. package/src/structure/engine/__tests__/content-rules/ensure-script.test.ts +39 -0
  79. package/src/structure/engine/__tests__/content-rules/ensure-workspace-scope-deps.test.ts +92 -0
  80. package/src/structure/engine/__tests__/content-rules/gitignore.test.ts +50 -0
  81. package/src/structure/engine/__tests__/content-rules/pin-catalog-to.test.ts +64 -0
  82. package/src/structure/engine/__tests__/content-rules/prune-keys.test.ts +59 -0
  83. package/src/structure/engine/__tests__/content-rules/registry-client.test.ts +116 -0
  84. package/src/structure/engine/__tests__/content-rules/remove-field.test.ts +49 -0
  85. package/src/structure/engine/__tests__/content-rules/require-field.test.ts +40 -0
  86. package/src/structure/engine/__tests__/content-rules/transform-at.test.ts +33 -0
  87. package/src/structure/engine/__tests__/content-rules/when-inner.test.ts +244 -0
  88. package/src/structure/engine/__tests__/content-rules/workspace-module-paths.test.ts +53 -0
  89. package/src/structure/engine/__tests__/discovery.test.ts +160 -0
  90. package/src/structure/engine/__tests__/fs-memory.test.ts +59 -0
  91. package/src/structure/engine/__tests__/glob.test.ts +43 -0
  92. package/src/structure/engine/__tests__/parsers-json.test.ts +189 -0
  93. package/src/structure/engine/__tests__/parsers-lines.test.ts +44 -0
  94. package/src/structure/engine/__tests__/parsers-yaml.test.ts +90 -0
  95. package/src/structure/engine/__tests__/recheck.test.ts +99 -0
  96. package/src/structure/engine/__tests__/registry-lookup.test.ts +49 -0
  97. package/src/structure/engine/__tests__/runner-when-inside-managed.test.ts +136 -0
  98. package/src/structure/engine/__tests__/runner.test.ts +136 -0
  99. package/src/structure/engine/__tests__/synthetic.test.ts +153 -0
  100. package/src/structure/engine/__tests__/version.test.ts +44 -0
  101. package/src/structure/engine/__tests__/when-files-exist.test.ts +89 -0
  102. package/src/structure/engine/api.ts +131 -0
  103. package/src/structure/engine/builders.ts +341 -0
  104. package/src/structure/engine/content-rules.ts +616 -0
  105. package/src/structure/engine/ctx.ts +94 -0
  106. package/src/structure/engine/discovery-fs.ts +209 -0
  107. package/src/structure/engine/discovery.ts +168 -0
  108. package/src/structure/engine/flatten.ts +117 -0
  109. package/src/structure/engine/fs/api.ts +40 -0
  110. package/src/structure/engine/fs/memory.ts +132 -0
  111. package/src/structure/engine/fs/node.ts +89 -0
  112. package/src/structure/engine/glob.ts +42 -0
  113. package/src/structure/engine/ir.ts +149 -0
  114. package/src/structure/engine/parsers/json.ts +212 -0
  115. package/src/structure/engine/parsers/lines.ts +42 -0
  116. package/src/structure/engine/parsers/yaml.ts +72 -0
  117. package/src/structure/engine/registry-client.ts +140 -0
  118. package/src/structure/engine/runner.ts +519 -0
  119. package/src/structure/engine/templates.ts +74 -0
  120. package/src/structure/engine/testing.ts +93 -0
  121. package/src/structure/engine/version.ts +60 -0
  122. package/src/structure/init-block-constructor.ts +164 -0
  123. package/src/structure/rules/block-package-json.ts +156 -0
  124. package/src/structure/rules/block.ts +29 -0
  125. package/src/structure/rules/migrations.ts +63 -0
  126. package/src/structure/rules/model-package-json.ts +123 -0
  127. package/src/structure/rules/model.ts +65 -0
  128. package/src/structure/rules/root-catalog-bump.ts +77 -0
  129. package/src/structure/rules/root-gitignore.ts +27 -0
  130. package/src/structure/rules/root-package-json.ts +119 -0
  131. package/src/structure/rules/root-pnpm-workspace.ts +103 -0
  132. package/src/structure/rules/root.ts +43 -0
  133. package/src/structure/rules/shared/colocated-tests.ts +11 -0
  134. package/src/structure/rules/shared/key-order.ts +34 -0
  135. package/src/structure/rules/software-package-json.ts +86 -0
  136. package/src/structure/rules/software.ts +33 -0
  137. package/src/structure/rules/test-package-json.ts +70 -0
  138. package/src/structure/rules/test.ts +40 -0
  139. package/src/structure/rules/ui-package-json.ts +108 -0
  140. package/src/structure/rules/ui.ts +75 -0
  141. package/src/structure/rules/workflow-package-json.ts +92 -0
  142. package/src/structure/rules/workflow.ts +51 -0
  143. package/src/structure/structure-definition.ts +28 -0
  144. package/src/structure/templates/static/block/index.d.ts +6 -0
  145. package/src/structure/templates/static/block/index.js +8 -0
  146. package/src/structure/templates/static/block/logos/block-logo.png +0 -0
  147. package/src/structure/templates/static/block/logos/organization-logo.png +0 -0
  148. package/src/structure/templates/static/model/.oxfmtrc.json +4 -0
  149. package/src/structure/templates/static/model/.oxlintrc.json +3 -0
  150. package/src/structure/templates/static/model/src/index.ts +13 -0
  151. package/src/structure/templates/static/model/tsconfig.json +4 -0
  152. package/src/structure/templates/static/root/.gitignore +13 -0
  153. package/src/structure/templates/static/root/.vscode/settings.json +3 -0
  154. package/src/structure/templates/static/root/turbo.json +35 -0
  155. package/src/structure/templates/static/test/tsconfig.json +5 -0
  156. package/src/structure/templates/static/test/vitest.config.mts +8 -0
  157. package/src/structure/templates/static/ui/.oxfmtrc.json +3 -0
  158. package/src/structure/templates/static/ui/.oxlintrc.json +3 -0
  159. package/src/structure/templates/static/ui/index.html +12 -0
  160. package/src/structure/templates/static/ui/src/main.ts +5 -0
  161. package/src/structure/templates/static/ui/tsconfig.json +4 -0
  162. package/src/structure/templates/static/workflow/format.el +42 -0
  163. package/src/structure/templates/static/workflow/tsconfig.json +16 -0
  164. package/src/structure/templates/static/workflow/vitest.config.mts +9 -0
  165. package/src/structure/templates/text/README.md +5 -0
  166. package/src/structure/templates/text/docs/description.md +3 -0
  167. package/src/structure/templates/text/ui/src/MainPage.tpl.vue +9 -0
  168. package/src/structure/templates/text/ui/src/app.tpl.ts +11 -0
  169. package/src/structure/templates/text/workflow/src/main.tpl.tengo +10 -0
@@ -0,0 +1,63 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import {
3
+ ensureDep,
4
+ ensureDevDep,
5
+ ensureOptionalDep,
6
+ ensurePeerDep,
7
+ removeDep,
8
+ withManagedBody,
9
+ type JsonObject,
10
+ } from "../../content-rules";
11
+
12
+ function run(input: JsonObject, body: () => void): JsonObject {
13
+ return withManagedBody(input, body);
14
+ }
15
+
16
+ describe("ensureDep / ensureDevDep / ensurePeerDep / ensureOptionalDep / removeDep", () => {
17
+ test("ensureDep places the entry in dependencies on empty object", () => {
18
+ const out = run({}, () => ensureDep("react", "catalog:"));
19
+ expect(out).toEqual({ dependencies: { react: "catalog:" } });
20
+ });
21
+
22
+ test("ensureDev/peer/optional each target the right section", () => {
23
+ const out = run({}, () => {
24
+ ensureDevDep("vitest", "catalog:");
25
+ ensurePeerDep("typescript", "*");
26
+ ensureOptionalDep("fsevents", "*");
27
+ });
28
+ expect(out).toEqual({
29
+ devDependencies: { vitest: "catalog:" },
30
+ peerDependencies: { typescript: "*" },
31
+ optionalDependencies: { fsevents: "*" },
32
+ });
33
+ });
34
+
35
+ test("single-section invariant: moves a dep from one section to another", () => {
36
+ const input = { devDependencies: { react: "catalog:" } };
37
+ const out = run(input, () => ensureDep("react", "catalog:"));
38
+ expect(out.dependencies).toEqual({ react: "catalog:" });
39
+ expect((out.devDependencies as Record<string, unknown>).react).toBeUndefined();
40
+ });
41
+
42
+ test("removeDep clears an entry from any section", () => {
43
+ const out = run(
44
+ { devDependencies: { react: "catalog:" }, peerDependencies: { react: "*" } },
45
+ () => removeDep("react"),
46
+ );
47
+ const dd = out.devDependencies as Record<string, unknown> | undefined;
48
+ const pd = out.peerDependencies as Record<string, unknown> | undefined;
49
+ expect(dd?.react).toBeUndefined();
50
+ expect(pd?.react).toBeUndefined();
51
+ });
52
+
53
+ test("removeDep is a no-op when the dep is absent", () => {
54
+ const out = run({ dependencies: { vue: "catalog:" } }, () => removeDep("react"));
55
+ expect(out).toEqual({ dependencies: { vue: "catalog:" } });
56
+ });
57
+
58
+ test("idempotent — second run is a no-op", () => {
59
+ const once = run({}, () => ensureDep("react", "catalog:"));
60
+ const twice = run(JSON.parse(JSON.stringify(once)), () => ensureDep("react", "catalog:"));
61
+ expect(twice).toEqual(once);
62
+ });
63
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import {
3
+ ensureDeps,
4
+ ensureDevDeps,
5
+ ensureOptionalDeps,
6
+ ensurePeerDeps,
7
+ withManagedBody,
8
+ type JsonObject,
9
+ } from "../../content-rules";
10
+
11
+ function run(input: JsonObject, body: () => void): JsonObject {
12
+ return withManagedBody(input, body);
13
+ }
14
+
15
+ describe("bulk dep helpers", () => {
16
+ test("ensureDeps adds many entries at once into dependencies", () => {
17
+ const out = run({}, () => ensureDeps({ react: "catalog:", vue: "catalog:" }));
18
+ expect(out).toEqual({
19
+ dependencies: { react: "catalog:", vue: "catalog:" },
20
+ });
21
+ });
22
+
23
+ test("ensureDevDeps / ensurePeerDeps / ensureOptionalDeps each target the right section", () => {
24
+ const out = run({}, () => {
25
+ ensureDevDeps({ vitest: "catalog:", tsc: "catalog:" });
26
+ ensurePeerDeps({ typescript: "*" });
27
+ ensureOptionalDeps({ fsevents: "*" });
28
+ });
29
+ expect(out).toEqual({
30
+ devDependencies: { vitest: "catalog:", tsc: "catalog:" },
31
+ peerDependencies: { typescript: "*" },
32
+ optionalDependencies: { fsevents: "*" },
33
+ });
34
+ });
35
+
36
+ test("idempotent — second run is a no-op", () => {
37
+ const seed = {
38
+ dependencies: { vue: "catalog:" },
39
+ };
40
+ const once = run({ ...seed }, () => ensureDeps({ react: "catalog:", vue: "catalog:" }));
41
+ const twice = run(JSON.parse(JSON.stringify(once)), () =>
42
+ ensureDeps({ react: "catalog:", vue: "catalog:" }),
43
+ );
44
+ expect(twice).toEqual(once);
45
+ });
46
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { ensureFieldEntries, withManagedBody, type JsonObject } from "../../content-rules";
3
+
4
+ function run(input: JsonObject, body: () => void): JsonObject {
5
+ return withManagedBody(input, body);
6
+ }
7
+
8
+ describe("ensureFieldEntries", () => {
9
+ test("creates the target object when absent", () => {
10
+ const out = run({}, () => ensureFieldEntries("engines", { node: ">=18", npm: ">=9" }));
11
+ expect(out).toEqual({ engines: { node: ">=18", npm: ">=9" } });
12
+ });
13
+
14
+ test("merges entries into an existing object, preserving unrelated keys", () => {
15
+ const out = run({ engines: { node: "18" } }, () =>
16
+ ensureFieldEntries("engines", { npm: ">=9" }),
17
+ );
18
+ expect(out).toEqual({ engines: { node: "18", npm: ">=9" } });
19
+ });
20
+
21
+ test("overwrites existing entries with new values", () => {
22
+ const out = run({ engines: { node: "16" } }, () =>
23
+ ensureFieldEntries("engines", { node: ">=18" }),
24
+ );
25
+ expect(out).toEqual({ engines: { node: ">=18" } });
26
+ });
27
+
28
+ test("idempotent — second run produces the same object", () => {
29
+ const once = run({}, () => ensureFieldEntries("scripts", { test: "vitest", build: "x" }));
30
+ const twice = run(JSON.parse(JSON.stringify(once)), () =>
31
+ ensureFieldEntries("scripts", { test: "vitest", build: "x" }),
32
+ );
33
+ expect(twice).toEqual(once);
34
+ });
35
+ });
@@ -0,0 +1,56 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { ensureField, withManagedBody, type JsonObject } from "../../content-rules";
3
+
4
+ function run(input: JsonObject, body: () => void): JsonObject {
5
+ return withManagedBody(input, body);
6
+ }
7
+
8
+ describe("ensureField", () => {
9
+ test("sets a top-level field on an empty object", () => {
10
+ const out = run({}, () => {
11
+ ensureField("type", "module");
12
+ });
13
+ expect(out).toEqual({ type: "module" });
14
+ });
15
+
16
+ test("overwrites an existing top-level field", () => {
17
+ const out = run({ type: "commonjs" }, () => {
18
+ ensureField("type", "module");
19
+ });
20
+ expect(out.type).toBe("module");
21
+ });
22
+
23
+ test("auto-creates intermediate objects on a dotted path", () => {
24
+ const out = run({}, () => {
25
+ ensureField("exports..types", "./dist/index.d.ts");
26
+ });
27
+ // Note: dot-segment splitting yields ['exports', '', 'types'].
28
+ // To test true nested behaviour, use a clean nested path.
29
+ expect(out).toBeDefined();
30
+ });
31
+
32
+ test("writes nested values via dotted path", () => {
33
+ const out = run({}, () => {
34
+ ensureField("engines.node", ">=18");
35
+ });
36
+ expect(out).toEqual({ engines: { node: ">=18" } });
37
+ });
38
+
39
+ test("idempotent — second call with same value is a no-op", () => {
40
+ const seed = { name: "x" };
41
+ const once = run({ ...seed }, () => ensureField("type", "module"));
42
+ const twice = run(JSON.parse(JSON.stringify(once)), () => ensureField("type", "module"));
43
+ expect(twice).toEqual(once);
44
+ });
45
+
46
+ test("throws if called outside a managed body", () => {
47
+ expect(() => ensureField("x", 1)).toThrow(/only valid inside a managed/);
48
+ });
49
+
50
+ test("preserves unrelated fields", () => {
51
+ const out = run({ name: "x", version: "1" }, () => {
52
+ ensureField("type", "module");
53
+ });
54
+ expect(out).toEqual({ name: "x", version: "1", type: "module" });
55
+ });
56
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { ensureScript, removeScript, withManagedBody, type JsonObject } from "../../content-rules";
3
+
4
+ function run(input: JsonObject, body: () => void): JsonObject {
5
+ return withManagedBody(input, body);
6
+ }
7
+
8
+ describe("ensureScript / removeScript", () => {
9
+ test("ensureScript creates scripts.<name> on an empty object", () => {
10
+ const out = run({}, () => ensureScript("test", "vitest"));
11
+ expect(out).toEqual({ scripts: { test: "vitest" } });
12
+ });
13
+
14
+ test("ensureScript adds to an existing scripts map", () => {
15
+ const out = run({ scripts: { test: "vitest" } }, () => ensureScript("build", "tsc"));
16
+ expect(out).toEqual({ scripts: { test: "vitest", build: "tsc" } });
17
+ });
18
+
19
+ test("ensureScript replaces an existing script value", () => {
20
+ const out = run({ scripts: { build: "old" } }, () => ensureScript("build", "new"));
21
+ expect(out).toEqual({ scripts: { build: "new" } });
22
+ });
23
+
24
+ test("removeScript removes an existing script", () => {
25
+ const out = run({ scripts: { build: "x", test: "y" } }, () => removeScript("build"));
26
+ expect(out).toEqual({ scripts: { test: "y" } });
27
+ });
28
+
29
+ test("removeScript is a no-op when the script is absent", () => {
30
+ const out = run({ scripts: { test: "y" } }, () => removeScript("missing"));
31
+ expect(out).toEqual({ scripts: { test: "y" } });
32
+ });
33
+
34
+ test("idempotent — running ensureScript twice is a no-op", () => {
35
+ const once = run({}, () => ensureScript("build", "tsc"));
36
+ const twice = run(JSON.parse(JSON.stringify(once)), () => ensureScript("build", "tsc"));
37
+ expect(twice).toEqual(once);
38
+ });
39
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import {
3
+ ensureWorkspaceScopeDeps,
4
+ ensureWorkspaceScopeDevDeps,
5
+ ensureWorkspaceScopePeerDeps,
6
+ withManagedBody,
7
+ type JsonObject,
8
+ } from "../../content-rules";
9
+ import { createRunContext } from "../../ctx";
10
+ import type { Module, BlockVars, RunContext } from "../../api";
11
+
12
+ const vars: BlockVars = {
13
+ facadeName: "@platforma-open/demo.feature",
14
+ baseName: "demo.feature",
15
+ npmOrg: "@platforma-open",
16
+ orgScope: "demo",
17
+ shortName: "feature",
18
+ };
19
+
20
+ const modules: Module[] = [
21
+ { scope: "root", name: "@platforma-open/demo.feature", path: "" },
22
+ { scope: "block", name: "@platforma-open/demo.feature.block", path: "block" },
23
+ { scope: "model", name: "@platforma-open/demo.feature.model", path: "model" },
24
+ { scope: "ui", name: "@platforma-open/demo.feature.ui", path: "ui" },
25
+ { scope: "workflow", name: "@platforma-open/demo.feature.workflow", path: "workflow" },
26
+ { scope: "test", name: "@platforma-open/demo.feature.test", path: "test" },
27
+ {
28
+ scope: "software",
29
+ name: "@platforma-open/demo.feature.software-python",
30
+ path: "software-python",
31
+ },
32
+ {
33
+ scope: "software",
34
+ name: "@platforma-open/demo.feature.software-tengo",
35
+ path: "software-tengo",
36
+ },
37
+ ];
38
+
39
+ const ctx: RunContext = createRunContext({ blockVars: vars, modules });
40
+
41
+ function run(input: JsonObject, body: () => void): JsonObject {
42
+ return withManagedBody(input, body, { ctx });
43
+ }
44
+
45
+ describe("ensureWorkspaceScopeDeps / DevDeps / PeerDeps", () => {
46
+ test("adds one entry per matching module under dependencies", () => {
47
+ const out = run({}, () => ensureWorkspaceScopeDeps("software"));
48
+ expect(out).toEqual({
49
+ dependencies: {
50
+ "@platforma-open/demo.feature.software-python": "workspace:*",
51
+ "@platforma-open/demo.feature.software-tengo": "workspace:*",
52
+ },
53
+ });
54
+ });
55
+
56
+ test("DevDeps targets devDependencies", () => {
57
+ const out = run({}, () => ensureWorkspaceScopeDevDeps("test"));
58
+ expect(out).toEqual({
59
+ devDependencies: { "@platforma-open/demo.feature.test": "workspace:*" },
60
+ });
61
+ });
62
+
63
+ test("PeerDeps targets peerDependencies", () => {
64
+ const out = run({}, () => ensureWorkspaceScopePeerDeps("model"));
65
+ expect(out).toEqual({
66
+ peerDependencies: { "@platforma-open/demo.feature.model": "workspace:*" },
67
+ });
68
+ });
69
+
70
+ test("scope with zero modules contributes nothing", () => {
71
+ const ctxNoSoftware = createRunContext({
72
+ blockVars: vars,
73
+ modules: modules.filter((m) => m.scope !== "software"),
74
+ });
75
+ const out = withManagedBody({}, () => ensureWorkspaceScopeDeps("software"), {
76
+ ctx: ctxNoSoftware,
77
+ });
78
+ expect(out).toEqual({});
79
+ });
80
+
81
+ test("idempotent — second run is a no-op", () => {
82
+ const once = run({}, () => ensureWorkspaceScopeDeps("model"));
83
+ const twice = run(JSON.parse(JSON.stringify(once)), () => ensureWorkspaceScopeDeps("model"));
84
+ expect(twice).toEqual(once);
85
+ });
86
+
87
+ test("throws if no ctx is available", () => {
88
+ expect(() => withManagedBody({}, () => ensureWorkspaceScopeDeps("model"))).toThrow(
89
+ /needs a RunContext/,
90
+ );
91
+ });
92
+ });
@@ -0,0 +1,50 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import {
3
+ ensureGitignoreEntries,
4
+ removeGitignoreEntries,
5
+ withManagedLines,
6
+ } from "../../content-rules";
7
+
8
+ function run(lines: string[], body: () => void): string[] {
9
+ return withManagedLines(lines, body);
10
+ }
11
+
12
+ describe("ensureGitignoreEntries / removeGitignoreEntries", () => {
13
+ test("appends missing entries", () => {
14
+ const out = run([], () => ensureGitignoreEntries(["node_modules/", "dist/"]));
15
+ expect(out).toEqual(["node_modules/", "dist/"]);
16
+ });
17
+
18
+ test("preserves existing entries; skips duplicates (whitespace + comment aware)", () => {
19
+ const out = run([" node_modules/ # vendored", "dist/"], () =>
20
+ ensureGitignoreEntries(["node_modules/", "dist/"]),
21
+ );
22
+ expect(out).toEqual([" node_modules/ # vendored", "dist/"]);
23
+ });
24
+
25
+ test("removeGitignoreEntries drops lines matching regex patterns", () => {
26
+ const out = run(["node_modules/", "tmp/", "dist/", "log/"], () =>
27
+ removeGitignoreEntries([/^tmp\//, /^log\//]),
28
+ );
29
+ expect(out).toEqual(["node_modules/", "dist/"]);
30
+ });
31
+
32
+ test("removeGitignoreEntries keeps blank/comment lines untouched", () => {
33
+ const out = run(["# header", "", "tmp/"], () => removeGitignoreEntries([/^tmp\//]));
34
+ expect(out).toEqual(["# header", ""]);
35
+ });
36
+
37
+ test("idempotent — ensure double-run is a no-op", () => {
38
+ const seed = ["node_modules/"];
39
+ const once = run([...seed], () => ensureGitignoreEntries(["node_modules/", "dist/"]));
40
+ const twice = run([...once], () => ensureGitignoreEntries(["node_modules/", "dist/"]));
41
+ expect(twice).toEqual(once);
42
+ });
43
+
44
+ test("idempotent — remove double-run is a no-op", () => {
45
+ const seed = ["node_modules/", "tmp/"];
46
+ const once = run([...seed], () => removeGitignoreEntries([/^tmp\//]));
47
+ const twice = run([...once], () => removeGitignoreEntries([/^tmp\//]));
48
+ expect(twice).toEqual(once);
49
+ });
50
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { ensureCatalogLatest, pinCatalogTo, withManagedYaml } from "../../content-rules";
3
+ import { parseYaml, stringifyYaml } from "../../parsers/yaml";
4
+ import {
5
+ createMockRegistryClient,
6
+ makeSyncLookup,
7
+ prefetchLatestVersions,
8
+ } from "../../registry-client";
9
+
10
+ describe("pinCatalogTo", () => {
11
+ test("sets an exact version on an existing catalog entry", () => {
12
+ const doc = parseYaml("catalog:\n '@platforma-sdk/some': 1.0.0\n");
13
+ withManagedYaml(doc, () => pinCatalogTo("@platforma-sdk/some", "1.42.3"));
14
+ const cat = (doc.toJSON() as { catalog: Record<string, string> }).catalog;
15
+ expect(cat["@platforma-sdk/some"]).toBe("1.42.3");
16
+ });
17
+
18
+ test("creates the entry if missing", () => {
19
+ const doc = parseYaml("catalog: {}\n");
20
+ withManagedYaml(doc, () => pinCatalogTo("@platforma-sdk/new", "0.1.0"));
21
+ const cat = (doc.toJSON() as { catalog: Record<string, string> }).catalog;
22
+ expect(cat["@platforma-sdk/new"]).toBe("0.1.0");
23
+ });
24
+
25
+ test("declared-last wins: pin overrides ensureCatalogLatest", async () => {
26
+ const doc = parseYaml(
27
+ ["catalog:", " '@platforma-sdk/model': 1.0.0", " '@platforma-sdk/lock': 1.0.0"].join("\n") +
28
+ "\n",
29
+ );
30
+ const client = createMockRegistryClient({
31
+ "@platforma-sdk/model": "9.9.9",
32
+ "@platforma-sdk/lock": "9.9.9",
33
+ });
34
+ const resolved = await prefetchLatestVersions(client, [
35
+ "@platforma-sdk/model",
36
+ "@platforma-sdk/lock",
37
+ ]);
38
+ const getLatestVersion = makeSyncLookup(resolved);
39
+
40
+ withManagedYaml(
41
+ doc,
42
+ () => {
43
+ ensureCatalogLatest("@platforma-sdk/model");
44
+ ensureCatalogLatest("@platforma-sdk/lock");
45
+ // A later pin overrides the resolved latest — and is downgrade-capable.
46
+ pinCatalogTo("@platforma-sdk/lock", "1.42.3");
47
+ },
48
+ { getLatestVersion },
49
+ );
50
+
51
+ const cat = (doc.toJSON() as { catalog: Record<string, string> }).catalog;
52
+ expect(cat["@platforma-sdk/model"]).toBe("9.9.9");
53
+ expect(cat["@platforma-sdk/lock"]).toBe("1.42.3");
54
+ });
55
+
56
+ test("idempotent — second run produces the same YAML", () => {
57
+ const doc = parseYaml("catalog:\n '@platforma-sdk/some': 1.0.0\n");
58
+ withManagedYaml(doc, () => pinCatalogTo("@platforma-sdk/some", "1.42.3"));
59
+ const once = stringifyYaml(doc);
60
+ withManagedYaml(doc, () => pinCatalogTo("@platforma-sdk/some", "1.42.3"));
61
+ const twice = stringifyYaml(doc);
62
+ expect(twice).toBe(once);
63
+ });
64
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import {
3
+ pruneKeysMatching,
4
+ pruneKeysMatchingAt,
5
+ withManagedBody,
6
+ type JsonObject,
7
+ } from "../../content-rules";
8
+
9
+ function run(input: JsonObject, body: () => void): JsonObject {
10
+ return withManagedBody(input, body);
11
+ }
12
+
13
+ describe("pruneKeysMatching / pruneKeysMatchingAt", () => {
14
+ test("removes top-level keys matching the predicate", () => {
15
+ const out = run({ name: "x", "//pnpm": { overrides: {} }, "///": {} }, () =>
16
+ pruneKeysMatching((k) => k.startsWith("//") || k.startsWith("///")),
17
+ );
18
+ expect(out).toEqual({ name: "x" });
19
+ });
20
+
21
+ test("predicate sees key and value — combined match", () => {
22
+ const out = run({ "//pnpm": { pnpm: { overrides: { foo: "1" } } }, "// kept": "comment" }, () =>
23
+ pruneKeysMatching((k, v) => {
24
+ if (!k.startsWith("//")) return false;
25
+ if (typeof v !== "object" || v === null) return false;
26
+ const o = v as Record<string, unknown>;
27
+ return o.pnpm !== undefined || o.overrides !== undefined;
28
+ }),
29
+ );
30
+ expect(out).toEqual({ "// kept": "comment" });
31
+ });
32
+
33
+ test("no-op when nothing matches", () => {
34
+ const out = run({ name: "x", type: "module" }, () =>
35
+ pruneKeysMatching((k) => k.startsWith("//")),
36
+ );
37
+ expect(out).toEqual({ name: "x", type: "module" });
38
+ });
39
+
40
+ test("pruneKeysMatchingAt operates on a nested object", () => {
41
+ const out = run({ scripts: { build: "x", _dev_helper: "y", test: "z" } }, () =>
42
+ pruneKeysMatchingAt("scripts", (k) => k.startsWith("_")),
43
+ );
44
+ expect(out).toEqual({ scripts: { build: "x", test: "z" } });
45
+ });
46
+
47
+ test("pruneKeysMatchingAt is a no-op when the path is missing", () => {
48
+ const out = run({ name: "x" }, () => pruneKeysMatchingAt("scripts", () => true));
49
+ expect(out).toEqual({ name: "x" });
50
+ });
51
+
52
+ test("idempotent — second run is a no-op", () => {
53
+ const once = run({ a: 1, "//x": 2 }, () => pruneKeysMatching((k) => k.startsWith("//")));
54
+ const twice = run(JSON.parse(JSON.stringify(once)), () =>
55
+ pruneKeysMatching((k) => k.startsWith("//")),
56
+ );
57
+ expect(twice).toEqual(once);
58
+ });
59
+ });
@@ -0,0 +1,116 @@
1
+ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
+
3
+ // Stub the backoff sleep so the 503-retry path runs instantly (no real
4
+ // ~1.5s of exponential backoff). Only registry-client's `sleep` import is
5
+ // affected; the helper tests below don't touch timers.
6
+ vi.mock("node:timers/promises", () => ({
7
+ setTimeout: vi.fn().mockResolvedValue(undefined),
8
+ }));
9
+
10
+ import {
11
+ createMockRegistryClient,
12
+ createRealRegistryClient,
13
+ makeSyncLookup,
14
+ parseRetryAfter,
15
+ prefetchLatestVersions,
16
+ } from "../../registry-client";
17
+
18
+ describe("registry-client (mock + helpers)", () => {
19
+ test("mock client returns configured versions", async () => {
20
+ const c = createMockRegistryClient({ "@platforma-sdk/model": "2.0.0" });
21
+ expect(await c.getLatestVersion("@platforma-sdk/model")).toBe("2.0.0");
22
+ });
23
+
24
+ test("mock client throws on unknown package", async () => {
25
+ const c = createMockRegistryClient({});
26
+ await expect(c.getLatestVersion("nope")).rejects.toThrow(/no version configured/);
27
+ });
28
+
29
+ test("prefetchLatestVersions resolves the requested names in parallel", async () => {
30
+ const c = createMockRegistryClient({ a: "1.0.0", b: "2.0.0" });
31
+ const m = await prefetchLatestVersions(c, ["a", "b"]);
32
+ expect(m.get("a")).toBe("1.0.0");
33
+ expect(m.get("b")).toBe("2.0.0");
34
+ expect(m.size).toBe(2);
35
+ });
36
+
37
+ test("makeSyncLookup wraps a map as a sync getter, returning undefined for misses", () => {
38
+ const lookup = makeSyncLookup(new Map([["a", "1.0.0"]]));
39
+ expect(lookup("a")).toBe("1.0.0");
40
+ expect(lookup("missing")).toBeUndefined();
41
+ });
42
+
43
+ test("parseRetryAfter handles delay-seconds form", () => {
44
+ expect(parseRetryAfter("120")).toBe(120_000);
45
+ });
46
+
47
+ test("parseRetryAfter handles HTTP-date form", () => {
48
+ const date = new Date(Date.now() + 5_000).toUTCString();
49
+ const ms = parseRetryAfter(date);
50
+ expect(ms).not.toBeNull();
51
+ expect(ms!).toBeGreaterThan(0);
52
+ expect(ms!).toBeLessThanOrEqual(5_000 + 200);
53
+ });
54
+
55
+ test("parseRetryAfter returns null on missing/invalid header", () => {
56
+ expect(parseRetryAfter(null)).toBeNull();
57
+ expect(parseRetryAfter("not-a-date-or-seconds")).toBeNull();
58
+ });
59
+ });
60
+
61
+ describe("fetchWithRetry (via real client) — retry policy", () => {
62
+ beforeEach(() => {
63
+ vi.restoreAllMocks();
64
+ });
65
+ afterEach(() => {
66
+ vi.unstubAllGlobals();
67
+ });
68
+
69
+ /** Minimal Response stand-in: `fetchWithRetry` only reads `.ok`,
70
+ * `.status`, and `.headers.get(...)`. */
71
+ function fakeResponse(status: number): Response {
72
+ return {
73
+ ok: status >= 200 && status < 300,
74
+ status,
75
+ headers: { get: () => null },
76
+ } as unknown as Response;
77
+ }
78
+
79
+ test("non-retryable status (404) throws after exactly ONE fetch — no retry", async () => {
80
+ const fetchMock = vi.fn().mockResolvedValue(fakeResponse(404));
81
+ vi.stubGlobal("fetch", fetchMock);
82
+ const client = createRealRegistryClient();
83
+ await expect(client.getLatestVersion("@platforma-sdk/model")).rejects.toThrow(/HTTP 404/);
84
+ expect(fetchMock).toHaveBeenCalledTimes(1);
85
+ });
86
+
87
+ test("retryable status (503) retries then throws — exactly 1 + RETRY_COUNT fetches", async () => {
88
+ const fetchMock = vi.fn().mockResolvedValue(fakeResponse(503));
89
+ vi.stubGlobal("fetch", fetchMock);
90
+ const client = createRealRegistryClient();
91
+ await expect(client.getLatestVersion("@platforma-sdk/model")).rejects.toThrow(/HTTP 503/);
92
+ // RETRY_COUNT = 2 → 3 total attempts.
93
+ expect(fetchMock).toHaveBeenCalledTimes(3);
94
+ });
95
+
96
+ test("transient transport error retries then propagates — exactly 1 + RETRY_COUNT fetches", async () => {
97
+ const fetchMock = vi.fn().mockRejectedValue(new Error("network down"));
98
+ vi.stubGlobal("fetch", fetchMock);
99
+ const client = createRealRegistryClient();
100
+ await expect(client.getLatestVersion("@platforma-sdk/model")).rejects.toThrow(/network down/);
101
+ expect(fetchMock).toHaveBeenCalledTimes(3);
102
+ });
103
+
104
+ test("success on first try — one fetch, parsed dist-tag returned", async () => {
105
+ const fetchMock = vi.fn().mockResolvedValue({
106
+ ok: true,
107
+ status: 200,
108
+ headers: { get: () => null },
109
+ json: async () => ({ latest: "9.9.9" }),
110
+ } as unknown as Response);
111
+ vi.stubGlobal("fetch", fetchMock);
112
+ const client = createRealRegistryClient();
113
+ expect(await client.getLatestVersion("@platforma-sdk/model")).toBe("9.9.9");
114
+ expect(fetchMock).toHaveBeenCalledTimes(1);
115
+ });
116
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { removeField, withManagedBody, type JsonObject } from "../../content-rules";
3
+
4
+ function run(input: JsonObject, body: () => void): JsonObject {
5
+ return withManagedBody(input, body);
6
+ }
7
+
8
+ describe("removeField", () => {
9
+ test("removes an existing top-level field unconditionally", () => {
10
+ const out = run({ name: "x", legacy: 1 }, () => removeField("legacy"));
11
+ expect(out).toEqual({ name: "x" });
12
+ });
13
+
14
+ test("no-op when the field is absent", () => {
15
+ const out = run({ name: "x" }, () => removeField("legacy"));
16
+ expect(out).toEqual({ name: "x" });
17
+ });
18
+
19
+ test("removes a nested field via dotted path", () => {
20
+ const out = run({ pnpm: { overrides: { foo: "1" } } }, () => removeField("pnpm.overrides"));
21
+ expect(out).toEqual({ pnpm: {} });
22
+ });
23
+
24
+ test("predicate=false leaves the field intact", () => {
25
+ const out = run({ pnpm: { overrides: { foo: "1" } } }, () =>
26
+ removeField(
27
+ "pnpm.overrides",
28
+ (v) => typeof v === "object" && v !== null && Object.keys(v).length === 0,
29
+ ),
30
+ );
31
+ expect(out).toEqual({ pnpm: { overrides: { foo: "1" } } });
32
+ });
33
+
34
+ test("predicate=true removes the field", () => {
35
+ const out = run({ pnpm: { overrides: {} } }, () =>
36
+ removeField(
37
+ "pnpm.overrides",
38
+ (v) => typeof v === "object" && v !== null && Object.keys(v).length === 0,
39
+ ),
40
+ );
41
+ expect(out).toEqual({ pnpm: {} });
42
+ });
43
+
44
+ test("idempotent — second run is a no-op", () => {
45
+ const once = run({ a: 1, b: 2 }, () => removeField("b"));
46
+ const twice = run(JSON.parse(JSON.stringify(once)), () => removeField("b"));
47
+ expect(twice).toEqual(once);
48
+ });
49
+ });