Skip to content

Commit 69b0fc9

Browse files
serpentbladeclaude
andcommitted
test(unplugin): full webpack build smoke with memfs + esbuild TS loader
Closes the deferred follow-up from 474025f. The previous webpack coverage was shape (factory returns plugin with apply method) + compiler-attach (plugin.apply() against a real webpack compiler without throwing). That catches the 90% failure mode but leaves a build-level regression invisible — a transform-hook mis-wiring would still construct a compiler and only surface when the bundle actually runs. New test: real programmatic webpack 5 build with memfs as the output filesystem. The inline TS loader is ~12 LOC that delegates to esbuild's transform API — self-contained, no dependency on ts-loader / esbuild-loader / @swc/loader. Same stable marker assertions as the Rollup / esbuild / Rolldown smokes (`data-rozie-s-` scope attribute, rozieSpread / rozieListeners runtime helpers, class Smoke / rozie-smoke). Lit + @rozie/runtime-lit marked external so the smoke is about the unplugin's transform pipeline integrating with webpack, not Lit's dependency graph. 30s test timeout for webpack's cold start. Suite: 15 files, 134 tests (was 133). turbo run test --filter @rozie/unplugin --force green; turbo run typecheck --force --continue 47/47 green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4545fea commit 69b0fc9

1 file changed

Lines changed: 114 additions & 7 deletions

File tree

packages/unplugin/src/__tests__/bundlers-smoke.test.ts

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,8 @@ describe('@rozie/unplugin/webpack — shape', () => {
227227
const webpack = (await import('webpack')).default;
228228
const rozieWebpack = (await import('../webpack.js')).default;
229229

230-
// Construct a minimal compiler with the rozie plugin attached. We do not
231-
// actually run the build — that would require ts-loader + memfs config,
232-
// which is significantly heavier. The smoke is "plugin.apply() runs
233-
// against a real webpack compiler without throwing". This catches the
234-
// 90% failure mode: a plugin export that fails to integrate with the
235-
// bundler's plugin API.
230+
// Phase 1 of the Webpack smoke: plugin.apply() against a real compiler
231+
// without throwing. The full-bundle smoke is the next test below.
236232
const compiler = webpack({
237233
entry: resolve(__dirname, 'entry-does-not-need-to-exist.ts'),
238234
mode: 'production',
@@ -241,9 +237,120 @@ describe('@rozie/unplugin/webpack — shape', () => {
241237

242238
expect(compiler).toBeDefined();
243239
expect(typeof compiler.run).toBe('function');
244-
// Close the compiler immediately — we never called .run().
245240
await new Promise<void>((res) => compiler.close(() => res()));
246241
});
242+
243+
it('full programmatic build: webpack + rozie + esbuild-as-TS-loader → bundle contains compiled Rozie output', async () => {
244+
const webpack = (await import('webpack')).default;
245+
const { createFsFromVolume, Volume } = await import('memfs');
246+
const esbuild = await import('esbuild');
247+
const rozieWebpack = (await import('../webpack.js')).default;
248+
249+
const { dir, entry } = setupTmpProject('webpack-build');
250+
251+
try {
252+
// In-memory output filesystem so we don't pollute the workspace.
253+
const outFs = createFsFromVolume(new Volume());
254+
255+
// Inline TS loader: a tiny webpack loader that delegates to esbuild's
256+
// transform API. Keeps the smoke self-contained — no dependency on
257+
// ts-loader / esbuild-loader / @swc/loader. The loader is registered
258+
// by path under `module.rules.use.loader`.
259+
const inlineLoaderPath = resolve(dir, 'inline-ts-loader.cjs');
260+
writeFileSync(
261+
inlineLoaderPath,
262+
`const esbuild = require('esbuild');
263+
module.exports = function (source) {
264+
const callback = this.async();
265+
esbuild.transform(source, {
266+
loader: 'ts',
267+
target: 'es2022',
268+
format: 'esm',
269+
sourcefile: this.resourcePath,
270+
}).then(
271+
(out) => callback(null, out.code, out.map ? JSON.parse(out.map) : undefined),
272+
(err) => callback(err),
273+
);
274+
};
275+
`,
276+
);
277+
278+
const compiler = webpack({
279+
entry,
280+
mode: 'production',
281+
// Tell webpack to treat the in-memory FS as the output destination.
282+
output: {
283+
path: '/dist',
284+
filename: 'bundle.js',
285+
library: { type: 'module' },
286+
},
287+
experiments: { outputModule: true },
288+
resolve: {
289+
extensions: ['.ts', '.js', '.rozie'],
290+
},
291+
module: {
292+
rules: [
293+
{
294+
test: /\.ts$|\.rozie$/,
295+
use: { loader: inlineLoaderPath },
296+
},
297+
],
298+
},
299+
plugins: [rozieWebpack({ target: 'lit' }) as never],
300+
// Mark Lit + Rozie runtime as external so we don't try to resolve
301+
// them — the smoke is about the unplugin transform, not Lit's
302+
// dependency graph.
303+
externals: [
304+
({ request }, callback) => {
305+
if (
306+
request === 'lit' ||
307+
(request && request.startsWith('lit/')) ||
308+
(request && request.startsWith('@lit-labs/')) ||
309+
(request && request.startsWith('@rozie/runtime-lit'))
310+
) {
311+
return callback(null, 'module ' + request);
312+
}
313+
callback();
314+
},
315+
],
316+
optimization: {
317+
minimize: false, // keep markers readable for the assertions below
318+
},
319+
// Suppress noisy warnings that don't affect the smoke.
320+
ignoreWarnings: [() => true],
321+
});
322+
323+
// Wire the in-memory output FS.
324+
(compiler as unknown as { outputFileSystem: unknown }).outputFileSystem = outFs;
325+
326+
const stats = await new Promise<import('webpack').Stats | undefined>((res, rej) => {
327+
compiler.run((err, statsArg) => {
328+
if (err) return rej(err);
329+
res(statsArg);
330+
});
331+
});
332+
333+
// Webpack may emit non-fatal errors for things like missing source
334+
// maps from the inline loader; we only fail on a complete build failure
335+
// (no output file produced).
336+
const bundlePath = '/dist/bundle.js';
337+
const bundleExists = outFs.existsSync(bundlePath);
338+
expect(bundleExists).toBe(true);
339+
340+
const bundleText = outFs.readFileSync(bundlePath, 'utf8') as string;
341+
// Same stable markers as the Rollup / esbuild / Rolldown smokes —
342+
// these survive TS-decorator lowering across every bundler.
343+
expect(bundleText).toMatch(/data-rozie-s-|rozieSpread|rozieListeners/);
344+
expect(bundleText).toMatch(/rozie[-_]smoke|class Smoke/);
345+
346+
await new Promise<void>((res) => compiler.close(() => res()));
347+
// stats is referenced to satisfy the linter; the actual gate is
348+
// `bundleExists` + the marker assertions above.
349+
void stats;
350+
} finally {
351+
rmSync(dir, { recursive: true, force: true });
352+
}
353+
}, 30_000); // webpack cold-start is slow; allow 30s
247354
});
248355

249356
describe('@rozie/unplugin/rspack — shape', () => {

0 commit comments

Comments
 (0)