1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-26 00:59:24 -05:00

Support .d.ts files (#2746)

Fixes #1432
This commit is contained in:
Kitson Kelly 2019-08-23 02:05:01 +10:00 committed by Ryan Dahl
parent bdc97b3976
commit 6c7d337960
13 changed files with 216 additions and 21 deletions

View file

@ -111,6 +111,7 @@ ts_sources = [
"../js/mock_builtin.js", "../js/mock_builtin.js",
"../js/net.ts", "../js/net.ts",
"../js/os.ts", "../js/os.ts",
"../js/performance.ts",
"../js/permissions.ts", "../js/permissions.ts",
"../js/plugins.d.ts", "../js/plugins.d.ts",
"../js/process.ts", "../js/process.ts",
@ -127,6 +128,7 @@ ts_sources = [
"../js/text_encoding.ts", "../js/text_encoding.ts",
"../js/timers.ts", "../js/timers.ts",
"../js/truncate.ts", "../js/truncate.ts",
"../js/type_directives.ts",
"../js/types.ts", "../js/types.ts",
"../js/url.ts", "../js/url.ts",
"../js/url_search_params.ts", "../js/url_search_params.ts",
@ -135,7 +137,6 @@ ts_sources = [
"../js/window.ts", "../js/window.ts",
"../js/workers.ts", "../js/workers.ts",
"../js/write_file.ts", "../js/write_file.ts",
"../js/performance.ts",
"../js/version.ts", "../js/version.ts",
"../js/xeval.ts", "../js/xeval.ts",
"../tsconfig.json", "../tsconfig.json",

View file

@ -10,6 +10,7 @@ import { cwd } from "./dir";
import { sendSync, msg, flatbuffers } from "./dispatch_flatbuffers"; import { sendSync, msg, flatbuffers } from "./dispatch_flatbuffers";
import * as os from "./os"; import * as os from "./os";
import { TextDecoder, TextEncoder } from "./text_encoding"; import { TextDecoder, TextEncoder } from "./text_encoding";
import { getMappedModuleName, parseTypeDirectives } from "./type_directives";
import { assert, notImplemented } from "./util"; import { assert, notImplemented } from "./util";
import * as util from "./util"; import * as util from "./util";
import { window } from "./window"; import { window } from "./window";
@ -110,6 +111,7 @@ interface SourceFile {
filename: string | undefined; filename: string | undefined;
mediaType: msg.MediaType; mediaType: msg.MediaType;
sourceCode: string | undefined; sourceCode: string | undefined;
typeDirectives?: Record<string, string>;
} }
interface EmitResult { interface EmitResult {
@ -119,7 +121,7 @@ interface EmitResult {
/** Ops to Rust to resolve and fetch a modules meta data. */ /** Ops to Rust to resolve and fetch a modules meta data. */
function fetchSourceFile(specifier: string, referrer: string): SourceFile { function fetchSourceFile(specifier: string, referrer: string): SourceFile {
util.log("compiler.fetchSourceFile", { specifier, referrer }); util.log("fetchSourceFile", { specifier, referrer });
// Send FetchSourceFile message // Send FetchSourceFile message
const builder = flatbuffers.createBuilder(); const builder = flatbuffers.createBuilder();
const specifier_ = builder.createString(specifier); const specifier_ = builder.createString(specifier);
@ -146,7 +148,8 @@ function fetchSourceFile(specifier: string, referrer: string): SourceFile {
moduleName: fetchSourceFileRes.moduleName() || undefined, moduleName: fetchSourceFileRes.moduleName() || undefined,
filename: fetchSourceFileRes.filename() || undefined, filename: fetchSourceFileRes.filename() || undefined,
mediaType: fetchSourceFileRes.mediaType(), mediaType: fetchSourceFileRes.mediaType(),
sourceCode sourceCode,
typeDirectives: parseTypeDirectives(sourceCode)
}; };
} }
@ -168,7 +171,7 @@ function humanFileSize(bytes: number): string {
/** Ops to rest for caching source map and compiled js */ /** Ops to rest for caching source map and compiled js */
function cache(extension: string, moduleId: string, contents: string): void { function cache(extension: string, moduleId: string, contents: string): void {
util.log("compiler.cache", moduleId); util.log("cache", extension, moduleId);
const builder = flatbuffers.createBuilder(); const builder = flatbuffers.createBuilder();
const extension_ = builder.createString(extension); const extension_ = builder.createString(extension);
const moduleId_ = builder.createString(moduleId); const moduleId_ = builder.createString(moduleId);
@ -189,7 +192,7 @@ const encoder = new TextEncoder();
function emitBundle(fileName: string, data: string): void { function emitBundle(fileName: string, data: string): void {
// For internal purposes, when trying to emit to `$deno$` just no-op // For internal purposes, when trying to emit to `$deno$` just no-op
if (fileName.startsWith("$deno$")) { if (fileName.startsWith("$deno$")) {
console.warn("skipping compiler.emitBundle", fileName); console.warn("skipping emitBundle", fileName);
return; return;
} }
const encodedData = encoder.encode(data); const encodedData = encoder.encode(data);
@ -217,7 +220,7 @@ function getExtension(
} }
class Host implements ts.CompilerHost { class Host implements ts.CompilerHost {
extensionCache: Record<string, ts.Extension> = {}; private _extensionCache: Record<string, ts.Extension> = {};
private readonly _options: ts.CompilerOptions = { private readonly _options: ts.CompilerOptions = {
allowJs: true, allowJs: true,
@ -232,23 +235,37 @@ class Host implements ts.CompilerHost {
target: ts.ScriptTarget.ESNext target: ts.ScriptTarget.ESNext
}; };
private _sourceFileCache: Record<string, SourceFile> = {};
private _resolveModule(specifier: string, referrer: string): SourceFile { private _resolveModule(specifier: string, referrer: string): SourceFile {
util.log("host._resolveModule", { specifier, referrer });
// Handle built-in assets specially. // Handle built-in assets specially.
if (specifier.startsWith(ASSETS)) { if (specifier.startsWith(ASSETS)) {
const moduleName = specifier.split("/").pop()!; const moduleName = specifier.split("/").pop()!;
if (moduleName in this._sourceFileCache) {
return this._sourceFileCache[moduleName];
}
const assetName = moduleName.includes(".") const assetName = moduleName.includes(".")
? moduleName ? moduleName
: `${moduleName}.d.ts`; : `${moduleName}.d.ts`;
assert(assetName in assetSourceCode, `No such asset "${assetName}"`); assert(assetName in assetSourceCode, `No such asset "${assetName}"`);
const sourceCode = assetSourceCode[assetName]; const sourceCode = assetSourceCode[assetName];
return { const sourceFile = {
moduleName, moduleName,
filename: specifier, filename: specifier,
mediaType: msg.MediaType.TypeScript, mediaType: msg.MediaType.TypeScript,
sourceCode sourceCode
}; };
this._sourceFileCache[moduleName] = sourceFile;
return sourceFile;
} }
return fetchSourceFile(specifier, referrer); const sourceFile = fetchSourceFile(specifier, referrer);
assert(sourceFile.moduleName != null);
const { moduleName } = sourceFile;
if (!(moduleName! in this._sourceFileCache)) {
this._sourceFileCache[moduleName!] = sourceFile;
}
return sourceFile;
} }
/* Deno specific APIs */ /* Deno specific APIs */
@ -277,7 +294,7 @@ class Host implements ts.CompilerHost {
* options which were ignored, or `undefined`. * options which were ignored, or `undefined`.
*/ */
configure(path: string, configurationText: string): ConfigureResponse { configure(path: string, configurationText: string): ConfigureResponse {
util.log("compile.configure", path); util.log("host.configure", path);
const { config, error } = ts.parseConfigFileTextToJson( const { config, error } = ts.parseConfigFileTextToJson(
path, path,
configurationText configurationText
@ -308,7 +325,10 @@ class Host implements ts.CompilerHost {
/* TypeScript CompilerHost APIs */ /* TypeScript CompilerHost APIs */
fileExists(_fileName: string): boolean { fileExists(fileName: string): boolean {
if (fileName.endsWith("package.json")) {
throw new TypeError("Automatic type resolution not supported");
}
return notImplemented(); return notImplemented();
} }
@ -342,13 +362,17 @@ class Host implements ts.CompilerHost {
): ts.SourceFile | undefined { ): ts.SourceFile | undefined {
assert(!shouldCreateNewSourceFile); assert(!shouldCreateNewSourceFile);
util.log("getSourceFile", fileName); util.log("getSourceFile", fileName);
const SourceFile = this._resolveModule(fileName, "."); const sourceFile =
if (!SourceFile || !SourceFile.sourceCode) { fileName in this._sourceFileCache
? this._sourceFileCache[fileName]
: this._resolveModule(fileName, ".");
assert(sourceFile != null);
if (!sourceFile.sourceCode) {
return undefined; return undefined;
} }
return ts.createSourceFile( return ts.createSourceFile(
fileName, fileName,
SourceFile.sourceCode, sourceFile.sourceCode,
languageVersion languageVersion
); );
} }
@ -362,26 +386,37 @@ class Host implements ts.CompilerHost {
containingFile: string containingFile: string
): Array<ts.ResolvedModuleFull | undefined> { ): Array<ts.ResolvedModuleFull | undefined> {
util.log("resolveModuleNames()", { moduleNames, containingFile }); util.log("resolveModuleNames()", { moduleNames, containingFile });
const typeDirectives: Record<string, string> | undefined =
containingFile in this._sourceFileCache
? this._sourceFileCache[containingFile].typeDirectives
: undefined;
return moduleNames.map( return moduleNames.map(
(moduleName): ts.ResolvedModuleFull | undefined => { (moduleName): ts.ResolvedModuleFull | undefined => {
const SourceFile = this._resolveModule(moduleName, containingFile); const mappedModuleName = getMappedModuleName(
if (SourceFile.moduleName) { moduleName,
const resolvedFileName = SourceFile.moduleName; containingFile,
typeDirectives
);
const sourceFile = this._resolveModule(
mappedModuleName,
containingFile
);
if (sourceFile.moduleName) {
const resolvedFileName = sourceFile.moduleName;
// This flags to the compiler to not go looking to transpile functional // This flags to the compiler to not go looking to transpile functional
// code, anything that is in `/$asset$/` is just library code // code, anything that is in `/$asset$/` is just library code
const isExternalLibraryImport = moduleName.startsWith(ASSETS); const isExternalLibraryImport = moduleName.startsWith(ASSETS);
const extension = getExtension( const extension = getExtension(
resolvedFileName, resolvedFileName,
SourceFile.mediaType sourceFile.mediaType
); );
this.extensionCache[resolvedFileName] = extension; this._extensionCache[resolvedFileName] = extension;
const r = { return {
resolvedFileName, resolvedFileName,
isExternalLibraryImport, isExternalLibraryImport,
extension extension
}; };
return r;
} else { } else {
return undefined; return undefined;
} }
@ -407,7 +442,7 @@ class Host implements ts.CompilerHost {
} else { } else {
assert(sourceFiles != null && sourceFiles.length == 1); assert(sourceFiles != null && sourceFiles.length == 1);
const sourceFileName = sourceFiles![0].fileName; const sourceFileName = sourceFiles![0].fileName;
const maybeExtension = this.extensionCache[sourceFileName]; const maybeExtension = this._extensionCache[sourceFileName];
if (maybeExtension) { if (maybeExtension) {
// NOTE: If it's a `.json` file we don't want to write it to disk. // NOTE: If it's a `.json` file we don't want to write it to disk.

87
js/type_directives.ts Normal file
View file

@ -0,0 +1,87 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
interface DirectiveInfo {
path: string;
start: number;
end: number;
}
/** Remap the module name based on any supplied type directives passed. */
export function getMappedModuleName(
moduleName: string,
containingFile: string,
typeDirectives?: Record<string, string>
): string {
if (containingFile.endsWith(".d.ts") && !moduleName.endsWith(".d.ts")) {
moduleName = `${moduleName}.d.ts`;
}
if (!typeDirectives) {
return moduleName;
}
if (moduleName in typeDirectives) {
return typeDirectives[moduleName];
}
return moduleName;
}
/** Matches directives that look something like this and parses out the value
* of the directive:
*
* // @deno-types="./foo.d.ts"
*
* [See Diagram](http://bit.ly/31nZPCF)
*/
const typeDirectiveRegEx = /@deno-types\s*=\s*(["'])((?:(?=(\\?))\3.)*?)\1/gi;
/** Matches `import` or `export from` statements and parses out the value of the
* module specifier in the second capture group:
*
* import * as foo from "./foo.js"
* export { a, b, c } from "./bar.js"
*
* [See Diagram](http://bit.ly/2GSkJlF)
*/
const importExportRegEx = /(?:import|export)\s+[\s\S]*?from\s+(["'])((?:(?=(\\?))\3.)*?)\1/;
/** Parses out any Deno type directives that are part of the source code, or
* returns `undefined` if there are not any.
*/
export function parseTypeDirectives(
sourceCode: string | undefined
): Record<string, string> | undefined {
if (!sourceCode) {
return;
}
// collect all the directives in the file and their start and end positions
const directives: DirectiveInfo[] = [];
let maybeMatch: RegExpExecArray | null = null;
while ((maybeMatch = typeDirectiveRegEx.exec(sourceCode))) {
const [matchString, , path] = maybeMatch;
const { index: start } = maybeMatch;
directives.push({
path,
start,
end: start + matchString.length
});
}
if (!directives.length) {
return;
}
// work from the last directive backwards for the next `import`/`export`
// statement
directives.reverse();
const directiveRecords: Record<string, string> = {};
for (const { path, start, end } of directives) {
const searchString = sourceCode.substring(end);
const maybeMatch = importExportRegEx.exec(searchString);
if (maybeMatch) {
const [, , fromPath] = maybeMatch;
directiveRecords[fromPath] = path;
}
sourceCode = sourceCode.substring(0, start);
}
return directiveRecords;
}

View file

@ -0,0 +1,4 @@
args: run --reload tests/error_type_definitions.ts
check_stderr: true
exit_code: 1
output: tests/error_type_definitions.ts.out

View file

@ -0,0 +1,5 @@
// @deno-types="./type_definitions/bar.d.ts"
import { Bar } from "./type_definitions/bar.js";
const bar = new Bar();
console.log(bar);

View file

@ -0,0 +1,4 @@
[WILDCARD]error: Uncaught TypeError: Automatic type resolution not supported
[WILDCARD]js/compiler.ts:[WILDCARD]
at fileExists (js/compiler.ts:[WILDCARD])
[WILDCARD]

View file

@ -0,0 +1,2 @@
args: run --reload tests/type_definitions.ts
output: tests/type_definitions.ts.out

View file

@ -0,0 +1,4 @@
// @deno-types="./type_definitions/foo.d.ts"
import { foo } from "./type_definitions/foo.js";
console.log(foo);

View file

@ -0,0 +1 @@
[WILDCARD]foo

7
tests/type_definitions/bar.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
/// <reference types="baz" />
declare namespace bar {
export class Bar {
baz: string;
}
}

2
tests/type_definitions/foo.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
/** An exported value. */
export const foo: string;

View file

@ -0,0 +1 @@
export const foo = "foo";

View file

@ -580,6 +580,48 @@ import { test, assertEquals } from "./deps.ts";
This design circumvents a plethora of complexity spawned by package management This design circumvents a plethora of complexity spawned by package management
software, centralized code repositories, and superfluous file formats. software, centralized code repositories, and superfluous file formats.
### Using external type definitions
Deno supports both JavaScript and TypeScript as first class languages at
runtime. This means it requires fully qualified module names, including the
extension (or a server providing the correct media type). In addition, Deno has
no "magical" module resolution.
The out of the box TypeScript compiler though relies on both extension-less
modules and the Node.js module resolution logic to apply types to JavaScript
modules.
In order to bridge this gap, Deno supports compiler hints that inform Deno the
location of `.d.ts` files and the JavaScript code they relate to. A compiler
hint looks like this:
```ts
// @deno-types="./foo.d.ts"
import * as foo from "./foo.js";
```
Where the hint effects the next `import` statement (or `export ... from`
statement) where the value of the `@deno-types` will be substituted at compile
time instead of the specified module. Like in the above example, the Deno
compiler will load `./foo.d.ts` instead of `./foo.js`. Deno will still load
`./foo.js` when it runs the program.
**Not all type definitions are supported.**
Deno will use the compiler hint to load the indicated `.d.ts` files, but some
`.d.ts` files contain unsupported features. Specifically, some `.d.ts` files
expect to be able to load or reference type definitions from other packages
using the module resolution logic. For example a type reference directive to
include `node`, expecting to resolve to some path like
`./node_modules/@types/node/index.d.ts`. Since this depends on non-relative
"magical" resolution, Deno cannot resolve this.
**Why not use the triple-slash type reference?**
The TypeScript compiler supports triple-slash directives, including a type
reference directive. If Deno used this, it would interfere with the behavior of
the TypeScript compiler.
### Testing if current file is the main program ### Testing if current file is the main program
To test if the current script has been executed as the main input to the program To test if the current script has been executed as the main input to the program