@platforma-sdk/block-tools 2.11.3 → 2.11.5
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 +634 -580
- package/dist/cli.mjs.map +1 -1
- package/dist/config-BmJWc8sR.js +3 -0
- package/dist/config-BmJWc8sR.js.map +1 -0
- package/dist/{config-DpfTzcgG.mjs → config-CiC347sB.mjs} +453 -398
- package/dist/config-CiC347sB.mjs.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +15 -15
- package/dist/structure/engine/discovery.d.ts +2 -3
- package/dist/structure/rules/block-package-json.d.ts +3 -3
- package/dist/structure/rules/shared/pascal-case.d.ts +1 -0
- package/dist/v2/index.d.ts +1 -0
- package/dist/v2/model/block_components.d.ts +11 -0
- package/dist/v2/model/block_meta.d.ts +7 -0
- package/dist/v2/registry/schema_public.d.ts +3600 -3600
- package/dist/v2/resolve_to_registry.d.ts +37 -0
- package/dist/v2/source_package.d.ts +12 -0
- package/package.json +8 -8
- package/src/cmd/publish.test.ts +35 -0
- package/src/cmd/publish.ts +20 -0
- package/src/structure/__tests__/discovery-shortname.test.ts +3 -0
- package/src/structure/__tests__/facade-end-state.test.ts +195 -0
- package/src/structure/__tests__/model-package-json.snapshot.test.ts +1 -1
- package/src/structure/engine/__tests__/discovery.test.ts +22 -26
- package/src/structure/engine/discovery.ts +12 -30
- package/src/structure/rules/block-package-json.ts +146 -64
- package/src/structure/rules/block.ts +40 -5
- package/src/structure/rules/model-package-json.ts +6 -1
- package/src/structure/rules/shared/pascal-case.ts +16 -0
- package/src/structure/rules/software-package-json.ts +6 -1
- package/src/structure/rules/test-package-json.ts +5 -1
- package/src/structure/rules/ui-package-json.ts +5 -1
- package/src/structure/rules/workflow-package-json.ts +5 -1
- package/src/structure/templates/static/block/src/AGENTS.ts +5 -0
- package/src/structure/templates/static/block/src/agents-extra.ts +8 -0
- package/src/structure/templates/static/block/src/block-extra.ts +9 -0
- package/src/structure/templates/static/block/tsconfig.json +4 -0
- package/src/structure/templates/text/block/src/index.tpl.ts +55 -0
- package/src/v2/from_pack_v2.test.ts +142 -0
- package/src/v2/index.ts +1 -0
- package/src/v2/model/block_components.ts +33 -0
- package/src/v2/model/block_meta.ts +18 -0
- package/src/v2/resolve_to_registry.ts +84 -0
- package/src/v2/source_package.ts +34 -1
- package/dist/config-DpfTzcgG.mjs.map +0 -1
- package/dist/config-xxVy3IQR.js +0 -3
- package/dist/config-xxVy3IQR.js.map +0 -1
- package/src/structure/templates/static/block/index.d.ts +0 -6
- package/src/structure/templates/static/block/index.js +0 -8
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { BlockPackDevV2, BlockPackFromPackV2, BlockPackFromRegistryV2, BlockPackId } from '@milaboratories/pl-model-middle-layer';
|
|
2
|
+
/** File written by `block-tools publish` into `block-pack/`, carrying the
|
|
3
|
+
* registry coordinates of the published version. Holds only the immutable
|
|
4
|
+
* coordinates (CDN serve URL + version id). No `channel`: channels are moving
|
|
5
|
+
* pointers in the registry — a version can be promoted/moved later — so the
|
|
6
|
+
* channel at publish time is transient state we can't pin here. Update-checking
|
|
7
|
+
* resolves the channel at read time (`BlockPackFromRegistryV2.channel` is
|
|
8
|
+
* optional; the watcher defaults stable → any). */
|
|
9
|
+
export declare const PublishedCoordsFileName = "published.json";
|
|
10
|
+
/** Registry coordinates persisted in `block-pack/published.json`. */
|
|
11
|
+
export interface PublishedCoords {
|
|
12
|
+
/** CDN serve URL the block is fetched from (not the S3 upload bucket). */
|
|
13
|
+
registryUrl: string;
|
|
14
|
+
id: BlockPackId;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Builds the `published.json` payload. Pure (no filesystem) so it is unit
|
|
18
|
+
* testable.
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildPublishedCoords(args: {
|
|
21
|
+
registryUrl: string;
|
|
22
|
+
id: BlockPackId;
|
|
23
|
+
}): PublishedCoords;
|
|
24
|
+
/** Writes `block-pack/published.json` into the given block-pack folder. */
|
|
25
|
+
export declare function writePublishedCoords(blockPackDir: string, coords: PublishedCoords): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Resolves a local block locator (`dev-v2` or `from-pack-v2`) to its registry
|
|
28
|
+
* coordinates by reading `published.json`. `published.json` lives inside the
|
|
29
|
+
* block-pack directory (written there by `block-tools publish`), so for
|
|
30
|
+
* `from-pack-v2` we read it directly from `packUrl` (converted to a path at this
|
|
31
|
+
* Node boundary) — the structurer owns the layout, we do not re-derive it. For a
|
|
32
|
+
* `dev-v2` block folder it sits under
|
|
33
|
+
* `<folder>/block-pack/`. The facade's engine-generated `from-pack-v2`
|
|
34
|
+
* BlockPointer feeds this so a consumed block can be installed via the registry
|
|
35
|
+
* path. Throws when the block was never published.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveToRegistry(locator: BlockPackDevV2 | BlockPackFromPackV2): Promise<BlockPackFromRegistryV2>;
|
|
@@ -6,3 +6,15 @@ export declare function parsePackageName(packageName: string): Pick<BlockPackId,
|
|
|
6
6
|
export declare function tryLoadPackDescription(moduleRoot: string, logger?: MiLogger): Promise<BlockPackDescriptionAbsolute | undefined>;
|
|
7
7
|
export declare function loadPackDescriptionRaw(moduleRoot: string): Promise<BlockPackDescriptionRaw>;
|
|
8
8
|
export declare function loadPackDescription(moduleRoot: string): Promise<BlockPackDescriptionAbsolute>;
|
|
9
|
+
/** Folder name (under a consumed package root) holding the v2 block-pack artifacts. */
|
|
10
|
+
export declare const BlockPackFolderName = "block-pack";
|
|
11
|
+
/**
|
|
12
|
+
* Loads a block-pack description from a block-pack directory's `manifest.json`,
|
|
13
|
+
* resolving every relative entry against that directory to an absolute
|
|
14
|
+
* filesystem path. Takes the pack directory directly (the `from-pack-v2`
|
|
15
|
+
* pointer's `packUrl`) — the structurer owns the layout, so this does not
|
|
16
|
+
* re-derive `<root>/block-pack`. Counterpart to `loadPackDescription`, which
|
|
17
|
+
* reads the authoring `block` field from `package.json` and resolves against
|
|
18
|
+
* source.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadPackDescriptionFromManifest(blockPackDir: string): Promise<BlockPackDescriptionAbsolute>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platforma-sdk/block-tools",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.5",
|
|
4
4
|
"description": "Utility to manipulate Platforma Blocks and Block Registry",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
"undici": "~7.16.0",
|
|
34
34
|
"yaml": "^2.8.0",
|
|
35
35
|
"zod": "~3.25.76",
|
|
36
|
-
"@milaboratories/pl-
|
|
37
|
-
"@milaboratories/pl-model-middle-layer": "1.30.
|
|
38
|
-
"@milaboratories/pl-model-backend": "1.4.8",
|
|
36
|
+
"@milaboratories/pl-model-backend": "1.4.9",
|
|
37
|
+
"@milaboratories/pl-model-middle-layer": "1.30.9",
|
|
39
38
|
"@milaboratories/pl-model-common": "1.46.2",
|
|
40
|
-
"@milaboratories/resolve-helper": "1.1.3",
|
|
41
39
|
"@platforma-sdk/blocks-deps-updater": "2.2.0",
|
|
42
|
-
"@milaboratories/
|
|
40
|
+
"@milaboratories/pl-http": "1.2.4",
|
|
41
|
+
"@milaboratories/ts-helpers": "1.8.3",
|
|
42
|
+
"@milaboratories/resolve-helper": "1.1.3"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"vite-plugin-dts": "^4.5.3",
|
|
56
56
|
"vitest": "^4.1.3",
|
|
57
57
|
"@milaboratories/build-configs": "2.0.0",
|
|
58
|
-
"@milaboratories/ts-
|
|
59
|
-
"@milaboratories/ts-
|
|
58
|
+
"@milaboratories/ts-builder": "1.6.0",
|
|
59
|
+
"@milaboratories/ts-configs": "1.3.0"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "vite build",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { test, expect } from "vitest";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import fsp from "node:fs/promises";
|
|
5
|
+
import { publishCommand } from "./publish";
|
|
6
|
+
|
|
7
|
+
function option(cmd: ReturnType<typeof publishCommand>, long: string) {
|
|
8
|
+
return cmd.options.find((o) => o.long === long);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
test("publish keeps --registry required and adds a required --registry-serve-url", () => {
|
|
12
|
+
const cmd = publishCommand();
|
|
13
|
+
expect(option(cmd, "--registry")?.mandatory).toBe(true);
|
|
14
|
+
expect(option(cmd, "--registry-serve-url")?.mandatory).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("publish errors when the serve URL is absent", async () => {
|
|
18
|
+
// A real manifest file so the action's read never runs — parsing must reject
|
|
19
|
+
// on the missing required --registry-serve-url before any registry I/O.
|
|
20
|
+
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), "publish-flags-"));
|
|
21
|
+
const manifestPath = path.join(dir, "manifest.json");
|
|
22
|
+
await fsp.writeFile(manifestPath, "{}");
|
|
23
|
+
|
|
24
|
+
// Clear the env binding so the mandatory check, not the environment, decides.
|
|
25
|
+
const prev = process.env.PL_REGISTRY_SERVE_URL;
|
|
26
|
+
delete process.env.PL_REGISTRY_SERVE_URL;
|
|
27
|
+
try {
|
|
28
|
+
const cmd = publishCommand().exitOverride();
|
|
29
|
+
await expect(
|
|
30
|
+
cmd.parseAsync(["-r", "s3://bucket", "-m", manifestPath], { from: "user" }),
|
|
31
|
+
).rejects.toThrow(/registry-serve-url/i);
|
|
32
|
+
} finally {
|
|
33
|
+
if (prev !== undefined) process.env.PL_REGISTRY_SERVE_URL = prev;
|
|
34
|
+
}
|
|
35
|
+
});
|
package/src/cmd/publish.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@milaboratories/pl-model-middle-layer";
|
|
10
10
|
import { storageByUrl } from "../io/storage";
|
|
11
11
|
import { BlockRegistryV2 } from "../v2/registry/registry";
|
|
12
|
+
import { buildPublishedCoords, writePublishedCoords } from "../v2/resolve_to_registry";
|
|
12
13
|
import path from "node:path";
|
|
13
14
|
|
|
14
15
|
function simpleDeepMerge<T extends Record<string, unknown>>(
|
|
@@ -45,6 +46,14 @@ export function publishCommand(): Command {
|
|
|
45
46
|
.env("PL_REGISTRY")
|
|
46
47
|
.makeOptionMandatory(),
|
|
47
48
|
);
|
|
49
|
+
cmd.addOption(
|
|
50
|
+
new Option(
|
|
51
|
+
"--registry-serve-url <url>",
|
|
52
|
+
"CDN serve URL written into block-pack/published.json (npm-consumed blocks)",
|
|
53
|
+
)
|
|
54
|
+
.env("PL_REGISTRY_SERVE_URL")
|
|
55
|
+
.makeOptionMandatory(),
|
|
56
|
+
);
|
|
48
57
|
cmd.option("-m, --manifest <path>", "manifest file path", `./block-pack/${ManifestFileName}`);
|
|
49
58
|
cmd.option("-v, --version-override <path>", "override package version");
|
|
50
59
|
cmd.addOption(
|
|
@@ -90,6 +99,17 @@ export function publishCommand(): Command {
|
|
|
90
99
|
await registry.addPackageToChannel(manifest.description.id, StableChannel);
|
|
91
100
|
}
|
|
92
101
|
|
|
102
|
+
// Write registry coordinates next to the manifest so npm consumers can
|
|
103
|
+
// resolve the published block via `resolveToRegistry`. Written before
|
|
104
|
+
// returning so `npm publish`'s tarball generation (after prepublishOnly)
|
|
105
|
+
// picks it up.
|
|
106
|
+
const coords = buildPublishedCoords({
|
|
107
|
+
registryUrl: flags.registryServeUrl,
|
|
108
|
+
id: manifest.description.id,
|
|
109
|
+
});
|
|
110
|
+
await writePublishedCoords(manifestRoot, coords);
|
|
111
|
+
logger.info(`Wrote published coordinates to ${path.resolve(manifestRoot, "published.json")}`);
|
|
112
|
+
|
|
93
113
|
if (flags.refresh) await registry.updateIfNeeded();
|
|
94
114
|
});
|
|
95
115
|
|
|
@@ -36,10 +36,13 @@ describe("DISCOVERY: shortName from the block package name", () => {
|
|
|
36
36
|
// Nameless root — the standalone shape; classified `root` by path.
|
|
37
37
|
"package.json": "{}",
|
|
38
38
|
// Legacy block package: name IS the bare facade, no `.block` suffix.
|
|
39
|
+
// Classified `block` via its top-level `block` field (every real block —
|
|
40
|
+
// legacy shim or migrated facade — carries one).
|
|
39
41
|
"block/package.json": JSON.stringify({
|
|
40
42
|
name: "@platforma-open/test-org.legacy",
|
|
41
43
|
files: ["index.d.ts", "index.js"],
|
|
42
44
|
dependencies: { "@platforma-sdk/block-tools": "*" },
|
|
45
|
+
block: { components: {}, meta: {} },
|
|
43
46
|
}),
|
|
44
47
|
});
|
|
45
48
|
const ctx = discoverRunContext({ fs, isSdkInternal: false });
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Facade end-state coverage — the slim `block/` the structurer emits and
|
|
2
|
+
// enforces: block-scope detection by the top-level `block` field, the four-file
|
|
3
|
+
// `src/` surface + facade `tsconfig.json`, the slim publishable `package.json`,
|
|
4
|
+
// sibling privacy, and legacy-shim cleanup.
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect } from "vitest";
|
|
7
|
+
import type { BlockVars, RunContext } from "../engine/api";
|
|
8
|
+
import { classifyOne, type WorkspacePackage } from "../engine/discovery";
|
|
9
|
+
import { STRUCTURE } from "../structure-definition";
|
|
10
|
+
import { run as engineRun } from "../engine/runner";
|
|
11
|
+
import { defaultTemplateProvider, simulateInit } from "../engine/testing";
|
|
12
|
+
|
|
13
|
+
const TEMPLATES = defaultTemplateProvider();
|
|
14
|
+
|
|
15
|
+
const VARS: BlockVars = {
|
|
16
|
+
facadeName: "@platforma-open/test-org.demo",
|
|
17
|
+
baseName: "test-org.demo",
|
|
18
|
+
npmOrg: "@platforma-open",
|
|
19
|
+
orgScope: "test-org",
|
|
20
|
+
shortName: "demo",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function pkg(path: string, pkgJson: Record<string, unknown>): WorkspacePackage {
|
|
24
|
+
return { path, pkg: pkgJson };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("facade: block-scope detection by the top-level `block` field", () => {
|
|
28
|
+
test("a package with a top-level `block` field is classified `block`", () => {
|
|
29
|
+
expect(classifyOne(pkg("block", { name: "x", block: { components: {} } }))).toBe("block");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("the `block` field wins over the model shape (the collision the order guards)", () => {
|
|
33
|
+
// A migrated facade carries `@platforma-sdk/model` (devDep) + `main`, which
|
|
34
|
+
// matches the model rule — the `block` field must take precedence.
|
|
35
|
+
expect(
|
|
36
|
+
classifyOne(
|
|
37
|
+
pkg("block", {
|
|
38
|
+
name: "x",
|
|
39
|
+
main: "./dist/index.js",
|
|
40
|
+
devDependencies: { "@platforma-sdk/model": "catalog:" },
|
|
41
|
+
block: { components: {} },
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
).toBe("block");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("a model-shaped package WITHOUT a `block` field is still `model`, not `block`", () => {
|
|
48
|
+
expect(
|
|
49
|
+
classifyOne(
|
|
50
|
+
pkg("model", {
|
|
51
|
+
name: "x.model",
|
|
52
|
+
main: "dist/index.cjs",
|
|
53
|
+
dependencies: { "@platforma-sdk/model": "catalog:" },
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
).toBe("model");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("facade: four-file source surface + tsconfig", () => {
|
|
61
|
+
test("init emits the 4 src files + tsconfig, and no legacy shim", () => {
|
|
62
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
63
|
+
for (const f of [
|
|
64
|
+
"block/src/index.ts",
|
|
65
|
+
"block/src/AGENTS.ts",
|
|
66
|
+
"block/src/block-extra.ts",
|
|
67
|
+
"block/src/agents-extra.ts",
|
|
68
|
+
"block/tsconfig.json",
|
|
69
|
+
]) {
|
|
70
|
+
expect(fs.exists(f), `expected ${f} to be emitted`).toBe(true);
|
|
71
|
+
}
|
|
72
|
+
expect(fs.exists("block/index.js")).toBe(false);
|
|
73
|
+
expect(fs.exists("block/index.d.ts")).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("index.ts carries the per-block model import path and PascalName aliases", () => {
|
|
77
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
78
|
+
const index = fs.read("block/src/index.ts");
|
|
79
|
+
expect(index).toContain('import { platforma } from "@platforma-open/test-org.demo.model"');
|
|
80
|
+
expect(index).toContain("export type DemoBlockContract = BlockContract;");
|
|
81
|
+
expect(index).toContain("export const DemoBlockPointer = BlockPointer;");
|
|
82
|
+
expect(index).toContain('export * from "./block-extra"');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("tsconfig extends the facade preset (singular `block/facade`)", () => {
|
|
86
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
87
|
+
const tsconfig = JSON.parse(fs.read("block/tsconfig.json"));
|
|
88
|
+
expect(tsconfig.extends).toBe("@milaboratories/ts-configs/block/facade");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("facade: slim `package.json` end-state", () => {
|
|
93
|
+
test("is slim and carries the facade fields, scripts, and sibling devDeps", () => {
|
|
94
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
95
|
+
const p = JSON.parse(fs.read("block/package.json"));
|
|
96
|
+
|
|
97
|
+
// Slim invariant: zero runtime dependencies.
|
|
98
|
+
expect(p.dependencies).toEqual({});
|
|
99
|
+
|
|
100
|
+
expect(p.type).toBe("module");
|
|
101
|
+
expect(p.files).toEqual(["dist", "block-pack"]);
|
|
102
|
+
expect(p.main).toBe("./dist/index.js");
|
|
103
|
+
expect(p.module).toBe("./dist/index.js");
|
|
104
|
+
expect(p.types).toBe("./dist/index.d.ts");
|
|
105
|
+
expect(p.exports).toEqual({
|
|
106
|
+
".": {
|
|
107
|
+
sources: "./src/index.ts",
|
|
108
|
+
types: "./dist/index.d.ts",
|
|
109
|
+
default: "./dist/index.js",
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(Object.keys(p.scripts).sort()).toEqual(["build", "check", "do-pack", "prepublishOnly"]);
|
|
114
|
+
expect(p.scripts.build).toBe("ts-builder build --target block-facade && block-tools pack");
|
|
115
|
+
expect(p.scripts.check).toBe("ts-builder type-check --target block-facade");
|
|
116
|
+
|
|
117
|
+
// Siblings are build-time devDeps (workspace links), never runtime deps.
|
|
118
|
+
expect(p.devDependencies["@platforma-open/test-org.demo.model"]).toBe("workspace:*");
|
|
119
|
+
expect(p.devDependencies["@platforma-open/test-org.demo.ui"]).toBe("workspace:*");
|
|
120
|
+
expect(p.devDependencies["@platforma-open/test-org.demo.workflow"]).toBe("workspace:*");
|
|
121
|
+
|
|
122
|
+
// Author-owned seed + engine-managed components both present.
|
|
123
|
+
expect(p.block.components).toBeTruthy();
|
|
124
|
+
expect(p.block.meta).toBeTruthy();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("facade: `prepublishOnly` publish target branches on isSdkInternal", () => {
|
|
129
|
+
const AUX_DEV =
|
|
130
|
+
"block-tools publish -r s3://milab-euce1-prod-pkgs-s3-block-registry/aux/dev/?region=eu-central-1 --registry-serve-url https://aux-blocks.pl-open.science";
|
|
131
|
+
|
|
132
|
+
test("in-SDK blocks publish to the aux/dev registry, regardless of org", () => {
|
|
133
|
+
// `@milaboratories` would route to production via ORG_PUBLISH_TARGETS — the
|
|
134
|
+
// isSdkInternal branch must override that and send it to aux/dev.
|
|
135
|
+
const vars: BlockVars = {
|
|
136
|
+
facadeName: "@milaboratories/test-org.demo",
|
|
137
|
+
baseName: "test-org.demo",
|
|
138
|
+
npmOrg: "@milaboratories",
|
|
139
|
+
orgScope: "test-org",
|
|
140
|
+
shortName: "demo",
|
|
141
|
+
};
|
|
142
|
+
const { fs } = simulateInit({ vars, isSdkInternal: true });
|
|
143
|
+
const p = JSON.parse(fs.read("block/package.json"));
|
|
144
|
+
expect(p.scripts.prepublishOnly).toBe(AUX_DEV);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("standalone blocks publish to their per-org production target", () => {
|
|
148
|
+
const open = simulateInit({ vars: VARS });
|
|
149
|
+
expect(JSON.parse(open.fs.read("block/package.json")).scripts.prepublishOnly).toBe(
|
|
150
|
+
"block-tools publish -r s3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1 --registry-serve-url https://blocks.pl-open.science",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const milab = simulateInit({
|
|
154
|
+
vars: {
|
|
155
|
+
facadeName: "@milaboratories/test-org.demo",
|
|
156
|
+
baseName: "test-org.demo",
|
|
157
|
+
npmOrg: "@milaboratories",
|
|
158
|
+
orgScope: "test-org",
|
|
159
|
+
shortName: "demo",
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
expect(JSON.parse(milab.fs.read("block/package.json")).scripts.prepublishOnly).toBe(
|
|
163
|
+
"block-tools publish -r s3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1 --registry-serve-url https://block.registry.platforma.bio/releases",
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("facade: sibling package privacy", () => {
|
|
169
|
+
test("model / ui / workflow / test are `private` with no `version`", () => {
|
|
170
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
171
|
+
for (const scope of ["model", "ui", "workflow", "test"]) {
|
|
172
|
+
const p = JSON.parse(fs.read(`${scope}/package.json`));
|
|
173
|
+
expect(p.private, `${scope} must be private`).toBe(true);
|
|
174
|
+
expect("version" in p, `${scope} must not carry a version`).toBe(false);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("facade: legacy shim cleanup on refresh", () => {
|
|
180
|
+
test("a refresh removes a pre-existing block/index.js + index.d.ts, then is idempotent", () => {
|
|
181
|
+
const { fs, ctx } = simulateInit({ vars: VARS });
|
|
182
|
+
// Inject the legacy boilerplate shim a pre-facade block would carry.
|
|
183
|
+
fs.write("block/index.js", "module.exports = { blockSpec: {} };\n");
|
|
184
|
+
fs.write("block/index.d.ts", "export declare const blockSpec: unknown;\n");
|
|
185
|
+
|
|
186
|
+
// Default-mode refresh (not dry-run): the engine's post-run recheck throws
|
|
187
|
+
// RecheckError if a second pass would change anything — so this asserts
|
|
188
|
+
// convergence too.
|
|
189
|
+
const refreshCtx: RunContext = { ...ctx, dryRun: false };
|
|
190
|
+
expect(() => engineRun(STRUCTURE, fs, refreshCtx, { templates: TEMPLATES })).not.toThrow();
|
|
191
|
+
|
|
192
|
+
expect(fs.exists("block/index.js")).toBe(false);
|
|
193
|
+
expect(fs.exists("block/index.d.ts")).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -9,7 +9,7 @@ import type { BlockVars } from "../engine/api";
|
|
|
9
9
|
|
|
10
10
|
const EXPECTED_MODEL_PACKAGE_JSON = `{
|
|
11
11
|
"name": "@platforma-open/test-org.demo.model",
|
|
12
|
-
"
|
|
12
|
+
"private": true,
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "dist/index.cjs",
|
|
15
15
|
"module": "dist/index.js",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Workspace-package classification — covers
|
|
2
|
-
//
|
|
1
|
+
// Workspace-package classification — covers the detection rules plus the
|
|
2
|
+
// block-scope override and the unclassified-error path.
|
|
3
3
|
|
|
4
4
|
import { describe, test, expect } from "vitest";
|
|
5
5
|
import { classifyOne, discoverModules, DiscoveryError, type WorkspacePackage } from "../discovery";
|
|
@@ -14,7 +14,7 @@ describe("discovery — single-package classification", () => {
|
|
|
14
14
|
name: "x.software",
|
|
15
15
|
"block-software": {},
|
|
16
16
|
});
|
|
17
|
-
expect(classifyOne(wp
|
|
17
|
+
expect(classifyOne(wp)).toBe("software");
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
test("rule 1 alt: software via package-builder + runenv dep", () => {
|
|
@@ -25,67 +25,62 @@ describe("discovery — single-package classification", () => {
|
|
|
25
25
|
"@platforma-open/runenv-python-3": "catalog:",
|
|
26
26
|
},
|
|
27
27
|
});
|
|
28
|
-
expect(classifyOne(wp
|
|
28
|
+
expect(classifyOne(wp)).toBe("software");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
test("
|
|
31
|
+
test("workflow via workflow-tengo dep", () => {
|
|
32
32
|
const wp = pkg("workflow", {
|
|
33
33
|
name: "x.workflow",
|
|
34
34
|
dependencies: { "@platforma-sdk/workflow-tengo": "catalog:" },
|
|
35
35
|
});
|
|
36
|
-
expect(classifyOne(wp
|
|
36
|
+
expect(classifyOne(wp)).toBe("workflow");
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
test("
|
|
39
|
+
test("ui via ui-vue dep and no main", () => {
|
|
40
40
|
const wp = pkg("ui", {
|
|
41
41
|
name: "x.ui",
|
|
42
42
|
dependencies: { "@platforma-sdk/ui-vue": "catalog:" },
|
|
43
43
|
});
|
|
44
|
-
expect(classifyOne(wp
|
|
44
|
+
expect(classifyOne(wp)).toBe("ui");
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
test("
|
|
48
|
-
const siblings = new Set(["x.model", "x.ui", "x.workflow"]);
|
|
47
|
+
test("block via the top-level `block` field", () => {
|
|
49
48
|
const wp = pkg("block", {
|
|
50
49
|
name: "x.block",
|
|
51
|
-
|
|
52
|
-
"@platforma-sdk/block-tools": "catalog:",
|
|
53
|
-
"x.model": "workspace:*",
|
|
54
|
-
"x.ui": "workspace:*",
|
|
55
|
-
},
|
|
50
|
+
block: { components: {}, meta: {} },
|
|
56
51
|
});
|
|
57
|
-
expect(classifyOne(wp
|
|
52
|
+
expect(classifyOne(wp)).toBe("block");
|
|
58
53
|
});
|
|
59
54
|
|
|
60
|
-
test("
|
|
55
|
+
test("a block-tools-ish package WITHOUT a `block` field is not classified `block`", () => {
|
|
61
56
|
const wp = pkg("block", {
|
|
62
57
|
name: "x.block",
|
|
63
58
|
files: ["index.d.ts", "index.js"],
|
|
64
59
|
dependencies: { "@platforma-sdk/block-tools": "catalog:" },
|
|
65
60
|
});
|
|
66
|
-
expect(classifyOne(wp
|
|
61
|
+
expect(classifyOne(wp)).toBeUndefined();
|
|
67
62
|
});
|
|
68
63
|
|
|
69
|
-
test("
|
|
64
|
+
test("model via model dep + main", () => {
|
|
70
65
|
const wp = pkg("model", {
|
|
71
66
|
name: "x.model",
|
|
72
67
|
main: "dist/index.js",
|
|
73
68
|
dependencies: { "@platforma-sdk/model": "catalog:" },
|
|
74
69
|
});
|
|
75
|
-
expect(classifyOne(wp
|
|
70
|
+
expect(classifyOne(wp)).toBe("model");
|
|
76
71
|
});
|
|
77
72
|
|
|
78
|
-
test("
|
|
73
|
+
test("test via @platforma-sdk/test dep", () => {
|
|
79
74
|
const wp = pkg("test", {
|
|
80
75
|
name: "x.test",
|
|
81
76
|
devDependencies: { "@platforma-sdk/test": "catalog:" },
|
|
82
77
|
});
|
|
83
|
-
expect(classifyOne(wp
|
|
78
|
+
expect(classifyOne(wp)).toBe("test");
|
|
84
79
|
});
|
|
85
80
|
|
|
86
|
-
test("
|
|
81
|
+
test("root as catch-all", () => {
|
|
87
82
|
const wp = pkg("", { name: "x" });
|
|
88
|
-
expect(classifyOne(wp
|
|
83
|
+
expect(classifyOne(wp)).toBe("root");
|
|
89
84
|
});
|
|
90
85
|
|
|
91
86
|
test("override: block-scope wins over detection", () => {
|
|
@@ -94,7 +89,7 @@ describe("discovery — single-package classification", () => {
|
|
|
94
89
|
"block-scope": "software",
|
|
95
90
|
dependencies: { "@platforma-sdk/workflow-tengo": "catalog:" },
|
|
96
91
|
});
|
|
97
|
-
expect(classifyOne(wp
|
|
92
|
+
expect(classifyOne(wp)).toBe("software");
|
|
98
93
|
});
|
|
99
94
|
|
|
100
95
|
test("override: bad value throws DiscoveryError", () => {
|
|
@@ -102,7 +97,7 @@ describe("discovery — single-package classification", () => {
|
|
|
102
97
|
name: "x",
|
|
103
98
|
"block-scope": "nope",
|
|
104
99
|
});
|
|
105
|
-
expect(() => classifyOne(wp
|
|
100
|
+
expect(() => classifyOne(wp)).toThrow(DiscoveryError);
|
|
106
101
|
});
|
|
107
102
|
|
|
108
103
|
test("unclassified non-root throws DiscoveryError", () => {
|
|
@@ -134,6 +129,7 @@ describe("discovery — full workspace", () => {
|
|
|
134
129
|
}),
|
|
135
130
|
pkg("block", {
|
|
136
131
|
name: "x.block",
|
|
132
|
+
block: { components: {}, meta: {} },
|
|
137
133
|
dependencies: {
|
|
138
134
|
"@platforma-sdk/block-tools": "catalog:",
|
|
139
135
|
"x.model": "workspace:*",
|
|
@@ -25,6 +25,8 @@ export type PackageJsonLike = {
|
|
|
25
25
|
devDependencies?: Record<string, string>;
|
|
26
26
|
peerDependencies?: Record<string, string>;
|
|
27
27
|
optionalDependencies?: Record<string, string>;
|
|
28
|
+
// Top-level `block` field — the unambiguous facade marker.
|
|
29
|
+
block?: unknown;
|
|
28
30
|
// Override field — value MUST be one of the seven scopes.
|
|
29
31
|
"block-scope"?: string;
|
|
30
32
|
"block-software"?: unknown;
|
|
@@ -61,11 +63,9 @@ function isRoot(p: string): boolean {
|
|
|
61
63
|
* Classify one package.
|
|
62
64
|
*
|
|
63
65
|
* @param wp The workspace package.
|
|
64
|
-
* @param siblingNames Set of all workspace package `name`s (used by
|
|
65
|
-
* rule 4 "block": depends on ≥2 sibling .model/.ui/.workflow).
|
|
66
66
|
* @returns Scope or `undefined` if unclassified.
|
|
67
67
|
*/
|
|
68
|
-
export function classifyOne(wp: WorkspacePackage
|
|
68
|
+
export function classifyOne(wp: WorkspacePackage): Scope | undefined {
|
|
69
69
|
const override = wp.pkg["block-scope"];
|
|
70
70
|
if (typeof override === "string") {
|
|
71
71
|
if (!(ALL_SCOPES as string[]).includes(override)) {
|
|
@@ -88,6 +88,12 @@ export function classifyOne(wp: WorkspacePackage, siblingNames: Set<string>): Sc
|
|
|
88
88
|
// sub-package dirs), so this only ever fires in standalone mode.
|
|
89
89
|
if (isRoot(wp.path)) return "root";
|
|
90
90
|
|
|
91
|
+
// block (facade) — a top-level `block` field is the unambiguous facade
|
|
92
|
+
// marker; no non-facade workspace package carries one. MUST precede the model
|
|
93
|
+
// rule: a migrated facade carries `@platforma-sdk/model` as a devDep plus
|
|
94
|
+
// `main`, which would otherwise match model.
|
|
95
|
+
if (wp.pkg.block !== undefined) return "block";
|
|
96
|
+
|
|
91
97
|
const deps = depKeys(wp.pkg);
|
|
92
98
|
|
|
93
99
|
// 1. software — top-level `block-software` field OR
|
|
@@ -107,30 +113,10 @@ export function classifyOne(wp: WorkspacePackage, siblingNames: Set<string>): Sc
|
|
|
107
113
|
return "ui";
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
// 4.
|
|
111
|
-
if (deps.has("@platforma-sdk/block-tools")) {
|
|
112
|
-
const matches = [...deps].filter(
|
|
113
|
-
(d) =>
|
|
114
|
-
siblingNames.has(d) &&
|
|
115
|
-
(d.endsWith(".model") || d.endsWith(".ui") || d.endsWith(".workflow")),
|
|
116
|
-
);
|
|
117
|
-
if (matches.length >= 2) return "block";
|
|
118
|
-
// Fallback: `files == ["index.d.ts", "index.js"]` + block-tools.
|
|
119
|
-
const files = wp.pkg.files;
|
|
120
|
-
if (
|
|
121
|
-
Array.isArray(files) &&
|
|
122
|
-
files.length === 2 &&
|
|
123
|
-
files.includes("index.d.ts") &&
|
|
124
|
-
files.includes("index.js")
|
|
125
|
-
) {
|
|
126
|
-
return "block";
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 5. model — @platforma-sdk/model in deps AND main is set.
|
|
116
|
+
// 4. model — @platforma-sdk/model in deps AND main is set.
|
|
131
117
|
if (deps.has("@platforma-sdk/model") && wp.pkg.main) return "model";
|
|
132
118
|
|
|
133
|
-
//
|
|
119
|
+
// 5. test — @platforma-sdk/test in deps.
|
|
134
120
|
if (deps.has("@platforma-sdk/test")) return "test";
|
|
135
121
|
|
|
136
122
|
// (root — the workspace root, path "", is classified above, before the
|
|
@@ -144,13 +130,9 @@ export function classifyOne(wp: WorkspacePackage, siblingNames: Set<string>): Sc
|
|
|
144
130
|
* package.json.
|
|
145
131
|
*/
|
|
146
132
|
export function discoverModules(packages: WorkspacePackage[]): Module[] {
|
|
147
|
-
const siblingNames = new Set<string>();
|
|
148
|
-
for (const p of packages) {
|
|
149
|
-
if (typeof p.pkg.name === "string") siblingNames.add(p.pkg.name);
|
|
150
|
-
}
|
|
151
133
|
const out: Module[] = [];
|
|
152
134
|
for (const p of packages) {
|
|
153
|
-
const scope = classifyOne(p
|
|
135
|
+
const scope = classifyOne(p);
|
|
154
136
|
if (!scope) {
|
|
155
137
|
throw new DiscoveryError(
|
|
156
138
|
`Unclassified workspace package at '${p.path}'. ` +
|