1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-01 09:24:20 -04:00
denoland-deno/cli/js/compiler_util.ts
2020-03-05 18:48:55 +01:00

464 lines
14 KiB
TypeScript

// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { bold, cyan, yellow } from "./colors.ts";
import { CompilerOptions } from "./compiler_api.ts";
import { buildBundle } from "./compiler_bundler.ts";
import { ConfigureResponse, Host } from "./compiler_host.ts";
import { SourceFile } from "./compiler_sourcefile.ts";
import { sendSync } from "./dispatch_json.ts";
import { atob, TextDecoder, TextEncoder } from "./web/text_encoding.ts";
import { core } from "./core.ts";
import * as util from "./util.ts";
import { assert } from "./util.ts";
import { writeFileSync } from "./write_file.ts";
/** Type for the write fall callback that allows delegation from the compiler
* host on writing files. */
export type WriteFileCallback = (
fileName: string,
data: string,
sourceFiles?: readonly ts.SourceFile[]
) => void;
/** An object which is passed to `createWriteFile` to be used to read and set
* state related to the emit of a program. */
export interface WriteFileState {
type: CompilerRequestType;
bundle?: boolean;
host?: Host;
outFile?: string;
rootNames: string[];
emitMap?: Record<string, string>;
emitBundle?: string;
sources?: Record<string, string>;
}
// Warning! The values in this enum are duplicated in `cli/msg.rs`
// Update carefully!
export enum CompilerRequestType {
Compile = 0,
RuntimeCompile = 1,
RuntimeTranspile = 2
}
export const OUT_DIR = "$deno$";
/** Cache the contents of a file on the trusted side. */
function cache(
moduleId: string,
emittedFileName: string,
contents: string,
checkJs = false
): void {
util.log("compiler::cache", { moduleId, emittedFileName, checkJs });
const sf = SourceFile.get(moduleId);
if (sf) {
// NOTE: If it's a `.json` file we don't want to write it to disk.
// JSON files are loaded and used by TS compiler to check types, but we don't want
// to emit them to disk because output file is the same as input file.
if (sf.extension === ts.Extension.Json) {
return;
}
// NOTE: JavaScript files are only cached to disk if `checkJs`
// option in on
if (sf.extension === ts.Extension.Js && !checkJs) {
return;
}
}
if (emittedFileName.endsWith(".map")) {
// Source Map
sendSync("op_cache", {
extension: ".map",
moduleId,
contents
});
} else if (
emittedFileName.endsWith(".js") ||
emittedFileName.endsWith(".json")
) {
// Compiled JavaScript
sendSync("op_cache", {
extension: ".js",
moduleId,
contents
});
} else {
assert(false, `Trying to cache unhandled file type "${emittedFileName}"`);
}
}
const encoder = new TextEncoder();
const decoder = new TextDecoder();
/** Retrieve an asset from Rust. */
export function getAsset(name: string): string {
const opId = core.ops()["op_fetch_asset"];
// We really don't want to depend on JSON dispatch during snapshotting, so
// this op exchanges strings with Rust as raw byte arrays.
const sourceCodeBytes = core.dispatch(opId, encoder.encode(name));
return decoder.decode(sourceCodeBytes!);
}
/** Generates a `writeFile` function which can be passed to the compiler `Host`
* to use when emitting files. */
export function createWriteFile(state: WriteFileState): WriteFileCallback {
const encoder = new TextEncoder();
if (state.type === CompilerRequestType.Compile) {
return function writeFile(
fileName: string,
data: string,
sourceFiles?: readonly ts.SourceFile[]
): void {
assert(
sourceFiles != null,
`Unexpected emit of "${fileName}" which isn't part of a program.`
);
assert(state.host);
if (!state.bundle) {
assert(sourceFiles.length === 1);
cache(
sourceFiles[0].fileName,
fileName,
data,
state.host.getCompilationSettings().checkJs
);
} else {
// if the fileName is set to an internal value, just noop, this is
// used in the Rust unit tests.
if (state.outFile && state.outFile.startsWith(OUT_DIR)) {
return;
}
// we only support single root names for bundles
assert(
state.rootNames.length === 1,
`Only one root name supported. Got "${JSON.stringify(
state.rootNames
)}"`
);
// this enriches the string with the loader and re-exports the
// exports of the root module
const content = buildBundle(state.rootNames[0], data, sourceFiles);
if (state.outFile) {
const encodedData = encoder.encode(content);
console.warn(`Emitting bundle to "${state.outFile}"`);
writeFileSync(state.outFile, encodedData);
console.warn(`${humanFileSize(encodedData.length)} emitted.`);
} else {
console.log(content);
}
}
};
}
return function writeFile(
fileName: string,
data: string,
sourceFiles?: readonly ts.SourceFile[]
): void {
assert(sourceFiles != null);
assert(state.host);
assert(state.emitMap);
if (!state.bundle) {
assert(sourceFiles.length === 1);
state.emitMap[fileName] = data;
// we only want to cache the compiler output if we are resolving
// modules externally
if (!state.sources) {
cache(
sourceFiles[0].fileName,
fileName,
data,
state.host.getCompilationSettings().checkJs
);
}
} else {
// we only support single root names for bundles
assert(state.rootNames.length === 1);
state.emitBundle = buildBundle(state.rootNames[0], data, sourceFiles);
}
};
}
export interface ConvertCompilerOptionsResult {
files?: string[];
options: ts.CompilerOptions;
}
/** Take a runtime set of compiler options as stringified JSON and convert it
* to a set of TypeScript compiler options. */
export function convertCompilerOptions(
str: string
): ConvertCompilerOptionsResult {
const options: CompilerOptions = JSON.parse(str);
const out: Record<string, unknown> = {};
const keys = Object.keys(options) as Array<keyof CompilerOptions>;
const files: string[] = [];
for (const key of keys) {
switch (key) {
case "jsx":
const value = options[key];
if (value === "preserve") {
out[key] = ts.JsxEmit.Preserve;
} else if (value === "react") {
out[key] = ts.JsxEmit.React;
} else {
out[key] = ts.JsxEmit.ReactNative;
}
break;
case "module":
switch (options[key]) {
case "amd":
out[key] = ts.ModuleKind.AMD;
break;
case "commonjs":
out[key] = ts.ModuleKind.CommonJS;
break;
case "es2015":
case "es6":
out[key] = ts.ModuleKind.ES2015;
break;
case "esnext":
out[key] = ts.ModuleKind.ESNext;
break;
case "none":
out[key] = ts.ModuleKind.None;
break;
case "system":
out[key] = ts.ModuleKind.System;
break;
case "umd":
out[key] = ts.ModuleKind.UMD;
break;
default:
throw new TypeError("Unexpected module type");
}
break;
case "target":
switch (options[key]) {
case "es3":
out[key] = ts.ScriptTarget.ES3;
break;
case "es5":
out[key] = ts.ScriptTarget.ES5;
break;
case "es6":
case "es2015":
out[key] = ts.ScriptTarget.ES2015;
break;
case "es2016":
out[key] = ts.ScriptTarget.ES2016;
break;
case "es2017":
out[key] = ts.ScriptTarget.ES2017;
break;
case "es2018":
out[key] = ts.ScriptTarget.ES2018;
break;
case "es2019":
out[key] = ts.ScriptTarget.ES2019;
break;
case "es2020":
out[key] = ts.ScriptTarget.ES2020;
break;
case "esnext":
out[key] = ts.ScriptTarget.ESNext;
break;
default:
throw new TypeError("Unexpected emit target.");
}
break;
case "types":
const types = options[key];
assert(types);
files.push(...types);
break;
default:
out[key] = options[key];
}
}
return {
options: out as ts.CompilerOptions,
files: files.length ? files : undefined
};
}
/** An array of TypeScript diagnostic types we ignore. */
export const ignoredDiagnostics = [
// TS2306: File 'file:///Users/rld/src/deno/cli/tests/subdir/amd_like.js' is
// not a module.
2306,
// TS1375: 'await' expressions are only allowed at the top level of a file
// when that file is a module, but this file has no imports or exports.
// Consider adding an empty 'export {}' to make this file a module.
1375,
// TS1103: 'for-await-of' statement is only allowed within an async function
// or async generator.
1103,
// TS2691: An import path cannot end with a '.ts' extension. Consider
// importing 'bad-module' instead.
2691,
// TS5009: Cannot find the common subdirectory path for the input files.
5009,
// TS5055: Cannot write file
// 'http://localhost:4545/cli/tests/subdir/mt_application_x_javascript.j4.js'
// because it would overwrite input file.
5055,
// TypeScript is overly opinionated that only CommonJS modules kinds can
// support JSON imports. Allegedly this was fixed in
// Microsoft/TypeScript#26825 but that doesn't seem to be working here,
// so we will ignore complaints about this compiler setting.
5070,
// TS7016: Could not find a declaration file for module '...'. '...'
// implicitly has an 'any' type. This is due to `allowJs` being off by
// default but importing of a JavaScript module.
7016
];
/** When doing a host configuration, processing the response and logging out
* and options which were ignored. */
export function processConfigureResponse(
configResult: ConfigureResponse,
configPath: string
): ts.Diagnostic[] | undefined {
const { ignoredOptions, diagnostics } = configResult;
if (ignoredOptions) {
console.warn(
yellow(`Unsupported compiler options in "${configPath}"\n`) +
cyan(` The following options were ignored:\n`) +
` ${ignoredOptions.map((value): string => bold(value)).join(", ")}`
);
}
return diagnostics;
}
// Constants used by `normalizeString` and `resolvePath`
export const CHAR_DOT = 46; /* . */
export const CHAR_FORWARD_SLASH = 47; /* / */
/** Resolves `.` and `..` elements in a path with directory names */
export function normalizeString(
path: string,
allowAboveRoot: boolean,
separator: string,
isPathSeparator: (code: number) => boolean
): string {
let res = "";
let lastSegmentLength = 0;
let lastSlash = -1;
let dots = 0;
let code: number;
for (let i = 0, len = path.length; i <= len; ++i) {
if (i < len) code = path.charCodeAt(i);
else if (isPathSeparator(code!)) break;
else code = CHAR_FORWARD_SLASH;
if (isPathSeparator(code)) {
if (lastSlash === i - 1 || dots === 1) {
// NOOP
} else if (lastSlash !== i - 1 && dots === 2) {
if (
res.length < 2 ||
lastSegmentLength !== 2 ||
res.charCodeAt(res.length - 1) !== CHAR_DOT ||
res.charCodeAt(res.length - 2) !== CHAR_DOT
) {
if (res.length > 2) {
const lastSlashIndex = res.lastIndexOf(separator);
if (lastSlashIndex === -1) {
res = "";
lastSegmentLength = 0;
} else {
res = res.slice(0, lastSlashIndex);
lastSegmentLength = res.length - 1 - res.lastIndexOf(separator);
}
lastSlash = i;
dots = 0;
continue;
} else if (res.length === 2 || res.length === 1) {
res = "";
lastSegmentLength = 0;
lastSlash = i;
dots = 0;
continue;
}
}
if (allowAboveRoot) {
if (res.length > 0) res += `${separator}..`;
else res = "..";
lastSegmentLength = 2;
}
} else {
if (res.length > 0) res += separator + path.slice(lastSlash + 1, i);
else res = path.slice(lastSlash + 1, i);
lastSegmentLength = i - lastSlash - 1;
}
lastSlash = i;
dots = 0;
} else if (code === CHAR_DOT && dots !== -1) {
++dots;
} else {
dots = -1;
}
}
return res;
}
/** Return the common path shared by the `paths`.
*
* @param paths The set of paths to compare.
* @param sep An optional separator to use. Defaults to `/`.
* @internal
*/
export function commonPath(paths: string[], sep = "/"): string {
const [first = "", ...remaining] = paths;
if (first === "" || remaining.length === 0) {
return first.substring(0, first.lastIndexOf(sep) + 1);
}
const parts = first.split(sep);
let endOfPrefix = parts.length;
for (const path of remaining) {
const compare = path.split(sep);
for (let i = 0; i < endOfPrefix; i++) {
if (compare[i] !== parts[i]) {
endOfPrefix = i;
}
}
if (endOfPrefix === 0) {
return "";
}
}
const prefix = parts.slice(0, endOfPrefix).join(sep);
return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`;
}
/** Utility function to turn the number of bytes into a human readable
* unit */
function humanFileSize(bytes: number): string {
const thresh = 1000;
if (Math.abs(bytes) < thresh) {
return bytes + " B";
}
const units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
let u = -1;
do {
bytes /= thresh;
++u;
} while (Math.abs(bytes) >= thresh && u < units.length - 1);
return `${bytes.toFixed(1)} ${units[u]}`;
}
// @internal
export function base64ToUint8Array(data: string): Uint8Array {
const binString = atob(data);
const size = binString.length;
const bytes = new Uint8Array(size);
for (let i = 0; i < size; i++) {
bytes[i] = binString.charCodeAt(i);
}
return bytes;
}