@platforma-sdk/block-tools 2.10.14 → 2.10.16
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 +23 -2
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1944 -100
- package/dist/cli.mjs.map +1 -1
- package/dist/cmd/index.d.ts +10 -4
- package/dist/cmd/structure/check.d.ts +10 -0
- package/dist/cmd/structure/init.d.ts +18 -0
- package/dist/cmd/structure/refresh.d.ts +11 -0
- package/dist/cmd/update-deps.d.ts +7 -0
- package/dist/structure/cli/run-structure.d.ts +19 -0
- package/dist/structure/engine/api.d.ts +55 -0
- package/dist/structure/engine/builders.d.ts +95 -0
- package/dist/structure/engine/content-rules.d.ts +148 -0
- package/dist/structure/engine/ctx.d.ts +34 -0
- package/dist/structure/engine/discovery-fs.d.ts +22 -0
- package/dist/structure/engine/discovery.d.ts +39 -0
- package/dist/structure/engine/flatten.d.ts +3 -0
- package/dist/structure/engine/fs/api.d.ts +27 -0
- package/dist/structure/engine/fs/memory.d.ts +16 -0
- package/dist/structure/engine/fs/node.d.ts +14 -0
- package/dist/structure/engine/glob.d.ts +4 -0
- package/dist/structure/engine/ir.d.ts +124 -0
- package/dist/structure/engine/parsers/json.d.ts +37 -0
- package/dist/structure/engine/parsers/lines.d.ts +9 -0
- package/dist/structure/engine/parsers/yaml.d.ts +18 -0
- package/dist/structure/engine/registry-client.d.ts +34 -0
- package/dist/structure/engine/runner.d.ts +45 -0
- package/dist/structure/engine/templates.d.ts +30 -0
- package/dist/structure/engine/testing.d.ts +34 -0
- package/dist/structure/engine/version.d.ts +18 -0
- package/dist/structure/init-block-constructor.d.ts +32 -0
- package/dist/structure/rules/block-package-json.d.ts +9 -0
- package/dist/structure/rules/block.d.ts +1 -0
- package/dist/structure/rules/migrations.d.ts +2 -0
- package/dist/structure/rules/model-package-json.d.ts +3 -0
- package/dist/structure/rules/model.d.ts +1 -0
- package/dist/structure/rules/root-catalog-bump.d.ts +1 -0
- package/dist/structure/rules/root-gitignore.d.ts +2 -0
- package/dist/structure/rules/root-package-json.d.ts +3 -0
- package/dist/structure/rules/root-pnpm-workspace.d.ts +15 -0
- package/dist/structure/rules/root.d.ts +1 -0
- package/dist/structure/rules/shared/colocated-tests.d.ts +2 -0
- package/dist/structure/rules/shared/key-order.d.ts +1 -0
- package/dist/structure/rules/software-package-json.d.ts +3 -0
- package/dist/structure/rules/software.d.ts +1 -0
- package/dist/structure/rules/test-package-json.d.ts +3 -0
- package/dist/structure/rules/test.d.ts +1 -0
- package/dist/structure/rules/ui-package-json.d.ts +3 -0
- package/dist/structure/rules/ui.d.ts +1 -0
- package/dist/structure/rules/workflow-package-json.d.ts +3 -0
- package/dist/structure/rules/workflow.d.ts +1 -0
- package/dist/structure/structure-definition.d.ts +1 -0
- package/package.json +9 -7
- package/src/cmd/index.ts +10 -4
- package/src/cmd/structure/check.ts +51 -0
- package/src/cmd/structure/init.ts +65 -0
- package/src/cmd/structure/refresh.ts +44 -0
- package/src/cmd/update-deps.ts +15 -3
- package/src/structure/__tests__/colocated-test-wiring.test.ts +107 -0
- package/src/structure/__tests__/floor-version.test.ts +52 -0
- package/src/structure/__tests__/init-catalog-resolution.test.ts +82 -0
- package/src/structure/__tests__/init-check-parity.test.ts +59 -0
- package/src/structure/__tests__/model-package-json.snapshot.test.ts +60 -0
- package/src/structure/__tests__/oxfmt-clean-init.test.ts +64 -0
- package/src/structure/__tests__/root-catalog-bump.test.ts +110 -0
- package/src/structure/__tests__/seed-disjointness.test.ts +79 -0
- package/src/structure/cli/run-structure.ts +89 -0
- package/src/structure/engine/__tests__/content-rules/active-state-guards.test.ts +61 -0
- package/src/structure/engine/__tests__/content-rules/catalog-pin.test.ts +46 -0
- package/src/structure/engine/__tests__/content-rules/enforce-alphabetical-order.test.ts +55 -0
- package/src/structure/engine/__tests__/content-rules/enforce-field-order.test.ts +73 -0
- package/src/structure/engine/__tests__/content-rules/ensure-catalog-absent.test.ts +29 -0
- package/src/structure/engine/__tests__/content-rules/ensure-catalog-latest.test.ts +70 -0
- package/src/structure/engine/__tests__/content-rules/ensure-dep.test.ts +63 -0
- package/src/structure/engine/__tests__/content-rules/ensure-deps-bulk.test.ts +46 -0
- package/src/structure/engine/__tests__/content-rules/ensure-field-entries.test.ts +35 -0
- package/src/structure/engine/__tests__/content-rules/ensure-field.test.ts +56 -0
- package/src/structure/engine/__tests__/content-rules/ensure-script.test.ts +39 -0
- package/src/structure/engine/__tests__/content-rules/ensure-workspace-scope-deps.test.ts +92 -0
- package/src/structure/engine/__tests__/content-rules/gitignore.test.ts +50 -0
- package/src/structure/engine/__tests__/content-rules/pin-catalog-to.test.ts +64 -0
- package/src/structure/engine/__tests__/content-rules/prune-keys.test.ts +59 -0
- package/src/structure/engine/__tests__/content-rules/registry-client.test.ts +116 -0
- package/src/structure/engine/__tests__/content-rules/remove-field.test.ts +49 -0
- package/src/structure/engine/__tests__/content-rules/require-field.test.ts +40 -0
- package/src/structure/engine/__tests__/content-rules/transform-at.test.ts +33 -0
- package/src/structure/engine/__tests__/content-rules/when-inner.test.ts +244 -0
- package/src/structure/engine/__tests__/content-rules/workspace-module-paths.test.ts +53 -0
- package/src/structure/engine/__tests__/discovery.test.ts +160 -0
- package/src/structure/engine/__tests__/fs-memory.test.ts +59 -0
- package/src/structure/engine/__tests__/glob.test.ts +43 -0
- package/src/structure/engine/__tests__/parsers-json.test.ts +189 -0
- package/src/structure/engine/__tests__/parsers-lines.test.ts +44 -0
- package/src/structure/engine/__tests__/parsers-yaml.test.ts +90 -0
- package/src/structure/engine/__tests__/recheck.test.ts +99 -0
- package/src/structure/engine/__tests__/registry-lookup.test.ts +49 -0
- package/src/structure/engine/__tests__/runner-when-inside-managed.test.ts +136 -0
- package/src/structure/engine/__tests__/runner.test.ts +136 -0
- package/src/structure/engine/__tests__/synthetic.test.ts +153 -0
- package/src/structure/engine/__tests__/version.test.ts +44 -0
- package/src/structure/engine/__tests__/when-files-exist.test.ts +89 -0
- package/src/structure/engine/api.ts +131 -0
- package/src/structure/engine/builders.ts +341 -0
- package/src/structure/engine/content-rules.ts +616 -0
- package/src/structure/engine/ctx.ts +94 -0
- package/src/structure/engine/discovery-fs.ts +209 -0
- package/src/structure/engine/discovery.ts +168 -0
- package/src/structure/engine/flatten.ts +117 -0
- package/src/structure/engine/fs/api.ts +40 -0
- package/src/structure/engine/fs/memory.ts +132 -0
- package/src/structure/engine/fs/node.ts +89 -0
- package/src/structure/engine/glob.ts +42 -0
- package/src/structure/engine/ir.ts +149 -0
- package/src/structure/engine/parsers/json.ts +212 -0
- package/src/structure/engine/parsers/lines.ts +42 -0
- package/src/structure/engine/parsers/yaml.ts +72 -0
- package/src/structure/engine/registry-client.ts +140 -0
- package/src/structure/engine/runner.ts +519 -0
- package/src/structure/engine/templates.ts +74 -0
- package/src/structure/engine/testing.ts +93 -0
- package/src/structure/engine/version.ts +60 -0
- package/src/structure/init-block-constructor.ts +164 -0
- package/src/structure/rules/block-package-json.ts +156 -0
- package/src/structure/rules/block.ts +29 -0
- package/src/structure/rules/migrations.ts +63 -0
- package/src/structure/rules/model-package-json.ts +123 -0
- package/src/structure/rules/model.ts +65 -0
- package/src/structure/rules/root-catalog-bump.ts +77 -0
- package/src/structure/rules/root-gitignore.ts +27 -0
- package/src/structure/rules/root-package-json.ts +119 -0
- package/src/structure/rules/root-pnpm-workspace.ts +103 -0
- package/src/structure/rules/root.ts +43 -0
- package/src/structure/rules/shared/colocated-tests.ts +11 -0
- package/src/structure/rules/shared/key-order.ts +34 -0
- package/src/structure/rules/software-package-json.ts +86 -0
- package/src/structure/rules/software.ts +33 -0
- package/src/structure/rules/test-package-json.ts +70 -0
- package/src/structure/rules/test.ts +40 -0
- package/src/structure/rules/ui-package-json.ts +108 -0
- package/src/structure/rules/ui.ts +75 -0
- package/src/structure/rules/workflow-package-json.ts +92 -0
- package/src/structure/rules/workflow.ts +51 -0
- package/src/structure/structure-definition.ts +28 -0
- package/src/structure/templates/static/block/index.d.ts +6 -0
- package/src/structure/templates/static/block/index.js +8 -0
- package/src/structure/templates/static/block/logos/block-logo.png +0 -0
- package/src/structure/templates/static/block/logos/organization-logo.png +0 -0
- package/src/structure/templates/static/model/.oxfmtrc.json +4 -0
- package/src/structure/templates/static/model/.oxlintrc.json +3 -0
- package/src/structure/templates/static/model/src/index.ts +13 -0
- package/src/structure/templates/static/model/tsconfig.json +4 -0
- package/src/structure/templates/static/root/.gitignore +13 -0
- package/src/structure/templates/static/root/.vscode/settings.json +3 -0
- package/src/structure/templates/static/root/turbo.json +35 -0
- package/src/structure/templates/static/test/tsconfig.json +5 -0
- package/src/structure/templates/static/test/vitest.config.mts +8 -0
- package/src/structure/templates/static/ui/.oxfmtrc.json +3 -0
- package/src/structure/templates/static/ui/.oxlintrc.json +3 -0
- package/src/structure/templates/static/ui/index.html +12 -0
- package/src/structure/templates/static/ui/src/main.ts +5 -0
- package/src/structure/templates/static/ui/tsconfig.json +4 -0
- package/src/structure/templates/static/workflow/format.el +42 -0
- package/src/structure/templates/static/workflow/tsconfig.json +16 -0
- package/src/structure/templates/static/workflow/vitest.config.mts +9 -0
- package/src/structure/templates/text/README.md +5 -0
- package/src/structure/templates/text/docs/description.md +3 -0
- package/src/structure/templates/text/ui/src/MainPage.tpl.vue +9 -0
- package/src/structure/templates/text/ui/src/app.tpl.ts +11 -0
- package/src/structure/templates/text/workflow/src/main.tpl.tengo +10 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Args, Command, Flags } from "@oclif/core";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
resolveBlockVars,
|
|
5
|
+
runInit,
|
|
6
|
+
SUPPORTED_PLATFORMS,
|
|
7
|
+
type InitFlagValues,
|
|
8
|
+
} from "../../structure/init-block-constructor";
|
|
9
|
+
|
|
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.");
|
|
45
|
+
}
|
|
46
|
+
const withSoftware = flags["with-software"] ? true : flags["no-software"] ? false : undefined;
|
|
47
|
+
|
|
48
|
+
const flagValues: InitFlagValues = {
|
|
49
|
+
npmOrg: flags["npm-org"],
|
|
50
|
+
orgScope: flags["org-scope"],
|
|
51
|
+
shortName: flags["short-name"],
|
|
52
|
+
withSoftware,
|
|
53
|
+
platform: flags.platform,
|
|
54
|
+
nonInteractive: flags["non-interactive"],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
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");
|
|
62
|
+
|
|
63
|
+
await runInit({ vars, blockPath, templatesRoot, log: (m) => this.log(m) });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Command, Flags } from "@oclif/core";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { runStructureForPath, formatChanges } from "../../structure/cli/run-structure";
|
|
4
|
+
|
|
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
|
+
];
|
|
14
|
+
|
|
15
|
+
static override strict = false;
|
|
16
|
+
|
|
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
|
+
};
|
|
26
|
+
|
|
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");
|
|
31
|
+
|
|
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
|
+
}
|
|
44
|
+
}
|
package/src/cmd/update-deps.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { Command } from "@oclif/core";
|
|
2
|
-
import { updatePackages } from "@platforma-sdk/blocks-deps-updater";
|
|
3
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Deprecated. Phase-2 forwarding alias (spec.md § "Coexistence And
|
|
5
|
+
* Retirement"): delegates to `structure refresh --update-deps-only`,
|
|
6
|
+
* which lands at parity with the old `blocks-deps-updater` catalog-bump
|
|
7
|
+
* semantics. Prints a deprecation warning; will be removed once the
|
|
8
|
+
* structurer path has been live without surprises.
|
|
9
|
+
*/
|
|
4
10
|
export default class UpdateDeps extends Command {
|
|
5
11
|
static override description =
|
|
6
|
-
"
|
|
12
|
+
"[DEPRECATED] Use `block-tools structure refresh --update-deps-only`. This alias delegates to it.";
|
|
7
13
|
|
|
8
14
|
static override examples = ["<%= config.bin %> <%= command.id %>"];
|
|
9
15
|
|
|
10
16
|
public async run(): Promise<void> {
|
|
11
|
-
|
|
17
|
+
this.warn(
|
|
18
|
+
"`block-tools update-deps` is deprecated and will be removed in a future release. " +
|
|
19
|
+
"Use `block-tools structure refresh --update-deps-only` instead. Delegating now.",
|
|
20
|
+
);
|
|
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"]);
|
|
12
24
|
}
|
|
13
25
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// End-to-end (real STRUCTURE): co-located unit tests wire the vitest `test`
|
|
2
|
+
// script + node ambient types into a scope, and ONLY that scope. A
|
|
3
|
+
// test-less scope stays lean (no test script, no @types/node, no node types
|
|
4
|
+
// in tsconfig) and a refresh of a test-less block is a fixpoint.
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect } from "vitest";
|
|
7
|
+
import { parse as parseYaml } from "yaml";
|
|
8
|
+
import { simulateInit, defaultTemplateProvider } from "../engine/testing";
|
|
9
|
+
import { run as engineRun } from "../engine/runner";
|
|
10
|
+
import { discoverRunContext } from "../engine/discovery-fs";
|
|
11
|
+
import { STRUCTURE } from "../structure-definition";
|
|
12
|
+
import { SDK_CATALOG_PACKAGES } from "../rules/root-pnpm-workspace";
|
|
13
|
+
import type { BlockVars } from "../engine/api";
|
|
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
|
+
const mockLookup = (name: string): string | undefined =>
|
|
24
|
+
SDK_CATALOG_PACKAGES.includes(name) ? "9.9.9" : undefined;
|
|
25
|
+
|
|
26
|
+
type Pkg = {
|
|
27
|
+
scripts?: Record<string, string>;
|
|
28
|
+
devDependencies?: Record<string, string>;
|
|
29
|
+
peerDependencies?: Record<string, string>;
|
|
30
|
+
};
|
|
31
|
+
type Tsconfig = { compilerOptions?: { types?: unknown } };
|
|
32
|
+
|
|
33
|
+
async function refresh(fs: Awaited<ReturnType<typeof simulateInit>>["fs"]) {
|
|
34
|
+
const ctx = discoverRunContext({ fs, isSdkInternal: false });
|
|
35
|
+
engineRun(STRUCTURE, fs, ctx, {
|
|
36
|
+
templates: defaultTemplateProvider(),
|
|
37
|
+
rediscover: () => discoverRunContext({ fs, isSdkInternal: false, dryRun: true }),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("co-located test wiring (real STRUCTURE)", () => {
|
|
42
|
+
test("a model test wires test script + @types/node devDep + node types; ui (no test) stays lean", async () => {
|
|
43
|
+
const { fs } = simulateInit({ vars: VARS, registryLookup: mockLookup });
|
|
44
|
+
// Author drops a co-located unit test into the model scope only.
|
|
45
|
+
fs.write("model/src/label.test.ts", `import { test } from "vitest";\ntest("x", () => {});\n`);
|
|
46
|
+
|
|
47
|
+
await refresh(fs);
|
|
48
|
+
|
|
49
|
+
const model = JSON.parse(fs.read("model/package.json")) as Pkg;
|
|
50
|
+
expect(model.scripts?.test).toBe("vitest run --passWithNoTests");
|
|
51
|
+
expect(model.devDependencies?.["@types/node"]).toBe("*");
|
|
52
|
+
// promoted from peer -> dev (single-section invariant drops the peer)
|
|
53
|
+
expect(model.peerDependencies?.["@types/node"]).toBeUndefined();
|
|
54
|
+
|
|
55
|
+
const modelTs = JSON.parse(fs.read("model/tsconfig.json")) as Tsconfig;
|
|
56
|
+
expect(modelTs.compilerOptions?.types).toEqual(["node"]);
|
|
57
|
+
|
|
58
|
+
// ui has no co-located test -> stays lean.
|
|
59
|
+
const ui = JSON.parse(fs.read("ui/package.json")) as Pkg;
|
|
60
|
+
expect(ui.scripts?.test).toBeUndefined();
|
|
61
|
+
expect(ui.devDependencies?.["@types/node"]).toBeUndefined();
|
|
62
|
+
const uiTs = JSON.parse(fs.read("ui/tsconfig.json")) as Tsconfig;
|
|
63
|
+
expect(uiTs.compilerOptions?.types).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("test-less block: refresh is a fixpoint (no test script, no node types)", async () => {
|
|
67
|
+
const { fs } = simulateInit({ vars: VARS, registryLookup: mockLookup });
|
|
68
|
+
|
|
69
|
+
const before = {
|
|
70
|
+
model: fs.read("model/package.json"),
|
|
71
|
+
modelTs: fs.read("model/tsconfig.json"),
|
|
72
|
+
ui: fs.read("ui/package.json"),
|
|
73
|
+
uiTs: fs.read("ui/tsconfig.json"),
|
|
74
|
+
workflow: fs.read("workflow/package.json"),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
await refresh(fs);
|
|
78
|
+
|
|
79
|
+
expect(fs.read("model/package.json")).toBe(before.model);
|
|
80
|
+
expect(fs.read("model/tsconfig.json")).toBe(before.modelTs);
|
|
81
|
+
expect(fs.read("ui/package.json")).toBe(before.ui);
|
|
82
|
+
expect(fs.read("ui/tsconfig.json")).toBe(before.uiTs);
|
|
83
|
+
expect(fs.read("workflow/package.json")).toBe(before.workflow);
|
|
84
|
+
|
|
85
|
+
// none of the test-less scopes carry a `test` script
|
|
86
|
+
const model = JSON.parse(before.model) as Pkg;
|
|
87
|
+
const ui = JSON.parse(before.ui) as Pkg;
|
|
88
|
+
const workflow = JSON.parse(before.workflow) as Pkg;
|
|
89
|
+
expect(model.scripts?.test).toBeUndefined();
|
|
90
|
+
expect(ui.scripts?.test).toBeUndefined();
|
|
91
|
+
expect(workflow.scripts?.test).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a workflow test wires its test script (no node-types wiring needed there)", async () => {
|
|
95
|
+
const { fs } = simulateInit({ vars: VARS, registryLookup: mockLookup });
|
|
96
|
+
fs.write("workflow/src/wf.test.ts", `import { test } from "vitest";\ntest("x", () => {});\n`);
|
|
97
|
+
|
|
98
|
+
await refresh(fs);
|
|
99
|
+
|
|
100
|
+
const workflow = JSON.parse(fs.read("workflow/package.json")) as Pkg;
|
|
101
|
+
expect(workflow.scripts?.test).toBe("vitest run --passWithNoTests");
|
|
102
|
+
// workflow tsconfig stays standalone — no node types added there
|
|
103
|
+
const wfTs = parseYaml(fs.read("workflow/tsconfig.json")) as { types?: unknown };
|
|
104
|
+
// (workflow tsconfig is JSON, but parseYaml reads JSON too; types:[] unchanged)
|
|
105
|
+
expect(wfTs.types).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Floor-version refusal — when `.structure` carries a version below
|
|
2
|
+
// `STRUCTURE_MIN_SUPPORTED`, structurer entry points must abort with a
|
|
3
|
+
// hard error pointing at the legacy-migration skill.
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
STRUCTURE_MIN_SUPPORTED,
|
|
8
|
+
StructureVersionFloorError,
|
|
9
|
+
assertVersionAboveFloor,
|
|
10
|
+
readStructureVersion,
|
|
11
|
+
} from "../engine/version";
|
|
12
|
+
import { MemoryFileSystem } from "../engine/fs/memory";
|
|
13
|
+
|
|
14
|
+
describe("floor-version refusal", () => {
|
|
15
|
+
test("version 0 below floor 1 throws StructureVersionFloorError", () => {
|
|
16
|
+
expect(() => assertVersionAboveFloor(0, 1)).toThrow(StructureVersionFloorError);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("error message names the version, the floor, and points at legacy migration", () => {
|
|
20
|
+
try {
|
|
21
|
+
assertVersionAboveFloor(0, 1);
|
|
22
|
+
throw new Error("expected throw");
|
|
23
|
+
} catch (e) {
|
|
24
|
+
expect(e).toBeInstanceOf(StructureVersionFloorError);
|
|
25
|
+
const msg = (e as Error).message;
|
|
26
|
+
expect(msg).toContain("0");
|
|
27
|
+
expect(msg).toContain("1");
|
|
28
|
+
expect(msg).toMatch(/legacy-migration/i);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("at-floor and above-floor versions do not throw", () => {
|
|
33
|
+
expect(() => assertVersionAboveFloor(1, 1)).not.toThrow();
|
|
34
|
+
expect(() => assertVersionAboveFloor(5, 1)).not.toThrow();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("default floor is STRUCTURE_MIN_SUPPORTED", () => {
|
|
38
|
+
// Document the constant invariant: shipping floor stays 0 until a
|
|
39
|
+
// legacy-migration skill exists. Bumping the floor without that
|
|
40
|
+
// skill in place would brick existing blocks.
|
|
41
|
+
expect(STRUCTURE_MIN_SUPPORTED).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("read+assert against a fixture .structure with version 0 and floor 1 throws", async () => {
|
|
45
|
+
const fs = new MemoryFileSystem({
|
|
46
|
+
".structure": JSON.stringify({ version: 0 }) + "\n",
|
|
47
|
+
});
|
|
48
|
+
const version = await readStructureVersion(fs);
|
|
49
|
+
expect(version).toBe(0);
|
|
50
|
+
expect(() => assertVersionAboveFloor(version, 1)).toThrow(StructureVersionFloorError);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Layer-2: the version-model redesign end-to-end against the real
|
|
2
|
+
// STRUCTURE, mocked registry (no network).
|
|
3
|
+
//
|
|
4
|
+
// Proves the three promises of the `onInitOrUpdate` redesign:
|
|
5
|
+
// 1. init writes REAL latest SDK versions (the `onInitOrUpdate` frame
|
|
6
|
+
// fires on init), while the infra floor stays `~`-pinned and untouched;
|
|
7
|
+
// 2. a default refresh touches NO catalog versions — the central
|
|
8
|
+
// no-pin-on-refresh guard (byte-identical pnpm-workspace.yaml);
|
|
9
|
+
// 3. init = fixpoint of check: a dry-run after init reports no
|
|
10
|
+
// pnpm-workspace.yaml change.
|
|
11
|
+
|
|
12
|
+
import { describe, test, expect } from "vitest";
|
|
13
|
+
import { parse as parseYaml } from "yaml";
|
|
14
|
+
import { simulateInit, defaultTemplateProvider } from "../engine/testing";
|
|
15
|
+
import { run as engineRun } from "../engine/runner";
|
|
16
|
+
import { discoverRunContext } from "../engine/discovery-fs";
|
|
17
|
+
import { STRUCTURE } from "../structure-definition";
|
|
18
|
+
import { matchesBumpPattern } from "../engine/registry-client";
|
|
19
|
+
import { SDK_CATALOG_PACKAGES, INFRA_CATALOG_FLOOR } from "../rules/root-pnpm-workspace";
|
|
20
|
+
import type { BlockVars } from "../engine/api";
|
|
21
|
+
|
|
22
|
+
const VARS: BlockVars = {
|
|
23
|
+
facadeName: "@platforma-open/test-org.demo",
|
|
24
|
+
baseName: "test-org.demo",
|
|
25
|
+
npmOrg: "@platforma-open",
|
|
26
|
+
orgScope: "test-org",
|
|
27
|
+
shortName: "demo",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// A distinct mock "latest" per SDK package so we can prove per-name
|
|
31
|
+
// resolution. Anything not in the SDK set resolves to undefined.
|
|
32
|
+
const LATEST: Record<string, string> = Object.fromEntries(
|
|
33
|
+
SDK_CATALOG_PACKAGES.map((n, i) => [n, `9${i}.4.2`]),
|
|
34
|
+
);
|
|
35
|
+
const mockLookup = (name: string): string | undefined => LATEST[name];
|
|
36
|
+
|
|
37
|
+
function catalogOf(yamlText: string): Record<string, string> {
|
|
38
|
+
return (parseYaml(yamlText) as { catalog: Record<string, string> }).catalog;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("version model — init fetch + no-pin-on-refresh", () => {
|
|
42
|
+
test("init writes latest SDK versions; infra floor stays ~-pinned", async () => {
|
|
43
|
+
const { fs } = simulateInit({ vars: VARS, registryLookup: mockLookup });
|
|
44
|
+
const cat = catalogOf(fs.read("pnpm-workspace.yaml"));
|
|
45
|
+
|
|
46
|
+
// Every SDK family entry resolved to the mocked latest (exact, no modifier).
|
|
47
|
+
for (const name of SDK_CATALOG_PACKAGES) {
|
|
48
|
+
expect(matchesBumpPattern(name)).toBe(true);
|
|
49
|
+
expect(cat[name]).toBe(LATEST[name]);
|
|
50
|
+
}
|
|
51
|
+
// Infra floor untouched — still its curated `~` pin.
|
|
52
|
+
for (const [name, floor] of Object.entries(INFRA_CATALOG_FLOOR)) {
|
|
53
|
+
expect(cat[name]).toBe(floor);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("default refresh touches NO catalog versions (byte-identical)", async () => {
|
|
58
|
+
const { fs } = simulateInit({ vars: VARS, registryLookup: mockLookup });
|
|
59
|
+
const afterInit = fs.read("pnpm-workspace.yaml");
|
|
60
|
+
|
|
61
|
+
// A real default refresh — no registryLookup passed, proving refresh
|
|
62
|
+
// never fetches. The onInitOrUpdate bump leaf is excluded from "default".
|
|
63
|
+
const refreshCtx = discoverRunContext({ fs, isSdkInternal: false });
|
|
64
|
+
engineRun(STRUCTURE, fs, refreshCtx, {
|
|
65
|
+
templates: defaultTemplateProvider(),
|
|
66
|
+
rediscover: () => discoverRunContext({ fs, isSdkInternal: false, dryRun: true }),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const afterRefresh = fs.read("pnpm-workspace.yaml");
|
|
70
|
+
expect(afterRefresh).toBe(afterInit);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("init = fixpoint of check: dry-run reports no pnpm-workspace.yaml change", async () => {
|
|
74
|
+
const { fs, ctx } = simulateInit({ vars: VARS, registryLookup: mockLookup });
|
|
75
|
+
const checkCtx = { ...ctx, dryRun: true };
|
|
76
|
+
const result = engineRun(STRUCTURE, fs, checkCtx, {
|
|
77
|
+
templates: defaultTemplateProvider(),
|
|
78
|
+
});
|
|
79
|
+
const workspaceChange = result.changes.find((c) => c.path === "pnpm-workspace.yaml");
|
|
80
|
+
expect(workspaceChange).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Invariant — `init → check` must report zero diff for every supported
|
|
2
|
+
// BlockVars variant.
|
|
3
|
+
//
|
|
4
|
+
// For each variant:
|
|
5
|
+
// 1. simulateInit populates an in-memory FS with seeds + scaffolds +
|
|
6
|
+
// fixed + managed in canonical form, then re-discovers the module set
|
|
7
|
+
// from that written FS (the same DISCOVERY `check` runs) — a real
|
|
8
|
+
// init→check round-trip, not a reuse of init's module set.
|
|
9
|
+
// 2. engine.run is invoked with `dryRun: true` on the same FS.
|
|
10
|
+
// 3. result.changes must be empty (every primitive idempotent).
|
|
11
|
+
|
|
12
|
+
import { describe, test, expect } from "vitest";
|
|
13
|
+
import type { BlockVars, RunContext } from "../engine/api";
|
|
14
|
+
import { STRUCTURE } from "../structure-definition";
|
|
15
|
+
import { run as engineRun } from "../engine/runner";
|
|
16
|
+
import { defaultTemplateProvider, simulateInit } from "../engine/testing";
|
|
17
|
+
|
|
18
|
+
const TEMPLATES = defaultTemplateProvider();
|
|
19
|
+
|
|
20
|
+
async function checkZeroDiff(vars: BlockVars): Promise<void> {
|
|
21
|
+
const { fs, ctx } = simulateInit({ vars });
|
|
22
|
+
const checkCtx: RunContext = { ...ctx, dryRun: true };
|
|
23
|
+
const result = engineRun(STRUCTURE, fs, checkCtx, { templates: TEMPLATES });
|
|
24
|
+
if (result.changes.length > 0) {
|
|
25
|
+
// Surface the first diff for diagnostics.
|
|
26
|
+
const first = result.changes[0]!;
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Layer-2 invariant violated for ${vars.facadeName}: ` +
|
|
29
|
+
`${first.primitive} '${first.path}' (scope=${first.scope}, action=${first.action}). ` +
|
|
30
|
+
`Total changes: ${result.changes.length}.`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
expect(result.changes).toEqual([]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("Layer-2: init → check zero diff", () => {
|
|
37
|
+
test("with software (single platform)", async () => {
|
|
38
|
+
const vars: BlockVars = {
|
|
39
|
+
facadeName: "@platforma-open/test-org.demo",
|
|
40
|
+
baseName: "test-org.demo",
|
|
41
|
+
npmOrg: "@platforma-open",
|
|
42
|
+
orgScope: "test-org",
|
|
43
|
+
shortName: "demo",
|
|
44
|
+
softwarePlatform: "python",
|
|
45
|
+
};
|
|
46
|
+
await checkZeroDiff(vars);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("no software", async () => {
|
|
50
|
+
const vars: BlockVars = {
|
|
51
|
+
facadeName: "@platforma-open/test-org.demo",
|
|
52
|
+
baseName: "test-org.demo",
|
|
53
|
+
npmOrg: "@platforma-open",
|
|
54
|
+
orgScope: "test-org",
|
|
55
|
+
shortName: "demo",
|
|
56
|
+
};
|
|
57
|
+
await checkZeroDiff(vars);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Snapshot of the canonical `model/package.json` that `init` produces
|
|
2
|
+
// for a representative BlockVars set. Catches accidental template /
|
|
3
|
+
// rule drift — any rule-set change that intentionally shifts the
|
|
4
|
+
// generated content must be paired with an updated fixture.
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import { simulateInit } from "../engine/testing";
|
|
8
|
+
import type { BlockVars } from "../engine/api";
|
|
9
|
+
|
|
10
|
+
const EXPECTED_MODEL_PACKAGE_JSON = `{
|
|
11
|
+
"name": "@platforma-open/test-org.demo.model",
|
|
12
|
+
"version": "1.0.0",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "dist/index.cjs",
|
|
15
|
+
"module": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"sources": "./src/index.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./dist/*": "./dist/*"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"fmt": "ts-builder format",
|
|
27
|
+
"watch": "ts-builder build --target block-model --watch",
|
|
28
|
+
"build": "ts-builder build --target block-model && block-tools build-model",
|
|
29
|
+
"check": "ts-builder check --target block-model"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@platforma-sdk/model": "catalog:"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@milaboratories/ts-builder": "catalog:",
|
|
36
|
+
"@milaboratories/ts-configs": "catalog:",
|
|
37
|
+
"@platforma-sdk/block-tools": "catalog:",
|
|
38
|
+
"vitest": "catalog:"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@types/node": "*",
|
|
42
|
+
"typescript": "*"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
describe("model/package.json snapshot", () => {
|
|
48
|
+
test("init produces canonical model/package.json", async () => {
|
|
49
|
+
const vars: BlockVars = {
|
|
50
|
+
facadeName: "@platforma-open/test-org.demo",
|
|
51
|
+
baseName: "test-org.demo",
|
|
52
|
+
npmOrg: "@platforma-open",
|
|
53
|
+
orgScope: "test-org",
|
|
54
|
+
shortName: "demo",
|
|
55
|
+
};
|
|
56
|
+
const { fs } = simulateInit({ vars });
|
|
57
|
+
const actual = fs.read("model/package.json");
|
|
58
|
+
expect(actual).toBe(EXPECTED_MODEL_PACKAGE_JSON);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The model + ui scopes are the two `ts-builder check` runs oxfmt on, so
|
|
2
|
+
// their rules cede field / dependency ordering to oxfmt (no enforce* calls).
|
|
3
|
+
// This guard proves the init output is already oxfmt-clean — `oxfmt --check`
|
|
4
|
+
// passes with no prior `pnpm fmt` — so the first refresh/check is a no-op.
|
|
5
|
+
//
|
|
6
|
+
// Requires the `oxfmt` binary (declared as a block-tools devDep). When it
|
|
7
|
+
// is genuinely unavailable the test skips with a warning rather than
|
|
8
|
+
// failing an environment that has no formatter installed.
|
|
9
|
+
|
|
10
|
+
import { describe, test, expect } from "vitest";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { simulateInit } from "../engine/testing";
|
|
16
|
+
import type { BlockVars } from "../engine/api";
|
|
17
|
+
|
|
18
|
+
function oxfmtAvailable(): boolean {
|
|
19
|
+
const r = spawnSync("oxfmt", ["--help"], { encoding: "utf-8" });
|
|
20
|
+
return !r.error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("oxfmt-clean init output (model + ui)", () => {
|
|
24
|
+
const available = oxfmtAvailable();
|
|
25
|
+
|
|
26
|
+
// Both files `ts-builder check` runs oxfmt on, per scope: package.json AND
|
|
27
|
+
// tsconfig.json. tsconfig is the regression that motivated the
|
|
28
|
+
// width-collapsing serialiser — oxfmt collapses its single-element
|
|
29
|
+
// `include` array, which the plain `JSON.stringify` expansion left
|
|
30
|
+
// non-clean. Guard both so a future serialiser change is caught here, not
|
|
31
|
+
// in the build→check gate.
|
|
32
|
+
const FILES = ["package.json", "tsconfig.json"];
|
|
33
|
+
|
|
34
|
+
test.runIf(available)("model + ui init package.json + tsconfig pass oxfmt --check", async () => {
|
|
35
|
+
const vars: BlockVars = {
|
|
36
|
+
facadeName: "@platforma-open/test-org.demo",
|
|
37
|
+
baseName: "test-org.demo",
|
|
38
|
+
npmOrg: "@platforma-open",
|
|
39
|
+
orgScope: "test-org",
|
|
40
|
+
shortName: "demo",
|
|
41
|
+
};
|
|
42
|
+
const { fs } = simulateInit({ vars });
|
|
43
|
+
|
|
44
|
+
const root = mkdtempSync(path.join(tmpdir(), "oxfmt-clean-"));
|
|
45
|
+
for (const scope of ["model", "ui"]) {
|
|
46
|
+
mkdirSync(path.join(root, scope), { recursive: true });
|
|
47
|
+
for (const fileName of FILES) {
|
|
48
|
+
const content = fs.read(`${scope}/${fileName}`);
|
|
49
|
+
writeFileSync(path.join(root, scope, fileName), content);
|
|
50
|
+
const r = spawnSync("oxfmt", ["--check", path.join(scope, fileName)], {
|
|
51
|
+
cwd: root,
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
});
|
|
54
|
+
expect(r.status, `${scope}/${fileName} is not oxfmt-clean:\n${r.stdout}\n${r.stderr}`).toBe(
|
|
55
|
+
0,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!available) {
|
|
62
|
+
test.skip("oxfmt binary unavailable — skipping oxfmt-clean guard", () => {});
|
|
63
|
+
}
|
|
64
|
+
});
|