Skip to content
Open
Show file tree
Hide file tree
Changes from 83 commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
29bb604
feat: cli updates
SkArchon Oct 21, 2025
e1c3dbd
fix: updates
SkArchon Oct 21, 2025
cc08a5b
feat: cli updates
SkArchon Oct 22, 2025
5e68f51
feat: cli updates
SkArchon Oct 22, 2025
c2f32f7
fix: cli
SkArchon Oct 22, 2025
be23381
fix: updates
SkArchon Oct 22, 2025
9053591
fix: updates
SkArchon Oct 22, 2025
7abe72f
fix: updates
SkArchon Oct 23, 2025
009ea52
fix: add health check service and move go router plugin
SkArchon Oct 23, 2025
f962e9d
fix: updates
SkArchon Oct 23, 2025
809ae2e
fix: router plugin
SkArchon Oct 27, 2025
1c480c5
fix: updates
SkArchon Oct 27, 2025
d41a606
fix: updates
SkArchon Oct 27, 2025
f6f7b61
fix: updates
SkArchon Oct 27, 2025
6ea8136
fix: updates
SkArchon Oct 27, 2025
2f4a1cc
fix: updates
SkArchon Oct 27, 2025
81d8e08
fix: updates
SkArchon Oct 27, 2025
1eb5db5
fix: updates
SkArchon Oct 27, 2025
52adc57
fix: updates
SkArchon Oct 27, 2025
11347a8
fix: updates
SkArchon Oct 28, 2025
f423a92
fix: updates
SkArchon Oct 28, 2025
6ab77e9
fix: cleanup
SkArchon Oct 28, 2025
19e1f19
fix: revert
SkArchon Oct 28, 2025
a06c05c
fix: cleanup
SkArchon Oct 28, 2025
a6a1255
fix: cleanup
SkArchon Oct 28, 2025
5cda4dc
fix: cleanup
SkArchon Oct 28, 2025
e0025be
fix: updates
SkArchon Oct 28, 2025
df8c076
fix: templates
SkArchon Oct 28, 2025
6eace15
fix: templates
SkArchon Oct 28, 2025
10be6cb
fix: updates
SkArchon Oct 28, 2025
6c1de9f
fix: lint changes
SkArchon Oct 28, 2025
4897a0f
fix: linting
SkArchon Oct 28, 2025
2a0292e
fix: updates
SkArchon Oct 28, 2025
69c434f
fix: updates
SkArchon Oct 29, 2025
a94f90f
fix: changes
SkArchon Oct 29, 2025
773d616
fix: temp
SkArchon Oct 29, 2025
df92322
fix: updates
SkArchon Oct 29, 2025
f8b2145
fix: updates
SkArchon Oct 29, 2025
c94513f
fix: updates
SkArchon Oct 29, 2025
40a3d48
fix: updates
SkArchon Oct 29, 2025
dc15d41
fix: refactoring
SkArchon Oct 29, 2025
c7229ae
fix: refactoring
SkArchon Oct 29, 2025
5340a19
fix: updates
SkArchon Oct 29, 2025
48831fd
fix: ci changes
SkArchon Oct 29, 2025
5feb446
fix: refactoring
SkArchon Oct 29, 2025
30f48e6
fix: ci updates
SkArchon Oct 29, 2025
7f4d714
fix: ci updates
SkArchon Oct 29, 2025
0f9c289
fix: updates
SkArchon Oct 29, 2025
2dddf0c
fix: refactoring`
SkArchon Oct 29, 2025
4bb7b10
fix: revert
SkArchon Oct 29, 2025
4bb5dd2
Merge branch 'main' into milinda/eng-7174-support-grpc-plugins-with-b…
SkArchon Oct 29, 2025
56f4e28
fix: review comments
SkArchon Oct 29, 2025
f5a7089
fix: updates
SkArchon Oct 29, 2025
ee9307a
fix: recompile templates
SkArchon Oct 29, 2025
a00ed32
fix: cleanup
SkArchon Oct 29, 2025
4e25bd4
fix: cleanup
SkArchon Oct 29, 2025
f20ff83
fix: updates
SkArchon Oct 29, 2025
db1d2f0
fix: refactoring
SkArchon Oct 29, 2025
489b50a
fix: linting
SkArchon Oct 29, 2025
760d277
Merge branch 'main' into milinda/eng-7174-support-grpc-plugins-with-b…
SkArchon Oct 29, 2025
747854a
fix: refactoring
SkArchon Oct 29, 2025
10125cc
fix: templates
SkArchon Oct 29, 2025
fc4bf7f
fix: platform updates
SkArchon Oct 30, 2025
c1580f2
fix: linting
SkArchon Oct 30, 2025
c9340e2
Merge branch 'main' into milinda/eng-7174-support-grpc-plugins-with-b…
SkArchon Oct 30, 2025
c2d0b81
fix: tests
SkArchon Oct 30, 2025
4dce726
fix: templates
SkArchon Oct 30, 2025
a91f3dd
fix: tests
SkArchon Oct 30, 2025
8a5bda0
fix: revert
SkArchon Oct 30, 2025
d4c8b78
fix: formatting
SkArchon Oct 30, 2025
1672582
fix: update imports`
SkArchon Oct 30, 2025
7883122
fix: demonstration
SkArchon Oct 30, 2025
6fbd524
fix: update configuration
SkArchon Oct 30, 2025
e553b06
fix: diff testing
SkArchon Oct 30, 2025
1991bf0
fix: diff updates
SkArchon Oct 30, 2025
349e5b1
fix: project updates
SkArchon Oct 30, 2025
10594aa
Merge branch 'main' into milinda/eng-7174-support-grpc-plugins-with-b…
SkArchon Nov 5, 2025
4a9234b
fix: patch the grpc health check node modules
SkArchon Nov 10, 2025
1291745
fix: template compilation
SkArchon Nov 10, 2025
22f7c5b
Merge branch 'main' into milinda/eng-7174-support-grpc-plugins-with-b…
SkArchon Nov 10, 2025
d27a5f3
fix: local builds
SkArchon Nov 11, 2025
3e523e8
fix: spinner updating
SkArchon Nov 11, 2025
a80cf59
fix: ts tests
SkArchon Nov 15, 2025
770c4ba
fix: tests
SkArchon Nov 15, 2025
49812b6
Merge remote-tracking branch 'origin/main' into milinda/eng-7174-supp…
SkArchon Nov 15, 2025
04b622b
fix: linting
SkArchon Nov 15, 2025
42273e1
fix: update makefile
SkArchon Nov 15, 2025
10ba33e
fix: plugin build
SkArchon Nov 15, 2025
3a08009
fix: workflows
SkArchon Nov 15, 2025
1238bb2
fix: ci updates
SkArchon Nov 15, 2025
9d291a2
fix: courses updates
SkArchon Nov 15, 2025
07437fc
fix: go module
SkArchon Nov 15, 2025
a95f179
fix: enable debugging
SkArchon Nov 15, 2025
acb1628
fix: linting
SkArchon Nov 15, 2025
2b4e469
fix: package updating
SkArchon Nov 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/cli-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- name: Generate code
run: pnpm buf generate --template buf.ts.gen.yaml

- name: Generate router templates
run: pnpm --filter ./cli compile-templates

- name: Check if git is not dirty after generating files
run: git diff --no-ext-diff --exit-code

Expand Down
97 changes: 97 additions & 0 deletions cli/courses/src/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, test, expect } from "bun:test";
import * as grpc from "@grpc/grpc-js";
import type { Subprocess } from "bun";

// Generated gRPC types
import { CoursesServiceClient } from '../generated/service_grpc_pb.js';
import { QueryHelloRequest, QueryHelloResponse } from "../generated/service_pb.js";

function queryHello(client: CoursesServiceClient, name: string): Promise<QueryHelloResponse> {
return new Promise((resolve, reject) => {
const req = new QueryHelloRequest();
req.setName(name);
client.queryHello(req, (err, resp) => {
if (err) {
reject(err);
return;
}
if (!resp) {
reject(new Error("empty response"));
return;
}
resolve(resp);
});
});
}

describe("CoursesServiceService.queryHello", () => {
test("returns greeting with sequential world IDs", async () => {
const [subprocess, address] = await startPluginProcess();
const client = createClient(address);
try {
const cases = [
{ name: "Alice", wantId: "world-1", wantName: "Hello from CoursesServiceService plugin! Alice" },
{ name: "", wantId: "world-2", wantName: "Hello from CoursesServiceService plugin! " },
{ name: "John & Jane", wantId: "world-3", wantName: "Hello from CoursesServiceService plugin! John & Jane" },
];

for (const c of cases) {
const resp = await queryHello(client, c.name);
const world = resp.getHello();
expect(world).toBeTruthy();
expect(world!.getId()).toBe(c.wantId);
expect(world!.getName()).toBe(c.wantName);
}
} finally {
client.close();
subprocess.kill();
}
});

test("IDs increment across multiple requests in a fresh process", async () => {
const [subprocess, address] = await startPluginProcess();
const client = createClient(address);
try {
const first = await queryHello(client, "First");
expect(first.getHello()!.getId()).toBe("world-1");

const second = await queryHello(client, "Second");
expect(second.getHello()!.getId()).toBe("world-2");

const third = await queryHello(client, "Third");
expect(third.getHello()!.getId()).toBe("world-3");
} finally {
client.close();
subprocess.kill();
}
});
});


async function startPluginProcess(): Promise<[Subprocess, string]> {
const proc = Bun.spawn(["bun", "run", "src/plugin.ts"], {
stdout: "pipe",
stderr: "inherit",
});

// Read the first line from stdout and parse the address
if (!proc.stdout) {
throw new Error("plugin stdout not available");
}
const reader = proc.stdout.getReader();
const decoder = new TextDecoder();
const { value } = await reader.read();
reader.releaseLock();

const text = decoder.decode(value ?? new Uint8Array());
const firstLine = text.split("\n")[0]?.trim() ?? "";
const parts = firstLine.split("|");
const address = parts[3];

return [proc, address];
}

function createClient(address: string): CoursesServiceClient {
const target = `unix://$\\{address\\}`;
return new CoursesServiceClient(target, grpc.credentials.createInsecure());
}
3 changes: 3 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
"url": "https://github.com/wundergraph/cosmo"
},
"scripts": {
"compile-templates": "tsx scripts/compile-templates.ts && prettier --write src/commands/router/commands/plugin/templates/*.ts",
"prebuild": "npm run compile-templates",
"prebuild:bun": "bun run compile-templates",
"build": "rm -rf dist && tsc",
"build:bun": "bun build --compile --production --outfile wgc src/index.ts",
"wgc": "tsx --env-file .env src/index.ts",
Expand Down
103 changes: 103 additions & 0 deletions cli/scripts/compile-templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
import { join, basename } from 'node:path';

interface TemplateMap {
[key: string]: string;
}

// Convert file names to camelCase property names
function fileNameToPropertyName(fileName: string): string {
// Remove .template extension
let name = fileName.replace('.template', '');

// Handle special cases for dotfiles
if (name.startsWith('.')) {
name = name.slice(1); // Remove the dot
}

// Convert to camelCase
// Split by dots, dashes, underscores, and spaces
const parts = name.split(/[ ._-]/);

return parts
.map((part, index) => {
// Normalize all-caps words (like README -> Readme)
if (part === part.toUpperCase() && part.length > 1) {
part = part.charAt(0) + part.slice(1).toLowerCase();
}

if (index === 0) {
// First part: lowercase first char, preserve rest
return part.charAt(0).toLowerCase() + part.slice(1);
}
// Subsequent parts: uppercase first char, preserve rest
return part.charAt(0).toUpperCase() + part.slice(1);
})
.join('');
}

function compileTemplates(dir: string, outputFile: string, comment?: string) {
const files = readdirSync(dir).filter((f) => f.endsWith('.template'));

if (files.length === 0) {
console.log(`No templates found in ${dir}`);
return;
}

const templates: TemplateMap = {};

for (const file of files) {
const filePath = join(dir, file);
const content = readFileSync(filePath, 'utf8');

// Convert file name to property name
const key = fileNameToPropertyName(file);
templates[key] = content;
}

// Generate TypeScript file
const lines: string[] = [];

if (comment) {
lines.push(`// ${comment}`);
}
lines.push('// This file is auto-generated. Do not edit manually.');
lines.push('/* eslint-disable no-template-curly-in-string */');
lines.push('');

// Create const declarations
for (const [key, content] of Object.entries(templates)) {
lines.push(`const ${key} = ${JSON.stringify(content)};`);
lines.push('');
}

// Export default object
lines.push('export default {');
for (const key of Object.keys(templates)) {
lines.push(` ${key},`);
}
lines.push('};');
lines.push('');

writeFileSync(outputFile, lines.join('\n'), 'utf8');
console.log(`Generated ${outputFile} with ${files.length} templates`);
}

// Compile all template subdirectories, generating <templates>/<folder>.ts in the templates root
const templatesDir = 'src/commands/router/commands/plugin/templates';

const entries = readdirSync(templatesDir, { withFileTypes: true });
const subdirs = entries.filter((e: any) => e.isDirectory());

if (subdirs.length === 0) {
console.log(`No template subdirectories found in ${templatesDir}`);
} else {
for (const dirent of subdirs) {
const dirName = dirent.name;
const dirPath = join(templatesDir, dirName);
const outFile = join(templatesDir, `${dirName}.ts`);
const comment = `Templates for ${dirName} (templating is done by pupa)`;
compileTemplates(dirPath, outFile, comment);
}
console.log('All templates compiled successfully');
}
49 changes: 35 additions & 14 deletions cli/src/commands/router/commands/plugin/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import Spinner from 'ora';
import { BaseCommandOptions } from '../../../../../core/types/types.js';
import { renderResultTree } from '../helper.js';
import {
buildBinaries,
buildGoBinaries,
checkAndInstallTools,
generateGRPCCode,
generateProtoAndMapping,
HOST_PLATFORM,
getLanguage,
installGoDependencies,
installTsDependencies,
typeCheckTs,
buildTsBinaries,
normalizePlatforms,
} from '../toolchain.js';

Expand All @@ -21,9 +24,11 @@ export default (opts: BaseCommandOptions) => {
command.argument('[directory]', 'Directory of the plugin', '.');
command.option('--generate-only', 'Generate only the proto and mapping files, do not compile the plugin');
command.option('--debug', 'Build the binary with debug information', false);
command.option('--platform [platforms...]', 'Platform-architecture combinations (e.g., darwin-arm64 linux-amd64)', [
HOST_PLATFORM,
]);
command.option(
'--platform [platforms...]',
'Platform-architecture combinations (e.g., darwin-arm64 linux-amd64)',
[],
);
command.option('--all-platforms', 'Build for all supported platforms', false);
command.option('--skip-tools-installation', 'Skip tool installation', false);
command.option(
Expand All @@ -46,29 +51,45 @@ export default (opts: BaseCommandOptions) => {
let platforms: string[] = [];

try {
const language = getLanguage(pluginDir);

// Check and install tools if needed
if (!options.skipToolsInstallation) {
await checkAndInstallTools(options.forceToolsInstallation);
}

// Normalize platform list
platforms = normalizePlatforms(options.platform, options.allPlatforms);

// Start the main build process
spinner.start('Building plugin...');

switch (language) {
case 'ts': {
await installTsDependencies(pluginDir, spinner);
break;
}
}

// Normalize platform list
platforms = normalizePlatforms(options.platform, options.allPlatforms, language);

// Generate proto and mapping files
await generateProtoAndMapping(pluginDir, goModulePath, spinner);

// Generate gRPC code
await generateGRPCCode(pluginDir, spinner);
await generateGRPCCode(pluginDir, spinner, language);

if (!options.generateOnly) {
// Install Go dependencies
await installGoDependencies(pluginDir, spinner);

// Build binaries for all platforms
await buildBinaries(pluginDir, platforms, options.debug, spinner);
switch (language) {
case 'go': {
await installGoDependencies(pluginDir, spinner);
await buildGoBinaries(pluginDir, platforms, options.debug, spinner);
break;
}
case 'ts': {
await typeCheckTs(pluginDir, spinner);
await buildTsBinaries(pluginDir, platforms, options.debug, spinner);
break;
}
}
}

// Calculate and format elapsed time
Expand Down
21 changes: 18 additions & 3 deletions cli/src/commands/router/commands/plugin/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
checkAndInstallTools,
generateGRPCCode,
generateProtoAndMapping,
getLanguage,
installGoDependencies,
installTsDependencies,
} from '../toolchain.js';

export default (opts: BaseCommandOptions) => {
Expand All @@ -36,22 +38,35 @@ export default (opts: BaseCommandOptions) => {
const goModulePath = options.goModulePath;

try {
const language = getLanguage(pluginDir);

// Check and install tools if needed
if (!options.skipToolsInstallation) {
await checkAndInstallTools(options.forceToolsInstallation);
}

switch (language) {
case 'ts': {
await installTsDependencies(pluginDir, spinner);
break;
}
}

// Start the generation process
spinner.start('Generating plugin code...');

// Generate proto and mapping files
await generateProtoAndMapping(pluginDir, goModulePath, spinner);

// Generate gRPC code
await generateGRPCCode(pluginDir, spinner);
await generateGRPCCode(pluginDir, spinner, language);

// Install Go dependencies
await installGoDependencies(pluginDir, spinner);
switch (language) {
case 'go': {
await installGoDependencies(pluginDir, spinner);
break;
}
}

// Calculate and format elapsed time
const endTime = performance.now();
Expand Down
Loading