diff --git a/package-lock.json b/package-lock.json index 080cf8fb..d4e55888 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,15 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@typescript/vfs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.2.0.tgz", + "integrity": "sha512-3YhBC+iyngEHjEedSAWk9rbJHoBwa2cd4h/tzb2TXmZc2CUclTl3x5AQRKNoRqm7t+X9PGTc2q2/Dpray/O4mA==", + "dev": true, + "requires": { + "debug": "^4.1.1" + } + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -16,6 +25,15 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, "monaco-editor-core": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/monaco-editor-core/-/monaco-editor-core-0.20.0.tgz", @@ -45,6 +63,12 @@ } } }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "requirejs": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz", diff --git a/package.json b/package.json index 73e7dcfb..d2eb1993 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "url": "https://github.com/Microsoft/monaco-typescript/issues" }, "devDependencies": { + "@typescript/vfs": "^1.2.0", "monaco-editor-core": "^0.20.0", "monaco-languages": "^1.10.0", "monaco-plugin-helpers": "^1.0.2", diff --git a/src/monaco.contribution.ts b/src/monaco.contribution.ts index c038d4b5..a26b7e2a 100644 --- a/src/monaco.contribution.ts +++ b/src/monaco.contribution.ts @@ -31,13 +31,15 @@ export class LanguageServiceDefaultsImpl implements monaco.languages.typescript. private _eagerModelSync: boolean; private _compilerOptions!: monaco.languages.typescript.CompilerOptions; private _diagnosticsOptions!: monaco.languages.typescript.DiagnosticsOptions; + private _workerOptions!: monaco.languages.typescript.WorkerOptions; private _onDidExtraLibsChangeTimeout: number; - constructor(compilerOptions: monaco.languages.typescript.CompilerOptions, diagnosticsOptions: monaco.languages.typescript.DiagnosticsOptions) { + constructor(compilerOptions: monaco.languages.typescript.CompilerOptions, diagnosticsOptions: monaco.languages.typescript.DiagnosticsOptions, workerOptions: monaco.languages.typescript.WorkerOptions) { this._extraLibs = Object.create(null); this._eagerModelSync = false; this.setCompilerOptions(compilerOptions); this.setDiagnosticsOptions(diagnosticsOptions); + this.setWorkerOptions(workerOptions) this._onDidExtraLibsChangeTimeout = -1; } @@ -49,6 +51,10 @@ export class LanguageServiceDefaultsImpl implements monaco.languages.typescript. return this._onDidExtraLibsChange.event; } + get workerOptions(): monaco.languages.typescript.WorkerOptions { + return this._workerOptions + } + getExtraLibs(): IExtraLibs { return this._extraLibs; } @@ -142,6 +148,11 @@ export class LanguageServiceDefaultsImpl implements monaco.languages.typescript. this._onDidChange.fire(undefined); } + setWorkerOptions(options: monaco.languages.typescript.WorkerOptions): void { + this._workerOptions = options || Object.create(null); + this._onDidChange.fire(undefined); + } + setMaximumWorkerIdleTime(value: number): void { } @@ -202,11 +213,13 @@ enum ModuleResolutionKind { const typescriptDefaults = new LanguageServiceDefaultsImpl( { allowNonTsExtensions: true, target: ScriptTarget.Latest }, - { noSemanticValidation: false, noSyntaxValidation: false }); + { noSemanticValidation: false, noSyntaxValidation: false }, + {}); const javascriptDefaults = new LanguageServiceDefaultsImpl( { allowNonTsExtensions: true, allowJs: true, target: ScriptTarget.Latest }, - { noSemanticValidation: true, noSyntaxValidation: false }); + { noSemanticValidation: true, noSyntaxValidation: false }, + {}); function getTypeScriptWorker(): Promise<(...uris: monaco.Uri[]) => Promise> { return getMode().then(mode => mode.getTypeScriptWorker()); diff --git a/src/monaco.d.ts b/src/monaco.d.ts index 1e99b41b..d63c60d6 100644 --- a/src/monaco.d.ts +++ b/src/monaco.d.ts @@ -137,6 +137,11 @@ declare module monaco.languages.typescript { diagnosticCodesToIgnore?: number[]; } + export interface WorkerOptions { + /** A full HTTP path to a JavaScript file which adds a function `customTSWorkerFactory` to the self inside a web-worker */ + customWorkerPath?: string; + } + interface IExtraLib { content: string; version: number; diff --git a/src/tsWorker.ts b/src/tsWorker.ts index 3758d46b..c785d75d 100644 --- a/src/tsWorker.ts +++ b/src/tsWorker.ts @@ -10,6 +10,7 @@ import { IExtraLibs } from './monaco.contribution'; import IWorkerContext = monaco.worker.IWorkerContext; + export class TypeScriptWorker implements ts.LanguageServiceHost, monaco.languages.typescript.TypeScriptWorker { // --- model sync ----------------------- @@ -253,8 +254,33 @@ export class TypeScriptWorker implements ts.LanguageServiceHost, monaco.language export interface ICreateData { compilerOptions: ts.CompilerOptions; extraLibs: IExtraLibs; + customWorkerPath?: string +} + +/** The shape of the factory */ +export interface CustomTSWebWorkerFactory { + (TSWorkerClass: typeof TypeScriptWorker, ts: typeof import("typescript"), libs: Record): typeof TypeScriptWorker } export function create(ctx: IWorkerContext, createData: ICreateData): TypeScriptWorker { - return new TypeScriptWorker(ctx, createData); + let TSWorkerClass = TypeScriptWorker + if (createData.customWorkerPath) { + // @ts-ignore - This is available in a webworker + if (typeof importScripts === "undefined") { + console.warn("Monaco is not using webworkers for background tasks, and that is needed to support the customWorkerPath flag") + } else { + // @ts-ignore - This is available in a webworker + importScripts(createData.customWorkerPath) + + // @ts-ignore - This should come from the above eval + const workerFactoryFunc: CustomTSWebWorkerFactory | undefined = self.customTSWorkerFactory + if (!workerFactoryFunc) { + throw new Error(`The script at ${createData.customWorkerPath} does not add customTSWorkerFactory to self`) + } + + TSWorkerClass = workerFactoryFunc(TypeScriptWorker, ts, libFileMap) + } + } + + return new TSWorkerClass(ctx, createData); } diff --git a/src/workerManager.ts b/src/workerManager.ts index ff0b560e..fe6e2455 100644 --- a/src/workerManager.ts +++ b/src/workerManager.ts @@ -72,7 +72,8 @@ export class WorkerManager { // passed in to the create() method createData: { compilerOptions: this._defaults.getCompilerOptions(), - extraLibs: this._defaults.getExtraLibs() + extraLibs: this._defaults.getExtraLibs(), + customWorkerPath: this._defaults.workerOptions.customWorkerPath } }); diff --git a/test/custom-worker.html b/test/custom-worker.html new file mode 100644 index 00000000..74bfdbb1 --- /dev/null +++ b/test/custom-worker.html @@ -0,0 +1,222 @@ + + + + + + + + + + + +

Monaco Editor TypeScript test page

+ +
+

Custom webworker

+ + + + + + + + + + + diff --git a/test/custom-worker.js b/test/custom-worker.js new file mode 100644 index 00000000..3eeacd1d --- /dev/null +++ b/test/custom-worker.js @@ -0,0 +1,60 @@ +// This example uses @typescript/vfs to create a virtual TS program +// which can do work on a bg thread. + +// This version of the vfs edits the global scope (in the case of a webworker, this is 'self') +importScripts("https://unpkg.com/@typescript/vfs@1.3.0/dist/vfs.globals.js") + +/** @type { import("@typescript/vfs") } */ +const tsvfs = globalThis.tsvfs + +/** @type {import("../src/tsWorker").CustomTSWebWorkerFactory }*/ +const worker = (TypeScriptWorker, ts, libFileMap) => { + return class MonacoTSWorker extends TypeScriptWorker { + + // Adds a custom function to the webworker + async getDTSEmitForFile(fileName) { + const result = await this.getEmitOutput(fileName) + const firstDTS = result.outputFiles.find(o => o.name.endsWith(".d.ts")) + return (firstDTS && firstDTS.text) || "" + } + + async printAST(fileName) { + console.log("Creating virtual TS project") + const compilerOptions = this.getCompilationSettings() + const fsMap = new Map() + for (const key of Object.keys(libFileMap)) { + fsMap.set(key, "/" + libFileMap[key]) + } + + const thisCode = await this.getScriptText(fileName) + fsMap.set("index.ts", thisCode) + + console.log("Starting up TS program") + const system = tsvfs.createSystem(fsMap) + const host = tsvfs.createVirtualCompilerHost(system, compilerOptions, ts) + + const program = ts.createProgram({ + rootNames: [...fsMap.keys()], + options: compilerOptions, + host: host.compilerHost, + }) + + // Now I can look at the AST for the .ts file too + const mainSrcFile = program.getSourceFile("index.ts") + let miniAST = "SourceFile" + + const recurse = (parent, depth) => { + if (depth > 5) return + ts.forEachChild(parent, node => { + const spaces = " ".repeat(depth + 1) + miniAST += `\n${spaces}${ts.SyntaxKind[node.kind]}` + recurse(node, depth + 1) + }) + } + recurse(mainSrcFile, 0) + return miniAST + } + } +} + +self.customTSWorkerFactory = worker