mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-23 01:58:53 +01:00
For #269213 This adds a new eslint rule for `as any` and `<any>({... })`. We'd like to remove almost all of these, however right now the first goal is to prevent them in new code. That's why with this first PR I simply add `eslint-disable` comments for all breaks Trying to get this change in soon after branching off for release to hopefully minimize disruption during debt week work
746 lines
22 KiB
TypeScript
746 lines
22 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import fs from 'fs';
|
|
import type * as ts from 'typescript';
|
|
import path from 'path';
|
|
import fancyLog from 'fancy-log';
|
|
import ansiColors from 'ansi-colors';
|
|
|
|
const dtsv = '3';
|
|
|
|
const tsfmt = require('../../tsfmt.json');
|
|
|
|
const SRC = path.join(__dirname, '../../src');
|
|
export const RECIPE_PATH = path.join(__dirname, '../monaco/monaco.d.ts.recipe');
|
|
const DECLARATION_PATH = path.join(__dirname, '../../src/vs/monaco.d.ts');
|
|
|
|
function logErr(message: any, ...rest: any[]): void {
|
|
fancyLog(ansiColors.yellow(`[monaco.d.ts]`), message, ...rest);
|
|
}
|
|
|
|
type SourceFileGetter = (moduleId: string) => ts.SourceFile | null;
|
|
|
|
type TSTopLevelDeclaration = ts.InterfaceDeclaration | ts.EnumDeclaration | ts.ClassDeclaration | ts.TypeAliasDeclaration | ts.FunctionDeclaration | ts.ModuleDeclaration;
|
|
type TSTopLevelDeclare = TSTopLevelDeclaration | ts.VariableStatement;
|
|
|
|
function isDeclaration(ts: typeof import('typescript'), a: TSTopLevelDeclare): a is TSTopLevelDeclaration {
|
|
return (
|
|
a.kind === ts.SyntaxKind.InterfaceDeclaration
|
|
|| a.kind === ts.SyntaxKind.EnumDeclaration
|
|
|| a.kind === ts.SyntaxKind.ClassDeclaration
|
|
|| a.kind === ts.SyntaxKind.TypeAliasDeclaration
|
|
|| a.kind === ts.SyntaxKind.FunctionDeclaration
|
|
|| a.kind === ts.SyntaxKind.ModuleDeclaration
|
|
);
|
|
}
|
|
|
|
function visitTopLevelDeclarations(ts: typeof import('typescript'), sourceFile: ts.SourceFile, visitor: (node: TSTopLevelDeclare) => boolean): void {
|
|
let stop = false;
|
|
|
|
const visit = (node: ts.Node): void => {
|
|
if (stop) {
|
|
return;
|
|
}
|
|
|
|
switch (node.kind) {
|
|
case ts.SyntaxKind.InterfaceDeclaration:
|
|
case ts.SyntaxKind.EnumDeclaration:
|
|
case ts.SyntaxKind.ClassDeclaration:
|
|
case ts.SyntaxKind.VariableStatement:
|
|
case ts.SyntaxKind.TypeAliasDeclaration:
|
|
case ts.SyntaxKind.FunctionDeclaration:
|
|
case ts.SyntaxKind.ModuleDeclaration:
|
|
stop = visitor(<TSTopLevelDeclare>node);
|
|
}
|
|
|
|
if (stop) {
|
|
return;
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
|
|
visit(sourceFile);
|
|
}
|
|
|
|
|
|
function getAllTopLevelDeclarations(ts: typeof import('typescript'), sourceFile: ts.SourceFile): TSTopLevelDeclare[] {
|
|
const all: TSTopLevelDeclare[] = [];
|
|
visitTopLevelDeclarations(ts, sourceFile, (node) => {
|
|
if (node.kind === ts.SyntaxKind.InterfaceDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ModuleDeclaration) {
|
|
const interfaceDeclaration = <ts.InterfaceDeclaration>node;
|
|
const triviaStart = interfaceDeclaration.pos;
|
|
const triviaEnd = interfaceDeclaration.name.pos;
|
|
const triviaText = getNodeText(sourceFile, { pos: triviaStart, end: triviaEnd });
|
|
|
|
if (triviaText.indexOf('@internal') === -1) {
|
|
all.push(node);
|
|
}
|
|
} else {
|
|
const nodeText = getNodeText(sourceFile, node);
|
|
if (nodeText.indexOf('@internal') === -1) {
|
|
all.push(node);
|
|
}
|
|
}
|
|
return false /*continue*/;
|
|
});
|
|
return all;
|
|
}
|
|
|
|
|
|
function getTopLevelDeclaration(ts: typeof import('typescript'), sourceFile: ts.SourceFile, typeName: string): TSTopLevelDeclare | null {
|
|
let result: TSTopLevelDeclare | null = null;
|
|
visitTopLevelDeclarations(ts, sourceFile, (node) => {
|
|
if (isDeclaration(ts, node) && node.name) {
|
|
if (node.name.text === typeName) {
|
|
result = node;
|
|
return true /*stop*/;
|
|
}
|
|
return false /*continue*/;
|
|
}
|
|
// node is ts.VariableStatement
|
|
if (getNodeText(sourceFile, node).indexOf(typeName) >= 0) {
|
|
result = node;
|
|
return true /*stop*/;
|
|
}
|
|
return false /*continue*/;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
|
|
function getNodeText(sourceFile: ts.SourceFile, node: { pos: number; end: number }): string {
|
|
return sourceFile.getFullText().substring(node.pos, node.end);
|
|
}
|
|
|
|
function hasModifier(modifiers: readonly ts.ModifierLike[] | undefined, kind: ts.SyntaxKind): boolean {
|
|
if (modifiers) {
|
|
for (let i = 0; i < modifiers.length; i++) {
|
|
const mod = modifiers[i];
|
|
if (mod.kind === kind) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isStatic(ts: typeof import('typescript'), member: ts.ClassElement | ts.TypeElement): boolean {
|
|
if (ts.canHaveModifiers(member)) {
|
|
return hasModifier(ts.getModifiers(member), ts.SyntaxKind.StaticKeyword);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isDefaultExport(ts: typeof import('typescript'), declaration: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean {
|
|
return (
|
|
hasModifier(declaration.modifiers, ts.SyntaxKind.DefaultKeyword)
|
|
&& hasModifier(declaration.modifiers, ts.SyntaxKind.ExportKeyword)
|
|
);
|
|
}
|
|
|
|
function getMassagedTopLevelDeclarationText(ts: typeof import('typescript'), sourceFile: ts.SourceFile, declaration: TSTopLevelDeclare, importName: string, usage: string[], enums: IEnumEntry[]): string {
|
|
let result = getNodeText(sourceFile, declaration);
|
|
if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration || declaration.kind === ts.SyntaxKind.ClassDeclaration) {
|
|
const interfaceDeclaration = <ts.InterfaceDeclaration | ts.ClassDeclaration>declaration;
|
|
|
|
const staticTypeName = (
|
|
isDefaultExport(ts, interfaceDeclaration)
|
|
? `${importName}.default`
|
|
: `${importName}.${declaration.name!.text}`
|
|
);
|
|
|
|
let instanceTypeName = staticTypeName;
|
|
const typeParametersCnt = (interfaceDeclaration.typeParameters ? interfaceDeclaration.typeParameters.length : 0);
|
|
if (typeParametersCnt > 0) {
|
|
const arr: string[] = [];
|
|
for (let i = 0; i < typeParametersCnt; i++) {
|
|
arr.push('any');
|
|
}
|
|
instanceTypeName = `${instanceTypeName}<${arr.join(',')}>`;
|
|
}
|
|
|
|
const members: ts.NodeArray<ts.ClassElement | ts.TypeElement> = interfaceDeclaration.members;
|
|
members.forEach((member) => {
|
|
try {
|
|
const memberText = getNodeText(sourceFile, member);
|
|
if (memberText.indexOf('@internal') >= 0 || memberText.indexOf('private') >= 0) {
|
|
result = result.replace(memberText, '');
|
|
} else {
|
|
const memberName = (<ts.Identifier | ts.StringLiteral>member.name).text;
|
|
const memberAccess = (memberName.indexOf('.') >= 0 ? `['${memberName}']` : `.${memberName}`);
|
|
if (isStatic(ts, member)) {
|
|
usage.push(`a = ${staticTypeName}${memberAccess};`);
|
|
} else {
|
|
usage.push(`a = (<${instanceTypeName}>b)${memberAccess};`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// life..
|
|
}
|
|
});
|
|
}
|
|
result = result.replace(/export default /g, 'export ');
|
|
result = result.replace(/export declare /g, 'export ');
|
|
result = result.replace(/declare /g, '');
|
|
const lines = result.split(/\r\n|\r|\n/);
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (/\s*\*/.test(lines[i])) {
|
|
// very likely a comment
|
|
continue;
|
|
}
|
|
lines[i] = lines[i].replace(/"/g, '\'');
|
|
}
|
|
result = lines.join('\n');
|
|
|
|
if (declaration.kind === ts.SyntaxKind.EnumDeclaration) {
|
|
result = result.replace(/const enum/, 'enum');
|
|
enums.push({
|
|
enumName: declaration.name.getText(sourceFile),
|
|
text: result
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function format(ts: typeof import('typescript'), text: string, endl: string): string {
|
|
const REALLY_FORMAT = false;
|
|
|
|
text = preformat(text, endl);
|
|
if (!REALLY_FORMAT) {
|
|
return text;
|
|
}
|
|
|
|
// Parse the source text
|
|
const sourceFile = ts.createSourceFile('file.ts', text, ts.ScriptTarget.Latest, /*setParentPointers*/ true);
|
|
|
|
// Get the formatting edits on the input sources
|
|
// eslint-disable-next-line local/code-no-any-casts
|
|
const edits = (<any>ts).formatting.formatDocument(sourceFile, getRuleProvider(tsfmt), tsfmt);
|
|
|
|
// Apply the edits on the input code
|
|
return applyEdits(text, edits);
|
|
|
|
function countParensCurly(text: string): number {
|
|
let cnt = 0;
|
|
for (let i = 0; i < text.length; i++) {
|
|
if (text.charAt(i) === '(' || text.charAt(i) === '{') {
|
|
cnt++;
|
|
}
|
|
if (text.charAt(i) === ')' || text.charAt(i) === '}') {
|
|
cnt--;
|
|
}
|
|
}
|
|
return cnt;
|
|
}
|
|
|
|
function repeatStr(s: string, cnt: number): string {
|
|
let r = '';
|
|
for (let i = 0; i < cnt; i++) {
|
|
r += s;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
function preformat(text: string, endl: string): string {
|
|
const lines = text.split(endl);
|
|
let inComment = false;
|
|
let inCommentDeltaIndent = 0;
|
|
let indent = 0;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
let line = lines[i].replace(/\s$/, '');
|
|
let repeat = false;
|
|
let lineIndent = 0;
|
|
do {
|
|
repeat = false;
|
|
if (line.substring(0, 4) === ' ') {
|
|
line = line.substring(4);
|
|
lineIndent++;
|
|
repeat = true;
|
|
}
|
|
if (line.charAt(0) === '\t') {
|
|
line = line.substring(1);
|
|
lineIndent++;
|
|
repeat = true;
|
|
}
|
|
} while (repeat);
|
|
|
|
if (line.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
if (inComment) {
|
|
if (/\*\//.test(line)) {
|
|
inComment = false;
|
|
}
|
|
lines[i] = repeatStr('\t', lineIndent + inCommentDeltaIndent) + line;
|
|
continue;
|
|
}
|
|
|
|
if (/\/\*/.test(line)) {
|
|
inComment = true;
|
|
inCommentDeltaIndent = indent - lineIndent;
|
|
lines[i] = repeatStr('\t', indent) + line;
|
|
continue;
|
|
}
|
|
|
|
const cnt = countParensCurly(line);
|
|
let shouldUnindentAfter = false;
|
|
let shouldUnindentBefore = false;
|
|
if (cnt < 0) {
|
|
if (/[({]/.test(line)) {
|
|
shouldUnindentAfter = true;
|
|
} else {
|
|
shouldUnindentBefore = true;
|
|
}
|
|
} else if (cnt === 0) {
|
|
shouldUnindentBefore = /^\}/.test(line);
|
|
}
|
|
let shouldIndentAfter = false;
|
|
if (cnt > 0) {
|
|
shouldIndentAfter = true;
|
|
} else if (cnt === 0) {
|
|
shouldIndentAfter = /{$/.test(line);
|
|
}
|
|
|
|
if (shouldUnindentBefore) {
|
|
indent--;
|
|
}
|
|
|
|
lines[i] = repeatStr('\t', indent) + line;
|
|
|
|
if (shouldUnindentAfter) {
|
|
indent--;
|
|
}
|
|
if (shouldIndentAfter) {
|
|
indent++;
|
|
}
|
|
}
|
|
return lines.join(endl);
|
|
}
|
|
|
|
function getRuleProvider(options: ts.FormatCodeSettings) {
|
|
// Share this between multiple formatters using the same options.
|
|
// This represents the bulk of the space the formatter uses.
|
|
// eslint-disable-next-line local/code-no-any-casts
|
|
return (ts as any).formatting.getFormatContext(options);
|
|
}
|
|
|
|
function applyEdits(text: string, edits: ts.TextChange[]): string {
|
|
// Apply edits in reverse on the existing text
|
|
let result = text;
|
|
for (let i = edits.length - 1; i >= 0; i--) {
|
|
const change = edits[i];
|
|
const head = result.slice(0, change.span.start);
|
|
const tail = result.slice(change.span.start + change.span.length);
|
|
result = head + change.newText + tail;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
function createReplacerFromDirectives(directives: [RegExp, string][]): (str: string) => string {
|
|
return (str: string) => {
|
|
for (let i = 0; i < directives.length; i++) {
|
|
str = str.replace(directives[i][0], directives[i][1]);
|
|
}
|
|
return str;
|
|
};
|
|
}
|
|
|
|
function createReplacer(data: string): (str: string) => string {
|
|
data = data || '';
|
|
const rawDirectives = data.split(';');
|
|
const directives: [RegExp, string][] = [];
|
|
rawDirectives.forEach((rawDirective) => {
|
|
if (rawDirective.length === 0) {
|
|
return;
|
|
}
|
|
const pieces = rawDirective.split('=>');
|
|
let findStr = pieces[0];
|
|
const replaceStr = pieces[1];
|
|
|
|
findStr = findStr.replace(/[\-\\\{\}\*\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&');
|
|
findStr = '\\b' + findStr + '\\b';
|
|
directives.push([new RegExp(findStr, 'g'), replaceStr]);
|
|
});
|
|
|
|
return createReplacerFromDirectives(directives);
|
|
}
|
|
|
|
interface ITempResult {
|
|
result: string;
|
|
usageContent: string;
|
|
enums: string;
|
|
}
|
|
|
|
interface IEnumEntry {
|
|
enumName: string;
|
|
text: string;
|
|
}
|
|
|
|
function generateDeclarationFile(ts: typeof import('typescript'), recipe: string, sourceFileGetter: SourceFileGetter): ITempResult | null {
|
|
const endl = /\r\n/.test(recipe) ? '\r\n' : '\n';
|
|
|
|
const lines = recipe.split(endl);
|
|
const result: string[] = [];
|
|
|
|
let usageCounter = 0;
|
|
const usageImports: string[] = [];
|
|
const usage: string[] = [];
|
|
|
|
let failed = false;
|
|
|
|
usage.push(`var a: any;`);
|
|
usage.push(`var b: any;`);
|
|
|
|
const generateUsageImport = (moduleId: string) => {
|
|
const importName = 'm' + (++usageCounter);
|
|
usageImports.push(`import * as ${importName} from './${moduleId.replace(/\.d\.ts$/, '')}';`);
|
|
return importName;
|
|
};
|
|
|
|
const enums: IEnumEntry[] = [];
|
|
let version: string | null = null;
|
|
|
|
lines.forEach(line => {
|
|
|
|
if (failed) {
|
|
return;
|
|
}
|
|
|
|
const m0 = line.match(/^\/\/dtsv=(\d+)$/);
|
|
if (m0) {
|
|
version = m0[1];
|
|
}
|
|
|
|
const m1 = line.match(/^\s*#include\(([^;)]*)(;[^)]*)?\)\:(.*)$/);
|
|
if (m1) {
|
|
const moduleId = m1[1];
|
|
const sourceFile = sourceFileGetter(moduleId);
|
|
if (!sourceFile) {
|
|
logErr(`While handling ${line}`);
|
|
logErr(`Cannot find ${moduleId}`);
|
|
failed = true;
|
|
return;
|
|
}
|
|
|
|
const importName = generateUsageImport(moduleId);
|
|
|
|
const replacer = createReplacer(m1[2]);
|
|
|
|
const typeNames = m1[3].split(/,/);
|
|
typeNames.forEach((typeName) => {
|
|
typeName = typeName.trim();
|
|
if (typeName.length === 0) {
|
|
return;
|
|
}
|
|
const declaration = getTopLevelDeclaration(ts, sourceFile, typeName);
|
|
if (!declaration) {
|
|
logErr(`While handling ${line}`);
|
|
logErr(`Cannot find ${typeName}`);
|
|
failed = true;
|
|
return;
|
|
}
|
|
result.push(replacer(getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums)));
|
|
});
|
|
return;
|
|
}
|
|
|
|
const m2 = line.match(/^\s*#includeAll\(([^;)]*)(;[^)]*)?\)\:(.*)$/);
|
|
if (m2) {
|
|
const moduleId = m2[1];
|
|
const sourceFile = sourceFileGetter(moduleId);
|
|
if (!sourceFile) {
|
|
logErr(`While handling ${line}`);
|
|
logErr(`Cannot find ${moduleId}`);
|
|
failed = true;
|
|
return;
|
|
}
|
|
|
|
const importName = generateUsageImport(moduleId);
|
|
|
|
const replacer = createReplacer(m2[2]);
|
|
|
|
const typeNames = m2[3].split(/,/);
|
|
const typesToExcludeMap: { [typeName: string]: boolean } = {};
|
|
const typesToExcludeArr: string[] = [];
|
|
typeNames.forEach((typeName) => {
|
|
typeName = typeName.trim();
|
|
if (typeName.length === 0) {
|
|
return;
|
|
}
|
|
typesToExcludeMap[typeName] = true;
|
|
typesToExcludeArr.push(typeName);
|
|
});
|
|
|
|
getAllTopLevelDeclarations(ts, sourceFile).forEach((declaration) => {
|
|
if (isDeclaration(ts, declaration) && declaration.name) {
|
|
if (typesToExcludeMap[declaration.name.text]) {
|
|
return;
|
|
}
|
|
} else {
|
|
// node is ts.VariableStatement
|
|
const nodeText = getNodeText(sourceFile, declaration);
|
|
for (let i = 0; i < typesToExcludeArr.length; i++) {
|
|
if (nodeText.indexOf(typesToExcludeArr[i]) >= 0) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
result.push(replacer(getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums)));
|
|
});
|
|
return;
|
|
}
|
|
|
|
result.push(line);
|
|
});
|
|
|
|
if (failed) {
|
|
return null;
|
|
}
|
|
|
|
if (version !== dtsv) {
|
|
if (!version) {
|
|
logErr(`gulp watch restart required. 'monaco.d.ts.recipe' is written before versioning was introduced.`);
|
|
} else {
|
|
logErr(`gulp watch restart required. 'monaco.d.ts.recipe' v${version} does not match runtime v${dtsv}.`);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
let resultTxt = result.join(endl);
|
|
resultTxt = resultTxt.replace(/\bURI\b/g, 'Uri');
|
|
resultTxt = resultTxt.replace(/\bEvent</g, 'IEvent<');
|
|
resultTxt = resultTxt.split(/\r\n|\n|\r/).join(endl);
|
|
resultTxt = format(ts, resultTxt, endl);
|
|
resultTxt = resultTxt.split(/\r\n|\n|\r/).join(endl);
|
|
|
|
enums.sort((e1, e2) => {
|
|
if (e1.enumName < e2.enumName) {
|
|
return -1;
|
|
}
|
|
if (e1.enumName > e2.enumName) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
let resultEnums = [
|
|
'/*---------------------------------------------------------------------------------------------',
|
|
' * Copyright (c) Microsoft Corporation. All rights reserved.',
|
|
' * Licensed under the MIT License. See License.txt in the project root for license information.',
|
|
' *--------------------------------------------------------------------------------------------*/',
|
|
'',
|
|
'// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.',
|
|
''
|
|
].concat(enums.map(e => e.text)).join(endl);
|
|
resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl);
|
|
resultEnums = format(ts, resultEnums, endl);
|
|
resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl);
|
|
|
|
return {
|
|
result: resultTxt,
|
|
usageContent: `${usageImports.join('\n')}\n\n${usage.join('\n')}`,
|
|
enums: resultEnums
|
|
};
|
|
}
|
|
|
|
export interface IMonacoDeclarationResult {
|
|
content: string;
|
|
usageContent: string;
|
|
enums: string;
|
|
filePath: string;
|
|
isTheSame: boolean;
|
|
}
|
|
|
|
function _run(ts: typeof import('typescript'), sourceFileGetter: SourceFileGetter): IMonacoDeclarationResult | null {
|
|
const recipe = fs.readFileSync(RECIPE_PATH).toString();
|
|
const t = generateDeclarationFile(ts, recipe, sourceFileGetter);
|
|
if (!t) {
|
|
return null;
|
|
}
|
|
|
|
const result = t.result;
|
|
const usageContent = t.usageContent;
|
|
const enums = t.enums;
|
|
|
|
const currentContent = fs.readFileSync(DECLARATION_PATH).toString();
|
|
const one = currentContent.replace(/\r\n/gm, '\n');
|
|
const other = result.replace(/\r\n/gm, '\n');
|
|
const isTheSame = (one === other);
|
|
|
|
return {
|
|
content: result,
|
|
usageContent: usageContent,
|
|
enums: enums,
|
|
filePath: DECLARATION_PATH,
|
|
isTheSame
|
|
};
|
|
}
|
|
|
|
export class FSProvider {
|
|
public existsSync(filePath: string): boolean {
|
|
return fs.existsSync(filePath);
|
|
}
|
|
public statSync(filePath: string): fs.Stats {
|
|
return fs.statSync(filePath);
|
|
}
|
|
public readFileSync(_moduleId: string, filePath: string): Buffer {
|
|
return fs.readFileSync(filePath);
|
|
}
|
|
}
|
|
|
|
class CacheEntry {
|
|
constructor(
|
|
public readonly sourceFile: ts.SourceFile,
|
|
public readonly mtime: number
|
|
) { }
|
|
}
|
|
|
|
export class DeclarationResolver {
|
|
|
|
public readonly ts: typeof import('typescript');
|
|
private _sourceFileCache: { [moduleId: string]: CacheEntry | null };
|
|
|
|
constructor(private readonly _fsProvider: FSProvider) {
|
|
this.ts = require('typescript') as typeof import('typescript');
|
|
this._sourceFileCache = Object.create(null);
|
|
}
|
|
|
|
public invalidateCache(moduleId: string): void {
|
|
this._sourceFileCache[moduleId] = null;
|
|
}
|
|
|
|
public getDeclarationSourceFile(moduleId: string): ts.SourceFile | null {
|
|
if (this._sourceFileCache[moduleId]) {
|
|
// Since we cannot trust file watching to invalidate the cache, check also the mtime
|
|
const fileName = this._getFileName(moduleId);
|
|
const mtime = this._fsProvider.statSync(fileName).mtime.getTime();
|
|
if (this._sourceFileCache[moduleId]!.mtime !== mtime) {
|
|
this._sourceFileCache[moduleId] = null;
|
|
}
|
|
}
|
|
if (!this._sourceFileCache[moduleId]) {
|
|
this._sourceFileCache[moduleId] = this._getDeclarationSourceFile(moduleId);
|
|
}
|
|
return this._sourceFileCache[moduleId] ? this._sourceFileCache[moduleId]!.sourceFile : null;
|
|
}
|
|
|
|
private _getFileName(moduleId: string): string {
|
|
if (/\.d\.ts$/.test(moduleId)) {
|
|
return path.join(SRC, moduleId);
|
|
}
|
|
return path.join(SRC, `${moduleId}.ts`);
|
|
}
|
|
|
|
private _getDeclarationSourceFile(moduleId: string): CacheEntry | null {
|
|
const fileName = this._getFileName(moduleId);
|
|
if (!this._fsProvider.existsSync(fileName)) {
|
|
return null;
|
|
}
|
|
const mtime = this._fsProvider.statSync(fileName).mtime.getTime();
|
|
if (/\.d\.ts$/.test(moduleId)) {
|
|
// const mtime = this._fsProvider.statFileSync()
|
|
const fileContents = this._fsProvider.readFileSync(moduleId, fileName).toString();
|
|
return new CacheEntry(
|
|
this.ts.createSourceFile(fileName, fileContents, this.ts.ScriptTarget.ES5),
|
|
mtime
|
|
);
|
|
}
|
|
const fileContents = this._fsProvider.readFileSync(moduleId, fileName).toString();
|
|
const fileMap: IFileMap = {
|
|
'file.ts': fileContents
|
|
};
|
|
const service = this.ts.createLanguageService(new TypeScriptLanguageServiceHost(this.ts, {}, fileMap, {}));
|
|
const text = service.getEmitOutput('file.ts', true, true).outputFiles[0].text;
|
|
return new CacheEntry(
|
|
this.ts.createSourceFile(fileName, text, this.ts.ScriptTarget.ES5),
|
|
mtime
|
|
);
|
|
}
|
|
}
|
|
|
|
export function run3(resolver: DeclarationResolver): IMonacoDeclarationResult | null {
|
|
const sourceFileGetter = (moduleId: string) => resolver.getDeclarationSourceFile(moduleId);
|
|
return _run(resolver.ts, sourceFileGetter);
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ILibMap { [libName: string]: string }
|
|
interface IFileMap { [fileName: string]: string }
|
|
|
|
class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost {
|
|
|
|
private readonly _ts: typeof import('typescript');
|
|
private readonly _libs: ILibMap;
|
|
private readonly _files: IFileMap;
|
|
private readonly _compilerOptions: ts.CompilerOptions;
|
|
|
|
constructor(ts: typeof import('typescript'), libs: ILibMap, files: IFileMap, compilerOptions: ts.CompilerOptions) {
|
|
this._ts = ts;
|
|
this._libs = libs;
|
|
this._files = files;
|
|
this._compilerOptions = compilerOptions;
|
|
}
|
|
|
|
// --- language service host ---------------
|
|
|
|
getCompilationSettings(): ts.CompilerOptions {
|
|
return this._compilerOptions;
|
|
}
|
|
getScriptFileNames(): string[] {
|
|
return (
|
|
([] as string[])
|
|
.concat(Object.keys(this._libs))
|
|
.concat(Object.keys(this._files))
|
|
);
|
|
}
|
|
getScriptVersion(_fileName: string): string {
|
|
return '1';
|
|
}
|
|
getProjectVersion(): string {
|
|
return '1';
|
|
}
|
|
getScriptSnapshot(fileName: string): ts.IScriptSnapshot {
|
|
if (this._files.hasOwnProperty(fileName)) {
|
|
return this._ts.ScriptSnapshot.fromString(this._files[fileName]);
|
|
} else if (this._libs.hasOwnProperty(fileName)) {
|
|
return this._ts.ScriptSnapshot.fromString(this._libs[fileName]);
|
|
} else {
|
|
return this._ts.ScriptSnapshot.fromString('');
|
|
}
|
|
}
|
|
getScriptKind(_fileName: string): ts.ScriptKind {
|
|
return this._ts.ScriptKind.TS;
|
|
}
|
|
getCurrentDirectory(): string {
|
|
return '';
|
|
}
|
|
getDefaultLibFileName(_options: ts.CompilerOptions): string {
|
|
return 'defaultLib:es5';
|
|
}
|
|
isDefaultLibFileName(fileName: string): boolean {
|
|
return fileName === this.getDefaultLibFileName(this._compilerOptions);
|
|
}
|
|
readFile(path: string, _encoding?: string): string | undefined {
|
|
return this._files[path] || this._libs[path];
|
|
}
|
|
fileExists(path: string): boolean {
|
|
return path in this._files || path in this._libs;
|
|
}
|
|
}
|
|
|
|
export function execute(): IMonacoDeclarationResult {
|
|
const r = run3(new DeclarationResolver(new FSProvider()));
|
|
if (!r) {
|
|
throw new Error(`monaco.d.ts generation error - Cannot continue`);
|
|
}
|
|
return r;
|
|
}
|