ts-morph
ts-morph copied to clipboard
Using ts-morph in transform plugin
Is your feature request related to a problem? Please describe.
I am trying to use ts-morph
to create a custom transformer that will enhance the limited TypeScript reflection system.
The raw TS API is really hard to use without proper docs and tutorials but I've tried your API and it's great and have all I need, works great using a standalone Project API.
In transformer signature I can use createWrappedNode
and pass program.getTypeChecker()
to read all the types info of class properties but I'm not able to manipulate the class node, even simple things like rename throws an error:
A language service is required for this operation. This might occur when manipulating or getting type information from a node that was not added to a Project object and created via createWrappedNode. Please submit a bug report if you don't believe a language service should be required for this operation.
Describe the solution you'd like
It would be nice if some manipulations could work without language service (as it's possible to use ts.updateFooBar
API to do it) as I think it's not possible to get a language service from Program, TransformationContext or SourceFile which are available in transform plugin hooks.
Describe alternatives you've considered
Using the raw compiler API... all I need is a way to pass additional data to the decorator factory call or emit a function call in the end of the file/class node + add import on top of file.
Hi @19majkel94, sorry for the delay in my reply.
In order to implement this properly, I believe a new program, type checker, and language service would need to be created between each AST update. That doesn't sound so performant. Overall, I would highly recommend not using this library in custom transformers.
I'd also recommend not using the program, type checker, or language service in transformation plugins with the vanilla compiler API. For example:
console.log(typeChecker.typeToString(typeChecker.getTypeAtLocation(declaration))); // number
declaration = ts.updateVariableDeclaration(declaration, declaration.name, ts.createTypeReferenceNode("Date", undefined), undefined);
console.log(typeChecker.typeToString(typeChecker.getTypeAtLocation(declaration))); // number
console.log(typeChecker.typeToString(typeChecker.getTypeAtLocation(declaration.type!))); // any
If someone wanted to use this library to make changes for emitting, then a better workflow would be to do the changes prior to entering the compiler API's emit. For example:
- Load a ts-morph project.
- Manipulate the files in the project. <-- Do everything here
- Emit and possibly use other compiler API transforms.
Regarding the language service specifically, a new document registry would need to be created for the emit language service and the source files updated on each transformation. I believe with the current API, that would mean reprinting & reparsing the transformed ASTs between each request to the language server. Anyway, at emit that's just a strange time for the language service to be around. It would be simpler/more performant to do any changes that require the language service before emitting.
Hopefully that makes sense... let me know if you want some clarification on anything.
@dsherret Thanks for your answer 😉
I've managed to overcome this problem by using the raw ts
API to emit new code. I'm just reading the type info and emitting a new decorator, so I don't suffer from the synchronization lost issue.
It's also not so slow using ts-morph
with typeChecker
and createWrappedNode
. Maybe in the future I will rewrite it to the raw ts
API when the PoC become a really functional transform plugin.
I have the same requirements, I generate a complex AST in my plugin, but it's hard to write it using ts.createXX
.
I'm fans of ts-morph's insertStatements('console.log()')
.
Is this still not recommended? I've built a script that does everything I need with ts-morph
and now I would like to use it as a transformation. Even if it's not recommended, can someone tell me how to use it anyways?
Hello
I found a way to use it in transform plugin easily, don't know if it's bad or bad or good but here it is:
export default function(program: ts.Program, pluginOptions: {}) {
const project = new Project();
project.addSourceFilesFromTsConfig("./tsconfig.json");
// Manipulate the files
project.getSourceFiles()[0].insertStatements(0, "console.log('Hello world!');");
return (ctx: ts.TransformationContext) => {
return (sourceFile: ts.SourceFile) => {
// Return source files from Project instead
return project.getSourceFile(sourceFile.fileName)!.compilerNode;
};
};
}
Using the program transformer of ts-patch, this now works:
import type * as ts from 'typescript';
import {CompilerOptions, Project} from "ts-morph";
export default function (program: ts.Program) {
const compilerOptions = program.getCompilerOptions() as CompilerOptions;
const project = new Project({compilerOptions});
program.getSourceFiles()
.forEach(sourceFile => project.createSourceFile(sourceFile.fileName, sourceFile.text, {overwrite: true}));
//perform ts-morph magic
project.createSourceFile('added-by-tsmorph.ts', 'console.log(true);');
return project.getProgram().compilerObject;
}
The big drawback from this approach is that all existing files in the program have to be recompiled (this is done in the ts-morph.Project.createSourceFile
call). This is not very performant, but I am willing to accept this in my project in favor of the much more readable ts-morph code.
@dsherret, do you think it is feasible to add a addSourceFile
function to ts-morph.Project
, which wraps an existing ts.SourceFile
without having to recompile it? I would be happy to help implementing this.
Hello
I found a way to use it in transform plugin easily, don't know if it's bad or bad or good but here it is:
export default function(program: ts.Program, pluginOptions: {}) { const project = new Project(); project.addSourceFilesFromTsConfig("./tsconfig.json"); // Manipulate the files project.getSourceFiles()[0].insertStatements(0, "console.log('Hello world!');"); return (ctx: ts.TransformationContext) => { return (sourceFile: ts.SourceFile) => { // Return source files from Project instead return project.getSourceFile(sourceFile.fileName)!.compilerNode; }; }; }
@Feavy, as far as I can tell you are recompiling all source files from the file system, which ignores changes to the AST already performed by other compiler plugins. You may want to adapt this to use the approach from my answer, which should solve this potential problem.
Hello I found a way to use it in transform plugin easily, don't know if it's bad or bad or good but here it is:
export default function(program: ts.Program, pluginOptions: {}) { const project = new Project(); project.addSourceFilesFromTsConfig("./tsconfig.json"); // Manipulate the files project.getSourceFiles()[0].insertStatements(0, "console.log('Hello world!');"); return (ctx: ts.TransformationContext) => { return (sourceFile: ts.SourceFile) => { // Return source files from Project instead return project.getSourceFile(sourceFile.fileName)!.compilerNode; }; }; }
@Feavy, as far as I can tell you are recompiling all source files from the file system, which ignores changes to the AST already performed by other compiler plugins. You may want to adapt this to use the approach from my answer, which should solve this potential problem.
It type are look like no bad, but when I try to run, it will throw error:
/node_modules/.pnpm/[email protected]/node_modules/typescript/lib/typescript.js:47076
if (symbol.flags & 33554432 /* Transient */)
^
TypeError: Cannot read properties of undefined (reading 'flags')
at getSymbolLinks (/node_modules/.pnpm/[email protected]/node_modules/typescript/lib/typescript.js:47076:18)
at isReferencedAliasDeclaration (/node_modules/.pnpm/[email protected]/node_modules/typescript/lib/typescript.js:84510:24)
at Object.isReferencedAliasDeclaration (/node_modules/.pnpm/[email protected]/node_modules/typescript/lib/typescript.js:84881:67)
at shouldEmitAliasDeclaration (/node_modules/.pnpm/[email protected]/node_modules/typescript/lib/typescript.js:92146:164)
at visitImportSpecifier (/node_modules/.pnpm/[email protected]/node_modules/typescript/lib/typescript.js:91845:34)
at visitArrayWorker (/node_modules/.pnpm/[email protected]/node_modules/typescript/lib/typescript.js:87231:51)
at visitNodes2 (/node_modules/.pnpm/[email protected]/node_modules/typescript/lib/typescript.js:87202:21)
at visitNamedImportBindings (/node_modules/.pnpm/[email protected]/node_modules/typescript/lib/typescript.js:91840:26)
at visitNode (/node_modules/.pnpm/[email protected]/node_modules/typescript/lib/typescript.js:87169:21)
at visitImportClause (/node_modules/.pnpm/[email protected]/node_modules/typescript/lib/typescript.js:91826:29) {
code: 'PLUGIN_ERROR',
plugin: 'typescript',
hook: 'buildStart'
}
Don't know how to resolve it, 😭