Skip to content

Commit 3086516

Browse files
committed
Make esbuild to produce bundled and minified VSIX
This fix makes 'esbuild' to produce the bundled and minified VSIX extension archive, free of unneeded dependency modules, when the packaging is started with `vsce package`. On other hand, when building with `npm install && npm run build` and testing with `npm test` the extension file structure is kept unchange and the trunspiled scripts aren't minified, so the unit testing and coverage tests work as usual. Note: `npm install` is needed to be executed after executing `vsce package` as the last one clears the `node_modules/` of the depencencies not needed in production. Fixes #4226 Signed-off-by: Victor Rubezhny <[email protected]>
1 parent 5de97ce commit 3086516

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+1385
-497
lines changed

.vscodeignore

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,42 @@
1-
.vscode/**
2-
.vscode-test/**
1+
azure-pipelines.yml
2+
build/**
3+
build/**
4+
CONTRIBUTING.md
5+
coverage/**
6+
coverconfig.json
7+
doc/**
8+
.eslintignore
9+
.gitattributes
10+
.github
11+
.gitignore
12+
header.js
13+
images/demo-featured-image.png
314
images/gif/**
4-
out/test/**
15+
Jenkinsfile
16+
.mocharc.js
17+
out/build**
518
out/build/**
6-
out/test-resources/**
7-
out/tools-cache/**
19+
out/coverage/**
820
out/**/*.map
921
out/src-orig/**
22+
out/test/**
23+
out/test-resources/**
24+
out/tools-cache/**
25+
out/webview/**
26+
*.sha256
1027
src/**
1128
!src/tools.json
12-
doc/**
13-
.gitignore
14-
.github
15-
tsconfig.json
16-
vsc-extension-quickstart.md
17-
tslint.json
18-
*.vsix
1929
test/**
20-
out/coverage/**
21-
coverconfig.json
22-
Jenkinsfile
23-
.gitattributes
24-
.travis.yml
25-
CONTRIBUTING.md
26-
build/**
27-
out/build**
28-
azure-pipelines.yml
29-
images/demo-featured-image.png
30+
test/**
31+
test-resources/**
3032
test-resources/**
31-
header.js
32-
.mocharc.js
33-
.eslintignore
3433
*.tgz
35-
*.sha256
36-
**.tsbuildinfo
34+
.travis.yml
35+
*/**.tsbuildinfo
36+
tsconfig.json
37+
tsconfig.tsbuildinfo
38+
tslint.json
39+
vsc-extension-quickstart.md
40+
.vscode/**
41+
.vscode-test/**
42+
*.vsix

build/esbuild.mjs

Lines changed: 165 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import * as esbuild from 'esbuild';
77
import svgr from 'esbuild-plugin-svgr';
88
import { sassPlugin } from 'esbuild-sass-plugin';
99
import * as fs from 'fs/promises';
10+
import * as glob from 'glob';
11+
import { createRequire } from 'module';
12+
import * as path from 'path';
13+
import { fileURLToPath } from 'url';
14+
15+
const require = createRequire(import.meta.url);
1016

1117
const webviews = [
1218
'cluster',
@@ -26,14 +32,138 @@ const webviews = [
2632
'openshift-terminal',
2733
];
2834

29-
await Promise.all([
30-
esbuild.build({
31-
entryPoints: webviews.map(webview => `./src/webview/${webview}/app/index.tsx`),
32-
bundle: true,
33-
outdir: 'out',
35+
const production = process.argv.includes('--production');
36+
37+
// eslint-disable no-console
38+
console.log(`esbuild: building for production: ${production ? 'Yes' : 'No'}`);
39+
40+
const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
41+
const __dirname = path.resolve(path.dirname(__filename), '..'); // get the name of the directory
42+
const srcDir = 'src'; // Input source directory
43+
const outDir = 'out'; // Output dist directory
44+
45+
function detectGoal(entryPoints) {
46+
if (production) {
47+
const isExtension = entryPoints.filter((ep) => `${ep}`.includes('extension.ts')).length > 0;
48+
const isWebviews = entryPoints.filter((ep) => `${ep}`.includes('.tsx')).length > 0;
49+
return isExtension ? 'Extension' : isWebviews ? 'the Webviews' : '';
50+
}
51+
return 'Extension and the Webviews for testing/debugging';
52+
}
53+
54+
/**
55+
* @type {import('esbuild').Plugin}
56+
*/
57+
const esbuildProblemMatcherPlugin = {
58+
name: 'esbuild-problem-matcher',
59+
60+
setup(build) {
61+
build.onStart(() => {
62+
const goal = detectGoal(build.initialOptions.entryPoints);
63+
console.log(`[watch] build started${goal ? ' for ' + goal : ''}...` );
64+
});
65+
build.onEnd(result => {
66+
result.errors.forEach(({ text, location }) => {
67+
console.error(`✘ [ERROR] ${text}`);
68+
if (location) {
69+
console.error(` ${location.file}:${location.line}:${location.column}:`);
70+
}
71+
});
72+
const goal = detectGoal(build.initialOptions.entryPoints);
73+
console.log(`[watch] build finished${goal ? ' for ' + goal : ''}`);
74+
});
75+
}
76+
};
77+
78+
const nativeNodeModulesPlugin = {
79+
name: 'native-node-modules',
80+
setup(build) {
81+
try {
82+
// If a ".node" file is imported within a module in the "file" namespace, resolve
83+
// it to an absolute path and put it into the "node-file" virtual namespace.
84+
build.onResolve({ filter: /\.node$/, namespace: 'file' }, args => ({
85+
path: require.resolve(args.path, { paths: [args.resolveDir] }),
86+
namespace: 'node-file',
87+
}));
88+
89+
// Files in the "node-file" virtual namespace call "require()" on the
90+
// path from esbuild of the ".node" file in the output directory.
91+
build.onLoad({ filter: /.*/, namespace: 'node-file' }, args => ({
92+
contents: `
93+
import path from ${JSON.stringify(args.path)}
94+
try {
95+
module.exports = require(path)
96+
} catch {}
97+
`,
98+
}))
99+
100+
// If a ".node" file is imported within a module in the "node-file" namespace, put
101+
// it in the "file" namespace where esbuild's default loading behavior will handle
102+
// it. It is already an absolute path since we resolved it to one above.
103+
build.onResolve({ filter: /\.node$/, namespace: 'node-file' }, args => ({
104+
path: args.path,
105+
namespace: 'file',
106+
}));
107+
108+
// Tell esbuild's default loading behavior to use the "file" loader for
109+
// these ".node" files.
110+
let opts = build.initialOptions
111+
opts.loader = opts.loader || {}
112+
opts.loader['.node'] = 'file'
113+
} catch (err) {
114+
console.error(`native-node-modules: ERROR: ${err}`);
115+
}
116+
},
117+
};
118+
119+
const baseConfig = {
120+
bundle: true,
121+
target: 'chrome108',
122+
minify: production,
123+
sourcemap: !production,
124+
logLevel: 'warning',
125+
};
126+
127+
if (production) {
128+
// Build the extension.js
129+
const extConfig = {
130+
...baseConfig,
131+
platform: 'node',
132+
entryPoints: [`./${srcDir}/extension.ts`],
133+
outfile: `${outDir}/${srcDir}/extension.js`,
134+
external: ['vscode', 'shelljs'],
135+
plugins: [
136+
nativeNodeModulesPlugin,
137+
esbuildProblemMatcherPlugin // this one is to be added to the end of plugins array
138+
]
139+
};
140+
await esbuild.build(extConfig);
141+
142+
// Build the Webviews
143+
const webviewsConfig = {
144+
...baseConfig,
145+
platform: 'browser',
146+
entryPoints: [...webviews.map(webview => `./${srcDir}/webview/${webview}/app/index.tsx`)],
147+
outdir: `${outDir}`,
148+
loader: {
149+
'.png': 'file',
150+
},
151+
plugins: [
152+
sassPlugin(),
153+
svgr({
154+
plugins: ['@svgr/plugin-jsx']
155+
}),
156+
esbuildProblemMatcherPlugin // this one is to be added to the end of plugins array
157+
]
158+
};
159+
await esbuild.build(webviewsConfig);
160+
} else {
161+
// Build the Webviews
162+
const devConfig = {
163+
...baseConfig,
34164
platform: 'browser',
35-
target: 'chrome108',
36-
sourcemap: true,
165+
entryPoints: [...webviews.map(webview => `./${srcDir}/webview/${webview}/app/index.tsx`)],
166+
outdir: `${outDir}`,
37167
loader: {
38168
'.png': 'file',
39169
},
@@ -42,9 +172,32 @@ await Promise.all([
42172
svgr({
43173
plugins: ['@svgr/plugin-jsx']
44174
}),
175+
esbuildProblemMatcherPlugin // this one is to be added to the end of plugins array
45176
]
46-
}),
47-
...webviews.map(webview =>
48-
fs.cp(`./src/webview/${webview}/app/index.html`, `./out/${webview}/app/index.html`)
49-
),
50-
]);
177+
};
178+
await esbuild.build(devConfig);
179+
}
180+
181+
async function dirExists(path) {
182+
try {
183+
if ((await fs.stat(path)).isDirectory()) {
184+
return true;
185+
}
186+
} catch {
187+
// Ignore
188+
}
189+
return false;
190+
}
191+
192+
// Copy webview's 'index.html's to the output webview dirs
193+
await Promise.all([
194+
...webviews.map(async webview => {
195+
const targetDir = path.join(__dirname, `${outDir}/${webview}/app`);
196+
if (!dirExists(targetDir)) {
197+
await fs.mkdir(targetDir, { recursive: true, mode: 0o750} );
198+
}
199+
glob.sync([ `${srcDir}/webview/${webview}/app/index.html` ]).map(async srcFile => {
200+
await fs.cp(path.join(__dirname, srcFile), path.join(targetDir, `${path.basename(srcFile)}`))
201+
});
202+
})
203+
]);

build/run-tests.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ async function main(): Promise<void> {
2727
'--disable-workspace-trust',
2828
],
2929
});
30-
} catch {
30+
} catch (err) {
3131
// eslint-disable-next-line no-console
32-
console.error('Failed to run tests');
32+
console.error(`Failed to run tests: ${err}`);
3333
process.exit(1);
3434
}
3535
}

0 commit comments

Comments
 (0)