From 5e2a54388d4ba576bcd93e1829da85e462da1812 Mon Sep 17 00:00:00 2001 From: Curtis Layne Date: Wed, 2 Jul 2025 23:20:48 -0700 Subject: [PATCH 1/5] fix(cli): add support for custom proto lock file paths in grpc generate command - Add --proto-lock/-l option to specify existing proto lock file path. This is necessary for environments like Bazel where input and output files must be at different paths. Making assumptions about paths in general with Bazel often leads to issues due to Bazel's sandboxing. --- .../grpc-service/commands/generate.ts | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/cli/src/commands/grpc-service/commands/generate.ts b/cli/src/commands/grpc-service/commands/generate.ts index 5b70aceb95..e51e26e335 100644 --- a/cli/src/commands/grpc-service/commands/generate.ts +++ b/cli/src/commands/grpc-service/commands/generate.ts @@ -1,5 +1,4 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises'; -import { existsSync, lstatSync } from 'node:fs'; import { resolve } from 'pathe'; import Spinner from 'ora'; import { Command, program } from 'commander'; @@ -16,6 +15,10 @@ export default (opts: BaseCommandOptions) => { command.option('-o, --output ', 'The output directory for the protobuf schema. (default ".").', '.'); command.option('-p, --package-name ', 'The name of the proto package. (default "service.v1")', 'service.v1'); command.option('-g, --go-package ', 'Adds an `option go_package` to the proto file.'); + command.option( + '-l, --proto-lock ', + 'The path to the existing proto lock file to use as the starting point for the updated proto lock file.', + ); command.action(generateCommandAction); return command; @@ -51,7 +54,7 @@ async function generateCommandAction(name: string, options: any) { program.error(`Input file ${options.input} does not exist`); } - const result = await generateProtoAndMapping(options.output, inputFile, name, options, spinner); + const result = await generateProtoAndMapping(options.output, inputFile, name, options, spinner, options.protoLock); // Write the generated files await writeFile(resolve(options.output, 'mapping.json'), result.mapping); @@ -81,6 +84,7 @@ async function generateProtoAndMapping( name: string, options: any, spinner: any, + protoLockFile?: string, ): Promise { spinner.text = 'Generating proto schema...'; const lockFile = resolve(outdir, 'service.proto.lock.json'); @@ -89,14 +93,7 @@ async function generateProtoAndMapping( const serviceName = upperFirst(camelCase(name)) + 'Service'; spinner.text = 'Generating mapping and proto files...'; - let lockData: ProtoLock | undefined; - - if (existsSync(lockFile)) { - const existingLockData = JSON.parse(await readFile(lockFile, 'utf8')); - if (existingLockData) { - lockData = existingLockData; - } - } + const lockData = await fetchLockData(outdir, protoLockFile); const mapping = compileGraphQLToMapping(schema, serviceName); const proto = compileGraphQLToProto(schema, { @@ -112,3 +109,22 @@ async function generateProtoAndMapping( lockData: proto.lockData, }; } + +async function fetchLockData(outdir: string, existingLockFile?: string): Promise { + if (existingLockFile != null && existsSync(existingLockFile)) { + const existingLockData = JSON.parse(await readFile(existingLockFile, 'utf8')); + if (existingLockData) { + return existingLockData; + } + + return undefined + } + + const lockFile = resolve(outdir, 'service.proto.lock.json'); + const existingLockData = JSON.parse(await readFile(lockFile, 'utf8')); + if (existingLockData) { + return existingLockData; + } + + return undefined; +} From 78368554f3171c800a870d04e538814104f0f1ad Mon Sep 17 00:00:00 2001 From: Curtis Layne Date: Thu, 3 Jul 2025 09:58:08 -0700 Subject: [PATCH 2/5] Fix code issues --- cli/src/commands/grpc-service/commands/generate.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cli/src/commands/grpc-service/commands/generate.ts b/cli/src/commands/grpc-service/commands/generate.ts index e51e26e335..ef24dd64e3 100644 --- a/cli/src/commands/grpc-service/commands/generate.ts +++ b/cli/src/commands/grpc-service/commands/generate.ts @@ -1,4 +1,5 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import { existsSync, lstatSync } from 'node:fs'; import { resolve } from 'pathe'; import Spinner from 'ora'; import { Command, program } from 'commander'; @@ -87,7 +88,6 @@ async function generateProtoAndMapping( protoLockFile?: string, ): Promise { spinner.text = 'Generating proto schema...'; - const lockFile = resolve(outdir, 'service.proto.lock.json'); const schema = await readFile(schemaFile, 'utf8'); const serviceName = upperFirst(camelCase(name)) + 'Service'; @@ -117,13 +117,15 @@ async function fetchLockData(outdir: string, existingLockFile?: string): Promise return existingLockData; } - return undefined + return undefined; } - const lockFile = resolve(outdir, 'service.proto.lock.json'); - const existingLockData = JSON.parse(await readFile(lockFile, 'utf8')); - if (existingLockData) { - return existingLockData; + const defaultLockFile = resolve(outdir, 'service.proto.lock.json'); + if (existsSync(defaultLockFile)) { + const existingLockData = JSON.parse(await readFile(defaultLockFile, 'utf8')); + if (existingLockData) { + return existingLockData; + } } return undefined; From 51f2951941932f7070427b7aa31cfbc38c29b136 Mon Sep 17 00:00:00 2001 From: Curtis Layne Date: Tue, 8 Jul 2025 13:13:33 -0700 Subject: [PATCH 3/5] Refactor the code --- .../grpc-service/commands/generate.ts | 103 ++++++++++++------ 1 file changed, 67 insertions(+), 36 deletions(-) diff --git a/cli/src/commands/grpc-service/commands/generate.ts b/cli/src/commands/grpc-service/commands/generate.ts index ef24dd64e3..5a76309369 100644 --- a/cli/src/commands/grpc-service/commands/generate.ts +++ b/cli/src/commands/grpc-service/commands/generate.ts @@ -1,13 +1,20 @@ -import { readFile, writeFile, mkdir } from 'node:fs/promises'; -import { existsSync, lstatSync } from 'node:fs'; -import { resolve } from 'pathe'; -import Spinner from 'ora'; -import { Command, program } from 'commander'; import { compileGraphQLToMapping, compileGraphQLToProto, ProtoLock } from '@wundergraph/protographic'; +import { Command, program } from 'commander'; import { camelCase, upperFirst } from 'lodash-es'; +import { access, constants, lstat, readFile, writeFile } from 'node:fs/promises'; +import Spinner, { Ora } from 'ora'; +import { resolve } from 'pathe'; import { BaseCommandOptions } from '../../../core/types/types.js'; import { renderResultTree } from '../../router/commands/plugin/helper.js'; +type CLIOptions = { + input: string; + output: string; + packageName?: string; + goPackage?: string; + protoLock?: string; +}; + export default (opts: BaseCommandOptions) => { const command = new Command('generate'); command.description('Generate a protobuf schema for a remote gRPC service.'); @@ -17,8 +24,9 @@ export default (opts: BaseCommandOptions) => { command.option('-p, --package-name ', 'The name of the proto package. (default "service.v1")', 'service.v1'); command.option('-g, --go-package ', 'Adds an `option go_package` to the proto file.'); command.option( - '-l, --proto-lock ', - 'The path to the existing proto lock file to use as the starting point for the updated proto lock file.', + '-l, --proto-lock ', + 'The path to the existing proto lock file to use as the starting point for the updated proto lock file. ' + + 'Default is to use and overwrite the output file "/service.proto.lock.json".', ); command.action(generateCommandAction); @@ -31,7 +39,7 @@ type GenerationResult = { lockData: ProtoLock | null; }; -async function generateCommandAction(name: string, options: any) { +async function generateCommandAction(name: string, options: CLIOptions) { if (!name) { program.error('A name is required for the proto service'); } @@ -42,20 +50,33 @@ async function generateCommandAction(name: string, options: any) { try { const inputFile = resolve(options.input); +<<<<<<< HEAD // Ensure output directory exists if (!existsSync(options.output)) { await mkdir(options.output, { recursive: true }); +======= + if (!(await exists(options.output))) { + program.error(`Output directory ${options.output} does not exist`); +>>>>>>> 2ef87dc0 (Refactor the code) } - if (!lstatSync(options.output).isDirectory()) { + if (!(await lstat(options.output)).isDirectory()) { program.error(`Output directory ${options.output} is not a directory`); } - if (!existsSync(inputFile)) { + if (!(await exists(inputFile))) { program.error(`Input file ${options.input} does not exist`); } - const result = await generateProtoAndMapping(options.output, inputFile, name, options, spinner, options.protoLock); + const result = await generateProtoAndMapping({ + outdir: options.output, + schemaFile: inputFile, + name, + spinner, + packageName: options.packageName, + goPackage: options.goPackage, + lockFile: options.protoLock, + }); // Write the generated files await writeFile(resolve(options.output, 'mapping.json'), result.mapping); @@ -76,30 +97,40 @@ async function generateCommandAction(name: string, options: any) { } } +type GenerationOptions = { + name: string; + outdir: string; + schemaFile: string; + spinner: Ora; + packageName?: string; + goPackage?: string; + lockFile?: string; +}; + /** * Generate proto and mapping data from schema */ -async function generateProtoAndMapping( - outdir: string, - schemaFile: string, - name: string, - options: any, - spinner: any, - protoLockFile?: string, -): Promise { +async function generateProtoAndMapping({ + name, + outdir, + schemaFile, + spinner, + packageName, + goPackage, + lockFile = resolve(outdir, 'service.proto.lock.json'), +}: GenerationOptions): Promise { spinner.text = 'Generating proto schema...'; const schema = await readFile(schemaFile, 'utf8'); const serviceName = upperFirst(camelCase(name)) + 'Service'; spinner.text = 'Generating mapping and proto files...'; - const lockData = await fetchLockData(outdir, protoLockFile); - + const lockData = await fetchLockData(lockFile); const mapping = compileGraphQLToMapping(schema, serviceName); const proto = compileGraphQLToProto(schema, { serviceName, - packageName: options.packageName, - goPackage: options.goPackage, + packageName, + goPackage, lockData, }); @@ -110,19 +141,9 @@ async function generateProtoAndMapping( }; } -async function fetchLockData(outdir: string, existingLockFile?: string): Promise { - if (existingLockFile != null && existsSync(existingLockFile)) { - const existingLockData = JSON.parse(await readFile(existingLockFile, 'utf8')); - if (existingLockData) { - return existingLockData; - } - - return undefined; - } - - const defaultLockFile = resolve(outdir, 'service.proto.lock.json'); - if (existsSync(defaultLockFile)) { - const existingLockData = JSON.parse(await readFile(defaultLockFile, 'utf8')); +async function fetchLockData(lockFile: string): Promise { + if (await exists(lockFile)) { + const existingLockData = JSON.parse(await readFile(lockFile, 'utf8')); if (existingLockData) { return existingLockData; } @@ -130,3 +151,13 @@ async function fetchLockData(outdir: string, existingLockFile?: string): Promise return undefined; } + +// Usage of existsSync from node:fs is not recommended. Use access instead. +async function exists(path: string): Promise { + try { + await access(path, constants.R_OK); + return true; + } catch { + return false; + } +} From aec33601cc47ededd45c1b7c8101df349603e4e1 Mon Sep 17 00:00:00 2001 From: Curtis Layne Date: Tue, 8 Jul 2025 13:33:31 -0700 Subject: [PATCH 4/5] Fix linter --- .../commands/grpc-service/commands/generate.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/cli/src/commands/grpc-service/commands/generate.ts b/cli/src/commands/grpc-service/commands/generate.ts index 5a76309369..0323159388 100644 --- a/cli/src/commands/grpc-service/commands/generate.ts +++ b/cli/src/commands/grpc-service/commands/generate.ts @@ -1,8 +1,8 @@ +import { access, constants, lstat, readFile, writeFile } from 'node:fs/promises'; import { compileGraphQLToMapping, compileGraphQLToProto, ProtoLock } from '@wundergraph/protographic'; import { Command, program } from 'commander'; import { camelCase, upperFirst } from 'lodash-es'; -import { access, constants, lstat, readFile, writeFile } from 'node:fs/promises'; -import Spinner, { Ora } from 'ora'; +import Spinner, { type Ora } from 'ora'; import { resolve } from 'pathe'; import { BaseCommandOptions } from '../../../core/types/types.js'; import { renderResultTree } from '../../router/commands/plugin/helper.js'; @@ -142,17 +142,15 @@ async function generateProtoAndMapping({ } async function fetchLockData(lockFile: string): Promise { - if (await exists(lockFile)) { - const existingLockData = JSON.parse(await readFile(lockFile, 'utf8')); - if (existingLockData) { - return existingLockData; - } + if (!(await exists(lockFile))) { + return undefined; } - return undefined; + const existingLockData = JSON.parse(await readFile(lockFile, 'utf8')); + return existingLockData == null ? undefined : existingLockData; } -// Usage of existsSync from node:fs is not recommended. Use access instead. +// Usage of exists from node:fs is not recommended. Use access instead. async function exists(path: string): Promise { try { await access(path, constants.R_OK); From 3464a4eedb1e3e1465a8bc42b92702cdcf49d778 Mon Sep 17 00:00:00 2001 From: Curtis Layne Date: Fri, 11 Jul 2025 12:59:02 -0700 Subject: [PATCH 5/5] Rebase main --- cli/src/commands/grpc-service/commands/generate.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/cli/src/commands/grpc-service/commands/generate.ts b/cli/src/commands/grpc-service/commands/generate.ts index 0323159388..3aec670f73 100644 --- a/cli/src/commands/grpc-service/commands/generate.ts +++ b/cli/src/commands/grpc-service/commands/generate.ts @@ -1,4 +1,4 @@ -import { access, constants, lstat, readFile, writeFile } from 'node:fs/promises'; +import { access, constants, lstat, mkdir, readFile, writeFile } from 'node:fs/promises'; import { compileGraphQLToMapping, compileGraphQLToProto, ProtoLock } from '@wundergraph/protographic'; import { Command, program } from 'commander'; import { camelCase, upperFirst } from 'lodash-es'; @@ -50,14 +50,9 @@ async function generateCommandAction(name: string, options: CLIOptions) { try { const inputFile = resolve(options.input); -<<<<<<< HEAD // Ensure output directory exists - if (!existsSync(options.output)) { - await mkdir(options.output, { recursive: true }); -======= if (!(await exists(options.output))) { - program.error(`Output directory ${options.output} does not exist`); ->>>>>>> 2ef87dc0 (Refactor the code) + await mkdir(options.output, { recursive: true }); } if (!(await lstat(options.output)).isDirectory()) {