• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*@internal*/
2/// <reference lib="dom" />
3/// <reference lib="webworker.importscripts" />
4
5namespace ts.server {
6    export interface HostWithWriteMessage {
7        writeMessage(s: any): void;
8    }
9    export interface WebHost extends HostWithWriteMessage {
10        readFile(path: string): string | undefined;
11        fileExists(path: string): boolean;
12    }
13
14    export class BaseLogger implements Logger {
15        private seq = 0;
16        private inGroup = false;
17        private firstInGroup = true;
18        constructor(protected readonly level: LogLevel) {
19        }
20        static padStringRight(str: string, padding: string) {
21            return (str + padding).slice(0, padding.length);
22        }
23        close() {
24        }
25        getLogFileName(): string | undefined {
26            return undefined;
27        }
28        perftrc(s: string) {
29            this.msg(s, Msg.Perf);
30        }
31        info(s: string) {
32            this.msg(s, Msg.Info);
33        }
34        err(s: string) {
35            this.msg(s, Msg.Err);
36        }
37        startGroup() {
38            this.inGroup = true;
39            this.firstInGroup = true;
40        }
41        endGroup() {
42            this.inGroup = false;
43        }
44        loggingEnabled() {
45            return true;
46        }
47        hasLevel(level: LogLevel) {
48            return this.loggingEnabled() && this.level >= level;
49        }
50        msg(s: string, type: Msg = Msg.Err) {
51            switch (type) {
52                case Msg.Info:
53                    perfLogger.logInfoEvent(s);
54                    break;
55                case Msg.Perf:
56                    perfLogger.logPerfEvent(s);
57                    break;
58                default: // Msg.Err
59                    perfLogger.logErrEvent(s);
60                    break;
61            }
62
63            if (!this.canWrite()) return;
64
65            s = `[${nowString()}] ${s}\n`;
66            if (!this.inGroup || this.firstInGroup) {
67                const prefix = BaseLogger.padStringRight(type + " " + this.seq.toString(), "          ");
68                s = prefix + s;
69            }
70            this.write(s, type);
71            if (!this.inGroup) {
72                this.seq++;
73            }
74        }
75        protected canWrite() {
76            return true;
77        }
78        protected write(_s: string, _type: Msg) {
79        }
80    }
81
82    export type MessageLogLevel = "info" | "perf" | "error";
83    export interface LoggingMessage {
84        readonly type: "log";
85        readonly level: MessageLogLevel;
86        readonly body: string
87    }
88    export class MainProcessLogger extends BaseLogger {
89        constructor(level: LogLevel, private host: HostWithWriteMessage) {
90            super(level);
91        }
92        protected write(body: string, type: Msg) {
93            let level: MessageLogLevel;
94            switch (type) {
95                case Msg.Info:
96                    level = "info";
97                    break;
98                case Msg.Perf:
99                    level = "perf";
100                    break;
101                case Msg.Err:
102                    level = "error";
103                    break;
104                default:
105                    Debug.assertNever(type);
106            }
107            this.host.writeMessage({
108                type: "log",
109                level,
110                body,
111            } as LoggingMessage);
112        }
113    }
114
115    export declare const dynamicImport: ((id: string) => Promise<any>) | undefined;
116
117    // Attempt to load `dynamicImport`
118    if (typeof importScripts === "function") {
119        try {
120            // NOTE: importScripts is synchronous
121            importScripts("dynamicImportCompat.js");
122        }
123        catch {
124            // ignored
125        }
126    }
127
128    export function createWebSystem(host: WebHost, args: string[], getExecutingFilePath: () => string): ServerHost {
129        const returnEmptyString = () => "";
130        const getExecutingDirectoryPath = memoize(() => memoize(() => ensureTrailingDirectorySeparator(getDirectoryPath(getExecutingFilePath()))));
131        // Later we could map ^memfs:/ to do something special if we want to enable more functionality like module resolution or something like that
132        const getWebPath = (path: string) => startsWith(path, directorySeparator) ? path.replace(directorySeparator, getExecutingDirectoryPath()) : undefined;
133
134        const dynamicImport = async (id: string): Promise<any> => {
135            // Use syntactic dynamic import first, if available
136            if (server.dynamicImport) {
137                return server.dynamicImport(id);
138            }
139
140            throw new Error("Dynamic import not implemented");
141        };
142
143        return {
144            args,
145            newLine: "\r\n", // This can be configured by clients
146            useCaseSensitiveFileNames: false, // Use false as the default on web since that is the safest option
147            readFile: path => {
148                const webPath = getWebPath(path);
149                return webPath && host.readFile(webPath);
150            },
151            write: host.writeMessage.bind(host),
152            watchFile: returnNoopFileWatcher,
153            watchDirectory: returnNoopFileWatcher,
154
155            getExecutingFilePath: () => directorySeparator,
156            getCurrentDirectory: returnEmptyString, // For inferred project root if projectRoot path is not set, normalizing the paths
157
158            /* eslint-disable no-restricted-globals */
159            setTimeout: (cb, ms, ...args) => setTimeout(cb, ms, ...args),
160            clearTimeout: handle => clearTimeout(handle),
161            setImmediate: x => setTimeout(x, 0),
162            clearImmediate: handle => clearTimeout(handle),
163            /* eslint-enable no-restricted-globals */
164
165            importPlugin: async (initialDir: string, moduleName: string): Promise<ModuleImportResult> => {
166                const packageRoot = combinePaths(initialDir, moduleName);
167
168                let packageJson: any | undefined;
169                try {
170                    const packageJsonResponse = await fetch(combinePaths(packageRoot, "package.json"));
171                    packageJson = await packageJsonResponse.json();
172                }
173                catch (e) {
174                    return { module: undefined, error: new Error("Could not load plugin. Could not load 'package.json'.") };
175                }
176
177                const browser = packageJson.browser;
178                if (!browser) {
179                    return { module: undefined, error: new Error("Could not load plugin. No 'browser' field found in package.json.") };
180                }
181
182                const scriptPath = combinePaths(packageRoot, browser);
183                try {
184                    const { default: module } = await dynamicImport(scriptPath);
185                    return { module, error: undefined };
186                }
187                catch (e) {
188                    return { module: undefined, error: e };
189                }
190            },
191            exit: notImplemented,
192
193            // Debugging related
194            getEnvironmentVariable: returnEmptyString, // TODO:: Used to enable debugging info
195            // tryEnableSourceMapsForHost?(): void;
196            // debugMode?: boolean;
197
198            // For semantic server mode
199            fileExists: path => {
200                const webPath = getWebPath(path);
201                return !!webPath && host.fileExists(webPath);
202            },
203            directoryExists: returnFalse, // Module resolution
204            readDirectory: notImplemented, // Configured project, typing installer
205            getDirectories: () => [], // For automatic type reference directives
206            createDirectory: notImplemented, // compile On save
207            writeFile: notImplemented, // compile on save
208            resolvePath: identity, // Plugins
209            // realpath? // Module resolution, symlinks
210            // getModifiedTime // File watching
211            // createSHA256Hash // telemetry of the project
212
213            // Logging related
214            // /*@internal*/ bufferFrom?(input: string, encoding?: string): Buffer;
215            // gc?(): void;
216            // getMemoryUsage?(): number;
217        };
218    }
219
220    export interface StartSessionOptions {
221        globalPlugins: SessionOptions["globalPlugins"];
222        pluginProbeLocations: SessionOptions["pluginProbeLocations"];
223        allowLocalPluginLoads: SessionOptions["allowLocalPluginLoads"];
224        useSingleInferredProject: SessionOptions["useSingleInferredProject"];
225        useInferredProjectPerProjectRoot: SessionOptions["useInferredProjectPerProjectRoot"];
226        suppressDiagnosticEvents: SessionOptions["suppressDiagnosticEvents"];
227        noGetErrOnBackgroundUpdate: SessionOptions["noGetErrOnBackgroundUpdate"];
228        syntaxOnly: SessionOptions["syntaxOnly"];
229        serverMode: SessionOptions["serverMode"];
230    }
231    export class WorkerSession extends Session<{}> {
232        constructor(host: ServerHost, private webHost: HostWithWriteMessage, options: StartSessionOptions, logger: Logger, cancellationToken: ServerCancellationToken, hrtime: SessionOptions["hrtime"]) {
233            super({
234                host,
235                cancellationToken,
236                ...options,
237                typingsInstaller: nullTypingsInstaller,
238                byteLength: notImplemented, // Formats the message text in send of Session which is overriden in this class so not needed
239                hrtime,
240                logger,
241                canUseEvents: true,
242            });
243        }
244
245        public send(msg: protocol.Message) {
246            if (msg.type === "event" && !this.canUseEvents) {
247                if (this.logger.hasLevel(LogLevel.verbose)) {
248                    this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`);
249                }
250                return;
251            }
252            if (this.logger.hasLevel(LogLevel.verbose)) {
253                this.logger.info(`${msg.type}:${indent(JSON.stringify(msg))}`);
254            }
255            this.webHost.writeMessage(msg);
256        }
257
258        protected parseMessage(message: {}): protocol.Request {
259            return message as protocol.Request;
260        }
261
262        protected toStringMessage(message: {}) {
263            return JSON.stringify(message, undefined, 2);
264        }
265    }
266}
267