Typescript:如何根据数据结构生成和打印 AST
Typescript: How to generate and print an AST based on a data structure
我正在开始一个新项目,作为其界面的一部分,我们有一大堆 "tokens",一个具有字符串值的递归对象,如下所示:
const colors = {
accent: '#f90',
primary: {
active: '#fff',
inactive: 'silver'
}
};
我们提供了一个实用程序,用于通过基于字符串的路径使用它们(例如,在这种情况下,primary.active
对应 #fff
)。将所有可能的路径提取到一个数组中很容易,但我们想为这个包的消费者提供更好的自动完成,而不是 'string',这些可能路径的联合或枚举。有人可能有这方面的经验吗?我最初的方法是编写一个简单的脚本,它接受一个数组并使用模板或类似的东西将其打印为联合,但鉴于我们想要更频繁地这样做并且我们的用例会增加复杂性,我认为生成打印 AST 可能是更好的方法。我之前写过 babel 和 recast codemods,我只是在寻找一些关于现有工具集、示例等的指导。我已经快速完成 Google 但找不到任何东西。理想情况下,这些将与我的正常 "watch" 过程一起重新编译,但这是一个延伸目标 ^_^。
我认为您可以结合使用枚举和 interfaces/types:
来完成您想要的
```
export enum COLORS {
accent = '#f90',
primary_active = '#fff',
primary_inactive = 'silver',
}
interface ICOLORS {
[COLORS.accent]: COLORS.accent,
[COLORS.primary_active]: COLORS.primary_active,
[COLORS.primary_inactive]: COLORS.primary_inactive
}
export type COLOR_OPTIONS = keyof ICOLORS;
export type PRIMARY_COLOR_OPTIONS = keyof Pick<ICOLORS, COLORS.primary_active | COLORS.primary_inactive>;
export function setColor (color: PRIMARY_COLOR_OPTIONS): void {}
// elsewhere:
import {COLORS, setColor} from 'somewhere';
setColor(COLORS.primary_inactive); // works
setColor(COLORS.accent); //error
```
您可以提取对象类型并使用编译器创建联合类型API
import * as ts from 'typescript'
import * as fs from 'fs'
var cmd = ts.parseCommandLine(['test.ts']); // replace with target file
// Create the program
let program = ts.createProgram(cmd.fileNames, cmd.options);
type ObjectDictionary = { [key: string]: string | ObjectDictionary}
function extractAllObjects(program: ts.Program, file: ts.SourceFile): ObjectDictionary {
let empty = ()=> {};
// Dummy transformation context
let context: ts.TransformationContext = {
startLexicalEnvironment: empty,
suspendLexicalEnvironment: empty,
resumeLexicalEnvironment: empty,
endLexicalEnvironment: ()=> [],
getCompilerOptions: ()=> program.getCompilerOptions(),
hoistFunctionDeclaration: empty,
hoistVariableDeclaration: empty,
readEmitHelpers: ()=>undefined,
requestEmitHelper: empty,
enableEmitNotification: empty,
enableSubstitution: empty,
isEmitNotificationEnabled: ()=> false,
isSubstitutionEnabled: ()=> false,
onEmitNode: empty,
onSubstituteNode: (hint, node)=>node,
};
let typeChecker = program.getTypeChecker();
function extractObject(node: ts.ObjectLiteralExpression): ObjectDictionary {
var result : ObjectDictionary = {};
for(let propDeclaration of node.properties){
if(!ts.isPropertyAssignment( propDeclaration )) continue;
const propName = propDeclaration.name.getText()
if(!propName) continue;
if(ts.isObjectLiteralExpression(propDeclaration.initializer)) {
result[propName] = extractObject(propDeclaration.initializer);
}else{
result[propName] = propDeclaration.initializer.getFullText()
}
}
return result;
}
let foundVariables: ObjectDictionary = {};
function visit(node: ts.Node, context: ts.TransformationContext): ts.Node {
if(ts.isVariableDeclarationList(node)) {
let triviaWidth = node.getLeadingTriviaWidth()
let sourceText = node.getSourceFile().text;
let trivia = sourceText.substr(node.getFullStart(), triviaWidth);
if(trivia.indexOf("Generate_Union") != -1) // Will generate fro variables with a comment Generate_Union above them
{
for(let declaration of node.declarations) {
if(declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)){
foundVariables[declaration.name.getText()] = extractObject(declaration.initializer)
}
}
}
}
return ts.visitEachChild(node, child => visit(child, context), context);
}
ts.visitEachChild(file, child => visit(child, context), context);
return foundVariables;
}
let result = extractAllObjects(program, program.getSourceFile("test.ts")!); // replace with file name
function generateUnions(dic: ObjectDictionary) {
function toPaths(dic: ObjectDictionary) : string[] {
let result: string[] = []
function extractPath(parent: string, object: ObjectDictionary) {
for (const key of Object.keys(object)) {
let value = object[key];
if(typeof value === "string") {
result.push(parent + key);
}else{
extractPath(key + ".", value);
}
}
}
extractPath("", dic);
return result;
}
return Object.entries(dic)
.map(([name, values])=>
{
let paths = toPaths(values as ObjectDictionary)
.map(ts.createStringLiteral)
.map(ts.createLiteralTypeNode);
let unionType = ts.createUnionTypeNode(paths);
return ts.createTypeAliasDeclaration(undefined, undefined, name + "Paths", undefined, unionType);
})
}
var source = ts.createSourceFile("d.ts", "", ts.ScriptTarget.ES2015);
source = ts.updateSourceFileNode(source, generateUnions(result));
var printer = ts.createPrinter({ });
let r = printer.printFile(source);
fs.writeFileSync("union.ts", r);
我正在开始一个新项目,作为其界面的一部分,我们有一大堆 "tokens",一个具有字符串值的递归对象,如下所示:
const colors = {
accent: '#f90',
primary: {
active: '#fff',
inactive: 'silver'
}
};
我们提供了一个实用程序,用于通过基于字符串的路径使用它们(例如,在这种情况下,primary.active
对应 #fff
)。将所有可能的路径提取到一个数组中很容易,但我们想为这个包的消费者提供更好的自动完成,而不是 'string',这些可能路径的联合或枚举。有人可能有这方面的经验吗?我最初的方法是编写一个简单的脚本,它接受一个数组并使用模板或类似的东西将其打印为联合,但鉴于我们想要更频繁地这样做并且我们的用例会增加复杂性,我认为生成打印 AST 可能是更好的方法。我之前写过 babel 和 recast codemods,我只是在寻找一些关于现有工具集、示例等的指导。我已经快速完成 Google 但找不到任何东西。理想情况下,这些将与我的正常 "watch" 过程一起重新编译,但这是一个延伸目标 ^_^。
我认为您可以结合使用枚举和 interfaces/types:
来完成您想要的```
export enum COLORS {
accent = '#f90',
primary_active = '#fff',
primary_inactive = 'silver',
}
interface ICOLORS {
[COLORS.accent]: COLORS.accent,
[COLORS.primary_active]: COLORS.primary_active,
[COLORS.primary_inactive]: COLORS.primary_inactive
}
export type COLOR_OPTIONS = keyof ICOLORS;
export type PRIMARY_COLOR_OPTIONS = keyof Pick<ICOLORS, COLORS.primary_active | COLORS.primary_inactive>;
export function setColor (color: PRIMARY_COLOR_OPTIONS): void {}
// elsewhere:
import {COLORS, setColor} from 'somewhere';
setColor(COLORS.primary_inactive); // works
setColor(COLORS.accent); //error
```
您可以提取对象类型并使用编译器创建联合类型API
import * as ts from 'typescript'
import * as fs from 'fs'
var cmd = ts.parseCommandLine(['test.ts']); // replace with target file
// Create the program
let program = ts.createProgram(cmd.fileNames, cmd.options);
type ObjectDictionary = { [key: string]: string | ObjectDictionary}
function extractAllObjects(program: ts.Program, file: ts.SourceFile): ObjectDictionary {
let empty = ()=> {};
// Dummy transformation context
let context: ts.TransformationContext = {
startLexicalEnvironment: empty,
suspendLexicalEnvironment: empty,
resumeLexicalEnvironment: empty,
endLexicalEnvironment: ()=> [],
getCompilerOptions: ()=> program.getCompilerOptions(),
hoistFunctionDeclaration: empty,
hoistVariableDeclaration: empty,
readEmitHelpers: ()=>undefined,
requestEmitHelper: empty,
enableEmitNotification: empty,
enableSubstitution: empty,
isEmitNotificationEnabled: ()=> false,
isSubstitutionEnabled: ()=> false,
onEmitNode: empty,
onSubstituteNode: (hint, node)=>node,
};
let typeChecker = program.getTypeChecker();
function extractObject(node: ts.ObjectLiteralExpression): ObjectDictionary {
var result : ObjectDictionary = {};
for(let propDeclaration of node.properties){
if(!ts.isPropertyAssignment( propDeclaration )) continue;
const propName = propDeclaration.name.getText()
if(!propName) continue;
if(ts.isObjectLiteralExpression(propDeclaration.initializer)) {
result[propName] = extractObject(propDeclaration.initializer);
}else{
result[propName] = propDeclaration.initializer.getFullText()
}
}
return result;
}
let foundVariables: ObjectDictionary = {};
function visit(node: ts.Node, context: ts.TransformationContext): ts.Node {
if(ts.isVariableDeclarationList(node)) {
let triviaWidth = node.getLeadingTriviaWidth()
let sourceText = node.getSourceFile().text;
let trivia = sourceText.substr(node.getFullStart(), triviaWidth);
if(trivia.indexOf("Generate_Union") != -1) // Will generate fro variables with a comment Generate_Union above them
{
for(let declaration of node.declarations) {
if(declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)){
foundVariables[declaration.name.getText()] = extractObject(declaration.initializer)
}
}
}
}
return ts.visitEachChild(node, child => visit(child, context), context);
}
ts.visitEachChild(file, child => visit(child, context), context);
return foundVariables;
}
let result = extractAllObjects(program, program.getSourceFile("test.ts")!); // replace with file name
function generateUnions(dic: ObjectDictionary) {
function toPaths(dic: ObjectDictionary) : string[] {
let result: string[] = []
function extractPath(parent: string, object: ObjectDictionary) {
for (const key of Object.keys(object)) {
let value = object[key];
if(typeof value === "string") {
result.push(parent + key);
}else{
extractPath(key + ".", value);
}
}
}
extractPath("", dic);
return result;
}
return Object.entries(dic)
.map(([name, values])=>
{
let paths = toPaths(values as ObjectDictionary)
.map(ts.createStringLiteral)
.map(ts.createLiteralTypeNode);
let unionType = ts.createUnionTypeNode(paths);
return ts.createTypeAliasDeclaration(undefined, undefined, name + "Paths", undefined, unionType);
})
}
var source = ts.createSourceFile("d.ts", "", ts.ScriptTarget.ES2015);
source = ts.updateSourceFileNode(source, generateUnions(result));
var printer = ts.createPrinter({ });
let r = printer.printFile(source);
fs.writeFileSync("union.ts", r);