mirror of
https://github.com/denoland/deno.git
synced 2025-08-04 10:59:13 +00:00
Incremental compilation for TypeScript (#6428)
This commit adds incremental compilation capabilities to internal TS compiler. Instead of using "ts.createProgram()" API for compilation step (during deno startup), "ts.createIncrementalProgram()" API is used instead. Thanks to TS' ".tsbuildinfo" file that already stores all necessary metadata for compilation I was able to remove our own invention that is ".graph" file. ".tsbuildinfo" file is stored alongside compiled source and is used to cache-bust outdated dependencies, facilitated by the "version" field. The value for "version" field is computed in Rust during loading of module graph and is basically a hash of the file contents. Please keep in mind that incremental compilation is only used for initial compilation (or dynamic imports compilation) - bundling and runtime compiler APIs haven't been changed at all. Due to problems with source map I changed compilation settings to inline source map (inlineSourceMap instead of sourceMap).
This commit is contained in:
parent
3314b46321
commit
3cbd1075c7
9 changed files with 245 additions and 188 deletions
|
@ -327,6 +327,7 @@ fn test_should_allow_js() {
|
|||
redirect: None,
|
||||
filename: "some/file.ts".to_string(),
|
||||
imports: vec![],
|
||||
version_hash: "1".to_string(),
|
||||
referenced_files: vec![],
|
||||
lib_directives: vec![],
|
||||
types_directives: vec![],
|
||||
|
@ -339,6 +340,7 @@ fn test_should_allow_js() {
|
|||
url: "file:///some/file1.js".to_string(),
|
||||
redirect: None,
|
||||
filename: "some/file1.js".to_string(),
|
||||
version_hash: "1".to_string(),
|
||||
imports: vec![ImportDescriptor {
|
||||
specifier: "./file.ts".to_string(),
|
||||
resolved_specifier: ModuleSpecifier::resolve_url(
|
||||
|
@ -369,6 +371,7 @@ fn test_should_allow_js() {
|
|||
redirect: None,
|
||||
filename: "some/file.jsx".to_string(),
|
||||
imports: vec![],
|
||||
version_hash: "1".to_string(),
|
||||
referenced_files: vec![],
|
||||
lib_directives: vec![],
|
||||
types_directives: vec![],
|
||||
|
@ -381,6 +384,7 @@ fn test_should_allow_js() {
|
|||
url: "file:///some/file.ts".to_string(),
|
||||
redirect: None,
|
||||
filename: "some/file.ts".to_string(),
|
||||
version_hash: "1".to_string(),
|
||||
imports: vec![ImportDescriptor {
|
||||
specifier: "./file.jsx".to_string(),
|
||||
resolved_specifier: ModuleSpecifier::resolve_url(
|
||||
|
@ -414,6 +418,7 @@ fn test_should_allow_js() {
|
|||
referenced_files: vec![],
|
||||
lib_directives: vec![],
|
||||
types_directives: vec![],
|
||||
version_hash: "1".to_string(),
|
||||
type_headers: vec![],
|
||||
media_type: MediaType::JavaScript,
|
||||
source_code: "function foo() {}".to_string(),
|
||||
|
@ -440,6 +445,7 @@ fn test_should_allow_js() {
|
|||
referenced_files: vec![],
|
||||
lib_directives: vec![],
|
||||
types_directives: vec![],
|
||||
version_hash: "1".to_string(),
|
||||
type_headers: vec![],
|
||||
media_type: MediaType::JavaScript,
|
||||
source_code: "function foo() {}".to_string(),
|
||||
|
@ -462,6 +468,7 @@ fn test_needs_compilation() {
|
|||
lib_directives: vec![],
|
||||
types_directives: vec![],
|
||||
type_headers: vec![],
|
||||
version_hash: "1".to_string(),
|
||||
media_type: MediaType::JavaScript,
|
||||
source_code: "function foo() {}".to_string(),
|
||||
}],
|
||||
|
@ -487,6 +494,7 @@ fn test_needs_compilation() {
|
|||
types_directives: vec![],
|
||||
type_headers: vec![],
|
||||
media_type: MediaType::TypeScript,
|
||||
version_hash: "1".to_string(),
|
||||
source_code: "function foo() {}".to_string(),
|
||||
},
|
||||
&ModuleGraphFile {
|
||||
|
@ -499,6 +507,7 @@ fn test_needs_compilation() {
|
|||
lib_directives: vec![],
|
||||
types_directives: vec![],
|
||||
type_headers: vec![],
|
||||
version_hash: "1".to_string(),
|
||||
media_type: MediaType::JavaScript,
|
||||
source_code: "function foo() {}".to_string(),
|
||||
},
|
||||
|
|
|
@ -32,8 +32,16 @@ function getAsset(name: string): string {
|
|||
// Constants used by `normalizeString` and `resolvePath`
|
||||
const CHAR_DOT = 46; /* . */
|
||||
const CHAR_FORWARD_SLASH = 47; /* / */
|
||||
const ASSETS = "$asset$";
|
||||
const OUT_DIR = "$deno$";
|
||||
// Using incremental compile APIs requires that all
|
||||
// paths must be either relative or absolute. Since
|
||||
// analysis in Rust operates on fully resolved URLs,
|
||||
// it makes sense to use the same scheme here.
|
||||
const ASSETS = "asset://";
|
||||
const OUT_DIR = "deno://";
|
||||
// This constant is passed to compiler settings when
|
||||
// doing incremental compiles. Contents of this
|
||||
// file are passed back to Rust and saved to $DENO_DIR.
|
||||
const TS_BUILD_INFO = "cache:///tsbuildinfo.json";
|
||||
|
||||
// TODO(Bartlomieju): this check should be done in Rust
|
||||
const IGNORED_COMPILER_OPTIONS: readonly string[] = [
|
||||
|
@ -104,6 +112,24 @@ const DEFAULT_BUNDLER_OPTIONS: ts.CompilerOptions = {
|
|||
sourceMap: false,
|
||||
};
|
||||
|
||||
const DEFAULT_INCREMENTAL_COMPILE_OPTIONS: ts.CompilerOptions = {
|
||||
allowJs: false,
|
||||
allowNonTsExtensions: true,
|
||||
checkJs: false,
|
||||
esModuleInterop: true,
|
||||
incremental: true,
|
||||
inlineSourceMap: true,
|
||||
jsx: ts.JsxEmit.React,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
outDir: OUT_DIR,
|
||||
resolveJsonModule: true,
|
||||
sourceMap: false,
|
||||
strict: true,
|
||||
stripComments: true,
|
||||
target: ts.ScriptTarget.ESNext,
|
||||
tsBuildInfoFile: TS_BUILD_INFO,
|
||||
};
|
||||
|
||||
const DEFAULT_COMPILE_OPTIONS: ts.CompilerOptions = {
|
||||
allowJs: false,
|
||||
allowNonTsExtensions: true,
|
||||
|
@ -142,6 +168,12 @@ interface CompilerHostOptions {
|
|||
target: CompilerHostTarget;
|
||||
unstable?: boolean;
|
||||
writeFile: WriteFileCallback;
|
||||
incremental?: boolean;
|
||||
}
|
||||
|
||||
interface IncrementalCompilerHostOptions extends CompilerHostOptions {
|
||||
rootNames?: string[];
|
||||
buildInfo?: string;
|
||||
}
|
||||
|
||||
interface ConfigureResponse {
|
||||
|
@ -166,6 +198,7 @@ interface SourceFileJson {
|
|||
filename: string;
|
||||
mediaType: MediaType;
|
||||
sourceCode: string;
|
||||
versionHash: string;
|
||||
}
|
||||
|
||||
function getExtension(fileName: string, mediaType: MediaType): ts.Extension {
|
||||
|
@ -274,19 +307,20 @@ function getAssetInternal(filename: string): SourceFile {
|
|||
url,
|
||||
filename: `${ASSETS}/${name}`,
|
||||
mediaType: MediaType.TypeScript,
|
||||
versionHash: "1",
|
||||
sourceCode,
|
||||
});
|
||||
}
|
||||
|
||||
class Host implements ts.CompilerHost {
|
||||
readonly #options = DEFAULT_COMPILE_OPTIONS;
|
||||
protected _options = DEFAULT_COMPILE_OPTIONS;
|
||||
#target: CompilerHostTarget;
|
||||
#writeFile: WriteFileCallback;
|
||||
|
||||
/* Deno specific APIs */
|
||||
|
||||
constructor({
|
||||
bundle = false,
|
||||
incremental = false,
|
||||
target,
|
||||
unstable,
|
||||
writeFile,
|
||||
|
@ -295,10 +329,12 @@ class Host implements ts.CompilerHost {
|
|||
this.#writeFile = writeFile;
|
||||
if (bundle) {
|
||||
// options we need to change when we are generating a bundle
|
||||
Object.assign(this.#options, DEFAULT_BUNDLER_OPTIONS);
|
||||
Object.assign(this._options, DEFAULT_BUNDLER_OPTIONS);
|
||||
} else if (incremental) {
|
||||
Object.assign(this._options, DEFAULT_INCREMENTAL_COMPILE_OPTIONS);
|
||||
}
|
||||
if (unstable) {
|
||||
this.#options.lib = [
|
||||
this._options.lib = [
|
||||
target === CompilerHostTarget.Worker
|
||||
? "lib.deno.worker.d.ts"
|
||||
: "lib.deno.window.d.ts",
|
||||
|
@ -308,7 +344,7 @@ class Host implements ts.CompilerHost {
|
|||
}
|
||||
|
||||
get options(): ts.CompilerOptions {
|
||||
return this.#options;
|
||||
return this._options;
|
||||
}
|
||||
|
||||
configure(
|
||||
|
@ -333,13 +369,13 @@ class Host implements ts.CompilerHost {
|
|||
for (const key of Object.keys(options)) {
|
||||
if (
|
||||
IGNORED_COMPILER_OPTIONS.includes(key) &&
|
||||
(!(key in this.#options) || options[key] !== this.#options[key])
|
||||
(!(key in this._options) || options[key] !== this._options[key])
|
||||
) {
|
||||
ignoredOptions.push(key);
|
||||
delete options[key];
|
||||
}
|
||||
}
|
||||
Object.assign(this.#options, options);
|
||||
Object.assign(this._options, options);
|
||||
return {
|
||||
ignoredOptions: ignoredOptions.length ? ignoredOptions : undefined,
|
||||
diagnostics: errors.length ? errors : undefined,
|
||||
|
@ -347,8 +383,8 @@ class Host implements ts.CompilerHost {
|
|||
}
|
||||
|
||||
mergeOptions(...options: ts.CompilerOptions[]): ts.CompilerOptions {
|
||||
Object.assign(this.#options, ...options);
|
||||
return Object.assign({}, this.#options);
|
||||
Object.assign(this._options, ...options);
|
||||
return Object.assign({}, this._options);
|
||||
}
|
||||
|
||||
/* TypeScript CompilerHost APIs */
|
||||
|
@ -363,7 +399,7 @@ class Host implements ts.CompilerHost {
|
|||
|
||||
getCompilationSettings(): ts.CompilerOptions {
|
||||
log("compiler::host.getCompilationSettings()");
|
||||
return this.#options;
|
||||
return this._options;
|
||||
}
|
||||
|
||||
getCurrentDirectory(): string {
|
||||
|
@ -409,6 +445,8 @@ class Host implements ts.CompilerHost {
|
|||
sourceFile.sourceCode,
|
||||
languageVersion
|
||||
);
|
||||
//@ts-ignore
|
||||
sourceFile.tsSourceFile.version = sourceFile.versionHash;
|
||||
delete sourceFile.sourceCode;
|
||||
}
|
||||
return sourceFile.tsSourceFile;
|
||||
|
@ -480,6 +518,25 @@ class Host implements ts.CompilerHost {
|
|||
}
|
||||
}
|
||||
|
||||
class IncrementalCompileHost extends Host {
|
||||
#buildInfo: undefined | string = undefined;
|
||||
|
||||
constructor(options: IncrementalCompilerHostOptions) {
|
||||
super(options);
|
||||
const { buildInfo } = options;
|
||||
if (buildInfo) {
|
||||
this.#buildInfo = buildInfo;
|
||||
}
|
||||
}
|
||||
|
||||
readFile(fileName: string): string | undefined {
|
||||
if (fileName == TS_BUILD_INFO) {
|
||||
return this.#buildInfo;
|
||||
}
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: target doesn't really matter here,
|
||||
// this is in fact a mock host created just to
|
||||
// load all type definitions and snapshot them.
|
||||
|
@ -547,6 +604,7 @@ function buildLocalSourceFileCache(
|
|||
filename: entry.url,
|
||||
mediaType: entry.mediaType,
|
||||
sourceCode: entry.sourceCode,
|
||||
versionHash: entry.versionHash,
|
||||
});
|
||||
|
||||
for (const importDesc of entry.imports) {
|
||||
|
@ -598,6 +656,7 @@ function buildSourceFileCache(
|
|||
filename: entry.url,
|
||||
mediaType: entry.mediaType,
|
||||
sourceCode: entry.sourceCode,
|
||||
versionHash: entry.versionHash,
|
||||
});
|
||||
|
||||
for (const importDesc of entry.imports) {
|
||||
|
@ -663,6 +722,7 @@ type WriteFileCallback = (
|
|||
interface CompileWriteFileState {
|
||||
rootNames: string[];
|
||||
emitMap: Record<string, EmittedSource>;
|
||||
buildInfo?: string;
|
||||
}
|
||||
|
||||
interface BundleWriteFileState {
|
||||
|
@ -708,7 +768,15 @@ function createCompileWriteFile(
|
|||
data: string,
|
||||
sourceFiles?: readonly ts.SourceFile[]
|
||||
): void {
|
||||
assert(sourceFiles != null);
|
||||
const isBuildInfo = fileName === TS_BUILD_INFO;
|
||||
|
||||
if (isBuildInfo) {
|
||||
assert(isBuildInfo);
|
||||
state.buildInfo = data;
|
||||
return;
|
||||
}
|
||||
|
||||
assert(sourceFiles);
|
||||
assert(sourceFiles.length === 1);
|
||||
state.emitMap[fileName] = {
|
||||
filename: sourceFiles[0].fileName,
|
||||
|
@ -717,6 +785,22 @@ function createCompileWriteFile(
|
|||
};
|
||||
}
|
||||
|
||||
function createRuntimeCompileWriteFile(
|
||||
state: CompileWriteFileState
|
||||
): WriteFileCallback {
|
||||
return function writeFile(
|
||||
fileName: string,
|
||||
data: string,
|
||||
sourceFiles?: readonly ts.SourceFile[]
|
||||
): void {
|
||||
assert(sourceFiles);
|
||||
assert(sourceFiles.length === 1);
|
||||
state.emitMap[fileName] = {
|
||||
filename: sourceFiles[0].fileName,
|
||||
contents: data,
|
||||
};
|
||||
};
|
||||
}
|
||||
interface ConvertCompilerOptionsResult {
|
||||
files?: string[];
|
||||
options: ts.CompilerOptions;
|
||||
|
@ -888,7 +972,6 @@ function performanceEnd(): Stats {
|
|||
}
|
||||
|
||||
// TODO(Bartlomieju): this check should be done in Rust; there should be no
|
||||
// console.log here
|
||||
function processConfigureResponse(
|
||||
configResult: ConfigureResponse,
|
||||
configPath: string
|
||||
|
@ -1106,6 +1189,7 @@ interface SourceFileMapEntry {
|
|||
libDirectives: ReferenceDescriptor[];
|
||||
typesDirectives: ReferenceDescriptor[];
|
||||
typeHeaders: ReferenceDescriptor[];
|
||||
versionHash: string;
|
||||
}
|
||||
|
||||
/** Used when "deno run" is invoked */
|
||||
|
@ -1121,6 +1205,7 @@ interface CompileRequest {
|
|||
cwd: string;
|
||||
// key value is fully resolved URL
|
||||
sourceFileMap: Record<string, SourceFileMapEntry>;
|
||||
buildInfo?: string;
|
||||
}
|
||||
|
||||
/** Used when "deno bundle" is invoked */
|
||||
|
@ -1174,6 +1259,7 @@ type CompilerRequest =
|
|||
interface CompileResponse {
|
||||
emitMap: Record<string, EmittedSource>;
|
||||
diagnostics: Diagnostic;
|
||||
buildInfo: undefined | string;
|
||||
stats?: Stats;
|
||||
}
|
||||
|
||||
|
@ -1195,19 +1281,16 @@ interface RuntimeBundleResponse {
|
|||
|
||||
function compile({
|
||||
allowJs,
|
||||
buildInfo,
|
||||
config,
|
||||
configPath,
|
||||
rootNames,
|
||||
target,
|
||||
unstable,
|
||||
performance,
|
||||
cwd,
|
||||
sourceFileMap,
|
||||
type,
|
||||
}: CompileRequest): CompileResponse {
|
||||
if (performance) {
|
||||
performanceStart();
|
||||
}
|
||||
log(">>> compile start", { rootNames, type: CompilerRequestType[type] });
|
||||
|
||||
// When a programme is emitted, TypeScript will call `writeFile` with
|
||||
|
@ -1218,11 +1301,14 @@ function compile({
|
|||
rootNames,
|
||||
emitMap: {},
|
||||
};
|
||||
const host = new Host({
|
||||
const host = new IncrementalCompileHost({
|
||||
bundle: false,
|
||||
target,
|
||||
unstable,
|
||||
incremental: true,
|
||||
writeFile: createCompileWriteFile(state),
|
||||
rootNames,
|
||||
buildInfo,
|
||||
});
|
||||
let diagnostics: readonly ts.Diagnostic[] = [];
|
||||
|
||||
|
@ -1239,15 +1325,23 @@ function compile({
|
|||
// to generate the program and possibly emit it.
|
||||
if (diagnostics.length === 0) {
|
||||
const options = host.getCompilationSettings();
|
||||
const program = ts.createProgram({
|
||||
const program = ts.createIncrementalProgram({
|
||||
rootNames,
|
||||
options,
|
||||
host,
|
||||
});
|
||||
|
||||
diagnostics = ts
|
||||
.getPreEmitDiagnostics(program)
|
||||
.filter(({ code }) => !ignoredDiagnostics.includes(code));
|
||||
// TODO(bartlomieju): check if this is ok
|
||||
diagnostics = [
|
||||
...program.getConfigFileParsingDiagnostics(),
|
||||
...program.getSyntacticDiagnostics(),
|
||||
...program.getOptionsDiagnostics(),
|
||||
...program.getGlobalDiagnostics(),
|
||||
...program.getSemanticDiagnostics(),
|
||||
];
|
||||
diagnostics = diagnostics.filter(
|
||||
({ code }) => !ignoredDiagnostics.includes(code)
|
||||
);
|
||||
|
||||
// We will only proceed with the emit if there are no diagnostics.
|
||||
if (diagnostics.length === 0) {
|
||||
|
@ -1265,18 +1359,14 @@ function compile({
|
|||
// without casting.
|
||||
diagnostics = emitResult.diagnostics;
|
||||
}
|
||||
if (performance) {
|
||||
performanceProgram(program);
|
||||
}
|
||||
}
|
||||
|
||||
log("<<< compile end", { rootNames, type: CompilerRequestType[type] });
|
||||
const stats = performance ? performanceEnd() : undefined;
|
||||
|
||||
return {
|
||||
emitMap: state.emitMap,
|
||||
buildInfo: state.buildInfo,
|
||||
diagnostics: fromTypeScriptDiagnostic(diagnostics),
|
||||
stats,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1402,7 +1492,7 @@ function runtimeCompile(
|
|||
const host = new Host({
|
||||
bundle: false,
|
||||
target,
|
||||
writeFile: createCompileWriteFile(state),
|
||||
writeFile: createRuntimeCompileWriteFile(state),
|
||||
});
|
||||
const compilerOptions = [DEFAULT_RUNTIME_COMPILE_OPTIONS];
|
||||
if (convertedOptions) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
use crate::checksum;
|
||||
use crate::doc::Location;
|
||||
use crate::file_fetcher::map_file_extension;
|
||||
use crate::file_fetcher::SourceFile;
|
||||
|
@ -13,6 +13,7 @@ use crate::tsc::ImportDesc;
|
|||
use crate::tsc::TsReferenceDesc;
|
||||
use crate::tsc::TsReferenceKind;
|
||||
use crate::tsc::AVAILABLE_LIBS;
|
||||
use crate::version;
|
||||
use deno_core::ErrBox;
|
||||
use deno_core::ModuleSpecifier;
|
||||
use futures::stream::FuturesUnordered;
|
||||
|
@ -228,6 +229,7 @@ pub struct ModuleGraphFile {
|
|||
pub url: String,
|
||||
pub redirect: Option<String>,
|
||||
pub filename: String,
|
||||
pub version_hash: String,
|
||||
pub imports: Vec<ImportDescriptor>,
|
||||
pub referenced_files: Vec<ReferenceDescriptor>,
|
||||
pub lib_directives: Vec<ReferenceDescriptor>,
|
||||
|
@ -369,6 +371,7 @@ impl ModuleGraphLoader {
|
|||
specifier: specifier.to_string(),
|
||||
url: specifier.to_string(),
|
||||
redirect: None,
|
||||
version_hash: "".to_string(),
|
||||
media_type: map_file_extension(&PathBuf::from(specifier.clone())),
|
||||
filename: specifier,
|
||||
source_code,
|
||||
|
@ -454,6 +457,10 @@ impl ModuleGraphLoader {
|
|||
url: module_specifier.to_string(),
|
||||
redirect: Some(source_file.url.to_string()),
|
||||
filename: source_file.filename.to_str().unwrap().to_string(),
|
||||
version_hash: checksum::gen(vec![
|
||||
&source_file.source_code,
|
||||
version::DENO.as_bytes(),
|
||||
]),
|
||||
media_type: source_file.media_type,
|
||||
source_code: "".to_string(),
|
||||
imports: vec![],
|
||||
|
@ -466,6 +473,8 @@ impl ModuleGraphLoader {
|
|||
}
|
||||
|
||||
let module_specifier = ModuleSpecifier::from(source_file.url.clone());
|
||||
let version_hash =
|
||||
checksum::gen(vec![&source_file.source_code, version::DENO.as_bytes()]);
|
||||
let source_code = String::from_utf8(source_file.source_code)?;
|
||||
|
||||
if SUPPORTED_MEDIA_TYPES.contains(&source_file.media_type) {
|
||||
|
@ -553,6 +562,7 @@ impl ModuleGraphLoader {
|
|||
specifier: module_specifier.to_string(),
|
||||
url: module_specifier.to_string(),
|
||||
redirect: None,
|
||||
version_hash,
|
||||
filename: source_file.filename.to_str().unwrap().to_string(),
|
||||
media_type: source_file.media_type,
|
||||
source_code,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
local: [WILDCARD]005_more_imports.ts
|
||||
type: TypeScript
|
||||
compiled: [WILDCARD]005_more_imports.ts.js
|
||||
map: [WILDCARD]005_more_imports.ts.js.map
|
||||
deps:
|
||||
file://[WILDCARD]/005_more_imports.ts
|
||||
└─┬ file://[WILDCARD]/subdir/mod1.ts
|
||||
|
|
|
@ -2049,7 +2049,7 @@ itest!(single_compile_with_reload {
|
|||
});
|
||||
|
||||
itest!(performance_stats {
|
||||
args: "run --reload --log-level debug 002_hello.ts",
|
||||
args: "bundle --log-level debug 002_hello.ts",
|
||||
output: "performance_stats.out",
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
await import("./005_more_imports.ts");
|
||||
await import("./single_compile_with_reload_dyn.ts");
|
||||
console.log("1");
|
||||
await import("./005_more_imports.ts");
|
||||
await import("./single_compile_with_reload_dyn.ts");
|
||||
console.log("2");
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Compile [WILDCARD]single_compile_with_reload.ts
|
||||
Compile [WILDCARD]005_more_imports.ts
|
||||
Compile [WILDCARD]single_compile_with_reload_dyn.ts
|
||||
Hello
|
||||
1
|
||||
2
|
||||
|
|
11
cli/tests/single_compile_with_reload_dyn.ts
Normal file
11
cli/tests/single_compile_with_reload_dyn.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { returnsHi, returnsFoo2, printHello3 } from "./subdir/mod1.ts";
|
||||
|
||||
printHello3();
|
||||
|
||||
if (returnsHi() !== "Hi") {
|
||||
throw Error("Unexpected");
|
||||
}
|
||||
|
||||
if (returnsFoo2() !== "Foo") {
|
||||
throw Error("Unexpected");
|
||||
}
|
244
cli/tsc.rs
244
cli/tsc.rs
|
@ -302,28 +302,6 @@ impl CompiledFileMetadata {
|
|||
}
|
||||
}
|
||||
|
||||
/// Information associated with compilation of a "module graph",
|
||||
/// ie. entry point and all its dependencies.
|
||||
/// It's used to perform cache invalidation if content of any
|
||||
/// dependency changes.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct GraphFileMetadata {
|
||||
pub deps: Vec<String>,
|
||||
pub version_hash: String,
|
||||
}
|
||||
|
||||
impl GraphFileMetadata {
|
||||
pub fn from_json_string(
|
||||
metadata_string: String,
|
||||
) -> Result<Self, serde_json::Error> {
|
||||
serde_json::from_str::<Self>(&metadata_string)
|
||||
}
|
||||
|
||||
pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a SHA256 hash based on source code, deno version and TS config.
|
||||
/// Used to check if a recompilation for source code is needed.
|
||||
pub fn source_code_version_hash(
|
||||
|
@ -367,7 +345,7 @@ impl Deref for TsCompiler {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Stat {
|
||||
key: String,
|
||||
|
@ -394,6 +372,7 @@ struct BundleResponse {
|
|||
struct CompileResponse {
|
||||
diagnostics: Diagnostic,
|
||||
emit_map: HashMap<String, EmittedSource>,
|
||||
build_info: Option<String>,
|
||||
stats: Option<Vec<Stat>>,
|
||||
}
|
||||
|
||||
|
@ -491,36 +470,59 @@ impl TsCompiler {
|
|||
) -> Result<(), ErrBox> {
|
||||
let mut has_cached_version = false;
|
||||
|
||||
let module_url = source_file.url.clone();
|
||||
let build_info_key = self
|
||||
.disk_cache
|
||||
.get_cache_filename_with_extension(&module_url, "buildinfo");
|
||||
|
||||
let build_info = match self.disk_cache.get(&build_info_key) {
|
||||
Ok(bytes) => Some(String::from_utf8(bytes)?),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
let file_fetcher = global_state.file_fetcher.clone();
|
||||
|
||||
// Only use disk cache if `--reload` flag was not used or
|
||||
// this file has already been compiled during current process
|
||||
// lifetime.
|
||||
if self.use_disk_cache || self.has_compiled(&source_file.url) {
|
||||
if let Some(metadata) = self.get_graph_metadata(&source_file.url) {
|
||||
if let Some(build_info_str) = build_info.as_ref() {
|
||||
let build_inf_json: Value = serde_json::from_str(build_info_str)?;
|
||||
let program_val = build_inf_json["program"].as_object().unwrap();
|
||||
let file_infos = program_val["fileInfos"].as_object().unwrap();
|
||||
|
||||
has_cached_version = true;
|
||||
|
||||
let version_hash = crate::checksum::gen(vec![
|
||||
version::DENO.as_bytes(),
|
||||
&self.config.hash,
|
||||
]);
|
||||
|
||||
has_cached_version &= metadata.version_hash == version_hash;
|
||||
has_cached_version &= self
|
||||
.has_compiled_source(&global_state.file_fetcher, &source_file.url);
|
||||
|
||||
for dep in metadata.deps {
|
||||
let url = Url::parse(&dep).expect("Dep is not a valid url");
|
||||
has_cached_version &=
|
||||
self.has_compiled_source(&global_state.file_fetcher, &url);
|
||||
for (filename, file_info) in file_infos.iter() {
|
||||
if filename.starts_with("asset://") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let url = Url::parse(&filename).expect("Filename is not a valid url");
|
||||
let specifier = ModuleSpecifier::from(url);
|
||||
if let Some(source_file) = file_fetcher
|
||||
.fetch_cached_source_file(&specifier, Permissions::allow_all())
|
||||
{
|
||||
let existing_hash = crate::checksum::gen(vec![
|
||||
&source_file.source_code,
|
||||
version::DENO.as_bytes(),
|
||||
]);
|
||||
let expected_hash =
|
||||
file_info["version"].as_str().unwrap().to_string();
|
||||
has_cached_version &= existing_hash == expected_hash
|
||||
} else {
|
||||
has_cached_version &= false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if has_cached_version {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let module_url = source_file.url.clone();
|
||||
|
||||
let module_graph_json =
|
||||
serde_json::to_value(module_graph).expect("Failed to serialize data");
|
||||
let target = match target {
|
||||
|
@ -535,6 +537,7 @@ impl TsCompiler {
|
|||
};
|
||||
let compiler_config = self.config.clone();
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
|
||||
let j = match (compiler_config.path, compiler_config.content) {
|
||||
(Some(config_path), Some(config_data)) => json!({
|
||||
"type": msg::CompilerRequestType::Compile,
|
||||
|
@ -547,6 +550,7 @@ impl TsCompiler {
|
|||
"config": str::from_utf8(&config_data).unwrap(),
|
||||
"cwd": cwd,
|
||||
"sourceFileMap": module_graph_json,
|
||||
"buildInfo": build_info,
|
||||
}),
|
||||
_ => json!({
|
||||
"type": msg::CompilerRequestType::Compile,
|
||||
|
@ -557,6 +561,7 @@ impl TsCompiler {
|
|||
"performance": performance,
|
||||
"cwd": cwd,
|
||||
"sourceFileMap": module_graph_json,
|
||||
"buildInfo": build_info,
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -583,69 +588,13 @@ impl TsCompiler {
|
|||
|
||||
maybe_log_stats(compile_response.stats);
|
||||
|
||||
self.set_graph_metadata(
|
||||
source_file.url.clone(),
|
||||
&compile_response.emit_map,
|
||||
)?;
|
||||
if let Some(build_info) = compile_response.build_info {
|
||||
self.cache_build_info(&module_url, build_info)?;
|
||||
}
|
||||
self.cache_emitted_files(compile_response.emit_map)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_graph_metadata(&self, url: &Url) -> Option<GraphFileMetadata> {
|
||||
let cache_key = self
|
||||
.disk_cache
|
||||
.get_cache_filename_with_extension(url, "graph");
|
||||
if let Ok(metadata_bytes) = self.disk_cache.get(&cache_key) {
|
||||
if let Ok(metadata) = std::str::from_utf8(&metadata_bytes) {
|
||||
if let Ok(read_metadata) =
|
||||
GraphFileMetadata::from_json_string(metadata.to_string())
|
||||
{
|
||||
return Some(read_metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn set_graph_metadata(
|
||||
&self,
|
||||
url: Url,
|
||||
emit_map: &HashMap<String, EmittedSource>,
|
||||
) -> std::io::Result<()> {
|
||||
let version_hash =
|
||||
crate::checksum::gen(vec![version::DENO.as_bytes(), &self.config.hash]);
|
||||
let mut deps = vec![];
|
||||
|
||||
for (_emitted_name, source) in emit_map.iter() {
|
||||
let specifier = ModuleSpecifier::resolve_url(&source.filename)
|
||||
.expect("Should be a valid module specifier");
|
||||
|
||||
let source_file = self
|
||||
.file_fetcher
|
||||
.fetch_cached_source_file(&specifier, Permissions::allow_all())
|
||||
.expect("Source file not found");
|
||||
|
||||
// NOTE: JavaScript files are only cached to disk if `checkJs`
|
||||
// option in on
|
||||
if source_file.media_type == msg::MediaType::JavaScript
|
||||
&& !self.compile_js
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
deps.push(specifier.to_string());
|
||||
}
|
||||
|
||||
let graph_metadata = GraphFileMetadata { deps, version_hash };
|
||||
let meta_key = self
|
||||
.disk_cache
|
||||
.get_cache_filename_with_extension(&url, "graph");
|
||||
self
|
||||
.disk_cache
|
||||
.set(&meta_key, graph_metadata.to_json_string()?.as_bytes())
|
||||
}
|
||||
|
||||
/// Get associated `CompiledFileMetadata` for given module if it exists.
|
||||
pub fn get_metadata(&self, url: &Url) -> Option<CompiledFileMetadata> {
|
||||
// Try to load cached version:
|
||||
|
@ -666,6 +615,19 @@ impl TsCompiler {
|
|||
None
|
||||
}
|
||||
|
||||
fn cache_build_info(
|
||||
&self,
|
||||
url: &Url,
|
||||
build_info: String,
|
||||
) -> std::io::Result<()> {
|
||||
let js_key = self
|
||||
.disk_cache
|
||||
.get_cache_filename_with_extension(url, "buildinfo");
|
||||
self.disk_cache.set(&js_key, build_info.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cache_emitted_files(
|
||||
&self,
|
||||
emit_map: HashMap<String, EmittedSource>,
|
||||
|
@ -750,33 +712,6 @@ impl TsCompiler {
|
|||
source_file: SourceFile,
|
||||
contents: &str,
|
||||
) -> std::io::Result<()> {
|
||||
// By default TSC output source map url that is relative; we need
|
||||
// to substitute it manually to correct file URL in DENO_DIR.
|
||||
let mut content_lines = contents
|
||||
.split('\n')
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
if !content_lines.is_empty() {
|
||||
let last_line = content_lines.pop().unwrap();
|
||||
if last_line.starts_with("//# sourceMappingURL=") {
|
||||
let source_map_key = self.disk_cache.get_cache_filename_with_extension(
|
||||
module_specifier.as_url(),
|
||||
"js.map",
|
||||
);
|
||||
let source_map_path = self.disk_cache.location.join(source_map_key);
|
||||
let source_map_file_url = Url::from_file_path(source_map_path)
|
||||
.expect("Bad file URL for source map");
|
||||
let new_last_line =
|
||||
format!("//# sourceMappingURL={}", source_map_file_url.to_string());
|
||||
content_lines.push(new_last_line);
|
||||
} else {
|
||||
content_lines.push(last_line);
|
||||
}
|
||||
}
|
||||
|
||||
let contents = content_lines.join("\n");
|
||||
|
||||
let js_key = self
|
||||
.disk_cache
|
||||
.get_cache_filename_with_extension(module_specifier.as_url(), "js");
|
||||
|
@ -856,9 +791,7 @@ impl TsCompiler {
|
|||
|
||||
impl SourceMapGetter for TsCompiler {
|
||||
fn get_source_map(&self, script_name: &str) -> Option<Vec<u8>> {
|
||||
self
|
||||
.try_to_resolve_and_get_source_map(script_name)
|
||||
.map(|out| out.source_code)
|
||||
self.try_to_resolve_and_get_source_map(script_name)
|
||||
}
|
||||
|
||||
fn get_source_line(&self, script_name: &str, line: usize) -> Option<String> {
|
||||
|
@ -901,11 +834,38 @@ impl TsCompiler {
|
|||
fn try_to_resolve_and_get_source_map(
|
||||
&self,
|
||||
script_name: &str,
|
||||
) -> Option<SourceFile> {
|
||||
) -> Option<Vec<u8>> {
|
||||
if let Some(module_specifier) = self.try_to_resolve(script_name) {
|
||||
return match self.get_source_map_file(&module_specifier) {
|
||||
Ok(out) => Some(out),
|
||||
Err(_) => None,
|
||||
Ok(out) => Some(out.source_code),
|
||||
Err(_) => {
|
||||
// Check if map is inlined
|
||||
if let Ok(compiled_source) =
|
||||
self.get_compiled_module(module_specifier.as_url())
|
||||
{
|
||||
let mut content_lines = compiled_source
|
||||
.code
|
||||
.split('\n')
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
if !content_lines.is_empty() {
|
||||
let last_line = content_lines.pop().unwrap();
|
||||
if last_line.starts_with(
|
||||
"//# sourceMappingURL=data:application/json;base64,",
|
||||
) {
|
||||
let encoded = last_line.trim_start_matches(
|
||||
"//# sourceMappingURL=data:application/json;base64,",
|
||||
);
|
||||
let decoded_map =
|
||||
base64::decode(encoded).expect("failed to parse source map");
|
||||
return Some(decoded_map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1560,30 +1520,8 @@ mod tests {
|
|||
let mut lines: Vec<String> =
|
||||
source_code.split('\n').map(|s| s.to_string()).collect();
|
||||
let last_line = lines.pop().unwrap();
|
||||
assert!(last_line.starts_with("//# sourceMappingURL=file://"));
|
||||
|
||||
// Get source map file and assert it has proper URLs
|
||||
let source_map = mock_state
|
||||
.ts_compiler
|
||||
.get_source_map_file(&specifier)
|
||||
.expect("Source map not found");
|
||||
let source_str = String::from_utf8(source_map.source_code).unwrap();
|
||||
let source_json: Value = serde_json::from_str(&source_str).unwrap();
|
||||
|
||||
let js_key = mock_state
|
||||
.ts_compiler
|
||||
.disk_cache
|
||||
.get_cache_filename_with_extension(specifier.as_url(), "js");
|
||||
let js_path = mock_state.ts_compiler.disk_cache.location.join(js_key);
|
||||
let js_file_url = Url::from_file_path(js_path).unwrap();
|
||||
|
||||
let file_str = source_json.get("file").unwrap().as_str().unwrap();
|
||||
assert_eq!(file_str, js_file_url.to_string());
|
||||
|
||||
let sources = source_json.get("sources").unwrap().as_array().unwrap();
|
||||
assert_eq!(sources.len(), 1);
|
||||
let source = sources.get(0).unwrap().as_str().unwrap();
|
||||
assert_eq!(source, specifier.to_string());
|
||||
assert!(last_line
|
||||
.starts_with("//# sourceMappingURL=data:application/json;base64"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue