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