@platforma-sdk/block-tools 2.11.3 → 2.11.4

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.
Files changed (51) hide show
  1. package/dist/cli.js +13 -13
  2. package/dist/cli.js.map +1 -1
  3. package/dist/cli.mjs +634 -580
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/config-BmJWc8sR.js +3 -0
  6. package/dist/config-BmJWc8sR.js.map +1 -0
  7. package/dist/{config-DpfTzcgG.mjs → config-CiC347sB.mjs} +453 -398
  8. package/dist/config-CiC347sB.mjs.map +1 -0
  9. package/dist/index.js +1 -1
  10. package/dist/index.mjs +15 -15
  11. package/dist/structure/engine/discovery.d.ts +2 -3
  12. package/dist/structure/rules/block-package-json.d.ts +3 -3
  13. package/dist/structure/rules/shared/pascal-case.d.ts +1 -0
  14. package/dist/v2/index.d.ts +1 -0
  15. package/dist/v2/model/block_components.d.ts +11 -0
  16. package/dist/v2/model/block_meta.d.ts +7 -0
  17. package/dist/v2/registry/schema_public.d.ts +3600 -3600
  18. package/dist/v2/resolve_to_registry.d.ts +37 -0
  19. package/dist/v2/source_package.d.ts +12 -0
  20. package/package.json +7 -7
  21. package/src/cmd/publish.test.ts +35 -0
  22. package/src/cmd/publish.ts +20 -0
  23. package/src/structure/__tests__/discovery-shortname.test.ts +3 -0
  24. package/src/structure/__tests__/facade-end-state.test.ts +195 -0
  25. package/src/structure/__tests__/model-package-json.snapshot.test.ts +1 -1
  26. package/src/structure/engine/__tests__/discovery.test.ts +22 -26
  27. package/src/structure/engine/discovery.ts +12 -30
  28. package/src/structure/rules/block-package-json.ts +146 -64
  29. package/src/structure/rules/block.ts +40 -5
  30. package/src/structure/rules/model-package-json.ts +6 -1
  31. package/src/structure/rules/shared/pascal-case.ts +16 -0
  32. package/src/structure/rules/software-package-json.ts +6 -1
  33. package/src/structure/rules/test-package-json.ts +5 -1
  34. package/src/structure/rules/ui-package-json.ts +5 -1
  35. package/src/structure/rules/workflow-package-json.ts +5 -1
  36. package/src/structure/templates/static/block/src/AGENTS.ts +5 -0
  37. package/src/structure/templates/static/block/src/agents-extra.ts +8 -0
  38. package/src/structure/templates/static/block/src/block-extra.ts +9 -0
  39. package/src/structure/templates/static/block/tsconfig.json +4 -0
  40. package/src/structure/templates/text/block/src/index.tpl.ts +55 -0
  41. package/src/v2/from_pack_v2.test.ts +142 -0
  42. package/src/v2/index.ts +1 -0
  43. package/src/v2/model/block_components.ts +33 -0
  44. package/src/v2/model/block_meta.ts +18 -0
  45. package/src/v2/resolve_to_registry.ts +84 -0
  46. package/src/v2/source_package.ts +34 -1
  47. package/dist/config-DpfTzcgG.mjs.map +0 -1
  48. package/dist/config-xxVy3IQR.js +0 -3
  49. package/dist/config-xxVy3IQR.js.map +0 -1
  50. package/src/structure/templates/static/block/index.d.ts +0 -6
  51. package/src/structure/templates/static/block/index.js +0 -8
@@ -0,0 +1,142 @@
1
+ import { test, expect } from "vitest";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import fsp from "node:fs/promises";
6
+ import { existsSync } from "node:fs";
7
+ import { loadPackDescriptionFromManifest } from "./source_package";
8
+ import { buildPublishedCoords, resolveToRegistry } from "./resolve_to_registry";
9
+
10
+ /** Writes a minimal-but-valid v2 block-pack into a fresh temp module root and
11
+ * returns its path (the folder containing `block-pack/`). */
12
+ async function makePackedBlock(): Promise<string> {
13
+ const moduleRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "from-pack-v2-"));
14
+ const blockPackDir = path.join(moduleRoot, "block-pack");
15
+ await fsp.mkdir(blockPackDir);
16
+
17
+ await fsp.writeFile(path.join(blockPackDir, "model.json"), JSON.stringify({ code: {} }));
18
+ await fsp.writeFile(path.join(blockPackDir, "main.plj.gz"), "workflow-bytes");
19
+ await fsp.writeFile(path.join(blockPackDir, "ui.tgz"), "ui-tarball-bytes");
20
+
21
+ const manifest = {
22
+ schema: "v2",
23
+ description: {
24
+ id: { organization: "milaboratories", name: "test-from-pack", version: "1.0.0" },
25
+ components: {
26
+ workflow: { type: "workflow-v1", main: { type: "relative", path: "main.plj.gz" } },
27
+ model: { type: "relative", path: "model.json" },
28
+ ui: { type: "relative", path: "ui.tgz" },
29
+ },
30
+ meta: {
31
+ title: "Test",
32
+ description: "Test block",
33
+ organization: { name: "MiLaboratories", url: "https://milaboratories.com" },
34
+ },
35
+ },
36
+ timestamp: 1782150933623,
37
+ files: [
38
+ { name: "main.plj.gz", size: 1, sha256: "A".repeat(64) },
39
+ { name: "model.json", size: 1, sha256: "A".repeat(64) },
40
+ { name: "ui.tgz", size: 1, sha256: "A".repeat(64) },
41
+ ],
42
+ };
43
+ await fsp.writeFile(path.join(blockPackDir, "manifest.json"), JSON.stringify(manifest, null, 2));
44
+
45
+ return moduleRoot;
46
+ }
47
+
48
+ test("loadPackDescriptionFromManifest resolves manifest entries to absolute FS paths", async () => {
49
+ const moduleRoot = await makePackedBlock();
50
+ const blockPackDir = path.join(moduleRoot, "block-pack");
51
+
52
+ const description = await loadPackDescriptionFromManifest(blockPackDir);
53
+
54
+ // workflow + model resolve to absolute-file refs inside block-pack/.
55
+ expect(description.components.workflow.main).toStrictEqual({
56
+ type: "absolute-file",
57
+ file: path.join(blockPackDir, "main.plj.gz"),
58
+ });
59
+ expect(description.components.model).toStrictEqual({
60
+ type: "absolute-file",
61
+ file: path.join(blockPackDir, "model.json"),
62
+ });
63
+ // ui (a ui.tgz archive) resolves into the absolute-folder slot.
64
+ expect(description.components.ui).toStrictEqual({
65
+ type: "absolute-folder",
66
+ folder: path.join(blockPackDir, "ui.tgz"),
67
+ });
68
+
69
+ // The id passes through unchanged.
70
+ expect(description.id).toStrictEqual({
71
+ organization: "milaboratories",
72
+ name: "test-from-pack",
73
+ version: "1.0.0",
74
+ });
75
+
76
+ // All resolved paths exist on disk.
77
+ expect(path.isAbsolute(description.components.model.file)).toBe(true);
78
+ expect(existsSync(description.components.workflow.main.file)).toBe(true);
79
+ expect(existsSync(description.components.model.file)).toBe(true);
80
+ expect(existsSync(description.components.ui.folder)).toBe(true);
81
+ });
82
+
83
+ test("loadPackDescriptionFromManifest works against a real packed sum-numbers-v3", async () => {
84
+ // Runs when the block has been built+packed; skips gracefully in a clean
85
+ // checkout where block-pack/ is gitignored. Guards against manifest-shape
86
+ // drift the hermetic fixture above can't catch.
87
+ const here = path.dirname(fileURLToPath(import.meta.url));
88
+ const real = path.resolve(here, "../../../../etc/blocks/sum-numbers-v3/block");
89
+ if (!existsSync(path.join(real, "block-pack", "manifest.json"))) return;
90
+
91
+ const description = await loadPackDescriptionFromManifest(path.join(real, "block-pack"));
92
+ expect(existsSync(description.components.model.file)).toBe(true);
93
+ expect(existsSync(description.components.workflow.main.file)).toBe(true);
94
+ expect(existsSync(description.components.ui.folder)).toBe(true);
95
+ expect(description.components.ui.folder.endsWith("ui.tgz")).toBe(true);
96
+ });
97
+
98
+ test("resolveToRegistry reads published.json into a from-registry-v2 spec", async () => {
99
+ const folder = await fsp.mkdtemp(path.join(os.tmpdir(), "resolve-reg-"));
100
+ const blockPackDir = path.join(folder, "block-pack");
101
+ await fsp.mkdir(blockPackDir);
102
+ // A stray `channel` in the file is ignored — channels are mutable registry
103
+ // pointers, not part of the pinned coordinates.
104
+ await fsp.writeFile(
105
+ path.join(blockPackDir, "published.json"),
106
+ JSON.stringify({
107
+ registryUrl: "https://cdn.platforma.bio/blocks",
108
+ id: { organization: "milaboratories", name: "test-from-pack", version: "2.3.4" },
109
+ channel: "stable",
110
+ }),
111
+ );
112
+
113
+ // from-pack-v2 carries `packUrl` (the block-pack dir as a file: URL);
114
+ // published.json lives inside it.
115
+ const spec = await resolveToRegistry({
116
+ type: "from-pack-v2",
117
+ packUrl: pathToFileURL(blockPackDir).href,
118
+ });
119
+ expect(spec).toStrictEqual({
120
+ type: "from-registry-v2",
121
+ registryUrl: "https://cdn.platforma.bio/blocks",
122
+ id: { organization: "milaboratories", name: "test-from-pack", version: "2.3.4" },
123
+ });
124
+ });
125
+
126
+ test("resolveToRegistry throws the documented error when published.json is missing", async () => {
127
+ const folder = await fsp.mkdtemp(path.join(os.tmpdir(), "resolve-reg-missing-"));
128
+ await expect(resolveToRegistry({ type: "dev-v2", folder })).rejects.toThrow(
129
+ /has no published\.json/,
130
+ );
131
+ });
132
+
133
+ test("buildPublishedCoords shapes the payload (immutable coords only)", () => {
134
+ const id = { organization: "milaboratories", name: "test-from-pack", version: "1.0.0" };
135
+
136
+ expect(
137
+ buildPublishedCoords({ registryUrl: "https://cdn.platforma.bio/blocks", id }),
138
+ ).toStrictEqual({
139
+ registryUrl: "https://cdn.platforma.bio/blocks",
140
+ id,
141
+ });
142
+ });
package/src/v2/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./model";
2
2
  export * from "./build_dist";
3
3
  export * from "./source_package";
4
+ export * from "./resolve_to_registry";
4
5
  export * from "./registry";
@@ -5,8 +5,10 @@ import type {
5
5
  ContentAbsoluteFile,
6
6
  ContentAbsoluteFolder,
7
7
  ContentAbsoluteUrl,
8
+ ContentRelative,
8
9
  } from "@milaboratories/pl-model-middle-layer";
9
10
  import { mapRemoteToAbsolute } from "@milaboratories/pl-model-middle-layer";
11
+ import path from "node:path";
10
12
  import {
11
13
  cpAbsoluteToRelative,
12
14
  packFolderToRelativeTgz,
@@ -45,6 +47,37 @@ export function resolveBlockComponents(
45
47
  };
46
48
  }
47
49
 
50
+ /**
51
+ * Resolves a manifest's relative component paths against the `block-pack/`
52
+ * folder of a consumed package. Workflow main and model become absolute-file
53
+ * refs; the UI (a `ui.tgz` archive) resolves to its absolute path carried in
54
+ * the `absolute-folder` slot — `BlockComponentsDescription` types `ui` as a
55
+ * folder, and for manifest-consumed packs that slot holds the tgz file path
56
+ * (the `from-pack-v2` loader reads `.ui.folder` and feeds it to the lazy
57
+ * `local-tgz` frontend). Counterpart to `resolveBlockComponents`, which
58
+ * resolves a raw `package.json` description against authoring source.
59
+ */
60
+ export function resolveManifestBlockComponents(
61
+ manifest: BlockComponentsManifest,
62
+ blockPackDir: string,
63
+ ): BlockComponentsDescription {
64
+ const toAbsFile = (c: ContentRelative): ContentAbsoluteFile => ({
65
+ type: "absolute-file",
66
+ file: path.resolve(blockPackDir, c.path),
67
+ });
68
+ return {
69
+ workflow: {
70
+ type: "workflow-v1",
71
+ main: toAbsFile(manifest.workflow.main),
72
+ },
73
+ model: toAbsFile(manifest.model),
74
+ ui: {
75
+ type: "absolute-folder",
76
+ folder: path.resolve(blockPackDir, manifest.ui.path),
77
+ },
78
+ };
79
+ }
80
+
48
81
  /**
49
82
  * Copies file references for workflow and model into `dstFolder`, packs the
50
83
  * UI folder into `ui.tgz`, returning the manifest form with relative paths.
@@ -73,6 +73,24 @@ export async function resolveBlockPackMeta(
73
73
  );
74
74
  }
75
75
 
76
+ /**
77
+ * Resolves a manifest's relative text/binary references against the
78
+ * `block-pack/` folder of a consumed package. Counterpart to
79
+ * `resolveBlockPackMeta` (which resolves raw `package.json` meta against
80
+ * authoring source); inlined explicit content passes through unchanged.
81
+ */
82
+ export async function resolveManifestBlockPackMeta(
83
+ manifest: BlockPackMetaManifest,
84
+ blockPackDir: string,
85
+ ): Promise<BlockPackMetaDescription> {
86
+ const toAbs = mapLocalToAbsolute(blockPackDir);
87
+ return transformBlockPackMeta(
88
+ manifest,
89
+ async (v) => toAbs(v),
90
+ async (v) => toAbs(v),
91
+ );
92
+ }
93
+
76
94
  /**
77
95
  * Copies absolute text/binary file references into `dstFolder` and returns
78
96
  * the manifest form with relative paths.
@@ -0,0 +1,84 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import fsp from "node:fs/promises";
4
+ import { fileURLToPath } from "node:url";
5
+ import type {
6
+ BlockPackDevV2,
7
+ BlockPackFromPackV2,
8
+ BlockPackFromRegistryV2,
9
+ BlockPackId,
10
+ } from "@milaboratories/pl-model-middle-layer";
11
+
12
+ /** File written by `block-tools publish` into `block-pack/`, carrying the
13
+ * registry coordinates of the published version. Holds only the immutable
14
+ * coordinates (CDN serve URL + version id). No `channel`: channels are moving
15
+ * pointers in the registry — a version can be promoted/moved later — so the
16
+ * channel at publish time is transient state we can't pin here. Update-checking
17
+ * resolves the channel at read time (`BlockPackFromRegistryV2.channel` is
18
+ * optional; the watcher defaults stable → any). */
19
+ export const PublishedCoordsFileName = "published.json";
20
+
21
+ /** Registry coordinates persisted in `block-pack/published.json`. */
22
+ export interface PublishedCoords {
23
+ /** CDN serve URL the block is fetched from (not the S3 upload bucket). */
24
+ registryUrl: string;
25
+ id: BlockPackId;
26
+ }
27
+
28
+ /**
29
+ * Builds the `published.json` payload. Pure (no filesystem) so it is unit
30
+ * testable.
31
+ */
32
+ export function buildPublishedCoords(args: {
33
+ registryUrl: string;
34
+ id: BlockPackId;
35
+ }): PublishedCoords {
36
+ return {
37
+ registryUrl: args.registryUrl,
38
+ id: args.id,
39
+ };
40
+ }
41
+
42
+ /** Writes `block-pack/published.json` into the given block-pack folder. */
43
+ export async function writePublishedCoords(
44
+ blockPackDir: string,
45
+ coords: PublishedCoords,
46
+ ): Promise<void> {
47
+ const file = path.resolve(blockPackDir, PublishedCoordsFileName);
48
+ await fsp.writeFile(file, JSON.stringify(coords, null, 2), { encoding: "utf-8" });
49
+ }
50
+
51
+ /**
52
+ * Resolves a local block locator (`dev-v2` or `from-pack-v2`) to its registry
53
+ * coordinates by reading `published.json`. `published.json` lives inside the
54
+ * block-pack directory (written there by `block-tools publish`), so for
55
+ * `from-pack-v2` we read it directly from `packUrl` (converted to a path at this
56
+ * Node boundary) — the structurer owns the layout, we do not re-derive it. For a
57
+ * `dev-v2` block folder it sits under
58
+ * `<folder>/block-pack/`. The facade's engine-generated `from-pack-v2`
59
+ * BlockPointer feeds this so a consumed block can be installed via the registry
60
+ * path. Throws when the block was never published.
61
+ */
62
+ export async function resolveToRegistry(
63
+ locator: BlockPackDevV2 | BlockPackFromPackV2,
64
+ ): Promise<BlockPackFromRegistryV2> {
65
+ const file =
66
+ locator.type === "from-pack-v2"
67
+ ? path.resolve(fileURLToPath(locator.packUrl), PublishedCoordsFileName)
68
+ : path.resolve(locator.folder, "block-pack", PublishedCoordsFileName);
69
+ if (!fs.existsSync(file)) {
70
+ throw new Error(
71
+ `Block has no ${PublishedCoordsFileName} at ${file} — ` +
72
+ `was the block published? Run block-tools publish before consuming via registry.`,
73
+ );
74
+ }
75
+ const raw = JSON.parse(await fsp.readFile(file, { encoding: "utf-8" })) as {
76
+ registryUrl: string;
77
+ id: BlockPackFromRegistryV2["id"];
78
+ };
79
+ return {
80
+ type: "from-registry-v2",
81
+ registryUrl: raw.registryUrl,
82
+ id: raw.id,
83
+ };
84
+ }
@@ -1,13 +1,19 @@
1
1
  import path from "node:path";
2
2
  import { tryLoadFile } from "../util";
3
3
  import type { BlockPackDescriptionAbsolute } from "./model";
4
- import { resolveBlockPackDescription } from "./model";
4
+ import {
5
+ resolveBlockPackDescription,
6
+ resolveManifestBlockComponents,
7
+ resolveManifestBlockPackMeta,
8
+ } from "./model";
5
9
  import type { MiLogger } from "@milaboratories/ts-helpers";
6
10
  import { notEmpty } from "@milaboratories/ts-helpers";
7
11
  import fsp from "node:fs/promises";
8
12
  import type { BlockPackDescriptionRaw, BlockPackId } from "@milaboratories/pl-model-middle-layer";
9
13
  import {
10
14
  BlockPackDescriptionFromPackageJsonRaw,
15
+ BlockPackManifest,
16
+ BlockPackManifestFile,
11
17
  SemVer,
12
18
  } from "@milaboratories/pl-model-middle-layer";
13
19
 
@@ -89,3 +95,30 @@ export async function loadPackDescription(
89
95
  const descriptionRaw = await loadPackDescriptionRaw(moduleRoot);
90
96
  return await resolveBlockPackDescription(descriptionRaw, moduleRoot);
91
97
  }
98
+
99
+ /** Folder name (under a consumed package root) holding the v2 block-pack artifacts. */
100
+ export const BlockPackFolderName = "block-pack";
101
+
102
+ /**
103
+ * Loads a block-pack description from a block-pack directory's `manifest.json`,
104
+ * resolving every relative entry against that directory to an absolute
105
+ * filesystem path. Takes the pack directory directly (the `from-pack-v2`
106
+ * pointer's `packUrl`) — the structurer owns the layout, so this does not
107
+ * re-derive `<root>/block-pack`. Counterpart to `loadPackDescription`, which
108
+ * reads the authoring `block` field from `package.json` and resolves against
109
+ * source.
110
+ */
111
+ export async function loadPackDescriptionFromManifest(
112
+ blockPackDir: string,
113
+ ): Promise<BlockPackDescriptionAbsolute> {
114
+ const manifestPath = path.resolve(blockPackDir, BlockPackManifestFile);
115
+ const raw = JSON.parse(await fsp.readFile(manifestPath, { encoding: "utf-8" })) as unknown;
116
+ const manifest = BlockPackManifest.parse(raw);
117
+ const components = resolveManifestBlockComponents(manifest.description.components, blockPackDir);
118
+ const meta = await resolveManifestBlockPackMeta(manifest.description.meta, blockPackDir);
119
+ return {
120
+ ...manifest.description,
121
+ components,
122
+ meta,
123
+ };
124
+ }