mirror of https://github.com/denoland/deno.git
377 lines
8.8 KiB
JavaScript
377 lines
8.8 KiB
JavaScript
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
|
|
|
// Because we're bootstrapping the TypeScript compiler without dependencies on
|
|
// Node, this is written in JavaScript, but leverages JSDoc that can be
|
|
// understood by the TypeScript language service, so it allows type safety
|
|
// checking in VSCode.
|
|
|
|
const ASSETS = "$asset$";
|
|
|
|
/**
|
|
* @param {string} configText
|
|
* @param {Array<string>} rootNames
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
function main(configText, rootNames) {
|
|
ops = Deno.core.ops();
|
|
println(`>>> ts version ${ts.version}`);
|
|
println(`>>> rootNames ${rootNames}`);
|
|
|
|
const host = new Host();
|
|
|
|
assert(rootNames.length > 0);
|
|
|
|
const { options, diagnostics } = configure(configText);
|
|
handleDiagnostics(host, diagnostics);
|
|
|
|
println(`>>> TS config: ${JSON.stringify(options)}`);
|
|
|
|
const program = ts.createProgram(rootNames, options, host);
|
|
|
|
handleDiagnostics(
|
|
host,
|
|
ts.getPreEmitDiagnostics(program).filter(({ code }) => {
|
|
// TS1063: An export assignment cannot be used in a namespace.
|
|
if (code === 1063) return false;
|
|
// TS2691: An import path cannot end with a '.ts' extension. Consider
|
|
// importing 'bad-module' instead.
|
|
if (code === 2691) return false;
|
|
// TS5009: Cannot find the common subdirectory path for the input files.
|
|
if (code === 5009) return false;
|
|
return true;
|
|
})
|
|
);
|
|
|
|
const emitResult = program.emit();
|
|
handleDiagnostics(host, emitResult.diagnostics);
|
|
|
|
dispatch(
|
|
"setEmitResult",
|
|
Object.assign(emitResult, { tsVersion: ts.version })
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {...string} s
|
|
*/
|
|
function println(...s) {
|
|
Deno.core.print(s.join(" ") + "\n");
|
|
}
|
|
|
|
/**
|
|
* @returns {never}
|
|
*/
|
|
function unreachable() {
|
|
throw Error("unreachable");
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} cond
|
|
* @returns {asserts cond}
|
|
*/
|
|
function assert(cond) {
|
|
if (!cond) {
|
|
throw Error("assert");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Uint8Array | null} ui8
|
|
*/
|
|
function decodeAscii(ui8) {
|
|
let out = "";
|
|
if (!ui8) {
|
|
return out;
|
|
}
|
|
for (let i = 0; i < ui8.length; i++) {
|
|
out += String.fromCharCode(ui8[i]);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* @param {string} str
|
|
*/
|
|
function encode(str) {
|
|
const charCodes = str.split("").map(c => c.charCodeAt(0));
|
|
const ui8 = new Uint8Array(charCodes);
|
|
return ui8;
|
|
}
|
|
|
|
//
|
|
/** **Warning!** Op ids must be acquired from Rust using `Deno.core.ops()`
|
|
* before dispatching any action.
|
|
* @type {Record<string, number>}
|
|
*/
|
|
let ops;
|
|
|
|
/**
|
|
* @type {Map<string, string>}
|
|
*/
|
|
const moduleMap = new Map();
|
|
|
|
const externalSpecifierRegEx = /^file:\/{3}\S+\/js(\/\S+\.ts)$/;
|
|
|
|
/**
|
|
* This is a minimal implementation of a compiler host to be able to allow the
|
|
* creation of runtime bundles. Some of the methods are implemented in a way
|
|
* to just appease the TypeScript compiler, not to necessarily be a general
|
|
* purpose implementation.
|
|
*
|
|
* @implements {ts.CompilerHost}
|
|
*/
|
|
class Host {
|
|
/**
|
|
* @param {string} _fileName
|
|
*/
|
|
fileExists(_fileName) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {string} _fileName
|
|
*/
|
|
readFile(_fileName) {
|
|
unreachable();
|
|
return undefined;
|
|
}
|
|
|
|
useCaseSensitiveFileNames() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {ts.CompilerOptions} _options
|
|
*/
|
|
getDefaultLibFileName(_options) {
|
|
return "lib.deno_core.d.ts";
|
|
}
|
|
|
|
getDefaultLibLocation() {
|
|
return ASSETS;
|
|
}
|
|
|
|
getCurrentDirectory() {
|
|
return ".";
|
|
}
|
|
|
|
/**
|
|
* @param {string} fileName
|
|
* @param {ts.ScriptTarget} languageVersion
|
|
* @param {(message: string) => void} _onError
|
|
* @param {boolean} shouldCreateNewSourceFile
|
|
*/
|
|
getSourceFile(
|
|
fileName,
|
|
languageVersion,
|
|
_onError,
|
|
shouldCreateNewSourceFile
|
|
) {
|
|
assert(!shouldCreateNewSourceFile); // We haven't yet encountered this.
|
|
|
|
// This hacks around the fact that TypeScript tries to magically guess the
|
|
// d.ts filename.
|
|
if (fileName.startsWith("$typeRoots$")) {
|
|
assert(fileName.startsWith("$typeRoots$/"));
|
|
assert(fileName.endsWith("/index.d.ts"));
|
|
fileName = fileName
|
|
.replace("$typeRoots$/", "")
|
|
.replace("/index.d.ts", "");
|
|
}
|
|
|
|
// This looks up any modules that have been mapped to internal names
|
|
if (moduleMap.has(fileName)) {
|
|
fileName = moduleMap.get(fileName);
|
|
}
|
|
|
|
const { sourceCode, moduleName } = dispatch("readFile", {
|
|
fileName,
|
|
languageVersion,
|
|
shouldCreateNewSourceFile
|
|
});
|
|
|
|
// If we match the external specifier regex, we will then create an internal
|
|
// specifier and then use that when creating the source file
|
|
let internalModuleName = moduleName;
|
|
const result = externalSpecifierRegEx.exec(moduleName);
|
|
if (result) {
|
|
const [, specifier] = result;
|
|
const internalSpecifier = `$deno$${specifier}`;
|
|
moduleMap.set(internalSpecifier, moduleName);
|
|
internalModuleName = internalSpecifier;
|
|
}
|
|
|
|
const sourceFile = ts.createSourceFile(
|
|
internalModuleName,
|
|
sourceCode,
|
|
languageVersion
|
|
);
|
|
sourceFile.moduleName = internalModuleName;
|
|
return sourceFile;
|
|
}
|
|
|
|
/**
|
|
* @param {string} fileName
|
|
* @param {string} data
|
|
* @param {boolean} _writeByteOrderMark
|
|
* @param {((message: string) => void)?} _onError
|
|
* @param {ReadonlyArray<ts.SourceFile>?} sourceFiles
|
|
*/
|
|
writeFile(
|
|
fileName,
|
|
data,
|
|
_writeByteOrderMark,
|
|
_onError = null,
|
|
sourceFiles = null
|
|
) {
|
|
if (sourceFiles == null) {
|
|
return;
|
|
}
|
|
const moduleName = sourceFiles[sourceFiles.length - 1].moduleName;
|
|
return dispatch("writeFile", { fileName, moduleName, data });
|
|
}
|
|
|
|
/**
|
|
* @param {string} _fileName
|
|
* @param {ts.Path} _path
|
|
* @param {ts.ScriptTarget} _languageVersion
|
|
* @param {*} _onError
|
|
* @param {boolean} _shouldCreateNewSourceFile
|
|
*/
|
|
getSourceFileByPath(
|
|
_fileName,
|
|
_path,
|
|
_languageVersion,
|
|
_onError,
|
|
_shouldCreateNewSourceFile
|
|
) {
|
|
unreachable();
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* @param {string} fileName
|
|
*/
|
|
getCanonicalFileName(fileName) {
|
|
return fileName;
|
|
}
|
|
|
|
getNewLine() {
|
|
return "\n";
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} moduleNames
|
|
* @param {string} containingFile
|
|
* @return {Array<ts.ResolvedModule | undefined>}
|
|
*/
|
|
resolveModuleNames(moduleNames, containingFile) {
|
|
// If the containing file is an internal specifier, map it back to the
|
|
// external specifier
|
|
containingFile = moduleMap.has(containingFile)
|
|
? moduleMap.get(containingFile)
|
|
: containingFile;
|
|
/** @type {string[]} */
|
|
const resolvedNames = dispatch("resolveModuleNames", {
|
|
moduleNames,
|
|
containingFile
|
|
});
|
|
/** @type {ts.ResolvedModule[]} */
|
|
const r = resolvedNames.map(resolvedFileName => {
|
|
const extension = getExtension(resolvedFileName);
|
|
return { resolvedFileName, extension };
|
|
});
|
|
return r;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} configurationText
|
|
*/
|
|
function configure(configurationText) {
|
|
const { config, error } = ts.parseConfigFileTextToJson(
|
|
"tsconfig.json",
|
|
configurationText
|
|
);
|
|
if (error) {
|
|
return { options: {}, diagnostics: [error] };
|
|
}
|
|
const { options, errors } = ts.convertCompilerOptionsFromJson(
|
|
config.compilerOptions,
|
|
""
|
|
);
|
|
return {
|
|
options,
|
|
diagnostics: errors.length ? errors : undefined
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {string} opName
|
|
* @param {Record<string,any>} obj
|
|
*/
|
|
function dispatch(opName, obj) {
|
|
const opId = ops[opName];
|
|
|
|
if (!opId) {
|
|
throw new Error(`Unknown op: ${opName}`);
|
|
}
|
|
|
|
const s = JSON.stringify(obj);
|
|
const msg = encode(s);
|
|
const resUi8 = Deno.core.dispatch(opId, msg);
|
|
const resStr = decodeAscii(resUi8);
|
|
const res = JSON.parse(resStr);
|
|
if (!res["ok"]) {
|
|
throw Error(`${opName} failed ${res["err"]}. Args: ${JSON.stringify(obj)}`);
|
|
}
|
|
return res["ok"];
|
|
}
|
|
|
|
/**
|
|
* @param {number} code
|
|
*/
|
|
function exit(code) {
|
|
dispatch("exit", { code });
|
|
return unreachable();
|
|
}
|
|
|
|
// Maximum number of diagnostics to display.
|
|
const MAX_ERRORS = 5;
|
|
|
|
/**
|
|
* @param {ts.CompilerHost} host
|
|
* @param {ReadonlyArray<ts.Diagnostic> | undefined} diagnostics
|
|
*/
|
|
function handleDiagnostics(host, diagnostics) {
|
|
if (diagnostics && diagnostics.length) {
|
|
let rest = 0;
|
|
if (diagnostics.length > MAX_ERRORS) {
|
|
rest = diagnostics.length - MAX_ERRORS;
|
|
diagnostics = diagnostics.slice(0, MAX_ERRORS);
|
|
}
|
|
const msg = ts.formatDiagnosticsWithColorAndContext(diagnostics, host);
|
|
println(msg);
|
|
if (rest) {
|
|
println(`And ${rest} other errors.`);
|
|
}
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
/** Returns the TypeScript Extension enum for a given media type.
|
|
* @param {string} fileName
|
|
* @returns {ts.Extension}
|
|
*/
|
|
function getExtension(fileName) {
|
|
if (fileName.endsWith(".d.ts")) {
|
|
return ts.Extension.Dts;
|
|
} else if (fileName.endsWith(".ts")) {
|
|
return ts.Extension.Ts;
|
|
} else if (fileName.endsWith(".js")) {
|
|
return ts.Extension.Js;
|
|
} else {
|
|
throw TypeError(`Cannot resolve extension for ${fileName}`);
|
|
}
|
|
}
|