@platforma-sdk/block-tools 2.10.16 → 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 +404 -373
- package/dist/cli.mjs.map +1 -1
- package/dist/structure/engine/api.d.ts +1 -1
- package/dist/structure/engine/builders.d.ts +9 -9
- 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/package.json +5 -5
- package/src/structure/__tests__/model-package-json.snapshot.test.ts +1 -2
- package/src/structure/__tests__/tsconfig-idempotency.test.ts +94 -0
- 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__/content-rules/when-inner.test.ts +30 -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/builders.ts +38 -23
- 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/model.ts +11 -35
- 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 +34 -13
- package/src/structure/rules/ui.ts +10 -22
- package/src/structure/rules/workflow-package-json.ts +25 -11
- package/src/structure/rules/workflow.ts +21 -2
- package/src/structure/templates/static/model/tsconfig.node.json +7 -0
- package/src/structure/templates/static/ui/tsconfig.node.json +7 -0
|
@@ -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");
|
|
@@ -2,47 +2,23 @@
|
|
|
2
2
|
// `src/index.ts` seed is dropped by `init` and never touched again
|
|
3
3
|
// (block author owns it).
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
scope,
|
|
7
|
-
fixed,
|
|
8
|
-
managed,
|
|
9
|
-
seed,
|
|
10
|
-
file,
|
|
11
|
-
generate,
|
|
12
|
-
when,
|
|
13
|
-
whenFilesExist,
|
|
14
|
-
ensureField,
|
|
15
|
-
removeField,
|
|
16
|
-
pruneKeysMatching,
|
|
17
|
-
} from "../engine/api";
|
|
5
|
+
import { scope, fixed, managed, seed, file, generate, when, whenFilesExist } from "../engine/api";
|
|
18
6
|
import { getActiveRunContext } from "../engine/builders";
|
|
19
7
|
import { modelPackageJsonInitial, modelPackageJsonRules } from "./model-package-json";
|
|
20
8
|
import { COLOCATED_TEST_GLOB } from "./shared/colocated-tests";
|
|
21
9
|
|
|
22
10
|
export function modelRules(): void {
|
|
23
11
|
scope("model", () => {
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// `
|
|
27
|
-
//
|
|
28
|
-
// `
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
ensureField("extends", "@milaboratories/ts-configs/block/model");
|
|
35
|
-
// Canonical default carries no compilerOptions; drop any legacy block's
|
|
36
|
-
// before (conditionally) re-adding only `types`.
|
|
37
|
-
removeField("compilerOptions");
|
|
38
|
-
when(whenFilesExist(COLOCATED_TEST_GLOB), () => {
|
|
39
|
-
ensureField("compilerOptions.types", ["node"]);
|
|
40
|
-
});
|
|
41
|
-
ensureField("include", ["src/**/*"]);
|
|
42
|
-
// Strip anything else a legacy tsconfig carried (files, references, a
|
|
43
|
-
// different extends layout) — keep only the canonical keys.
|
|
44
|
-
pruneKeysMatching((k) => k !== "extends" && k !== "include" && k !== "compilerOptions");
|
|
45
|
-
});
|
|
12
|
+
// Two static end states by co-located-test presence: a test-bearing model
|
|
13
|
+
// gets node ambient types (`.node.json`) so the tests type-check —
|
|
14
|
+
// `@types/node` is wired alongside by the package.json rule; a test-less
|
|
15
|
+
// model stays bare. `fixed` (engine-owned, whole-file overwrite) not
|
|
16
|
+
// `managed`, so refresh is idempotent by construction — no key-order drift.
|
|
17
|
+
when(
|
|
18
|
+
whenFilesExist(COLOCATED_TEST_GLOB),
|
|
19
|
+
() => fixed("tsconfig.json", file("model/tsconfig.node.json")),
|
|
20
|
+
() => fixed("tsconfig.json", file("model/tsconfig.json")),
|
|
21
|
+
);
|
|
46
22
|
fixed(".oxlintrc.json", file("model/.oxlintrc.json"));
|
|
47
23
|
fixed(".oxfmtrc.json", file("model/.oxfmtrc.json"));
|
|
48
24
|
|
|
@@ -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);
|
|
@@ -38,7 +38,7 @@ export function rootPackageJsonInitial(_ctx: RunContext): Record<string, unknown
|
|
|
38
38
|
// refresh → format flow); kept for compatibility.
|
|
39
39
|
"update-sdk": "block-tools structure refresh --update-deps-only",
|
|
40
40
|
update:
|
|
41
|
-
"block-tools structure refresh --update-deps-only && pnpm i && block-tools structure refresh && pnpm fmt",
|
|
41
|
+
"block-tools structure refresh --update-deps-only && pnpm i && block-tools structure refresh && pnpm i && pnpm fmt",
|
|
42
42
|
},
|
|
43
43
|
peerDependencies: {
|
|
44
44
|
oxlint: "*",
|
|
@@ -79,12 +79,14 @@ export function rootPackageJsonRules(): void {
|
|
|
79
79
|
// Deprecated in favour of `update`; kept for compatibility.
|
|
80
80
|
ensureScript("update-sdk", "block-tools structure refresh --update-deps-only");
|
|
81
81
|
// Full SDK-update flow: bump catalog (deps-only) → install → re-apply
|
|
82
|
-
// structure against the freshly-pulled deps →
|
|
83
|
-
//
|
|
84
|
-
//
|
|
82
|
+
// structure against the freshly-pulled deps → install AGAIN (the structural
|
|
83
|
+
// pass can add new devDeps — e.g. ts-builder/oxlint/oxfmt on a first
|
|
84
|
+
// migration — that `pnpm fmt` then needs on PATH) → format. Wrapped as one
|
|
85
|
+
// script. `pnpm fmt` (turbo run fmt) leaves the tree oxfmt-clean after the
|
|
86
|
+
// structural rewrite so a follow-up `pnpm check` passes.
|
|
85
87
|
ensureScript(
|
|
86
88
|
"update",
|
|
87
|
-
"block-tools structure refresh --update-deps-only && pnpm i && block-tools structure refresh && pnpm fmt",
|
|
89
|
+
"block-tools structure refresh --update-deps-only && pnpm i && block-tools structure refresh && pnpm i && pnpm fmt",
|
|
88
90
|
);
|
|
89
91
|
|
|
90
92
|
ensureDevDeps({
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
// npm-latest for the SDK families) lives in the separate `onInitOrUpdate`
|
|
19
19
|
// frame (`rootCatalogBumpRules`), which fires on init + update-deps only.
|
|
20
20
|
|
|
21
|
-
import { ensureWorkspaceModulePaths, type RunContext } from "../engine/api";
|
|
22
|
-
import { matchesBumpPattern } from "../engine/registry-client";
|
|
21
|
+
import { ensureWorkspaceModulePaths, ensureCatalogAbsent, type RunContext } from "../engine/api";
|
|
22
|
+
import { matchesBumpPattern, type DerivedCatalogPin } from "../engine/registry-client";
|
|
23
23
|
|
|
24
24
|
/** SDK packages that live in the catalog (membership only — no versions).
|
|
25
25
|
* Init fetches npm latest for each; refresh leaves them as the lockfile has
|
|
@@ -62,14 +62,23 @@ export const INFRA_CATALOG_FLOOR: Record<string, string> = {
|
|
|
62
62
|
shx: "~0.4.0",
|
|
63
63
|
"@changesets/cli": "~2.29.8",
|
|
64
64
|
vitest: "~4.0.18",
|
|
65
|
-
// The seeded ui depends on `vue` (catalog:). `@platforma-sdk/ui-vue` pins
|
|
66
|
-
// vue as an EXACT regular dependency (not a peer), so the block must pin the
|
|
67
|
-
// SAME exact version — a floated `~` lets the ui resolve a newer 3.5.x than
|
|
68
|
-
// ui-vue's, yielding two vue instances and a SdkPluginV3-vs-Plugin type
|
|
69
|
-
// clash in `main.ts`. Keep this in lockstep with ui-vue's vue pin.
|
|
70
|
-
vue: "3.5.24",
|
|
71
65
|
};
|
|
72
66
|
|
|
67
|
+
/** Catalog entries whose exact version is DERIVED from the version another
|
|
68
|
+
* package declares for them (see `pinCatalogToDependencyOf`). Resolved on
|
|
69
|
+
* init + update-deps and OVERWRITES the on-disk entry.
|
|
70
|
+
*
|
|
71
|
+
* `vue`: the seeded ui depends on `vue` (catalog:). `@platforma-sdk/ui-vue`
|
|
72
|
+
* pins vue as an EXACT regular dependency (not a peer), so the block must
|
|
73
|
+
* pin the SAME exact version — a floated `~` lets the ui resolve a newer
|
|
74
|
+
* 3.5.x than ui-vue's, yielding two vue instances and a SdkPluginV3-vs-Plugin
|
|
75
|
+
* type clash in `main.ts`. Deriving the pin from ui-vue's declared dep keeps
|
|
76
|
+
* the block in lockstep with whatever vue ui-vue's npm-latest pins, with no
|
|
77
|
+
* hand-maintained version here. */
|
|
78
|
+
export const DERIVED_CATALOG_PINS: readonly DerivedCatalogPin[] = [
|
|
79
|
+
{ entry: "vue", of: "@platforma-sdk/ui-vue" },
|
|
80
|
+
];
|
|
81
|
+
|
|
73
82
|
/** Catalog name of the python runenv. Added to a block's catalog only when
|
|
74
83
|
* the block carries a software module. Pinned to an EXACT version — updating
|
|
75
84
|
* the python runenv is an explicit author decision, never automatic. */
|
|
@@ -100,4 +109,11 @@ export function rootPnpmWorkspaceInitial(ctx: RunContext): Record<string, unknow
|
|
|
100
109
|
|
|
101
110
|
export function rootPnpmWorkspaceRules(): void {
|
|
102
111
|
ensureWorkspaceModulePaths();
|
|
112
|
+
// vite/tsup/vue-tsc-era catalog entries: the canonical toolchain (ts-builder
|
|
113
|
+
// owns vue-tsc; ts-builder + rolldown replace vite/tsup) does not use them.
|
|
114
|
+
// Drop the orphaned catalog keys — paired with the per-package `removeDep`
|
|
115
|
+
// so a migrated block sheds both the dep and its catalog entry.
|
|
116
|
+
ensureCatalogAbsent("vite");
|
|
117
|
+
ensureCatalogAbsent("tsup");
|
|
118
|
+
ensureCatalogAbsent("vue-tsc");
|
|
103
119
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ensurePeerDeps,
|
|
17
17
|
ensureWorkspaceScopeDeps,
|
|
18
18
|
removeDep,
|
|
19
|
+
removeScript,
|
|
19
20
|
enforceAlphabeticalOrder,
|
|
20
21
|
enforceFieldOrder,
|
|
21
22
|
when,
|
|
@@ -38,8 +39,6 @@ export function uiPackageJsonInitial(ctx: RunContext): Record<string, unknown> {
|
|
|
38
39
|
watch: "ts-builder build --target block-ui --watch",
|
|
39
40
|
build: "ts-builder build --target block-ui",
|
|
40
41
|
check: "ts-builder check --target block-ui",
|
|
41
|
-
// No `test`: the vitest `test` script is wired by the body rule ONLY
|
|
42
|
-
// when co-located test files exist (a freshly-init'd ui has none).
|
|
43
42
|
},
|
|
44
43
|
dependencies: {
|
|
45
44
|
"@platforma-sdk/ui-vue": "sdk:",
|
|
@@ -51,7 +50,6 @@ export function uiPackageJsonInitial(ctx: RunContext): Record<string, unknown> {
|
|
|
51
50
|
devDependencies: {
|
|
52
51
|
"@milaboratories/ts-builder": "sdk:",
|
|
53
52
|
"@milaboratories/ts-configs": "sdk:",
|
|
54
|
-
vitest: "catalog:",
|
|
55
53
|
},
|
|
56
54
|
peerDependencies: {
|
|
57
55
|
typescript: "*",
|
|
@@ -77,9 +75,20 @@ export function uiPackageJsonRules(): void {
|
|
|
77
75
|
ensureDevDeps({
|
|
78
76
|
"@milaboratories/ts-builder": "sdk:",
|
|
79
77
|
"@milaboratories/ts-configs": "sdk:",
|
|
80
|
-
vitest: "catalog:",
|
|
81
78
|
});
|
|
82
79
|
|
|
80
|
+
// `vue-tsc` is owned by `ts-builder` (which runs it internally for
|
|
81
|
+
// `--target block-ui`), so the ui must NOT declare its own. A direct
|
|
82
|
+
// `vue-tsc` dep is the vestigial vite/vue-tsc-era artefact: it resolves
|
|
83
|
+
// independently of the version ts-builder pins, which is how a block ended
|
|
84
|
+
// up type-checking under a different vue-tsc than the toolchain intends.
|
|
85
|
+
removeDep("vue-tsc");
|
|
86
|
+
|
|
87
|
+
// vite-era artefacts: the canonical ui builds + dev-serves via ts-builder,
|
|
88
|
+
// so a legacy `vite` dep and the `preview` (vite preview) script are dropped.
|
|
89
|
+
removeDep("vite");
|
|
90
|
+
removeScript("preview");
|
|
91
|
+
|
|
83
92
|
// The ui builds with `--target block-ui` (types: []), so by default it
|
|
84
93
|
// carries no browser/vitest/node ambient types — only the `typescript`
|
|
85
94
|
// peer for IDE type resolution. Drop any stray `@types/node` a legacy
|
|
@@ -90,15 +99,27 @@ export function uiPackageJsonRules(): void {
|
|
|
90
99
|
removeDep("@types/node");
|
|
91
100
|
|
|
92
101
|
// ...but a ui WITH co-located unit tests (`src/**/*.test.ts`) needs the
|
|
93
|
-
// vitest `test` script AND node ambient types: re-add
|
|
94
|
-
// devDep so `types: ["node"]` in ui/tsconfig (wired under
|
|
95
|
-
// predicate) resolves the tests' `node:*` imports. A test-less ui
|
|
96
|
-
// the lean default (no test script, no @types/node) and
|
|
97
|
-
// Runs AFTER removeDep so the dev
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
// vitest `test` script, the `vitest` devDep, AND node ambient types: re-add
|
|
103
|
+
// `@types/node` as a devDep so `types: ["node"]` in ui/tsconfig (wired under
|
|
104
|
+
// the same predicate) resolves the tests' `node:*` imports. A test-less ui
|
|
105
|
+
// keeps the lean default (no test script, no vitest, no @types/node) and
|
|
106
|
+
// stays a fixpoint. Runs AFTER removeDep so the dev deps are not stripped.
|
|
107
|
+
//
|
|
108
|
+
// Skipped entirely for sdk-internal (in-monorepo) blocks: they own their test
|
|
109
|
+
// wiring under the monorepo's shared infrastructure — see model-package-json.
|
|
110
|
+
when(
|
|
111
|
+
({ ctx }) => !ctx.isSdkInternal,
|
|
112
|
+
() =>
|
|
113
|
+
when(
|
|
114
|
+
whenFilesExist(COLOCATED_TEST_GLOB),
|
|
115
|
+
() => {
|
|
116
|
+
ensureScript("test", "vitest run --passWithNoTests");
|
|
117
|
+
ensureDevDep("vitest", "catalog:");
|
|
118
|
+
ensureDevDep("@types/node", "*");
|
|
119
|
+
},
|
|
120
|
+
() => removeDep("vitest"),
|
|
121
|
+
),
|
|
122
|
+
);
|
|
102
123
|
|
|
103
124
|
enforceAlphabeticalOrder("dependencies");
|
|
104
125
|
enforceAlphabeticalOrder("devDependencies");
|