Using the Compiler API (TypeScript 1.4)

Mohamed Hegazy edited this page Apr 2, 2015 · 4 revisions

Getting set up

First you'll need to install TypeScript ==1.4 from npm.

For API Samples compatible with TypeScript >= 1.5 please see Using the Compiler API

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

Once that's done, just grab our definitions file either

  • By using tsd with the command tsd query typescript --action install.
  • Going directly to the source on our repository.

For a overview of the general TypeScript compiler architecture and layering, see Architectural Overview

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that can compile a TypeScript string to its corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctioncompile(filenames: string[],options: ts.CompilerOptions): void{varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(filenames,options,host);varchecker=ts.createTypeChecker(program,/*produceDiagnostics*/true);varresult=checker.emitFiles();varallDiagnostics=program.getDiagnostics().concat(checker.getDiagnostics()).concat(result.diagnostics);allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(`${diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});console.log(`Process exiting with code '${result.emitResultStatus}'.`);process.exit(result.emitResultStatus);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may not want to actually do traditional reads and writes from the file system; for instance, you may have a text buffer for your TypeScript input, and you may want to send/store the resulting JavaScript as JSON. What's more, you may want to use/modify the resulting JavaScript in some way. In such a case, you will need to provide your own CompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");importfs= require("fs");importpath= require("path");functiontransform(contents: string,libSource: string,compilerOptions: ts.CompilerOptions={}){// Generated outputsvaroutputs=[];// Create a compilerHost object to allow the compiler to read and write filesvarcompilerHost={getSourceFile: function(filename,languageVersion){if(filename==="file.ts")returnts.createSourceFile(filename,contents,compilerOptions.target,"0");if(filename==="lib.d.ts")returnts.createSourceFile(filename,libSource,compilerOptions.target,"0");returnundefined;},writeFile: function(name,text,writeByteOrderMark){outputs.push({name: name,text: text,writeByteOrderMark: writeByteOrderMark});},getDefaultLibFilename: function(){return"lib.d.ts";},useCaseSensitiveFileNames: function(){returnfalse;},getCanonicalFileName: function(filename){returnfilename;},getCurrentDirectory: function(){return"";},getNewLine: function(){return"\n";}};// Create a program from inputsvarprogram=ts.createProgram(["file.ts"],compilerOptions,compilerHost);// Query for early errorsvarerrors=program.getDiagnostics();// Do not generate code in the presence of early errorsif(!errors.length){// Type check and get semantic errorsvarchecker=program.getTypeChecker(true);errors=checker.getDiagnostics();// Generate outputchecker.emitFiles();}return{outputs: outputs,errors: errors.map(function(e){returne.file.filename+"("+e.file.getLineAndCharacterFromPosition(e.start).line+"): "+e.messageText;})};}// Calling our transform function using a simple TypeScript variable declarations, // and loading the default library like:varsource="var x: number = 'string'";varlibSource=fs.readFileSync(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts')).toString();varresult=transform(source,libSource);console.log(JSON.stringify(result));

will generate the following output:

{
"outputs": [
{
"name": "file.js",
"text": "var x = 'string';\n"
}
],
"errors": [
"file.ts(1): Type 'string' is not assignable to type 'number'."
]
}

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
varifStatement=(<ts.IfStatement>node);if(ifStatement.thenStatement.kind!==ts.SyntaxKind.Block){report(ifStatement.thenStatement,"An if statement's contents should be wrapped in a block body.");}if(ifStatement.elseStatement&&ifStatement.elseStatement.kind!==ts.SyntaxKind.Block&&ifStatement.elseStatement.kind!==ts.SyntaxKind.IfStatement){report(ifStatement.elseStatement,"An else statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.BinaryExpression:
varop=(<ts.BinaryExpression>node).operator;if(op===ts.SyntaxKind.EqualsEqualsToken||op===ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){varlineChar=sourceFile.getLineAndCharacterFromPosition(node.getStart());console.log(`${sourceFile.filename} (${lineChar.line},${lineChar.character}): ${message}`)}}varfileNames=process.argv.slice(2);varoptions: ts.CompilerOptions={target: ts.ScriptTarget.ES6,module: ts.ModuleKind.AMD};varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(fileNames,options,host);program.getSourceFiles().forEach(delint);

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional set of utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and update the only the outputs of the file that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version, allows the language service to track changes to files. isOpen, tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importfs= require("fs");importts= require("typescript");importpath= require("path");functionwatch(filenames: string[],options: ts.CompilerOptions){varfiles: ts.Map<{version: number;text: string;}>={};// Add the default library filefilenames.unshift(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts'));// initialize the list of filesfilenames.forEach(filename=>{files[filename]={version: 0,text: fs.readFileSync(filename).toString()};});// Create the language service host to allow the LS to communicate with the hostvarservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>filenames,getScriptVersion: (filename)=>files[filename]&&files[filename].version.toString(),getScriptSnapshot: (filename)=>{varfile=files[filename];return{getText: (start,end)=>file.text.substring(start,end),getLength: ()=>file.text.length,getLineStartPositions: ()=>[],getChangeRange: (oldSnapshot)=>undefined};},getCurrentDirectory: ()=>process.cwd(),getScriptIsOpen: ()=>true,getCompilationSettings: ()=>options,getDefaultLibFilename:(options)=>'lib.d.ts',log: (message)=>console.log(message)};// Create the language service filesvarservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesfilenames.forEach(filename=>{// First time around, emit all filesemitFile(filename);// Add a watch on the file to handle next changefs.watchFile(filename,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}varfile=files[filename];// Update the version to signal a change in the filefile.version++;// Clear the text to force a new readfile.text=fs.readFileSync(filename).toString();// write the changes to diskemitFile(filename);});});functionemitFile(filename: string){varoutput=services.getEmitOutput(filename);if(output.emitOutputStatus===ts.EmitReturnStatus.Succeeded){console.log(`Emitting ${filename}`);}else{console.log(`Emitting ${filename} failed`);varallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(filename)).concat(services.getSemanticDiagnostics(filename));allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(` ${diagnostic.file&&diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});}output.outputFiles.forEach(o=>{fs.writeFileSync(o.name,o.text,"utf8");});}}// Initialize files constituting the program as all .ts files in the current directoryvarcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(filename=>filename.length>=3&&filename.substr(filename.length-3,3)===".ts");//map(filename => path.join(process.cwd(), filename));// Start the watcherwatch(currentDirectoryFiles,{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){varoptions=getDefaultOptions();// Parse the source textvarsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,"0");fixupParentReferences(sourceFile);// Get the formatting edits on the input sourcesvaredits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.varruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textvarresult=text;for(vari=edits.length-1;i>=0;i--){varchange=edits[i];varhead=result.slice(0,change.span.start());vartail=result.slice(change.span.start()+change.span.length())result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}functionfixupParentReferences(sourceFile: ts.SourceFile){varparent: ts.Node=sourceFile;functionwalk(n: ts.Node): void{n.parent=parent;varsaveParent=parent;parent=n;ts.forEachChild(n,walk);parent=saveParent;}ts.forEachChild(sourceFile,walk);}}varcode="var a=function(v:number){return 0+1+2+3;\n}";varresult=format(code);console.log(result);

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Using the Compiler API (TypeScript 1.4)

Mohamed Hegazy edited this page Apr 2, 2015 · 4 revisions

Getting set up

First you'll need to install TypeScript ==1.4 from npm.

For API Samples compatible with TypeScript >= 1.5 please see Using the Compiler API

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

Once that's done, just grab our definitions file either

  • By using tsd with the command tsd query typescript --action install.
  • Going directly to the source on our repository.

For a overview of the general TypeScript compiler architecture and layering, see Architectural Overview

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that can compile a TypeScript string to its corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctioncompile(filenames: string[],options: ts.CompilerOptions): void{varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(filenames,options,host);varchecker=ts.createTypeChecker(program,/*produceDiagnostics*/true);varresult=checker.emitFiles();varallDiagnostics=program.getDiagnostics().concat(checker.getDiagnostics()).concat(result.diagnostics);allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(`${diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});console.log(`Process exiting with code '${result.emitResultStatus}'.`);process.exit(result.emitResultStatus);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may not want to actually do traditional reads and writes from the file system; for instance, you may have a text buffer for your TypeScript input, and you may want to send/store the resulting JavaScript as JSON. What's more, you may want to use/modify the resulting JavaScript in some way. In such a case, you will need to provide your own CompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");importfs= require("fs");importpath= require("path");functiontransform(contents: string,libSource: string,compilerOptions: ts.CompilerOptions={}){// Generated outputsvaroutputs=[];// Create a compilerHost object to allow the compiler to read and write filesvarcompilerHost={getSourceFile: function(filename,languageVersion){if(filename==="file.ts")returnts.createSourceFile(filename,contents,compilerOptions.target,"0");if(filename==="lib.d.ts")returnts.createSourceFile(filename,libSource,compilerOptions.target,"0");returnundefined;},writeFile: function(name,text,writeByteOrderMark){outputs.push({name: name,text: text,writeByteOrderMark: writeByteOrderMark});},getDefaultLibFilename: function(){return"lib.d.ts";},useCaseSensitiveFileNames: function(){returnfalse;},getCanonicalFileName: function(filename){returnfilename;},getCurrentDirectory: function(){return"";},getNewLine: function(){return"\n";}};// Create a program from inputsvarprogram=ts.createProgram(["file.ts"],compilerOptions,compilerHost);// Query for early errorsvarerrors=program.getDiagnostics();// Do not generate code in the presence of early errorsif(!errors.length){// Type check and get semantic errorsvarchecker=program.getTypeChecker(true);errors=checker.getDiagnostics();// Generate outputchecker.emitFiles();}return{outputs: outputs,errors: errors.map(function(e){returne.file.filename+"("+e.file.getLineAndCharacterFromPosition(e.start).line+"): "+e.messageText;})};}// Calling our transform function using a simple TypeScript variable declarations, // and loading the default library like:varsource="var x: number = 'string'";varlibSource=fs.readFileSync(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts')).toString();varresult=transform(source,libSource);console.log(JSON.stringify(result));

will generate the following output:

{
"outputs": [
{
"name": "file.js",
"text": "var x = 'string';\n"
}
],
"errors": [
"file.ts(1): Type 'string' is not assignable to type 'number'."
]
}

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
varifStatement=(<ts.IfStatement>node);if(ifStatement.thenStatement.kind!==ts.SyntaxKind.Block){report(ifStatement.thenStatement,"An if statement's contents should be wrapped in a block body.");}if(ifStatement.elseStatement&&ifStatement.elseStatement.kind!==ts.SyntaxKind.Block&&ifStatement.elseStatement.kind!==ts.SyntaxKind.IfStatement){report(ifStatement.elseStatement,"An else statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.BinaryExpression:
varop=(<ts.BinaryExpression>node).operator;if(op===ts.SyntaxKind.EqualsEqualsToken||op===ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){varlineChar=sourceFile.getLineAndCharacterFromPosition(node.getStart());console.log(`${sourceFile.filename} (${lineChar.line},${lineChar.character}): ${message}`)}}varfileNames=process.argv.slice(2);varoptions: ts.CompilerOptions={target: ts.ScriptTarget.ES6,module: ts.ModuleKind.AMD};varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(fileNames,options,host);program.getSourceFiles().forEach(delint);

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional set of utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and update the only the outputs of the file that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version, allows the language service to track changes to files. isOpen, tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importfs= require("fs");importts= require("typescript");importpath= require("path");functionwatch(filenames: string[],options: ts.CompilerOptions){varfiles: ts.Map<{version: number;text: string;}>={};// Add the default library filefilenames.unshift(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts'));// initialize the list of filesfilenames.forEach(filename=>{files[filename]={version: 0,text: fs.readFileSync(filename).toString()};});// Create the language service host to allow the LS to communicate with the hostvarservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>filenames,getScriptVersion: (filename)=>files[filename]&&files[filename].version.toString(),getScriptSnapshot: (filename)=>{varfile=files[filename];return{getText: (start,end)=>file.text.substring(start,end),getLength: ()=>file.text.length,getLineStartPositions: ()=>[],getChangeRange: (oldSnapshot)=>undefined};},getCurrentDirectory: ()=>process.cwd(),getScriptIsOpen: ()=>true,getCompilationSettings: ()=>options,getDefaultLibFilename:(options)=>'lib.d.ts',log: (message)=>console.log(message)};// Create the language service filesvarservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesfilenames.forEach(filename=>{// First time around, emit all filesemitFile(filename);// Add a watch on the file to handle next changefs.watchFile(filename,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}varfile=files[filename];// Update the version to signal a change in the filefile.version++;// Clear the text to force a new readfile.text=fs.readFileSync(filename).toString();// write the changes to diskemitFile(filename);});});functionemitFile(filename: string){varoutput=services.getEmitOutput(filename);if(output.emitOutputStatus===ts.EmitReturnStatus.Succeeded){console.log(`Emitting ${filename}`);}else{console.log(`Emitting ${filename} failed`);varallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(filename)).concat(services.getSemanticDiagnostics(filename));allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(` ${diagnostic.file&&diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});}output.outputFiles.forEach(o=>{fs.writeFileSync(o.name,o.text,"utf8");});}}// Initialize files constituting the program as all .ts files in the current directoryvarcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(filename=>filename.length>=3&&filename.substr(filename.length-3,3)===".ts");//map(filename => path.join(process.cwd(), filename));// Start the watcherwatch(currentDirectoryFiles,{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){varoptions=getDefaultOptions();// Parse the source textvarsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,"0");fixupParentReferences(sourceFile);// Get the formatting edits on the input sourcesvaredits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.varruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textvarresult=text;for(vari=edits.length-1;i>=0;i--){varchange=edits[i];varhead=result.slice(0,change.span.start());vartail=result.slice(change.span.start()+change.span.length())result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}functionfixupParentReferences(sourceFile: ts.SourceFile){varparent: ts.Node=sourceFile;functionwalk(n: ts.Node): void{n.parent=parent;varsaveParent=parent;parent=n;ts.forEachChild(n,walk);parent=saveParent;}ts.forEachChild(sourceFile,walk);}}varcode="var a=function(v:number){return 0+1+2+3;\n}";varresult=format(code);console.log(result);

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Using the Compiler API (TypeScript 1.4)

Mohamed Hegazy edited this page Apr 2, 2015 · 4 revisions

Getting set up

First you'll need to install TypeScript ==1.4 from npm.

For API Samples compatible with TypeScript >= 1.5 please see Using the Compiler API

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

Once that's done, just grab our definitions file either

  • By using tsd with the command tsd query typescript --action install.
  • Going directly to the source on our repository.

For a overview of the general TypeScript compiler architecture and layering, see Architectural Overview

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that can compile a TypeScript string to its corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctioncompile(filenames: string[],options: ts.CompilerOptions): void{varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(filenames,options,host);varchecker=ts.createTypeChecker(program,/*produceDiagnostics*/true);varresult=checker.emitFiles();varallDiagnostics=program.getDiagnostics().concat(checker.getDiagnostics()).concat(result.diagnostics);allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(`${diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});console.log(`Process exiting with code '${result.emitResultStatus}'.`);process.exit(result.emitResultStatus);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may not want to actually do traditional reads and writes from the file system; for instance, you may have a text buffer for your TypeScript input, and you may want to send/store the resulting JavaScript as JSON. What's more, you may want to use/modify the resulting JavaScript in some way. In such a case, you will need to provide your own CompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");importfs= require("fs");importpath= require("path");functiontransform(contents: string,libSource: string,compilerOptions: ts.CompilerOptions={}){// Generated outputsvaroutputs=[];// Create a compilerHost object to allow the compiler to read and write filesvarcompilerHost={getSourceFile: function(filename,languageVersion){if(filename==="file.ts")returnts.createSourceFile(filename,contents,compilerOptions.target,"0");if(filename==="lib.d.ts")returnts.createSourceFile(filename,libSource,compilerOptions.target,"0");returnundefined;},writeFile: function(name,text,writeByteOrderMark){outputs.push({name: name,text: text,writeByteOrderMark: writeByteOrderMark});},getDefaultLibFilename: function(){return"lib.d.ts";},useCaseSensitiveFileNames: function(){returnfalse;},getCanonicalFileName: function(filename){returnfilename;},getCurrentDirectory: function(){return"";},getNewLine: function(){return"\n";}};// Create a program from inputsvarprogram=ts.createProgram(["file.ts"],compilerOptions,compilerHost);// Query for early errorsvarerrors=program.getDiagnostics();// Do not generate code in the presence of early errorsif(!errors.length){// Type check and get semantic errorsvarchecker=program.getTypeChecker(true);errors=checker.getDiagnostics();// Generate outputchecker.emitFiles();}return{outputs: outputs,errors: errors.map(function(e){returne.file.filename+"("+e.file.getLineAndCharacterFromPosition(e.start).line+"): "+e.messageText;})};}// Calling our transform function using a simple TypeScript variable declarations, // and loading the default library like:varsource="var x: number = 'string'";varlibSource=fs.readFileSync(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts')).toString();varresult=transform(source,libSource);console.log(JSON.stringify(result));

will generate the following output:

{
"outputs": [
{
"name": "file.js",
"text": "var x = 'string';\n"
}
],
"errors": [
"file.ts(1): Type 'string' is not assignable to type 'number'."
]
}

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
varifStatement=(<ts.IfStatement>node);if(ifStatement.thenStatement.kind!==ts.SyntaxKind.Block){report(ifStatement.thenStatement,"An if statement's contents should be wrapped in a block body.");}if(ifStatement.elseStatement&&ifStatement.elseStatement.kind!==ts.SyntaxKind.Block&&ifStatement.elseStatement.kind!==ts.SyntaxKind.IfStatement){report(ifStatement.elseStatement,"An else statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.BinaryExpression:
varop=(<ts.BinaryExpression>node).operator;if(op===ts.SyntaxKind.EqualsEqualsToken||op===ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){varlineChar=sourceFile.getLineAndCharacterFromPosition(node.getStart());console.log(`${sourceFile.filename} (${lineChar.line},${lineChar.character}): ${message}`)}}varfileNames=process.argv.slice(2);varoptions: ts.CompilerOptions={target: ts.ScriptTarget.ES6,module: ts.ModuleKind.AMD};varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(fileNames,options,host);program.getSourceFiles().forEach(delint);

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional set of utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and update the only the outputs of the file that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version, allows the language service to track changes to files. isOpen, tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importfs= require("fs");importts= require("typescript");importpath= require("path");functionwatch(filenames: string[],options: ts.CompilerOptions){varfiles: ts.Map<{version: number;text: string;}>={};// Add the default library filefilenames.unshift(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts'));// initialize the list of filesfilenames.forEach(filename=>{files[filename]={version: 0,text: fs.readFileSync(filename).toString()};});// Create the language service host to allow the LS to communicate with the hostvarservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>filenames,getScriptVersion: (filename)=>files[filename]&&files[filename].version.toString(),getScriptSnapshot: (filename)=>{varfile=files[filename];return{getText: (start,end)=>file.text.substring(start,end),getLength: ()=>file.text.length,getLineStartPositions: ()=>[],getChangeRange: (oldSnapshot)=>undefined};},getCurrentDirectory: ()=>process.cwd(),getScriptIsOpen: ()=>true,getCompilationSettings: ()=>options,getDefaultLibFilename:(options)=>'lib.d.ts',log: (message)=>console.log(message)};// Create the language service filesvarservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesfilenames.forEach(filename=>{// First time around, emit all filesemitFile(filename);// Add a watch on the file to handle next changefs.watchFile(filename,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}varfile=files[filename];// Update the version to signal a change in the filefile.version++;// Clear the text to force a new readfile.text=fs.readFileSync(filename).toString();// write the changes to diskemitFile(filename);});});functionemitFile(filename: string){varoutput=services.getEmitOutput(filename);if(output.emitOutputStatus===ts.EmitReturnStatus.Succeeded){console.log(`Emitting ${filename}`);}else{console.log(`Emitting ${filename} failed`);varallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(filename)).concat(services.getSemanticDiagnostics(filename));allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(` ${diagnostic.file&&diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});}output.outputFiles.forEach(o=>{fs.writeFileSync(o.name,o.text,"utf8");});}}// Initialize files constituting the program as all .ts files in the current directoryvarcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(filename=>filename.length>=3&&filename.substr(filename.length-3,3)===".ts");//map(filename => path.join(process.cwd(), filename));// Start the watcherwatch(currentDirectoryFiles,{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){varoptions=getDefaultOptions();// Parse the source textvarsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,"0");fixupParentReferences(sourceFile);// Get the formatting edits on the input sourcesvaredits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.varruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textvarresult=text;for(vari=edits.length-1;i>=0;i--){varchange=edits[i];varhead=result.slice(0,change.span.start());vartail=result.slice(change.span.start()+change.span.length())result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}functionfixupParentReferences(sourceFile: ts.SourceFile){varparent: ts.Node=sourceFile;functionwalk(n: ts.Node): void{n.parent=parent;varsaveParent=parent;parent=n;ts.forEachChild(n,walk);parent=saveParent;}ts.forEachChild(sourceFile,walk);}}varcode="var a=function(v:number){return 0+1+2+3;\n}";varresult=format(code);console.log(result);

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Using the Compiler API (TypeScript 1.4)

Mohamed Hegazy edited this page Apr 2, 2015 · 4 revisions

Getting set up

First you'll need to install TypeScript ==1.4 from npm.

For API Samples compatible with TypeScript >= 1.5 please see Using the Compiler API

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

Once that's done, just grab our definitions file either

  • By using tsd with the command tsd query typescript --action install.
  • Going directly to the source on our repository.

For a overview of the general TypeScript compiler architecture and layering, see Architectural Overview

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that can compile a TypeScript string to its corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctioncompile(filenames: string[],options: ts.CompilerOptions): void{varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(filenames,options,host);varchecker=ts.createTypeChecker(program,/*produceDiagnostics*/true);varresult=checker.emitFiles();varallDiagnostics=program.getDiagnostics().concat(checker.getDiagnostics()).concat(result.diagnostics);allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(`${diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});console.log(`Process exiting with code '${result.emitResultStatus}'.`);process.exit(result.emitResultStatus);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may not want to actually do traditional reads and writes from the file system; for instance, you may have a text buffer for your TypeScript input, and you may want to send/store the resulting JavaScript as JSON. What's more, you may want to use/modify the resulting JavaScript in some way. In such a case, you will need to provide your own CompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");importfs= require("fs");importpath= require("path");functiontransform(contents: string,libSource: string,compilerOptions: ts.CompilerOptions={}){// Generated outputsvaroutputs=[];// Create a compilerHost object to allow the compiler to read and write filesvarcompilerHost={getSourceFile: function(filename,languageVersion){if(filename==="file.ts")returnts.createSourceFile(filename,contents,compilerOptions.target,"0");if(filename==="lib.d.ts")returnts.createSourceFile(filename,libSource,compilerOptions.target,"0");returnundefined;},writeFile: function(name,text,writeByteOrderMark){outputs.push({name: name,text: text,writeByteOrderMark: writeByteOrderMark});},getDefaultLibFilename: function(){return"lib.d.ts";},useCaseSensitiveFileNames: function(){returnfalse;},getCanonicalFileName: function(filename){returnfilename;},getCurrentDirectory: function(){return"";},getNewLine: function(){return"\n";}};// Create a program from inputsvarprogram=ts.createProgram(["file.ts"],compilerOptions,compilerHost);// Query for early errorsvarerrors=program.getDiagnostics();// Do not generate code in the presence of early errorsif(!errors.length){// Type check and get semantic errorsvarchecker=program.getTypeChecker(true);errors=checker.getDiagnostics();// Generate outputchecker.emitFiles();}return{outputs: outputs,errors: errors.map(function(e){returne.file.filename+"("+e.file.getLineAndCharacterFromPosition(e.start).line+"): "+e.messageText;})};}// Calling our transform function using a simple TypeScript variable declarations, // and loading the default library like:varsource="var x: number = 'string'";varlibSource=fs.readFileSync(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts')).toString();varresult=transform(source,libSource);console.log(JSON.stringify(result));

will generate the following output:

{
"outputs": [
{
"name": "file.js",
"text": "var x = 'string';\n"
}
],
"errors": [
"file.ts(1): Type 'string' is not assignable to type 'number'."
]
}

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
varifStatement=(<ts.IfStatement>node);if(ifStatement.thenStatement.kind!==ts.SyntaxKind.Block){report(ifStatement.thenStatement,"An if statement's contents should be wrapped in a block body.");}if(ifStatement.elseStatement&&ifStatement.elseStatement.kind!==ts.SyntaxKind.Block&&ifStatement.elseStatement.kind!==ts.SyntaxKind.IfStatement){report(ifStatement.elseStatement,"An else statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.BinaryExpression:
varop=(<ts.BinaryExpression>node).operator;if(op===ts.SyntaxKind.EqualsEqualsToken||op===ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){varlineChar=sourceFile.getLineAndCharacterFromPosition(node.getStart());console.log(`${sourceFile.filename} (${lineChar.line},${lineChar.character}): ${message}`)}}varfileNames=process.argv.slice(2);varoptions: ts.CompilerOptions={target: ts.ScriptTarget.ES6,module: ts.ModuleKind.AMD};varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(fileNames,options,host);program.getSourceFiles().forEach(delint);

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional set of utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and update the only the outputs of the file that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version, allows the language service to track changes to files. isOpen, tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importfs= require("fs");importts= require("typescript");importpath= require("path");functionwatch(filenames: string[],options: ts.CompilerOptions){varfiles: ts.Map<{version: number;text: string;}>={};// Add the default library filefilenames.unshift(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts'));// initialize the list of filesfilenames.forEach(filename=>{files[filename]={version: 0,text: fs.readFileSync(filename).toString()};});// Create the language service host to allow the LS to communicate with the hostvarservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>filenames,getScriptVersion: (filename)=>files[filename]&&files[filename].version.toString(),getScriptSnapshot: (filename)=>{varfile=files[filename];return{getText: (start,end)=>file.text.substring(start,end),getLength: ()=>file.text.length,getLineStartPositions: ()=>[],getChangeRange: (oldSnapshot)=>undefined};},getCurrentDirectory: ()=>process.cwd(),getScriptIsOpen: ()=>true,getCompilationSettings: ()=>options,getDefaultLibFilename:(options)=>'lib.d.ts',log: (message)=>console.log(message)};// Create the language service filesvarservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesfilenames.forEach(filename=>{// First time around, emit all filesemitFile(filename);// Add a watch on the file to handle next changefs.watchFile(filename,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}varfile=files[filename];// Update the version to signal a change in the filefile.version++;// Clear the text to force a new readfile.text=fs.readFileSync(filename).toString();// write the changes to diskemitFile(filename);});});functionemitFile(filename: string){varoutput=services.getEmitOutput(filename);if(output.emitOutputStatus===ts.EmitReturnStatus.Succeeded){console.log(`Emitting ${filename}`);}else{console.log(`Emitting ${filename} failed`);varallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(filename)).concat(services.getSemanticDiagnostics(filename));allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(` ${diagnostic.file&&diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});}output.outputFiles.forEach(o=>{fs.writeFileSync(o.name,o.text,"utf8");});}}// Initialize files constituting the program as all .ts files in the current directoryvarcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(filename=>filename.length>=3&&filename.substr(filename.length-3,3)===".ts");//map(filename => path.join(process.cwd(), filename));// Start the watcherwatch(currentDirectoryFiles,{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){varoptions=getDefaultOptions();// Parse the source textvarsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,"0");fixupParentReferences(sourceFile);// Get the formatting edits on the input sourcesvaredits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.varruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textvarresult=text;for(vari=edits.length-1;i>=0;i--){varchange=edits[i];varhead=result.slice(0,change.span.start());vartail=result.slice(change.span.start()+change.span.length())result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}functionfixupParentReferences(sourceFile: ts.SourceFile){varparent: ts.Node=sourceFile;functionwalk(n: ts.Node): void{n.parent=parent;varsaveParent=parent;parent=n;ts.forEachChild(n,walk);parent=saveParent;}ts.forEachChild(sourceFile,walk);}}varcode="var a=function(v:number){return 0+1+2+3;\n}";varresult=format(code);console.log(result);

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Using the Compiler API (TypeScript 1.4)

Mohamed Hegazy edited this page Apr 2, 2015 · 4 revisions

Getting set up

First you'll need to install TypeScript ==1.4 from npm.

For API Samples compatible with TypeScript >= 1.5 please see Using the Compiler API

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

Once that's done, just grab our definitions file either

  • By using tsd with the command tsd query typescript --action install.
  • Going directly to the source on our repository.

For a overview of the general TypeScript compiler architecture and layering, see Architectural Overview

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that can compile a TypeScript string to its corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctioncompile(filenames: string[],options: ts.CompilerOptions): void{varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(filenames,options,host);varchecker=ts.createTypeChecker(program,/*produceDiagnostics*/true);varresult=checker.emitFiles();varallDiagnostics=program.getDiagnostics().concat(checker.getDiagnostics()).concat(result.diagnostics);allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(`${diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});console.log(`Process exiting with code '${result.emitResultStatus}'.`);process.exit(result.emitResultStatus);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may not want to actually do traditional reads and writes from the file system; for instance, you may have a text buffer for your TypeScript input, and you may want to send/store the resulting JavaScript as JSON. What's more, you may want to use/modify the resulting JavaScript in some way. In such a case, you will need to provide your own CompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");importfs= require("fs");importpath= require("path");functiontransform(contents: string,libSource: string,compilerOptions: ts.CompilerOptions={}){// Generated outputsvaroutputs=[];// Create a compilerHost object to allow the compiler to read and write filesvarcompilerHost={getSourceFile: function(filename,languageVersion){if(filename==="file.ts")returnts.createSourceFile(filename,contents,compilerOptions.target,"0");if(filename==="lib.d.ts")returnts.createSourceFile(filename,libSource,compilerOptions.target,"0");returnundefined;},writeFile: function(name,text,writeByteOrderMark){outputs.push({name: name,text: text,writeByteOrderMark: writeByteOrderMark});},getDefaultLibFilename: function(){return"lib.d.ts";},useCaseSensitiveFileNames: function(){returnfalse;},getCanonicalFileName: function(filename){returnfilename;},getCurrentDirectory: function(){return"";},getNewLine: function(){return"\n";}};// Create a program from inputsvarprogram=ts.createProgram(["file.ts"],compilerOptions,compilerHost);// Query for early errorsvarerrors=program.getDiagnostics();// Do not generate code in the presence of early errorsif(!errors.length){// Type check and get semantic errorsvarchecker=program.getTypeChecker(true);errors=checker.getDiagnostics();// Generate outputchecker.emitFiles();}return{outputs: outputs,errors: errors.map(function(e){returne.file.filename+"("+e.file.getLineAndCharacterFromPosition(e.start).line+"): "+e.messageText;})};}// Calling our transform function using a simple TypeScript variable declarations, // and loading the default library like:varsource="var x: number = 'string'";varlibSource=fs.readFileSync(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts')).toString();varresult=transform(source,libSource);console.log(JSON.stringify(result));

will generate the following output:

{
"outputs": [
{
"name": "file.js",
"text": "var x = 'string';\n"
}
],
"errors": [
"file.ts(1): Type 'string' is not assignable to type 'number'."
]
}

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
varifStatement=(<ts.IfStatement>node);if(ifStatement.thenStatement.kind!==ts.SyntaxKind.Block){report(ifStatement.thenStatement,"An if statement's contents should be wrapped in a block body.");}if(ifStatement.elseStatement&&ifStatement.elseStatement.kind!==ts.SyntaxKind.Block&&ifStatement.elseStatement.kind!==ts.SyntaxKind.IfStatement){report(ifStatement.elseStatement,"An else statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.BinaryExpression:
varop=(<ts.BinaryExpression>node).operator;if(op===ts.SyntaxKind.EqualsEqualsToken||op===ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){varlineChar=sourceFile.getLineAndCharacterFromPosition(node.getStart());console.log(`${sourceFile.filename} (${lineChar.line},${lineChar.character}): ${message}`)}}varfileNames=process.argv.slice(2);varoptions: ts.CompilerOptions={target: ts.ScriptTarget.ES6,module: ts.ModuleKind.AMD};varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(fileNames,options,host);program.getSourceFiles().forEach(delint);

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional set of utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and update the only the outputs of the file that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version, allows the language service to track changes to files. isOpen, tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importfs= require("fs");importts= require("typescript");importpath= require("path");functionwatch(filenames: string[],options: ts.CompilerOptions){varfiles: ts.Map<{version: number;text: string;}>={};// Add the default library filefilenames.unshift(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts'));// initialize the list of filesfilenames.forEach(filename=>{files[filename]={version: 0,text: fs.readFileSync(filename).toString()};});// Create the language service host to allow the LS to communicate with the hostvarservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>filenames,getScriptVersion: (filename)=>files[filename]&&files[filename].version.toString(),getScriptSnapshot: (filename)=>{varfile=files[filename];return{getText: (start,end)=>file.text.substring(start,end),getLength: ()=>file.text.length,getLineStartPositions: ()=>[],getChangeRange: (oldSnapshot)=>undefined};},getCurrentDirectory: ()=>process.cwd(),getScriptIsOpen: ()=>true,getCompilationSettings: ()=>options,getDefaultLibFilename:(options)=>'lib.d.ts',log: (message)=>console.log(message)};// Create the language service filesvarservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesfilenames.forEach(filename=>{// First time around, emit all filesemitFile(filename);// Add a watch on the file to handle next changefs.watchFile(filename,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}varfile=files[filename];// Update the version to signal a change in the filefile.version++;// Clear the text to force a new readfile.text=fs.readFileSync(filename).toString();// write the changes to diskemitFile(filename);});});functionemitFile(filename: string){varoutput=services.getEmitOutput(filename);if(output.emitOutputStatus===ts.EmitReturnStatus.Succeeded){console.log(`Emitting ${filename}`);}else{console.log(`Emitting ${filename} failed`);varallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(filename)).concat(services.getSemanticDiagnostics(filename));allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(` ${diagnostic.file&&diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});}output.outputFiles.forEach(o=>{fs.writeFileSync(o.name,o.text,"utf8");});}}// Initialize files constituting the program as all .ts files in the current directoryvarcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(filename=>filename.length>=3&&filename.substr(filename.length-3,3)===".ts");//map(filename => path.join(process.cwd(), filename));// Start the watcherwatch(currentDirectoryFiles,{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){varoptions=getDefaultOptions();// Parse the source textvarsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,"0");fixupParentReferences(sourceFile);// Get the formatting edits on the input sourcesvaredits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.varruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textvarresult=text;for(vari=edits.length-1;i>=0;i--){varchange=edits[i];varhead=result.slice(0,change.span.start());vartail=result.slice(change.span.start()+change.span.length())result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}functionfixupParentReferences(sourceFile: ts.SourceFile){varparent: ts.Node=sourceFile;functionwalk(n: ts.Node): void{n.parent=parent;varsaveParent=parent;parent=n;ts.forEachChild(n,walk);parent=saveParent;}ts.forEachChild(sourceFile,walk);}}varcode="var a=function(v:number){return 0+1+2+3;\n}";varresult=format(code);console.log(result);

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Using the Compiler API (TypeScript 1.4)

Mohamed Hegazy edited this page Apr 2, 2015 · 4 revisions

Getting set up

First you'll need to install TypeScript ==1.4 from npm.

For API Samples compatible with TypeScript >= 1.5 please see Using the Compiler API

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

Once that's done, just grab our definitions file either

  • By using tsd with the command tsd query typescript --action install.
  • Going directly to the source on our repository.

For a overview of the general TypeScript compiler architecture and layering, see Architectural Overview

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that can compile a TypeScript string to its corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctioncompile(filenames: string[],options: ts.CompilerOptions): void{varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(filenames,options,host);varchecker=ts.createTypeChecker(program,/*produceDiagnostics*/true);varresult=checker.emitFiles();varallDiagnostics=program.getDiagnostics().concat(checker.getDiagnostics()).concat(result.diagnostics);allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(`${diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});console.log(`Process exiting with code '${result.emitResultStatus}'.`);process.exit(result.emitResultStatus);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may not want to actually do traditional reads and writes from the file system; for instance, you may have a text buffer for your TypeScript input, and you may want to send/store the resulting JavaScript as JSON. What's more, you may want to use/modify the resulting JavaScript in some way. In such a case, you will need to provide your own CompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");importfs= require("fs");importpath= require("path");functiontransform(contents: string,libSource: string,compilerOptions: ts.CompilerOptions={}){// Generated outputsvaroutputs=[];// Create a compilerHost object to allow the compiler to read and write filesvarcompilerHost={getSourceFile: function(filename,languageVersion){if(filename==="file.ts")returnts.createSourceFile(filename,contents,compilerOptions.target,"0");if(filename==="lib.d.ts")returnts.createSourceFile(filename,libSource,compilerOptions.target,"0");returnundefined;},writeFile: function(name,text,writeByteOrderMark){outputs.push({name: name,text: text,writeByteOrderMark: writeByteOrderMark});},getDefaultLibFilename: function(){return"lib.d.ts";},useCaseSensitiveFileNames: function(){returnfalse;},getCanonicalFileName: function(filename){returnfilename;},getCurrentDirectory: function(){return"";},getNewLine: function(){return"\n";}};// Create a program from inputsvarprogram=ts.createProgram(["file.ts"],compilerOptions,compilerHost);// Query for early errorsvarerrors=program.getDiagnostics();// Do not generate code in the presence of early errorsif(!errors.length){// Type check and get semantic errorsvarchecker=program.getTypeChecker(true);errors=checker.getDiagnostics();// Generate outputchecker.emitFiles();}return{outputs: outputs,errors: errors.map(function(e){returne.file.filename+"("+e.file.getLineAndCharacterFromPosition(e.start).line+"): "+e.messageText;})};}// Calling our transform function using a simple TypeScript variable declarations, // and loading the default library like:varsource="var x: number = 'string'";varlibSource=fs.readFileSync(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts')).toString();varresult=transform(source,libSource);console.log(JSON.stringify(result));

will generate the following output:

{
"outputs": [
{
"name": "file.js",
"text": "var x = 'string';\n"
}
],
"errors": [
"file.ts(1): Type 'string' is not assignable to type 'number'."
]
}

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
varifStatement=(<ts.IfStatement>node);if(ifStatement.thenStatement.kind!==ts.SyntaxKind.Block){report(ifStatement.thenStatement,"An if statement's contents should be wrapped in a block body.");}if(ifStatement.elseStatement&&ifStatement.elseStatement.kind!==ts.SyntaxKind.Block&&ifStatement.elseStatement.kind!==ts.SyntaxKind.IfStatement){report(ifStatement.elseStatement,"An else statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.BinaryExpression:
varop=(<ts.BinaryExpression>node).operator;if(op===ts.SyntaxKind.EqualsEqualsToken||op===ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){varlineChar=sourceFile.getLineAndCharacterFromPosition(node.getStart());console.log(`${sourceFile.filename} (${lineChar.line},${lineChar.character}): ${message}`)}}varfileNames=process.argv.slice(2);varoptions: ts.CompilerOptions={target: ts.ScriptTarget.ES6,module: ts.ModuleKind.AMD};varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(fileNames,options,host);program.getSourceFiles().forEach(delint);

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional set of utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and update the only the outputs of the file that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version, allows the language service to track changes to files. isOpen, tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importfs= require("fs");importts= require("typescript");importpath= require("path");functionwatch(filenames: string[],options: ts.CompilerOptions){varfiles: ts.Map<{version: number;text: string;}>={};// Add the default library filefilenames.unshift(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts'));// initialize the list of filesfilenames.forEach(filename=>{files[filename]={version: 0,text: fs.readFileSync(filename).toString()};});// Create the language service host to allow the LS to communicate with the hostvarservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>filenames,getScriptVersion: (filename)=>files[filename]&&files[filename].version.toString(),getScriptSnapshot: (filename)=>{varfile=files[filename];return{getText: (start,end)=>file.text.substring(start,end),getLength: ()=>file.text.length,getLineStartPositions: ()=>[],getChangeRange: (oldSnapshot)=>undefined};},getCurrentDirectory: ()=>process.cwd(),getScriptIsOpen: ()=>true,getCompilationSettings: ()=>options,getDefaultLibFilename:(options)=>'lib.d.ts',log: (message)=>console.log(message)};// Create the language service filesvarservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesfilenames.forEach(filename=>{// First time around, emit all filesemitFile(filename);// Add a watch on the file to handle next changefs.watchFile(filename,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}varfile=files[filename];// Update the version to signal a change in the filefile.version++;// Clear the text to force a new readfile.text=fs.readFileSync(filename).toString();// write the changes to diskemitFile(filename);});});functionemitFile(filename: string){varoutput=services.getEmitOutput(filename);if(output.emitOutputStatus===ts.EmitReturnStatus.Succeeded){console.log(`Emitting ${filename}`);}else{console.log(`Emitting ${filename} failed`);varallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(filename)).concat(services.getSemanticDiagnostics(filename));allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(` ${diagnostic.file&&diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});}output.outputFiles.forEach(o=>{fs.writeFileSync(o.name,o.text,"utf8");});}}// Initialize files constituting the program as all .ts files in the current directoryvarcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(filename=>filename.length>=3&&filename.substr(filename.length-3,3)===".ts");//map(filename => path.join(process.cwd(), filename));// Start the watcherwatch(currentDirectoryFiles,{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){varoptions=getDefaultOptions();// Parse the source textvarsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,"0");fixupParentReferences(sourceFile);// Get the formatting edits on the input sourcesvaredits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.varruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textvarresult=text;for(vari=edits.length-1;i>=0;i--){varchange=edits[i];varhead=result.slice(0,change.span.start());vartail=result.slice(change.span.start()+change.span.length())result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}functionfixupParentReferences(sourceFile: ts.SourceFile){varparent: ts.Node=sourceFile;functionwalk(n: ts.Node): void{n.parent=parent;varsaveParent=parent;parent=n;ts.forEachChild(n,walk);parent=saveParent;}ts.forEachChild(sourceFile,walk);}}varcode="var a=function(v:number){return 0+1+2+3;\n}";varresult=format(code);console.log(result);

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Using the Compiler API (TypeScript 1.4)

Mohamed Hegazy edited this page Apr 2, 2015 · 4 revisions

Getting set up

First you'll need to install TypeScript ==1.4 from npm.

For API Samples compatible with TypeScript >= 1.5 please see Using the Compiler API

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

Once that's done, just grab our definitions file either

  • By using tsd with the command tsd query typescript --action install.
  • Going directly to the source on our repository.

For a overview of the general TypeScript compiler architecture and layering, see Architectural Overview

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that can compile a TypeScript string to its corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctioncompile(filenames: string[],options: ts.CompilerOptions): void{varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(filenames,options,host);varchecker=ts.createTypeChecker(program,/*produceDiagnostics*/true);varresult=checker.emitFiles();varallDiagnostics=program.getDiagnostics().concat(checker.getDiagnostics()).concat(result.diagnostics);allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(`${diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});console.log(`Process exiting with code '${result.emitResultStatus}'.`);process.exit(result.emitResultStatus);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may not want to actually do traditional reads and writes from the file system; for instance, you may have a text buffer for your TypeScript input, and you may want to send/store the resulting JavaScript as JSON. What's more, you may want to use/modify the resulting JavaScript in some way. In such a case, you will need to provide your own CompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");importfs= require("fs");importpath= require("path");functiontransform(contents: string,libSource: string,compilerOptions: ts.CompilerOptions={}){// Generated outputsvaroutputs=[];// Create a compilerHost object to allow the compiler to read and write filesvarcompilerHost={getSourceFile: function(filename,languageVersion){if(filename==="file.ts")returnts.createSourceFile(filename,contents,compilerOptions.target,"0");if(filename==="lib.d.ts")returnts.createSourceFile(filename,libSource,compilerOptions.target,"0");returnundefined;},writeFile: function(name,text,writeByteOrderMark){outputs.push({name: name,text: text,writeByteOrderMark: writeByteOrderMark});},getDefaultLibFilename: function(){return"lib.d.ts";},useCaseSensitiveFileNames: function(){returnfalse;},getCanonicalFileName: function(filename){returnfilename;},getCurrentDirectory: function(){return"";},getNewLine: function(){return"\n";}};// Create a program from inputsvarprogram=ts.createProgram(["file.ts"],compilerOptions,compilerHost);// Query for early errorsvarerrors=program.getDiagnostics();// Do not generate code in the presence of early errorsif(!errors.length){// Type check and get semantic errorsvarchecker=program.getTypeChecker(true);errors=checker.getDiagnostics();// Generate outputchecker.emitFiles();}return{outputs: outputs,errors: errors.map(function(e){returne.file.filename+"("+e.file.getLineAndCharacterFromPosition(e.start).line+"): "+e.messageText;})};}// Calling our transform function using a simple TypeScript variable declarations, // and loading the default library like:varsource="var x: number = 'string'";varlibSource=fs.readFileSync(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts')).toString();varresult=transform(source,libSource);console.log(JSON.stringify(result));

will generate the following output:

{
"outputs": [
{
"name": "file.js",
"text": "var x = 'string';\n"
}
],
"errors": [
"file.ts(1): Type 'string' is not assignable to type 'number'."
]
}

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
varifStatement=(<ts.IfStatement>node);if(ifStatement.thenStatement.kind!==ts.SyntaxKind.Block){report(ifStatement.thenStatement,"An if statement's contents should be wrapped in a block body.");}if(ifStatement.elseStatement&&ifStatement.elseStatement.kind!==ts.SyntaxKind.Block&&ifStatement.elseStatement.kind!==ts.SyntaxKind.IfStatement){report(ifStatement.elseStatement,"An else statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.BinaryExpression:
varop=(<ts.BinaryExpression>node).operator;if(op===ts.SyntaxKind.EqualsEqualsToken||op===ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){varlineChar=sourceFile.getLineAndCharacterFromPosition(node.getStart());console.log(`${sourceFile.filename} (${lineChar.line},${lineChar.character}): ${message}`)}}varfileNames=process.argv.slice(2);varoptions: ts.CompilerOptions={target: ts.ScriptTarget.ES6,module: ts.ModuleKind.AMD};varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(fileNames,options,host);program.getSourceFiles().forEach(delint);

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional set of utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and update the only the outputs of the file that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version, allows the language service to track changes to files. isOpen, tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importfs= require("fs");importts= require("typescript");importpath= require("path");functionwatch(filenames: string[],options: ts.CompilerOptions){varfiles: ts.Map<{version: number;text: string;}>={};// Add the default library filefilenames.unshift(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts'));// initialize the list of filesfilenames.forEach(filename=>{files[filename]={version: 0,text: fs.readFileSync(filename).toString()};});// Create the language service host to allow the LS to communicate with the hostvarservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>filenames,getScriptVersion: (filename)=>files[filename]&&files[filename].version.toString(),getScriptSnapshot: (filename)=>{varfile=files[filename];return{getText: (start,end)=>file.text.substring(start,end),getLength: ()=>file.text.length,getLineStartPositions: ()=>[],getChangeRange: (oldSnapshot)=>undefined};},getCurrentDirectory: ()=>process.cwd(),getScriptIsOpen: ()=>true,getCompilationSettings: ()=>options,getDefaultLibFilename:(options)=>'lib.d.ts',log: (message)=>console.log(message)};// Create the language service filesvarservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesfilenames.forEach(filename=>{// First time around, emit all filesemitFile(filename);// Add a watch on the file to handle next changefs.watchFile(filename,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}varfile=files[filename];// Update the version to signal a change in the filefile.version++;// Clear the text to force a new readfile.text=fs.readFileSync(filename).toString();// write the changes to diskemitFile(filename);});});functionemitFile(filename: string){varoutput=services.getEmitOutput(filename);if(output.emitOutputStatus===ts.EmitReturnStatus.Succeeded){console.log(`Emitting ${filename}`);}else{console.log(`Emitting ${filename} failed`);varallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(filename)).concat(services.getSemanticDiagnostics(filename));allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(` ${diagnostic.file&&diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});}output.outputFiles.forEach(o=>{fs.writeFileSync(o.name,o.text,"utf8");});}}// Initialize files constituting the program as all .ts files in the current directoryvarcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(filename=>filename.length>=3&&filename.substr(filename.length-3,3)===".ts");//map(filename => path.join(process.cwd(), filename));// Start the watcherwatch(currentDirectoryFiles,{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){varoptions=getDefaultOptions();// Parse the source textvarsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,"0");fixupParentReferences(sourceFile);// Get the formatting edits on the input sourcesvaredits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.varruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textvarresult=text;for(vari=edits.length-1;i>=0;i--){varchange=edits[i];varhead=result.slice(0,change.span.start());vartail=result.slice(change.span.start()+change.span.length())result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}functionfixupParentReferences(sourceFile: ts.SourceFile){varparent: ts.Node=sourceFile;functionwalk(n: ts.Node): void{n.parent=parent;varsaveParent=parent;parent=n;ts.forEachChild(n,walk);parent=saveParent;}ts.forEachChild(sourceFile,walk);}}varcode="var a=function(v:number){return 0+1+2+3;\n}";varresult=format(code);console.log(result);

Clone this wiki locally

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Using the Compiler API (TypeScript 1.4)

Mohamed Hegazy edited this page Apr 2, 2015 · 4 revisions

Getting set up

First you'll need to install TypeScript ==1.4 from npm.

For API Samples compatible with TypeScript >= 1.5 please see Using the Compiler API

Once that's done, you'll need to link it from wherever your project resides. If you don't link from within a Node project, it will just link globally.

npm install -g typescript
npm link typescript

Once that's done, just grab our definitions file either

  • By using tsd with the command tsd query typescript --action install.
  • Going directly to the source on our repository.

For a overview of the general TypeScript compiler architecture and layering, see Architectural Overview

That's it, you're ready to go. Now you can try out some of the following examples.

A minimal compiler

Let's try to write a barebones compiler that can compile a TypeScript string to its corresponding JavaScript. We will need to create a Program. This is as simple as calling createProgram. createProgram abstracts any interaction with the underlying system in the CompilerHost interface. The CompilerHost allows the compiler to read and write files, get the current directory, ensure that files and directories exist, and query some of the underlying system properties such as case sensitivity and new line characters. For convenience, we expose a function to create a default host using createCompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctioncompile(filenames: string[],options: ts.CompilerOptions): void{varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(filenames,options,host);varchecker=ts.createTypeChecker(program,/*produceDiagnostics*/true);varresult=checker.emitFiles();varallDiagnostics=program.getDiagnostics().concat(checker.getDiagnostics()).concat(result.diagnostics);allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(`${diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});console.log(`Process exiting with code '${result.emitResultStatus}'.`);process.exit(result.emitResultStatus);}compile(process.argv.slice(2),{noEmitOnError: true,noImplicitAny: true,target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

A simple transform function

Creating a compiler is simple enough, but you may not want to actually do traditional reads and writes from the file system; for instance, you may have a text buffer for your TypeScript input, and you may want to send/store the resulting JavaScript as JSON. What's more, you may want to use/modify the resulting JavaScript in some way. In such a case, you will need to provide your own CompilerHost.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");importfs= require("fs");importpath= require("path");functiontransform(contents: string,libSource: string,compilerOptions: ts.CompilerOptions={}){// Generated outputsvaroutputs=[];// Create a compilerHost object to allow the compiler to read and write filesvarcompilerHost={getSourceFile: function(filename,languageVersion){if(filename==="file.ts")returnts.createSourceFile(filename,contents,compilerOptions.target,"0");if(filename==="lib.d.ts")returnts.createSourceFile(filename,libSource,compilerOptions.target,"0");returnundefined;},writeFile: function(name,text,writeByteOrderMark){outputs.push({name: name,text: text,writeByteOrderMark: writeByteOrderMark});},getDefaultLibFilename: function(){return"lib.d.ts";},useCaseSensitiveFileNames: function(){returnfalse;},getCanonicalFileName: function(filename){returnfilename;},getCurrentDirectory: function(){return"";},getNewLine: function(){return"\n";}};// Create a program from inputsvarprogram=ts.createProgram(["file.ts"],compilerOptions,compilerHost);// Query for early errorsvarerrors=program.getDiagnostics();// Do not generate code in the presence of early errorsif(!errors.length){// Type check and get semantic errorsvarchecker=program.getTypeChecker(true);errors=checker.getDiagnostics();// Generate outputchecker.emitFiles();}return{outputs: outputs,errors: errors.map(function(e){returne.file.filename+"("+e.file.getLineAndCharacterFromPosition(e.start).line+"): "+e.messageText;})};}// Calling our transform function using a simple TypeScript variable declarations, // and loading the default library like:varsource="var x: number = 'string'";varlibSource=fs.readFileSync(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts')).toString();varresult=transform(source,libSource);console.log(JSON.stringify(result));

will generate the following output:

{
"outputs": [
{
"name": "file.js",
"text": "var x = 'string';\n"
}
],
"errors": [
"file.ts(1): Type 'string' is not assignable to type 'number'."
]
}

Traversing the AST with a little linter

As mentioned above, the Node interface is the root of our AST. Generally, we use the forEachChild function in a recursive manner to traverse. This subsumes the visitor pattern and often gives more flexibility.

As an example of how one could traverse the AST, consider a minimal linter that does the following:

  • Checks that all looping construct bodies are enclosed by curly braces.
  • Checks that all if/else bodies are enclosed by curly braces.
  • The "stricter" equality operators (===/!==) are used instead of the "loose" ones (==/!=).
/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");exportfunctiondelint(sourceFile: ts.SourceFile){delintNode(sourceFile);functiondelintNode(node: ts.Node){switch(node.kind){casets.SyntaxKind.ForStatement:
casets.SyntaxKind.ForInStatement:
casets.SyntaxKind.WhileStatement:
casets.SyntaxKind.DoStatement:
if((<ts.IterationStatement>node).statement.kind!==ts.SyntaxKind.Block){report(node,"A looping statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.IfStatement:
varifStatement=(<ts.IfStatement>node);if(ifStatement.thenStatement.kind!==ts.SyntaxKind.Block){report(ifStatement.thenStatement,"An if statement's contents should be wrapped in a block body.");}if(ifStatement.elseStatement&&ifStatement.elseStatement.kind!==ts.SyntaxKind.Block&&ifStatement.elseStatement.kind!==ts.SyntaxKind.IfStatement){report(ifStatement.elseStatement,"An else statement's contents should be wrapped in a block body.");}break;casets.SyntaxKind.BinaryExpression:
varop=(<ts.BinaryExpression>node).operator;if(op===ts.SyntaxKind.EqualsEqualsToken||op===ts.SyntaxKind.ExclamationEqualsToken){report(node,"Use '===' and '!=='.")}break;}ts.forEachChild(node,delintNode);}functionreport(node: ts.Node,message: string){varlineChar=sourceFile.getLineAndCharacterFromPosition(node.getStart());console.log(`${sourceFile.filename} (${lineChar.line},${lineChar.character}): ${message}`)}}varfileNames=process.argv.slice(2);varoptions: ts.CompilerOptions={target: ts.ScriptTarget.ES6,module: ts.ModuleKind.AMD};varhost=ts.createCompilerHost(options);varprogram=ts.createProgram(fileNames,options,host);program.getSourceFiles().forEach(delint);

In this example, we did not need to create a type checker because all we wanted to do was traverse each SourceFile.

Incremental build support using the language services

Please refer to the Using the Language Service API page for more details.

The services layer provide a set of additional set of utilities that can help simplify some complex scenarios. In the snippet below, we will try to build an incremental build server that watches a set of files and update the only the outputs of the file that changed. We will achieve this through creating a LanguageService object. Similar to the program in the previous example, we need a LanguageServiceHost. The LanguageServiceHost augments the concept of a file with a version, isOpen flag, and a ScriptSnapshot. Version, allows the language service to track changes to files. isOpen, tells the language service to keep AST in memory as the file is in use. ScriptSnapshot is an abstraction over text that allows the language service to query for changes.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importfs= require("fs");importts= require("typescript");importpath= require("path");functionwatch(filenames: string[],options: ts.CompilerOptions){varfiles: ts.Map<{version: number;text: string;}>={};// Add the default library filefilenames.unshift(path.join(path.dirname(require.resolve('typescript')),'lib.d.ts'));// initialize the list of filesfilenames.forEach(filename=>{files[filename]={version: 0,text: fs.readFileSync(filename).toString()};});// Create the language service host to allow the LS to communicate with the hostvarservicesHost: ts.LanguageServiceHost={getScriptFileNames: ()=>filenames,getScriptVersion: (filename)=>files[filename]&&files[filename].version.toString(),getScriptSnapshot: (filename)=>{varfile=files[filename];return{getText: (start,end)=>file.text.substring(start,end),getLength: ()=>file.text.length,getLineStartPositions: ()=>[],getChangeRange: (oldSnapshot)=>undefined};},getCurrentDirectory: ()=>process.cwd(),getScriptIsOpen: ()=>true,getCompilationSettings: ()=>options,getDefaultLibFilename:(options)=>'lib.d.ts',log: (message)=>console.log(message)};// Create the language service filesvarservices=ts.createLanguageService(servicesHost,ts.createDocumentRegistry())// Now let's watch the filesfilenames.forEach(filename=>{// First time around, emit all filesemitFile(filename);// Add a watch on the file to handle next changefs.watchFile(filename,{persistent: true,interval: 250},(curr,prev)=>{// Check timestampif(+curr.mtime<=+prev.mtime){return;}varfile=files[filename];// Update the version to signal a change in the filefile.version++;// Clear the text to force a new readfile.text=fs.readFileSync(filename).toString();// write the changes to diskemitFile(filename);});});functionemitFile(filename: string){varoutput=services.getEmitOutput(filename);if(output.emitOutputStatus===ts.EmitReturnStatus.Succeeded){console.log(`Emitting ${filename}`);}else{console.log(`Emitting ${filename} failed`);varallDiagnostics=services.getCompilerOptionsDiagnostics().concat(services.getSyntacticDiagnostics(filename)).concat(services.getSemanticDiagnostics(filename));allDiagnostics.forEach(diagnostic=>{varlineChar=diagnostic.file.getLineAndCharacterFromPosition(diagnostic.start);console.log(` ${diagnostic.file&&diagnostic.file.filename} (${lineChar.line},${lineChar.character}): ${diagnostic.messageText}`);});}output.outputFiles.forEach(o=>{fs.writeFileSync(o.name,o.text,"utf8");});}}// Initialize files constituting the program as all .ts files in the current directoryvarcurrentDirectoryFiles=fs.readdirSync(process.cwd()).filter(filename=>filename.length>=3&&filename.substr(filename.length-3,3)===".ts");//map(filename => path.join(process.cwd(), filename));// Start the watcherwatch(currentDirectoryFiles,{target: ts.ScriptTarget.ES5,module: ts.ModuleKind.CommonJS});

Pretty printer using the LS Formatter

The formatting interfaces used here are part of the typescript 1.4 package but is not currently exposed in the public typescript.d.ts. The typings should be exposed in the next release.

/// <reference path="typings/node/node.d.ts" />/// <reference path="typings/typescript/typescript.d.ts" />importts= require("typescript");// Note: this uses ts.formatting which is part of the typescript 1.4 package but is not currently // exposed in the public typescript.d.ts. The typings should be exposed in the next release. functionformat(text: string){varoptions=getDefaultOptions();// Parse the source textvarsourceFile=ts.createSourceFile("file.ts",text,ts.ScriptTarget.Latest,"0");fixupParentReferences(sourceFile);// Get the formatting edits on the input sourcesvaredits=(<any>ts).formatting.formatDocument(sourceFile,getRuleProvider(options),options);// Apply the edits on the input codereturnapplyEdits(text,edits);functiongetRuleProvider(options: ts.FormatCodeOptions){// Share this between multiple formatters using the same options.// This represents the bulk of the space the formatter uses.varruleProvider=new(<any>ts).formatting.RulesProvider();ruleProvider.ensureUpToDate(options);returnruleProvider;}functionapplyEdits(text: string,edits: ts.TextChange[]): string{// Apply edits in reverse on the existing textvarresult=text;for(vari=edits.length-1;i>=0;i--){varchange=edits[i];varhead=result.slice(0,change.span.start());vartail=result.slice(change.span.start()+change.span.length())result=head+change.newText+tail;}returnresult;}functiongetDefaultOptions(): ts.FormatCodeOptions{return{IndentSize: 4,TabSize: 4,NewLineCharacter: '\r\n',ConvertTabsToSpaces: true,InsertSpaceAfterCommaDelimiter: true,InsertSpaceAfterSemicolonInForStatements: true,InsertSpaceBeforeAndAfterBinaryOperators: true,InsertSpaceAfterKeywordsInControlFlowStatements: true,InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false,InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,PlaceOpenBraceOnNewLineForFunctions: false,PlaceOpenBraceOnNewLineForControlBlocks: false,};}functionfixupParentReferences(sourceFile: ts.SourceFile){varparent: ts.Node=sourceFile;functionwalk(n: ts.Node): void{n.parent=parent;varsaveParent=parent;parent=n;ts.forEachChild(n,walk);parent=saveParent;}ts.forEachChild(sourceFile,walk);}}varcode="var a=function(v:number){return 0+1+2+3;\n}";varresult=format(code);console.log(result);

Clone this wiki locally