Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/util.ts
5221 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 es from 'event-stream';
7
import _debounce from 'debounce';
8
import _filter from 'gulp-filter';
9
import rename from 'gulp-rename';
10
import path from 'path';
11
import fs from 'fs';
12
import _rimraf from 'rimraf';
13
import VinylFile from 'vinyl';
14
import through from 'through';
15
import sm from 'source-map';
16
import { pathToFileURL } from 'url';
17
import ternaryStream from 'ternary-stream';
18
import type { Transform } from 'stream';
19
import * as tar from 'tar';
20
21
const root = path.dirname(path.dirname(import.meta.dirname));
22
23
export interface ICancellationToken {
24
isCancellationRequested(): boolean;
25
}
26
27
const NoCancellationToken: ICancellationToken = { isCancellationRequested: () => false };
28
29
export interface IStreamProvider {
30
(cancellationToken?: ICancellationToken): NodeJS.ReadWriteStream;
31
}
32
33
export function incremental(streamProvider: IStreamProvider, initial: NodeJS.ReadWriteStream, supportsCancellation?: boolean): NodeJS.ReadWriteStream {
34
const input = es.through();
35
const output = es.through();
36
let state = 'idle';
37
let buffer = Object.create(null);
38
39
const token: ICancellationToken | undefined = !supportsCancellation ? undefined : { isCancellationRequested: () => Object.keys(buffer).length > 0 };
40
41
const run = (input: NodeJS.ReadWriteStream, isCancellable: boolean) => {
42
state = 'running';
43
44
const stream = !supportsCancellation ? streamProvider() : streamProvider(isCancellable ? token : NoCancellationToken);
45
46
input
47
.pipe(stream)
48
.pipe(es.through(undefined, () => {
49
state = 'idle';
50
eventuallyRun();
51
}))
52
.pipe(output);
53
};
54
55
if (initial) {
56
run(initial, false);
57
}
58
59
const eventuallyRun = _debounce(() => {
60
const paths = Object.keys(buffer);
61
62
if (paths.length === 0) {
63
return;
64
}
65
66
const data = paths.map(path => buffer[path]);
67
buffer = Object.create(null);
68
run(es.readArray(data), true);
69
}, 500);
70
71
input.on('data', (f: any) => {
72
buffer[f.path] = f;
73
74
if (state === 'idle') {
75
eventuallyRun();
76
}
77
});
78
79
return es.duplex(input, output);
80
}
81
82
export function debounce(task: () => NodeJS.ReadWriteStream, duration = 500): NodeJS.ReadWriteStream {
83
const input = es.through();
84
const output = es.through();
85
let state = 'idle';
86
87
const run = () => {
88
state = 'running';
89
90
task()
91
.pipe(es.through(undefined, () => {
92
const shouldRunAgain = state === 'stale';
93
state = 'idle';
94
95
if (shouldRunAgain) {
96
eventuallyRun();
97
}
98
}))
99
.pipe(output);
100
};
101
102
run();
103
104
const eventuallyRun = _debounce(() => run(), duration);
105
106
input.on('data', () => {
107
if (state === 'idle') {
108
eventuallyRun();
109
} else {
110
state = 'stale';
111
}
112
});
113
114
return es.duplex(input, output);
115
}
116
117
export function fixWin32DirectoryPermissions(): NodeJS.ReadWriteStream {
118
if (!/win32/.test(process.platform)) {
119
return es.through();
120
}
121
122
return es.mapSync<VinylFile, VinylFile>(f => {
123
if (f.stat && f.stat.isDirectory && f.stat.isDirectory()) {
124
f.stat.mode = 16877;
125
}
126
127
return f;
128
});
129
}
130
131
export function setExecutableBit(pattern?: string | string[]): NodeJS.ReadWriteStream {
132
const setBit = es.mapSync<VinylFile, VinylFile>(f => {
133
if (!f.stat) {
134
const stat: Pick<fs.Stats, 'isFile' | 'mode'> = { isFile() { return true; }, mode: 0 };
135
f.stat = stat as fs.Stats;
136
}
137
f.stat!.mode = /* 100755 */ 33261;
138
return f;
139
});
140
141
if (!pattern) {
142
return setBit;
143
}
144
145
const input = es.through();
146
const filter = _filter(pattern, { restore: true });
147
const output = input
148
.pipe(filter)
149
.pipe(setBit)
150
.pipe(filter.restore);
151
152
return es.duplex(input, output);
153
}
154
155
export function toFileUri(filePath: string): string {
156
const match = filePath.match(/^([a-z])\:(.*)$/i);
157
158
if (match) {
159
filePath = '/' + match[1].toUpperCase() + ':' + match[2];
160
}
161
162
return 'file://' + filePath.replace(/\\/g, '/');
163
}
164
165
export function skipDirectories(): NodeJS.ReadWriteStream {
166
return es.mapSync<VinylFile, VinylFile | undefined>(f => {
167
if (!f.isDirectory()) {
168
return f;
169
}
170
});
171
}
172
173
export function cleanNodeModules(rulePath: string): NodeJS.ReadWriteStream {
174
const rules = fs.readFileSync(rulePath, 'utf8')
175
.split(/\r?\n/g)
176
.map(line => line.trim())
177
.filter(line => line && !/^#/.test(line));
178
179
const excludes = rules.filter(line => !/^!/.test(line)).map(line => `!**/node_modules/${line}`);
180
const includes = rules.filter(line => /^!/.test(line)).map(line => `**/node_modules/${line.substr(1)}`);
181
182
const input = es.through();
183
const output = es.merge(
184
input.pipe(_filter(['**', ...excludes])),
185
input.pipe(_filter(includes))
186
);
187
188
return es.duplex(input, output);
189
}
190
191
type FileSourceMap = VinylFile & { sourceMap: sm.RawSourceMap };
192
193
export function loadSourcemaps(): NodeJS.ReadWriteStream {
194
const input = es.through();
195
196
const output = input
197
.pipe(es.map<FileSourceMap, FileSourceMap | undefined>((f, cb): FileSourceMap | undefined => {
198
if (f.sourceMap) {
199
cb(undefined, f);
200
return;
201
}
202
203
if (!f.contents) {
204
cb(undefined, f);
205
return;
206
}
207
208
const contents = (f.contents as Buffer).toString('utf8');
209
const reg = /\/\/# sourceMappingURL=(.*)$/g;
210
let lastMatch: RegExpExecArray | null = null;
211
let match: RegExpExecArray | null = null;
212
213
while (match = reg.exec(contents)) {
214
lastMatch = match;
215
}
216
217
if (!lastMatch) {
218
f.sourceMap = {
219
version: '3',
220
names: [],
221
mappings: '',
222
sources: [f.relative.replace(/\\/g, '/')],
223
sourcesContent: [contents]
224
};
225
226
cb(undefined, f);
227
return;
228
}
229
230
f.contents = Buffer.from(contents.replace(/\/\/# sourceMappingURL=(.*)$/g, ''), 'utf8');
231
232
fs.readFile(path.join(path.dirname(f.path), lastMatch[1]), 'utf8', (err, contents) => {
233
if (err) { return cb(err); }
234
235
f.sourceMap = JSON.parse(contents);
236
cb(undefined, f);
237
});
238
}));
239
240
return es.duplex(input, output);
241
}
242
243
export function stripSourceMappingURL(): NodeJS.ReadWriteStream {
244
const input = es.through();
245
246
const output = input
247
.pipe(es.mapSync<VinylFile, VinylFile>(f => {
248
const contents = (f.contents as Buffer).toString('utf8');
249
f.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8');
250
return f;
251
}));
252
253
return es.duplex(input, output);
254
}
255
256
/** Splits items in the stream based on the predicate, sending them to onTrue if true, or onFalse otherwise */
257
export function $if(test: boolean | ((f: VinylFile) => boolean), onTrue: NodeJS.ReadWriteStream, onFalse: NodeJS.ReadWriteStream = es.through()) {
258
if (typeof test === 'boolean') {
259
return test ? onTrue : onFalse;
260
}
261
262
return ternaryStream(test, onTrue, onFalse);
263
}
264
265
/** Operator that appends the js files' original path a sourceURL, so debug locations map */
266
export function appendOwnPathSourceURL(): NodeJS.ReadWriteStream {
267
const input = es.through();
268
269
const output = input
270
.pipe(es.mapSync<VinylFile, VinylFile>(f => {
271
if (!(f.contents instanceof Buffer)) {
272
throw new Error(`contents of ${f.path} are not a buffer`);
273
}
274
275
f.contents = Buffer.concat([f.contents, Buffer.from(`\n//# sourceURL=${pathToFileURL(f.path)}`)]);
276
return f;
277
}));
278
279
return es.duplex(input, output);
280
}
281
282
export function rewriteSourceMappingURL(sourceMappingURLBase: string): NodeJS.ReadWriteStream {
283
const input = es.through();
284
285
const output = input
286
.pipe(es.mapSync<VinylFile, VinylFile>(f => {
287
const contents = (f.contents as Buffer).toString('utf8');
288
const str = `//# sourceMappingURL=${sourceMappingURLBase}/${path.dirname(f.relative).replace(/\\/g, '/')}/$1`;
289
f.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, str));
290
return f;
291
}));
292
293
return es.duplex(input, output);
294
}
295
296
export function rimraf(dir: string): () => Promise<void> {
297
const result = () => new Promise<void>((c, e) => {
298
let retries = 0;
299
300
const retry = () => {
301
_rimraf(dir, { maxBusyTries: 1 }, (err: any) => {
302
if (!err) {
303
return c();
304
}
305
306
if (err.code === 'ENOTEMPTY' && ++retries < 5) {
307
return setTimeout(() => retry(), 10);
308
}
309
310
return e(err);
311
});
312
};
313
314
retry();
315
});
316
317
result.taskName = `clean-${path.basename(dir).toLowerCase()}`;
318
return result;
319
}
320
321
function _rreaddir(dirPath: string, prepend: string, result: string[]): void {
322
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
323
for (const entry of entries) {
324
if (entry.isDirectory()) {
325
_rreaddir(path.join(dirPath, entry.name), `${prepend}/${entry.name}`, result);
326
} else {
327
result.push(`${prepend}/${entry.name}`);
328
}
329
}
330
}
331
332
export function rreddir(dirPath: string): string[] {
333
const result: string[] = [];
334
_rreaddir(dirPath, '', result);
335
return result;
336
}
337
338
export function ensureDir(dirPath: string): void {
339
if (fs.existsSync(dirPath)) {
340
return;
341
}
342
ensureDir(path.dirname(dirPath));
343
fs.mkdirSync(dirPath);
344
}
345
346
export function rebase(count: number): NodeJS.ReadWriteStream {
347
return rename(f => {
348
const parts = f.dirname ? f.dirname.split(/[\/\\]/) : [];
349
f.dirname = parts.slice(count).join(path.sep);
350
});
351
}
352
353
export interface FilterStream extends NodeJS.ReadWriteStream {
354
restore: through.ThroughStream;
355
}
356
357
export function filter(fn: (data: any) => boolean): FilterStream {
358
const result = es.through(function (data) {
359
if (fn(data)) {
360
this.emit('data', data);
361
} else {
362
result.restore.push(data);
363
}
364
}) as unknown as FilterStream;
365
366
result.restore = es.through();
367
return result;
368
}
369
370
export function streamToPromise(stream: NodeJS.ReadWriteStream): Promise<void> {
371
return new Promise((c, e) => {
372
stream.on('error', err => e(err));
373
stream.on('end', () => c());
374
});
375
}
376
377
export function getElectronVersion(): Record<string, string> {
378
const npmrc = fs.readFileSync(path.join(root, '.npmrc'), 'utf8');
379
const electronVersion = /^target="(.*)"$/m.exec(npmrc)![1];
380
const msBuildId = /^ms_build_id="(.*)"$/m.exec(npmrc)![1];
381
return { electronVersion, msBuildId };
382
}
383
384
export class VinylStat implements fs.Stats {
385
386
readonly dev: number;
387
readonly ino: number;
388
readonly mode: number;
389
readonly nlink: number;
390
readonly uid: number;
391
readonly gid: number;
392
readonly rdev: number;
393
readonly size: number;
394
readonly blksize: number;
395
readonly blocks: number;
396
readonly atimeMs: number;
397
readonly mtimeMs: number;
398
readonly ctimeMs: number;
399
readonly birthtimeMs: number;
400
readonly atime: Date;
401
readonly mtime: Date;
402
readonly ctime: Date;
403
readonly birthtime: Date;
404
405
constructor(stat: Partial<fs.Stats>) {
406
this.dev = stat.dev ?? 0;
407
this.ino = stat.ino ?? 0;
408
this.mode = stat.mode ?? 0;
409
this.nlink = stat.nlink ?? 0;
410
this.uid = stat.uid ?? 0;
411
this.gid = stat.gid ?? 0;
412
this.rdev = stat.rdev ?? 0;
413
this.size = stat.size ?? 0;
414
this.blksize = stat.blksize ?? 0;
415
this.blocks = stat.blocks ?? 0;
416
this.atimeMs = stat.atimeMs ?? 0;
417
this.mtimeMs = stat.mtimeMs ?? 0;
418
this.ctimeMs = stat.ctimeMs ?? 0;
419
this.birthtimeMs = stat.birthtimeMs ?? 0;
420
this.atime = stat.atime ?? new Date(0);
421
this.mtime = stat.mtime ?? new Date(0);
422
this.ctime = stat.ctime ?? new Date(0);
423
this.birthtime = stat.birthtime ?? new Date(0);
424
}
425
426
isFile(): boolean { return true; }
427
isDirectory(): boolean { return false; }
428
isBlockDevice(): boolean { return false; }
429
isCharacterDevice(): boolean { return false; }
430
isSymbolicLink(): boolean { return false; }
431
isFIFO(): boolean { return false; }
432
isSocket(): boolean { return false; }
433
}
434
435
export function untar(): Transform {
436
return es.through(function (this: through.ThroughStream, f: VinylFile) {
437
if (!f.contents || !Buffer.isBuffer(f.contents)) {
438
this.emit('error', new Error('Expected file with Buffer contents'));
439
return;
440
}
441
442
const self = this;
443
const parser = new tar.Parser();
444
445
parser.on('entry', (entry: tar.ReadEntry) => {
446
if (entry.type === 'File') {
447
const chunks: Buffer[] = [];
448
entry.on('data', (chunk: Buffer) => chunks.push(chunk));
449
entry.on('end', () => {
450
const file = new VinylFile({
451
path: entry.path,
452
contents: Buffer.concat(chunks),
453
stat: new VinylStat({
454
mode: entry.mode,
455
mtime: entry.mtime,
456
size: entry.size
457
})
458
});
459
self.emit('data', file);
460
});
461
} else {
462
entry.resume();
463
}
464
});
465
466
parser.on('error', (err: Error) => self.emit('error', err));
467
parser.end(f.contents);
468
}) as Transform;
469
}
470
471