diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index c0b79bc0f9d..3c5f90f16d6 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -64,6 +64,7 @@ const compilations = [ 'references-view/tsconfig.json', 'simple-browser/tsconfig.json', 'typescript-language-features/test-workspace/tsconfig.json', + 'typescript-language-features/web/tsconfig.json', 'typescript-language-features/tsconfig.json', 'vscode-api-tests/tsconfig.json', 'vscode-colorize-tests/tsconfig.json', diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 8e7f238bb3c..1f763386e2e 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -104,7 +104,7 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName) { // check for a webpack configuration files, then invoke webpack // and merge its output with the files stream. const webpackConfigLocations = glob.sync(path.join(extensionPath, '**', webpackConfigFileName), { ignore: ['**/node_modules'] }); - const webpackStreams = webpackConfigLocations.map(webpackConfigPath => { + const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { const webpackDone = (err, stats) => { fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); if (err) { @@ -118,27 +118,30 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName) { result.emit('error', compilation.warnings.join('\n')); } }; - const webpackConfig = { - ...require(webpackConfigPath), - ...{ mode: 'production' } - }; - const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); - return webpackGulp(webpackConfig, webpack, webpackDone) - .pipe(es.through(function (data) { - data.stat = data.stat || {}; - data.base = extensionPath; - this.emit('data', data); - })) - .pipe(es.through(function (data) { - // source map handling: - // * rewrite sourceMappingURL - // * save to disk so that upload-task picks this up - const contents = data.contents.toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { - return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; - }), 'utf8'); - this.emit('data', data); - })); + const exportedConfig = require(webpackConfigPath); + return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => { + const webpackConfig = { + ...config, + ...{ mode: 'production' } + }; + const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); + return webpackGulp(webpackConfig, webpack, webpackDone) + .pipe(es.through(function (data) { + data.stat = data.stat || {}; + data.base = extensionPath; + this.emit('data', data); + })) + .pipe(es.through(function (data) { + // source map handling: + // * rewrite sourceMappingURL + // * save to disk so that upload-task picks this up + const contents = data.contents.toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { + return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; + }), 'utf8'); + this.emit('data', data); + })); + }); }); es.merge(...webpackStreams, es.readArray(files)) // .pipe(es.through(function (data) { @@ -414,19 +417,14 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { const webpackConfigs = []; for (const { configPath, outputRoot } of webpackConfigLocations) { const configOrFnOrArray = require(configPath); - function addConfig(configOrFn) { - let config; - if (typeof configOrFn === 'function') { - config = configOrFn({}, {}); + function addConfig(configOrFnOrArray) { + for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) { + const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn; + if (outputRoot) { + config.output.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output.path)); + } webpackConfigs.push(config); } - else { - config = configOrFn; - } - if (outputRoot) { - config.output.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output.path)); - } - webpackConfigs.push(configOrFn); } addConfig(configOrFnOrArray); } diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 6cad21018d0..56a47071b5a 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -121,7 +121,7 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string): { ignore: ['**/node_modules'] } )); - const webpackStreams = webpackConfigLocations.map(webpackConfigPath => { + const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { const webpackDone = (err: any, stats: any) => { fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); @@ -137,29 +137,32 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string): } }; - const webpackConfig = { - ...require(webpackConfigPath), - ...{ mode: 'production' } - }; - const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); + const exportedConfig = require(webpackConfigPath); + return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => { + const webpackConfig = { + ...config, + ...{ mode: 'production' } + }; + const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); - return webpackGulp(webpackConfig, webpack, webpackDone) - .pipe(es.through(function (data) { - data.stat = data.stat || {}; - data.base = extensionPath; - this.emit('data', data); - })) - .pipe(es.through(function (data: File) { - // source map handling: - // * rewrite sourceMappingURL - // * save to disk so that upload-task picks this up - const contents = (data.contents).toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { - return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; - }), 'utf8'); + return webpackGulp(webpackConfig, webpack, webpackDone) + .pipe(es.through(function (data) { + data.stat = data.stat || {}; + data.base = extensionPath; + this.emit('data', data); + })) + .pipe(es.through(function (data: File) { + // source map handling: + // * rewrite sourceMappingURL + // * save to disk so that upload-task picks this up + const contents = (data.contents).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { + return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; + }), 'utf8'); - this.emit('data', data); - })); + this.emit('data', data); + })); + }); }); es.merge(...webpackStreams, es.readArray(files)) @@ -506,20 +509,14 @@ export async function webpackExtensions(taskName: string, isWatch: boolean, webp for (const { configPath, outputRoot } of webpackConfigLocations) { const configOrFnOrArray = require(configPath); - function addConfig(configOrFn: webpack.Configuration | Function) { - let config; - if (typeof configOrFn === 'function') { - config = (configOrFn as Function)({}, {}); + function addConfig(configOrFnOrArray: webpack.Configuration | ((env: unknown, args: unknown) => webpack.Configuration) | webpack.Configuration[]) { + for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) { + const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn; + if (outputRoot) { + config.output!.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output!.path!)); + } webpackConfigs.push(config); - } else { - config = configOrFn; } - - if (outputRoot) { - config.output.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output.path)); - } - - webpackConfigs.push(configOrFn); } addConfig(configOrFnOrArray); } diff --git a/extensions/.vscodeignore b/extensions/.vscodeignore new file mode 100644 index 00000000000..6e4d8fc1118 --- /dev/null +++ b/extensions/.vscodeignore @@ -0,0 +1,3 @@ +node_modules/typescript/lib/tsc.js +node_modules/typescript/lib/typescriptServices.js +node_modules/typescript/lib/tsserverlibrary.js diff --git a/extensions/postinstall.mjs b/extensions/postinstall.mjs index 110b9b3b476..04e54dd6e2c 100644 --- a/extensions/postinstall.mjs +++ b/extensions/postinstall.mjs @@ -26,7 +26,6 @@ function processRoot() { function processLib() { const toDelete = new Set([ 'tsc.js', - 'tsserverlibrary.js', 'typescriptServices.js', ]); diff --git a/extensions/typescript-language-features/.vscodeignore b/extensions/typescript-language-features/.vscodeignore index 079f06f08d9..76d64ae8237 100644 --- a/extensions/typescript-language-features/.vscodeignore +++ b/extensions/typescript-language-features/.vscodeignore @@ -1,5 +1,6 @@ build/** src/** +web/** test/** test-workspace/** out/** diff --git a/extensions/typescript-language-features/extension-browser.webpack.config.js b/extensions/typescript-language-features/extension-browser.webpack.config.js index 7fb010d0448..c931b906965 100644 --- a/extensions/typescript-language-features/extension-browser.webpack.config.js +++ b/extensions/typescript-language-features/extension-browser.webpack.config.js @@ -7,8 +7,6 @@ 'use strict'; const CopyPlugin = require('copy-webpack-plugin'); -const Terser = require('terser'); -const fs = require('fs'); const path = require('path'); const defaultConfig = require('../shared.webpack.config'); @@ -30,8 +28,7 @@ const languages = [ 'tr', 'zh-cn', ]; - -module.exports = withBrowserDefaults({ +module.exports = [withBrowserDefaults({ context: __dirname, entry: { extension: './src/extension.browser.ts', @@ -60,30 +57,24 @@ module.exports = withBrowserDefaults({ })) ], }), - // @ts-ignore - new CopyPlugin({ - patterns: [ - { - from: '../node_modules/typescript/lib/tsserver.js', - to: 'typescript/tsserver.web.js', - transform: async (content) => { - const dynamicImportCompatPath = path.join(__dirname, '..', 'node_modules', 'typescript', 'lib', 'dynamicImportCompat.js'); - const prefix = fs.existsSync(dynamicImportCompatPath) ? fs.readFileSync(dynamicImportCompatPath) : undefined; - const output = await Terser.minify(content.toString()); - if (!output.code) { - throw new Error('Terser returned undefined code'); - } - - if (prefix) { - return prefix.toString() + '\n' + output.code; - } - return output.code; - }, - transformPath: (targetPath) => { - return targetPath.replace('tsserver.js', 'tsserver.web.js'); - } - } - ], - }), ], -}); +}), withBrowserDefaults({ + context: __dirname, + entry: { + 'typescript/tsserver.web': './web/webServer.ts' + }, + module: { + exprContextCritical: false, + }, + ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/], + output: { + // all output goes into `dist`. + // packaging depends on that and this must always be like it + filename: '[name].js', + path: path.join(__dirname, 'dist', 'browser'), + libraryTarget: undefined, + }, + externals: { + 'perf_hooks': 'commonjs perf_hooks', + } +})]; diff --git a/extensions/typescript-language-features/web/tsconfig.json b/extensions/typescript-language-features/web/tsconfig.json new file mode 100644 index 00000000000..9944d5b63d8 --- /dev/null +++ b/extensions/typescript-language-features/web/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../out", + "module": "nodenext", + "moduleDetection": "legacy", + "experimentalDecorators": true, + "types": [ + "node" + ] + }, + "files": [ + "webServer.ts" + ] +} diff --git a/extensions/typescript-language-features/web/webServer.ts b/extensions/typescript-language-features/web/webServer.ts new file mode 100644 index 00000000000..680228881fa --- /dev/null +++ b/extensions/typescript-language-features/web/webServer.ts @@ -0,0 +1,528 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +/// +/// +import * as ts from 'typescript/lib/tsserverlibrary'; +// BEGIN misc internals +const hasArgument: (argumentName: string) => boolean = (ts as any).server.hasArgument; +const findArgument: (argumentName: string) => string | undefined = (ts as any).server.findArgument; +const nowString: () => string = (ts as any).server.nowString; +const noop = () => { }; +const perfLogger = { + logEvent: noop, + logErrEvent(_: any) { }, + logPerfEvent(_: any) { }, + logInfoEvent(_: any) { }, + logStartCommand: noop, + logStopCommand: noop, + logStartUpdateProgram: noop, + logStopUpdateProgram: noop, + logStartUpdateGraph: noop, + logStopUpdateGraph: noop, + logStartResolveModule: noop, + logStopResolveModule: noop, + logStartParseSourceFile: noop, + logStopParseSourceFile: noop, + logStartReadFile: noop, + logStopReadFile: noop, + logStartBindFile: noop, + logStopBindFile: noop, + logStartScheduledOperation: noop, + logStopScheduledOperation: noop, +}; +const assertNever: (member: never) => never = (ts as any).Debug.assertNever; +const memoize: (callback: () => T) => () => T = (ts as any).memoize; +const ensureTrailingDirectorySeparator: (path: string) => string = (ts as any).ensureTrailingDirectorySeparator; +const getDirectoryPath: (path: string) => string = (ts as any).getDirectoryPath; +const directorySeparator: string = (ts as any).directorySeparator; +const combinePaths: (path: string, ...paths: (string | undefined)[]) => string = (ts as any).combinePaths; +const noopFileWatcher: ts.FileWatcher = { close: noop }; +const returnNoopFileWatcher = () => noopFileWatcher; +function getLogLevel(level: string | undefined) { + if (level) { + const l = level.toLowerCase(); + for (const name in ts.server.LogLevel) { + if (isNaN(+name) && l === name.toLowerCase()) { + return ts.server.LogLevel[name] as any as ts.server.LogLevel; + } + } + } + return undefined; +} + +const notImplemented: () => never = (ts as any).notImplemented; +const returnFalse: () => false = (ts as any).returnFalse; +const returnUndefined: () => undefined = (ts as any).returnUndefined; +const identity: (x: T) => T = (ts as any).identity; +const indent: (str: string) => string = (ts as any).server.indent; +const setSys: (s: ts.System) => void = (ts as any).setSys; +const validateLocaleAndSetLanguage: ( + locale: string, + sys: { getExecutingFilePath(): string; resolvePath(path: string): string; fileExists(fileName: string): boolean; readFile(fileName: string): string | undefined }, +) => void = (ts as any).validateLocaleAndSetLanguage; +const setStackTraceLimit: () => void = (ts as any).setStackTraceLimit; + +// End misc internals +// BEGIN webServer/webServer.ts +interface HostWithWriteMessage { + writeMessage(s: any): void; +} +interface WebHost extends HostWithWriteMessage { + readFile(path: string): string | undefined; + fileExists(path: string): boolean; +} + +class BaseLogger implements ts.server.Logger { + private seq = 0; + private inGroup = false; + private firstInGroup = true; + constructor(protected readonly level: ts.server.LogLevel) { + } + static padStringRight(str: string, padding: string) { + return (str + padding).slice(0, padding.length); + } + close() { + } + getLogFileName(): string | undefined { + return undefined; + } + perftrc(s: string) { + this.msg(s, ts.server.Msg.Perf); + } + info(s: string) { + this.msg(s, ts.server.Msg.Info); + } + err(s: string) { + this.msg(s, ts.server.Msg.Err); + } + startGroup() { + this.inGroup = true; + this.firstInGroup = true; + } + endGroup() { + this.inGroup = false; + } + loggingEnabled() { + return true; + } + hasLevel(level: ts.server.LogLevel) { + return this.loggingEnabled() && this.level >= level; + } + msg(s: string, type: ts.server.Msg = ts.server.Msg.Err) { + switch (type) { + case ts.server.Msg.Info: + perfLogger.logInfoEvent(s); + break; + case ts.server.Msg.Perf: + perfLogger.logPerfEvent(s); + break; + default: // Msg.Err + perfLogger.logErrEvent(s); + break; + } + + if (!this.canWrite()) { return; } + + s = `[${nowString()}] ${s}\n`; + if (!this.inGroup || this.firstInGroup) { + const prefix = BaseLogger.padStringRight(type + ' ' + this.seq.toString(), ' '); + s = prefix + s; + } + this.write(s, type); + if (!this.inGroup) { + this.seq++; + } + } + protected canWrite() { + return true; + } + protected write(_s: string, _type: ts.server.Msg) { + } +} + +type MessageLogLevel = 'info' | 'perf' | 'error'; +interface LoggingMessage { + readonly type: 'log'; + readonly level: MessageLogLevel; + readonly body: string; +} +class MainProcessLogger extends BaseLogger { + constructor(level: ts.server.LogLevel, private host: HostWithWriteMessage) { + super(level); + } + protected override write(body: string, type: ts.server.Msg) { + let level: MessageLogLevel; + switch (type) { + case ts.server.Msg.Info: + level = 'info'; + break; + case ts.server.Msg.Perf: + level = 'perf'; + break; + case ts.server.Msg.Err: + level = 'error'; + break; + default: + assertNever(type); + } + this.host.writeMessage({ + type: 'log', + level, + body, + } as LoggingMessage); + } +} + +function serverCreateWebSystem(host: WebHost, args: string[], getExecutingFilePath: () => string): + ts.server.ServerHost & { + importPlugin?(root: string, moduleName: string): Promise; + getEnvironmentVariable(name: string): string; + } { + const returnEmptyString = () => ''; + const getExecutingDirectoryPath = memoize(() => memoize(() => ensureTrailingDirectorySeparator(getDirectoryPath(getExecutingFilePath())))); + // Later we could map ^memfs:/ to do something special if we want to enable more functionality like module resolution or something like that + const getWebPath = (path: string) => path.startsWith(directorySeparator) ? path.replace(directorySeparator, getExecutingDirectoryPath()) : undefined; + + return { + args, + newLine: '\r\n', // This can be configured by clients + useCaseSensitiveFileNames: false, // Use false as the default on web since that is the safest option + readFile: path => { + const webPath = getWebPath(path); + return webPath && host.readFile(webPath); + }, + write: host.writeMessage.bind(host), + watchFile: returnNoopFileWatcher, + watchDirectory: returnNoopFileWatcher, + + getExecutingFilePath: () => directorySeparator, + getCurrentDirectory: returnEmptyString, // For inferred project root if projectRoot path is not set, normalizing the paths + + /* eslint-disable no-restricted-globals */ + setTimeout: (cb, ms, ...args) => setTimeout(cb, ms, ...args), + clearTimeout: handle => clearTimeout(handle), + setImmediate: x => setTimeout(x, 0), + clearImmediate: handle => clearTimeout(handle), + /* eslint-enable no-restricted-globals */ + + importPlugin: async (initialDir: string, moduleName: string): Promise => { + const packageRoot = combinePaths(initialDir, moduleName); + + let packageJson: any | undefined; + try { + const packageJsonResponse = await fetch(combinePaths(packageRoot, 'package.json')); + packageJson = await packageJsonResponse.json(); + } + catch (e) { + return { module: undefined, error: new Error('Could not load plugin. Could not load "package.json".') }; + } + + const browser = packageJson.browser; + if (!browser) { + return { module: undefined, error: new Error('Could not load plugin. No "browser" field found in package.json.') }; + } + + const scriptPath = combinePaths(packageRoot, browser); + try { + const { default: module } = await import(/* webpackIgnore: true */scriptPath); + return { module, error: undefined }; + } + catch (e) { + return { module: undefined, error: e }; + } + }, + exit: notImplemented, + + // Debugging related + getEnvironmentVariable: returnEmptyString, // TODO:: Used to enable debugging info + // tryEnableSourceMapsForHost?(): void; + // debugMode?: boolean; + + // For semantic server mode + fileExists: path => { + const webPath = getWebPath(path); + return !!webPath && host.fileExists(webPath); + }, + directoryExists: returnFalse, // Module resolution + readDirectory: notImplemented, // Configured project, typing installer + getDirectories: () => [], // For automatic type reference directives + createDirectory: notImplemented, // compile On save + writeFile: notImplemented, // compile on save + resolvePath: identity, // Plugins + // realpath? // Module resolution, symlinks + // getModifiedTime // File watching + // createSHA256Hash // telemetry of the project + + // Logging related + // /*@internal*/ bufferFrom?(input: string, encoding?: string): Buffer; + // gc?(): void; + // getMemoryUsage?(): number; + }; +} + +interface StartSessionOptions { + globalPlugins: ts.server.SessionOptions['globalPlugins']; + pluginProbeLocations: ts.server.SessionOptions['pluginProbeLocations']; + allowLocalPluginLoads: ts.server.SessionOptions['allowLocalPluginLoads']; + useSingleInferredProject: ts.server.SessionOptions['useSingleInferredProject']; + useInferredProjectPerProjectRoot: ts.server.SessionOptions['useInferredProjectPerProjectRoot']; + suppressDiagnosticEvents: ts.server.SessionOptions['suppressDiagnosticEvents']; + noGetErrOnBackgroundUpdate: ts.server.SessionOptions['noGetErrOnBackgroundUpdate']; + syntaxOnly: ts.server.SessionOptions['syntaxOnly']; + serverMode: ts.server.SessionOptions['serverMode']; +} +class ServerWorkerSession extends ts.server.Session<{}> { + constructor( + host: ts.server.ServerHost, + private webHost: HostWithWriteMessage, + options: StartSessionOptions, + logger: ts.server.Logger, + cancellationToken: ts.server.ServerCancellationToken, + hrtime: ts.server.SessionOptions['hrtime'] + ) { + super({ + host, + cancellationToken, + ...options, + typingsInstaller: ts.server.nullTypingsInstaller, + byteLength: notImplemented, // Formats the message text in send of Session which is overriden in this class so not needed + hrtime, + logger, + canUseEvents: true, + }); + } + + public override send(msg: ts.server.protocol.Message) { + if (msg.type === 'event' && !this.canUseEvents) { + if (this.logger.hasLevel(ts.server.LogLevel.verbose)) { + this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); + } + return; + } + if (this.logger.hasLevel(ts.server.LogLevel.verbose)) { + this.logger.info(`${msg.type}:${indent(JSON.stringify(msg))}`); + } + this.webHost.writeMessage(msg); + } + + protected override parseMessage(message: {}): ts.server.protocol.Request { + return message as ts.server.protocol.Request; + } + + protected override toStringMessage(message: {}) { + return JSON.stringify(message, undefined, 2); + } +} +// END webServer/webServer.ts +// BEGIN tsserver/webServer.ts +const nullLogger: ts.server.Logger = { + close: noop, + hasLevel: returnFalse, + loggingEnabled: returnFalse, + perftrc: noop, + info: noop, + msg: noop, + startGroup: noop, + endGroup: noop, + getLogFileName: returnUndefined, +}; + +function parseServerMode(): ts.LanguageServiceMode | string | undefined { + const mode = findArgument('--serverMode'); + if (!mode) { return undefined; } + switch (mode.toLowerCase()) { + case 'partialsemantic': + return ts.LanguageServiceMode.PartialSemantic; + case 'syntactic': + return ts.LanguageServiceMode.Syntactic; + default: + return mode; + } +} + +function initializeWebSystem(args: string[]): StartInput { + createWebSystem(args); + const modeOrUnknown = parseServerMode(); + let serverMode: ts.LanguageServiceMode | undefined; + let unknownServerMode: string | undefined; + if (typeof modeOrUnknown === 'number') { serverMode = modeOrUnknown; } + else { unknownServerMode = modeOrUnknown; } + const logger = createLogger(); + + // enable deprecation logging + (ts as any).Debug.loggingHost = { + log(level: unknown, s: string) { + switch (level) { + case (ts as any).LogLevel.Error: + case (ts as any).LogLevel.Warning: + return logger.msg(s, ts.server.Msg.Err); + case (ts as any).LogLevel.Info: + case (ts as any).LogLevel.Verbose: + return logger.msg(s, ts.server.Msg.Info); + } + } + }; + + return { + args, + logger, + cancellationToken: ts.server.nullCancellationToken, + // Webserver defaults to partial semantic mode + serverMode: serverMode ?? ts.LanguageServiceMode.PartialSemantic, + unknownServerMode, + startSession: startWebSession + }; +} + +function createLogger() { + const cmdLineVerbosity = getLogLevel(findArgument('--logVerbosity')); + return cmdLineVerbosity !== undefined ? new MainProcessLogger(cmdLineVerbosity, { writeMessage }) : nullLogger; +} + +function writeMessage(s: any) { + postMessage(s); +} + +function createWebSystem(args: string[]) { + (ts as any).Debug.assert(ts.sys === undefined); + const webHost: WebHost = { + readFile: webPath => { + const request = new XMLHttpRequest(); + request.open('GET', webPath, /* asynchronous */ false); + request.send(); + return request.status === 200 ? request.responseText : undefined; + }, + fileExists: webPath => { + const request = new XMLHttpRequest(); + request.open('HEAD', webPath, /* asynchronous */ false); + request.send(); + return request.status === 200; + }, + writeMessage, + }; + // Do this after sys has been set as findArguments is going to work only then + const sys = serverCreateWebSystem(webHost, args, () => findArgument('--executingFilePath') || location + ''); + setSys(sys); + const localeStr = findArgument('--locale'); + if (localeStr) { + validateLocaleAndSetLanguage(localeStr, sys); + } +} + +function hrtime(previous?: number[]) { + const now = self.performance.now() * 1e-3; + let seconds = Math.floor(now); + let nanoseconds = Math.floor((now % 1) * 1e9); + // NOTE: This check is added probably because it's missed without strictFunctionTypes on + if (previous?.[0] !== undefined && previous?.[1] !== undefined) { + seconds = seconds - previous[0]; + nanoseconds = nanoseconds - previous[1]; + if (nanoseconds < 0) { + seconds--; + nanoseconds += 1e9; + } + } + return [seconds, nanoseconds]; +} + +function startWebSession(options: StartSessionOptions, logger: ts.server.Logger, cancellationToken: ts.server.ServerCancellationToken) { + class WorkerSession extends ServerWorkerSession { + constructor() { + super( + ts.sys as ts.server.ServerHost & { tryEnableSourceMapsForHost?(): void; getEnvironmentVariable(name: string): string }, + { writeMessage }, + options, + logger, + cancellationToken, + hrtime); + } + + override exit() { + this.logger.info('Exiting...'); + this.projectService.closeLog(); + close(); + } + + listen() { + addEventListener('message', (message: any) => { + this.onMessage(message.data); + }); + } + } + + const session = new WorkerSession(); + + // Start listening + session.listen(); +} +// END tsserver/webServer.ts +// BEGIN tsserver/server.ts +function findArgumentStringArray(argName: string): readonly string[] { + const arg = findArgument(argName); + if (arg === undefined) { + return []; + } + return arg.split(',').filter(name => name !== ''); +} + +interface StartInput { + args: readonly string[]; + logger: ts.server.Logger; + cancellationToken: ts.server.ServerCancellationToken; + serverMode: ts.LanguageServiceMode | undefined; + unknownServerMode?: string; + startSession: (option: StartSessionOptions, logger: ts.server.Logger, cancellationToken: ts.server.ServerCancellationToken) => void; +} +function start({ args, logger, cancellationToken, serverMode, unknownServerMode, startSession: startServer }: StartInput, platform: string) { + const syntaxOnly = hasArgument('--syntaxOnly'); + + logger.info(`Starting TS Server`); + logger.info(`Version: Moved from Typescript 5.0.0-dev`); + logger.info(`Arguments: ${args.join(' ')}`); + logger.info(`Platform: ${platform} NodeVersion: N/A CaseSensitive: ${ts.sys.useCaseSensitiveFileNames}`); + logger.info(`ServerMode: ${serverMode} syntaxOnly: ${syntaxOnly} hasUnknownServerMode: ${unknownServerMode}`); + + setStackTraceLimit(); + + if ((ts as any).Debug.isDebugging) { + (ts as any).Debug.enableDebugInfo(); + } + + if ((ts as any).sys.tryEnableSourceMapsForHost && /^development$/i.test((ts as any).sys.getEnvironmentVariable('NODE_ENV'))) { + (ts as any).sys.tryEnableSourceMapsForHost(); + } + + // Overwrites the current console messages to instead write to + // the log. This is so that language service plugins which use + // console.log don't break the message passing between tsserver + // and the client + console.log = (...args) => logger.msg(args.length === 1 ? args[0] : args.join(', '), ts.server.Msg.Info); + console.warn = (...args) => logger.msg(args.length === 1 ? args[0] : args.join(', '), ts.server.Msg.Err); + console.error = (...args) => logger.msg(args.length === 1 ? args[0] : args.join(', '), ts.server.Msg.Err); + + startServer( + { + globalPlugins: findArgumentStringArray('--globalPlugins'), + pluginProbeLocations: findArgumentStringArray('--pluginProbeLocations'), + allowLocalPluginLoads: hasArgument('--allowLocalPluginLoads'), + useSingleInferredProject: hasArgument('--useSingleInferredProject'), + useInferredProjectPerProjectRoot: hasArgument('--useInferredProjectPerProjectRoot'), + suppressDiagnosticEvents: hasArgument('--suppressDiagnosticEvents'), + noGetErrOnBackgroundUpdate: hasArgument('--noGetErrOnBackgroundUpdate'), + syntaxOnly, + serverMode + }, + logger, + cancellationToken + ); +} +// Get args from first message +const listener = (e: any) => { + removeEventListener('message', listener); + const args = e.data; + start(initializeWebSystem(args), 'web'); +}; +addEventListener('message', listener); +// END tsserver/server.ts