|
| 1 | +#!/usr/bin/env node |
| 2 | +/* |
| 3 | + * Copyright Elasticsearch B.V. and contributors |
| 4 | + * SPDX-License-Identifier: Apache-2.0 |
| 5 | + */ |
| 6 | + |
| 7 | +/** |
| 8 | + * Orchestrates regeneration of the ES and Cloud API bindings against the |
| 9 | + * upstream elastic/elastic-client-generator-js repo. |
| 10 | + * |
| 11 | + * Usage (from package.json scripts): |
| 12 | + * node scripts/codegen.mjs es |
| 13 | + * node scripts/codegen.mjs cloud |
| 14 | + * |
| 15 | + * Environment: |
| 16 | + * CODEGEN_GENERATOR_DIR Reuse an existing generator checkout (absolute |
| 17 | + * path). When unset, the script clones the repo |
| 18 | + * into a fresh temporary directory and installs |
| 19 | + * its dependencies. |
| 20 | + * CODEGEN_GENERATOR_REF Ref/branch/tag to clone (default: main). |
| 21 | + * CODEGEN_ES_VERSION Elasticsearch schema version (default: main). |
| 22 | + * |
| 23 | + * Design rationale: |
| 24 | + * The upstream generator's npm scripts (`npm run zod`, `npm run cli-es`, |
| 25 | + * `npm run cli-cloud`) share a single `output/` directory that each run |
| 26 | + * wipes via `npm run clean`. This script runs them sequentially and copies |
| 27 | + * the relevant files out of `output/` between runs, mapping them onto the |
| 28 | + * repo layout at `src/es/apis/`, `src/es/apis/schemas/` and |
| 29 | + * `src/cloud/apis/`. |
| 30 | + */ |
| 31 | + |
| 32 | +import { spawnSync } from 'node:child_process' |
| 33 | +import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs' |
| 34 | +import { tmpdir } from 'node:os' |
| 35 | +import { dirname, join, resolve } from 'node:path' |
| 36 | +import { fileURLToPath } from 'node:url' |
| 37 | + |
| 38 | +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') |
| 39 | +const GENERATOR_REPO = 'https://github.com/elastic/elastic-client-generator-js.git' |
| 40 | + |
| 41 | +function run (command, args, opts = {}) { |
| 42 | + const result = spawnSync(command, args, { stdio: 'inherit', shell: false, ...opts }) |
| 43 | + if (result.status !== 0) { |
| 44 | + const cwd = opts.cwd ?? process.cwd() |
| 45 | + throw new Error(`Command failed (${command} ${args.join(' ')}) in ${cwd}: exit ${result.status ?? 'signal'}`) |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +/** |
| 50 | + * Ensure a checkout of the generator exists and has its dependencies installed. |
| 51 | + * Returns an absolute path to it. When CODEGEN_GENERATOR_DIR is set the caller |
| 52 | + * is responsible for the checkout; this function only verifies it exists and |
| 53 | + * skips install (the caller should have installed). Otherwise a fresh temp |
| 54 | + * clone is produced and its dependencies installed via npm. |
| 55 | + */ |
| 56 | +function ensureGenerator () { |
| 57 | + const existing = process.env.CODEGEN_GENERATOR_DIR |
| 58 | + if (existing != null && existing.length > 0) { |
| 59 | + if (!existsSync(existing)) { |
| 60 | + throw new Error(`CODEGEN_GENERATOR_DIR="${existing}" does not exist`) |
| 61 | + } |
| 62 | + console.log(`[codegen] Using existing generator checkout: ${existing}`) |
| 63 | + return existing |
| 64 | + } |
| 65 | + |
| 66 | + const ref = process.env.CODEGEN_GENERATOR_REF ?? 'main' |
| 67 | + const dir = mkdtempSync(join(tmpdir(), 'elastic-client-generator-')) |
| 68 | + console.log(`[codegen] Cloning ${GENERATOR_REPO} @ ${ref} -> ${dir}`) |
| 69 | + run('git', ['clone', '--depth', '1', '--branch', ref, GENERATOR_REPO, dir]) |
| 70 | + console.log('[codegen] Installing generator dependencies (npm install)') |
| 71 | + run('npm', ['install', '--no-audit', '--no-fund'], { cwd: dir, shell: process.platform === 'win32' }) |
| 72 | + return dir |
| 73 | +} |
| 74 | + |
| 75 | +/** Remove every file in `dir` matching `predicate`. Non-recursive. */ |
| 76 | +function clearDir (dir, predicate = () => true) { |
| 77 | + if (!existsSync(dir)) { |
| 78 | + mkdirSync(dir, { recursive: true }) |
| 79 | + return |
| 80 | + } |
| 81 | + for (const entry of readdirSync(dir)) { |
| 82 | + if (!predicate(entry)) continue |
| 83 | + const full = join(dir, entry) |
| 84 | + if (statSync(full).isDirectory()) continue // leave sub-directories alone |
| 85 | + rmSync(full, { force: true }) |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +/** Copy every *.ts file from `src` (non-recursive) into `dest`. */ |
| 90 | +function copyTsFiles (src, dest) { |
| 91 | + if (!existsSync(src)) throw new Error(`Expected generator output at ${src}`) |
| 92 | + mkdirSync(dest, { recursive: true }) |
| 93 | + for (const entry of readdirSync(src)) { |
| 94 | + if (!entry.endsWith('.ts')) continue |
| 95 | + cpSync(join(src, entry), join(dest, entry)) |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +function generateEs (generatorDir) { |
| 100 | + const version = process.env.CODEGEN_ES_VERSION ?? 'main' |
| 101 | + const output = join(generatorDir, 'output') |
| 102 | + |
| 103 | + console.log(`[codegen] Step 1/2: Zod schemas (version: ${version})`) |
| 104 | + run('npm', ['run', 'zod', '--', '--version', version], { cwd: generatorDir, shell: process.platform === 'win32' }) |
| 105 | + const schemasDest = join(REPO_ROOT, 'src', 'es', 'apis', 'schemas') |
| 106 | + clearDir(schemasDest, (entry) => entry.endsWith('.ts')) |
| 107 | + copyTsFiles(output, schemasDest) |
| 108 | + console.log(`[codegen] wrote schemas -> ${schemasDest}`) |
| 109 | + |
| 110 | + console.log(`[codegen] Step 2/2: ES API namespace files (version: ${version})`) |
| 111 | + run('npm', ['run', 'cli-es', '--', '--version', version], { cwd: generatorDir, shell: process.platform === 'win32' }) |
| 112 | + const apisDest = join(REPO_ROOT, 'src', 'es', 'apis') |
| 113 | + clearDir(apisDest, (entry) => entry.endsWith('.ts')) |
| 114 | + copyTsFiles(join(output, 'es', 'apis'), apisDest) |
| 115 | + // Generator emits the barrel as `output/es/index.ts`; the repo expects it |
| 116 | + // as `src/es/apis.ts` next to the `apis/` directory. |
| 117 | + const barrelSrc = join(output, 'es', 'index.ts') |
| 118 | + const barrelDest = join(REPO_ROOT, 'src', 'es', 'apis.ts') |
| 119 | + if (!existsSync(barrelSrc)) throw new Error(`Missing barrel: ${barrelSrc}`) |
| 120 | + cpSync(barrelSrc, barrelDest) |
| 121 | + console.log(`[codegen] wrote APIs -> ${apisDest}`) |
| 122 | + console.log(`[codegen] wrote barrel -> ${barrelDest}`) |
| 123 | +} |
| 124 | + |
| 125 | +function generateCloud (generatorDir) { |
| 126 | + const output = join(generatorDir, 'output') |
| 127 | + |
| 128 | + console.log('[codegen] Cloud API namespace files') |
| 129 | + // `npm run cli-cloud` does not call clean itself, so wipe any leftover |
| 130 | + // output from a previous step before running the generator. |
| 131 | + rmSync(output, { recursive: true, force: true }) |
| 132 | + run('npm', ['run', 'cli-cloud'], { cwd: generatorDir, shell: process.platform === 'win32' }) |
| 133 | + const apisDest = join(REPO_ROOT, 'src', 'cloud', 'apis') |
| 134 | + clearDir(apisDest, (entry) => entry.endsWith('.ts')) |
| 135 | + copyTsFiles(join(output, 'cloud', 'apis'), apisDest) |
| 136 | + const barrelSrc = join(output, 'cloud', 'apis.ts') |
| 137 | + const barrelDest = join(REPO_ROOT, 'src', 'cloud', 'apis.ts') |
| 138 | + if (!existsSync(barrelSrc)) throw new Error(`Missing barrel: ${barrelSrc}`) |
| 139 | + cpSync(barrelSrc, barrelDest) |
| 140 | + console.log(`[codegen] wrote APIs -> ${apisDest}`) |
| 141 | + console.log(`[codegen] wrote barrel -> ${barrelDest}`) |
| 142 | +} |
| 143 | + |
| 144 | +const target = process.argv[2] |
| 145 | +if (target !== 'es' && target !== 'cloud') { |
| 146 | + console.error('Usage: node scripts/codegen.mjs <es|cloud>') |
| 147 | + process.exit(1) |
| 148 | +} |
| 149 | + |
| 150 | +try { |
| 151 | + const generatorDir = ensureGenerator() |
| 152 | + if (target === 'es') generateEs(generatorDir) |
| 153 | + else generateCloud(generatorDir) |
| 154 | + console.log(`[codegen] Done (${target}).`) |
| 155 | +} catch (err) { |
| 156 | + console.error(`[codegen] Failed: ${err instanceof Error ? err.message : String(err)}`) |
| 157 | + process.exit(1) |
| 158 | +} |
0 commit comments