From 4c3f396745625b27fa56cd9a78a999c9c6532da0 Mon Sep 17 00:00:00 2001
From: Ryan Christian <33403762+rschristian@users.noreply.github.com>
Date: Fri, 6 Dec 2024 19:48:00 -0600
Subject: [PATCH] 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
---
src/prerender.ts | 63 ++++++++++++++++++++++++++++++++++++++++-----
test/build.test.mjs | 5 ++++
2 files changed, 62 insertions(+), 6 deletions(-)
diff --git a/src/prerender.ts b/src/prerender.ts
index 28aa11d..3d46270 100644
--- a/src/prerender.ts
+++ b/src/prerender.ts
@@ -10,7 +10,12 @@ import type { Plugin, ResolvedConfig } from "vite";
// Vite re-exports Rollup's type defs in newer versions,
// merge into above type import when we bump the Vite devDep
-import type { InputOption, OutputAsset, OutputChunk } from "rollup";
+import type {
+ InputOption,
+ OutputAsset,
+ OutputChunk,
+ OutputOptions,
+} from "rollup";
interface HeadElement {
type: string;
@@ -74,6 +79,7 @@ export function PrerenderPlugin({
additionalPrerenderRoutes,
}: PrerenderPluginOptions = {}): Plugin {
const preloadHelperId = "vite/preload-helper";
+ const preloadPolyfillId = "vite/modulepreload-polyfill";
let viteConfig = {} as ResolvedConfig;
let userEnabledSourceMaps: boolean | undefined;
@@ -123,6 +129,34 @@ export function PrerenderPlugin({
config.build.sourcemap = true;
viteConfig = config;
+
+ // With this plugin adding an additional input, Rollup/Vite tries to be smart
+ // and extract our prerender script (which is often their main bundle) to a separate
+ // chunk that the entry & prerender chunks can depend on. Unfortunately, this means the
+ // first script the browser loads is the module preload polyfill & a sync import of the main
+ // bundle. This is obviously less than ideal as the main bundle should be directly referenced
+ // by the user's HTML to speed up loading a bit.
+
+ // We're only going to alter the chunking behavior in the default cases, where the user and/or
+ // other plugins haven't already configured this. It'd be impossible to avoid breakages otherwise.
+ if (
+ Array.isArray(config.build.rollupOptions.output) ||
+ (config.build.rollupOptions.output as OutputOptions)?.manualChunks
+ ) {
+ return;
+ }
+
+ config.build.rollupOptions.output ??= {};
+ (config.build.rollupOptions.output as OutputOptions).manualChunks = (
+ id: string,
+ ) => {
+ if (
+ id.includes(prerenderScript as string) ||
+ id.includes(preloadPolyfillId)
+ ) {
+ return "index";
+ }
+ };
},
async options(opts) {
if (!opts.input) return;
@@ -139,15 +173,15 @@ export function PrerenderPlugin({
: { ...opts.input, prerenderEntry: prerenderScript };
opts.preserveEntrySignatures = "allow-extension";
},
- // Injects a window check into Vite's preload helper, instantly resolving
- // the module rather than attempting to add a to the document.
+ // Injects window checks into Vite's preload helper & modulepreload polyfill
transform(code, id) {
- // Vite keeps changing up the ID, best we can do for cross-version
- // compat is an `includes`
if (id.includes(preloadHelperId)) {
+ // Injects a window check into Vite's preload helper, instantly resolving
+ // the module rather than attempting to add a to the document.
+ const s = new MagicString(code);
+
// Through v5.0.4
// https://github.com/vitejs/vite/blob/b93dfe3e08f56cafe2e549efd80285a12a3dc2f0/packages/vite/src/node/plugins/importAnalysisBuild.ts#L95-L98
- const s = new MagicString(code);
s.replace(
`if (!__VITE_IS_MODERN__ || !deps || deps.length === 0) {`,
`if (!__VITE_IS_MODERN__ || !deps || deps.length === 0 || typeof window === 'undefined') {`,
@@ -162,6 +196,23 @@ export function PrerenderPlugin({
code: s.toString(),
map: s.generateMap({ hires: true }),
};
+ } else if (id.includes(preloadPolyfillId)) {
+ const s = new MagicString(code);
+ // Replacement for `'link'` && `"link"` as the output from their tooling has
+ // differed over the years. Should be better than switching to regex.
+ // https://github.com/vitejs/vite/blob/20fdf210ee0ac0824b2db74876527cb7f378a9e8/packages/vite/src/node/plugins/modulePreloadPolyfill.ts#L62
+ s.replace(
+ `const relList = document.createElement('link').relList;`,
+ `if (typeof window === "undefined") return;\n const relList = document.createElement('link').relList;`,
+ );
+ s.replace(
+ `const relList = document.createElement("link").relList;`,
+ `if (typeof window === "undefined") return;\n const relList = document.createElement("link").relList;`,
+ );
+ return {
+ code: s.toString(),
+ map: s.generateMap({ hires: true }),
+ };
}
},
async generateBundle(_opts, bundle) {
diff --git a/test/build.test.mjs b/test/build.test.mjs
index 6dba0da..06b2f2a 100644
--- a/test/build.test.mjs
+++ b/test/build.test.mjs
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
import { test } from "node:test";
import { promisify } from "node:util";
import { promises as fs } from "node:fs";
+import path from "node:path";
import assert from "node:assert";
import { dir } from "./util.mjs";
@@ -34,4 +35,8 @@ test("builds demo successfully", async () => {
// `additionalPrerenderRoutes` config option
assert.doesNotThrow(async () => await fs.access(dir("demo/dist/404/index.html")));
+
+ const outputFiles = await fs.readdir(path.join(dir("demo/dist"), 'assets'));
+ const outputIndexJS = outputFiles.filter(f => /^index\..+\.js$/.test(f));
+ assert.strictEqual(outputIndexJS.length, 1);
});