Skip to content

Commit 4c3f396

Browse files
authored
refactor: Ensure module preload polyfill is inlined into main bundle (#147)
* refactor: Ensure module preload polyfill is inlined into main bundle * docs: Update patch comments * refactor: Bail out on multiple outputs
1 parent 9deabbf commit 4c3f396

File tree

2 files changed

+62
-6
lines changed

2 files changed

+62
-6
lines changed

src/prerender.ts

+57-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import type { Plugin, ResolvedConfig } from "vite";
1010

1111
// Vite re-exports Rollup's type defs in newer versions,
1212
// merge into above type import when we bump the Vite devDep
13-
import type { InputOption, OutputAsset, OutputChunk } from "rollup";
13+
import type {
14+
InputOption,
15+
OutputAsset,
16+
OutputChunk,
17+
OutputOptions,
18+
} from "rollup";
1419

1520
interface HeadElement {
1621
type: string;
@@ -74,6 +79,7 @@ export function PrerenderPlugin({
7479
additionalPrerenderRoutes,
7580
}: PrerenderPluginOptions = {}): Plugin {
7681
const preloadHelperId = "vite/preload-helper";
82+
const preloadPolyfillId = "vite/modulepreload-polyfill";
7783
let viteConfig = {} as ResolvedConfig;
7884
let userEnabledSourceMaps: boolean | undefined;
7985

@@ -123,6 +129,34 @@ export function PrerenderPlugin({
123129
config.build.sourcemap = true;
124130

125131
viteConfig = config;
132+
133+
// With this plugin adding an additional input, Rollup/Vite tries to be smart
134+
// and extract our prerender script (which is often their main bundle) to a separate
135+
// chunk that the entry & prerender chunks can depend on. Unfortunately, this means the
136+
// first script the browser loads is the module preload polyfill & a sync import of the main
137+
// bundle. This is obviously less than ideal as the main bundle should be directly referenced
138+
// by the user's HTML to speed up loading a bit.
139+
140+
// We're only going to alter the chunking behavior in the default cases, where the user and/or
141+
// other plugins haven't already configured this. It'd be impossible to avoid breakages otherwise.
142+
if (
143+
Array.isArray(config.build.rollupOptions.output) ||
144+
(config.build.rollupOptions.output as OutputOptions)?.manualChunks
145+
) {
146+
return;
147+
}
148+
149+
config.build.rollupOptions.output ??= {};
150+
(config.build.rollupOptions.output as OutputOptions).manualChunks = (
151+
id: string,
152+
) => {
153+
if (
154+
id.includes(prerenderScript as string) ||
155+
id.includes(preloadPolyfillId)
156+
) {
157+
return "index";
158+
}
159+
};
126160
},
127161
async options(opts) {
128162
if (!opts.input) return;
@@ -139,15 +173,15 @@ export function PrerenderPlugin({
139173
: { ...opts.input, prerenderEntry: prerenderScript };
140174
opts.preserveEntrySignatures = "allow-extension";
141175
},
142-
// Injects a window check into Vite's preload helper, instantly resolving
143-
// the module rather than attempting to add a <link> to the document.
176+
// Injects window checks into Vite's preload helper & modulepreload polyfill
144177
transform(code, id) {
145-
// Vite keeps changing up the ID, best we can do for cross-version
146-
// compat is an `includes`
147178
if (id.includes(preloadHelperId)) {
179+
// Injects a window check into Vite's preload helper, instantly resolving
180+
// the module rather than attempting to add a <link> to the document.
181+
const s = new MagicString(code);
182+
148183
// Through v5.0.4
149184
// https://github.com/vitejs/vite/blob/b93dfe3e08f56cafe2e549efd80285a12a3dc2f0/packages/vite/src/node/plugins/importAnalysisBuild.ts#L95-L98
150-
const s = new MagicString(code);
151185
s.replace(
152186
`if (!__VITE_IS_MODERN__ || !deps || deps.length === 0) {`,
153187
`if (!__VITE_IS_MODERN__ || !deps || deps.length === 0 || typeof window === 'undefined') {`,
@@ -162,6 +196,23 @@ export function PrerenderPlugin({
162196
code: s.toString(),
163197
map: s.generateMap({ hires: true }),
164198
};
199+
} else if (id.includes(preloadPolyfillId)) {
200+
const s = new MagicString(code);
201+
// Replacement for `'link'` && `"link"` as the output from their tooling has
202+
// differed over the years. Should be better than switching to regex.
203+
// https://github.com/vitejs/vite/blob/20fdf210ee0ac0824b2db74876527cb7f378a9e8/packages/vite/src/node/plugins/modulePreloadPolyfill.ts#L62
204+
s.replace(
205+
`const relList = document.createElement('link').relList;`,
206+
`if (typeof window === "undefined") return;\n const relList = document.createElement('link').relList;`,
207+
);
208+
s.replace(
209+
`const relList = document.createElement("link").relList;`,
210+
`if (typeof window === "undefined") return;\n const relList = document.createElement("link").relList;`,
211+
);
212+
return {
213+
code: s.toString(),
214+
map: s.generateMap({ hires: true }),
215+
};
165216
}
166217
},
167218
async generateBundle(_opts, bundle) {

test/build.test.mjs

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
22
import { test } from "node:test";
33
import { promisify } from "node:util";
44
import { promises as fs } from "node:fs";
5+
import path from "node:path";
56
import assert from "node:assert";
67
import { dir } from "./util.mjs";
78

@@ -34,4 +35,8 @@ test("builds demo successfully", async () => {
3435

3536
// `additionalPrerenderRoutes` config option
3637
assert.doesNotThrow(async () => await fs.access(dir("demo/dist/404/index.html")));
38+
39+
const outputFiles = await fs.readdir(path.join(dir("demo/dist"), 'assets'));
40+
const outputIndexJS = outputFiles.filter(f => /^index\..+\.js$/.test(f));
41+
assert.strictEqual(outputIndexJS.length, 1);
3742
});

0 commit comments

Comments
 (0)