npm install tscripter --save
tsconfig.json
setup, you can simply add it as a file, or include it as an ambient reference in your source.{
files: ['./node_modules/tscripter/index.d.ts', ...]
}
/// <reference path="./node_modules/tscripter/index.d.ts" />
var analyzer = require('tscripter').analyzer; Object.keys(analyzer);
To get started analyzing some existing code, we'll need to create 'host' for our language services and describe the entry point files.
host = new analyzer.AnalyzerHost(["./node_modules/tscripter/index.d.ts"]); Object.keys(host.__proto__);
Now, we'll create an analyzer for our file and call it's analyze method to generate our AST.
analyzer = host.getAnalyzer("./node_modules/tscripter/index.d.ts"); indexSource = analyzer.analyze(); indexSource.toJSON();
Check it out, we've got our first CodeNode
representing a Module
. We also have access to its underlying ts.Node.
moduleElement = indexSource.elements[0]; [moduleElement.constructor.name, moduleElement.node.flags];
But of course, the structured details are more useful. Like it's name and modifiers...
[moduleElement.name.token, moduleElement.modifiers];
Of course, the most common task is to render a string from an element. Let's take a peak!
console.log(moduleElement.toString());
We can access the child elements of the module, too. But wait, it's empty?
moduleElement.elements
That's because tscripter
did not analyze the body of the module yet. By default, any child blocks do not get analyed recursively themselves to save heavy object instantiation in large files. We can programmatically ask the analyzer to fill out the child elements of the block.
analyzer.analyzeBody(moduleElement).elements.length;
Or, if you're certain you'll need the entire file analyzed or want to simplify your process, you can recursively analyze everything from the git-go.
analyzer = host.getAnalyzer(indexSource.fileName, true); indexSource = analyzer.analyze(); indexSource.elements[0].elements.length;
Great, but reading in some code isn't that impressive. We can do that with fs.readFileSync
. What about modifying the structure of the code?
moduleElement.elements[1].toString();
requireAnalyzer = moduleElement.elements[1]; Object.keys(requireAnalyzer)
Object.keys(requireAnalyzer.importedAs);
requireAnalyzer.importedAs.token = "hacked"; requireAnalyzer.toString();
What gives? Didn't we change identifier of the require, why is it the same string as before? That's because tscripter
caches the old rendering result toString()
into the attribute text
. After we make changes, we should call markDirty
to clear the text attribute and allow toString() to build the correct result.
console.log("text was", requireAnalyzer.text); requireAnalyzer.markDirty(); requireAnalyzer.toString();
WHAT? This isn't working! The trick is that markDirty only applies to the exact CodeNode being applied by default, not the parent or the children. Beacuse we modified the child Identifier
object, the importedAs.token, we need to mark it dirty as well.
requireAnalyzer.importedAs.markDirty(); requireAnalyzer.markDirty(); requireAnalyzer.toString();
That's alot of effort! But there's a simpler way -- you can provide true
as an argument to markDirty to recursively clear the rendered text cache.
requireAnalyzer.importedAs.token = "withrecursive"; requireAnalyzer.markDirty(true); requireAnalyzer.toString();
As mentioned before, however, the parent still has cached its previous rendering as we can see below.
console.log(moduleElement.toString());
We'll need to mark this parent as dirty in order for it to render the new child view as well.
moduleElement.markDirty(); console.log(moduleElement.toString());
Managing the dirty states can be tricky, and is definitely a source of improvement in coming versions of the api. The motivation for the cached views of nodes is two fold: to prevent redundant rendering of a single CodeNode that is shared across a tree structure, and to preserve existing formatting of code that is not being rewritten.
In general, if you're not concerned with performance, you can simply apply all your modifications to the code elements structure, and call the sourceFile's markDirty(true) at the end to ensure that all child nodes will render cleanly.
Let's have some more fun by actually reworking the existing code some.
var statements = require('tscripter').statements; Object.keys(statements).sort().join(", ");
That's alot of constructs! In general, however, if you're simply outputting some code based on a static template, you can always just use the simplest construct, the statements.CodeNode
, in conjunction with setting it's text property. The more complex subclasses are useful for transforming existing code, or composing complex, dynamic source together from pieces.
For completeness, we'll demonstrate using the verbose class constructs to build up some source code from pieces.
var counter = new statements.Identifier("i"); counter.toString()
var initializer = statements.VariableDeclaration.forProperty(new statements.Property(counter, null, new statements.AtomicValue(0))); initializer.toString();
var predicate = new statements.BinaryOperation("<", counter, new statements.AtomicValue(10)); predicate.toString();
var iterator = new statements.UnaryOperation("++", counter); iterator.toString();
var forLoop = new statements.For(initializer, predicate, iterator); forLoop.toString();
We've got the Loop created, but we need to add a more complete body. Because for loops technically only support single statement bodies, we'll need to supply it with an explicit statements.CodeBlock containing our loop's body. Other block constructs, however, like the moduleElement from before, will have their bodies attached as the member "elements" more conveniently.
forLoop.body = new statements.CodeBlock(); forLoop.markDirty(); forLoop.toString();
Cool, our loop now as a block body, let's just shove some of our existing code into for the fun of it!
forLoop.body.elements = moduleElement.elements; forLoop.markDirty(true); console.log(forLoop.toString());
Hahaha, ok, that's kinda silly. Let's try replacing our moduleElement's body now with this forloop!
moduleElement.elements = [forLoop]; moduleElement.markDirty(); console.log(moduleElement.toString());
Womp womp. We've got some formatting issues. There are two approaches to solving this problem. The first, is to simply apply an outside formatter to your generated code. It's hard to get generated code to be styled correctly with a lot of manual effort, so if you've hook up to some other tool to add the newlines and indentions for you, that's probably preferred. That said, of course, it's always nice when you can just generate nice looking code. So let's do that.
The statements.CodeNode
subclass we're interested in is statements.Trivia
. tscripter
puts all trivia, including comments, inside statements.Trivia
objects from the original source code into our analyzed blocks. We can modify the existing ones and add a few ones of our own to get the formatting we're looking for.
var indentions = ["", " ", " "].map(function(level) { return new statements.Trivia("\n" + level); });
moduleElement.elements.unshift(indentions[1]); moduleElement.elements.push(indentions[0]);
moduleElement.markDirty(true); console.log(moduleElement.toString());
Looks like we still have to shift the for loop over by an indention level. Hmm...
forLoop.body.elements.forEach(function(e) { e.token += " "; }); moduleElement.markDirty(true); console.log(moduleElement.toString());
And that covers basic usage of tscripter
. Feel free to pose additional questions / issues found to https://github.com/corps/typescripter/issues