@platforma-sdk/block-tools 2.11.2 → 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 (89) hide show
  1. package/bin/run.js +9 -5
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.js +13 -13
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cli.mjs +845 -981
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/cmd/build-meta.d.ts +2 -9
  8. package/dist/cmd/build-model.d.ts +2 -10
  9. package/dist/cmd/list-overview-snapshots.d.ts +2 -9
  10. package/dist/cmd/mark-stable.d.ts +2 -13
  11. package/dist/cmd/pack.d.ts +2 -9
  12. package/dist/cmd/publish.d.ts +2 -12
  13. package/dist/cmd/refresh-registry.d.ts +2 -9
  14. package/dist/cmd/restore-overview-from-snapshot.d.ts +2 -10
  15. package/dist/cmd/structure/check.d.ts +2 -10
  16. package/dist/cmd/structure/init.d.ts +2 -18
  17. package/dist/cmd/structure/refresh.d.ts +10 -11
  18. package/dist/cmd/update-deps.d.ts +2 -6
  19. package/dist/cmd/upload-package-v1.d.ts +2 -14
  20. package/dist/config-BmJWc8sR.js +3 -0
  21. package/dist/config-BmJWc8sR.js.map +1 -0
  22. package/dist/{config-DpfTzcgG.mjs → config-CiC347sB.mjs} +453 -398
  23. package/dist/config-CiC347sB.mjs.map +1 -0
  24. package/dist/index.js +1 -1
  25. package/dist/index.mjs +15 -15
  26. package/dist/registry_v1/config_schema.d.ts +7 -7
  27. package/dist/registry_v1/flags.d.ts +2 -4
  28. package/dist/structure/cli/run-structure.d.ts +1 -1
  29. package/dist/structure/engine/discovery.d.ts +2 -3
  30. package/dist/structure/rules/block-package-json.d.ts +3 -3
  31. package/dist/structure/rules/shared/pascal-case.d.ts +1 -0
  32. package/dist/v2/index.d.ts +1 -0
  33. package/dist/v2/model/block_components.d.ts +11 -0
  34. package/dist/v2/model/block_meta.d.ts +7 -0
  35. package/dist/v2/resolve_to_registry.d.ts +37 -0
  36. package/dist/v2/source_package.d.ts +12 -0
  37. package/package.json +9 -24
  38. package/src/cli.test.ts +56 -0
  39. package/src/cli.ts +50 -0
  40. package/src/cmd/build-meta.ts +11 -22
  41. package/src/cmd/build-model.ts +16 -33
  42. package/src/cmd/list-overview-snapshots.ts +24 -28
  43. package/src/cmd/mark-stable.ts +35 -55
  44. package/src/cmd/pack.ts +11 -22
  45. package/src/cmd/publish.test.ts +35 -0
  46. package/src/cmd/publish.ts +55 -49
  47. package/src/cmd/refresh-registry.ts +22 -26
  48. package/src/cmd/restore-overview-from-snapshot.ts +24 -34
  49. package/src/cmd/structure/check.ts +19 -28
  50. package/src/cmd/structure/init.ts +40 -47
  51. package/src/cmd/structure/refresh.ts +45 -36
  52. package/src/cmd/update-deps.ts +22 -12
  53. package/src/cmd/upload-package-v1.ts +42 -60
  54. package/src/registry_v1/flags.ts +7 -7
  55. package/src/structure/__tests__/discovery-shortname.test.ts +3 -0
  56. package/src/structure/__tests__/facade-end-state.test.ts +195 -0
  57. package/src/structure/__tests__/model-package-json.snapshot.test.ts +1 -1
  58. package/src/structure/cli/run-structure.ts +2 -2
  59. package/src/structure/engine/__tests__/discovery.test.ts +22 -26
  60. package/src/structure/engine/discovery.ts +12 -30
  61. package/src/structure/rules/block-package-json.ts +146 -64
  62. package/src/structure/rules/block.ts +40 -5
  63. package/src/structure/rules/model-package-json.ts +6 -1
  64. package/src/structure/rules/shared/pascal-case.ts +16 -0
  65. package/src/structure/rules/software-package-json.ts +6 -1
  66. package/src/structure/rules/test-package-json.ts +5 -1
  67. package/src/structure/rules/ui-package-json.ts +5 -1
  68. package/src/structure/rules/workflow-package-json.ts +5 -1
  69. package/src/structure/templates/static/block/src/AGENTS.ts +5 -0
  70. package/src/structure/templates/static/block/src/agents-extra.ts +8 -0
  71. package/src/structure/templates/static/block/src/block-extra.ts +9 -0
  72. package/src/structure/templates/static/block/tsconfig.json +4 -0
  73. package/src/structure/templates/text/block/src/index.tpl.ts +55 -0
  74. package/src/v2/from_pack_v2.test.ts +142 -0
  75. package/src/v2/index.ts +1 -0
  76. package/src/v2/model/block_components.ts +33 -0
  77. package/src/v2/model/block_meta.ts +18 -0
  78. package/src/v2/model/content_conversion.ts +0 -1
  79. package/src/v2/resolve_to_registry.ts +84 -0
  80. package/src/v2/source_package.ts +34 -1
  81. package/bin/dev.cmd +0 -3
  82. package/bin/dev.js +0 -6
  83. package/dist/cmd/index.d.ts +0 -28
  84. package/dist/config-DpfTzcgG.mjs.map +0 -1
  85. package/dist/config-xxVy3IQR.js +0 -3
  86. package/dist/config-xxVy3IQR.js.map +0 -1
  87. package/src/cmd/index.ts +0 -31
  88. package/src/structure/templates/static/block/index.d.ts +0 -6
  89. package/src/structure/templates/static/block/index.js +0 -8
@@ -1,5 +1,6 @@
1
- import { Args, Command, Flags } from "@oclif/core";
1
+ import { Command, Option } from "commander";
2
2
  import path from "node:path";
3
+ import { ConsoleLoggerAdapter } from "@milaboratories/ts-helpers";
3
4
  import {
4
5
  resolveBlockVars,
5
6
  runInit,
@@ -7,59 +8,51 @@ import {
7
8
  type InitFlagValues,
8
9
  } from "../../structure/init-block-constructor";
9
10
 
10
- export default class StructureInit extends Command {
11
- static override description =
12
- "Scaffold a fresh Platforma block from the canonical structure. Missing flags are prompted unless --non-interactive.";
13
-
14
- static override examples = [
15
- "<%= config.bin %> <%= command.id %> --npm-org @platforma-open --org-scope my-org --short-name demo --no-software --non-interactive",
16
- "<%= config.bin %> <%= command.id %> ./demo --npm-org @platforma-open --org-scope my-org --short-name demo --with-software --platform python --non-interactive",
17
- ];
18
-
19
- static override args = {
20
- path: Args.string({
21
- description: "target directory (default: <short-name> under the current directory)",
22
- }),
23
- };
24
-
25
- static override flags = {
26
- "npm-org": Flags.string({ description: "npm org, e.g. @platforma-open" }),
27
- "org-scope": Flags.string({ description: "org scope, e.g. my-org" }),
28
- "short-name": Flags.string({ description: "block short name, e.g. mixcr-clonotyping" }),
29
- "with-software": Flags.boolean({ description: "include a software module" }),
30
- "no-software": Flags.boolean({ description: "omit the software module" }),
31
- platform: Flags.string({
32
- description: "software platform (single-valued)",
33
- options: [...SUPPORTED_PLATFORMS],
34
- }),
35
- "non-interactive": Flags.boolean({
36
- description: "error on any missing flag instead of prompting",
37
- }),
38
- };
39
-
40
- public async run(): Promise<void> {
41
- const { args, flags } = await this.parse(StructureInit);
42
-
43
- if (flags["with-software"] && flags["no-software"]) {
44
- this.error("--with-software and --no-software are mutually exclusive.");
11
+ export function structureInitCommand(packageRoot: string): Command {
12
+ const cmd = new Command("init").description(
13
+ "Scaffold a fresh Platforma block from the canonical structure. Missing flags are prompted unless --non-interactive.",
14
+ );
15
+
16
+ cmd.argument("[path]", "target directory (default: <short-name> under the current directory)");
17
+ cmd.option("--npm-org <org>", "npm org, e.g. @platforma-open");
18
+ cmd.option("--org-scope <scope>", "org scope, e.g. my-org");
19
+ cmd.option("--short-name <name>", "block short name, e.g. mixcr-clonotyping");
20
+ cmd.option("--with-software", "include a software module");
21
+ cmd.option("--no-software", "omit the software module");
22
+ cmd.addOption(
23
+ new Option("--platform <platform>", "software platform (single-valued)").choices([
24
+ ...SUPPORTED_PLATFORMS,
25
+ ]),
26
+ );
27
+ cmd.option("--non-interactive", "error on any missing flag instead of prompting");
28
+
29
+ cmd.action(async (argPath: string | undefined, flags) => {
30
+ const logger = new ConsoleLoggerAdapter();
31
+
32
+ // commander maps `--with-software` -> flags.withSoftware===true and
33
+ // `--no-software` -> flags.software===false
34
+ const withSoftwareFlag = flags.withSoftware === true;
35
+ const noSoftwareFlag = flags.software === false;
36
+ if (withSoftwareFlag && noSoftwareFlag) {
37
+ throw new Error("--with-software and --no-software are mutually exclusive.");
45
38
  }
46
- const withSoftware = flags["with-software"] ? true : flags["no-software"] ? false : undefined;
39
+ const withSoftware = withSoftwareFlag ? true : noSoftwareFlag ? false : undefined;
47
40
 
48
41
  const flagValues: InitFlagValues = {
49
- npmOrg: flags["npm-org"],
50
- orgScope: flags["org-scope"],
51
- shortName: flags["short-name"],
42
+ npmOrg: flags.npmOrg,
43
+ orgScope: flags.orgScope,
44
+ shortName: flags.shortName,
52
45
  withSoftware,
53
46
  platform: flags.platform,
54
- nonInteractive: flags["non-interactive"],
47
+ nonInteractive: flags.nonInteractive,
55
48
  };
56
49
 
57
50
  const vars = await resolveBlockVars(flagValues);
58
- const blockPath = args.path
59
- ? path.resolve(args.path)
60
- : path.resolve(process.cwd(), vars.shortName);
61
- const templatesRoot = path.join(this.config.root, "src", "structure", "templates");
51
+ const blockPath = argPath ? path.resolve(argPath) : path.resolve(process.cwd(), vars.shortName);
52
+ const templatesRoot = path.join(packageRoot, "src", "structure", "templates");
53
+
54
+ await runInit({ vars, blockPath, templatesRoot, log: (m) => logger.info(m) });
55
+ });
62
56
 
63
- await runInit({ vars, blockPath, templatesRoot, log: (m) => this.log(m) });
64
- }
57
+ return cmd;
65
58
  }
@@ -1,44 +1,53 @@
1
- import { Command, Flags } from "@oclif/core";
1
+ import { Command } from "commander";
2
2
  import path from "node:path";
3
+ import type { MiLogger } from "@milaboratories/ts-helpers";
4
+ import { ConsoleLoggerAdapter } from "@milaboratories/ts-helpers";
3
5
  import { runStructureForPath, formatChanges } from "../../structure/cli/run-structure";
4
6
 
5
- export default class StructureRefresh extends Command {
6
- static override description =
7
- "Bring block(s) up to date with the canonical structure. Applies rules, writes files, bumps .structure version. Default mode is offline; --update-deps-only fires only catalog-bump rules (network).";
8
-
9
- static override examples = [
10
- "<%= config.bin %> <%= command.id %> ./my-block",
11
- "<%= config.bin %> <%= command.id %> --sdk-internal etc/blocks/*",
12
- "<%= config.bin %> <%= command.id %> --update-deps-only",
13
- ];
7
+ // Shared by `structure refresh` and the deprecated `update-deps` alias.
8
+ export async function runRefresh(opts: {
9
+ paths: string[];
10
+ isSdkInternal: boolean;
11
+ updateDepsOnly: boolean;
12
+ templatesRoot: string;
13
+ logger: MiLogger;
14
+ }): Promise<void> {
15
+ for (const p of opts.paths) {
16
+ const res = await runStructureForPath({
17
+ blockPath: p,
18
+ isSdkInternal: opts.isSdkInternal,
19
+ updateDepsOnly: opts.updateDepsOnly,
20
+ mode: "refresh",
21
+ templatesRoot: opts.templatesRoot,
22
+ log: (m) => opts.logger.info(m),
23
+ });
24
+ opts.logger.info(formatChanges(p, res.changes));
25
+ }
26
+ }
14
27
 
15
- static override strict = false;
28
+ export function structureRefreshCommand(packageRoot: string): Command {
29
+ const cmd = new Command("refresh").description(
30
+ "Bring block(s) up to date with the canonical structure. Applies rules, writes files, bumps .structure version. Default mode is offline; --update-deps-only fires only catalog-bump rules (network).",
31
+ );
16
32
 
17
- static override flags = {
18
- "sdk-internal": Flags.boolean({
19
- description: "block(s) live inside the SDK monorepo; skip root-scope rules",
20
- }),
21
- "update-deps-only": Flags.boolean({
22
- description:
23
- "fire ONLY catalog-bump rules (npm network). Follow with `pnpm install` then a plain refresh.",
24
- }),
25
- };
33
+ cmd.argument("[paths...]", "block path(s); defaults to the current directory");
34
+ cmd.option("--sdk-internal", "block(s) live inside the SDK monorepo; skip root-scope rules");
35
+ cmd.option(
36
+ "--update-deps-only",
37
+ "fire ONLY catalog-bump rules (npm network). Follow with `pnpm install` then a plain refresh.",
38
+ );
26
39
 
27
- public async run(): Promise<void> {
28
- const { argv, flags } = await this.parse(StructureRefresh);
29
- const paths = (argv as string[]).length > 0 ? (argv as string[]) : ["."];
30
- const templatesRoot = path.join(this.config.root, "src", "structure", "templates");
40
+ cmd.action(async (argv: string[], flags) => {
41
+ const paths = argv.length > 0 ? argv : ["."];
42
+ const templatesRoot = path.join(packageRoot, "src", "structure", "templates");
43
+ await runRefresh({
44
+ paths,
45
+ isSdkInternal: Boolean(flags.sdkInternal),
46
+ updateDepsOnly: Boolean(flags.updateDepsOnly),
47
+ templatesRoot,
48
+ logger: new ConsoleLoggerAdapter(),
49
+ });
50
+ });
31
51
 
32
- for (const p of paths) {
33
- const res = await runStructureForPath({
34
- blockPath: p,
35
- isSdkInternal: flags["sdk-internal"],
36
- updateDepsOnly: flags["update-deps-only"],
37
- mode: "refresh",
38
- templatesRoot,
39
- log: (m) => this.log(m),
40
- });
41
- this.log(formatChanges(p, res.changes));
42
- }
43
- }
52
+ return cmd;
44
53
  }
@@ -1,4 +1,7 @@
1
- import { Command } from "@oclif/core";
1
+ import { Command } from "commander";
2
+ import path from "node:path";
3
+ import { ConsoleLoggerAdapter } from "@milaboratories/ts-helpers";
4
+ import { runRefresh } from "./structure/refresh";
2
5
 
3
6
  /**
4
7
  * Deprecated. Phase-2 forwarding alias (spec.md § "Coexistence And
@@ -7,19 +10,26 @@ import { Command } from "@oclif/core";
7
10
  * semantics. Prints a deprecation warning; will be removed once the
8
11
  * structurer path has been live without surprises.
9
12
  */
10
- export default class UpdateDeps extends Command {
11
- static override description =
12
- "[DEPRECATED] Use `block-tools structure refresh --update-deps-only`. This alias delegates to it.";
13
+ export function updateDepsCommand(packageRoot: string): Command {
14
+ const cmd = new Command("update-deps").description(
15
+ "[DEPRECATED] Use `block-tools structure refresh --update-deps-only`. This alias delegates to it.",
16
+ );
13
17
 
14
- static override examples = ["<%= config.bin %> <%= command.id %>"];
15
-
16
- public async run(): Promise<void> {
17
- this.warn(
18
+ cmd.action(async () => {
19
+ const logger = new ConsoleLoggerAdapter();
20
+ logger.warn(
18
21
  "`block-tools update-deps` is deprecated and will be removed in a future release. " +
19
22
  "Use `block-tools structure refresh --update-deps-only` instead. Delegating now.",
20
23
  );
21
- // oclif resolves commands by their internal colon-form id, even
22
- // though the user-facing form uses the configured " " topic separator.
23
- await this.config.runCommand("structure:refresh", ["--update-deps-only"]);
24
- }
24
+ const templatesRoot = path.join(packageRoot, "src", "structure", "templates");
25
+ await runRefresh({
26
+ paths: ["."],
27
+ isSdkInternal: false,
28
+ updateDepsOnly: true,
29
+ templatesRoot,
30
+ logger,
31
+ });
32
+ });
33
+
34
+ return cmd;
25
35
  }
@@ -1,67 +1,47 @@
1
- import { Command, Flags } from "@oclif/core";
1
+ import { Command, Option } from "commander";
2
2
  import { getConfig } from "../registry_v1/config";
3
- import { targetFile } from "../registry_v1/flags";
3
+ import { targetFileOption } from "../registry_v1/flags";
4
4
  import fs from "node:fs";
5
5
  import YAML from "yaml";
6
6
  import { PlRegPackageConfigDataShard } from "../registry_v1/config_schema";
7
- import { OclifLoggerAdapter } from "@milaboratories/ts-helpers-oclif";
7
+ import { ConsoleLoggerAdapter } from "@milaboratories/ts-helpers";
8
8
 
9
9
  type BasicConfigField = keyof PlRegPackageConfigDataShard &
10
10
  ("registry" | "organization" | "package" | "version");
11
11
  const BasicConfigFields: BasicConfigField[] = ["registry", "organization", "package", "version"];
12
12
 
13
- export default class UploadPackageV1 extends Command {
14
- static description = "Uploads V1 package and refreshes the registry";
15
-
16
- static flags = {
17
- registry: Flags.string({
18
- char: "r",
19
- summary: "full address of the registry or alias from .pl.reg",
20
- helpValue: "<address|alias>",
21
- env: "PL_REGISTRY",
22
- }),
23
-
24
- organization: Flags.string({
25
- char: "o",
26
- summary: "target organisation",
27
- env: "PL_PACKAGE_ORGANIZATION",
28
- }),
29
-
30
- package: Flags.string({
31
- char: "p",
32
- summary: "target package",
33
- env: "PL_PACKAGE_NAME",
34
- }),
35
-
36
- version: Flags.string({
37
- char: "v",
38
- summary: "target version",
39
- env: "PL_PACKAGE_VERSION",
40
- }),
41
-
42
- meta: Flags.file({
43
- char: "m",
44
- summary: "json file containing meta information to associate with tha package",
45
- exists: true,
46
- }),
47
-
48
- file: targetFile({
49
- char: "f",
50
- summary: "package files",
51
- multiple: true,
52
- default: [],
53
- }),
54
-
55
- refresh: Flags.boolean({
56
- summary: "refresh repository after adding the package",
57
- default: true,
58
- allowNo: true,
59
- env: "PL_REGISTRY_REFRESH",
60
- }),
61
- };
62
-
63
- public async run(): Promise<void> {
64
- const { flags } = await this.parse(UploadPackageV1);
13
+ export function uploadPackageV1Command(): Command {
14
+ const cmd = new Command("upload-package-v1").description(
15
+ "Uploads V1 package and refreshes the registry",
16
+ );
17
+
18
+ cmd.addOption(
19
+ new Option(
20
+ "-r, --registry <address|alias>",
21
+ "full address of the registry or alias from .pl.reg",
22
+ ).env("PL_REGISTRY"),
23
+ );
24
+ cmd.addOption(
25
+ new Option("-o, --organization <organization>", "target organisation").env(
26
+ "PL_PACKAGE_ORGANIZATION",
27
+ ),
28
+ );
29
+ cmd.addOption(new Option("-p, --package <package>", "target package").env("PL_PACKAGE_NAME"));
30
+ cmd.addOption(new Option("-v, --version <version>", "target version").env("PL_PACKAGE_VERSION"));
31
+ cmd.option(
32
+ "-m, --meta <path>",
33
+ "json file containing meta information to associate with tha package",
34
+ );
35
+ cmd.addOption(targetFileOption("-f, --file <file>", "package files"));
36
+ cmd.addOption(
37
+ new Option("--refresh", "refresh repository after adding the package")
38
+ .default(true)
39
+ .env("PL_REGISTRY_REFRESH"),
40
+ );
41
+ cmd.option("--no-refresh", "do not refresh repository after adding the package");
42
+
43
+ cmd.action(async (flags) => {
44
+ const logger = new ConsoleLoggerAdapter();
65
45
  const configFromFlags: PlRegPackageConfigDataShard = PlRegPackageConfigDataShard.parse({});
66
46
 
67
47
  for (const field of BasicConfigFields) if (flags[field]) configFromFlags[field] = flags[field];
@@ -83,23 +63,25 @@ export default class UploadPackageV1 extends Command {
83
63
 
84
64
  const conf = await getConfig(configFromFlags);
85
65
 
86
- this.log(YAML.stringify(conf.conf));
66
+ logger.info(YAML.stringify(conf.conf));
87
67
 
88
- const registry = conf.createRegistry(new OclifLoggerAdapter(this));
68
+ const registry = conf.createRegistry(logger);
89
69
  const name = conf.fullPackageName;
90
70
 
91
71
  const builder = registry.constructNewPackage(name);
92
72
 
93
73
  for (const [dst, src] of Object.entries(conf.conf.files)) {
94
- this.log(`Uploading ${src} -> ${dst} ...`);
74
+ logger.info(`Uploading ${src} -> ${dst} ...`);
95
75
  const content = await fs.promises.readFile(src);
96
76
  await builder.addFile(dst, content);
97
77
  }
98
78
 
99
- this.log(`Uploading meta information...`);
79
+ logger.info(`Uploading meta information...`);
100
80
  await builder.writeMeta(conf.conf.meta);
101
81
  await builder.finish();
102
82
 
103
83
  if (flags.refresh) await registry.updateIfNeeded();
104
- }
84
+ });
85
+
86
+ return cmd;
105
87
  }
@@ -1,4 +1,4 @@
1
- import { Flags } from "@oclif/core";
1
+ import { Option } from "commander";
2
2
  import path from "node:path";
3
3
 
4
4
  export interface TargetFile {
@@ -16,9 +16,9 @@ function parseTargetFile(arg: string): TargetFile {
16
16
  }
17
17
  }
18
18
 
19
- export const targetFile = Flags.custom<TargetFile>({
20
- summary: "target files to upload",
21
- helpValue: "file_path | package_name=file_path",
22
- // eslint-disable-next-line @typescript-eslint/require-await -- oclif requires async but parsing is sync
23
- parse: async (arg) => parseTargetFile(arg),
24
- });
19
+ // Repeatable `file_path | package_name=file_path` option.
20
+ export function targetFileOption(flags: string, summary: string): Option {
21
+ return new Option(flags, summary)
22
+ .argParser((arg: string, prev?: TargetFile[]) => [...(prev ?? []), parseTargetFile(arg)])
23
+ .default([] as TargetFile[]);
24
+ }
@@ -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
- "version": "1.0.0",
12
+ "private": true,
13
13
  "type": "module",
14
14
  "main": "dist/index.cjs",
15
15
  "module": "dist/index.js",
@@ -1,5 +1,5 @@
1
1
  // Shared driver for the `structure check` and `structure refresh`
2
- // oclif commands. Wraps DISCOVERY (discover-fs) + the engine runner for
2
+ // commands. Wraps DISCOVERY (discover-fs) + the engine runner for
3
3
  // one block path, handling the standalone vs `--sdk-internal` split, the
4
4
  // `--update-deps-only` registry prefetch, and the post-run recheck's
5
5
  // fresh re-discovery. Each block path is processed independently (no
@@ -26,7 +26,7 @@ export type RunStructureInput = {
26
26
  isSdkInternal: boolean;
27
27
  updateDepsOnly: boolean;
28
28
  mode: StructureMode;
29
- /** `<block-tools>/src/structure/templates` (from oclif `config.root`). */
29
+ /** `<block-tools>/src/structure/templates`. */
30
30
  templatesRoot: string;
31
31
  log: (msg: string) => void;
32
32
  };