Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/scripts/playground-server.ts
3520 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as fsPromise from 'fs/promises';
7
import path from 'path';
8
import * as http from 'http';
9
import * as parcelWatcher from '@parcel/watcher';
10
11
/**
12
* Launches the server for the monaco editor playground
13
*/
14
function main() {
15
const server = new HttpServer({ host: 'localhost', port: 5001, cors: true });
16
server.use('/', redirectToMonacoEditorPlayground());
17
18
const rootDir = path.join(__dirname, '..');
19
const fileServer = new FileServer(rootDir);
20
server.use(fileServer.handleRequest);
21
22
const moduleIdMapper = new SimpleModuleIdPathMapper(path.join(rootDir, 'out'));
23
const editorMainBundle = new CachedBundle('vs/editor/editor.main', moduleIdMapper);
24
fileServer.overrideFileContent(editorMainBundle.entryModulePath, () => editorMainBundle.bundle());
25
26
const loaderPath = path.join(rootDir, 'out/vs/loader.js');
27
fileServer.overrideFileContent(loaderPath, async () =>
28
Buffer.from(new TextEncoder().encode(makeLoaderJsHotReloadable(await fsPromise.readFile(loaderPath, 'utf8'), new URL('/file-changes', server.url))))
29
);
30
31
const watcher = DirWatcher.watchRecursively(moduleIdMapper.rootDir);
32
watcher.onDidChange((path, newContent) => {
33
editorMainBundle.setModuleContent(path, newContent);
34
editorMainBundle.bundle();
35
console.log(`${new Date().toLocaleTimeString()}, file change: ${path}`);
36
});
37
server.use('/file-changes', handleGetFileChangesRequest(watcher, fileServer, moduleIdMapper));
38
39
console.log(`Server listening on ${server.url}`);
40
}
41
setTimeout(main, 0);
42
43
// #region Http/File Server
44
45
type RequestHandler = (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void>;
46
type ChainableRequestHandler = (req: http.IncomingMessage, res: http.ServerResponse, next: RequestHandler) => Promise<void>;
47
48
class HttpServer {
49
private readonly server: http.Server;
50
public readonly url: URL;
51
52
private handler: ChainableRequestHandler[] = [];
53
54
constructor(options: { host: string; port: number; cors: boolean }) {
55
this.server = http.createServer(async (req, res) => {
56
if (options.cors) {
57
res.setHeader('Access-Control-Allow-Origin', '*');
58
}
59
60
let i = 0;
61
const next = async (req: http.IncomingMessage, res: http.ServerResponse) => {
62
if (i >= this.handler.length) {
63
res.writeHead(404, { 'Content-Type': 'text/plain' });
64
res.end('404 Not Found');
65
return;
66
}
67
const handler = this.handler[i];
68
i++;
69
await handler(req, res, next);
70
};
71
await next(req, res);
72
});
73
this.server.listen(options.port, options.host);
74
this.url = new URL(`http://${options.host}:${options.port}`);
75
}
76
77
use(handler: ChainableRequestHandler);
78
use(path: string, handler: ChainableRequestHandler);
79
use(...args: [path: string, handler: ChainableRequestHandler] | [handler: ChainableRequestHandler]) {
80
const handler = args.length === 1 ? args[0] : (req, res, next) => {
81
const path = args[0];
82
const requestedUrl = new URL(req.url, this.url);
83
if (requestedUrl.pathname === path) {
84
return args[1](req, res, next);
85
} else {
86
return next(req, res);
87
}
88
};
89
90
this.handler.push(handler);
91
}
92
}
93
94
function redirectToMonacoEditorPlayground(): ChainableRequestHandler {
95
return async (req, res) => {
96
const url = new URL('https://microsoft.github.io/monaco-editor/playground.html');
97
url.searchParams.append('source', `http://${req.headers.host}/out/vs`);
98
res.writeHead(302, { Location: url.toString() });
99
res.end();
100
};
101
}
102
103
class FileServer {
104
private readonly overrides = new Map<string, () => Promise<Buffer>>();
105
106
constructor(public readonly publicDir: string) { }
107
108
public readonly handleRequest: ChainableRequestHandler = async (req, res, next) => {
109
const requestedUrl = new URL(req.url!, `http://${req.headers.host}`);
110
111
const pathName = requestedUrl.pathname;
112
113
const filePath = path.join(this.publicDir, pathName);
114
if (!filePath.startsWith(this.publicDir)) {
115
res.writeHead(403, { 'Content-Type': 'text/plain' });
116
res.end('403 Forbidden');
117
return;
118
}
119
120
try {
121
const override = this.overrides.get(filePath);
122
let content: Buffer;
123
if (override) {
124
content = await override();
125
} else {
126
content = await fsPromise.readFile(filePath);
127
}
128
129
const contentType = getContentType(filePath);
130
res.writeHead(200, { 'Content-Type': contentType });
131
res.end(content);
132
} catch (err) {
133
if (err.code === 'ENOENT') {
134
next(req, res);
135
} else {
136
res.writeHead(500, { 'Content-Type': 'text/plain' });
137
res.end('500 Internal Server Error');
138
}
139
}
140
};
141
142
public filePathToUrlPath(filePath: string): string | undefined {
143
const relative = path.relative(this.publicDir, filePath);
144
const isSubPath = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
145
146
if (!isSubPath) {
147
return undefined;
148
}
149
const relativePath = relative.replace(/\\/g, '/');
150
return `/${relativePath}`;
151
}
152
153
public overrideFileContent(filePath: string, content: () => Promise<Buffer>): void {
154
this.overrides.set(filePath, content);
155
}
156
}
157
158
function getContentType(filePath: string): string {
159
const extname = path.extname(filePath);
160
switch (extname) {
161
case '.js':
162
return 'text/javascript';
163
case '.css':
164
return 'text/css';
165
case '.json':
166
return 'application/json';
167
case '.png':
168
return 'image/png';
169
case '.jpg':
170
return 'image/jpg';
171
case '.svg':
172
return 'image/svg+xml';
173
case '.html':
174
return 'text/html';
175
case '.wasm':
176
return 'application/wasm';
177
default:
178
return 'text/plain';
179
}
180
}
181
182
// #endregion
183
184
// #region File Watching
185
186
interface IDisposable {
187
dispose(): void;
188
}
189
190
class DirWatcher {
191
public static watchRecursively(dir: string): DirWatcher {
192
const listeners: ((path: string, newContent: string) => void)[] = [];
193
const fileContents = new Map<string, string>();
194
const event = (handler: (path: string, newContent: string) => void) => {
195
listeners.push(handler);
196
return {
197
dispose: () => {
198
const idx = listeners.indexOf(handler);
199
if (idx >= 0) {
200
listeners.splice(idx, 1);
201
}
202
}
203
};
204
};
205
parcelWatcher.subscribe(dir, async (err, events) => {
206
for (const e of events) {
207
if (e.type === 'update') {
208
const newContent = await fsPromise.readFile(e.path, 'utf8');
209
if (fileContents.get(e.path) !== newContent) {
210
fileContents.set(e.path, newContent);
211
listeners.forEach(l => l(e.path, newContent));
212
}
213
}
214
}
215
});
216
return new DirWatcher(event);
217
}
218
219
constructor(public readonly onDidChange: (handler: (path: string, newContent: string) => void) => IDisposable) {
220
}
221
}
222
223
function handleGetFileChangesRequest(watcher: DirWatcher, fileServer: FileServer, moduleIdMapper: SimpleModuleIdPathMapper): ChainableRequestHandler {
224
return async (req, res) => {
225
res.writeHead(200, { 'Content-Type': 'text/plain' });
226
const d = watcher.onDidChange((fsPath, newContent) => {
227
const path = fileServer.filePathToUrlPath(fsPath);
228
if (path) {
229
res.write(JSON.stringify({ changedPath: path, moduleId: moduleIdMapper.getModuleId(fsPath), newContent }) + '\n');
230
}
231
});
232
res.on('close', () => d.dispose());
233
};
234
}
235
function makeLoaderJsHotReloadable(loaderJsCode: string, fileChangesUrl: URL): string {
236
loaderJsCode = loaderJsCode.replace(
237
/constructor\(env, scriptLoader, defineFunc, requireFunc, loaderAvailableTimestamp = 0\) {/,
238
'$&globalThis.___globalModuleManager = this; globalThis.vscode = { process: { env: { VSCODE_DEV: true } } }'
239
);
240
241
const ___globalModuleManager: any = undefined;
242
243
// This code will be appended to loader.js
244
function $watchChanges(fileChangesUrl: string) {
245
interface HotReloadConfig { }
246
247
let reloadFn;
248
if (globalThis.$sendMessageToParent) {
249
reloadFn = () => globalThis.$sendMessageToParent({ kind: 'reload' });
250
} else if (typeof window !== 'undefined') {
251
reloadFn = () => window.location.reload();
252
} else {
253
reloadFn = () => { };
254
}
255
256
console.log('Connecting to server to watch for changes...');
257
(fetch as any)(fileChangesUrl)
258
.then(async request => {
259
const reader = request.body.getReader();
260
let buffer = '';
261
while (true) {
262
const { done, value } = await reader.read();
263
if (done) { break; }
264
buffer += new TextDecoder().decode(value);
265
const lines = buffer.split('\n');
266
buffer = lines.pop()!;
267
268
const changes: { relativePath: string; config: HotReloadConfig | undefined; path: string; newContent: string }[] = [];
269
270
for (const line of lines) {
271
const data = JSON.parse(line);
272
const relativePath = data.changedPath.replace(/\\/g, '/').split('/out/')[1];
273
changes.push({ config: {}, path: data.changedPath, relativePath, newContent: data.newContent });
274
}
275
276
const result = handleChanges(changes, 'playground-server');
277
if (result.reloadFailedJsFiles.length > 0) {
278
reloadFn();
279
}
280
}
281
}).catch(err => {
282
console.error(err);
283
setTimeout(() => $watchChanges(fileChangesUrl), 1000);
284
});
285
286
287
function handleChanges(changes: {
288
relativePath: string;
289
config: HotReloadConfig | undefined;
290
path: string;
291
newContent: string;
292
}[], debugSessionName: string) {
293
// This function is stringified and injected into the debuggee.
294
295
const hotReloadData: { count: number; originalWindowTitle: any; timeout: any; shouldReload: boolean } = globalThis.$hotReloadData || (globalThis.$hotReloadData = { count: 0, messageHideTimeout: undefined, shouldReload: false });
296
297
const reloadFailedJsFiles: { relativePath: string; path: string }[] = [];
298
299
for (const change of changes) {
300
handleChange(change.relativePath, change.path, change.newContent, change.config);
301
}
302
303
return { reloadFailedJsFiles };
304
305
function handleChange(relativePath: string, path: string, newSrc: string, config: any) {
306
if (relativePath.endsWith('.css')) {
307
handleCssChange(relativePath);
308
} else if (relativePath.endsWith('.js')) {
309
handleJsChange(relativePath, path, newSrc, config);
310
}
311
}
312
313
function handleCssChange(relativePath: string) {
314
if (typeof document === 'undefined') {
315
return;
316
}
317
318
const styleSheet = (([...document.querySelectorAll(`link[rel='stylesheet']`)] as HTMLLinkElement[]))
319
.find(l => new URL(l.href, document.location.href).pathname.endsWith(relativePath));
320
if (styleSheet) {
321
setMessage(`reload ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`);
322
console.log(debugSessionName, 'css reloaded', relativePath);
323
styleSheet.href = styleSheet.href.replace(/\?.*/, '') + '?' + Date.now();
324
} else {
325
setMessage(`could not reload ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`);
326
console.log(debugSessionName, 'ignoring css change, as stylesheet is not loaded', relativePath);
327
}
328
}
329
330
331
function handleJsChange(relativePath: string, path: string, newSrc: string, config: any) {
332
const moduleIdStr = trimEnd(relativePath, '.js');
333
334
const requireFn: any = globalThis.require;
335
const moduleManager = (requireFn as any).moduleManager;
336
if (!moduleManager) {
337
console.log(debugSessionName, 'ignoring js change, as moduleManager is not available', relativePath);
338
return;
339
}
340
341
const moduleId = moduleManager._moduleIdProvider.getModuleId(moduleIdStr);
342
const oldModule = moduleManager._modules2[moduleId];
343
344
if (!oldModule) {
345
console.log(debugSessionName, 'ignoring js change, as module is not loaded', relativePath);
346
return;
347
}
348
349
// Check if we can reload
350
const g = globalThis as any;
351
352
// A frozen copy of the previous exports
353
const oldExports = Object.freeze({ ...oldModule.exports });
354
const reloadFn = g.$hotReload_applyNewExports?.({ oldExports, newSrc, config });
355
356
if (!reloadFn) {
357
console.log(debugSessionName, 'ignoring js change, as module does not support hot-reload', relativePath);
358
hotReloadData.shouldReload = true;
359
360
reloadFailedJsFiles.push({ relativePath, path });
361
362
setMessage(`hot reload not supported for ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`);
363
return;
364
}
365
366
// Eval maintains source maps
367
function newScript(/* this parameter is used by newSrc */ define) {
368
// eslint-disable-next-line no-eval
369
eval(newSrc); // CodeQL [SM01632] This code is only executed during development. It is required for the hot-reload functionality.
370
}
371
372
newScript(/* define */ function (deps, callback) {
373
// Evaluating the new code was successful.
374
375
// Redefine the module
376
delete moduleManager._modules2[moduleId];
377
moduleManager.defineModule(moduleIdStr, deps, callback);
378
const newModule = moduleManager._modules2[moduleId];
379
380
381
// Patch the exports of the old module, so that modules using the old module get the new exports
382
Object.assign(oldModule.exports, newModule.exports);
383
// We override the exports so that future reloads still patch the initial exports.
384
newModule.exports = oldModule.exports;
385
386
const successful = reloadFn(newModule.exports);
387
if (!successful) {
388
hotReloadData.shouldReload = true;
389
setMessage(`hot reload failed ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`);
390
console.log(debugSessionName, 'hot reload was not successful', relativePath);
391
return;
392
}
393
394
console.log(debugSessionName, 'hot reloaded', moduleIdStr);
395
setMessage(`successfully reloaded ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`);
396
});
397
}
398
399
function setMessage(message: string) {
400
const domElem = (document.querySelector('.titlebar-center .window-title')) as HTMLDivElement | undefined;
401
if (!domElem) { return; }
402
if (!hotReloadData.timeout) {
403
hotReloadData.originalWindowTitle = domElem.innerText;
404
} else {
405
clearTimeout(hotReloadData.timeout);
406
}
407
if (hotReloadData.shouldReload) {
408
message += ' (manual reload required)';
409
}
410
411
domElem.innerText = message;
412
hotReloadData.timeout = setTimeout(() => {
413
hotReloadData.timeout = undefined;
414
// If wanted, we can restore the previous title message
415
// domElem.replaceChildren(hotReloadData.originalWindowTitle);
416
}, 5000);
417
}
418
419
function formatPath(path: string): string {
420
const parts = path.split('/');
421
parts.reverse();
422
let result = parts[0];
423
parts.shift();
424
for (const p of parts) {
425
if (result.length + p.length > 40) {
426
break;
427
}
428
result = p + '/' + result;
429
if (result.length > 20) {
430
break;
431
}
432
}
433
return result;
434
}
435
436
function trimEnd(str, suffix) {
437
if (str.endsWith(suffix)) {
438
return str.substring(0, str.length - suffix.length);
439
}
440
return str;
441
}
442
}
443
}
444
445
const additionalJsCode = `
446
(${(function () {
447
globalThis.$hotReload_deprecateExports = new Set<(oldExports: any, newExports: any) => void>();
448
}).toString()})();
449
${$watchChanges.toString()}
450
$watchChanges(${JSON.stringify(fileChangesUrl)});
451
`;
452
453
return `${loaderJsCode}\n${additionalJsCode}`;
454
}
455
456
// #endregion
457
458
// #region Bundling
459
460
class CachedBundle {
461
public readonly entryModulePath = this.mapper.resolveRequestToPath(this.moduleId)!;
462
463
constructor(
464
private readonly moduleId: string,
465
private readonly mapper: SimpleModuleIdPathMapper,
466
) {
467
}
468
469
private loader: ModuleLoader | undefined = undefined;
470
471
private bundlePromise: Promise<Buffer> | undefined = undefined;
472
public async bundle(): Promise<Buffer> {
473
if (!this.bundlePromise) {
474
this.bundlePromise = (async () => {
475
if (!this.loader) {
476
this.loader = new ModuleLoader(this.mapper);
477
await this.loader.addModuleAndDependencies(this.entryModulePath);
478
}
479
const editorEntryPoint = await this.loader.getModule(this.entryModulePath);
480
const content = bundleWithDependencies(editorEntryPoint!);
481
return content;
482
})();
483
}
484
return this.bundlePromise;
485
}
486
487
public async setModuleContent(path: string, newContent: string): Promise<void> {
488
if (!this.loader) {
489
return;
490
}
491
const module = await this.loader!.getModule(path);
492
if (module) {
493
if (!this.loader.updateContent(module, newContent)) {
494
this.loader = undefined;
495
}
496
}
497
this.bundlePromise = undefined;
498
}
499
}
500
501
function bundleWithDependencies(module: IModule): Buffer {
502
const visited = new Set<IModule>();
503
const builder = new SourceMapBuilder();
504
505
function visit(module: IModule) {
506
if (visited.has(module)) {
507
return;
508
}
509
visited.add(module);
510
for (const dep of module.dependencies) {
511
visit(dep);
512
}
513
builder.addSource(module.source);
514
}
515
516
visit(module);
517
518
const sourceMap = builder.toSourceMap();
519
sourceMap.sourceRoot = module.source.sourceMap.sourceRoot;
520
const sourceMapBase64Str = Buffer.from(JSON.stringify(sourceMap)).toString('base64');
521
522
builder.addLine(`//# sourceMappingURL=data:application/json;base64,${sourceMapBase64Str}`);
523
524
return builder.toContent();
525
}
526
527
class ModuleLoader {
528
private readonly modules = new Map<string, Promise<IModule | undefined>>();
529
530
constructor(private readonly mapper: SimpleModuleIdPathMapper) { }
531
532
public getModule(path: string): Promise<IModule | undefined> {
533
return Promise.resolve(this.modules.get(path));
534
}
535
536
public updateContent(module: IModule, newContent: string): boolean {
537
const parsedModule = parseModule(newContent, module.path, this.mapper);
538
if (!parsedModule) {
539
return false;
540
}
541
if (!arrayEquals(parsedModule.dependencyRequests, module.dependencyRequests)) {
542
return false;
543
}
544
545
module.dependencyRequests = parsedModule.dependencyRequests;
546
module.source = parsedModule.source;
547
548
return true;
549
}
550
551
async addModuleAndDependencies(path: string): Promise<IModule | undefined> {
552
if (this.modules.has(path)) {
553
return this.modules.get(path)!;
554
}
555
556
const promise = (async () => {
557
const content = await fsPromise.readFile(path, { encoding: 'utf-8' });
558
559
const parsedModule = parseModule(content, path, this.mapper);
560
if (!parsedModule) {
561
return undefined;
562
}
563
564
const dependencies = (await Promise.all(parsedModule.dependencyRequests.map(async r => {
565
if (r === 'require' || r === 'exports' || r === 'module') {
566
return null;
567
}
568
569
const depPath = this.mapper.resolveRequestToPath(r, path);
570
if (!depPath) {
571
return null;
572
}
573
return await this.addModuleAndDependencies(depPath);
574
}))).filter((d): d is IModule => !!d);
575
576
const module: IModule = {
577
id: this.mapper.getModuleId(path)!,
578
dependencyRequests: parsedModule.dependencyRequests,
579
dependencies,
580
path,
581
source: parsedModule.source,
582
};
583
return module;
584
})();
585
586
this.modules.set(path, promise);
587
return promise;
588
}
589
}
590
591
function arrayEquals<T>(a: T[], b: T[]): boolean {
592
if (a.length !== b.length) {
593
return false;
594
}
595
for (let i = 0; i < a.length; i++) {
596
if (a[i] !== b[i]) {
597
return false;
598
}
599
}
600
return true;
601
}
602
603
const encoder = new TextEncoder();
604
605
function parseModule(content: string, path: string, mapper: SimpleModuleIdPathMapper): { source: Source; dependencyRequests: string[] } | undefined {
606
const m = content.match(/define\((\[.*?\])/);
607
if (!m) {
608
return undefined;
609
}
610
611
const dependencyRequests = JSON.parse(m[1].replace(/'/g, '"')) as string[];
612
613
const sourceMapHeader = '//# sourceMappingURL=data:application/json;base64,';
614
const idx = content.indexOf(sourceMapHeader);
615
616
let sourceMap: any = null;
617
if (idx !== -1) {
618
const sourceMapJsonStr = Buffer.from(content.substring(idx + sourceMapHeader.length), 'base64').toString('utf-8');
619
sourceMap = JSON.parse(sourceMapJsonStr);
620
content = content.substring(0, idx);
621
}
622
623
content = content.replace('define([', `define("${mapper.getModuleId(path)}", [`);
624
625
const contentBuffer = Buffer.from(encoder.encode(content));
626
const source = new Source(contentBuffer, sourceMap);
627
628
return { dependencyRequests, source };
629
}
630
631
class SimpleModuleIdPathMapper {
632
constructor(public readonly rootDir: string) { }
633
634
public getModuleId(path: string): string | null {
635
if (!path.startsWith(this.rootDir) || !path.endsWith('.js')) {
636
return null;
637
}
638
const moduleId = path.substring(this.rootDir.length + 1);
639
640
641
return moduleId.replace(/\\/g, '/').substring(0, moduleId.length - 3);
642
}
643
644
public resolveRequestToPath(request: string, requestingModulePath?: string): string | null {
645
if (request.indexOf('css!') !== -1) {
646
return null;
647
}
648
649
if (request.startsWith('.')) {
650
return path.join(path.dirname(requestingModulePath!), request + '.js');
651
} else {
652
return path.join(this.rootDir, request + '.js');
653
}
654
}
655
}
656
657
interface IModule {
658
id: string;
659
dependencyRequests: string[];
660
dependencies: IModule[];
661
path: string;
662
source: Source;
663
}
664
665
// #endregion
666
667
// #region SourceMapBuilder
668
669
// From https://stackoverflow.com/questions/29905373/how-to-create-sourcemaps-for-concatenated-files with modifications
670
671
class Source {
672
// Ends with \n
673
public readonly content: Buffer;
674
public readonly sourceMap: SourceMap;
675
public readonly sourceLines: number;
676
677
public readonly sourceMapMappings: Buffer;
678
679
680
constructor(content: Buffer, sourceMap: SourceMap | undefined) {
681
if (!sourceMap) {
682
sourceMap = SourceMapBuilder.emptySourceMap;
683
}
684
685
let sourceLines = countNL(content);
686
if (content.length > 0 && content[content.length - 1] !== 10) {
687
sourceLines++;
688
content = Buffer.concat([content, Buffer.from([10])]);
689
}
690
691
this.content = content;
692
this.sourceMap = sourceMap;
693
this.sourceLines = sourceLines;
694
this.sourceMapMappings = typeof this.sourceMap.mappings === 'string'
695
? Buffer.from(encoder.encode(sourceMap.mappings as string))
696
: this.sourceMap.mappings;
697
}
698
}
699
700
class SourceMapBuilder {
701
public static emptySourceMap: SourceMap = { version: 3, sources: [], mappings: Buffer.alloc(0) };
702
703
private readonly outputBuffer = new DynamicBuffer();
704
private readonly sources: string[] = [];
705
private readonly mappings = new DynamicBuffer();
706
private lastSourceIndex = 0;
707
private lastSourceLine = 0;
708
private lastSourceCol = 0;
709
710
addLine(text: string) {
711
this.outputBuffer.addString(text);
712
this.outputBuffer.addByte(10);
713
this.mappings.addByte(59); // ;
714
}
715
716
addSource(source: Source) {
717
const sourceMap = source.sourceMap;
718
this.outputBuffer.addBuffer(source.content);
719
720
const sourceRemap: number[] = [];
721
for (const v of sourceMap.sources) {
722
let pos = this.sources.indexOf(v);
723
if (pos < 0) {
724
pos = this.sources.length;
725
this.sources.push(v);
726
}
727
sourceRemap.push(pos);
728
}
729
let lastOutputCol = 0;
730
731
const inputMappings = source.sourceMapMappings;
732
let outputLine = 0;
733
let ip = 0;
734
let inOutputCol = 0;
735
let inSourceIndex = 0;
736
let inSourceLine = 0;
737
let inSourceCol = 0;
738
let shift = 0;
739
let value = 0;
740
let valpos = 0;
741
const commit = () => {
742
if (valpos === 0) { return; }
743
this.mappings.addVLQ(inOutputCol - lastOutputCol);
744
lastOutputCol = inOutputCol;
745
if (valpos === 1) {
746
valpos = 0;
747
return;
748
}
749
const outSourceIndex = sourceRemap[inSourceIndex];
750
this.mappings.addVLQ(outSourceIndex - this.lastSourceIndex);
751
this.lastSourceIndex = outSourceIndex;
752
this.mappings.addVLQ(inSourceLine - this.lastSourceLine);
753
this.lastSourceLine = inSourceLine;
754
this.mappings.addVLQ(inSourceCol - this.lastSourceCol);
755
this.lastSourceCol = inSourceCol;
756
valpos = 0;
757
};
758
while (ip < inputMappings.length) {
759
let b = inputMappings[ip++];
760
if (b === 59) { // ;
761
commit();
762
this.mappings.addByte(59);
763
inOutputCol = 0;
764
lastOutputCol = 0;
765
outputLine++;
766
} else if (b === 44) { // ,
767
commit();
768
this.mappings.addByte(44);
769
} else {
770
b = charToInteger[b];
771
if (b === 255) { throw new Error('Invalid sourceMap'); }
772
value += (b & 31) << shift;
773
if (b & 32) {
774
shift += 5;
775
} else {
776
const shouldNegate = value & 1;
777
value >>= 1;
778
if (shouldNegate) { value = -value; }
779
switch (valpos) {
780
case 0: inOutputCol += value; break;
781
case 1: inSourceIndex += value; break;
782
case 2: inSourceLine += value; break;
783
case 3: inSourceCol += value; break;
784
}
785
valpos++;
786
value = shift = 0;
787
}
788
}
789
}
790
commit();
791
while (outputLine < source.sourceLines) {
792
this.mappings.addByte(59);
793
outputLine++;
794
}
795
}
796
797
toContent(): Buffer {
798
return this.outputBuffer.toBuffer();
799
}
800
801
toSourceMap(sourceRoot?: string): SourceMap {
802
return { version: 3, sourceRoot, sources: this.sources, mappings: this.mappings.toBuffer().toString() };
803
}
804
}
805
806
export interface SourceMap {
807
version: number; // always 3
808
file?: string;
809
sourceRoot?: string;
810
sources: string[];
811
sourcesContent?: string[];
812
names?: string[];
813
mappings: string | Buffer;
814
}
815
816
const charToInteger = Buffer.alloc(256);
817
const integerToChar = Buffer.alloc(64);
818
819
charToInteger.fill(255);
820
821
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split('').forEach((char, i) => {
822
charToInteger[char.charCodeAt(0)] = i;
823
integerToChar[i] = char.charCodeAt(0);
824
});
825
826
class DynamicBuffer {
827
private buffer: Buffer;
828
private size: number;
829
830
constructor() {
831
this.buffer = Buffer.alloc(512);
832
this.size = 0;
833
}
834
835
ensureCapacity(capacity: number) {
836
if (this.buffer.length >= capacity) {
837
return;
838
}
839
const oldBuffer = this.buffer;
840
this.buffer = Buffer.alloc(Math.max(oldBuffer.length * 2, capacity));
841
oldBuffer.copy(this.buffer);
842
}
843
844
addByte(b: number) {
845
this.ensureCapacity(this.size + 1);
846
this.buffer[this.size++] = b;
847
}
848
849
addVLQ(num: number) {
850
let clamped: number;
851
852
if (num < 0) {
853
num = (-num << 1) | 1;
854
} else {
855
num <<= 1;
856
}
857
858
do {
859
clamped = num & 31;
860
num >>= 5;
861
862
if (num > 0) {
863
clamped |= 32;
864
}
865
866
this.addByte(integerToChar[clamped]);
867
} while (num > 0);
868
}
869
870
addString(s: string) {
871
const l = Buffer.byteLength(s);
872
this.ensureCapacity(this.size + l);
873
this.buffer.write(s, this.size);
874
this.size += l;
875
}
876
877
addBuffer(b: Buffer) {
878
this.ensureCapacity(this.size + b.length);
879
b.copy(this.buffer, this.size);
880
this.size += b.length;
881
}
882
883
toBuffer(): Buffer {
884
return this.buffer.slice(0, this.size);
885
}
886
}
887
888
function countNL(b: Buffer): number {
889
let res = 0;
890
for (let i = 0; i < b.length; i++) {
891
if (b[i] === 10) { res++; }
892
}
893
return res;
894
}
895
896
// #endregion
897
898