Skip to content

Commit 77e5996

Browse files
authored
feat: support grpc plugins with ts and bun (#2293)
1 parent 1f48a2d commit 77e5996

File tree

70 files changed

+11060
-1854
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+11060
-1854
lines changed

.github/workflows/cli-ci.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ jobs:
3434
- name: Generate code
3535
run: pnpm buf generate --template buf.ts.gen.yaml
3636

37+
- name: Generate router templates
38+
run: pnpm --filter ./cli compile-templates
39+
3740
- name: Check if git is not dirty after generating files
3841
run: git diff --no-ext-diff --exit-code
3942

.github/workflows/router-ci.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ jobs:
4949
- name: Install tools
5050
run: make setup-build-tools
5151

52+
- name: Install Bun For Plugin Building
53+
uses: oven-sh/setup-bun@v2
54+
with:
55+
bun-version: 1.2.15
56+
5257
- name: Generate code
5358
run: make generate-go
5459

@@ -127,6 +132,11 @@ jobs:
127132
- name: Install tools
128133
run: make setup-build-tools
129134

135+
- name: Install Bun For Plugin Building
136+
uses: oven-sh/setup-bun@v2
137+
with:
138+
bun-version: 1.2.15
139+
130140
- name: Generate code
131141
run: make generate-go
132142

@@ -299,6 +309,12 @@ jobs:
299309
router-tests/go.sum
300310
- name: Install tools
301311
run: make setup-build-tools
312+
313+
- name: Install Bun For Plugin Building
314+
uses: oven-sh/setup-bun@v2
315+
with:
316+
bun-version: 1.2.15
317+
302318
- name: Install dependencies
303319
working-directory: ./router-tests
304320
run: go mod download

cli/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
"url": "https://github.com/wundergraph/cosmo"
2020
},
2121
"scripts": {
22+
"compile-templates": "tsx scripts/compile-templates.ts && prettier --write src/commands/router/commands/plugin/templates/*.ts",
23+
"prebuild": "npm run compile-templates",
24+
"prebuild:bun": "bun run compile-templates",
2225
"build": "rm -rf dist && tsc",
2326
"build:bun": "bun build --compile --production --outfile wgc src/index.ts",
2427
"wgc": "tsx --env-file .env src/index.ts",

cli/scripts/compile-templates.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
2+
import { join, basename } from 'node:path';
3+
4+
interface TemplateMap {
5+
[key: string]: string;
6+
}
7+
8+
// Convert file names to camelCase property names
9+
function fileNameToPropertyName(fileName: string): string {
10+
// Remove .template extension
11+
let name = fileName.replace('.template', '');
12+
13+
// Handle special cases for dotfiles
14+
if (name.startsWith('.')) {
15+
name = name.slice(1); // Remove the dot
16+
}
17+
18+
// Convert to camelCase
19+
// Split by dots, dashes, underscores, and spaces
20+
const parts = name.split(/[ ._-]/);
21+
22+
return parts
23+
.map((part, index) => {
24+
// Normalize all-caps words (like README -> Readme)
25+
if (part === part.toUpperCase() && part.length > 1) {
26+
part = part.charAt(0) + part.slice(1).toLowerCase();
27+
}
28+
29+
if (index === 0) {
30+
// First part: lowercase first char, preserve rest
31+
return part.charAt(0).toLowerCase() + part.slice(1);
32+
}
33+
// Subsequent parts: uppercase first char, preserve rest
34+
return part.charAt(0).toUpperCase() + part.slice(1);
35+
})
36+
.join('');
37+
}
38+
39+
function compileTemplates(dir: string, outputFile: string, comment?: string) {
40+
const files = readdirSync(dir).filter((f) => f.endsWith('.template'));
41+
42+
if (files.length === 0) {
43+
console.log(`No templates found in ${dir}`);
44+
return;
45+
}
46+
47+
const templates: TemplateMap = {};
48+
49+
for (const file of files) {
50+
const filePath = join(dir, file);
51+
const content = readFileSync(filePath, 'utf8');
52+
53+
// Convert file name to property name
54+
const key = fileNameToPropertyName(file);
55+
templates[key] = content;
56+
}
57+
58+
// Generate TypeScript file
59+
const lines: string[] = [];
60+
61+
if (comment) {
62+
lines.push(`// ${comment}`);
63+
}
64+
lines.push('// This file is auto-generated. Do not edit manually.');
65+
lines.push('/* eslint-disable no-template-curly-in-string */');
66+
lines.push('');
67+
68+
// Create const declarations
69+
for (const [key, content] of Object.entries(templates)) {
70+
lines.push(`const ${key} = ${JSON.stringify(content)};`);
71+
lines.push('');
72+
}
73+
74+
// Export default object
75+
lines.push('export default {');
76+
for (const key of Object.keys(templates)) {
77+
lines.push(` ${key},`);
78+
}
79+
lines.push('};');
80+
lines.push('');
81+
82+
writeFileSync(outputFile, lines.join('\n'), 'utf8');
83+
console.log(`Generated ${outputFile} with ${files.length} templates`);
84+
}
85+
86+
// Compile all template subdirectories, generating <templates>/<folder>.ts in the templates root
87+
const templatesDir = 'src/commands/router/commands/plugin/templates';
88+
89+
const entries = readdirSync(templatesDir, { withFileTypes: true });
90+
const subdirs = entries.filter((e: any) => e.isDirectory());
91+
92+
if (subdirs.length === 0) {
93+
console.log(`No template subdirectories found in ${templatesDir}`);
94+
} else {
95+
for (const dirent of subdirs) {
96+
const dirName = dirent.name;
97+
const dirPath = join(templatesDir, dirName);
98+
const outFile = join(templatesDir, `${dirName}.ts`);
99+
const comment = `Templates for ${dirName} (templating is done by pupa)`;
100+
compileTemplates(dirPath, outFile, comment);
101+
}
102+
console.log('All templates compiled successfully');
103+
}

cli/src/commands/grpc-service/commands/generate.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
compileGraphQLToMapping,
44
compileGraphQLToProto,
55
ProtoLock,
6+
ProtoOption,
67
validateGraphQLSDL,
78
} from '@wundergraph/protographic';
89
import { Command, program } from 'commander';
@@ -11,6 +12,7 @@ import Spinner, { type Ora } from 'ora';
1112
import { resolve } from 'pathe';
1213
import { BaseCommandOptions } from '../../../core/types/types.js';
1314
import { renderResultTree, renderValidationResults } from '../../router/commands/plugin/helper.js';
15+
import { getGoModulePathProtoOption } from '../../router/commands/plugin/toolchain.js';
1416

1517
type CLIOptions = {
1618
input: string;
@@ -135,11 +137,17 @@ async function generateProtoAndMapping({
135137
// Continue with generation if validation passed (no errors)
136138
spinner.text = 'Generating mapping and proto files...';
137139
const mapping = compileGraphQLToMapping(schema, serviceName);
140+
141+
const protoOptions: ProtoOption[] = [];
142+
if (goPackage) {
143+
protoOptions.push(getGoModulePathProtoOption(goPackage!));
144+
}
145+
138146
const proto = compileGraphQLToProto(schema, {
139147
serviceName,
140148
packageName,
141-
goPackage,
142149
lockData,
150+
protoOptions,
143151
});
144152

145153
return {

cli/src/commands/router/commands/plugin/commands/build.ts

Lines changed: 60 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@ import os from 'node:os';
33
import { Command, program } from 'commander';
44
import { resolve } from 'pathe';
55
import Spinner from 'ora';
6+
import { ProtoOption } from '@wundergraph/protographic';
67
import { BaseCommandOptions } from '../../../../../core/types/types.js';
78
import { renderResultTree } from '../helper.js';
89
import {
9-
buildBinaries,
10+
buildGoBinaries,
1011
checkAndInstallTools,
1112
generateGRPCCode,
1213
generateProtoAndMapping,
13-
HOST_PLATFORM,
14+
getLanguage,
1415
installGoDependencies,
16+
installTsDependencies,
17+
typeCheckTs,
18+
buildTsBinaries,
1519
normalizePlatforms,
20+
validateAndGetGoModulePath,
21+
getGoModulePathProtoOption,
1622
} from '../toolchain.js';
1723

1824
export default (opts: BaseCommandOptions) => {
@@ -21,54 +27,81 @@ export default (opts: BaseCommandOptions) => {
2127
command.argument('[directory]', 'Directory of the plugin', '.');
2228
command.option('--generate-only', 'Generate only the proto and mapping files, do not compile the plugin');
2329
command.option('--debug', 'Build the binary with debug information', false);
24-
command.option('--platform [platforms...]', 'Platform-architecture combinations (e.g., darwin-arm64 linux-amd64)', [
25-
HOST_PLATFORM,
26-
]);
30+
command.option(
31+
'--platform [platforms...]',
32+
'Platform-architecture combinations (e.g., darwin-arm64 linux-amd64)',
33+
[],
34+
);
2735
command.option('--all-platforms', 'Build for all supported platforms', false);
2836
command.option('--skip-tools-installation', 'Skip tool installation', false);
2937
command.option(
3038
'--force-tools-installation',
3139
'Force tools installation regardless of version check or confirmation',
3240
false,
3341
);
34-
command.option(
35-
'--go-module-path <path>',
36-
'Go module path to use for the plugin',
37-
'github.com/wundergraph/cosmo/plugin',
38-
);
42+
command.option('--go-module-path <path>', 'Go module path to use for the plugin');
3943

4044
command.action(async (directory, options) => {
4145
const startTime = performance.now();
4246
const pluginDir = resolve(directory);
4347
const spinner = Spinner();
4448
const pluginName = path.basename(pluginDir);
45-
const goModulePath = options.goModulePath;
49+
50+
const language = getLanguage(pluginDir);
51+
if (!language) {
52+
renderResultTree(spinner, 'Plugin language detection failed!', false, pluginName, {
53+
output: pluginDir,
54+
});
55+
program.error('');
56+
}
57+
58+
const protoOptions: ProtoOption[] = [];
4659
let platforms: string[] = [];
4760

4861
try {
4962
// Check and install tools if needed
5063
if (!options.skipToolsInstallation) {
51-
await checkAndInstallTools(options.forceToolsInstallation);
64+
await checkAndInstallTools(options.forceToolsInstallation, language);
5265
}
5366

54-
// Normalize platform list
55-
platforms = normalizePlatforms(options.platform, options.allPlatforms);
56-
5767
// Start the main build process
5868
spinner.start('Building plugin...');
5969

70+
const goModulePath = validateAndGetGoModulePath(language, options.goModulePath);
71+
72+
switch (language) {
73+
case 'ts': {
74+
await installTsDependencies(pluginDir, spinner);
75+
break;
76+
}
77+
case 'go': {
78+
protoOptions.push(getGoModulePathProtoOption(goModulePath!));
79+
break;
80+
}
81+
}
82+
83+
// Normalize platform list
84+
platforms = normalizePlatforms(options.platform, options.allPlatforms, language);
85+
6086
// Generate proto and mapping files
61-
await generateProtoAndMapping(pluginDir, goModulePath, spinner);
87+
await generateProtoAndMapping(pluginDir, protoOptions, spinner);
6288

6389
// Generate gRPC code
64-
await generateGRPCCode(pluginDir, spinner);
90+
await generateGRPCCode(pluginDir, spinner, language);
6591

6692
if (!options.generateOnly) {
67-
// Install Go dependencies
68-
await installGoDependencies(pluginDir, spinner);
69-
70-
// Build binaries for all platforms
71-
await buildBinaries(pluginDir, platforms, options.debug, spinner);
93+
switch (language) {
94+
case 'go': {
95+
await installGoDependencies(pluginDir, spinner);
96+
await buildGoBinaries(pluginDir, platforms, options.debug, spinner);
97+
break;
98+
}
99+
case 'ts': {
100+
await typeCheckTs(pluginDir, spinner);
101+
await buildTsBinaries(pluginDir, platforms, options.debug, spinner);
102+
break;
103+
}
104+
}
72105
}
73106

74107
// Calculate and format elapsed time
@@ -79,24 +112,24 @@ export default (opts: BaseCommandOptions) => {
79112

80113
renderResultTree(spinner, 'Plugin built successfully!', true, pluginName, {
81114
output: pluginDir,
82-
'go module': goModulePath,
83115
platforms: platforms.join(', '),
84116
env: `${os.platform()} ${os.arch()}`,
85117
build: options.debug ? 'debug' : 'release',
86118
type: options.generateOnly ? 'generate-only' : 'full',
87119
time: formattedTime,
120+
protoOptions: protoOptions.map(({ name, constant }) => `${name}=${constant}`).join(','),
88121
});
89122
} catch (error: any) {
90-
renderResultTree(spinner, 'Plugin build failed!', false, pluginName, {
123+
const details: Record<string, any> = {
91124
output: pluginDir,
92-
'go module': goModulePath,
93125
platforms: platforms.join(', '),
94126
env: `${os.platform()} ${os.arch()}`,
95127
build: options.debug ? 'debug' : 'release',
96128
type: options.generateOnly ? 'generate-only' : 'full',
97129
error: error.message,
98-
});
99-
130+
protoOptions: protoOptions.map(({ name, constant }) => `${name}=${constant}`).join(','),
131+
};
132+
renderResultTree(spinner, 'Plugin build failed!', false, pluginName, details);
100133
program.error('');
101134
}
102135
});

0 commit comments

Comments
 (0)