Refactoring Compas code-gen: part 2
Table of contents
Recently we started working on refactoring the code generators of Compas.js. Compas.js is a framework based on a structure providing generated routers, validators, typed api clients and more.
Current situation
Compas.js has a main entrypoint provided by the @compas/code-gen
package: App
. The idea behind that name was that you would define your whole application structure through that single App
instance. The App
instance then exposes three methods to generate some output, as mentioned in Part 1 of this series:
App#generate
: main entrypoint for generatingApp#generateTypes
: generate call to combine types of various other generate callsApp#generateOpenApi
: convert Compas.js structure to an OpenAPI 3.x compatible specification
@compas/code-gen/experimental
Our redesign still has a single entrypoint, but now called Generator
instead of App
. We experienced that most of the time you need more than a single generator, for example when importing the OpenAPI spec of a service that you are using. This entrypoint will also have a single generate call Generator#generate
. It combines the options and usecases of the previous three generate calls in to a single method, combining and redesigning the accepted options, which are discussed below.
Using the Generator
will look something like this (minor changes could still be done):
import { Generator } from "@compas/code-gen/experimental";
// The TypeCreators are backwards compatible
import { TypeCreator } from "@compas/code-gen";
const appGenerator = new Generator();
appGenerator.add(/* ... */);
appGenerator.generate({
outputDirectory: "./src/generated",
generators: {
structure: {},
router: {
target: { library: "koa" },
},
},
});
const publicGenerator = appGenerator.selectGroups("public");
const publicFiles = publicGenerator.generate({
generators: {
openApi: {},
}
});
// Do something with the public files
Improving targets
The current situation to define targets accepted by App#generate
looks something like:
type GenerateOptions = {
isBrowser?: boolean;
isNode?: boolean;
isNodeServer?: boolean;
useTypescript?: boolean;
}
As mentioned earlier, I personally have no clue what the difference is between isNode
and isNodeServer
. Other problems with the current setup are that isBrowser
currently should be used for both browser as well as react-native targets. Adding a new option to for example add support for generating Go would result in a new useGolang
option.
In the new setup we use a top-level targetLanguage
option. The output of a single generate call could add imports between its generated files, so these should be using the same language. For generator specific targets, like the generate api clients, we accept a nested target
type GenerateOptions = {
targetLanguage: "js" | "ts";
generators: {
apiClient: {
target: {
library: "axios";
targetRuntime: "node.js" | "browser" | "react-native";
};
};
};
};
This new target definition for in this case api client ensure that we could add support for Fetch, Undici or a completely different client for a different targetLanguage
without having unrelated options top level affecting the output like isNodeServer
would.
A drawback here is that the targetLanguage
and the nested target
definitions have some relation between them. This could lead to a degraded developer experience. It is then more important than ever to provide clear errors when the targetLanguage
and target
are not compatible, nudging the user in either a different target
or a different targetLanguage
.
Scope
The flexibility of the new options give a lot of oppurtinity to directly support new targets like the aforementioned Fetch or Undici based api clients. However, we limit the scope of this refactor to only support the current outputs:
For the all supported options see the initial refactor issue on GitHub or the TypeScript definition on the experimental branch
Subscribe to my newsletter
Read articles from Dirk de Visser directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by