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