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