Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 4 additions & 1 deletion web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ node_modules
.plans/
optimization_results/
optimizations/
filestorage/
filestorage/

# SDK output is generated on `pnpm install` (see packages/sdk/generateAll.ts).
packages/sdk/generated/
3 changes: 2 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"description": "A monorepo comprising the packages for nemo studio.",
"scripts": {
"preinstall": "npx only-allow pnpm",
"postinstall": "pnpm --filter @nemo/sdk gen:all",
"test": "pnpm run -r test",
"test:e2e": "pnpm run -r test:e2e",
"build": "pnpm run -r build",
Expand All @@ -14,7 +15,7 @@
"format": "prettier --check .",
"format:fix": "prettier --write .",
"check": "pnpm run -r check",
"gen": "pnpm --filter @nemo/sdk gen:all",
"gen": "pnpm --filter @nemo/sdk gen:all-force",
"gen:check": "pnpm tsx packages/scripts/src/check-generated-files.ts",
"deps:studio": "pnpm tsx packages/scripts/src/deps-check.ts packages/studio",
"deps:common": "pnpm tsx packages/scripts/src/deps-check.ts packages/common",
Expand Down
101 changes: 95 additions & 6 deletions web/packages/sdk/generateAll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@
// SPDX-License-Identifier: Apache-2.0

/**
* This script generates types for all OpenAPI specs in the NeMo Platform repository.
* It uses concurrently for parallel execution while maintaining a clean structure.
* Generates types for all OpenAPI specs in the NeMo Platform repository.
*
* The generated tree under `./generated/` is gitignored. To keep `pnpm install`
* cheap, this script writes a content-hash sentinel after a successful run and
* skips regeneration when the inputs haven't changed. Pass `--force` to bypass.
*/

import crypto from 'crypto';
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { serviceConfigs } from './orval/constants';

const FORCE = process.argv.includes('--force');
const GENERATED_DIR = path.join(__dirname, 'generated');
const HASH_FILE = path.join(GENERATED_DIR, '.input-hash');
const ORVAL_DIR = path.join(__dirname, 'orval');

const services = Object.keys(serviceConfigs) as Array<keyof typeof serviceConfigs>;

interface GenerationConfig {
Expand All @@ -31,13 +42,90 @@ const generateCommands = (config: GenerationConfig) => {
return baseCommand;
};

const readOrvalVersion = (): string => {
try {
const orvalPkg = JSON.parse(
fs.readFileSync(path.join(__dirname, 'node_modules', 'orval', 'package.json'), 'utf8')
);
return String(orvalPkg.version ?? 'unknown');
} catch {
return 'unknown';
}
};

/**
* Hash inputs to generation: all spec YAMLs, the orval version, and the
* generator source files. Any of these changing invalidates the cache.
*/
const computeInputHash = (): string => {
const hash = crypto.createHash('sha256');

for (const [service, config] of Object.entries(serviceConfigs)) {
hash.update(`service:${service}\n`);
if (config.url.startsWith('http')) {
// Remote specs aren't cacheable here; including the URL still lets the
// hash invalidate when the URL itself changes.
hash.update(`url:${config.url}\n`);
continue;
}
const filePath = path.resolve(ORVAL_DIR, config.url);
hash.update(fs.readFileSync(filePath));
hash.update('\n');
}

hash.update(`orval:${readOrvalVersion()}\n`);

const generatorSources = [
path.join(ORVAL_DIR, 'generate.ts'),
path.join(ORVAL_DIR, 'constants.ts'),
path.join(ORVAL_DIR, 'generateCustomFetcher.ts'),
path.join(__dirname, 'generateAll.ts'),
];
for (const file of generatorSources) {
if (fs.existsSync(file)) {
hash.update(fs.readFileSync(file));
hash.update('\n');
}
}

return hash.digest('hex');
};

/**
* The cache is valid only when (1) the hash file exists and matches the
* current input hash, and (2) the generated tree actually has content beyond
* the sentinel — protects against partially-deleted output.
*/
const isCacheValid = (currentHash: string): boolean => {
if (!fs.existsSync(HASH_FILE)) return false;
if (!fs.existsSync(GENERATED_DIR)) return false;
const entries = fs.readdirSync(GENERATED_DIR).filter((name) => name !== '.input-hash');
if (entries.length === 0) return false;
const stored = fs.readFileSync(HASH_FILE, 'utf8').trim();
return stored === currentHash;
};

const writeHash = (hash: string) => {
fs.mkdirSync(GENERATED_DIR, { recursive: true });
fs.writeFileSync(HASH_FILE, hash);
};

const main = async () => {
console.log('🚀 Starting parallel type generation for all services...\n');
const currentHash = computeInputHash();

// Build the concurrently command with all generation commands
const commands = generationConfigs.map(generateCommands);
if (!FORCE && isCacheValid(currentHash)) {
console.log('✓ SDK is up to date (input hash matches). Skipping generation.');
console.log(' Pass --force to regenerate anyway.');
return;
}

// Create the concurrently command with names and colors
if (FORCE) {
console.log('🔁 --force passed; regenerating regardless of input hash.\n');
} else {
console.log('🚀 Inputs changed (or no cached hash). Regenerating SDK...\n');
}

const commands = generationConfigs.map(generateCommands);
const serviceNames = generationConfigs.map((config) => config.service);
const colors = ['red', 'blue', 'green', 'yellow', 'magenta', 'cyan', 'purple', 'white', 'gray'];

Expand All @@ -59,6 +147,7 @@ const main = async () => {

try {
execSync(concurrentlyCommand, { stdio: 'inherit' });
writeHash(currentHash);
console.log('\n🎉 All type generation completed successfully!');
} catch {
console.error('\n💥 Some type generation failed. Check the output above for details.');
Expand Down
Loading
Loading