Skip to content

Commit 1945aaa

Browse files
committed
Support loading external TS lib files
Fixes denoland#3726 This PR provides support for loading TypeScript libs that are not bundled with Deno lazily. It also provides the ability for Deno to support the triple-slash lib reference. There are a couple other minor fixes and improvements as well.
1 parent 184be99 commit 1945aaa

22 files changed

+313
-58
lines changed

cli/build.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@ fn op_fetch_asset(
1414
) -> impl Fn(&[u8], Option<ZeroCopyBuf>) -> CoreOp {
1515
move |control: &[u8], zero_copy_buf: Option<ZeroCopyBuf>| -> CoreOp {
1616
assert!(zero_copy_buf.is_none()); // zero_copy_buf unused in this op.
17-
let custom_assets = custom_assets.clone();
1817
let name = std::str::from_utf8(control).unwrap();
1918

2019
let asset_code = if let Some(source_code) = deno_typescript::get_asset(name)
2120
{
2221
source_code.to_string()
23-
} else if let Some(asset_path) = custom_assets.get(name) {
22+
} else if let Some(asset_path) = custom_assets.clone().get(name) {
2423
let source_code_vec =
2524
std::fs::read(&asset_path).expect("Asset not found");
2625
let source_code = std::str::from_utf8(&source_code_vec).unwrap();

cli/file_fetcher.rs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::http_util;
77
use crate::http_util::create_http_client;
88
use crate::http_util::FetchOnceResult;
99
use crate::http_util::ResultPayload;
10+
use crate::js::get_remote_lib_str;
1011
use crate::msg;
1112
use crate::progress::Progress;
1213
use deno_core::ErrBox;
@@ -161,13 +162,25 @@ impl SourceFileFetcher {
161162
futures::executor::block_on(fut).ok()
162163
}
163164

165+
/// Given a `name`, resolve a fully qualified URL to fetch the asset from and
166+
/// return a future which resolves with the source file.
167+
pub fn fetch_remote_asset_async(
168+
&self,
169+
name: &str,
170+
) -> Pin<Box<SourceFileFuture>> {
171+
debug!("fetch_remote_asset_async name: {} ", name);
172+
let specifier =
173+
ModuleSpecifier::resolve_url(&get_remote_lib_str(name)).unwrap();
174+
self.fetch_source_file_async(&specifier, None)
175+
}
176+
164177
pub fn fetch_source_file_async(
165178
&self,
166179
specifier: &ModuleSpecifier,
167180
maybe_referrer: Option<ModuleSpecifier>,
168181
) -> Pin<Box<SourceFileFuture>> {
169182
let module_url = specifier.as_url().to_owned();
170-
debug!("fetch_source_file. specifier {} ", &module_url);
183+
debug!("fetch_source_file_async specifier: {} ", &module_url);
171184

172185
// Check if this file was already fetched and can be retrieved from in-process cache.
173186
if let Some(source_file) = self.source_file_cache.get(specifier.to_string())
@@ -368,18 +381,13 @@ impl SourceFileFetcher {
368381
}
369382
Ok(c) => c,
370383
};
371-
let media_type = map_content_type(
372-
&filepath,
373-
source_code_headers.mime_type.as_ref().map(String::as_str),
374-
);
384+
let media_type =
385+
map_content_type(&filepath, source_code_headers.mime_type.as_deref());
375386
let types_url = match media_type {
376387
msg::MediaType::JavaScript | msg::MediaType::JSX => get_types_url(
377388
&module_url,
378389
&source_code,
379-
source_code_headers
380-
.x_typescript_types
381-
.as_ref()
382-
.map(String::as_str),
390+
source_code_headers.x_typescript_types.as_deref(),
383391
),
384392
_ => None,
385393
};
@@ -517,16 +525,14 @@ impl SourceFileFetcher {
517525
.location
518526
.join(dir.deps_cache.get_cache_filename(&module_url));
519527

520-
let media_type = map_content_type(
521-
&filepath,
522-
maybe_content_type.as_ref().map(String::as_str),
523-
);
528+
let media_type =
529+
map_content_type(&filepath, maybe_content_type.as_deref());
524530

525531
let types_url = match media_type {
526532
msg::MediaType::JavaScript | msg::MediaType::JSX => get_types_url(
527533
&module_url,
528534
&source,
529-
x_typescript_types.as_ref().map(String::as_str),
535+
x_typescript_types.as_deref(),
530536
),
531537
_ => None,
532538
};
@@ -1685,6 +1691,17 @@ mod tests {
16851691
}
16861692
}
16871693

1694+
#[test]
1695+
fn test_fetch_remote_asset() {
1696+
let (_temp_dir, fetcher) = test_setup();
1697+
1698+
tokio_util::run(fetcher.fetch_remote_asset_async("lib.dom.d.ts").map(
1699+
|r| {
1700+
assert!(r.is_ok());
1701+
},
1702+
));
1703+
}
1704+
16881705
#[test]
16891706
fn test_map_file_extension() {
16901707
assert_eq!(

cli/js.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
pub const TS_VERSION: &str = env!("TS_VERSION");
22

3+
pub fn get_remote_lib_str(name: &str) -> String {
4+
format!(
5+
"{}{}{}{}",
6+
"https://raw.githubusercontent.com/microsoft/TypeScript/v",
7+
env!("TS_VERSION"),
8+
"/lib/",
9+
name
10+
)
11+
}
12+
313
pub static CLI_SNAPSHOT: &[u8] =
414
include_bytes!(concat!(env!("OUT_DIR"), "/CLI_SNAPSHOT.bin"));
515
pub static CLI_SNAPSHOT_MAP: &[u8] =
@@ -54,3 +64,14 @@ fn compiler_snapshot() {
5464
"#,
5565
));
5666
}
67+
68+
#[test]
69+
fn test_get_remote_url() {
70+
let actual = get_remote_lib_str("lib.dom.d.ts");
71+
let expected = concat!(
72+
"https://raw.githubusercontent.com/microsoft/TypeScript/v",
73+
env!("TS_VERSION"),
74+
"/lib/lib.dom.d.ts"
75+
);
76+
assert_eq!(expected, actual);
77+
}

cli/js/compiler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
// Either of these functions must be called when creating isolate
1111
// to properly setup runtime.
1212

13+
// <reference types="../../deno_typescript/globals" />
14+
1315
// NOTE: this import has side effects!
1416
import "./ts_global.d.ts";
1517

cli/js/compiler_api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ export interface CompilerOptions {
101101
* Does not apply to `"esnext"` target. */
102102
useDefineForClassFields?: boolean;
103103

104+
/** List of library files to be included in the compilation. If omitted,
105+
* then the Deno main runtime libs are used. */
106+
lib?: string[];
107+
104108
/** The locale to use to show error messages. */
105109
locale?: string;
106110

cli/js/compiler_bootstrap.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
22

3-
import { ASSETS, CompilerHostTarget, Host } from "./compiler_host.ts";
3+
import { CompilerHostTarget, Host } from "./compiler_host.ts";
4+
import { ASSETS } from "./compiler_sourcefile.ts";
45
import { getAsset } from "./compiler_util.ts";
56

67
// NOTE: target doesn't really matter here,
@@ -14,18 +15,11 @@ const options = host.getCompilationSettings();
1415

1516
// This is a hacky way of adding our libs to the libs available in TypeScript()
1617
// as these are internal APIs of TypeScript which maintain valid libs
17-
/* eslint-disable @typescript-eslint/no-explicit-any */
18-
(ts as any).libs.push(
19-
"deno_ns",
20-
"deno_window",
21-
"deno_worker",
22-
"deno_shared_globals"
23-
);
24-
(ts as any).libMap.set("deno_ns", "lib.deno.ns.d.ts");
25-
(ts as any).libMap.set("deno_window", "lib.deno.window.d.ts");
26-
(ts as any).libMap.set("deno_worker", "lib.deno.worker.d.ts");
27-
(ts as any).libMap.set("deno_shared_globals", "lib.deno.shared_globals.d.ts");
28-
/* eslint-enable @typescript-eslint/no-explicit-any */
18+
ts.libs.push("deno.ns", "deno.window", "deno.worker", "deno.shared_globals");
19+
ts.libMap.set("deno.ns", "lib.deno.ns.d.ts");
20+
ts.libMap.set("deno.window", "lib.deno.window.d.ts");
21+
ts.libMap.set("deno.worker", "lib.deno.worker.d.ts");
22+
ts.libMap.set("deno.shared_globals", "lib.deno.shared_globals.d.ts");
2923

3024
// this pre-populates the cache at snapshot time of our library files, so they
3125
// are available in the future when needed.

cli/js/compiler_host.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
22

3-
import { MediaType, SourceFile } from "./compiler_sourcefile.ts";
3+
import { ASSETS, MediaType, SourceFile } from "./compiler_sourcefile.ts";
44
import { OUT_DIR, WriteFileCallback, getAsset } from "./compiler_util.ts";
55
import { cwd } from "./dir.ts";
66
import { assert, notImplemented } from "./util.ts";
@@ -18,8 +18,14 @@ export enum CompilerHostTarget {
1818
}
1919

2020
export interface CompilerHostOptions {
21+
/** Flag determines if the host should assume a single bundle output. */
2122
bundle?: boolean;
23+
24+
/** Determines what the default library that should be used when type checking
25+
* TS code. */
2226
target: CompilerHostTarget;
27+
28+
/** A function to be used when the program emit occurs to write out files. */
2329
writeFile: WriteFileCallback;
2430
}
2531

@@ -28,8 +34,6 @@ export interface ConfigureResponse {
2834
diagnostics?: ts.Diagnostic[];
2935
}
3036

31-
export const ASSETS = "$asset$";
32-
3337
/** Options that need to be used when generating a bundle (either trusted or
3438
* runtime). */
3539
export const defaultBundlerOptions: ts.CompilerOptions = {
@@ -96,7 +100,7 @@ const ignoredCompilerOptions: readonly string[] = [
96100
"inlineSources",
97101
"init",
98102
"isolatedModules",
99-
"lib",
103+
// "lib",
100104
"listEmittedFiles",
101105
"listFiles",
102106
"mapRoot",
@@ -141,7 +145,10 @@ export class Host implements ts.CompilerHost {
141145
private _writeFile: WriteFileCallback;
142146

143147
private _getAsset(filename: string): SourceFile {
144-
const url = filename.split("/").pop()!;
148+
const lastSegment = filename.split("/").pop()!;
149+
const url = ts.libMap.has(lastSegment)
150+
? ts.libMap.get(lastSegment)!
151+
: lastSegment;
145152
const sourceFile = SourceFile.get(url);
146153
if (sourceFile) {
147154
return sourceFile;
@@ -150,7 +157,7 @@ export class Host implements ts.CompilerHost {
150157
const sourceCode = getAsset(name);
151158
return new SourceFile({
152159
url,
153-
filename,
160+
filename: `${ASSETS}/${name}`,
154161
mediaType: MediaType.TypeScript,
155162
sourceCode
156163
});
@@ -230,6 +237,7 @@ export class Host implements ts.CompilerHost {
230237
}
231238

232239
getDefaultLibFileName(_options: ts.CompilerOptions): string {
240+
util.log("compiler::host.getDefaultLibFileName()");
233241
switch (this._target) {
234242
case CompilerHostTarget.Main:
235243
case CompilerHostTarget.Runtime:
@@ -259,7 +267,7 @@ export class Host implements ts.CompilerHost {
259267
if (!sourceFile.tsSourceFile) {
260268
assert(sourceFile.sourceCode != null);
261269
sourceFile.tsSourceFile = ts.createSourceFile(
262-
fileName,
270+
fileName.startsWith(ASSETS) ? sourceFile.filename : fileName,
263271
sourceFile.sourceCode,
264272
languageVersion
265273
);

cli/js/compiler_sourcefile.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface SourceFileJson {
2626
sourceCode: string;
2727
}
2828

29+
export const ASSETS = "$asset$";
30+
2931
/** Returns the TypeScript Extension enum for a given media type. */
3032
function getExtension(fileName: string, mediaType: MediaType): ts.Extension {
3133
switch (mediaType) {
@@ -102,7 +104,7 @@ export class SourceFile {
102104
this.processed = true;
103105
const files = (this.importedFiles = [] as Array<[string, string]>);
104106

105-
function process(references: ts.FileReference[]): void {
107+
function process(references: Array<{ fileName: string }>): void {
106108
for (const { fileName } of references) {
107109
files.push([fileName, fileName]);
108110
}
@@ -126,7 +128,15 @@ export class SourceFile {
126128
process(importedFiles);
127129
}
128130
process(referencedFiles);
129-
process(libReferenceDirectives);
131+
// built in libs comes across as `"dom"` for example, and should be filtered
132+
// out during pre-processing as they are either already cached or they will
133+
// be lazily fetched by the compiler host. Ones that contain full files are
134+
// not filtered out and will be fetched as normal.
135+
process(
136+
libReferenceDirectives.filter(
137+
({ fileName }) => !ts.libMap.has(fileName.toLowerCase())
138+
)
139+
);
130140
process(typeReferenceDirectives);
131141
return files;
132142
}

cli/js/compiler_util.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,28 +92,39 @@ function cache(
9292
}
9393

9494
let OP_FETCH_ASSET: number;
95+
const encoder = new TextEncoder();
96+
const decoder = new TextDecoder();
9597

96-
/**
97-
* This op is called only during snapshotting.
98-
*
99-
* We really don't want to depend on JSON dispatch
100-
* during snapshotting, so this op exchanges strings with Rust
101-
* as raw byte arrays.
102-
*/
98+
/** Retrieve an asset from Rust. */
10399
export function getAsset(name: string): string {
100+
// this path should only be called for assets that are lazily loaded at
101+
// runtime
102+
if (dispatch.OP_FETCH_REMOTE_ASSET) {
103+
util.log("compiler_util::getAsset", name);
104+
return sendSync(dispatch.OP_FETCH_REMOTE_ASSET, { name }).sourceCode;
105+
}
106+
107+
// this path should only be taken during snapshotting
104108
if (!OP_FETCH_ASSET) {
105109
const ops = core.ops();
106110
const opFetchAsset = ops["fetch_asset"];
107111
assert(opFetchAsset, "OP_FETCH_ASSET is not registered");
108112
OP_FETCH_ASSET = opFetchAsset;
109113
}
110114

111-
const encoder = new TextEncoder();
112-
const decoder = new TextDecoder();
115+
// We really don't want to depend on JSON dispatch during snapshotting, so
116+
// this op exchanges strings with Rust as raw byte arrays.
113117
const sourceCodeBytes = core.dispatch(OP_FETCH_ASSET, encoder.encode(name));
114118
return decoder.decode(sourceCodeBytes!);
115119
}
116120

121+
/** Some assets are only lazily requested at runtime via the compiler APIs,
122+
* This will invoke an op to request that.
123+
*/
124+
export function getRemoteAsset(name: string): { sourceCode: string } {
125+
return sendSync(dispatch.OP_FETCH_REMOTE_ASSET, { name });
126+
}
127+
117128
/** Generates a `writeFile` function which can be passed to the compiler `Host`
118129
* to use when emitting files. */
119130
export function createWriteFile(state: WriteFileState): WriteFileCallback {

cli/js/dispatch.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export let OP_APPLY_SOURCE_MAP: number;
1919
export let OP_FORMAT_ERROR: number;
2020
export let OP_CACHE: number;
2121
export let OP_RESOLVE_MODULES: number;
22+
export let OP_FETCH_REMOTE_ASSET: number;
2223
export let OP_FETCH_SOURCE_FILES: number;
2324
export let OP_OPEN: number;
2425
export let OP_CLOSE: number;
@@ -78,10 +79,6 @@ export let OP_SIGNAL_BIND: number;
7879
export let OP_SIGNAL_UNBIND: number;
7980
export let OP_SIGNAL_POLL: number;
8081

81-
/** **WARNING:** This is only available during the snapshotting process and is
82-
* unavailable at runtime. */
83-
export let OP_FETCH_ASSET: number;
84-
8582
const PLUGIN_ASYNC_HANDLER_MAP: Map<number, AsyncHandler> = new Map();
8683

8784
export function setPluginAsyncHandler(

cli/js/lib.deno.ns.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
22

33
/// <reference no-default-lib="true" />
4-
/// <reference lib="esnext" />
54

65
declare namespace Deno {
76
/** The current process id of the runtime. */
@@ -1863,6 +1862,10 @@ declare namespace Deno {
18631862
* Does not apply to `"esnext"` target. */
18641863
useDefineForClassFields?: boolean;
18651864

1865+
/** List of library files to be included in the compilation. If omitted,
1866+
* then the Deno main runtime libs are used. */
1867+
lib?: string[];
1868+
18661869
/** The locale to use to show error messages. */
18671870
locale?: string;
18681871

0 commit comments

Comments
 (0)