Merge pull request #65 from microsoft/custom_worker

Add support for creating a custom webworker subclass
pull/2748/head
Alexandru Dima 5 years ago committed by GitHub
commit 251cec6aa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

24
package-lock.json generated

@ -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",

@ -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",

@ -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<monaco.languages.typescript.TypeScriptWorker>> {
return getMode().then(mode => mode.getTypeScriptWorker());

5
src/monaco.d.ts vendored

@ -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;

@ -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<string, string>): 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);
}

@ -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
}
});

@ -0,0 +1,222 @@
<!--
To test this file, you need to use a local server. The recommendation is that you run:
npx serve .
Then open http://localhost:5000/test/custom-worker
-->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<link rel="stylesheet" data-name="vs/editor/editor.main" href="../node_modules/monaco-editor-core/dev/vs/editor/editor.main.css">
</head>
<body>
<h2>Monaco Editor TypeScript test page</h2>
<button id="resetBtn">Reset Sample</button>
<div id="container" style="width:800px;height:600px;border:1px solid grey"></div>
<h3>Custom webworker</h3>
<button id="logDTS">Log DTS</button>
<button id="getAST">Print AST to console</button>
<script>
var paths = {
'vs/basic-languages': '../node_modules/monaco-languages/release/dev',
'vs/language/typescript': '../release/dev',
'vs': '../node_modules/monaco-editor-core/dev/vs'
};
if (document.location.protocol === 'http:') {
// Add support for running local http server
let testIndex = document.location.pathname.indexOf('/test/');
if (testIndex !== -1) {
let prefix = document.location.pathname.substr(0, testIndex);
paths['vs/language/typescript'] = prefix + '/release/dev';
}
}
var require = {
paths: paths
};
</script>
<script src="../node_modules/monaco-editor-core/dev/vs/loader.js"></script>
<script src="../node_modules/monaco-editor-core/dev/vs/editor/editor.main.nls.js"></script>
<script src="../node_modules/monaco-editor-core/dev/vs/editor/editor.main.js"></script>
<script>
function getDefaultCode() {
return [
'/* Game of Life',
' * Implemented in TypeScript',
' * To learn more about TypeScript, please visit http://www.typescriptlang.org/',
' */',
'',
'module Conway {',
'',
' export class Cell {',
' public row: number;',
' public col: number;',
' public live: boolean;',
'',
' constructor(row: number, col: number, live: boolean) {',
' this.row = row;',
' this.col = col;',
' this.live = live',
' }',
' }',
'',
' export class GameOfLife {',
' private gridSize: number;',
' private canvasSize: number;',
' private lineColor: string;',
' private liveColor: string;',
' private deadColor: string;',
' private initialLifeProbability: number;',
' private animationRate: number;',
' private cellSize: number;',
' private context: CanvasRenderingContext2D;',
' private world;',
'',
'',
' constructor() {',
' this.gridSize = 50;',
' this.canvasSize = 600;',
' this.lineColor = \'#cdcdcd\';',
' this.liveColor = \'#666\';',
' this.deadColor = \'#eee\';',
' this.initialLifeProbability = 0.5;',
' this.animationRate = 60;',
' this.cellSize = 0;',
' this.world = this.createWorld();',
' this.circleOfLife();',
' }',
'',
' public createWorld() {',
' return this.travelWorld( (cell : Cell) => {',
' cell.live = Math.random() < this.initialLifeProbability;',
' return cell;',
' });',
' }',
'',
' public circleOfLife() : void {',
' this.world = this.travelWorld( (cell: Cell) => {',
' cell = this.world[cell.row][cell.col];',
' this.draw(cell);',
' return this.resolveNextGeneration(cell);',
' });',
' setTimeout( () => {this.circleOfLife()}, this.animationRate);',
' }',
'',
' public resolveNextGeneration(cell : Cell) {',
' var count = this.countNeighbors(cell);',
' var newCell = new Cell(cell.row, cell.col, cell.live);',
' if(count < 2 || count > 3) newCell.live = false;',
' else if(count == 3) newCell.live = true;',
' return newCell;',
' }',
'',
' public countNeighbors(cell : Cell) {',
' var neighbors = 0;',
' for(var row = -1; row <=1; row++) {',
' for(var col = -1; col <= 1; col++) {',
' if(row == 0 && col == 0) continue;',
' if(this.isAlive(cell.row + row, cell.col + col)) {',
' neighbors++;',
' }',
' }',
' }',
' return neighbors;',
' }',
'',
' public isAlive(row : number, col : number) {',
' if(row < 0 || col < 0 || row >= this.gridSize || col >= this.gridSize) return false;',
' return this.world[row][col].live;',
' }',
'',
' public travelWorld(callback) {',
' var result = [];',
' for(var row = 0; row < this.gridSize; row++) {',
' var rowData = [];',
' for(var col = 0; col < this.gridSize; col++) {',
' rowData.push(callback(new Cell(row, col, false)));',
' }',
' result.push(rowData);',
' }',
' return result;',
' }',
'',
' public draw(cell : Cell) {',
' if(this.context == null) this.context = this.createDrawingContext();',
' if(this.cellSize == 0) this.cellSize = this.canvasSize/this.gridSize;',
'',
' this.context.strokeStyle = this.lineColor;',
' this.context.strokeRect(cell.row * this.cellSize, cell.col*this.cellSize, this.cellSize, this.cellSize);',
' this.context.fillStyle = cell.live ? this.liveColor : this.deadColor;',
' this.context.fillRect(cell.row * this.cellSize, cell.col*this.cellSize, this.cellSize, this.cellSize);',
' }',
'',
' public createDrawingContext() {',
' var canvas = <HTMLCanvasElement> document.getElementById(\'conway-canvas\');',
' if(canvas == null) {',
' canvas = document.createElement(\'canvas\');',
' canvas.id = \'conway-canvas\';',
' canvas.width = this.canvasSize;',
' canvas.height = this.canvasSize;',
' document.body.appendChild(canvas);',
' }',
' return canvas.getContext(\'2d\');',
' }',
' }',
'}',
'',
'var game = new Conway.GameOfLife();',
].join('\n');
}
function getDefaultComplierOpts() {
return { target: 99, jsx: 1, allowNonTsExtensions: true }
}
require([
'vs/basic-languages/monaco.contribution',
'vs/language/typescript/monaco.contribution'
], () => {
monaco.languages.typescript.typescriptDefaults.setWorkerOptions({ customWorkerPath: "http://localhost:5000/test/custom-worker.js" })
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ target: 99, jsx: 1, allowNonTsExtensions: true, declaration: true, noLibCheck: true })
var editor = monaco.editor.create(document.getElementById('container'), {
value: localStorage.getItem("code") || getDefaultCode(),
language: 'typescript',
lightbulb: { enabled: true }
});
editor.onDidChangeModelContent(() => {
const code = editor.getModel().getValue()
localStorage.setItem("code", code)
});
document.getElementById('resetBtn').onclick = () => {
editor.setValue(getDefaultCode());
};
document.getElementById('logDTS').onclick = async () => {
const model = editor.getModel()
const worker = await monaco.languages.typescript.getTypeScriptWorker()
const thisWorker = await worker(model.uri)
const dts = await thisWorker.getDTSEmitForFile(model.uri.toString())
console.log(dts)
};
document.getElementById('getAST').onclick = async () => {
const model = editor.getModel()
const worker = await monaco.languages.typescript.getTypeScriptWorker()
const thisWorker = await worker(model.uri)
const ast = await thisWorker.printAST(model.uri.toString())
console.log(ast)
};
});
</script>
</body>
</html>

@ -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
Loading…
Cancel
Save