@platforma-sdk/block-tools 2.14.1 → 2.14.3
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 +17 -13
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +779 -730
- package/dist/cli.mjs.map +1 -1
- package/dist/structure/engine/testing.d.ts +3 -0
- package/dist/structure/rules/root-changeset-config.d.ts +8 -0
- package/dist/structure/rules/shared/initial-version.d.ts +10 -0
- package/package.json +8 -8
- package/src/structure/__tests__/facade-end-state.test.ts +89 -4
- package/src/structure/__tests__/init-catalog-completeness.test.ts +86 -0
- package/src/structure/__tests__/init-lint-clean.test.ts +84 -0
- package/src/structure/__tests__/model-package-json.snapshot.test.ts +1 -0
- package/src/structure/__tests__/root-changeset-config.test.ts +94 -0
- package/src/structure/__tests__/root-ci-workflows.test.ts +3 -0
- package/src/structure/engine/testing.ts +4 -0
- package/src/structure/rules/block-package-json.ts +6 -3
- package/src/structure/rules/kind-package-json.ts +5 -1
- package/src/structure/rules/model-package-json.ts +5 -1
- package/src/structure/rules/root-changeset-config.ts +61 -0
- package/src/structure/rules/root-package-json.ts +2 -1
- package/src/structure/rules/root-pnpm-workspace.ts +2 -0
- package/src/structure/rules/root.ts +15 -1
- package/src/structure/rules/shared/initial-version.ts +17 -0
- package/src/structure/rules/software-package-json.ts +6 -0
- 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/text/model/src/index.tpl.ts +1 -1
- package/src/structure/templates/text/workflows/build.tpl.yaml +1 -1
- package/src/structure/templates/text/workflows/mark-stable.tpl.yaml +1 -1
|
@@ -19,6 +19,9 @@ export type SimulateInitInput = {
|
|
|
19
19
|
* Absent → the SDK catalog keeps its seed versions (no bump), matching
|
|
20
20
|
* an offline scaffold; tests that assert init writes latest pass a mock. */
|
|
21
21
|
registryLookup?: (packageName: string) => string | undefined;
|
|
22
|
+
/** Sync lookup for the derived catalog pins. Absent → those entries are
|
|
23
|
+
* left out of the catalog; the real `init` always passes one. */
|
|
24
|
+
derivedPinLookup?: (catalogEntry: string) => string | undefined;
|
|
22
25
|
};
|
|
23
26
|
export type SimulatedInit = {
|
|
24
27
|
fs: MemoryFileSystem;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Seeded so the scaffold's own PR passes the `require-package-path-bump` gate
|
|
2
|
+
* without bumping: `changeset version` consumes it and leaves the facade at
|
|
3
|
+
* `INITIAL_MODULE_VERSION`, so the first version a user sees is 1.0.0 rather
|
|
4
|
+
* than 1.0.1. Seeded (not managed) — a refresh must never resurrect it once
|
|
5
|
+
* the first release has consumed it. */
|
|
6
|
+
export declare const EMPTY_CHANGESET_SEED = "---\n---\n\nInitial release.\n";
|
|
7
|
+
export declare function rootChangesetConfigInitial(): Record<string, unknown>;
|
|
8
|
+
export declare function rootChangesetConfigRules(): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Seeded at init for every module. `pnpm pack` rewrites the facade's
|
|
2
|
+
* `workspace:*` devDeps to concrete ranges, so a versionless sibling breaks
|
|
3
|
+
* `do-pack`. Changesets owns the field afterwards. */
|
|
4
|
+
export declare const INITIAL_MODULE_VERSION = "1.0.0";
|
|
5
|
+
/** Backfill `version` on a sibling that has none — a block scaffolded before
|
|
6
|
+
* the initials seeded the field keeps its versionless manifests through a
|
|
7
|
+
* `structure refresh`, and `do-pack` then dies resolving the facade's
|
|
8
|
+
* `workspace:*` deps. Never touches an existing value: past init the field
|
|
9
|
+
* belongs to changesets. */
|
|
10
|
+
export declare function ensureModuleVersion(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platforma-sdk/block-tools",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.3",
|
|
4
4
|
"description": "Utility to manipulate Platforma Blocks and Block Registry",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
"undici": "~7.16.0",
|
|
35
35
|
"yaml": "^2.8.0",
|
|
36
36
|
"zod": "~3.25.76",
|
|
37
|
-
"@milaboratories/pl-model-middle-layer": "1.32.0",
|
|
38
|
-
"@milaboratories/pl-http": "1.2.4",
|
|
39
|
-
"@milaboratories/pl-model-common": "1.48.0",
|
|
40
37
|
"@milaboratories/pl-model-backend": "1.4.21",
|
|
38
|
+
"@milaboratories/pl-model-common": "1.48.0",
|
|
39
|
+
"@milaboratories/pl-http": "1.2.4",
|
|
40
|
+
"@milaboratories/ts-helpers": "1.8.6",
|
|
41
41
|
"@platforma-sdk/blocks-deps-updater": "2.2.0",
|
|
42
|
-
"@milaboratories/
|
|
42
|
+
"@milaboratories/pl-model-middle-layer": "1.32.0",
|
|
43
43
|
"@platforma-sdk/package-builder-lib": "1.3.0",
|
|
44
|
-
"@milaboratories/
|
|
44
|
+
"@milaboratories/resolve-helper": "1.1.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
"vite": "^8.0.6",
|
|
57
57
|
"vite-plugin-dts": "^4.5.3",
|
|
58
58
|
"vitest": "^4.1.3",
|
|
59
|
-
"@milaboratories/
|
|
59
|
+
"@milaboratories/build-configs": "2.0.0",
|
|
60
60
|
"@milaboratories/ts-builder": "1.7.0",
|
|
61
|
-
"@milaboratories/
|
|
61
|
+
"@milaboratories/ts-configs": "1.4.0"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"build": "vite build",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Facade end-state coverage — the slim `block/` the structurer emits and
|
|
2
2
|
// enforces: block-scope detection by the top-level `block` field, the four-file
|
|
3
3
|
// `src/` surface + facade `tsconfig.json`, the slim publishable `package.json`,
|
|
4
|
-
// sibling privacy,
|
|
4
|
+
// sibling privacy, legacy-shim cleanup, and author-script preservation.
|
|
5
5
|
|
|
6
6
|
import { describe, test, expect } from "vitest";
|
|
7
7
|
import type { BlockVars, RunContext } from "../engine/api";
|
|
@@ -9,6 +9,7 @@ import { classifyOne, type WorkspacePackage } from "../engine/discovery";
|
|
|
9
9
|
import { STRUCTURE } from "../structure-definition";
|
|
10
10
|
import { run as engineRun } from "../engine/runner";
|
|
11
11
|
import { defaultTemplateProvider, simulateInit } from "../engine/testing";
|
|
12
|
+
import { INITIAL_MODULE_VERSION } from "../rules/shared/initial-version";
|
|
12
13
|
|
|
13
14
|
const TEMPLATES = defaultTemplateProvider();
|
|
14
15
|
|
|
@@ -166,14 +167,98 @@ describe("facade: `prepublishOnly` publish target branches on isSdkInternal", ()
|
|
|
166
167
|
});
|
|
167
168
|
|
|
168
169
|
describe("facade: sibling package privacy", () => {
|
|
169
|
-
|
|
170
|
+
// Private but versioned. `pnpm pack` in the facade has to rewrite each
|
|
171
|
+
// `workspace:*` devDep to a concrete range, so a versionless sibling fails
|
|
172
|
+
// `do-pack` with ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL. The kind needs
|
|
173
|
+
// one for a second reason: its own src imports `version` from its
|
|
174
|
+
// package.json.
|
|
175
|
+
test("kind / model / ui / workflow / test are `private` and versioned", () => {
|
|
170
176
|
const { fs } = simulateInit({ vars: VARS });
|
|
171
|
-
for (const scope of ["model", "ui", "workflow", "test"]) {
|
|
177
|
+
for (const scope of ["kind", "model", "ui", "workflow", "test"]) {
|
|
172
178
|
const p = JSON.parse(fs.read(`${scope}/package.json`));
|
|
173
179
|
expect(p.private, `${scope} must be private`).toBe(true);
|
|
174
|
-
expect(
|
|
180
|
+
expect(p.version, `${scope} must carry a version`).toBe(INITIAL_MODULE_VERSION);
|
|
175
181
|
}
|
|
176
182
|
});
|
|
183
|
+
|
|
184
|
+
test("every `workspace:*` dep of the facade resolves to a versioned package", () => {
|
|
185
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
186
|
+
const facade = JSON.parse(fs.read("block/package.json"));
|
|
187
|
+
const byName = new Map<string, string | undefined>();
|
|
188
|
+
for (const scope of ["kind", "model", "ui", "workflow", "test"]) {
|
|
189
|
+
const p = JSON.parse(fs.read(`${scope}/package.json`));
|
|
190
|
+
byName.set(p.name, p.version);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const workspaceDeps = Object.entries({
|
|
194
|
+
...facade.dependencies,
|
|
195
|
+
...facade.devDependencies,
|
|
196
|
+
}).filter(([, spec]) => String(spec).startsWith("workspace:"));
|
|
197
|
+
expect(workspaceDeps.length).toBeGreaterThan(0);
|
|
198
|
+
|
|
199
|
+
for (const [name] of workspaceDeps) {
|
|
200
|
+
expect(byName.get(name), `${name} is a workspace dep with no version`).toBeDefined();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("facade: sibling versions on refresh", () => {
|
|
206
|
+
// A block scaffolded before the initials seeded `version` carries versionless
|
|
207
|
+
// sibling manifests. `refresh` never re-runs an initial for a file that
|
|
208
|
+
// exists, so the bodies have to backfill the field themselves — otherwise
|
|
209
|
+
// `do-pack` dies on ERR_PNPM_CANNOT_RESOLVE_WORKSPACE_PROTOCOL rewriting the
|
|
210
|
+
// facade's `workspace:*` deps.
|
|
211
|
+
test("a refresh backfills `version` on versionless siblings", () => {
|
|
212
|
+
const { fs, ctx } = simulateInit({ vars: VARS });
|
|
213
|
+
const scopes = ["kind", "model", "ui", "workflow", "test"];
|
|
214
|
+
for (const scope of scopes) {
|
|
215
|
+
const p = JSON.parse(fs.read(`${scope}/package.json`));
|
|
216
|
+
delete p.version;
|
|
217
|
+
fs.write(`${scope}/package.json`, `${JSON.stringify(p, null, 2)}\n`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const refreshCtx: RunContext = { ...ctx, dryRun: false };
|
|
221
|
+
expect(() => engineRun(STRUCTURE, fs, refreshCtx, { templates: TEMPLATES })).not.toThrow();
|
|
222
|
+
|
|
223
|
+
for (const scope of scopes) {
|
|
224
|
+
const p = JSON.parse(fs.read(`${scope}/package.json`));
|
|
225
|
+
expect(p.version, `${scope} must be versioned after refresh`).toBe(INITIAL_MODULE_VERSION);
|
|
226
|
+
// `version` sits right after `name` — the canonical order the formatter
|
|
227
|
+
// enforces; a backfill appended at the end would fail `oxfmt --check`.
|
|
228
|
+
expect(Object.keys(p).indexOf("version")).toBe(1);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("a refresh leaves an existing sibling `version` alone (changesets owns it)", () => {
|
|
233
|
+
const { fs, ctx } = simulateInit({ vars: VARS });
|
|
234
|
+
const bumped = JSON.parse(fs.read("model/package.json"));
|
|
235
|
+
bumped.version = "3.4.5";
|
|
236
|
+
fs.write("model/package.json", `${JSON.stringify(bumped, null, 2)}\n`);
|
|
237
|
+
|
|
238
|
+
engineRun(STRUCTURE, fs, { ...ctx, dryRun: false }, { templates: TEMPLATES });
|
|
239
|
+
|
|
240
|
+
expect(JSON.parse(fs.read("model/package.json")).version).toBe("3.4.5");
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("facade: author scripts beyond the canonical set survive a refresh", () => {
|
|
245
|
+
test("a pre-existing `mark-stable` script is preserved", () => {
|
|
246
|
+
const { fs, ctx } = simulateInit({ vars: VARS });
|
|
247
|
+
const MARK_STABLE =
|
|
248
|
+
"block-tools mark-stable -r 's3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1'";
|
|
249
|
+
|
|
250
|
+
// `.github/workflows/mark-stable.yaml` (engine-generated) runs
|
|
251
|
+
// `cd block && pnpm run mark-stable`, so the facade must keep the script.
|
|
252
|
+
const before = JSON.parse(fs.read("block/package.json"));
|
|
253
|
+
before.scripts["mark-stable"] = MARK_STABLE;
|
|
254
|
+
fs.write("block/package.json", `${JSON.stringify(before, null, 2)}\n`);
|
|
255
|
+
|
|
256
|
+
const refreshCtx: RunContext = { ...ctx, dryRun: false };
|
|
257
|
+
expect(() => engineRun(STRUCTURE, fs, refreshCtx, { templates: TEMPLATES })).not.toThrow();
|
|
258
|
+
|
|
259
|
+
const after = JSON.parse(fs.read("block/package.json"));
|
|
260
|
+
expect(after.scripts["mark-stable"]).toBe(MARK_STABLE);
|
|
261
|
+
});
|
|
177
262
|
});
|
|
178
263
|
|
|
179
264
|
describe("facade: legacy shim cleanup on refresh", () => {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// One `"x": "catalog:"` with no `x` catalog key fails the whole `pnpm install`
|
|
2
|
+
// with ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC. The dep sets live in
|
|
3
|
+
// `rules/*-package-json.ts` and the catalog in `rules/root-pnpm-workspace.ts`;
|
|
4
|
+
// nothing else ties the two together.
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect } from "vitest";
|
|
7
|
+
import { parse as parseYaml } from "yaml";
|
|
8
|
+
import { simulateInit } from "../engine/testing";
|
|
9
|
+
import { MemoryFileSystem } from "../engine/fs/memory";
|
|
10
|
+
import { SDK_CATALOG_PACKAGES, DERIVED_CATALOG_PINS } from "../rules/root-pnpm-workspace";
|
|
11
|
+
import type { BlockVars } from "../engine/api";
|
|
12
|
+
|
|
13
|
+
const DEP_SECTIONS = [
|
|
14
|
+
"dependencies",
|
|
15
|
+
"devDependencies",
|
|
16
|
+
"peerDependencies",
|
|
17
|
+
"optionalDependencies",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
const BASE_VARS: BlockVars = {
|
|
21
|
+
facadeName: "@platforma-open/test-org.demo",
|
|
22
|
+
baseName: "test-org.demo",
|
|
23
|
+
npmOrg: "@platforma-open",
|
|
24
|
+
orgScope: "test-org",
|
|
25
|
+
shortName: "demo",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Distinct mock versions, so a wrong-name lookup cannot pass by accident.
|
|
29
|
+
const registryLookup = (name: string): string | undefined =>
|
|
30
|
+
SDK_CATALOG_PACKAGES.includes(name) ? "9.9.9" : undefined;
|
|
31
|
+
const derivedPinLookup = (entry: string): string | undefined =>
|
|
32
|
+
DERIVED_CATALOG_PINS.some((pin) => pin.entry === entry) ? "8.8.8" : undefined;
|
|
33
|
+
|
|
34
|
+
/** `path → catalog-referencing dep names`, over every package.json written. */
|
|
35
|
+
function catalogReferences(fs: MemoryFileSystem): Map<string, string[]> {
|
|
36
|
+
const out = new Map<string, string[]>();
|
|
37
|
+
for (const path of fs.list("")) {
|
|
38
|
+
if (!path.endsWith("package.json")) continue;
|
|
39
|
+
const manifest = JSON.parse(fs.read(path)) as Record<string, unknown>;
|
|
40
|
+
const names: string[] = [];
|
|
41
|
+
for (const section of DEP_SECTIONS) {
|
|
42
|
+
const deps = manifest[section];
|
|
43
|
+
if (typeof deps !== "object" || deps === null) continue;
|
|
44
|
+
for (const [name, spec] of Object.entries(deps as Record<string, string>)) {
|
|
45
|
+
if (spec === "catalog:") names.push(name);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (names.length > 0) out.set(path, names);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function catalogOf(fs: MemoryFileSystem): Record<string, string> {
|
|
54
|
+
const parsed = parseYaml(fs.read("pnpm-workspace.yaml")) as {
|
|
55
|
+
catalog?: Record<string, string>;
|
|
56
|
+
};
|
|
57
|
+
return parsed.catalog ?? {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("init output is installable: every `catalog:` dep has a catalog entry", () => {
|
|
61
|
+
for (const [shape, vars] of [
|
|
62
|
+
["without software", BASE_VARS],
|
|
63
|
+
["with software", { ...BASE_VARS, softwarePlatform: "python" }],
|
|
64
|
+
] as const) {
|
|
65
|
+
test(`a block ${shape}`, () => {
|
|
66
|
+
const { fs } = simulateInit({ vars, registryLookup, derivedPinLookup });
|
|
67
|
+
const catalog = catalogOf(fs);
|
|
68
|
+
const references = catalogReferences(fs);
|
|
69
|
+
|
|
70
|
+
// Guard the guard: a walk that finds nothing would pass vacuously.
|
|
71
|
+
expect(references.size).toBeGreaterThan(0);
|
|
72
|
+
|
|
73
|
+
for (const [path, names] of references) {
|
|
74
|
+
for (const name of names) {
|
|
75
|
+
expect(
|
|
76
|
+
catalog[name],
|
|
77
|
+
`${path} declares "${name}": "catalog:", but pnpm-workspace.yaml has no ` +
|
|
78
|
+
`'${name}' catalog entry — pnpm install fails with ` +
|
|
79
|
+
`ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC. Add it to ` +
|
|
80
|
+
`SDK_CATALOG_PACKAGES or INFRA_CATALOG_FLOOR in rules/root-pnpm-workspace.ts.`,
|
|
81
|
+
).toBeDefined();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// `ts-builder format` runs oxlint with `--deny-warnings`, so one unused
|
|
2
|
+
// parameter in a seeded file fails `pnpm fmt` — the last step of `upgrade-sdk`.
|
|
3
|
+
|
|
4
|
+
import { describe, test, expect } from "vitest";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { simulateInit } from "../engine/testing";
|
|
11
|
+
import type { BlockVars } from "../engine/api";
|
|
12
|
+
|
|
13
|
+
const VARS: BlockVars = {
|
|
14
|
+
facadeName: "@platforma-open/test-org.demo",
|
|
15
|
+
baseName: "test-org.demo",
|
|
16
|
+
npmOrg: "@platforma-open",
|
|
17
|
+
orgScope: "test-org",
|
|
18
|
+
shortName: "demo",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function resolveTsBuilderConfig(configFile: string): string {
|
|
22
|
+
return fileURLToPath(import.meta.resolve(`@milaboratories/ts-builder/configs/${configFile}`));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** ts-builder pins its own oxlint, so block-tools' PATH copy is a different
|
|
26
|
+
* version. */
|
|
27
|
+
function resolveOxlintBinary(): string | undefined {
|
|
28
|
+
const configsDir = path.dirname(resolveTsBuilderConfig("oxlint-base.json"));
|
|
29
|
+
const binary = path.resolve(configsDir, "..", "..", "node_modules", ".bin", "oxlint");
|
|
30
|
+
return existsSync(binary) ? binary : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The scopes whose `fmt` script lints. */
|
|
34
|
+
const LINTED_SCOPES = ["kind", "model", "test", "ui"];
|
|
35
|
+
|
|
36
|
+
describe("init output is lint-clean (oxlint --deny-warnings)", () => {
|
|
37
|
+
const oxlint = resolveOxlintBinary();
|
|
38
|
+
|
|
39
|
+
test.runIf(oxlint)("every seeded source passes its scope's oxlint config", () => {
|
|
40
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
41
|
+
const root = mkdtempSync(path.join(tmpdir(), "init-lint-clean-"));
|
|
42
|
+
|
|
43
|
+
for (const scope of LINTED_SCOPES) {
|
|
44
|
+
const configPath = `${scope}/.oxlintrc.json`;
|
|
45
|
+
expect(fs.exists(configPath), `${scope} must carry an .oxlintrc.json`).toBe(true);
|
|
46
|
+
|
|
47
|
+
// The generated `extends` are relative to an installed node_modules.
|
|
48
|
+
const config = JSON.parse(fs.read(configPath)) as { extends?: string[] };
|
|
49
|
+
const extendsAbsolute = (config.extends ?? []).map((entry) =>
|
|
50
|
+
resolveTsBuilderConfig(path.basename(entry)),
|
|
51
|
+
);
|
|
52
|
+
expect(
|
|
53
|
+
extendsAbsolute.length,
|
|
54
|
+
`${scope}/.oxlintrc.json must extend a config`,
|
|
55
|
+
).toBeGreaterThan(0);
|
|
56
|
+
|
|
57
|
+
mkdirSync(path.join(root, scope), { recursive: true });
|
|
58
|
+
writeFileSync(
|
|
59
|
+
path.join(root, scope, ".oxlintrc.json"),
|
|
60
|
+
JSON.stringify({ ...config, extends: extendsAbsolute }),
|
|
61
|
+
);
|
|
62
|
+
for (const file of fs.list(`${scope}/src`)) {
|
|
63
|
+
const target = path.join(root, file);
|
|
64
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
65
|
+
writeFileSync(target, fs.read(file));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = spawnSync(
|
|
69
|
+
oxlint!,
|
|
70
|
+
["--config", path.join(root, scope, ".oxlintrc.json"), "--deny-warnings", "src"],
|
|
71
|
+
{ cwd: path.join(root, scope), encoding: "utf-8" },
|
|
72
|
+
);
|
|
73
|
+
expect(
|
|
74
|
+
result.status,
|
|
75
|
+
`seeded ${scope}/src is not lint-clean, so \`pnpm run fmt\` (and with it ` +
|
|
76
|
+
`\`pnpm run upgrade-sdk\`) fails on a fresh block:\n${result.stdout}\n${result.stderr}`,
|
|
77
|
+
).toBe(0);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!oxlint) {
|
|
82
|
+
test.skip("oxlint binary unavailable — skipping lint-clean guard", () => {});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// CI runs `pnpm run version-packages` on every PR, so a block with no
|
|
2
|
+
// `.changeset/config.json` fails its first CI run.
|
|
3
|
+
|
|
4
|
+
import { describe, test, expect } from "vitest";
|
|
5
|
+
import { simulateInit, defaultTemplateProvider } from "../engine/testing";
|
|
6
|
+
import { run as engineRun } from "../engine/runner";
|
|
7
|
+
import { discoverRunContext } from "../engine/discovery-fs";
|
|
8
|
+
import { STRUCTURE } from "../structure-definition";
|
|
9
|
+
import { MemoryFileSystem } from "../engine/fs/memory";
|
|
10
|
+
import type { BlockVars } from "../engine/api";
|
|
11
|
+
|
|
12
|
+
const VARS: BlockVars = {
|
|
13
|
+
facadeName: "@platforma-open/test-org.demo",
|
|
14
|
+
baseName: "test-org.demo",
|
|
15
|
+
npmOrg: "@platforma-open",
|
|
16
|
+
orgScope: "test-org",
|
|
17
|
+
shortName: "demo",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const PATH = ".changeset/config.json";
|
|
21
|
+
|
|
22
|
+
function refresh(fs: MemoryFileSystem, isSdkInternal = false) {
|
|
23
|
+
return engineRun(STRUCTURE, fs, discoverRunContext({ fs, isSdkInternal }), {
|
|
24
|
+
templates: defaultTemplateProvider(),
|
|
25
|
+
rediscover: () => discoverRunContext({ fs, isSdkInternal, dryRun: true }),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("root .changeset/config.json", () => {
|
|
30
|
+
test("init writes the config CI needs", () => {
|
|
31
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
32
|
+
const config = JSON.parse(fs.read(PATH));
|
|
33
|
+
expect(config.changelog).toBe("@changesets/cli/changelog");
|
|
34
|
+
expect(config.commit).toBe(false);
|
|
35
|
+
expect(config.access).toBe("restricted");
|
|
36
|
+
expect(config.baseBranch).toBe("main");
|
|
37
|
+
expect(config.updateInternalDependencies).toBe("patch");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("refresh creates it for a block that predates the rule", () => {
|
|
41
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
42
|
+
fs.delete(".changeset");
|
|
43
|
+
expect(fs.exists(PATH)).toBe(false);
|
|
44
|
+
|
|
45
|
+
refresh(fs);
|
|
46
|
+
expect(JSON.parse(fs.read(PATH)).baseBranch).toBe("main");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("refresh keeps author-owned lists and re-asserts the CI fields", () => {
|
|
50
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
51
|
+
const authored = {
|
|
52
|
+
...JSON.parse(fs.read(PATH)),
|
|
53
|
+
$schema: "https://unpkg.com/@changesets/config@3.0.2/schema.json",
|
|
54
|
+
ignore: ["@platforma-open/test-org.demo.ui"],
|
|
55
|
+
access: "public",
|
|
56
|
+
"//privatePackages": "a note the author added",
|
|
57
|
+
};
|
|
58
|
+
fs.write(PATH, JSON.stringify(authored, null, 2));
|
|
59
|
+
|
|
60
|
+
refresh(fs);
|
|
61
|
+
const after = JSON.parse(fs.read(PATH));
|
|
62
|
+
expect(after.ignore).toEqual(["@platforma-open/test-org.demo.ui"]);
|
|
63
|
+
expect(after.$schema).toBe("https://unpkg.com/@changesets/config@3.0.2/schema.json");
|
|
64
|
+
expect(after["//privatePackages"]).toBe("a note the author added");
|
|
65
|
+
expect(after.access).toBe("restricted");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("sdk-internal blocks get no config — the monorepo owns it", () => {
|
|
69
|
+
const { fs } = simulateInit({ vars: VARS, isSdkInternal: true });
|
|
70
|
+
expect(fs.exists(PATH)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("root .changeset/scaffold.md", () => {
|
|
75
|
+
const SEED = ".changeset/scaffold.md";
|
|
76
|
+
|
|
77
|
+
// An empty changeset names no package, so `changeset version` consumes it
|
|
78
|
+
// without bumping. That is what keeps the first RELEASED facade version at
|
|
79
|
+
// 1.0.0 instead of 1.0.1, and it passes CI's require-package-path-bump gate.
|
|
80
|
+
test("init seeds an empty changeset — no package named", () => {
|
|
81
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
82
|
+
const body = fs.read(SEED);
|
|
83
|
+
expect(body.startsWith("---\n---\n")).toBe(true);
|
|
84
|
+
expect(body).not.toContain(VARS.facadeName);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("refresh does not resurrect it once a release consumed it", () => {
|
|
88
|
+
const { fs } = simulateInit({ vars: VARS });
|
|
89
|
+
fs.delete(SEED);
|
|
90
|
+
|
|
91
|
+
refresh(fs);
|
|
92
|
+
expect(fs.exists(SEED)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -37,6 +37,9 @@ describe("root CI workflows (fixed, generated)", () => {
|
|
|
37
37
|
// Shared constants.
|
|
38
38
|
expect(build).toContain("team-id: 'ciplopen'");
|
|
39
39
|
expect(build).toContain("test: true");
|
|
40
|
+
// Both workflows track the toolchain's node major (mise pins 22).
|
|
41
|
+
expect(build).toContain("node-version: '22.x'");
|
|
42
|
+
expect(fs.read(MARK_STABLE)).toContain("node-version: '22.x'");
|
|
40
43
|
// Opt-in gate: the published `block` package must be bumped on PRs.
|
|
41
44
|
expect(build).toContain("require-package-path-bump: true");
|
|
42
45
|
// No bare `build` script exists (root-package-json removes it), so both
|
|
@@ -45,6 +45,9 @@ export type SimulateInitInput = {
|
|
|
45
45
|
* Absent → the SDK catalog keeps its seed versions (no bump), matching
|
|
46
46
|
* an offline scaffold; tests that assert init writes latest pass a mock. */
|
|
47
47
|
registryLookup?: (packageName: string) => string | undefined;
|
|
48
|
+
/** Sync lookup for the derived catalog pins. Absent → those entries are
|
|
49
|
+
* left out of the catalog; the real `init` always passes one. */
|
|
50
|
+
derivedPinLookup?: (catalogEntry: string) => string | undefined;
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
export type SimulatedInit = {
|
|
@@ -84,6 +87,7 @@ export function simulateInit(input: SimulateInitInput): SimulatedInit {
|
|
|
84
87
|
templates,
|
|
85
88
|
initMode: true,
|
|
86
89
|
registryLookup: input.registryLookup,
|
|
90
|
+
derivedPinLookup: input.derivedPinLookup,
|
|
87
91
|
});
|
|
88
92
|
|
|
89
93
|
// Re-discover from the written FS — the returned ctx is what `check`
|
|
@@ -11,13 +11,13 @@ import {
|
|
|
11
11
|
ensureDevDep,
|
|
12
12
|
ensureWorkspaceScopeDevDeps,
|
|
13
13
|
requireField,
|
|
14
|
-
removeScript,
|
|
15
14
|
enforceAlphabeticalOrder,
|
|
16
15
|
enforceFieldOrder,
|
|
17
16
|
type RunContext,
|
|
18
17
|
} from "../engine/api";
|
|
19
18
|
import { findModules, scopeDepMaps } from "../engine/ctx";
|
|
20
19
|
import { canonicalPackageJsonOrder } from "./shared/key-order";
|
|
20
|
+
import { INITIAL_MODULE_VERSION } from "./shared/initial-version";
|
|
21
21
|
|
|
22
22
|
/** Per-org publish coordinates: the S3 upload bucket and the public registry
|
|
23
23
|
* serve URL the facade's `prepublishOnly` script passes to `block-tools
|
|
@@ -177,7 +177,7 @@ export function blockPackageJsonInitial(ctx: RunContext): Record<string, unknown
|
|
|
177
177
|
assertBlockFieldsFilled(block);
|
|
178
178
|
return {
|
|
179
179
|
name: `${v.facadeName}.block`,
|
|
180
|
-
version:
|
|
180
|
+
version: INITIAL_MODULE_VERSION,
|
|
181
181
|
// Facade is ESM: rolldown-plugin-dts requires it, and the BlockPointer's
|
|
182
182
|
// `import.meta.url` path math needs it.
|
|
183
183
|
type: "module",
|
|
@@ -220,7 +220,10 @@ export function blockPackageJsonRules(ctx: RunContext): void {
|
|
|
220
220
|
|
|
221
221
|
const scripts = blockScripts(v.npmOrg, ctx.isSdkInternal);
|
|
222
222
|
for (const [name, command] of Object.entries(scripts)) ensureScript(name, command);
|
|
223
|
-
|
|
223
|
+
// Scripts beyond the canonical set are block-author territory — left as-is.
|
|
224
|
+
// `mark-stable` in particular is invoked by the engine-generated
|
|
225
|
+
// `.github/workflows/mark-stable.yaml` (which does `cd block && pnpm run
|
|
226
|
+
// mark-stable`), so stripping it broke that workflow.
|
|
224
227
|
|
|
225
228
|
// Slim invariant: the facade carries ZERO runtime dependencies. The siblings
|
|
226
229
|
// and the SDK build/publish deps are all devDeps, so `dependencies` ends
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type RunContext,
|
|
30
30
|
} from "../engine/api";
|
|
31
31
|
import { canonicalPackageJsonOrder } from "./shared/key-order";
|
|
32
|
+
import { ensureModuleVersion, INITIAL_MODULE_VERSION } from "./shared/initial-version";
|
|
32
33
|
|
|
33
34
|
const KIND_EXPORTS = {
|
|
34
35
|
".": {
|
|
@@ -44,6 +45,7 @@ export function kindPackageJsonInitial(ctx: RunContext): Record<string, unknown>
|
|
|
44
45
|
const v = ctx.blockVars;
|
|
45
46
|
return {
|
|
46
47
|
name: `${v.facadeName}.kind`,
|
|
48
|
+
version: INITIAL_MODULE_VERSION,
|
|
47
49
|
private: true,
|
|
48
50
|
type: "module",
|
|
49
51
|
// `main` is the CommonJS entry (require fallback), `module` the ESM one —
|
|
@@ -81,7 +83,9 @@ export function kindPackageJsonInitial(ctx: RunContext): Record<string, unknown>
|
|
|
81
83
|
export function kindPackageJsonRules(): void {
|
|
82
84
|
// Controlled sibling — workspace-only, never published to npm. `private: true`
|
|
83
85
|
// makes npm refuse to publish it; the `version` is kept (changesets-owned) and
|
|
84
|
-
// is what the kind's on-wire `{name}@{version}` reference is baked from
|
|
86
|
+
// is what the kind's on-wire `{name}@{version}` reference is baked from — only
|
|
87
|
+
// backfilled when the manifest predates the seeded field.
|
|
88
|
+
ensureModuleVersion();
|
|
85
89
|
ensureField("private", true);
|
|
86
90
|
|
|
87
91
|
ensureField("type", "module");
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "../engine/api";
|
|
29
29
|
import { scopeDepMaps } from "../engine/ctx";
|
|
30
30
|
import { canonicalPackageJsonOrder } from "./shared/key-order";
|
|
31
|
+
import { ensureModuleVersion, INITIAL_MODULE_VERSION } from "./shared/initial-version";
|
|
31
32
|
import { COLOCATED_TEST_GLOB } from "./shared/colocated-tests";
|
|
32
33
|
import { removeRetiredToolchainDeps } from "./shared/retired-deps";
|
|
33
34
|
|
|
@@ -35,6 +36,7 @@ export function modelPackageJsonInitial(ctx: RunContext): Record<string, unknown
|
|
|
35
36
|
const v = ctx.blockVars;
|
|
36
37
|
return {
|
|
37
38
|
name: `${v.facadeName}.model`,
|
|
39
|
+
version: INITIAL_MODULE_VERSION,
|
|
38
40
|
private: true,
|
|
39
41
|
type: "module",
|
|
40
42
|
// The block-model build emits both index.cjs and index.js. `main` is
|
|
@@ -77,7 +79,9 @@ export function modelPackageJsonInitial(ctx: RunContext): Record<string, unknown
|
|
|
77
79
|
export function modelPackageJsonRules(): void {
|
|
78
80
|
// Controlled sibling — workspace-only, never published. `private: true` makes
|
|
79
81
|
// npm refuse to publish it; the `version` is kept (changesets-owned) so the
|
|
80
|
-
// packed template's lib carries a version
|
|
82
|
+
// packed template's lib carries a version, and backfilled when the manifest
|
|
83
|
+
// predates the seeded field.
|
|
84
|
+
ensureModuleVersion();
|
|
81
85
|
ensureField("private", true);
|
|
82
86
|
|
|
83
87
|
ensureField("type", "module");
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Root `.changeset/config.json`. CI runs `pnpm run version-packages`
|
|
2
|
+
// (`changeset version`) on every PR and sets `require-package-path-bump`, so a
|
|
3
|
+
// block without this file fails its first CI run with "There is no .changeset
|
|
4
|
+
// folder".
|
|
5
|
+
//
|
|
6
|
+
// Managed, not fixed: `$schema` is whatever `changeset init` last wrote (seven
|
|
7
|
+
// versions are in use across the blocks) and `fixed`/`linked`/`ignore` are
|
|
8
|
+
// author-owned lists. Only the fields CI depends on are re-asserted.
|
|
9
|
+
|
|
10
|
+
import { ensureField, enforceFieldOrder } from "../engine/api";
|
|
11
|
+
|
|
12
|
+
/** Seeded so the scaffold's own PR passes the `require-package-path-bump` gate
|
|
13
|
+
* without bumping: `changeset version` consumes it and leaves the facade at
|
|
14
|
+
* `INITIAL_MODULE_VERSION`, so the first version a user sees is 1.0.0 rather
|
|
15
|
+
* than 1.0.1. Seeded (not managed) — a refresh must never resurrect it once
|
|
16
|
+
* the first release has consumed it. */
|
|
17
|
+
export const EMPTY_CHANGESET_SEED = `---
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
Initial release.
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const CHANGELOG = "@changesets/cli/changelog";
|
|
24
|
+
const ACCESS = "restricted";
|
|
25
|
+
const BASE_BRANCH = "main";
|
|
26
|
+
const UPDATE_INTERNAL_DEPENDENCIES = "patch";
|
|
27
|
+
|
|
28
|
+
const FIELD_ORDER = [
|
|
29
|
+
"$schema",
|
|
30
|
+
"changelog",
|
|
31
|
+
"commit",
|
|
32
|
+
"fixed",
|
|
33
|
+
"linked",
|
|
34
|
+
"access",
|
|
35
|
+
"baseBranch",
|
|
36
|
+
"updateInternalDependencies",
|
|
37
|
+
"ignore",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export function rootChangesetConfigInitial(): Record<string, unknown> {
|
|
41
|
+
return {
|
|
42
|
+
$schema: "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
|
43
|
+
changelog: CHANGELOG,
|
|
44
|
+
commit: false,
|
|
45
|
+
fixed: [],
|
|
46
|
+
linked: [],
|
|
47
|
+
access: ACCESS,
|
|
48
|
+
baseBranch: BASE_BRANCH,
|
|
49
|
+
updateInternalDependencies: UPDATE_INTERNAL_DEPENDENCIES,
|
|
50
|
+
ignore: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function rootChangesetConfigRules(): void {
|
|
55
|
+
ensureField("changelog", CHANGELOG);
|
|
56
|
+
ensureField("commit", false);
|
|
57
|
+
ensureField("access", ACCESS);
|
|
58
|
+
ensureField("baseBranch", BASE_BRANCH);
|
|
59
|
+
ensureField("updateInternalDependencies", UPDATE_INTERNAL_DEPENDENCIES);
|
|
60
|
+
enforceFieldOrder(FIELD_ORDER);
|
|
61
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
enforceFieldOrder,
|
|
16
16
|
} from "../engine/api";
|
|
17
17
|
import { canonicalPackageJsonOrder } from "./shared/key-order";
|
|
18
|
+
import { INITIAL_MODULE_VERSION } from "./shared/initial-version";
|
|
18
19
|
|
|
19
20
|
// `variant=all` builds every variant the software declares — docker-vs-binary is not a per-script
|
|
20
21
|
// choice. `no-software` builds placeholder software for validators.
|
|
@@ -42,7 +43,7 @@ export function rootPackageJsonInitial(): Record<string, unknown> {
|
|
|
42
43
|
// No `name`: the root is never published. Body rules re-assert
|
|
43
44
|
// (removeField("name")) as a drift-corrector.
|
|
44
45
|
return {
|
|
45
|
-
version:
|
|
46
|
+
version: INITIAL_MODULE_VERSION,
|
|
46
47
|
private: true,
|
|
47
48
|
scripts: {
|
|
48
49
|
fmt: "turbo run fmt",
|