Skip to content

Commit d01c722

Browse files
committed
test: Speed up suite by prebundling barrel imports
Every test file that renders the grid paid ~7s loading modules. All source files import from the igniteui-webcomponents barrel, which re-exports the full component library. Rollup tree-shakes this in the build, but web-test-runner serves unbundled ESM, so the browser fetched 249 modules per test page - banner, card, carousel and i18n-core included, none of which the grid uses. Prebundle the barrel with esbuild, tree-shaken to the 8 runtime symbols src/ imports, and resolve the bare specifier to it. lit stays external so no second copy of the runtime enters the graph. The export list is derived by scanning src/ so it cannot drift. Suite drops from 40.8s to 11.0s.
1 parent c97c6e7 commit d01c722

6 files changed

Lines changed: 97 additions & 2 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ storybook-static
3232

3333
custom-elements.json
3434
*.css.ts
35+
36+
# Prebundled test dependencies
37+
.test-deps/

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@web/test-runner-playwright": "^1.0.0",
6060
"autoprefixer": "^10.5.4",
6161
"concurrently": "^10.0.5",
62+
"esbuild": "^0.28.1",
6263
"husky": "^9.1.7",
6364
"igniteui-theming": "^27.5.1",
6465
"lint-staged": "^17.3.0",

scripts/prebundle-test-deps.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { glob, readFile } from 'node:fs/promises';
2+
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
import esbuild from 'esbuild';
5+
6+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
7+
const BARREL = 'igniteui-webcomponents';
8+
9+
export const TEST_DEPS_DIR = '.test-deps';
10+
export const BARREL_BUNDLE = `/${TEST_DEPS_DIR}/${BARREL}.js`;
11+
12+
/** Named import statements pulling from the barrel, e.g. `import { A, type B } from 'igniteui-webcomponents'` */
13+
const BARREL_IMPORT = new RegExp(`import\\s+(type\\s+)?{([^}]*)}\\s*from\\s*'${BARREL}'`, 'g');
14+
15+
/** lit is left external so the bundle cannot introduce a second copy of the runtime. */
16+
const EXTERNAL = [
17+
'lit',
18+
'lit/*',
19+
'lit-html',
20+
'lit-html/*',
21+
'lit-element',
22+
'lit-element/*',
23+
'@lit/*',
24+
'@lit-labs/*',
25+
];
26+
27+
/**
28+
* Collects the runtime (non-type) symbols `src/` imports from the barrel.
29+
* Aliases resolve to their source name: `θaddThemingController as x` => `θaddThemingController`.
30+
*/
31+
async function collectUsedExports() {
32+
const used = new Set();
33+
34+
for await (const file of glob('src/**/*.ts', { cwd: ROOT })) {
35+
const source = await readFile(path.join(ROOT, file), 'utf8');
36+
37+
for (const [, typeOnly, specifiers] of source.matchAll(BARREL_IMPORT)) {
38+
if (typeOnly) {
39+
continue;
40+
}
41+
42+
for (const specifier of specifiers.split(',')) {
43+
const name = specifier
44+
.trim()
45+
.split(/\s+as\s+/)[0]
46+
.trim();
47+
48+
if (name && !name.startsWith('type ')) {
49+
used.add(name);
50+
}
51+
}
52+
}
53+
}
54+
55+
return Array.from(used).sort();
56+
}
57+
58+
/**
59+
* The dev server serves unbundled ESM, so importing the barrel costs ~250 module
60+
* requests (every component in the library). Tree-shaking it down to the handful
61+
* of symbols `src/` actually uses cuts the test suite from ~41s to ~8s.
62+
*/
63+
export async function prebundleTestDeps() {
64+
const exports = await collectUsedExports();
65+
66+
await esbuild.build({
67+
stdin: {
68+
contents: `export { ${exports.join(', ')} } from '${BARREL}';`,
69+
resolveDir: ROOT,
70+
sourcefile: 'test-deps-facade.js',
71+
loader: 'js',
72+
},
73+
bundle: true,
74+
format: 'esm',
75+
treeShaking: true,
76+
outfile: path.join(ROOT, TEST_DEPS_DIR, `${BARREL}.js`),
77+
conditions: ['browser', 'production'],
78+
external: EXTERNAL,
79+
logLevel: 'error',
80+
});
81+
}

test/utils/grid-fixture.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default class GridTestFixture<T extends object> {
4040
}
4141

4242
protected async waitForUpdate() {
43-
await Promise.all([elementUpdated(this.grid), nextFrame]);
43+
await Promise.all([elementUpdated(this.grid), nextFrame()]);
4444
await nextFrame();
4545
}
4646

web-test-runner.config.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { fileURLToPath } from 'node:url';
22
import { esbuildPlugin } from '@web/dev-server-esbuild';
33
import { playwrightLauncher } from '@web/test-runner-playwright';
4+
import { BARREL_BUNDLE, prebundleTestDeps, TEST_DEPS_DIR } from './scripts/prebundle-test-deps.js';
45

56
const filteredLogs = ['in dev mode'];
67

@@ -14,7 +15,7 @@ export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({
1415
},
1516

1617
coverageConfig: {
17-
exclude: ['node_modules/**/*', '**/styles/**', 'test/**']
18+
exclude: ['node_modules/**/*', `${TEST_DEPS_DIR}/**/*`, '**/styles/**', 'test/**']
1819
},
1920

2021
/** Browsers to run tests on */
@@ -27,6 +28,14 @@ export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({
2728
},
2829

2930
plugins: [
31+
{
32+
name: 'prebundled-test-deps',
33+
34+
serverStart: () => prebundleTestDeps(),
35+
36+
// Serve the tree-shaken bundle instead of the unbundled barrel.
37+
resolveImport: ({ source }) => (source === 'igniteui-webcomponents' ? BARREL_BUNDLE : undefined),
38+
},
3039
esbuildPlugin({
3140
ts: true,
3241
tsconfig: fileURLToPath(new URL('./tsconfig.json', import.meta.url)),

0 commit comments

Comments
 (0)