diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 174e5a33d9169a..c9d265036754e5 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -50,6 +50,7 @@ import { watchPackageDataPlugin } from './packages' import { ensureWatchPlugin } from './plugins/ensureWatch' import { ESBUILD_MODULES_TARGET, VERSION } from './constants' import { resolveChokidarOptions } from './watch' +import { completeSystemWrapPlugin } from './plugins/completeSystemWrap' export interface BuildOptions { /** @@ -305,6 +306,7 @@ export function resolveBuildPlugins(config: ResolvedConfig): { commonjsOptions?.include.length !== 0 return { pre: [ + completeSystemWrapPlugin(), ...(options.watch ? [ensureWatchPlugin()] : []), watchPackageDataPlugin(config), ...(usePluginCommonjs ? [commonjsPlugin(options.commonjsOptions)] : []), @@ -857,6 +859,7 @@ const relativeUrlMechanisms: Record< )} : ${getRelativeUrlFromDocument(relativePath)})`, es: (relativePath) => getResolveUrl(`'${relativePath}', import.meta.url`), iife: (relativePath) => getRelativeUrlFromDocument(relativePath), + // NOTE: make sure rollup generate `module` params system: (relativePath) => getResolveUrl(`'${relativePath}', module.meta.url`), umd: (relativePath) => `(typeof document === 'undefined' && typeof location === 'undefined' ? ${getResolveUrl( diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 0db1301a876fdb..d3fc794754ab4b 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -2,7 +2,13 @@ import path from 'node:path' import { parse as parseUrl } from 'node:url' import fs, { promises as fsp } from 'node:fs' import * as mrmime from 'mrmime' -import type { OutputOptions, PluginContext, PreRenderedAsset } from 'rollup' +import type { + NormalizedOutputOptions, + OutputOptions, + PluginContext, + PreRenderedAsset, + RenderedChunk +} from 'rollup' import MagicString from 'magic-string' import { toOutputFilePathInString } from '../build' import type { Plugin } from '../plugin' @@ -36,6 +42,76 @@ export function registerCustomMime(): void { mrmime.mimes['eot'] = 'application/vnd.ms-fontobject' } +export function renderAssetUrlInJS( + ctx: PluginContext, + config: ResolvedConfig, + chunk: RenderedChunk, + opts: NormalizedOutputOptions, + code: string +): MagicString | undefined { + let match: RegExpExecArray | null + let s: MagicString | undefined + + // Urls added with JS using e.g. + // imgElement.src = "__VITE_ASSET__5aa0ddc0__" are using quotes + + // Urls added in CSS that is imported in JS end up like + // var inlined = ".inlined{color:green;background:url(__VITE_ASSET__5aa0ddc0__)}\n"; + + // In both cases, the wrapping should already be fine + + while ((match = assetUrlRE.exec(code))) { + s ||= new MagicString(code) + const [full, hash, postfix = ''] = match + // some internal plugins may still need to emit chunks (e.g. worker) so + // fallback to this.getFileName for that. TODO: remove, not needed + const file = getAssetFilename(hash, config) || ctx.getFileName(hash) + chunk.viteMetadata.importedAssets.add(cleanUrl(file)) + const filename = file + postfix + const replacement = toOutputFilePathInString( + filename, + 'asset', + chunk.fileName, + 'js', + config, + opts.format + ) + const replacementString = + typeof replacement === 'string' + ? JSON.stringify(replacement).slice(1, -1) + : `"+${replacement.runtime}+"` + s.overwrite(match.index, match.index + full.length, replacementString, { + contentOnly: true + }) + } + + // Replace __VITE_PUBLIC_ASSET__5aa0ddc0__ with absolute paths + + const publicAssetUrlMap = publicAssetUrlCache.get(config)! + while ((match = publicAssetUrlRE.exec(code))) { + s ||= new MagicString(code) + const [full, hash] = match + const publicUrl = publicAssetUrlMap.get(hash)!.slice(1) + const replacement = toOutputFilePathInString( + publicUrl, + 'public', + chunk.fileName, + 'js', + config, + opts.format + ) + const replacementString = + typeof replacement === 'string' + ? JSON.stringify(replacement).slice(1, -1) + : `"+${replacement.runtime}+"` + s.overwrite(match.index, match.index + full.length, replacementString, { + contentOnly: true + }) + } + + return s +} + /** * Also supports loading plain strings with import text from './foo.txt?raw' */ @@ -90,66 +166,8 @@ export function assetPlugin(config: ResolvedConfig): Plugin { return `export default ${JSON.stringify(url)}` }, - renderChunk(code, chunk, outputOptions) { - let match: RegExpExecArray | null - let s: MagicString | undefined - - // Urls added with JS using e.g. - // imgElement.src = "__VITE_ASSET__5aa0ddc0__" are using quotes - - // Urls added in CSS that is imported in JS end up like - // var inlined = ".inlined{color:green;background:url(__VITE_ASSET__5aa0ddc0__)}\n"; - - // In both cases, the wrapping should already be fine - - while ((match = assetUrlRE.exec(code))) { - s = s || (s = new MagicString(code)) - const [full, hash, postfix = ''] = match - // some internal plugins may still need to emit chunks (e.g. worker) so - // fallback to this.getFileName for that. TODO: remove, not needed - const file = getAssetFilename(hash, config) || this.getFileName(hash) - chunk.viteMetadata.importedAssets.add(cleanUrl(file)) - const filename = file + postfix - const replacement = toOutputFilePathInString( - filename, - 'asset', - chunk.fileName, - 'js', - config, - outputOptions.format - ) - const replacementString = - typeof replacement === 'string' - ? JSON.stringify(replacement).slice(1, -1) - : `"+${replacement.runtime}+"` - s.overwrite(match.index, match.index + full.length, replacementString, { - contentOnly: true - }) - } - - // Replace __VITE_PUBLIC_ASSET__5aa0ddc0__ with absolute paths - - const publicAssetUrlMap = publicAssetUrlCache.get(config)! - while ((match = publicAssetUrlRE.exec(code))) { - s = s || (s = new MagicString(code)) - const [full, hash] = match - const publicUrl = publicAssetUrlMap.get(hash)!.slice(1) - const replacement = toOutputFilePathInString( - publicUrl, - 'public', - chunk.fileName, - 'js', - config, - outputOptions.format - ) - const replacementString = - typeof replacement === 'string' - ? JSON.stringify(replacement).slice(1, -1) - : `"+${replacement.runtime}+"` - s.overwrite(match.index, match.index + full.length, replacementString, { - contentOnly: true - }) - } + renderChunk(code, chunk, opts) { + const s = renderAssetUrlInJS(this, config, chunk, opts, code) if (s) { return { diff --git a/packages/vite/src/node/plugins/completeSystemWrap.ts b/packages/vite/src/node/plugins/completeSystemWrap.ts new file mode 100644 index 00000000000000..700166fc5408bf --- /dev/null +++ b/packages/vite/src/node/plugins/completeSystemWrap.ts @@ -0,0 +1,23 @@ +import type { Plugin } from '../plugin' + +/** + * make sure systemjs register wrap to had complete parameters in system format + */ +export function completeSystemWrapPlugin(): Plugin { + const SystemJSWrapRE = /System.register\(.*\((exports)\)/g + + return { + name: 'vite:force-systemjs-wrap-complete', + + renderChunk(code, chunk, opts) { + if (opts.format === 'system') { + return { + code: code.replace(SystemJSWrapRE, (s, s1) => + s.replace(s1, 'exports, module') + ), + map: null + } + } + } + } +} diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index b2df2df9f7f0a5..c8c454d2f1a267 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -55,6 +55,7 @@ import { publicAssetUrlCache, publicAssetUrlRE, publicFileToBuiltUrl, + renderAssetUrlInJS, resolveAssetFileNames } from './asset' import type { ESBuildOptions } from './esbuild' @@ -556,27 +557,34 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { }) chunk.viteMetadata.importedCss.add(this.getFileName(fileHandle)) } else if (!config.build.ssr) { - // legacy build and inline css - - // __VITE_ASSET__ and __VITE_PUBLIC_ASSET__ urls are processed by - // the vite:asset plugin, don't call resolveAssetUrlsInCss here chunkCSS = await finalizeCss(chunkCSS, true, config) - + let cssString = JSON.stringify(chunkCSS) + cssString = + renderAssetUrlInJS( + this, + config, + chunk, + opts, + cssString + )?.toString() || cssString const style = `__vite_style__` const injectCode = `var ${style} = document.createElement('style');` + - `${style}.innerHTML = ${JSON.stringify(chunkCSS)};` + + `${style}.innerHTML = ${cssString};` + `document.head.appendChild(${style});` + const wrapIdx = code.indexOf('System.register') + const insertMark = "'use strict';" + const insertIdx = code.indexOf(insertMark, wrapIdx) + const s = new MagicString(code) + s.appendLeft(insertIdx + insertMark.length, injectCode) if (config.build.sourcemap) { - const s = new MagicString(code) - s.prepend(injectCode) // resolve public URL from CSS paths, we need to use absolute paths return { code: s.toString(), map: s.generateMap({ hires: true }) } } else { - return { code: injectCode + code } + return { code: s.toString() } } } } else { diff --git a/playground/vitestSetup.ts b/playground/vitestSetup.ts index 7b960811eb3f7e..3d3d45b5145f60 100644 --- a/playground/vitestSetup.ts +++ b/playground/vitestSetup.ts @@ -243,12 +243,15 @@ export async function startDefaultServe(): Promise { watcher = rollupOutput as RollupWatcher await notifyRebuildComplete(watcher) } - viteTestUrl = await startStaticServer(config) + viteTestUrl = await startStaticServer(resolvedConfig, config) await page.goto(viteTestUrl) } } -function startStaticServer(config?: InlineConfig): Promise { +function startStaticServer( + resolved: ResolvedConfig, + config?: InlineConfig +): Promise { if (!config) { // check if the test project has base config const configFile = resolve(rootDir, 'vite.config.js') @@ -267,6 +270,10 @@ function startStaticServer(config?: InlineConfig): Promise { if (config && config.__test__) { // @ts-ignore config.__test__() + // @ts-ignore + } else if (resolved && resolved.__test__) { + // @ts-ignore + resolved.__test__() } // start static file server diff --git a/playground/vue-legacy/Main.vue b/playground/vue-legacy/Main.vue new file mode 100644 index 00000000000000..a582c2e6aa6d62 --- /dev/null +++ b/playground/vue-legacy/Main.vue @@ -0,0 +1,32 @@ + + + diff --git a/playground/vue-legacy/__tests__/vue-legacy.spec.ts b/playground/vue-legacy/__tests__/vue-legacy.spec.ts new file mode 100644 index 00000000000000..908e04567ca35b --- /dev/null +++ b/playground/vue-legacy/__tests__/vue-legacy.spec.ts @@ -0,0 +1,10 @@ +import { test } from 'vitest' +import { getBg, untilUpdated } from '~utils' + +test('vue legacy assets', async () => { + await untilUpdated(() => getBg('.main'), 'assets/asset', true) +}) + +test('async vue legacy assets', async () => { + await untilUpdated(() => getBg('.module'), 'assets/asset', true) +}) diff --git a/playground/vue-legacy/assets/asset.png b/playground/vue-legacy/assets/asset.png new file mode 100644 index 00000000000000..1b3356a746b8bb Binary files /dev/null and b/playground/vue-legacy/assets/asset.png differ diff --git a/playground/vue-legacy/env.d.ts b/playground/vue-legacy/env.d.ts new file mode 100644 index 00000000000000..31dca6bb40c906 --- /dev/null +++ b/playground/vue-legacy/env.d.ts @@ -0,0 +1 @@ +declare module '*.png' diff --git a/playground/vue-legacy/index.html b/playground/vue-legacy/index.html new file mode 100644 index 00000000000000..0f7b79435ed47d --- /dev/null +++ b/playground/vue-legacy/index.html @@ -0,0 +1,7 @@ +
+ diff --git a/playground/vue-legacy/inline.css b/playground/vue-legacy/inline.css new file mode 100644 index 00000000000000..2207a25763ca6d --- /dev/null +++ b/playground/vue-legacy/inline.css @@ -0,0 +1,3 @@ +.inline-css { + color: #0088ff; +} diff --git a/playground/vue-legacy/module.vue b/playground/vue-legacy/module.vue new file mode 100644 index 00000000000000..10c7b42e4c4215 --- /dev/null +++ b/playground/vue-legacy/module.vue @@ -0,0 +1,13 @@ + + diff --git a/playground/vue-legacy/package.json b/playground/vue-legacy/package.json new file mode 100644 index 00000000000000..201a5ae47bb293 --- /dev/null +++ b/playground/vue-legacy/package.json @@ -0,0 +1,18 @@ +{ + "name": "test-vue-legacy", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.2.37" + }, + "devDependencies": { + "@vitejs/plugin-vue": "workspace:*", + "@vitejs/plugin-legacy": "workspace:*" + } +} diff --git a/playground/vue-legacy/vite.config.ts b/playground/vue-legacy/vite.config.ts new file mode 100644 index 00000000000000..5bb2f0efa06f53 --- /dev/null +++ b/playground/vue-legacy/vite.config.ts @@ -0,0 +1,35 @@ +import path from 'node:path' +import fs from 'node:fs' +import { defineConfig } from 'vite' +import vuePlugin from '@vitejs/plugin-vue' +import legacyPlugin from '@vitejs/plugin-legacy' + +export default defineConfig({ + base: '', + resolve: { + alias: { + '@': __dirname + } + }, + plugins: [ + legacyPlugin({ + targets: ['defaults', 'not IE 11', 'chrome > 48'] + }), + vuePlugin() + ], + build: { + minify: false + }, + // special test only hook + // for tests, remove `