Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/webpack-runtime-watch-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agoda-devfeedback-webpack": patch
---

Only inject the DevFeedback client runtime module in watch mode (dev server). The browser snippet was previously added to every compilation's initial chunks, including one-off production/CI builds and SSR bundles — it broke html-webpack-plugin template evaluation (`window is not defined` in node:vm) and would crash SSR bundles running in Node. The generated client code also now guards on `typeof window`/`document` as a defensive measure.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const mockedSendBuildData = sendBuildData as unknown as ReturnType<typeof vi.fn>
*/
const createMockCompiler = () => {
return {
watchMode: false,
hooks: {
watchRun: {
tap: vi.fn(),
Expand Down Expand Up @@ -67,13 +68,27 @@ describe('WebpackBuildStatsPlugin', () => {
'WebpackBuildStatsPlugin',
expect.any(Function),
);
// The plugin also uses `compilation.tap(...)` internally
});

it('should inject the client runtime module in watch mode', () => {
mockedCompiler.watchMode = true;
plugin.apply(mockedCompiler as unknown as Compiler);

expect(mockedCompiler.hooks.compilation.tap).toHaveBeenCalledWith(
'WebpackBuildStatsPlugin',
expect.any(Function),
);
});

it('should not inject the client runtime module for one-off builds', () => {
mockedCompiler.watchMode = false;
plugin.apply(mockedCompiler as unknown as Compiler);

// One-off builds (production/CI/SSR) must not get the browser runtime —
// it breaks html-webpack-plugin template evaluation and SSR bundles.
expect(mockedCompiler.hooks.compilation.tap).not.toHaveBeenCalled();
});

it('should send the correct build stats data on done', async () => {
// Arrange
const plugin = new WebpackBuildStatsPlugin('my custom identifier');
Expand Down
43 changes: 28 additions & 15 deletions packages/webpack-plugin/src/lib/webpack-build-stats-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,23 +96,31 @@ export class WebpackBuildStatsPlugin {
});

/**
* 4) Inject client code as a runtime module
* 4) Inject client code as a runtime module — watch mode only.
*
* The client snippet exists to report HMR / DOM-update events during local
* development, so it only makes sense when webpack is watching (dev server).
* Injecting it into one-off builds (production, CI, SSR) breaks consumers:
* html-webpack-plugin child compilations evaluate the template in node:vm
* where `window` is undefined, and SSR bundles execute the runtime in Node.
*/
compiler.hooks.compilation.tap(
'WebpackBuildStatsPlugin',
(compilation: Compilation) => {
compilation.hooks.afterChunks.tap('WebpackBuildStatsPlugin', (chunks) => {
for (const chunk of chunks) {
if (chunk.canBeInitial()) {
compilation.addRuntimeModule(
chunk,
new DevFeedbackRuntimeModule(() => this.generateClientCode()),
);
if (compiler.watchMode) {
compiler.hooks.compilation.tap(
'WebpackBuildStatsPlugin',
(compilation: Compilation) => {
compilation.hooks.afterChunks.tap('WebpackBuildStatsPlugin', (chunks) => {
for (const chunk of chunks) {
if (chunk.canBeInitial()) {
compilation.addRuntimeModule(
chunk,
new DevFeedbackRuntimeModule(() => this.generateClientCode()),
);
}
}
}
});
},
);
});
},
);
}
}

/**
Expand Down Expand Up @@ -165,6 +173,11 @@ export class WebpackBuildStatsPlugin {

return `
(function(){
// Defensive: never execute outside a browser (e.g. SSR bundles or
// html-webpack-plugin template evaluation in node:vm).
if (typeof window === 'undefined' || typeof document === 'undefined') {
return;
}
window.__BUILD_START__ = ${buildStartTime};

var ws = new WebSocket('${wsUrl}');
Expand Down
Loading