Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/extensions.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 es from 'event-stream';
7
import fs from 'fs';
8
import cp from 'child_process';
9
import glob from 'glob';
10
import gulp from 'gulp';
11
import path from 'path';
12
import crypto from 'crypto';
13
import { Stream } from 'stream';
14
import File from 'vinyl';
15
import { createStatsStream } from './stats';
16
import * as util2 from './util';
17
import filter from 'gulp-filter';
18
import rename from 'gulp-rename';
19
import fancyLog from 'fancy-log';
20
import ansiColors from 'ansi-colors';
21
import buffer from 'gulp-buffer';
22
import * as jsoncParser from 'jsonc-parser';
23
import webpack from 'webpack';
24
import { getProductionDependencies } from './dependencies';
25
import { IExtensionDefinition, getExtensionStream } from './builtInExtensions';
26
import { getVersion } from './getVersion';
27
import { fetchUrls, fetchGithub } from './fetch';
28
const vzip = require('gulp-vinyl-zip');
29
30
const root = path.dirname(path.dirname(__dirname));
31
const commit = getVersion(root);
32
const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`;
33
34
function minifyExtensionResources(input: Stream): Stream {
35
const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true });
36
return input
37
.pipe(jsonFilter)
38
.pipe(buffer())
39
.pipe(es.mapSync((f: File) => {
40
const errors: jsoncParser.ParseError[] = [];
41
const value = jsoncParser.parse(f.contents!.toString('utf8'), errors, { allowTrailingComma: true });
42
if (errors.length === 0) {
43
// file parsed OK => just stringify to drop whitespace and comments
44
f.contents = Buffer.from(JSON.stringify(value));
45
}
46
return f;
47
}))
48
.pipe(jsonFilter.restore);
49
}
50
51
function updateExtensionPackageJSON(input: Stream, update: (data: any) => any): Stream {
52
const packageJsonFilter = filter('extensions/*/package.json', { restore: true });
53
return input
54
.pipe(packageJsonFilter)
55
.pipe(buffer())
56
.pipe(es.mapSync((f: File) => {
57
const data = JSON.parse(f.contents!.toString('utf8'));
58
f.contents = Buffer.from(JSON.stringify(update(data)));
59
return f;
60
}))
61
.pipe(packageJsonFilter.restore);
62
}
63
64
function fromLocal(extensionPath: string, forWeb: boolean, disableMangle: boolean): Stream {
65
66
const webpackConfigFileName = forWeb
67
? `extension-browser.webpack.config.js`
68
: `extension.webpack.config.js`;
69
70
const isWebPacked = fs.existsSync(path.join(extensionPath, webpackConfigFileName));
71
let input = isWebPacked
72
? fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle)
73
: fromLocalNormal(extensionPath);
74
75
if (isWebPacked) {
76
input = updateExtensionPackageJSON(input, (data: any) => {
77
delete data.scripts;
78
delete data.dependencies;
79
delete data.devDependencies;
80
if (data.main) {
81
data.main = data.main.replace('/out/', '/dist/');
82
}
83
return data;
84
});
85
}
86
87
return input;
88
}
89
90
91
function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, disableMangle: boolean): Stream {
92
const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce');
93
const webpack = require('webpack');
94
const webpackGulp = require('webpack-stream');
95
const result = es.through();
96
97
const packagedDependencies: string[] = [];
98
const packageJsonConfig = require(path.join(extensionPath, 'package.json'));
99
if (packageJsonConfig.dependencies) {
100
const webpackRootConfig = require(path.join(extensionPath, webpackConfigFileName)).default;
101
for (const key in webpackRootConfig.externals) {
102
if (key in packageJsonConfig.dependencies) {
103
packagedDependencies.push(key);
104
}
105
}
106
}
107
108
// TODO: add prune support based on packagedDependencies to vsce.PackageManager.Npm similar
109
// to vsce.PackageManager.Yarn.
110
// A static analysis showed there are no webpack externals that are dependencies of the current
111
// local extensions so we can use the vsce.PackageManager.None config to ignore dependencies list
112
// as a temporary workaround.
113
vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.None, packagedDependencies }).then(fileNames => {
114
const files = fileNames
115
.map(fileName => path.join(extensionPath, fileName))
116
.map(filePath => new File({
117
path: filePath,
118
stat: fs.statSync(filePath),
119
base: extensionPath,
120
contents: fs.createReadStream(filePath) as any
121
}));
122
123
// check for a webpack configuration files, then invoke webpack
124
// and merge its output with the files stream.
125
const webpackConfigLocations = (<string[]>glob.sync(
126
path.join(extensionPath, '**', webpackConfigFileName),
127
{ ignore: ['**/node_modules'] }
128
));
129
130
const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => {
131
132
const webpackDone = (err: any, stats: any) => {
133
fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`);
134
if (err) {
135
result.emit('error', err);
136
}
137
const { compilation } = stats;
138
if (compilation.errors.length > 0) {
139
result.emit('error', compilation.errors.join('\n'));
140
}
141
if (compilation.warnings.length > 0) {
142
result.emit('error', compilation.warnings.join('\n'));
143
}
144
};
145
146
const exportedConfig = require(webpackConfigPath).default;
147
return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => {
148
const webpackConfig = {
149
...config,
150
...{ mode: 'production' }
151
};
152
if (disableMangle) {
153
if (Array.isArray(config.module.rules)) {
154
for (const rule of config.module.rules) {
155
if (Array.isArray(rule.use)) {
156
for (const use of rule.use) {
157
if (String(use.loader).endsWith('mangle-loader.js')) {
158
use.options.disabled = true;
159
}
160
}
161
}
162
}
163
}
164
}
165
const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path);
166
167
return webpackGulp(webpackConfig, webpack, webpackDone)
168
.pipe(es.through(function (data) {
169
data.stat = data.stat || {};
170
data.base = extensionPath;
171
this.emit('data', data);
172
}))
173
.pipe(es.through(function (data: File) {
174
// source map handling:
175
// * rewrite sourceMappingURL
176
// * save to disk so that upload-task picks this up
177
if (path.extname(data.basename) === '.js') {
178
const contents = (<Buffer>data.contents).toString('utf8');
179
data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) {
180
return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`;
181
}), 'utf8');
182
}
183
184
this.emit('data', data);
185
}));
186
});
187
});
188
189
es.merge(...webpackStreams, es.readArray(files))
190
// .pipe(es.through(function (data) {
191
// // debug
192
// console.log('out', data.path, data.contents.length);
193
// this.emit('data', data);
194
// }))
195
.pipe(result);
196
197
}).catch(err => {
198
console.error(extensionPath);
199
console.error(packagedDependencies);
200
result.emit('error', err);
201
});
202
203
return result.pipe(createStatsStream(path.basename(extensionPath)));
204
}
205
206
function fromLocalNormal(extensionPath: string): Stream {
207
const vsce = require('@vscode/vsce') as typeof import('@vscode/vsce');
208
const result = es.through();
209
210
vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.Npm })
211
.then(fileNames => {
212
const files = fileNames
213
.map(fileName => path.join(extensionPath, fileName))
214
.map(filePath => new File({
215
path: filePath,
216
stat: fs.statSync(filePath),
217
base: extensionPath,
218
contents: fs.createReadStream(filePath) as any
219
}));
220
221
es.readArray(files).pipe(result);
222
})
223
.catch(err => result.emit('error', err));
224
225
return result.pipe(createStatsStream(path.basename(extensionPath)));
226
}
227
228
const userAgent = 'VSCode Build';
229
const baseHeaders = {
230
'X-Market-Client-Id': 'VSCode Build',
231
'User-Agent': userAgent,
232
'X-Market-User-Id': '291C1CD0-051A-4123-9B4B-30D60EF52EE2',
233
};
234
235
export function fromMarketplace(serviceUrl: string, { name: extensionName, version, sha256, metadata }: IExtensionDefinition): Stream {
236
const json = require('gulp-json-editor') as typeof import('gulp-json-editor');
237
238
const [publisher, name] = extensionName.split('.');
239
const url = `${serviceUrl}/publishers/${publisher}/vsextensions/${name}/${version}/vspackage`;
240
241
fancyLog('Downloading extension:', ansiColors.yellow(`${extensionName}@${version}`), '...');
242
243
const packageJsonFilter = filter('package.json', { restore: true });
244
245
return fetchUrls('', {
246
base: url,
247
nodeFetchOptions: {
248
headers: baseHeaders
249
},
250
checksumSha256: sha256
251
})
252
.pipe(vzip.src())
253
.pipe(filter('extension/**'))
254
.pipe(rename(p => p.dirname = p.dirname!.replace(/^extension\/?/, '')))
255
.pipe(packageJsonFilter)
256
.pipe(buffer())
257
.pipe(json({ __metadata: metadata }))
258
.pipe(packageJsonFilter.restore);
259
}
260
261
export function fromVsix(vsixPath: string, { name: extensionName, version, sha256, metadata }: IExtensionDefinition): Stream {
262
const json = require('gulp-json-editor') as typeof import('gulp-json-editor');
263
264
fancyLog('Using local VSIX for extension:', ansiColors.yellow(`${extensionName}@${version}`), '...');
265
266
const packageJsonFilter = filter('package.json', { restore: true });
267
268
return gulp.src(vsixPath)
269
.pipe(buffer())
270
.pipe(es.mapSync((f: File) => {
271
const hash = crypto.createHash('sha256');
272
hash.update(f.contents as Buffer);
273
const checksum = hash.digest('hex');
274
if (checksum !== sha256) {
275
throw new Error(`Checksum mismatch for ${vsixPath} (expected ${sha256}, actual ${checksum}))`);
276
}
277
return f;
278
}))
279
.pipe(vzip.src())
280
.pipe(filter('extension/**'))
281
.pipe(rename(p => p.dirname = p.dirname!.replace(/^extension\/?/, '')))
282
.pipe(packageJsonFilter)
283
.pipe(buffer())
284
.pipe(json({ __metadata: metadata }))
285
.pipe(packageJsonFilter.restore);
286
}
287
288
289
export function fromGithub({ name, version, repo, sha256, metadata }: IExtensionDefinition): Stream {
290
const json = require('gulp-json-editor') as typeof import('gulp-json-editor');
291
292
fancyLog('Downloading extension from GH:', ansiColors.yellow(`${name}@${version}`), '...');
293
294
const packageJsonFilter = filter('package.json', { restore: true });
295
296
return fetchGithub(new URL(repo).pathname, {
297
version,
298
name: name => name.endsWith('.vsix'),
299
checksumSha256: sha256
300
})
301
.pipe(buffer())
302
.pipe(vzip.src())
303
.pipe(filter('extension/**'))
304
.pipe(rename(p => p.dirname = p.dirname!.replace(/^extension\/?/, '')))
305
.pipe(packageJsonFilter)
306
.pipe(buffer())
307
.pipe(json({ __metadata: metadata }))
308
.pipe(packageJsonFilter.restore);
309
}
310
311
/**
312
* All extensions that are known to have some native component and thus must be built on the
313
* platform that is being built.
314
*/
315
const nativeExtensions = [
316
'microsoft-authentication',
317
];
318
319
const excludedExtensions = [
320
'vscode-api-tests',
321
'vscode-colorize-tests',
322
'vscode-colorize-perf-tests',
323
'vscode-test-resolver',
324
'ms-vscode.node-debug',
325
'ms-vscode.node-debug2',
326
];
327
328
const marketplaceWebExtensionsExclude = new Set([
329
'ms-vscode.node-debug',
330
'ms-vscode.node-debug2',
331
'ms-vscode.js-debug-companion',
332
'ms-vscode.js-debug',
333
'ms-vscode.vscode-js-profile-table'
334
]);
335
336
const productJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8'));
337
const builtInExtensions: IExtensionDefinition[] = productJson.builtInExtensions || [];
338
const webBuiltInExtensions: IExtensionDefinition[] = productJson.webBuiltInExtensions || [];
339
340
type ExtensionKind = 'ui' | 'workspace' | 'web';
341
interface IExtensionManifest {
342
main?: string;
343
browser?: string;
344
extensionKind?: ExtensionKind | ExtensionKind[];
345
extensionPack?: string[];
346
extensionDependencies?: string[];
347
contributes?: { [id: string]: any };
348
}
349
/**
350
* Loosely based on `getExtensionKind` from `src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts`
351
*/
352
function isWebExtension(manifest: IExtensionManifest): boolean {
353
if (Boolean(manifest.browser)) {
354
return true;
355
}
356
if (Boolean(manifest.main)) {
357
return false;
358
}
359
// neither browser nor main
360
if (typeof manifest.extensionKind !== 'undefined') {
361
const extensionKind = Array.isArray(manifest.extensionKind) ? manifest.extensionKind : [manifest.extensionKind];
362
if (extensionKind.indexOf('web') >= 0) {
363
return true;
364
}
365
}
366
if (typeof manifest.contributes !== 'undefined') {
367
for (const id of ['debuggers', 'terminal', 'typescriptServerPlugins']) {
368
if (manifest.contributes.hasOwnProperty(id)) {
369
return false;
370
}
371
}
372
}
373
return true;
374
}
375
376
/**
377
* Package local extensions that are known to not have native dependencies. Mutually exclusive to {@link packageNativeLocalExtensionsStream}.
378
* @param forWeb build the extensions that have web targets
379
* @param disableMangle disable the mangler
380
* @returns a stream
381
*/
382
export function packageNonNativeLocalExtensionsStream(forWeb: boolean, disableMangle: boolean): Stream {
383
return doPackageLocalExtensionsStream(forWeb, disableMangle, false);
384
}
385
386
/**
387
* Package local extensions that are known to have native dependencies. Mutually exclusive to {@link packageNonNativeLocalExtensionsStream}.
388
* @note it's possible that the extension does not have native dependencies for the current platform, especially if building for the web,
389
* but we simplify the logic here by having a flat list of extensions (See {@link nativeExtensions}) that are known to have native
390
* dependencies on some platform and thus should be packaged on the platform that they are building for.
391
* @param forWeb build the extensions that have web targets
392
* @param disableMangle disable the mangler
393
* @returns a stream
394
*/
395
export function packageNativeLocalExtensionsStream(forWeb: boolean, disableMangle: boolean): Stream {
396
return doPackageLocalExtensionsStream(forWeb, disableMangle, true);
397
}
398
399
/**
400
* Package all the local extensions... both those that are known to have native dependencies and those that are not.
401
* @param forWeb build the extensions that have web targets
402
* @param disableMangle disable the mangler
403
* @returns a stream
404
*/
405
export function packageAllLocalExtensionsStream(forWeb: boolean, disableMangle: boolean): Stream {
406
return es.merge([
407
packageNonNativeLocalExtensionsStream(forWeb, disableMangle),
408
packageNativeLocalExtensionsStream(forWeb, disableMangle)
409
]);
410
}
411
412
/**
413
* @param forWeb build the extensions that have web targets
414
* @param disableMangle disable the mangler
415
* @param native build the extensions that are marked as having native dependencies
416
*/
417
function doPackageLocalExtensionsStream(forWeb: boolean, disableMangle: boolean, native: boolean): Stream {
418
const nativeExtensionsSet = new Set(nativeExtensions);
419
const localExtensionsDescriptions = (
420
(<string[]>glob.sync('extensions/*/package.json'))
421
.map(manifestPath => {
422
const absoluteManifestPath = path.join(root, manifestPath);
423
const extensionPath = path.dirname(path.join(root, manifestPath));
424
const extensionName = path.basename(extensionPath);
425
return { name: extensionName, path: extensionPath, manifestPath: absoluteManifestPath };
426
})
427
.filter(({ name }) => native ? nativeExtensionsSet.has(name) : !nativeExtensionsSet.has(name))
428
.filter(({ name }) => excludedExtensions.indexOf(name) === -1)
429
.filter(({ name }) => builtInExtensions.every(b => b.name !== name))
430
.filter(({ manifestPath }) => (forWeb ? isWebExtension(require(manifestPath)) : true))
431
);
432
const localExtensionsStream = minifyExtensionResources(
433
es.merge(
434
...localExtensionsDescriptions.map(extension => {
435
return fromLocal(extension.path, forWeb, disableMangle)
436
.pipe(rename(p => p.dirname = `extensions/${extension.name}/${p.dirname}`));
437
})
438
)
439
);
440
441
let result: Stream;
442
if (forWeb) {
443
result = localExtensionsStream;
444
} else {
445
// also include shared production node modules
446
const productionDependencies = getProductionDependencies('extensions/');
447
const dependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat();
448
449
result = es.merge(
450
localExtensionsStream,
451
gulp.src(dependenciesSrc, { base: '.' })
452
.pipe(util2.cleanNodeModules(path.join(root, 'build', '.moduleignore')))
453
.pipe(util2.cleanNodeModules(path.join(root, 'build', `.moduleignore.${process.platform}`))));
454
}
455
456
return (
457
result
458
.pipe(util2.setExecutableBit(['**/*.sh']))
459
);
460
}
461
462
export function packageMarketplaceExtensionsStream(forWeb: boolean): Stream {
463
const marketplaceExtensionsDescriptions = [
464
...builtInExtensions.filter(({ name }) => (forWeb ? !marketplaceWebExtensionsExclude.has(name) : true)),
465
...(forWeb ? webBuiltInExtensions : [])
466
];
467
const marketplaceExtensionsStream = minifyExtensionResources(
468
es.merge(
469
...marketplaceExtensionsDescriptions
470
.map(extension => {
471
const src = getExtensionStream(extension).pipe(rename(p => p.dirname = `extensions/${p.dirname}`));
472
return updateExtensionPackageJSON(src, (data: any) => {
473
delete data.scripts;
474
delete data.dependencies;
475
delete data.devDependencies;
476
return data;
477
});
478
})
479
)
480
);
481
482
return (
483
marketplaceExtensionsStream
484
.pipe(util2.setExecutableBit(['**/*.sh']))
485
);
486
}
487
488
export interface IScannedBuiltinExtension {
489
extensionPath: string;
490
packageJSON: any;
491
packageNLS?: any;
492
readmePath?: string;
493
changelogPath?: string;
494
}
495
496
export function scanBuiltinExtensions(extensionsRoot: string, exclude: string[] = []): IScannedBuiltinExtension[] {
497
const scannedExtensions: IScannedBuiltinExtension[] = [];
498
499
try {
500
const extensionsFolders = fs.readdirSync(extensionsRoot);
501
for (const extensionFolder of extensionsFolders) {
502
if (exclude.indexOf(extensionFolder) >= 0) {
503
continue;
504
}
505
const packageJSONPath = path.join(extensionsRoot, extensionFolder, 'package.json');
506
if (!fs.existsSync(packageJSONPath)) {
507
continue;
508
}
509
const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath).toString('utf8'));
510
if (!isWebExtension(packageJSON)) {
511
continue;
512
}
513
const children = fs.readdirSync(path.join(extensionsRoot, extensionFolder));
514
const packageNLSPath = children.filter(child => child === 'package.nls.json')[0];
515
const packageNLS = packageNLSPath ? JSON.parse(fs.readFileSync(path.join(extensionsRoot, extensionFolder, packageNLSPath)).toString()) : undefined;
516
const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
517
const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
518
519
scannedExtensions.push({
520
extensionPath: extensionFolder,
521
packageJSON,
522
packageNLS,
523
readmePath: readme ? path.join(extensionFolder, readme) : undefined,
524
changelogPath: changelog ? path.join(extensionFolder, changelog) : undefined,
525
});
526
}
527
return scannedExtensions;
528
} catch (ex) {
529
return scannedExtensions;
530
}
531
}
532
533
export function translatePackageJSON(packageJSON: string, packageNLSPath: string) {
534
interface NLSFormat {
535
[key: string]: string | { message: string; comment: string[] };
536
}
537
const CharCode_PC = '%'.charCodeAt(0);
538
const packageNls: NLSFormat = JSON.parse(fs.readFileSync(packageNLSPath).toString());
539
const translate = (obj: any) => {
540
for (const key in obj) {
541
const val = obj[key];
542
if (Array.isArray(val)) {
543
val.forEach(translate);
544
} else if (val && typeof val === 'object') {
545
translate(val);
546
} else if (typeof val === 'string' && val.charCodeAt(0) === CharCode_PC && val.charCodeAt(val.length - 1) === CharCode_PC) {
547
const translated = packageNls[val.substr(1, val.length - 2)];
548
if (translated) {
549
obj[key] = typeof translated === 'string' ? translated : (typeof translated.message === 'string' ? translated.message : val);
550
}
551
}
552
}
553
};
554
translate(packageJSON);
555
return packageJSON;
556
}
557
558
const extensionsPath = path.join(root, 'extensions');
559
560
// Additional projects to run esbuild on. These typically build code for webviews
561
const esbuildMediaScripts = [
562
'ipynb/esbuild.mjs',
563
'markdown-language-features/esbuild-notebook.mjs',
564
'markdown-language-features/esbuild-preview.mjs',
565
'markdown-math/esbuild.mjs',
566
'mermaid-chat-features/esbuild-chat-webview.mjs',
567
'notebook-renderers/esbuild.mjs',
568
'simple-browser/esbuild-preview.mjs',
569
];
570
571
export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string; outputRoot?: string }[]) {
572
const webpack = require('webpack') as typeof import('webpack');
573
574
const webpackConfigs: webpack.Configuration[] = [];
575
576
for (const { configPath, outputRoot } of webpackConfigLocations) {
577
const configOrFnOrArray = require(configPath).default;
578
function addConfig(configOrFnOrArray: webpack.Configuration | ((env: unknown, args: unknown) => webpack.Configuration) | webpack.Configuration[]) {
579
for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) {
580
const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn;
581
if (outputRoot) {
582
config.output!.path = path.join(outputRoot, path.relative(path.dirname(configPath), config.output!.path!));
583
}
584
webpackConfigs.push(config);
585
}
586
}
587
addConfig(configOrFnOrArray);
588
}
589
590
function reporter(fullStats: any) {
591
if (Array.isArray(fullStats.children)) {
592
for (const stats of fullStats.children) {
593
const outputPath = stats.outputPath;
594
if (outputPath) {
595
const relativePath = path.relative(extensionsPath, outputPath).replace(/\\/g, '/');
596
const match = relativePath.match(/[^\/]+(\/server|\/client)?/);
597
fancyLog(`Finished ${ansiColors.green(taskName)} ${ansiColors.cyan(match![0])} with ${stats.errors.length} errors.`);
598
}
599
if (Array.isArray(stats.errors)) {
600
stats.errors.forEach((error: any) => {
601
fancyLog.error(error);
602
});
603
}
604
if (Array.isArray(stats.warnings)) {
605
stats.warnings.forEach((warning: any) => {
606
fancyLog.warn(warning);
607
});
608
}
609
}
610
}
611
}
612
return new Promise<void>((resolve, reject) => {
613
if (isWatch) {
614
webpack(webpackConfigs).watch({}, (err, stats) => {
615
if (err) {
616
reject();
617
} else {
618
reporter(stats?.toJson());
619
}
620
});
621
} else {
622
webpack(webpackConfigs).run((err, stats) => {
623
if (err) {
624
fancyLog.error(err);
625
reject();
626
} else {
627
reporter(stats?.toJson());
628
resolve();
629
}
630
});
631
}
632
});
633
}
634
635
async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { script: string; outputRoot?: string }[]) {
636
function reporter(stdError: string, script: string) {
637
const matches = (stdError || '').match(/\> (.+): error: (.+)?/g);
638
fancyLog(`Finished ${ansiColors.green(taskName)} ${script} with ${matches ? matches.length : 0} errors.`);
639
for (const match of matches || []) {
640
fancyLog.error(match);
641
}
642
}
643
644
const tasks = scripts.map(({ script, outputRoot }) => {
645
return new Promise<void>((resolve, reject) => {
646
const args = [script];
647
if (isWatch) {
648
args.push('--watch');
649
}
650
if (outputRoot) {
651
args.push('--outputRoot', outputRoot);
652
}
653
const proc = cp.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => {
654
if (error) {
655
return reject(error);
656
}
657
reporter(stderr, script);
658
return resolve();
659
});
660
661
proc.stdout!.on('data', (data) => {
662
fancyLog(`${ansiColors.green(taskName)}: ${data.toString('utf8')}`);
663
});
664
});
665
});
666
return Promise.all(tasks);
667
}
668
669
export async function buildExtensionMedia(isWatch: boolean, outputRoot?: string) {
670
return esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({
671
script: path.join(extensionsPath, p),
672
outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined
673
})));
674
}
675
676