From d01c7224e70292181676f1bb22e8bddf5bce48fc Mon Sep 17 00:00:00 2001 From: Radoslav Karaivanov Date: Tue, 25 Aug 2026 09:19:37 +0300 Subject: [PATCH] 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. --- .gitignore | 3 ++ package-lock.json | 1 + package.json | 1 + scripts/prebundle-test-deps.js | 81 ++++++++++++++++++++++++++++++++++ test/utils/grid-fixture.ts | 2 +- web-test-runner.config.mjs | 11 ++++- 6 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 scripts/prebundle-test-deps.js diff --git a/.gitignore b/.gitignore index 4ee5736..95f49de 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ storybook-static custom-elements.json *.css.ts + +# Prebundled test dependencies +.test-deps/ diff --git a/package-lock.json b/package-lock.json index c3965b1..33d20d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@web/test-runner-playwright": "^1.0.0", "autoprefixer": "^10.5.4", "concurrently": "^10.0.5", + "esbuild": "^0.28.1", "husky": "^9.1.7", "igniteui-theming": "^27.5.1", "lint-staged": "^17.3.0", diff --git a/package.json b/package.json index 5c8494c..f96b67b 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@web/test-runner-playwright": "^1.0.0", "autoprefixer": "^10.5.4", "concurrently": "^10.0.5", + "esbuild": "^0.28.1", "husky": "^9.1.7", "igniteui-theming": "^27.5.1", "lint-staged": "^17.3.0", diff --git a/scripts/prebundle-test-deps.js b/scripts/prebundle-test-deps.js new file mode 100644 index 0000000..da783b8 --- /dev/null +++ b/scripts/prebundle-test-deps.js @@ -0,0 +1,81 @@ +import { glob, readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import esbuild from 'esbuild'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const BARREL = 'igniteui-webcomponents'; + +export const TEST_DEPS_DIR = '.test-deps'; +export const BARREL_BUNDLE = `/${TEST_DEPS_DIR}/${BARREL}.js`; + +/** Named import statements pulling from the barrel, e.g. `import { A, type B } from 'igniteui-webcomponents'` */ +const BARREL_IMPORT = new RegExp(`import\\s+(type\\s+)?{([^}]*)}\\s*from\\s*'${BARREL}'`, 'g'); + +/** lit is left external so the bundle cannot introduce a second copy of the runtime. */ +const EXTERNAL = [ + 'lit', + 'lit/*', + 'lit-html', + 'lit-html/*', + 'lit-element', + 'lit-element/*', + '@lit/*', + '@lit-labs/*', +]; + +/** + * Collects the runtime (non-type) symbols `src/` imports from the barrel. + * Aliases resolve to their source name: `ΞaddThemingController as x` => `ΞaddThemingController`. + */ +async function collectUsedExports() { + const used = new Set(); + + for await (const file of glob('src/**/*.ts', { cwd: ROOT })) { + const source = await readFile(path.join(ROOT, file), 'utf8'); + + for (const [, typeOnly, specifiers] of source.matchAll(BARREL_IMPORT)) { + if (typeOnly) { + continue; + } + + for (const specifier of specifiers.split(',')) { + const name = specifier + .trim() + .split(/\s+as\s+/)[0] + .trim(); + + if (name && !name.startsWith('type ')) { + used.add(name); + } + } + } + } + + return Array.from(used).sort(); +} + +/** + * The dev server serves unbundled ESM, so importing the barrel costs ~250 module + * requests (every component in the library). Tree-shaking it down to the handful + * of symbols `src/` actually uses cuts the test suite from ~41s to ~8s. + */ +export async function prebundleTestDeps() { + const exports = await collectUsedExports(); + + await esbuild.build({ + stdin: { + contents: `export { ${exports.join(', ')} } from '${BARREL}';`, + resolveDir: ROOT, + sourcefile: 'test-deps-facade.js', + loader: 'js', + }, + bundle: true, + format: 'esm', + treeShaking: true, + outfile: path.join(ROOT, TEST_DEPS_DIR, `${BARREL}.js`), + conditions: ['browser', 'production'], + external: EXTERNAL, + logLevel: 'error', + }); +} diff --git a/test/utils/grid-fixture.ts b/test/utils/grid-fixture.ts index ab76106..6581d9b 100644 --- a/test/utils/grid-fixture.ts +++ b/test/utils/grid-fixture.ts @@ -40,7 +40,7 @@ export default class GridTestFixture { } protected async waitForUpdate() { - await Promise.all([elementUpdated(this.grid), nextFrame]); + await Promise.all([elementUpdated(this.grid), nextFrame()]); await nextFrame(); } diff --git a/web-test-runner.config.mjs b/web-test-runner.config.mjs index c2b6221..24d1a9c 100644 --- a/web-test-runner.config.mjs +++ b/web-test-runner.config.mjs @@ -1,6 +1,7 @@ import { fileURLToPath } from 'node:url'; import { esbuildPlugin } from '@web/dev-server-esbuild'; import { playwrightLauncher } from '@web/test-runner-playwright'; +import { BARREL_BUNDLE, prebundleTestDeps, TEST_DEPS_DIR } from './scripts/prebundle-test-deps.js'; const filteredLogs = ['in dev mode']; @@ -14,7 +15,7 @@ export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({ }, coverageConfig: { - exclude: ['node_modules/**/*', '**/styles/**', 'test/**'] + exclude: ['node_modules/**/*', `${TEST_DEPS_DIR}/**/*`, '**/styles/**', 'test/**'] }, /** Browsers to run tests on */ @@ -27,6 +28,14 @@ export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({ }, plugins: [ + { + name: 'prebundled-test-deps', + + serverStart: () => prebundleTestDeps(), + + // Serve the tree-shaken bundle instead of the unbundled barrel. + resolveImport: ({ source }) => (source === 'igniteui-webcomponents' ? BARREL_BUNDLE : undefined), + }, esbuildPlugin({ ts: true, tsconfig: fileURLToPath(new URL('./tsconfig.json', import.meta.url)),