@platforma-sdk/block-tools 2.10.18 → 2.11.0
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 +12 -12
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +391 -355
- 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/structure/rules/shared/retired-deps.d.ts +7 -0
- package/package.json +7 -7
- package/src/structure/CLAUDE.md +30 -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 +16 -18
- package/src/structure/rules/model-package-json.ts +4 -5
- 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-package-json.ts +8 -6
- package/src/structure/rules/root-pnpm-workspace.ts +4 -7
- package/src/structure/rules/root.ts +2 -4
- package/src/structure/rules/shared/retired-deps.ts +42 -0
- package/src/structure/rules/software.ts +1 -2
- package/src/structure/rules/test-package-json.ts +12 -9
- package/src/structure/rules/test.ts +5 -2
- package/src/structure/rules/ui-package-json.ts +15 -21
- package/src/structure/rules/ui.ts +3 -5
- package/src/structure/rules/workflow-package-json.ts +7 -3
- package/src/structure/rules/workflow.ts +20 -17
- package/src/structure/structure-definition.ts +2 -0
- package/src/structure/templates/static/test/.oxfmtrc.json +4 -0
- package/src/structure/templates/static/test/.oxlintrc.json +3 -0
- package/src/structure/templates/text/workflows/build.tpl.yaml +67 -0
- package/src/structure/templates/text/workflows/mark-stable.tpl.yaml +34 -0
|
@@ -82,12 +82,13 @@ export declare function binaryFile(path: string): ContentForm;
|
|
|
82
82
|
/** Content form: an inline literal string. */
|
|
83
83
|
export declare function text(value: string): ContentForm;
|
|
84
84
|
/** Content form: a text template (the structurer's `text/` templates) with
|
|
85
|
-
* `${var}` placeholders substituted from `vars`.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
*
|
|
90
|
-
|
|
85
|
+
* `${var}` placeholders substituted from `vars`. When `vars` is a function it
|
|
86
|
+
* receives the active {@link RunContext} at resolve time. */
|
|
87
|
+
export declare function tpl(path: string, vars: Record<string, string> | ((ctx: RunContext) => Record<string, string>)): ContentForm;
|
|
88
|
+
/** Content form: content computed at run time from the active
|
|
89
|
+
* {@link RunContext}. The return value is serialised as JSON / YAML by the
|
|
90
|
+
* target file's extension, or used verbatim if it is already a string. */
|
|
91
|
+
export declare function generate(fn: (ctx: RunContext) => unknown): ContentForm;
|
|
91
92
|
/** The active run's {@link BlockVars} (block name parts + software platform).
|
|
92
93
|
* Valid only during a run (inside a generator or managed body); throws
|
|
93
94
|
* otherwise. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RunContext, Scope } from './api';
|
|
2
|
-
import { TriggerContext } from './ir';
|
|
2
|
+
import { ManagedBody, TriggerContext } from './ir';
|
|
3
3
|
import { JsonObject } from './parsers/json';
|
|
4
4
|
import { YamlDocument } from './parsers/yaml';
|
|
5
5
|
export type { JsonObject };
|
|
@@ -30,7 +30,7 @@ export type WithManagedOpts = {
|
|
|
30
30
|
/** Engine-internal: run `body` with `obj` as the active JSON state so the
|
|
31
31
|
* JSON builders mutate it; returns the mutated object. Exported for tests —
|
|
32
32
|
* the runner calls this for managed `.json` files. */
|
|
33
|
-
export declare function withManagedBody(obj: JsonObject, body:
|
|
33
|
+
export declare function withManagedBody(obj: JsonObject, body: ManagedBody, opts?: WithManagedOpts): JsonObject;
|
|
34
34
|
export type WithManagedYamlOpts = WithManagedOpts & {
|
|
35
35
|
/** Sync accessor for prefetched latest versions. Required for
|
|
36
36
|
* `ensureCatalogLatest` to take effect. */
|
|
@@ -41,10 +41,10 @@ export type WithManagedYamlOpts = WithManagedOpts & {
|
|
|
41
41
|
};
|
|
42
42
|
/** Engine-internal: run `body` with `doc` as the active YAML state (for
|
|
43
43
|
* `pnpm-workspace.yaml`); returns the mutated document. Exported for tests. */
|
|
44
|
-
export declare function withManagedYaml(doc: YamlDocument, body:
|
|
44
|
+
export declare function withManagedYaml(doc: YamlDocument, body: ManagedBody, opts?: WithManagedYamlOpts): YamlDocument;
|
|
45
45
|
/** Engine-internal: run `body` with `lines` as the active line-based state
|
|
46
46
|
* (for `.gitignore`); returns the mutated lines. Exported for tests. */
|
|
47
|
-
export declare function withManagedLines(lines: string[], body:
|
|
47
|
+
export declare function withManagedLines(lines: string[], body: ManagedBody, opts?: WithManagedOpts): string[];
|
|
48
48
|
/** Set the value at `jsonPath` in the active managed JSON object. */
|
|
49
49
|
export declare function ensureField(jsonPath: string, value: unknown): void;
|
|
50
50
|
/** Remove the field at `jsonPath`. With `predicate`, only removes when
|
|
@@ -24,9 +24,11 @@ export type TriggerContext = {
|
|
|
24
24
|
};
|
|
25
25
|
export type TriggerFn = (tctx: TriggerContext) => boolean;
|
|
26
26
|
/** Content forms — what to write into a file when creating it.
|
|
27
|
-
* `tpl.vars` may be
|
|
28
|
-
*
|
|
29
|
-
|
|
27
|
+
* `tpl.vars` and `generate.fn` may be functions; when they are, the
|
|
28
|
+
* resolver calls them with the active `RunContext` (the same object
|
|
29
|
+
* triggers see as `tctx.ctx`), so lambdas read block identity via their
|
|
30
|
+
* argument rather than the module-global `getActiveRunContext()`. */
|
|
31
|
+
export type TplVarsLike = Record<string, string> | ((ctx: RunContext) => Record<string, string>);
|
|
30
32
|
export type ContentForm = {
|
|
31
33
|
kind: "file";
|
|
32
34
|
path: string;
|
|
@@ -39,13 +41,16 @@ export type ContentForm = {
|
|
|
39
41
|
vars: TplVarsLike;
|
|
40
42
|
} | {
|
|
41
43
|
kind: "generate";
|
|
42
|
-
fn: () => unknown;
|
|
44
|
+
fn: (ctx: RunContext) => unknown;
|
|
43
45
|
} | {
|
|
44
46
|
kind: "binary";
|
|
45
47
|
path: string;
|
|
46
48
|
};
|
|
47
|
-
/** Builder-body for a `managed` file (the content-rules lambda).
|
|
48
|
-
|
|
49
|
+
/** Builder-body for a `managed` file (the content-rules lambda). Receives the
|
|
50
|
+
* active `RunContext` — the same object triggers see as `tctx.ctx` and that
|
|
51
|
+
* `generate`/`tpl` lambdas receive — so the body reads block identity via its
|
|
52
|
+
* argument rather than the module-global `getActiveRunContext()`. */
|
|
53
|
+
export type ManagedBody = (ctx: RunContext) => void;
|
|
49
54
|
export type FixedItem = {
|
|
50
55
|
kind: "fixed";
|
|
51
56
|
path: string;
|
|
@@ -6,4 +6,4 @@ import { RunContext } from '../engine/api';
|
|
|
6
6
|
* workflow → pl-tengo, model → build-model, ui → ts-builder. */
|
|
7
7
|
export declare function blockComponents(ctx: RunContext): Record<string, string>;
|
|
8
8
|
export declare function blockPackageJsonInitial(ctx: RunContext): Record<string, unknown>;
|
|
9
|
-
export declare function blockPackageJsonRules(): void;
|
|
9
|
+
export declare function blockPackageJsonRules(ctx: RunContext): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function rootCiRules(): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const RETIRED_TOOLCHAIN_DEPS: readonly string[];
|
|
2
|
+
/** Drop every retired toolchain dep from the current package.json scope.
|
|
3
|
+
* Idempotent (a `removeDep` on an absent key is a no-op), so it is safe to
|
|
4
|
+
* call uniformly from every package.json rules body regardless of which deps
|
|
5
|
+
* a given scope ever carried. Pairs with the catalog-side removal in
|
|
6
|
+
* `rootPnpmWorkspaceRules`. */
|
|
7
|
+
export declare function removeRetiredToolchainDeps(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platforma-sdk/block-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
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-
|
|
37
|
+
"@milaboratories/pl-model-backend": "1.4.7",
|
|
38
38
|
"@milaboratories/pl-model-common": "1.46.1",
|
|
39
|
+
"@milaboratories/pl-model-middle-layer": "1.30.6",
|
|
39
40
|
"@milaboratories/resolve-helper": "1.1.3",
|
|
40
|
-
"@milaboratories/pl-model-backend": "1.4.7",
|
|
41
41
|
"@milaboratories/ts-helpers": "1.8.3",
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
42
|
+
"@platforma-sdk/blocks-deps-updater": "2.2.0",
|
|
43
|
+
"@milaboratories/ts-helpers-oclif": "1.1.42"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
@@ -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",
|
|
59
60
|
"@milaboratories/oclif-index": "1.1.1",
|
|
60
|
-
"@milaboratories/ts-configs": "1.2.3",
|
|
61
61
|
"@milaboratories/ts-builder": "1.5.2",
|
|
62
|
-
"@milaboratories/
|
|
62
|
+
"@milaboratories/ts-configs": "1.2.3"
|
|
63
63
|
},
|
|
64
64
|
"oclif": {
|
|
65
65
|
"bin": "block-tools",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# structurer — mental model
|
|
2
|
+
|
|
3
|
+
A declarative reconciler for a block's scaffolding. `rules/` DECLARE the
|
|
4
|
+
canonical end-state of every non-source file in a block (package.json, configs,
|
|
5
|
+
tsconfig, scripts, catalog); `engine/` reconciles any block toward that state.
|
|
6
|
+
To change what "canonical" means, edit `rules/`; the engine is block-agnostic.
|
|
7
|
+
|
|
8
|
+
## Boundaries
|
|
9
|
+
|
|
10
|
+
- `engine/` — generic reconciliation primitives, module discovery, IR. Knows nothing about blocks.
|
|
11
|
+
- `rules/` — the only home of block-specific layout knowledge. `templates/` holds the verbatim file bodies the rules reference.
|
|
12
|
+
- Owns: the shape of a block's scaffolding. Is NOT a build tool, and NOT the author of a block's source — seeded/scaffolded files become author-owned once written.
|
|
13
|
+
|
|
14
|
+
## Reading posture
|
|
15
|
+
|
|
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
|
+
- 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.
|
|
19
|
+
|
|
20
|
+
## Invariants
|
|
21
|
+
|
|
22
|
+
- Fixpoint: `refresh` on an already-canonical block makes zero changes. Every rule must converge.
|
|
23
|
+
- Generators emit oxfmt-clean output (the `enforce*` calls mirror oxfmt's order), so build→check passes with no prior `fmt`.
|
|
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.
|
|
26
|
+
|
|
27
|
+
## Gotchas
|
|
28
|
+
|
|
29
|
+
- A rule change also changes the canonical for `etc/blocks/*`: refresh them in the SAME PR (`pnpm run check-blocks`) or CI fails.
|
|
30
|
+
- Dep/peer changes need a `pnpm i` lockfile sync — pnpm records peers and per-importer deps in pnpm-lock.yaml; frozen-install CI fails otherwise.
|
|
@@ -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
|
});
|