diff --git a/README.md b/README.md index adfffb4..64e3810 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,10 @@ The extension supports the following ways to load external declaration files ```ts // @deno-types="./foo.d.ts" -import * as foo from "./foo.js"; +import { foo } from "./foo.js"; ``` -> This will not be implemented in then extensions. +see [example](/examples/compile-hint/mod.ts) 2. `Triple-slash` reference directive @@ -99,6 +99,8 @@ import { format } from "https://deno.land/x/date_fns/index.js"; format(new Date(), "yyyy/MM/DD"); ``` +see [example](/examples/react/mod.tsx) + 3. `X-TypeScript-Types` custom header ```ts diff --git a/core/deno_deps.test.ts b/core/deno_deps.test.ts index 7c47140..a8f8b62 100644 --- a/core/deno_deps.test.ts +++ b/core/deno_deps.test.ts @@ -42,9 +42,11 @@ import test from "test.ts"; import * as test from "test.ts"; export { window } from "export.ts"; export * from "export_as.ts"; -export * as xx from "export_as_default.ts"; +export * as xx from "export_as_default.ts"; // example `, - ts.ScriptTarget.ESNext + ts.ScriptTarget.ESNext, + true, + ts.ScriptKind.TSX ); expect(getImportModules(ts)(sourceFile)).toStrictEqual([ @@ -57,6 +59,16 @@ export * as xx from "export_as_default.ts"; }, }, { + hint: undefined, + leadingComments: [ + { + end: 97, + hasTrailingNewLine: true, + kind: 2, + pos: 0, + text: `/// `, + }, + ], moduleName: "./foo.ts", location: { start: { line: 1, character: 8 }, @@ -118,6 +130,15 @@ export * as xx from "export_as_default.ts"; start: { line: 9, character: 21 }, end: { line: 9, character: 41 }, }, + trailingComments: [ + { + end: 373, + hasTrailingNewLine: true, + kind: 2, + pos: 363, + text: "// example", + }, + ], }, ] as ImportModule[]); }); diff --git a/core/deno_deps.ts b/core/deno_deps.ts index ef5c57f..6c49ab3 100644 --- a/core/deno_deps.ts +++ b/core/deno_deps.ts @@ -5,6 +5,11 @@ import typescript = require("typescript"); import { getDenoDepsDir } from "./deno"; import { HashMeta } from "./hash_meta"; +import { parseCompileHint } from "./deno_type_hint"; + +interface Comment extends typescript.CommentRange { + text: string; +} export type Deps = { url: string; @@ -70,9 +75,18 @@ export async function getAllDenoCachedDeps(): Promise { return deps; } +export type Hint = { + text: string; + range: Range; + contentRange: Range; +}; + export type ImportModule = { moduleName: string; + hint?: Hint; // if import module with @deno-types="xxx" hint location: Range; + leadingComments?: Comment[]; + trailingComments?: Comment[]; }; export function getImportModules(ts: typeof typescript) { @@ -145,15 +159,52 @@ export function getImportModules(ts: typeof typescript) { // delint it delint(sourceFile); + const text = sourceFile.getFullText(); + + const getComments = ( + node: typescript.Node, + isTrailing: boolean + ): Comment[] | undefined => { + /* istanbul ignore else */ + if (node.parent) { + const nodePos = isTrailing ? node.end : node.pos; + const parentPos = isTrailing ? node.parent.end : node.parent.pos; + + if ( + node.parent.kind === ts.SyntaxKind.SourceFile || + nodePos !== parentPos + ) { + const comments = isTrailing + ? ts.getTrailingCommentRanges(sourceFile.text, nodePos) + : ts.getLeadingCommentRanges(sourceFile.text, nodePos); + + if (Array.isArray(comments)) { + return comments.map((v) => { + const target: Comment = { + ...v, + text: text.substring(v.pos, v.end), + }; + + return target; + }); + } + + return undefined; + } + } + }; + const modules: ImportModule[] = sourceFile.typeReferenceDirectives .map((directive) => { const start = sourceFile.getLineAndCharacterOfPosition(directive.pos); const end = sourceFile.getLineAndCharacterOfPosition(directive.end); - return { + const module: ImportModule = { moduleName: directive.fileName, location: { start, end }, }; + + return module; }) .concat( moduleNodes.map((node) => { @@ -175,10 +226,30 @@ export function getImportModules(ts: typeof typescript) { end, }; - return { + const leadingComments = getComments(node.parent, false); + const trailingComments = getComments(node.parent, true); + + const module: ImportModule = { moduleName: node.text, location, }; + + if (trailingComments) { + module.trailingComments = trailingComments; + } + + if (leadingComments) { + module.leadingComments = leadingComments; + // get the last comment + const comment = + module.leadingComments[module.leadingComments.length - 1]; + + const hint = parseCompileHint(sourceFile, comment); + + module.hint = hint; + } + + return module; }) ); diff --git a/core/deno_type_hint.test.ts b/core/deno_type_hint.test.ts index 639cb41..44c0ed3 100644 --- a/core/deno_type_hint.test.ts +++ b/core/deno_type_hint.test.ts @@ -19,13 +19,14 @@ test("core / deno_type_hint: with compile hint", async () => { `// @deno-types="./foo.d.ts" import "./foo.ts" `, - ts.ScriptTarget.ESNext + ts.ScriptTarget.ESNext, + true, + ts.ScriptKind.TSX ); const [comment] = getDenoCompileHint(ts)(sourceFile); expect(comment).not.toBe(undefined); - expect(comment.module).toEqual("./foo.d.ts"); - expect(comment.text).toEqual(`// @deno-types="./foo.d.ts"`); + expect(comment.text).toEqual(`./foo.d.ts`); expect(comment.range).toEqual({ start: { line: 0, character: 0 }, end: { line: 0, character: 27 }, @@ -42,13 +43,14 @@ test("core / deno_type_hint: with compile hint", async () => { `// @deno-types="/absolute/path/to/foo.d.ts" import "./foo.ts" `, - ts.ScriptTarget.ESNext + ts.ScriptTarget.ESNext, + true, + ts.ScriptKind.TSX ); const [comment] = getDenoCompileHint(ts)(sourceFile); expect(comment).not.toBe(undefined); - expect(comment.module).toEqual("/absolute/path/to/foo.d.ts"); - expect(comment.text).toEqual(`// @deno-types="/absolute/path/to/foo.d.ts"`); + expect(comment.text).toEqual(`/absolute/path/to/foo.d.ts`); }); test("core / deno_type_hint: with compile hint 1", async () => { @@ -62,13 +64,14 @@ test("core / deno_type_hint: with compile hint 1", async () => { import "./foo.ts" `, - ts.ScriptTarget.ESNext + ts.ScriptTarget.ESNext, + true, + ts.ScriptKind.TSX ); const [comment] = getDenoCompileHint(ts)(sourceFile); expect(comment).not.toBe(undefined); - expect(comment.module).toEqual("./foo.d.ts"); - expect(comment.text).toEqual(`// @deno-types="./foo.d.ts"`); + expect(comment.text).toEqual(`./foo.d.ts`); expect(comment.range).toEqual({ start: { line: 3, character: 0 }, end: { line: 3, character: 27 }, @@ -93,13 +96,14 @@ test("core / deno_type_hint: with compile hint 2", async () => { */ /* prefix */ import "./foo.ts" // hasTrailingNewLine `, - ts.ScriptTarget.ESNext + ts.ScriptTarget.ESNext, + true, + ts.ScriptKind.TSX ); const [comment] = getDenoCompileHint(ts)(sourceFile); expect(comment).not.toBe(undefined); - expect(comment.module).toEqual("./foo.d.ts"); - expect(comment.text).toEqual(`// @deno-types="./foo.d.ts"`); + expect(comment.text).toEqual(`./foo.d.ts`); expect(comment.range).toEqual({ start: { line: 3, character: 0 }, end: { line: 3, character: 27 }, diff --git a/core/deno_type_hint.ts b/core/deno_type_hint.ts index ed7c743..c34fec7 100644 --- a/core/deno_type_hint.ts +++ b/core/deno_type_hint.ts @@ -1,16 +1,5 @@ -import * as path from "path"; import typescript = require("typescript"); -import { normalizeFilepath } from "./util"; - -interface CommentRange extends typescript.CommentRange { - text: string; - module: string; - filepath: string; - range: Range; - contentRange: Range; -} - export interface Position { line: number; character: number; @@ -38,16 +27,53 @@ export const Range = { }, }; +export type compileHint = { + text: string; + range: Range; + contentRange: Range; +}; + +export function parseCompileHint( + sourceFile: typescript.SourceFile, + comment: typescript.CommentRange +): compileHint | undefined { + const text = sourceFile.getFullText().substring(comment.pos, comment.end); + const regexp = /@deno-types=['"]([^'"]+)['"]/; + + const matchers = regexp.exec(text); + + if (!matchers) { + return; + } + + const start = sourceFile.getLineAndCharacterOfPosition(comment.pos); + const end = sourceFile.getLineAndCharacterOfPosition(comment.end); + + const moduleNameStart = Position.create( + start.line, + start.character + '// @deno-types="'.length + ); + const moduleNameEnd = Position.create(end.line, end.character - '"'.length); + + const moduleName = matchers[1]; + + return { + text: moduleName, + range: Range.create(start, end), + contentRange: Range.create(moduleNameStart, moduleNameEnd), + }; +} + /** * Get Deno compile hint from a source file * @param ts */ export function getDenoCompileHint(ts: typeof typescript) { - return function (sourceFile: typescript.SourceFile) { - const denoTypesComments: CommentRange[] = []; + return function (sourceFile: typescript.SourceFile, pos = 0): compileHint[] { + const denoTypesComments: compileHint[] = []; const comments = - ts.getLeadingCommentRanges(sourceFile.getFullText(), 0) || []; + ts.getLeadingCommentRanges(sourceFile.getFullText(), pos) || []; for (const comment of comments) { if (comment.hasTrailingNewLine) { @@ -59,34 +85,12 @@ export function getDenoCompileHint(ts: typeof typescript) { const matchers = regexp.exec(text); if (matchers) { - const start = sourceFile.getLineAndCharacterOfPosition(comment.pos); - const end = sourceFile.getLineAndCharacterOfPosition(comment.end); - - const moduleNameStart = Position.create( - start.line, - start.character + '// @deno-types="'.length - ); - const moduleNameEnd = Position.create( - end.line, - end.character - '"'.length - ); - - const moduleName = matchers[1]; - - const moduleFilepath = normalizeFilepath(moduleName); - - const targetFilepath = path.isAbsolute(moduleFilepath) - ? moduleFilepath - : path.resolve(path.dirname(sourceFile.fileName), moduleFilepath); - - denoTypesComments.push({ - ...comment, - text, - module: moduleName, - filepath: targetFilepath, - range: Range.create(start, end), - contentRange: Range.create(moduleNameStart, moduleNameEnd), - }); + const compileHint = parseCompileHint(sourceFile, comment); + + /* istanbul ignore else */ + if (compileHint) { + denoTypesComments.push(compileHint); + } } } } diff --git a/core/hash_meta.ts b/core/hash_meta.ts index 74378dd..1a13715 100644 --- a/core/hash_meta.ts +++ b/core/hash_meta.ts @@ -110,6 +110,9 @@ export class HashMeta implements HashMetaInterface { case Type.JavaScriptReact: return ".jsx"; case Type.TypeScript: + if (this.url.pathname.endsWith(".d.ts")) { + return ".d.ts"; + } return ".ts"; /* istanbul ignore next */ case Type.TypeScriptReact: diff --git a/core/module_resolver.test.ts b/core/module_resolver.test.ts index c36baae..a9df43b 100644 --- a/core/module_resolver.test.ts +++ b/core/module_resolver.test.ts @@ -147,7 +147,7 @@ test("core / module_resolver: resolve module from local", () => { undefined, undefined, { - extension: ".ts", + extension: ".d.ts", origin: "https://example.com/x-typescript-types", filepath: path.join( denoDir, diff --git a/examples/compile-hint/.vscode/settings.json b/examples/compile-hint/.vscode/settings.json new file mode 100644 index 0000000..cbac569 --- /dev/null +++ b/examples/compile-hint/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} diff --git a/examples/compile-hint/foo.js b/examples/compile-hint/foo.js new file mode 100644 index 0000000..4436ec1 --- /dev/null +++ b/examples/compile-hint/foo.js @@ -0,0 +1,5 @@ +export const foo = "foo"; + +export function updateProfile() { + // +} diff --git a/examples/compile-hint/mod.ts b/examples/compile-hint/mod.ts new file mode 100644 index 0000000..906a058 --- /dev/null +++ b/examples/compile-hint/mod.ts @@ -0,0 +1,10 @@ +// @deno-types="./types/foo.d.ts" +import { foo, bar } from "./foo.js"; + +function getName(name: string) { + console.log(name); +} + +getName(foo); + +bar(); diff --git a/examples/compile-hint/types/foo.d.ts b/examples/compile-hint/types/foo.d.ts new file mode 100644 index 0000000..c04adfe --- /dev/null +++ b/examples/compile-hint/types/foo.d.ts @@ -0,0 +1,3 @@ +export const foo: string; + +export function bar(): void; diff --git a/server/src/deno_types.ts b/server/src/deno_types.ts index 16b50b7..b6a54c5 100644 --- a/server/src/deno_types.ts +++ b/server/src/deno_types.ts @@ -15,7 +15,7 @@ export function getDenoTypesHintsFromDocument(document: TextDocument) { uri.fsPath, document.getText(), ts.ScriptTarget.ESNext, - false, + true, ts.ScriptKind.TSX ); diff --git a/server/src/dependency_tree.ts b/server/src/dependency_tree.ts index 0c9caee..cf2a58d 100644 --- a/server/src/dependency_tree.ts +++ b/server/src/dependency_tree.ts @@ -53,7 +53,7 @@ export class DependencyTree { filepath, await fs.readFile(filepath, { encoding: "utf8" }), ts.ScriptTarget.ESNext, - false, + true, ts.ScriptKind.TSX ); diff --git a/server/src/language/definition.ts b/server/src/language/definition.ts index 7766142..ea8212a 100644 --- a/server/src/language/definition.ts +++ b/server/src/language/definition.ts @@ -8,7 +8,7 @@ import { TextDocument } from "vscode-languageserver-textdocument"; import { URI } from "vscode-uri"; import { getDenoTypesHintsFromDocument } from "../deno_types"; -import { pathExists } from "../../../core/util"; +import { ModuleResolver } from "../../../core/module_resolver"; export class Definition { constructor(connection: IConnection, documents: TextDocuments) { @@ -20,6 +20,10 @@ export class Definition { return; } + const uri = URI.parse(document.uri); + + const resolver = ModuleResolver.create(uri.fsPath); + const locations: Location[] = []; const denoTypesComments = getDenoTypesHintsFromDocument(document); @@ -33,10 +37,11 @@ export class Definition { position.character >= start.character && position.character <= end.character ) { - if ((await pathExists(typeComment.filepath)) === true) { + const [typeModule] = resolver.resolveModules([typeComment.text]); + if (typeModule) { locations.push( Location.create( - URI.file(typeComment.filepath).toString(), + URI.file(typeModule.filepath).toString(), Range.create(0, 0, 0, 0) ) ); diff --git a/server/src/language/diagnostics.ts b/server/src/language/diagnostics.ts index b1b683e..e5865bc 100644 --- a/server/src/language/diagnostics.ts +++ b/server/src/language/diagnostics.ts @@ -18,7 +18,7 @@ import { Bridge } from "../bridge"; import { ModuleResolver } from "../../../core/module_resolver"; import { pathExists, isHttpURL, isValidDenoDocument } from "../../../core/util"; import { ImportMap } from "../../../core/import_map"; -import { getImportModules } from "../../../core/deno_deps"; +import { getImportModules, Range } from "../../../core/deno_deps"; type Fix = { title: string; @@ -136,7 +136,7 @@ export class Diagnostics { uri.fsPath, document.getText(), ts.ScriptTarget.ESNext, - false, + true, ts.ScriptKind.TSX ); @@ -145,10 +145,10 @@ export class Diagnostics { const diagnosticsForThisDocument: Diagnostic[] = []; const resolver = ModuleResolver.create(uri.fsPath, importMapFilepath); - for (const importModule of importModules) { - const [resolvedModule] = resolver.resolveModules([ - importModule.moduleName, - ]); + const handle = async (originModuleName: string, location: Range) => { + const importModuleName = originModuleName; + + const [resolvedModule] = resolver.resolveModules([importModuleName]); if ( !resolvedModule || @@ -156,14 +156,12 @@ export class Diagnostics { ) { const moduleName = resolvedModule ? resolvedModule.origin - : ImportMap.create(importMapFilepath).resolveModule( - importModule.moduleName - ); + : ImportMap.create(importMapFilepath).resolveModule(importModuleName); if (isHttpURL(moduleName)) { diagnosticsForThisDocument.push( Diagnostic.create( - importModule.location, + location, localize( "diagnostic.report.module_not_found_locally", moduleName @@ -173,7 +171,7 @@ export class Diagnostics { this.name ) ); - continue; + return; } console.log(moduleName); @@ -185,7 +183,7 @@ export class Diagnostics { ) { diagnosticsForThisDocument.push( Diagnostic.create( - importModule.location, + location, localize( "diagnostic.report.module_not_found_locally", moduleName @@ -195,13 +193,13 @@ export class Diagnostics { this.name ) ); - continue; + return; } // invalid module diagnosticsForThisDocument.push( Diagnostic.create( - importModule.location, + location, localize("diagnostic.report.invalid_import", moduleName), DiagnosticSeverity.Error, DiagnosticCode.InvalidImport, @@ -209,6 +207,13 @@ export class Diagnostics { ) ); } + }; + + for (const importModule of importModules) { + await handle(importModule.moduleName, importModule.location); + if (importModule.hint) { + await handle(importModule.hint.text, importModule.hint.contentRange); + } } return diagnosticsForThisDocument; diff --git a/server/src/language/references.ts b/server/src/language/references.ts index b30a0dd..229fe1e 100644 --- a/server/src/language/references.ts +++ b/server/src/language/references.ts @@ -8,7 +8,7 @@ import { TextDocument } from "vscode-languageserver-textdocument"; import { URI } from "vscode-uri"; import { getDenoTypesHintsFromDocument } from "../deno_types"; -import { pathExists } from "../../../core/util"; +import { ModuleResolver } from "../../../core/module_resolver"; export class References { constructor(connection: IConnection, documents: TextDocuments) { @@ -20,6 +20,10 @@ export class References { return; } + const uri = URI.parse(document.uri); + + const resolver = ModuleResolver.create(uri.fsPath); + const locations: Location[] = []; const denoTypesComments = getDenoTypesHintsFromDocument(document); @@ -33,10 +37,11 @@ export class References { position.character >= start.character && position.character <= end.character ) { - if ((await pathExists(typeComment.filepath)) === true) { + const [typeModule] = resolver.resolveModules([typeComment.text]); + if (typeModule) { locations.push( Location.create( - URI.file(typeComment.filepath).toString(), + URI.file(typeModule.filepath).toString(), Range.create(0, 0, 0, 0) ) ); diff --git a/typescript-deno-plugin/src/plugin.ts b/typescript-deno-plugin/src/plugin.ts index fcea5ec..1ea22b6 100644 --- a/typescript-deno-plugin/src/plugin.ts +++ b/typescript-deno-plugin/src/plugin.ts @@ -11,6 +11,7 @@ import { CacheModule } from "../../core/deno_cache"; import { pathExistsSync, normalizeFilepath } from "../../core/util"; import { normalizeImportStatement } from "../../core/deno_normalize_import_statement"; import { readConfigurationFromVscodeSettings } from "../../core/vscode_settings"; +import { getImportModules } from "../../core/deno_deps"; export class DenoPlugin implements ts_module.server.PluginModule { // plugin name @@ -93,9 +94,6 @@ export class DenoPlugin implements ts_module.server.PluginModule { this.MUST_OVERWRITE_OPTIONS ); - this.logger.info( - `compilationSettings:${JSON.stringify(compilationSettings)}` - ); return compilationSettings; }; @@ -363,6 +361,28 @@ export class DenoPlugin implements ts_module.server.PluginModule { importMapsFilepath ); + const content = this.typescript.sys.readFile(containingFile, "utf8"); + + // handle @deno-types + if (content && content.indexOf("// @deno-types=") >= 0) { + const sourceFile = this.typescript.createSourceFile( + containingFile, + content, + this.typescript.ScriptTarget.ESNext, + true + ); + + const modules = getImportModules(this.typescript)(sourceFile); + + for (const m of modules) { + if (m.hint) { + const index = moduleNames.findIndex((v) => v === m.moduleName); + + moduleNames[index] = m.hint.text; + } + } + } + const resolvedModules = resolver.resolveModules(moduleNames); // try resolve typeReferenceDirectives