diff --git a/docs/config/build-options.md b/docs/config/build-options.md index 3713daf534b349..86982d514dad7e 100644 --- a/docs/config/build-options.md +++ b/docs/config/build-options.md @@ -19,8 +19,8 @@ Note the build will fail if the code contains features that cannot be safely tra ## build.modulePreload -- **Type:** `boolean | { polyfill?: boolean, resolveDependencies?: ResolveModulePreloadDependenciesFn }` -- **Default:** `{ polyfill: true }` +- **Type:** `boolean | { crossOrigin?: boolean | 'anonymous' | 'use-credentials', polyfill?: boolean, resolveDependencies?: ResolveModulePreloadDependenciesFn }` +- **Default:** `{ polyfill: true, crossOrigin: true }` By default, a [module preload polyfill](https://guybedford.com/es-module-preloading-integrity#modulepreload-polyfill) is automatically injected. The polyfill is auto injected into the proxy module of each `index.html` entry. If the build is configured to use a non-HTML custom entry via `build.rollupOptions.input`, then it is necessary to manually import the polyfill in your custom entry: @@ -66,6 +66,8 @@ modulePreload: { The resolved dependency paths can be further modified using [`experimental.renderBuiltUrl`](../guide/build.md#advanced-base-options). +The `crossOrigin` option controls setting the `crossorigin` attribute on preloaded dynamic imports chunks. This is true by default and can be disabled using `{ crossOrigin: false }`. It can also be set to one of the string values accepted by the [link tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#acrossorigin). + ## build.polyfillModulePreload - **Type:** `boolean` diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 75f207482d010c..fcf47199ad509f 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -275,6 +275,11 @@ export interface LibraryOptions { export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife' | 'system' export interface ModulePreloadOptions { + /** + * Whether to add crossorigin attribute to dynamic imported JS link preloads, accepts boolean or string value used by link tag + * @default true + */ + crossOrigin?: boolean | 'anonymous' | 'use-credentials' /** * Whether to inject a module preload polyfill. * Note: does not apply to library mode. @@ -288,6 +293,7 @@ export interface ModulePreloadOptions { resolveDependencies?: ResolveModulePreloadDependenciesFn } export interface ResolvedModulePreloadOptions { + crossOrigin: boolean | 'anonymous' | 'use-credentials' polyfill: boolean resolveDependencies?: ResolveModulePreloadDependenciesFn } @@ -330,6 +336,7 @@ export function resolveBuildOptions( const modulePreload = raw?.modulePreload const defaultModulePreload = { + crossOrigin: true, polyfill: true, } diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index ed424d56c49102..a52605a8ab5212 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -120,7 +120,11 @@ function preload( link.rel = isCss ? 'stylesheet' : scriptRel if (!isCss) { link.as = 'script' - link.crossOrigin = '' + // @ts-expect-error crossOrigin is declared before preload.toString() + if (crossOrigin) + link.crossOrigin = + // @ts-expect-error crossOrigin is declared before preload.toString() + typeof crossOrigin === 'string' ? crossOrigin : '' } link.href = dep if (cspNonce) { @@ -162,6 +166,8 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const resolveModulePreloadDependencies = config.build.modulePreload && config.build.modulePreload.resolveDependencies + const modulePreloadCrossOrigin = + config.build.modulePreload && config.build.modulePreload.crossOrigin const renderBuiltUrl = config.experimental.renderBuiltUrl const customModulePreloadPaths = !!( resolveModulePreloadDependencies || renderBuiltUrl @@ -196,7 +202,11 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { : // If the base isn't relative, then the deps are relative to the projects `outDir` and the base // is appended inside __vitePreload too. `function(dep) { return ${JSON.stringify(config.base)}+dep }` - const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}` + const crossOrigin = + typeof modulePreloadCrossOrigin === 'string' + ? `'${modulePreloadCrossOrigin}'` + : modulePreloadCrossOrigin + const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};const crossOrigin = ${crossOrigin};export const ${preloadMethod} = ${preload.toString()}` return { name: 'vite:build-import-analysis', diff --git a/playground/preload/__tests__/preload-credentials/preload-credentials.spec.ts b/playground/preload/__tests__/preload-credentials/preload-credentials.spec.ts new file mode 100644 index 00000000000000..7fc86741c694c0 --- /dev/null +++ b/playground/preload/__tests__/preload-credentials/preload-credentials.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from 'vitest' +import { browserLogs, isBuild, page } from '~utils' + +test('should have no 404s', () => { + browserLogs.forEach((msg) => { + expect(msg).not.toMatch('404') + }) +}) + +describe.runIf(isBuild)('build', () => { + test('dynamic import', async () => { + await page.waitForSelector('#done') + expect(await page.textContent('#done')).toBe('ran js') + }) + + test('dynamic import with crossorigin set to use-credentials', async () => { + await page.click('#hello .load') + await page.waitForSelector('#hello output') + + const html = await page.content() + expect(html).toMatch( + /link rel="modulepreload" as="script" crossorigin="use-credentials".*?href=".*?\/hello-\w{8}\.js"/, + ) + }) +}) diff --git a/playground/preload/__tests__/preload-credentials/vite.config.js b/playground/preload/__tests__/preload-credentials/vite.config.js new file mode 100644 index 00000000000000..832fb9ac77d372 --- /dev/null +++ b/playground/preload/__tests__/preload-credentials/vite.config.js @@ -0,0 +1 @@ +export { default } from '../../vite.config-preload-credentials' diff --git a/playground/preload/package.json b/playground/preload/package.json index 84855f60fffd4e..f14a4e44977434 100644 --- a/playground/preload/package.json +++ b/playground/preload/package.json @@ -15,7 +15,11 @@ "dev:preload-disabled": "vite --config vite.config-preload-disabled.ts", "build:preload-disabled": "vite build --config vite.config-preload-disabled.ts", "debug:preload-disabled": "node --inspect-brk ../../packages/vite/bin/vite --config vite.config-preload-disabled.ts", - "preview:preload-disabled": "vite preview --config vite.config-preload-disabled.ts" + "preview:preload-disabled": "vite preview --config vite.config-preload-disabled.ts", + "dev:preload-credentials": "vite --config vite.config-preload-credentials.ts", + "build:preload-credentials": "vite build --config vite.config-preload-credentials.ts", + "debug:preload-credentials": "node --inspect-brk ../../packages/vite/bin/vite --config vite.config-preload-credentials.ts", + "preview:preload-credentials": "vite preview --config vite.config-preload-credentials.ts" }, "devDependencies": { "terser": "^5.31.0", diff --git a/playground/preload/vite.config-preload-credentials.js b/playground/preload/vite.config-preload-credentials.js new file mode 100644 index 00000000000000..767c895d3c0832 --- /dev/null +++ b/playground/preload/vite.config-preload-credentials.js @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + outDir: 'dist/preload-credentials', + minify: 'terser', + terserOptions: { + format: { + beautify: true, + }, + compress: { + passes: 3, + }, + }, + modulePreload: { + crossOrigin: 'use-credentials', + }, + }, + cacheDir: 'node_modules/.vite-preload-credentials', +})