@xemahq/repo-build-tooling 0.27.0 → 0.27.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/repo-build-tooling",
3
- "version": "0.27.0",
3
+ "version": "0.27.1",
4
4
  "description": "Dev-time build tooling shared by every Xema repository. Ships as plain ESM, depending only on @xemahq/release-policy for registry access, so the published artifact is the reviewed source.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
@@ -50,6 +50,6 @@
50
50
  "test": "node src/scrub-swagger-plugin-paths.mjs --self-test && node --test src/check-workspace-range-matches-local.test.mjs && node --test src/check-no-vendored-deps.test.mjs && node --test src/readiness.test.mjs && node --test src/composed-image.test.mjs && node --test src/composed-image-coordinate-cli.test.mjs && node --test src/declaration-refusal.test.mjs && node --test src/smoke-refusal.test.mjs && node --test src/load-image.test.mjs && node --test src/composed-image-coordinate.test.mjs && node --test src/composed-image-dockerfile.test.mjs && node --test src/service-bootstrap.test.mjs && node --test src/service-bootstrap-protocols.test.mjs && node --test src/composed-image-prebuilt-dockerfile.test.mjs && node --test src/composed-image-in-image-closure.test.mjs && node --test src/composed-image-stage.test.mjs && node --test src/composed-image-stage-tree.test.mjs && node --test src/composed-image-install.test.mjs && node --test src/composed-image-proof.test.mjs && node --test src/composed-image-pnpm-context.test.mjs && node --test src/composed-image-sync-injected.test.mjs && node --test src/composed-image-prisma-check.test.mjs && node --test src/composed-image-prisma-engine.test.mjs && node --test src/composed-migrate.test.mjs && node --test src/promote-image.test.mjs && node --test src/arm64-supplement-needed.test.mjs && node --test src/service-image.test.mjs && node --test src/image-cpu-arch.test.mjs"
51
51
  },
52
52
  "xemaRelease": {
53
- "sourceDigest": "sha256:4a2cf6b565691b2837d7562f58925a40e3ad2284eded9395b72a74312788d417"
53
+ "sourceDigest": "sha256:65b9132685b9423a89b13ec6d03b21f3c7c66c529c98fbf09ee6a7bd4cde39d9"
54
54
  }
55
55
  }
@@ -1,18 +1,19 @@
1
1
  // `xema-composed-image prisma-check`, INSIDE the image build, on the TARGET arch.
2
2
  // Every member that owns a Prisma schema carries its generated client(s) in
3
3
  // `<member>/node_modules/.prisma/<client>/`, generated on the CI runner and staged
4
- // without running anything. Each client's schema must be the member's own schema as
4
+ // without running anything; non-client generator outputs there (the org-erasure graph)
5
+ // must load and export their names. Each client's schema must be the member's own schema as
5
6
  // `prisma format` writes it, or the client is stale. The image carries no native engine: the only supported
6
7
  // client is Rust-free (a wasm query compiler plus a driver adapter). This refuses any
7
8
  // native engine file, compiles the wasm, checks the client matches the member's
8
9
  // `prisma` CLI, and constructs it. It never connects.
9
10
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
10
11
  import { createRequire } from 'node:module';
11
- import { join } from 'node:path';
12
+ import { basename, join } from 'node:path';
12
13
 
13
14
  import { COMPOSED_IMAGE_DIR } from './composed-image-paths.mjs';
14
15
  import { readMembers } from './composed-image-members.mjs';
15
- import { clientSchemaProblems, memberPrismaSchema } from './composed-migrate.mjs';
16
+ import { clientSchemaProblems, memberPrismaOutputs, memberPrismaSchema } from './composed-migrate.mjs';
16
17
 
17
18
  const NATIVE_ENGINE = [/^libquery_engine-.*\.node$/, /^query-engine-/, /\.so\.node$/, /\.dll\.node$/, /\.dylib\.node$/];
18
19
  const WASM_COMPILER = /^query_compiler.*_bg\.wasm$/;
@@ -35,16 +36,14 @@ function cliVersion(memberRequire, memberDirectory) {
35
36
  async function schemaVerdicts({ root, imageDir, formatSpawn, concurrency }) {
36
37
  const jobs = [];
37
38
  for (const [, directory] of readMembers({ root, imageDir })) {
39
+ let schema;
38
40
  try {
39
- if (memberPrismaSchema(root, directory) === undefined) continue;
41
+ schema = memberPrismaSchema(root, directory);
40
42
  } catch {
41
43
  continue; // reported by the main pass
42
44
  }
43
- const container = join(root, directory, 'node_modules', '.prisma');
44
- if (!existsSync(container)) continue;
45
- for (const entry of readdirSync(container, { withFileTypes: true }).filter((e) => e.isDirectory())) {
46
- jobs.push({ root, memberDir: directory, clientDir: join(container, entry.name) });
47
- }
45
+ if (schema === undefined) continue;
46
+ for (const clientDir of memberPrismaOutputs(root, directory, schema).clients) jobs.push({ root, memberDir: directory, clientDir });
48
47
  }
49
48
  const verdicts = await clientSchemaProblems(jobs, { spawn: formatSpawn, concurrency });
50
49
  return new Map(jobs.map((job, i) => [job.clientDir, verdicts[i]]));
@@ -75,10 +74,22 @@ export async function prismaCheck({ root = process.cwd(), imageDir = COMPOSED_IM
75
74
  continue;
76
75
  }
77
76
  const memberRequire = createRequire(join(memberDirectory, 'package.json'));
78
- for (const name of dirs) {
77
+ const outputs = memberPrismaOutputs(root, directory, schema);
78
+ for (const problem of outputs.problems) problems.push(`${key}: ${problem}`);
79
+ // A non-client artifact (e.g. the org-erasure graph) is imported at boot: it must load and export its names.
80
+ for (const { dir, kind, exports } of outputs.artifacts) {
81
+ const label = `${key} (.prisma/${basename(dir)})`;
82
+ try {
83
+ const module = memberRequire(dir);
84
+ const absent = exports.filter((name) => module?.[name] === undefined);
85
+ if (absent.length > 0) problems.push(`${label}: the ${kind} does not export ${absent.join(', ')}`);
86
+ } catch (error) {
87
+ problems.push(`${label}: the ${kind} does not load: ${error?.message ?? String(error)}`);
88
+ }
89
+ }
90
+ for (const clientDirectory of outputs.clients) {
79
91
  clients += 1;
80
- const label = `${key} (.prisma/${name})`;
81
- const clientDirectory = join(container, name);
92
+ const label = `${key} (.prisma/${basename(clientDirectory)})`;
82
93
  const staleProblem = stale.get(clientDirectory);
83
94
  if (staleProblem !== undefined) {
84
95
  problems.push(`${label}: ${staleProblem}`);
@@ -38,3 +38,21 @@ test('a stale client (the source gained a field after generation) is refused; th
38
38
  await assert.rejects(prismaCheck({ root: stale, log() {}, formatSpawn: formatNoop }), /differs from the member's prisma\/schema\.prisma .*stale/);
39
39
  await assert.rejects(prismaCheck({ root: workspace(), log() {}, formatSpawn: formatNoop }), (e) => !/stale/.test(e.message));
40
40
  });
41
+
42
+ test('an org-erasure graph is not treated as a client: it must load and export orgErasureGraph', async () => {
43
+ const schema =
44
+ 'generator client {\n provider = "prisma-client-js"\n output = "../node_modules/.prisma/alpha-db"\n}\n\n' +
45
+ 'generator orgErasureGraph {\n provider = "xema-org-erasure-graph"\n output = "../node_modules/.prisma/alpha-db-org-erasure-graph"\n}\n\ndatasource db {}\n';
46
+ const graph = (root, js) => {
47
+ write(root, 'biomes/alpha/api/alpha-db-api/prisma/schema.prisma', schema);
48
+ write(root, 'biomes/alpha/api/alpha-db-api/node_modules/.prisma/alpha-db/schema.prisma', schema);
49
+ write(root, 'biomes/alpha/api/alpha-db-api/node_modules/.prisma/alpha-db-org-erasure-graph/index.js', js);
50
+ write(root, 'biomes/alpha/api/alpha-db-api/node_modules/.prisma/alpha-db-org-erasure-graph/index.d.ts', '');
51
+ return root;
52
+ };
53
+ // The fixture client has no wasm compiler, so the check fails, but only on the client, never on the graph.
54
+ await assert.rejects(prismaCheck({ root: graph(workspace(), 'exports.orgErasureGraph = {};\n'), log() {}, formatSpawn: formatNoop }), (e) =>
55
+ /\.prisma\/alpha-db\): no query_compiler/.test(e.message) && !/org-erasure-graph/.test(e.message));
56
+ await assert.rejects(prismaCheck({ root: graph(workspace(), 'exports.other = 1;\n'), log() {}, formatSpawn: formatNoop }),
57
+ /\.prisma\/alpha-db-org-erasure-graph\): the org-erasure graph does not export orgErasureGraph/);
58
+ });
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // <dir>/out/<package dir>/<output>/ every closure package's outputs (default: dist);
7
7
  // `--also <pkg>` adds that package's closure (no member checks)
8
- // <dir>/out/<pkg dir>/node_modules/.prisma/ the generated Prisma clients of every closure root that has
8
+ // <dir>/out/<pkg dir>/node_modules/.prisma/ the generated Prisma clients (and artifacts such as the org-erasure graph) of every closure root that has
9
9
  // `prisma/schema.prisma` (members and `--also` roots alike)
10
10
  // <dir>/tool/ this package (package.json + src, no tests):
11
11
  // bind-mounted into the build, never copied into a layer
@@ -18,7 +18,7 @@ import { fileURLToPath } from 'node:url';
18
18
 
19
19
  import { COMPOSED_IMAGE_DIR, NODE_MAJOR_LABEL } from './composed-image-paths.mjs';
20
20
  import { memberClosure, readMembers } from './composed-image-members.mjs';
21
- import { clientSchemaProblems, memberPrismaSchema } from './composed-migrate.mjs';
21
+ import { clientSchemaProblems, memberPrismaOutputs, memberPrismaSchema } from './composed-migrate.mjs';
22
22
 
23
23
  export const DEFAULT_OUTPUTS = Object.freeze(['dist']);
24
24
  const TOOL_PACKAGE = join(dirname(fileURLToPath(import.meta.url)), '..');
@@ -114,9 +114,11 @@ export async function planBuildOutputs({
114
114
  const hasClient = existsSync(clients) && readdirSync(clients, { withFileTypes: true }).some((e) => e.isDirectory());
115
115
  if (schema !== undefined && !hasClient) problems.push(`${key}: ${directory} has prisma/schema.prisma but no node_modules/.prisma/<client>/ — run prisma generate`);
116
116
  if (schema !== undefined && hasClient) {
117
- for (const entry of readdirSync(clients, { withFileTypes: true }).filter((e) => e.isDirectory())) {
118
- schemaJobs.push({ key, root, memberDir: directory, clientDir: join(clients, entry.name) });
119
- }
117
+ // Each folder by what generated it: clients are checked against the schema, other artifacts
118
+ // (e.g. the org-erasure graph the process imports at boot) must be present and complete.
119
+ const prismaOutputs = memberPrismaOutputs(root, directory, schema);
120
+ for (const problem of prismaOutputs.problems) problems.push(`${key}: ${problem}`);
121
+ for (const clientDir of prismaOutputs.clients) schemaJobs.push({ key, root, memberDir: directory, clientDir });
120
122
  }
121
123
  if (schema === undefined && hasClient) problems.push(`${key}: ${directory} ships node_modules/.prisma/<client>/ but has no prisma/schema.prisma`);
122
124
  }
@@ -173,3 +173,44 @@ test('--also refuses a non-member closure root with prisma/schema.prisma but no
173
173
  await assert.rejects(stage(root, { exec: alsoPnpm(root, EXTRA), also: ['@t/extra'] }), /packages\/sdks\/extra has prisma\/schema\.prisma but no generated client: run prisma generate/);
174
174
  assert.equal(fs.existsSync(path.join(root, '.xema-stage')), false);
175
175
  });
176
+
177
+ const GRAPH_SCHEMA =
178
+ 'generator client {\n provider = "prisma-client-js"\n output = "../node_modules/.prisma/alpha-db"\n}\n\n' +
179
+ 'generator orgErasureGraph {\n provider = "xema-org-erasure-graph"\n output = "../node_modules/.prisma/alpha-db-org-erasure-graph"\n}\n\ndatasource db {}\n';
180
+
181
+ function withErasureGraph() {
182
+ const root = ready();
183
+ write(root, 'biomes/alpha/api/alpha-db-api/prisma/schema.prisma', GRAPH_SCHEMA);
184
+ write(root, 'biomes/alpha/api/alpha-db-api/node_modules/.prisma/alpha-db/schema.prisma', GRAPH_SCHEMA);
185
+ write(root, 'biomes/alpha/api/alpha-db-api/node_modules/.prisma/alpha-db-org-erasure-graph/index.js', 'exports.orgErasureGraph = {};\n');
186
+ write(root, 'biomes/alpha/api/alpha-db-api/node_modules/.prisma/alpha-db-org-erasure-graph/index.d.ts', 'export declare const orgErasureGraph: unknown;\n');
187
+ return root;
188
+ }
189
+
190
+ test('stages a member with a Prisma client and an org-erasure graph, carrying the graph', async () => {
191
+ const root = withErasureGraph();
192
+ await stage(root);
193
+ const staged = files(path.join(root, '.xema-stage/out'));
194
+ assert.ok(staged.includes('biomes/alpha/api/alpha-db-api/node_modules/.prisma/alpha-db-org-erasure-graph/index.js'));
195
+ assert.ok(staged.includes('biomes/alpha/api/alpha-db-api/node_modules/.prisma/alpha-db/index.js'));
196
+ });
197
+
198
+ test('a client without its schema still fails beside an org-erasure graph; a missing or incomplete graph fails clearly', async () => {
199
+ const noClientSchema = withErasureGraph();
200
+ fs.rmSync(path.join(noClientSchema, 'biomes/alpha/api/alpha-db-api/node_modules/.prisma/alpha-db/schema.prisma'));
201
+ await assert.rejects(stage(noClientSchema), (e) => /\.prisma\/alpha-db: carries no schema\.prisma/.test(e.message) && !/org-erasure-graph/.test(e.message));
202
+ const missing = withErasureGraph();
203
+ fs.rmSync(path.join(missing, 'biomes/alpha/api/alpha-db-api/node_modules/.prisma/alpha-db-org-erasure-graph'), { recursive: true });
204
+ await assert.rejects(stage(missing), /generator orgErasureGraph \(provider "xema-org-erasure-graph"\): node_modules\/\.prisma\/alpha-db-org-erasure-graph is missing — run prisma generate/);
205
+ const incomplete = withErasureGraph();
206
+ fs.rmSync(path.join(incomplete, 'biomes/alpha/api/alpha-db-api/node_modules/.prisma/alpha-db-org-erasure-graph/index.d.ts'));
207
+ await assert.rejects(stage(incomplete), /the org-erasure graph node_modules\/\.prisma\/alpha-db-org-erasure-graph lacks index\.d\.ts/);
208
+ });
209
+
210
+ test('refuses a generator of an unknown provider writing into node_modules/.prisma', async () => {
211
+ const root = withErasureGraph();
212
+ const schema = `${GRAPH_SCHEMA}generator other {\n provider = "some-generator"\n output = "../node_modules/.prisma/alpha-db-other"\n}\n`;
213
+ write(root, 'biomes/alpha/api/alpha-db-api/prisma/schema.prisma', schema);
214
+ write(root, 'biomes/alpha/api/alpha-db-api/node_modules/.prisma/alpha-db/schema.prisma', schema);
215
+ await assert.rejects(stage(root), /generator other \(provider "some-generator"\) writes node_modules\/\.prisma\/alpha-db-other, a kind the composed image does not know/);
216
+ });
@@ -11,9 +11,9 @@
11
11
  // platform migration runner drops it the same way). Exits with the child's code.
12
12
  // This file is also the single home of `memberPrismaSchema`, which the build tool imports.
13
13
  import { spawn as spawnAsync, spawnSync } from 'node:child_process';
14
- import { copyFileSync, existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs';
14
+ import { copyFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync } from 'node:fs';
15
15
  import { availableParallelism, tmpdir } from 'node:os';
16
- import { dirname, join, resolve } from 'node:path';
16
+ import { basename, dirname, join, resolve } from 'node:path';
17
17
  import { pathToFileURL } from 'node:url';
18
18
 
19
19
  /**
@@ -42,6 +42,75 @@ export function memberPrismaSchema(root, memberDir) {
42
42
  return existsSync(schema) ? schema : undefined;
43
43
  }
44
44
 
45
+ /**
46
+ * Generators that write into `node_modules/.prisma/` something that is NOT a Prisma client,
47
+ * keyed by provider: what the output is, the files it must carry and the names its module
48
+ * must export. The process imports these at boot, so a missing one is a stage failure.
49
+ */
50
+ export const PRISMA_ARTIFACT_GENERATORS = Object.freeze({
51
+ 'xema-org-erasure-graph': Object.freeze({ kind: 'org-erasure graph', files: ['index.js', 'index.d.ts'], exports: ['orgErasureGraph'] }),
52
+ });
53
+ const PRISMA_CLIENT_PROVIDERS = new Set(['prisma-client-js', 'prisma-client']);
54
+
55
+ /**
56
+ * The schema's generator blocks: `[{ name, provider, output }]`. `output` is the absolute
57
+ * directory (resolved against the schema's directory, as Prisma does), or undefined when the
58
+ * block sets none or not as a string literal (e.g. `env(...)`).
59
+ */
60
+ export function schemaGenerators(schemaPath) {
61
+ const text = readFileSync(schemaPath, 'utf8').replace(/^\s*\/\/.*$/gm, '');
62
+ const generators = [];
63
+ for (const block of text.matchAll(/^\s*generator\s+(\w+)\s*\{([^}]*)\}/gm)) {
64
+ const provider = /^\s*provider\s*=\s*"([^"]*)"/m.exec(block[2])?.[1];
65
+ const output = /^\s*output\s*=\s*"([^"]*)"/m.exec(block[2])?.[1];
66
+ generators.push({ name: block[1], provider, output: output === undefined ? undefined : resolve(dirname(schemaPath), output) });
67
+ }
68
+ return generators;
69
+ }
70
+
71
+ /**
72
+ * Sort a member's `node_modules/.prisma/<dir>/` folders by the generator that wrote them, read
73
+ * from the member's schema: `{ clients, artifacts, problems }`. A folder a client generator
74
+ * declares, or one no generator declares, is a Prisma client (`clients`: absolute dirs, to be
75
+ * checked against the schema). A folder a `PRISMA_ARTIFACT_GENERATORS` provider declares is that
76
+ * artifact (`artifacts`: `{ dir, provider, kind, exports }`) and must carry its files. A declared
77
+ * output that is absent, or a generator of an unknown provider writing into `.prisma/`, is a problem.
78
+ */
79
+ export function memberPrismaOutputs(root, memberDir, schemaPath) {
80
+ const container = join(resolve(root, memberDir), 'node_modules', '.prisma');
81
+ const present = new Set(
82
+ existsSync(container) ? readdirSync(container, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => join(container, e.name)) : [],
83
+ );
84
+ const declared = new Map(); // dir -> provider
85
+ const problems = [];
86
+ for (const { name, provider, output } of schemaGenerators(schemaPath)) {
87
+ if (output === undefined || dirname(output) !== container) continue;
88
+ const label = `${memberDir}: generator ${name} (provider "${provider}")`;
89
+ if (!PRISMA_CLIENT_PROVIDERS.has(provider) && PRISMA_ARTIFACT_GENERATORS[provider] === undefined) {
90
+ problems.push(`${label} writes node_modules/.prisma/${basename(output)}, a kind the composed image does not know`);
91
+ continue;
92
+ }
93
+ declared.set(output, provider);
94
+ if (!present.has(output)) problems.push(`${label}: node_modules/.prisma/${basename(output)} is missing — run prisma generate`);
95
+ }
96
+ const clients = [];
97
+ const artifacts = [];
98
+ for (const dir of [...present].sort()) {
99
+ const spec = PRISMA_ARTIFACT_GENERATORS[declared.get(dir)];
100
+ if (spec === undefined) {
101
+ clients.push(dir);
102
+ continue;
103
+ }
104
+ const missing = spec.files.filter((file) => !existsSync(join(dir, file)));
105
+ if (missing.length > 0) {
106
+ problems.push(`${memberDir}: the ${spec.kind} node_modules/.prisma/${basename(dir)} lacks ${missing.join(', ')} — run prisma generate`);
107
+ continue;
108
+ }
109
+ artifacts.push({ dir, provider: declared.get(dir), kind: spec.kind, exports: spec.exports });
110
+ }
111
+ return { clients, artifacts, problems };
112
+ }
113
+
45
114
  /** Async process run with collected output: `{ status, stdout, stderr }` or `{ error }`. */
46
115
  function runAsync(command, args, options) {
47
116
  return new Promise((done) => {