@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.
- 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 +9 -7
- 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,40 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { requireField, withManagedBody, type JsonObject } from "../../content-rules";
|
|
3
|
+
|
|
4
|
+
function run(input: JsonObject, body: () => void): JsonObject {
|
|
5
|
+
return withManagedBody(input, body);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe("requireField", () => {
|
|
9
|
+
test("no-op when the field is present", () => {
|
|
10
|
+
const out = run({ name: "x" }, () => requireField("name"));
|
|
11
|
+
expect(out).toEqual({ name: "x" });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("throws default message when the field is absent", () => {
|
|
15
|
+
expect(() =>
|
|
16
|
+
run({}, () => {
|
|
17
|
+
requireField("name");
|
|
18
|
+
}),
|
|
19
|
+
).toThrow(/requireField: missing field 'name'/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("throws custom message when supplied", () => {
|
|
23
|
+
expect(() =>
|
|
24
|
+
run({}, () => {
|
|
25
|
+
requireField("name", "block name is required");
|
|
26
|
+
}),
|
|
27
|
+
).toThrow(/block name is required/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("accepts a nested field via dotted path", () => {
|
|
31
|
+
const out = run({ engines: { node: ">=18" } }, () => requireField("engines.node"));
|
|
32
|
+
expect(out).toEqual({ engines: { node: ">=18" } });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("idempotent — running twice on a present field is a no-op", () => {
|
|
36
|
+
const once = run({ name: "x" }, () => requireField("name"));
|
|
37
|
+
const twice = run(JSON.parse(JSON.stringify(once)), () => requireField("name"));
|
|
38
|
+
expect(twice).toEqual(once);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { transformAt, withManagedBody, type JsonObject } from "../../content-rules";
|
|
3
|
+
|
|
4
|
+
function run(input: JsonObject, body: () => void): JsonObject {
|
|
5
|
+
return withManagedBody(input, body);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe("transformAt", () => {
|
|
9
|
+
test("replaces a primitive at the given path", () => {
|
|
10
|
+
const out = run({ count: 1 }, () => transformAt<number>("count", (v) => (v ?? 0) + 1));
|
|
11
|
+
expect(out).toEqual({ count: 2 });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("replaces a nested object at the given path", () => {
|
|
15
|
+
const out = run({ block: { foo: 1 } }, () =>
|
|
16
|
+
transformAt<{ foo: number }>("block", (v) => ({ ...v, bar: 2 }) as { foo: number }),
|
|
17
|
+
);
|
|
18
|
+
expect(out).toEqual({ block: { foo: 1, bar: 2 } });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("auto-creates intermediate objects for missing nested paths", () => {
|
|
22
|
+
const out = run({}, () => transformAt<string | undefined>("nested.key", () => "ok"));
|
|
23
|
+
expect(out).toEqual({ nested: { key: "ok" } });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("idempotent — when the lambda is a fixed point, second run is a no-op", () => {
|
|
27
|
+
const fix = (v: number | undefined) => ((v ?? 0) > 5 ? v! : 5);
|
|
28
|
+
const once = run({ count: 1 }, () => transformAt<number>("count", fix));
|
|
29
|
+
const twice = run(JSON.parse(JSON.stringify(once)), () => transformAt<number>("count", fix));
|
|
30
|
+
expect(once).toEqual({ count: 5 });
|
|
31
|
+
expect(twice).toEqual(once);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// Unified `when()` — same symbol dispatches by active state.
|
|
2
|
+
//
|
|
3
|
+
// Outer mode (inside `defineStructure`) is covered by existing
|
|
4
|
+
// builders/runner tests. This file covers inner-mode dispatch:
|
|
5
|
+
// `when()` called inside a managed(...) body evaluates the trigger
|
|
6
|
+
// against the runner-supplied TriggerContext and runs / skips the
|
|
7
|
+
// body synchronously.
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect } from "vitest";
|
|
10
|
+
import { when } from "../../builders";
|
|
11
|
+
import {
|
|
12
|
+
ensureField,
|
|
13
|
+
ensureCatalogVersion,
|
|
14
|
+
ensureGitignoreEntries,
|
|
15
|
+
removeDep,
|
|
16
|
+
ensureDep,
|
|
17
|
+
withManagedBody,
|
|
18
|
+
withManagedLines,
|
|
19
|
+
withManagedYaml,
|
|
20
|
+
type JsonObject,
|
|
21
|
+
} from "../../content-rules";
|
|
22
|
+
import { parseYaml } from "../../parsers/yaml";
|
|
23
|
+
import { createRunContext } from "../../ctx";
|
|
24
|
+
import type { BlockVars, RunContext } from "../../api";
|
|
25
|
+
import type { TriggerContext } from "../../ir";
|
|
26
|
+
|
|
27
|
+
const vars: BlockVars = {
|
|
28
|
+
facadeName: "@platforma-open/demo.x",
|
|
29
|
+
baseName: "demo.x",
|
|
30
|
+
npmOrg: "@platforma-open",
|
|
31
|
+
orgScope: "demo",
|
|
32
|
+
shortName: "x",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const ctx: RunContext = createRunContext({ blockVars: vars, modules: [] });
|
|
36
|
+
|
|
37
|
+
function makeTctx(
|
|
38
|
+
opts: {
|
|
39
|
+
paths?: ReadonlySet<string>;
|
|
40
|
+
version?: number;
|
|
41
|
+
} = {},
|
|
42
|
+
): TriggerContext {
|
|
43
|
+
const paths = opts.paths ?? new Set<string>();
|
|
44
|
+
return {
|
|
45
|
+
ctx,
|
|
46
|
+
version: opts.version ?? 1,
|
|
47
|
+
pathExists: (p) => paths.has(p),
|
|
48
|
+
pathMissing: (p) => !paths.has(p),
|
|
49
|
+
filesMatch: () => false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("when() — inner-mode dispatch inside managed bodies", () => {
|
|
54
|
+
test("predicate=true runs the body (JSON state)", () => {
|
|
55
|
+
const out = withManagedBody(
|
|
56
|
+
{ name: "x" } as JsonObject,
|
|
57
|
+
() => {
|
|
58
|
+
when(
|
|
59
|
+
({ pathExists }) => pathExists("test-legacy/"),
|
|
60
|
+
() => {
|
|
61
|
+
ensureField("flag", "fired");
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
{ triggerContext: makeTctx({ paths: new Set(["test-legacy/"]) }) },
|
|
66
|
+
);
|
|
67
|
+
expect(out.flag).toBe("fired");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("predicate=false skips the body", () => {
|
|
71
|
+
const out = withManagedBody(
|
|
72
|
+
{ name: "x" } as JsonObject,
|
|
73
|
+
() => {
|
|
74
|
+
when(
|
|
75
|
+
({ pathExists }) => pathExists("test-legacy/"),
|
|
76
|
+
() => {
|
|
77
|
+
ensureField("flag", "fired");
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
{ triggerContext: makeTctx({ paths: new Set() }) },
|
|
82
|
+
);
|
|
83
|
+
expect(out.flag).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("inner when works inside a YAML managed body", () => {
|
|
87
|
+
const doc = parseYaml("catalog: {}\n");
|
|
88
|
+
withManagedYaml(
|
|
89
|
+
doc,
|
|
90
|
+
() => {
|
|
91
|
+
when(
|
|
92
|
+
({ ctx }) => !ctx.isSdkInternal,
|
|
93
|
+
() => {
|
|
94
|
+
ensureCatalogVersion("@platforma-sdk/model", "1.2.3");
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
{ triggerContext: makeTctx() },
|
|
99
|
+
);
|
|
100
|
+
const cat = (doc.toJSON() as { catalog: Record<string, string> }).catalog;
|
|
101
|
+
expect(cat["@platforma-sdk/model"]).toBe("1.2.3");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("inner when works inside a lines managed body", () => {
|
|
105
|
+
const out = withManagedLines(
|
|
106
|
+
[],
|
|
107
|
+
() => {
|
|
108
|
+
when(
|
|
109
|
+
({ version }) => version >= 1,
|
|
110
|
+
() => {
|
|
111
|
+
ensureGitignoreEntries(["node_modules/"]);
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
{ triggerContext: makeTctx({ version: 1 }) },
|
|
116
|
+
);
|
|
117
|
+
expect(out).toEqual(["node_modules/"]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("inner when nests: outer predicate false short-circuits inner", () => {
|
|
121
|
+
const seen: string[] = [];
|
|
122
|
+
withManagedBody(
|
|
123
|
+
{} as JsonObject,
|
|
124
|
+
() => {
|
|
125
|
+
when(
|
|
126
|
+
() => false,
|
|
127
|
+
() => {
|
|
128
|
+
seen.push("outer");
|
|
129
|
+
when(
|
|
130
|
+
() => true,
|
|
131
|
+
() => {
|
|
132
|
+
seen.push("inner");
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
{ triggerContext: makeTctx() },
|
|
139
|
+
);
|
|
140
|
+
expect(seen).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("inner when nests: both true → both bodies run", () => {
|
|
144
|
+
const seen: string[] = [];
|
|
145
|
+
withManagedBody(
|
|
146
|
+
{} as JsonObject,
|
|
147
|
+
() => {
|
|
148
|
+
when(
|
|
149
|
+
() => true,
|
|
150
|
+
() => {
|
|
151
|
+
seen.push("outer");
|
|
152
|
+
when(
|
|
153
|
+
() => true,
|
|
154
|
+
() => {
|
|
155
|
+
seen.push("inner");
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
{ triggerContext: makeTctx() },
|
|
162
|
+
);
|
|
163
|
+
expect(seen).toEqual(["outer", "inner"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("inner when exposes ctx via the predicate", () => {
|
|
167
|
+
const out = withManagedBody(
|
|
168
|
+
{} as JsonObject,
|
|
169
|
+
() => {
|
|
170
|
+
when(
|
|
171
|
+
({ ctx }) => ctx.blockVars.shortName === "x",
|
|
172
|
+
() => {
|
|
173
|
+
ensureField("matched", true);
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
{ triggerContext: makeTctx() },
|
|
178
|
+
);
|
|
179
|
+
expect(out.matched).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("compound migration pattern: rewrite dep when sibling path exists", () => {
|
|
183
|
+
const out = withManagedBody(
|
|
184
|
+
{ dependencies: { "test-old": "workspace:*" } } as JsonObject,
|
|
185
|
+
() => {
|
|
186
|
+
ensureField("type", "module");
|
|
187
|
+
when(
|
|
188
|
+
({ pathExists }) => pathExists("test-legacy/"),
|
|
189
|
+
() => {
|
|
190
|
+
removeDep("test-old");
|
|
191
|
+
ensureDep("test-legacy", "workspace:*");
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
},
|
|
195
|
+
{ triggerContext: makeTctx({ paths: new Set(["test-legacy/"]) }) },
|
|
196
|
+
);
|
|
197
|
+
expect(out).toEqual({
|
|
198
|
+
type: "module",
|
|
199
|
+
dependencies: { "test-legacy": "workspace:*" },
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("inner when inside a body throws if triggerContext was not supplied", () => {
|
|
204
|
+
expect(() =>
|
|
205
|
+
withManagedBody({}, () => {
|
|
206
|
+
when(
|
|
207
|
+
() => true,
|
|
208
|
+
() => {
|
|
209
|
+
ensureField("x", 1);
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
}),
|
|
213
|
+
).toThrow(/requires a triggerContext/);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("when() called outside any context throws", () => {
|
|
217
|
+
expect(() =>
|
|
218
|
+
when(
|
|
219
|
+
() => true,
|
|
220
|
+
() => {},
|
|
221
|
+
),
|
|
222
|
+
).toThrow(/outside defineStructure.*outside any managed/);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("idempotent — double-run yields the same parsed state", () => {
|
|
226
|
+
const body = () => {
|
|
227
|
+
ensureField("type", "module");
|
|
228
|
+
when(
|
|
229
|
+
({ pathExists }) => pathExists("legacy/"),
|
|
230
|
+
() => {
|
|
231
|
+
ensureField("legacy-flag", true);
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
const tctx = makeTctx({ paths: new Set(["legacy/"]) });
|
|
236
|
+
const once = withManagedBody({} as JsonObject, body, {
|
|
237
|
+
triggerContext: tctx,
|
|
238
|
+
});
|
|
239
|
+
const twice = withManagedBody(JSON.parse(JSON.stringify(once)) as JsonObject, body, {
|
|
240
|
+
triggerContext: tctx,
|
|
241
|
+
});
|
|
242
|
+
expect(twice).toEqual(once);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { ensureWorkspaceModulePaths, withManagedYaml } from "../../content-rules";
|
|
3
|
+
import { parseYaml, stringifyYaml } from "../../parsers/yaml";
|
|
4
|
+
import { createRunContext } from "../../ctx";
|
|
5
|
+
import type { Module, BlockVars } from "../../api";
|
|
6
|
+
|
|
7
|
+
const vars: BlockVars = {
|
|
8
|
+
facadeName: "@platforma-open/demo.feature",
|
|
9
|
+
baseName: "demo.feature",
|
|
10
|
+
npmOrg: "@platforma-open",
|
|
11
|
+
orgScope: "demo",
|
|
12
|
+
shortName: "feature",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const modules: Module[] = [
|
|
16
|
+
{ scope: "root", name: "@platforma-open/demo.feature", path: "" },
|
|
17
|
+
{ scope: "block", name: "@platforma-open/demo.feature.block", path: "block" },
|
|
18
|
+
{ scope: "model", name: "@platforma-open/demo.feature.model", path: "model" },
|
|
19
|
+
{ scope: "ui", name: "@platforma-open/demo.feature.ui", path: "ui" },
|
|
20
|
+
{ scope: "workflow", name: "@platforma-open/demo.feature.workflow", path: "workflow" },
|
|
21
|
+
{ scope: "test", name: "@platforma-open/demo.feature.test", path: "test" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const ctx = createRunContext({ blockVars: vars, modules });
|
|
25
|
+
|
|
26
|
+
describe("ensureWorkspaceModulePaths", () => {
|
|
27
|
+
// The root module ("") is discovered implicitly and is NEVER written
|
|
28
|
+
// to `packages:` — listing "." there would break turbo's task graph.
|
|
29
|
+
const nonRootModules = modules.filter((m) => m.path !== "");
|
|
30
|
+
|
|
31
|
+
test("writes a sorted packages list of non-root modules (root excluded)", () => {
|
|
32
|
+
const doc = parseYaml("packages: []\n");
|
|
33
|
+
withManagedYaml(doc, () => ensureWorkspaceModulePaths(), { ctx });
|
|
34
|
+
const json = doc.toJSON() as { packages: string[] };
|
|
35
|
+
expect(json.packages).toEqual(["block", "model", "test", "ui", "workflow"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("creates packages list if missing", () => {
|
|
39
|
+
const doc = parseYaml("# empty\n");
|
|
40
|
+
withManagedYaml(doc, () => ensureWorkspaceModulePaths(), { ctx });
|
|
41
|
+
const json = doc.toJSON() as { packages: string[] };
|
|
42
|
+
expect(json.packages.length).toBe(nonRootModules.length);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("idempotent — second run leaves the YAML output unchanged", () => {
|
|
46
|
+
const doc = parseYaml("packages: []\n");
|
|
47
|
+
withManagedYaml(doc, () => ensureWorkspaceModulePaths(), { ctx });
|
|
48
|
+
const once = stringifyYaml(doc);
|
|
49
|
+
withManagedYaml(doc, () => ensureWorkspaceModulePaths(), { ctx });
|
|
50
|
+
const twice = stringifyYaml(doc);
|
|
51
|
+
expect(twice).toBe(once);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Workspace-package classification — covers all 7 detection rules
|
|
2
|
+
// plus the block-scope override and the unclassified-error path.
|
|
3
|
+
|
|
4
|
+
import { describe, test, expect } from "vitest";
|
|
5
|
+
import { classifyOne, discoverModules, DiscoveryError, type WorkspacePackage } from "../discovery";
|
|
6
|
+
|
|
7
|
+
function pkg(path: string, data: WorkspacePackage["pkg"]): WorkspacePackage {
|
|
8
|
+
return { path, pkg: data };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("discovery — single-package classification", () => {
|
|
12
|
+
test("rule 1: software via block-software field", () => {
|
|
13
|
+
const wp = pkg("software", {
|
|
14
|
+
name: "x.software",
|
|
15
|
+
"block-software": {},
|
|
16
|
+
});
|
|
17
|
+
expect(classifyOne(wp, new Set())).toBe("software");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("rule 1 alt: software via package-builder + runenv dep", () => {
|
|
21
|
+
const wp = pkg("software-py", {
|
|
22
|
+
name: "x.software-py",
|
|
23
|
+
dependencies: {
|
|
24
|
+
"@platforma-sdk/package-builder": "catalog:",
|
|
25
|
+
"@platforma-open/runenv-python-3": "catalog:",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
expect(classifyOne(wp, new Set())).toBe("software");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("rule 2: workflow via workflow-tengo dep", () => {
|
|
32
|
+
const wp = pkg("workflow", {
|
|
33
|
+
name: "x.workflow",
|
|
34
|
+
dependencies: { "@platforma-sdk/workflow-tengo": "catalog:" },
|
|
35
|
+
});
|
|
36
|
+
expect(classifyOne(wp, new Set())).toBe("workflow");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("rule 3: ui via ui-vue dep and no main", () => {
|
|
40
|
+
const wp = pkg("ui", {
|
|
41
|
+
name: "x.ui",
|
|
42
|
+
dependencies: { "@platforma-sdk/ui-vue": "catalog:" },
|
|
43
|
+
});
|
|
44
|
+
expect(classifyOne(wp, new Set())).toBe("ui");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("rule 4: block via block-tools + sibling model/ui/workflow", () => {
|
|
48
|
+
const siblings = new Set(["x.model", "x.ui", "x.workflow"]);
|
|
49
|
+
const wp = pkg("block", {
|
|
50
|
+
name: "x.block",
|
|
51
|
+
dependencies: {
|
|
52
|
+
"@platforma-sdk/block-tools": "catalog:",
|
|
53
|
+
"x.model": "workspace:*",
|
|
54
|
+
"x.ui": "workspace:*",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
expect(classifyOne(wp, siblings)).toBe("block");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("rule 4 fallback: files=[index.d.ts,index.js] + block-tools", () => {
|
|
61
|
+
const wp = pkg("block", {
|
|
62
|
+
name: "x.block",
|
|
63
|
+
files: ["index.d.ts", "index.js"],
|
|
64
|
+
dependencies: { "@platforma-sdk/block-tools": "catalog:" },
|
|
65
|
+
});
|
|
66
|
+
expect(classifyOne(wp, new Set())).toBe("block");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("rule 5: model via model dep + main", () => {
|
|
70
|
+
const wp = pkg("model", {
|
|
71
|
+
name: "x.model",
|
|
72
|
+
main: "dist/index.js",
|
|
73
|
+
dependencies: { "@platforma-sdk/model": "catalog:" },
|
|
74
|
+
});
|
|
75
|
+
expect(classifyOne(wp, new Set())).toBe("model");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("rule 6: test via @platforma-sdk/test dep", () => {
|
|
79
|
+
const wp = pkg("test", {
|
|
80
|
+
name: "x.test",
|
|
81
|
+
devDependencies: { "@platforma-sdk/test": "catalog:" },
|
|
82
|
+
});
|
|
83
|
+
expect(classifyOne(wp, new Set())).toBe("test");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("rule 7: root as catch-all", () => {
|
|
87
|
+
const wp = pkg("", { name: "x" });
|
|
88
|
+
expect(classifyOne(wp, new Set())).toBe("root");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("override: block-scope wins over detection", () => {
|
|
92
|
+
const wp = pkg("strange-dir", {
|
|
93
|
+
name: "x.strange",
|
|
94
|
+
"block-scope": "software",
|
|
95
|
+
dependencies: { "@platforma-sdk/workflow-tengo": "catalog:" },
|
|
96
|
+
});
|
|
97
|
+
expect(classifyOne(wp, new Set())).toBe("software");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("override: bad value throws DiscoveryError", () => {
|
|
101
|
+
const wp = pkg("x", {
|
|
102
|
+
name: "x",
|
|
103
|
+
"block-scope": "nope",
|
|
104
|
+
});
|
|
105
|
+
expect(() => classifyOne(wp, new Set())).toThrow(DiscoveryError);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("unclassified non-root throws DiscoveryError", () => {
|
|
109
|
+
const wp = pkg("orphan", { name: "x.orphan" });
|
|
110
|
+
expect(() => discoverModules([wp])).toThrow(DiscoveryError);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("discovery — full workspace", () => {
|
|
115
|
+
test("classifies an example workspace", () => {
|
|
116
|
+
const packages: WorkspacePackage[] = [
|
|
117
|
+
pkg("", { name: "x" }),
|
|
118
|
+
pkg("model", {
|
|
119
|
+
name: "x.model",
|
|
120
|
+
main: "dist/index.js",
|
|
121
|
+
dependencies: { "@platforma-sdk/model": "catalog:" },
|
|
122
|
+
}),
|
|
123
|
+
pkg("ui", {
|
|
124
|
+
name: "x.ui",
|
|
125
|
+
dependencies: { "@platforma-sdk/ui-vue": "catalog:" },
|
|
126
|
+
}),
|
|
127
|
+
pkg("workflow", {
|
|
128
|
+
name: "x.workflow",
|
|
129
|
+
dependencies: { "@platforma-sdk/workflow-tengo": "catalog:" },
|
|
130
|
+
}),
|
|
131
|
+
pkg("test", {
|
|
132
|
+
name: "x.test",
|
|
133
|
+
devDependencies: { "@platforma-sdk/test": "catalog:" },
|
|
134
|
+
}),
|
|
135
|
+
pkg("block", {
|
|
136
|
+
name: "x.block",
|
|
137
|
+
dependencies: {
|
|
138
|
+
"@platforma-sdk/block-tools": "catalog:",
|
|
139
|
+
"x.model": "workspace:*",
|
|
140
|
+
"x.ui": "workspace:*",
|
|
141
|
+
"x.workflow": "workspace:*",
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
pkg("software-py", {
|
|
145
|
+
name: "x.software-py",
|
|
146
|
+
dependencies: {
|
|
147
|
+
"@platforma-sdk/package-builder": "catalog:",
|
|
148
|
+
"@platforma-open/runenv-python-3": "catalog:",
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
];
|
|
152
|
+
const mods = discoverModules(packages);
|
|
153
|
+
const byScope = Object.fromEntries(mods.map((m) => [m.scope, m]));
|
|
154
|
+
expect(Object.keys(byScope).sort()).toEqual(
|
|
155
|
+
["block", "model", "root", "software", "test", "ui", "workflow"].sort(),
|
|
156
|
+
);
|
|
157
|
+
expect(byScope.root!.path).toBe("");
|
|
158
|
+
expect(byScope.software!.path).toBe("software-py");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// In-memory FileSystem round-trip.
|
|
2
|
+
//
|
|
3
|
+
// Covers read/write, exists for files and synthesised directories,
|
|
4
|
+
// recursive listing, move (file + directory), and recursive delete.
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect } from "vitest";
|
|
7
|
+
import { MemoryFileSystem } from "../fs/memory";
|
|
8
|
+
|
|
9
|
+
describe("MemoryFileSystem", () => {
|
|
10
|
+
test("write + read + exists", async () => {
|
|
11
|
+
const fs = new MemoryFileSystem();
|
|
12
|
+
expect(fs.exists("a.json")).toBe(false);
|
|
13
|
+
fs.write("a.json", "{}");
|
|
14
|
+
expect(fs.read("a.json")).toBe("{}");
|
|
15
|
+
expect(fs.exists("a.json")).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("directory exists when a child file is present", async () => {
|
|
19
|
+
const fs = new MemoryFileSystem({ "model/package.json": "{}" });
|
|
20
|
+
expect(fs.exists("model")).toBe(true);
|
|
21
|
+
expect(fs.exists("model/")).toBe(true);
|
|
22
|
+
expect(fs.exists("ui")).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("list recurses", async () => {
|
|
26
|
+
const fs = new MemoryFileSystem({
|
|
27
|
+
"a.txt": "1",
|
|
28
|
+
"b/c.txt": "2",
|
|
29
|
+
"b/d/e.txt": "3",
|
|
30
|
+
});
|
|
31
|
+
expect(fs.list("")).toEqual(["a.txt", "b/c.txt", "b/d/e.txt"]);
|
|
32
|
+
expect(fs.list("b")).toEqual(["b/c.txt", "b/d/e.txt"]);
|
|
33
|
+
expect(fs.list("missing")).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("move renames directory subtrees", async () => {
|
|
37
|
+
const fs = new MemoryFileSystem({
|
|
38
|
+
"test/a.ts": "x",
|
|
39
|
+
"test/b/c.ts": "y",
|
|
40
|
+
"keep.txt": "z",
|
|
41
|
+
});
|
|
42
|
+
fs.move("test", "test-legacy");
|
|
43
|
+
expect(fs.exists("test")).toBe(false);
|
|
44
|
+
expect(fs.read("test-legacy/a.ts")).toBe("x");
|
|
45
|
+
expect(fs.read("test-legacy/b/c.ts")).toBe("y");
|
|
46
|
+
expect(fs.read("keep.txt")).toBe("z");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("delete is recursive and idempotent", async () => {
|
|
50
|
+
const fs = new MemoryFileSystem({
|
|
51
|
+
"x/y.ts": "1",
|
|
52
|
+
"x/z.ts": "2",
|
|
53
|
+
});
|
|
54
|
+
fs.delete("x");
|
|
55
|
+
expect(fs.exists("x")).toBe(false);
|
|
56
|
+
fs.delete("x"); // no-op
|
|
57
|
+
expect(fs.exists("x")).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { matchesGlob } from "../glob";
|
|
3
|
+
|
|
4
|
+
describe("matchesGlob", () => {
|
|
5
|
+
test("`*` matches within a segment, not across `/`", () => {
|
|
6
|
+
expect(matchesGlob("src/*.test.ts", "src/foo.test.ts")).toBe(true);
|
|
7
|
+
expect(matchesGlob("src/*.test.ts", "src/foo.ts")).toBe(false);
|
|
8
|
+
// `*` does not cross a slash
|
|
9
|
+
expect(matchesGlob("src/*.test.ts", "src/sub/foo.test.ts")).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("`**/` matches any depth, including zero segments", () => {
|
|
13
|
+
expect(matchesGlob("src/**/*.test.ts", "src/foo.test.ts")).toBe(true);
|
|
14
|
+
expect(matchesGlob("src/**/*.test.ts", "src/sub/foo.test.ts")).toBe(true);
|
|
15
|
+
expect(matchesGlob("src/**/*.test.ts", "src/a/b/c/foo.test.ts")).toBe(true);
|
|
16
|
+
expect(matchesGlob("src/**/*.test.ts", "src/foo.ts")).toBe(false);
|
|
17
|
+
expect(matchesGlob("src/**/*.test.ts", "other/foo.test.ts")).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("module-relative prefixes compose (as the runner resolves them)", () => {
|
|
21
|
+
expect(matchesGlob("model/src/**/*.test.ts", "model/src/label.test.ts")).toBe(true);
|
|
22
|
+
expect(matchesGlob("model/src/**/*.test.ts", "ui/src/label.test.ts")).toBe(false);
|
|
23
|
+
expect(matchesGlob("workflow/src/**/*.test.ts", "workflow/src/test/columns.test.ts")).toBe(
|
|
24
|
+
true,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("literal dots are not wildcards", () => {
|
|
29
|
+
expect(matchesGlob("a.b", "axb")).toBe(false);
|
|
30
|
+
expect(matchesGlob("a.b", "a.b")).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("`?` matches exactly one non-slash char", () => {
|
|
34
|
+
expect(matchesGlob("src/?.ts", "src/a.ts")).toBe(true);
|
|
35
|
+
expect(matchesGlob("src/?.ts", "src/ab.ts")).toBe(false);
|
|
36
|
+
expect(matchesGlob("src/?.ts", "src//.ts")).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("whole-path anchored (no partial matches)", () => {
|
|
40
|
+
expect(matchesGlob("src/*.ts", "x/src/foo.ts")).toBe(false);
|
|
41
|
+
expect(matchesGlob("src/*.ts", "src/foo.ts.bak")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|