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