diff --git a/MODULE.bazel b/MODULE.bazel index deedabd2ee..765c904a41 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -153,6 +153,15 @@ use_repo(playwright, "playwright_browsers") node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node") node.toolchain(node_version = "22.11.0") # LTS 'Jod' +# Pin pnpm v9 to match the lockfile format (rules_js 2.9.2 defaults to pnpm v8 +# which cannot read lockfileVersion 9 and triggers a non-hermetic regeneration). +pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm", dev_dependency = True) +pnpm.pnpm( + pnpm_version = "9.15.9", + pnpm_version_integrity = "sha512-aARhQYk8ZvrQHAeSMRKOmvuJ74fiaR1p5NQO7iKJiClf1GghgbrlW1hBjDolO95lpQXsfF+UA+zlzDzTfc8lMQ==", +) +use_repo(pnpm, "pnpm") + # Workspace-level npm packages (pnpm workspace) # All JS/TS projects share a single lockfile and node_modules npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm", dev_dependency = True) @@ -207,6 +216,7 @@ npm.npm_translate_lock( }, # Auto-regenerate pnpm-lock.yaml when package.json changes update_pnpm_lock = True, + use_pnpm = "@pnpm//:package/bin/pnpm.cjs", ) use_repo(npm, "npm_ducktape") diff --git a/props/backend/BUILD.bazel b/props/backend/BUILD.bazel index 0a2a98abfc..ef892b17b2 100644 --- a/props/backend/BUILD.bazel +++ b/props/backend/BUILD.bazel @@ -198,14 +198,6 @@ py_binary( deps = [":export_schema"], ) -genrule( - name = "openapi_schema", - outs = ["openapi.json"], - cmd = "$(location :export_schema_bin) > $@", - tools = [":export_schema_bin"], - visibility = ["//props/frontend:__pkg__"], -) - # ============================================================================= # Linting # ============================================================================= diff --git a/props/frontend/BUILD.bazel b/props/frontend/BUILD.bazel index 7887f24c87..54424a2b4c 100644 --- a/props/frontend/BUILD.bazel +++ b/props/frontend/BUILD.bazel @@ -64,29 +64,6 @@ js_binary( no_copy_to_bin = ["//props/backend:backend_cli"], ) -# ============================================================ -# OpenAPI TypeScript schema generation (binary + input only; -# the js_run_binary lives in //props/frontend/src/lib) -# ============================================================ - -js_binary( - name = "generate_schema_bin", - data = [ - "generate-schema.mjs", - ":node_modules", - ], - entry_point = "generate-schema.mjs", - visibility = ["//props/frontend/src/lib:__pkg__"], -) - -genrule( - name = "openapi_schema_local", - srcs = ["//props/backend:openapi_schema"], - outs = ["openapi.json"], - cmd = "cp $< $@", - visibility = ["//props/frontend/src/lib:__pkg__"], -) - # ============================================================ # Production bundle # ============================================================ diff --git a/props/frontend/generate-schema.mjs b/props/frontend/generate-schema.mjs deleted file mode 100644 index 8d82c4edd1..0000000000 --- a/props/frontend/generate-schema.mjs +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env node -/** - * Wrapper script to run openapi-typescript. - * Usage: node generate-schema.mjs - * - * Paths are used as-is (already resolved by Bazel). - */ -import openapiTS, { astToString } from 'openapi-typescript'; -import fs from 'fs/promises'; -import { dirname } from 'path'; - -const args = process.argv.slice(2); - -if (args.length < 2) { - console.error('Usage: generate-schema.mjs '); - process.exit(1); -} - -const [inputFile, outputFile] = args; - -async function main() { - // Read the OpenAPI schema - const schemaJson = JSON.parse(await fs.readFile(inputFile, 'utf-8')); - - // Generate TypeScript types - returns an AST, use astToString to convert - const ast = await openapiTS(schemaJson); - const output = astToString(ast); - - // Write output - await fs.mkdir(dirname(outputFile), { recursive: true }); - await fs.writeFile(outputFile, output); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/props/frontend/src/lib/BUILD.bazel b/props/frontend/src/lib/BUILD.bazel index ab95a181c8..1604871326 100644 --- a/props/frontend/src/lib/BUILD.bazel +++ b/props/frontend/src/lib/BUILD.bazel @@ -1,27 +1,14 @@ -load("@aspect_rules_js//js:defs.bzl", "js_library", "js_run_binary") +load("@aspect_rules_js//js:defs.bzl", "js_library") +load("//tools/js:openapi.bzl", "js_openapi_schema") package(default_visibility = [ "//props/frontend:__pkg__", "//props/frontend:__subpackages__", ]) -# OpenAPI TypeScript schema generation -js_run_binary( - name = "_generate_schema_run", - srcs = ["//props/frontend:openapi_schema_local"], - outs = ["api/schema.d.ts"], - args = [ - "openapi.json", - "src/lib/api/schema.d.ts", - ], - chdir = "props/frontend", - tool = "//props/frontend:generate_schema_bin", -) - -js_library( +js_openapi_schema( name = "schema", - srcs = [":_generate_schema_run"], - tags = ["no-lint"], + generator = "//props/backend:export_schema_bin", ) # Standalone utilities (no local deps) diff --git a/tools/js/BUILD.bazel b/tools/js/BUILD.bazel new file mode 100644 index 0000000000..a4f9dd4e8d --- /dev/null +++ b/tools/js/BUILD.bazel @@ -0,0 +1 @@ +# Shared JavaScript/TypeScript Bazel macros. diff --git a/tools/js/openapi.bzl b/tools/js/openapi.bzl new file mode 100644 index 0000000000..cc1845649b --- /dev/null +++ b/tools/js/openapi.bzl @@ -0,0 +1,60 @@ +"""Macro: js_openapi_schema — generate TypeScript types from an OpenAPI-emitting binary.""" + +load("@aspect_rules_js//js:defs.bzl", "js_library", "js_run_binary") +load("@npm_ducktape//:openapi-typescript/package_json.bzl", openapi_ts_bin = "bin") + +def js_openapi_schema(name, generator, out = "api/schema.d.ts", visibility = None): + """Generate a js_library with TypeScript type definitions from an OpenAPI schema. + + Runs *generator* (a binary that prints an OpenAPI JSON schema to stdout), + then pipes the result through openapi-typescript to produce a ``.d.ts`` + file, and wraps that in a ``js_library`` target. + + Private intermediate targets are prefixed with ``__`` to avoid + collisions when the macro is called more than once in a package. + + Args: + name: Name of the output ``js_library`` target. + generator: Label of the executable that writes OpenAPI JSON to stdout. + out: Package-relative path for the generated ``.d.ts`` file. + Defaults to ``api/schema.d.ts``. + visibility: Visibility of the output ``js_library``. + + Example: + js_openapi_schema( + name = "schema", + generator = "//my/backend:export_schema_bin", + ) + """ + json_out = "_" + name + "_openapi.json" + + openapi_ts_bin.openapi_typescript_binary( + name = "_" + name + "_openapi_ts_bin", + ) + + native.genrule( + name = "_" + name + "_openapi_json", + outs = [json_out], + cmd = "$(location {}) > $@".format(generator), + tools = [generator], + ) + + js_run_binary( + name = "_" + name + "_generate", + srcs = [":_" + name + "_openapi_json"], + outs = [out], + args = [ + json_out, + "-o", + out, + ], + chdir = native.package_name(), + tool = ":_" + name + "_openapi_ts_bin", + ) + + js_library( + name = name, + srcs = [":_" + name + "_generate"], + tags = ["no-lint"], + visibility = visibility, + )