Installation

  • Install tscripter via npm.
npm install tscripter --save
  • You'll want to add reference to tscripter's d.ts files, as tscripter comes precompiled into plain ol' javascript. Using a 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" />
  • If you use tsd, you can also simply use the tsd link command.

Reading in existing code

In [1]:
var analyzer = require('tscripter').analyzer;  Object.keys(analyzer);
Out[1]:
[ 'strictMode', 'AnalyzerHost', 'SourceAnalyzer' ]

To get started analyzing some existing code, we'll need to create 'host' for our language services and describe the entry point files.

In [2]:
host = new analyzer.AnalyzerHost(["./node_modules/tscripter/index.d.ts"]); Object.keys(host.__proto__);
Out[2]:
[ 'analyzeAll', 'analyze', 'getAnalyzer', 'getSource' ]

Now, we'll create an analyzer for our file and call it's analyze method to generate our AST.

In [3]:
analyzer = host.getAnalyzer("./node_modules/tscripter/index.d.ts"); indexSource = analyzer.analyze(); indexSource.toJSON();
Out[3]:
{ constructorName: 'Source',
  data:
   { elements: [ [Object], [Object] ],
     fileName: 'node_modules/tscripter/index.d.ts' } }

Check it out, we've got our first CodeNode representing a Module. We also have access to its underlying ts.Node.

In [4]:
moduleElement = indexSource.elements[0]; [moduleElement.constructor.name, moduleElement.node.flags];
Out[4]:
[ 'Module', 32770 ]

But of course, the structured details are more useful. Like it's name and modifiers...

In [5]:
[moduleElement.name.token, moduleElement.modifiers];
Out[5]:
[ '"tscripter"', [ 'declare' ] ]

Of course, the most common task is to render a string from an element. Let's take a peak!

In [6]:
console.log(moduleElement.toString());
declare module "tscripter" {
  export import analyzer = require("lib/analyzer");
  export import statements = require("lib/statements");
}
Out[6]:
undefined

We can access the child elements of the module, too. But wait, it's empty?

In [7]:
moduleElement.elements
Out[7]:
[]

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.

In [8]:
analyzer.analyzeBody(moduleElement).elements.length;
Out[8]:
5

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.

In [9]:
analyzer = host.getAnalyzer(indexSource.fileName, true); indexSource = analyzer.analyze(); indexSource.elements[0].elements.length;
Out[9]:
5

Modify existing code

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?

In [10]:
moduleElement.elements[1].toString();
Out[10]:
'export import analyzer = require("lib/analyzer")'
In [11]:
requireAnalyzer = moduleElement.elements[1]; Object.keys(requireAnalyzer)
Out[11]:
[ 'node', 'importedAs', 'importPath', 'modifiers', 'text' ]
In [12]:
Object.keys(requireAnalyzer.importedAs);
Out[12]:
[ 'node', 'token', '_isIdentifier', 'text' ]
In [13]:
requireAnalyzer.importedAs.token = "hacked";  requireAnalyzer.toString();
Out[13]:
'export import analyzer = require("lib/analyzer")'

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.

In [14]:
console.log("text was", requireAnalyzer.text); requireAnalyzer.markDirty(); requireAnalyzer.toString();
text was export import analyzer = require("lib/analyzer")
Out[14]:
'export import analyzer = require("lib/analyzer")'

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.

In [15]:
requireAnalyzer.importedAs.markDirty(); requireAnalyzer.markDirty(); requireAnalyzer.toString();
Out[15]:
'export import hacked = require("lib/analyzer")'

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.

In [16]:
requireAnalyzer.importedAs.token = "withrecursive";  requireAnalyzer.markDirty(true); requireAnalyzer.toString();
Out[16]:
'export import withrecursive = require("lib/analyzer")'

As mentioned before, however, the parent still has cached its previous rendering as we can see below.

In [17]:
console.log(moduleElement.toString());
declare module "tscripter" {
  export import analyzer = require("lib/analyzer");
  export import statements = require("lib/statements");
}
Out[17]:
undefined

We'll need to mark this parent as dirty in order for it to render the new child view as well.

In [18]:
moduleElement.markDirty(); console.log(moduleElement.toString());
declare module "tscripter" {
  export import withrecursive = require("lib/analyzer");
  export import statements = require("lib/statements");
}
Out[18]:
undefined

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.

Generating new code

Let's have some more fun by actually reworking the existing code some.

In [19]:
var statements = require('tscripter').statements; Object.keys(statements).sort().join(", ");
Out[19]:
'AbstractBlock, AbstractCallableSignature, AbstractExpressionBlock, AbstractStatementBlock, ArrayBinding, ArrayLiteral, ArrayType, AtomicValue, BinaryOperation, BindingElement, Break, Call, CallableSignature, CallableType, Case, Class, CodeBlock, CodeNode, ComputedDeclarationName, Continue, Delete, ES6Import, ElementAccess, EmptyExpression, EnumEntry, Enumeration, ExportAssignment, ExportDeclaration, For, ForInOf, Function, Identifier, If, Index, InstanceOf, Interface, InternalModuleImport, Keyword, KeywordOperator, KeywordType, LabeledStatement, Lambda, Loop, Module, NamedImportOrExports, NamespaceBinding, New, ObjectBinding, ObjectLiteral, ObjectLiteralProperty, ParenthesizedType, Parenthetical, Property, PropertyAccess, QualifiedName, QualifiedTypeName, RegexLiteral, RequireImport, Return, SimpleImport, SimpleNode, Source, Spacing, Spread, Switch, TaggedTemplate, TemplateLiteralPiece, TemplatePattern, TernaryOperation, Throw, Try, TupleType, TypeAlias, TypeAssertion, TypeLiteral, TypeOf, TypeParameter, UnaryOperation, UnionType, VariableDeclaration, VariableDeclarationType, Void, With'

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.

In [20]:
var counter = new statements.Identifier("i"); counter.toString()
Out[20]:
'i'
In [21]:
var initializer = statements.VariableDeclaration.forProperty(new statements.Property(counter, null, new statements.AtomicValue(0))); initializer.toString();
Out[21]:
'var i = 0'
In [22]:
var predicate = new statements.BinaryOperation("<", counter, new statements.AtomicValue(10)); predicate.toString();
Out[22]:
'i < 10'
In [23]:
var iterator = new statements.UnaryOperation("++", counter); iterator.toString();
Out[23]:
'++i'
In [24]:
var forLoop = new statements.For(initializer, predicate, iterator); forLoop.toString();
Out[24]:
'for (var i = 0; i < 10; ++i) '

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.

In [25]:
forLoop.body = new statements.CodeBlock(); forLoop.markDirty(); forLoop.toString();
Out[25]:
'for (var i = 0; i < 10; ++i) {}'

Cool, our loop now as a block body, let's just shove some of our existing code into for the fun of it!

In [26]:
forLoop.body.elements = moduleElement.elements;  forLoop.markDirty(true); console.log(forLoop.toString());
for (var i = 0; i < 10; ++i) {
  export import withrecursive = require("lib/analyzer");
  export import statements = require("lib/statements");
}
Out[26]:
undefined

Hahaha, ok, that's kinda silly. Let's try replacing our moduleElement's body now with this forloop!

In [27]:
moduleElement.elements = [forLoop]; moduleElement.markDirty(); console.log(moduleElement.toString());
declare module "tscripter" {for (var i = 0; i < 10; ++i) {
  export import withrecursive = require("lib/analyzer");
  export import statements = require("lib/statements");
}}
Out[27]:
undefined

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.

In [28]:
var indentions = ["", "  ", "    "].map(function(level) { return new statements.Trivia("\n" + level); });
TypeError: undefined is not a function
    at evalmachine.<anonymous>:1:66
    at Array.map (native)
    at evalmachine.<anonymous>:1:37
    at run ([eval]:179:19)
    at onMessage ([eval]:63:41)
    at process.emit (events.js:98:17)
    at handleMessage (child_process.js:322:10)
    at Pipe.channel.onread (child_process.js:349:11)
In [29]:
moduleElement.elements.unshift(indentions[1]); moduleElement.elements.push(indentions[0]);
TypeError: Cannot read property '1' of undefined
    at evalmachine.<anonymous>:1:42
    at run ([eval]:179:19)
    at onMessage ([eval]:63:41)
    at process.emit (events.js:98:17)
    at handleMessage (child_process.js:322:10)
    at Pipe.channel.onread (child_process.js:349:11)
In [30]:
moduleElement.markDirty(true); console.log(moduleElement.toString());
declare module "tscripter" {for (var i = 0; i < 10; ++i) {
  export import withrecursive = require("lib/analyzer");
  export import statements = require("lib/statements");
}}
Out[30]:
undefined

Looks like we still have to shift the for loop over by an indention level. Hmm...

In [31]:
forLoop.body.elements.forEach(function(e) { e.token += "  "; }); moduleElement.markDirty(true);  console.log(moduleElement.toString());
declare module "tscripter" {for (var i = 0; i < 10; ++i) {
    export import withrecursive = require("lib/analyzer");
    export import statements = require("lib/statements");
  }}
Out[31]:
undefined

And that covers basic usage of tscripter. Feel free to pose additional questions / issues found to https://github.com/corps/typescripter/issues