@platforma-sdk/block-tools 2.10.17 → 2.10.18
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 +13 -13
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +574 -541
- package/dist/cli.mjs.map +1 -1
- package/dist/structure/engine/api.d.ts +1 -1
- package/dist/structure/engine/content-rules.d.ts +17 -0
- package/dist/structure/engine/registry-client.d.ts +30 -2
- package/dist/structure/engine/runner.d.ts +6 -0
- package/dist/structure/rules/root-pnpm-workspace.d.ts +13 -0
- package/dist/v2/registry/schema_public.d.ts +3600 -3600
- package/package.json +6 -6
- package/src/structure/__tests__/model-package-json.snapshot.test.ts +1 -2
- package/src/structure/cli/run-structure.ts +12 -2
- package/src/structure/engine/__tests__/content-rules/pin-catalog-to-dependency-of.test.ts +65 -0
- package/src/structure/engine/__tests__/derived-pin-lookup.test.ts +54 -0
- package/src/structure/engine/api.ts +1 -0
- package/src/structure/engine/content-rules.ts +29 -0
- package/src/structure/engine/registry-client.ts +116 -2
- package/src/structure/engine/runner.ts +18 -2
- package/src/structure/init-block-constructor.ts +16 -3
- package/src/structure/rules/model-package-json.ts +35 -16
- package/src/structure/rules/root-catalog-bump.ts +12 -1
- package/src/structure/rules/root-package-json.ts +7 -5
- package/src/structure/rules/root-pnpm-workspace.ts +24 -8
- package/src/structure/rules/ui-package-json.ts +27 -13
- package/src/structure/rules/workflow-package-json.ts +25 -11
- package/src/structure/rules/workflow.ts +21 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platforma-sdk/block-tools",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.18",
|
|
4
4
|
"description": "Utility to manipulate Platforma Blocks and Block Registry",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
"undici": "~7.16.0",
|
|
34
34
|
"yaml": "^2.8.0",
|
|
35
35
|
"zod": "~3.25.76",
|
|
36
|
+
"@milaboratories/pl-http": "1.2.4",
|
|
37
|
+
"@milaboratories/pl-model-middle-layer": "1.30.6",
|
|
36
38
|
"@milaboratories/pl-model-common": "1.46.1",
|
|
37
|
-
"@milaboratories/pl-model-backend": "1.4.7",
|
|
38
39
|
"@milaboratories/resolve-helper": "1.1.3",
|
|
39
|
-
"@milaboratories/pl-model-
|
|
40
|
+
"@milaboratories/pl-model-backend": "1.4.7",
|
|
40
41
|
"@milaboratories/ts-helpers": "1.8.3",
|
|
41
|
-
"@milaboratories/pl-http": "1.2.4",
|
|
42
42
|
"@milaboratories/ts-helpers-oclif": "1.1.42",
|
|
43
43
|
"@platforma-sdk/blocks-deps-updater": "2.2.0"
|
|
44
44
|
},
|
|
@@ -56,10 +56,10 @@
|
|
|
56
56
|
"vite": "^8.0.6",
|
|
57
57
|
"vite-plugin-dts": "^4.5.3",
|
|
58
58
|
"vitest": "^4.1.3",
|
|
59
|
-
"@milaboratories/build-configs": "2.0.0",
|
|
60
59
|
"@milaboratories/oclif-index": "1.1.1",
|
|
61
60
|
"@milaboratories/ts-configs": "1.2.3",
|
|
62
|
-
"@milaboratories/ts-builder": "1.5.
|
|
61
|
+
"@milaboratories/ts-builder": "1.5.2",
|
|
62
|
+
"@milaboratories/build-configs": "2.0.0"
|
|
63
63
|
},
|
|
64
64
|
"oclif": {
|
|
65
65
|
"bin": "block-tools",
|
|
@@ -34,8 +34,7 @@ const EXPECTED_MODEL_PACKAGE_JSON = `{
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@milaboratories/ts-builder": "catalog:",
|
|
36
36
|
"@milaboratories/ts-configs": "catalog:",
|
|
37
|
-
"@platforma-sdk/block-tools": "catalog:"
|
|
38
|
-
"vitest": "catalog:"
|
|
37
|
+
"@platforma-sdk/block-tools": "catalog:"
|
|
39
38
|
},
|
|
40
39
|
"peerDependencies": {
|
|
41
40
|
"@types/node": "*",
|
|
@@ -11,8 +11,12 @@ import { NodeTemplateProvider } from "../engine/templates";
|
|
|
11
11
|
import { run as engineRun, type Change } from "../engine/runner";
|
|
12
12
|
import { discoverRunContext } from "../engine/discovery-fs";
|
|
13
13
|
import { STRUCTURE } from "../structure-definition";
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
matchesBumpPattern,
|
|
16
|
+
buildRegistryLookupForNames,
|
|
17
|
+
buildDerivedPinLookupForPins,
|
|
18
|
+
} from "../engine/registry-client";
|
|
19
|
+
import { SDK_CATALOG_PACKAGES, DERIVED_CATALOG_PINS } from "../rules/root-pnpm-workspace";
|
|
16
20
|
|
|
17
21
|
export type StructureMode = "check" | "refresh";
|
|
18
22
|
|
|
@@ -60,10 +64,16 @@ export async function runStructureForPath(input: RunStructureInput): Promise<Run
|
|
|
60
64
|
});
|
|
61
65
|
|
|
62
66
|
const registryLookup = input.updateDepsOnly ? await buildRegistryLookup() : undefined;
|
|
67
|
+
// Derived catalog pins (e.g. vue ← ui-vue's declared dep) resolve on the
|
|
68
|
+
// same prefetch budget as the SDK latest bump; absent on a default refresh.
|
|
69
|
+
const derivedPinLookup = input.updateDepsOnly
|
|
70
|
+
? await buildDerivedPinLookupForPins(DERIVED_CATALOG_PINS)
|
|
71
|
+
: undefined;
|
|
63
72
|
|
|
64
73
|
const result = engineRun(STRUCTURE, fs, ctx, {
|
|
65
74
|
templates,
|
|
66
75
|
registryLookup,
|
|
76
|
+
derivedPinLookup,
|
|
67
77
|
// Fresh re-discovery for the post-run recheck (default refresh only)
|
|
68
78
|
// — re-reads the just-written tree so a rename that shifted the
|
|
69
79
|
// module map is observed.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { pinCatalogToDependencyOf, withManagedYaml } from "../../content-rules";
|
|
3
|
+
import { parseYaml, stringifyYaml } from "../../parsers/yaml";
|
|
4
|
+
|
|
5
|
+
// The builder is sync: it reads a value the runner pre-resolved + validated
|
|
6
|
+
// into `getDerivedDependencyPin`. These tests inject that accessor directly.
|
|
7
|
+
const lookup =
|
|
8
|
+
(m: Record<string, string>) =>
|
|
9
|
+
(entry: string): string | undefined =>
|
|
10
|
+
m[entry];
|
|
11
|
+
|
|
12
|
+
describe("pinCatalogToDependencyOf (mocked derived-pin lookup)", () => {
|
|
13
|
+
test("overwrites a present LOOSE entry with the derived exact version", () => {
|
|
14
|
+
const doc = parseYaml("catalog:\n vue: ^3.5.24\n");
|
|
15
|
+
withManagedYaml(doc, () => pinCatalogToDependencyOf("vue", { of: "@platforma-sdk/ui-vue" }), {
|
|
16
|
+
getDerivedDependencyPin: lookup({ vue: "3.5.24" }),
|
|
17
|
+
});
|
|
18
|
+
const cat = (doc.toJSON() as { catalog: Record<string, string> }).catalog;
|
|
19
|
+
expect(cat.vue).toBe("3.5.24");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("creates an absent entry", () => {
|
|
23
|
+
const doc = parseYaml("catalog:\n unrelated: 1.0.0\n");
|
|
24
|
+
withManagedYaml(doc, () => pinCatalogToDependencyOf("vue", { of: "@platforma-sdk/ui-vue" }), {
|
|
25
|
+
getDerivedDependencyPin: lookup({ vue: "3.5.24" }),
|
|
26
|
+
});
|
|
27
|
+
const cat = (doc.toJSON() as { catalog: Record<string, string> }).catalog;
|
|
28
|
+
expect(cat.vue).toBe("3.5.24");
|
|
29
|
+
expect(cat.unrelated).toBe("1.0.0");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("no-op when no derived-pin lookup is provided (default refresh)", () => {
|
|
33
|
+
const doc = parseYaml("catalog:\n vue: ^3.5.24\n");
|
|
34
|
+
withManagedYaml(doc, () => pinCatalogToDependencyOf("vue", { of: "@platforma-sdk/ui-vue" }));
|
|
35
|
+
const cat = (doc.toJSON() as { catalog: Record<string, string> }).catalog;
|
|
36
|
+
expect(cat.vue).toBe("^3.5.24");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("no-op when the entry was not prefetched (lookup returns undefined)", () => {
|
|
40
|
+
const doc = parseYaml("catalog:\n vue: ^3.5.24\n");
|
|
41
|
+
withManagedYaml(doc, () => pinCatalogToDependencyOf("vue", { of: "@platforma-sdk/ui-vue" }), {
|
|
42
|
+
getDerivedDependencyPin: lookup({ other: "1.0.0" }),
|
|
43
|
+
});
|
|
44
|
+
const cat = (doc.toJSON() as { catalog: Record<string, string> }).catalog;
|
|
45
|
+
expect(cat.vue).toBe("^3.5.24");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("idempotent — second run produces the same YAML", () => {
|
|
49
|
+
const doc = parseYaml("catalog:\n vue: ^3.5.24\n");
|
|
50
|
+
const opts = { getDerivedDependencyPin: lookup({ vue: "3.5.24" }) };
|
|
51
|
+
withManagedYaml(
|
|
52
|
+
doc,
|
|
53
|
+
() => pinCatalogToDependencyOf("vue", { of: "@platforma-sdk/ui-vue" }),
|
|
54
|
+
opts,
|
|
55
|
+
);
|
|
56
|
+
const once = stringifyYaml(doc);
|
|
57
|
+
withManagedYaml(
|
|
58
|
+
doc,
|
|
59
|
+
() => pinCatalogToDependencyOf("vue", { of: "@platforma-sdk/ui-vue" }),
|
|
60
|
+
opts,
|
|
61
|
+
);
|
|
62
|
+
const twice = stringifyYaml(doc);
|
|
63
|
+
expect(twice).toBe(once);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { buildDerivedPinLookupForPins, createMockRegistryClient } from "../registry-client";
|
|
3
|
+
|
|
4
|
+
describe("buildDerivedPinLookupForPins (mocked registry)", () => {
|
|
5
|
+
test("resolves the catalog entry to the source's declared exact dep version", async () => {
|
|
6
|
+
const client = createMockRegistryClient(
|
|
7
|
+
{ "@platforma-sdk/ui-vue": "1.79.6" },
|
|
8
|
+
{ "@platforma-sdk/ui-vue@1.79.6": { vue: "3.5.24" } },
|
|
9
|
+
);
|
|
10
|
+
const lookup = await buildDerivedPinLookupForPins(
|
|
11
|
+
[{ entry: "vue", of: "@platforma-sdk/ui-vue" }],
|
|
12
|
+
client,
|
|
13
|
+
);
|
|
14
|
+
expect(lookup("vue")).toBe("3.5.24");
|
|
15
|
+
expect(lookup("unrelated")).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("reads from a pinned source version when `ofVersion` is given", async () => {
|
|
19
|
+
const client = createMockRegistryClient(
|
|
20
|
+
{ "@platforma-sdk/ui-vue": "9.9.9" }, // latest — must NOT be used
|
|
21
|
+
{ "@platforma-sdk/ui-vue@1.79.3": { vue: "3.5.20" } },
|
|
22
|
+
);
|
|
23
|
+
const lookup = await buildDerivedPinLookupForPins(
|
|
24
|
+
[{ entry: "vue", of: "@platforma-sdk/ui-vue", ofVersion: "1.79.3" }],
|
|
25
|
+
client,
|
|
26
|
+
);
|
|
27
|
+
expect(lookup("vue")).toBe("3.5.20");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("empty pin list → lookup returns undefined for everything", async () => {
|
|
31
|
+
const lookup = await buildDerivedPinLookupForPins([], createMockRegistryClient({}));
|
|
32
|
+
expect(lookup("vue")).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("policy (b): throws when the source declares a NON-EXACT range", async () => {
|
|
36
|
+
const client = createMockRegistryClient(
|
|
37
|
+
{ "@platforma-sdk/ui-vue": "1.79.6" },
|
|
38
|
+
{ "@platforma-sdk/ui-vue@1.79.6": { vue: "^3.5.24" } },
|
|
39
|
+
);
|
|
40
|
+
await expect(
|
|
41
|
+
buildDerivedPinLookupForPins([{ entry: "vue", of: "@platforma-sdk/ui-vue" }], client),
|
|
42
|
+
).rejects.toThrow(/not an exact version/i);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("policy (b): throws when the source declares no such dependency", async () => {
|
|
46
|
+
const client = createMockRegistryClient(
|
|
47
|
+
{ "@platforma-sdk/ui-vue": "1.79.6" },
|
|
48
|
+
{ "@platforma-sdk/ui-vue@1.79.6": { react: "19.0.0" } },
|
|
49
|
+
);
|
|
50
|
+
await expect(
|
|
51
|
+
buildDerivedPinLookupForPins([{ entry: "vue", of: "@platforma-sdk/ui-vue" }], client),
|
|
52
|
+
).rejects.toThrow(/declares no/i);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -83,6 +83,9 @@ type YamlState = {
|
|
|
83
83
|
triggerContext?: TriggerContext;
|
|
84
84
|
/** Synchronous accessor for prefetched npm latest versions. */
|
|
85
85
|
getLatestVersion?: (packageName: string) => string | undefined;
|
|
86
|
+
/** Synchronous accessor for prefetched derived catalog pins
|
|
87
|
+
* (`catalogEntry → exact version`); see `pinCatalogToDependencyOf`. */
|
|
88
|
+
getDerivedDependencyPin?: (catalogEntry: string) => string | undefined;
|
|
86
89
|
};
|
|
87
90
|
|
|
88
91
|
type LinesState = {
|
|
@@ -177,6 +180,9 @@ export type WithManagedYamlOpts = WithManagedOpts & {
|
|
|
177
180
|
/** Sync accessor for prefetched latest versions. Required for
|
|
178
181
|
* `ensureCatalogLatest` to take effect. */
|
|
179
182
|
getLatestVersion?: (packageName: string) => string | undefined;
|
|
183
|
+
/** Sync accessor for prefetched derived catalog pins. Required for
|
|
184
|
+
* `pinCatalogToDependencyOf` to take effect. */
|
|
185
|
+
getDerivedDependencyPin?: (catalogEntry: string) => string | undefined;
|
|
180
186
|
};
|
|
181
187
|
|
|
182
188
|
/** Engine-internal: run `body` with `doc` as the active YAML state (for
|
|
@@ -193,6 +199,7 @@ export function withManagedYaml(
|
|
|
193
199
|
ctx: opts.ctx,
|
|
194
200
|
triggerContext: opts.triggerContext,
|
|
195
201
|
getLatestVersion: opts.getLatestVersion,
|
|
202
|
+
getDerivedDependencyPin: opts.getDerivedDependencyPin,
|
|
196
203
|
};
|
|
197
204
|
try {
|
|
198
205
|
body();
|
|
@@ -579,6 +586,28 @@ export function ensureCatalogLatest(name: string): void {
|
|
|
579
586
|
state.doc.setIn(["catalog", name], latest);
|
|
580
587
|
}
|
|
581
588
|
|
|
589
|
+
/** Pin `catalog.<catalogEntry>` to the EXACT version that package `opts.of`
|
|
590
|
+
* declares for `catalogEntry` in its own manifest (read at npm latest, or at
|
|
591
|
+
* `opts.ofVersion` if given). The value is resolved by the runner's
|
|
592
|
+
* prefetched `getDerivedDependencyPin` accessor (the caller pre-resolves and
|
|
593
|
+
* validates — exact-version-only — at prefetch time; no network here).
|
|
594
|
+
* OVERWRITES like `pinCatalogTo` (not add-if-absent): a pre-existing loose
|
|
595
|
+
* entry (`vue: ^3.5.24`) is tightened to the exact derived version. No-op
|
|
596
|
+
* when no derived-pin lookup is available — default refresh/check pass none,
|
|
597
|
+
* leaving the entry exactly as the lockfile has it; the resolution leaves
|
|
598
|
+
* live in the `onInitOrUpdate` frame. */
|
|
599
|
+
export function pinCatalogToDependencyOf(
|
|
600
|
+
catalogEntry: string,
|
|
601
|
+
_opts: { of: string; ofVersion?: string },
|
|
602
|
+
): void {
|
|
603
|
+
const state = requireYaml("pinCatalogToDependencyOf");
|
|
604
|
+
if (!state.getDerivedDependencyPin) return;
|
|
605
|
+
const version = state.getDerivedDependencyPin(catalogEntry);
|
|
606
|
+
if (version === undefined) return;
|
|
607
|
+
if (readCatalogString(state, catalogEntry) === version) return;
|
|
608
|
+
state.doc.setIn(["catalog", catalogEntry], version);
|
|
609
|
+
}
|
|
610
|
+
|
|
582
611
|
/** Remove `catalog.<name>` if present; no-op when absent. */
|
|
583
612
|
export function ensureCatalogAbsent(name: string): void {
|
|
584
613
|
const state = requireYaml("ensureCatalogAbsent");
|
|
@@ -12,6 +12,15 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
12
12
|
|
|
13
13
|
export interface RegistryClient {
|
|
14
14
|
getLatestVersion(packageName: string): Promise<string>;
|
|
15
|
+
/** Read the version `packageName@version` declares for `depName` in its
|
|
16
|
+
* published manifest. `version` may be "latest" (resolved via the
|
|
17
|
+
* dist-tags). Looks in `dependencies` first, then `peerDependencies`.
|
|
18
|
+
* Returns `undefined` when the dep is declared in neither section. */
|
|
19
|
+
getDeclaredDependency(
|
|
20
|
+
packageName: string,
|
|
21
|
+
version: string,
|
|
22
|
+
depName: string,
|
|
23
|
+
): Promise<string | undefined>;
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
const RETRY_COUNT = 2;
|
|
@@ -60,6 +69,14 @@ async function fetchWithRetry(url: string): Promise<Response> {
|
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
71
|
|
|
72
|
+
/** Shape of the per-version manifest doc fetched from
|
|
73
|
+
* `https://registry.npmjs.org/<pkg>/<version>` (only the dep sections are
|
|
74
|
+
* read). */
|
|
75
|
+
type PackageManifest = {
|
|
76
|
+
dependencies?: Record<string, string>;
|
|
77
|
+
peerDependencies?: Record<string, string>;
|
|
78
|
+
};
|
|
79
|
+
|
|
63
80
|
/** Live npm registry client. Hits the network on each call. */
|
|
64
81
|
export function createRealRegistryClient(): RegistryClient {
|
|
65
82
|
return {
|
|
@@ -74,11 +91,33 @@ export function createRealRegistryClient(): RegistryClient {
|
|
|
74
91
|
}
|
|
75
92
|
return latest;
|
|
76
93
|
},
|
|
94
|
+
async getDeclaredDependency(
|
|
95
|
+
packageName: string,
|
|
96
|
+
version: string,
|
|
97
|
+
depName: string,
|
|
98
|
+
): Promise<string | undefined> {
|
|
99
|
+
const res = await fetchWithRetry(`https://registry.npmjs.org/${packageName}/${version}`);
|
|
100
|
+
const manifest = (await res.json()) as PackageManifest;
|
|
101
|
+
return manifest.dependencies?.[depName] ?? manifest.peerDependencies?.[depName];
|
|
102
|
+
},
|
|
77
103
|
};
|
|
78
104
|
}
|
|
79
105
|
|
|
80
|
-
/** In-memory client backed by a fixed `name → version` map.
|
|
81
|
-
|
|
106
|
+
/** In-memory client backed by a fixed `name → version` map. The optional
|
|
107
|
+
* `manifests` map (`pkg@version` → declared deps) backs
|
|
108
|
+
* `getDeclaredDependency`; "latest" keys resolve via the `versions` map. */
|
|
109
|
+
export function createMockRegistryClient(
|
|
110
|
+
versions: Record<string, string>,
|
|
111
|
+
manifests: Record<string, Record<string, string>> = {},
|
|
112
|
+
): RegistryClient {
|
|
113
|
+
function resolveVersion(packageName: string, version: string): string {
|
|
114
|
+
if (version !== "latest") return version;
|
|
115
|
+
const v = versions[packageName];
|
|
116
|
+
if (v === undefined) {
|
|
117
|
+
throw new Error(`mock registry: no version configured for '${packageName}'`);
|
|
118
|
+
}
|
|
119
|
+
return v;
|
|
120
|
+
}
|
|
82
121
|
return {
|
|
83
122
|
async getLatestVersion(packageName: string): Promise<string> {
|
|
84
123
|
const v = versions[packageName];
|
|
@@ -87,6 +126,18 @@ export function createMockRegistryClient(versions: Record<string, string>): Regi
|
|
|
87
126
|
}
|
|
88
127
|
return v;
|
|
89
128
|
},
|
|
129
|
+
async getDeclaredDependency(
|
|
130
|
+
packageName: string,
|
|
131
|
+
version: string,
|
|
132
|
+
depName: string,
|
|
133
|
+
): Promise<string | undefined> {
|
|
134
|
+
const resolved = resolveVersion(packageName, version);
|
|
135
|
+
const deps = manifests[`${packageName}@${resolved}`];
|
|
136
|
+
if (deps === undefined) {
|
|
137
|
+
throw new Error(`mock registry: no manifest configured for '${packageName}@${resolved}'`);
|
|
138
|
+
}
|
|
139
|
+
return deps[depName];
|
|
140
|
+
},
|
|
90
141
|
};
|
|
91
142
|
}
|
|
92
143
|
|
|
@@ -138,3 +189,66 @@ export async function buildRegistryLookupForNames(
|
|
|
138
189
|
const resolved = await prefetchLatestVersions(client, names);
|
|
139
190
|
return makeSyncLookup(resolved);
|
|
140
191
|
}
|
|
192
|
+
|
|
193
|
+
/** A catalog entry whose version is DERIVED from the version another
|
|
194
|
+
* package declares for it. `entry` is the catalog key (== the dep name in
|
|
195
|
+
* the source manifest); `of` is the package whose manifest is read; the
|
|
196
|
+
* optional `ofVersion` pins which release of `of` to read (default: npm
|
|
197
|
+
* latest, the same release the catalog bump pins `of` to). Consumed by
|
|
198
|
+
* `pinCatalogToDependencyOf`. */
|
|
199
|
+
export type DerivedCatalogPin = {
|
|
200
|
+
entry: string;
|
|
201
|
+
of: string;
|
|
202
|
+
ofVersion?: string;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/** True iff `version` is a bare exact `x.y.z(-prerelease)?` — no range
|
|
206
|
+
* modifier (`^`, `~`, `>=`, `*`, `||`, ` - `, `x`, workspace:, etc.). */
|
|
207
|
+
function isExactVersion(version: string): boolean {
|
|
208
|
+
return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Prefetch every derived pin and build the sync lookup the runner passes
|
|
212
|
+
* to `pinCatalogToDependencyOf` (`entry → exact version`). Reads
|
|
213
|
+
* `pin.of`'s manifest (at `pin.ofVersion`, default npm latest) and the
|
|
214
|
+
* version it declares for `pin.entry`. Policy (b): THROWS at prefetch time
|
|
215
|
+
* if the source declares the dep as a non-exact (ranged) version or does
|
|
216
|
+
* not declare it at all — a derived pin must resolve to an exact version
|
|
217
|
+
* or it is a configuration error. Empty list → a lookup that resolves
|
|
218
|
+
* nothing (no network). A registry failure REJECTS. `client` defaults to
|
|
219
|
+
* the live npm client; tests inject a mock. */
|
|
220
|
+
export async function buildDerivedPinLookupForPins(
|
|
221
|
+
pins: readonly DerivedCatalogPin[],
|
|
222
|
+
client: RegistryClient = createRealRegistryClient(),
|
|
223
|
+
): Promise<(entry: string) => string | undefined> {
|
|
224
|
+
if (pins.length === 0) return () => undefined;
|
|
225
|
+
const entries = await Promise.all(
|
|
226
|
+
pins.map(async (pin) => {
|
|
227
|
+
const declared = await client.getDeclaredDependency(
|
|
228
|
+
pin.of,
|
|
229
|
+
pin.ofVersion ?? "latest",
|
|
230
|
+
pin.entry,
|
|
231
|
+
);
|
|
232
|
+
if (declared === undefined) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`derived catalog pin '${pin.entry}': package '${pin.of}'` +
|
|
235
|
+
`${pin.ofVersion ? `@${pin.ofVersion}` : " (latest)"} declares no ` +
|
|
236
|
+
`dependency '${pin.entry}'. A derived pin requires the source to ` +
|
|
237
|
+
`declare the dep as an exact version.`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (!isExactVersion(declared)) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`derived catalog pin '${pin.entry}': package '${pin.of}'` +
|
|
243
|
+
`${pin.ofVersion ? `@${pin.ofVersion}` : " (latest)"} declares ` +
|
|
244
|
+
`'${pin.entry}: ${declared}', which is not an exact version. ` +
|
|
245
|
+
`A derived pin requires an exact ('x.y.z') source dependency so ` +
|
|
246
|
+
`the catalog can pin to it precisely.`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return [pin.entry, declared] as const;
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
const resolved = new Map(entries);
|
|
253
|
+
return (entry) => resolved.get(entry);
|
|
254
|
+
}
|
|
@@ -83,6 +83,12 @@ export type RunOptions = {
|
|
|
83
83
|
* and passes the resulting sync map here; unit tests pass a mock.
|
|
84
84
|
* Absent → `ensureCatalogLatest` is a no-op (default refresh). */
|
|
85
85
|
registryLookup?: (packageName: string) => string | undefined;
|
|
86
|
+
/** Sync accessor for prefetched derived catalog pins
|
|
87
|
+
* (`catalogEntry → exact version`), consumed by `pinCatalogToDependencyOf`
|
|
88
|
+
* inside `onInitOrUpdate` managed bodies. The CLI / init constructor
|
|
89
|
+
* prefetches + validates these before the run (network). Absent →
|
|
90
|
+
* `pinCatalogToDependencyOf` is a no-op (default refresh). */
|
|
91
|
+
derivedPinLookup?: (catalogEntry: string) => string | undefined;
|
|
86
92
|
};
|
|
87
93
|
|
|
88
94
|
export class RecheckError extends Error {
|
|
@@ -189,6 +195,7 @@ function runManagedBody(
|
|
|
189
195
|
body: ManagedBody,
|
|
190
196
|
tctx: TriggerContext,
|
|
191
197
|
registryLookup: ((packageName: string) => string | undefined) | undefined,
|
|
198
|
+
derivedPinLookup: ((catalogEntry: string) => string | undefined) | undefined,
|
|
192
199
|
): string {
|
|
193
200
|
const base = path.split("/").pop() ?? path;
|
|
194
201
|
if (path.endsWith(".json")) {
|
|
@@ -203,7 +210,11 @@ function runManagedBody(
|
|
|
203
210
|
}
|
|
204
211
|
if (path.endsWith(".yaml") || path.endsWith(".yml")) {
|
|
205
212
|
const doc = parseYaml(raw);
|
|
206
|
-
withManagedYaml(doc, body, {
|
|
213
|
+
withManagedYaml(doc, body, {
|
|
214
|
+
triggerContext: tctx,
|
|
215
|
+
getLatestVersion: registryLookup,
|
|
216
|
+
getDerivedDependencyPin: derivedPinLookup,
|
|
217
|
+
});
|
|
207
218
|
return stringifyYaml(doc);
|
|
208
219
|
}
|
|
209
220
|
if (base === ".gitignore" || base.endsWith(".gitignore")) {
|
|
@@ -371,6 +382,7 @@ function applyManaged(
|
|
|
371
382
|
tctx: TriggerContext,
|
|
372
383
|
templates: TemplateProvider | undefined,
|
|
373
384
|
registryLookup: ((packageName: string) => string | undefined) | undefined,
|
|
385
|
+
derivedPinLookup: ((catalogEntry: string) => string | undefined) | undefined,
|
|
374
386
|
isSdkInternal: boolean,
|
|
375
387
|
): Change | undefined {
|
|
376
388
|
if (item.leaf.kind !== "managed") return undefined;
|
|
@@ -388,7 +400,7 @@ function applyManaged(
|
|
|
388
400
|
beforeRaw = fs.read(path);
|
|
389
401
|
}
|
|
390
402
|
|
|
391
|
-
const after = runManagedBody(path, beforeRaw, leaf.body, tctx, registryLookup);
|
|
403
|
+
const after = runManagedBody(path, beforeRaw, leaf.body, tctx, registryLookup, derivedPinLookup);
|
|
392
404
|
|
|
393
405
|
if (created) {
|
|
394
406
|
if (!dryRun) fs.write(path, after);
|
|
@@ -419,6 +431,7 @@ function executePass(
|
|
|
419
431
|
templates: TemplateProvider | undefined,
|
|
420
432
|
initMode: boolean,
|
|
421
433
|
registryLookup: ((packageName: string) => string | undefined) | undefined,
|
|
434
|
+
derivedPinLookup: ((catalogEntry: string) => string | undefined) | undefined,
|
|
422
435
|
): Change[] {
|
|
423
436
|
const items = filterByMode(flat, currentMode(ctx, initMode));
|
|
424
437
|
const changes: Change[] = [];
|
|
@@ -448,6 +461,7 @@ function executePass(
|
|
|
448
461
|
tctx2,
|
|
449
462
|
templates,
|
|
450
463
|
registryLookup,
|
|
464
|
+
derivedPinLookup,
|
|
451
465
|
ctx.isSdkInternal,
|
|
452
466
|
);
|
|
453
467
|
if (change) changes.push(change);
|
|
@@ -478,6 +492,7 @@ export function run(
|
|
|
478
492
|
opts.templates,
|
|
479
493
|
initMode,
|
|
480
494
|
opts.registryLookup,
|
|
495
|
+
opts.derivedPinLookup,
|
|
481
496
|
);
|
|
482
497
|
|
|
483
498
|
const isRefreshDefault = !ctx.dryRun && !ctx.updateDepsOnly && !initMode;
|
|
@@ -499,6 +514,7 @@ export function run(
|
|
|
499
514
|
opts.templates,
|
|
500
515
|
false,
|
|
501
516
|
opts.registryLookup,
|
|
517
|
+
opts.derivedPinLookup,
|
|
502
518
|
);
|
|
503
519
|
// Ignore .structure self-write (already up to date after
|
|
504
520
|
// writeStructureVersion above).
|
|
@@ -14,8 +14,12 @@ import { run as engineRun } from "./engine/runner";
|
|
|
14
14
|
import { createRunContext, modulesForInit } from "./engine/ctx";
|
|
15
15
|
import { STRUCTURE } from "./structure-definition";
|
|
16
16
|
import { STRUCTURE_VERSION } from "./engine/version";
|
|
17
|
-
import {
|
|
18
|
-
|
|
17
|
+
import {
|
|
18
|
+
matchesBumpPattern,
|
|
19
|
+
buildRegistryLookupForNames,
|
|
20
|
+
buildDerivedPinLookupForPins,
|
|
21
|
+
} from "./engine/registry-client";
|
|
22
|
+
import { SDK_CATALOG_PACKAGES, DERIVED_CATALOG_PINS } from "./rules/root-pnpm-workspace";
|
|
19
23
|
import type { BlockVars } from "./engine/api";
|
|
20
24
|
|
|
21
25
|
/** Platform choices `init` can scaffold a software module for. Python is
|
|
@@ -145,8 +149,12 @@ export async function runInit(init: InitInput): Promise<BlockVars> {
|
|
|
145
149
|
// aborts init rather than silently shipping an unresolved placeholder.
|
|
146
150
|
const sdkNames = SDK_CATALOG_PACKAGES.filter(matchesBumpPattern);
|
|
147
151
|
let registryLookup: (name: string) => string | undefined;
|
|
152
|
+
let derivedPinLookup: (entry: string) => string | undefined;
|
|
148
153
|
try {
|
|
149
154
|
registryLookup = await buildRegistryLookupForNames(sdkNames);
|
|
155
|
+
// Derived catalog pins (e.g. vue ← ui-vue's declared dep) are resolved on
|
|
156
|
+
// init too, so a freshly-scaffolded block pins vue exactly from the start.
|
|
157
|
+
derivedPinLookup = await buildDerivedPinLookupForPins(DERIVED_CATALOG_PINS);
|
|
150
158
|
} catch (err) {
|
|
151
159
|
throw new Error(
|
|
152
160
|
`init requires network access to resolve SDK catalog versions from npm, ` +
|
|
@@ -154,7 +162,12 @@ export async function runInit(init: InitInput): Promise<BlockVars> {
|
|
|
154
162
|
);
|
|
155
163
|
}
|
|
156
164
|
|
|
157
|
-
engineRun(STRUCTURE, fs, ctx, {
|
|
165
|
+
engineRun(STRUCTURE, fs, ctx, {
|
|
166
|
+
templates,
|
|
167
|
+
initMode: true,
|
|
168
|
+
registryLookup,
|
|
169
|
+
derivedPinLookup,
|
|
170
|
+
});
|
|
158
171
|
init.log(
|
|
159
172
|
`Initialised block '${vars.facadeName}'${
|
|
160
173
|
vars.softwarePlatform ? ` (software: ${vars.softwarePlatform})` : " (no software)"
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
ensureDep,
|
|
16
16
|
ensureDevDep,
|
|
17
17
|
ensureDevDeps,
|
|
18
|
+
removeDep,
|
|
19
|
+
removeField,
|
|
18
20
|
ensurePeerDeps,
|
|
19
21
|
enforceAlphabeticalOrder,
|
|
20
22
|
enforceFieldOrder,
|
|
@@ -49,8 +51,6 @@ export function modelPackageJsonInitial(ctx: RunContext): Record<string, unknown
|
|
|
49
51
|
watch: "ts-builder build --target block-model --watch",
|
|
50
52
|
build: "ts-builder build --target block-model && block-tools build-model",
|
|
51
53
|
check: "ts-builder check --target block-model",
|
|
52
|
-
// No `test`: the vitest `test` script is wired by the body rule ONLY
|
|
53
|
-
// when co-located test files exist (a freshly-init'd model has none).
|
|
54
54
|
},
|
|
55
55
|
dependencies: {
|
|
56
56
|
"@platforma-sdk/model": "sdk:",
|
|
@@ -59,7 +59,6 @@ export function modelPackageJsonInitial(ctx: RunContext): Record<string, unknown
|
|
|
59
59
|
"@milaboratories/ts-builder": "sdk:",
|
|
60
60
|
"@milaboratories/ts-configs": "sdk:",
|
|
61
61
|
"@platforma-sdk/block-tools": "sdk:",
|
|
62
|
-
vitest: "catalog:",
|
|
63
62
|
},
|
|
64
63
|
peerDependencies: {
|
|
65
64
|
"@types/node": "*",
|
|
@@ -94,7 +93,6 @@ export function modelPackageJsonRules(): void {
|
|
|
94
93
|
"@milaboratories/ts-builder": "sdk:",
|
|
95
94
|
"@milaboratories/ts-configs": "sdk:",
|
|
96
95
|
"@platforma-sdk/block-tools": "sdk:",
|
|
97
|
-
vitest: "catalog:",
|
|
98
96
|
});
|
|
99
97
|
|
|
100
98
|
ensurePeerDeps({
|
|
@@ -102,18 +100,39 @@ export function modelPackageJsonRules(): void {
|
|
|
102
100
|
typescript: "*",
|
|
103
101
|
});
|
|
104
102
|
|
|
105
|
-
// The vitest `test` script
|
|
106
|
-
// model carries co-located unit tests (`src/**/*.test.ts`).
|
|
107
|
-
// model gets
|
|
108
|
-
// scope stays a refresh fixpoint. When tests ARE present, `@types/node`
|
|
109
|
-
// is promoted from peer to a devDep (the single-section invariant drops
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
103
|
+
// The vitest `test` script, the `vitest` devDep, and node ambient types are
|
|
104
|
+
// wired ONLY when the model carries co-located unit tests (`src/**/*.test.ts`).
|
|
105
|
+
// A test-less model gets none — turbo simply has no `test` task for it, and
|
|
106
|
+
// the scope stays a refresh fixpoint. When tests ARE present, `@types/node`
|
|
107
|
+
// is promoted from peer to a devDep (the single-section invariant drops the
|
|
108
|
+
// peer) so the tests' `node:*` imports resolve under the `types: ["node"]`
|
|
109
|
+
// that model/tsconfig adds under the same predicate. Runs AFTER ensurePeerDeps
|
|
110
|
+
// so the promotion is not reverted.
|
|
111
|
+
//
|
|
112
|
+
// Skipped entirely for sdk-internal (in-monorepo) blocks: those live inside
|
|
113
|
+
// the platforma monorepo's own test infrastructure (build-configs, shared
|
|
114
|
+
// catalog, coverage providers) and own their test wiring — the structurer
|
|
115
|
+
// must not impose or strip standalone-block test conventions on them.
|
|
116
|
+
when(
|
|
117
|
+
({ ctx }) => !ctx.isSdkInternal,
|
|
118
|
+
() =>
|
|
119
|
+
when(
|
|
120
|
+
whenFilesExist(COLOCATED_TEST_GLOB),
|
|
121
|
+
() => {
|
|
122
|
+
ensureScript("test", "vitest run --passWithNoTests");
|
|
123
|
+
ensureDevDep("vitest", "catalog:");
|
|
124
|
+
ensureDevDep("@types/node", "*");
|
|
125
|
+
},
|
|
126
|
+
() => removeDep("vitest"),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// vite/tsup-era artefacts: the canonical model builds via ts-builder, so the
|
|
131
|
+
// legacy `tsup`/`vite` devDeps and the top-level `tsup` bundler config block
|
|
132
|
+
// are dropped.
|
|
133
|
+
removeDep("tsup");
|
|
134
|
+
removeDep("vite");
|
|
135
|
+
removeField("tsup");
|
|
117
136
|
|
|
118
137
|
enforceAlphabeticalOrder("dependencies");
|
|
119
138
|
enforceAlphabeticalOrder("devDependencies");
|
|
@@ -26,12 +26,14 @@ import {
|
|
|
26
26
|
generate,
|
|
27
27
|
ensureCatalogLatest,
|
|
28
28
|
ensureCatalogVersion,
|
|
29
|
+
pinCatalogToDependencyOf,
|
|
29
30
|
} from "../engine/api";
|
|
30
31
|
import { getActiveRunContext } from "../engine/builders";
|
|
31
32
|
import {
|
|
32
33
|
rootPnpmWorkspaceInitial,
|
|
33
34
|
SDK_CATALOG_PACKAGES,
|
|
34
35
|
INFRA_CATALOG_FLOOR,
|
|
36
|
+
DERIVED_CATALOG_PINS,
|
|
35
37
|
RUNENV_PYTHON,
|
|
36
38
|
RUNENV_PYTHON_VERSION,
|
|
37
39
|
} from "./root-pnpm-workspace";
|
|
@@ -57,8 +59,17 @@ export function rootCatalogBumpRules(): void {
|
|
|
57
59
|
// block's missing standard keys + overwrites the init
|
|
58
60
|
// placeholder).
|
|
59
61
|
for (const name of SDK_CATALOG_PACKAGES) ensureCatalogLatest(name);
|
|
62
|
+
// Derived catalog pins → the EXACT version their source package
|
|
63
|
+
// declares (e.g. `vue` from `@platforma-sdk/ui-vue`). OVERWRITES
|
|
64
|
+
// (seeds when absent, tightens a loose on-disk entry). Resolved
|
|
65
|
+
// by the prefetched `derivedPinLookup`; a no-op when absent
|
|
66
|
+
// (default refresh passes none). Must run AFTER ensureCatalogLatest
|
|
67
|
+
// — the source package's own version is bumped there first.
|
|
68
|
+
for (const pin of DERIVED_CATALOG_PINS) {
|
|
69
|
+
pinCatalogToDependencyOf(pin.entry, { of: pin.of, ofVersion: pin.ofVersion });
|
|
70
|
+
}
|
|
60
71
|
// Curated infra floor → fixed version, ADD-IF-ABSENT (seeds
|
|
61
|
-
// shx/turbo/vitest/
|
|
72
|
+
// shx/turbo/vitest/changesets when missing; never touches a
|
|
62
73
|
// version the block already pins).
|
|
63
74
|
for (const [name, version] of Object.entries(INFRA_CATALOG_FLOOR)) {
|
|
64
75
|
ensureCatalogVersion(name, version);
|