@platforma-sdk/block-tools 2.10.19 → 2.11.1
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 +11 -11
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +362 -352
- package/dist/cli.mjs.map +1 -1
- package/dist/structure/engine/builders.d.ts +7 -6
- package/dist/structure/engine/content-rules.d.ts +4 -4
- package/dist/structure/engine/ir.d.ts +11 -6
- package/dist/structure/rules/block-package-json.d.ts +1 -1
- package/dist/structure/rules/root-ci.d.ts +1 -0
- package/dist/v2/registry/schema_public.d.ts +150 -150
- package/package.json +6 -6
- package/src/structure/CLAUDE.md +2 -0
- package/src/structure/__tests__/discovery-shortname.test.ts +50 -0
- package/src/structure/__tests__/root-ci-workflows.test.ts +68 -0
- package/src/structure/engine/builders.ts +7 -6
- package/src/structure/engine/content-rules.ts +7 -7
- package/src/structure/engine/discovery-fs.ts +9 -1
- package/src/structure/engine/ir.ts +11 -6
- package/src/structure/engine/runner.ts +14 -14
- package/src/structure/rules/block-package-json.ts +2 -3
- package/src/structure/rules/block.ts +3 -4
- package/src/structure/rules/migrations.ts +1 -2
- package/src/structure/rules/model.ts +1 -2
- package/src/structure/rules/root-catalog-bump.ts +3 -4
- package/src/structure/rules/root-ci.ts +55 -0
- package/src/structure/rules/root.ts +2 -3
- package/src/structure/rules/software.ts +1 -2
- package/src/structure/rules/test.ts +1 -2
- package/src/structure/rules/ui.ts +3 -5
- package/src/structure/rules/workflow.ts +3 -5
- package/src/structure/structure-definition.ts +2 -0
- package/src/structure/templates/text/workflows/build.tpl.yaml +67 -0
- package/src/structure/templates/text/workflows/mark-stable.tpl.yaml +34 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platforma-sdk/block-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.1",
|
|
4
4
|
"description": "Utility to manipulate Platforma Blocks and Block Registry",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"yaml": "^2.8.0",
|
|
35
35
|
"zod": "~3.25.76",
|
|
36
36
|
"@milaboratories/pl-http": "1.2.4",
|
|
37
|
-
"@milaboratories/pl-model-
|
|
38
|
-
"@milaboratories/pl-model-
|
|
39
|
-
"@milaboratories/
|
|
37
|
+
"@milaboratories/pl-model-backend": "1.4.8",
|
|
38
|
+
"@milaboratories/pl-model-common": "1.46.2",
|
|
39
|
+
"@milaboratories/ts-helpers": "1.8.3",
|
|
40
|
+
"@milaboratories/pl-model-middle-layer": "1.30.7",
|
|
40
41
|
"@milaboratories/ts-helpers-oclif": "1.1.42",
|
|
41
42
|
"@platforma-sdk/blocks-deps-updater": "2.2.0",
|
|
42
|
-
"@milaboratories/
|
|
43
|
-
"@milaboratories/pl-model-backend": "1.4.7"
|
|
43
|
+
"@milaboratories/resolve-helper": "1.1.3"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
package/src/structure/CLAUDE.md
CHANGED
|
@@ -15,12 +15,14 @@ To change what "canonical" means, edit `rules/`; the engine is block-agnostic.
|
|
|
15
15
|
|
|
16
16
|
- Each `rules/*` file pairs a generator (init: full initial content) with a body (refresh: drift-correcting assertions). Read as "what the file should be" + "how to nudge an existing file there", not as sequential logic.
|
|
17
17
|
- The primitive choice IS the design: `fixed` (overwrite verbatim, engine-owned), `managed` (reconcile named fields, author keeps the rest), `scaffold` (write-once default, author may tune), `seed` (written once at init, author owns forever), `remove` (one-way). Wrong primitive is the main design error — `fixed` on an author-tunable file clobbers their work; `seed` on engine-owned content lets drift accumulate.
|
|
18
|
+
- Two layers. Calls inside `defineStructure` (`scope`/`fixed`/`managed`/`seed`/`when`-framing) run once at tree-build time to DECLARE the tree — there is no block and no `ctx` yet. The lambdas the engine runs later, per block — trigger predicates, `generate`/`tpl` producers, and `managed` bodies — are the EXECUTION layer, where per-block values are computed.
|
|
18
19
|
|
|
19
20
|
## Invariants
|
|
20
21
|
|
|
21
22
|
- Fixpoint: `refresh` on an already-canonical block makes zero changes. Every rule must converge.
|
|
22
23
|
- Generators emit oxfmt-clean output (the `enforce*` calls mirror oxfmt's order), so build→check passes with no prior `fmt`.
|
|
23
24
|
- sdk-internal blocks (`etc/blocks/*`) skip root-scope and standalone test wiring — the monorepo owns that infra; the structurer must neither impose nor strip it.
|
|
25
|
+
- Rule code never reaches for `ctx` ambiently — `getActiveRunContext()`/`blockVars()` are engine-internal plumbing. A lambda that needs block identity takes `ctx` by argument (triggers get the richer `TriggerContext`, the rest `RunContext`); a `managed` body that doesn't take it relies on the builders it calls, which resolve `ctx` themselves.
|
|
24
26
|
|
|
25
27
|
## Gotchas
|
|
26
28
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// DISCOVERY derives BlockVars from the block-scope module's package name.
|
|
2
|
+
// This structurer names that package `<facade>.block`, while the root
|
|
3
|
+
// package carries no `name` — so on refresh/check the `.block` suffix would
|
|
4
|
+
// leak into `shortName` unless discovery strips it (see discovery-fs.ts).
|
|
5
|
+
// These tests pin both shapes: the structurer-shaped block (suffix stripped)
|
|
6
|
+
// and a legacy block whose block package IS the bare facade (nothing to
|
|
7
|
+
// strip).
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect } from "vitest";
|
|
10
|
+
import type { BlockVars } from "../engine/api";
|
|
11
|
+
import { simulateInit } from "../engine/testing";
|
|
12
|
+
import { discoverRunContext } from "../engine/discovery-fs";
|
|
13
|
+
import { MemoryFileSystem } from "../engine/fs/memory";
|
|
14
|
+
|
|
15
|
+
describe("DISCOVERY: shortName from the block package name", () => {
|
|
16
|
+
test("strips the trailing .block for a <facade>.block-shaped block", () => {
|
|
17
|
+
const vars: BlockVars = {
|
|
18
|
+
facadeName: "@platforma-open/test-org.demo",
|
|
19
|
+
baseName: "test-org.demo",
|
|
20
|
+
npmOrg: "@platforma-open",
|
|
21
|
+
orgScope: "test-org",
|
|
22
|
+
shortName: "demo",
|
|
23
|
+
};
|
|
24
|
+
// simulateInit runs the real init then RE-DISCOVERS from the written FS,
|
|
25
|
+
// so this is the genuine refresh/check derivation path.
|
|
26
|
+
const { ctx } = simulateInit({ vars });
|
|
27
|
+
// Sanity: the block package really is named `<facade>.block` on disk.
|
|
28
|
+
const blockMod = ctx.modules.find((m) => m.scope === "block");
|
|
29
|
+
expect(blockMod?.name).toBe("@platforma-open/test-org.demo.block");
|
|
30
|
+
expect(ctx.blockVars.shortName).toBe("demo");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("keeps the real shortName for a legacy bare-facade block package", () => {
|
|
34
|
+
const fs = new MemoryFileSystem({
|
|
35
|
+
"pnpm-workspace.yaml": "packages:\n - block\n",
|
|
36
|
+
// Nameless root — the standalone shape; classified `root` by path.
|
|
37
|
+
"package.json": "{}",
|
|
38
|
+
// Legacy block package: name IS the bare facade, no `.block` suffix.
|
|
39
|
+
"block/package.json": JSON.stringify({
|
|
40
|
+
name: "@platforma-open/test-org.legacy",
|
|
41
|
+
files: ["index.d.ts", "index.js"],
|
|
42
|
+
dependencies: { "@platforma-sdk/block-tools": "*" },
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
const ctx = discoverRunContext({ fs, isSdkInternal: false });
|
|
46
|
+
const blockMod = ctx.modules.find((m) => m.scope === "block");
|
|
47
|
+
expect(blockMod?.name).toBe("@platforma-open/test-org.legacy");
|
|
48
|
+
expect(ctx.blockVars.shortName).toBe("legacy");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Root CI workflow files (`rules/root-ci.ts`) — `fixed`, generated from the
|
|
2
|
+
// block's short name. These rules are `!isSdkInternal`, so the `etc/blocks`
|
|
3
|
+
// check-blocks run never exercises them; this is their only coverage.
|
|
4
|
+
//
|
|
5
|
+
// Asserts: init writes both workflows with derived identity (app-name /
|
|
6
|
+
// app-name-slug) + shared constants (team-id, the `@v4` pins, test: true);
|
|
7
|
+
// the files are idempotent (init → check zero diff); and `--sdk-internal`
|
|
8
|
+
// blocks get neither file.
|
|
9
|
+
|
|
10
|
+
import { describe, test, expect } from "vitest";
|
|
11
|
+
import type { BlockVars, RunContext } from "../engine/api";
|
|
12
|
+
import { STRUCTURE } from "../structure-definition";
|
|
13
|
+
import { run as engineRun } from "../engine/runner";
|
|
14
|
+
import { defaultTemplateProvider, simulateInit } from "../engine/testing";
|
|
15
|
+
|
|
16
|
+
const TEMPLATES = defaultTemplateProvider();
|
|
17
|
+
|
|
18
|
+
const VARS: BlockVars = {
|
|
19
|
+
facadeName: "@platforma-open/milaboratories.mixcr-clonotyping-2",
|
|
20
|
+
baseName: "milaboratories.mixcr-clonotyping-2",
|
|
21
|
+
npmOrg: "@platforma-open",
|
|
22
|
+
orgScope: "milaboratories",
|
|
23
|
+
shortName: "mixcr-clonotyping-2",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const BUILD = ".github/workflows/build.yaml";
|
|
27
|
+
const MARK_STABLE = ".github/workflows/mark-stable.yaml";
|
|
28
|
+
|
|
29
|
+
describe("root CI workflows (fixed, generated)", () => {
|
|
30
|
+
test("init writes both workflows with derived identity + shared constants", () => {
|
|
31
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
32
|
+
|
|
33
|
+
const build = fs.read(BUILD);
|
|
34
|
+
// Derived identity from the short name.
|
|
35
|
+
expect(build).toContain("app-name: 'Block: Mixcr Clonotyping 2'");
|
|
36
|
+
expect(build).toContain("app-name-slug: 'block-mixcr-clonotyping-2'");
|
|
37
|
+
// Shared constants.
|
|
38
|
+
expect(build).toContain("team-id: 'ciplopen'");
|
|
39
|
+
expect(build).toContain("test: true");
|
|
40
|
+
// The reusable-workflow pins — the whole point of engine-owning the file.
|
|
41
|
+
expect(build).toContain(
|
|
42
|
+
"uses: milaboratory/github-ci/.github/workflows/node-simple-pnpm.yaml@v4",
|
|
43
|
+
);
|
|
44
|
+
expect(build).toContain("uses: milaboratory/github-ci/actions/context/init@v4");
|
|
45
|
+
// `${{ ... }}` GitHub expressions survive `tpl` substitution untouched.
|
|
46
|
+
expect(build).toContain("${{ toJSON(secrets.NPMJS_TOKEN) }}");
|
|
47
|
+
|
|
48
|
+
const markStable = fs.read(MARK_STABLE);
|
|
49
|
+
expect(markStable).toContain("app-name: 'Block: Mixcr Clonotyping 2 - Mark Stable'");
|
|
50
|
+
expect(markStable).toContain(
|
|
51
|
+
"uses: milaboratory/github-ci/.github/workflows/block-mark-stable.yaml@v4",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("init → check is zero-diff for the workflow files (fixed idempotency)", () => {
|
|
56
|
+
const { fs, ctx } = simulateInit({ vars: VARS });
|
|
57
|
+
const checkCtx: RunContext = { ...ctx, dryRun: true };
|
|
58
|
+
const result = engineRun(STRUCTURE, fs, checkCtx, { templates: TEMPLATES });
|
|
59
|
+
const ciChanges = result.changes.filter((c) => c.path === BUILD || c.path === MARK_STABLE);
|
|
60
|
+
expect(ciChanges).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("sdk-internal blocks get neither workflow file", () => {
|
|
64
|
+
const { fs } = simulateInit({ vars: VARS, isSdkInternal: true });
|
|
65
|
+
expect(fs.exists(BUILD)).toBe(false);
|
|
66
|
+
expect(fs.exists(MARK_STABLE)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -327,18 +327,19 @@ export function text(value: string): ContentForm {
|
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
/** Content form: a text template (the structurer's `text/` templates) with
|
|
330
|
-
* `${var}` placeholders substituted from `vars`.
|
|
330
|
+
* `${var}` placeholders substituted from `vars`. When `vars` is a function it
|
|
331
|
+
* receives the active {@link RunContext} at resolve time. */
|
|
331
332
|
export function tpl(
|
|
332
333
|
path: string,
|
|
333
|
-
vars: Record<string, string> | (() => Record<string, string>),
|
|
334
|
+
vars: Record<string, string> | ((ctx: RunContext) => Record<string, string>),
|
|
334
335
|
): ContentForm {
|
|
335
336
|
return { kind: "tpl", path, vars };
|
|
336
337
|
}
|
|
337
338
|
|
|
338
|
-
/** Content form: content computed at run time
|
|
339
|
-
*
|
|
340
|
-
* already a string. */
|
|
341
|
-
export function generate(fn: () => unknown): ContentForm {
|
|
339
|
+
/** Content form: content computed at run time from the active
|
|
340
|
+
* {@link RunContext}. The return value is serialised as JSON / YAML by the
|
|
341
|
+
* target file's extension, or used verbatim if it is already a string. */
|
|
342
|
+
export function generate(fn: (ctx: RunContext) => unknown): ContentForm {
|
|
342
343
|
return { kind: "generate", fn };
|
|
343
344
|
}
|
|
344
345
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { registerInnerWhenHandler, tryGetActiveRunContext } from "./builders";
|
|
15
15
|
import type { RunContext, Scope } from "./api";
|
|
16
|
-
import type { TriggerContext, TriggerFn } from "./ir";
|
|
16
|
+
import type { ManagedBody, TriggerContext, TriggerFn } from "./ir";
|
|
17
17
|
import {
|
|
18
18
|
deleteAtPath,
|
|
19
19
|
getAtPath,
|
|
@@ -163,13 +163,13 @@ export type WithManagedOpts = {
|
|
|
163
163
|
* the runner calls this for managed `.json` files. */
|
|
164
164
|
export function withManagedBody(
|
|
165
165
|
obj: JsonObject,
|
|
166
|
-
body:
|
|
166
|
+
body: ManagedBody,
|
|
167
167
|
opts: WithManagedOpts = {},
|
|
168
168
|
): JsonObject {
|
|
169
169
|
if (active) throw new Error("Nested managed(...) body — engine bug.");
|
|
170
170
|
active = { kind: "json", obj, ctx: opts.ctx, triggerContext: opts.triggerContext };
|
|
171
171
|
try {
|
|
172
|
-
body();
|
|
172
|
+
body((opts.ctx ?? opts.triggerContext?.ctx) as RunContext);
|
|
173
173
|
return obj;
|
|
174
174
|
} finally {
|
|
175
175
|
active = undefined;
|
|
@@ -189,7 +189,7 @@ export type WithManagedYamlOpts = WithManagedOpts & {
|
|
|
189
189
|
* `pnpm-workspace.yaml`); returns the mutated document. Exported for tests. */
|
|
190
190
|
export function withManagedYaml(
|
|
191
191
|
doc: YamlDocument,
|
|
192
|
-
body:
|
|
192
|
+
body: ManagedBody,
|
|
193
193
|
opts: WithManagedYamlOpts = {},
|
|
194
194
|
): YamlDocument {
|
|
195
195
|
if (active) throw new Error("Nested managed(...) body — engine bug.");
|
|
@@ -202,7 +202,7 @@ export function withManagedYaml(
|
|
|
202
202
|
getDerivedDependencyPin: opts.getDerivedDependencyPin,
|
|
203
203
|
};
|
|
204
204
|
try {
|
|
205
|
-
body();
|
|
205
|
+
body((opts.ctx ?? opts.triggerContext?.ctx) as RunContext);
|
|
206
206
|
return doc;
|
|
207
207
|
} finally {
|
|
208
208
|
active = undefined;
|
|
@@ -213,7 +213,7 @@ export function withManagedYaml(
|
|
|
213
213
|
* (for `.gitignore`); returns the mutated lines. Exported for tests. */
|
|
214
214
|
export function withManagedLines(
|
|
215
215
|
lines: string[],
|
|
216
|
-
body:
|
|
216
|
+
body: ManagedBody,
|
|
217
217
|
opts: WithManagedOpts = {},
|
|
218
218
|
): string[] {
|
|
219
219
|
if (active) throw new Error("Nested managed(...) body — engine bug.");
|
|
@@ -224,7 +224,7 @@ export function withManagedLines(
|
|
|
224
224
|
triggerContext: opts.triggerContext,
|
|
225
225
|
};
|
|
226
226
|
try {
|
|
227
|
-
body();
|
|
227
|
+
body((opts.ctx ?? opts.triggerContext?.ctx) as RunContext);
|
|
228
228
|
return lines;
|
|
229
229
|
} finally {
|
|
230
230
|
active = undefined;
|
|
@@ -196,7 +196,15 @@ export function discoverRunContext(input: DiscoverInput): RunContext {
|
|
|
196
196
|
`Could not find a 'block' (or root-as-block) package to derive BlockVars from.`,
|
|
197
197
|
);
|
|
198
198
|
}
|
|
199
|
-
|
|
199
|
+
// This structurer names the block package `<facade>.block`
|
|
200
|
+
// (block-package-json.ts). The root package carries no `name`, so DISCOVERY
|
|
201
|
+
// derives BlockVars from the block module — whose `.block` suffix would
|
|
202
|
+
// otherwise leak into `shortName` on refresh/check (but not init, which is
|
|
203
|
+
// fed the facade directly). Strip it so `shortName` is the real short name
|
|
204
|
+
// everywhere. No-op for legacy blocks whose block package IS the bare facade
|
|
205
|
+
// and for the root-as-block fallback.
|
|
206
|
+
const facadeName = blockMod.name.replace(/\.block$/, "");
|
|
207
|
+
const blockVars = parseBlockVars(facadeName);
|
|
200
208
|
|
|
201
209
|
return createRunContext({
|
|
202
210
|
isSdkInternal,
|
|
@@ -37,21 +37,26 @@ export type TriggerContext = {
|
|
|
37
37
|
export type TriggerFn = (tctx: TriggerContext) => boolean;
|
|
38
38
|
|
|
39
39
|
/** Content forms — what to write into a file when creating it.
|
|
40
|
-
* `tpl.vars` may be
|
|
41
|
-
*
|
|
42
|
-
|
|
40
|
+
* `tpl.vars` and `generate.fn` may be functions; when they are, the
|
|
41
|
+
* resolver calls them with the active `RunContext` (the same object
|
|
42
|
+
* triggers see as `tctx.ctx`), so lambdas read block identity via their
|
|
43
|
+
* argument rather than the module-global `getActiveRunContext()`. */
|
|
44
|
+
export type TplVarsLike = Record<string, string> | ((ctx: RunContext) => Record<string, string>);
|
|
43
45
|
export type ContentForm =
|
|
44
46
|
| { kind: "file"; path: string }
|
|
45
47
|
| { kind: "text"; value: string }
|
|
46
48
|
| { kind: "tpl"; path: string; vars: TplVarsLike }
|
|
47
|
-
| { kind: "generate"; fn: () => unknown }
|
|
49
|
+
| { kind: "generate"; fn: (ctx: RunContext) => unknown }
|
|
48
50
|
// A binary static asset (e.g. a logo PNG) copied verbatim from the
|
|
49
51
|
// template tree. Carried as raw bytes, never decoded to a string —
|
|
50
52
|
// only `seed` / `scaffold` consume it (via fs.writeBinary).
|
|
51
53
|
| { kind: "binary"; path: string };
|
|
52
54
|
|
|
53
|
-
/** Builder-body for a `managed` file (the content-rules lambda).
|
|
54
|
-
|
|
55
|
+
/** Builder-body for a `managed` file (the content-rules lambda). Receives the
|
|
56
|
+
* active `RunContext` — the same object triggers see as `tctx.ctx` and that
|
|
57
|
+
* `generate`/`tpl` lambdas receive — so the body reads block identity via its
|
|
58
|
+
* argument rather than the module-global `getActiveRunContext()`. */
|
|
59
|
+
export type ManagedBody = (ctx: RunContext) => void;
|
|
55
60
|
|
|
56
61
|
// --- Leaf primitives ---
|
|
57
62
|
|
|
@@ -110,7 +110,7 @@ function resolveContent(
|
|
|
110
110
|
form: ContentForm,
|
|
111
111
|
filePath: string,
|
|
112
112
|
templates: TemplateProvider | undefined,
|
|
113
|
-
|
|
113
|
+
ctx: RunContext,
|
|
114
114
|
): string {
|
|
115
115
|
switch (form.kind) {
|
|
116
116
|
case "text":
|
|
@@ -132,15 +132,15 @@ function resolveContent(
|
|
|
132
132
|
);
|
|
133
133
|
}
|
|
134
134
|
const raw = templates.textRead(form.path);
|
|
135
|
-
const vars = typeof form.vars === "function" ? form.vars() : form.vars;
|
|
135
|
+
const vars = typeof form.vars === "function" ? form.vars(ctx) : form.vars;
|
|
136
136
|
return substituteVars(raw, vars);
|
|
137
137
|
}
|
|
138
138
|
case "generate": {
|
|
139
|
-
const value = form.fn();
|
|
139
|
+
const value = form.fn(ctx);
|
|
140
140
|
if (filePath.endsWith(".json")) {
|
|
141
141
|
// Resolve `sdk:` sentinels in the generated dep sections so the
|
|
142
142
|
// initial content matches what the body rules re-assert.
|
|
143
|
-
resolveSdkSentinelsInJson(value, isSdkInternal);
|
|
143
|
+
resolveSdkSentinelsInJson(value, ctx.isSdkInternal);
|
|
144
144
|
// package.json stays fully expanded (oxfmt's package.json shape);
|
|
145
145
|
// every other .json collapses to oxfmt's generic-file shape.
|
|
146
146
|
return stringifyJson(value, { collapse: !filePath.endsWith("package.json") });
|
|
@@ -170,7 +170,7 @@ function writeLeafContent(
|
|
|
170
170
|
resolvedPath: string,
|
|
171
171
|
form: ContentForm,
|
|
172
172
|
templates: TemplateProvider | undefined,
|
|
173
|
-
|
|
173
|
+
ctx: RunContext,
|
|
174
174
|
): void {
|
|
175
175
|
if (form.kind === "binary") {
|
|
176
176
|
if (!templates) {
|
|
@@ -182,7 +182,7 @@ function writeLeafContent(
|
|
|
182
182
|
fs.writeBinary(resolvedPath, templates.staticReadBinary(form.path));
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
|
-
fs.write(resolvedPath, resolveContent(form, resolvedPath, templates,
|
|
185
|
+
fs.write(resolvedPath, resolveContent(form, resolvedPath, templates, ctx));
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
/** Dispatch managed-body parse → mutate → serialize by file extension /
|
|
@@ -292,11 +292,11 @@ function applyStructural(
|
|
|
292
292
|
dryRun: boolean,
|
|
293
293
|
templates: TemplateProvider | undefined,
|
|
294
294
|
initMode: boolean,
|
|
295
|
-
|
|
295
|
+
ctx: RunContext,
|
|
296
296
|
): Change | undefined {
|
|
297
297
|
const leaf = item.leaf;
|
|
298
298
|
if (leaf.kind === "fixed") {
|
|
299
|
-
const desired = resolveContent(leaf.content, item.resolvedPath, templates,
|
|
299
|
+
const desired = resolveContent(leaf.content, item.resolvedPath, templates, ctx);
|
|
300
300
|
const exists = fs.exists(item.resolvedPath);
|
|
301
301
|
if (!exists) {
|
|
302
302
|
if (!dryRun) fs.write(item.resolvedPath, desired);
|
|
@@ -320,7 +320,7 @@ function applyStructural(
|
|
|
320
320
|
if (leaf.kind === "scaffold") {
|
|
321
321
|
if (fs.exists(item.resolvedPath)) return undefined;
|
|
322
322
|
if (!dryRun) {
|
|
323
|
-
writeLeafContent(fs, item.resolvedPath, leaf.initial, templates,
|
|
323
|
+
writeLeafContent(fs, item.resolvedPath, leaf.initial, templates, ctx);
|
|
324
324
|
}
|
|
325
325
|
return {
|
|
326
326
|
scope: item.scope,
|
|
@@ -338,7 +338,7 @@ function applyStructural(
|
|
|
338
338
|
if (!initMode) return undefined;
|
|
339
339
|
if (fs.exists(item.resolvedPath)) return undefined;
|
|
340
340
|
if (!dryRun) {
|
|
341
|
-
writeLeafContent(fs, item.resolvedPath, leaf.initial, templates,
|
|
341
|
+
writeLeafContent(fs, item.resolvedPath, leaf.initial, templates, ctx);
|
|
342
342
|
}
|
|
343
343
|
return {
|
|
344
344
|
scope: item.scope,
|
|
@@ -383,7 +383,7 @@ function applyManaged(
|
|
|
383
383
|
templates: TemplateProvider | undefined,
|
|
384
384
|
registryLookup: ((packageName: string) => string | undefined) | undefined,
|
|
385
385
|
derivedPinLookup: ((catalogEntry: string) => string | undefined) | undefined,
|
|
386
|
-
|
|
386
|
+
ctx: RunContext,
|
|
387
387
|
): Change | undefined {
|
|
388
388
|
if (item.leaf.kind !== "managed") return undefined;
|
|
389
389
|
const leaf = item.leaf;
|
|
@@ -394,7 +394,7 @@ function applyManaged(
|
|
|
394
394
|
let created = false;
|
|
395
395
|
|
|
396
396
|
if (!exists) {
|
|
397
|
-
beforeRaw = resolveContent(leaf.initial, path, templates,
|
|
397
|
+
beforeRaw = resolveContent(leaf.initial, path, templates, ctx);
|
|
398
398
|
created = true;
|
|
399
399
|
} else {
|
|
400
400
|
beforeRaw = fs.read(path);
|
|
@@ -443,7 +443,7 @@ function executePass(
|
|
|
443
443
|
if (item.leaf.kind === "managed") continue;
|
|
444
444
|
const tctx1 = buildTriggerCtx(snap1, ctx, item.modulePath);
|
|
445
445
|
if (!passesTriggers(item, tctx1)) continue;
|
|
446
|
-
const change = applyStructural(item, fs, dryRun, templates, initMode, ctx
|
|
446
|
+
const change = applyStructural(item, fs, dryRun, templates, initMode, ctx);
|
|
447
447
|
if (change) changes.push(change);
|
|
448
448
|
}
|
|
449
449
|
|
|
@@ -462,7 +462,7 @@ function executePass(
|
|
|
462
462
|
templates,
|
|
463
463
|
registryLookup,
|
|
464
464
|
derivedPinLookup,
|
|
465
|
-
ctx
|
|
465
|
+
ctx,
|
|
466
466
|
);
|
|
467
467
|
if (change) changes.push(change);
|
|
468
468
|
}
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
enforceFieldOrder,
|
|
13
13
|
type RunContext,
|
|
14
14
|
} from "../engine/api";
|
|
15
|
-
import { getActiveRunContext } from "../engine/builders";
|
|
16
15
|
import { findModules, scopeDepMap } from "../engine/ctx";
|
|
17
16
|
import { canonicalPackageJsonOrder } from "./shared/key-order";
|
|
18
17
|
|
|
@@ -111,7 +110,7 @@ export function blockPackageJsonInitial(ctx: RunContext): Record<string, unknown
|
|
|
111
110
|
};
|
|
112
111
|
}
|
|
113
112
|
|
|
114
|
-
export function blockPackageJsonRules(): void {
|
|
113
|
+
export function blockPackageJsonRules(ctx: RunContext): void {
|
|
115
114
|
// type:"module" intentionally omitted: the facade's index.js is the
|
|
116
115
|
// CommonJS dev-block descriptor (module.exports / __dirname); ESM would
|
|
117
116
|
// break it. The facade has no TS sources to compile.
|
|
@@ -146,7 +145,7 @@ export function blockPackageJsonRules(): void {
|
|
|
146
145
|
// it in sync on refresh. `block.meta` is deliberately NOT touched here: it
|
|
147
146
|
// is an author-owned seed (set once in the init package.json), so refresh
|
|
148
147
|
// must never overwrite the author's title / description / logo.
|
|
149
|
-
ensureField("block.components", blockComponents(
|
|
148
|
+
ensureField("block.components", blockComponents(ctx));
|
|
150
149
|
|
|
151
150
|
enforceAlphabeticalOrder("dependencies");
|
|
152
151
|
enforceAlphabeticalOrder("devDependencies");
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// Workspace-scope deps in the body resolve from ctx.modules.
|
|
3
3
|
|
|
4
4
|
import { scope, fixed, managed, seed, file, binaryFile, generate } from "../engine/api";
|
|
5
|
-
import { getActiveRunContext } from "../engine/builders";
|
|
6
5
|
import { blockPackageJsonInitial, blockPackageJsonRules } from "./block-package-json";
|
|
7
6
|
|
|
8
7
|
export function blockRules(): void {
|
|
@@ -20,9 +19,9 @@ export function blockRules(): void {
|
|
|
20
19
|
|
|
21
20
|
managed(
|
|
22
21
|
"package.json",
|
|
23
|
-
generate(() => blockPackageJsonInitial(
|
|
24
|
-
() => {
|
|
25
|
-
blockPackageJsonRules();
|
|
22
|
+
generate((ctx) => blockPackageJsonInitial(ctx)),
|
|
23
|
+
(ctx) => {
|
|
24
|
+
blockPackageJsonRules(ctx);
|
|
26
25
|
},
|
|
27
26
|
);
|
|
28
27
|
});
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
// keys/paths). Each line is a one-way decision: every block converges.
|
|
13
13
|
|
|
14
14
|
import { scope, remove, managed, generate, removeScript, removeDep } from "../engine/api";
|
|
15
|
-
import { getActiveRunContext } from "../engine/builders";
|
|
16
15
|
import { rootPackageJsonInitial } from "./root-package-json";
|
|
17
16
|
|
|
18
17
|
export function testFrameworkMigration(): void {
|
|
@@ -50,7 +49,7 @@ export function legacyCleanup(): void {
|
|
|
50
49
|
remove(".prettierrc");
|
|
51
50
|
managed(
|
|
52
51
|
"package.json",
|
|
53
|
-
generate(() => rootPackageJsonInitial(
|
|
52
|
+
generate((ctx) => rootPackageJsonInitial(ctx)),
|
|
54
53
|
() => {
|
|
55
54
|
// `pretty` (prettier) is superseded by `fmt` (oxfmt); the standalone
|
|
56
55
|
// deps-updater is now part of block-tools.
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// (block author owns it).
|
|
4
4
|
|
|
5
5
|
import { scope, fixed, managed, seed, file, generate, when, whenFilesExist } from "../engine/api";
|
|
6
|
-
import { getActiveRunContext } from "../engine/builders";
|
|
7
6
|
import { modelPackageJsonInitial, modelPackageJsonRules } from "./model-package-json";
|
|
8
7
|
import { COLOCATED_TEST_GLOB } from "./shared/colocated-tests";
|
|
9
8
|
|
|
@@ -32,7 +31,7 @@ export function modelRules(): void {
|
|
|
32
31
|
|
|
33
32
|
managed(
|
|
34
33
|
"package.json",
|
|
35
|
-
generate(() => modelPackageJsonInitial(
|
|
34
|
+
generate((ctx) => modelPackageJsonInitial(ctx)),
|
|
36
35
|
() => {
|
|
37
36
|
modelPackageJsonRules();
|
|
38
37
|
},
|
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
ensureCatalogVersion,
|
|
29
29
|
pinCatalogToDependencyOf,
|
|
30
30
|
} from "../engine/api";
|
|
31
|
-
import { getActiveRunContext } from "../engine/builders";
|
|
32
31
|
import {
|
|
33
32
|
rootPnpmWorkspaceInitial,
|
|
34
33
|
SDK_CATALOG_PACKAGES,
|
|
@@ -53,8 +52,8 @@ export function rootCatalogBumpRules(): void {
|
|
|
53
52
|
// generator.
|
|
54
53
|
managed(
|
|
55
54
|
"pnpm-workspace.yaml",
|
|
56
|
-
generate(() => rootPnpmWorkspaceInitial(
|
|
57
|
-
() => {
|
|
55
|
+
generate((ctx) => rootPnpmWorkspaceInitial(ctx)),
|
|
56
|
+
(ctx) => {
|
|
58
57
|
// SDK families → npm latest, ADD-IF-ABSENT (seeds a migrated
|
|
59
58
|
// block's missing standard keys + overwrites the init
|
|
60
59
|
// placeholder).
|
|
@@ -76,7 +75,7 @@ export function rootCatalogBumpRules(): void {
|
|
|
76
75
|
}
|
|
77
76
|
// The python runenv pin is relevant only to software-bearing
|
|
78
77
|
// blocks; seed it add-if-absent when one is present.
|
|
79
|
-
if (
|
|
78
|
+
if (ctx.modules.some((m) => m.scope === "software")) {
|
|
80
79
|
ensureCatalogVersion(RUNENV_PYTHON, RUNENV_PYTHON_VERSION);
|
|
81
80
|
}
|
|
82
81
|
},
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Root-scope CI workflows — the block's GitHub Actions `build.yaml` and
|
|
2
|
+
// `mark-stable.yaml`. Engine-OWNED (`fixed`): rewritten verbatim on every
|
|
3
|
+
// refresh, generated from the block's identity (short name); everything else
|
|
4
|
+
// is a constant baked into the templates.
|
|
5
|
+
//
|
|
6
|
+
// Why fixed, not managed: previously each block carried a hand-copied
|
|
7
|
+
// workflow pinning the shared reusable workflow at `@v4` (a moving branch),
|
|
8
|
+
// so a breaking shared-CI change was absorbed silently and per-block edits
|
|
9
|
+
// drifted. Engine-owning the file makes the pin + job wiring centrally
|
|
10
|
+
// refreshable; the only per-block bits are derived (app-name, slug), so
|
|
11
|
+
// nothing author-tunable is clobbered.
|
|
12
|
+
//
|
|
13
|
+
// Standalone blocks only — `--sdk-internal` blocks (`etc/blocks/*`) carry no
|
|
14
|
+
// per-block workflow (the monorepo owns their CI), hence the
|
|
15
|
+
// `when(!isSdkInternal)` guard, matching the rest of the root scope.
|
|
16
|
+
|
|
17
|
+
import { scope, when, fixed, tpl } from "../engine/api";
|
|
18
|
+
|
|
19
|
+
/** Readable default CI label from a short name (`mixcr-clonotyping-2` →
|
|
20
|
+
* `Mixcr Clonotyping 2`). Dash/underscore words, title-cased. Human-curated
|
|
21
|
+
* casing (e.g. `MiXCR`) is not recoverable from the slug — this is a
|
|
22
|
+
* generated label, not the marketing name. */
|
|
23
|
+
function humanizeShortName(shortName: string): string {
|
|
24
|
+
return shortName
|
|
25
|
+
.split(/[-_]+/)
|
|
26
|
+
.filter((w) => w.length > 0)
|
|
27
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
28
|
+
.join(" ");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function rootCiRules(): void {
|
|
32
|
+
scope("root", () => {
|
|
33
|
+
when(
|
|
34
|
+
({ ctx }) => !ctx.isSdkInternal,
|
|
35
|
+
() => {
|
|
36
|
+
fixed(
|
|
37
|
+
".github/workflows/build.yaml",
|
|
38
|
+
tpl("workflows/build.tpl.yaml", (ctx) => {
|
|
39
|
+
const shortName = ctx.blockVars.shortName;
|
|
40
|
+
return {
|
|
41
|
+
appName: `Block: ${humanizeShortName(shortName)}`,
|
|
42
|
+
appNameSlug: `block-${shortName}`,
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
fixed(
|
|
47
|
+
".github/workflows/mark-stable.yaml",
|
|
48
|
+
tpl("workflows/mark-stable.tpl.yaml", (ctx) => ({
|
|
49
|
+
appName: `Block: ${humanizeShortName(ctx.blockVars.shortName)} - Mark Stable`,
|
|
50
|
+
})),
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// `when(({ ctx }) => !ctx.isSdkInternal, ...)` wrapper.
|
|
5
5
|
|
|
6
6
|
import { scope, when, fixed, managed, file, generate } from "../engine/api";
|
|
7
|
-
import { getActiveRunContext } from "../engine/builders";
|
|
8
7
|
import { rootPackageJsonInitial, rootPackageJsonRules } from "./root-package-json";
|
|
9
8
|
import { rootPnpmWorkspaceInitial, rootPnpmWorkspaceRules } from "./root-pnpm-workspace";
|
|
10
9
|
import { rootGitignoreRules } from "./root-gitignore";
|
|
@@ -19,7 +18,7 @@ export function rootRules(): void {
|
|
|
19
18
|
|
|
20
19
|
managed(
|
|
21
20
|
"package.json",
|
|
22
|
-
generate(() => rootPackageJsonInitial(
|
|
21
|
+
generate((ctx) => rootPackageJsonInitial(ctx)),
|
|
23
22
|
() => {
|
|
24
23
|
rootPackageJsonRules();
|
|
25
24
|
},
|
|
@@ -27,7 +26,7 @@ export function rootRules(): void {
|
|
|
27
26
|
|
|
28
27
|
managed(
|
|
29
28
|
"pnpm-workspace.yaml",
|
|
30
|
-
generate(() => rootPnpmWorkspaceInitial(
|
|
29
|
+
generate((ctx) => rootPnpmWorkspaceInitial(ctx)),
|
|
31
30
|
() => {
|
|
32
31
|
rootPnpmWorkspaceRules();
|
|
33
32
|
},
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
// `init` and never touched again — the block author owns them.
|
|
7
7
|
|
|
8
8
|
import { scope, managed, seed, generate, text } from "../engine/api";
|
|
9
|
-
import { getActiveRunContext } from "../engine/builders";
|
|
10
9
|
import { softwarePackageJsonInitial, softwarePackageJsonRules } from "./software-package-json";
|
|
11
10
|
|
|
12
11
|
const MAIN_PY_SEED = `import sys
|
|
@@ -24,7 +23,7 @@ export function softwareRules(): void {
|
|
|
24
23
|
|
|
25
24
|
managed(
|
|
26
25
|
"package.json",
|
|
27
|
-
generate(() => softwarePackageJsonInitial(
|
|
26
|
+
generate((ctx) => softwarePackageJsonInitial(ctx)),
|
|
28
27
|
() => {
|
|
29
28
|
softwarePackageJsonRules();
|
|
30
29
|
},
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Test-scope rules.
|
|
2
2
|
|
|
3
3
|
import { scope, fixed, managed, scaffold, seed, file, text, generate } from "../engine/api";
|
|
4
|
-
import { getActiveRunContext } from "../engine/builders";
|
|
5
4
|
import { testPackageJsonInitial, testPackageJsonRules } from "./test-package-json";
|
|
6
5
|
|
|
7
6
|
export function testRules(): void {
|
|
@@ -35,7 +34,7 @@ export function testRules(): void {
|
|
|
35
34
|
|
|
36
35
|
managed(
|
|
37
36
|
"package.json",
|
|
38
|
-
generate(() => testPackageJsonInitial(
|
|
37
|
+
generate((ctx) => testPackageJsonInitial(ctx)),
|
|
39
38
|
() => {
|
|
40
39
|
testPackageJsonRules();
|
|
41
40
|
},
|