@webda/grpc 4.0.0-beta.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.
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Generate a .proto file from registered operations and their schemas.
3
+ *
4
+ * Groups operations by prefix (e.g., "Post.Create" → service PostService)
5
+ * and converts JSON Schema input/output types to protobuf messages.
6
+ * @param operations - map of operation IDs to their definitions
7
+ * @param schemas - all known JSON schemas keyed by schema reference name
8
+ * @param packageName - protobuf package name to use in the generated file
9
+ * @returns the complete .proto file content as a string
10
+ */
11
+ export function generateProto(operations, schemas, packageName = "webda") {
12
+ const lines = [];
13
+ const messages = new Map(); // messageName → proto definition
14
+ const fieldCounter = 0;
15
+ lines.push('syntax = "proto3";');
16
+ lines.push("");
17
+ lines.push(`package ${packageName};`);
18
+ lines.push("");
19
+ lines.push('import "google/protobuf/empty.proto";');
20
+ lines.push('import "google/protobuf/struct.proto";');
21
+ lines.push("");
22
+ // Group operations by service prefix
23
+ const serviceGroups = new Map();
24
+ for (const [opId, op] of Object.entries(operations)) {
25
+ if (op.hidden)
26
+ continue;
27
+ const dotIdx = opId.indexOf(".");
28
+ const prefix = dotIdx > 0 ? opId.substring(0, dotIdx) : "Default";
29
+ // proto3 RPC method names must match `[A-Za-z_][A-Za-z0-9_]*` — dots are
30
+ // namespace separators and rejected by the parser. Behavior operations
31
+ // are namespaced as `<Model>.<Attribute>.<Action>` (e.g.
32
+ // `Post.MainImage.Delete`), so the trailing portion still carries a
33
+ // dot. Flatten to underscore so the parser accepts the rpc name.
34
+ const rpcName = (dotIdx > 0 ? opId.substring(dotIdx + 1) : opId).replace(/\./g, "_");
35
+ if (!serviceGroups.has(prefix)) {
36
+ serviceGroups.set(prefix, []);
37
+ }
38
+ serviceGroups.get(prefix).push({ opId, op, rpcName });
39
+ }
40
+ // Webda operation inputs ending in `?` are the "ignoreRequired" variant of
41
+ // the base schema; strip the suffix when looking the schema up since only
42
+ // the base key exists in the registry.
43
+ const resolveSchemaRef = (ref) => {
44
+ if (!ref || ref === "void")
45
+ return undefined;
46
+ if (schemas[ref])
47
+ return ref;
48
+ if (ref.endsWith("?") && schemas[ref.slice(0, -1)])
49
+ return ref.slice(0, -1);
50
+ return undefined;
51
+ };
52
+ // Hoist every operation schema's local `definitions` block into the flat
53
+ // schema registry so $refs like `#/definitions/BinaryMap<T>` (which the
54
+ // schema generator emits next to its hoisted definitions block) resolve
55
+ // to a real shape instead of falling back to `google.protobuf.Struct`.
56
+ // The compiler's `normalizeSchemaDefinitions` already lifted these to the
57
+ // schema root; we just need to fold them into the flat registry the proto
58
+ // walker consults.
59
+ const enrichedSchemas = { ...schemas };
60
+ for (const op of Object.values(operations)) {
61
+ if (op.hidden)
62
+ continue;
63
+ for (const ref of [op.input, op.output]) {
64
+ if (!ref || ref === "void")
65
+ continue;
66
+ const baseRef = ref.endsWith("?") ? ref.slice(0, -1) : ref;
67
+ const target = enrichedSchemas[baseRef];
68
+ const localDefs = target?.definitions;
69
+ if (!localDefs)
70
+ continue;
71
+ for (const [k, v] of Object.entries(localDefs)) {
72
+ if (!(k in enrichedSchemas)) {
73
+ enrichedSchemas[k] = v;
74
+ }
75
+ }
76
+ }
77
+ }
78
+ // Generate messages for all referenced schemas
79
+ for (const [opId, op] of Object.entries(operations)) {
80
+ if (op.hidden)
81
+ continue;
82
+ const inputRef = resolveSchemaRef(op.input);
83
+ if (inputRef) {
84
+ const msgName = schemaToMessageName(inputRef);
85
+ if (!messages.has(msgName)) {
86
+ messages.set(msgName, jsonSchemaToMessage(msgName, enrichedSchemas[inputRef], enrichedSchemas, messages));
87
+ }
88
+ }
89
+ const outputRef = resolveSchemaRef(op.output);
90
+ if (outputRef) {
91
+ const msgName = schemaToMessageName(outputRef);
92
+ if (!messages.has(msgName)) {
93
+ messages.set(msgName, jsonSchemaToMessage(msgName, enrichedSchemas[outputRef], enrichedSchemas, messages));
94
+ }
95
+ }
96
+ }
97
+ // Write all messages
98
+ for (const [, msgDef] of messages) {
99
+ lines.push(msgDef);
100
+ lines.push("");
101
+ }
102
+ // Write services
103
+ for (const [prefix, ops] of serviceGroups) {
104
+ lines.push(`service ${prefix}Service {`);
105
+ for (const { opId, op, rpcName } of ops) {
106
+ const inputType = getInputType(op, schemas, messages);
107
+ const outputType = getOutputType(op, schemas, messages);
108
+ const streaming = op.grpc?.streaming || "none";
109
+ const inputMod = streaming === "client" || streaming === "bidi" ? "stream " : "";
110
+ const outputMod = streaming === "server" || streaming === "bidi" ? "stream " : "";
111
+ if (op.summary) {
112
+ lines.push(` // ${op.summary}`);
113
+ }
114
+ lines.push(` rpc ${rpcName}(${inputMod}${inputType}) returns (${outputMod}${outputType});`);
115
+ }
116
+ lines.push("}");
117
+ lines.push("");
118
+ }
119
+ return lines.join("\n");
120
+ }
121
+ /**
122
+ * Convert a schema reference name to a protobuf message name.
123
+ * "WebdaSample/Post" → "Post", "uuidRequest" → "UuidRequest"
124
+ * @param schemaRef - the schema reference string, e.g. "MyApp/Post" or "uuidRequest"
125
+ * @returns the PascalCase protobuf message name derived from the reference
126
+ */
127
+ function schemaToMessageName(schemaRef) {
128
+ // JSON-Pointer escapes (`%3C%7B%7D%3E`) survive into `$ref` strings when the
129
+ // schema generator names a definition like `BinaryFileInfo<{}>`. Decode
130
+ // them first so we can strip the generic-bracket noise rather than emitting
131
+ // `BinaryFileInfo%3C%7B%7D%3E` as a proto identifier (which the parser
132
+ // rejects as an illegal token).
133
+ let decoded;
134
+ try {
135
+ decoded = decodeURIComponent(schemaRef);
136
+ }
137
+ catch {
138
+ decoded = schemaRef;
139
+ }
140
+ const tail = (decoded.includes("/") ? decoded.split("/").pop() : decoded) ?? decoded;
141
+ // Drop generic args (`<...>`) entirely — proto3 has no generic concept and
142
+ // the unresolved `T` made the generated schema collapse them to `<{}>`.
143
+ const stripped = tail.replace(/<[^>]*>/g, "");
144
+ // Sanitize anything left that proto3 doesn't accept in an identifier.
145
+ // proto3 idents match `[A-Za-z_][A-Za-z0-9_]*`; splitting on every other
146
+ // char and PascalCasing keeps the original word boundaries readable.
147
+ const cleaned = stripped
148
+ .split(/[^A-Za-z0-9]+/)
149
+ .filter(Boolean)
150
+ .map(s => s.charAt(0).toUpperCase() + s.slice(1))
151
+ .join("") || "Empty";
152
+ // proto3 identifiers must start with a letter or underscore; underscore
153
+ // prefix the cleaned name when it begins with a digit (e.g. `1Metadata`
154
+ // from a `&1$metadata` schema-generator artefact for unbound generics).
155
+ return /^[A-Za-z_]/.test(cleaned) ? cleaned : `_${cleaned}`;
156
+ }
157
+ /**
158
+ * Get the protobuf input type for an operation.
159
+ * Merges parameters + input into a single request message if both exist.
160
+ * @param op - the operation definition containing optional input and parameters schema references
161
+ * @param schemas - all known JSON schemas keyed by schema reference name
162
+ * @param messages - accumulator map of already-generated protobuf message definitions
163
+ * @returns the protobuf type name to use as the RPC input type
164
+ */
165
+ function getInputType(op, schemas, messages) {
166
+ const ref = op.input && op.input !== "void"
167
+ ? (schemas[op.input]
168
+ ? op.input
169
+ : op.input.endsWith("?") && schemas[op.input.slice(0, -1)]
170
+ ? op.input.slice(0, -1)
171
+ : undefined)
172
+ : undefined;
173
+ if (ref) {
174
+ return schemaToMessageName(ref);
175
+ }
176
+ return "google.protobuf.Empty";
177
+ }
178
+ /**
179
+ * Get the protobuf output type for an operation.
180
+ * @param op - the operation definition containing an optional output schema reference
181
+ * @param schemas - all known JSON schemas keyed by schema reference name
182
+ * @param messages - accumulator map of already-generated protobuf message definitions
183
+ * @returns the protobuf type name to use as the RPC output type
184
+ */
185
+ function getOutputType(op, schemas, messages) {
186
+ if (op.output && op.output !== "void" && schemas[op.output]) {
187
+ return schemaToMessageName(op.output);
188
+ }
189
+ return "google.protobuf.Empty";
190
+ }
191
+ /**
192
+ * Convert a JSON Schema object to a protobuf message definition.
193
+ * @param name - the protobuf message name to assign to this definition
194
+ * @param schema - the JSON Schema object describing the message fields
195
+ * @param allSchemas - all known JSON schemas, used to resolve nested references
196
+ * @param messages - accumulator map updated with any nested message definitions generated
197
+ * @returns the protobuf message block as a string
198
+ */
199
+ function jsonSchemaToMessage(name, schema, allSchemas, messages) {
200
+ const lines = [];
201
+ lines.push(`message ${name} {`);
202
+ if (schema.properties) {
203
+ // JSON-Schema `required` list drives proto3 `optional` marker so that
204
+ // fields the caller didn't set aren't sent as empty-string defaults and
205
+ // trip pattern/format/minLength validations server-side.
206
+ const required = new Set(schema.required || []);
207
+ let fieldNum = 1;
208
+ for (const [propName, propSchema] of Object.entries(schema.properties)) {
209
+ const prop = propSchema;
210
+ const protoType = jsonTypeToProto(propName, prop, name, allSchemas, messages);
211
+ const fieldType = prop.type === "array" ? jsonTypeToProto(propName, prop.items || {}, name, allSchemas, messages) : protoType;
212
+ if (prop.type === "array") {
213
+ lines.push(` repeated ${fieldType} ${propName} = ${fieldNum};`);
214
+ }
215
+ else if (required.has(propName)) {
216
+ lines.push(` ${protoType} ${propName} = ${fieldNum};`);
217
+ }
218
+ else {
219
+ lines.push(` optional ${protoType} ${propName} = ${fieldNum};`);
220
+ }
221
+ fieldNum++;
222
+ }
223
+ }
224
+ lines.push("}");
225
+ return lines.join("\n");
226
+ }
227
+ /**
228
+ * Map a JSON Schema type to a protobuf type.
229
+ * @param fieldName - the property name in the parent schema, used to derive nested message names
230
+ * @param schema - the JSON Schema describing this field's type
231
+ * @param parentName - the name of the enclosing protobuf message, used for nested message naming
232
+ * @param allSchemas - all known JSON schemas, used to resolve nested references
233
+ * @param messages - accumulator map updated with any nested message definitions generated
234
+ * @returns the protobuf scalar or message type name for this field
235
+ */
236
+ function jsonTypeToProto(fieldName, schema, parentName, allSchemas, messages) {
237
+ if (schema.$ref) {
238
+ // Distinguish two ref shapes:
239
+ // - `#/components/schemas/<name>` points at a top-level schema in the
240
+ // OpenAPI components registry. The original behaviour was to emit
241
+ // the bare message name and trust that some other schema source
242
+ // defines it; preserve that to keep `componentsSchemaRef` passing.
243
+ // - `#/definitions/<name>` is the typescript-json-schema shape: refs
244
+ // are local to the same schema's `definitions` block. The compiler
245
+ // already hoists those into `allSchemas` for us. If the lookup
246
+ // misses, we have a genuinely-unresolvable inline ref (e.g. a
247
+ // dropped placeholder from a generic-class method) — fall back to
248
+ // `google.protobuf.Struct` so proto-loader doesn't reject the
249
+ // output.
250
+ const isComponentsRef = schema.$ref.startsWith("#/components/schemas/");
251
+ const rawRefName = schema.$ref.replace("#/definitions/", "").replace("#/components/schemas/", "");
252
+ let decodedRefName;
253
+ try {
254
+ decodedRefName = decodeURIComponent(rawRefName);
255
+ }
256
+ catch {
257
+ decodedRefName = rawRefName;
258
+ }
259
+ const msgName = schemaToMessageName(rawRefName);
260
+ const targetSchema = allSchemas[rawRefName] || allSchemas[decodedRefName];
261
+ if (!messages.has(msgName) && targetSchema) {
262
+ messages.set(msgName, ""); // placeholder to break recursion cycles
263
+ messages.set(msgName, jsonSchemaToMessage(msgName, targetSchema, allSchemas, messages));
264
+ }
265
+ if (!isComponentsRef && !targetSchema && !messages.has(msgName)) {
266
+ return "google.protobuf.Struct";
267
+ }
268
+ return msgName;
269
+ }
270
+ switch (schema.type) {
271
+ case "string":
272
+ if (schema.format === "date-time" || schema.format === "date")
273
+ return "string"; // Timestamps as ISO strings
274
+ if (schema.format === "int64")
275
+ return "int64";
276
+ return "string";
277
+ case "number":
278
+ if (schema.format === "float")
279
+ return "float";
280
+ if (schema.format === "int32" || schema.format === "integer")
281
+ return "int32";
282
+ if (schema.format === "int64")
283
+ return "int64";
284
+ return "double";
285
+ case "integer":
286
+ if (schema.format === "int64")
287
+ return "int64";
288
+ return "int32";
289
+ case "boolean":
290
+ return "bool";
291
+ case "object":
292
+ if (schema.properties) {
293
+ // Nested message
294
+ const nestedName = `${parentName}${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}`;
295
+ if (!messages.has(nestedName)) {
296
+ messages.set(nestedName, jsonSchemaToMessage(nestedName, schema, allSchemas, messages));
297
+ }
298
+ return nestedName;
299
+ }
300
+ return "google.protobuf.Struct";
301
+ case "array":
302
+ // Handled by caller
303
+ return jsonTypeToProto(fieldName, schema.items || {}, parentName, allSchemas, messages);
304
+ default:
305
+ return "google.protobuf.Struct";
306
+ }
307
+ // Enum support
308
+ if (schema.enum) {
309
+ return "string"; // Map enums to strings for simplicity
310
+ }
311
+ }
312
+ //# sourceMappingURL=proto-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proto-generator.js","sourceRoot":"","sources":["../src/proto-generator.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@webda/grpc",
3
+ "version": "4.0.0-beta.3",
4
+ "description": "gRPC transport for Webda.io — exposes operations as gRPC services",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "dependencies": {
9
+ "@grpc/proto-loader": "^0.7.0",
10
+ "@webda/workout": "4.0.0-beta.3"
11
+ },
12
+ "peerDependencies": {
13
+ "@webda/core": "^4.0.0-beta.1"
14
+ },
15
+ "devDependencies": {
16
+ "@vitest/coverage-v8": "^4.1.2",
17
+ "vite": "^6.0.0",
18
+ "vitest": "^4.0.0",
19
+ "@webda/compiler": "^4.0.0-beta.3",
20
+ "@webda/core": "^4.0.0-beta.3",
21
+ "@webda/test": "^4.0.0-beta.3",
22
+ "@webda/utils": "^4.0.0-beta.3"
23
+ },
24
+ "webda": {
25
+ "namespace": "Webda",
26
+ "capabilities": {
27
+ "grpc": "Webda/GrpcService"
28
+ }
29
+ },
30
+ "license": "LGPL-3.0-only",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/loopingz/webda.io.git",
34
+ "directory": "packages/grpc"
35
+ },
36
+ "scripts": {
37
+ "build": "webdac build",
38
+ "test": "vitest run",
39
+ "lint": "eslint src/",
40
+ "lint:fix": "eslint --fix src/"
41
+ }
42
+ }
@@ -0,0 +1,187 @@
1
+ import { suite, test } from "@webda/test";
2
+ import * as assert from "assert";
3
+ import { execSync } from "node:child_process";
4
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { shouldDispatchBuildHooks } from "@webda/compiler/lib/shell-build-dispatch.js";
9
+ import { listConfiguredServiceTypes } from "@webda/utils";
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+
14
+ // Resolve key fixtures from the repo layout. These paths are stable:
15
+ // - packages/grpc/webda.module.json is produced by `webdac build` against this package
16
+ // - sample-apps/blog-system is a tracked fixture that configures Webda/GrpcService
17
+ const GRPC_PKG_ROOT = resolve(__dirname, "..");
18
+ const GRPC_MODULE_JSON = join(GRPC_PKG_ROOT, "webda.module.json");
19
+ const BLOG_SYSTEM_DIR = resolve(GRPC_PKG_ROOT, "..", "..", "sample-apps", "blog-system");
20
+
21
+ /**
22
+ * End-to-end-ish checks for the build-hook dispatch flow:
23
+ *
24
+ * webdac build
25
+ * → reads webda.module.json
26
+ * → reads webda.config.json
27
+ * → shouldDispatchBuildHooks(module, configuredTypes) === true
28
+ * → spawns `webda build`
29
+ * → GrpcService.build() writes .webda/app.proto
30
+ *
31
+ * The decision-helpers (listConfiguredServiceTypes, shouldDispatchBuildHooks) each
32
+ * have focused unit tests in their home packages. The tests below verify their
33
+ * *composition* against the real grpc webda.module.json — so a refactor that
34
+ * breaks either piece for this specific user journey fails here.
35
+ */
36
+ @suite
37
+ class GrpcBuildDispatchIntegrationTest {
38
+ private tmpDir!: string;
39
+
40
+ beforeEach() {
41
+ this.tmpDir = join(tmpdir(), `grpc-build-int-${Date.now()}-${Math.random().toString(36).slice(2)}`);
42
+ mkdirSync(this.tmpDir, { recursive: true });
43
+ }
44
+
45
+ afterEach() {
46
+ try {
47
+ rmSync(this.tmpDir, { recursive: true, force: true });
48
+ } catch {
49
+ // Best-effort cleanup.
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Write a minimal webda.config.json and return its absolute path.
55
+ */
56
+ private writeConfig(services: Record<string, { type: string }>): string {
57
+ const configPath = join(this.tmpDir, "webda.config.json");
58
+ writeFileSync(configPath, JSON.stringify({ services }, null, 2));
59
+ return configPath;
60
+ }
61
+
62
+ @test
63
+ grpcModuleJsonDeclaresBuildCommandOnGrpcService() {
64
+ // Sanity check: Task 12 adopted @BuildCommand on GrpcService. If someone
65
+ // removes the decorator the whole dispatch chain silently breaks — catch
66
+ // that regression explicitly.
67
+ assert.ok(existsSync(GRPC_MODULE_JSON), `grpc webda.module.json missing at ${GRPC_MODULE_JSON}`);
68
+ const mod = JSON.parse(readFileSync(GRPC_MODULE_JSON, "utf-8"));
69
+ const entry = mod?.moddas?.["Webda/GrpcService"];
70
+ assert.ok(entry, "Webda/GrpcService missing from moddas");
71
+ assert.ok(entry?.commands?.build, "Webda/GrpcService is missing a build command — did the @BuildCommand decorator get removed?");
72
+ }
73
+
74
+ @test
75
+ dispatchesWhenConfigFullyQualifiesGrpcService() {
76
+ const configPath = this.writeConfig({
77
+ GRPCService: { type: "Webda/GrpcService" }
78
+ });
79
+ const mod = JSON.parse(readFileSync(GRPC_MODULE_JSON, "utf-8"));
80
+
81
+ const configured = listConfiguredServiceTypes(configPath, "Webda");
82
+ assert.ok(configured.includes("Webda/GrpcService"), `Expected Webda/GrpcService in configured types, got: ${configured.join(", ")}`);
83
+
84
+ assert.strictEqual(
85
+ shouldDispatchBuildHooks(mod, configured),
86
+ true,
87
+ "webdac build should dispatch when GrpcService is configured"
88
+ );
89
+ }
90
+
91
+ @test
92
+ dispatchesWhenConfigUsesUnqualifiedGrpcServiceAndMatchingNamespace() {
93
+ // Users often drop the namespace when the package namespace matches.
94
+ // listConfiguredServiceTypes prefixes unqualified names with the project
95
+ // namespace — so "GrpcService" in a "Webda"-namespace app should still
96
+ // trigger dispatch.
97
+ const configPath = this.writeConfig({
98
+ GRPCService: { type: "GrpcService" }
99
+ });
100
+ const mod = JSON.parse(readFileSync(GRPC_MODULE_JSON, "utf-8"));
101
+
102
+ const configured = listConfiguredServiceTypes(configPath, "Webda");
103
+ assert.deepStrictEqual(configured, ["Webda/GrpcService"]);
104
+ assert.strictEqual(shouldDispatchBuildHooks(mod, configured), true);
105
+ }
106
+
107
+ @test
108
+ doesNotDispatchWhenGrpcServiceIsNotConfigured() {
109
+ const configPath = this.writeConfig({
110
+ Store: { type: "Webda/MemoryStore" }
111
+ });
112
+ const mod = JSON.parse(readFileSync(GRPC_MODULE_JSON, "utf-8"));
113
+
114
+ const configured = listConfiguredServiceTypes(configPath, "Webda");
115
+ assert.ok(!configured.includes("Webda/GrpcService"));
116
+ assert.strictEqual(
117
+ shouldDispatchBuildHooks(mod, configured),
118
+ false,
119
+ "Dispatch should be skipped when no configured service owns a build hook"
120
+ );
121
+ }
122
+
123
+ @test
124
+ doesNotDispatchOnEmptyConfig() {
125
+ const configPath = this.writeConfig({});
126
+ const mod = JSON.parse(readFileSync(GRPC_MODULE_JSON, "utf-8"));
127
+
128
+ const configured = listConfiguredServiceTypes(configPath, "Webda");
129
+ assert.deepStrictEqual(configured, []);
130
+ assert.strictEqual(shouldDispatchBuildHooks(mod, configured), false);
131
+ }
132
+
133
+ /**
134
+ * Full end-to-end: run the `webda build` binary against the blog-system
135
+ * fixture (which configures Webda/GrpcService) and assert that the proto
136
+ * file lands at the configured path.
137
+ *
138
+ * This is the only test here that spawns a subprocess. It relies on:
139
+ * - pnpm having linked the `webda` bin into blog-system's node_modules
140
+ * - blog-system's checked-in webda.module.json being up-to-date enough
141
+ * for `webda build` to enter the resolved phase
142
+ *
143
+ * If either precondition fails we skip rather than assert-fail, so that
144
+ * a partial workspace install can still run the rest of the grpc tests.
145
+ */
146
+ @test
147
+ webdaBuildOnBlogSystemFixtureProducesProtoFile() {
148
+ if (!existsSync(BLOG_SYSTEM_DIR)) {
149
+ // Fixture not checked out — nothing to exercise.
150
+ return;
151
+ }
152
+ const webdaBin = join(BLOG_SYSTEM_DIR, "node_modules", ".bin", "webda");
153
+ const blogModuleJson = join(BLOG_SYSTEM_DIR, "webda.module.json");
154
+ if (!existsSync(webdaBin) || !existsSync(blogModuleJson)) {
155
+ // Workspace not installed — cannot spawn `webda`. Skip cleanly.
156
+ return;
157
+ }
158
+
159
+ const protoPath = join(BLOG_SYSTEM_DIR, ".webda", "app.proto");
160
+ // Remove any prior artifact so we know this run produced the file.
161
+ if (existsSync(protoPath)) {
162
+ rmSync(protoPath);
163
+ }
164
+
165
+ try {
166
+ execSync(`"${webdaBin}" build`, {
167
+ cwd: BLOG_SYSTEM_DIR,
168
+ stdio: "pipe",
169
+ timeout: 60_000
170
+ });
171
+ } catch (err: any) {
172
+ // `webda build` is supposed to succeed on the fixture. Surface the
173
+ // stderr to make diagnosis easier when it doesn't.
174
+ const stderr = err?.stderr?.toString?.() ?? "";
175
+ const stdout = err?.stdout?.toString?.() ?? "";
176
+ throw new Error(`webda build failed on blog-system fixture:\n stdout: ${stdout}\n stderr: ${stderr}`);
177
+ }
178
+
179
+ assert.ok(existsSync(protoPath), `Expected ${protoPath} to be produced by 'webda build'`);
180
+ const proto = readFileSync(protoPath, "utf-8");
181
+ assert.ok(proto.includes('syntax = "proto3"'), "Output should be a proto3 file");
182
+ assert.ok(proto.includes("package webda;"), "Should use the default webda package name");
183
+ // Sanity: blog-system exposes operations over several prefixes (Post, User, Tag, …).
184
+ // At least one service block should appear.
185
+ assert.ok(/service \w+Service \{/.test(proto), "Should declare at least one proto service");
186
+ }
187
+ }