Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/optimize.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 gulp from 'gulp';
8
import filter from 'gulp-filter';
9
import path from 'path';
10
import fs from 'fs';
11
import pump from 'pump';
12
import VinylFile from 'vinyl';
13
import * as bundle from './bundle';
14
import esbuild from 'esbuild';
15
import sourcemaps from 'gulp-sourcemaps';
16
import fancyLog from 'fancy-log';
17
import ansiColors from 'ansi-colors';
18
19
const REPO_ROOT_PATH = path.join(__dirname, '../..');
20
21
export interface IBundleESMTaskOpts {
22
/**
23
* The folder to read files from.
24
*/
25
src: string;
26
/**
27
* The entry points to bundle.
28
*/
29
entryPoints: Array<bundle.IEntryPoint | string>;
30
/**
31
* Other resources to consider (svg, etc.)
32
*/
33
resources?: string[];
34
/**
35
* File contents interceptor for a given path.
36
*/
37
fileContentMapper?: (path: string) => ((contents: string) => Promise<string> | string) | undefined;
38
/**
39
* Allows to skip the removal of TS boilerplate. Use this when
40
* the entry point is small and the overhead of removing the
41
* boilerplate makes the file larger in the end.
42
*/
43
skipTSBoilerplateRemoval?: (entryPointName: string) => boolean;
44
}
45
46
const DEFAULT_FILE_HEADER = [
47
'/*!--------------------------------------------------------',
48
' * Copyright (C) Microsoft Corporation. All rights reserved.',
49
' *--------------------------------------------------------*/'
50
].join('\n');
51
52
function bundleESMTask(opts: IBundleESMTaskOpts): NodeJS.ReadWriteStream {
53
const resourcesStream = es.through(); // this stream will contain the resources
54
const bundlesStream = es.through(); // this stream will contain the bundled files
55
56
const entryPoints = opts.entryPoints.map(entryPoint => {
57
if (typeof entryPoint === 'string') {
58
return { name: path.parse(entryPoint).name };
59
}
60
61
return entryPoint;
62
});
63
64
const bundleAsync = async () => {
65
const files: VinylFile[] = [];
66
const tasks: Promise<any>[] = [];
67
68
for (const entryPoint of entryPoints) {
69
fancyLog(`Bundled entry point: ${ansiColors.yellow(entryPoint.name)}...`);
70
71
// support for 'dest' via esbuild#in/out
72
const dest = entryPoint.dest?.replace(/\.[^/.]+$/, '') ?? entryPoint.name;
73
74
// banner contents
75
const banner = {
76
js: DEFAULT_FILE_HEADER,
77
css: DEFAULT_FILE_HEADER
78
};
79
80
// TS Boilerplate
81
if (!opts.skipTSBoilerplateRemoval?.(entryPoint.name)) {
82
const tslibPath = path.join(require.resolve('tslib'), '../tslib.es6.js');
83
banner.js += await fs.promises.readFile(tslibPath, 'utf-8');
84
}
85
86
const contentsMapper: esbuild.Plugin = {
87
name: 'contents-mapper',
88
setup(build) {
89
build.onLoad({ filter: /\.js$/ }, async ({ path }) => {
90
const contents = await fs.promises.readFile(path, 'utf-8');
91
92
// TS Boilerplate
93
let newContents: string;
94
if (!opts.skipTSBoilerplateRemoval?.(entryPoint.name)) {
95
newContents = bundle.removeAllTSBoilerplate(contents);
96
} else {
97
newContents = contents;
98
}
99
100
// File Content Mapper
101
const mapper = opts.fileContentMapper?.(path.replace(/\\/g, '/'));
102
if (mapper) {
103
newContents = await mapper(newContents);
104
}
105
106
return { contents: newContents };
107
});
108
}
109
};
110
111
const externalOverride: esbuild.Plugin = {
112
name: 'external-override',
113
setup(build) {
114
// We inline selected modules that are we depend on on startup without
115
// a conditional `await import(...)` by hooking into the resolution.
116
build.onResolve({ filter: /^minimist$/ }, () => {
117
return { path: path.join(REPO_ROOT_PATH, 'node_modules', 'minimist', 'index.js'), external: false };
118
});
119
},
120
};
121
122
const task = esbuild.build({
123
bundle: true,
124
packages: 'external', // "external all the things", see https://esbuild.github.io/api/#packages
125
platform: 'neutral', // makes esm
126
format: 'esm',
127
sourcemap: 'external',
128
plugins: [contentsMapper, externalOverride],
129
target: ['es2022'],
130
loader: {
131
'.ttf': 'file',
132
'.svg': 'file',
133
'.png': 'file',
134
'.sh': 'file',
135
},
136
assetNames: 'media/[name]', // moves media assets into a sub-folder "media"
137
banner: entryPoint.name === 'vs/workbench/workbench.web.main' ? undefined : banner, // TODO@esm remove line when we stop supporting web-amd-esm-bridge
138
entryPoints: [
139
{
140
in: path.join(REPO_ROOT_PATH, opts.src, `${entryPoint.name}.js`),
141
out: dest,
142
}
143
],
144
outdir: path.join(REPO_ROOT_PATH, opts.src),
145
write: false, // enables res.outputFiles
146
metafile: true, // enables res.metafile
147
// minify: NOT enabled because we have a separate minify task that takes care of the TSLib banner as well
148
}).then(res => {
149
for (const file of res.outputFiles) {
150
let sourceMapFile: esbuild.OutputFile | undefined = undefined;
151
if (file.path.endsWith('.js')) {
152
sourceMapFile = res.outputFiles.find(f => f.path === `${file.path}.map`);
153
}
154
155
const fileProps = {
156
contents: Buffer.from(file.contents),
157
sourceMap: sourceMapFile ? JSON.parse(sourceMapFile.text) : undefined, // support gulp-sourcemaps
158
path: file.path,
159
base: path.join(REPO_ROOT_PATH, opts.src)
160
};
161
files.push(new VinylFile(fileProps));
162
}
163
});
164
165
tasks.push(task);
166
}
167
168
await Promise.all(tasks);
169
return { files };
170
};
171
172
bundleAsync().then((output) => {
173
174
// bundle output (JS, CSS, SVG...)
175
es.readArray(output.files).pipe(bundlesStream);
176
177
// forward all resources
178
gulp.src(opts.resources ?? [], { base: `${opts.src}`, allowEmpty: true }).pipe(resourcesStream);
179
});
180
181
const result = es.merge(
182
bundlesStream,
183
resourcesStream
184
);
185
186
return result
187
.pipe(sourcemaps.write('./', {
188
sourceRoot: undefined,
189
addComment: true,
190
includeContent: true
191
}));
192
}
193
194
export interface IBundleESMTaskOpts {
195
/**
196
* Destination folder for the bundled files.
197
*/
198
out: string;
199
/**
200
* Bundle ESM modules (using esbuild).
201
*/
202
esm: IBundleESMTaskOpts;
203
}
204
205
export function bundleTask(opts: IBundleESMTaskOpts): () => NodeJS.ReadWriteStream {
206
return function () {
207
return bundleESMTask(opts.esm).pipe(gulp.dest(opts.out));
208
};
209
}
210
211
export function minifyTask(src: string, sourceMapBaseUrl?: string): (cb: any) => void {
212
const sourceMappingURL = sourceMapBaseUrl ? ((f: any) => `${sourceMapBaseUrl}/${f.relative}.map`) : undefined;
213
214
return cb => {
215
const svgmin = require('gulp-svgmin') as typeof import('gulp-svgmin');
216
217
const esbuildFilter = filter('**/*.{js,css}', { restore: true });
218
const svgFilter = filter('**/*.svg', { restore: true });
219
220
pump(
221
gulp.src([src + '/**', '!' + src + '/**/*.map']),
222
esbuildFilter,
223
sourcemaps.init({ loadMaps: true }),
224
es.map((f: any, cb) => {
225
esbuild.build({
226
entryPoints: [f.path],
227
minify: true,
228
sourcemap: 'external',
229
outdir: '.',
230
packages: 'external', // "external all the things", see https://esbuild.github.io/api/#packages
231
platform: 'neutral', // makes esm
232
target: ['es2022'],
233
write: false,
234
}).then(res => {
235
const jsOrCSSFile = res.outputFiles.find(f => /\.(js|css)$/.test(f.path))!;
236
const sourceMapFile = res.outputFiles.find(f => /\.(js|css)\.map$/.test(f.path))!;
237
238
const contents = Buffer.from(jsOrCSSFile.contents);
239
const unicodeMatch = contents.toString().match(/[^\x00-\xFF]+/g);
240
if (unicodeMatch) {
241
cb(new Error(`Found non-ascii character ${unicodeMatch[0]} in the minified output of ${f.path}. Non-ASCII characters in the output can cause performance problems when loading. Please review if you have introduced a regular expression that esbuild is not automatically converting and convert it to using unicode escape sequences.`));
242
} else {
243
f.contents = contents;
244
f.sourceMap = JSON.parse(sourceMapFile.text);
245
246
cb(undefined, f);
247
}
248
}, cb);
249
}),
250
esbuildFilter.restore,
251
svgFilter,
252
svgmin(),
253
svgFilter.restore,
254
sourcemaps.write('./', {
255
sourceMappingURL,
256
sourceRoot: undefined,
257
includeContent: true,
258
addComment: true
259
} as any),
260
gulp.dest(src + '-min'),
261
(err: any) => cb(err));
262
};
263
}
264
265