TypeScript code generation

Original link: https://blog.rxliuli.com/p/b8e8ce8bccff49d191480a40a18a7fc8/

foreword

Code generation is not an unfamiliar concept to many developers. From generating projects with scaffolding (create-react-app), to generating code with IDE, or generating code from backend api schema, it is almost impossible to avoid using it. It can solve a variety of problems

  • Generate projects from the same source to avoid monolithic project structures
  • Reduce writing boilerplate code
  • Avoid duplicating code in multiple places

But it can also do some other interesting things when using TypeScript, including

  • Generate types to improve developer experience, e.g. generate type definitions for env, module css, i18n config
  • Support for files that were not originally supported for import, such as importing it for graphql to generate code changes

Some people might think that code generation needs to deal with ast (ie abstract syntax tree), and dealing with ast is a complex thing, and therefore do not try to do something similar. What I want to say is that the actual structure of ast may indeed be very complex, such as the one obtained by TypeScript’s official parser parsing ts, but its core is quite simple, and this field is just a little bit high. If you choose a proper syntax tree manipulation tool, plus the various code <=> ast visualizers out there, things get a lot easier.

basic

Generating code is basically like putting an elephant in a refrigerator in three steps

  1. get some type of metadata, like get its ast from css
  2. Convert the metadata to get the ast that generates the object code
  3. Convert ast to code

Code generation step.drawio.svg

As the title says, the main target code here is TypeScript. Correspondingly, there are various sources of metadata, from json data, to ast in other languages, to remote interfaces. There are actually no restrictions, as long as you It can be converted to the target ast.

Let’s try the first and simplest one in the future, generating type definitions from module css files

Generate type definitions from css

motivation

Why do this?

When using css modules, we usually use build tools such as rollup/vite/webpack to parse the *.module.css files and make the result in the final bundle as expected. But in the development phase, it doesn’t have many hints, for example, a css class is defined, but when you use it in ts, it doesn’t have any hints. When you delete a css class, there will be no code hints.
For example the following example

 /* App.module.css */ .hide { display : none ; }
 // App.tsximport { useReducer } from "react";import css from "./App.module.css";export function App() { const [hide, toggle] = useReducer((s) => !s, false); return ( <div> <button onClick={toggle}>toggle</button> <p className={hide ? css.hide : ""}>test</p> </div> );} 

If we put an App.module.css.d.ts file next to App.module.css, it will be happy when used in ts

 const css: { hide: string;};export default css;

Of course, this function can also be done in ide, but each ide needs to implement this function again, which is the problem, plug-ins cannot be used across ide, but code hints based on TypeScript can be used across ide, including vscode, jetbrains ide, vim, etc.

Technical selection

As mentioned above, if you want to generate code, you need to get css ast and convert css ast to ts ast, and this needs to choose a suitable parser to parse css to get ast and generate ts ast and convert it into code.

Generate interface basic process from css.drawio.svg

We use the following two libraries

  • css-tree: Parse css code to cssom
  • ast-types: a generic ts/js ast high-level abstraction
  • recast: an ast parser generator based on ast-types

Tip: The ast of the code can be visually inspected at https://astexplorer.net/
1664382002113.png

Parse css

First, parse css to get ast, and filter out all class selector class names from it

 function parse(code: string): string[] { const ast = csstree.parse(code); const r: string[] = []; csstree.walk(ast, (node) => { if (node.type === "ClassSelector") { r.push(node.name); } }); return r;} 

Then convert the list of css class names to ast

 function convert(classes: string[]): n.Program { return b.program([ b.variableDeclaration("const", [ b.variableDeclarator( b.identifier.from({ name: "css", typeAnnotation: b.tsTypeAnnotation( b.tsTypeLiteral( classes.map((s) => b.tsPropertySignature( b.identifier(s), b.tsTypeAnnotation(b.tsStringKeyword()) ) ) ) ), }) ), ]), b.exportDefaultDeclaration(b.identifier("css")), ]);} 

Finally, convert the ast to ts code

 function format(ast: n.ASTNode): string { return prettyPrint(ast).code;}

Combine 3 methods

 export function generate(cssCode: string): string { const classes = parse(cssCode); const ast = convert(classes); return format(ast);}

Do the simplest test

 console.log( generate(`/* App.module.css */.hide { display: none;}`));// 会得到以下代码// const css: {// hide: string// };// export default css; 

Looks like we are done with basic css to dts code generation, but it needs some extra steps if you want to be practical

  1. Better usage encapsulation, such as encapsulation as cli to automatically scan all *.module.css files in the specified directory and generate corresponding dts files, or directly integrate into the process of development tools through plugins, such as vite plugins
  2. Publish as an npm package, or use some form of monorepo for reuse across multiple projects

The following uses the vite plugin as a demonstration

 import { defineConfig, Plugin, ResolvedConfig } from "vite";import react from "@vitejs/plugin-react";import { globby } from "globby";import path from "path";import * as csstree from "css-tree";import { namedTypes as n, builders as b } from "ast-types";import { prettyPrint } from "recast";import fsExtra from "fs-extra";import { watch } from "chokidar";// 上面的代码。。。const { pathExists, readFile, remove, writeFile } = fsExtra;function cssdts(): Plugin { let config: ResolvedConfig; async function generateByPath(item: string) { const cssPath = path.resolve(config.root, item); const code = await readFile(cssPath, "utf-8"); await writeFile(cssPath + ".d.ts", generate(code)); } return { name: "vite-plugin-cssdts", configResolved(_config) { config = _config; }, async buildStart() { const list = await globby("src/**/*.module.css", { cwd: config.root, }); await Promise.all( list.map(async (item) => { const cssPath = path.resolve(config.root, item); await generateByPath(cssPath); }) ); }, configureServer(server) { watch("src/**/*.module.css", { cwd: config.root }) .on("add", generateByPath) .on("change", generateByPath) .on("unlink", async (cssPath) => { if (cssPath.endsWith(".module.css")) { const dtsPath = cssPath + ".d.ts"; if (await pathExists(dtsPath)) { await remove(dtsPath); } } }); }, };}export default defineConfig({ plugins: [react(), cssdts()],}); 

Now, whenever vite is started, it will automatically scan all *.module.css to generate corresponding type definitions, and it will continue to monitor file changes in development mode.

Epilogue

In the next few articles, we will demonstrate the practical use of code generation, implement some simple examples, and also give the existing more complete tools (if any).

  • Generate type definitions from env environment variables
  • Generate type definitions from i18n config
  • Generate code from graphql
  • Generate type definitions from open api schema

This article is reproduced from: https://blog.rxliuli.com/p/b8e8ce8bccff49d191480a40a18a7fc8/
This site is for inclusion only, and the copyright belongs to the original author.