Typescript: как создать и распечатать AST на основе структуры данных - PullRequest
0 голосов
/ 09 ноября 2018

Я начинаю новый проект, и как часть его интерфейса у нас есть целая куча «токенов», рекурсивный объект со строковыми значениями, например:

const colors = {
  accent: '#f90',
  primary: {
    active: '#fff',
    inactive: 'silver'
  }
};

Мы предлагаем утилиту для их использования по строковому пути (например, primary.active для #fff в данном случае). Извлечь все возможные пути в массив достаточно просто, но мы хотели бы предложить автозаполнение для потребителей этого пакета, а не 'string', объединение или перечисление этих возможных путей. Кто-нибудь может иметь опыт с этим? Мой первоначальный подход заключается в написании простого скрипта, который принимает массив и печатает его как объединение с использованием шаблона или чего-то подобного, но, учитывая, что мы хотим делать это чаще, и наши варианты использования будут усложняться, я думаю, генерация и печать AST - возможно лучший подход. Я уже писал babel и переделывал кодовые модули, я просто ищу руководство относительно существующих наборов инструментов, примеров и т. Д. Я сделал быстрый Google, но ничего не смог найти. В идеале они будут перекомпилированы вместе с моим обычным процессом «наблюдения», но это непростая цель ^ _ ^.

Ответы [ 2 ]

0 голосов
/ 16 ноября 2018

Вы можете извлечь тип объекта и создать типы объединения, используя 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);
0 голосов
/ 14 ноября 2018

Я думаю, что вы можете достичь желаемого с помощью комбинации перечислений и интерфейсов / типов:

``` 
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

```
...