Execute typescript files passed as arguments from a bazel rule

414 views Asked by At

I'm writing a bazel rule in js for a mono repo. The idea behind this is services inside the mono repo(micro services) define there api spec in a open api schema files. In our case these schemas are in ts files since spec is broken as each route and final spec file import each route schema and integrate to the final object.

I want to write a bazel rule in a centralized location so each project can load the rule and call it with it's schema file(s)

generate_yaml_from_ts(
    name = 'generate_yaml',
    schema = "src/api/routes/openapi.schema.ts"
)

I'm not exactly sure whether all schema files ( openapi.schema.ts import other ts files) needed to made available to the rule.

In the rule I have a ts code where I want to refer to the passed schema files and generate yaml file. (Something like follows, ya I know static imports will not work)

import fs from 'fs';

import YAML from 'yaml';

import openapiJson from './src/api/routes/openapi.schema';

fs.writeFileSync(process.argv[2], YAML.stringify(openapiJson));

I create a node executable to run from the code using ts-node but the problem is input that we provide also are ts files and hence needed to be compiled first.

I can think of two possible way to fix this but both I'm not sure exactly how to do.

  1. Compile the schema ts before passing to the rule from service. ( Use a ts_library and pass the out put to the rule ? )
  2. Create the ts executable at rule ( use ts-node in the bzl file ) passing source files concatenating generateYml.ts and schema files passed.

Simple project with bazel setup is available at github

I wrote a rule to take a json file from project and generate yaml build/rules/json2yaml which works.

Effort to do the same with type script it in build/rules/ts-yaml.

My questions are following

  • Can I use ts_library and pass output the bazel rule?
  • Can I pass ts files from service and compile and execute logic from rule sile ( Better ) ?
1

There are 1 answers

0
Susitha Ravinda Senarath On

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