Add a compiler module

This change adds a simple compiler module that hosts TypeScript and
compiles a program.  The compile function takes a path and optional options
data structure; the path can be one of three things: 1) a path to a single `*.ts`
file, in which case it, and it alone, is compiled; 2) a path to a `tsconfig.json`
file, in which case it is loaded, parsed, and used to drive compilation just
like the `tsc` command line driver; or 3) a path to a directory containing a
`tsconfig.json` file, in which case it is discovered and used just like #2.

There is also a new command line driver that just blindly passes arguments to
this compiler API, and prints the results.  Next up, this will begin lowering and
transforming the resulting TypeScript AST to MuPack and MuIL.

Note that I've reorganized the source code slightly, so that instead of just
`src/*`, we now have `lib/*` for the library, `cmd/*` for any CLI drivers, and,
soon, `test/*` for test cases.
This commit is contained in:
joeduffy 2016-12-31 11:23:20 -08:00
parent 366648d1d4
commit 4de470af46
10 changed files with 270 additions and 8 deletions

74
tools/mujs/cmd/index.ts Normal file
View file

@ -0,0 +1,74 @@
// Copyright 2016 Marapongo. All rights reserved.
"use strict";
import * as minimist from "minimist";
import {log} from "nodets";
import "source-map-support/register"; // just for side-effects.
import * as mujs from "../lib";
async function main(args: string[]): Promise<number> {
// Parse options.
let failed: boolean = false;
let parsed: minimist.ParsedArgs = minimist(args, {
boolean: [ "debug", "verbose" ],
string: [ "loglevel" ],
alias: {
"ll": "loglevel",
},
unknown: (arg: string) => {
if (arg[0] === "-") {
console.error(`Unrecognized option '${arg}'`);
failed = true;
return false;
}
return true;
},
});
if (failed) {
return -2;
}
args = parsed._;
// If higher logging levels were requested, set them.
if (parsed["debug"]) {
log.configure(7);
}
else if (parsed["verbose"]) {
log.configure(5);
}
else if (parsed["loglevel"]) {
let ll: number = parseInt(parsed["loglevel"], 10);
log.configure(ll);
}
// Now check for required arguments.
let path: string =
args.length > 0 ?
args[0] :
// Default to pwd if no argument was supplied.
process.cwd();
let comp: mujs.compiler.ICompilation = await mujs.compiler.compile(path);
if (comp.diagnostics.length > 0) {
// If any errors occurred, print them out, and skip pretty-printing the AST.
console.log(mujs.compiler.formatDiagnostics(comp));
}
else {
// No errors, great, transform the AST into a MuPack program, and print it.
// TODO(joe): do this.
}
return 0;
}
// Fire off the main process, and log any errors that go unhandled.
main(process.argv.slice(2)).then(
(code: number) => process.exit(code),
(err: Error) => {
console.error("Unhandled exception:");
console.error(err.stack);
process.exit(-1);
},
);

View file

@ -0,0 +1,163 @@
// Copyright 2016 Marapongo. All rights reserved.
"use strict";
import { fs, log } from "nodets";
import * as os from "os";
import * as fspath from "path";
import * as ts from "typescript";
const TS_PROJECT_FILE = "tsconfig.json";
export interface ICompilation {
root: string; // the root directory for the compilation.
program: ts.Program | undefined; // the resulting TypeScript program object.
diagnostics: ts.Diagnostic[]; // any diagnostics resulting from compilation.
}
// Compiles a TypeScript program and returns its output. The path can be one of three things: 1) a single TypeScript
// file (`*.ts`), 2) a TypeScript project file (`tsconfig.json`), or 3) a directory containing a TypeScript project
// file. An optional set of compiler options may also be supplied. In the project file cases, both options and files
// are read in the from the project file, and will override any options passed in the argument form.
export async function compile(path: string, options?: ts.CompilerOptions): Promise<ICompilation> {
// Default the options to TypeScript's usual defaults if not provided.
options = options || ts.getDefaultCompilerOptions();
// See if we"re dealing with a tsproject.json file. This happens when path directly points to one, or when
// path refers to a directory, in which case we will assume we"re searching for a config file underneath it.
let root: string | undefined;
let configPath: string | undefined;
if (fspath.basename(path) === TS_PROJECT_FILE) {
configPath = path;
root = fspath.dirname(path);
}
else if ((await fs.lstat(path)).isDirectory()) {
configPath = fspath.join(path, TS_PROJECT_FILE);
root = path;
}
else {
root = fspath.dirname(path);
}
let files: string[] = [];
let diagnostics: ts.Diagnostic[] = [];
if (configPath) {
// A config file is suspected; try to load it up and parse its contents.
let config: any = JSON.parse(await fs.readFile(configPath));
if (!config) {
throw new Error(`No ${TS_PROJECT_FILE} found underneath the path ${path}`);
}
const parseConfigHost: ts.ParseConfigHost = new ParseConfigHost();
const parsedConfig: ts.ParsedCommandLine = ts.parseJsonConfigFileContent(
config, parseConfigHost, root, options);
if (parsedConfig.errors.length > 0) {
diagnostics = diagnostics.concat(parsedConfig.errors);
}
if (parsedConfig.options) {
options = parsedConfig.options;
}
if (parsedConfig.fileNames) {
files = files.concat(parsedConfig.fileNames);
}
} else {
// Otherwise, assume it's a single file, and populate the paths with it.
files.push(path);
}
// Many options can be supplied, however, we want to hook the outputs to translate them on the fly.
options.rootDir = root;
options.outDir = undefined;
options.declaration = false;
if (log.v(5)) {
log.out(5).infof(`files: ${JSON.stringify(files)}`);
log.out(5).infof(`options: ${JSON.stringify(options, null, 4)}`);
}
if (log.v(7)) {
options.traceResolution = true;
}
let program: ts.Program | undefined;
if (diagnostics.length === 0) {
// Create a compiler host and perform the compilation.
const host: ts.CompilerHost = ts.createCompilerHost(options);
host.writeFile = (_: string, __: string, ___: boolean) => { /*ignore outputs*/ };
program = ts.createProgram(files, options, host);
// Concatenate all of the diagnostics into a single array.
diagnostics = diagnostics.concat(program.getSyntacticDiagnostics());
if (diagnostics.length === 0) {
diagnostics = diagnostics.concat(program.getOptionsDiagnostics());
diagnostics = diagnostics.concat(program.getGlobalDiagnostics());
if (diagnostics.length === 0) {
diagnostics = diagnostics.concat(program.getSemanticDiagnostics());
diagnostics = diagnostics.concat(program.getDeclarationDiagnostics());
}
}
// Now perform the creation of the AST data structures.
const emitOutput: ts.EmitResult = program.emit();
diagnostics = diagnostics.concat(emitOutput.diagnostics);
}
return {
root: root,
program: program,
diagnostics: diagnostics,
};
}
class ParseConfigHost implements ts.ParseConfigHost {
public readonly useCaseSensitiveFileNames = isFilesystemCaseSensitive();
public readDirectory(path: string, extensions: string[], exclude: string[], include: string[]): string[] {
return ts.sys.readDirectory(path, extensions, exclude, include);
}
public fileExists(path: string): boolean {
return ts.sys.fileExists(path);
}
public readFile(path: string): string {
return ts.sys.readFile(path);
}
}
export function formatDiagnostics(comp: ICompilation): string {
// TODO: implement colorization and fancy source context pretty-printing.
return ts.formatDiagnostics(comp.diagnostics, new FormatDiagnosticsHost(comp.root));
}
class FormatDiagnosticsHost implements ts.FormatDiagnosticsHost {
private readonly cwd: string;
constructor(cwd: string) {
this.cwd = cwd;
}
public getCurrentDirectory(): string {
return this.cwd;
}
public getNewLine(): string {
return os.EOL;
}
public getCanonicalFileName(filename: string): string {
if (isFilesystemCaseSensitive()) {
return filename;
}
else {
return filename.toLowerCase();
}
}
}
function isFilesystemCaseSensitive(): boolean {
let platform: string = os.platform();
return platform === "win32" || platform === "win64";
}

View file

@ -0,0 +1,4 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
export * from "./compile";

View file

@ -1,7 +1,7 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
import * as il from "./il";
import * as compiler from "./compiler";
import * as pack from "./pack";
import * as symbols from "./symbols";
export { il, pack, symbols };
export { compiler, pack, symbols };

View file

@ -7,14 +7,17 @@
"main": "bin/lib/index.js",
"typings": "bin/lib/index.d.ts",
"scripts": {
"build": "tsc && tslint src/*.ts src/**/*.ts",
"build": "tsc && tslint cmd/*.ts cmd/**/*.ts lib/*.ts lib/**/*.ts",
"postinstall": "typings install",
"test": "npm run cov && npm run covreport",
"cov": "istanbul cover --print none node_modules/.bin/_mocha -- --recursive --es_staging --timeout 15000 bin/tests/",
"covreport": "istanbul report text-summary && istanbul report text"
},
"dependencies": {
"@types/minimist": "^1.2.0",
"@types/node": "^6.0.55",
"minimist": "^1.2.0",
"source-map-support": "^0.4.8",
"typescript": "^2.1.4"
},
"devDependencies": {

View file

@ -18,10 +18,14 @@
"strictNullChecks": true
},
"files": [
"src/index.ts",
"src/il/index.ts",
"src/pack/index.ts",
"src/symbols/index.ts"
"cmd/index.ts",
"lib/index.ts",
"lib/compiler/index.ts",
"lib/compiler/compile.ts",
"lib/il/index.ts",
"lib/pack/index.ts",
"lib/symbols/index.ts"
]
}

View file

@ -1,5 +1,9 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/minimist":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
"@types/node":
version "6.0.55"
resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.55.tgz#e5cb679a43561f42afd1bd6d58d3992ec8f31720"
@ -295,7 +299,7 @@ minimatch@^3.0.2, "minimatch@2 || 3":
dependencies:
brace-expansion "^1.0.0"
minimist@^1.2.0:
minimist, minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
@ -454,6 +458,16 @@ slide@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
source-map-support:
version "0.4.8"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.8.tgz#4871918d8a3af07289182e974e32844327b2e98b"
dependencies:
source-map "^0.5.3"
source-map@^0.5.3:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
sprintf-js@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"