Skip to content

Commit a7f8487

Browse files
authored
fix(librarian/nodejs): copy missing transitive proto dependencies (#4589)
The Node.js library generation now ensures that all transitive proto dependencies referenced in the generated package are copied to the protos directory. This is handled by a new copyMissingProtos function in the post-processor. Previously, the generator only copied the API's own protos, but Node.js libraries sometimes reference common protos that are required for proto compilation. The post-processor now reads the `*_proto_list.json` files generated by the GAPIC generator and copies any missing referenced protos from the googleapis directory. For #4404
1 parent 0eba28d commit a7f8487

2 files changed

Lines changed: 69 additions & 7 deletions

File tree

internal/librarian/nodejs/generate.go

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package nodejs
1717

1818
import (
1919
"context"
20+
"encoding/json"
2021
"errors"
2122
"fmt"
2223
"os"
@@ -45,7 +46,7 @@ func Generate(ctx context.Context, library *config.Library, googleapisDir string
4546
return fmt.Errorf("failed to generate api %q: %w", api.Path, err)
4647
}
4748
}
48-
if err := runPostProcessor(ctx, library, repoRoot, outdir); err != nil {
49+
if err := runPostProcessor(ctx, library, googleapisDir, repoRoot, outdir); err != nil {
4950
return fmt.Errorf("failed to run post processor: %w", err)
5051
}
5152
return nil
@@ -149,7 +150,7 @@ func buildGeneratorArgs(api *config.API, library *config.Library, googleapisDir,
149150

150151
// runPostProcessor combines versioned API outputs from owl-bot-staging/ into
151152
// the output directory using gapic-node-processing, then compiles protos.
152-
func runPostProcessor(ctx context.Context, library *config.Library, repoRoot, outDir string) error {
153+
func runPostProcessor(ctx context.Context, library *config.Library, googleapisDir, repoRoot, outDir string) error {
153154
owlbotPath := filepath.Join(outDir, "owlbot.py")
154155
if _, err := os.Stat(owlbotPath); err == nil {
155156
// Old way: use synthtool
@@ -205,6 +206,10 @@ func runPostProcessor(ctx context.Context, library *config.Library, repoRoot, ou
205206
}
206207
}
207208

209+
if err := copyMissingProtos(googleapisDir, outDir); err != nil {
210+
return fmt.Errorf("copyMissingProtos: %w", err)
211+
}
212+
208213
if err := command.RunInDir(ctx, outDir, "compileProtos", "src"); err != nil {
209214
return fmt.Errorf("compileProtos: %w", err)
210215
}
@@ -251,7 +256,64 @@ func runPostProcessor(ctx context.Context, library *config.Library, repoRoot, ou
251256
return nil
252257
}
253258

254-
// Format runs eslint --fix on the library directory.
259+
// copyMissingProtos reads *_proto_list.json files under outDir/src/ and copies
260+
// any referenced protos that are missing from outDir/protos/ using the source
261+
// files in googleapisDir. The generator copies the API's own protos but not
262+
// transitive dependencies (e.g. google/logging/type/log_severity.proto).
263+
func copyMissingProtos(googleapisDir, outDir string) error {
264+
googleapisDir, err := filepath.Abs(googleapisDir)
265+
if err != nil {
266+
return fmt.Errorf("failed to resolve googleapis directory: %w", err)
267+
}
268+
269+
lists, err := filepath.Glob(filepath.Join(outDir, "src", "*", "*_proto_list.json"))
270+
if err != nil {
271+
return fmt.Errorf("failed to glob proto list files: %w", err)
272+
}
273+
274+
for _, listPath := range lists {
275+
data, err := os.ReadFile(listPath)
276+
if err != nil {
277+
return fmt.Errorf("failed to read %s: %w", listPath, err)
278+
}
279+
var entries []string
280+
if err := json.Unmarshal(data, &entries); err != nil {
281+
return fmt.Errorf("failed to parse %s: %w", listPath, err)
282+
}
283+
284+
listDir := filepath.Dir(listPath)
285+
for _, entry := range entries {
286+
absPath := filepath.Join(listDir, entry)
287+
absPath = filepath.Clean(absPath)
288+
if _, err := os.Stat(absPath); err == nil {
289+
continue
290+
}
291+
292+
// Extract the proto-relative path after "protos/".
293+
const protosPrefix = "protos/"
294+
idx := strings.Index(entry, protosPrefix)
295+
if idx < 0 {
296+
continue
297+
}
298+
relPath := entry[idx+len(protosPrefix):]
299+
300+
srcPath := filepath.Join(googleapisDir, relPath)
301+
content, err := os.ReadFile(srcPath)
302+
if err != nil {
303+
return fmt.Errorf("failed to read source proto %s: %w", srcPath, err)
304+
}
305+
if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil {
306+
return fmt.Errorf("failed to create directory for %s: %w", absPath, err)
307+
}
308+
if err := os.WriteFile(absPath, content, 0644); err != nil {
309+
return fmt.Errorf("failed to write proto %s: %w", absPath, err)
310+
}
311+
}
312+
}
313+
return nil
314+
}
315+
316+
// Format runs gts (npm run fix) on the library directory.
255317
func Format(ctx context.Context, library *config.Library) error {
256318
if err := ctx.Err(); err != nil {
257319
return err

internal/librarian/nodejs/generate_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ func TestRunPostProcessor_Owlbot(t *testing.T) {
333333
t.Fatal(err)
334334
}
335335

336-
if err := runPostProcessor(t.Context(), library, repoRoot, outDir); err != nil {
336+
if err := runPostProcessor(t.Context(), library, "", repoRoot, outDir); err != nil {
337337
t.Fatal(err)
338338
}
339339
if _, err := os.Stat(filepath.Join(outDir, "owlbot-ran.txt")); err != nil {
@@ -451,7 +451,7 @@ func TestRunPostProcessor(t *testing.T) {
451451
}
452452
}
453453

454-
if err := runPostProcessor(t.Context(), library, repoRoot, outDir); err != nil {
454+
if err := runPostProcessor(t.Context(), library, "", repoRoot, outDir); err != nil {
455455
t.Fatal(err)
456456
}
457457
if _, err := os.Stat(filepath.Join(repoRoot, "owl-bot-staging")); !os.IsNotExist(err) {
@@ -509,7 +509,7 @@ func TestRunPostProcessor_CustomScripts(t *testing.T) {
509509
t.Fatal(err)
510510
}
511511

512-
if err := runPostProcessor(t.Context(), library, repoRoot, outDir); err != nil {
512+
if err := runPostProcessor(t.Context(), library, "", repoRoot, outDir); err != nil {
513513
t.Fatal(err)
514514
}
515515
if _, err := os.Stat(filepath.Join(repoRoot, "owl-bot-staging")); !os.IsNotExist(err) {
@@ -610,7 +610,7 @@ func TestRunPostProcessor_PreservesFiles(t *testing.T) {
610610
t.Fatal(err)
611611
}
612612

613-
if err := runPostProcessor(t.Context(), library, repoRoot, outDir); err != nil {
613+
if err := runPostProcessor(t.Context(), library, "", repoRoot, outDir); err != nil {
614614
t.Fatal(err)
615615
}
616616

0 commit comments

Comments
 (0)