I was able to find a way to get this done so I'm gonna try and explain it so if someone else has to do the same this might save his day.
First, problem with passing ts
files as arguments is they need to be compiled before running. Usually when you create an executable with ts-node
, ts_project
or nodejs_binary
the processing part which you already have is compiled but arguments are not.
So what I needed is something that compile and execute typescript at runtime. Following was the solution I found.
You can require ts-node and register the loader for future requires by
using require('ts-node').register({ /* options */ }). You can also use
file shortcuts - node -r ts-node/register or node -r
ts-node/register/transpile-only - depending on your preferences.
Documentation here
Basically you can do following and import typescript at runtime.
require('ts-node').register({/* options */})
const something = require('some-ts-file`);
So my yaml generator code can use this to import ts files passed as arguments.
First the BUILD.bazel for the rule
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
nodejs_binary(
name = "ys-2-yaml",
data = [
"main.js",
"@npm//yaml",
"@npm//openapi-core",
"@npm//ts-node"
],
entry_point = "main.js",
visibility = ["//visibility:public"],
)
main.js
is the file that is going to do the processing. It needs yaml
library from the npm so is provided and ts-node
to load the typescript files at runtime.
Bazel rule is like following
"""Generate the openapi yml file from the JSON version."""
def _generateYaml(ctx):
inputs = [] + ctx.files.schemas
inputs.extend(ctx.attr.generator[DefaultInfo].data_runfiles.files.to_list())
ctx.actions.run(
inputs = inputs,
outputs = [ctx.outputs.yaml],
arguments = [ctx.outputs.yaml.path, ctx.file.main_file.path],
executable = ctx.executable.generator,
)
ts_2_yaml = rule(
implementation = _generateYaml,
attrs = {
"generator": attr.label(
default = "//build/rules/tsnoderegister:ys-2-yaml",
cfg = "target",
executable = True,
),
"schemas": attr.label_list(default = [], allow_files = True),
"main_file": attr.label(
allow_single_file = True,
mandatory = True,
),
},
outputs = {
"yaml": "openapi.yaml",
},
)
executable(generator) is the nodejs_binary
target from before. rule is expecting two arguments. schemas
which are the schema files in ts code. The reason that this is multiple files is schema is broken in to different objects and stored with each route for readability. So the main schema file import and append everything together. I needed another variable so the rule know which one is the main schema file. So the schema files made available to the executable by passing to inputs
and main ts file is passed as an argument.
Following is the main.js
file.
const fs = require("fs");
const yaml = require("yaml");
require("ts-node").register({
transpileOnly: true,
// insert other options with a boolean flag here
});
const schemaFile = require("../../../" + process.argv[3]);
fs.writeFileSync(process.argv[2], yaml.stringify(schemaFile));
Basically it import the passed typescript file and parse into yaml
and save it to a file. There is a bit of a issue with the path ( hence ../../../
) that I need to do more gracefully.
Finally rule can be used in a package passing schemas as below.
load("//build/rules/tsnoderegister:runtimets.bzl", "ts_2_yaml")
ts_2_yaml(
name = "generate_yaml",
schemas = glob(["src/**/*schema.ts"]),
main_file = "src/api/routes/openapi.schema.ts"
)
Run the target and rule will generate the yaml file
bazel build //services/my-sample-service:generate_yaml
bazel build //services/my-sample-service:generate_yaml
INFO: Analyzed target //services/my-sample-service:generate_yaml (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //services/my-sample-service:generate_yaml up-to-date:
bazel-bin/services/my-sample-service/openapi.yaml
INFO: Elapsed time: 0.053s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
Link to the gihub code of this example