Skip to content

Commit 606fe2a

Browse files
committed
[WIP] Support .d.ts files
Fixes #1432
1 parent e438ac2 commit 606fe2a

File tree

9 files changed

+162
-20
lines changed

9 files changed

+162
-20
lines changed

cli/BUILD.gn

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ ts_sources = [
110110
"../js/mock_builtin.js",
111111
"../js/net.ts",
112112
"../js/os.ts",
113+
"../js/performance.ts",
113114
"../js/permissions.ts",
114115
"../js/plugins.d.ts",
115116
"../js/process.ts",
@@ -126,6 +127,7 @@ ts_sources = [
126127
"../js/text_encoding.ts",
127128
"../js/timers.ts",
128129
"../js/truncate.ts",
130+
"../js/type_directives.ts",
129131
"../js/types.ts",
130132
"../js/url.ts",
131133
"../js/url_search_params.ts",
@@ -134,7 +136,6 @@ ts_sources = [
134136
"../js/window.ts",
135137
"../js/workers.ts",
136138
"../js/write_file.ts",
137-
"../js/performance.ts",
138139
"../js/version.ts",
139140
"../js/xeval.ts",
140141
"../tsconfig.json",

js/compiler.ts

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { sendSync } from "./dispatch";
1212
import * as flatbuffers from "./flatbuffers";
1313
import * as os from "./os";
1414
import { TextDecoder, TextEncoder } from "./text_encoding";
15+
import { getMappedModuleName, parseTypeDirectives } from "./type_directives";
1516
import { assert, notImplemented } from "./util";
1617
import * as util from "./util";
1718
import { window } from "./window";
@@ -112,6 +113,7 @@ interface SourceFile {
112113
filename: string | undefined;
113114
mediaType: msg.MediaType;
114115
sourceCode: string | undefined;
116+
typeDirectives?: Record<string, string>;
115117
}
116118

117119
interface EmitResult {
@@ -121,7 +123,7 @@ interface EmitResult {
121123

122124
/** Ops to Rust to resolve and fetch a modules meta data. */
123125
function fetchSourceFile(specifier: string, referrer: string): SourceFile {
124-
util.log("compiler.fetchSourceFile", { specifier, referrer });
126+
util.log("fetchSourceFile", { specifier, referrer });
125127
// Send FetchSourceFile message
126128
const builder = flatbuffers.createBuilder();
127129
const specifier_ = builder.createString(specifier);
@@ -148,7 +150,8 @@ function fetchSourceFile(specifier: string, referrer: string): SourceFile {
148150
moduleName: fetchSourceFileRes.moduleName() || undefined,
149151
filename: fetchSourceFileRes.filename() || undefined,
150152
mediaType: fetchSourceFileRes.mediaType(),
151-
sourceCode
153+
sourceCode,
154+
typeDirectives: parseTypeDirectives(sourceCode)
152155
};
153156
}
154157

@@ -170,7 +173,7 @@ function humanFileSize(bytes: number): string {
170173

171174
/** Ops to rest for caching source map and compiled js */
172175
function cache(extension: string, moduleId: string, contents: string): void {
173-
util.log("compiler.cache", moduleId);
176+
util.log("cache", extension, moduleId);
174177
const builder = flatbuffers.createBuilder();
175178
const extension_ = builder.createString(extension);
176179
const moduleId_ = builder.createString(moduleId);
@@ -191,7 +194,7 @@ const encoder = new TextEncoder();
191194
function emitBundle(fileName: string, data: string): void {
192195
// For internal purposes, when trying to emit to `$deno$` just no-op
193196
if (fileName.startsWith("$deno$")) {
194-
console.warn("skipping compiler.emitBundle", fileName);
197+
console.warn("skipping emitBundle", fileName);
195198
return;
196199
}
197200
const encodedData = encoder.encode(data);
@@ -219,7 +222,7 @@ function getExtension(
219222
}
220223

221224
class Host implements ts.CompilerHost {
222-
extensionCache: Record<string, ts.Extension> = {};
225+
private _extensionCache: Record<string, ts.Extension> = {};
223226

224227
private readonly _options: ts.CompilerOptions = {
225228
allowJs: true,
@@ -234,23 +237,37 @@ class Host implements ts.CompilerHost {
234237
target: ts.ScriptTarget.ESNext
235238
};
236239

240+
private _sourceFileCache: Record<string, SourceFile> = {};
241+
237242
private _resolveModule(specifier: string, referrer: string): SourceFile {
243+
util.log("host._resolveModule", { specifier, referrer });
238244
// Handle built-in assets specially.
239245
if (specifier.startsWith(ASSETS)) {
240246
const moduleName = specifier.split("/").pop()!;
247+
if (moduleName in this._sourceFileCache) {
248+
return this._sourceFileCache[moduleName];
249+
}
241250
const assetName = moduleName.includes(".")
242251
? moduleName
243252
: `${moduleName}.d.ts`;
244253
assert(assetName in assetSourceCode, `No such asset "${assetName}"`);
245254
const sourceCode = assetSourceCode[assetName];
246-
return {
255+
const sourceFile = {
247256
moduleName,
248257
filename: specifier,
249258
mediaType: msg.MediaType.TypeScript,
250259
sourceCode
251260
};
261+
this._sourceFileCache[moduleName] = sourceFile;
262+
return sourceFile;
263+
}
264+
const sourceFile = fetchSourceFile(specifier, referrer);
265+
assert(sourceFile.moduleName != null);
266+
const { moduleName } = sourceFile;
267+
if (!(moduleName! in this._sourceFileCache)) {
268+
this._sourceFileCache[moduleName!] = sourceFile;
252269
}
253-
return fetchSourceFile(specifier, referrer);
270+
return sourceFile;
254271
}
255272

256273
/* Deno specific APIs */
@@ -279,7 +296,7 @@ class Host implements ts.CompilerHost {
279296
* options which were ignored, or `undefined`.
280297
*/
281298
configure(path: string, configurationText: string): ConfigureResponse {
282-
util.log("compile.configure", path);
299+
util.log("host.configure", path);
283300
const { config, error } = ts.parseConfigFileTextToJson(
284301
path,
285302
configurationText
@@ -344,13 +361,17 @@ class Host implements ts.CompilerHost {
344361
): ts.SourceFile | undefined {
345362
assert(!shouldCreateNewSourceFile);
346363
util.log("getSourceFile", fileName);
347-
const SourceFile = this._resolveModule(fileName, ".");
348-
if (!SourceFile || !SourceFile.sourceCode) {
364+
const sourceFile =
365+
fileName in this._sourceFileCache
366+
? this._sourceFileCache[fileName]
367+
: this._resolveModule(fileName, ".");
368+
assert(sourceFile != null);
369+
if (!sourceFile.sourceCode) {
349370
return undefined;
350371
}
351372
return ts.createSourceFile(
352373
fileName,
353-
SourceFile.sourceCode,
374+
sourceFile.sourceCode,
354375
languageVersion
355376
);
356377
}
@@ -364,26 +385,37 @@ class Host implements ts.CompilerHost {
364385
containingFile: string
365386
): Array<ts.ResolvedModuleFull | undefined> {
366387
util.log("resolveModuleNames()", { moduleNames, containingFile });
388+
const typeDirectives: Record<string, string> | undefined =
389+
containingFile in this._sourceFileCache
390+
? this._sourceFileCache[containingFile].typeDirectives
391+
: undefined;
367392
return moduleNames.map(
368393
(moduleName): ts.ResolvedModuleFull | undefined => {
369-
const SourceFile = this._resolveModule(moduleName, containingFile);
370-
if (SourceFile.moduleName) {
371-
const resolvedFileName = SourceFile.moduleName;
394+
const mappedModuleName = getMappedModuleName(
395+
moduleName,
396+
containingFile,
397+
typeDirectives
398+
);
399+
const sourceFile = this._resolveModule(
400+
mappedModuleName,
401+
containingFile
402+
);
403+
if (sourceFile.moduleName) {
404+
const resolvedFileName = sourceFile.moduleName;
372405
// This flags to the compiler to not go looking to transpile functional
373406
// code, anything that is in `/$asset$/` is just library code
374407
const isExternalLibraryImport = moduleName.startsWith(ASSETS);
375408
const extension = getExtension(
376409
resolvedFileName,
377-
SourceFile.mediaType
410+
sourceFile.mediaType
378411
);
379-
this.extensionCache[resolvedFileName] = extension;
412+
this._extensionCache[resolvedFileName] = extension;
380413

381-
const r = {
414+
return {
382415
resolvedFileName,
383416
isExternalLibraryImport,
384417
extension
385418
};
386-
return r;
387419
} else {
388420
return undefined;
389421
}
@@ -409,7 +441,7 @@ class Host implements ts.CompilerHost {
409441
} else {
410442
assert(sourceFiles != null && sourceFiles.length == 1);
411443
const sourceFileName = sourceFiles![0].fileName;
412-
const maybeExtension = this.extensionCache[sourceFileName];
444+
const maybeExtension = this._extensionCache[sourceFileName];
413445

414446
if (maybeExtension) {
415447
// NOTE: If it's a `.json` file we don't want to write it to disk.

js/type_directives.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
2+
3+
interface DirectiveInfo {
4+
path: string;
5+
start: number;
6+
end: number;
7+
}
8+
9+
/** Remap the module name based on any supplied type directives passed. */
10+
export function getMappedModuleName(
11+
moduleName: string,
12+
containingFile: string,
13+
typeDirectives?: Record<string, string>
14+
): string {
15+
if (containingFile.endsWith(".d.ts") && !moduleName.endsWith(".d.ts")) {
16+
moduleName = `${moduleName}.d.ts`;
17+
}
18+
if (!typeDirectives) {
19+
return moduleName;
20+
}
21+
if (moduleName in typeDirectives) {
22+
return typeDirectives[moduleName];
23+
}
24+
return moduleName;
25+
}
26+
27+
/** Matches directives that look something like this and parses out the value
28+
* of the directive:
29+
*
30+
* // @deno-types="./foo.d.ts"
31+
*
32+
* [See Diagram](http://bit.ly/31nZPCF)
33+
*/
34+
const typeDirectiveRegEx = /@deno-types\s*=\s*(["'])((?:(?=(\\?))\3.)*?)\1/gi;
35+
36+
/** Matches `import` or `export from` statements and parses out the value of the
37+
* module specifier in the second capture group:
38+
*
39+
* import * as foo from "./foo.js"
40+
* export { a, b, c } from "./bar.js"
41+
*
42+
* [See Diagram](http://bit.ly/2GSkJlF)
43+
*/
44+
const importExportRegEx = /(?:import|export)\s+[\s\S]*?from\s+(["'])((?:(?=(\\?))\3.)*?)\1/;
45+
46+
/** Parses out any Deno type directives that are part of the source code, or
47+
* returns `undefined` if there are not any.
48+
*/
49+
export function parseTypeDirectives(
50+
sourceCode: string | undefined
51+
): Record<string, string> | undefined {
52+
if (!sourceCode) {
53+
return;
54+
}
55+
56+
// collect all the directives in the file and their start and end positions
57+
const directives: DirectiveInfo[] = [];
58+
let maybeMatch: RegExpExecArray | null = null;
59+
while ((maybeMatch = typeDirectiveRegEx.exec(sourceCode))) {
60+
const [matchString, , path] = maybeMatch;
61+
const { index: start } = maybeMatch;
62+
directives.push({
63+
path,
64+
start,
65+
end: start + matchString.length
66+
});
67+
}
68+
if (!directives.length) {
69+
return;
70+
}
71+
72+
// work from the last directive backwards for the next `import`/`export`
73+
// statement
74+
directives.reverse();
75+
const directiveRecords: Record<string, string> = {};
76+
for (const { path, start, end } of directives) {
77+
const searchString = sourceCode.substring(end);
78+
const maybeMatch = importExportRegEx.exec(searchString);
79+
if (maybeMatch) {
80+
const [, , fromPath] = maybeMatch;
81+
directiveRecords[fromPath] = path;
82+
}
83+
sourceCode = sourceCode.substring(0, start);
84+
}
85+
86+
return directiveRecords;
87+
}

tests/type_definitions/local/bar.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const bar: string;

tests/type_definitions/local/bar.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const bar = "bar";

tests/type_definitions/local/foo.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/** An exported value. */
2+
export const foo: string;

tests/type_definitions/local/foo.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const foo = "foo";

tests/type_definitions/local/main.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// @deno-types="./foo.d.ts"
2+
import * as foo from "./foo.js";
3+
4+
// @deno-types="./bar.d.ts"
5+
import * as bar from "./bar.js";
6+
7+
console.log(foo.foo, bar.bar);

tests/type_definitions/remote/main.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// @deno-types="https://unpkg.com/[email protected]/index.d.ts"
2+
import dayjs from "https://unpkg.com/[email protected]/esm/index.js";
3+
4+
const date = dayjs();
5+
console.log(date.second());
6+
7+
// @deno-types="https://unpkg.com/@types/[email protected]/index.d.ts"
8+
import * as _ from "https://unpkg.com/[email protected]/lodash.js";
9+
10+
console.log(_.add(1, 2));

0 commit comments

Comments
 (0)