-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding import declarations using the Compiler API #18369
Comments
Are you doing this in a |
Ah good question. Currently traversing the AST in a before transform.
…On Tue, Sep 12, 2017 at 1:17 AM Wesley Wigham ***@***.***> wrote:
Are you doing this in a before or an after transform?
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#18369 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAKZO4bWC5ANzqGYXT1vLEFbfPpdsuyQks5shXWpgaJpZM4PSU87>
.
|
I think the changes made in #18017 should have got you covered, then - are you still broken on |
Awesome! I'll give it a shot. At first glance though, it seems like this PR addresses updating an existing import, it doesn't test adding an import that wasn't previously there. Is adding an import addressed in this PR? I think I basically need a way to attach a new import onto the P.S. Thanks a lot for exposing the compiler API. This is the last hurdle to having only 1 compiler step (no babel!). |
@MatthewMuller You should just be able to use |
@weswigham, thanks for pointing that out. I wasn't sure if that would cause the whole file to be re-parsed or not. I just tried implementing your suggestion but I got a bit stuck: const src = (node as ts.Node) as ts.SourceFile
const imp = "var style = require('Style');\n"
const range = ts.createTextChangeRange(
ts.createTextSpanFromBounds(0, imp.length),
imp.length + src.text.length
)
const updated = ts.updateSourceFile(src, imp + src.text, range)
return (updated as ts.Node) as T Results in a compiler error:
I'm sure I'm doing something wrong here with the range but it's not clear to me what that could be. Any pointers would be greatly appreciated! EDIT: Okay! I was able to get this working with: const src = (node as ts.Node) as ts.SourceFile
const imp = "var style = require('styles');\n"
const updated = ts.createSourceFile(
src.fileName,
imp + src.text,
src.languageVersion
)
return (updated as ts.Node) as T I also found a way to get around the issue above. However, with both create and update the |
@matthewmueller If you want to write it as a transformer (and not string manipulations), you should write it like so to add const file = node as SourceFile;
updateSourceFileNode(file, [createImportEqualsDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
"style",
createExternalModuleReference(createLiteral("styles"))
), ...file.statements]); or like so to add const file = node as SourceFile;
updateSourceFileNode(file, [createVariableStatement(
/*modifiers*/ undefined,
createVariableDeclarationList([createVariableDeclaration("style", /*type*/ undefined, createCall(createIdentifier("require"), [], [createLiteral("styles")]))])
), ...file.statements]); Reparsing an entire file is incredibly wasteful (and will likely cause you to lose accurate sourcemaps and other things, like prior AST modifications 😉 ). |
Soooooo good! I had no idea about the Interestingly, it doesn't seem possible to do: const update: ts.NodeArray<ts.Statement> = [].concat(file.statements) Thanks for all your help! I'll drop a link here once the repo is public in case anyone else wants another example of modifying typescript's AST. |
Ahh one more thing. How can we indicate that it should be using the If the original file is: import sandwich from "sandwich";
console.log(sandwich);
console.log(salami) And I want to add "use strict";
exports.__esModule = true;
var salami = require("salami");
var sandwich_1 = require("sandwich");
console.log(sandwich_1["default"]);
console.log(salami); You'll notice that I've put together a self-contained example runnable via import * as ts from 'typescript'
const transform = {
[ts.SyntaxKind.SourceFile]: (node: any) => {
const file = (node as ts.Node) as ts.SourceFile
const update = ts.updateSourceFileNode(file, [
ts.createImportDeclaration(
undefined,
undefined,
ts.createImportClause(
ts.createIdentifier('salami'),
undefined
// ts.createNamespaceImport(ts.createIdentifier('salami'))
),
ts.createLiteral('salami')
),
...file.statements
])
return update as ts.Node
}
}
console.log(
ts.transpileModule(
'import sandwich from "sandwich"; console.log(sandwich); console.log(salami)',
{
reportDiagnostics: true,
transformers: {
before: [visitor(transform)]
},
compilerOptions: {
noEmitOnError: true,
jsx: ts.JsxEmit.React,
module: ts.ModuleKind.CommonJS,
// module: ts.ModuleKind.ESNext,
importHelpers: true,
jsxFactory: 'h'
}
}
)
)
function visitor(transform: any) {
return (context: ts.TransformationContext) => {
return (sourceFile: ts.SourceFile): ts.SourceFile => {
return visit(sourceFile)
function visit<T extends ts.Node>(node: T): T {
const t = transform[node.kind]
if (!t) {
return ts.visitEachChild(node, visit, context)
}
// enter a node
if (typeof t === 'object' && t.enter) {
const tr = t.enter(node, context)
if (tr) node = tr as T
}
// if it's a function, treat it
// the same way as t.enter
if (typeof t === 'function') {
const tr = t(node, context)
if (tr) node = tr as T
}
// visit each child
let exitNode = ts.visitEachChild(node, visit, context)
// if we have an exit call function
if (typeof t === 'object' && t.exit) {
const tr = t.exit(exitNode, context)
if (tr) exitNode = tr as T
}
return exitNode
}
}
}
} Any assistance would be greatly appreciated! I think this is the last hurdle :-) |
@weswigham sorry to be a bother, but do you mind taking a look at this if you have a moment? This is the last piece of the puzzle. I'm just a bit stuck with why my newly created import doesn't get compiled to:
|
Hm. I'll take a look; but this should also give you the effective output you want, as a workaround in the meantime: const file = (node as ts.Node) as ts.SourceFile;
const update = ts.updateSourceFileNode(file, [
ts.createImportDeclaration(
undefined,
undefined,
ts.createImportClause(
undefined,
ts.createNamedImports([ts.createImportSpecifier(ts.createIdentifier("default"), ts.createIdentifier("salami"))])
),
ts.createLiteral('salami')
),
...file.statements
]); |
OH, wait, you're asking why the references don't get updated! That's on you and your transform, sorry! Since your new nodes aren't bound to the same reference in the binder phase (as transforms happen after binding), TS has no idea that the If this is too cumbersome for your use-case; I'd suggest opening an issue asking for a prebind transform phase, where you could add new nodes which can be bound/typechecked (with the downside being that there is no symbol/type/reference information ready). |
Ahh that makes sense now. I thought that would be happening in between the I'll give it a go manually and open another issue if it's quite difficult. I'd imagine it's not so bad if I change the I really appreciate you taking the time to help me out, thanks! |
Alrighty, in case this helps anyone else out. Here's how you'd support different module options. {
[ts.SyntaxKind.SourceFile]: (node, context) => {
if (!context) return
const opts = context.getCompilerOptions()
const { module } = opts
// const StyledTSX = require('styled-tsx')
if (module === ts.ModuleKind.CommonJS) {
const file = (node as ts.Node) as ts.SourceFile
const update = ts.updateSourceFileNode(file, [
ts.createVariableStatement(
/*modifiers*/ undefined,
ts.createVariableDeclarationList([
ts.createVariableDeclaration(
importName,
/*type*/ undefined,
ts.createPropertyAccess(
ts.createCall(
ts.createIdentifier('require'),
[],
[ts.createLiteral(importFrom)]
),
ts.createIdentifier('default')
)
)
])
),
...file.statements
])
return (update as ts.Node) as T
}
// import StyledTSX from 'styled-tsx'
if (module === ts.ModuleKind.ES2015 || module === ts.ModuleKind.ESNext) {
const file = (node as ts.Node) as ts.SourceFile
const update = ts.updateSourceFileNode(file, [
ts.createImportDeclaration(
/* decorators */ undefined,
/* modifiers */ undefined,
ts.createImportClause(ts.createIdentifier(importName), undefined),
ts.createLiteral(importFrom)
),
...file.statements
])
return (update as ts.Node) as T
}
return node
}
} Doesn't yet cover AMD/UMD modules though. |
Right now it doesn't seem possible to add an
ImportDeclaration
programmatically via the Compiler API without updating the entire AST. My use-case is to be able to create https://github.com/zeit/styled-jsx with the TS Compiler over babel. When it detects a<style>
JSXElement, it will appendimport JSXStyle from 'styled-jsx
to the top of the code. I'd like to be able to do this too.I've looked at how Typescript adds the
tslib
and it seems like it uses an internalimports
key on theSourceFile
node. Would it be possible to give us access to that?TypeScript Version: Version 2.4.1
Code
Sorry this isn't entirely self-contained, but the original is about 100 LOC. This should give you the gist, and it's mostly from the
tslib
code in the compiler.Expected behavior:
Get the result text back with an import statement at the top of the source file
Actual behavior:
No changes
The text was updated successfully, but these errors were encountered: