Skip to content
This repository was archived by the owner on Sep 4, 2020. It is now read-only.

Commit 7b45013

Browse files
authored
support @deno-types (#46)
Signed-off-by: 迷渡 <justjavac@gmail.com>
1 parent e4fd984 commit 7b45013

File tree

3 files changed

+323
-0
lines changed

3 files changed

+323
-0
lines changed

src/deno_modules.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import ts from "typescript/lib/tsserverlibrary";
2+
3+
import { parseCompileHint } from "./deno_type_hint";
4+
5+
interface Comment extends ts.CommentRange {
6+
text: string;
7+
}
8+
9+
export type Position = {
10+
line: number; // zero base
11+
character: number; // zero base
12+
};
13+
14+
export type Range = {
15+
start: Position;
16+
end: Position;
17+
};
18+
19+
export type Hint = {
20+
text: string;
21+
range: Range;
22+
contentRange: Range;
23+
};
24+
25+
export type ImportModule = {
26+
moduleName: string;
27+
hint?: Hint; // if import module with @deno-types="xxx" hint
28+
location: Range;
29+
start: number;
30+
length: number;
31+
leadingComments?: Comment[];
32+
trailingComments?: Comment[];
33+
};
34+
35+
export function getImportModules(sourceFile: ts.SourceFile): ImportModule[] {
36+
const moduleNodes: ts.LiteralLikeNode[] = [];
37+
38+
function delint(SourceFile: ts.SourceFile) {
39+
function delintNode(node: ts.Node) {
40+
let moduleNode: ts.LiteralLikeNode | null = null;
41+
42+
// import('xxx')
43+
if (ts.isCallExpression(node)) {
44+
const expression = node.expression;
45+
const args = node.arguments;
46+
const isDynamicImport = expression.kind === ts.SyntaxKind.ImportKeyword;
47+
/* istanbul ignore else */
48+
if (isDynamicImport) {
49+
const argv = args[0] as ts.StringLiteral;
50+
51+
/* istanbul ignore else */
52+
if (argv && ts.isStringLiteral(argv)) {
53+
moduleNode = argv;
54+
}
55+
}
56+
} // import ts = require('ts')
57+
else if (ts.isImportEqualsDeclaration(node)) {
58+
const ref = node.moduleReference;
59+
60+
/* istanbul ignore else */
61+
if (
62+
ts.isExternalModuleReference(ref) &&
63+
ref.expression &&
64+
ts.isStringLiteral(ref.expression)
65+
) {
66+
moduleNode = ref.expression;
67+
}
68+
} // import * as from 'xx'
69+
// import 'xx'
70+
// import xx from 'xx'
71+
else if (ts.isImportDeclaration(node)) {
72+
const spec = node.moduleSpecifier;
73+
/* istanbul ignore else */
74+
if (ts.isStringLiteral(spec)) {
75+
moduleNode = spec;
76+
}
77+
} // export { window } from "xxx";
78+
// export * from "xxx";
79+
// export * as xxx from "xxx";
80+
else if (ts.isExportDeclaration(node)) {
81+
const exportSpec = node.moduleSpecifier;
82+
/* istanbul ignore else */
83+
if (exportSpec && ts.isStringLiteral(exportSpec)) {
84+
moduleNode = exportSpec;
85+
}
86+
}
87+
88+
if (moduleNode) {
89+
moduleNodes.push(moduleNode);
90+
}
91+
92+
ts.forEachChild(node, delintNode);
93+
}
94+
95+
delintNode(SourceFile);
96+
}
97+
98+
// delint it
99+
delint(sourceFile);
100+
101+
const text: string = sourceFile.getFullText();
102+
103+
const getComments = (
104+
node: ts.Node,
105+
isTrailing: boolean,
106+
): Comment[] | undefined => {
107+
/* istanbul ignore else */
108+
if (node.parent) {
109+
const nodePos: number = isTrailing ? node.end : node.pos;
110+
const parentPos: number = isTrailing ? node.parent.end : node.parent.pos;
111+
112+
if (
113+
node.parent.kind === ts.SyntaxKind.SourceFile ||
114+
nodePos !== parentPos
115+
) {
116+
const comments: ts.CommentRange[] | undefined = isTrailing
117+
? ts.getTrailingCommentRanges(sourceFile.text, nodePos)
118+
: ts.getLeadingCommentRanges(sourceFile.text, nodePos);
119+
120+
if (Array.isArray(comments)) {
121+
return comments.map((v) => {
122+
const target: Comment = {
123+
...v,
124+
text: text.substring(v.pos, v.end),
125+
};
126+
127+
return target;
128+
});
129+
}
130+
131+
return undefined;
132+
}
133+
}
134+
};
135+
136+
const modules: ImportModule[] = sourceFile.typeReferenceDirectives
137+
.map((directive: ts.FileReference) => {
138+
const start = sourceFile.getLineAndCharacterOfPosition(directive.pos);
139+
const end = sourceFile.getLineAndCharacterOfPosition(directive.end);
140+
141+
const module: ImportModule = {
142+
moduleName: directive.fileName,
143+
location: { start, end },
144+
start: directive.pos,
145+
length: directive.end - directive.pos,
146+
};
147+
148+
return module;
149+
})
150+
.concat(
151+
moduleNodes.map((node) => {
152+
const numberOfSpaces = Math.abs(
153+
// why plus 2?
154+
// because `moduleNode.text` only contain the plaintext without two quotes
155+
// eg `import "./test"`
156+
node.end - node.pos - (node.text.length + 2),
157+
);
158+
159+
const startPosition = node.pos + numberOfSpaces + 1; // +1 to remove quotes
160+
const endPosition = startPosition + node.text.length;
161+
162+
const start = sourceFile.getLineAndCharacterOfPosition(startPosition);
163+
const end = sourceFile.getLineAndCharacterOfPosition(endPosition);
164+
165+
const location = {
166+
start,
167+
end,
168+
};
169+
170+
const leadingComments = getComments(node.parent, false);
171+
const trailingComments = getComments(node.parent, true);
172+
173+
const module: ImportModule = {
174+
moduleName: node.text,
175+
location,
176+
start: startPosition,
177+
length: endPosition - startPosition,
178+
};
179+
180+
if (trailingComments) {
181+
module.trailingComments = trailingComments;
182+
}
183+
184+
if (leadingComments) {
185+
module.leadingComments = leadingComments;
186+
// get the last comment
187+
const comment =
188+
module.leadingComments[module.leadingComments.length - 1];
189+
190+
const hint = parseCompileHint(sourceFile, comment);
191+
192+
module.hint = hint;
193+
}
194+
195+
return module;
196+
}),
197+
);
198+
199+
return modules;
200+
}

src/deno_type_hint.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import ts from "typescript/lib/tsserverlibrary";
2+
3+
export interface Position {
4+
line: number;
5+
character: number;
6+
}
7+
8+
export interface Range {
9+
start: Position;
10+
end: Position;
11+
}
12+
13+
export const Position = {
14+
/**
15+
* Creates a new Position literal from the given line and character.
16+
* @param line The position's line.
17+
* @param character The position's character.
18+
*/
19+
create(line: number, character: number): Position {
20+
return { line, character };
21+
},
22+
};
23+
24+
export const Range = {
25+
create(start: Position, end: Position): Range {
26+
return { start, end };
27+
},
28+
};
29+
30+
export type compileHint = {
31+
text: string;
32+
range: Range;
33+
contentRange: Range;
34+
};
35+
36+
export function parseCompileHint(
37+
sourceFile: ts.SourceFile,
38+
comment: ts.CommentRange,
39+
): compileHint | undefined {
40+
const text = sourceFile.getFullText().substring(comment.pos, comment.end);
41+
const regexp = /@deno-types=['"]([^'"]+)['"]/;
42+
43+
const matchers = regexp.exec(text);
44+
45+
if (!matchers) {
46+
return;
47+
}
48+
49+
const start = sourceFile.getLineAndCharacterOfPosition(comment.pos);
50+
const end = sourceFile.getLineAndCharacterOfPosition(comment.end);
51+
52+
const moduleNameStart = Position.create(
53+
start.line,
54+
start.character + '// @deno-types="'.length,
55+
);
56+
const moduleNameEnd = Position.create(end.line, end.character - '"'.length);
57+
58+
const moduleName = matchers[1];
59+
60+
return {
61+
text: moduleName,
62+
range: Range.create(start, end),
63+
contentRange: Range.create(moduleNameStart, moduleNameEnd),
64+
};
65+
}
66+
67+
/**
68+
* Get Deno compile hint from a source file
69+
* @param ts
70+
*/
71+
export function getDenoCompileHint(
72+
sourceFile: ts.SourceFile,
73+
pos = 0,
74+
): compileHint[] {
75+
const denoTypesComments: compileHint[] = [];
76+
77+
const comments = ts.getLeadingCommentRanges(sourceFile.getFullText(), pos) ||
78+
[];
79+
80+
for (const comment of comments) {
81+
if (comment.hasTrailingNewLine) {
82+
const text = sourceFile
83+
.getFullText()
84+
.substring(comment.pos, comment.end);
85+
const regexp = /@deno-types=['"]([^'"]+)['"]/;
86+
87+
const matchers = regexp.exec(text);
88+
89+
if (matchers) {
90+
const compileHint = parseCompileHint(sourceFile, comment);
91+
92+
/* istanbul ignore else */
93+
if (compileHint) {
94+
denoTypesComments.push(compileHint);
95+
}
96+
}
97+
}
98+
}
99+
100+
return denoTypesComments;
101+
}

src/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424

2525
import { universalModuleResolver } from "./module_resolver/universal_module_resolver";
2626
import { HashMeta } from "./module_resolver/hash_meta";
27+
import { getImportModules } from "./deno_modules";
2728
import { errorCodeToFixes } from "./codefix_provider";
2829
import "./code_fixes";
2930

@@ -153,6 +154,27 @@ module.exports = function init(
153154
config.importmap,
154155
);
155156

157+
const content = typescript.sys.readFile(containingFile, "utf8");
158+
159+
// handle @deno-types
160+
if (content && content.indexOf("// @deno-types=") >= 0) {
161+
const sourceFile = typescript.createSourceFile(
162+
containingFile,
163+
content,
164+
typescript.ScriptTarget.ESNext,
165+
true,
166+
);
167+
168+
const modules = getImportModules(sourceFile);
169+
170+
for (const m of modules) {
171+
if (m.hint) {
172+
const index = moduleNames.findIndex((v) => v === m.moduleName);
173+
moduleNames[index] = m.hint.text;
174+
}
175+
}
176+
}
177+
156178
// try resolve typeReferenceDirectives
157179
for (let moduleName of moduleNames) {
158180
const parsedModuleName = parseModuleName(

0 commit comments

Comments
 (0)