Good')
- })
- const content = await page.waitForSelector('text=Good Html')
- expect(content).toBeTruthy()
- })
- })
-}
diff --git a/packages/playground/html/index.html b/packages/playground/html/index.html
deleted file mode 100644
index 7320ff2b097db0..00000000000000
--- a/packages/playground/html/index.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/packages/playground/html/invalid.html b/packages/playground/html/invalid.html
deleted file mode 100644
index 5b5cf429687466..00000000000000
--- a/packages/playground/html/invalid.html
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/packages/playground/html/nested/index.html b/packages/playground/html/nested/index.html
deleted file mode 100644
index 4fb855b783c890..00000000000000
--- a/packages/playground/html/nested/index.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
Nested
-
diff --git a/packages/playground/html/package.json b/packages/playground/html/package.json
deleted file mode 100644
index a101033f8d3470..00000000000000
--- a/packages/playground/html/package.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "test-html",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- }
-}
diff --git "a/packages/playground/html/unicode-path/\344\270\255\346\226\207-\343\201\253\343\201\273\343\202\223\343\201\224-\355\225\234\352\270\200-\360\237\214\225\360\237\214\226\360\237\214\227/index.html" "b/packages/playground/html/unicode-path/\344\270\255\346\226\207-\343\201\253\343\201\273\343\202\223\343\201\224-\355\225\234\352\270\200-\360\237\214\225\360\237\214\226\360\237\214\227/index.html"
deleted file mode 100644
index f3c55befe1f315..00000000000000
--- "a/packages/playground/html/unicode-path/\344\270\255\346\226\207-\343\201\253\343\201\273\343\202\223\343\201\224-\355\225\234\352\270\200-\360\237\214\225\360\237\214\226\360\237\214\227/index.html"
+++ /dev/null
@@ -1 +0,0 @@
-
unicode-path
diff --git a/packages/playground/html/vite.config.js b/packages/playground/html/vite.config.js
deleted file mode 100644
index 1703e02cc05366..00000000000000
--- a/packages/playground/html/vite.config.js
+++ /dev/null
@@ -1,162 +0,0 @@
-const { resolve } = require('path')
-
-/**
- * @type {import('vite').UserConfig}
- */
-module.exports = {
- build: {
- rollupOptions: {
- input: {
- main: resolve(__dirname, 'index.html'),
- nested: resolve(__dirname, 'nested/index.html'),
- scriptAsync: resolve(__dirname, 'scriptAsync.html'),
- scriptMixed: resolve(__dirname, 'scriptMixed.html'),
- emptyAttr: resolve(__dirname, 'emptyAttr.html'),
- link: resolve(__dirname, 'link.html'),
- 'link/target': resolve(__dirname, 'index.html'),
- zeroJS: resolve(__dirname, 'zeroJS.html'),
- noHead: resolve(__dirname, 'noHead.html'),
- noBody: resolve(__dirname, 'noBody.html'),
- inline1: resolve(__dirname, 'inline/shared-1.html'),
- inline2: resolve(__dirname, 'inline/shared-2.html'),
- inline3: resolve(__dirname, 'inline/unique.html'),
- unicodePath: resolve(
- __dirname,
- 'unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html'
- )
- }
- }
- },
-
- plugins: [
- {
- name: 'pre-transform',
- transformIndexHtml: {
- enforce: 'pre',
- transform(html, { filename }) {
- if (html.includes('/@vite/client')) {
- throw new Error('pre transform applied at wrong time!')
- }
- const head = `
-
-
-
-
{{ title }}
- `
- return `
-${filename.includes('noHead') ? '' : head}
-${
- filename.includes('noBody')
- ? html
- : `
- ${html}
-`
-}
-
- `
- }
- }
- },
- {
- name: 'string-transform',
- transformIndexHtml(html) {
- return html.replace('Hello', 'Transformed')
- }
- },
- {
- name: 'tags-transform',
- transformIndexHtml() {
- return [
- {
- tag: 'meta',
- attrs: { name: 'description', content: 'a vite app' }
- // default injection is head-prepend
- },
- {
- tag: 'meta',
- attrs: { name: 'keywords', content: 'es modules' },
- injectTo: 'head'
- }
- ]
- }
- },
- {
- name: 'combined-transform',
- transformIndexHtml(html) {
- return {
- html: html.replace('{{ title }}', 'Test HTML transforms'),
- tags: [
- {
- tag: 'p',
- attrs: { class: 'inject' },
- children: 'This is injected',
- injectTo: 'body'
- }
- ]
- }
- }
- },
- {
- name: 'serve-only-transform',
- transformIndexHtml(_, ctx) {
- if (ctx.server) {
- return [
- {
- tag: 'p',
- attrs: { class: 'server' },
- children: 'This is injected only during dev',
- injectTo: 'body'
- }
- ]
- }
- }
- },
- {
- name: 'build-only-transform',
- transformIndexHtml(_, ctx) {
- if (ctx.bundle) {
- return [
- {
- tag: 'p',
- attrs: { class: 'build' },
- children: 'This is injected only during build',
- injectTo: 'body'
- }
- ]
- }
- }
- },
- {
- name: 'path-conditional-transform',
- transformIndexHtml(_, ctx) {
- if (ctx.path.includes('nested')) {
- return [
- {
- tag: 'p',
- attrs: { class: 'conditional' },
- children: 'This is injected only for /nested/index.html',
- injectTo: 'body'
- }
- ]
- }
- }
- },
- {
- name: 'body-prepend-transform',
- transformIndexHtml() {
- return [
- {
- tag: 'noscript',
- children: '',
- injectTo: 'body'
- },
- {
- tag: 'noscript',
- children: '',
- injectTo: 'body-prepend'
- }
- ]
- }
- }
- ]
-}
diff --git a/packages/playground/json/__tests__/json.spec.ts b/packages/playground/json/__tests__/json.spec.ts
deleted file mode 100644
index 2897ee22332e44..00000000000000
--- a/packages/playground/json/__tests__/json.spec.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { isBuild } from '../../testUtils'
-
-const json = require('../test.json')
-const deepJson = require('vue/package.json')
-const stringified = JSON.stringify(json)
-const deepStringified = JSON.stringify(deepJson)
-
-test('default import', async () => {
- expect(await page.textContent('.full')).toBe(stringified)
-})
-
-test('named import', async () => {
- expect(await page.textContent('.named')).toBe(json.hello)
-})
-
-test('deep import', async () => {
- expect(await page.textContent('.deep-full')).toBe(deepStringified)
-})
-
-test('named deep import', async () => {
- expect(await page.textContent('.deep-named')).toBe(deepJson.name)
-})
-
-test('dynamic import', async () => {
- expect(await page.textContent('.dynamic')).toBe(stringified)
-})
-
-test('dynamic import, named', async () => {
- expect(await page.textContent('.dynamic-named')).toBe(json.hello)
-})
-
-test('fetch', async () => {
- expect(await page.textContent('.fetch')).toBe(stringified)
-})
-
-test('?url', async () => {
- expect(await page.textContent('.url')).toMatch(
- isBuild ? 'data:application/json' : '/test.json'
- )
-})
-
-test('?raw', async () => {
- expect(await page.textContent('.raw')).toBe(
- require('fs').readFileSync(require.resolve('../test.json'), 'utf-8')
- )
-})
diff --git a/packages/playground/json/index.html b/packages/playground/json/index.html
deleted file mode 100644
index cf16636f91cb68..00000000000000
--- a/packages/playground/json/index.html
+++ /dev/null
@@ -1,58 +0,0 @@
-
Normal Import
-
-
-
-
Deep Import
-
-
-
-
Dynamic Import
-
-
-
-
fetch
-
-
-
Importing as URL
-
-
-
Raw Import
-
-
-
JSON Module
-
-
-
diff --git a/packages/playground/json/json-module/package.json b/packages/playground/json/json-module/package.json
deleted file mode 100644
index 17db87793a74e3..00000000000000
--- a/packages/playground/json/json-module/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "json-module",
- "version": "0.0.0"
-}
diff --git a/packages/playground/json/package.json b/packages/playground/json/package.json
deleted file mode 100644
index 203846bab73b48..00000000000000
--- a/packages/playground/json/package.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "test-json",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- },
- "devDependencies": {
- "vue": "^3.2.25",
- "json-module": "file:./json-module"
- }
-}
diff --git a/packages/playground/legacy/__tests__/legacy.spec.ts b/packages/playground/legacy/__tests__/legacy.spec.ts
deleted file mode 100644
index b8025694437502..00000000000000
--- a/packages/playground/legacy/__tests__/legacy.spec.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import {
- listAssets,
- findAssetFile,
- isBuild,
- readManifest,
- untilUpdated,
- getColor
-} from '../../testUtils'
-
-test('should work', async () => {
- expect(await page.textContent('#app')).toMatch('Hello')
-})
-
-test('import.meta.env.LEGACY', async () => {
- expect(await page.textContent('#env')).toMatch(isBuild ? 'true' : 'false')
-})
-
-// https://github.com/vitejs/vite/issues/3400
-test('transpiles down iterators correctly', async () => {
- expect(await page.textContent('#iterators')).toMatch('hello')
-})
-
-test('wraps with iife', async () => {
- expect(await page.textContent('#babel-helpers')).toMatch(
- 'exposed babel helpers: false'
- )
-})
-
-test('generates assets', async () => {
- await untilUpdated(
- () => page.textContent('#assets'),
- isBuild
- ? [
- 'index: 404',
- 'index-legacy: 404',
- 'chunk-async: 404',
- 'chunk-async-legacy: 404',
- 'immutable-chunk: 200',
- 'immutable-chunk-legacy: 200',
- 'polyfills-legacy: 404'
- ].join('\n')
- : [
- 'index: 404',
- 'index-legacy: 404',
- 'chunk-async: 404',
- 'chunk-async-legacy: 404',
- 'immutable-chunk: 404',
- 'immutable-chunk-legacy: 404',
- 'polyfills-legacy: 404'
- ].join('\n'),
- true
- )
-})
-
-test('correctly emits styles', async () => {
- expect(await getColor('#app')).toBe('red')
-})
-
-if (isBuild) {
- test('should generate correct manifest', async () => {
- const manifest = readManifest()
- expect(manifest['../../../vite/legacy-polyfills']).toBeDefined()
- expect(manifest['../../../vite/legacy-polyfills'].src).toBe(
- '../../../vite/legacy-polyfills'
- )
- })
-
- test('should minify legacy chunks with terser', async () => {
- // This is a ghetto heuristic, but terser output seems to reliably start
- // with one of the following, and non-terser output (including unminified or
- // ebuild-minified) does not!
- const terserPatt = /^(?:!function|System.register)/
-
- expect(findAssetFile(/chunk-async-legacy/)).toMatch(terserPatt)
- expect(findAssetFile(/chunk-async\./)).not.toMatch(terserPatt)
- expect(findAssetFile(/immutable-chunk-legacy/)).toMatch(terserPatt)
- expect(findAssetFile(/immutable-chunk\./)).not.toMatch(terserPatt)
- expect(findAssetFile(/index-legacy/)).toMatch(terserPatt)
- expect(findAssetFile(/index\./)).not.toMatch(terserPatt)
- expect(findAssetFile(/polyfills-legacy/)).toMatch(terserPatt)
- })
-
- test('should emit css file', async () => {
- expect(listAssets().some((filename) => filename.endsWith('.css')))
- })
-}
diff --git a/packages/playground/legacy/__tests__/ssr/legacy-ssr.spec.ts b/packages/playground/legacy/__tests__/ssr/legacy-ssr.spec.ts
deleted file mode 100644
index dad9b94d83509e..00000000000000
--- a/packages/playground/legacy/__tests__/ssr/legacy-ssr.spec.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { isBuild } from '../../../testUtils'
-import { port } from './serve'
-
-const url = `http://localhost:${port}`
-
-if (isBuild) {
- test('should work', async () => {
- await page.goto(url)
- expect(await page.textContent('#app')).toMatch('Hello')
- })
-
- test('import.meta.env.LEGACY', async () => {
- // SSR build is always modern
- expect(await page.textContent('#env')).toMatch('false')
- })
-} else {
- // this test doesn't support serve mode
- // must contain at least one test
- test('should work', () => void 0)
-}
diff --git a/packages/playground/legacy/__tests__/ssr/serve.js b/packages/playground/legacy/__tests__/ssr/serve.js
deleted file mode 100644
index df43f180afb188..00000000000000
--- a/packages/playground/legacy/__tests__/ssr/serve.js
+++ /dev/null
@@ -1,52 +0,0 @@
-// @ts-check
-// this is automtically detected by scripts/jestPerTestSetup.ts and will replace
-// the default e2e test serve behavior
-const path = require('path')
-
-const port = (exports.port = 9527)
-
-/**
- * @param {string} root
- * @param {boolean} _isProd
- */
-exports.serve = async function serve(root, _isProd) {
- const { build } = require('vite')
- await build({
- root,
- logLevel: 'silent',
- build: {
- target: 'esnext',
- ssr: 'entry-server.js',
- outDir: 'dist/server'
- }
- })
-
- const express = require('express')
- const app = express()
-
- app.use('/', async (_req, res) => {
- const { render } = require(path.resolve(
- root,
- './dist/server/entry-server.js'
- ))
- const html = await render()
- res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
- })
-
- return new Promise((resolve, reject) => {
- try {
- const server = app.listen(port, () => {
- resolve({
- // for test teardown
- async close() {
- await new Promise((resolve) => {
- server.close(resolve)
- })
- }
- })
- })
- } catch (e) {
- reject(e)
- }
- })
-}
diff --git a/packages/playground/legacy/immutable-chunk.js b/packages/playground/legacy/immutable-chunk.js
deleted file mode 100644
index 4227b718b98309..00000000000000
--- a/packages/playground/legacy/immutable-chunk.js
+++ /dev/null
@@ -1,18 +0,0 @@
-const chunks = [
- 'index',
- 'index-legacy',
- 'chunk-async',
- 'chunk-async-legacy',
- 'immutable-chunk',
- 'immutable-chunk-legacy',
- 'polyfills-legacy'
-]
-
-export function fn() {
- return Promise.all(
- chunks.map(async (name) => {
- const response = await fetch(`/assets/${name}.js`)
- return `${name}: ${response.status}`
- })
- )
-}
diff --git a/packages/playground/legacy/index.html b/packages/playground/legacy/index.html
deleted file mode 100644
index bdc2feac6b4fbe..00000000000000
--- a/packages/playground/legacy/index.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/packages/playground/legacy/main.js b/packages/playground/legacy/main.js
deleted file mode 100644
index b05acf439bdff8..00000000000000
--- a/packages/playground/legacy/main.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import './style.css'
-
-async function run() {
- const { fn } = await import('./async.js')
- fn()
-}
-
-run()
-
-let isLegacy
-
-// make sure that branching works despite esbuild's constant folding (#1999)
-if (import.meta.env.LEGACY) {
- if (import.meta.env.LEGACY === true) isLegacy = true
-} else {
- if (import.meta.env.LEGACY === false) isLegacy = false
-}
-
-text('#env', `is legacy: ${isLegacy}`)
-
-// Iterators
-text('#iterators', [...new Set(['hello'])].join(''))
-
-// babel-helpers
-// Using `String.raw` to inject `@babel/plugin-transform-template-literals`
-// helpers.
-text(
- '#babel-helpers',
- String.raw`exposed babel helpers: ${window._templateObject != null}`
-)
-
-// dynamic chunk names
-import('./immutable-chunk.js')
- .then(({ fn }) => fn())
- .then((assets) => {
- text('#assets', assets.join('\n'))
- })
-
-function text(el, text) {
- document.querySelector(el).textContent = text
-}
diff --git a/packages/playground/legacy/package.json b/packages/playground/legacy/package.json
deleted file mode 100644
index 3a3315c42aa832..00000000000000
--- a/packages/playground/legacy/package.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "name": "test-legacy",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build --debug legacy",
- "build:custom-filename": "vite --config ./vite.config-custom-filename.js build --debug legacy",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- },
- "devDependencies": {
- "@vitejs/plugin-legacy": "workspace:*",
- "express": "^4.17.1"
- }
-}
diff --git a/packages/playground/legacy/vite.config-custom-filename.js b/packages/playground/legacy/vite.config-custom-filename.js
deleted file mode 100644
index 9a96133b015588..00000000000000
--- a/packages/playground/legacy/vite.config-custom-filename.js
+++ /dev/null
@@ -1,15 +0,0 @@
-const legacy = require('@vitejs/plugin-legacy').default
-
-module.exports = {
- plugins: [legacy()],
- build: {
- manifest: true,
- minify: false,
- rollupOptions: {
- output: {
- entryFileNames: `assets/[name].js`,
- chunkFileNames: `assets/[name].js`
- }
- }
- }
-}
diff --git a/packages/playground/legacy/vite.config.js b/packages/playground/legacy/vite.config.js
deleted file mode 100644
index 90d3be7f7c56a0..00000000000000
--- a/packages/playground/legacy/vite.config.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const fs = require('fs')
-const path = require('path')
-const legacy = require('@vitejs/plugin-legacy').default
-
-module.exports = {
- plugins: [
- legacy({
- targets: 'IE 11'
- })
- ],
-
- build: {
- cssCodeSplit: false,
- manifest: true,
- rollupOptions: {
- output: {
- chunkFileNames(chunkInfo) {
- if (chunkInfo.name === 'immutable-chunk') {
- return `assets/${chunkInfo.name}.js`
- }
-
- return `assets/chunk-[name].[hash].js`
- }
- }
- }
- },
-
- // special test only hook
- // for tests, remove `
-
-
-
-
-
-
-
-
diff --git a/packages/playground/lib/package.json b/packages/playground/lib/package.json
deleted file mode 100644
index 2c3ae4be3d4bcb..00000000000000
--- a/packages/playground/lib/package.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "@example/my-lib",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- }
-}
diff --git a/packages/playground/lib/src/main.js b/packages/playground/lib/src/main.js
deleted file mode 100644
index 2422edf5829a0e..00000000000000
--- a/packages/playground/lib/src/main.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function myLib(sel) {
- document.querySelector(sel).textContent = 'It works'
-}
diff --git a/packages/playground/lib/src/main2.js b/packages/playground/lib/src/main2.js
deleted file mode 100644
index 0c729fad8d165c..00000000000000
--- a/packages/playground/lib/src/main2.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export default async function message(sel) {
- const message = await import('./message.js')
- document.querySelector(sel).textContent = message.default
-}
diff --git a/packages/playground/lib/vite.config.js b/packages/playground/lib/vite.config.js
deleted file mode 100644
index 50cd188b1a40cc..00000000000000
--- a/packages/playground/lib/vite.config.js
+++ /dev/null
@@ -1,31 +0,0 @@
-const fs = require('fs')
-const path = require('path')
-
-/**
- * @type {import('vite').UserConfig}
- */
-module.exports = {
- build: {
- lib: {
- entry: path.resolve(__dirname, 'src/main.js'),
- name: 'MyLib',
- formats: ['es', 'umd', 'iife'],
- fileName: (format) => `my-lib-custom-filename.${format}.js`
- }
- },
- plugins: [
- {
- name: 'emit-index',
- generateBundle() {
- this.emitFile({
- type: 'asset',
- fileName: 'index.html',
- source: fs.readFileSync(
- path.resolve(__dirname, 'index.dist.html'),
- 'utf-8'
- )
- })
- }
- }
- ]
-}
diff --git a/packages/playground/lib/vite.dyimport.config.js b/packages/playground/lib/vite.dyimport.config.js
deleted file mode 100644
index 76311f4b8ba138..00000000000000
--- a/packages/playground/lib/vite.dyimport.config.js
+++ /dev/null
@@ -1,18 +0,0 @@
-const fs = require('fs')
-const path = require('path')
-
-/**
- * @type {import('vite').UserConfig}
- */
-module.exports = {
- build: {
- minify: false,
- lib: {
- entry: path.resolve(__dirname, 'src/main2.js'),
- formats: ['es'],
- name: 'message',
- fileName: () => 'dynamic-import-message.js'
- },
- outDir: 'dist/lib'
- }
-}
diff --git a/packages/playground/multiple-entrypoints/__tests__/multiple-entrypoints.spec.ts b/packages/playground/multiple-entrypoints/__tests__/multiple-entrypoints.spec.ts
deleted file mode 100644
index 56c0b46c8a3e6f..00000000000000
--- a/packages/playground/multiple-entrypoints/__tests__/multiple-entrypoints.spec.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { getColor, untilUpdated } from '../../testUtils'
-
-test('should have css applied on second dynamic import', async () => {
- await untilUpdated(() => page.textContent('.content'), 'Initial', true)
- await page.click('.b')
-
- await untilUpdated(() => page.textContent('.content'), 'Reference', true)
- expect(await getColor('.content')).toBe('red')
-})
diff --git a/packages/playground/multiple-entrypoints/package.json b/packages/playground/multiple-entrypoints/package.json
deleted file mode 100644
index 6c338a64518ddb..00000000000000
--- a/packages/playground/multiple-entrypoints/package.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "multiple-entrypoints",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- },
- "devDependencies": {
- "fast-glob": "^3.2.11",
- "sass": "^1.43.4"
- }
-}
diff --git a/packages/playground/multiple-entrypoints/vite.config.js b/packages/playground/multiple-entrypoints/vite.config.js
deleted file mode 100644
index c2a44858a3ce6b..00000000000000
--- a/packages/playground/multiple-entrypoints/vite.config.js
+++ /dev/null
@@ -1,40 +0,0 @@
-const { resolve } = require('path')
-const fs = require('fs')
-
-module.exports = {
- build: {
- outDir: './dist',
- emptyOutDir: true,
- rollupOptions: {
- preserveEntrySignatures: 'strict',
- input: {
- a0: resolve(__dirname, 'entrypoints/a0.js'),
- a1: resolve(__dirname, 'entrypoints/a1.js'),
- a2: resolve(__dirname, 'entrypoints/a2.js'),
- a3: resolve(__dirname, 'entrypoints/a3.js'),
- a4: resolve(__dirname, 'entrypoints/a4.js'),
- a5: resolve(__dirname, 'entrypoints/a5.js'),
- a6: resolve(__dirname, 'entrypoints/a6.js'),
- a7: resolve(__dirname, 'entrypoints/a7.js'),
- a8: resolve(__dirname, 'entrypoints/a8.js'),
- a9: resolve(__dirname, 'entrypoints/a9.js'),
- a10: resolve(__dirname, 'entrypoints/a10.js'),
- a11: resolve(__dirname, 'entrypoints/a11.js'),
- a12: resolve(__dirname, 'entrypoints/a12.js'),
- a13: resolve(__dirname, 'entrypoints/a13.js'),
- a14: resolve(__dirname, 'entrypoints/a14.js'),
- a15: resolve(__dirname, 'entrypoints/a15.js'),
- a16: resolve(__dirname, 'entrypoints/a16.js'),
- a17: resolve(__dirname, 'entrypoints/a17.js'),
- a18: resolve(__dirname, 'entrypoints/a18.js'),
- a19: resolve(__dirname, 'entrypoints/a19.js'),
- a20: resolve(__dirname, 'entrypoints/a20.js'),
- a21: resolve(__dirname, 'entrypoints/a21.js'),
- a22: resolve(__dirname, 'entrypoints/a22.js'),
- a23: resolve(__dirname, 'entrypoints/a23.js'),
- a24: resolve(__dirname, 'entrypoints/a24.js'),
- index: resolve(__dirname, './index.html')
- }
- }
- }
-}
diff --git a/packages/playground/nested-deps/__tests__/nested-deps.spec.ts b/packages/playground/nested-deps/__tests__/nested-deps.spec.ts
deleted file mode 100644
index 2ef0e191da7b50..00000000000000
--- a/packages/playground/nested-deps/__tests__/nested-deps.spec.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-test('handle nested package', async () => {
- expect(await page.textContent('.a')).toBe('A@2.0.0')
- expect(await page.textContent('.b')).toBe('B@1.0.0')
- expect(await page.textContent('.nested-a')).toBe('A@1.0.0')
- const c = await page.textContent('.c')
- expect(c).toBe('es-C@1.0.0')
- expect(await page.textContent('.side-c')).toBe(c)
- expect(await page.textContent('.d')).toBe('D@1.0.0')
- expect(await page.textContent('.nested-d')).toBe('D-nested@1.0.0')
- expect(await page.textContent('.nested-e')).toBe('1')
-})
diff --git a/packages/playground/nested-deps/index.html b/packages/playground/nested-deps/index.html
deleted file mode 100644
index 3243c1689bf0cd..00000000000000
--- a/packages/playground/nested-deps/index.html
+++ /dev/null
@@ -1,48 +0,0 @@
-
direct dependency A
-
-
-
direct dependency B
-
-
-
nested dependency A
-
-
-
direct dependency C
-
-
-
side dependency C
-
-
-
direct dependency D
-
-
-
nested dependency nested-D (dep of D)
-
-
-
exclude dependency of pre-bundled dependency
-
nested module instance count:
-
-
diff --git a/packages/playground/nested-deps/package.json b/packages/playground/nested-deps/package.json
deleted file mode 100644
index d7450d0545fcb4..00000000000000
--- a/packages/playground/nested-deps/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "@test/nested-deps",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- },
- "dependencies": {
- "test-package-a": "link:./test-package-a",
- "test-package-b": "link:./test-package-b",
- "test-package-c": "link:./test-package-c",
- "test-package-d": "link:./test-package-d",
- "test-package-e": "link:./test-package-e"
- }
-}
diff --git a/packages/playground/nested-deps/test-package-a/package.json b/packages/playground/nested-deps/test-package-a/package.json
deleted file mode 100644
index 688fab78bab766..00000000000000
--- a/packages/playground/nested-deps/test-package-a/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "test-package-a",
- "private": true,
- "version": "2.0.0",
- "main": "index.js"
-}
diff --git a/packages/playground/nested-deps/test-package-b/package.json b/packages/playground/nested-deps/test-package-b/package.json
deleted file mode 100644
index 6e32e8eba153a1..00000000000000
--- a/packages/playground/nested-deps/test-package-b/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "test-package-b",
- "private": true,
- "version": "1.0.0",
- "main": "index.js"
-}
diff --git a/packages/playground/nested-deps/test-package-c/package.json b/packages/playground/nested-deps/test-package-c/package.json
deleted file mode 100644
index 47672d07b3881f..00000000000000
--- a/packages/playground/nested-deps/test-package-c/package.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name": "test-package-c",
- "private": true,
- "version": "1.0.0",
- "main": "index.js",
- "module": "index-es.js"
-}
diff --git a/packages/playground/nested-deps/test-package-c/side.js b/packages/playground/nested-deps/test-package-c/side.js
deleted file mode 100644
index 4d46da7c26e02e..00000000000000
--- a/packages/playground/nested-deps/test-package-c/side.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default as C } from 'test-package-c'
diff --git a/packages/playground/nested-deps/test-package-d/index.js b/packages/playground/nested-deps/test-package-d/index.js
deleted file mode 100644
index f5b35d06fd5001..00000000000000
--- a/packages/playground/nested-deps/test-package-d/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export { default as nestedD } from 'test-package-d-nested'
-
-export default 'D@1.0.0'
diff --git a/packages/playground/nested-deps/test-package-d/package.json b/packages/playground/nested-deps/test-package-d/package.json
deleted file mode 100644
index e9f522c2c5ddf6..00000000000000
--- a/packages/playground/nested-deps/test-package-d/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "test-package-d",
- "private": true,
- "version": "1.0.0",
- "main": "index.js",
- "dependencies": {
- "test-package-d-nested": "link:./test-package-d-nested"
- }
-}
diff --git a/packages/playground/nested-deps/test-package-d/test-package-d-nested/package.json b/packages/playground/nested-deps/test-package-d/test-package-d-nested/package.json
deleted file mode 100644
index 50f1123b6f7ff7..00000000000000
--- a/packages/playground/nested-deps/test-package-d/test-package-d-nested/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "test-package-d-nested",
- "private": true,
- "version": "1.0.0",
- "main": "index.js"
-}
diff --git a/packages/playground/nested-deps/test-package-e/index.js b/packages/playground/nested-deps/test-package-e/index.js
deleted file mode 100644
index 3d9c38b0ab3886..00000000000000
--- a/packages/playground/nested-deps/test-package-e/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export { testIncluded } from 'test-package-e-included'
-export { testExcluded } from 'test-package-e-excluded'
diff --git a/packages/playground/nested-deps/test-package-e/package.json b/packages/playground/nested-deps/test-package-e/package.json
deleted file mode 100644
index 45779d1a7676ad..00000000000000
--- a/packages/playground/nested-deps/test-package-e/package.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "test-package-e",
- "private": true,
- "version": "0.1.0",
- "main": "index.js",
- "dependencies": {
- "test-package-e-excluded": "link:./test-package-e-excluded",
- "test-package-e-included": "link:./test-package-e-included"
- }
-}
diff --git a/packages/playground/nested-deps/test-package-e/test-package-e-excluded/package.json b/packages/playground/nested-deps/test-package-e/test-package-e-excluded/package.json
deleted file mode 100644
index 8722324da53499..00000000000000
--- a/packages/playground/nested-deps/test-package-e/test-package-e-excluded/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "test-package-e-excluded",
- "private": true,
- "version": "0.1.0",
- "main": "index.js"
-}
diff --git a/packages/playground/nested-deps/test-package-e/test-package-e-included/index.js b/packages/playground/nested-deps/test-package-e/test-package-e-included/index.js
deleted file mode 100644
index 23239f157bfa38..00000000000000
--- a/packages/playground/nested-deps/test-package-e/test-package-e-included/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { testExcluded } from 'test-package-e-excluded'
-
-export function testIncluded() {
- return testExcluded()
-}
diff --git a/packages/playground/nested-deps/test-package-e/test-package-e-included/package.json b/packages/playground/nested-deps/test-package-e/test-package-e-included/package.json
deleted file mode 100644
index 37198ee7d6a7c7..00000000000000
--- a/packages/playground/nested-deps/test-package-e/test-package-e-included/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "test-package-e-included",
- "private": true,
- "version": "0.1.0",
- "main": "index.js",
- "dependencies": {
- "test-package-e-excluded": "link:../test-package-e-excluded"
- }
-}
diff --git a/packages/playground/nested-deps/vite.config.js b/packages/playground/nested-deps/vite.config.js
deleted file mode 100644
index 015598af64b016..00000000000000
--- a/packages/playground/nested-deps/vite.config.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * @type {import('vite').UserConfig}
- */
-module.exports = {
- optimizeDeps: {
- include: [
- 'test-package-a',
- 'test-package-b',
- 'test-package-c',
- 'test-package-c/side',
- 'test-package-d > test-package-d-nested',
- 'test-package-e-included'
- ],
- exclude: ['test-package-d', 'test-package-e-excluded']
- }
-}
diff --git a/packages/playground/optimize-deps/__tests__/optimize-deps.spec.ts b/packages/playground/optimize-deps/__tests__/optimize-deps.spec.ts
deleted file mode 100644
index d95a6d984cd9aa..00000000000000
--- a/packages/playground/optimize-deps/__tests__/optimize-deps.spec.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { getColor, isBuild } from '../../testUtils'
-
-test('default + named imports from cjs dep (react)', async () => {
- expect(await page.textContent('.cjs button')).toBe('count is 0')
- await page.click('.cjs button')
- expect(await page.textContent('.cjs button')).toBe('count is 1')
-})
-
-test('named imports from webpacked cjs (phoenix)', async () => {
- expect(await page.textContent('.cjs-phoenix')).toBe('ok')
-})
-
-test('default import from webpacked cjs (clipboard)', async () => {
- expect(await page.textContent('.cjs-clipboard')).toBe('ok')
-})
-
-test('dynamic imports from cjs dep (react)', async () => {
- expect(await page.textContent('.cjs-dynamic button')).toBe('count is 0')
- await page.click('.cjs-dynamic button')
- expect(await page.textContent('.cjs-dynamic button')).toBe('count is 1')
-})
-
-test('dynamic named imports from webpacked cjs (phoenix)', async () => {
- expect(await page.textContent('.cjs-dynamic-phoenix')).toBe('ok')
-})
-
-test('dynamic default import from webpacked cjs (clipboard)', async () => {
- expect(await page.textContent('.cjs-dynamic-clipboard')).toBe('ok')
-})
-
-test('dynamic default import from cjs (cjs-dynamic-dep-cjs-compiled-from-esm)', async () => {
- expect(await page.textContent('.cjs-dynamic-dep-cjs-compiled-from-esm')).toBe(
- 'ok'
- )
-})
-
-test('dynamic default import from cjs (cjs-dynamic-dep-cjs-compiled-from-cjs)', async () => {
- expect(await page.textContent('.cjs-dynamic-dep-cjs-compiled-from-cjs')).toBe(
- 'ok'
- )
-})
-
-test('dedupe', async () => {
- expect(await page.textContent('.dedupe button')).toBe('count is 0')
- await page.click('.dedupe button')
- expect(await page.textContent('.dedupe button')).toBe('count is 1')
-})
-
-test('cjs browser field (axios)', async () => {
- expect(await page.textContent('.cjs-browser-field')).toBe('pong')
-})
-
-test('dep from linked dep (lodash-es)', async () => {
- expect(await page.textContent('.deps-linked')).toBe('fooBarBaz')
-})
-
-test('forced include', async () => {
- expect(await page.textContent('.force-include')).toMatch(`[success]`)
-})
-
-test('import * from optimized dep', async () => {
- expect(await page.textContent('.import-star')).toMatch(`[success]`)
-})
-
-test('import from dep with .notjs files', async () => {
- expect(await page.textContent('.not-js')).toMatch(`[success]`)
-})
-
-test('dep with dynamic import', async () => {
- expect(await page.textContent('.dep-with-dynamic-import')).toMatch(
- `[success]`
- )
-})
-
-test('dep with css import', async () => {
- expect(await getColor('h1')).toBe('red')
-})
-
-test('dep w/ non-js files handled via plugin', async () => {
- expect(await page.textContent('.plugin')).toMatch(`[success]`)
-})
-
-test('vue + vuex', async () => {
- expect(await page.textContent('.vue')).toMatch(`[success]`)
-})
-
-test('esbuild-plugin', async () => {
- expect(await page.textContent('.esbuild-plugin')).toMatch(
- isBuild ? `Hello from a package` : `Hello from an esbuild plugin`
- )
-})
-
-test('import from hidden dir', async () => {
- expect(await page.textContent('.hidden-dir')).toBe('hello!')
-})
-
-test('import optimize-excluded package that imports optimized-included package', async () => {
- expect(await page.textContent('.nested-include')).toBe('nested-include')
-})
-
-test('import aliased package with colon', async () => {
- expect(await page.textContent('.url')).toBe('vitejs.dev')
-})
-
-test('variable names are reused in different scripts', async () => {
- expect(await page.textContent('.reused-variable-names')).toBe('reused')
-})
diff --git a/packages/playground/optimize-deps/cjs-dynamic.js b/packages/playground/optimize-deps/cjs-dynamic.js
deleted file mode 100644
index 91dc5a964d5481..00000000000000
--- a/packages/playground/optimize-deps/cjs-dynamic.js
+++ /dev/null
@@ -1,53 +0,0 @@
-// test dynamic import to cjs deps
-// mostly ensuring consistency between dev server behavior and build behavior
-// of @rollup/plugin-commonjs
-;(async () => {
- const { useState } = await import('react')
- const React = (await import('react')).default
- const ReactDOM = await import('react-dom')
-
- const clip = await import('clipboard')
- if (typeof clip.default === 'function') {
- text('.cjs-dynamic-clipboard', 'ok')
- }
-
- const { Socket } = await import('phoenix')
- if (typeof Socket === 'function') {
- text('.cjs-dynamic-phoenix', 'ok')
- }
-
- const cjsFromESM = await import('dep-cjs-compiled-from-esm')
- console.log('cjsFromESM', cjsFromESM)
- if (typeof cjsFromESM.default === 'function') {
- text('.cjs-dynamic-dep-cjs-compiled-from-esm', 'ok')
- }
-
- const cjsFromCJS = await import('dep-cjs-compiled-from-cjs')
- console.log('cjsFromCJS', cjsFromCJS)
- if (typeof cjsFromCJS.default === 'function') {
- text('.cjs-dynamic-dep-cjs-compiled-from-cjs', 'ok')
- }
-
- function App() {
- const [count, setCount] = useState(0)
-
- return React.createElement(
- 'button',
- {
- onClick() {
- setCount(count + 1)
- }
- },
- `count is ${count}`
- )
- }
-
- ReactDOM.render(
- React.createElement(App),
- document.querySelector('.cjs-dynamic')
- )
-
- function text(el, text) {
- document.querySelector(el).textContent = text
- }
-})()
diff --git a/packages/playground/optimize-deps/cjs.js b/packages/playground/optimize-deps/cjs.js
deleted file mode 100644
index 9dc613c4b3ba71..00000000000000
--- a/packages/playground/optimize-deps/cjs.js
+++ /dev/null
@@ -1,35 +0,0 @@
-// test importing both default and named exports from a CommonJS module
-// React is the ultimate test of this because its dynamic exports assignments
-// are not statically detectable by @rollup/plugin-commonjs.
-import React, { useState } from 'react'
-import ReactDOM from 'react-dom'
-import { Socket } from 'phoenix'
-import clip from 'clipboard'
-
-if (typeof clip === 'function') {
- text('.cjs-clipboard', 'ok')
-}
-
-if (typeof Socket === 'function') {
- text('.cjs-phoenix', 'ok')
-}
-
-function App() {
- const [count, setCount] = useState(0)
-
- return React.createElement(
- 'button',
- {
- onClick() {
- setCount(count + 1)
- }
- },
- `count is ${count}`
- )
-}
-
-ReactDOM.render(React.createElement(App), document.querySelector('.cjs'))
-
-function text(el, text) {
- document.querySelector(el).textContent = text
-}
diff --git a/packages/playground/optimize-deps/dedupe.js b/packages/playground/optimize-deps/dedupe.js
deleted file mode 100644
index d04726330a6138..00000000000000
--- a/packages/playground/optimize-deps/dedupe.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react'
-import ReactDOM from 'react-dom'
-
-// #1302: The linked package has a different version of React in its deps
-// and is itself optimized. Without `dedupe`, the linked package is optimized
-// with a separate copy of React included, and results in runtime errors.
-import { useCount } from 'dep-linked-include/index.mjs'
-
-function App() {
- const [count, setCount] = useCount()
-
- return React.createElement(
- 'button',
- {
- onClick() {
- setCount(count + 1)
- }
- },
- `count is ${count}`
- )
-}
-
-ReactDOM.render(React.createElement(App), document.querySelector('.dedupe'))
diff --git a/packages/playground/optimize-deps/dep-cjs-compiled-from-cjs/package.json b/packages/playground/optimize-deps/dep-cjs-compiled-from-cjs/package.json
deleted file mode 100644
index 8fbd661730eafd..00000000000000
--- a/packages/playground/optimize-deps/dep-cjs-compiled-from-cjs/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "dep-cjs-compiled-from-cjs",
- "private": true,
- "version": "0.0.0",
- "main": "index.js"
-}
diff --git a/packages/playground/optimize-deps/dep-cjs-compiled-from-esm/package.json b/packages/playground/optimize-deps/dep-cjs-compiled-from-esm/package.json
deleted file mode 100644
index 27ba12a1ec6f7b..00000000000000
--- a/packages/playground/optimize-deps/dep-cjs-compiled-from-esm/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "dep-cjs-compiled-from-esm",
- "private": true,
- "version": "0.0.0",
- "main": "index.js"
-}
diff --git a/packages/playground/optimize-deps/dep-esbuild-plugin-transform/package.json b/packages/playground/optimize-deps/dep-esbuild-plugin-transform/package.json
deleted file mode 100644
index 4adb0e7032ae20..00000000000000
--- a/packages/playground/optimize-deps/dep-esbuild-plugin-transform/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "dep-esbuild-plugin-transform",
- "private": true,
- "version": "0.0.0",
- "main": "index.js"
-}
diff --git a/packages/playground/optimize-deps/dep-linked-include/index.mjs b/packages/playground/optimize-deps/dep-linked-include/index.mjs
deleted file mode 100644
index 81c43abc0387ac..00000000000000
--- a/packages/playground/optimize-deps/dep-linked-include/index.mjs
+++ /dev/null
@@ -1,21 +0,0 @@
-export { msg } from './foo.js'
-
-import { useState } from 'react'
-
-export function useCount() {
- return useState(0)
-}
-
-// test dep with css/asset imports
-import './test.css'
-
-// test importing node built-ins
-import fs from 'fs'
-
-if (false) {
- fs.readFileSync()
-} else {
- console.log('ok')
-}
-
-export { default as VueSFC } from './Test.vue'
diff --git a/packages/playground/optimize-deps/dep-linked-include/package.json b/packages/playground/optimize-deps/dep-linked-include/package.json
deleted file mode 100644
index 0cfabdb9a2f2b9..00000000000000
--- a/packages/playground/optimize-deps/dep-linked-include/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "dep-linked-include",
- "private": true,
- "version": "0.0.0",
- "main": "index.mjs",
- "dependencies": {
- "react": "17.0.2"
- }
-}
diff --git a/packages/playground/optimize-deps/dep-linked-include/test.css b/packages/playground/optimize-deps/dep-linked-include/test.css
deleted file mode 100644
index 60f1eab97137f7..00000000000000
--- a/packages/playground/optimize-deps/dep-linked-include/test.css
+++ /dev/null
@@ -1,3 +0,0 @@
-body {
- color: red;
-}
diff --git a/packages/playground/optimize-deps/dep-linked/package.json b/packages/playground/optimize-deps/dep-linked/package.json
deleted file mode 100644
index 915340f10ae4a0..00000000000000
--- a/packages/playground/optimize-deps/dep-linked/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "dep-linked",
- "private": true,
- "version": "0.0.0",
- "main": "index.js",
- "dependencies": {
- "lodash-es": "^4.17.21"
- }
-}
diff --git a/packages/playground/optimize-deps/dep-not-js/package.json b/packages/playground/optimize-deps/dep-not-js/package.json
deleted file mode 100644
index 39ebafb6217b6e..00000000000000
--- a/packages/playground/optimize-deps/dep-not-js/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "dep-not-js",
- "private": true,
- "version": "1.0.0",
- "main": "index.notjs"
-}
diff --git a/packages/playground/optimize-deps/dep-with-dynamic-import/package.json b/packages/playground/optimize-deps/dep-with-dynamic-import/package.json
deleted file mode 100644
index 81c5d2dda6a62a..00000000000000
--- a/packages/playground/optimize-deps/dep-with-dynamic-import/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "dep-with-dynamic-import",
- "private": true,
- "version": "0.0.0",
- "main": "index.js"
-}
diff --git a/packages/playground/optimize-deps/index.html b/packages/playground/optimize-deps/index.html
deleted file mode 100644
index 2be896d00acba9..00000000000000
--- a/packages/playground/optimize-deps/index.html
+++ /dev/null
@@ -1,124 +0,0 @@
-
Optimize Deps
-
-
CommonJS w/ named imports (react)
-
-
CommonJS w/ named imports (phoenix)
-
fail
-
CommonJS w/ default export (clipboard)
-
fail
-
-
-
-
CommonJS dynamic import default + named (react)
-
-
CommonJS dynamic import named (phoenix)
-
-
CommonJS dynamic import default (clipboard)
-
-
CommonJS dynamic import default (dep-cjs-compiled-from-esm)
-
-
CommonJS dynamic import default (dep-cjs-compiled-from-cjs)
-
-
-
-
-
Dedupe (dep in linked & optimized package)
-
-
-
-
CommonJS w/ browser field mapping (axios)
-
This should show pong:
-
-
Detecting linked src package and optimizing its deps (lodash-es)
-
This should show fooBarBaz:
-
-
Optimizing force included dep even when it's linked
-
-
-
import * as ...
-
-
-
Import from dependency with .notjs files
-
-
-
Import from dependency with dynamic import
-
-
-
Dep w/ special file format supported via plugins
-
-
-
Vue & Vuex
-
-
-
Dep with changes from esbuild plugin
-
This should show a greeting:
-
-
Dep from hidden dir
-
This should show hello!:
-
-
Nested include
-
Module path:
-
-
Alias with colon
-
URL:
-
-
Reused variable names
-
This should show reused:
-
-
-
-
-
-
-
diff --git a/packages/playground/optimize-deps/nested-exclude/index.js b/packages/playground/optimize-deps/nested-exclude/index.js
deleted file mode 100644
index 3910458ef4d26d..00000000000000
--- a/packages/playground/optimize-deps/nested-exclude/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export { default as nestedInclude } from 'nested-include'
-
-export default 'nested-exclude'
diff --git a/packages/playground/optimize-deps/nested-exclude/nested-include/package.json b/packages/playground/optimize-deps/nested-exclude/nested-include/package.json
deleted file mode 100644
index 581ef4dada69ce..00000000000000
--- a/packages/playground/optimize-deps/nested-exclude/nested-include/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "nested-include",
- "private": true,
- "version": "1.0.0",
- "main": "index.js"
-}
diff --git a/packages/playground/optimize-deps/nested-exclude/package.json b/packages/playground/optimize-deps/nested-exclude/package.json
deleted file mode 100644
index 57dfc20ea1f801..00000000000000
--- a/packages/playground/optimize-deps/nested-exclude/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "nested-exclude",
- "private": true,
- "version": "1.0.0",
- "main": "index.js",
- "dependencies": {
- "nested-include": "link:./nested-include"
- }
-}
diff --git a/packages/playground/optimize-deps/package.json b/packages/playground/optimize-deps/package.json
deleted file mode 100644
index 2752e691da6fb2..00000000000000
--- a/packages/playground/optimize-deps/package.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "name": "test-optimize-deps",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview",
- "postinstall": "ts-node ../../../scripts/patchFileDeps.ts"
- },
- "dependencies": {
- "axios": "^0.24.0",
- "clipboard": "^2.0.8",
- "dep-cjs-compiled-from-cjs": "file:./dep-cjs-compiled-from-cjs",
- "dep-cjs-compiled-from-esm": "file:./dep-cjs-compiled-from-esm",
- "dep-esbuild-plugin-transform": "file:./dep-esbuild-plugin-transform",
- "dep-linked": "link:./dep-linked",
- "dep-linked-include": "link:./dep-linked-include",
- "dep-not-js": "file:./dep-not-js",
- "dep-with-dynamic-import": "file:./dep-with-dynamic-import",
- "lodash-es": "^4.17.21",
- "nested-exclude": "file:./nested-exclude",
- "phoenix": "^1.6.2",
- "react": "^17.0.2",
- "react-dom": "^17.0.2",
- "resolve-linked": "workspace:0.0.0",
- "url": "^0.11.0",
- "vue": "^3.2.25",
- "vuex": "^4.0.0"
- },
- "devDependencies": {
- "@vitejs/plugin-vue": "workspace:*"
- }
-}
diff --git a/packages/playground/optimize-deps/vite.config.js b/packages/playground/optimize-deps/vite.config.js
deleted file mode 100644
index a989cf1961de11..00000000000000
--- a/packages/playground/optimize-deps/vite.config.js
+++ /dev/null
@@ -1,91 +0,0 @@
-const fs = require('fs')
-const vue = require('@vitejs/plugin-vue')
-
-/**
- * @type {import('vite').UserConfig}
- */
-module.exports = {
- resolve: {
- dedupe: ['react'],
- alias: {
- 'node:url': 'url'
- }
- },
-
- optimizeDeps: {
- include: ['dep-linked-include', 'nested-exclude > nested-include'],
- exclude: ['nested-exclude'],
- esbuildOptions: {
- plugins: [
- {
- name: 'replace-a-file',
- setup(build) {
- build.onLoad(
- { filter: /dep-esbuild-plugin-transform(\\|\/)index\.js$/ },
- () => ({
- contents: `export const hello = () => 'Hello from an esbuild plugin'`,
- loader: 'js'
- })
- )
- }
- }
- ]
- }
- },
-
- build: {
- // to make tests faster
- minify: false
- },
-
- plugins: [
- vue(),
- notjs(),
- // for axios request test
- {
- name: 'mock',
- configureServer({ middlewares }) {
- middlewares.use('/ping', (_, res) => {
- res.statusCode = 200
- res.end('pong')
- })
- }
- }
- ]
-}
-
-// Handles .notjs file, basically remove wrapping
and tags
-function notjs() {
- return {
- name: 'notjs',
- config() {
- return {
- optimizeDeps: {
- extensions: ['.notjs'],
- esbuildOptions: {
- plugins: [
- {
- name: 'esbuild-notjs',
- setup(build) {
- build.onLoad({ filter: /\.notjs$/ }, ({ path }) => {
- let contents = fs.readFileSync(path, 'utf-8')
- contents = contents
- .replace('
', '')
- .replace(' ', '')
- return { contents, loader: 'js' }
- })
- }
- }
- ]
- }
- }
- }
- },
- transform(code, id) {
- if (id.endsWith('.notjs')) {
- code = code.replace('
', '').replace(' ', '')
- return { code }
- }
- }
- }
-}
diff --git a/packages/playground/optimize-missing-deps/__test__/optimize-missing-deps.spec.ts b/packages/playground/optimize-missing-deps/__test__/optimize-missing-deps.spec.ts
deleted file mode 100644
index dd776daeceadbf..00000000000000
--- a/packages/playground/optimize-missing-deps/__test__/optimize-missing-deps.spec.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { port } from './serve'
-import fetch from 'node-fetch'
-import { untilUpdated } from '../../testUtils'
-
-const url = `http://localhost:${port}`
-
-test('*', async () => {
- await page.goto(url)
- // reload page to get optimized missing deps
- await page.reload()
- await untilUpdated(() => page.textContent('div'), 'Client')
-
- // raw http request
- const aboutHtml = await (await fetch(url)).text()
- expect(aboutHtml).toMatch('Server')
-})
diff --git a/packages/playground/optimize-missing-deps/__test__/serve.js b/packages/playground/optimize-missing-deps/__test__/serve.js
deleted file mode 100644
index 9f293024f83913..00000000000000
--- a/packages/playground/optimize-missing-deps/__test__/serve.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// @ts-check
-// this is automtically detected by scripts/jestPerTestSetup.ts and will replace
-// the default e2e test serve behavior
-
-const path = require('path')
-
-const port = (exports.port = 9529)
-
-/**
- * @param {string} root
- * @param {boolean} isProd
- */
-exports.serve = async function serve(root, isProd) {
- const { createServer } = require(path.resolve(root, 'server.js'))
- const { app, vite } = await createServer(root, isProd)
-
- return new Promise((resolve, reject) => {
- try {
- const server = app.listen(port, () => {
- resolve({
- // for test teardown
- async close() {
- await new Promise((resolve) => {
- server.close(resolve)
- })
- if (vite) {
- await vite.close()
- }
- }
- })
- })
- } catch (e) {
- reject(e)
- }
- })
-}
diff --git a/packages/playground/optimize-missing-deps/index.html b/packages/playground/optimize-missing-deps/index.html
deleted file mode 100644
index 13e9831870aa18..00000000000000
--- a/packages/playground/optimize-missing-deps/index.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
Vite App
-
-
-
-
-
diff --git a/packages/playground/optimize-missing-deps/main.js b/packages/playground/optimize-missing-deps/main.js
deleted file mode 100644
index 93f3e1221298bf..00000000000000
--- a/packages/playground/optimize-missing-deps/main.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { sayName } from 'missing-dep'
-
-export const name = sayName()
diff --git a/packages/playground/optimize-missing-deps/missing-dep/index.js b/packages/playground/optimize-missing-deps/missing-dep/index.js
deleted file mode 100644
index f5d61c545d080a..00000000000000
--- a/packages/playground/optimize-missing-deps/missing-dep/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { name } from 'multi-entry-dep'
-
-export function sayName() {
- return name
-}
diff --git a/packages/playground/optimize-missing-deps/missing-dep/package.json b/packages/playground/optimize-missing-deps/missing-dep/package.json
deleted file mode 100644
index bbfc8ba2c87a57..00000000000000
--- a/packages/playground/optimize-missing-deps/missing-dep/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "missing-dep",
- "private": true,
- "version": "0.0.0",
- "main": "index.js",
- "dependencies": {
- "multi-entry-dep": "file:../multi-entry-dep"
- }
-}
diff --git a/packages/playground/optimize-missing-deps/multi-entry-dep/index.js b/packages/playground/optimize-missing-deps/multi-entry-dep/index.js
deleted file mode 100644
index 0717b87c27c2d8..00000000000000
--- a/packages/playground/optimize-missing-deps/multi-entry-dep/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-const path = require('path')
-
-exports.name = path.normalize('./Server')
diff --git a/packages/playground/optimize-missing-deps/multi-entry-dep/package.json b/packages/playground/optimize-missing-deps/multi-entry-dep/package.json
deleted file mode 100644
index ac4f3e542d152b..00000000000000
--- a/packages/playground/optimize-missing-deps/multi-entry-dep/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "multi-entry-dep",
- "private": true,
- "version": "0.0.0",
- "main": "index.js",
- "browser": {
- "./index.js": "./index.browser.js"
- }
-}
diff --git a/packages/playground/optimize-missing-deps/package.json b/packages/playground/optimize-missing-deps/package.json
deleted file mode 100644
index 431cf3b33c3847..00000000000000
--- a/packages/playground/optimize-missing-deps/package.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "name": "optimize-missing-deps",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "node server",
- "postinstall": "ts-node ../../../scripts/patchFileDeps.ts"
- },
- "dependencies": {
- "missing-dep": "file:./missing-dep",
- "multi-entry-dep": "file:./multi-entry-dep"
- },
- "devDependencies": {
- "express": "^4.17.1"
- }
-}
diff --git a/packages/playground/optimize-missing-deps/server.js b/packages/playground/optimize-missing-deps/server.js
deleted file mode 100644
index b9422feb622584..00000000000000
--- a/packages/playground/optimize-missing-deps/server.js
+++ /dev/null
@@ -1,60 +0,0 @@
-// @ts-check
-const fs = require('fs')
-const path = require('path')
-const express = require('express')
-
-const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD
-
-async function createServer(root = process.cwd()) {
- const resolve = (p) => path.resolve(__dirname, p)
-
- const app = express()
-
- /**
- * @type {import('vite').ViteDevServer}
- */
- const vite = await require('vite').createServer({
- root,
- logLevel: isTest ? 'error' : 'info',
- server: { middlewareMode: 'ssr' }
- })
- app.use(vite.middlewares)
-
- app.use('*', async (req, res) => {
- try {
- let template = fs.readFileSync(resolve('index.html'), 'utf-8')
- template = await vite.transformIndexHtml(req.originalUrl, template)
-
- // this will import missing deps nest built-in deps that should not be optimized
- const { name } = await vite.ssrLoadModule('./main.js')
-
- // this will import missing deps that should be optimized correctly
- const appHtml = `
${name}
-`
-
- const html = template.replace(``, appHtml)
-
- res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
- } catch (e) {
- vite.ssrFixStacktrace(e)
- console.log(e.stack)
- res.status(500).end(e.stack)
- }
- })
-
- return { app, vite }
-}
-
-if (!isTest) {
- createServer().then(({ app }) =>
- app.listen(3000, () => {
- console.log('http://localhost:3000')
- })
- )
-}
-
-// for test use
-exports.createServer = createServer
diff --git a/packages/playground/package.json b/packages/playground/package.json
deleted file mode 100644
index 58ef368099e82f..00000000000000
--- a/packages/playground/package.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "name": "vite-playground",
- "private": true,
- "version": "1.0.0",
- "devDependencies": {
- "css-color-names": "^1.0.1"
- }
-}
diff --git a/packages/playground/preload/__tests__/preload.spec.ts b/packages/playground/preload/__tests__/preload.spec.ts
deleted file mode 100644
index 27a64930487797..00000000000000
--- a/packages/playground/preload/__tests__/preload.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { isBuild } from '../../testUtils'
-
-test('should have no 404s', () => {
- browserLogs.forEach((msg) => {
- expect(msg).not.toMatch('404')
- })
-})
-
-if (isBuild) {
- test('dynamic import', async () => {
- const appHtml = await page.content()
- expect(appHtml).toMatch('This is
home page.')
- })
-
- test('dynamic import with comments', async () => {
- await page.goto(viteTestUrl + '/#/hello')
- const html = await page.content()
- expect(html).toMatch(
- /link rel="modulepreload".*?href="\/assets\/Hello\.\w{8}\.js"/
- )
- expect(html).toMatch(
- /link rel="stylesheet".*?href="\/assets\/Hello\.\w{8}\.css"/
- )
- })
-}
diff --git a/packages/playground/preload/index.html b/packages/playground/preload/index.html
deleted file mode 100644
index affed21f9791cf..00000000000000
--- a/packages/playground/preload/index.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
diff --git a/packages/playground/preload/package.json b/packages/playground/preload/package.json
deleted file mode 100644
index 5e65dafc8099c4..00000000000000
--- a/packages/playground/preload/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "test-preload",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- },
- "dependencies": {
- "vue": "^3.2.25",
- "vue-router": "^4.0.0"
- },
- "devDependencies": {
- "@vitejs/plugin-vue": "workspace:*"
- }
-}
diff --git a/packages/playground/preload/router.js b/packages/playground/preload/router.js
deleted file mode 100644
index d3d5afdc99f6a3..00000000000000
--- a/packages/playground/preload/router.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { createRouter, createWebHashHistory } from 'vue-router'
-import Home from './src/components/Home.vue'
-
-const routes = [
- { path: '/', name: 'Home', component: Home },
- {
- path: '/hello',
- name: 'Hello',
- component: () => import(/* a comment */ './src/components/Hello.vue')
- },
- {
- path: '/about',
- name: 'About',
- component: () => import('./src/components/About.vue')
- } // Lazy load route component
-]
-
-export default createRouter({
- routes,
- history: createWebHashHistory()
-})
diff --git a/packages/playground/preload/src/App.vue b/packages/playground/preload/src/App.vue
deleted file mode 100644
index 3582faf75e1216..00000000000000
--- a/packages/playground/preload/src/App.vue
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/packages/playground/preload/src/components/About.vue b/packages/playground/preload/src/components/About.vue
deleted file mode 100644
index 2e73e80099446b..00000000000000
--- a/packages/playground/preload/src/components/About.vue
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
- This is about page.
-
- Go to Home page
-
-
-
-
diff --git a/packages/playground/preload/src/components/Hello.vue b/packages/playground/preload/src/components/Hello.vue
deleted file mode 100644
index 33b44d278d305d..00000000000000
--- a/packages/playground/preload/src/components/Hello.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-
- {{ msg }}
-
- This is hello page.
-
- Go to Home page
-
-
-
-
-
-
diff --git a/packages/playground/preload/src/components/Home.vue b/packages/playground/preload/src/components/Home.vue
deleted file mode 100644
index 20f6b4948ac30a..00000000000000
--- a/packages/playground/preload/src/components/Home.vue
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
- This is home page.
-
- Go to About page
-
- Go to Hello page
-
-
-
-
diff --git a/packages/playground/preload/vite.config.js b/packages/playground/preload/vite.config.js
deleted file mode 100644
index 96fb82f51ed349..00000000000000
--- a/packages/playground/preload/vite.config.js
+++ /dev/null
@@ -1,15 +0,0 @@
-const vuePlugin = require('@vitejs/plugin-vue')
-
-module.exports = {
- plugins: [vuePlugin()],
- build: {
- terserOptions: {
- format: {
- beautify: true
- },
- compress: {
- passes: 3
- }
- }
- }
-}
diff --git a/packages/playground/preserve-symlinks/__tests__/preserve-symlinks.spec.ts b/packages/playground/preserve-symlinks/__tests__/preserve-symlinks.spec.ts
deleted file mode 100644
index 7e0b546d7dbdbb..00000000000000
--- a/packages/playground/preserve-symlinks/__tests__/preserve-symlinks.spec.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-test('should have no 404s', () => {
- browserLogs.forEach((msg) => {
- expect(msg).not.toMatch('404')
- })
-})
-
-test('not-preserve-symlinks', async () => {
- expect(await page.textContent('#root')).toBe('hello vite')
-})
diff --git a/packages/playground/preserve-symlinks/index.html b/packages/playground/preserve-symlinks/index.html
deleted file mode 100644
index 07f82cb05c3eec..00000000000000
--- a/packages/playground/preserve-symlinks/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
Vite App
-
-
-
-
-
-
diff --git a/packages/playground/preserve-symlinks/moduleA/package.json b/packages/playground/preserve-symlinks/moduleA/package.json
deleted file mode 100644
index 3df68a0a78a164..00000000000000
--- a/packages/playground/preserve-symlinks/moduleA/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "@symlinks/moduleA",
- "private": true,
- "version": "0.0.0",
- "main": "linked.js"
-}
diff --git a/packages/playground/preserve-symlinks/moduleA/src/data.js b/packages/playground/preserve-symlinks/moduleA/src/data.js
deleted file mode 100644
index e1bc98ec67da12..00000000000000
--- a/packages/playground/preserve-symlinks/moduleA/src/data.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export const data = {
- msg: 'hello vite'
-}
diff --git a/packages/playground/preserve-symlinks/package.json b/packages/playground/preserve-symlinks/package.json
deleted file mode 100644
index 00a8ef23a3b05e..00000000000000
--- a/packages/playground/preserve-symlinks/package.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "name": "preserve-symlinks",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite --force",
- "build": "vite build",
- "preview": "vite preview"
- },
- "dependencies": {
- "@symlinks/moduleA": "link:./moduleA"
- }
-}
diff --git a/packages/playground/preserve-symlinks/src/main.js b/packages/playground/preserve-symlinks/src/main.js
deleted file mode 100644
index 7257c44f1ba83f..00000000000000
--- a/packages/playground/preserve-symlinks/src/main.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { sayHi } from '@symlinks/moduleA'
-
-document.getElementById('root').innerText = sayHi().msg
diff --git a/packages/playground/react-emotion/App.jsx b/packages/playground/react-emotion/App.jsx
deleted file mode 100644
index b3715369614530..00000000000000
--- a/packages/playground/react-emotion/App.jsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useState } from 'react'
-import { css } from '@emotion/react'
-
-import _Switch from 'react-switch'
-const Switch = _Switch.default || _Switch
-
-export function Counter() {
- const [count, setCount] = useState(0)
-
- return (
-
setCount((count) => count + 1)}
- >
- count is: {count}
-
- )
-}
-
-function FragmentTest() {
- const [checked, setChecked] = useState(false)
- return (
- <>
-
-
-
-
- >
- )
-}
-
-function App() {
- return (
-
- )
-}
-
-export default App
diff --git a/packages/playground/react-emotion/__tests__/react.spec.ts b/packages/playground/react-emotion/__tests__/react.spec.ts
deleted file mode 100644
index 49a66b9e103374..00000000000000
--- a/packages/playground/react-emotion/__tests__/react.spec.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { editFile, untilUpdated } from '../../testUtils'
-
-test('should render', async () => {
- expect(await page.textContent('h1')).toMatch(
- 'Hello Vite + React + @emotion/react'
- )
-})
-
-test('should update', async () => {
- expect(await page.textContent('button')).toMatch('count is: 0')
- await page.click('button')
- expect(await page.textContent('button')).toMatch('count is: 1')
-})
-
-test('should hmr', async () => {
- editFile('App.jsx', (code) =>
- code.replace('Vite + React + @emotion/react', 'Updated')
- )
- await untilUpdated(() => page.textContent('h1'), 'Hello Updated')
- // preserve state
- expect(await page.textContent('button')).toMatch('count is: 1')
-})
-
-test('should update button style', async () => {
- function getButtonBorderStyle() {
- return page.evaluate(() => {
- return window.getComputedStyle(document.querySelector('button')).border
- })
- }
-
- const styles = await page.evaluate(() => {
- return document.querySelector('button').style
- })
-
- expect(await getButtonBorderStyle()).toMatch('2px solid rgb(0, 0, 0)')
-
- editFile('App.jsx', (code) =>
- code.replace('border: 2px solid #000', 'border: 4px solid red')
- )
-
- await untilUpdated(getButtonBorderStyle, '4px solid rgb(255, 0, 0)')
-
- // preserve state
- expect(await page.textContent('button')).toMatch('count is: 1')
-})
diff --git a/packages/playground/react-emotion/index.html b/packages/playground/react-emotion/index.html
deleted file mode 100644
index ce1b9213c82763..00000000000000
--- a/packages/playground/react-emotion/index.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
diff --git a/packages/playground/react-emotion/package.json b/packages/playground/react-emotion/package.json
deleted file mode 100644
index fc78ac30b34a8d..00000000000000
--- a/packages/playground/react-emotion/package.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "name": "test-react-emotion",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- },
- "dependencies": {
- "@emotion/react": "^11.5.0",
- "react": "^17.0.2",
- "react-dom": "^17.0.2",
- "react-switch": "^6.0.0"
- },
- "devDependencies": {
- "@babel/plugin-proposal-pipeline-operator": "^7.16.0",
- "@emotion/babel-plugin": "^11.3.0",
- "@vitejs/plugin-react": "workspace:*"
- },
- "babel": {
- "presets": [
- "@babel/preset-env"
- ]
- }
-}
diff --git a/packages/playground/react-emotion/vite.config.ts b/packages/playground/react-emotion/vite.config.ts
deleted file mode 100644
index 9364c8f616c2f5..00000000000000
--- a/packages/playground/react-emotion/vite.config.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import react from '@vitejs/plugin-react'
-import type { UserConfig } from 'vite'
-
-const config: UserConfig = {
- plugins: [
- react({
- jsxImportSource: '@emotion/react',
- babel: {
- plugins: ['@emotion/babel-plugin']
- }
- })
- ],
- clearScreen: false,
- build: {
- // to make tests faster
- minify: false
- }
-}
-
-export default config
diff --git a/packages/playground/react/App.jsx b/packages/playground/react/App.jsx
deleted file mode 100644
index 70d1f961b8cce9..00000000000000
--- a/packages/playground/react/App.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useState } from 'react'
-import Dummy from './components/Dummy?qs-should-not-break-plugin-react'
-
-function App() {
- const [count, setCount] = useState(0)
- return (
-
-
- Hello Vite + React
-
- setCount((count) => count + 1)}>
- count is: {count}
-
-
-
- Edit App.jsx
and save to test HMR updates.
-
-
- Learn React
-
-
-
-
-
- )
-}
-
-export default App
diff --git a/packages/playground/react/__tests__/react.spec.ts b/packages/playground/react/__tests__/react.spec.ts
deleted file mode 100644
index 46eb752924f801..00000000000000
--- a/packages/playground/react/__tests__/react.spec.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { editFile, untilUpdated, isBuild } from '../../testUtils'
-
-test('should render', async () => {
- expect(await page.textContent('h1')).toMatch('Hello Vite + React')
-})
-
-test('should update', async () => {
- expect(await page.textContent('button')).toMatch('count is: 0')
- await page.click('button')
- expect(await page.textContent('button')).toMatch('count is: 1')
-})
-
-test('should hmr', async () => {
- editFile('App.jsx', (code) => code.replace('Vite + React', 'Updated'))
- await untilUpdated(() => page.textContent('h1'), 'Hello Updated')
- // preserve state
- expect(await page.textContent('button')).toMatch('count is: 1')
-})
-
-test('should have annotated jsx with file location metadata', async () => {
- // we're not annotating in prod,
- // so we skip this test when isBuild is true
- if (isBuild) return
-
- const meta = await page.evaluate(() => {
- const button = document.querySelector('button')
- const key = Object.keys(button).find(
- (key) => key.indexOf('__reactFiber') === 0
- )
- return button[key]._debugSource
- })
- // If the evaluate call doesn't crash, and the returned metadata has
- // the expected fields, we're good.
- expect(Object.keys(meta).sort()).toEqual([
- 'columnNumber',
- 'fileName',
- 'lineNumber'
- ])
-})
diff --git a/packages/playground/react/components/Dummy.jsx b/packages/playground/react/components/Dummy.jsx
deleted file mode 100644
index 27ec3c21de30fd..00000000000000
--- a/packages/playground/react/components/Dummy.jsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Dummy() {
- return <>>
-}
diff --git a/packages/playground/react/index.html b/packages/playground/react/index.html
deleted file mode 100644
index ce1b9213c82763..00000000000000
--- a/packages/playground/react/index.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
diff --git a/packages/playground/react/package.json b/packages/playground/react/package.json
deleted file mode 100644
index fc9b8e69d3999e..00000000000000
--- a/packages/playground/react/package.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "name": "test-react",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- },
- "dependencies": {
- "react": "^17.0.2",
- "react-dom": "^17.0.2"
- },
- "devDependencies": {
- "@vitejs/plugin-react": "workspace:*"
- },
- "babel": {
- "presets": [
- "@babel/preset-env"
- ]
- }
-}
diff --git a/packages/playground/react/vite.config.ts b/packages/playground/react/vite.config.ts
deleted file mode 100644
index c6955a131d375f..00000000000000
--- a/packages/playground/react/vite.config.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import react from '@vitejs/plugin-react'
-import type { UserConfig } from 'vite'
-
-const config: UserConfig = {
- plugins: [react()],
- build: {
- // to make tests faster
- minify: false
- }
-}
-
-export default config
diff --git a/packages/playground/resolve-config/__tests__/resolve-config.spec.ts b/packages/playground/resolve-config/__tests__/resolve-config.spec.ts
deleted file mode 100644
index 13ea5ea6f59a4f..00000000000000
--- a/packages/playground/resolve-config/__tests__/resolve-config.spec.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import fs from 'fs'
-import path from 'path'
-import { commandSync } from 'execa'
-import { isBuild, testDir, workspaceRoot } from '../../testUtils'
-
-const viteBin = path.join(workspaceRoot, 'packages', 'vite', 'bin', 'vite.js')
-
-const fromTestDir = (...p: string[]) => path.resolve(testDir, ...p)
-
-const build = (configName: string) => {
- commandSync(`${viteBin} build`, { cwd: fromTestDir(configName) })
-}
-const getDistFile = (configName: string) => {
- return fs.readFileSync(fromTestDir(`${configName}/dist/index.es.js`), 'utf8')
-}
-
-if (isBuild) {
- it('loads vite.config.js', () => {
- build('js')
- expect(getDistFile('js')).toContain('console.log(true)')
- })
- it('loads vite.config.js with package#type module', () => {
- build('js-module')
- expect(getDistFile('js-module')).toContain('console.log(true)')
- })
- it('loads vite.config.cjs', () => {
- build('cjs')
- expect(getDistFile('cjs')).toContain('console.log(true)')
- })
- it('loads vite.config.cjs with package#type module', () => {
- build('cjs-module')
- expect(getDistFile('cjs-module')).toContain('console.log(true)')
- })
- it('loads vite.config.mjs', () => {
- build('mjs')
- expect(getDistFile('mjs')).toContain('console.log(true)')
- })
- it('loads vite.config.mjs with package#type module', () => {
- build('mjs-module')
- expect(getDistFile('mjs-module')).toContain('console.log(true)')
- })
- it('loads vite.config.ts', () => {
- build('ts')
- expect(getDistFile('ts')).toContain('console.log(true)')
- })
- it('loads vite.config.ts with package#type module', () => {
- build('ts-module')
- expect(getDistFile('ts-module')).toContain('console.log(true)')
- })
-} else {
- // this test doesn't support serve mode
- // must contain at least one test
- test('should work', () => void 0)
-}
diff --git a/packages/playground/resolve-config/__tests__/serve.js b/packages/playground/resolve-config/__tests__/serve.js
deleted file mode 100644
index bd451d4cf6f6bc..00000000000000
--- a/packages/playground/resolve-config/__tests__/serve.js
+++ /dev/null
@@ -1,39 +0,0 @@
-// @ts-check
-// this is automtically detected by scripts/jestPerTestSetup.ts and will replace
-// the default e2e test serve behavior
-
-const path = require('path')
-const fs = require('fs-extra')
-const { testDir } = require('../../testUtils')
-
-const fromTestDir = (/** @type{string[]} */ ...p) => path.resolve(testDir, ...p)
-
-const configNames = ['js', 'cjs', 'mjs', 'ts']
-
-/** @param {string} root @param {boolean} isProd */
-exports.serve = async function serve(root, isProd) {
- if (!isProd) return
-
- // create separate directories for all config types:
- // ./{js,cjs,mjs,ts} and ./{js,cjs,mjs,ts}-module (with package#type)
- for (const configName of configNames) {
- const pathToConf = fromTestDir(configName, `vite.config.${configName}`)
-
- await fs.copy(fromTestDir('root'), fromTestDir(configName))
- await fs.rename(fromTestDir(configName, 'vite.config.js'), pathToConf)
-
- if (configName === 'cjs') {
- const conf = await fs.readFile(pathToConf, 'utf8')
- await fs.writeFile(
- pathToConf,
- conf.replace('export default', 'module.exports = ')
- )
- }
-
- // copy directory and add package.json with "type": "module"
- await fs.copy(fromTestDir(configName), fromTestDir(`${configName}-module`))
- await fs.writeJSON(fromTestDir(`${configName}-module`, 'package.json'), {
- type: 'module'
- })
- }
-}
diff --git a/packages/playground/resolve-config/package.json b/packages/playground/resolve-config/package.json
deleted file mode 100644
index 5459dad99003cd..00000000000000
--- a/packages/playground/resolve-config/package.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "resolve-config",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite --force",
- "build": "vite build",
- "preview": "vite preview"
- }
-}
diff --git a/packages/playground/resolve-config/root/index.js b/packages/playground/resolve-config/root/index.js
deleted file mode 100644
index a3f8f13f20f96e..00000000000000
--- a/packages/playground/resolve-config/root/index.js
+++ /dev/null
@@ -1 +0,0 @@
-console.log(__CONFIG_LOADED__)
diff --git a/packages/playground/resolve-config/root/vite.config.js b/packages/playground/resolve-config/root/vite.config.js
deleted file mode 100644
index ed72046f940d59..00000000000000
--- a/packages/playground/resolve-config/root/vite.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export default {
- define: { __CONFIG_LOADED__: true },
- logLevel: 'silent',
- build: {
- minify: false,
- sourcemap: false,
- lib: { entry: 'index.js', fileName: 'index', formats: ['es'] }
- }
-}
diff --git a/packages/playground/resolve-linked/package.json b/packages/playground/resolve-linked/package.json
deleted file mode 100644
index 204cfd931c63ab..00000000000000
--- a/packages/playground/resolve-linked/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "resolve-linked",
- "private": true,
- "version": "0.0.0",
- "main": "src/index.js"
-}
diff --git a/packages/playground/resolve/__tests__/resolve.spec.ts b/packages/playground/resolve/__tests__/resolve.spec.ts
deleted file mode 100644
index b64da138033fc0..00000000000000
--- a/packages/playground/resolve/__tests__/resolve.spec.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { isBuild } from '../../testUtils'
-
-test('bom import', async () => {
- expect(await page.textContent('.utf8-bom')).toMatch('[success]')
-})
-
-test('deep import', async () => {
- expect(await page.textContent('.deep-import')).toMatch('[2,4]')
-})
-
-test('entry with exports field', async () => {
- expect(await page.textContent('.exports-entry')).toMatch('[success]')
-})
-
-test('deep import with exports field', async () => {
- expect(await page.textContent('.exports-deep')).toMatch('[success]')
-})
-
-test('deep import with query with exports field', async () => {
- expect(await page.textContent('.exports-deep-query')).not.toMatch('fail')
-})
-
-test('deep import with exports field + exposed dir', async () => {
- expect(await page.textContent('.exports-deep-exposed-dir')).toMatch(
- '[success]'
- )
-})
-
-test('deep import with exports field + mapped dir', async () => {
- expect(await page.textContent('.exports-deep-mapped-dir')).toMatch(
- '[success]'
- )
-})
-
-test('Respect exports field env key priority', async () => {
- expect(await page.textContent('.exports-env')).toMatch('[success]')
-})
-
-test('Respect production/development conditionals', async () => {
- expect(await page.textContent('.exports-env')).toMatch(
- isBuild ? `browser.prod.mjs` : `browser.mjs`
- )
-})
-
-test('implicit dir/index.js', async () => {
- expect(await page.textContent('.index')).toMatch('[success]')
-})
-
-test('implicit dir/index.js vs explicit file', async () => {
- expect(await page.textContent('.dir-vs-file')).toMatch('[success]')
-})
-
-test('exact extension vs. duplicated (.js.js)', async () => {
- expect(await page.textContent('.exact-extension')).toMatch('[success]')
-})
-
-test('dont add extension to directory name (./dir-with-ext.js/index.js)', async () => {
- expect(await page.textContent('.dir-with-ext')).toMatch('[success]')
-})
-
-test('a ts module can import another ts module using its corresponding js file name', async () => {
- expect(await page.textContent('.ts-extension')).toMatch('[success]')
-})
-
-test('filename with dot', async () => {
- expect(await page.textContent('.dot')).toMatch('[success]')
-})
-
-test('browser field', async () => {
- expect(await page.textContent('.browser')).toMatch('[success]')
-})
-
-test('css entry', async () => {
- expect(await page.textContent('.css')).toMatch('[success]')
-})
-
-test('monorepo linked dep', async () => {
- expect(await page.textContent('.monorepo')).toMatch('[success]')
-})
-
-test('plugin resolved virtual file', async () => {
- expect(await page.textContent('.virtual')).toMatch('[success]')
-})
-
-test('plugin resolved custom virtual file', async () => {
- expect(await page.textContent('.custom-virtual')).toMatch('[success]')
-})
-
-test('resolve inline package', async () => {
- expect(await page.textContent('.inline-pkg')).toMatch('[success]')
-})
-
-test('resolve.extensions', async () => {
- expect(await page.textContent('.custom-ext')).toMatch('[success]')
-})
-
-test('resolve.mainFields', async () => {
- expect(await page.textContent('.custom-main-fields')).toMatch('[success]')
-})
-
-test('resolve.conditions', async () => {
- expect(await page.textContent('.custom-condition')).toMatch('[success]')
-})
-
-test('resolve package that contains # in path', async () => {
- expect(await page.textContent('.path-contains-sharp-symbol')).toMatch(
- '[success]'
- )
-})
diff --git a/packages/playground/resolve/browser-field/multiple.dot.path.js b/packages/playground/resolve/browser-field/multiple.dot.path.js
deleted file mode 100644
index b4022f73daaf6e..00000000000000
--- a/packages/playground/resolve/browser-field/multiple.dot.path.js
+++ /dev/null
@@ -1,2 +0,0 @@
-const fs = require('fs')
-console.log('this should not run in the browser')
diff --git a/packages/playground/resolve/browser-field/no-ext-index/index.js b/packages/playground/resolve/browser-field/no-ext-index/index.js
deleted file mode 100644
index d3f4967324ffb7..00000000000000
--- a/packages/playground/resolve/browser-field/no-ext-index/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import jsdom from 'jsdom' // should be redireted to empty module
-export default ''
diff --git a/packages/playground/resolve/browser-field/no-ext.js b/packages/playground/resolve/browser-field/no-ext.js
deleted file mode 100644
index d3f4967324ffb7..00000000000000
--- a/packages/playground/resolve/browser-field/no-ext.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import jsdom from 'jsdom' // should be redireted to empty module
-export default ''
diff --git a/packages/playground/resolve/browser-field/not-browser.js b/packages/playground/resolve/browser-field/not-browser.js
deleted file mode 100644
index b4022f73daaf6e..00000000000000
--- a/packages/playground/resolve/browser-field/not-browser.js
+++ /dev/null
@@ -1,2 +0,0 @@
-const fs = require('fs')
-console.log('this should not run in the browser')
diff --git a/packages/playground/resolve/browser-field/out/esm.browser.js b/packages/playground/resolve/browser-field/out/esm.browser.js
deleted file mode 100644
index bddf03df0b0c22..00000000000000
--- a/packages/playground/resolve/browser-field/out/esm.browser.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import jsdom from 'jsdom' // should be redireted to empty module
-export default '[success] resolve browser field'
diff --git a/packages/playground/resolve/browser-field/package.json b/packages/playground/resolve/browser-field/package.json
deleted file mode 100644
index 006f9b4b5f4fc6..00000000000000
--- a/packages/playground/resolve/browser-field/package.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "name": "resolve-browser-field",
- "private": true,
- "version": "1.0.0",
- "//": "real world example: https://github.com/axios/axios/blob/3f2ef030e001547eb06060499f8a2e3f002b5a14/package.json#L71-L73",
- "main": "out/cjs.node.js",
- "browser": {
- "./out/cjs.node.js": "./out/esm.browser.js",
- "./no-ext": "./out/esm.browser.js",
- "./ext.js": "./out/esm.browser.js",
- "./ext-index/index.js": "./out/esm.browser.js",
- "./no-ext-index": "./out/esm.browser.js",
- "./not-browser.js": false,
- "./multiple.dot.path.js": false,
- "jsdom": false
- }
-}
diff --git a/packages/playground/resolve/config-dep.js b/packages/playground/resolve/config-dep.js
deleted file mode 100644
index 8bc3563c743bcd..00000000000000
--- a/packages/playground/resolve/config-dep.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- a: 1
-}
diff --git a/packages/playground/resolve/custom-condition/package.json b/packages/playground/resolve/custom-condition/package.json
deleted file mode 100644
index 490a420fe2dbfc..00000000000000
--- a/packages/playground/resolve/custom-condition/package.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "name": "resolve-custom-condition",
- "private": true,
- "version": "1.0.0",
- "main": "index.js",
- "exports": {
- ".": {
- "custom": "./index.custom.js",
- "import": "./index.js",
- "require": "./index.js"
- }
- }
-}
diff --git a/packages/playground/resolve/custom-main-field/package.json b/packages/playground/resolve/custom-main-field/package.json
deleted file mode 100644
index bb948c3261eb1c..00000000000000
--- a/packages/playground/resolve/custom-main-field/package.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name": "resolve-custom-main-field",
- "private": true,
- "version": "1.0.0",
- "main": "index.js",
- "custom": "index.custom.js"
-}
diff --git a/packages/playground/resolve/exports-env/package.json b/packages/playground/resolve/exports-env/package.json
deleted file mode 100644
index f9e635b5a19c24..00000000000000
--- a/packages/playground/resolve/exports-env/package.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "resolve-exports-env",
- "private": true,
- "version": "1.0.0",
- "exports": {
- "import": {
- "browser": {
- "production": "./browser.prod.mjs",
- "development": "./browser.mjs"
- }
- },
- "browser": "./browser.js",
- "default": "./fallback.umd.js"
- }
-}
diff --git a/packages/playground/resolve/exports-path/package.json b/packages/playground/resolve/exports-path/package.json
deleted file mode 100644
index 7355da2f63f616..00000000000000
--- a/packages/playground/resolve/exports-path/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "resolve-exports-path",
- "private": true,
- "version": "1.0.0",
- "exports": {
- ".": {
- "import": "./main.js",
- "require": "./cjs.js"
- },
- "./deep.js": "./deep.js",
- "./deep.json": "./deep.json",
- "./dir/": "./dir/",
- "./dir-mapped/*": {
- "import": "./dir/*",
- "require": "./dir-cjs/*"
- }
- }
-}
diff --git a/packages/playground/resolve/index.html b/packages/playground/resolve/index.html
deleted file mode 100644
index c0569345d86837..00000000000000
--- a/packages/playground/resolve/index.html
+++ /dev/null
@@ -1,227 +0,0 @@
-
Resolve
-
-
Utf8-bom import
-
fail
-
-
Deep import
-
Should show [2,4]:fail
-
-
Entry resolving with exports field
-
fail
-
-
Deep import with exports field
-
fail
-
-
Deep import with query with exports field
-
fail
-
-
Deep import with exports field + exposed directory
-
fail
-
-
Deep import with exports field + mapped directory
-
fail
-
-
Exports field env priority
-
fail
-
-
Resolve /index.*
-
fail
-
-
Resolve dir and file of the same name (should prioritize file)
-
fail
-
-
Resolve to non-duplicated file extension
-
fail
-
-
Don't add extensions to directory names
-
fail
-
-
- A ts module can import another ts module using its corresponding js file name
-
-
fail
-
-
- A ts module can import another tsx module using its corresponding jsx file
- name
-
-
fail
-
-
- A ts module can import another tsx module using its corresponding js file name
-
-
fail
-
-
Resolve file name containing dot
-
fail
-
-
Browser Field
-
fail
-
-
CSS Entry
-
-
-
Monorepo linked dep
-
-
-
Plugin resolved virtual file
-
-
-
Plugin resolved custom virtual file
-
-
-
Inline package
-
-
-
resolve.extensions
-
-
-
resolve.mainFields
-
-
-
resolve.conditions
-
-
-
resolve package that contains # in path
-
-
-
-
-
diff --git a/packages/playground/resolve/inline-package/package.json b/packages/playground/resolve/inline-package/package.json
deleted file mode 100644
index bbbb6b0b0381d8..00000000000000
--- a/packages/playground/resolve/inline-package/package.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name": "inline-package",
- "private": true,
- "version": "0.0.0",
- "sideEffects": false,
- "main": "./inline"
-}
diff --git a/packages/playground/resolve/package.json b/packages/playground/resolve/package.json
deleted file mode 100644
index 5e0f53b4c8468a..00000000000000
--- a/packages/playground/resolve/package.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "name": "test-resolve",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- },
- "dependencies": {
- "@babel/runtime": "^7.16.0",
- "es5-ext": "0.10.53",
- "normalize.css": "^8.0.1",
- "resolve-browser-field": "link:./browser-field",
- "resolve-custom-condition": "link:./custom-condition",
- "resolve-custom-main-field": "link:./custom-main-field",
- "resolve-exports-env": "link:./exports-env",
- "resolve-exports-path": "link:./exports-path",
- "resolve-linked": "workspace:*"
- }
-}
diff --git a/packages/playground/resolve/ts-extension/index.ts b/packages/playground/resolve/ts-extension/index.ts
deleted file mode 100644
index bdb326f8778e64..00000000000000
--- a/packages/playground/resolve/ts-extension/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { msg } from './hello.js'
-import { msgJsx } from './hellojsx.jsx'
-import { msgTsx } from './hellotsx.js'
-
-export { msg, msgJsx, msgTsx }
diff --git a/packages/playground/resolve/vite.config.js b/packages/playground/resolve/vite.config.js
deleted file mode 100644
index be1b75e431383a..00000000000000
--- a/packages/playground/resolve/vite.config.js
+++ /dev/null
@@ -1,44 +0,0 @@
-const virtualFile = '@virtual-file'
-const virtualId = '\0' + virtualFile
-
-const customVirtualFile = '@custom-virtual-file'
-const { a } = require('./config-dep')
-
-module.exports = {
- resolve: {
- extensions: ['.mjs', '.js', '.es', '.ts'],
- mainFields: ['custom', 'module'],
- conditions: ['custom']
- },
- define: {
- VITE_CONFIG_DEP_TEST: a
- },
- plugins: [
- {
- name: 'virtual-module',
- resolveId(id) {
- if (id === virtualFile) {
- return virtualId
- }
- },
- load(id) {
- if (id === virtualId) {
- return `export const msg = "[success] from conventional virtual file"`
- }
- }
- },
- {
- name: 'custom-resolve',
- resolveId(id) {
- if (id === customVirtualFile) {
- return id
- }
- },
- load(id) {
- if (id === customVirtualFile) {
- return `export const msg = "[success] from custom virtual file"`
- }
- }
- }
- ]
-}
diff --git a/packages/playground/shims.d.ts b/packages/playground/shims.d.ts
deleted file mode 100644
index ced8fb1ad585ae..00000000000000
--- a/packages/playground/shims.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-declare module 'css-color-names' {
- const colors: Record
- export default colors
-}
-
-declare module '*.vue' {
- import type { ComponentOptions } from 'vue'
- const component: ComponentOptions
- export default component
-}
diff --git a/packages/playground/ssr-deps/__tests__/serve.js b/packages/playground/ssr-deps/__tests__/serve.js
deleted file mode 100644
index 5ba5724f2b7a94..00000000000000
--- a/packages/playground/ssr-deps/__tests__/serve.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// @ts-check
-// this is automtically detected by scripts/jestPerTestSetup.ts and will replace
-// the default e2e test serve behavior
-
-const path = require('path')
-
-const port = (exports.port = 9530)
-
-/**
- * @param {string} root
- * @param {boolean} isProd
- */
-exports.serve = async function serve(root, isProd) {
- const { createServer } = require(path.resolve(root, 'server.js'))
- const { app, vite } = await createServer(root, isProd)
-
- return new Promise((resolve, reject) => {
- try {
- const server = app.listen(port, () => {
- resolve({
- // for test teardown
- async close() {
- await new Promise((resolve) => {
- server.close(resolve)
- })
- if (vite) {
- await vite.close()
- }
- }
- })
- })
- } catch (e) {
- reject(e)
- }
- })
-}
diff --git a/packages/playground/ssr-deps/__tests__/ssr-deps.spec.ts b/packages/playground/ssr-deps/__tests__/ssr-deps.spec.ts
deleted file mode 100644
index 8a201c9eb87455..00000000000000
--- a/packages/playground/ssr-deps/__tests__/ssr-deps.spec.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { port } from './serve'
-
-const url = `http://localhost:${port}`
-
-/**
- * test for #5809
- *
- * NOTE: This test will always succeed now, unless the temporary workaround for Jest can be removed
- * See https://github.com/vitejs/vite/pull/5197#issuecomment-938054077
- */
-test('msg should be encrypted', async () => {
- await page.goto(url)
- expect(await page.textContent('.encrypted-msg')).not.toMatch(
- 'Secret Message!'
- )
-})
-
-test('msg read by fs/promises', async () => {
- await page.goto(url)
- expect(await page.textContent('.file-message')).toMatch('File Content!')
-})
-
-test('msg from primitive export', async () => {
- await page.goto(url)
- expect(await page.textContent('.primitive-export-message')).toMatch(
- 'Hello World!'
- )
-})
-
-test('msg from TS transpiled exports', async () => {
- await page.goto(url)
- expect(await page.textContent('.ts-default-export-message')).toMatch(
- 'Hello World!'
- )
- expect(await page.textContent('.ts-named-export-message')).toMatch(
- 'Hello World!'
- )
-})
-
-test('msg from Object.assign exports', async () => {
- await page.goto(url)
- expect(await page.textContent('.object-assigned-exports-message')).toMatch(
- 'Hello World!'
- )
-})
-
-test('msg from forwarded exports', async () => {
- await page.goto(url)
- expect(await page.textContent('.forwarded-export-message')).toMatch(
- 'Hello World!'
- )
-})
-
-test('msg from define properties exports', async () => {
- await page.goto(url)
- expect(await page.textContent('.define-properties-exports-msg')).toMatch(
- 'Hello World!'
- )
-})
-
-test('msg from define property exports', async () => {
- await page.goto(url)
- expect(await page.textContent('.define-property-exports-msg')).toMatch(
- 'Hello World!'
- )
-})
-
-test('msg from only object assigned exports', async () => {
- await page.goto(url)
- expect(await page.textContent('.only-object-assigned-exports-msg')).toMatch(
- 'Hello World!'
- )
-})
diff --git a/packages/playground/ssr-deps/define-properties-exports/package.json b/packages/playground/ssr-deps/define-properties-exports/package.json
deleted file mode 100644
index 3cf10f8cced539..00000000000000
--- a/packages/playground/ssr-deps/define-properties-exports/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "define-properties-exports",
- "private": true,
- "version": "0.0.0"
-}
diff --git a/packages/playground/ssr-deps/define-property-exports/index.js b/packages/playground/ssr-deps/define-property-exports/index.js
deleted file mode 100644
index 4506dd6200051e..00000000000000
--- a/packages/playground/ssr-deps/define-property-exports/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-Object.defineProperty(exports, 'hello', {
- value() {
- return 'Hello World!'
- }
-})
diff --git a/packages/playground/ssr-deps/define-property-exports/package.json b/packages/playground/ssr-deps/define-property-exports/package.json
deleted file mode 100644
index 38ef7fdf5f410a..00000000000000
--- a/packages/playground/ssr-deps/define-property-exports/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "define-property-exports",
- "private": true,
- "version": "0.0.0"
-}
diff --git a/packages/playground/ssr-deps/forwarded-export/package.json b/packages/playground/ssr-deps/forwarded-export/package.json
deleted file mode 100644
index 1a0a62e0b4472d..00000000000000
--- a/packages/playground/ssr-deps/forwarded-export/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "forwarded-export",
- "private": true,
- "version": "0.0.0"
-}
diff --git a/packages/playground/ssr-deps/index.html b/packages/playground/ssr-deps/index.html
deleted file mode 100644
index b1e884efaab01a..00000000000000
--- a/packages/playground/ssr-deps/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
- SSR Dependencies
-
-
- SSR Dependencies
-
-
-
diff --git a/packages/playground/ssr-deps/object-assigned-exports/index.js b/packages/playground/ssr-deps/object-assigned-exports/index.js
deleted file mode 100644
index d6510e38f3a36f..00000000000000
--- a/packages/playground/ssr-deps/object-assigned-exports/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-Object.defineProperty(exports, '__esModule', { value: true })
-
-const obj = {
- hello() {
- return 'Hello World!'
- }
-}
-
-Object.assign(exports, obj)
diff --git a/packages/playground/ssr-deps/object-assigned-exports/package.json b/packages/playground/ssr-deps/object-assigned-exports/package.json
deleted file mode 100644
index a385dc9b7ec1b7..00000000000000
--- a/packages/playground/ssr-deps/object-assigned-exports/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "object-assigned-exports",
- "private": true,
- "version": "0.0.0"
-}
diff --git a/packages/playground/ssr-deps/only-object-assigned-exports/index.js b/packages/playground/ssr-deps/only-object-assigned-exports/index.js
deleted file mode 100644
index b6a4ab368b133d..00000000000000
--- a/packages/playground/ssr-deps/only-object-assigned-exports/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-Object.assign(exports, {
- hello() {
- return 'Hello World!'
- }
-})
diff --git a/packages/playground/ssr-deps/only-object-assigned-exports/package.json b/packages/playground/ssr-deps/only-object-assigned-exports/package.json
deleted file mode 100644
index 22a071b59e411d..00000000000000
--- a/packages/playground/ssr-deps/only-object-assigned-exports/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "only-object-assigned-exports",
- "private": true,
- "version": "0.0.0"
-}
diff --git a/packages/playground/ssr-deps/package.json b/packages/playground/ssr-deps/package.json
deleted file mode 100644
index 7af243c3b4769a..00000000000000
--- a/packages/playground/ssr-deps/package.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "name": "test-ssr-deps",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "node server",
- "serve": "cross-env NODE_ENV=production node server",
- "debug": "node --inspect-brk server",
- "postinstall": "ts-node ../../../scripts/patchFileDeps.ts"
- },
- "dependencies": {
- "bcrypt": "^5.0.1",
- "define-properties-exports": "file:./define-properties-exports",
- "define-property-exports": "file:./define-property-exports",
- "forwarded-export": "file:./forwarded-export",
- "object-assigned-exports": "file:./object-assigned-exports",
- "only-object-assigned-exports": "file:./only-object-assigned-exports",
- "primitive-export": "file:./primitive-export",
- "read-file-content": "file:./read-file-content",
- "require-absolute": "file:./require-absolute",
- "ts-transpiled-exports": "file:./ts-transpiled-exports"
- },
- "devDependencies": {
- "cross-env": "^7.0.3",
- "express": "^4.17.1"
- }
-}
diff --git a/packages/playground/ssr-deps/primitive-export/package.json b/packages/playground/ssr-deps/primitive-export/package.json
deleted file mode 100644
index d86685f6b9a8f1..00000000000000
--- a/packages/playground/ssr-deps/primitive-export/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "primitive-export",
- "private": true,
- "version": "0.0.0"
-}
diff --git a/packages/playground/ssr-deps/read-file-content/index.js b/packages/playground/ssr-deps/read-file-content/index.js
deleted file mode 100644
index c8761b3b4734c1..00000000000000
--- a/packages/playground/ssr-deps/read-file-content/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const path = require('path')
-
-module.exports = async function readFileContent(filePath) {
- const fs =
- process.versions.node.split('.')[0] >= '14'
- ? require('fs/promises')
- : require('fs').promises
- return await fs.readFile(path.resolve(filePath), 'utf-8')
-}
diff --git a/packages/playground/ssr-deps/read-file-content/package.json b/packages/playground/ssr-deps/read-file-content/package.json
deleted file mode 100644
index 6145e7821f7067..00000000000000
--- a/packages/playground/ssr-deps/read-file-content/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "read-file-content",
- "private": true,
- "version": "0.0.0"
-}
diff --git a/packages/playground/ssr-deps/require-absolute/index.js b/packages/playground/ssr-deps/require-absolute/index.js
deleted file mode 100644
index c2f844f3e2f6ed..00000000000000
--- a/packages/playground/ssr-deps/require-absolute/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-const path = require('path')
-
-module.exports.hello = () => require(path.resolve(__dirname, './foo.js')).hello
diff --git a/packages/playground/ssr-deps/require-absolute/package.json b/packages/playground/ssr-deps/require-absolute/package.json
deleted file mode 100644
index 352f550e184745..00000000000000
--- a/packages/playground/ssr-deps/require-absolute/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "require-absolute",
- "private": true,
- "version": "0.0.0"
-}
diff --git a/packages/playground/ssr-deps/server.js b/packages/playground/ssr-deps/server.js
deleted file mode 100644
index 89a64ae51fdc94..00000000000000
--- a/packages/playground/ssr-deps/server.js
+++ /dev/null
@@ -1,68 +0,0 @@
-// @ts-check
-const fs = require('fs')
-const path = require('path')
-const express = require('express')
-
-const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD
-
-async function createServer(
- root = process.cwd(),
- isProd = process.env.NODE_ENV === 'production'
-) {
- const resolve = (p) => path.resolve(__dirname, p)
-
- const app = express()
-
- /**
- * @type {import('vite').ViteDevServer}
- */
- const vite = await require('vite').createServer({
- root,
- logLevel: isTest ? 'error' : 'info',
- server: {
- middlewareMode: 'ssr',
- watch: {
- // During tests we edit the files too fast and sometimes chokidar
- // misses change events, so enforce polling for consistency
- usePolling: true,
- interval: 100
- }
- }
- })
- // use vite's connect instance as middleware
- app.use(vite.middlewares)
-
- app.use('*', async (req, res) => {
- try {
- const url = req.originalUrl
-
- let template
- template = fs.readFileSync(resolve('index.html'), 'utf-8')
- template = await vite.transformIndexHtml(url, template)
- const render = (await vite.ssrLoadModule('/src/app.js')).render
-
- const appHtml = await render(url, __dirname)
-
- const html = template.replace(``, appHtml)
-
- res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
- } catch (e) {
- vite && vite.ssrFixStacktrace(e)
- console.log(e.stack)
- res.status(500).end(e.stack)
- }
- })
-
- return { app, vite }
-}
-
-if (!isTest) {
- createServer().then(({ app }) =>
- app.listen(3000, () => {
- console.log('http://localhost:3000')
- })
- )
-}
-
-// for test use
-exports.createServer = createServer
diff --git a/packages/playground/ssr-deps/src/app.js b/packages/playground/ssr-deps/src/app.js
deleted file mode 100644
index 9646cdcf2bf688..00000000000000
--- a/packages/playground/ssr-deps/src/app.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import path from 'path'
-import readFileContent from 'read-file-content'
-import primitiveExport from 'primitive-export'
-import tsDefaultExport, { hello as tsNamedExport } from 'ts-transpiled-exports'
-import objectAssignedExports from 'object-assigned-exports'
-import forwardedExport from 'forwarded-export'
-import bcrypt from 'bcrypt'
-import definePropertiesExports from 'define-properties-exports'
-import definePropertyExports from 'define-property-exports'
-import onlyObjectAssignedExports from 'only-object-assigned-exports'
-import requireAbsolute from 'require-absolute'
-
-export async function render(url, rootDir) {
- let html = ''
-
- const encryptedMsg = await bcrypt.hash('Secret Message!', 10)
- html += `\nencrypted message: ${encryptedMsg}
`
-
- const fileContent = await readFileContent(path.resolve(rootDir, 'message'))
- html += `\nmsg read via fs/promises: ${fileContent}
`
-
- html += `\nmessage from primitive export: ${primitiveExport}
`
-
- const tsDefaultExportMessage = tsDefaultExport()
- html += `\nmessage from ts-default-export: ${tsDefaultExportMessage}
`
-
- const tsNamedExportMessage = tsNamedExport()
- html += `\nmessage from ts-named-export: ${tsNamedExportMessage}
`
-
- const objectAssignedExportsMessage = objectAssignedExports.hello()
- html += `\nmessage from object-assigned-exports: ${objectAssignedExportsMessage}
`
-
- const forwardedExportMessage = forwardedExport.hello()
- html += `\nmessage from forwarded-export: ${forwardedExportMessage}
`
-
- const definePropertiesExportsMsg = definePropertiesExports.hello()
- html += `\nmessage from define-properties-exports: ${definePropertiesExportsMsg}
`
-
- const definePropertyExportsMsg = definePropertyExports.hello()
- html += `\nmessage from define-property-exports: ${definePropertyExportsMsg}
`
-
- const onlyObjectAssignedExportsMessage = onlyObjectAssignedExports.hello()
- html += `\nmessage from only-object-assigned-exports: ${onlyObjectAssignedExportsMessage}
`
-
- const requireAbsoluteMessage = requireAbsolute.hello()
- html += `\nmessage from require-absolute: ${requireAbsoluteMessage}
`
-
- return html + '\n'
-}
diff --git a/packages/playground/ssr-deps/ts-transpiled-exports/package.json b/packages/playground/ssr-deps/ts-transpiled-exports/package.json
deleted file mode 100644
index 7dbeff43974e42..00000000000000
--- a/packages/playground/ssr-deps/ts-transpiled-exports/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "ts-transpiled-exports",
- "private": true,
- "version": "0.0.0"
-}
diff --git a/packages/playground/ssr-html/__tests__/serve.js b/packages/playground/ssr-html/__tests__/serve.js
deleted file mode 100644
index 5ba5724f2b7a94..00000000000000
--- a/packages/playground/ssr-html/__tests__/serve.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// @ts-check
-// this is automtically detected by scripts/jestPerTestSetup.ts and will replace
-// the default e2e test serve behavior
-
-const path = require('path')
-
-const port = (exports.port = 9530)
-
-/**
- * @param {string} root
- * @param {boolean} isProd
- */
-exports.serve = async function serve(root, isProd) {
- const { createServer } = require(path.resolve(root, 'server.js'))
- const { app, vite } = await createServer(root, isProd)
-
- return new Promise((resolve, reject) => {
- try {
- const server = app.listen(port, () => {
- resolve({
- // for test teardown
- async close() {
- await new Promise((resolve) => {
- server.close(resolve)
- })
- if (vite) {
- await vite.close()
- }
- }
- })
- })
- } catch (e) {
- reject(e)
- }
- })
-}
diff --git a/packages/playground/ssr-html/__tests__/ssr-html.spec.ts b/packages/playground/ssr-html/__tests__/ssr-html.spec.ts
deleted file mode 100644
index e34b8a91fc3421..00000000000000
--- a/packages/playground/ssr-html/__tests__/ssr-html.spec.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { port } from './serve'
-import fetch from 'node-fetch'
-
-const url = `http://localhost:${port}`
-
-describe('injected inline scripts', () => {
- test('no injected inline scripts are present', async () => {
- await page.goto(url)
- const inlineScripts = await page.$$eval('script', (nodes) =>
- nodes.filter((n) => !n.getAttribute('src') && n.innerHTML)
- )
- expect(inlineScripts).toHaveLength(0)
- })
-
- test('injected script proxied correctly', async () => {
- await page.goto(url)
- const proxiedScripts = await page.$$eval('script', (nodes) =>
- nodes
- .filter((n) => {
- const src = n.getAttribute('src')
- if (!src) return false
- return src.includes('?html-proxy&index')
- })
- .map((n) => n.getAttribute('src'))
- )
-
- // assert at least 1 proxied script exists
- expect(proxiedScripts).not.toHaveLength(0)
-
- const scriptContents = await Promise.all(
- proxiedScripts.map((src) => fetch(url + src).then((res) => res.text()))
- )
-
- // all proxied scripts return code
- for (const code of scriptContents) {
- expect(code).toBeTruthy()
- }
- })
-})
diff --git a/packages/playground/ssr-html/index.html b/packages/playground/ssr-html/index.html
deleted file mode 100644
index c37dcc7e366ae8..00000000000000
--- a/packages/playground/ssr-html/index.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
- SSR HTML
-
-
- SSR Dynamic HTML
-
-
diff --git a/packages/playground/ssr-html/package.json b/packages/playground/ssr-html/package.json
deleted file mode 100644
index a14756422a8b28..00000000000000
--- a/packages/playground/ssr-html/package.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "test-ssr-html",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "node server",
- "serve": "cross-env NODE_ENV=production node server",
- "debug": "node --inspect-brk server"
- },
- "dependencies": {},
- "devDependencies": {
- "cross-env": "^7.0.3",
- "express": "^4.17.1"
- }
-}
diff --git a/packages/playground/ssr-html/server.js b/packages/playground/ssr-html/server.js
deleted file mode 100644
index ad115f1be01163..00000000000000
--- a/packages/playground/ssr-html/server.js
+++ /dev/null
@@ -1,75 +0,0 @@
-// @ts-check
-const fs = require('fs')
-const path = require('path')
-const express = require('express')
-
-const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD
-
-const DYNAMIC_SCRIPTS = `
-
-
-`
-
-async function createServer(
- root = process.cwd(),
- isProd = process.env.NODE_ENV === 'production'
-) {
- const resolve = (p) => path.resolve(__dirname, p)
-
- const app = express()
-
- /**
- * @type {import('vite').ViteDevServer}
- */
- let vite
- vite = await require('vite').createServer({
- root,
- logLevel: isTest ? 'error' : 'info',
- server: {
- middlewareMode: 'ssr',
- watch: {
- // During tests we edit the files too fast and sometimes chokidar
- // misses change events, so enforce polling for consistency
- usePolling: true,
- interval: 100
- }
- }
- })
- // use vite's connect instance as middleware
- app.use(vite.middlewares)
-
- app.use('*', async (req, res) => {
- try {
- let [url] = req.originalUrl.split('?')
- if (url.endsWith('/')) url += 'index.html'
-
- const htmlLoc = resolve(`.${url}`)
- let html = fs.readFileSync(htmlLoc, 'utf8')
- html = html.replace('', `${DYNAMIC_SCRIPTS}`)
- html = await vite.transformIndexHtml(url, html)
-
- res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
- } catch (e) {
- vite && vite.ssrFixStacktrace(e)
- console.log(e.stack)
- res.status(500).end(e.stack)
- }
- })
-
- return { app, vite }
-}
-
-if (!isTest) {
- createServer().then(({ app }) =>
- app.listen(3000, () => {
- console.log('http://localhost:3000')
- })
- )
-}
-
-// for test use
-exports.createServer = createServer
diff --git a/packages/playground/ssr-pug/__tests__/serve.js b/packages/playground/ssr-pug/__tests__/serve.js
deleted file mode 100644
index 5ba5724f2b7a94..00000000000000
--- a/packages/playground/ssr-pug/__tests__/serve.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// @ts-check
-// this is automtically detected by scripts/jestPerTestSetup.ts and will replace
-// the default e2e test serve behavior
-
-const path = require('path')
-
-const port = (exports.port = 9530)
-
-/**
- * @param {string} root
- * @param {boolean} isProd
- */
-exports.serve = async function serve(root, isProd) {
- const { createServer } = require(path.resolve(root, 'server.js'))
- const { app, vite } = await createServer(root, isProd)
-
- return new Promise((resolve, reject) => {
- try {
- const server = app.listen(port, () => {
- resolve({
- // for test teardown
- async close() {
- await new Promise((resolve) => {
- server.close(resolve)
- })
- if (vite) {
- await vite.close()
- }
- }
- })
- })
- } catch (e) {
- reject(e)
- }
- })
-}
diff --git a/packages/playground/ssr-pug/package.json b/packages/playground/ssr-pug/package.json
deleted file mode 100644
index e2282b20565c1b..00000000000000
--- a/packages/playground/ssr-pug/package.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "test-ssr-pug",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "node server",
- "serve": "cross-env NODE_ENV=production node server",
- "debug": "node --inspect-brk server"
- },
- "devDependencies": {
- "cross-env": "^7.0.3",
- "express": "^4.17.1",
- "pug": "^3.0.2"
- }
-}
diff --git a/packages/playground/ssr-pug/server.js b/packages/playground/ssr-pug/server.js
deleted file mode 100644
index 3cea5c48dde00b..00000000000000
--- a/packages/playground/ssr-pug/server.js
+++ /dev/null
@@ -1,76 +0,0 @@
-// @ts-check
-const path = require('path')
-const pug = require('pug')
-const express = require('express')
-
-const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD
-
-const DYNAMIC_SCRIPTS = `
-
-
-`
-
-async function createServer(
- root = process.cwd(),
- isProd = process.env.NODE_ENV === 'production'
-) {
- const resolve = (p) => path.resolve(__dirname, p)
-
- const app = express()
-
- /**
- * @type {import('vite').ViteDevServer}
- */
- let vite
- vite = await require('vite').createServer({
- root,
- logLevel: isTest ? 'error' : 'info',
- server: {
- middlewareMode: 'ssr',
- watch: {
- // During tests we edit the files too fast and sometimes chokidar
- // misses change events, so enforce polling for consistency
- usePolling: true,
- interval: 100
- }
- }
- })
- // use vite's connect instance as middleware
- app.use(vite.middlewares)
-
- app.use('*', async (req, res) => {
- try {
- let [url] = req.originalUrl.split('?')
- url = url.replace(/\.html$/, '.pug')
- if (url.endsWith('/')) url += 'index.pug'
-
- const htmlLoc = resolve(`.${url}`)
- let html = pug.renderFile(htmlLoc)
- html = html.replace('', `${DYNAMIC_SCRIPTS}`)
- html = await vite.transformIndexHtml(url, html)
-
- res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
- } catch (e) {
- vite && vite.ssrFixStacktrace(e)
- console.log(e.stack)
- res.status(500).end(e.stack)
- }
- })
-
- return { app, vite }
-}
-
-if (!isTest) {
- createServer().then(({ app }) =>
- app.listen(3000, () => {
- console.log('http://localhost:3000')
- })
- )
-}
-
-// for test use
-exports.createServer = createServer
diff --git a/packages/playground/ssr-pug/src/app.js b/packages/playground/ssr-pug/src/app.js
deleted file mode 100644
index 5b0175bb863d70..00000000000000
--- a/packages/playground/ssr-pug/src/app.js
+++ /dev/null
@@ -1,3 +0,0 @@
-const p = document.createElement('p')
-p.innerHTML = '✅ Dynamically injected script from file'
-document.body.appendChild(p)
diff --git a/packages/playground/ssr-react/__tests__/serve.js b/packages/playground/ssr-react/__tests__/serve.js
deleted file mode 100644
index 1bc028c03dc27c..00000000000000
--- a/packages/playground/ssr-react/__tests__/serve.js
+++ /dev/null
@@ -1,62 +0,0 @@
-// @ts-check
-// this is automtically detected by scripts/jestPerTestSetup.ts and will replace
-// the default e2e test serve behavior
-
-const path = require('path')
-
-const port = (exports.port = 9528)
-
-/**
- * @param {string} root
- * @param {boolean} isProd
- */
-exports.serve = async function serve(root, isProd) {
- if (isProd) {
- // build first
- const { build } = require('vite')
- // client build
- await build({
- root,
- logLevel: 'silent', // exceptions are logged by Jest
- build: {
- target: 'esnext',
- minify: false,
- ssrManifest: true,
- outDir: 'dist/client'
- }
- })
- // server build
- await build({
- root,
- logLevel: 'silent',
- build: {
- target: 'esnext',
- ssr: 'src/entry-server.jsx',
- outDir: 'dist/server'
- }
- })
- }
-
- const { createServer } = require(path.resolve(root, 'server.js'))
- const { app, vite } = await createServer(root, isProd)
-
- return new Promise((resolve, reject) => {
- try {
- const server = app.listen(port, () => {
- resolve({
- // for test teardown
- async close() {
- await new Promise((resolve) => {
- server.close(resolve)
- })
- if (vite) {
- await vite.close()
- }
- }
- })
- })
- } catch (e) {
- reject(e)
- }
- })
-}
diff --git a/packages/playground/ssr-react/__tests__/ssr-react.spec.ts b/packages/playground/ssr-react/__tests__/ssr-react.spec.ts
deleted file mode 100644
index 2235d4ae4d0edf..00000000000000
--- a/packages/playground/ssr-react/__tests__/ssr-react.spec.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { editFile, untilUpdated } from '../../testUtils'
-import { port } from './serve'
-import fetch from 'node-fetch'
-
-const url = `http://localhost:${port}`
-
-test('/env', async () => {
- await page.goto(url + '/env')
- expect(await page.textContent('h1')).toMatch('default message here')
-
- // raw http request
- const envHtml = await (await fetch(url + '/env')).text()
- expect(envHtml).toMatch('API_KEY_qwertyuiop')
-})
-
-test('/about', async () => {
- await page.goto(url + '/about')
- expect(await page.textContent('h1')).toMatch('About')
- // should not have hydration mismatch
- browserLogs.forEach((msg) => {
- expect(msg).not.toMatch('Expected server HTML')
- })
-
- // raw http request
- const aboutHtml = await (await fetch(url + '/about')).text()
- expect(aboutHtml).toMatch('About')
-})
-
-test('/', async () => {
- await page.goto(url)
- expect(await page.textContent('h1')).toMatch('Home')
- // should not have hydration mismatch
- browserLogs.forEach((msg) => {
- expect(msg).not.toMatch('Expected server HTML')
- })
-
- // raw http request
- const html = await (await fetch(url)).text()
- expect(html).toMatch('Home')
-})
-
-test('hmr', async () => {
- editFile('src/pages/Home.jsx', (code) =>
- code.replace('Home', 'changed')
- )
- await untilUpdated(() => page.textContent('h1'), 'changed')
-})
-
-test('client navigation', async () => {
- await untilUpdated(() => page.textContent('a[href="/about"]'), 'About')
- await page.click('a[href="/about"]')
- await untilUpdated(() => page.textContent('h1'), 'About')
- editFile('src/pages/About.jsx', (code) =>
- code.replace('About', 'changed')
- )
- await untilUpdated(() => page.textContent('h1'), 'changed')
-})
-
-test(`circular dependecies modules doesn't throw`, async () => {
- await page.goto(url)
- expect(await page.textContent('.circ-dep-init')).toMatch(
- 'circ-dep-init-a circ-dep-init-b'
- )
-})
diff --git a/packages/playground/ssr-react/index.html b/packages/playground/ssr-react/index.html
deleted file mode 100644
index 1c891c04355068..00000000000000
--- a/packages/playground/ssr-react/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
- Vite App
-
-
-
-
-
-
diff --git a/packages/playground/ssr-react/package.json b/packages/playground/ssr-react/package.json
deleted file mode 100644
index a05bcc08806f3b..00000000000000
--- a/packages/playground/ssr-react/package.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "name": "test-ssr-react",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "node server",
- "build": "npm run build:client && npm run build:server",
- "build:client": "vite build --outDir dist/client",
- "build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server",
- "generate": "vite build --outDir dist/static && npm run build:server && node prerender",
- "serve": "cross-env NODE_ENV=production node server",
- "debug": "node --inspect-brk server"
- },
- "dependencies": {
- "react": "^17.0.2",
- "react-dom": "^17.0.2",
- "react-router": "^5.2.1",
- "react-router-dom": "^5.3.0"
- },
- "devDependencies": {
- "@vitejs/plugin-react": "workspace:*",
- "compression": "^1.7.4",
- "cross-env": "^7.0.3",
- "express": "^4.17.1",
- "serve-static": "^1.14.1"
- }
-}
diff --git a/packages/playground/ssr-react/prerender.js b/packages/playground/ssr-react/prerender.js
deleted file mode 100644
index ac88ef632ec6f5..00000000000000
--- a/packages/playground/ssr-react/prerender.js
+++ /dev/null
@@ -1,32 +0,0 @@
-// Pre-render the app into static HTML.
-// run `yarn generate` and then `dist/static` can be served as a static site.
-
-const fs = require('fs')
-const path = require('path')
-
-const toAbsolute = (p) => path.resolve(__dirname, p)
-
-const template = fs.readFileSync(toAbsolute('dist/static/index.html'), 'utf-8')
-const { render } = require('./dist/server/entry-server.js')
-
-// determine routes to pre-render from src/pages
-const routesToPrerender = fs
- .readdirSync(toAbsolute('src/pages'))
- .map((file) => {
- const name = file.replace(/\.jsx$/, '').toLowerCase()
- return name === 'home' ? `/` : `/${name}`
- })
-
-;(async () => {
- // pre-render each route...
- for (const url of routesToPrerender) {
- const context = {}
- const appHtml = await render(url, context)
-
- const html = template.replace(``, appHtml)
-
- const filePath = `dist/static${url === '/' ? '/index' : url}.html`
- fs.writeFileSync(toAbsolute(filePath), html)
- console.log('pre-rendered:', filePath)
- }
-})()
diff --git a/packages/playground/ssr-react/server.js b/packages/playground/ssr-react/server.js
deleted file mode 100644
index 1876439c18fa88..00000000000000
--- a/packages/playground/ssr-react/server.js
+++ /dev/null
@@ -1,96 +0,0 @@
-// @ts-check
-const fs = require('fs')
-const path = require('path')
-const express = require('express')
-
-const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD
-
-process.env.MY_CUSTOM_SECRET = 'API_KEY_qwertyuiop'
-
-async function createServer(
- root = process.cwd(),
- isProd = process.env.NODE_ENV === 'production'
-) {
- const resolve = (p) => path.resolve(__dirname, p)
-
- const indexProd = isProd
- ? fs.readFileSync(resolve('dist/client/index.html'), 'utf-8')
- : ''
-
- const app = express()
-
- /**
- * @type {import('vite').ViteDevServer}
- */
- let vite
- if (!isProd) {
- vite = await require('vite').createServer({
- root,
- logLevel: isTest ? 'error' : 'info',
- server: {
- middlewareMode: 'ssr',
- watch: {
- // During tests we edit the files too fast and sometimes chokidar
- // misses change events, so enforce polling for consistency
- usePolling: true,
- interval: 100
- }
- }
- })
- // use vite's connect instance as middleware
- app.use(vite.middlewares)
- } else {
- app.use(require('compression')())
- app.use(
- require('serve-static')(resolve('dist/client'), {
- index: false
- })
- )
- }
-
- app.use('*', async (req, res) => {
- try {
- const url = req.originalUrl
-
- let template, render
- if (!isProd) {
- // always read fresh template in dev
- template = fs.readFileSync(resolve('index.html'), 'utf-8')
- template = await vite.transformIndexHtml(url, template)
- render = (await vite.ssrLoadModule('/src/entry-server.jsx')).render
- } else {
- template = indexProd
- render = require('./dist/server/entry-server.js').render
- }
-
- const context = {}
- const appHtml = render(url, context)
-
- if (context.url) {
- // Somewhere a `` was rendered
- return res.redirect(301, context.url)
- }
-
- const html = template.replace(``, appHtml)
-
- res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
- } catch (e) {
- !isProd && vite.ssrFixStacktrace(e)
- console.log(e.stack)
- res.status(500).end(e.stack)
- }
- })
-
- return { app, vite }
-}
-
-if (!isTest) {
- createServer().then(({ app }) =>
- app.listen(3000, () => {
- console.log('http://localhost:3000')
- })
- )
-}
-
-// for test use
-exports.createServer = createServer
diff --git a/packages/playground/ssr-react/src/App.jsx b/packages/playground/ssr-react/src/App.jsx
deleted file mode 100644
index 1c598add666efb..00000000000000
--- a/packages/playground/ssr-react/src/App.jsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Link, Route, Switch } from 'react-router-dom'
-
-// Auto generates routes from files under ./pages
-// https://vitejs.dev/guide/features.html#glob-import
-const pages = import.meta.globEager('./pages/*.jsx')
-
-const routes = Object.keys(pages).map((path) => {
- const name = path.match(/\.\/pages\/(.*)\.jsx$/)[1]
- return {
- name,
- path: name === 'Home' ? '/' : `/${name.toLowerCase()}`,
- component: pages[path].default
- }
-})
-
-export function App() {
- return (
- <>
-
-
- {routes.map(({ name, path }) => {
- return (
-
- {name}
-
- )
- })}
-
-
-
- {routes.map(({ path, component: RouteComp }) => {
- return (
-
-
-
- )
- })}
-
- >
- )
-}
diff --git a/packages/playground/ssr-react/src/add.js b/packages/playground/ssr-react/src/add.js
deleted file mode 100644
index a0e419e9cfcacf..00000000000000
--- a/packages/playground/ssr-react/src/add.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { multiply } from './multiply'
-
-export function add(a, b) {
- return a + b
-}
-
-export function addAndMultiply(a, b, c) {
- return multiply(add(a, b), c)
-}
diff --git a/packages/playground/ssr-react/src/entry-client.jsx b/packages/playground/ssr-react/src/entry-client.jsx
deleted file mode 100644
index 8757bdc929d0e4..00000000000000
--- a/packages/playground/ssr-react/src/entry-client.jsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import ReactDOM from 'react-dom'
-import { BrowserRouter } from 'react-router-dom'
-import { App } from './App'
-
-ReactDOM.hydrate(
-
-
- ,
- document.getElementById('app')
-)
diff --git a/packages/playground/ssr-react/src/entry-server.jsx b/packages/playground/ssr-react/src/entry-server.jsx
deleted file mode 100644
index 56d4810d11ba3c..00000000000000
--- a/packages/playground/ssr-react/src/entry-server.jsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import ReactDOMServer from 'react-dom/server'
-import { StaticRouter } from 'react-router-dom'
-import { App } from './App'
-
-export function render(url, context) {
- return ReactDOMServer.renderToString(
-
-
-
- )
-}
diff --git a/packages/playground/ssr-react/src/multiply.js b/packages/playground/ssr-react/src/multiply.js
deleted file mode 100644
index 94f43efbff58bd..00000000000000
--- a/packages/playground/ssr-react/src/multiply.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { add } from './add'
-
-export function multiply(a, b) {
- return a * b
-}
-
-export function multiplyAndAdd(a, b, c) {
- return add(multiply(a, b), c)
-}
diff --git a/packages/playground/ssr-react/src/pages/About.jsx b/packages/playground/ssr-react/src/pages/About.jsx
deleted file mode 100644
index 0fe4de69078504..00000000000000
--- a/packages/playground/ssr-react/src/pages/About.jsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { addAndMultiply } from '../add'
-import { multiplyAndAdd } from '../multiply'
-
-export default function About() {
- return (
- <>
- About
- {addAndMultiply(1, 2, 3)}
- {multiplyAndAdd(1, 2, 3)}
- >
- )
-}
diff --git a/packages/playground/ssr-react/src/pages/Env.jsx b/packages/playground/ssr-react/src/pages/Env.jsx
deleted file mode 100644
index 1102990f11c8cb..00000000000000
--- a/packages/playground/ssr-react/src/pages/Env.jsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function Env() {
- let msg = 'default message here'
- try {
- msg = process.env.MY_CUSTOM_SECRET || msg
- } catch {}
- return {msg}
-}
diff --git a/packages/playground/ssr-react/src/pages/Home.jsx b/packages/playground/ssr-react/src/pages/Home.jsx
deleted file mode 100644
index d1f4944810cc98..00000000000000
--- a/packages/playground/ssr-react/src/pages/Home.jsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { addAndMultiply } from '../add'
-import { multiplyAndAdd } from '../multiply'
-import { commonModuleExport } from '../forked-deadlock/common-module'
-import { getValueAB } from '../circular-dep-init/circular-dep-init'
-
-export default function Home() {
- commonModuleExport()
-
- return (
- <>
- Home
- {addAndMultiply(1, 2, 3)}
- {multiplyAndAdd(1, 2, 3)}
- {getValueAB()}
- >
- )
-}
diff --git a/packages/playground/ssr-react/vite.config.js b/packages/playground/ssr-react/vite.config.js
deleted file mode 100644
index bcc1369313cc5a..00000000000000
--- a/packages/playground/ssr-react/vite.config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const react = require('@vitejs/plugin-react')
-
-/**
- * @type {import('vite').UserConfig}
- */
-module.exports = {
- plugins: [react()],
- build: {
- minify: false
- }
-}
diff --git a/packages/playground/ssr-vue/__tests__/serve.js b/packages/playground/ssr-vue/__tests__/serve.js
deleted file mode 100644
index 1e220fed9630e4..00000000000000
--- a/packages/playground/ssr-vue/__tests__/serve.js
+++ /dev/null
@@ -1,62 +0,0 @@
-// @ts-check
-// this is automtically detected by scripts/jestPerTestSetup.ts and will replace
-// the default e2e test serve behavior
-
-const path = require('path')
-
-const port = (exports.port = 9527)
-
-/**
- * @param {string} root
- * @param {boolean} isProd
- */
-exports.serve = async function serve(root, isProd) {
- if (isProd) {
- // build first
- const { build } = require('vite')
- // client build
- await build({
- root,
- logLevel: 'silent', // exceptions are logged by Jest
- build: {
- target: 'esnext',
- minify: false,
- ssrManifest: true,
- outDir: 'dist/client'
- }
- })
- // server build
- await build({
- root,
- logLevel: 'silent',
- build: {
- target: 'esnext',
- ssr: 'src/entry-server.js',
- outDir: 'dist/server'
- }
- })
- }
-
- const { createServer } = require(path.resolve(root, 'server.js'))
- const { app, vite } = await createServer(root, isProd)
-
- return new Promise((resolve, reject) => {
- try {
- const server = app.listen(port, () => {
- resolve({
- // for test teardown
- async close() {
- await new Promise((resolve) => {
- server.close(resolve)
- })
- if (vite) {
- await vite.close()
- }
- }
- })
- })
- } catch (e) {
- reject(e)
- }
- })
-}
diff --git a/packages/playground/ssr-vue/__tests__/ssr-vue.spec.ts b/packages/playground/ssr-vue/__tests__/ssr-vue.spec.ts
deleted file mode 100644
index 952e287a7f12aa..00000000000000
--- a/packages/playground/ssr-vue/__tests__/ssr-vue.spec.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import { editFile, getColor, isBuild, untilUpdated } from '../../testUtils'
-import { port } from './serve'
-import fetch from 'node-fetch'
-import { resolve } from 'path'
-
-const url = `http://localhost:${port}`
-
-test('vuex can be import succeed by named import', async () => {
- await page.goto(url + '/store')
- expect(await page.textContent('h1')).toMatch('bar')
-
- // raw http request
- const storeHtml = await (await fetch(url + '/store')).text()
- expect(storeHtml).toMatch('bar')
-})
-
-test('/about', async () => {
- await page.goto(url + '/about')
- expect(await page.textContent('h1')).toMatch('About')
- // should not have hydration mismatch
- browserLogs.forEach((msg) => {
- expect(msg).not.toMatch('mismatch')
- })
-
- // fetch sub route
- const aboutHtml = await (await fetch(url + '/about')).text()
- expect(aboutHtml).toMatch('About')
- if (isBuild) {
- // assert correct preload directive generation for async chunks and CSS
- expect(aboutHtml).not.toMatch(
- /link rel="modulepreload".*?href="\/assets\/Home\.\w{8}\.js"/
- )
- expect(aboutHtml).not.toMatch(
- /link rel="stylesheet".*?href="\/assets\/Home\.\w{8}\.css"/
- )
- expect(aboutHtml).toMatch(
- /link rel="modulepreload".*?href="\/assets\/About\.\w{8}\.js"/
- )
- expect(aboutHtml).toMatch(
- /link rel="stylesheet".*?href="\/assets\/About\.\w{8}\.css"/
- )
- }
-})
-
-test('/external', async () => {
- await page.goto(url + '/external')
- expect(await page.textContent('div')).toMatch(
- 'Example external component content'
- )
- // should not have hydration mismatch
- browserLogs.forEach((msg) => {
- expect(msg).not.toMatch('mismatch')
- })
-
- // fetch sub route
- const externalHtml = await (await fetch(url + '/external')).text()
- expect(externalHtml).toMatch('Example external component content')
- if (isBuild) {
- // assert correct preload directive generation for async chunks and CSS
- expect(externalHtml).not.toMatch(
- /link rel="modulepreload".*?href="\/assets\/Home\.\w{8}\.js"/
- )
- expect(externalHtml).not.toMatch(
- /link rel="stylesheet".*?href="\/assets\/Home\.\w{8}\.css"/
- )
- expect(externalHtml).toMatch(
- /link rel="modulepreload".*?href="\/assets\/External\.\w{8}\.js"/
- )
- }
-})
-
-test('/', async () => {
- await page.goto(url)
- expect(await page.textContent('h1')).toMatch('Home')
- // should not have hydration mismatch
- browserLogs.forEach((msg) => {
- expect(msg).not.toMatch('mismatch')
- })
-
- const html = await (await fetch(url)).text()
- expect(html).toMatch('Home')
- if (isBuild) {
- // assert correct preload directive generation for async chunks and CSS
- expect(html).toMatch(
- /link rel="modulepreload".*?href="\/assets\/Home\.\w{8}\.js"/
- )
- expect(html).toMatch(
- /link rel="stylesheet".*?href="\/assets\/Home\.\w{8}\.css"/
- )
- // JSX component preload registration
- expect(html).toMatch(
- /link rel="modulepreload".*?href="\/assets\/Foo\.\w{8}\.js"/
- )
- expect(html).toMatch(
- /link rel="stylesheet".*?href="\/assets\/Foo\.\w{8}\.css"/
- )
- expect(html).not.toMatch(
- /link rel="modulepreload".*?href="\/assets\/About\.\w{8}\.js"/
- )
- expect(html).not.toMatch(
- /link rel="stylesheet".*?href="\/assets\/About\.\w{8}\.css"/
- )
- }
-})
-
-test('css', async () => {
- if (isBuild) {
- expect(await getColor('h1')).toBe('green')
- expect(await getColor('.jsx')).toBe('blue')
- } else {
- // During dev, the CSS is loaded from async chunk and we may have to wait
- // when the test runs concurrently.
- await untilUpdated(() => getColor('h1'), 'green')
- await untilUpdated(() => getColor('.jsx'), 'blue')
- }
-})
-
-test('asset', async () => {
- // should have no 404s
- browserLogs.forEach((msg) => {
- expect(msg).not.toMatch('404')
- })
- const img = await page.$('img')
- expect(await img.getAttribute('src')).toMatch(
- isBuild ? /\/assets\/logo\.\w{8}\.png/ : '/src/assets/logo.png'
- )
-})
-
-test('jsx', async () => {
- expect(await page.textContent('.jsx')).toMatch('from JSX')
-})
-
-test('virtual module', async () => {
- expect(await page.textContent('.virtual')).toMatch('hi')
-})
-
-test('nested virtual module', async () => {
- expect(await page.textContent('.nested-virtual')).toMatch('[success]')
-})
-
-test('hydration', async () => {
- expect(await page.textContent('button')).toMatch('0')
- await page.click('button')
- expect(await page.textContent('button')).toMatch('1')
-})
-
-test('hmr', async () => {
- editFile('src/pages/Home.vue', (code) => code.replace('Home', 'changed'))
- await untilUpdated(() => page.textContent('h1'), 'changed')
-})
-
-test('client navigation', async () => {
- await untilUpdated(() => page.textContent('a[href="/about"]'), 'About')
- await page.click('a[href="/about"]')
- await untilUpdated(() => page.textContent('h1'), 'About')
- editFile('src/pages/About.vue', (code) => code.replace('About', 'changed'))
- await untilUpdated(() => page.textContent('h1'), 'changed')
- await page.click('a[href="/"]')
- await untilUpdated(() => page.textContent('a[href="/"]'), 'Home')
-})
-
-test('import.meta.url', async () => {
- await page.goto(url)
- expect(await page.textContent('.protocol')).toEqual('file:')
-})
-
-test('dynamic css file should be preloaded', async () => {
- if (isBuild) {
- await page.goto(url)
- const homeHtml = await (await fetch(url)).text()
- const re = /link rel="modulepreload".*?href="\/assets\/(Home\.\w{8}\.js)"/
- const filename = re.exec(homeHtml)[1]
- const manifest = require(resolve(
- process.cwd(),
- './packages/temp/ssr-vue/dist/client/ssr-manifest.json'
- ))
- const depFile = manifest[filename]
- for (const file of depFile) {
- expect(homeHtml).toMatch(file)
- }
- }
-})
diff --git a/packages/playground/ssr-vue/dep-import-type/deep/index.d.ts b/packages/playground/ssr-vue/dep-import-type/deep/index.d.ts
deleted file mode 100644
index 39df3b83f7e9b8..00000000000000
--- a/packages/playground/ssr-vue/dep-import-type/deep/index.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-export interface Foo {}
diff --git a/packages/playground/ssr-vue/dep-import-type/package.json b/packages/playground/ssr-vue/dep-import-type/package.json
deleted file mode 100644
index 935f28eb7f7157..00000000000000
--- a/packages/playground/ssr-vue/dep-import-type/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "dep-import-type",
- "private": true,
- "version": "0.0.0",
- "main": "index.js"
-}
diff --git a/packages/playground/ssr-vue/example-external-component/ExampleExternalComponent.vue b/packages/playground/ssr-vue/example-external-component/ExampleExternalComponent.vue
deleted file mode 100644
index 55f3cea40e0399..00000000000000
--- a/packages/playground/ssr-vue/example-external-component/ExampleExternalComponent.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-
- Example external component content
-
diff --git a/packages/playground/ssr-vue/example-external-component/index.js b/packages/playground/ssr-vue/example-external-component/index.js
deleted file mode 100644
index 8fc72c3aee0652..00000000000000
--- a/packages/playground/ssr-vue/example-external-component/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import ExampleExternalComponent from './ExampleExternalComponent.vue'
-
-export default ExampleExternalComponent
diff --git a/packages/playground/ssr-vue/example-external-component/package.json b/packages/playground/ssr-vue/example-external-component/package.json
deleted file mode 100644
index 302e7fd4d9ff05..00000000000000
--- a/packages/playground/ssr-vue/example-external-component/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "example-external-component",
- "private": true,
- "version": "0.0.0",
- "main": "index.js"
-}
diff --git a/packages/playground/ssr-vue/index.html b/packages/playground/ssr-vue/index.html
deleted file mode 100644
index 17b46a2f7a2267..00000000000000
--- a/packages/playground/ssr-vue/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
- Vite App
-
-
-
-
-
-
-
diff --git a/packages/playground/ssr-vue/package.json b/packages/playground/ssr-vue/package.json
deleted file mode 100644
index 4a385336a97603..00000000000000
--- a/packages/playground/ssr-vue/package.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "name": "test-ssr-vue",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "node server",
- "build": "npm run build:client && npm run build:server",
- "build:noExternal": "npm run build:client && npm run build:server:noExternal",
- "build:client": "vite build --ssrManifest --outDir dist/client",
- "build:server": "vite build --ssr src/entry-server.js --outDir dist/server",
- "build:server:noExternal": "vite build --config vite.config.noexternal.js --ssr src/entry-server.js --outDir dist/server",
- "generate": "vite build --ssrManifest --outDir dist/static && npm run build:server && node prerender",
- "serve": "cross-env NODE_ENV=production node server",
- "debug": "node --inspect-brk server"
- },
- "dependencies": {
- "example-external-component": "file:example-external-component",
- "vue": "^3.2.25",
- "vue-router": "^4.0.0",
- "vuex": "^4.0.2"
- },
- "devDependencies": {
- "@vitejs/plugin-vue": "workspace:*",
- "@vitejs/plugin-vue-jsx": "workspace:*",
- "compression": "^1.7.4",
- "cross-env": "^7.0.3",
- "dep-import-type": "link:./dep-import-type",
- "express": "^4.17.1",
- "serve-static": "^1.14.1"
- }
-}
diff --git a/packages/playground/ssr-vue/prerender.js b/packages/playground/ssr-vue/prerender.js
deleted file mode 100644
index c4158dbe3357a9..00000000000000
--- a/packages/playground/ssr-vue/prerender.js
+++ /dev/null
@@ -1,37 +0,0 @@
-// Pre-render the app into static HTML.
-// run `npm run generate` and then `dist/static` can be served as a static site.
-
-const fs = require('fs')
-const path = require('path')
-
-const toAbsolute = (p) => path.resolve(__dirname, p)
-
-const manifest = require('./dist/static/ssr-manifest.json')
-const template = fs.readFileSync(toAbsolute('dist/static/index.html'), 'utf-8')
-const { render } = require('./dist/server/entry-server.js')
-
-// determine routes to pre-render from src/pages
-const routesToPrerender = fs
- .readdirSync(toAbsolute('src/pages'))
- .map((file) => {
- const name = file.replace(/\.vue$/, '').toLowerCase()
- return name === 'home' ? `/` : `/${name}`
- })
-
-;(async () => {
- // pre-render each route...
- for (const url of routesToPrerender) {
- const [appHtml, preloadLinks] = await render(url, manifest)
-
- const html = template
- .replace(``, preloadLinks)
- .replace(``, appHtml)
-
- const filePath = `dist/static${url === '/' ? '/index' : url}.html`
- fs.writeFileSync(toAbsolute(filePath), html)
- console.log('pre-rendered:', filePath)
- }
-
- // done, delete ssr manifest
- fs.unlinkSync(toAbsolute('dist/static/ssr-manifest.json'))
-})()
diff --git a/packages/playground/ssr-vue/server.js b/packages/playground/ssr-vue/server.js
deleted file mode 100644
index 642f274647294f..00000000000000
--- a/packages/playground/ssr-vue/server.js
+++ /dev/null
@@ -1,95 +0,0 @@
-// @ts-check
-const fs = require('fs')
-const path = require('path')
-const express = require('express')
-
-const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD
-
-async function createServer(
- root = process.cwd(),
- isProd = process.env.NODE_ENV === 'production'
-) {
- const resolve = (p) => path.resolve(__dirname, p)
-
- const indexProd = isProd
- ? fs.readFileSync(resolve('dist/client/index.html'), 'utf-8')
- : ''
-
- const manifest = isProd
- ? // @ts-ignore
- require('./dist/client/ssr-manifest.json')
- : {}
-
- const app = express()
-
- /**
- * @type {import('vite').ViteDevServer}
- */
- let vite
- if (!isProd) {
- vite = await require('vite').createServer({
- root,
- logLevel: isTest ? 'error' : 'info',
- server: {
- middlewareMode: 'ssr',
- watch: {
- // During tests we edit the files too fast and sometimes chokidar
- // misses change events, so enforce polling for consistency
- usePolling: true,
- interval: 100
- }
- }
- })
- // use vite's connect instance as middleware
- app.use(vite.middlewares)
- } else {
- app.use(require('compression')())
- app.use(
- require('serve-static')(resolve('dist/client'), {
- index: false
- })
- )
- }
-
- app.use('*', async (req, res) => {
- try {
- const url = req.originalUrl
-
- let template, render
- if (!isProd) {
- // always read fresh template in dev
- template = fs.readFileSync(resolve('index.html'), 'utf-8')
- template = await vite.transformIndexHtml(url, template)
- render = (await vite.ssrLoadModule('/src/entry-server.js')).render
- } else {
- template = indexProd
- render = require('./dist/server/entry-server.js').render
- }
-
- const [appHtml, preloadLinks] = await render(url, manifest)
-
- const html = template
- .replace(``, preloadLinks)
- .replace(``, appHtml)
-
- res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
- } catch (e) {
- vite && vite.ssrFixStacktrace(e)
- console.log(e.stack)
- res.status(500).end(e.stack)
- }
- })
-
- return { app, vite }
-}
-
-if (!isTest) {
- createServer().then(({ app }) =>
- app.listen(3000, () => {
- console.log('http://localhost:3000')
- })
- )
-}
-
-// for test use
-exports.createServer = createServer
diff --git a/packages/playground/ssr-vue/src/App.vue b/packages/playground/ssr-vue/src/App.vue
deleted file mode 100644
index dc8bfca16a59ab..00000000000000
--- a/packages/playground/ssr-vue/src/App.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
- Home |
- About
-
-
-
-
-
-
-
-
-
diff --git a/packages/playground/ssr-vue/src/assets/button.css b/packages/playground/ssr-vue/src/assets/button.css
deleted file mode 100644
index 8e1ebc58c0891f..00000000000000
--- a/packages/playground/ssr-vue/src/assets/button.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.btn {
- background-color: #65b587;
- border-radius: 8px;
- border-style: none;
- box-sizing: border-box;
- cursor: pointer;
- display: inline-block;
- font-size: 14px;
- font-weight: 500;
- height: 40px;
- line-height: 20px;
- list-style: none;
- outline: none;
- padding: 10px 16px;
-}
diff --git a/packages/playground/ssr-vue/src/assets/fonts/Inter-Italic.woff b/packages/playground/ssr-vue/src/assets/fonts/Inter-Italic.woff
deleted file mode 100644
index e7da6663fe5e47..00000000000000
Binary files a/packages/playground/ssr-vue/src/assets/fonts/Inter-Italic.woff and /dev/null differ
diff --git a/packages/playground/ssr-vue/src/assets/fonts/Inter-Italic.woff2 b/packages/playground/ssr-vue/src/assets/fonts/Inter-Italic.woff2
deleted file mode 100644
index 8559dfde38986e..00000000000000
Binary files a/packages/playground/ssr-vue/src/assets/fonts/Inter-Italic.woff2 and /dev/null differ
diff --git a/packages/playground/ssr-vue/src/assets/logo.png b/packages/playground/ssr-vue/src/assets/logo.png
deleted file mode 100644
index f3d2503fc2a44b..00000000000000
Binary files a/packages/playground/ssr-vue/src/assets/logo.png and /dev/null differ
diff --git a/packages/playground/ssr-vue/src/components/Foo.jsx b/packages/playground/ssr-vue/src/components/Foo.jsx
deleted file mode 100644
index 427815b2d252d2..00000000000000
--- a/packages/playground/ssr-vue/src/components/Foo.jsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { defineComponent } from 'vue'
-import './foo.css'
-
-// named exports w/ variable declaration: ok
-export const Foo = defineComponent({
- name: 'foo',
- setup() {
- return () => from JSX
- }
-})
diff --git a/packages/playground/ssr-vue/src/components/ImportType.vue b/packages/playground/ssr-vue/src/components/ImportType.vue
deleted file mode 100644
index 144d36bc34e7ec..00000000000000
--- a/packages/playground/ssr-vue/src/components/ImportType.vue
+++ /dev/null
@@ -1,8 +0,0 @@
-
- import type should be removed without side-effect
-
-
-
diff --git a/packages/playground/ssr-vue/src/components/button.js b/packages/playground/ssr-vue/src/components/button.js
deleted file mode 100644
index 3b39f53fd96c47..00000000000000
--- a/packages/playground/ssr-vue/src/components/button.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { createVNode, defineComponent } from 'vue'
-import '../assets/button.css'
-
-export default defineComponent({
- setup() {
- return () => {
- return createVNode(
- 'div',
- {
- class: 'btn'
- },
- 'dynamicBtn'
- )
- }
- }
-})
diff --git a/packages/playground/ssr-vue/src/components/foo.css b/packages/playground/ssr-vue/src/components/foo.css
deleted file mode 100644
index f8baa0d15b90d3..00000000000000
--- a/packages/playground/ssr-vue/src/components/foo.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.jsx {
- color: blue;
-}
diff --git a/packages/playground/ssr-vue/src/entry-client.js b/packages/playground/ssr-vue/src/entry-client.js
deleted file mode 100644
index 842acce7dc685b..00000000000000
--- a/packages/playground/ssr-vue/src/entry-client.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { createApp } from './main'
-
-const { app, router } = createApp()
-
-// wait until router is ready before mounting to ensure hydration match
-router.isReady().then(() => {
- app.mount('#app')
-})
diff --git a/packages/playground/ssr-vue/src/entry-server.js b/packages/playground/ssr-vue/src/entry-server.js
deleted file mode 100644
index 0f4e47711c17a1..00000000000000
--- a/packages/playground/ssr-vue/src/entry-server.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { createApp } from './main'
-import { renderToString } from 'vue/server-renderer'
-import path, { basename } from 'path'
-
-export async function render(url, manifest) {
- const { app, router } = createApp()
-
- // set the router to the desired URL before rendering
- router.push(url)
- await router.isReady()
-
- // passing SSR context object which will be available via useSSRContext()
- // @vitejs/plugin-vue injects code into a component's setup() that registers
- // itself on ctx.modules. After the render, ctx.modules would contain all the
- // components that have been instantiated during this render call.
- const ctx = {}
- const html = await renderToString(app, ctx)
-
- // the SSR manifest generated by Vite contains module -> chunk/asset mapping
- // which we can then use to determine what files need to be preloaded for this
- // request.
- const preloadLinks = renderPreloadLinks(ctx.modules, manifest)
- return [html, preloadLinks]
-}
-
-function renderPreloadLinks(modules, manifest) {
- let links = ''
- const seen = new Set()
- modules.forEach((id) => {
- const files = manifest[id]
- if (files) {
- files.forEach((file) => {
- if (!seen.has(file)) {
- seen.add(file)
- const filename = basename(file)
- if (manifest[filename]) {
- for (const depFile of manifest[filename]) {
- links += renderPreloadLink(depFile)
- seen.add(depFile)
- }
- }
- links += renderPreloadLink(file)
- }
- })
- }
- })
- return links
-}
-
-function renderPreloadLink(file) {
- if (file.endsWith('.js')) {
- return ` `
- } else if (file.endsWith('.css')) {
- return ` `
- } else if (file.endsWith('.woff')) {
- return ` `
- } else if (file.endsWith('.woff2')) {
- return ` `
- } else if (file.endsWith('.gif')) {
- return ` `
- } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) {
- return ` `
- } else if (file.endsWith('.png')) {
- return ` `
- } else {
- // TODO
- return ''
- }
-}
diff --git a/packages/playground/ssr-vue/src/main.js b/packages/playground/ssr-vue/src/main.js
deleted file mode 100644
index dbf4287b0baf3c..00000000000000
--- a/packages/playground/ssr-vue/src/main.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import App from './App.vue'
-import { createSSRApp } from 'vue'
-import { createRouter } from './router'
-
-// SSR requires a fresh app instance per request, therefore we export a function
-// that creates a fresh app instance. If using Vuex, we'd also be creating a
-// fresh store here.
-export function createApp() {
- const app = createSSRApp(App)
- const router = createRouter()
- app.use(router)
- return { app, router }
-}
diff --git a/packages/playground/ssr-vue/src/pages/About.vue b/packages/playground/ssr-vue/src/pages/About.vue
deleted file mode 100644
index 2c8589f7ff109a..00000000000000
--- a/packages/playground/ssr-vue/src/pages/About.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-
- {{ msg }}
- {{ url }}
- CommonButton
-
-
-
-
-
diff --git a/packages/playground/ssr-vue/src/pages/External.vue b/packages/playground/ssr-vue/src/pages/External.vue
deleted file mode 100644
index ffdcd03b85be84..00000000000000
--- a/packages/playground/ssr-vue/src/pages/External.vue
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
diff --git a/packages/playground/ssr-vue/src/pages/Home.vue b/packages/playground/ssr-vue/src/pages/Home.vue
deleted file mode 100644
index 32a33882cc2324..00000000000000
--- a/packages/playground/ssr-vue/src/pages/Home.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-
- Home
-
-
-
- count is: {{ state.count }}
-
- msg from virtual module: {{ foo.msg }}
- this will be styled with a font-face
- {{ state.url }}
- {{ state.protocol }}
- msg from nested virtual module: {{ virtualMsg }}
- CommonButton
-
-
-
-
-
-
-
-
diff --git a/packages/playground/ssr-vue/src/pages/Store.vue b/packages/playground/ssr-vue/src/pages/Store.vue
deleted file mode 100644
index df4d6b302d8474..00000000000000
--- a/packages/playground/ssr-vue/src/pages/Store.vue
+++ /dev/null
@@ -1,24 +0,0 @@
-
- {{ foo }}
-
-
-
-
-
diff --git a/packages/playground/ssr-vue/src/router.js b/packages/playground/ssr-vue/src/router.js
deleted file mode 100644
index b80b76b0bf4e2a..00000000000000
--- a/packages/playground/ssr-vue/src/router.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import {
- createMemoryHistory,
- createRouter as _createRouter,
- createWebHistory
-} from 'vue-router'
-
-// Auto generates routes from vue files under ./pages
-// https://vitejs.dev/guide/features.html#glob-import
-const pages = import.meta.glob('./pages/*.vue')
-
-const routes = Object.keys(pages).map((path) => {
- const name = path.match(/\.\/pages(.*)\.vue$/)[1].toLowerCase()
- return {
- path: name === '/home' ? '/' : name,
- component: pages[path] // () => import('./pages/*.vue')
- }
-})
-
-export function createRouter() {
- return _createRouter({
- // use appropriate history implementation for server/client
- // import.meta.env.SSR is injected by Vite.
- history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
- routes
- })
-}
diff --git a/packages/playground/ssr-vue/vite.config.js b/packages/playground/ssr-vue/vite.config.js
deleted file mode 100644
index 0adfa551b3b134..00000000000000
--- a/packages/playground/ssr-vue/vite.config.js
+++ /dev/null
@@ -1,49 +0,0 @@
-const vuePlugin = require('@vitejs/plugin-vue')
-const vueJsx = require('@vitejs/plugin-vue-jsx')
-const virtualFile = '@virtual-file'
-const virtualId = '\0' + virtualFile
-const nestedVirtualFile = '@nested-virtual-file'
-const nestedVirtualId = '\0' + nestedVirtualFile
-
-/**
- * @type {import('vite').UserConfig}
- */
-module.exports = {
- plugins: [
- vuePlugin(),
- vueJsx(),
- {
- name: 'virtual',
- resolveId(id) {
- if (id === '@foo') {
- return id
- }
- },
- load(id) {
- if (id === '@foo') {
- return `export default { msg: 'hi' }`
- }
- }
- },
- {
- name: 'virtual-module',
- resolveId(id) {
- if (id === virtualFile) {
- return virtualId
- } else if (id === nestedVirtualFile) {
- return nestedVirtualId
- }
- },
- load(id) {
- if (id === virtualId) {
- return `export { msg } from "@nested-virtual-file";`
- } else if (id === nestedVirtualId) {
- return `export const msg = "[success] from conventional virtual file"`
- }
- }
- }
- ],
- build: {
- minify: false
- }
-}
diff --git a/packages/playground/ssr-vue/vite.config.noexternal.js b/packages/playground/ssr-vue/vite.config.noexternal.js
deleted file mode 100644
index ac74bf1430e94e..00000000000000
--- a/packages/playground/ssr-vue/vite.config.noexternal.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const config = require('./vite.config.js')
-/**
- * @type {import('vite').UserConfig}
- */
-module.exports = Object.assign(config, {
- ssr: {
- noExternal: /./
- },
- resolve: {
- // necessary because vue.ssrUtils is only exported on cjs modules
- alias: [
- {
- find: '@vue/runtime-dom',
- replacement: '@vue/runtime-dom/dist/runtime-dom.cjs.js'
- },
- {
- find: '@vue/runtime-core',
- replacement: '@vue/runtime-core/dist/runtime-core.cjs.js'
- }
- ]
- }
-})
diff --git a/packages/playground/ssr-webworker/__tests__/serve.js b/packages/playground/ssr-webworker/__tests__/serve.js
deleted file mode 100644
index f4f207b85026c6..00000000000000
--- a/packages/playground/ssr-webworker/__tests__/serve.js
+++ /dev/null
@@ -1,48 +0,0 @@
-// @ts-check
-// this is automtically detected by scripts/jestPerTestSetup.ts and will replace
-// the default e2e test serve behavior
-
-const path = require('path')
-
-const port = (exports.port = 9528)
-
-/**
- * @param {string} root
- * @param {boolean} isProd
- */
-exports.serve = async function serve(root, isProd) {
- // we build first, regardless of whether it's prod/build mode
- // because Vite doesn't support the concept of a "webworker server"
- const { build } = require('vite')
-
- // worker build
- await build({
- root,
- logLevel: 'silent',
- build: {
- target: 'esnext',
- ssr: 'src/entry-worker.jsx',
- outDir: 'dist/worker'
- }
- })
-
- const { createServer } = require(path.resolve(root, 'worker.js'))
- const { app } = await createServer(root, isProd)
-
- return new Promise((resolve, reject) => {
- try {
- const server = app.listen(port, () => {
- resolve({
- // for test teardown
- async close() {
- await new Promise((resolve) => {
- server.close(resolve)
- })
- }
- })
- })
- } catch (e) {
- reject(e)
- }
- })
-}
diff --git a/packages/playground/ssr-webworker/__tests__/ssr-webworker.spec.ts b/packages/playground/ssr-webworker/__tests__/ssr-webworker.spec.ts
deleted file mode 100644
index 30d2bb93e495b1..00000000000000
--- a/packages/playground/ssr-webworker/__tests__/ssr-webworker.spec.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { port } from './serve'
-
-const url = `http://localhost:${port}`
-
-test('/', async () => {
- await page.goto(url + '/')
- expect(await page.textContent('h1')).toMatch('hello from webworker')
- expect(await page.textContent('.linked')).toMatch('dep from upper directory')
- expect(await page.textContent('.external')).toMatch('object')
-})
diff --git a/packages/playground/ssr-webworker/package.json b/packages/playground/ssr-webworker/package.json
deleted file mode 100644
index a7ebdf27ea22aa..00000000000000
--- a/packages/playground/ssr-webworker/package.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "name": "test-ssr-webworker",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "DEV=1 node worker",
- "build:worker": "vite build --ssr src/entry-worker.jsx --outDir dist/worker"
- },
- "dependencies": {
- "react": "^17.0.2"
- },
- "devDependencies": {
- "miniflare": "^1.4.1",
- "resolve-linked": "workspace:*"
- }
-}
diff --git a/packages/playground/ssr-webworker/src/entry-worker.jsx b/packages/playground/ssr-webworker/src/entry-worker.jsx
deleted file mode 100644
index c885657b18a6d3..00000000000000
--- a/packages/playground/ssr-webworker/src/entry-worker.jsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { msg as linkedMsg } from 'resolve-linked'
-import React from 'react'
-
-addEventListener('fetch', function (event) {
- return event.respondWith(
- new Response(
- `
- hello from webworker
- ${linkedMsg}
- ${typeof React}
- `,
- {
- headers: {
- 'content-type': 'text/html'
- }
- }
- )
- )
-})
diff --git a/packages/playground/ssr-webworker/vite.config.js b/packages/playground/ssr-webworker/vite.config.js
deleted file mode 100644
index 80cc1784cdc565..00000000000000
--- a/packages/playground/ssr-webworker/vite.config.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * @type {import('vite').UserConfig}
- */
-module.exports = {
- build: {
- minify: false
- },
- resolve: {
- dedupe: ['react']
- },
- ssr: {
- target: 'webworker',
- noExternal: true
- },
- plugins: [
- {
- config() {
- return {
- ssr: {
- noExternal: ['this-should-not-replace-the-boolean']
- }
- }
- }
- }
- ]
-}
diff --git a/packages/playground/ssr-webworker/worker.js b/packages/playground/ssr-webworker/worker.js
deleted file mode 100644
index 09725aaa9d71bb..00000000000000
--- a/packages/playground/ssr-webworker/worker.js
+++ /dev/null
@@ -1,26 +0,0 @@
-// @ts-check
-const path = require('path')
-const { Miniflare } = require('miniflare')
-
-const isDev = process.env.DEV
-
-async function createServer(root = process.cwd()) {
- const mf = new Miniflare({
- scriptPath: path.resolve(root, 'dist/worker/entry-worker.js')
- })
-
- const app = mf.createServer()
-
- return { app }
-}
-
-if (isDev) {
- createServer().then(({ app }) =>
- app.listen(3000, () => {
- console.log('http://localhost:3000')
- })
- )
-}
-
-// for test use
-exports.createServer = createServer
diff --git a/packages/playground/tailwind/__test__/tailwind.spec.ts b/packages/playground/tailwind/__test__/tailwind.spec.ts
deleted file mode 100644
index 47f6b7ccf49037..00000000000000
--- a/packages/playground/tailwind/__test__/tailwind.spec.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { isBuild, editFile, untilUpdated, getColor } from '../../testUtils'
-
-test('should render', async () => {
- expect(await page.textContent('#pagetitle')).toBe('|Page title|')
-})
-
-if (!isBuild) {
- test('regenerate CSS and HMR (glob pattern)', async () => {
- browserLogs.length = 0
- const el = await page.$('#pagetitle')
- const el2 = await page.$('#helloroot')
-
- expect(await getColor(el)).toBe('rgb(11, 22, 33)')
-
- editFile('src/views/Page.vue', (code) =>
- code.replace('|Page title|', '|Page title updated|')
- )
- await untilUpdated(() => el.textContent(), '|Page title updated|')
-
- expect(browserLogs).toMatchObject([
- '[vite] css hot updated: /index.css',
- '[vite] hot updated: /src/views/Page.vue'
- ])
-
- browserLogs.length = 0
-
- editFile('src/components/HelloWorld.vue', (code) =>
- code.replace('text-gray-800', 'text-[rgb(10,20,30)]')
- )
-
- await untilUpdated(() => getColor(el2), 'rgb(10, 20, 30)')
-
- expect(browserLogs).toMatchObject([
- '[vite] css hot updated: /index.css',
- '[vite] hot updated: /src/components/HelloWorld.vue'
- ])
-
- browserLogs.length = 0
- })
-
- test('regenerate CSS and HMR (relative path)', async () => {
- browserLogs.length = 0
- const el = await page.$('h1')
-
- expect(await getColor(el)).toBe('black')
-
- editFile('src/App.vue', (code) =>
- code.replace('text-black', 'text-[rgb(11,22,33)]')
- )
-
- await untilUpdated(() => getColor(el), 'rgb(11, 22, 33)')
-
- expect(browserLogs).toMatchObject([
- '[vite] css hot updated: /index.css',
- '[vite] hot updated: /src/App.vue'
- ])
-
- browserLogs.length = 0
- })
-}
diff --git a/packages/playground/tailwind/index.html b/packages/playground/tailwind/index.html
deleted file mode 100644
index 5165c9f6325cde..00000000000000
--- a/packages/playground/tailwind/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
- Vite App
-
-
-
-
-
-
-
diff --git a/packages/playground/tailwind/package.json b/packages/playground/tailwind/package.json
deleted file mode 100644
index ff79908d386e96..00000000000000
--- a/packages/playground/tailwind/package.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "test-tailwind",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- },
- "dependencies": {
- "autoprefixer": "^10.4.0",
- "tailwindcss": "^2.2.19",
- "vue": "^3.2.25",
- "vue-router": "^4.0.0"
- },
- "devDependencies": {
- "@vitejs/plugin-vue": "workspace:*"
- }
-}
diff --git a/packages/playground/tailwind/postcss.config.js b/packages/playground/tailwind/postcss.config.js
deleted file mode 100644
index b73493f7f96fae..00000000000000
--- a/packages/playground/tailwind/postcss.config.js
+++ /dev/null
@@ -1,7 +0,0 @@
-// postcss.config.js
-module.exports = {
- plugins: {
- tailwindcss: { config: __dirname + '/tailwind.config.js' },
- autoprefixer: {}
- }
-}
diff --git a/packages/playground/tailwind/public/favicon.ico b/packages/playground/tailwind/public/favicon.ico
deleted file mode 100644
index df36fcfb72584e..00000000000000
Binary files a/packages/playground/tailwind/public/favicon.ico and /dev/null differ
diff --git a/packages/playground/tailwind/src/App.vue b/packages/playground/tailwind/src/App.vue
deleted file mode 100644
index 25835fc414a06f..00000000000000
--- a/packages/playground/tailwind/src/App.vue
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
Tailwind app
- {{ foo }}
-
-
-
-
-
diff --git a/packages/playground/tailwind/src/assets/logo.png b/packages/playground/tailwind/src/assets/logo.png
deleted file mode 100644
index f3d2503fc2a44b..00000000000000
Binary files a/packages/playground/tailwind/src/assets/logo.png and /dev/null differ
diff --git a/packages/playground/tailwind/src/components/HelloWorld.vue b/packages/playground/tailwind/src/components/HelloWorld.vue
deleted file mode 100644
index 1ea9fdf2573697..00000000000000
--- a/packages/playground/tailwind/src/components/HelloWorld.vue
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
- HelloWorld - {{ count }}
-
-
-
-
diff --git a/packages/playground/tailwind/src/main.js b/packages/playground/tailwind/src/main.js
deleted file mode 100644
index 78494e75b4741d..00000000000000
--- a/packages/playground/tailwind/src/main.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createApp } from 'vue'
-import App from './App.vue'
-import router from './router'
-// import '../index.css';
-
-createApp(App).use(router).mount('#app')
diff --git a/packages/playground/tailwind/src/router.ts b/packages/playground/tailwind/src/router.ts
deleted file mode 100644
index 32f1a47b40540d..00000000000000
--- a/packages/playground/tailwind/src/router.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { createWebHistory, createRouter } from 'vue-router'
-import Page from './views/Page.vue'
-
-const history = createWebHistory()
-
-const routeur = createRouter({
- history: history,
- routes: [
- {
- path: '/',
- component: Page
- }
- ]
-})
-
-export default routeur
diff --git a/packages/playground/tailwind/src/views/Page.vue b/packages/playground/tailwind/src/views/Page.vue
deleted file mode 100644
index 764a2a18e54fdb..00000000000000
--- a/packages/playground/tailwind/src/views/Page.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
|Page title|
-
{{ val }}
-
- Tailwind style
-
-
-
-
-
-
diff --git a/packages/playground/tailwind/tailwind.config.js b/packages/playground/tailwind/tailwind.config.js
deleted file mode 100644
index 64a2b1a2499bb4..00000000000000
--- a/packages/playground/tailwind/tailwind.config.js
+++ /dev/null
@@ -1,17 +0,0 @@
-module.exports = {
- mode: 'jit',
- purge: [
- // Before editing this section, make sure no paths are matching with `/src/App.vue`
- // Look https://github.com/vitejs/vite/pull/6959 for more details
- __dirname + '/src/{components,views}/**/*.vue',
- __dirname + '/src/App.vue'
- ],
- darkMode: false, // or 'media' or 'class'
- theme: {
- extend: {}
- },
- variants: {
- extend: {}
- },
- plugins: []
-}
diff --git a/packages/playground/tailwind/vite.config.ts b/packages/playground/tailwind/vite.config.ts
deleted file mode 100644
index e7c50bf3c3ae78..00000000000000
--- a/packages/playground/tailwind/vite.config.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { defineConfig } from 'vite'
-import vue from '@vitejs/plugin-vue'
-
-export default defineConfig({
- resolve: {
- alias: {
- '/@': __dirname
- }
- },
- plugins: [vue()],
- build: {
- // to make tests faster
- minify: false
- },
- server: {
- // This option caused issues with HMR,
- // although it should not affect the build
- origin: 'http://localhost:8080/'
- }
-})
diff --git a/packages/playground/testUtils.ts b/packages/playground/testUtils.ts
deleted file mode 100644
index 0c8186d4ed121d..00000000000000
--- a/packages/playground/testUtils.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-// test utils used in e2e tests for playgrounds.
-// this can be directly imported in any playground tests as 'testUtils', e.g.
-// `import { getColor } from 'testUtils'`
-
-import fs from 'fs'
-import path from 'path'
-import colors from 'css-color-names'
-import type { ElementHandle } from 'playwright-chromium'
-import type { Manifest } from 'vite'
-
-export function slash(p: string): string {
- return p.replace(/\\/g, '/')
-}
-
-export const isBuild = !!process.env.VITE_TEST_BUILD
-
-const testPath = expect.getState().testPath
-const testName = slash(testPath).match(/playground\/([\w-]+)\//)?.[1]
-export const testDir = path.resolve(__dirname, '../../packages/temp', testName)
-export const workspaceRoot = path.resolve(__dirname, '../../')
-
-const hexToNameMap: Record = {}
-Object.keys(colors).forEach((color) => {
- hexToNameMap[colors[color]] = color
-})
-
-function componentToHex(c: number): string {
- const hex = c.toString(16)
- return hex.length === 1 ? '0' + hex : hex
-}
-
-function rgbToHex(rgb: string): string {
- const match = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/)
- if (match) {
- const [_, rs, gs, bs] = match
- return (
- '#' +
- componentToHex(parseInt(rs, 10)) +
- componentToHex(parseInt(gs, 10)) +
- componentToHex(parseInt(bs, 10))
- )
- } else {
- return '#000000'
- }
-}
-
-const timeout = (n: number) => new Promise((r) => setTimeout(r, n))
-
-async function toEl(el: string | ElementHandle): Promise {
- if (typeof el === 'string') {
- return await page.$(el)
- }
- return el
-}
-
-export async function getColor(el: string | ElementHandle): Promise {
- el = await toEl(el)
- const rgb = await el.evaluate((el) => getComputedStyle(el as Element).color)
- return hexToNameMap[rgbToHex(rgb)] ?? rgb
-}
-
-export async function getBg(el: string | ElementHandle): Promise {
- el = await toEl(el)
- return el.evaluate((el) => getComputedStyle(el as Element).backgroundImage)
-}
-
-export async function getBgColor(el: string | ElementHandle): Promise {
- el = await toEl(el)
- return el.evaluate((el) => getComputedStyle(el as Element).backgroundColor)
-}
-
-export function readFile(filename: string): string {
- return fs.readFileSync(path.resolve(testDir, filename), 'utf-8')
-}
-
-export function editFile(
- filename: string,
- replacer: (str: string) => string,
- runInBuild: boolean = false
-): void {
- if (isBuild && !runInBuild) return
- filename = path.resolve(testDir, filename)
- const content = fs.readFileSync(filename, 'utf-8')
- const modified = replacer(content)
- fs.writeFileSync(filename, modified)
-}
-
-export function addFile(filename: string, content: string): void {
- fs.writeFileSync(path.resolve(testDir, filename), content)
-}
-
-export function removeFile(filename: string): void {
- fs.unlinkSync(path.resolve(testDir, filename))
-}
-
-export function listAssets(base = ''): string[] {
- const assetsDir = path.join(testDir, 'dist', base, 'assets')
- return fs.readdirSync(assetsDir)
-}
-
-export function findAssetFile(match: string | RegExp, base = ''): string {
- const assetsDir = path.join(testDir, 'dist', base, 'assets')
- const files = fs.readdirSync(assetsDir)
- const file = files.find((file) => {
- return file.match(match)
- })
- return file ? fs.readFileSync(path.resolve(assetsDir, file), 'utf-8') : ''
-}
-
-export function readManifest(base = ''): Manifest {
- return JSON.parse(
- fs.readFileSync(path.join(testDir, 'dist', base, 'manifest.json'), 'utf-8')
- )
-}
-
-/**
- * Poll a getter until the value it returns includes the expected value.
- */
-export async function untilUpdated(
- poll: () => string | Promise,
- expected: string,
- runInBuild = false
-): Promise {
- if (isBuild && !runInBuild) return
- const maxTries = process.env.CI ? 100 : 50
- for (let tries = 0; tries < maxTries; tries++) {
- const actual = (await poll()) ?? ''
- if (actual.indexOf(expected) > -1 || tries === maxTries - 1) {
- expect(actual).toMatch(expected)
- break
- } else {
- await timeout(50)
- }
- }
-}
-
-/**
- * Send the rebuild complete message in build watch
- */
-export { notifyRebuildComplete } from '../../scripts/jestPerTestSetup'
diff --git a/packages/playground/tsconfig-json-load-error/__tests__/tsconfig-json-load-error.spec.ts b/packages/playground/tsconfig-json-load-error/__tests__/tsconfig-json-load-error.spec.ts
deleted file mode 100644
index 699f658da6a255..00000000000000
--- a/packages/playground/tsconfig-json-load-error/__tests__/tsconfig-json-load-error.spec.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { editFile, isBuild, readFile, untilUpdated } from '../../testUtils'
-
-if (isBuild) {
- test('should throw an error on build', () => {
- const buildError = beforeAllError
- expect(buildError).toBeTruthy()
- expect(buildError.message).toMatch(
- /^parsing .* failed: SyntaxError: Unexpected token } in JSON at position \d+$/
- )
- beforeAllError = null // got expected error, null it here so testsuite does not fail from rethrow in afterAll
- })
-
- test('should not output files to dist', () => {
- let err
- try {
- readFile('dist/index.html')
- } catch (e) {
- err = e
- }
- expect(err).toBeTruthy()
- expect(err.code).toBe('ENOENT')
- })
-} else {
- test('should log 500 error in browser for malformed tsconfig', () => {
- // don't test for actual complete message as this might be locale dependant. chrome does log 500 consistently though
- expect(browserLogs.find((x) => x.includes('500'))).toBeTruthy()
- expect(browserLogs).not.toContain('tsconfig error fixed, file loaded')
- })
-
- test('should show error overlay for tsconfig error', async () => {
- const errorOverlay = await page.waitForSelector('vite-error-overlay')
- expect(errorOverlay).toBeTruthy()
- const message = await errorOverlay.$$eval('.message-body', (m) => {
- return m[0].innerHTML
- })
- // use regex with variable filename and position values because they are different on win
- expect(message).toMatch(
- /^parsing .* failed: SyntaxError: Unexpected token } in JSON at position \d+$/
- )
- })
-
- test('should reload when tsconfig is changed', async () => {
- await editFile('has-error/tsconfig.json', (content) => {
- return content.replace('"compilerOptions":', '"compilerOptions":{}')
- })
- await untilUpdated(() => {
- return browserLogs.find((x) => x === 'tsconfig error fixed, file loaded')
- }, 'tsconfig error fixed, file loaded')
- })
-}
diff --git a/packages/playground/tsconfig-json-load-error/index.html b/packages/playground/tsconfig-json-load-error/index.html
deleted file mode 100644
index a59cd7cce619a7..00000000000000
--- a/packages/playground/tsconfig-json-load-error/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
- Vite App
-
-
-
-
-
-
diff --git a/packages/playground/tsconfig-json-load-error/package.json b/packages/playground/tsconfig-json-load-error/package.json
deleted file mode 100644
index b02c6e5ee5ab53..00000000000000
--- a/packages/playground/tsconfig-json-load-error/package.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "tsconfig-json-load-error",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- }
-}
diff --git a/packages/playground/tsconfig-json-load-error/tsconfig.json b/packages/playground/tsconfig-json-load-error/tsconfig.json
deleted file mode 100644
index e91cdec493e28f..00000000000000
--- a/packages/playground/tsconfig-json-load-error/tsconfig.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "compilerOptions": {
- "target": "ESNext",
- "module": "ESNext",
- "lib": ["ESNext", "DOM"],
- "moduleResolution": "Node",
- "strict": true,
- "sourceMap": true,
- "resolveJsonModule": true,
- "esModuleInterop": true,
- "noEmit": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noImplicitReturns": true,
-
- "useDefineForClassFields": true,
- "importsNotUsedAsValues": "preserve"
- },
- "include": ["./src"]
-}
diff --git a/packages/playground/tsconfig-json/__tests__/tsconfig-json.spec.ts b/packages/playground/tsconfig-json/__tests__/tsconfig-json.spec.ts
deleted file mode 100644
index 0cd6af909f045b..00000000000000
--- a/packages/playground/tsconfig-json/__tests__/tsconfig-json.spec.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import path from 'path'
-import fs from 'fs'
-import { transformWithEsbuild } from 'vite'
-
-test('should respected each `tsconfig.json`s compilerOptions', () => {
- // main side effect should be called (because of `"importsNotUsedAsValues": "preserve"`)
- expect(browserLogs).toContain('main side effect')
- // main base setter should not be called (because of `"useDefineForClassFields": true"`)
- expect(browserLogs).not.toContain('data setter in MainBase')
-
- // nested side effect should not be called (because "importsNotUsedAsValues" is not set, defaults to "remove")
- expect(browserLogs).not.toContain('nested side effect')
- // nested base setter should be called (because of `"useDefineForClassFields": false"`)
- expect(browserLogs).toContain('data setter in NestedBase')
-
- // nested-with-extends side effect should be called (because "importsNotUsedAsValues" is extended from the main tsconfig.json, which is "preserve")
- expect(browserLogs).toContain('nested-with-extends side effect')
- // nested-with-extends base setter should be called (because of `"useDefineForClassFields": false"`)
- expect(browserLogs).toContain('data setter in NestedWithExtendsBase')
-})
-
-describe('transformWithEsbuild', () => {
- test('merge tsconfigRaw object', async () => {
- const main = path.resolve(__dirname, '../src/main.ts')
- const mainContent = fs.readFileSync(main, 'utf-8')
- const result = await transformWithEsbuild(mainContent, main, {
- tsconfigRaw: {
- compilerOptions: {
- useDefineForClassFields: false
- }
- }
- })
- // "importsNotUsedAsValues": "preserve" from tsconfig.json should still work
- expect(result.code).toContain('import "./not-used-type";')
- })
-
- test('overwrite tsconfigRaw string', async () => {
- const main = path.resolve(__dirname, '../src/main.ts')
- const mainContent = fs.readFileSync(main, 'utf-8')
- const result = await transformWithEsbuild(mainContent, main, {
- tsconfigRaw: `{
- "compilerOptions": {
- "useDefineForClassFields": false
- }
- }`
- })
- // "importsNotUsedAsValues": "preserve" from tsconfig.json should not be read
- // and defaults to "remove"
- expect(result.code).not.toContain('import "./not-used-type";')
- })
-
- test('preserveValueImports', async () => {
- const main = path.resolve(__dirname, '../src/main.ts')
- const mainContent = fs.readFileSync(main, 'utf-8')
- const result = await transformWithEsbuild(mainContent, main, {
- tsconfigRaw: {
- compilerOptions: {
- useDefineForClassFields: false,
- preserveValueImports: true
- }
- }
- })
- // "importsNotUsedAsValues": "preserve" from tsconfig.json should still work
- expect(result.code).toContain(
- 'import { MainTypeOnlyClass } from "./not-used-type";'
- )
- })
-})
diff --git a/packages/playground/tsconfig-json/index.html b/packages/playground/tsconfig-json/index.html
deleted file mode 100644
index a59cd7cce619a7..00000000000000
--- a/packages/playground/tsconfig-json/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
- Vite App
-
-
-
-
-
-
diff --git a/packages/playground/tsconfig-json/nested/tsconfig.json b/packages/playground/tsconfig-json/nested/tsconfig.json
deleted file mode 100644
index 23b6b8779f649a..00000000000000
--- a/packages/playground/tsconfig-json/nested/tsconfig.json
+++ /dev/null
@@ -1,21 +0,0 @@
-// prettier-ignore
-{
- "include": ["./"],
- "compilerOptions": {
- "target": "ESNext",
- "module": "ESNext",
- "lib": ["ESNext", "DOM"],
- "moduleResolution": "Node",
- "strict": true,
- "sourceMap": true,
- "resolveJsonModule": true,
- "esModuleInterop": true,
- "noEmit": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noImplicitReturns": true,
-
- /* tsconfig.json should support comments and trailing comma */
- "useDefineForClassFields": false,
- }
-}
diff --git a/packages/playground/tsconfig-json/package.json b/packages/playground/tsconfig-json/package.json
deleted file mode 100644
index c4248463facdb9..00000000000000
--- a/packages/playground/tsconfig-json/package.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "tsconfig-json",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- }
-}
diff --git a/packages/playground/tsconfig-json/src/main.ts b/packages/playground/tsconfig-json/src/main.ts
deleted file mode 100644
index 6ae1fe03b7d023..00000000000000
--- a/packages/playground/tsconfig-json/src/main.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-// @ts-nocheck
-import '../nested/main'
-import '../nested-with-extends/main'
-
-// eslint-disable-next-line @typescript-eslint/consistent-type-imports
-import { MainTypeOnlyClass } from './not-used-type'
-
-class MainBase {
- set data(value: string) {
- console.log('data setter in MainBase')
- }
-}
-class MainDerived extends MainBase {
- // No longer triggers a 'console.log'
- // when using 'useDefineForClassFields'.
- data = 10
-
- foo?: MainTypeOnlyClass
-}
-
-const d = new MainDerived()
diff --git a/packages/playground/tsconfig-json/tsconfig.json b/packages/playground/tsconfig-json/tsconfig.json
deleted file mode 100644
index e91cdec493e28f..00000000000000
--- a/packages/playground/tsconfig-json/tsconfig.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "compilerOptions": {
- "target": "ESNext",
- "module": "ESNext",
- "lib": ["ESNext", "DOM"],
- "moduleResolution": "Node",
- "strict": true,
- "sourceMap": true,
- "resolveJsonModule": true,
- "esModuleInterop": true,
- "noEmit": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noImplicitReturns": true,
-
- "useDefineForClassFields": true,
- "importsNotUsedAsValues": "preserve"
- },
- "include": ["./src"]
-}
diff --git a/packages/playground/tsconfig.json b/packages/playground/tsconfig.json
deleted file mode 100644
index d60edb9f78c801..00000000000000
--- a/packages/playground/tsconfig.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "include": ["."],
- "exclude": ["**/dist/**"],
- "compilerOptions": {
- "target": "es2019",
- "outDir": "dist",
- "allowJs": true,
- "esModuleInterop": true,
- "moduleResolution": "node",
- "baseUrl": ".",
- "jsx": "preserve",
- "types": ["vite/client", "jest", "node"]
- }
-}
diff --git a/packages/playground/vue-jsx/Comp.tsx b/packages/playground/vue-jsx/Comp.tsx
deleted file mode 100644
index fe8add4d428a2c..00000000000000
--- a/packages/playground/vue-jsx/Comp.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { defineComponent, ref } from 'vue'
-
-const Default = defineComponent(() => {
- const count = ref(3)
- const inc = () => count.value++
-
- return () => (
-
- default tsx {count.value}
-
- )
-})
-
-export default Default
diff --git a/packages/playground/vue-jsx/Comps.jsx b/packages/playground/vue-jsx/Comps.jsx
deleted file mode 100644
index e5cc405a77581b..00000000000000
--- a/packages/playground/vue-jsx/Comps.jsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { defineComponent, ref } from 'vue'
-
-export const Named = defineComponent(() => {
- const count = ref(0)
- const inc = () => count.value++
-
- return () => (
-
- named {count.value}
-
- )
-})
-
-const NamedSpec = defineComponent(() => {
- const count = ref(1)
- const inc = () => count.value++
-
- return () => (
-
- named specifier {count.value}
-
- )
-})
-export { NamedSpec }
-
-export default defineComponent(() => {
- const count = ref(2)
- const inc = () => count.value++
-
- return () => (
-
- default {count.value}
-
- )
-})
diff --git a/packages/playground/vue-jsx/OtherExt.tesx b/packages/playground/vue-jsx/OtherExt.tesx
deleted file mode 100644
index 7ae585a014c566..00000000000000
--- a/packages/playground/vue-jsx/OtherExt.tesx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { defineComponent } from 'vue'
-
-const Default = defineComponent(() => {
- return () => (
- Other Ext
- )
-})
-
-export default Default
diff --git a/packages/playground/vue-jsx/Query.jsx b/packages/playground/vue-jsx/Query.jsx
deleted file mode 100644
index 60de93eafb7b1c..00000000000000
--- a/packages/playground/vue-jsx/Query.jsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineComponent, ref } from 'vue'
-
-export default defineComponent(() => {
- const count = ref(6)
- const inc = () => count.value++
-
- return () => (
-
- import with query transform fail
-
- )
-})
diff --git a/packages/playground/vue-jsx/Script.vue b/packages/playground/vue-jsx/Script.vue
deleted file mode 100644
index 2689ed2dfe6ffb..00000000000000
--- a/packages/playground/vue-jsx/Script.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-
diff --git a/packages/playground/vue-jsx/SrcImport.jsx b/packages/playground/vue-jsx/SrcImport.jsx
deleted file mode 100644
index dc775be205af73..00000000000000
--- a/packages/playground/vue-jsx/SrcImport.jsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { defineComponent, ref } from 'vue'
-
-export default defineComponent(() => {
- const count = ref(5)
- const inc = () => count.value++
-
- return () => (
-
- src import {count.value}
-
- )
-})
diff --git a/packages/playground/vue-jsx/SrcImport.vue b/packages/playground/vue-jsx/SrcImport.vue
deleted file mode 100644
index 89f6fb3eb77e2b..00000000000000
--- a/packages/playground/vue-jsx/SrcImport.vue
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/packages/playground/vue-jsx/__tests__/vue-jsx.spec.ts b/packages/playground/vue-jsx/__tests__/vue-jsx.spec.ts
deleted file mode 100644
index 999fdc19af51ec..00000000000000
--- a/packages/playground/vue-jsx/__tests__/vue-jsx.spec.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import { editFile, isBuild, untilUpdated } from 'testUtils'
-
-test('should render', async () => {
- expect(await page.textContent('.named')).toMatch('0')
- expect(await page.textContent('.named-specifier')).toMatch('1')
- expect(await page.textContent('.default')).toMatch('2')
- expect(await page.textContent('.default-tsx')).toMatch('3')
- expect(await page.textContent('.script')).toMatch('4')
- expect(await page.textContent('.src-import')).toMatch('5')
- expect(await page.textContent('.jsx-with-query')).toMatch('6')
- expect(await page.textContent('.other-ext')).toMatch('Other Ext')
-})
-
-test('should update', async () => {
- await page.click('.named')
- expect(await page.textContent('.named')).toMatch('1')
- await page.click('.named-specifier')
- expect(await page.textContent('.named-specifier')).toMatch('2')
- await page.click('.default')
- expect(await page.textContent('.default')).toMatch('3')
- await page.click('.default-tsx')
- expect(await page.textContent('.default-tsx')).toMatch('4')
- await page.click('.script')
- expect(await page.textContent('.script')).toMatch('5')
- await page.click('.src-import')
- expect(await page.textContent('.src-import')).toMatch('6')
- await page.click('.jsx-with-query')
- expect(await page.textContent('.jsx-with-query')).toMatch('7')
-})
-
-if (!isBuild) {
- test('hmr: named export', async () => {
- editFile('Comps.jsx', (code) =>
- code.replace('named {count', 'named updated {count')
- )
- await untilUpdated(() => page.textContent('.named'), 'named updated 0')
-
- // affect all components in same file
- expect(await page.textContent('.named-specifier')).toMatch('1')
- expect(await page.textContent('.default')).toMatch('2')
- // should not affect other components from different file
- expect(await page.textContent('.default-tsx')).toMatch('4')
- })
-
- test('hmr: named export via specifier', async () => {
- editFile('Comps.jsx', (code) =>
- code.replace('named specifier {count', 'named specifier updated {count')
- )
- await untilUpdated(
- () => page.textContent('.named-specifier'),
- 'named specifier updated 1'
- )
-
- // affect all components in same file
- expect(await page.textContent('.default')).toMatch('2')
- // should not affect other components on the page
- expect(await page.textContent('.default-tsx')).toMatch('4')
- })
-
- test('hmr: default export', async () => {
- editFile('Comps.jsx', (code) =>
- code.replace('default {count', 'default updated {count')
- )
- await untilUpdated(() => page.textContent('.default'), 'default updated 2')
-
- // should not affect other components on the page
- expect(await page.textContent('.default-tsx')).toMatch('4')
- })
-
- test('hmr: named export via specifier', async () => {
- // update another component
- await page.click('.named')
- expect(await page.textContent('.named')).toMatch('1')
-
- editFile('Comp.tsx', (code) =>
- code.replace('default tsx {count', 'default tsx updated {count')
- )
- await untilUpdated(
- () => page.textContent('.default-tsx'),
- 'default tsx updated 3'
- )
-
- // should not affect other components on the page
- expect(await page.textContent('.named')).toMatch('1')
- })
-
- test('hmr: script in .vue', async () => {
- editFile('Script.vue', (code) =>
- code.replace('script {count', 'script updated {count')
- )
- await untilUpdated(() => page.textContent('.script'), 'script updated 4')
-
- expect(await page.textContent('.src-import')).toMatch('6')
- })
-
- test('hmr: src import in .vue', async () => {
- await page.click('.script')
- editFile('SrcImport.jsx', (code) =>
- code.replace('src import {count', 'src import updated {count')
- )
- await untilUpdated(
- () => page.textContent('.src-import'),
- 'src import updated 5'
- )
-
- expect(await page.textContent('.script')).toMatch('5')
- })
-
- test('hmr: setup jsx in .vue', async () => {
- editFile('setup-syntax-jsx.vue', (code) =>
- code.replace('let count = ref(100)', 'let count = ref(1000)')
- )
- await untilUpdated(() => page.textContent('.setup-jsx'), '1000')
- })
-}
diff --git a/packages/playground/vue-jsx/index.html b/packages/playground/vue-jsx/index.html
deleted file mode 100644
index a285a008c13a9e..00000000000000
--- a/packages/playground/vue-jsx/index.html
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/packages/playground/vue-jsx/main.jsx b/packages/playground/vue-jsx/main.jsx
deleted file mode 100644
index e304e7788e49e7..00000000000000
--- a/packages/playground/vue-jsx/main.jsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { createApp } from 'vue'
-import { Named, NamedSpec, default as Default } from './Comps'
-import { default as TsxDefault } from './Comp'
-import OtherExt from './OtherExt.tesx'
-import JsxScript from './Script.vue'
-import JsxSrcImport from './SrcImport.vue'
-import JsxSetupSyntax from './setup-syntax-jsx.vue'
-// eslint-disable-next-line
-import JsxWithQuery from './Query.jsx?query=true'
-
-function App() {
- return (
- <>
-
-
-
-
-
-
-
-
-
- >
- )
-}
-
-createApp(App).mount('#app')
diff --git a/packages/playground/vue-jsx/package.json b/packages/playground/vue-jsx/package.json
deleted file mode 100644
index 4b2135906b2833..00000000000000
--- a/packages/playground/vue-jsx/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "test-vue-jsx",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- },
- "dependencies": {
- "vue": "^3.2.25"
- },
- "devDependencies": {
- "@vitejs/plugin-vue": "workspace:*",
- "@vitejs/plugin-vue-jsx": "workspace:*"
- }
-}
diff --git a/packages/playground/vue-jsx/setup-syntax-jsx.vue b/packages/playground/vue-jsx/setup-syntax-jsx.vue
deleted file mode 100644
index 0b16be7e773280..00000000000000
--- a/packages/playground/vue-jsx/setup-syntax-jsx.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
- {{ count }}
-
-
diff --git a/packages/playground/vue-jsx/vite.config.js b/packages/playground/vue-jsx/vite.config.js
deleted file mode 100644
index d6eb84e05f4e4a..00000000000000
--- a/packages/playground/vue-jsx/vite.config.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const vueJsxPlugin = require('@vitejs/plugin-vue-jsx')
-const vuePlugin = require('@vitejs/plugin-vue')
-
-/**
- * @type {import('vite').UserConfig}
- */
-module.exports = {
- plugins: [
- vueJsxPlugin({
- include: [/\.tesx$/, /\.[jt]sx$/]
- }),
- vuePlugin(),
- {
- name: 'jsx-query-plugin',
- transform(code, id) {
- if (id.includes('?query=true')) {
- return `
-import { createVNode as _createVNode } from "vue";
-import { defineComponent, ref } from 'vue';
-export default defineComponent(() => {
- const count = ref(6);
-
- const inc = () => count.value++;
-
- return () => _createVNode("button", {
- "class": "jsx-with-query",
- "onClick": inc
- }, [count.value]);
-});
-`
- }
- }
- }
- ],
- build: {
- // to make tests faster
- minify: false
- }
-}
diff --git a/packages/playground/vue-lib/__tests__/serve.js b/packages/playground/vue-lib/__tests__/serve.js
deleted file mode 100644
index 73f89eee44ea3e..00000000000000
--- a/packages/playground/vue-lib/__tests__/serve.js
+++ /dev/null
@@ -1,7 +0,0 @@
-// @ts-check
-// this is automtically detected by scripts/jestPerTestSetup.ts and will replace
-// the default e2e test serve behavior
-
-exports.serve = async function serve() {
- // do nothing, skip default behavior
-}
diff --git a/packages/playground/vue-lib/__tests__/vue-lib.spec.ts b/packages/playground/vue-lib/__tests__/vue-lib.spec.ts
deleted file mode 100644
index 0504160f17d2f0..00000000000000
--- a/packages/playground/vue-lib/__tests__/vue-lib.spec.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { build } from 'vite'
-import path from 'path'
-import type { OutputChunk, RollupOutput } from 'rollup'
-
-describe('vue component library', () => {
- test('should output tree shakeable css module code', async () => {
- // Build lib
- await build({
- logLevel: 'silent',
- configFile: path.resolve(__dirname, '../vite.config.lib.ts')
- })
- // Build app
- const { output } = (await build({
- logLevel: 'silent',
- configFile: path.resolve(__dirname, '../vite.config.consumer.ts')
- })) as RollupOutput
- const { code } = output.find(
- (e) => e.type === 'chunk' && e.isEntry
- ) as OutputChunk
- // Unused css module should be treeshaked
- expect(code).toContain('styleA') // styleA is used by CompA
- expect(code).not.toContain('styleB') // styleB is not used
- })
-})
diff --git a/packages/playground/vue-lib/index.html b/packages/playground/vue-lib/index.html
deleted file mode 100644
index e016cf7d760797..00000000000000
--- a/packages/playground/vue-lib/index.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/packages/playground/vue-lib/package.json b/packages/playground/vue-lib/package.json
deleted file mode 100644
index df82cf48b62a9c..00000000000000
--- a/packages/playground/vue-lib/package.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "name": "test-vue-lib",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev-consumer": "vite --config ./vite.config.consumer.ts",
- "build-lib": "vite build --config ./vite.config.lib.ts",
- "build-consumer": "vite build --config ./vite.config.consumer.ts"
- },
- "dependencies": {
- "vue": "^3.2.25"
- },
- "devDependencies": {
- "@vitejs/plugin-vue": "workspace:*"
- }
-}
diff --git a/packages/playground/vue-lib/src-consumer/index.ts b/packages/playground/vue-lib/src-consumer/index.ts
deleted file mode 100644
index ac0f65e2a3ed9d..00000000000000
--- a/packages/playground/vue-lib/src-consumer/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-// @ts-ignore
-/* eslint-disable node/no-missing-import */
-import { CompA } from '../dist/lib/my-vue-lib.es'
-import '../dist/lib/style.css'
-import { createApp } from 'vue'
-
-const app = createApp(CompA)
-app.mount('#app')
diff --git a/packages/playground/vue-lib/src-lib/CompA.vue b/packages/playground/vue-lib/src-lib/CompA.vue
deleted file mode 100644
index dac9298b3bedf4..00000000000000
--- a/packages/playground/vue-lib/src-lib/CompA.vue
+++ /dev/null
@@ -1,8 +0,0 @@
-
- CompA
-
-
diff --git a/packages/playground/vue-lib/src-lib/CompB.vue b/packages/playground/vue-lib/src-lib/CompB.vue
deleted file mode 100644
index cca30168fb6753..00000000000000
--- a/packages/playground/vue-lib/src-lib/CompB.vue
+++ /dev/null
@@ -1,8 +0,0 @@
-
- CompB
-
-
diff --git a/packages/playground/vue-lib/src-lib/index.ts b/packages/playground/vue-lib/src-lib/index.ts
deleted file mode 100644
index f83abd4ec72118..00000000000000
--- a/packages/playground/vue-lib/src-lib/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as CompA } from './CompA.vue'
-export { default as CompB } from './CompB.vue'
diff --git a/packages/playground/vue-lib/vite.config.consumer.ts b/packages/playground/vue-lib/vite.config.consumer.ts
deleted file mode 100644
index 9e75b5cfbeabcb..00000000000000
--- a/packages/playground/vue-lib/vite.config.consumer.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { defineConfig } from 'vite'
-import vue from '@vitejs/plugin-vue'
-
-export default defineConfig({
- root: __dirname,
- build: {
- outDir: 'dist/consumer'
- },
- plugins: [vue()]
-})
diff --git a/packages/playground/vue-lib/vite.config.lib.ts b/packages/playground/vue-lib/vite.config.lib.ts
deleted file mode 100644
index a888382d008a8c..00000000000000
--- a/packages/playground/vue-lib/vite.config.lib.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import path from 'path'
-import { defineConfig } from 'vite'
-import vue from '@vitejs/plugin-vue'
-
-export default defineConfig({
- root: __dirname,
- build: {
- outDir: 'dist/lib',
- lib: {
- entry: path.resolve(__dirname, 'src-lib/index.ts'),
- name: 'MyVueLib',
- formats: ['es'],
- fileName: (format) => `my-vue-lib.${format}.js`
- },
- rollupOptions: {
- external: ['vue'],
- output: {
- globals: { vue: 'Vue' }
- }
- }
- },
- plugins: [vue()]
-})
diff --git a/packages/playground/vue/Assets.vue b/packages/playground/vue/Assets.vue
deleted file mode 100644
index 875ac1b243b393..00000000000000
--- a/packages/playground/vue/Assets.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-
- Template Static Asset Reference
-
- Relative
-
-
-
- Absolute
-
-
-
- Absolute import from public dir
-
-
-
- Relative URL in style
-
-
-
- SVG Fragment reference
-
-
-
-
-
diff --git a/packages/playground/vue/AsyncComponent.vue b/packages/playground/vue/AsyncComponent.vue
deleted file mode 100644
index 4e66630c4d2edd..00000000000000
--- a/packages/playground/vue/AsyncComponent.vue
+++ /dev/null
@@ -1,15 +0,0 @@
-
- Async Component
- Testing TLA and for await compatibility with esbuild
- ab == {{ test }}
-
-
-
diff --git a/packages/playground/vue/CssModules.vue b/packages/playground/vue/CssModules.vue
deleted file mode 100644
index f7897e2e57f652..00000000000000
--- a/packages/playground/vue/CssModules.vue
+++ /dev/null
@@ -1,23 +0,0 @@
-
- CSS Modules
-
- <style module> - this should be blue
-
{{ $style }}
-
-
- CSS - this should be orange
-
{{ mod }}
-
-
-
-
-
-
diff --git a/packages/playground/vue/CustomBlock.vue b/packages/playground/vue/CustomBlock.vue
deleted file mode 100644
index 0a7b3901693154..00000000000000
--- a/packages/playground/vue/CustomBlock.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-
- Custom Blocks
- {{ t('hello') }}
-
-
-
-
-
-en:
- hello: 'hello,vite!'
-ja:
- hello: 'こんにちは、vite!'
-
diff --git a/packages/playground/vue/CustomBlockPlugin.ts b/packages/playground/vue/CustomBlockPlugin.ts
deleted file mode 100644
index 4f5def023902bc..00000000000000
--- a/packages/playground/vue/CustomBlockPlugin.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { Plugin } from 'vite'
-
-export const vueI18nPlugin: Plugin = {
- name: 'vue-i18n',
- transform(code, id) {
- if (!/vue&type=i18n/.test(id)) {
- return
- }
- if (/\.ya?ml$/.test(id)) {
- code = JSON.stringify(require('js-yaml').load(code.trim()))
- }
- return {
- code: `export default Comp => {
- Comp.i18n = ${code}
- }`,
- map: { mappings: '' }
- }
- }
-}
diff --git a/packages/playground/vue/CustomElement.ce.vue b/packages/playground/vue/CustomElement.ce.vue
deleted file mode 100644
index 58d94650d1a74a..00000000000000
--- a/packages/playground/vue/CustomElement.ce.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-
- Custom Element
-
- {{ label }}: {{ state.count }}
-
-
-
-
-
-
diff --git a/packages/playground/vue/Hmr.vue b/packages/playground/vue/Hmr.vue
deleted file mode 100644
index 5535467af3858f..00000000000000
--- a/packages/playground/vue/Hmr.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-
- HMR
- Click the button then edit this message. The count should be preserved.
- count is {{ count }}
-
-
-
-
-
diff --git a/packages/playground/vue/Main.vue b/packages/playground/vue/Main.vue
deleted file mode 100644
index d10ae401f7aa8e..00000000000000
--- a/packages/playground/vue/Main.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
- Vue SFCs
- {{ time as string }}
-
-
-
-
-
-
-
-
-
-
- this should be red
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/playground/vue/Node.vue b/packages/playground/vue/Node.vue
deleted file mode 100644
index 246442d29f522c..00000000000000
--- a/packages/playground/vue/Node.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-
- this is node
-
diff --git a/packages/playground/vue/PreProcessors.vue b/packages/playground/vue/PreProcessors.vue
deleted file mode 100644
index ddb636678e8cdd..00000000000000
--- a/packages/playground/vue/PreProcessors.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-
-h2.pre-processors Pre-Processors
-p.pug
- | This is rendered from <template lang="pug">
- | and styled with <style lang="sass">. It should be megenta.
-p.pug-less
- | This is rendered from <template lang="pug">
- | and styled with <style lang="less">. It should be green.
-p.pug-stylus
- | This is rendered from <template lang="pug">
- | and styled with <style lang="stylus">. It should be orange.
-SlotComponent
- template(v-slot:test-slot)
- div.pug-slot slot content
-
-
-
-
-
-
-
-
-
diff --git a/packages/playground/vue/ReactivityTransform.vue b/packages/playground/vue/ReactivityTransform.vue
deleted file mode 100644
index 0dc2b09343d641..00000000000000
--- a/packages/playground/vue/ReactivityTransform.vue
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- Reactivity Transform
- Prop foo: {{ bar }}
- {{ a }}
-
diff --git a/packages/playground/vue/ScanDep.vue b/packages/playground/vue/ScanDep.vue
deleted file mode 100644
index 17b398beab1cd2..00000000000000
--- a/packages/playground/vue/ScanDep.vue
+++ /dev/null
@@ -1,8 +0,0 @@
-
- Scan Deps from <script setup lang=ts> blocks
- {{ typeof debounce === 'function' ? 'ok' : 'error' }}
-
-
-
diff --git a/packages/playground/vue/Slotted.vue b/packages/playground/vue/Slotted.vue
deleted file mode 100644
index fb25a9c5100215..00000000000000
--- a/packages/playground/vue/Slotted.vue
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
:slotted
-
-
-
-
-
diff --git a/packages/playground/vue/Syntax.vue b/packages/playground/vue/Syntax.vue
deleted file mode 100644
index de100226922c55..00000000000000
--- a/packages/playground/vue/Syntax.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-
- Syntax Support
- {{ a?.b }}
-
-
-
diff --git a/packages/playground/vue/__tests__/vue.spec.ts b/packages/playground/vue/__tests__/vue.spec.ts
deleted file mode 100644
index 63680d6f021684..00000000000000
--- a/packages/playground/vue/__tests__/vue.spec.ts
+++ /dev/null
@@ -1,244 +0,0 @@
-import { editFile, getBg, getColor, isBuild, untilUpdated } from 'testUtils'
-
-test('should render', async () => {
- expect(await page.textContent('h1')).toMatch('Vue SFCs')
-})
-
-test('should update', async () => {
- expect(await page.textContent('.hmr-inc')).toMatch('count is 0')
- await page.click('.hmr-inc')
- expect(await page.textContent('.hmr-inc')).toMatch('count is 1')
-})
-
-test('template/script latest syntax support', async () => {
- expect(await page.textContent('.syntax')).toBe('baz')
-})
-
-test('should remove comments in prod', async () => {
- expect(await page.innerHTML('.comments')).toBe(isBuild ? `` : ``)
-})
-
-test(':slotted', async () => {
- expect(await getColor('.slotted')).toBe('red')
-})
-
-describe('dep scan', () => {
- test('scan deps from
-
diff --git a/packages/playground/vue/package.json b/packages/playground/vue/package.json
deleted file mode 100644
index f493e9028b6ec3..00000000000000
--- a/packages/playground/vue/package.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "name": "test-vue",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- },
- "dependencies": {
- "lodash-es": "^4.17.21",
- "vue": "^3.2.25"
- },
- "devDependencies": {
- "@vitejs/plugin-vue": "workspace:*",
- "js-yaml": "^4.1.0",
- "less": "^4.1.2",
- "pug": "^3.0.2",
- "sass": "^1.43.4",
- "stylus": "^0.55.0"
- }
-}
diff --git a/packages/playground/vue/public/favicon.ico b/packages/playground/vue/public/favicon.ico
deleted file mode 100644
index df36fcfb72584e..00000000000000
Binary files a/packages/playground/vue/public/favicon.ico and /dev/null differ
diff --git a/packages/playground/vue/setup-import-template/SetupImportTemplate.vue b/packages/playground/vue/setup-import-template/SetupImportTemplate.vue
deleted file mode 100644
index d7fb119e3cfdc0..00000000000000
--- a/packages/playground/vue/setup-import-template/SetupImportTemplate.vue
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
diff --git a/packages/playground/vue/setup-import-template/template.html b/packages/playground/vue/setup-import-template/template.html
deleted file mode 100644
index 414069f2e9e929..00000000000000
--- a/packages/playground/vue/setup-import-template/template.html
+++ /dev/null
@@ -1,2 +0,0 @@
-Setup Import Template
-{{ count }}
diff --git a/packages/playground/vue/src-import/SrcImport.vue b/packages/playground/vue/src-import/SrcImport.vue
deleted file mode 100644
index d70e1f48a84331..00000000000000
--- a/packages/playground/vue/src-import/SrcImport.vue
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/packages/playground/vue/src-import/script.ts b/packages/playground/vue/src-import/script.ts
deleted file mode 100644
index 54e6e35db41f46..00000000000000
--- a/packages/playground/vue/src-import/script.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { defineComponent } from 'vue'
-import SrcImportStyle from './srcImportStyle.vue'
-import SrcImportStyle2 from './srcImportStyle2.vue'
-
-export default defineComponent({
- components: {
- SrcImportStyle,
- SrcImportStyle2
- },
- setup() {
- return {
- msg: 'hello from script src!'
- }
- }
-})
diff --git a/packages/playground/vue/src-import/srcImportStyle.vue b/packages/playground/vue/src-import/srcImportStyle.vue
deleted file mode 100644
index de91769858fe93..00000000000000
--- a/packages/playground/vue/src-import/srcImportStyle.vue
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
- {{ msg }}
-
-
diff --git a/packages/playground/vue/src-import/srcImportStyle2.vue b/packages/playground/vue/src-import/srcImportStyle2.vue
deleted file mode 100644
index 1e0f327413103e..00000000000000
--- a/packages/playground/vue/src-import/srcImportStyle2.vue
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- This should be tan
-
diff --git a/packages/playground/vue/src-import/style.css b/packages/playground/vue/src-import/style.css
deleted file mode 100644
index 49ab2d93176f4f..00000000000000
--- a/packages/playground/vue/src-import/style.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.src-imports-style {
- color: tan;
-}
diff --git a/packages/playground/vue/src-import/style2.css b/packages/playground/vue/src-import/style2.css
deleted file mode 100644
index 8c93cb983cc09d..00000000000000
--- a/packages/playground/vue/src-import/style2.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.src-imports-script {
- color: #0088ff;
-}
diff --git a/packages/playground/vue/src-import/template.html b/packages/playground/vue/src-import/template.html
deleted file mode 100644
index 6b55c545daac6a..00000000000000
--- a/packages/playground/vue/src-import/template.html
+++ /dev/null
@@ -1,5 +0,0 @@
-SFC Src Imports
-{{ msg }}
-This should be tan
-
-
diff --git a/packages/playground/vue/vite.config.ts b/packages/playground/vue/vite.config.ts
deleted file mode 100644
index f99a68ce8b6b10..00000000000000
--- a/packages/playground/vue/vite.config.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { defineConfig, splitVendorChunkPlugin } from 'vite'
-import vuePlugin from '@vitejs/plugin-vue'
-import { vueI18nPlugin } from './CustomBlockPlugin'
-
-export default defineConfig({
- resolve: {
- alias: {
- '/@': __dirname
- }
- },
- plugins: [
- vuePlugin({
- reactivityTransform: true
- }),
- splitVendorChunkPlugin(),
- vueI18nPlugin
- ],
- build: {
- // to make tests faster
- minify: false,
- rollupOptions: {
- output: {
- // Test splitVendorChunkPlugin composition
- manualChunks(id) {
- if (id.includes('src-import')) {
- return 'src-import'
- }
- }
- }
- }
- },
- css: {
- modules: {
- localsConvention: 'camelCaseOnly'
- }
- }
-})
diff --git a/packages/playground/wasm/__tests__/wasm.spec.ts b/packages/playground/wasm/__tests__/wasm.spec.ts
deleted file mode 100644
index 112617212251fa..00000000000000
--- a/packages/playground/wasm/__tests__/wasm.spec.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { untilUpdated } from '../../testUtils'
-
-test('should work when inlined', async () => {
- await page.click('.inline-wasm .run')
- await untilUpdated(() => page.textContent('.inline-wasm .result'), '42')
-})
-
-test('should work when output', async () => {
- await page.click('.output-wasm .run')
- await untilUpdated(() => page.textContent('.output-wasm .result'), '24')
-})
-
-test('should work when wasm in worker', async () => {
- await untilUpdated(() => page.textContent('.worker-wasm .result'), '3')
-})
diff --git a/packages/playground/wasm/index.html b/packages/playground/wasm/index.html
deleted file mode 100644
index ecb0b66e913fbb..00000000000000
--- a/packages/playground/wasm/index.html
+++ /dev/null
@@ -1,54 +0,0 @@
-Web Assembly
-
-
-
When wasm is inline, result should be 42
- Click to run
-
-
-
-
-
When wasm is output, result should be 24
- Click to run
-
-
-
-
-
worker wasm
-
-
-
-
diff --git a/packages/playground/wasm/package.json b/packages/playground/wasm/package.json
deleted file mode 100644
index 9d903be37887b8..00000000000000
--- a/packages/playground/wasm/package.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "test-wasm",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- }
-}
diff --git a/packages/playground/wasm/vite.config.ts b/packages/playground/wasm/vite.config.ts
deleted file mode 100644
index 43833d2f95d302..00000000000000
--- a/packages/playground/wasm/vite.config.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { defineConfig } from 'vite'
-export default defineConfig({
- build: {
- // make can no emit light.wasm
- // and emit add.wasm
- assetsInlineLimit: 80
- }
-})
diff --git a/packages/playground/wasm/worker.js b/packages/playground/wasm/worker.js
deleted file mode 100644
index a483865bd42bff..00000000000000
--- a/packages/playground/wasm/worker.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import init from './add.wasm'
-init().then((exports) => {
- // eslint-disable-next-line no-undef
- self.postMessage({ result: exports.add(1, 2) })
-})
diff --git a/packages/playground/worker/__tests__/worker.spec.ts b/packages/playground/worker/__tests__/worker.spec.ts
deleted file mode 100644
index 6d93e810c0c510..00000000000000
--- a/packages/playground/worker/__tests__/worker.spec.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import fs from 'fs'
-import path from 'path'
-import { untilUpdated, isBuild, testDir } from '../../testUtils'
-import type { Page } from 'playwright-chromium'
-
-test('normal', async () => {
- await page.click('.ping')
- await untilUpdated(() => page.textContent('.pong'), 'pong')
- await untilUpdated(
- () => page.textContent('.mode'),
- isBuild ? 'production' : 'development'
- )
- await untilUpdated(
- () => page.textContent('.bundle-with-plugin'),
- 'worker bundle with plugin success!'
- )
-})
-
-test('TS output', async () => {
- await page.click('.ping-ts-output')
- await untilUpdated(() => page.textContent('.pong-ts-output'), 'pong')
-})
-
-test('inlined', async () => {
- await page.click('.ping-inline')
- await untilUpdated(() => page.textContent('.pong-inline'), 'pong')
-})
-
-const waitSharedWorkerTick = (
- (resolvedSharedWorkerCount: number) => async (page: Page) => {
- await untilUpdated(async () => {
- const count = await page.textContent('.tick-count')
- // ignore the initial 0
- return count === '1' ? 'page loaded' : ''
- }, 'page loaded')
- // test.concurrent sequential is not guaranteed
- // force page to wait to ensure two pages overlap in time
- resolvedSharedWorkerCount++
- if (resolvedSharedWorkerCount < 2) return
-
- await untilUpdated(() => {
- return resolvedSharedWorkerCount === 2 ? 'all pages loaded' : ''
- }, 'all pages loaded')
- }
-)(0)
-
-test.concurrent.each([[true], [false]])('shared worker', async (doTick) => {
- if (doTick) {
- await page.click('.tick-shared')
- }
- await waitSharedWorkerTick(page)
-})
-
-test('worker emitted', async () => {
- await untilUpdated(() => page.textContent('.nested-worker'), 'pong')
- await untilUpdated(
- () => page.textContent('.nested-worker-dynamic-import'),
- '"msg":"pong"'
- )
-})
-
-if (isBuild) {
- const assetsDir = path.resolve(testDir, 'dist/assets')
- // assert correct files
- test('inlined code generation', async () => {
- const files = fs.readdirSync(assetsDir)
- expect(files.length).toBe(11)
- const index = files.find((f) => f.includes('index'))
- const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
- const worker = files.find((f) => f.includes('my-worker'))
- const workerContent = fs.readFileSync(
- path.resolve(assetsDir, worker),
- 'utf-8'
- )
-
- // worker should have all imports resolved and no exports
- expect(workerContent).not.toMatch(`import`)
- expect(workerContent).not.toMatch(`export`)
- // chunk
- expect(content).toMatch(`new Worker("/assets`)
- expect(content).toMatch(`new SharedWorker("/assets`)
- // inlined
- expect(content).toMatch(`(window.URL||window.webkitURL).createObjectURL`)
- expect(content).toMatch(`window.Blob`)
- })
-}
-
-test('classic worker is run', async () => {
- expect(await page.textContent('.classic-worker')).toMatch('A classic')
- expect(await page.textContent('.classic-shared-worker')).toMatch('A classic')
-})
diff --git a/packages/playground/worker/classic-worker.js b/packages/playground/worker/classic-worker.js
deleted file mode 100644
index bb6f9c3f49fc84..00000000000000
--- a/packages/playground/worker/classic-worker.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// prettier-ignore
-function text(el, text) {
- document.querySelector(el).textContent = text
-}
-
-const classicWorker = new Worker(
- new URL('./newUrl/classic-worker.js', import.meta.url) /* , */ ,
- // test comment
-
-)
-
-classicWorker.addEventListener('message', ({ data }) => {
- text('.classic-worker', data)
-})
-classicWorker.postMessage('ping')
-
-const classicSharedWorker = new SharedWorker(
- new URL('./newUrl/classic-shared-worker.js', import.meta.url),
- {
- type: 'classic'
- }
-)
-classicSharedWorker.port.addEventListener('message', (ev) => {
- text(
- '.classic-shared-worker',
- ev.data
- )
-})
-classicSharedWorker.port.start()
diff --git a/packages/playground/worker/index.html b/packages/playground/worker/index.html
deleted file mode 100644
index b3525da299ff5a..00000000000000
--- a/packages/playground/worker/index.html
+++ /dev/null
@@ -1,132 +0,0 @@
-Expected values:
-Ping
-
- Response from worker:
-
-bundle-with-plugin:
-
-Ping Inline Worker
-Response from inline worker:
-
-Ping Possible Compiled TS Worker
-
- Response from worker imported from code that might be compiled TS:
-
-
-
-Tick Shared Worker
-
- Tick from shared worker, it syncs between pages:
- 0
-
-
-new Worker(new Url('path', import.meta.url), { type: 'module' })
-
-
-new SharedWorker(new Url('path', import.meta.url), { type: 'module' })
-
-
-nested worker
-
-
-new Worker(new Url('path', import.meta.url))
-
-
-new Worker(new Url('path', import.meta.url), { type: 'classic' })
-
-
-
diff --git a/packages/playground/worker/my-shared-worker.ts b/packages/playground/worker/my-shared-worker.ts
deleted file mode 100644
index cd5b24f265b955..00000000000000
--- a/packages/playground/worker/my-shared-worker.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-let count = 0
-const ports = new Set()
-
-onconnect = (event) => {
- const port = event.ports[0]
- ports.add(port)
- port.postMessage(count)
- port.onmessage = (message) => {
- if (message.data === 'tick') {
- count++
- ports.forEach((p) => {
- p.postMessage(count)
- })
- }
- }
-}
diff --git a/packages/playground/worker/my-worker.ts b/packages/playground/worker/my-worker.ts
deleted file mode 100644
index 550382be72c331..00000000000000
--- a/packages/playground/worker/my-worker.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { msg, mode } from './workerImport'
-import { bundleWithPlugin } from './test-plugin'
-
-self.onmessage = (e) => {
- if (e.data === 'ping') {
- self.postMessage({ msg, mode, bundleWithPlugin })
- }
-}
diff --git a/packages/playground/worker/newUrl/classic-shared-worker.js b/packages/playground/worker/newUrl/classic-shared-worker.js
deleted file mode 100644
index 462e49dfa8847f..00000000000000
--- a/packages/playground/worker/newUrl/classic-shared-worker.js
+++ /dev/null
@@ -1,6 +0,0 @@
-importScripts('/classic.js')
-
-self.onconnect = (event) => {
- const port = event.ports[0]
- port.postMessage(self.constant)
-}
diff --git a/packages/playground/worker/newUrl/classic-worker.js b/packages/playground/worker/newUrl/classic-worker.js
deleted file mode 100644
index 865810c76fbf85..00000000000000
--- a/packages/playground/worker/newUrl/classic-worker.js
+++ /dev/null
@@ -1,5 +0,0 @@
-importScripts('/classic.js')
-
-self.addEventListener('message', () => {
- self.postMessage(self.constant)
-})
diff --git a/packages/playground/worker/newUrl/url-shared-worker.js b/packages/playground/worker/newUrl/url-shared-worker.js
deleted file mode 100644
index f52de169243056..00000000000000
--- a/packages/playground/worker/newUrl/url-shared-worker.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import constant from './module'
-
-self.onconnect = (event) => {
- const port = event.ports[0]
- port.postMessage(constant)
-}
diff --git a/packages/playground/worker/newUrl/url-worker.js b/packages/playground/worker/newUrl/url-worker.js
deleted file mode 100644
index afd91bfe613dc2..00000000000000
--- a/packages/playground/worker/newUrl/url-worker.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import constant from './module'
-
-self.postMessage(constant)
diff --git a/packages/playground/worker/package.json b/packages/playground/worker/package.json
deleted file mode 100644
index 131df8c4cbf336..00000000000000
--- a/packages/playground/worker/package.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "name": "test-worker",
- "private": true,
- "version": "0.0.0",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "debug": "node --inspect-brk ../../vite/bin/vite",
- "preview": "vite preview"
- },
- "devDependencies": {
- "@vitejs/plugin-vue-jsx": "workspace:*"
- }
-}
diff --git a/packages/playground/worker/possible-ts-output-worker.mjs b/packages/playground/worker/possible-ts-output-worker.mjs
deleted file mode 100644
index 2bcce3faa8a50e..00000000000000
--- a/packages/playground/worker/possible-ts-output-worker.mjs
+++ /dev/null
@@ -1,7 +0,0 @@
-import { msg, mode } from './workerImport'
-
-self.onmessage = (e) => {
- if (e.data === 'ping') {
- self.postMessage({ msg, mode })
- }
-}
diff --git a/packages/playground/worker/sub-worker.js b/packages/playground/worker/sub-worker.js
deleted file mode 100644
index ab64b3667099bb..00000000000000
--- a/packages/playground/worker/sub-worker.js
+++ /dev/null
@@ -1,13 +0,0 @@
-self.onmessage = (event) => {
- if (event.data === 'ping') {
- self.postMessage('pong')
- }
-}
-const data = import('./workerImport')
-data.then((data) => {
- const { mode, msg } = data
- self.postMessage({
- mode,
- msg
- })
-})
diff --git a/packages/playground/worker/test-plugin.tsx b/packages/playground/worker/test-plugin.tsx
deleted file mode 100644
index 15b6b94f460bc3..00000000000000
--- a/packages/playground/worker/test-plugin.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export const bundleWithPlugin: string = 'worker bundle with plugin success!'
diff --git a/packages/playground/worker/vite.config.ts b/packages/playground/worker/vite.config.ts
deleted file mode 100644
index 6cef7d9cea0bed..00000000000000
--- a/packages/playground/worker/vite.config.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import vueJsx from '@vitejs/plugin-vue-jsx'
-import { defineConfig } from 'vite'
-
-export default defineConfig({
- worker: {
- format: 'es',
- plugins: [vueJsx()]
- }
-})
diff --git a/packages/playground/worker/worker-nested-worker.js b/packages/playground/worker/worker-nested-worker.js
deleted file mode 100644
index 6d4d1e4969005f..00000000000000
--- a/packages/playground/worker/worker-nested-worker.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import SubWorker from './sub-worker?worker'
-
-const subWorker = new SubWorker()
-
-self.onmessage = (event) => {
- if (event.data === 'ping') {
- subWorker.postMessage('ping')
- }
-}
-
-subWorker.onmessage = (event) => {
- self.postMessage(event.data)
-}
diff --git a/packages/plugin-legacy/CHANGELOG.md b/packages/plugin-legacy/CHANGELOG.md
index e92ca1e12357fe..1606541415deaa 100644
--- a/packages/plugin-legacy/CHANGELOG.md
+++ b/packages/plugin-legacy/CHANGELOG.md
@@ -1,3 +1,457 @@
+## 6.1.1 (2025-04-28)
+
+* fix(legacy): use unbuild 3.4 for now (#19928) ([96f73d1](https://github.com/vitejs/vite/commit/96f73d16c8501013be57aee1c8a2353a56460281)), closes [#19928](https://github.com/vitejs/vite/issues/19928)
+
+
+
+## 6.1.0 (2025-04-16)
+
+* feat(legacy): add 'assumptions' option (#19719) ([d1d99c9](https://github.com/vitejs/vite/commit/d1d99c9220989ce903dea9cae6c3608f57f377ea)), closes [#19719](https://github.com/vitejs/vite/issues/19719)
+* feat(legacy): add sourcemapBaseUrl support (#19281) ([a92c74b](https://github.com/vitejs/vite/commit/a92c74b088a253257c01596bd7c67e0f8fa39512)), closes [#19281](https://github.com/vitejs/vite/issues/19281)
+* fix(deps): update all non-major dependencies (#19555) ([f612e0f](https://github.com/vitejs/vite/commit/f612e0fdf6810317b61fcca1ded125755f261d78)), closes [#19555](https://github.com/vitejs/vite/issues/19555)
+* fix(deps): update all non-major dependencies (#19613) ([363d691](https://github.com/vitejs/vite/commit/363d691b4995d72f26a14eb59ed88a9483b1f931)), closes [#19613](https://github.com/vitejs/vite/issues/19613)
+* fix(deps): update all non-major dependencies (#19649) ([f4e712f](https://github.com/vitejs/vite/commit/f4e712ff861f8a9504594a4a5e6d35a7547e5a7e)), closes [#19649](https://github.com/vitejs/vite/issues/19649)
+* refactor: restore endsWith usage (#19554) ([6113a96](https://github.com/vitejs/vite/commit/6113a9670cc9b7d29fe0bffe033f7823e36ded00)), closes [#19554](https://github.com/vitejs/vite/issues/19554)
+
+
+
+## 6.0.2 (2025-02-25)
+
+* fix(deps): update all non-major dependencies (#19392) ([60456a5](https://github.com/vitejs/vite/commit/60456a54fe90872dbd4bed332ecbd85bc88deb92)), closes [#19392](https://github.com/vitejs/vite/issues/19392)
+* fix(deps): update all non-major dependencies (#19440) ([ccac73d](https://github.com/vitejs/vite/commit/ccac73d9d0e92c7232f09207d1d6b893e823ed8e)), closes [#19440](https://github.com/vitejs/vite/issues/19440)
+* fix(legacy): warn if plugin-legacy is passed to `worker.plugins` (#19079) ([171f2fb](https://github.com/vitejs/vite/commit/171f2fbe0afe09eeb49f5f29f9ecd845c39a8401)), closes [#19079](https://github.com/vitejs/vite/issues/19079)
+* chore: fix typos (#19398) ([b44e3d4](https://github.com/vitejs/vite/commit/b44e3d43db65babe1c32e143964add02e080dc15)), closes [#19398](https://github.com/vitejs/vite/issues/19398)
+
+
+
+## 6.0.1 (2025-02-05)
+
+* fix(deps): update all non-major dependencies (#18853) ([5c02236](https://github.com/vitejs/vite/commit/5c0223636fa277d5daeb4d93c3f32d9f3cd69fc5)), closes [#18853](https://github.com/vitejs/vite/issues/18853)
+* fix(deps): update all non-major dependencies (#18967) ([d88d000](https://github.com/vitejs/vite/commit/d88d0004a8e891ca6026d356695e0b319caa7fce)), closes [#18967](https://github.com/vitejs/vite/issues/18967)
+* fix(deps): update all non-major dependencies (#19098) ([8639538](https://github.com/vitejs/vite/commit/8639538e6498d1109da583ad942c1472098b5919)), closes [#19098](https://github.com/vitejs/vite/issues/19098)
+* fix(deps): update all non-major dependencies (#19190) ([f2c07db](https://github.com/vitejs/vite/commit/f2c07dbfc874b46f6e09bb04996d0514663e4544)), closes [#19190](https://github.com/vitejs/vite/issues/19190)
+* fix(deps): update all non-major dependencies (#19296) ([2bea7ce](https://github.com/vitejs/vite/commit/2bea7cec4b7fddbd5f2fb6090a7eaf5ae7ca0f1b)), closes [#19296](https://github.com/vitejs/vite/issues/19296)
+* fix(legacy): build respect `hashCharacters` config (#19262) ([3aa10b7](https://github.com/vitejs/vite/commit/3aa10b7d618b178aec0f027b1f5fcd3353d2b166)), closes [#19262](https://github.com/vitejs/vite/issues/19262)
+* fix(legacy): import babel once (#19152) ([282496d](https://github.com/vitejs/vite/commit/282496daaca43494feceaa59809f6ceafd62dedd)), closes [#19152](https://github.com/vitejs/vite/issues/19152)
+* revert: update moduleResolution value casing (#18409) (#18774) ([b0fc6e3](https://github.com/vitejs/vite/commit/b0fc6e3c2591a30360d3714263cf7cc0e2acbfdf)), closes [#18409](https://github.com/vitejs/vite/issues/18409) [#18774](https://github.com/vitejs/vite/issues/18774)
+
+
+
+## 6.0.0 (2024-11-26)
+
+* chore: upgrade to unbuild v3 rc (#18502) ([ddd5c5d](https://github.com/vitejs/vite/commit/ddd5c5d00ff7894462a608841560883f9c771f22)), closes [#18502](https://github.com/vitejs/vite/issues/18502)
+* chore(deps): update all non-major dependencies (#18562) ([fb227ec](https://github.com/vitejs/vite/commit/fb227ec4402246b5a13e274c881d9de6dd8082dd)), closes [#18562](https://github.com/vitejs/vite/issues/18562)
+* chore(legacy): bump terser peer dep to ^5.16 (#18772) ([3f6d5fe](https://github.com/vitejs/vite/commit/3f6d5fed8739f30cddb821a680576d93b3a60bba)), closes [#18772](https://github.com/vitejs/vite/issues/18772)
+* chore(legacy): update peer dep Vite to 6 (#18771) ([63c62b3](https://github.com/vitejs/vite/commit/63c62b3059b589a51d1673bfdcefdb0b4e87c089)), closes [#18771](https://github.com/vitejs/vite/issues/18771)
+* chore(plugin-legacy): add type module in package.json (#18535) ([28cefca](https://github.com/vitejs/vite/commit/28cefcaf2861b72901abe1f047d9ec6298b745f8)), closes [#18535](https://github.com/vitejs/vite/issues/18535)
+* feat!: drop node 21 support in version ranges (#18729) ([a384d8f](https://github.com/vitejs/vite/commit/a384d8fd39162190675abcfea31ba657383a3d03)), closes [#18729](https://github.com/vitejs/vite/issues/18729)
+* fix(deps): update all non-major dependencies (#18484) ([2ec12df](https://github.com/vitejs/vite/commit/2ec12df98d07eb4c986737e86a4a9f8066724658)), closes [#18484](https://github.com/vitejs/vite/issues/18484)
+* fix(deps): update all non-major dependencies (#18691) ([f005461](https://github.com/vitejs/vite/commit/f005461ecce89ada21cb0c021f7af460b5479736)), closes [#18691](https://github.com/vitejs/vite/issues/18691)
+
+
+
+## 5.4.3 (2024-10-25)
+
+* chore: enable some eslint rules (#18084) ([e9a2746](https://github.com/vitejs/vite/commit/e9a2746ca77473b1814fd05db3d299c074135fe5)), closes [#18084](https://github.com/vitejs/vite/issues/18084)
+* chore: remove stale TODOs (#17866) ([e012f29](https://github.com/vitejs/vite/commit/e012f296df583bd133d26399397bd4ae49de1497)), closes [#17866](https://github.com/vitejs/vite/issues/17866)
+* chore: update license copyright (#18278) ([56eb869](https://github.com/vitejs/vite/commit/56eb869a67551a257d20cba00016ea59b1e1a2c4)), closes [#18278](https://github.com/vitejs/vite/issues/18278)
+* chore(deps): update all non-major dependencies (#17945) ([cfb621e](https://github.com/vitejs/vite/commit/cfb621e7a5a3e24d710a9af156e6855e73caf891)), closes [#17945](https://github.com/vitejs/vite/issues/17945)
+* chore(deps): update all non-major dependencies (#18050) ([7cac03f](https://github.com/vitejs/vite/commit/7cac03fa5197a72d2e2422bd0243a85a9a18abfc)), closes [#18050](https://github.com/vitejs/vite/issues/18050)
+* chore(deps): update all non-major dependencies (#18404) ([802839d](https://github.com/vitejs/vite/commit/802839d48335a69eb15f71f2cd816d0b6e4d3556)), closes [#18404](https://github.com/vitejs/vite/issues/18404)
+* fix(deps): update all non-major dependencies (#18170) ([c8aea5a](https://github.com/vitejs/vite/commit/c8aea5ae0af90dc6796ef3bdd612d1eb819f157b)), closes [#18170](https://github.com/vitejs/vite/issues/18170)
+* fix(deps): update all non-major dependencies (#18292) ([5cac054](https://github.com/vitejs/vite/commit/5cac0544dca2764f0114aac38e9922a0c13d7ef4)), closes [#18292](https://github.com/vitejs/vite/issues/18292)
+* fix(deps): update all non-major dependencies (#18345) ([5552583](https://github.com/vitejs/vite/commit/5552583a2272cd4208b30ad60e99d984e34645f0)), closes [#18345](https://github.com/vitejs/vite/issues/18345)
+* fix(legacy): generate sourcemap for polyfill chunks (#18250) ([f311ff3](https://github.com/vitejs/vite/commit/f311ff3c2b19636457c3023095ef32ab9a96b84a)), closes [#18250](https://github.com/vitejs/vite/issues/18250)
+* perf: use `crypto.hash` when available (#18317) ([2a14884](https://github.com/vitejs/vite/commit/2a148844cf2382a5377b75066351f00207843352)), closes [#18317](https://github.com/vitejs/vite/issues/18317)
+
+
+
+## 5.4.2 (2024-08-15)
+
+* chore: extend commit hash (#17709) ([4fc9b64](https://github.com/vitejs/vite/commit/4fc9b6424c27aca8004c368b69991a56264e4fdb)), closes [#17709](https://github.com/vitejs/vite/issues/17709)
+* chore(deps): update all non-major dependencies (#17820) ([bb2f8bb](https://github.com/vitejs/vite/commit/bb2f8bb55fdd64e4f16831ff98921c221a5e734a)), closes [#17820](https://github.com/vitejs/vite/issues/17820)
+* fix: handle encoded base paths (#17577) ([720447e](https://github.com/vitejs/vite/commit/720447ee725046323387f661341d44e2ad390f41)), closes [#17577](https://github.com/vitejs/vite/issues/17577)
+* fix(deps): update all non-major dependencies (#17430) ([4453d35](https://github.com/vitejs/vite/commit/4453d3578b343d16a8a5298bf154f280088968d9)), closes [#17430](https://github.com/vitejs/vite/issues/17430)
+* fix(deps): update all non-major dependencies (#17494) ([bf123f2](https://github.com/vitejs/vite/commit/bf123f2c6242424a3648cf9234281fd9ff44e3d5)), closes [#17494](https://github.com/vitejs/vite/issues/17494)
+* fix(deps): update all non-major dependencies (#17629) ([93281b0](https://github.com/vitejs/vite/commit/93281b0e09ff8b00e21c24b80ed796db89cbc1ef)), closes [#17629](https://github.com/vitejs/vite/issues/17629)
+* fix(deps): update all non-major dependencies (#17780) ([e408542](https://github.com/vitejs/vite/commit/e408542748edebd93dba07f21e3fd107725cadca)), closes [#17780](https://github.com/vitejs/vite/issues/17780)
+* perf: improve regex performance (#17789) ([952bae3](https://github.com/vitejs/vite/commit/952bae3efcbd871fc3df5b1947060de7ebdafa36)), closes [#17789](https://github.com/vitejs/vite/issues/17789)
+* docs: rename cdnjs link (#17565) ([61357f6](https://github.com/vitejs/vite/commit/61357f67cdb8eca2c551150a1f0329e272f4da62)), closes [#17565](https://github.com/vitejs/vite/issues/17565)
+
+
+
+## 5.4.1 (2024-05-30)
+
+* fix(deps): update all non-major dependencies (#17321) ([4a89766](https://github.com/vitejs/vite/commit/4a89766d838527c144f14e842211100b16792018)), closes [#17321](https://github.com/vitejs/vite/issues/17321)
+* fix(plugin-legacy): group discovered polyfills by output (#17347) ([c735cc7](https://github.com/vitejs/vite/commit/c735cc7895b34dd760f57145a00ddc1da7526b8c)), closes [#17347](https://github.com/vitejs/vite/issues/17347)
+* fix(plugin-legacy): improve deterministic polyfills discovery (#16566) ([48edfcd](https://github.com/vitejs/vite/commit/48edfcd91386a28817cbe5a361beb87c7d17f490)), closes [#16566](https://github.com/vitejs/vite/issues/16566)
+* docs(plugin-legacy): update outdated warning about `modernPolyfills` (#17335) ([e6a70b7](https://github.com/vitejs/vite/commit/e6a70b7c2d8a23cd2c3a20fbd9c33f199fdc3944)), closes [#17335](https://github.com/vitejs/vite/issues/17335)
+* chore(deps): remove unused deps (#17329) ([5a45745](https://github.com/vitejs/vite/commit/5a457454bfee1892b0d58c4b1c401cfb15986097)), closes [#17329](https://github.com/vitejs/vite/issues/17329)
+* chore(deps): update all non-major dependencies (#16722) ([b45922a](https://github.com/vitejs/vite/commit/b45922a91d4a73c27f78f26e369b7b1fd8d800e3)), closes [#16722](https://github.com/vitejs/vite/issues/16722)
+
+
+
+## 5.4.0 (2024-05-08)
+
+* fix(deps): update all non-major dependencies (#16258) ([7caef42](https://github.com/vitejs/vite/commit/7caef4216e16d9ac71e38598a9ecedce2281d42f)), closes [#16258](https://github.com/vitejs/vite/issues/16258)
+* fix(deps): update all non-major dependencies (#16376) ([58a2938](https://github.com/vitejs/vite/commit/58a2938a9766981fdc2ed89bec8ff1c96cae0716)), closes [#16376](https://github.com/vitejs/vite/issues/16376)
+* fix(deps): update all non-major dependencies (#16488) ([2d50be2](https://github.com/vitejs/vite/commit/2d50be2a5424e4f4c22774652ed313d2a232f8af)), closes [#16488](https://github.com/vitejs/vite/issues/16488)
+* fix(deps): update all non-major dependencies (#16549) ([2d6a13b](https://github.com/vitejs/vite/commit/2d6a13b0aa1f3860482dac2ce260cfbb0713033f)), closes [#16549](https://github.com/vitejs/vite/issues/16549)
+* fix(legacy): modern polyfill autodetection was not injecting enough polyfills (#16367) ([4af9f97](https://github.com/vitejs/vite/commit/4af9f97cade9fdb349e4928871bbf15c190f9e2b)), closes [#16367](https://github.com/vitejs/vite/issues/16367)
+* feat(plugin-legacy): support `additionalModernPolyfills` (#16514) ([2322657](https://github.com/vitejs/vite/commit/232265783670563e34cf96240bf0e383a3653e6c)), closes [#16514](https://github.com/vitejs/vite/issues/16514)
+* docs(legacy): update `modernTargets` option default value description (#16491) ([7171837](https://github.com/vitejs/vite/commit/7171837abbf8634be2c2e9c32d5dc6a8cbf31e0d)), closes [#16491](https://github.com/vitejs/vite/issues/16491)
+* chore(deps): update all non-major dependencies (#16131) ([a862ecb](https://github.com/vitejs/vite/commit/a862ecb941a432b6e3bab62331012e4b53ddd4e8)), closes [#16131](https://github.com/vitejs/vite/issues/16131)
+
+
+
+## 5.3.2 (2024-03-08)
+
+* fix(plugin-legacy): dynamic import browserslist-to-esbuild (#16011) ([42fd11c](https://github.com/vitejs/vite/commit/42fd11c1c6d37402bd15ba816fbf65dbed3abe55)), closes [#16011](https://github.com/vitejs/vite/issues/16011)
+* fix(plugin-legacy): replace `esbuild-plugin-browserslist` with `browserslist-to-esbuild` (#15988) ([37af8a7](https://github.com/vitejs/vite/commit/37af8a7be417f1fb2cf9a0d5e9ad90b76ff211b4)), closes [#15988](https://github.com/vitejs/vite/issues/15988)
+* fix(plugin-legacy): respect modernTargets option if renderLegacyChunks disabled (#15789) ([0813531](https://github.com/vitejs/vite/commit/081353179a4029d8aedaf3dfd78b95d95b757668)), closes [#15789](https://github.com/vitejs/vite/issues/15789)
+
+
+
+## 5.3.1 (2024-02-21)
+
+* fix(deps): update all non-major dependencies (#15675) ([4d9363a](https://github.com/vitejs/vite/commit/4d9363ad6bc460fe2da811cb48b036e53b8cfc75)), closes [#15675](https://github.com/vitejs/vite/issues/15675)
+* fix(deps): update all non-major dependencies (#15803) ([e0a6ef2](https://github.com/vitejs/vite/commit/e0a6ef2b9e6f1df8c5e71efab6182b7cf662d18d)), closes [#15803](https://github.com/vitejs/vite/issues/15803)
+* fix(deps): update all non-major dependencies (#15959) ([571a3fd](https://github.com/vitejs/vite/commit/571a3fde438d60540cfeba132e24646badf5ff2f)), closes [#15959](https://github.com/vitejs/vite/issues/15959)
+
+
+
+## 5.3.0 (2024-01-25)
+
+* docs: fix commit id collision (#15105) ([0654d1b](https://github.com/vitejs/vite/commit/0654d1b52448db4d7a9b69aee6aad9e015481452)), closes [#15105](https://github.com/vitejs/vite/issues/15105)
+* docs: fix dead link (#15700) ([aa7916a](https://github.com/vitejs/vite/commit/aa7916a5c2d580cdd9968fc221826ddd17443bac)), closes [#15700](https://github.com/vitejs/vite/issues/15700)
+* feat(legacy): build file name optimization (#15115) ([39f435d](https://github.com/vitejs/vite/commit/39f435d8ce329870754f33509e9fdbb61883c9fc)), closes [#15115](https://github.com/vitejs/vite/issues/15115)
+* feat(legacy): support any separator before hash in fileNames (#15170) ([ecab41a](https://github.com/vitejs/vite/commit/ecab41a7f8ee09c43e7ace6ef20d2f8693a5978a)), closes [#15170](https://github.com/vitejs/vite/issues/15170)
+* feat(plugin-legacy): add `modernTargets` option (#15506) ([cf56507](https://github.com/vitejs/vite/commit/cf56507dbfd41c4af63de511a320971668d5204f)), closes [#15506](https://github.com/vitejs/vite/issues/15506)
+* fix(deps): update all non-major dependencies (#15233) ([ad3adda](https://github.com/vitejs/vite/commit/ad3adda7215c33874a07cbd4d430fcffe4c85dce)), closes [#15233](https://github.com/vitejs/vite/issues/15233)
+* fix(deps): update all non-major dependencies (#15304) ([bb07f60](https://github.com/vitejs/vite/commit/bb07f605cca698a81f1b4606ddefb34485069dd1)), closes [#15304](https://github.com/vitejs/vite/issues/15304)
+* fix(deps): update all non-major dependencies (#15375) ([ab56227](https://github.com/vitejs/vite/commit/ab56227d89c92bfa781264e1474ed522892e3b8f)), closes [#15375](https://github.com/vitejs/vite/issues/15375)
+* chore(deps): update all non-major dependencies (#15145) ([7ff2c0a](https://github.com/vitejs/vite/commit/7ff2c0afe8c6b6901385af829f2e7e80c1fe344c)), closes [#15145](https://github.com/vitejs/vite/issues/15145)
+
+
+
+## 5.2.0 (2023-11-22)
+
+* test(legacy): add a test to checks all inline snippets are valid JS (#15098) ([1b9ca66](https://github.com/vitejs/vite/commit/1b9ca66b6d777bd4a03165512de5d65d83a2f25b)), closes [#15098](https://github.com/vitejs/vite/issues/15098)
+* fix(plugin-legacy): syntax error in variable `detectModernBrowserCode` (#15095) ([1c605ff](https://github.com/vitejs/vite/commit/1c605ffe9b56dcf731f341a687b1d5b55bba48c6)), closes [#15095](https://github.com/vitejs/vite/issues/15095)
+
+
+
+## 5.1.0 (2023-11-21)
+
+* docs(legacy): clarify that csp hashes could change between minors (#15057) ([cd353306](https://github.com/vitejs/vite/commit/cd353306)), closes [#15057](https://github.com/vitejs/vite/issues/15057)
+* fix(legacy): preserve async generator function invocation (#15021) ([47551a6](https://github.com/vitejs/vite/commit/47551a6f32eace64a4f5b669f997892c5ab867af)), closes [#15021](https://github.com/vitejs/vite/issues/15021)
+
+
+
+## 5.0.0 (2023-11-16)
+
+* feat(plugin-legacy)!: bump vite peer dep (#15004) ([3c92c7b](https://github.com/vitejs/vite/commit/3c92c7bca23616f156b70311b149cbc1af59d40b)), closes [#15004](https://github.com/vitejs/vite/issues/15004)
+
+
+
+## 5.0.0-beta.3 (2023-11-14)
+
+* fix(deps): update all non-major dependencies (#14635) ([21017a9](https://github.com/vitejs/vite/commit/21017a9408643cbc7204215ecc5a3fdaf74dc81e)), closes [#14635](https://github.com/vitejs/vite/issues/14635)
+* fix(deps): update all non-major dependencies (#14729) ([d5d96e7](https://github.com/vitejs/vite/commit/d5d96e712788bc762d9c135bc84628dbcfc7fb58)), closes [#14729](https://github.com/vitejs/vite/issues/14729)
+* fix(deps): update all non-major dependencies (#14883) ([e5094e5](https://github.com/vitejs/vite/commit/e5094e5bf2aee3516d04ce35ba2fb27e70ea9858)), closes [#14883](https://github.com/vitejs/vite/issues/14883)
+* fix(deps): update all non-major dependencies (#14961) ([0bb3995](https://github.com/vitejs/vite/commit/0bb3995a7d2245ef1cc7b2ed8a5242e33af16874)), closes [#14961](https://github.com/vitejs/vite/issues/14961)
+* fix(plugin-legacy): add invoke to modern detector to avoid terser treeshaking (#14968) ([4033a32](https://github.com/vitejs/vite/commit/4033a320d6809c9a0c2552f0ef2bf686c63aa35a)), closes [#14968](https://github.com/vitejs/vite/issues/14968)
+* feat(legacy): export `Options` (#14933) ([071bfc8](https://github.com/vitejs/vite/commit/071bfc8e18ebe3981bded8e7bab605bd473d72b9)), closes [#14933](https://github.com/vitejs/vite/issues/14933)
+* chore(deps): update dependency eslint-plugin-regexp to v2 (#14730) ([0a7c753](https://github.com/vitejs/vite/commit/0a7c75305a312161979eaf13d7b48d784bdb6b76)), closes [#14730](https://github.com/vitejs/vite/issues/14730)
+
+
+
+## 5.0.0-beta.2 (2023-10-09)
+
+* fix(legacy)!: should rename `x.[hash].js` to `x-legacy.[hash].js` (#11599) ([e7d7a6f](https://github.com/vitejs/vite/commit/e7d7a6f4ee095bca4ed4fddf387a9ff06fcea7bb)), closes [#11599](https://github.com/vitejs/vite/issues/11599)
+* fix(deps): update all non-major dependencies (#14510) ([eb204fd](https://github.com/vitejs/vite/commit/eb204fd3c5bffb6c6fb40f562f762e426fbaf183)), closes [#14510](https://github.com/vitejs/vite/issues/14510)
+* fix(legacy): fix broken build when renderModernChunks=false & polyfills=false (fix #14324) (#14346) ([27e5b11](https://github.com/vitejs/vite/commit/27e5b1114ce653b5716cd175aed9e2775da2f97a)), closes [#14324](https://github.com/vitejs/vite/issues/14324) [#14346](https://github.com/vitejs/vite/issues/14346)
+
+
+
+## 5.0.0-beta.1 (2023-09-25)
+
+* fix(deps): update all non-major dependencies (#14460) ([b77bff0](https://github.com/vitejs/vite/commit/b77bff0b93ba9449f63c8373ecf82289a39832a0)), closes [#14460](https://github.com/vitejs/vite/issues/14460)
+* fix(legacy): add guard to modern polyfill chunk (#13719) ([945dc4d](https://github.com/vitejs/vite/commit/945dc4de52e64a1a8f6e2451fadf6aba7e460234)), closes [#13719](https://github.com/vitejs/vite/issues/13719)
+* fix(legacy): modern polyfill autodetection was injecting more polyfills than needed (#14428) ([1c2e941](https://github.com/vitejs/vite/commit/1c2e941d03621a4b77d9dfca8841e336b716691c)), closes [#14428](https://github.com/vitejs/vite/issues/14428)
+* fix(legacy): suppress babel warning during polyfill scan (#14425) ([aae3a83](https://github.com/vitejs/vite/commit/aae3a83b5fb49bbd9f174cfeac66f00483829da4)), closes [#14425](https://github.com/vitejs/vite/issues/14425)
+* fix(plugin-legacy): ensure correct typing for node esm (#13892) ([d914a9d](https://github.com/vitejs/vite/commit/d914a9d79adfe0aed2ee5d69f6f6d1e80b613b98)), closes [#13892](https://github.com/vitejs/vite/issues/13892)
+* refactor(legacy)!: remove `ignoreBrowserslistConfig` option (#14429) ([941bb16](https://github.com/vitejs/vite/commit/941bb1610c9c9576e0b5738c9075b3eb2f16a357)), closes [#14429](https://github.com/vitejs/vite/issues/14429)
+
+
+
+## 5.0.0-beta.0 (2023-09-19)
+
+* fix(deps): update all non-major dependencies (#14092) ([68638f7](https://github.com/vitejs/vite/commit/68638f7b0b04ddfdf35dc8686c6a022aadbb9453)), closes [#14092](https://github.com/vitejs/vite/issues/14092)
+* chore: upgrade babel and release-scripts (#14330) ([b361ffa](https://github.com/vitejs/vite/commit/b361ffa6724d9191fc6a581acfeab5bc3ebbd931)), closes [#14330](https://github.com/vitejs/vite/issues/14330)
+* chore(deps): update all non-major dependencies (#13938) ([a1b519e](https://github.com/vitejs/vite/commit/a1b519e2c71593b6b4286c2f0bd8bfe2e0ad046d)), closes [#13938](https://github.com/vitejs/vite/issues/13938)
+* chore(eslint): allow type annotations (#13920) ([d1264fd](https://github.com/vitejs/vite/commit/d1264fd34313a2da80af8dadbeab1c8e6013bb10)), closes [#13920](https://github.com/vitejs/vite/issues/13920)
+* docs(legacy): correct `modernPolyfills` description (#14233) ([a57f388](https://github.com/vitejs/vite/commit/a57f388f53bdcbcacd7585724b43953c32e6806e)), closes [#14233](https://github.com/vitejs/vite/issues/14233)
+* docs(plugin-legacy): fix typo (#13936) ([28ddd43](https://github.com/vitejs/vite/commit/28ddd43906825db9e1ffa030551e8f975d97f3a9)), closes [#13936](https://github.com/vitejs/vite/issues/13936)
+* feat!: bump minimum node version to 18 (#14030) ([2c1a45c](https://github.com/vitejs/vite/commit/2c1a45c86cab6ecf02abb6e50385f773d5ed568e)), closes [#14030](https://github.com/vitejs/vite/issues/14030)
+* perf: use magic-string hires boundary for sourcemaps (#13971) ([b9a8d65](https://github.com/vitejs/vite/commit/b9a8d65fd64d101ea596bc98a0aea0f95674a95a)), closes [#13971](https://github.com/vitejs/vite/issues/13971)
+
+
+
+## 4.1.1 (2023-07-20)
+
+* fix(deps): update all non-major dependencies (#13758) ([8ead116](https://github.com/vitejs/vite/commit/8ead11648514ae4975bf4328d6e15bd4dd42e45e)), closes [#13758](https://github.com/vitejs/vite/issues/13758)
+* fix(deps): update all non-major dependencies (#13872) ([975a631](https://github.com/vitejs/vite/commit/975a631ec7c2373354aeeac6bc2977f24b54d13d)), closes [#13872](https://github.com/vitejs/vite/issues/13872)
+
+
+
+## 4.1.0 (2023-07-06)
+
+* feat(plugin-legacy): add option to output only legacy builds (#10139) ([931b24f](https://github.com/vitejs/vite/commit/931b24f5eac4b9b5422a235782ca13baa9a99563)), closes [#10139](https://github.com/vitejs/vite/issues/10139)
+* fix(deps): update all non-major dependencies (#13701) ([02c6bc3](https://github.com/vitejs/vite/commit/02c6bc38645ce18f9e1c8a71421fb8aad7081688)), closes [#13701](https://github.com/vitejs/vite/issues/13701)
+
+
+
+## 4.0.5 (2023-06-21)
+
+* chore: add funding field (#13585) ([2501627](https://github.com/vitejs/vite/commit/250162775031a8798f67e8be71fd226a79c9831b)), closes [#13585](https://github.com/vitejs/vite/issues/13585)
+* chore(deps): update all non-major dependencies (#13553) ([3ea0534](https://github.com/vitejs/vite/commit/3ea05342d41277baf11a73763f082e6e75c46a8f)), closes [#13553](https://github.com/vitejs/vite/issues/13553)
+* fix(deps): update all non-major dependencies (#13059) ([123ef4c](https://github.com/vitejs/vite/commit/123ef4c47c611ebd99d8b41c89c547422aea9c1d)), closes [#13059](https://github.com/vitejs/vite/issues/13059)
+* fix(deps): update all non-major dependencies (#13488) ([bd09248](https://github.com/vitejs/vite/commit/bd09248e50ae50ec57b9a72efe0a27aa397ec2e1)), closes [#13488](https://github.com/vitejs/vite/issues/13488)
+* docs(legacy): add test case to ensure correct csp hashes in readme.md (#13384) ([bf0cd25](https://github.com/vitejs/vite/commit/bf0cd25adb3b8bb5d53433ba9323d0a95e9f756a)), closes [#13384](https://github.com/vitejs/vite/issues/13384)
+
+
+
+## 4.0.4 (2023-05-24)
+
+* fix(legacy): import `@babel/preset-env` (#12961) ([d53c650](https://github.com/vitejs/vite/commit/d53c650a69aeb43efd99b210ccc3a5606f2fc31b)), closes [#12961](https://github.com/vitejs/vite/issues/12961)
+* chore(deps): update all non-major dependencies (#12805) ([5731ac9](https://github.com/vitejs/vite/commit/5731ac9caaef629e892e20394f0cc73c565d9a87)), closes [#12805](https://github.com/vitejs/vite/issues/12805)
+
+
+
+## 4.0.3 (2023-04-25)
+
+* feat(plugin-legacy): support file protocol (#8524) ([7a87ff4](https://github.com/vitejs/vite/commit/7a87ff4b0950012ad0d85b05fe36b17e1ee2ee76)), closes [#8524](https://github.com/vitejs/vite/issues/8524)
+* refactor(eslint): migrate to `eslint-plugin-n` (#12895) ([62ebe28](https://github.com/vitejs/vite/commit/62ebe28d4023c1f67578b1977edd3371f44f475a)), closes [#12895](https://github.com/vitejs/vite/issues/12895)
+* fix(deps): update all non-major dependencies (#12389) ([3e60b77](https://github.com/vitejs/vite/commit/3e60b778b0ed178a83f674031f5edb123e6c123c)), closes [#12389](https://github.com/vitejs/vite/issues/12389)
+
+
+
+## 4.0.2 (2023-03-16)
+
+* chore(deps): update all non-major dependencies (#12299) ([b41336e](https://github.com/vitejs/vite/commit/b41336e450b093fb3e454806ec4245ebad7ba9c5)), closes [#12299](https://github.com/vitejs/vite/issues/12299)
+* chore(deps): update rollup to 3.17.2 (#12110) ([e54ffbd](https://github.com/vitejs/vite/commit/e54ffbdcbd5d90d560a1bda7a140de046019fcf5)), closes [#12110](https://github.com/vitejs/vite/issues/12110)
+* fix(deps): update all non-major dependencies (#12036) ([48150f2](https://github.com/vitejs/vite/commit/48150f2ea4d7ff8e3b67f15239ae05f5be317436)), closes [#12036](https://github.com/vitejs/vite/issues/12036)
+* fix(plugin-legacy): no `build.target` override on SSR build (#12171) ([a1019f8](https://github.com/vitejs/vite/commit/a1019f80a5d5b6242d8fb0975994ce1ae6e78e94)), closes [#12171](https://github.com/vitejs/vite/issues/12171)
+* docs(plugin-legacy): outdated csp hash (fix #12112) (#12118) ([5f7f5dc](https://github.com/vitejs/vite/commit/5f7f5dcb0c006012631c1d5df61d79307d9097f4)), closes [#12112](https://github.com/vitejs/vite/issues/12112) [#12118](https://github.com/vitejs/vite/issues/12118)
+
+
+
+## 4.0.1 (2023-02-02)
+
+* fix(legacy): fix browserslist import, close https://github.com/vitejs/vite/issues/11898 (#11899) ([9241d08](https://github.com/vitejs/vite/commit/9241d0895b37c658a2dccfd961958c0c5238a49b)), closes [#11899](https://github.com/vitejs/vite/issues/11899)
+
+
+
+## 4.0.0 (2023-02-02)
+
+* feat(legacy)!: bump modern target to support async generator (#11896) ([55b9711](https://github.com/vitejs/vite/commit/55b971139557f65f249f5385b580fa45946cb1d3)), closes [#11896](https://github.com/vitejs/vite/issues/11896)
+* fix(plugin-legacy)!: support browserslist and update default target (#11318) ([d5b8f86](https://github.com/vitejs/vite/commit/d5b8f8615e880e854a3e1105e3193c24cc964f30)), closes [#11318](https://github.com/vitejs/vite/issues/11318)
+* fix: typo (#11283) ([bf234a6](https://github.com/vitejs/vite/commit/bf234a63b46f0dc7a629abe0d69863ac15c381e1)), closes [#11283](https://github.com/vitejs/vite/issues/11283)
+* fix(deps): update all non-major dependencies (#11846) ([5d55083](https://github.com/vitejs/vite/commit/5d5508311f9856de69babd72dc4de0e7c21c7ae8)), closes [#11846](https://github.com/vitejs/vite/issues/11846)
+* fix(plugin-legacy): legacy sourcemap not generate (fix #11693) (#11841) ([2ff5930](https://github.com/vitejs/vite/commit/2ff5930e02d80d6254037281b4c62b8e489d63ba)), closes [#11693](https://github.com/vitejs/vite/issues/11693) [#11841](https://github.com/vitejs/vite/issues/11841)
+* chore: enable `@typescript-eslint/ban-ts-comment` (#11326) ([e58a4f0](https://github.com/vitejs/vite/commit/e58a4f00e201e9c0d43ddda51ccac7b612d58650)), closes [#11326](https://github.com/vitejs/vite/issues/11326)
+* chore: update packages' (vite, vite-legacy) keywords (#11402) ([a56bc34](https://github.com/vitejs/vite/commit/a56bc3434e9d4bc7f9d580ae630ccc633e7d436a)), closes [#11402](https://github.com/vitejs/vite/issues/11402)
+* chore(deps): update all non-major dependencies (#11419) ([896475d](https://github.com/vitejs/vite/commit/896475dc6c7e5f1168e21d556201a61659552617)), closes [#11419](https://github.com/vitejs/vite/issues/11419)
+* chore(deps): update all non-major dependencies (#11787) ([271394f](https://github.com/vitejs/vite/commit/271394fc7157a08b19f22d3751c8ec6e69f0bd5f)), closes [#11787](https://github.com/vitejs/vite/issues/11787)
+* refactor(plugin-legacy): optimize cspHashes array (#11734) ([b1a8e58](https://github.com/vitejs/vite/commit/b1a8e5856db91df264a7d1e40bf847dde5eb0981)), closes [#11734](https://github.com/vitejs/vite/issues/11734)
+
+
+
+## 3.0.1 (2022-12-09)
+
+* chore: update vite and plugins to stable (#11278) ([026f41e](https://github.com/vitejs/vite/commit/026f41e87e6eb89491c88f62952d7a094f810811)), closes [#11278](https://github.com/vitejs/vite/issues/11278)
+
+
+
+## 3.0.0 (2022-12-09)
+
+* chore: enable prettier trailing commas (#11167) ([134ce68](https://github.com/vitejs/vite/commit/134ce6817984bad0f5fb043481502531fee9b1db)), closes [#11167](https://github.com/vitejs/vite/issues/11167)
+* chore(deps): update all non-major dependencies (#11182) ([8b83089](https://github.com/vitejs/vite/commit/8b830899ef0ce4ebe257ed18222543f60b775832)), closes [#11182](https://github.com/vitejs/vite/issues/11182)
+
+
+
+## 3.0.0-alpha.0 (2022-11-30)
+
+* fix: support polyfill import paths containing an escaping char (e.g. '\') (#10859) ([7ac2535](https://github.com/vitejs/vite/commit/7ac2535cfc1eb276237a66f9776f9cda3db1148a)), closes [#10859](https://github.com/vitejs/vite/issues/10859)
+* fix(deps): update all non-major dependencies (#10804) ([f686afa](https://github.com/vitejs/vite/commit/f686afa6d3bc0f501b936dcbc2c4552c865fa3f9)), closes [#10804](https://github.com/vitejs/vite/issues/10804)
+* fix(deps): update all non-major dependencies (#11091) ([073a4bf](https://github.com/vitejs/vite/commit/073a4bfe2642a4dda2183a9dfecac864524893e1)), closes [#11091](https://github.com/vitejs/vite/issues/11091)
+* chore(deps): update all non-major dependencies (#10910) ([f6ad607](https://github.com/vitejs/vite/commit/f6ad607d2430a44ea7dc71ecd3c44c1e8bf8446f)), closes [#10910](https://github.com/vitejs/vite/issues/10910)
+* chore(deps): update all non-major dependencies (#11006) ([96f2e98](https://github.com/vitejs/vite/commit/96f2e98f6a196652962ccb5f2fa6195c050c463f)), closes [#11006](https://github.com/vitejs/vite/issues/11006)
+* feat: align default chunk and asset file names with rollup (#10927) ([cc2adb3](https://github.com/vitejs/vite/commit/cc2adb39254d6de139bc3dfad976430c03250b27)), closes [#10927](https://github.com/vitejs/vite/issues/10927)
+* feat: rollup 3 (#9870) ([beb7166](https://github.com/vitejs/vite/commit/beb716695d5dd11fd9f3d7350c1c807dfa37a216)), closes [#9870](https://github.com/vitejs/vite/issues/9870)
+
+
+
+## 2.3.1 (2022-11-07)
+
+* chore(deps): update all non-major dependencies (#10725) ([22cfad8](https://github.com/vitejs/vite/commit/22cfad87c824e717b6c616129f3b579be2e979b2)), closes [#10725](https://github.com/vitejs/vite/issues/10725)
+
+
+
+## 2.3.0 (2022-10-26)
+
+* fix(deps): update all non-major dependencies (#10610) ([bb95467](https://github.com/vitejs/vite/commit/bb954672e3ee863e5cb37fa78167e5fc6df9ae4e)), closes [#10610](https://github.com/vitejs/vite/issues/10610)
+* chore(deps): update all non-major dependencies (#10393) ([f519423](https://github.com/vitejs/vite/commit/f519423170fafeee9d58aeb2052cb3bc224f25f8)), closes [#10393](https://github.com/vitejs/vite/issues/10393)
+* chore(deps): update all non-major dependencies (#10488) ([15aa827](https://github.com/vitejs/vite/commit/15aa827283d6cbf9f55c02d6d8a3fe43dbd792e4)), closes [#10488](https://github.com/vitejs/vite/issues/10488)
+
+
+
+## 2.3.0-beta.0 (2022-10-05)
+
+* fix(deps): update all non-major dependencies (#10160) ([6233c83](https://github.com/vitejs/vite/commit/6233c830201085d869fbbd2a7e622a59272e0f43)), closes [#10160](https://github.com/vitejs/vite/issues/10160)
+* fix(deps): update all non-major dependencies (#10246) ([81d4d04](https://github.com/vitejs/vite/commit/81d4d04c37b805843ea83075d1c0819c31726c4e)), closes [#10246](https://github.com/vitejs/vite/issues/10246)
+* fix(deps): update all non-major dependencies (#10316) ([a38b450](https://github.com/vitejs/vite/commit/a38b450441eea02a680b80ac0624126ba6abe3f7)), closes [#10316](https://github.com/vitejs/vite/issues/10316)
+* fix(legacy): don't force set `build.target` when `renderLegacyChunks=false` (fixes #10201) (#10220) ([7f548e8](https://github.com/vitejs/vite/commit/7f548e874a2fb2b09f08fe123bb3ebc10aa2f54b)), closes [#10201](https://github.com/vitejs/vite/issues/10201) [#10220](https://github.com/vitejs/vite/issues/10220)
+* refactor(types): bundle client types (#9966) ([da632bf](https://github.com/vitejs/vite/commit/da632bf36f561c0fc4031830721a7d4d86135efb)), closes [#9966](https://github.com/vitejs/vite/issues/9966)
+
+
+
+## 2.2.0 (2022-09-19)
+
+* docs(plugin-legacy): fix Vite default target (#10158) ([62ff788](https://github.com/vitejs/vite/commit/62ff7887870392f0cce2a40b3cc5d1b7c48a9a47)), closes [#10158](https://github.com/vitejs/vite/issues/10158)
+* fix(deps): update all non-major dependencies (#10077) ([caf00c8](https://github.com/vitejs/vite/commit/caf00c8c7a5c81a92182116ffa344b34ce4c3b5e)), closes [#10077](https://github.com/vitejs/vite/issues/10077)
+* fix(deps): update all non-major dependencies (#9985) ([855f2f0](https://github.com/vitejs/vite/commit/855f2f077eb8dc41b395bccecb6a5b836eb526a9)), closes [#9985](https://github.com/vitejs/vite/issues/9985)
+* fix(plugin-legacy): force set `build.target` (#10072) ([a13a7eb](https://github.com/vitejs/vite/commit/a13a7eb4165d38ce0ab6eefd4e4d38104ce63699)), closes [#10072](https://github.com/vitejs/vite/issues/10072)
+
+
+
+## 2.1.0 (2022-09-05)
+
+
+
+
+## 2.1.0-beta.0 (2022-08-29)
+
+* fix(deps): update all non-major dependencies (#9888) ([e35a58b](https://github.com/vitejs/vite/commit/e35a58ba46f906feea8ab46886c3306257c61560)), closes [#9888](https://github.com/vitejs/vite/issues/9888)
+* fix(plugin-legacy): prevent global process.env.NODE_ENV mutation (#9741) ([a8279af](https://github.com/vitejs/vite/commit/a8279af608657861b64af5980942cced0b04c8ac)), closes [#9741](https://github.com/vitejs/vite/issues/9741)
+* chore(deps): update all non-major dependencies (#9675) ([4e56e87](https://github.com/vitejs/vite/commit/4e56e87623501109198e019ebe809872528ab088)), closes [#9675](https://github.com/vitejs/vite/issues/9675)
+* chore(deps): update all non-major dependencies (#9778) ([aceaefc](https://github.com/vitejs/vite/commit/aceaefc19eaa05c76b8a7adec035a0e4b33694c6)), closes [#9778](https://github.com/vitejs/vite/issues/9778)
+* refactor(legacy): build polyfill chunk (#9639) ([7ba0c9f](https://github.com/vitejs/vite/commit/7ba0c9f60e147a0039d2607a10c55e4feecd4bee)), closes [#9639](https://github.com/vitejs/vite/issues/9639)
+* refactor(legacy): remove code for Vite 2 (#9640) ([b1bbc5b](https://github.com/vitejs/vite/commit/b1bbc5bcc01bfc9b5923e9e58d744c594799a873)), closes [#9640](https://github.com/vitejs/vite/issues/9640)
+
+
+
+## 2.0.1 (2022-08-11)
+
+* fix: mention that Node.js 13/15 support is dropped (fixes #9113) (#9116) ([2826303](https://github.com/vitejs/vite/commit/2826303bd253e20df2746f84f6a7c06cb5cf3d6b)), closes [#9113](https://github.com/vitejs/vite/issues/9113) [#9116](https://github.com/vitejs/vite/issues/9116)
+* fix(deps): update all non-major dependencies (#9176) ([31d3b70](https://github.com/vitejs/vite/commit/31d3b70672ea8759a8d7ff1993d64bb4f0e30fab)), closes [#9176](https://github.com/vitejs/vite/issues/9176)
+* fix(deps): update all non-major dependencies (#9575) ([8071325](https://github.com/vitejs/vite/commit/80713256d0dd5716e42086fb617e96e9e92c3675)), closes [#9575](https://github.com/vitejs/vite/issues/9575)
+* fix(legacy): skip esbuild transform for systemjs (#9635) ([ac16abd](https://github.com/vitejs/vite/commit/ac16abda0a3f96daa61507bda666ade5867ec909)), closes [#9635](https://github.com/vitejs/vite/issues/9635)
+* chore: fix code typos (#9033) ([ed02861](https://github.com/vitejs/vite/commit/ed0286186b24748ec7bfa336f83c382363a22f1d)), closes [#9033](https://github.com/vitejs/vite/issues/9033)
+* chore(deps): update all non-major dependencies (#9347) ([2fcb027](https://github.com/vitejs/vite/commit/2fcb0272442664c395322acfc7899ab6a32bd86c)), closes [#9347](https://github.com/vitejs/vite/issues/9347)
+* chore(deps): update all non-major dependencies (#9478) ([c530d16](https://github.com/vitejs/vite/commit/c530d168309557c7a254128364f07f7b4f017e14)), closes [#9478](https://github.com/vitejs/vite/issues/9478)
+
+
+
+## 2.0.0 (2022-07-13)
+
+* chore: 3.0 release notes and bump peer deps (#9072) ([427ba26](https://github.com/vitejs/vite/commit/427ba26fa720a11530d135b2ee39876fc9a778fb)), closes [#9072](https://github.com/vitejs/vite/issues/9072)
+* chore(deps): update all non-major dependencies (#9022) ([6342140](https://github.com/vitejs/vite/commit/6342140e6ac7e033ca83d3494f94ea20ca2eaf07)), closes [#9022](https://github.com/vitejs/vite/issues/9022)
+* docs: cleanup changes (#8989) ([07aef1b](https://github.com/vitejs/vite/commit/07aef1b4c02a64732b31b00591d2d9d9c8025aab)), closes [#8989](https://github.com/vitejs/vite/issues/8989)
+
+
+
+## 2.0.0-beta.1 (2022-07-06)
+
+* fix(deps): update all non-major dependencies (#8802) ([a4a634d](https://github.com/vitejs/vite/commit/a4a634d6a08f8b54f052cfc2cc1b60c1bca6d48a)), closes [#8802](https://github.com/vitejs/vite/issues/8802)
+* feat: experimental.renderBuiltUrl (revised build base options) (#8762) ([895a7d6](https://github.com/vitejs/vite/commit/895a7d66bc93beaf18ebcbee23b00fda9ca4c33c)), closes [#8762](https://github.com/vitejs/vite/issues/8762)
+* chore: use `tsx` directly instead of indirect `esno` (#8773) ([f018f13](https://github.com/vitejs/vite/commit/f018f135ffa5a2885c063d4509d39958c788120c)), closes [#8773](https://github.com/vitejs/vite/issues/8773)
+
+
+
+## 2.0.0-beta.0 (2022-06-21)
+
+* feat: bump minimum node version to 14.18.0 (#8662) ([8a05432](https://github.com/vitejs/vite/commit/8a05432e6dcc0e11d78c7b029e7340fa47fceb92)), closes [#8662](https://github.com/vitejs/vite/issues/8662)
+* feat: experimental.buildAdvancedBaseOptions (#8450) ([8ef7333](https://github.com/vitejs/vite/commit/8ef733368fd6618a252e44616f7577f593fd4fbc)), closes [#8450](https://github.com/vitejs/vite/issues/8450)
+* chore: use node prefix (#8309) ([60721ac](https://github.com/vitejs/vite/commit/60721ac53a1bf326d1cac097f23642faede234ff)), closes [#8309](https://github.com/vitejs/vite/issues/8309)
+* chore(deps): update all non-major dependencies (#8669) ([628863d](https://github.com/vitejs/vite/commit/628863dc6120804cc1af8bda2ea98e802ded0e84)), closes [#8669](https://github.com/vitejs/vite/issues/8669)
+* fix(plugin-legacy): prevent esbuild injecting arrow function (#8660) ([c0e74e5](https://github.com/vitejs/vite/commit/c0e74e5f687b8f2bb308acb51cd94c31aea2808b)), closes [#8660](https://github.com/vitejs/vite/issues/8660)
+
+
+
+## 2.0.0-alpha.2 (2022-06-19)
+
+* fix(build): use crossorigin for nomodule (#8322) ([7f59989](https://github.com/vitejs/vite/commit/7f59989ec1fee7f8b71d297169589e010d3b84e3)), closes [#8322](https://github.com/vitejs/vite/issues/8322)
+* fix(deps): update all non-major dependencies (#8281) ([c68db4d](https://github.com/vitejs/vite/commit/c68db4d7ad2c1baee41f280b34ae89a85ba0373d)), closes [#8281](https://github.com/vitejs/vite/issues/8281)
+* fix(deps): update all non-major dependencies (#8391) ([842f995](https://github.com/vitejs/vite/commit/842f995ca69600c4c06c46d202fe713b80373418)), closes [#8391](https://github.com/vitejs/vite/issues/8391)
+* fix(plugin-legacy): disable babel.compact when minify is disabled (#8244) ([742188c](https://github.com/vitejs/vite/commit/742188cc04526439060bdac7125237c20463d5a5)), closes [#8244](https://github.com/vitejs/vite/issues/8244)
+* fix(plugin-legacy): don't include SystemJS in modern polyfills (#6902) ([eb47b1e](https://github.com/vitejs/vite/commit/eb47b1e2580cd6f8285dadba8f943e1b667ec390)), closes [#6902](https://github.com/vitejs/vite/issues/6902)
+* fix(plugin-legacy): empty base makes import fail (fixes #4212) (#8387) ([1a16f12](https://github.com/vitejs/vite/commit/1a16f123e0781449c511af2d0112b8c4639972f1)), closes [#4212](https://github.com/vitejs/vite/issues/4212) [#8387](https://github.com/vitejs/vite/issues/8387)
+* fix(plugin-legacy): modern polyfill latest features (fixes #8399) (#8408) ([ed25817](https://github.com/vitejs/vite/commit/ed2581778baff3201f47866799f006a490a7e35b)), closes [#8399](https://github.com/vitejs/vite/issues/8399) [#8408](https://github.com/vitejs/vite/issues/8408)
+* fix(plugin-legacy): prevent failed to load module (#8285) ([d671811](https://github.com/vitejs/vite/commit/d67181195aec99ee6aa71bd8fdb69f1f09f57c9d)), closes [#8285](https://github.com/vitejs/vite/issues/8285)
+* fix(plugin-legacy): respect `entryFileNames` for polyfill chunks (#8247) ([baa9632](https://github.com/vitejs/vite/commit/baa9632a2c2befafdfde0f131f84f247fa8b6478)), closes [#8247](https://github.com/vitejs/vite/issues/8247)
+* chore: enable `@typescript-eslint/explicit-module-boundary-types` (#8372) ([104caf9](https://github.com/vitejs/vite/commit/104caf95ecd8cdf2d21ca7171931622b52fd74ff)), closes [#8372](https://github.com/vitejs/vite/issues/8372)
+* chore: update major deps (#8572) ([0e20949](https://github.com/vitejs/vite/commit/0e20949dbf0ba38bdaefbf32a36764fe29858e20)), closes [#8572](https://github.com/vitejs/vite/issues/8572)
+* chore: use `esno` to replace `ts-node` (#8162) ([c18a5f3](https://github.com/vitejs/vite/commit/c18a5f36410e418aaf8309102f1cacf7aef31b43)), closes [#8162](https://github.com/vitejs/vite/issues/8162)
+* chore(deps): update all non-major dependencies (#8474) ([6d0ede7](https://github.com/vitejs/vite/commit/6d0ede7c60aaa4c010207a047bf30a2b87b5049f)), closes [#8474](https://github.com/vitejs/vite/issues/8474)
+* refactor!: make terser an optional dependency (#8049) ([164f528](https://github.com/vitejs/vite/commit/164f528838f3a146c82d68992d38316b9214f9b8)), closes [#8049](https://github.com/vitejs/vite/issues/8049)
+* refactor(plugin-legacy): improve default polyfill (#8312) ([4370d91](https://github.com/vitejs/vite/commit/4370d9123da20c586938753d9f606d84907334c9)), closes [#8312](https://github.com/vitejs/vite/issues/8312)
+
+
+
+## 2.0.0-alpha.1 (2022-05-19)
+
+* fix: rewrite CJS specific funcs/vars in plugins (#8227) ([9baa70b](https://github.com/vitejs/vite/commit/9baa70b788ec0b0fc419db30d627567242c6af7d)), closes [#8227](https://github.com/vitejs/vite/issues/8227)
+* fix(plugin-legacy): fail to get the fileName (#5250) ([c7fc1d4](https://github.com/vitejs/vite/commit/c7fc1d4a532eae7b519bd70c6eba701e23b0635a)), closes [#5250](https://github.com/vitejs/vite/issues/5250)
+* build!: bump targets (#8045) ([66efd69](https://github.com/vitejs/vite/commit/66efd69a399fd73284cc7a3bffc904e154291a14)), closes [#8045](https://github.com/vitejs/vite/issues/8045)
+* feat!: relative base (#7644) ([09648c2](https://github.com/vitejs/vite/commit/09648c220a67852c38da0ba742501a15837e16c2)), closes [#7644](https://github.com/vitejs/vite/issues/7644)
+* docs: use latest core-js unpkg link (#8190) ([102b678](https://github.com/vitejs/vite/commit/102b678335ba74ac8f0ab94c8c49cba97e836e6d)), closes [#8190](https://github.com/vitejs/vite/issues/8190)
+
+
+
+## 2.0.0-alpha.0 (2022-05-13)
+
+* chore: bump minors and rebuild lock (#8074) ([aeb5b74](https://github.com/vitejs/vite/commit/aeb5b7436df5a4d7cf0ee1a9f6f110d00ef7aac1)), closes [#8074](https://github.com/vitejs/vite/issues/8074)
+* chore: revert vitejs/vite#8152 (#8161) ([85b8b55](https://github.com/vitejs/vite/commit/85b8b55c0d39f53581047f622717d4a009c594f6)), closes [vitejs/vite#8152](https://github.com/vitejs/vite/issues/8152) [#8161](https://github.com/vitejs/vite/issues/8161)
+* chore: update plugins peer deps ([d57c23c](https://github.com/vitejs/vite/commit/d57c23ca9b59491160017cea996fdbff4216263c))
+* chore: use `unbuild` to bundle plugins (#8139) ([638b168](https://github.com/vitejs/vite/commit/638b1686288ad685243d34cd9f1db3814f4db1c0)), closes [#8139](https://github.com/vitejs/vite/issues/8139)
+* chore(deps): use `esno` to replace `ts-node` (#8152) ([2363bd3](https://github.com/vitejs/vite/commit/2363bd3e5443aad43351ac16400b5a6ab7e0ef83)), closes [#8152](https://github.com/vitejs/vite/issues/8152)
+* build!: remove node v12 support (#7833) ([eeac2d2](https://github.com/vitejs/vite/commit/eeac2d2e217ddbca79d5b1dfde9bb5097e821b6a)), closes [#7833](https://github.com/vitejs/vite/issues/7833)
+* docs(plugin-legacy): remove regenerator-runtime note (#8007) ([834efe9](https://github.com/vitejs/vite/commit/834efe94fe2c26fcdeabcc34a667dcc6a52326ee)), closes [#8007](https://github.com/vitejs/vite/issues/8007)
+
+
+
+## 1.8.2 (2022-05-02)
+
+* chore(deps): update all non-major dependencies (#7780) ([eba9d05](https://github.com/vitejs/vite/commit/eba9d05d7adbb5d4dd25f14b085b15eb3488dfe4)), closes [#7780](https://github.com/vitejs/vite/issues/7780)
+* chore(deps): update all non-major dependencies (#7847) ([e29d1d9](https://github.com/vitejs/vite/commit/e29d1d92f7810c5160aac2f1e56f7b03bfa4c933)), closes [#7847](https://github.com/vitejs/vite/issues/7847)
+* chore(deps): update all non-major dependencies (#7949) ([b877d30](https://github.com/vitejs/vite/commit/b877d30a05691bb6ea2da4e67b931a5a3d32809f)), closes [#7949](https://github.com/vitejs/vite/issues/7949)
+* refactor(legacy): remove unneeded dynamic import var init code (#7759) ([12a4e7d](https://github.com/vitejs/vite/commit/12a4e7d8bbf06d35d6fcc0135dcb76fd06a57c22)), closes [#7759](https://github.com/vitejs/vite/issues/7759)
+
+
+
+## 1.8.1 (2022-04-13)
+
+* fix(deps): update all non-major dependencies (#7668) ([485263c](https://github.com/vitejs/vite/commit/485263cdca78e5b30fce77f1af9862b3ea3d76f1)), closes [#7668](https://github.com/vitejs/vite/issues/7668)
+* docs(legacy): note works in build only (#7596) ([f26b14a](https://github.com/vitejs/vite/commit/f26b14a0d8f4c909cb8cf3188684333b488c0714)), closes [#7596](https://github.com/vitejs/vite/issues/7596)
+
+
+
+## 1.8.0 (2022-03-30)
+
+* fix(deps): update all non-major dependencies (#6782) ([e38be3e](https://github.com/vitejs/vite/commit/e38be3e6ca7bf79319d5d7188e1d347b1d6091ef)), closes [#6782](https://github.com/vitejs/vite/issues/6782)
+* fix(deps): update all non-major dependencies (#7392) ([b63fc3b](https://github.com/vitejs/vite/commit/b63fc3bbdaf59358b89a0844c264deea1b25c034)), closes [#7392](https://github.com/vitejs/vite/issues/7392)
+* fix(plugin-legacy): always fallback legacy build when CSP (#6535) ([a118a1d](https://github.com/vitejs/vite/commit/a118a1d98c63028ddc8b2b3389b8cfa58d771e76)), closes [#6535](https://github.com/vitejs/vite/issues/6535)
+* fix(plugin-legacy): polyfill latest features (#7514) ([cb388e2](https://github.com/vitejs/vite/commit/cb388e2dfd39fab751d0656a811c39f8440c48e2)), closes [#7514](https://github.com/vitejs/vite/issues/7514)
+* fix(plugin-legacy): require Vite 2.8.0 (#6272) (#6869) ([997b8f1](https://github.com/vitejs/vite/commit/997b8f11cb156cc374ae991875a09534b5489a93)), closes [#6272](https://github.com/vitejs/vite/issues/6272) [#6869](https://github.com/vitejs/vite/issues/6869)
+* chore(deps): update all non-major dependencies (#6905) ([839665c](https://github.com/vitejs/vite/commit/839665c5985101c1765f0d68cf429ac96157d062)), closes [#6905](https://github.com/vitejs/vite/issues/6905)
+* docs(vite-legacy): Note about using `regenerator-runtime` in Content Security Policy environment (#7 ([0fd6422](https://github.com/vitejs/vite/commit/0fd64223304442bb483c55d818fcf808b7ffbaa8)), closes [#7234](https://github.com/vitejs/vite/issues/7234)
+* workflow: separate version bumping and publishing on release (#6879) ([fe8ef39](https://github.com/vitejs/vite/commit/fe8ef39eb37df7565dbbfce6a09c2eb7ceeaa56e)), closes [#6879](https://github.com/vitejs/vite/issues/6879)
+* release: plugin-legacy@1.7.1 ([19a58dd](https://github.com/vitejs/vite/commit/19a58dd320e9dd582bb7868e1621d8bde835eda6))
+
+
+
## [1.7.1](https://github.com/vitejs/vite/compare/plugin-legacy@1.7.0...plugin-legacy@1.7.1) (2022-02-11)
### Bug Fixes
diff --git a/packages/plugin-legacy/LICENSE b/packages/plugin-legacy/LICENSE
new file mode 100644
index 00000000000000..b7e97ecb6aa4dd
--- /dev/null
+++ b/packages/plugin-legacy/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019-present, VoidZero Inc. and Vite contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/plugin-legacy/README.md b/packages/plugin-legacy/README.md
index b5bf572e53fa2c..ad90980609a285 100644
--- a/packages/plugin-legacy/README.md
+++ b/packages/plugin-legacy/README.md
@@ -1,8 +1,6 @@
# @vitejs/plugin-legacy [](https://npmjs.com/package/@vitejs/plugin-legacy)
-**Note: this plugin requires `vite@^2.0.0`**.
-
-Vite's default browser support baseline is [Native ESM](https://caniuse.com/es6-module). This plugin provides support for legacy browsers that do not support native ESM.
+Vite's default browser support baseline is [Native ESM](https://caniuse.com/es6-module), [native ESM dynamic import](https://caniuse.com/es6-module-dynamic-import), and [`import.meta`](https://caniuse.com/mdn-javascript_operators_import_meta). This plugin provides support for legacy browsers that do not support those features when building for production.
By default, this plugin will:
@@ -10,7 +8,7 @@ By default, this plugin will:
- Generate a polyfill chunk including SystemJS runtime, and any necessary polyfills determined by specified browser targets and **actual usage** in the bundle.
-- Inject `
diff --git a/packages/vite/src/node/__tests__/packages/child/index.js b/packages/vite/src/node/__tests__/packages/child/index.js
new file mode 100644
index 00000000000000..186b120756be19
--- /dev/null
+++ b/packages/vite/src/node/__tests__/packages/child/index.js
@@ -0,0 +1 @@
+export default true
diff --git a/packages/vite/src/node/__tests__/packages/child/package.json b/packages/vite/src/node/__tests__/packages/child/package.json
new file mode 100644
index 00000000000000..77e2aa64615b63
--- /dev/null
+++ b/packages/vite/src/node/__tests__/packages/child/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vitejs/child",
+ "type": "module",
+ "main": "./index.js"
+}
diff --git a/packages/vite/src/node/__tests__/packages/module/package.json b/packages/vite/src/node/__tests__/packages/module/package.json
new file mode 100644
index 00000000000000..67756e1d2c410e
--- /dev/null
+++ b/packages/vite/src/node/__tests__/packages/module/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "mylib",
+ "type": "module"
+}
diff --git a/packages/vite/src/node/__tests__/packages/package.json b/packages/vite/src/node/__tests__/packages/package.json
new file mode 100644
index 00000000000000..bd6442dcacf7c9
--- /dev/null
+++ b/packages/vite/src/node/__tests__/packages/package.json
@@ -0,0 +1,3 @@
+{
+ "name": "named-testing-package"
+}
diff --git a/packages/vite/src/node/__tests__/packages/parent/index.ts b/packages/vite/src/node/__tests__/packages/parent/index.ts
new file mode 100644
index 00000000000000..747305283cadb2
--- /dev/null
+++ b/packages/vite/src/node/__tests__/packages/parent/index.ts
@@ -0,0 +1,6 @@
+// @ts-expect-error not typed
+import child from '@vitejs/child'
+
+export default {
+ child,
+}
diff --git a/packages/vite/src/node/__tests__/packages/parent/package.json b/packages/vite/src/node/__tests__/packages/parent/package.json
new file mode 100644
index 00000000000000..d966448a0560a8
--- /dev/null
+++ b/packages/vite/src/node/__tests__/packages/parent/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@vitejs/parent",
+ "type": "module",
+ "main": "./index.ts",
+ "dependencies": {
+ "@vitejs/child": "link:../child"
+ }
+}
diff --git a/packages/vite/src/node/__tests__/plugins/assetImportMetaUrl.spec.ts b/packages/vite/src/node/__tests__/plugins/assetImportMetaUrl.spec.ts
new file mode 100644
index 00000000000000..38355b38fe6b31
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/assetImportMetaUrl.spec.ts
@@ -0,0 +1,65 @@
+import { describe, expect, test } from 'vitest'
+import { parseAst } from 'rollup/parseAst'
+import { assetImportMetaUrlPlugin } from '../../plugins/assetImportMetaUrl'
+import { resolveConfig } from '../../config'
+import { PartialEnvironment } from '../../baseEnvironment'
+
+async function createAssetImportMetaurlPluginTransform() {
+ const config = await resolveConfig({ configFile: false }, 'serve')
+ const instance = assetImportMetaUrlPlugin(config)
+ const environment = new PartialEnvironment('client', config)
+
+ return async (code: string) => {
+ // @ts-expect-error transform.handler should exist
+ const result = await instance.transform.handler.call(
+ { environment, parse: parseAst },
+ code,
+ 'foo.ts',
+ )
+ return result?.code || result
+ }
+}
+
+describe('assetImportMetaUrlPlugin', async () => {
+ const transform = await createAssetImportMetaurlPluginTransform()
+
+ test('variable between /', async () => {
+ expect(
+ await transform('new URL(`./foo/${dir}/index.js`, import.meta.url)'),
+ ).toMatchInlineSnapshot(
+ `"new URL((import.meta.glob("./foo/*/index.js", {"eager":true,"import":"default","query":"?url"}))[\`./foo/\${dir}/index.js\`], import.meta.url)"`,
+ )
+ })
+
+ test('variable before non-/', async () => {
+ expect(
+ await transform('new URL(`./foo/${dir}.js`, import.meta.url)'),
+ ).toMatchInlineSnapshot(
+ `"new URL((import.meta.glob("./foo/*.js", {"eager":true,"import":"default","query":"?url"}))[\`./foo/\${dir}.js\`], import.meta.url)"`,
+ )
+ })
+
+ test('two variables', async () => {
+ expect(
+ await transform('new URL(`./foo/${dir}${file}.js`, import.meta.url)'),
+ ).toMatchInlineSnapshot(
+ `"new URL((import.meta.glob("./foo/*.js", {"eager":true,"import":"default","query":"?url"}))[\`./foo/\${dir}\${file}.js\`], import.meta.url)"`,
+ )
+ })
+
+ test('two variables between /', async () => {
+ expect(
+ await transform(
+ 'new URL(`./foo/${dir}${dir2}/index.js`, import.meta.url)',
+ ),
+ ).toMatchInlineSnapshot(
+ `"new URL((import.meta.glob("./foo/*/index.js", {"eager":true,"import":"default","query":"?url"}))[\`./foo/\${dir}\${dir2}/index.js\`], import.meta.url)"`,
+ )
+ })
+
+ test('ignore starting with a variable', async () => {
+ expect(
+ await transform('new URL(`${file}.js`, import.meta.url)'),
+ ).toMatchInlineSnapshot(`"new URL(\`\${file}.js\`, import.meta.url)"`)
+ })
+})
diff --git a/packages/vite/src/node/__tests__/plugins/css.spec.ts b/packages/vite/src/node/__tests__/plugins/css.spec.ts
index 539ec2f1af1810..380358d875d090 100644
--- a/packages/vite/src/node/__tests__/plugins/css.spec.ts
+++ b/packages/vite/src/node/__tests__/plugins/css.spec.ts
@@ -1,18 +1,31 @@
-import { cssUrlRE, cssPlugin } from '../../plugins/css'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { describe, expect, test } from 'vitest'
import { resolveConfig } from '../../config'
-import fs from 'fs'
-import path from 'path'
+import type { InlineConfig } from '../../config'
+import {
+ convertTargets,
+ cssPlugin,
+ cssUrlRE,
+ getEmptyChunkReplacer,
+ hoistAtRules,
+ preprocessCSS,
+ resolveLibCssFilename,
+} from '../../plugins/css'
+import { PartialEnvironment } from '../../baseEnvironment'
+
+const __dirname = path.resolve(fileURLToPath(import.meta.url), '..')
describe('search css url function', () => {
test('some spaces before it', () => {
expect(
- cssUrlRE.test("list-style-image: url('../images/bullet.jpg');")
+ cssUrlRE.test("list-style-image: url('../images/bullet.jpg');"),
).toBe(true)
})
test('no space after colon', () => {
expect(cssUrlRE.test("list-style-image:url('../images/bullet.jpg');")).toBe(
- true
+ true,
)
})
@@ -24,93 +37,418 @@ describe('search css url function', () => {
expect(
cssUrlRE.test(`@function svg-url($string) {
@return "";
- }`)
+ }`),
).toBe(false)
})
test('after parenthesis', () => {
expect(
cssUrlRE.test(
- 'mask-image: image(url(mask.png), skyblue, linear-gradient(rgba(0, 0, 0, 1.0), transparent));'
- )
+ 'mask-image: image(url(mask.png), skyblue, linear-gradient(rgba(0, 0, 0, 1.0), transparent));',
+ ),
).toBe(true)
})
test('after comma', () => {
expect(
cssUrlRE.test(
- 'mask-image: image(skyblue,url(mask.png), linear-gradient(rgba(0, 0, 0, 1.0), transparent));'
- )
+ 'mask-image: image(skyblue,url(mask.png), linear-gradient(rgba(0, 0, 0, 1.0), transparent));',
+ ),
).toBe(true)
})
})
-describe('css path resolutions', () => {
- const mockedProjectPath = path.join(process.cwd(), '/foo/bar/project')
- const mockedBarCssRelativePath = '/css/bar.module.css'
- const mockedFooCssRelativePath = '/css/foo.module.css'
+describe('css modules', () => {
+ test('css module compose/from path resolutions', async () => {
+ const { transform } = await createCssPluginTransform({
+ configFile: false,
+ resolve: {
+ alias: [
+ {
+ find: '@',
+ replacement: path.join(
+ import.meta.dirname,
+ './fixtures/css-module-compose',
+ ),
+ },
+ ],
+ },
+ })
- test('cssmodule compose/from path resolutions', async () => {
- const config = await resolveConfig(
- {
- resolve: {
- alias: [
- {
- find: '@',
- replacement: mockedProjectPath
- }
- ]
- }
+ const result = await transform(
+ `\
+.foo {
+position: fixed;
+composes: bar from '@/css/bar.module.css';
+}`,
+ '/css/foo.module.css',
+ )
+
+ expect(result.code).toMatchInlineSnapshot(
+ `
+ "._bar_1b4ow_1 {
+ display: block;
+ background: #f0f;
+ }
+ ._foo_86148_1 {
+ position: fixed;
+ }"
+ `,
+ )
+ })
+
+ test('custom generateScopedName', async () => {
+ const { transform } = await createCssPluginTransform({
+ configFile: false,
+ css: {
+ modules: {
+ generateScopedName: 'custom__[hash:base64:5]',
+ },
+ },
+ })
+ const css = `\
+.foo {
+ color: red;
+}`
+ const result1 = await transform(css, '/foo.module.css') // server
+ const result2 = await transform(css, '/foo.module.css?direct') // client
+ expect(result1.code).toBe(result2.code)
+ })
+
+ test('custom generateScopedName with lightningcss', async () => {
+ const { transform } = await createCssPluginTransform({
+ configFile: false,
+ css: {
+ modules: {
+ generateScopedName: 'custom__[hash:base64:5]',
+ },
+ transformer: 'lightningcss',
},
- 'serve'
+ })
+ const css = `\
+.foo {
+ color: red;
+}`
+ const result1 = await transform(css, '/foo.module.css') // server
+ const result2 = await transform(css, '/foo.module.css?direct') // client
+ expect(result1.code).toBe(result2.code)
+ })
+})
+
+describe('hoist @ rules', () => {
+ test('hoist @import', async () => {
+ const css = `.foo{color:red;}@import "bla";`
+ const result = await hoistAtRules(css)
+ expect(result).toBe(`@import "bla";.foo{color:red;}`)
+ })
+
+ test('hoist @import url with semicolon', async () => {
+ const css = `.foo{color:red;}@import url("bla;bla");`
+ const result = await hoistAtRules(css)
+ expect(result).toBe(`@import url("bla;bla");.foo{color:red;}`)
+ })
+
+ test('hoist @import url data with semicolon', async () => {
+ const css = `.foo{color:red;}@import url(data:image/png;base64,iRxVB0);`
+ const result = await hoistAtRules(css)
+ expect(result).toBe(
+ `@import url(data:image/png;base64,iRxVB0);.foo{color:red;}`,
)
+ })
- const { transform, buildStart } = cssPlugin(config)
+ test('hoist @import with semicolon in quotes', async () => {
+ const css = `.foo{color:red;}@import "bla;bar";`
+ const result = await hoistAtRules(css)
+ expect(result).toBe(`@import "bla;bar";.foo{color:red;}`)
+ })
- await buildStart.call({})
+ test('hoist @charset', async () => {
+ const css = `.foo{color:red;}@charset "utf-8";`
+ const result = await hoistAtRules(css)
+ expect(result).toBe(`@charset "utf-8";.foo{color:red;}`)
+ })
- const mockFs = jest
- .spyOn(fs, 'readFile')
- // @ts-ignore jest.spyOn not recognize overrided `fs.readFile` definition.
- .mockImplementationOnce((p, encoding, callback) => {
- expect(p).toBe(path.join(mockedProjectPath, mockedBarCssRelativePath))
- expect(encoding).toBe('utf-8')
- callback(
- null,
- Buffer.from(`
-.bar {
- display: block;
- background: #f0f;
+ test('hoist one @charset only', async () => {
+ const css = `.foo{color:red;}@charset "utf-8";@charset "utf-8";`
+ const result = await hoistAtRules(css)
+ expect(result).toBe(`@charset "utf-8";.foo{color:red;}`)
+ })
+
+ test('hoist @import and @charset', async () => {
+ const css = `.foo{color:red;}@import "bla";@charset "utf-8";.bar{color:green;}@import "baz";`
+ const result = await hoistAtRules(css)
+ expect(result).toBe(
+ `@charset "utf-8";@import "bla";@import "baz";.foo{color:red;}.bar{color:green;}`,
+ )
+ })
+
+ test('dont hoist @import in comments', async () => {
+ const css = `.foo{color:red;}/* @import "bla"; */@import "bar";`
+ const result = await hoistAtRules(css)
+ expect(result).toBe(`@import "bar";.foo{color:red;}/* @import "bla"; */`)
+ })
+
+ test('dont hoist @charset in comments', async () => {
+ const css = `.foo{color:red;}/* @charset "utf-8"; */@charset "utf-8";`
+ const result = await hoistAtRules(css)
+ expect(result).toBe(
+ `@charset "utf-8";.foo{color:red;}/* @charset "utf-8"; */`,
+ )
+ })
+
+ test('dont hoist @import and @charset in comments', async () => {
+ const css = `
+.foo{color:red;}
+/*
+ @import "bla";
+*/
+@charset "utf-8";
+/*
+ @charset "utf-8";
+ @import "bar";
+*/
+@import "baz";`
+ const result = await hoistAtRules(css)
+ expect(result).toMatchInlineSnapshot(`
+ "@charset "utf-8";@import "baz";
+ .foo{color:red;}
+ /*
+ @import "bla";
+ */
+
+ /*
+ @charset "utf-8";
+ @import "bar";
+ */
+ "
+ `)
+ })
+})
+
+async function createCssPluginTransform(inlineConfig: InlineConfig = {}) {
+ const config = await resolveConfig(inlineConfig, 'serve')
+ const environment = new PartialEnvironment('client', config)
+
+ const { transform, buildStart } = cssPlugin(config)
+
+ // @ts-expect-error buildStart is function
+ await buildStart.call({})
+
+ return {
+ async transform(code: string, id: string) {
+ // @ts-expect-error transform.handler is function
+ return await transform.handler.call(
+ {
+ addWatchFile() {
+ return
+ },
+ environment,
+ },
+ code,
+ id,
+ )
+ },
+ }
}
- `)
- )
- })
- const { code } = await transform.call(
+describe('convertTargets', () => {
+ test('basic cases', () => {
+ expect(convertTargets('es2018')).toStrictEqual({
+ chrome: 4128768,
+ edge: 5177344,
+ firefox: 3801088,
+ safari: 786432,
+ opera: 3276800,
+ })
+ expect(convertTargets(['safari13.1', 'ios13', 'node14'])).toStrictEqual({
+ ios_saf: 851968,
+ safari: 852224,
+ })
+ })
+})
+
+describe('getEmptyChunkReplacer', () => {
+ test('replaces import call', () => {
+ const code = `\
+import "some-module";
+import "pure_css_chunk.js";
+import "other-module";`
+
+ const replacer = getEmptyChunkReplacer(['pure_css_chunk.js'], 'es')
+ const replaced = replacer(code)
+ expect(replaced.length).toBe(code.length)
+ expect(replaced).toMatchInlineSnapshot(`
+ "import "some-module";
+ /* empty css */
+ import "other-module";"
+ `)
+ })
+
+ test('replaces import call without new lines', () => {
+ const code = `import "some-module";import "pure_css_chunk.js";import "other-module";`
+
+ const replacer = getEmptyChunkReplacer(['pure_css_chunk.js'], 'es')
+ const replaced = replacer(code)
+ expect(replaced.length).toBe(code.length)
+ expect(replaced).toMatchInlineSnapshot(
+ `"import "some-module";/* empty css */import "other-module";"`,
+ )
+ })
+
+ test('replaces require call', () => {
+ const code = `\
+require("some-module");
+require("pure_css_chunk.js");
+require("other-module");`
+
+ const replacer = getEmptyChunkReplacer(['pure_css_chunk.js'], 'cjs')
+ const replaced = replacer(code)
+ expect(replaced.length).toBe(code.length)
+ expect(replaced).toMatchInlineSnapshot(`
+ "require("some-module");
+ ;/* empty css */
+ require("other-module");"
+ `)
+ })
+
+ test('replaces require call in minified code without new lines', () => {
+ const code = `require("some-module");require("pure_css_chunk.js");require("other-module");`
+
+ const replacer = getEmptyChunkReplacer(['pure_css_chunk.js'], 'cjs')
+ const replaced = replacer(code)
+ expect(replaced.length).toBe(code.length)
+ expect(replaced).toMatchInlineSnapshot(
+ `"require("some-module");;/* empty css */require("other-module");"`,
+ )
+ })
+
+ test('replaces require call in minified code that uses comma operator', () => {
+ const code =
+ 'require("some-module"),require("pure_css_chunk.js"),require("other-module");'
+
+ const replacer = getEmptyChunkReplacer(['pure_css_chunk.js'], 'cjs')
+ const newCode = replacer(code)
+ expect(newCode.length).toBe(code.length)
+ expect(newCode).toMatchInlineSnapshot(
+ `"require("some-module"),/* empty css */require("other-module");"`,
+ )
+ // So there should be no pure css chunk anymore
+ expect(newCode).not.toContain('pure_css_chunk.js')
+ })
+
+ test('replaces require call in minified code that uses comma operator 2', () => {
+ const code = 'require("pure_css_chunk.js"),console.log();'
+ const replacer = getEmptyChunkReplacer(['pure_css_chunk.js'], 'cjs')
+ const newCode = replacer(code)
+ expect(newCode.length).toBe(code.length)
+ expect(newCode).toMatchInlineSnapshot(
+ `"/* empty css */console.log();"`,
+ )
+ expect(newCode).not.toContain('pure_css_chunk.js')
+ })
+
+ test('replaces require call in minified code that uses comma operator followed by assignment', () => {
+ const code =
+ 'require("some-module"),require("pure_css_chunk.js");const v=require("other-module");'
+
+ const replacer = getEmptyChunkReplacer(['pure_css_chunk.js'], 'cjs')
+ const newCode = replacer(code)
+ expect(newCode.length).toBe(code.length)
+ expect(newCode).toMatchInlineSnapshot(
+ `"require("some-module");/* empty css */const v=require("other-module");"`,
+ )
+ expect(newCode).not.toContain('pure_css_chunk.js')
+ })
+})
+
+describe('preprocessCSS', () => {
+ test('works', async () => {
+ const resolvedConfig = await resolveConfig({ configFile: false }, 'serve')
+ const result = await preprocessCSS(
+ `\
+.foo {
+ color:red;
+ background: url(./foo.png);
+}`,
+ 'foo.css',
+ resolvedConfig,
+ )
+ expect(result.code).toMatchInlineSnapshot(`
+ ".foo {
+ color:red;
+ background: url(./foo.png);
+ }"
+ `)
+ })
+
+ test('works with lightningcss', async () => {
+ const resolvedConfig = await resolveConfig(
{
- addWatchFile() {
- return
- }
+ configFile: false,
+ css: { transformer: 'lightningcss' },
},
- `
+ 'serve',
+ )
+ const result = await preprocessCSS(
+ `\
.foo {
- position: fixed;
- composes: bar from '@${mockedBarCssRelativePath}';
-}
- `,
- path.join(mockedProjectPath, mockedFooCssRelativePath)
+ color: red;
+ background: url(./foo.png);
+}`,
+ 'foo.css',
+ resolvedConfig,
)
-
- expect(code).toBe(`
-._bar_soicv_2 {
- display: block;
- background: #f0f;
-}
-._foo_sctn3_2 {
- position: fixed;
-}
+ expect(result.code).toMatchInlineSnapshot(`
+ ".foo {
+ color: red;
+ background: url("./foo.png");
+ }
+ "
`)
+ })
+})
+
+describe('resolveLibCssFilename', () => {
+ test('use name from package.json', () => {
+ const filename = resolveLibCssFilename(
+ {
+ entry: 'mylib.js',
+ },
+ path.resolve(__dirname, '../packages/name'),
+ )
+ expect(filename).toBe('mylib.css')
+ })
- mockFs.mockReset()
+ test('set cssFileName', () => {
+ const filename = resolveLibCssFilename(
+ {
+ entry: 'mylib.js',
+ cssFileName: 'style',
+ },
+ path.resolve(__dirname, '../packages/noname'),
+ )
+ expect(filename).toBe('style.css')
+ })
+
+ test('use fileName if set', () => {
+ const filename = resolveLibCssFilename(
+ {
+ entry: 'mylib.js',
+ fileName: 'custom-name',
+ },
+ path.resolve(__dirname, '../packages/name'),
+ )
+ expect(filename).toBe('custom-name.css')
+ })
+
+ test('use fileName if set and has array entry', () => {
+ const filename = resolveLibCssFilename(
+ {
+ entry: ['mylib.js', 'mylib2.js'],
+ fileName: 'custom-name',
+ },
+ path.resolve(__dirname, '../packages/name'),
+ )
+ expect(filename).toBe('custom-name.css')
})
})
diff --git a/packages/vite/src/node/__tests__/plugins/define.spec.ts b/packages/vite/src/node/__tests__/plugins/define.spec.ts
index 9a65f8f3a51cea..ca88a177ff7343 100644
--- a/packages/vite/src/node/__tests__/plugins/define.spec.ts
+++ b/packages/vite/src/node/__tests__/plugins/define.spec.ts
@@ -1,15 +1,27 @@
+import { describe, expect, test } from 'vitest'
import { definePlugin } from '../../plugins/define'
import { resolveConfig } from '../../config'
+import { PartialEnvironment } from '../../baseEnvironment'
async function createDefinePluginTransform(
define: Record = {},
build = true,
- ssr = false
+ ssr = false,
) {
- const config = await resolveConfig({ define }, build ? 'build' : 'serve')
+ const config = await resolveConfig(
+ { configFile: false, define },
+ build ? 'build' : 'serve',
+ )
const instance = definePlugin(config)
+ const environment = new PartialEnvironment(ssr ? 'ssr' : 'client', config)
+
return async (code: string) => {
- const result = await instance.transform.call({}, code, 'foo.ts', { ssr })
+ // @ts-expect-error transform.handler should exist
+ const result = await instance.transform.handler.call(
+ { environment },
+ code,
+ 'foo.ts',
+ )
return result?.code || result
}
}
@@ -17,23 +29,118 @@ async function createDefinePluginTransform(
describe('definePlugin', () => {
test('replaces custom define', async () => {
const transform = await createDefinePluginTransform({
- __APP_VERSION__: JSON.stringify('1.0')
+ __APP_VERSION__: JSON.stringify('1.0'),
})
expect(await transform('const version = __APP_VERSION__ ;')).toBe(
- 'const version = "1.0" ;'
+ 'const version = "1.0";\n',
)
expect(await transform('const version = __APP_VERSION__;')).toBe(
- 'const version = "1.0";'
+ 'const version = "1.0";\n',
+ )
+ })
+
+ test('should not replace if not defined', async () => {
+ const transform = await createDefinePluginTransform({
+ __APP_VERSION__: JSON.stringify('1.0'),
+ })
+ expect(await transform('const version = "1.0";')).toBe(undefined)
+ expect(await transform('const version = import.meta.SOMETHING')).toBe(
+ undefined,
)
})
test('replaces import.meta.env.SSR with false', async () => {
const transform = await createDefinePluginTransform()
- expect(await transform('const isSSR = import.meta.env.SSR ;')).toBe(
- 'const isSSR = false ;'
- )
expect(await transform('const isSSR = import.meta.env.SSR;')).toBe(
- 'const isSSR = false;'
+ 'const isSSR = false;\n',
+ )
+ })
+
+ test('preserve import.meta.hot with override', async () => {
+ // assert that the default behavior is to replace import.meta.hot with undefined
+ const transform = await createDefinePluginTransform()
+ expect(await transform('const hot = import.meta.hot;')).toBe(
+ 'const hot = void 0;\n',
+ )
+ // assert that we can specify a user define to preserve import.meta.hot
+ const overrideTransform = await createDefinePluginTransform({
+ 'import.meta.hot': 'import.meta.hot',
+ })
+ expect(await overrideTransform('const hot = import.meta.hot;')).toBe(
+ 'const hot = import.meta.hot;\n',
+ )
+ })
+
+ test('replace import.meta.env.UNKNOWN with undefined', async () => {
+ const transform = await createDefinePluginTransform()
+ expect(await transform('const foo = import.meta.env.UNKNOWN;')).toBe(
+ 'const foo = undefined ;\n',
+ )
+ })
+
+ test('leave import.meta.env["UNKNOWN"] to runtime', async () => {
+ const transform = await createDefinePluginTransform()
+ expect(await transform('const foo = import.meta.env["UNKNOWN"];')).toMatch(
+ /const __vite_import_meta_env__ = .*;\nconst foo = __vite_import_meta_env__\["UNKNOWN"\];/,
+ )
+ })
+
+ test('preserve import.meta.env.UNKNOWN with override', async () => {
+ const transform = await createDefinePluginTransform({
+ 'import.meta.env.UNKNOWN': 'import.meta.env.UNKNOWN',
+ })
+ expect(await transform('const foo = import.meta.env.UNKNOWN;')).toBe(
+ 'const foo = import.meta.env.UNKNOWN;\n',
+ )
+ })
+
+ test('replace import.meta.env when it is a invalid json', async () => {
+ const transform = await createDefinePluginTransform({
+ 'import.meta.env.LEGACY': '__VITE_IS_LEGACY__',
+ })
+
+ expect(
+ await transform(
+ 'const isLegacy = import.meta.env.LEGACY;\nimport.meta.env.UNDEFINED && console.log(import.meta.env.UNDEFINED);',
+ ),
+ ).toMatchInlineSnapshot(`
+ "const isLegacy = __VITE_IS_LEGACY__;
+ undefined && console.log(undefined );
+ "
+ `)
+ })
+
+ test('replace bare import.meta.env', async () => {
+ const transform = await createDefinePluginTransform()
+ expect(await transform('const env = import.meta.env;')).toMatch(
+ /const __vite_import_meta_env__ = .*;\nconst env = __vite_import_meta_env__;/,
+ )
+ })
+
+ test('already has marker', async () => {
+ const transform = await createDefinePluginTransform()
+ expect(
+ await transform(
+ 'console.log(__vite_import_meta_env__);\nconst env = import.meta.env;',
+ ),
+ ).toMatch(
+ /const __vite_import_meta_env__1 = .*;\nconsole.log\(__vite_import_meta_env__\);\nconst env = __vite_import_meta_env__1;/,
+ )
+
+ expect(
+ await transform(
+ 'console.log(__vite_import_meta_env__, __vite_import_meta_env__1);\n const env = import.meta.env;',
+ ),
+ ).toMatch(
+ /const __vite_import_meta_env__2 = .*;\nconsole.log\(__vite_import_meta_env__, __vite_import_meta_env__1\);\nconst env = __vite_import_meta_env__2;/,
+ )
+
+ expect(
+ await transform(
+ 'console.log(__vite_import_meta_env__);\nconst env = import.meta.env;\nconsole.log(import.meta.env.UNDEFINED);',
+ ),
+ ).toMatch(
+ /const __vite_import_meta_env__1 = .*;\nconsole.log\(__vite_import_meta_env__\);\nconst env = __vite_import_meta_env__1;\nconsole.log\(undefined {26}\);/,
)
})
})
diff --git a/packages/vite/src/node/__tests__/plugins/dynamicImportVar/__snapshots__/parse.spec.ts.snap b/packages/vite/src/node/__tests__/plugins/dynamicImportVar/__snapshots__/parse.spec.ts.snap
new file mode 100644
index 00000000000000..3d6a6910422d7c
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/dynamicImportVar/__snapshots__/parse.spec.ts.snap
@@ -0,0 +1,23 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`parse positives > ? in url 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob("./mo\\\\?ds/*.js", {"query":"?url","import":"*"})), \`./mo?ds/\${base ?? foo}.js\`)"`;
+
+exports[`parse positives > ? in variables 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob("./mods/*.js", {"query":"?raw","import":"*"})), \`./mods/\${base ?? foo}.js\`)"`;
+
+exports[`parse positives > ? in worker 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob("./mo\\\\?ds/*.js", {"query":"?worker","import":"*"})), \`./mo?ds/\${base ?? foo}.js\`)"`;
+
+exports[`parse positives > alias path 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob("./mods/*.js")), \`./mods/\${base}.js\`)"`;
+
+exports[`parse positives > alias path with multi ../ 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob("../../*.js")), \`../../\${base}.js\`)"`;
+
+exports[`parse positives > basic 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob("./mods/*.js")), \`./mods/\${base}.js\`)"`;
+
+exports[`parse positives > with ../ and itself 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob("../dynamicImportVar/*.js")), \`./\${name}.js\`)"`;
+
+exports[`parse positives > with multi ../ and itself 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob("../../plugins/dynamicImportVar/*.js")), \`./\${name}.js\`)"`;
+
+exports[`parse positives > with query 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob("./mods/*.js", {"query":"?foo=bar"})), \`./mods/\${base}.js\`)"`;
+
+exports[`parse positives > with query raw 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob("./mods/*.js", {"query":"?raw","import":"*"})), \`./mods/\${base}.js\`)"`;
+
+exports[`parse positives > with query url 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob("./mods/*.js", {"query":"?url","import":"*"})), \`./mods/\${base}.js\`)"`;
diff --git a/packages/vite/src/node/__tests__/plugins/dynamicImportVar/mods/hello.js b/packages/vite/src/node/__tests__/plugins/dynamicImportVar/mods/hello.js
new file mode 100644
index 00000000000000..67900ef0999962
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/dynamicImportVar/mods/hello.js
@@ -0,0 +1,3 @@
+export function hello() {
+ return 'hello'
+}
diff --git a/packages/vite/src/node/__tests__/plugins/dynamicImportVar/mods/hi.js b/packages/vite/src/node/__tests__/plugins/dynamicImportVar/mods/hi.js
new file mode 100644
index 00000000000000..45d3506803b2b6
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/dynamicImportVar/mods/hi.js
@@ -0,0 +1,3 @@
+export function hi() {
+ return 'hi'
+}
diff --git a/packages/vite/src/node/__tests__/plugins/dynamicImportVar/parse.spec.ts b/packages/vite/src/node/__tests__/plugins/dynamicImportVar/parse.spec.ts
new file mode 100644
index 00000000000000..8a67660258386a
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/dynamicImportVar/parse.spec.ts
@@ -0,0 +1,72 @@
+import { resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { describe, expect, it } from 'vitest'
+import { transformDynamicImport } from '../../../plugins/dynamicImportVars'
+import { normalizePath } from '../../../utils'
+import { isWindows } from '../../../../shared/utils'
+
+const __dirname = resolve(fileURLToPath(import.meta.url), '..')
+
+async function run(input: string) {
+ const { glob, rawPattern } =
+ (await transformDynamicImport(
+ input,
+ normalizePath(resolve(__dirname, 'index.js')),
+ (id) =>
+ id
+ .replace('@', resolve(__dirname, './mods/'))
+ .replace('#', resolve(__dirname, '../../')),
+ __dirname,
+ )) || {}
+ return `__variableDynamicImportRuntimeHelper(${glob}, \`${rawPattern}\`)`
+}
+
+describe('parse positives', () => {
+ it('basic', async () => {
+ expect(await run('`./mods/${base}.js`')).toMatchSnapshot()
+ })
+
+ it('alias path', async () => {
+ expect(await run('`@/${base}.js`')).toMatchSnapshot()
+ })
+
+ it('alias path with multi ../', async () => {
+ expect(await run('`#/${base}.js`')).toMatchSnapshot()
+ })
+
+ it('with query', async () => {
+ expect(await run('`./mods/${base}.js?foo=bar`')).toMatchSnapshot()
+ })
+
+ it('with query raw', async () => {
+ expect(await run('`./mods/${base}.js?raw`')).toMatchSnapshot()
+ })
+
+ it('with query url', async () => {
+ expect(await run('`./mods/${base}.js?url`')).toMatchSnapshot()
+ })
+
+ it('? in variables', async () => {
+ expect(await run('`./mods/${base ?? foo}.js?raw`')).toMatchSnapshot()
+ })
+
+ // ? is not escaped on windows (? cannot be used as a filename on windows)
+ it.skipIf(isWindows)('? in url', async () => {
+ expect(await run('`./mo?ds/${base ?? foo}.js?url`')).toMatchSnapshot()
+ })
+
+ // ? is not escaped on windows (? cannot be used as a filename on windows)
+ it.skipIf(isWindows)('? in worker', async () => {
+ expect(await run('`./mo?ds/${base ?? foo}.js?worker`')).toMatchSnapshot()
+ })
+
+ it('with ../ and itself', async () => {
+ expect(await run('`../dynamicImportVar/${name}.js`')).toMatchSnapshot()
+ })
+
+ it('with multi ../ and itself', async () => {
+ expect(
+ await run('`../../plugins/dynamicImportVar/${name}.js`'),
+ ).toMatchSnapshot()
+ })
+})
diff --git a/packages/vite/src/node/__tests__/plugins/esbuild.spec.ts b/packages/vite/src/node/__tests__/plugins/esbuild.spec.ts
new file mode 100644
index 00000000000000..936415f9c33826
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/esbuild.spec.ts
@@ -0,0 +1,402 @@
+import { describe, expect, test } from 'vitest'
+import type { ResolvedConfig, UserConfig } from '../../config'
+import {
+ resolveEsbuildTranspileOptions,
+ transformWithEsbuild,
+} from '../../plugins/esbuild'
+
+describe('resolveEsbuildTranspileOptions', () => {
+ test('resolve default', () => {
+ const options = resolveEsbuildTranspileOptions(
+ defineResolvedConfig({
+ build: {
+ target: 'es2020',
+ minify: 'esbuild',
+ },
+ esbuild: {
+ keepNames: true,
+ },
+ }),
+ 'es',
+ )
+ expect(options).toEqual({
+ charset: 'utf8',
+ loader: 'js',
+ target: 'es2020',
+ format: 'esm',
+ keepNames: true,
+ minify: true,
+ treeShaking: true,
+ supported: {
+ 'dynamic-import': true,
+ 'import-meta': true,
+ },
+ })
+ })
+
+ test('resolve esnext no minify', () => {
+ const options = resolveEsbuildTranspileOptions(
+ defineResolvedConfig({
+ build: {
+ target: 'esnext',
+ minify: false,
+ },
+ esbuild: {
+ keepNames: true,
+ },
+ }),
+ 'es',
+ )
+ expect(options).toEqual(null)
+ })
+
+ test('resolve specific minify options', () => {
+ const options = resolveEsbuildTranspileOptions(
+ defineResolvedConfig({
+ build: {
+ minify: 'esbuild',
+ },
+ esbuild: {
+ keepNames: true,
+ minifyIdentifiers: false,
+ },
+ }),
+ 'es',
+ )
+ expect(options).toEqual({
+ charset: 'utf8',
+ loader: 'js',
+ target: undefined,
+ format: 'esm',
+ keepNames: true,
+ minify: false,
+ minifyIdentifiers: false,
+ minifySyntax: true,
+ minifyWhitespace: true,
+ treeShaking: true,
+ supported: {
+ 'dynamic-import': true,
+ 'import-meta': true,
+ },
+ })
+ })
+
+ test('resolve no minify', () => {
+ const options = resolveEsbuildTranspileOptions(
+ defineResolvedConfig({
+ build: {
+ target: 'es2020',
+ minify: false,
+ },
+ esbuild: {
+ keepNames: true,
+ },
+ }),
+ 'es',
+ )
+ expect(options).toEqual({
+ charset: 'utf8',
+ loader: 'js',
+ target: 'es2020',
+ format: 'esm',
+ keepNames: true,
+ minify: false,
+ minifyIdentifiers: false,
+ minifySyntax: false,
+ minifyWhitespace: false,
+ treeShaking: false,
+ supported: {
+ 'dynamic-import': true,
+ 'import-meta': true,
+ },
+ })
+ })
+
+ test('resolve es lib', () => {
+ const options = resolveEsbuildTranspileOptions(
+ defineResolvedConfig({
+ build: {
+ minify: 'esbuild',
+ lib: {
+ entry: './somewhere.js',
+ },
+ },
+ esbuild: {
+ keepNames: true,
+ },
+ }),
+ 'es',
+ )
+ expect(options).toEqual({
+ charset: 'utf8',
+ loader: 'js',
+ target: undefined,
+ format: 'esm',
+ keepNames: true,
+ minify: false,
+ minifyIdentifiers: true,
+ minifySyntax: true,
+ minifyWhitespace: false,
+ treeShaking: true,
+ supported: {
+ 'dynamic-import': true,
+ 'import-meta': true,
+ },
+ })
+ })
+
+ test('resolve cjs lib', () => {
+ const options = resolveEsbuildTranspileOptions(
+ defineResolvedConfig({
+ build: {
+ minify: 'esbuild',
+ lib: {
+ entry: './somewhere.js',
+ },
+ },
+ esbuild: {
+ keepNames: true,
+ },
+ }),
+ 'cjs',
+ )
+ expect(options).toEqual({
+ charset: 'utf8',
+ loader: 'js',
+ target: undefined,
+ format: 'cjs',
+ keepNames: true,
+ minify: true,
+ treeShaking: true,
+ supported: {
+ 'dynamic-import': true,
+ 'import-meta': true,
+ },
+ })
+ })
+
+ test('resolve es lib with specific minify options', () => {
+ const options = resolveEsbuildTranspileOptions(
+ defineResolvedConfig({
+ build: {
+ minify: 'esbuild',
+ lib: {
+ entry: './somewhere.js',
+ },
+ },
+ esbuild: {
+ keepNames: true,
+ minifyIdentifiers: true,
+ minifyWhitespace: true,
+ },
+ }),
+ 'es',
+ )
+ expect(options).toEqual({
+ charset: 'utf8',
+ loader: 'js',
+ target: undefined,
+ format: 'esm',
+ keepNames: true,
+ minify: false,
+ minifyIdentifiers: true,
+ minifySyntax: true,
+ minifyWhitespace: false,
+ treeShaking: true,
+ supported: {
+ 'dynamic-import': true,
+ 'import-meta': true,
+ },
+ })
+ })
+
+ test('resolve cjs lib with specific minify options', () => {
+ const options = resolveEsbuildTranspileOptions(
+ defineResolvedConfig({
+ build: {
+ minify: 'esbuild',
+ lib: {
+ entry: './somewhere.js',
+ },
+ },
+ esbuild: {
+ keepNames: true,
+ minifyIdentifiers: true,
+ minifySyntax: false,
+ treeShaking: true,
+ },
+ }),
+ 'cjs',
+ )
+ expect(options).toEqual({
+ charset: 'utf8',
+ loader: 'js',
+ target: undefined,
+ format: 'cjs',
+ keepNames: true,
+ minify: false,
+ minifyIdentifiers: true,
+ minifySyntax: false,
+ minifyWhitespace: true,
+ treeShaking: true,
+ supported: {
+ 'dynamic-import': true,
+ 'import-meta': true,
+ },
+ })
+ })
+})
+
+describe('transformWithEsbuild', () => {
+ test('not throw on inline sourcemap', async () => {
+ const result = await transformWithEsbuild(`const foo = 'bar'`, '', {
+ sourcemap: 'inline',
+ })
+ expect(result?.code).toBeTruthy()
+ expect(result?.map).toBeTruthy()
+ })
+
+ test('correctly overrides TS configuration and applies automatic transform', async () => {
+ const jsxImportSource = 'bar'
+ const result = await transformWithEsbuild(
+ 'const foo = () => <>>',
+ 'baz.jsx',
+ {
+ tsconfigRaw: {
+ compilerOptions: {
+ jsx: 'preserve',
+ },
+ },
+ jsx: 'automatic',
+ jsxImportSource,
+ },
+ )
+ expect(result?.code).toContain(`${jsxImportSource}/jsx-runtime`)
+ expect(result?.code).toContain('/* @__PURE__ */')
+ })
+
+ test('correctly overrides TS configuration and preserves code', async () => {
+ const foo = 'const foo = () => <>>'
+ const result = await transformWithEsbuild(foo, 'baz.jsx', {
+ tsconfigRaw: {
+ compilerOptions: {
+ jsx: 'react-jsx',
+ },
+ },
+ jsx: 'preserve',
+ })
+ expect(result?.code).toContain(foo)
+ })
+
+ test('correctly overrides TS configuration and transforms code', async () => {
+ const jsxFactory = 'h',
+ jsxFragment = 'bar'
+ const result = await transformWithEsbuild(
+ 'const foo = () => <>>',
+ 'baz.jsx',
+ {
+ tsconfigRaw: {
+ compilerOptions: {
+ jsxFactory: 'g',
+ jsxFragmentFactory: 'foo',
+ jsxImportSource: 'baz',
+ },
+ },
+ jsx: 'transform',
+ jsxFactory,
+ jsxFragment,
+ },
+ )
+ expect(result?.code).toContain(
+ `/* @__PURE__ */ ${jsxFactory}(${jsxFragment}, null)`,
+ )
+ })
+
+ describe('useDefineForClassFields', async () => {
+ const transformClassCode = async (
+ target: string,
+ tsconfigCompilerOptions: {
+ target?: string
+ useDefineForClassFields?: boolean
+ },
+ ) => {
+ const result = await transformWithEsbuild(
+ `
+ class foo {
+ bar = 'bar'
+ }
+ `,
+ 'bar.ts',
+ {
+ target,
+ tsconfigRaw: { compilerOptions: tsconfigCompilerOptions },
+ },
+ )
+ return result?.code
+ }
+
+ const [
+ defineForClassFieldsTrueTransformedCode,
+ defineForClassFieldsTrueLowerTransformedCode,
+ defineForClassFieldsFalseTransformedCode,
+ ] = await Promise.all([
+ transformClassCode('esnext', {
+ useDefineForClassFields: true,
+ }),
+ transformClassCode('es2021', {
+ useDefineForClassFields: true,
+ }),
+ transformClassCode('esnext', {
+ useDefineForClassFields: false,
+ }),
+ ])
+
+ test('target: esnext and tsconfig.target: esnext => true', async () => {
+ const actual = await transformClassCode('esnext', {
+ target: 'esnext',
+ })
+ expect(actual).toBe(defineForClassFieldsTrueTransformedCode)
+ })
+
+ test('target: es2021 and tsconfig.target: esnext => true', async () => {
+ const actual = await transformClassCode('es2021', {
+ target: 'esnext',
+ })
+ expect(actual).toBe(defineForClassFieldsTrueLowerTransformedCode)
+ })
+
+ test('target: es2021 and tsconfig.target: es2021 => false', async () => {
+ const actual = await transformClassCode('es2021', {
+ target: 'es2021',
+ })
+ expect(actual).toBe(defineForClassFieldsFalseTransformedCode)
+ })
+
+ test('target: esnext and tsconfig.target: es2021 => false', async () => {
+ const actual = await transformClassCode('esnext', {
+ target: 'es2021',
+ })
+ expect(actual).toBe(defineForClassFieldsFalseTransformedCode)
+ })
+
+ test('target: es2022 and tsconfig.target: es2022 => true', async () => {
+ const actual = await transformClassCode('es2022', {
+ target: 'es2022',
+ })
+ expect(actual).toBe(defineForClassFieldsTrueTransformedCode)
+ })
+
+ test('target: es2022 and tsconfig.target: undefined => false', async () => {
+ const actual = await transformClassCode('es2022', {})
+ expect(actual).toBe(defineForClassFieldsFalseTransformedCode)
+ })
+ })
+})
+
+/**
+ * Helper for `resolveEsbuildTranspileOptions` to created resolved config with types.
+ * Note: The function only uses `build.target`, `build.minify` and `esbuild` options.
+ */
+function defineResolvedConfig(config: UserConfig): ResolvedConfig {
+ return config as any
+}
diff --git a/packages/vite/src/node/__tests__/plugins/fixtures/css-module-compose/css/bar.module.css b/packages/vite/src/node/__tests__/plugins/fixtures/css-module-compose/css/bar.module.css
new file mode 100644
index 00000000000000..fa163ddd5179cc
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/fixtures/css-module-compose/css/bar.module.css
@@ -0,0 +1,4 @@
+.bar {
+ display: block;
+ background: #f0f;
+}
diff --git a/packages/vite/src/node/__tests__/plugins/import.spec.ts b/packages/vite/src/node/__tests__/plugins/import.spec.ts
index f0341e81b50f3c..89fbd80d8ecdc1 100644
--- a/packages/vite/src/node/__tests__/plugins/import.spec.ts
+++ b/packages/vite/src/node/__tests__/plugins/import.spec.ts
@@ -1,30 +1,51 @@
+import { beforeEach, describe, expect, test, vi } from 'vitest'
import { transformCjsImport } from '../../plugins/importAnalysis'
describe('transformCjsImport', () => {
- const url = './node_modules/.vite/react.js'
+ const url = './node_modules/.vite/deps/react.js'
const rawUrl = 'react'
+ const config: any = {
+ command: 'serve',
+ logger: {
+ warn: vi.fn(),
+ },
+ }
+
+ beforeEach(() => {
+ config.logger.warn.mockClear()
+ })
test('import specifier', () => {
expect(
transformCjsImport(
- 'import { useState, Component } from "react"',
+ 'import { useState, Component, "👋" as fake } from "react"',
url,
rawUrl,
- 0
- )
+ 0,
+ '',
+ config,
+ ),
).toBe(
- 'import __vite__cjsImport0_react from "./node_modules/.vite/react.js"; ' +
+ 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' +
'const useState = __vite__cjsImport0_react["useState"]; ' +
- 'const Component = __vite__cjsImport0_react["Component"]'
+ 'const Component = __vite__cjsImport0_react["Component"]; ' +
+ 'const fake = __vite__cjsImport0_react["👋"]',
)
})
test('import default specifier', () => {
expect(
- transformCjsImport('import React from "react"', url, rawUrl, 0)
+ transformCjsImport(
+ 'import React from "react"',
+ url,
+ rawUrl,
+ 0,
+ '',
+ config,
+ ),
).toBe(
- 'import __vite__cjsImport0_react from "./node_modules/.vite/react.js"; ' +
- 'const React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react'
+ 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' +
+ 'const React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react',
)
expect(
@@ -32,70 +53,112 @@ describe('transformCjsImport', () => {
'import { default as React } from "react"',
url,
rawUrl,
- 0
- )
+ 0,
+ '',
+ config,
+ ),
).toBe(
- 'import __vite__cjsImport0_react from "./node_modules/.vite/react.js"; ' +
- 'const React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react'
+ 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' +
+ 'const React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react',
)
})
test('import all specifier', () => {
expect(
- transformCjsImport('import * as react from "react"', url, rawUrl, 0)
+ transformCjsImport(
+ 'import * as react from "react"',
+ url,
+ rawUrl,
+ 0,
+ '',
+ config,
+ ),
).toBe(
- 'import __vite__cjsImport0_react from "./node_modules/.vite/react.js"; ' +
- 'const react = __vite__cjsImport0_react'
+ 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' +
+ `const react = ((m) => m?.__esModule ? m : { ...typeof m === "object" && !Array.isArray(m) || typeof m === "function" ? m : {}, default: m })(__vite__cjsImport0_react)`,
)
})
test('export all specifier', () => {
- expect(transformCjsImport('export * from "react"', url, rawUrl, 0)).toBe(
- undefined
+ expect(
+ transformCjsImport(
+ 'export * from "react"',
+ url,
+ rawUrl,
+ 0,
+ 'modA',
+ config,
+ ),
+ ).toBe(undefined)
+
+ expect(config.logger.warn).toBeCalledWith(
+ expect.stringContaining(`export * from "react"\` in modA`),
)
expect(
- transformCjsImport('export * as react from "react"', url, rawUrl, 0)
+ transformCjsImport(
+ 'export * as react from "react"',
+ url,
+ rawUrl,
+ 0,
+ '',
+ config,
+ ),
).toBe(undefined)
+
+ expect(config.logger.warn).toBeCalledTimes(1)
})
test('export name specifier', () => {
expect(
transformCjsImport(
- 'export { useState, Component } from "react"',
+ 'export { useState, Component, "👋" } from "react"',
url,
rawUrl,
- 0
- )
+ 0,
+ '',
+ config,
+ ),
).toBe(
- 'import __vite__cjsImport0_react from "./node_modules/.vite/react.js"; ' +
- 'const useState = __vite__cjsImport0_react["useState"]; ' +
- 'const Component = __vite__cjsImport0_react["Component"]; ' +
- 'export { useState, Component }'
+ 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' +
+ 'const __vite__cjsExportI_useState = __vite__cjsImport0_react["useState"]; ' +
+ 'const __vite__cjsExportI_Component = __vite__cjsImport0_react["Component"]; ' +
+ 'const __vite__cjsExportL_1d0452e3 = __vite__cjsImport0_react["👋"]; ' +
+ 'export { __vite__cjsExportI_useState as useState, __vite__cjsExportI_Component as Component, __vite__cjsExportL_1d0452e3 as "👋" }',
)
expect(
transformCjsImport(
- 'export { useState as useStateAlias, Component as ComponentAlias } from "react"',
+ 'export { useState as useStateAlias, Component as ComponentAlias, "👋" as "👍" } from "react"',
url,
rawUrl,
- 0
- )
+ 0,
+ '',
+ config,
+ ),
).toBe(
- 'import __vite__cjsImport0_react from "./node_modules/.vite/react.js"; ' +
- 'const useStateAlias = __vite__cjsImport0_react["useState"]; ' +
- 'const ComponentAlias = __vite__cjsImport0_react["Component"]; ' +
- 'export { useStateAlias, ComponentAlias }'
+ 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' +
+ 'const __vite__cjsExportI_useStateAlias = __vite__cjsImport0_react["useState"]; ' +
+ 'const __vite__cjsExportI_ComponentAlias = __vite__cjsImport0_react["Component"]; ' +
+ 'const __vite__cjsExportL_5d57d39e = __vite__cjsImport0_react["👋"]; ' +
+ 'export { __vite__cjsExportI_useStateAlias as useStateAlias, __vite__cjsExportI_ComponentAlias as ComponentAlias, __vite__cjsExportL_5d57d39e as "👍" }',
)
})
test('export default specifier', () => {
expect(
- transformCjsImport('export { default } from "react"', url, rawUrl, 0)
+ transformCjsImport(
+ 'export { default } from "react"',
+ url,
+ rawUrl,
+ 0,
+ '',
+ config,
+ ),
).toBe(
- 'import __vite__cjsImport0_react from "./node_modules/.vite/react.js"; ' +
+ 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' +
'const __vite__cjsExportDefault_0 = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react; ' +
- 'export default __vite__cjsExportDefault_0'
+ 'export default __vite__cjsExportDefault_0',
)
expect(
@@ -103,12 +166,14 @@ describe('transformCjsImport', () => {
'export { default as React} from "react"',
url,
rawUrl,
- 0
- )
+ 0,
+ '',
+ config,
+ ),
).toBe(
- 'import __vite__cjsImport0_react from "./node_modules/.vite/react.js"; ' +
- 'const React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react; ' +
- 'export { React }'
+ 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' +
+ 'const __vite__cjsExportI_React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react; ' +
+ 'export { __vite__cjsExportI_React as React }',
)
expect(
@@ -116,12 +181,14 @@ describe('transformCjsImport', () => {
'export { Component as default } from "react"',
url,
rawUrl,
- 0
- )
+ 0,
+ '',
+ config,
+ ),
).toBe(
- 'import __vite__cjsImport0_react from "./node_modules/.vite/react.js"; ' +
+ 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' +
'const __vite__cjsExportDefault_0 = __vite__cjsImport0_react["Component"]; ' +
- 'export default __vite__cjsExportDefault_0'
+ 'export default __vite__cjsExportDefault_0',
)
})
})
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/__snapshots__/fixture.spec.ts.snap b/packages/vite/src/node/__tests__/plugins/importGlob/__snapshots__/fixture.spec.ts.snap
new file mode 100644
index 00000000000000..3ab5e217015e28
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/__snapshots__/fixture.spec.ts.snap
@@ -0,0 +1,198 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`fixture > transform 1`] = `
+"import * as __vite_glob_3_0 from "./modules/a.ts";import * as __vite_glob_3_1 from "./modules/b.ts";import * as __vite_glob_3_2 from "./modules/index.ts";import * as __vite_glob_5_0 from "./modules/a.ts";import * as __vite_glob_5_1 from "./modules/b.ts";import * as __vite_glob_5_2 from "./modules/index.ts";import { name as __vite_glob_9_0 } from "./modules/a.ts";import { name as __vite_glob_9_1 } from "./modules/b.ts";import { name as __vite_glob_9_2 } from "./modules/index.ts";import { name as __vite_glob_11_0 } from "./modules/a.ts";import { name as __vite_glob_11_1 } from "./modules/b.ts";import { name as __vite_glob_11_2 } from "./modules/index.ts";import { default as __vite_glob_15_0 } from "./modules/a.ts?raw";import { default as __vite_glob_15_1 } from "./modules/b.ts?raw";import "types/importMeta";
+export const basic = /* #__PURE__ */ Object.assign({"./modules/a.ts": () => import("./modules/a.ts"),"./modules/b.ts": () => import("./modules/b.ts"),"./modules/index.ts": () => import("./modules/index.ts")});
+export const basicWithObjectKeys = Object.keys({"./modules/a.ts": 0,"./modules/b.ts": 0,"./modules/index.ts": 0});
+export const basicWithObjectValues = Object.values([() => import("./modules/a.ts"),() => import("./modules/b.ts"),() => import("./modules/index.ts")]);
+export const basicEager = /* #__PURE__ */ Object.assign({"./modules/a.ts": __vite_glob_3_0,"./modules/b.ts": __vite_glob_3_1,"./modules/index.ts": __vite_glob_3_2
+
+});
+export const basicEagerWithObjectKeys = Object.keys(
+ {"./modules/a.ts": 0,"./modules/b.ts": 0,"./modules/index.ts": 0
+
+}
+);
+export const basicEagerWithObjectValues = Object.values(
+ [__vite_glob_5_0,__vite_glob_5_1,__vite_glob_5_2
+
+]
+);
+export const ignore = /* #__PURE__ */ Object.assign({"./modules/a.ts": () => import("./modules/a.ts"),"./modules/b.ts": () => import("./modules/b.ts")});
+export const ignoreWithObjectKeys = Object.keys(
+ {"./modules/a.ts": 0,"./modules/b.ts": 0}
+);
+export const ignoreWithObjectValues = Object.values(
+ [() => import("./modules/a.ts"),() => import("./modules/b.ts")]
+);
+export const namedEager = /* #__PURE__ */ Object.assign({"./modules/a.ts": __vite_glob_9_0,"./modules/b.ts": __vite_glob_9_1,"./modules/index.ts": __vite_glob_9_2
+
+
+});
+export const namedEagerWithObjectKeys = Object.keys(
+ {"./modules/a.ts": 0,"./modules/b.ts": 0,"./modules/index.ts": 0
+
+
+}
+);
+export const namedEagerWithObjectValues = Object.values(
+ [__vite_glob_11_0,__vite_glob_11_1,__vite_glob_11_2
+
+
+]
+);
+export const namedDefault = /* #__PURE__ */ Object.assign({"./modules/a.ts": () => import("./modules/a.ts").then(m => m["default"]),"./modules/b.ts": () => import("./modules/b.ts").then(m => m["default"]),"./modules/index.ts": () => import("./modules/index.ts").then(m => m["default"])
+
+});
+export const namedDefaultWithObjectKeys = Object.keys(
+ {"./modules/a.ts": 0,"./modules/b.ts": 0,"./modules/index.ts": 0
+
+}
+);
+export const namedDefaultWithObjectValues = Object.values(
+ [() => import("./modules/a.ts").then(m => m["default"]),() => import("./modules/b.ts").then(m => m["default"]),() => import("./modules/index.ts").then(m => m["default"])
+
+]
+);
+export const eagerAs = /* #__PURE__ */ Object.assign({"./modules/a.ts": __vite_glob_15_0,"./modules/b.ts": __vite_glob_15_1
+
+
+});
+export const rawImportModule = /* #__PURE__ */ Object.assign({"./modules/a.ts": () => import("./modules/a.ts?raw"),"./modules/b.ts": () => import("./modules/b.ts?raw")
+
+
+});
+export const excludeSelf = /* #__PURE__ */ Object.assign({"./sibling.ts": () => import("./sibling.ts")
+
+
+
+
+
+});
+export const excludeSelfRaw = /* #__PURE__ */ Object.assign({"./sibling.ts": () => import("./sibling.ts?raw")});
+export const customQueryString = /* #__PURE__ */ Object.assign({"./sibling.ts": () => import("./sibling.ts?custom")});
+export const customQueryObject = /* #__PURE__ */ Object.assign({"./sibling.ts": () => import("./sibling.ts?foo=bar&raw=true")
+
+
+
+
+});
+export const parent = /* #__PURE__ */ Object.assign({
+
+
+});
+export const rootMixedRelative = /* #__PURE__ */ Object.assign({"/fixture-b/a.ts": () => import("../fixture-b/a.ts?url").then(m => m["default"]),"/fixture-b/b.ts": () => import("../fixture-b/b.ts?url").then(m => m["default"]),"/fixture-b/index.ts": () => import("../fixture-b/index.ts?url").then(m => m["default"]),"/fixture.spec.ts": () => import("../fixture.spec.ts?url").then(m => m["default"]),"/parse.spec.ts": () => import("../parse.spec.ts?url").then(m => m["default"]),"/utils.spec.ts": () => import("../utils.spec.ts?url").then(m => m["default"])
+
+
+});
+export const cleverCwd1 = /* #__PURE__ */ Object.assign({"./node_modules/framework/pages/hello.page.js": () => import("./node_modules/framework/pages/hello.page.js")
+
+});
+export const cleverCwd2 = /* #__PURE__ */ Object.assign({"./modules/a.ts": () => import("./modules/a.ts"),"./modules/b.ts": () => import("./modules/b.ts"),"../fixture-b/a.ts": () => import("../fixture-b/a.ts"),"../fixture-b/b.ts": () => import("../fixture-b/b.ts")
+
+
+
+});
+"
+`;
+
+exports[`fixture > transform with restoreQueryExtension 1`] = `
+"import * as __vite_glob_3_0 from "./modules/a.ts";import * as __vite_glob_3_1 from "./modules/b.ts";import * as __vite_glob_3_2 from "./modules/index.ts";import * as __vite_glob_5_0 from "./modules/a.ts";import * as __vite_glob_5_1 from "./modules/b.ts";import * as __vite_glob_5_2 from "./modules/index.ts";import { name as __vite_glob_9_0 } from "./modules/a.ts";import { name as __vite_glob_9_1 } from "./modules/b.ts";import { name as __vite_glob_9_2 } from "./modules/index.ts";import { name as __vite_glob_11_0 } from "./modules/a.ts";import { name as __vite_glob_11_1 } from "./modules/b.ts";import { name as __vite_glob_11_2 } from "./modules/index.ts";import { default as __vite_glob_15_0 } from "./modules/a.ts?raw";import { default as __vite_glob_15_1 } from "./modules/b.ts?raw";import "types/importMeta";
+export const basic = /* #__PURE__ */ Object.assign({"./modules/a.ts": () => import("./modules/a.ts"),"./modules/b.ts": () => import("./modules/b.ts"),"./modules/index.ts": () => import("./modules/index.ts")});
+export const basicWithObjectKeys = Object.keys({"./modules/a.ts": 0,"./modules/b.ts": 0,"./modules/index.ts": 0});
+export const basicWithObjectValues = Object.values([() => import("./modules/a.ts"),() => import("./modules/b.ts"),() => import("./modules/index.ts")]);
+export const basicEager = /* #__PURE__ */ Object.assign({"./modules/a.ts": __vite_glob_3_0,"./modules/b.ts": __vite_glob_3_1,"./modules/index.ts": __vite_glob_3_2
+
+});
+export const basicEagerWithObjectKeys = Object.keys(
+ {"./modules/a.ts": 0,"./modules/b.ts": 0,"./modules/index.ts": 0
+
+}
+);
+export const basicEagerWithObjectValues = Object.values(
+ [__vite_glob_5_0,__vite_glob_5_1,__vite_glob_5_2
+
+]
+);
+export const ignore = /* #__PURE__ */ Object.assign({"./modules/a.ts": () => import("./modules/a.ts"),"./modules/b.ts": () => import("./modules/b.ts")});
+export const ignoreWithObjectKeys = Object.keys(
+ {"./modules/a.ts": 0,"./modules/b.ts": 0}
+);
+export const ignoreWithObjectValues = Object.values(
+ [() => import("./modules/a.ts"),() => import("./modules/b.ts")]
+);
+export const namedEager = /* #__PURE__ */ Object.assign({"./modules/a.ts": __vite_glob_9_0,"./modules/b.ts": __vite_glob_9_1,"./modules/index.ts": __vite_glob_9_2
+
+
+});
+export const namedEagerWithObjectKeys = Object.keys(
+ {"./modules/a.ts": 0,"./modules/b.ts": 0,"./modules/index.ts": 0
+
+
+}
+);
+export const namedEagerWithObjectValues = Object.values(
+ [__vite_glob_11_0,__vite_glob_11_1,__vite_glob_11_2
+
+
+]
+);
+export const namedDefault = /* #__PURE__ */ Object.assign({"./modules/a.ts": () => import("./modules/a.ts").then(m => m["default"]),"./modules/b.ts": () => import("./modules/b.ts").then(m => m["default"]),"./modules/index.ts": () => import("./modules/index.ts").then(m => m["default"])
+
+});
+export const namedDefaultWithObjectKeys = Object.keys(
+ {"./modules/a.ts": 0,"./modules/b.ts": 0,"./modules/index.ts": 0
+
+}
+);
+export const namedDefaultWithObjectValues = Object.values(
+ [() => import("./modules/a.ts").then(m => m["default"]),() => import("./modules/b.ts").then(m => m["default"]),() => import("./modules/index.ts").then(m => m["default"])
+
+]
+);
+export const eagerAs = /* #__PURE__ */ Object.assign({"./modules/a.ts": __vite_glob_15_0,"./modules/b.ts": __vite_glob_15_1
+
+
+});
+export const rawImportModule = /* #__PURE__ */ Object.assign({"./modules/a.ts": () => import("./modules/a.ts?raw"),"./modules/b.ts": () => import("./modules/b.ts?raw")
+
+
+});
+export const excludeSelf = /* #__PURE__ */ Object.assign({"./sibling.ts": () => import("./sibling.ts")
+
+
+
+
+
+});
+export const excludeSelfRaw = /* #__PURE__ */ Object.assign({"./sibling.ts": () => import("./sibling.ts?raw")});
+export const customQueryString = /* #__PURE__ */ Object.assign({"./sibling.ts": () => import("./sibling.ts?custom&lang.ts")});
+export const customQueryObject = /* #__PURE__ */ Object.assign({"./sibling.ts": () => import("./sibling.ts?foo=bar&raw=true&lang.ts")
+
+
+
+
+});
+export const parent = /* #__PURE__ */ Object.assign({
+
+
+});
+export const rootMixedRelative = /* #__PURE__ */ Object.assign({"/fixture-b/a.ts": () => import("../fixture-b/a.ts?url&lang.ts").then(m => m["default"]),"/fixture-b/b.ts": () => import("../fixture-b/b.ts?url&lang.ts").then(m => m["default"]),"/fixture-b/index.ts": () => import("../fixture-b/index.ts?url&lang.ts").then(m => m["default"]),"/fixture.spec.ts": () => import("../fixture.spec.ts?url&lang.ts").then(m => m["default"]),"/parse.spec.ts": () => import("../parse.spec.ts?url&lang.ts").then(m => m["default"]),"/utils.spec.ts": () => import("../utils.spec.ts?url&lang.ts").then(m => m["default"])
+
+
+});
+export const cleverCwd1 = /* #__PURE__ */ Object.assign({"./node_modules/framework/pages/hello.page.js": () => import("./node_modules/framework/pages/hello.page.js")
+
+});
+export const cleverCwd2 = /* #__PURE__ */ Object.assign({"./modules/a.ts": () => import("./modules/a.ts"),"./modules/b.ts": () => import("./modules/b.ts"),"../fixture-b/a.ts": () => import("../fixture-b/a.ts"),"../fixture-b/b.ts": () => import("../fixture-b/b.ts")
+
+
+
+});
+"
+`;
+
+exports[`fixture > virtual modules 1`] = `
+"/* #__PURE__ */ Object.assign({"/modules/a.ts": () => import("/modules/a.ts"),"/modules/b.ts": () => import("/modules/b.ts"),"/modules/index.ts": () => import("/modules/index.ts")})
+/* #__PURE__ */ Object.assign({"/../fixture-b/a.ts": () => import("/../fixture-b/a.ts"),"/../fixture-b/b.ts": () => import("/../fixture-b/b.ts"),"/../fixture-b/index.ts": () => import("/../fixture-b/index.ts")})"
+`;
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/.gitignore b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/.gitignore
new file mode 100644
index 00000000000000..2b9b8877da603f
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/.gitignore
@@ -0,0 +1 @@
+!/node_modules/
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/index.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/index.ts
new file mode 100644
index 00000000000000..12acbf28d109ef
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/index.ts
@@ -0,0 +1,112 @@
+import 'types/importMeta'
+
+export interface ModuleType {
+ name: string
+}
+
+export const basic = import.meta.glob('./modules/*.ts')
+// prettier-ignore
+export const basicWithObjectKeys = Object.keys(import.meta.glob('./modules/*.ts'))
+// prettier-ignore
+export const basicWithObjectValues = Object.values(import.meta.glob('./modules/*.ts'))
+
+export const basicEager = import.meta.glob('./modules/*.ts', {
+ eager: true,
+})
+export const basicEagerWithObjectKeys = Object.keys(
+ import.meta.glob('./modules/*.ts', {
+ eager: true,
+ }),
+)
+export const basicEagerWithObjectValues = Object.values(
+ import.meta.glob('./modules/*.ts', {
+ eager: true,
+ }),
+)
+
+export const ignore = import.meta.glob(['./modules/*.ts', '!**/index.ts'])
+export const ignoreWithObjectKeys = Object.keys(
+ import.meta.glob(['./modules/*.ts', '!**/index.ts']),
+)
+export const ignoreWithObjectValues = Object.values(
+ import.meta.glob(['./modules/*.ts', '!**/index.ts']),
+)
+
+export const namedEager = import.meta.glob('./modules/*.ts', {
+ eager: true,
+ import: 'name',
+})
+export const namedEagerWithObjectKeys = Object.keys(
+ import.meta.glob('./modules/*.ts', {
+ eager: true,
+ import: 'name',
+ }),
+)
+export const namedEagerWithObjectValues = Object.values(
+ import.meta.glob('./modules/*.ts', {
+ eager: true,
+ import: 'name',
+ }),
+)
+
+export const namedDefault = import.meta.glob('./modules/*.ts', {
+ import: 'default',
+})
+export const namedDefaultWithObjectKeys = Object.keys(
+ import.meta.glob('./modules/*.ts', {
+ import: 'default',
+ }),
+)
+export const namedDefaultWithObjectValues = Object.values(
+ import.meta.glob('./modules/*.ts', {
+ import: 'default',
+ }),
+)
+
+export const eagerAs = import.meta.glob(
+ ['./modules/*.ts', '!**/index.ts'],
+ { eager: true, query: '?raw', import: 'default' },
+)
+
+export const rawImportModule = import.meta.glob(
+ ['./modules/*.ts', '!**/index.ts'],
+ { query: '?raw', import: '*' },
+)
+
+export const excludeSelf = import.meta.glob(
+ './*.ts',
+ // for test: annotation contain ")"
+ /*
+ * for test: annotation contain ")"
+ * */
+)
+export const excludeSelfRaw = import.meta.glob('./*.ts', { query: '?raw' })
+
+export const customQueryString = import.meta.glob('./*.ts', { query: 'custom' })
+
+export const customQueryObject = import.meta.glob('./*.ts', {
+ query: {
+ foo: 'bar',
+ raw: true,
+ },
+})
+
+export const parent = import.meta.glob('../../playground/src/*.ts', {
+ query: '?url',
+ import: 'default',
+})
+
+export const rootMixedRelative = import.meta.glob(
+ ['/*.ts', '../fixture-b/*.ts'],
+ { query: '?url', import: 'default' },
+)
+
+export const cleverCwd1 = import.meta.glob(
+ './node_modules/framework/**/*.page.js',
+)
+
+export const cleverCwd2 = import.meta.glob([
+ './modules/*.ts',
+ '../fixture-b/*.ts',
+ '!**/index.ts',
+])
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/a.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/a.ts
new file mode 100644
index 00000000000000..facd48a0875e65
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/a.ts
@@ -0,0 +1 @@
+export const name = 'a'
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/b.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/b.ts
new file mode 100644
index 00000000000000..0b1eb38d9087a2
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/b.ts
@@ -0,0 +1 @@
+export const name = 'b'
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/index.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/index.ts
new file mode 100644
index 00000000000000..25b59ae7d30714
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/index.ts
@@ -0,0 +1,6 @@
+export { name as a } from './a'
+export { name as b } from './b'
+
+export const name = 'index'
+
+export default 'indexDefault'
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/node_modules/framework/pages/hello.page.js b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/node_modules/framework/pages/hello.page.js
new file mode 100644
index 00000000000000..cbe518a8e79477
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/node_modules/framework/pages/hello.page.js
@@ -0,0 +1,4 @@
+// A fake Page file. (This technique of globbing into `node_modules/`
+// is used by vite-plugin-ssr frameworks and Hydrogen.)
+
+export const a = 1
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/sibling.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/sibling.ts
new file mode 100644
index 00000000000000..b286816bf5d63a
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/sibling.ts
@@ -0,0 +1 @@
+export const name = 'I am your sibling!'
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/a.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/a.ts
new file mode 100644
index 00000000000000..facd48a0875e65
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/a.ts
@@ -0,0 +1 @@
+export const name = 'a'
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/b.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/b.ts
new file mode 100644
index 00000000000000..0b1eb38d9087a2
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/b.ts
@@ -0,0 +1 @@
+export const name = 'b'
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/index.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/index.ts
new file mode 100644
index 00000000000000..39bdbfd1a8befb
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/index.ts
@@ -0,0 +1,2 @@
+export { name as a } from './a'
+export { name as b } from './b'
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture.spec.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture.spec.ts
new file mode 100644
index 00000000000000..62eb06581531a7
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture.spec.ts
@@ -0,0 +1,82 @@
+import { dirname, resolve } from 'node:path'
+import { promises as fs } from 'node:fs'
+import { fileURLToPath } from 'node:url'
+import { describe, expect, it } from 'vitest'
+import { transformGlobImport } from '../../../plugins/importMetaGlob'
+import { transformWithEsbuild } from '../../../plugins/esbuild'
+
+const __dirname = resolve(dirname(fileURLToPath(import.meta.url)))
+
+describe('fixture', async () => {
+ const resolveId = (id: string) => id
+ const root = __dirname
+
+ it('transform', async () => {
+ const id = resolve(__dirname, './fixture-a/index.ts')
+ const code = (
+ await transformWithEsbuild(await fs.readFile(id, 'utf-8'), id)
+ ).code
+
+ expect(
+ (await transformGlobImport(code, id, root, resolveId))?.s.toString(),
+ ).toMatchSnapshot()
+ })
+
+ it('preserve line count', async () => {
+ const getTransformedLineCount = async (code: string) =>
+ (await transformGlobImport(code, 'virtual:module', root, resolveId))?.s
+ .toString()
+ .split('\n').length
+
+ expect(await getTransformedLineCount("import.meta.glob('./*.js')")).toBe(1)
+ expect(
+ await getTransformedLineCount(
+ `
+ import.meta.glob(
+ './*.js'
+ )
+ `.trim(),
+ ),
+ ).toBe(3)
+ })
+
+ it('virtual modules', async () => {
+ const root = resolve(__dirname, './fixture-a')
+ const code = [
+ "import.meta.glob('/modules/*.ts')",
+ "import.meta.glob(['/../fixture-b/*.ts'])",
+ ].join('\n')
+ expect(
+ (
+ await transformGlobImport(code, 'virtual:module', root, resolveId)
+ )?.s.toString(),
+ ).toMatchSnapshot()
+
+ try {
+ await transformGlobImport(
+ "import.meta.glob('./modules/*.ts')",
+ 'virtual:module',
+ root,
+ resolveId,
+ )
+ expect('no error').toBe('should throw an error')
+ } catch (err) {
+ expect(err).toMatchInlineSnapshot(
+ "[Error: In virtual modules, all globs must start with '/']",
+ )
+ }
+ })
+
+ it('transform with restoreQueryExtension', async () => {
+ const id = resolve(__dirname, './fixture-a/index.ts')
+ const code = (
+ await transformWithEsbuild(await fs.readFile(id, 'utf-8'), id)
+ ).code
+
+ expect(
+ (
+ await transformGlobImport(code, id, root, resolveId, true)
+ )?.s.toString(),
+ ).toMatchSnapshot()
+ })
+})
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/parse.spec.ts b/packages/vite/src/node/__tests__/plugins/importGlob/parse.spec.ts
new file mode 100644
index 00000000000000..bb1304076661bc
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/parse.spec.ts
@@ -0,0 +1,379 @@
+import { describe, expect, it } from 'vitest'
+import { parseImportGlob } from '../../../plugins/importMetaGlob'
+
+async function run(input: string) {
+ const items = await parseImportGlob(
+ input,
+ process.cwd(),
+ process.cwd(),
+ (id) => id,
+ )
+ return items.map((i) => ({
+ globs: i.globs,
+ options: i.options,
+ start: i.start,
+ }))
+}
+
+async function runError(input: string) {
+ try {
+ await run(input)
+ } catch (e) {
+ return e
+ }
+}
+
+describe('parse positives', async () => {
+ it('basic', async () => {
+ expect(
+ await run(`
+ import.meta.glob('./modules/*.ts')
+ `),
+ ).toMatchInlineSnapshot(`
+ [
+ {
+ "globs": [
+ "./modules/*.ts",
+ ],
+ "options": {},
+ "start": 5,
+ },
+ ]
+ `)
+ })
+
+ it('array', async () => {
+ expect(
+ await run(`
+ import.meta.glob(['./modules/*.ts', './dir/*.{js,ts}'])
+ `),
+ ).toMatchInlineSnapshot(`
+ [
+ {
+ "globs": [
+ "./modules/*.ts",
+ "./dir/*.{js,ts}",
+ ],
+ "options": {},
+ "start": 5,
+ },
+ ]
+ `)
+ })
+
+ it('options with multilines', async () => {
+ expect(
+ await run(`
+ import.meta.glob([
+ './modules/*.ts',
+ "!./dir/*.{js,ts}"
+ ], {
+ eager: true,
+ import: 'named'
+ })
+ `),
+ ).toMatchInlineSnapshot(`
+ [
+ {
+ "globs": [
+ "./modules/*.ts",
+ "!./dir/*.{js,ts}",
+ ],
+ "options": {
+ "eager": true,
+ "import": "named",
+ },
+ "start": 5,
+ },
+ ]
+ `)
+ })
+
+ it('options with multilines', async () => {
+ expect(
+ await run(`
+ const modules = import.meta.glob(
+ '/dir/**'
+ // for test: annotation contain ")"
+ /*
+ * for test: annotation contain ")"
+ * */
+ )
+ `),
+ ).toMatchInlineSnapshot(`
+ [
+ {
+ "globs": [
+ "/dir/**",
+ ],
+ "options": {},
+ "start": 21,
+ },
+ ]
+ `)
+ })
+
+ it('options query', async () => {
+ expect(
+ await run(`
+ const modules = import.meta.glob(
+ '/dir/**',
+ {
+ query: {
+ foo: 'bar',
+ raw: true,
+ }
+ }
+ )
+ `),
+ ).toMatchInlineSnapshot(`
+ [
+ {
+ "globs": [
+ "/dir/**",
+ ],
+ "options": {
+ "query": "?foo=bar&raw=true",
+ },
+ "start": 21,
+ },
+ ]
+ `)
+ })
+
+ it('object properties - 1', async () => {
+ expect(
+ await run(`
+ export const pageFiles = {
+ '.page': import.meta.glob('/**/*.page.*([a-zA-Z0-9])')
+};`),
+ ).toMatchInlineSnapshot(`
+ [
+ {
+ "globs": [
+ "/**/*.page.*([a-zA-Z0-9])",
+ ],
+ "options": {},
+ "start": 47,
+ },
+ ]
+`)
+ })
+
+ it('object properties - 2', async () => {
+ expect(
+ await run(`
+ export const pageFiles = {
+ '.page': import.meta.glob('/**/*.page.*([a-zA-Z0-9])'),
+};`),
+ ).toMatchInlineSnapshot(`
+ [
+ {
+ "globs": [
+ "/**/*.page.*([a-zA-Z0-9])",
+ ],
+ "options": {},
+ "start": 47,
+ },
+ ]
+`)
+ })
+
+ it('object properties - 3', async () => {
+ expect(
+ await run(`
+ export const pageFiles = {
+ '.page.client': import.meta.glob('/**/*.page.client.*([a-zA-Z0-9])'),
+ '.page.server': import.meta.glob('/**/*.page.server.*([a-zA-Z0-9])'),
+};`),
+ ).toMatchInlineSnapshot(`
+ [
+ {
+ "globs": [
+ "/**/*.page.client.*([a-zA-Z0-9])",
+ ],
+ "options": {},
+ "start": 54,
+ },
+ {
+ "globs": [
+ "/**/*.page.server.*([a-zA-Z0-9])",
+ ],
+ "options": {},
+ "start": 130,
+ },
+ ]
+`)
+ })
+
+ it('array item', async () => {
+ expect(
+ await run(`
+ export const pageFiles = [
+ import.meta.glob('/**/*.page.client.*([a-zA-Z0-9])'),
+ import.meta.glob('/**/*.page.server.*([a-zA-Z0-9])'),
+ ]`),
+ ).toMatchInlineSnapshot(`
+ [
+ {
+ "globs": [
+ "/**/*.page.client.*([a-zA-Z0-9])",
+ ],
+ "options": {},
+ "start": 38,
+ },
+ {
+ "globs": [
+ "/**/*.page.server.*([a-zA-Z0-9])",
+ ],
+ "options": {},
+ "start": 98,
+ },
+ ]
+ `)
+ })
+})
+
+describe('parse negatives', async () => {
+ it('syntax error', async () => {
+ expect(await runError('import.meta.glob(')).toMatchInlineSnapshot(
+ '[Error: Invalid glob import syntax: Close parenthesis not found]',
+ )
+ })
+
+ it('empty', async () => {
+ expect(await runError('import.meta.glob()')).toMatchInlineSnapshot(
+ '[Error: Invalid glob import syntax: Expected 1-2 arguments, but got 0]',
+ )
+ })
+
+ it('3 args', async () => {
+ expect(
+ await runError('import.meta.glob("", {}, {})'),
+ ).toMatchInlineSnapshot(
+ '[Error: Invalid glob import syntax: Expected 1-2 arguments, but got 3]',
+ )
+ })
+
+ it('in string', async () => {
+ expect(await runError('"import.meta.glob()"')).toBeUndefined()
+ })
+
+ it('variable', async () => {
+ expect(await runError('import.meta.glob(hey)')).toMatchInlineSnapshot(
+ '[Error: Invalid glob import syntax: Could only use literals]',
+ )
+ })
+
+ it('template', async () => {
+ expect(
+ await runError('import.meta.glob(`hi ${hey}`)'),
+ ).toMatchInlineSnapshot(
+ '[Error: Invalid glob import syntax: Expected glob to be a string, but got dynamic template literal]',
+ )
+ })
+
+ it('template with unicode', async () => {
+ expect(await run('import.meta.glob(`/\u0068\u0065\u006c\u006c\u006f`)'))
+ .toMatchInlineSnapshot(`
+ [
+ {
+ "globs": [
+ "/hello",
+ ],
+ "options": {},
+ "start": 0,
+ },
+ ]
+ `)
+ })
+
+ it('template without expressions', async () => {
+ expect(await run('import.meta.glob(`/**/*.page.client.*([a-zA-Z0-9])`)'))
+ .toMatchInlineSnapshot(`
+ [
+ {
+ "globs": [
+ "/**/*.page.client.*([a-zA-Z0-9])",
+ ],
+ "options": {},
+ "start": 0,
+ },
+ ]
+ `)
+ })
+
+ it('be string', async () => {
+ expect(await runError('import.meta.glob(1)')).toMatchInlineSnapshot(
+ '[Error: Invalid glob import syntax: Expected glob to be a string, but got "number"]',
+ )
+ })
+
+ it('be array variable', async () => {
+ expect(await runError('import.meta.glob([hey])')).toMatchInlineSnapshot(
+ '[Error: Invalid glob import syntax: Could only use literals]',
+ )
+ expect(
+ await runError('import.meta.glob(["1", hey])'),
+ ).toMatchInlineSnapshot(
+ '[Error: Invalid glob import syntax: Could only use literals]',
+ )
+ })
+
+ it('options', async () => {
+ expect(
+ await runError('import.meta.glob("hey", hey)'),
+ ).toMatchInlineSnapshot(
+ '[Error: Invalid glob import syntax: Expected the second argument to be an object literal, but got "Identifier"]',
+ )
+ expect(await runError('import.meta.glob("hey", [])')).toMatchInlineSnapshot(
+ '[Error: Invalid glob import syntax: Expected the second argument to be an object literal, but got "ArrayExpression"]',
+ )
+ })
+
+ it('options props', async () => {
+ expect(
+ await runError('import.meta.glob("hey", { hey: 1 })'),
+ ).toMatchInlineSnapshot('[Error: Unknown glob option "hey"]')
+ expect(
+ await runError('import.meta.glob("hey", { import: hey })'),
+ ).toMatchInlineSnapshot(
+ '[Error: Vite is unable to parse the glob options as the value is not static]',
+ )
+ expect(
+ await runError('import.meta.glob("hey", { eager: 123 })'),
+ ).toMatchInlineSnapshot(
+ '[Error: Expected glob option "eager" to be of type boolean, but got number]',
+ )
+ })
+
+ it('options query', async () => {
+ expect(
+ await runError('import.meta.glob("./*.js", { as: "raw", query: "hi" })'),
+ ).toMatchInlineSnapshot(
+ '[Error: Options "as" and "query" cannot be used together]',
+ )
+ expect(
+ await runError('import.meta.glob("./*.js", { query: 123 })'),
+ ).toMatchInlineSnapshot(
+ '[Error: Expected glob option "query" to be of type object or string, but got number]',
+ )
+ expect(
+ await runError('import.meta.glob("./*.js", { query: { foo: {} } })'),
+ ).toMatchInlineSnapshot(
+ '[Error: Expected glob option "query.foo" to be of type string, number, or boolean, but got object]',
+ )
+ expect(
+ await runError('import.meta.glob("./*.js", { query: { foo: hey } })'),
+ ).toMatchInlineSnapshot(
+ '[Error: Vite is unable to parse the glob options as the value is not static]',
+ )
+ expect(
+ await runError(
+ 'import.meta.glob("./*.js", { query: { foo: 123, ...a } })',
+ ),
+ ).toMatchInlineSnapshot(
+ '[Error: Vite is unable to parse the glob options as the value is not static]',
+ )
+ })
+})
diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/utils.spec.ts b/packages/vite/src/node/__tests__/plugins/importGlob/utils.spec.ts
new file mode 100644
index 00000000000000..bd91c6165f798e
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/importGlob/utils.spec.ts
@@ -0,0 +1,31 @@
+import { describe, expect, it } from 'vitest'
+import { getCommonBase } from '../../../plugins/importMetaGlob'
+
+describe('getCommonBase()', async () => {
+ it('basic', () => {
+ expect(getCommonBase(['/a/b/*.js', '/a/c/*.js'])).toBe('/a')
+ })
+ it('common base', () => {
+ expect(getCommonBase(['/a/b/**/*.vue', '/a/b/**/*.jsx'])).toBe('/a/b')
+ })
+ it('static file', () => {
+ expect(
+ getCommonBase(['/a/b/**/*.vue', '/a/b/**/*.jsx', '/a/b/foo.js']),
+ ).toBe('/a/b')
+ expect(getCommonBase(['/a/b/**/*.vue', '/a/b/**/*.jsx', '/a/foo.js'])).toBe(
+ '/a',
+ )
+ })
+ it('correct `scan()`', () => {
+ expect(getCommonBase(['/a/*.vue'])).toBe('/a')
+ expect(getCommonBase(['/a/some.vue'])).toBe('/a')
+ expect(getCommonBase(['/a/b/**/c/foo.vue', '/a/b/c/**/*.jsx'])).toBe('/a/b')
+ })
+ it('single', () => {
+ expect(getCommonBase(['/a/b/c/*.vue'])).toBe('/a/b/c')
+ expect(getCommonBase(['/a/b/c/foo.vue'])).toBe('/a/b/c')
+ })
+ it('no common base', () => {
+ expect(getCommonBase(['/a/b/*.js', '/c/a/b/*.js'])).toBe('/')
+ })
+})
diff --git a/packages/vite/src/node/__tests__/plugins/index.spec.ts b/packages/vite/src/node/__tests__/plugins/index.spec.ts
new file mode 100644
index 00000000000000..831e0217524076
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/index.spec.ts
@@ -0,0 +1,162 @@
+import { afterAll, describe, expect, test, vi } from 'vitest'
+import { type InlineConfig, type Plugin, build, createServer } from '../..'
+
+const getConfigWithPlugin = (
+ plugins: Plugin[],
+ input?: string[],
+): InlineConfig => {
+ return {
+ configFile: false,
+ server: { middlewareMode: true, ws: false },
+ optimizeDeps: { noDiscovery: true, include: [] },
+ build: { rollupOptions: { input } },
+ plugins,
+ logLevel: 'silent',
+ }
+}
+
+describe('hook filter with plugin container', async () => {
+ const resolveId = vi.fn()
+ const load = vi.fn()
+ const transformWithId = vi.fn()
+ const transformWithCode = vi.fn()
+ const any = expect.toSatisfy(() => true) // anything including undefined and null
+ const config = getConfigWithPlugin([
+ {
+ name: 'test',
+ resolveId: {
+ filter: { id: /\.js$/ },
+ handler: resolveId,
+ },
+ load: {
+ filter: { id: '**/*.js' },
+ handler: load,
+ },
+ transform: {
+ filter: { id: '**/*.js' },
+ handler: transformWithId,
+ },
+ },
+ {
+ name: 'test2',
+ transform: {
+ filter: { code: 'import.meta' },
+ handler: transformWithCode,
+ },
+ },
+ ])
+ const server = await createServer(config)
+ afterAll(async () => {
+ await server.close()
+ })
+ const pluginContainer = server.environments.ssr.pluginContainer
+
+ test('resolveId', async () => {
+ await pluginContainer.resolveId('foo.js')
+ await pluginContainer.resolveId('foo.ts')
+ expect(resolveId).toHaveBeenCalledTimes(1)
+ expect(resolveId).toHaveBeenCalledWith('foo.js', any, any)
+ })
+
+ test('load', async () => {
+ await pluginContainer.load('foo.js')
+ await pluginContainer.load('foo.ts')
+ expect(load).toHaveBeenCalledTimes(1)
+ expect(load).toHaveBeenCalledWith('foo.js', any)
+ })
+
+ test('transform', async () => {
+ await server.environments.ssr.moduleGraph.ensureEntryFromUrl('foo.js')
+ await server.environments.ssr.moduleGraph.ensureEntryFromUrl('foo.ts')
+
+ await pluginContainer.transform('import_meta', 'foo.js')
+ await pluginContainer.transform('import.meta', 'foo.ts')
+ expect(transformWithId).toHaveBeenCalledTimes(1)
+ expect(transformWithId).toHaveBeenCalledWith(
+ expect.stringContaining('import_meta'),
+ 'foo.js',
+ any,
+ )
+ expect(transformWithCode).toHaveBeenCalledTimes(1)
+ expect(transformWithCode).toHaveBeenCalledWith(
+ expect.stringContaining('import.meta'),
+ 'foo.ts',
+ any,
+ )
+ })
+})
+
+describe('hook filter with build', async () => {
+ const resolveId = vi.fn()
+ const load = vi.fn()
+ const transformWithId = vi.fn()
+ const transformWithCode = vi.fn()
+ const any = expect.anything()
+ const config = getConfigWithPlugin(
+ [
+ {
+ name: 'test',
+ resolveId: {
+ filter: { id: /\.js$/ },
+ handler: resolveId,
+ },
+ load: {
+ filter: { id: '**/*.js' },
+ handler: load,
+ },
+ transform: {
+ filter: { id: '**/*.js' },
+ handler: transformWithId,
+ },
+ },
+ {
+ name: 'test2',
+ transform: {
+ filter: { code: 'import.meta' },
+ handler: transformWithCode,
+ },
+ },
+ {
+ name: 'resolver',
+ resolveId(id) {
+ return id
+ },
+ load(id) {
+ if (id === 'foo.js') {
+ return 'import "foo.ts"\n' + 'import_meta'
+ }
+ if (id === 'foo.ts') {
+ return 'import.meta'
+ }
+ },
+ },
+ ],
+ ['foo.js', 'foo.ts'],
+ )
+ await build(config)
+
+ test('resolveId', async () => {
+ expect(resolveId).toHaveBeenCalledTimes(1)
+ expect(resolveId).toHaveBeenCalledWith('foo.js', undefined, any)
+ })
+
+ test('load', async () => {
+ expect(load).toHaveBeenCalledTimes(1)
+ expect(load).toHaveBeenCalledWith('foo.js', any)
+ })
+
+ test('transform', async () => {
+ expect(transformWithId).toHaveBeenCalledTimes(1)
+ expect(transformWithId).toHaveBeenCalledWith(
+ expect.stringContaining('import_meta'),
+ 'foo.js',
+ any,
+ )
+ expect(transformWithCode).toHaveBeenCalledTimes(1)
+ expect(transformWithCode).toHaveBeenCalledWith(
+ expect.stringContaining('import.meta'),
+ 'foo.ts',
+ any,
+ )
+ })
+})
diff --git a/packages/vite/src/node/__tests__/plugins/json.spec.ts b/packages/vite/src/node/__tests__/plugins/json.spec.ts
new file mode 100644
index 00000000000000..644fd1a925084d
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/json.spec.ts
@@ -0,0 +1,168 @@
+import { describe, expect, test } from 'vitest'
+import {
+ type JsonOptions,
+ extractJsonErrorPosition,
+ jsonPlugin,
+} from '../../plugins/json'
+
+const getErrorMessage = (input: string) => {
+ try {
+ JSON.parse(input)
+ throw new Error('No error happened')
+ } catch (e) {
+ return e.message
+ }
+}
+
+test('can extract json error position', () => {
+ const cases = [
+ { input: '{', expectedPosition: 0 },
+ { input: '{},', expectedPosition: 1 },
+ { input: '"f', expectedPosition: 1 },
+ { input: '[', expectedPosition: 0 },
+ ]
+
+ for (const { input, expectedPosition } of cases) {
+ expect(extractJsonErrorPosition(getErrorMessage(input), input.length)).toBe(
+ expectedPosition,
+ )
+ }
+})
+
+describe('transform', () => {
+ const transform = (
+ input: string,
+ opts: Required,
+ isBuild: boolean,
+ ) => {
+ const plugin = jsonPlugin(opts, isBuild)
+ // @ts-expect-error transform.handler should exist
+ return plugin.transform.handler(input, 'test.json').code
+ }
+
+ test("namedExports: true, stringify: 'auto' should not transformed an array input", () => {
+ const actualSmall = transform(
+ '[{"a":1,"b":2}]',
+ { namedExports: true, stringify: 'auto' },
+ false,
+ )
+ expect(actualSmall).toMatchInlineSnapshot(`
+"export default [
+ {
+ a: 1,
+ b: 2
+ }
+];"
+ `)
+ })
+
+ test('namedExports: true, stringify: true should not transformed an array input', () => {
+ const actualSmall = transform(
+ '[{"a":1,"b":2}]',
+ { namedExports: true, stringify: true },
+ false,
+ )
+ expect(actualSmall).toMatchInlineSnapshot(
+ `"export default /* #__PURE__ */ JSON.parse("[{\\"a\\":1,\\"b\\":2}]")"`,
+ )
+ })
+
+ test('namedExports: true, stringify: false', () => {
+ const actual = transform(
+ '{"a":1,\n"🫠": "",\n"const": false}',
+ { namedExports: true, stringify: false },
+ false,
+ )
+ expect(actual).toMatchInlineSnapshot(`
+ "export const a = 1;
+ export default {
+ a: a,
+ "🫠": "",
+ "const": false
+ };
+ "
+ `)
+ })
+
+ test('namedExports: false, stringify: false', () => {
+ const actual = transform(
+ '{"a":1,\n"🫠": "",\n"const": false}',
+ { namedExports: false, stringify: false },
+ false,
+ )
+ expect(actual).toMatchInlineSnapshot(`
+ "export default {
+ a: 1,
+ "🫠": "",
+ "const": false
+ };"
+ `)
+ })
+
+ test('namedExports: true, stringify: true', () => {
+ const actual = transform(
+ '{"a":1,\n"🫠": "",\n"const": false}',
+ { namedExports: true, stringify: true },
+ false,
+ )
+ expect(actual).toMatchInlineSnapshot(`
+ "export const a = 1;
+ export default {
+ a,
+ "🫠": "",
+ "const": false,
+ };
+ "
+ `)
+ })
+
+ test('namedExports: false, stringify: true', () => {
+ const actualDev = transform(
+ '{"a":1,\n"🫠": "",\n"const": false}',
+ { namedExports: false, stringify: true },
+ false,
+ )
+ expect(actualDev).toMatchInlineSnapshot(
+ `"export default /* #__PURE__ */ JSON.parse("{\\"a\\":1,\\n\\"🫠\\": \\"\\",\\n\\"const\\": false}")"`,
+ )
+
+ const actualBuild = transform(
+ '{"a":1,\n"🫠": "",\n"const": false}',
+ { namedExports: false, stringify: true },
+ true,
+ )
+ expect(actualBuild).toMatchInlineSnapshot(
+ `"export default /* #__PURE__ */ JSON.parse("{\\"a\\":1,\\"🫠\\":\\"\\",\\"const\\":false}")"`,
+ )
+ })
+
+ test("namedExports: true, stringify: 'auto'", () => {
+ const actualSmall = transform(
+ '{"a":1,\n"🫠": "",\n"const": false}',
+ { namedExports: true, stringify: 'auto' },
+ false,
+ )
+ expect(actualSmall).toMatchInlineSnapshot(`
+ "export const a = 1;
+ export default {
+ a,
+ "🫠": "",
+ "const": false,
+ };
+ "
+ `)
+ const actualLargeNonObject = transform(
+ `{"a":1,\n"🫠": "${'vite'.repeat(3000)}",\n"const": false}`,
+ { namedExports: true, stringify: 'auto' },
+ false,
+ )
+ expect(actualLargeNonObject).not.toContain('JSON.parse(')
+
+ const actualLarge = transform(
+ `{"a":1,\n"🫠": {\n"foo": "${'vite'.repeat(3000)}"\n},\n"const": false}`,
+ { namedExports: true, stringify: 'auto' },
+ false,
+ )
+ expect(actualLarge).toContain('JSON.parse(')
+ })
+})
diff --git a/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/__snapshots__/modulePreloadPolyfill.spec.ts.snap b/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/__snapshots__/modulePreloadPolyfill.spec.ts.snap
new file mode 100644
index 00000000000000..d00d19e409978c
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/__snapshots__/modulePreloadPolyfill.spec.ts.snap
@@ -0,0 +1,47 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`load > doesn't load modulepreload polyfill when format is cjs 1`] = `
+""use strict";
+"
+`;
+
+exports[`load > loads modulepreload polyfill 1`] = `
+"(function polyfill() {
+ const relList = document.createElement("link").relList;
+ if (relList && relList.supports && relList.supports("modulepreload")) {
+ return;
+ }
+ for (const link of document.querySelectorAll('link[rel="modulepreload"]')) {
+ processPreload(link);
+ }
+ new MutationObserver((mutations) => {
+ for (const mutation of mutations) {
+ if (mutation.type !== "childList") {
+ continue;
+ }
+ for (const node of mutation.addedNodes) {
+ if (node.tagName === "LINK" && node.rel === "modulepreload")
+ processPreload(node);
+ }
+ }
+ }).observe(document, { childList: true, subtree: true });
+ function getFetchOpts(link) {
+ const fetchOpts = {};
+ if (link.integrity) fetchOpts.integrity = link.integrity;
+ if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy;
+ if (link.crossOrigin === "use-credentials")
+ fetchOpts.credentials = "include";
+ else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit";
+ else fetchOpts.credentials = "same-origin";
+ return fetchOpts;
+ }
+ function processPreload(link) {
+ if (link.ep)
+ return;
+ link.ep = true;
+ const fetchOpts = getFetchOpts(link);
+ fetch(link.href, fetchOpts);
+ }
+})();
+"
+`;
diff --git a/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/modulePreloadPolyfill.spec.ts b/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/modulePreloadPolyfill.spec.ts
new file mode 100644
index 00000000000000..3b24fbd5203baa
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/modulePreloadPolyfill.spec.ts
@@ -0,0 +1,53 @@
+import { describe, it } from 'vitest'
+import type { ModuleFormat, RollupOutput } from 'rollup'
+import { build } from '../../../build'
+import { modulePreloadPolyfillId } from '../../../plugins/modulePreloadPolyfill'
+
+const buildProject = ({ format = 'es' as ModuleFormat } = {}) =>
+ build({
+ logLevel: 'silent',
+ build: {
+ write: false,
+ rollupOptions: {
+ input: 'main.js',
+ output: {
+ format,
+ },
+ treeshake: {
+ moduleSideEffects: false,
+ },
+ },
+ minify: false,
+ },
+ plugins: [
+ {
+ name: 'test',
+ resolveId(id) {
+ if (id === 'main.js') {
+ return `\0${id}`
+ }
+ },
+ load(id) {
+ if (id === '\0main.js') {
+ return `import '${modulePreloadPolyfillId}'`
+ }
+ },
+ },
+ ],
+ }) as Promise
+
+describe('load', () => {
+ it('loads modulepreload polyfill', async ({ expect }) => {
+ const { output } = await buildProject()
+ expect(output).toHaveLength(1)
+ expect(output[0].code).toMatchSnapshot()
+ })
+
+ it("doesn't load modulepreload polyfill when format is cjs", async ({
+ expect,
+ }) => {
+ const { output } = await buildProject({ format: 'cjs' })
+ expect(output).toHaveLength(1)
+ expect(output[0].code).toMatchSnapshot()
+ })
+})
diff --git a/packages/vite/src/node/__tests__/plugins/pluginFilter.spec.ts b/packages/vite/src/node/__tests__/plugins/pluginFilter.spec.ts
new file mode 100644
index 00000000000000..934ff5781eaecf
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/pluginFilter.spec.ts
@@ -0,0 +1,310 @@
+import util from 'node:util'
+import path from 'node:path'
+import { describe, expect, test } from 'vitest'
+import {
+ createCodeFilter,
+ createFilterForTransform,
+ createIdFilter,
+} from '../../plugins/pluginFilter'
+
+describe('createIdFilter', () => {
+ const filters = [
+ { inputFilter: undefined, cases: undefined },
+ {
+ inputFilter: 'foo.js',
+ cases: [
+ { id: 'foo.js', expected: true },
+ { id: 'foo.ts', expected: false },
+ { id: '\0foo.js', expected: false },
+ { id: '\0' + path.resolve('foo.js'), expected: false },
+ ],
+ },
+ {
+ inputFilter: ['foo.js'],
+ cases: [
+ { id: 'foo.js', expected: true },
+ { id: 'foo.ts', expected: false },
+ ],
+ },
+ {
+ inputFilter: { include: 'foo.js' },
+ cases: [
+ { id: 'foo.js', expected: true },
+ { id: 'foo.ts', expected: false },
+ ],
+ },
+ {
+ inputFilter: { include: '*.js' },
+ cases: [
+ { id: 'foo.js', expected: true },
+ { id: 'foo.ts', expected: false },
+ ],
+ },
+ {
+ inputFilter: { include: /\.js$/ },
+ cases: [
+ { id: 'foo.js', expected: true },
+ { id: 'foo.ts', expected: false },
+ ],
+ },
+ {
+ inputFilter: { include: /\/foo\.js$/ },
+ cases: [
+ { id: 'a/foo.js', expected: true },
+ ...(process.platform === 'win32'
+ ? [{ id: 'a\\foo.js', expected: true }]
+ : []),
+ { id: 'a_foo.js', expected: false },
+ ],
+ },
+ {
+ inputFilter: { include: [/\.js$/] },
+ cases: [
+ { id: 'foo.js', expected: true },
+ { id: 'foo.ts', expected: false },
+ ],
+ },
+ {
+ inputFilter: { exclude: 'foo.js' },
+ cases: [
+ { id: 'foo.js', expected: false },
+ { id: 'foo.ts', expected: true },
+ ],
+ },
+ {
+ inputFilter: { exclude: '*.js' },
+ cases: [
+ { id: 'foo.js', expected: false },
+ { id: 'foo.ts', expected: true },
+ ],
+ },
+ {
+ inputFilter: { exclude: /\.js$/ },
+ cases: [
+ { id: 'foo.js', expected: false },
+ { id: 'foo.ts', expected: true },
+ ],
+ },
+ {
+ inputFilter: { exclude: [/\.js$/] },
+ cases: [
+ { id: 'foo.js', expected: false },
+ { id: 'foo.ts', expected: true },
+ ],
+ },
+ {
+ inputFilter: { include: 'foo.js', exclude: 'bar.js' },
+ cases: [
+ { id: 'foo.js', expected: true },
+ { id: 'bar.js', expected: false },
+ { id: 'baz.js', expected: false },
+ ],
+ },
+ {
+ inputFilter: { include: '*.js', exclude: 'foo.*' },
+ cases: [
+ { id: 'foo.js', expected: false }, // exclude has higher priority
+ { id: 'bar.js', expected: true },
+ { id: 'foo.ts', expected: false },
+ ],
+ },
+ {
+ inputFilter: '/virtual/foo',
+ cases: [{ id: '/virtual/foo', expected: true }],
+ },
+ ]
+
+ for (const filter of filters) {
+ test(`${util.inspect(filter.inputFilter)}`, () => {
+ const idFilter = createIdFilter(filter.inputFilter, '')
+ if (!filter.cases) {
+ expect(idFilter).toBeUndefined()
+ return
+ }
+ expect(idFilter).not.toBeUndefined()
+
+ for (const testCase of filter.cases) {
+ const { id, expected } = testCase
+ expect(idFilter!(id), id).toBe(expected)
+ }
+ })
+ }
+})
+
+describe('createCodeFilter', () => {
+ const filters = [
+ { inputFilter: undefined, cases: undefined },
+ {
+ inputFilter: 'import.meta',
+ cases: [
+ { code: 'import.meta', expected: true },
+ { code: 'import_meta', expected: false },
+ ],
+ },
+ {
+ inputFilter: ['import.meta'],
+ cases: [
+ { code: 'import.meta', expected: true },
+ { code: 'import_meta', expected: false },
+ ],
+ },
+ {
+ inputFilter: { include: 'import.meta' },
+ cases: [
+ { code: 'import.meta', expected: true },
+ { code: 'import_meta', expected: false },
+ ],
+ },
+ {
+ inputFilter: { include: /import\.\w+/ },
+ cases: [
+ { code: 'import.meta', expected: true },
+ { code: 'import_meta', expected: false },
+ ],
+ },
+ {
+ inputFilter: { include: [/import\.\w+/] },
+ cases: [
+ { code: 'import.meta', expected: true },
+ { code: 'import_meta', expected: false },
+ ],
+ },
+ {
+ inputFilter: { exclude: 'import.meta' },
+ cases: [
+ { code: 'import.meta', expected: false },
+ { code: 'import_meta', expected: true },
+ ],
+ },
+ {
+ inputFilter: { exclude: /import\.\w+/ },
+ cases: [
+ { code: 'import.meta', expected: false },
+ { code: 'import_meta', expected: true },
+ ],
+ },
+ {
+ inputFilter: { exclude: [/import\.\w+/] },
+ cases: [
+ { code: 'import.meta', expected: false },
+ { code: 'import_meta', expected: true },
+ ],
+ },
+ {
+ inputFilter: { include: 'import.meta', exclude: 'import_meta' },
+ cases: [
+ { code: 'import.meta', expected: true },
+ { code: 'import_meta', expected: false },
+ { code: 'importmeta', expected: false },
+ ],
+ },
+ {
+ inputFilter: { include: /import\.\w+/, exclude: /\w+\.meta/ },
+ cases: [
+ { code: 'import.meta', expected: false }, // exclude has higher priority
+ { code: 'import.foo', expected: true },
+ { code: 'foo.meta', expected: false },
+ ],
+ },
+ ]
+
+ for (const filter of filters) {
+ test(`${util.inspect(filter.inputFilter)}`, () => {
+ const codeFilter = createCodeFilter(filter.inputFilter)
+ if (!filter.cases) {
+ expect(codeFilter).toBeUndefined()
+ return
+ }
+ expect(codeFilter).not.toBeUndefined()
+
+ for (const testCase of filter.cases) {
+ const { code, expected } = testCase
+ expect(codeFilter!(code), code).toBe(expected)
+ }
+ })
+ }
+})
+
+describe('createFilterForTransform', () => {
+ const filters = [
+ { inputFilter: [undefined, undefined], cases: undefined },
+ {
+ inputFilter: ['*.js', undefined],
+ cases: [
+ { id: 'foo.js', code: 'foo', expected: true },
+ { id: 'foo.ts', code: 'foo', expected: false },
+ ],
+ },
+ {
+ inputFilter: [undefined, 'import.meta'],
+ cases: [
+ { id: 'foo.js', code: 'import.meta', expected: true },
+ { id: 'foo.js', code: 'import_meta', expected: false },
+ ],
+ },
+ {
+ inputFilter: [{ exclude: '*.js' }, 'import.meta'],
+ cases: [
+ { id: 'foo.js', code: 'import.meta', expected: false },
+ { id: 'foo.js', code: 'import_meta', expected: false },
+ { id: 'foo.ts', code: 'import.meta', expected: true },
+ { id: 'foo.ts', code: 'import_meta', expected: false },
+ ],
+ },
+ {
+ inputFilter: [{ include: 'foo.ts', exclude: '*.js' }, 'import.meta'],
+ cases: [
+ { id: 'foo.js', code: 'import.meta', expected: false },
+ { id: 'foo.js', code: 'import_meta', expected: false },
+ { id: 'foo.ts', code: 'import.meta', expected: true },
+ { id: 'foo.ts', code: 'import_meta', expected: false },
+ ],
+ },
+ {
+ inputFilter: [
+ { include: 'a*', exclude: '*b' },
+ { include: 'a', exclude: 'b' },
+ ],
+ cases: [
+ { id: 'ab', code: '', expected: false },
+ { id: 'a', code: 'b', expected: false },
+ { id: 'a', code: '', expected: false },
+ { id: 'c', code: 'a', expected: false },
+ { id: 'a', code: 'a', expected: true },
+ ],
+ },
+ {
+ inputFilter: [{ include: 'a*', exclude: '*b' }, { exclude: 'b' }],
+ cases: [
+ { id: 'ab', code: '', expected: false },
+ { id: 'a', code: 'b', expected: false },
+ { id: 'a', code: '', expected: true },
+ { id: 'c', code: 'a', expected: false },
+ { id: 'a', code: 'a', expected: true },
+ ],
+ },
+ ]
+
+ for (const filter of filters) {
+ test(`${util.inspect(filter.inputFilter)}`, () => {
+ const [idFilter, codeFilter] = filter.inputFilter
+ const filterForTransform = createFilterForTransform(
+ idFilter,
+ codeFilter,
+ '',
+ )
+ if (!filter.cases) {
+ expect(filterForTransform).toBeUndefined()
+ return
+ }
+ expect(filterForTransform).not.toBeUndefined()
+
+ for (const testCase of filter.cases) {
+ const { id, code, expected } = testCase
+ expect(filterForTransform!(id, code), util.inspect({ id, code })).toBe(
+ expected,
+ )
+ }
+ })
+ }
+})
diff --git a/packages/vite/src/node/__tests__/plugins/terser.spec.ts b/packages/vite/src/node/__tests__/plugins/terser.spec.ts
new file mode 100644
index 00000000000000..9fed1ec2c5ee7e
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/terser.spec.ts
@@ -0,0 +1,55 @@
+import { resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { describe, expect, test } from 'vitest'
+import { build } from 'vite'
+import type { RollupOutput } from 'rollup'
+import type { TerserOptions } from '../../plugins/terser'
+
+const __dirname = resolve(fileURLToPath(import.meta.url), '..')
+
+describe('terser', () => {
+ const run = async (terserOptions: TerserOptions) => {
+ const result = (await build({
+ root: resolve(__dirname, '../packages/build-project'),
+ logLevel: 'silent',
+ build: {
+ write: false,
+ minify: 'terser',
+ terserOptions,
+ },
+ plugins: [
+ {
+ name: 'test',
+ resolveId(id) {
+ if (id === 'entry.js') {
+ return '\0' + id
+ }
+ },
+ load(id) {
+ if (id === '\0entry.js') {
+ return `const foo = 1;console.log(foo)`
+ }
+ },
+ },
+ ],
+ })) as RollupOutput
+ return result.output[0].code
+ }
+
+ test('basic', async () => {
+ await run({})
+ })
+
+ test('nth', async () => {
+ const resultCode = await run({
+ mangle: {
+ nth_identifier: {
+ get: (n) => {
+ return 'prefix_' + n.toString()
+ },
+ },
+ },
+ })
+ expect(resultCode).toContain('prefix_')
+ })
+})
diff --git a/packages/vite/src/node/__tests__/plugins/workerImportMetaUrl.spec.ts b/packages/vite/src/node/__tests__/plugins/workerImportMetaUrl.spec.ts
new file mode 100644
index 00000000000000..1c35a178ed2ade
--- /dev/null
+++ b/packages/vite/src/node/__tests__/plugins/workerImportMetaUrl.spec.ts
@@ -0,0 +1,220 @@
+import { describe, expect, test } from 'vitest'
+import { parseAst } from 'rollup/parseAst'
+import { workerImportMetaUrlPlugin } from '../../plugins/workerImportMetaUrl'
+import { resolveConfig } from '../../config'
+import { PartialEnvironment } from '../../baseEnvironment'
+
+async function createWorkerImportMetaUrlPluginTransform() {
+ const config = await resolveConfig({ configFile: false }, 'serve')
+ const instance = workerImportMetaUrlPlugin(config)
+ const environment = new PartialEnvironment('client', config)
+
+ return async (code: string) => {
+ // @ts-expect-error transform.handler should exist
+ const result = await instance.transform.handler.call(
+ { environment, parse: parseAst },
+ code,
+ 'foo.ts',
+ )
+ return result?.code || result
+ }
+}
+
+describe('workerImportMetaUrlPlugin', async () => {
+ const transform = await createWorkerImportMetaUrlPluginTransform()
+
+ test('without worker options', async () => {
+ expect(
+ await transform('new Worker(new URL("./worker.js", import.meta.url))'),
+ ).toMatchInlineSnapshot(
+ `"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url))"`,
+ )
+ })
+
+ test('with shared worker', async () => {
+ expect(
+ await transform(
+ 'new SharedWorker(new URL("./worker.js", import.meta.url))',
+ ),
+ ).toMatchInlineSnapshot(
+ `"new SharedWorker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url))"`,
+ )
+ })
+
+ test('with static worker options and identifier properties', async () => {
+ expect(
+ await transform(
+ 'new Worker(new URL("./worker.js", import.meta.url), { type: "module", name: "worker1" })',
+ ),
+ ).toMatchInlineSnapshot(
+ `"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { type: "module", name: "worker1" })"`,
+ )
+ })
+
+ test('with static worker options and literal properties', async () => {
+ expect(
+ await transform(
+ 'new Worker(new URL("./worker.js", import.meta.url), { "type": "module", "name": "worker1" })',
+ ),
+ ).toMatchInlineSnapshot(
+ `"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { "type": "module", "name": "worker1" })"`,
+ )
+ })
+
+ test('with dynamic name field in worker options', async () => {
+ expect(
+ await transform(
+ 'const id = 1; new Worker(new URL("./worker.js", import.meta.url), { name: "worker" + id })',
+ ),
+ ).toMatchInlineSnapshot(
+ `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url), { name: "worker" + id })"`,
+ )
+ })
+
+ test('with interpolated dynamic name field in worker options', async () => {
+ expect(
+ await transform(
+ 'const id = 1; new Worker(new URL("./worker.js", import.meta.url), { name: `worker-${id}` })',
+ ),
+ ).toMatchInlineSnapshot(
+ `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url), { name: \`worker-\${id}\` })"`,
+ )
+ })
+
+ test('with dynamic name field and static type in worker options', async () => {
+ expect(
+ await transform(
+ 'const id = 1; new Worker(new URL("./worker.js", import.meta.url), { name: "worker" + id, type: "module" })',
+ ),
+ ).toMatchInlineSnapshot(
+ `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { name: "worker" + id, type: "module" })"`,
+ )
+ })
+
+ test('with interpolated dynamic name field and static type in worker options', async () => {
+ expect(
+ await transform(
+ 'const id = 1; new Worker(new URL("./worker.js", import.meta.url), { name: `worker-${id}`, type: "module" })',
+ ),
+ ).toMatchInlineSnapshot(
+ `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { name: \`worker-\${id}\`, type: "module" })"`,
+ )
+ })
+
+ test('with parenthesis inside of worker options', async () => {
+ expect(
+ await transform(
+ 'const worker = new Worker(new URL("./worker.js", import.meta.url), { name: genName(), type: "module"})',
+ ),
+ ).toMatchInlineSnapshot(
+ `"const worker = new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { name: genName(), type: "module"})"`,
+ )
+ })
+
+ test('with multi-line code and worker options', async () => {
+ expect(
+ await transform(`
+const worker = new Worker(new URL("./worker.js", import.meta.url), {
+ name: genName(),
+ type: "module",
+ },
+)
+
+worker.addEventListener('message', (ev) => text('.simple-worker-url', JSON.stringify(ev.data)))
+`),
+ ).toMatchInlineSnapshot(`"
+const worker = new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), {
+ name: genName(),
+ type: "module",
+ },
+)
+
+worker.addEventListener('message', (ev) => text('.simple-worker-url', JSON.stringify(ev.data)))
+"`)
+ })
+
+ test('trailing comma', async () => {
+ expect(
+ await transform(`
+new Worker(
+ new URL('./worker.js', import.meta.url),
+ {
+ type: 'module'
+ }, // },
+)
+`),
+ ).toMatchInlineSnapshot(`"
+new Worker(
+ new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url),
+ {
+ type: 'module'
+ }, // },
+)
+"`)
+ })
+
+ test('throws an error when non-static worker options are provided', async () => {
+ await expect(
+ transform(
+ 'new Worker(new URL("./worker.js", import.meta.url), myWorkerOptions)',
+ ),
+ ).rejects.toThrow(
+ 'Vite is unable to parse the worker options as the value is not static. To ignore this error, please use /* @vite-ignore */ in the worker options.',
+ )
+ })
+
+ test('throws an error when worker options are not an object', async () => {
+ await expect(
+ transform(
+ 'new Worker(new URL("./worker.js", import.meta.url), "notAnObject")',
+ ),
+ ).rejects.toThrow('Expected worker options to be an object, got string')
+ })
+
+ test('throws an error when non-literal type field in worker options', async () => {
+ await expect(
+ transform(
+ 'const type = "module"; new Worker(new URL("./worker.js", import.meta.url), { type })',
+ ),
+ ).rejects.toThrow(
+ 'Expected worker options type property to be a literal value.',
+ )
+ })
+
+ test('throws an error when spread operator used without the type field', async () => {
+ await expect(
+ transform(
+ 'const options = { name: "worker1" }; new Worker(new URL("./worker.js", import.meta.url), { ...options })',
+ ),
+ ).rejects.toThrow(
+ 'Expected object spread to be used before the definition of the type property. Vite needs a static value for the type property to correctly infer it.',
+ )
+ })
+
+ test('throws an error when spread operator used after definition of type field', async () => {
+ await expect(
+ transform(
+ 'const options = { name: "worker1" }; new Worker(new URL("./worker.js", import.meta.url), { type: "module", ...options })',
+ ),
+ ).rejects.toThrow(
+ 'Expected object spread to be used before the definition of the type property. Vite needs a static value for the type property to correctly infer it.',
+ )
+ })
+
+ test('find closing parenthesis correctly', async () => {
+ expect(
+ await transform(
+ `(() => { new Worker(new URL('./worker', import.meta.url)); repro({ test: "foo", }); })();`,
+ ),
+ ).toMatchInlineSnapshot(
+ `"(() => { new Worker(new URL(/* @vite-ignore */ "/worker?worker_file&type=classic", import.meta.url)); repro({ test: "foo", }); })();"`,
+ )
+ expect(
+ await transform(
+ `repro(new Worker(new URL('./worker', import.meta.url)), { type: "module" })`,
+ ),
+ ).toMatchInlineSnapshot(
+ `"repro(new Worker(new URL(/* @vite-ignore */ "/worker?worker_file&type=classic", import.meta.url)), { type: "module" })"`,
+ )
+ })
+})
diff --git a/packages/vite/src/node/__tests__/resolve.spec.ts b/packages/vite/src/node/__tests__/resolve.spec.ts
new file mode 100644
index 00000000000000..f089a1518a1f6e
--- /dev/null
+++ b/packages/vite/src/node/__tests__/resolve.spec.ts
@@ -0,0 +1,312 @@
+import { join } from 'node:path'
+import { describe, expect, onTestFinished, test, vi } from 'vitest'
+import { createServer } from '../server'
+import { createServerModuleRunner } from '../ssr/runtime/serverModuleRunner'
+import type { EnvironmentOptions, InlineConfig } from '../config'
+import { build } from '../build'
+
+describe('import and resolveId', () => {
+ async function createTestServer() {
+ const server = await createServer({
+ configFile: false,
+ root: import.meta.dirname,
+ logLevel: 'error',
+ server: {
+ middlewareMode: true,
+ },
+ })
+ onTestFinished(() => server.close())
+ const runner = createServerModuleRunner(server.environments.ssr, {
+ hmr: {
+ logger: false,
+ },
+ sourcemapInterceptor: false,
+ })
+ return { server, runner }
+ }
+
+ test('import first', async () => {
+ const { server, runner } = await createTestServer()
+ const mod = await runner.import(
+ '/fixtures/test-dep-conditions-app/entry-with-module',
+ )
+ const resolved = await server.environments.ssr.pluginContainer.resolveId(
+ '@vitejs/test-dep-conditions/with-module',
+ )
+ expect([mod.default, resolved?.id]).toEqual([
+ 'dir/index.default.js',
+ expect.stringContaining('dir/index.module.js'),
+ ])
+ })
+
+ test('resolveId first', async () => {
+ const { server, runner } = await createTestServer()
+ const resolved = await server.environments.ssr.pluginContainer.resolveId(
+ '@vitejs/test-dep-conditions/with-module',
+ )
+ const mod = await runner.import(
+ '/fixtures/test-dep-conditions-app/entry-with-module',
+ )
+ expect([mod.default, resolved?.id]).toEqual([
+ 'dir/index.default.js',
+ expect.stringContaining('dir/index.module.js'),
+ ])
+ })
+})
+
+describe('file url', () => {
+ const fileUrl = new URL('./fixtures/file-url/entry.js', import.meta.url)
+
+ function getConfig(): InlineConfig {
+ return {
+ configFile: false,
+ root: join(import.meta.dirname, 'fixtures/file-url'),
+ logLevel: 'error',
+ server: {
+ middlewareMode: true,
+ },
+ plugins: [
+ {
+ name: 'virtual-file-url',
+ resolveId(source) {
+ if (source.startsWith('virtual:test-dep/')) {
+ return '\0' + source
+ }
+ },
+ load(id) {
+ if (id === '\0virtual:test-dep/static') {
+ return `
+ import * as dep from ${JSON.stringify(fileUrl.href)};
+ export default dep;
+ `
+ }
+ if (id === '\0virtual:test-dep/static-postfix') {
+ return `
+ import * as dep from ${JSON.stringify(fileUrl.href + '?query=test')};
+ export default dep;
+ `
+ }
+ if (id === '\0virtual:test-dep/non-static') {
+ return `
+ const dep = await import(/* @vite-ignore */ String(${JSON.stringify(fileUrl.href)}));
+ export default dep;
+ `
+ }
+ if (id === '\0virtual:test-dep/non-static-postfix') {
+ return `
+ const dep = await import(/* @vite-ignore */ String(${JSON.stringify(fileUrl.href + '?query=test')}));
+ export default dep;
+ `
+ }
+ },
+ },
+ ],
+ }
+ }
+
+ test('dev', async () => {
+ const server = await createServer(getConfig())
+ onTestFinished(() => server.close())
+
+ const runner = createServerModuleRunner(server.environments.ssr, {
+ hmr: {
+ logger: false,
+ },
+ sourcemapInterceptor: false,
+ })
+
+ const mod = await runner.import('/entry.js')
+ expect(mod.default).toEqual('ok')
+
+ const mod2 = await runner.import(fileUrl.href)
+ expect(mod2).toBe(mod)
+
+ const mod3 = await runner.import('virtual:test-dep/static')
+ expect(mod3.default).toBe(mod)
+
+ const mod4 = await runner.import('virtual:test-dep/non-static')
+ expect(mod4.default).toBe(mod)
+
+ const mod5 = await runner.import(fileUrl.href + '?query=test')
+ expect(mod5).toEqual(mod)
+ expect(mod5).not.toBe(mod)
+
+ const mod6 = await runner.import('virtual:test-dep/static-postfix')
+ expect(mod6.default).toEqual(mod)
+ expect(mod6.default).not.toBe(mod)
+ expect(mod6.default).toBe(mod5)
+
+ const mod7 = await runner.import('virtual:test-dep/non-static-postfix')
+ expect(mod7.default).toEqual(mod)
+ expect(mod7.default).not.toBe(mod)
+ expect(mod7.default).toBe(mod5)
+ })
+
+ describe('environment builtins', () => {
+ function getConfig(
+ targetEnv: 'client' | 'ssr' | string,
+ builtins: NonNullable['builtins'],
+ ): InlineConfig {
+ return {
+ configFile: false,
+ root: join(import.meta.dirname, 'fixtures/file-url'),
+ logLevel: 'error',
+ server: {
+ middlewareMode: true,
+ },
+ environments: {
+ [targetEnv]: {
+ resolve: {
+ builtins,
+ },
+ },
+ },
+ }
+ }
+
+ async function run({
+ builtins,
+ targetEnv = 'custom',
+ testEnv = 'custom',
+ idToResolve,
+ }: {
+ builtins?: NonNullable['builtins']
+ targetEnv?: 'client' | 'ssr' | string
+ testEnv?: 'client' | 'ssr' | string
+ idToResolve: string
+ }) {
+ const server = await createServer(getConfig(targetEnv, builtins))
+ vi.spyOn(server.config.logger, 'warn').mockImplementationOnce(
+ (message) => {
+ throw new Error(message)
+ },
+ )
+ onTestFinished(() => server.close())
+
+ return server.environments[testEnv]?.pluginContainer.resolveId(
+ idToResolve,
+ )
+ }
+
+ test('declared builtin string', async () => {
+ const resolved = await run({
+ builtins: ['my-env:custom-builtin'],
+ idToResolve: 'my-env:custom-builtin',
+ })
+ expect(resolved?.external).toBe(true)
+ })
+
+ test('declared builtin regexp', async () => {
+ const resolved = await run({
+ builtins: [/^my-env:\w/],
+ idToResolve: 'my-env:custom-builtin',
+ })
+ expect(resolved?.external).toBe(true)
+ })
+
+ test('non declared builtin', async () => {
+ const resolved = await run({
+ builtins: [
+ /* empty */
+ ],
+ idToResolve: 'my-env:custom-builtin',
+ })
+ expect(resolved).toBeNull()
+ })
+
+ test('non declared node builtin', async () => {
+ await expect(
+ run({
+ builtins: [
+ /* empty */
+ ],
+ idToResolve: 'node:fs',
+ }),
+ ).rejects.toThrowError(
+ /warning: Automatically externalized node built-in module "node:fs"/,
+ )
+ })
+
+ test('default to node-like builtins', async () => {
+ const resolved = await run({
+ idToResolve: 'node:fs',
+ })
+ expect(resolved?.external).toBe(true)
+ })
+
+ test('default to node-like builtins for ssr environment', async () => {
+ const resolved = await run({
+ idToResolve: 'node:fs',
+ testEnv: 'ssr',
+ })
+ expect(resolved?.external).toBe(true)
+ })
+
+ test('no default to node-like builtins for client environment', async () => {
+ const resolved = await run({
+ idToResolve: 'node:fs',
+ testEnv: 'client',
+ })
+ expect(resolved?.id).toEqual('__vite-browser-external:node:fs')
+ })
+
+ test('no builtins overriding for client environment', async () => {
+ const resolved = await run({
+ idToResolve: 'node:fs',
+ testEnv: 'client',
+ targetEnv: 'client',
+ })
+ expect(resolved?.id).toEqual('__vite-browser-external:node:fs')
+ })
+
+ test('declared node builtin', async () => {
+ const resolved = await run({
+ builtins: [/^node:/],
+ idToResolve: 'node:fs',
+ })
+ expect(resolved?.external).toBe(true)
+ })
+
+ test('declared builtin string in different environment', async () => {
+ const resolved = await run({
+ builtins: ['my-env:custom-builtin'],
+ idToResolve: 'my-env:custom-builtin',
+ targetEnv: 'custom',
+ testEnv: 'ssr',
+ })
+ expect(resolved).toBe(null)
+ })
+ })
+
+ test('build', async () => {
+ await build({
+ ...getConfig(),
+ build: {
+ ssr: true,
+ outDir: 'dist/basic',
+ rollupOptions: {
+ input: { index: fileUrl.href },
+ },
+ },
+ })
+ const mod1 = await import(
+ join(import.meta.dirname, 'fixtures/file-url/dist/basic/index.js')
+ )
+ expect(mod1.default).toBe('ok')
+
+ await build({
+ ...getConfig(),
+ build: {
+ ssr: true,
+ outDir: 'dist/virtual',
+ rollupOptions: {
+ input: { index: 'virtual:test-dep/static' },
+ },
+ },
+ })
+ const mod2 = await import(
+ join(import.meta.dirname, 'fixtures/file-url/dist/virtual/index.js')
+ )
+ expect(mod2.default.default).toBe('ok')
+ })
+})
diff --git a/packages/vite/src/node/__tests__/runnerImport.spec.ts b/packages/vite/src/node/__tests__/runnerImport.spec.ts
new file mode 100644
index 00000000000000..d6084b84bbbf30
--- /dev/null
+++ b/packages/vite/src/node/__tests__/runnerImport.spec.ts
@@ -0,0 +1,73 @@
+import { resolve } from 'node:path'
+import { describe, expect, test } from 'vitest'
+import { loadConfigFromFile } from 'vite'
+import { runnerImport } from '../ssr/runnerImport'
+import { slash } from '../../shared/utils'
+
+describe('importing files using inlined environment', () => {
+ const fixture = (name: string) =>
+ resolve(import.meta.dirname, './fixtures/runner-import', name)
+
+ test('importing a basic file works', async () => {
+ const { module } = await runnerImport<
+ typeof import('./fixtures/runner-import/basic')
+ >(fixture('basic'))
+ expect(module.test).toEqual({
+ field: true,
+ })
+ })
+
+ test("cannot import cjs, 'runnerImport' doesn't support CJS syntax at all", async () => {
+ await expect(() =>
+ runnerImport(
+ fixture('cjs.js'),
+ ),
+ ).rejects.toThrow('module is not defined')
+ })
+
+ test('can import vite config', async () => {
+ const { module, dependencies } = await runnerImport<
+ typeof import('./fixtures/runner-import/vite.config')
+ >(fixture('vite.config'))
+ expect(module.default).toEqual({
+ root: './test',
+ plugins: [
+ {
+ name: 'test',
+ },
+ ],
+ })
+ expect(dependencies).toEqual([slash(fixture('plugin.ts'))])
+ })
+
+ test('can import vite config that imports a TS external module', async () => {
+ const { module, dependencies } = await runnerImport<
+ typeof import('./fixtures/runner-import/vite.config.outside-pkg-import.mjs')
+ >(fixture('vite.config.outside-pkg-import.mts'))
+
+ expect(module.default.__injected).toBe(true)
+ expect(dependencies).toEqual([
+ slash(resolve(import.meta.dirname, './packages/parent/index.ts')),
+ ])
+
+ // confirm that it fails with a bundle approach
+ await expect(async () => {
+ const root = resolve(import.meta.dirname, './fixtures/runner-import')
+ await loadConfigFromFile(
+ { mode: 'production', command: 'serve' },
+ resolve(root, './vite.config.outside-pkg-import.mts'),
+ root,
+ 'silent',
+ )
+ }).rejects.toThrow('Unknown file extension ".ts"')
+ })
+
+ test('dynamic import', async () => {
+ const { module } = await runnerImport(fixture('dynamic-import.ts'))
+ await expect(() => module.default()).rejects.toMatchInlineSnapshot(
+ `[Error: Vite module runner has been closed.]`,
+ )
+ // const dep = await module.default();
+ // expect(dep.default).toMatchInlineSnapshot(`"ok"`)
+ })
+})
diff --git a/packages/vite/src/node/__tests__/scan.spec.ts b/packages/vite/src/node/__tests__/scan.spec.ts
index db11bcc45b284c..6fa9f76d1ceac4 100644
--- a/packages/vite/src/node/__tests__/scan.spec.ts
+++ b/packages/vite/src/node/__tests__/scan.spec.ts
@@ -1,4 +1,5 @@
-import { scriptRE, commentRE, importsRE } from '../optimizer/scan'
+import { describe, expect, test } from 'vitest'
+import { commentRE, importsRE, scriptRE } from '../optimizer/scan'
import { multilineCommentsRE, singlelineCommentsRE } from '../utils'
describe('optimizer-scan:script-test', () => {
@@ -13,15 +14,15 @@ describe('optimizer-scan:script-test', () => {
test('component return value test', () => {
scriptRE.lastIndex = 0
const [, tsOpenTag, tsContent] = scriptRE.exec(
- ``
- )
+ ``,
+ )!
expect(tsOpenTag).toEqual('`
- )
+ ``,
+ )!
expect(openTag).toEqual(' -->
- `.replace(commentRE, '')
+ `.replace(commentRE, ''),
)
expect(ret).toEqual(null)
})
@@ -44,25 +45,29 @@ describe('optimizer-scan:script-test', () => {
scriptRE.lastIndex = 0
ret = scriptRE.exec(
- ` `
+ ` `,
)
expect(ret).toBe(null)
scriptRE.lastIndex = 0
ret = scriptRE.exec(
- ` content `
+ ` content `,
)
expect(ret).toBe(null)
})
test('ordinary script tag test', () => {
scriptRE.lastIndex = 0
- const [, tag, content] = scriptRE.exec(``)
+ const [, tag, content] = scriptRE.exec(
+ ``,
+ )!
expect(tag).toEqual('`)
+ const [, tag1, content1] = scriptRE.exec(
+ ``,
+ )!
expect(tag1).toEqual('
- const filePath = id.replace(normalizePath(config.root), '')
- addToHTMLProxyCache(
- config,
- filePath,
- inlineModuleIndex,
- contents
- )
- js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}.js"`
- shouldRemove = true
- }
+ const assetUrlsPromises: Promise[] = []
- everyScriptIsAsync &&= isAsync
- someScriptsAreAsync ||= isAsync
- someScriptsAreDefer ||= !isAsync
- } else if (url && !isPublicFile) {
- if (!isExcludedUrl(url)) {
- config.logger.warn(
- `
+ const filePath = id.replace(normalizePath(config.root), '')
+ addToHTMLProxyCache(config, filePath, inlineModuleIndex, {
+ code: contents,
+ })
+ js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}.js"`
+ shouldRemove = true
}
+
+ everyScriptIsAsync &&= isAsync
+ someScriptsAreAsync ||= isAsync
+ someScriptsAreDefer ||= !isAsync
+ } else if (url && !isPublicFile) {
+ if (!isExcludedUrl(url)) {
+ config.logger.warn(
+ ` asset
+ for (const { start, end, url } of scriptUrls) {
+ if (checkPublicFile(url, config)) {
+ s.update(
+ start,
+ end,
+ partialEncodeURIPath(toOutputPublicFilePath(url)),
+ )
+ } else if (!isExcludedUrl(url)) {
+ s.update(
+ start,
+ end,
+ partialEncodeURIPath(await urlToBuiltUrl(this, url, id)),
+ )
+ }
+ }
- if (someScriptsAreAsync && someScriptsAreDefer) {
- config.logger.warn(
- `\nMixed async and defer script modules in ${id}, output script will fallback to defer. Every script, including inline ones, need to be marked as async for your output script to be async.`
+ // ignore if its url can't be resolved
+ const resolvedStyleUrls = await Promise.all(
+ styleUrls.map(async (styleUrl) => ({
+ ...styleUrl,
+ resolved: await this.resolve(styleUrl.url, id),
+ })),
)
- }
-
- // for each encountered asset url, rewrite original html so that it
- // references the post-build location, ignoring empty attributes and
- // attributes that directly reference named output.
- const namedOutput = Object.keys(
- config?.build?.rollupOptions?.input || {}
- )
- for (const attr of assetUrls) {
- const value = attr.value!
- // assetsUrl may be encodeURI
- const content = decodeURI(value.content)
- if (
- content !== '' && // Empty attribute
- !namedOutput.includes(content) && // Direct reference to named output
- !namedOutput.includes(content.replace(/^\//, '')) // Allow for absolute references as named output can't be an absolute path
- ) {
- try {
- const url =
- attr.name === 'srcset'
- ? await processSrcSet(content, ({ url }) =>
- urlToBuiltUrl(url, id, config, this)
- )
- : await urlToBuiltUrl(content, id, config, this)
-
- s.overwrite(
- value.loc.start.offset,
- value.loc.end.offset,
- `"${url}"`
+ for (const { start, end, url, resolved } of resolvedStyleUrls) {
+ if (resolved == null) {
+ config.logger.warnOnce(
+ `\n${url} doesn't exist at build time, it will remain unchanged to be resolved at runtime`,
)
- } catch (e) {
- if (e.code !== 'ENOENT') {
- throw e
- }
+ const importExpression = `\nimport ${JSON.stringify(url)}`
+ js = js.replace(importExpression, '')
+ } else {
+ s.remove(start, end)
}
}
- }
- // emit asset
- for (const { start, end, url } of scriptUrls) {
- if (!isExcludedUrl(url)) {
- s.overwrite(start, end, await urlToBuiltUrl(url, id, config, this))
- } else if (checkPublicFile(url, config)) {
- s.overwrite(start, end, config.base + url.slice(1))
+
+ processedHtml(this).set(id, s.toString())
+
+ // inject module preload polyfill only when configured and needed
+ const { modulePreload } = this.environment.config.build
+ if (
+ modulePreload !== false &&
+ modulePreload.polyfill &&
+ (someScriptsAreAsync || someScriptsAreDefer)
+ ) {
+ js = `import "${modulePreloadPolyfillId}";\n${js}`
}
- }
- processedHtml.set(id, s.toString())
+ await Promise.all(setModuleSideEffectPromises)
- // inject module preload polyfill only when configured and needed
- if (
- config.build.polyfillModulePreload &&
- (someScriptsAreAsync || someScriptsAreDefer)
- ) {
- js = `import "${modulePreloadPolyfillId}";\n${js}`
+ // Force rollup to keep this module from being shared between other entry points.
+ // If the resulting chunk is empty, it will be removed in generateBundle.
+ return { code: js, moduleSideEffects: 'no-treeshake' }
}
-
- return js
- }
+ },
},
async generateBundle(options, bundle) {
- const analyzedChunk: Map = new Map()
+ const analyzedImportedCssFiles = new Map()
+ const inlineEntryChunk = new Set()
const getImportedChunks = (
chunk: OutputChunk,
- seen: Set = new Set()
- ): OutputChunk[] => {
- const chunks: OutputChunk[] = []
+ seen: Set = new Set(),
+ ): (OutputChunk | string)[] => {
+ const chunks: (OutputChunk | string)[] = []
chunk.imports.forEach((file) => {
const importee = bundle[file]
- if (importee?.type === 'chunk' && !seen.has(file)) {
- seen.add(file)
+ if (importee) {
+ if (importee.type === 'chunk' && !seen.has(file)) {
+ seen.add(file)
- // post-order traversal
- chunks.push(...getImportedChunks(importee, seen))
- chunks.push(importee)
+ // post-order traversal
+ chunks.push(...getImportedChunks(importee, seen))
+ chunks.push(importee)
+ }
+ } else {
+ // external imports
+ chunks.push(file)
}
})
return chunks
}
const toScriptTag = (
- chunk: OutputChunk,
- isAsync: boolean
+ chunkOrUrl: OutputChunk | string,
+ toOutputPath: (filename: string) => string,
+ isAsync: boolean,
): HtmlTagDescriptor => ({
tag: 'script',
attrs: {
...(isAsync ? { async: true } : {}),
type: 'module',
+ // crossorigin must be set not only for serving assets in a different origin
+ // but also to make it possible to preload the script using ` `.
+ // `
diff --git a/packages/vite/src/node/server/middlewares/hostCheck.ts b/packages/vite/src/node/server/middlewares/hostCheck.ts
new file mode 100644
index 00000000000000..755c38036cfa0e
--- /dev/null
+++ b/packages/vite/src/node/server/middlewares/hostCheck.ts
@@ -0,0 +1,180 @@
+import net from 'node:net'
+import type { Connect } from 'dep-types/connect'
+import type { ResolvedConfig } from '../../config'
+import type { ResolvedPreviewOptions, ResolvedServerOptions } from '../..'
+
+const allowedHostsServerCache = new WeakMap>()
+const allowedHostsPreviewCache = new WeakMap>()
+
+const isFileOrExtensionProtocolRE = /^(?:file|.+-extension):/i
+
+export function getAdditionalAllowedHosts(
+ resolvedServerOptions: Pick,
+ resolvedPreviewOptions: Pick,
+): string[] {
+ const list = []
+
+ // allow host option by default as that indicates that the user is
+ // expecting Vite to respond on that host
+ if (
+ typeof resolvedServerOptions.host === 'string' &&
+ resolvedServerOptions.host
+ ) {
+ list.push(resolvedServerOptions.host)
+ }
+ if (
+ typeof resolvedServerOptions.hmr === 'object' &&
+ resolvedServerOptions.hmr.host
+ ) {
+ list.push(resolvedServerOptions.hmr.host)
+ }
+ if (
+ typeof resolvedPreviewOptions.host === 'string' &&
+ resolvedPreviewOptions.host
+ ) {
+ list.push(resolvedPreviewOptions.host)
+ }
+
+ // allow server origin by default as that indicates that the user is
+ // expecting Vite to respond on that host
+ if (resolvedServerOptions.origin) {
+ // some frameworks may pass the origin as a placeholder, so it's not
+ // possible to parse as URL, so use a try-catch here as a best effort
+ try {
+ const serverOriginUrl = new URL(resolvedServerOptions.origin)
+ list.push(serverOriginUrl.hostname)
+ } catch {}
+ }
+
+ return list
+}
+
+// Based on webpack-dev-server's `checkHeader` function: https://github.com/webpack/webpack-dev-server/blob/v5.2.0/lib/Server.js#L3086
+// https://github.com/webpack/webpack-dev-server/blob/v5.2.0/LICENSE
+export function isHostAllowedWithoutCache(
+ allowedHosts: string[],
+ additionalAllowedHosts: string[],
+ host: string,
+): boolean {
+ if (isFileOrExtensionProtocolRE.test(host)) {
+ return true
+ }
+
+ // We don't care about malformed Host headers,
+ // because we only need to consider browser requests.
+ // Non-browser clients can send any value they want anyway.
+ //
+ // `Host = uri-host [ ":" port ]`
+ const trimmedHost = host.trim()
+
+ // IPv6
+ if (trimmedHost[0] === '[') {
+ const endIpv6 = trimmedHost.indexOf(']')
+ if (endIpv6 < 0) {
+ return false
+ }
+ // DNS rebinding attacks does not happen with IP addresses
+ return net.isIP(trimmedHost.slice(1, endIpv6)) === 6
+ }
+
+ // uri-host does not include ":" unless IPv6 address
+ const colonPos = trimmedHost.indexOf(':')
+ const hostname =
+ colonPos === -1 ? trimmedHost : trimmedHost.slice(0, colonPos)
+
+ // DNS rebinding attacks does not happen with IP addresses
+ if (net.isIP(hostname) === 4) {
+ return true
+ }
+
+ // allow localhost and .localhost by default as they always resolve to the loopback address
+ // https://datatracker.ietf.org/doc/html/rfc6761#section-6.3
+ if (hostname === 'localhost' || hostname.endsWith('.localhost')) {
+ return true
+ }
+
+ for (const additionalAllowedHost of additionalAllowedHosts) {
+ if (additionalAllowedHost === hostname) {
+ return true
+ }
+ }
+
+ for (const allowedHost of allowedHosts) {
+ if (allowedHost === hostname) {
+ return true
+ }
+
+ // allow all subdomains of it
+ // e.g. `.foo.example` will allow `foo.example`, `*.foo.example`, `*.*.foo.example`, etc
+ if (
+ allowedHost[0] === '.' &&
+ (allowedHost.slice(1) === hostname || hostname.endsWith(allowedHost))
+ ) {
+ return true
+ }
+ }
+
+ return false
+}
+
+/**
+ * @param config resolved config
+ * @param isPreview whether it's for the preview server or not
+ * @param host the value of host header. See [RFC 9110 7.2](https://datatracker.ietf.org/doc/html/rfc9110#name-host-and-authority).
+ */
+export function isHostAllowed(
+ config: ResolvedConfig,
+ isPreview: boolean,
+ host: string,
+): boolean {
+ const allowedHosts = isPreview
+ ? config.preview.allowedHosts
+ : config.server.allowedHosts
+ if (allowedHosts === true) {
+ return true
+ }
+
+ const cache = isPreview ? allowedHostsPreviewCache : allowedHostsServerCache
+ if (!cache.has(config)) {
+ cache.set(config, new Set())
+ }
+
+ const cachedAllowedHosts = cache.get(config)!
+ if (cachedAllowedHosts.has(host)) {
+ return true
+ }
+
+ const result = isHostAllowedWithoutCache(
+ allowedHosts,
+ config.additionalAllowedHosts,
+ host,
+ )
+ if (result) {
+ cachedAllowedHosts.add(host)
+ }
+ return result
+}
+
+export function hostCheckMiddleware(
+ config: ResolvedConfig,
+ isPreview: boolean,
+): Connect.NextHandleFunction {
+ // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
+ return function viteHostCheckMiddleware(req, res, next) {
+ const hostHeader = req.headers.host
+ if (!hostHeader || !isHostAllowed(config, isPreview, hostHeader)) {
+ const hostname = hostHeader?.replace(/:\d+$/, '')
+ const hostnameWithQuotes = JSON.stringify(hostname)
+ const optionName = `${isPreview ? 'preview' : 'server'}.allowedHosts`
+ res.writeHead(403, {
+ 'Content-Type': 'text/plain',
+ })
+ res.end(
+ `Blocked request. This host (${hostnameWithQuotes}) is not allowed.\n` +
+ `To allow this host, add ${hostnameWithQuotes} to \`${optionName}\` in vite.config.js.`,
+ )
+ return
+ }
+ return next()
+ }
+}
diff --git a/packages/vite/src/node/server/middlewares/htmlFallback.ts b/packages/vite/src/node/server/middlewares/htmlFallback.ts
new file mode 100644
index 00000000000000..b61b44bf061180
--- /dev/null
+++ b/packages/vite/src/node/server/middlewares/htmlFallback.ts
@@ -0,0 +1,72 @@
+import path from 'node:path'
+import fs from 'node:fs'
+import type { Connect } from 'dep-types/connect'
+import { createDebugger } from '../../utils'
+import { cleanUrl } from '../../../shared/utils'
+
+const debug = createDebugger('vite:html-fallback')
+
+export function htmlFallbackMiddleware(
+ root: string,
+ spaFallback: boolean,
+): Connect.NextHandleFunction {
+ // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
+ return function viteHtmlFallbackMiddleware(req, _res, next) {
+ if (
+ // Only accept GET or HEAD
+ (req.method !== 'GET' && req.method !== 'HEAD') ||
+ // Exclude default favicon requests
+ req.url === '/favicon.ico' ||
+ // Require Accept: text/html or */*
+ !(
+ req.headers.accept === undefined || // equivalent to `Accept: */*`
+ req.headers.accept === '' || // equivalent to `Accept: */*`
+ req.headers.accept.includes('text/html') ||
+ req.headers.accept.includes('*/*')
+ )
+ ) {
+ return next()
+ }
+
+ const url = cleanUrl(req.url!)
+ const pathname = decodeURIComponent(url)
+
+ // .html files are not handled by serveStaticMiddleware
+ // so we need to check if the file exists
+ if (pathname.endsWith('.html')) {
+ const filePath = path.join(root, pathname)
+ if (fs.existsSync(filePath)) {
+ debug?.(`Rewriting ${req.method} ${req.url} to ${url}`)
+ req.url = url
+ return next()
+ }
+ }
+ // trailing slash should check for fallback index.html
+ else if (pathname.endsWith('/')) {
+ const filePath = path.join(root, pathname, 'index.html')
+ if (fs.existsSync(filePath)) {
+ const newUrl = url + 'index.html'
+ debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`)
+ req.url = newUrl
+ return next()
+ }
+ }
+ // non-trailing slash should check for fallback .html
+ else {
+ const filePath = path.join(root, pathname + '.html')
+ if (fs.existsSync(filePath)) {
+ const newUrl = url + '.html'
+ debug?.(`Rewriting ${req.method} ${req.url} to ${newUrl}`)
+ req.url = newUrl
+ return next()
+ }
+ }
+
+ if (spaFallback) {
+ debug?.(`Rewriting ${req.method} ${req.url} to /index.html`)
+ req.url = '/index.html'
+ }
+
+ next()
+ }
+}
diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts
index 3c76d94c526930..2181602687d580 100644
--- a/packages/vite/src/node/server/middlewares/indexHtml.ts
+++ b/packages/vite/src/node/server/middlewares/indexHtml.ts
@@ -1,35 +1,95 @@
-import fs from 'fs'
-import path from 'path'
+import fs from 'node:fs'
+import fsp from 'node:fs/promises'
+import path from 'node:path'
import MagicString from 'magic-string'
-import type { AttributeNode, ElementNode } from '@vue/compiler-dom'
-import { NodeTypes } from '@vue/compiler-dom'
-import type { Connect } from 'types/connect'
+import type { SourceMapInput } from 'rollup'
+import type { Connect } from 'dep-types/connect'
+import type { DefaultTreeAdapterMap, Token } from 'parse5'
import type { IndexHtmlTransformHook } from '../../plugins/html'
import {
addToHTMLProxyCache,
applyHtmlTransforms,
- assetAttrsConfig,
+ extractImportExpressionFromClassicScript,
+ findNeedTransformStyleAttribute,
getScriptInfo,
+ htmlEnvHook,
+ htmlProxyResult,
+ injectCspNonceMetaTagHook,
+ injectNonceAttributeTagHook,
+ nodeIsElement,
+ overwriteAttrValue,
+ postImportMapHook,
+ preImportMapHook,
+ removeViteIgnoreAttr,
resolveHtmlTransforms,
- traverseHtml
+ traverseHtml,
} from '../../plugins/html'
-import type { ResolvedConfig, ViteDevServer } from '../..'
+import type { PreviewServer, ResolvedConfig, ViteDevServer } from '../..'
import { send } from '../send'
import { CLIENT_PUBLIC_PATH, FS_PREFIX } from '../../constants'
-import { cleanUrl, fsPathFromId, normalizePath, injectQuery } from '../../utils'
-import type { ModuleGraph } from '../moduleGraph'
+import {
+ ensureWatchedFile,
+ fsPathFromId,
+ getHash,
+ injectQuery,
+ isCSSRequest,
+ isDevServer,
+ isJSRequest,
+ joinUrlSegments,
+ normalizePath,
+ processSrcSetSync,
+ stripBase,
+} from '../../utils'
+import { checkPublicFile } from '../../publicDir'
+import { getCodeWithSourcemap, injectSourcesContent } from '../sourcemap'
+import { cleanUrl, unwrapId, wrapId } from '../../../shared/utils'
+import { getNodeAssetAttributes } from '../../assetSource'
-export function createDevHtmlTransformFn(
- server: ViteDevServer
-): (url: string, html: string, originalUrl: string) => Promise {
- const [preHooks, postHooks] = resolveHtmlTransforms(server.config.plugins)
+interface AssetNode {
+ start: number
+ end: number
+ code: string
+}
+
+interface InlineStyleAttribute {
+ index: number
+ location: Token.Location
+ code: string
+}
- return (url: string, html: string, originalUrl: string): Promise => {
- return applyHtmlTransforms(html, [...preHooks, devHtmlHook, ...postHooks], {
+export function createDevHtmlTransformFn(
+ config: ResolvedConfig,
+): (
+ server: ViteDevServer,
+ url: string,
+ html: string,
+ originalUrl?: string,
+) => Promise {
+ const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms(
+ config.plugins,
+ )
+ const transformHooks = [
+ preImportMapHook(config),
+ injectCspNonceMetaTagHook(config),
+ ...preHooks,
+ htmlEnvHook(config),
+ devHtmlHook,
+ ...normalHooks,
+ ...postHooks,
+ injectNonceAttributeTagHook(config),
+ postImportMapHook(),
+ ]
+ return (
+ server: ViteDevServer,
+ url: string,
+ html: string,
+ originalUrl?: string,
+ ): Promise => {
+ return applyHtmlTransforms(html, transformHooks, {
path: url,
filename: getHtmlFilename(url, server),
server,
- originalUrl
+ originalUrl,
})
}
}
@@ -38,130 +98,318 @@ function getHtmlFilename(url: string, server: ViteDevServer) {
if (url.startsWith(FS_PREFIX)) {
return decodeURIComponent(fsPathFromId(url))
} else {
- return decodeURIComponent(path.join(server.config.root, url.slice(1)))
+ return decodeURIComponent(
+ normalizePath(path.join(server.config.root, url.slice(1))),
+ )
}
}
-const startsWithSingleSlashRE = /^\/(?!\/)/
+function shouldPreTransform(url: string, config: ResolvedConfig) {
+ return (
+ !checkPublicFile(url, config) && (isJSRequest(url) || isCSSRequest(url))
+ )
+}
+
+const wordCharRE = /\w/
+
+function isBareRelative(url: string) {
+ return wordCharRE.test(url[0]) && !url.includes(':')
+}
+
const processNodeUrl = (
- node: AttributeNode,
- s: MagicString,
+ url: string,
+ useSrcSetReplacer: boolean,
config: ResolvedConfig,
htmlPath: string,
originalUrl?: string,
- moduleGraph?: ModuleGraph
-) => {
- let url = node.value?.content || ''
+ server?: ViteDevServer,
+ isClassicScriptLink?: boolean,
+): string => {
+ // prefix with base (dev only, base is never relative)
+ const replacer = (url: string) => {
+ if (
+ (url[0] === '/' && url[1] !== '/') ||
+ // #3230 if some request url (localhost:3000/a/b) return to fallback html, the relative assets
+ // path will add `/a/` prefix, it will caused 404.
+ //
+ // skip if url contains `:` as it implies a url protocol or Windows path that we don't want to replace.
+ //
+ // rewrite `./index.js` -> `localhost:5173/a/index.js`.
+ // rewrite `../index.js` -> `localhost:5173/index.js`.
+ // rewrite `relative/index.js` -> `localhost:5173/a/relative/index.js`.
+ ((url[0] === '.' || isBareRelative(url)) &&
+ originalUrl &&
+ originalUrl !== '/' &&
+ htmlPath === '/index.html')
+ ) {
+ url = path.posix.join(config.base, url)
+ }
- if (moduleGraph) {
- const mod = moduleGraph.urlToModuleMap.get(url)
- if (mod && mod.lastHMRTimestamp > 0) {
- url = injectQuery(url, `t=${mod.lastHMRTimestamp}`)
+ let preTransformUrl: string | undefined
+
+ if (!isClassicScriptLink && shouldPreTransform(url, config)) {
+ if (url[0] === '/' && url[1] !== '/') {
+ preTransformUrl = url
+ } else if (url[0] === '.' || isBareRelative(url)) {
+ preTransformUrl = path.posix.join(
+ config.base,
+ path.posix.dirname(htmlPath),
+ url,
+ )
+ }
}
+
+ if (server) {
+ const mod = server.environments.client.moduleGraph.urlToModuleMap.get(
+ preTransformUrl || url,
+ )
+ if (mod && mod.lastHMRTimestamp > 0) {
+ url = injectQuery(url, `t=${mod.lastHMRTimestamp}`)
+ }
+ }
+
+ if (server && preTransformUrl) {
+ try {
+ preTransformUrl = decodeURI(preTransformUrl)
+ } catch {
+ // Malformed uri. Skip pre-transform.
+ return url
+ }
+ preTransformRequest(server, preTransformUrl, config.decodedBase)
+ }
+
+ return url
}
- if (startsWithSingleSlashRE.test(url)) {
- // prefix with base
- s.overwrite(
- node.value!.loc.start.offset,
- node.value!.loc.end.offset,
- `"${config.base + url.slice(1)}"`
- )
- } else if (
- url.startsWith('.') &&
- originalUrl &&
- originalUrl !== '/' &&
- htmlPath === '/index.html'
- ) {
- // #3230 if some request url (localhost:3000/a/b) return to fallback html, the relative assets
- // path will add `/a/` prefix, it will caused 404.
- // rewrite before `./index.js` -> `localhost:3000/a/index.js`.
- // rewrite after `../index.js` -> `localhost:3000/index.js`.
- s.overwrite(
- node.value!.loc.start.offset,
- node.value!.loc.end.offset,
- `"${path.posix.join(
- path.posix.relative(originalUrl, '/'),
- url.slice(1)
- )}"`
- )
- }
+
+ const processedUrl = useSrcSetReplacer
+ ? processSrcSetSync(url, ({ url }) => replacer(url))
+ : replacer(url)
+ return processedUrl
}
const devHtmlHook: IndexHtmlTransformHook = async (
html,
- { path: htmlPath, server, originalUrl }
+ { path: htmlPath, filename, server, originalUrl },
) => {
- const { config, moduleGraph } = server!
+ const { config, watcher } = server!
const base = config.base || '/'
+ const decodedBase = config.decodedBase || '/'
+
+ let proxyModulePath: string
+ let proxyModuleUrl: string
+
+ const trailingSlash = htmlPath.endsWith('/')
+ if (!trailingSlash && fs.existsSync(filename)) {
+ proxyModulePath = htmlPath
+ proxyModuleUrl = proxyModulePath
+ } else {
+ // There are users of vite.transformIndexHtml calling it with url '/'
+ // for SSR integrations #7993, filename is root for this case
+ // A user may also use a valid name for a virtual html file
+ // Mark the path as virtual in both cases so sourcemaps aren't processed
+ // and ids are properly handled
+ const validPath = `${htmlPath}${trailingSlash ? 'index.html' : ''}`
+ proxyModulePath = `\0${validPath}`
+ proxyModuleUrl = wrapId(proxyModulePath)
+ }
+ proxyModuleUrl = joinUrlSegments(decodedBase, proxyModuleUrl)
const s = new MagicString(html)
let inlineModuleIndex = -1
- const filePath = cleanUrl(htmlPath)
+ // The key to the proxyHtml cache is decoded, as it will be compared
+ // against decoded URLs by the HTML plugins.
+ const proxyCacheUrl = decodeURI(
+ cleanUrl(proxyModulePath).replace(normalizePath(config.root), ''),
+ )
+ const styleUrl: AssetNode[] = []
+ const inlineStyles: InlineStyleAttribute[] = []
+ const inlineModulePaths: string[] = []
- const addInlineModule = (node: ElementNode, ext: 'js' | 'css') => {
+ const addInlineModule = (
+ node: DefaultTreeAdapterMap['element'],
+ ext: 'js',
+ ) => {
inlineModuleIndex++
- const url = filePath.replace(normalizePath(config.root), '')
+ const contentNode = node.childNodes[0] as DefaultTreeAdapterMap['textNode']
- const contents = node.children
- .map((child: any) => child.content || '')
- .join('')
+ const code = contentNode.value
- // add HTML Proxy to Map
- addToHTMLProxyCache(config, url, inlineModuleIndex, contents)
+ let map: SourceMapInput | undefined
+ if (proxyModulePath[0] !== '\0') {
+ map = new MagicString(html)
+ .snip(
+ contentNode.sourceCodeLocation!.startOffset,
+ contentNode.sourceCodeLocation!.endOffset,
+ )
+ .generateMap({ hires: 'boundary' })
+ map.sources = [filename]
+ map.file = filename
+ }
- // inline js module. convert to src="proxy"
- const modulePath = `${
- config.base + htmlPath.slice(1)
- }?html-proxy&index=${inlineModuleIndex}.${ext}`
+ // add HTML Proxy to Map
+ addToHTMLProxyCache(config, proxyCacheUrl, inlineModuleIndex, { code, map })
- // invalidate the module so the newly cached contents will be served
- const module = server?.moduleGraph.getModuleById(modulePath)
- if (module) {
- server?.moduleGraph.invalidateModule(module)
- }
+ // inline js module. convert to src="proxy" (dev only, base is never relative)
+ const modulePath = `${proxyModuleUrl}?html-proxy&index=${inlineModuleIndex}.${ext}`
+ inlineModulePaths.push(modulePath)
- s.overwrite(
- node.loc.start.offset,
- node.loc.end.offset,
- ``
+ s.update(
+ node.sourceCodeLocation!.startOffset,
+ node.sourceCodeLocation!.endOffset,
+ ``,
)
+ preTransformRequest(server!, modulePath, decodedBase)
}
- await traverseHtml(html, htmlPath, (node) => {
- if (node.type !== NodeTypes.ELEMENT) {
+ await traverseHtml(html, filename, (node) => {
+ if (!nodeIsElement(node)) {
return
}
// script tags
- if (node.tag === 'script') {
- const { src, isModule } = getScriptInfo(node)
+ if (node.nodeName === 'script') {
+ const { src, srcSourceCodeLocation, isModule, isIgnored } =
+ getScriptInfo(node)
- if (src) {
- processNodeUrl(src, s, config, htmlPath, originalUrl, moduleGraph)
- } else if (isModule) {
+ if (isIgnored) {
+ removeViteIgnoreAttr(s, node.sourceCodeLocation!)
+ } else if (src) {
+ const processedUrl = processNodeUrl(
+ src.value,
+ /* useSrcSetReplacer */ false,
+ config,
+ htmlPath,
+ originalUrl,
+ server,
+ !isModule,
+ )
+ if (processedUrl !== src.value) {
+ overwriteAttrValue(s, srcSourceCodeLocation!, processedUrl)
+ }
+ } else if (isModule && node.childNodes.length) {
addInlineModule(node, 'js')
+ } else if (node.childNodes.length) {
+ const scriptNode = node.childNodes[
+ node.childNodes.length - 1
+ ] as DefaultTreeAdapterMap['textNode']
+ for (const {
+ url,
+ start,
+ end,
+ } of extractImportExpressionFromClassicScript(scriptNode)) {
+ const processedUrl = processNodeUrl(
+ url,
+ false,
+ config,
+ htmlPath,
+ originalUrl,
+ )
+ if (processedUrl !== url) {
+ s.update(start, end, processedUrl)
+ }
+ }
}
}
- if (node.tag === 'style' && node.children.length) {
- addInlineModule(node, 'css')
+ const inlineStyle = findNeedTransformStyleAttribute(node)
+ if (inlineStyle) {
+ inlineModuleIndex++
+ inlineStyles.push({
+ index: inlineModuleIndex,
+ location: inlineStyle.location!,
+ code: inlineStyle.attr.value,
+ })
+ }
+
+ if (node.nodeName === 'style' && node.childNodes.length) {
+ const children = node.childNodes[0] as DefaultTreeAdapterMap['textNode']
+ styleUrl.push({
+ start: children.sourceCodeLocation!.startOffset,
+ end: children.sourceCodeLocation!.endOffset,
+ code: children.value,
+ })
}
// elements with [href/src] attrs
- const assetAttrs = assetAttrsConfig[node.tag]
- if (assetAttrs) {
- for (const p of node.props) {
- if (
- p.type === NodeTypes.ATTRIBUTE &&
- p.value &&
- assetAttrs.includes(p.name)
- ) {
- processNodeUrl(p, s, config, htmlPath, originalUrl)
+ const assetAttributes = getNodeAssetAttributes(node)
+ for (const attr of assetAttributes) {
+ if (attr.type === 'remove') {
+ s.remove(attr.location.startOffset, attr.location.endOffset)
+ } else {
+ const processedUrl = processNodeUrl(
+ attr.value,
+ attr.type === 'srcset',
+ config,
+ htmlPath,
+ originalUrl,
+ )
+ if (processedUrl !== attr.value) {
+ overwriteAttrValue(s, attr.location, processedUrl)
}
}
}
})
+ // invalidate the module so the newly cached contents will be served
+ const clientModuelGraph = server?.environments.client.moduleGraph
+ if (clientModuelGraph) {
+ await Promise.all(
+ inlineModulePaths.map(async (url) => {
+ const module = await clientModuelGraph.getModuleByUrl(url)
+ if (module) {
+ clientModuelGraph.invalidateModule(module)
+ }
+ }),
+ )
+ }
+
+ await Promise.all([
+ ...styleUrl.map(async ({ start, end, code }, index) => {
+ const url = `${proxyModulePath}?html-proxy&direct&index=${index}.css`
+
+ // ensure module in graph after successful load
+ const mod =
+ await server!.environments.client.moduleGraph.ensureEntryFromUrl(
+ url,
+ false,
+ )
+ ensureWatchedFile(watcher, mod.file, config.root)
+
+ const result = await server!.pluginContainer.transform(code, mod.id!, {
+ environment: server!.environments.client,
+ })
+ let content = ''
+ if (result.map && 'version' in result.map) {
+ if (result.map.mappings) {
+ await injectSourcesContent(result.map, proxyModulePath, config.logger)
+ }
+ content = getCodeWithSourcemap('css', result.code, result.map)
+ } else {
+ content = result.code
+ }
+ s.overwrite(start, end, content)
+ }),
+ ...inlineStyles.map(async ({ index, location, code }) => {
+ // will transform with css plugin and cache result with css-post plugin
+ const url = `${proxyModulePath}?html-proxy&inline-css&style-attr&index=${index}.css`
+
+ const mod =
+ await server!.environments.client.moduleGraph.ensureEntryFromUrl(
+ url,
+ false,
+ )
+ ensureWatchedFile(watcher, mod.file, config.root)
+
+ await server?.pluginContainer.transform(code, mod.id!, {
+ environment: server!.environments.client,
+ })
+
+ const hash = getHash(cleanUrl(mod.id!))
+ const result = htmlProxyResult.get(`${hash}_${index}`)
+ overwriteAttrValue(s, location, result ?? '')
+ }),
+ ])
+
html = s.toString()
return {
@@ -171,17 +419,20 @@ const devHtmlHook: IndexHtmlTransformHook = async (
tag: 'script',
attrs: {
type: 'module',
- src: path.posix.join(base, CLIENT_PUBLIC_PATH)
+ src: path.posix.join(base, CLIENT_PUBLIC_PATH),
},
- injectTo: 'head-prepend'
- }
- ]
+ injectTo: 'head-prepend',
+ },
+ ],
}
}
export function indexHtmlMiddleware(
- server: ViteDevServer
+ root: string,
+ server: ViteDevServer | PreviewServer,
): Connect.NextHandleFunction {
+ const isDev = isDevServer(server)
+
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return async function viteIndexHtmlMiddleware(req, res, next) {
if (res.writableEnded) {
@@ -189,16 +440,26 @@ export function indexHtmlMiddleware(
}
const url = req.url && cleanUrl(req.url)
- // spa-fallback always redirects to /index.html
+ // htmlFallbackMiddleware appends '.html' to URLs
if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') {
- const filename = getHtmlFilename(url, server)
- if (fs.existsSync(filename)) {
+ let filePath: string
+ if (isDev && url.startsWith(FS_PREFIX)) {
+ filePath = decodeURIComponent(fsPathFromId(url))
+ } else {
+ filePath = path.join(root, decodeURIComponent(url))
+ }
+
+ if (fs.existsSync(filePath)) {
+ const headers = isDev
+ ? server.config.server.headers
+ : server.config.preview.headers
+
try {
- let html = fs.readFileSync(filename, 'utf-8')
- html = await server.transformIndexHtml(url, html, req.originalUrl)
- return send(req, res, html, 'html', {
- headers: server.config.server.headers
- })
+ let html = await fsp.readFile(filePath, 'utf-8')
+ if (isDev) {
+ html = await server.transformIndexHtml(url, html, req.originalUrl)
+ }
+ return send(req, res, html, 'html', { headers })
} catch (e) {
return next(e)
}
@@ -207,3 +468,17 @@ export function indexHtmlMiddleware(
next()
}
}
+
+// NOTE: We usually don't prefix `url` and `base` with `decoded`, but in this file particularly
+// we're dealing with mixed encoded/decoded paths often, so we make this explicit for now.
+function preTransformRequest(
+ server: ViteDevServer,
+ decodedUrl: string,
+ decodedBase: string,
+) {
+ if (!server.config.server.preTransformRequests) return
+
+ // transform all url as non-ssr as html includes client-side assets only
+ decodedUrl = unwrapId(stripBase(decodedUrl, decodedBase))
+ server.warmupRequest(decodedUrl)
+}
diff --git a/packages/vite/src/node/server/middlewares/notFound.ts b/packages/vite/src/node/server/middlewares/notFound.ts
new file mode 100644
index 00000000000000..4ecf6823dcaf89
--- /dev/null
+++ b/packages/vite/src/node/server/middlewares/notFound.ts
@@ -0,0 +1,9 @@
+import type { Connect } from 'dep-types/connect'
+
+export function notFoundMiddleware(): Connect.NextHandleFunction {
+ // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
+ return function vite404Middleware(_, res) {
+ res.statusCode = 404
+ res.end()
+ }
+}
diff --git a/packages/vite/src/node/server/middlewares/proxy.ts b/packages/vite/src/node/server/middlewares/proxy.ts
index aa1100f13d5229..d8850e5549036d 100644
--- a/packages/vite/src/node/server/middlewares/proxy.ts
+++ b/packages/vite/src/node/server/middlewares/proxy.ts
@@ -1,11 +1,12 @@
-import type * as http from 'http'
-import { createDebugger, isObject } from '../../utils'
+import type * as http from 'node:http'
+import type * as net from 'node:net'
import httpProxy from 'http-proxy'
-import { HMR_HEADER } from '../ws'
-import type { Connect } from 'types/connect'
-import type { HttpProxy } from 'types/http-proxy'
+import type { Connect } from 'dep-types/connect'
+import type { HttpProxy } from 'dep-types/http-proxy'
import colors from 'picocolors'
-import type { ResolvedConfig } from '../..'
+import { createDebugger } from '../../utils'
+import type { CommonServerOptions, ResolvedConfig } from '../..'
+import type { HttpServer } from '..'
const debug = createDebugger('vite:proxy')
@@ -23,55 +24,185 @@ export interface ProxyOptions extends HttpProxy.ServerOptions {
*/
bypass?: (
req: http.IncomingMessage,
- res: http.ServerResponse,
- options: ProxyOptions
- ) => void | null | undefined | false | string
+ /** undefined for WebSocket upgrade requests */
+ res: http.ServerResponse | undefined,
+ options: ProxyOptions,
+ ) =>
+ | void
+ | null
+ | undefined
+ | false
+ | string
+ | Promise
+ /**
+ * rewrite the Origin header of a WebSocket request to match the target
+ *
+ * **Exercise caution as rewriting the Origin can leave the proxying open to [CSRF attacks](https://owasp.org/www-community/attacks/csrf).**
+ */
+ rewriteWsOrigin?: boolean | undefined
+}
+
+const rewriteOriginHeader = (
+ proxyReq: http.ClientRequest,
+ options: ProxyOptions,
+ config: ResolvedConfig,
+) => {
+ // Browsers may send Origin headers even with same-origin
+ // requests. It is common for WebSocket servers to check the Origin
+ // header, so if rewriteWsOrigin is true we change the Origin to match
+ // the target URL.
+ if (options.rewriteWsOrigin) {
+ const { target } = options
+
+ if (proxyReq.headersSent) {
+ config.logger.warn(
+ colors.yellow(
+ `Unable to rewrite Origin header as headers are already sent.`,
+ ),
+ )
+ return
+ }
+
+ if (proxyReq.getHeader('origin') && target) {
+ const changedOrigin =
+ typeof target === 'object'
+ ? `${target.protocol}//${target.host}`
+ : target
+
+ proxyReq.setHeader('origin', changedOrigin)
+ }
+ }
}
export function proxyMiddleware(
- httpServer: http.Server | null,
- config: ResolvedConfig
+ httpServer: HttpServer | null,
+ options: NonNullable,
+ config: ResolvedConfig,
): Connect.NextHandleFunction {
- const options = config.server.proxy!
-
// lazy require only when proxy is used
const proxies: Record = {}
Object.keys(options).forEach((context) => {
let opts = options[context]
+ if (!opts) {
+ return
+ }
if (typeof opts === 'string') {
opts = { target: opts, changeOrigin: true } as ProxyOptions
}
const proxy = httpProxy.createProxyServer(opts) as HttpProxy.Server
- proxy.on('error', (err) => {
- config.logger.error(`${colors.red(`http proxy error:`)}\n${err.stack}`, {
- timestamp: true,
- error: err
- })
- })
-
if (opts.configure) {
opts.configure(proxy, opts)
}
+
+ proxy.on('error', (err, _req, originalRes) => {
+ // When it is ws proxy, res is net.Socket
+ // originalRes can be falsy if the proxy itself errored
+ const res = originalRes as http.ServerResponse | net.Socket | undefined
+ if (!res) {
+ config.logger.error(
+ `${colors.red(`http proxy error: ${err.message}`)}\n${err.stack}`,
+ {
+ timestamp: true,
+ error: err,
+ },
+ )
+ } else if ('req' in res) {
+ config.logger.error(
+ `${colors.red(`http proxy error: ${originalRes.req.url}`)}\n${
+ err.stack
+ }`,
+ {
+ timestamp: true,
+ error: err,
+ },
+ )
+ if (!res.headersSent && !res.writableEnded) {
+ res
+ .writeHead(500, {
+ 'Content-Type': 'text/plain',
+ })
+ .end()
+ }
+ } else {
+ config.logger.error(`${colors.red(`ws proxy error:`)}\n${err.stack}`, {
+ timestamp: true,
+ error: err,
+ })
+ res.end()
+ }
+ })
+
+ proxy.on('proxyReqWs', (proxyReq, _req, socket, options) => {
+ rewriteOriginHeader(proxyReq, options, config)
+
+ socket.on('error', (err) => {
+ config.logger.error(
+ `${colors.red(`ws proxy socket error:`)}\n${err.stack}`,
+ {
+ timestamp: true,
+ error: err,
+ },
+ )
+ })
+ })
+
+ // https://github.com/http-party/node-http-proxy/issues/1520#issue-877626125
+ // https://github.com/chimurai/http-proxy-middleware/blob/cd58f962aec22c925b7df5140502978da8f87d5f/src/plugins/default/debug-proxy-errors-plugin.ts#L25-L37
+ proxy.on('proxyRes', (proxyRes, _req, res) => {
+ res.on('close', () => {
+ if (!res.writableEnded) {
+ debug?.('destroying proxyRes in proxyRes close event')
+ proxyRes.destroy()
+ }
+ })
+ })
+
// clone before saving because http-proxy mutates the options
proxies[context] = [proxy, { ...opts }]
})
if (httpServer) {
- httpServer.on('upgrade', (req, socket, head) => {
+ httpServer.on('upgrade', async (req, socket, head) => {
const url = req.url!
for (const context in proxies) {
if (doesProxyContextMatchUrl(context, url)) {
const [proxy, opts] = proxies[context]
if (
- (opts.ws || opts.target?.toString().startsWith('ws:')) &&
- req.headers['sec-websocket-protocol'] !== HMR_HEADER
+ opts.ws ||
+ opts.target?.toString().startsWith('ws:') ||
+ opts.target?.toString().startsWith('wss:')
) {
+ if (opts.bypass) {
+ try {
+ const bypassResult = await opts.bypass(req, undefined, opts)
+ if (typeof bypassResult === 'string') {
+ debug?.(`bypass: ${req.url} -> ${bypassResult}`)
+ req.url = bypassResult
+ return
+ }
+ if (bypassResult === false) {
+ debug?.(`bypass: ${req.url} -> 404`)
+ socket.end('HTTP/1.1 404 Not Found\r\n\r\n', '')
+ return
+ }
+ } catch (err) {
+ config.logger.error(
+ `${colors.red(`ws proxy bypass error:`)}\n${err.stack}`,
+ {
+ timestamp: true,
+ error: err,
+ },
+ )
+ return
+ }
+ }
+
if (opts.rewrite) {
req.url = opts.rewrite(url)
}
- debug(`${req.url} -> ws ${opts.target}`)
+ debug?.(`${req.url} -> ws ${opts.target}`)
proxy.ws(req, socket, head)
return
}
@@ -81,7 +212,7 @@ export function proxyMiddleware(
}
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
- return function viteProxyMiddleware(req, res, next) {
+ return async function viteProxyMiddleware(req, res, next) {
const url = req.url!
for (const context in proxies) {
if (doesProxyContextMatchUrl(context, url)) {
@@ -89,22 +220,28 @@ export function proxyMiddleware(
const options: HttpProxy.ServerOptions = {}
if (opts.bypass) {
- const bypassResult = opts.bypass(req, res, opts)
- if (typeof bypassResult === 'string') {
- req.url = bypassResult
- debug(`bypass: ${req.url} -> ${bypassResult}`)
- return next()
- } else if (isObject(bypassResult)) {
- Object.assign(options, bypassResult)
- debug(`bypass: ${req.url} use modified options: %O`, options)
- return next()
- } else if (bypassResult === false) {
- debug(`bypass: ${req.url} -> 404`)
- return res.end(404)
+ try {
+ const bypassResult = await opts.bypass(req, res, opts)
+ if (typeof bypassResult === 'string') {
+ debug?.(`bypass: ${req.url} -> ${bypassResult}`)
+ req.url = bypassResult
+ if (res.writableEnded) {
+ return
+ }
+ return next()
+ }
+ if (bypassResult === false) {
+ debug?.(`bypass: ${req.url} -> 404`)
+ res.statusCode = 404
+ return res.end()
+ }
+ } catch (e) {
+ debug?.(`bypass: ${req.url} -> ${e}`)
+ return next(e)
}
}
- debug(`${req.url} -> ${opts.target || opts.forward}`)
+ debug?.(`${req.url} -> ${opts.target || opts.forward}`)
if (opts.rewrite) {
req.url = opts.rewrite(req.url!)
}
@@ -118,7 +255,7 @@ export function proxyMiddleware(
function doesProxyContextMatchUrl(context: string, url: string): boolean {
return (
- (context.startsWith('^') && new RegExp(context).test(url)) ||
+ (context[0] === '^' && new RegExp(context).test(url)) ||
url.startsWith(context)
)
}
diff --git a/packages/vite/src/node/server/middlewares/rejectInvalidRequest.ts b/packages/vite/src/node/server/middlewares/rejectInvalidRequest.ts
new file mode 100644
index 00000000000000..e43801d6a78a01
--- /dev/null
+++ b/packages/vite/src/node/server/middlewares/rejectInvalidRequest.ts
@@ -0,0 +1,20 @@
+import type { Connect } from 'dep-types/connect'
+
+export function rejectInvalidRequestMiddleware(): Connect.NextHandleFunction {
+ // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
+ return function viteRejectInvalidRequestMiddleware(req, res, next) {
+ // HTTP spec does not allow `#` in the request-target
+ // (HTTP 1.1: https://datatracker.ietf.org/doc/html/rfc9112#section-3.2)
+ // (HTTP 2: https://datatracker.ietf.org/doc/html/rfc9113#section-8.3.1-2.4.1)
+ // But Node.js allows those requests.
+ // Our middlewares don't expect `#` to be included in `req.url`, especially the `server.fs.deny` checks.
+ if (req.url?.includes('#')) {
+ // HTTP 1.1 spec recommends sending 400 Bad Request
+ // (https://datatracker.ietf.org/doc/html/rfc9112#section-3.2-4)
+ res.writeHead(400)
+ res.end()
+ return
+ }
+ return next()
+ }
+}
diff --git a/packages/vite/src/node/server/middlewares/spaFallback.ts b/packages/vite/src/node/server/middlewares/spaFallback.ts
deleted file mode 100644
index 1aade764d6993a..00000000000000
--- a/packages/vite/src/node/server/middlewares/spaFallback.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import fs from 'fs'
-import history from 'connect-history-api-fallback'
-import path from 'path'
-import type { Connect } from 'types/connect'
-import { createDebugger } from '../../utils'
-
-export function spaFallbackMiddleware(
- root: string
-): Connect.NextHandleFunction {
- const historySpaFallbackMiddleware = history({
- logger: createDebugger('vite:spa-fallback'),
- // support /dir/ without explicit index.html
- rewrites: [
- {
- from: /\/$/,
- to({ parsedUrl }: any) {
- const rewritten =
- decodeURIComponent(parsedUrl.pathname) + 'index.html'
-
- if (fs.existsSync(path.join(root, rewritten))) {
- return rewritten
- } else {
- return `/index.html`
- }
- }
- }
- ]
- })
-
- // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
- return function viteSpaFallbackMiddleware(req, res, next) {
- return historySpaFallbackMiddleware(req, res, next)
- }
-}
diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts
index a6623338783cc8..59d9b7311d27c1 100644
--- a/packages/vite/src/node/server/middlewares/static.ts
+++ b/packages/vite/src/node/server/middlewares/static.ts
@@ -1,46 +1,119 @@
-import path from 'path'
-import type { ServerResponse } from 'http'
+import path from 'node:path'
+import type { OutgoingHttpHeaders, ServerResponse } from 'node:http'
import type { Options } from 'sirv'
import sirv from 'sirv'
-import type { Connect } from 'types/connect'
-import type { ViteDevServer } from '../..'
+import type { Connect } from 'dep-types/connect'
+import escapeHtml from 'escape-html'
+import type { ViteDevServer } from '../../server'
+import type { ResolvedConfig } from '../../config'
import { FS_PREFIX } from '../../constants'
import {
- cleanUrl,
- fsPathFromId,
fsPathFromUrl,
+ isFileReadable,
isImportRequest,
isInternalRequest,
+ isParentDirectory,
+ isSameFileUri,
+ normalizePath,
+ removeLeadingSlash,
+ urlRE,
+} from '../../utils'
+import {
+ cleanUrl,
isWindows,
slash,
- isFileReadable,
- isParentDirectory
-} from '../../utils'
-import { isMatch } from 'micromatch'
-
-const sirvOptions: Options = {
- dev: true,
- etag: true,
- extensions: [],
- setHeaders(res, pathname) {
- // Matches js, jsx, ts, tsx.
- // The reason this is done, is that the .ts file extension is reserved
- // for the MIME type video/mp2t. In almost all cases, we can expect
- // these files to be TypeScript files, and for Vite to serve them with
- // this Content-Type.
- if (/\.[tj]sx?$/.test(pathname)) {
- res.setHeader('Content-Type', 'application/javascript')
- }
+ withTrailingSlash,
+} from '../../../shared/utils'
+
+const knownJavascriptExtensionRE = /\.(?:[tj]sx?|[cm][tj]s)$/
+const ERR_DENIED_FILE = 'ERR_DENIED_FILE'
+
+const sirvOptions = ({
+ config,
+ getHeaders,
+ disableFsServeCheck,
+}: {
+ config: ResolvedConfig
+ getHeaders: () => OutgoingHttpHeaders | undefined
+ disableFsServeCheck?: boolean
+}): Options => {
+ return {
+ dev: true,
+ etag: true,
+ extensions: [],
+ setHeaders(res, pathname) {
+ // Matches js, jsx, ts, tsx, mts, mjs, cjs, cts, ctx, mtx
+ // The reason this is done, is that the .ts and .mts file extensions are
+ // reserved for the MIME type video/mp2t. In almost all cases, we can expect
+ // these files to be TypeScript files, and for Vite to serve them with
+ // this Content-Type.
+ if (knownJavascriptExtensionRE.test(pathname)) {
+ res.setHeader('Content-Type', 'text/javascript')
+ }
+ const headers = getHeaders()
+ if (headers) {
+ for (const name in headers) {
+ res.setHeader(name, headers[name]!)
+ }
+ }
+ },
+ shouldServe: disableFsServeCheck
+ ? undefined
+ : (filePath) => {
+ const servingAccessResult = checkLoadingAccess(config, filePath)
+ if (servingAccessResult === 'denied') {
+ const error: any = new Error('denied access')
+ error.code = ERR_DENIED_FILE
+ error.path = filePath
+ throw error
+ }
+ if (servingAccessResult === 'fallback') {
+ return false
+ }
+ servingAccessResult satisfies 'allowed'
+ return true
+ },
}
}
-export function servePublicMiddleware(dir: string): Connect.NextHandleFunction {
- const serve = sirv(dir, sirvOptions)
+export function servePublicMiddleware(
+ server: ViteDevServer,
+ publicFiles?: Set,
+): Connect.NextHandleFunction {
+ const dir = server.config.publicDir
+ const serve = sirv(
+ dir,
+ sirvOptions({
+ config: server.config,
+ getHeaders: () => server.config.server.headers,
+ disableFsServeCheck: true,
+ }),
+ )
+
+ const toFilePath = (url: string) => {
+ let filePath = cleanUrl(url)
+ if (filePath.indexOf('%') !== -1) {
+ try {
+ filePath = decodeURI(filePath)
+ } catch {
+ /* malform uri */
+ }
+ }
+ return normalizePath(filePath)
+ }
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteServePublicMiddleware(req, res, next) {
- // skip import request and internal requests `/@fs/ /@vite-client` etc...
- if (isImportRequest(req.url!) || isInternalRequest(req.url!)) {
+ // To avoid the performance impact of `existsSync` on every request, we check against an
+ // in-memory set of known public files. This set is updated on restarts.
+ // also skip import request and internal requests `/@fs/ /@vite-client` etc...
+ if (
+ (publicFiles && !publicFiles.has(toFilePath(req.url!))) ||
+ isImportRequest(req.url!) ||
+ isInternalRequest(req.url!) ||
+ // for `/public-file.js?url` to be transformed
+ urlRE.test(req.url!)
+ ) {
return next()
}
serve(req, res, next)
@@ -48,10 +121,16 @@ export function servePublicMiddleware(dir: string): Connect.NextHandleFunction {
}
export function serveStaticMiddleware(
- dir: string,
- server: ViteDevServer
+ server: ViteDevServer,
): Connect.NextHandleFunction {
- const serve = sirv(dir, sirvOptions)
+ const dir = server.config.root
+ const serve = sirv(
+ dir,
+ sirvOptions({
+ config: server.config,
+ getHeaders: () => server.config.server.headers,
+ }),
+ )
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteServeStaticMiddleware(req, res, next) {
@@ -63,131 +142,193 @@ export function serveStaticMiddleware(
if (
cleanedUrl.endsWith('/') ||
path.extname(cleanedUrl) === '.html' ||
- isInternalRequest(req.url!)
+ isInternalRequest(req.url!) ||
+ // skip url starting with // as these will be interpreted as
+ // scheme relative URLs by new URL() and will not be a valid file path
+ req.url?.startsWith('//')
) {
return next()
}
- const url = decodeURI(req.url!)
+ const url = new URL(req.url!, 'http://example.com')
+ const pathname = decodeURI(url.pathname)
// apply aliases to static requests as well
- let redirected: string | undefined
+ let redirectedPathname: string | undefined
for (const { find, replacement } of server.config.resolve.alias) {
const matches =
- typeof find === 'string' ? url.startsWith(find) : find.test(url)
+ typeof find === 'string'
+ ? pathname.startsWith(find)
+ : find.test(pathname)
if (matches) {
- redirected = url.replace(find, replacement)
+ redirectedPathname = pathname.replace(find, replacement)
break
}
}
- if (redirected) {
+ if (redirectedPathname) {
// dir is pre-normalized to posix style
- if (redirected.startsWith(dir)) {
- redirected = redirected.slice(dir.length)
+ if (redirectedPathname.startsWith(withTrailingSlash(dir))) {
+ redirectedPathname = redirectedPathname.slice(dir.length)
}
}
- const resolvedUrl = redirected || url
- let fileUrl = path.resolve(dir, resolvedUrl.replace(/^\//, ''))
- if (resolvedUrl.endsWith('/') && !fileUrl.endsWith('/')) {
- fileUrl = fileUrl + '/'
+ const resolvedPathname = redirectedPathname || pathname
+ let fileUrl = path.resolve(dir, removeLeadingSlash(resolvedPathname))
+ if (resolvedPathname.endsWith('/') && fileUrl[fileUrl.length - 1] !== '/') {
+ fileUrl = withTrailingSlash(fileUrl)
}
- if (!ensureServingAccess(fileUrl, server, res, next)) {
- return
+ if (redirectedPathname) {
+ url.pathname = encodeURI(redirectedPathname)
+ req.url = url.href.slice(url.origin.length)
}
- if (redirected) {
- req.url = redirected
+ try {
+ serve(req, res, next)
+ } catch (e) {
+ if (e && 'code' in e && e.code === ERR_DENIED_FILE) {
+ respondWithAccessDenied(e.path, server, res)
+ return
+ }
+ throw e
}
-
- serve(req, res, next)
}
}
export function serveRawFsMiddleware(
- server: ViteDevServer
+ server: ViteDevServer,
): Connect.NextHandleFunction {
- const serveFromRoot = sirv('/', sirvOptions)
+ const serveFromRoot = sirv(
+ '/',
+ sirvOptions({
+ config: server.config,
+ getHeaders: () => server.config.server.headers,
+ }),
+ )
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteServeRawFsMiddleware(req, res, next) {
- let url = req.url!
// In some cases (e.g. linked monorepos) files outside of root will
// reference assets that are also out of served root. In such cases
// the paths are rewritten to `/@fs/` prefixed paths and must be served by
// searching based from fs root.
- if (url.startsWith(FS_PREFIX)) {
- // restrict files outside of `fs.allow`
- if (
- !ensureServingAccess(
- slash(path.resolve(fsPathFromId(url))),
- server,
- res,
- next
- )
- ) {
- return
- }
+ if (req.url!.startsWith(FS_PREFIX)) {
+ const url = new URL(req.url!, 'http://example.com')
+ const pathname = decodeURI(url.pathname)
+ let newPathname = pathname.slice(FS_PREFIX.length)
+ if (isWindows) newPathname = newPathname.replace(/^[A-Z]:/i, '')
+ url.pathname = encodeURI(newPathname)
+ req.url = url.href.slice(url.origin.length)
- url = url.slice(FS_PREFIX.length)
- if (isWindows) url = url.replace(/^[A-Z]:/i, '')
-
- req.url = url
- serveFromRoot(req, res, next)
+ try {
+ serveFromRoot(req, res, next)
+ } catch (e) {
+ if (e && 'code' in e && e.code === ERR_DENIED_FILE) {
+ respondWithAccessDenied(e.path, server, res)
+ return
+ }
+ throw e
+ }
} else {
next()
}
}
}
-const _matchOptions = { matchBase: true }
-
+/**
+ * Check if the url is allowed to be served, via the `server.fs` config.
+ * @deprecated Use the `isFileLoadingAllowed` function instead.
+ */
export function isFileServingAllowed(
+ config: ResolvedConfig,
url: string,
- server: ViteDevServer
+): boolean
+export function isFileServingAllowed(
+ url: string,
+ server: ViteDevServer,
+): boolean
+export function isFileServingAllowed(
+ configOrUrl: ResolvedConfig | string,
+ urlOrServer: string | ViteDevServer,
): boolean {
- if (!server.config.server.fs.strict) return true
+ const config = (
+ typeof urlOrServer === 'string' ? configOrUrl : urlOrServer.config
+ ) as ResolvedConfig
+ const url = (
+ typeof urlOrServer === 'string' ? urlOrServer : configOrUrl
+ ) as string
- const file = fsPathFromUrl(url)
+ if (!config.server.fs.strict) return true
+ const filePath = fsPathFromUrl(url)
+ return isFileLoadingAllowed(config, filePath)
+}
+
+function isUriInFilePath(uri: string, filePath: string) {
+ return isSameFileUri(uri, filePath) || isParentDirectory(uri, filePath)
+}
- if (server.config.server.fs.deny.some((i) => isMatch(file, i, _matchOptions)))
- return false
+export function isFileLoadingAllowed(
+ config: ResolvedConfig,
+ filePath: string,
+): boolean {
+ const { fs } = config.server
- if (server.moduleGraph.safeModulesPath.has(file)) return true
+ if (!fs.strict) return true
- if (server.config.server.fs.allow.some((dir) => isParentDirectory(dir, file)))
- return true
+ if (config.fsDenyGlob(filePath)) return false
+
+ if (config.safeModulePaths.has(filePath)) return true
+
+ if (fs.allow.some((uri) => isUriInFilePath(uri, filePath))) return true
return false
}
-function ensureServingAccess(
+export function checkLoadingAccess(
+ config: ResolvedConfig,
+ path: string,
+): 'allowed' | 'denied' | 'fallback' {
+ if (isFileLoadingAllowed(config, slash(path))) {
+ return 'allowed'
+ }
+ if (isFileReadable(path)) {
+ return 'denied'
+ }
+ // if the file doesn't exist, we shouldn't restrict this path as it can
+ // be an API call. Middlewares would issue a 404 if the file isn't handled
+ return 'fallback'
+}
+
+export function checkServingAccess(
url: string,
server: ViteDevServer,
- res: ServerResponse,
- next: Connect.NextFunction
-): boolean {
+): 'allowed' | 'denied' | 'fallback' {
if (isFileServingAllowed(url, server)) {
- return true
+ return 'allowed'
}
if (isFileReadable(cleanUrl(url))) {
- const urlMessage = `The request url "${url}" is outside of Vite serving allow list.`
- const hintMessage = `
+ return 'denied'
+ }
+ // if the file doesn't exist, we shouldn't restrict this path as it can
+ // be an API call. Middlewares would issue a 404 if the file isn't handled
+ return 'fallback'
+}
+
+export function respondWithAccessDenied(
+ url: string,
+ server: ViteDevServer,
+ res: ServerResponse,
+): void {
+ const urlMessage = `The request url "${url}" is outside of Vite serving allow list.`
+ const hintMessage = `
${server.config.server.fs.allow.map((i) => `- ${i}`).join('\n')}
-Refer to docs https://vitejs.dev/config/#server-fs-allow for configurations and more details.`
-
- server.config.logger.error(urlMessage)
- server.config.logger.warnOnce(hintMessage + '\n')
- res.statusCode = 403
- res.write(renderRestrictedErrorHTML(urlMessage + '\n' + hintMessage))
- res.end()
- } else {
- // if the file doesn't exist, we shouldn't restrict this path as it can
- // be an API call. Middlewares would issue a 404 if the file isn't handled
- next()
- }
- return false
+Refer to docs https://vite.dev/config/server-options.html#server-fs-allow for configurations and more details.`
+
+ server.config.logger.error(urlMessage)
+ server.config.logger.warnOnce(hintMessage + '\n')
+ res.statusCode = 403
+ res.write(renderRestrictedErrorHTML(urlMessage + '\n' + hintMessage))
+ res.end()
}
function renderRestrictedErrorHTML(msg: string): string {
@@ -196,7 +337,7 @@ function renderRestrictedErrorHTML(msg: string): string {
return html`
403 Restricted
- ${msg.replace(/\n/g, ' ')}
+ ${escapeHtml(msg).replace(/\n/g, ' ')}
+test elements below should show circles and their url
+
+
+
+Denied .env
+
+
+
+
diff --git a/playground/assets-sanitize/index.js b/playground/assets-sanitize/index.js
new file mode 100644
index 00000000000000..bac3b3b83e6b1d
--- /dev/null
+++ b/playground/assets-sanitize/index.js
@@ -0,0 +1,9 @@
+import plusCircle from './+circle.svg'
+import underscoreCircle from './_circle.svg'
+function setData(classname, file) {
+ const el = document.body.querySelector(classname)
+ el.style.backgroundImage = `url(${file})`
+ el.textContent = file
+}
+setData('.plus-circle', plusCircle)
+setData('.underscore-circle', underscoreCircle)
diff --git a/playground/assets-sanitize/package.json b/playground/assets-sanitize/package.json
new file mode 100644
index 00000000000000..1a110936d06db9
--- /dev/null
+++ b/playground/assets-sanitize/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-assets-sanitize",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/assets-sanitize/vite.config.js b/playground/assets-sanitize/vite.config.js
new file mode 100644
index 00000000000000..7eeb717a97b6b5
--- /dev/null
+++ b/playground/assets-sanitize/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ //speed up build
+ minify: false,
+ target: 'esnext',
+ assetsInlineLimit: 0,
+ manifest: true,
+ },
+})
diff --git a/playground/assets/__tests__/assets.spec.ts b/playground/assets/__tests__/assets.spec.ts
new file mode 100644
index 00000000000000..635f4c0e2678f3
--- /dev/null
+++ b/playground/assets/__tests__/assets.spec.ts
@@ -0,0 +1,714 @@
+import path from 'node:path'
+import { describe, expect, test } from 'vitest'
+import {
+ browserLogs,
+ editFile,
+ findAssetFile,
+ getBg,
+ getColor,
+ isBuild,
+ isServe,
+ listAssets,
+ notifyRebuildComplete,
+ page,
+ readFile,
+ readManifest,
+ serverLogs,
+ untilUpdated,
+ viteTestUrl,
+ watcher,
+} from '~utils'
+
+const assetMatch = isBuild
+ ? /\/foo\/bar\/assets\/asset-[-\w]{8}\.png/
+ : '/foo/bar/nested/asset.png'
+
+const encodedAssetMatch = isBuild
+ ? /\/foo\/bar\/assets\/asset_small_-[-\w]{8}\.png/
+ : '/foo/bar/nested/asset[small].png'
+
+const iconMatch = `/foo/bar/icon.png`
+
+const fetchPath = (p: string) => {
+ return fetch(path.posix.join(viteTestUrl, p), {
+ headers: { Accept: 'text/html,*/*' },
+ })
+}
+
+test('should have no 404s', () => {
+ browserLogs.forEach((msg) => {
+ expect(msg).not.toMatch('404')
+ })
+})
+
+test('should get a 404 when using incorrect case', async () => {
+ expect((await fetchPath('icon.png')).headers.get('Content-Type')).toBe(
+ 'image/png',
+ )
+ // fallback to index.html
+ const iconPngResult = await fetchPath('ICON.png')
+ expect(iconPngResult.headers.get('Content-Type')).toBe('text/html')
+ expect(iconPngResult.status).toBe(200)
+
+ expect((await fetchPath('bar')).headers.get('Content-Type')).toBe('')
+ // fallback to index.html
+ const barResult = await fetchPath('BAR')
+ expect(barResult.headers.get('Content-Type')).toContain('text/html')
+ expect(barResult.status).toBe(200)
+})
+
+test('should fallback to index.html when accessing non-existant html file', async () => {
+ expect((await fetchPath('doesnt-exist.html')).status).toBe(200)
+})
+
+describe.runIf(isServe)('outside base', () => {
+ test('should get a 404 with html', async () => {
+ const res = await fetch(new URL('/baz', viteTestUrl), {
+ headers: { Accept: 'text/html,*/*' },
+ })
+ expect(res.status).toBe(404)
+ expect(res.headers.get('Content-Type')).toBe('text/html')
+ })
+
+ test('should get a 404 with text', async () => {
+ const res = await fetch(new URL('/baz', viteTestUrl))
+ expect(res.status).toBe(404)
+ expect(res.headers.get('Content-Type')).toBe('text/plain')
+ })
+})
+
+describe('injected scripts', () => {
+ test('@vite/client', async () => {
+ const hasClient = await page.$(
+ 'script[type="module"][src="/foo/bar/@vite/client"]',
+ )
+ if (isBuild) {
+ expect(hasClient).toBeFalsy()
+ } else {
+ expect(hasClient).toBeTruthy()
+ }
+ })
+
+ test('html-proxy', async () => {
+ const hasHtmlProxy = await page.$(
+ 'script[type="module"][src^="/foo/bar/index.html?html-proxy"]',
+ )
+ if (isBuild) {
+ expect(hasHtmlProxy).toBeFalsy()
+ } else {
+ expect(hasHtmlProxy).toBeTruthy()
+ }
+ })
+})
+
+describe('raw references from /public', () => {
+ test('load raw js from /public', async () => {
+ expect(await page.textContent('.raw-js')).toMatch('[success]')
+ })
+
+ test('load raw css from /public', async () => {
+ expect(await getColor('.raw-css')).toBe('red')
+ })
+})
+
+test('import-expression from simple script', async () => {
+ expect(await page.textContent('.import-expression')).toMatch(
+ '[success][success]',
+ )
+})
+
+describe('asset imports from js', () => {
+ test('relative', async () => {
+ expect(await page.textContent('.asset-import-relative')).toMatch(assetMatch)
+ })
+
+ test('absolute', async () => {
+ expect(await page.textContent('.asset-import-absolute')).toMatch(assetMatch)
+ })
+
+ test('from /public', async () => {
+ expect(await page.textContent('.public-import')).toMatch(iconMatch)
+ })
+
+ test('from /public (json)', async () => {
+ expect(await page.textContent('.public-json-import')).toMatch(
+ '/foo/bar/foo.json',
+ )
+ expect(await page.textContent('.public-json-import-content'))
+ .toMatchInlineSnapshot(`
+ "{
+ "foo": "bar"
+ }
+ "
+ `)
+ })
+
+ test('from /public (js)', async () => {
+ expect(await page.textContent('.public-js-import')).toMatch(
+ '/foo/bar/raw.js',
+ )
+ expect(await page.textContent('.public-js-import-content'))
+ .toMatchInlineSnapshot(`
+ "document.querySelector('.raw-js').textContent =
+ '[success] Raw js from /public loaded'
+ "
+ `)
+ expect(await page.textContent('.public-js-import-content-type')).toMatch(
+ 'text/javascript',
+ )
+ })
+
+ test('from /public (ts)', async () => {
+ expect(await page.textContent('.public-ts-import')).toMatch(
+ '/foo/bar/raw.ts',
+ )
+ expect(await page.textContent('.public-ts-import-content'))
+ .toMatchInlineSnapshot(`
+ "export default function other() {
+ return 1 + 2
+ }
+ "
+ `)
+ // NOTE: users should configure the mime type for .ts files for preview server
+ if (isServe) {
+ expect(await page.textContent('.public-ts-import-content-type')).toMatch(
+ 'text/javascript',
+ )
+ }
+ })
+
+ test('from /public (mts)', async () => {
+ expect(await page.textContent('.public-mts-import')).toMatch(
+ '/foo/bar/raw.mts',
+ )
+ expect(await page.textContent('.public-mts-import-content'))
+ .toMatchInlineSnapshot(`
+ "export default function foobar() {
+ return 1 + 2
+ }
+ "
+ `)
+ // NOTE: users should configure the mime type for .ts files for preview server
+ if (isServe) {
+ expect(await page.textContent('.public-mts-import-content-type')).toMatch(
+ 'text/javascript',
+ )
+ }
+ })
+})
+
+describe('css url() references', () => {
+ test('fonts', async () => {
+ expect(
+ await page.evaluate(() => {
+ return (document as any).fonts.check('700 32px Inter')
+ }),
+ ).toBe(true)
+ })
+
+ test('relative', async () => {
+ expect(await getBg('.css-url-relative')).toMatch(assetMatch)
+ })
+
+ test('encoded', async () => {
+ expect(await getBg('.css-url-encoded')).toMatch(encodedAssetMatch)
+ })
+
+ test('image-set relative', async () => {
+ const imageSet = await getBg('.css-image-set-relative')
+ imageSet.split(', ').forEach((s) => {
+ expect(s).toMatch(assetMatch)
+ })
+ })
+
+ test('image-set without the url() call', async () => {
+ const imageSet = await getBg('.css-image-set-without-url-call')
+ imageSet.split(', ').forEach((s) => {
+ expect(s).toMatch(assetMatch)
+ })
+ })
+
+ test('image-set with var', async () => {
+ const imageSet = await getBg('.css-image-set-with-var')
+ imageSet.split(', ').forEach((s) => {
+ expect(s).toMatch(assetMatch)
+ })
+ })
+
+ test('image-set with mix', async () => {
+ const imageSet = await getBg('.css-image-set-mix-url-var')
+ imageSet.split(', ').forEach((s) => {
+ expect(s).toMatch(assetMatch)
+ })
+ })
+
+ test('image-set with base64', async () => {
+ const imageSet = await getBg('.css-image-set-base64')
+ expect(imageSet).toContain('image-set(url("data:image/png;base64,')
+ })
+
+ test('image-set with gradient', async () => {
+ const imageSet = await getBg('.css-image-set-gradient')
+ expect(imageSet).toContain('image-set(url("data:image/png;base64,')
+ })
+
+ test('image-set with multiple descriptor', async () => {
+ const imageSet = await getBg('.css-image-set-multiple-descriptor')
+ imageSet.split(', ').forEach((s) => {
+ expect(s).toMatch(assetMatch)
+ })
+ })
+
+ test('image-set with multiple descriptor as inline style', async () => {
+ const imageSet = await getBg(
+ '.css-image-set-multiple-descriptor-inline-style',
+ )
+ imageSet.split(', ').forEach((s) => {
+ expect(s).toMatch(assetMatch)
+ })
+ })
+
+ test('image-set and url exist at the same time.', async () => {
+ const imageSet = await getBg('.image-set-and-url-exsiting-at-same-time')
+ expect(imageSet).toMatch(assetMatch)
+ })
+
+ test('relative in @import', async () => {
+ expect(await getBg('.css-url-relative-at-imported')).toMatch(assetMatch)
+ })
+
+ test('absolute', async () => {
+ expect(await getBg('.css-url-absolute')).toMatch(assetMatch)
+ })
+
+ test('from /public', async () => {
+ expect(await getBg('.css-url-public')).toMatch(iconMatch)
+ })
+
+ test('base64 inline', async () => {
+ const match = isBuild ? `data:image/png;base64` : `/foo/bar/nested/icon.png`
+ expect(await getBg('.css-url-base64-inline')).toMatch(match)
+ expect(await getBg('.css-url-quotes-base64-inline')).toMatch(match)
+ })
+
+ test('no base64 inline for icon and manifest links', async () => {
+ const iconEl = await page.$(`link.ico`)
+ const href = await iconEl.getAttribute('href')
+ expect(href).toMatch(
+ isBuild ? /\/foo\/bar\/assets\/favicon-[-\w]{8}\.ico/ : 'favicon.ico',
+ )
+
+ const manifestEl = await page.$(`link[rel="manifest"]`)
+ const manifestHref = await manifestEl.getAttribute('href')
+ expect(manifestHref).toMatch(
+ isBuild ? /\/foo\/bar\/assets\/manifest-[-\w]{8}\.json/ : 'manifest.json',
+ )
+ })
+
+ test('multiple urls on the same line', async () => {
+ const bg = await getBg('.css-url-same-line')
+ expect(bg).toMatch(assetMatch)
+ expect(bg).toMatch(iconMatch)
+ })
+
+ test('aliased', async () => {
+ const bg = await getBg('.css-url-aliased')
+ expect(bg).toMatch(assetMatch)
+ })
+
+ test('preinlined SVG', async () => {
+ expect(await getBg('.css-url-preinlined-svg')).toMatch(
+ /data:image\/svg\+xml,.+/,
+ )
+ })
+
+ test.runIf(isBuild)('generated paths in CSS', () => {
+ const css = findAssetFile(/index-[-\w]{8}\.css$/, 'foo')
+
+ // preserve postfix query/hash
+ expect(css).toMatch(`woff2?#iefix`)
+
+ // generate non-relative base for public path in CSS
+ expect(css).not.toMatch(`../icon.png`)
+ })
+
+ test('url() with svg', async () => {
+ const bg = await getBg('.css-url-svg')
+ expect(bg).toMatch(/data:image\/svg\+xml,.+/)
+ expect(bg).toContain('blue')
+ expect(bg).not.toContain('red')
+
+ if (isServe) {
+ editFile('nested/fragment-bg-hmr.svg', (code) =>
+ code.replace('fill="blue"', 'fill="red"'),
+ )
+ await untilUpdated(() => getBg('.css-url-svg'), 'red')
+ }
+ })
+
+ test('image-set() with svg', async () => {
+ expect(await getBg('.css-image-set-svg')).toMatch(/data:image\/svg\+xml,.+/)
+ })
+
+ test('url() with svg in .css?url', async () => {
+ const bg = await getBg('.css-url-svg-in-url')
+ expect(bg).toMatch(/data:image\/svg\+xml,.+/)
+ expect(bg).toContain('blue')
+ expect(bg).not.toContain('red')
+
+ if (isServe) {
+ editFile('nested/fragment-bg-hmr2.svg', (code) =>
+ code.replace('fill="blue"', 'fill="red"'),
+ )
+ await untilUpdated(() => getBg('.css-url-svg'), 'red')
+ }
+ })
+})
+
+describe('image', () => {
+ test('src', async () => {
+ const img = await page.$('.img-src')
+ const src = await img.getAttribute('src')
+ expect(src).toMatch(
+ isBuild
+ ? /\/foo\/bar\/assets\/html-only-asset-[-\w]{8}\.jpg/
+ : /\/foo\/bar\/nested\/html-only-asset.jpg/,
+ )
+ })
+
+ test('src inline', async () => {
+ const img = await page.$('.img-src-inline')
+ const src = await img.getAttribute('src')
+ expect(src).toMatch(
+ isBuild
+ ? /^data:image\/svg\+xml,%3csvg/
+ : /\/foo\/bar\/nested\/inlined.svg/,
+ )
+ })
+
+ test('srcset', async () => {
+ const img = await page.$('.img-src-set')
+ const srcset = await img.getAttribute('srcset')
+ srcset.split(', ').forEach((s) => {
+ expect(s).toMatch(
+ isBuild
+ ? /\/foo\/bar\/assets\/asset-[-\w]{8}\.png \dx/
+ : /\/foo\/bar\/nested\/asset.png \dx/,
+ )
+ })
+ })
+
+ test('srcset (public)', async () => {
+ const img = await page.$('.img-src-set-public')
+ const srcset = await img.getAttribute('srcset')
+ srcset.split(', ').forEach((s) => {
+ expect(s).toMatch(/\/foo\/bar\/icon\.png \dx/)
+ })
+ })
+
+ test('srcset (mixed)', async () => {
+ const img = await page.$('.img-src-set-mixed')
+ const srcset = await img.getAttribute('srcset')
+ const srcs = srcset.split(', ')
+ expect(srcs[1]).toMatch(
+ isBuild
+ ? /\/foo\/bar\/assets\/asset-[-\w]{8}\.png \dx/
+ : /\/foo\/bar\/nested\/asset.png \dx/,
+ )
+ })
+})
+
+describe('meta', () => {
+ test('og image', async () => {
+ const meta = await page.$('.meta-og-image')
+ const content = await meta.getAttribute('content')
+ expect(content).toMatch(
+ isBuild
+ ? /\/foo\/bar\/assets\/asset-\w{8}\.png/
+ : /\/foo\/bar\/nested\/asset.png/,
+ )
+ })
+})
+
+describe('svg fragments', () => {
+ // 404 is checked already, so here we just ensure the urls end with #fragment
+ test('img url', async () => {
+ const img = await page.$('.svg-frag-img')
+ expect(await img.getAttribute('src')).toMatch(/svg#icon-clock-view$/)
+ })
+
+ test('via css url()', async () => {
+ const bg = await page.evaluate(() => {
+ return getComputedStyle(document.querySelector('.icon')).backgroundImage
+ })
+ expect(bg).toMatch(/svg#icon-clock-view"\)$/)
+ })
+
+ test('from js import', async () => {
+ const img = await page.$('.svg-frag-import')
+ expect(await img.getAttribute('src')).toMatch(
+ // Assert trimmed (data URI starts with < and ends with >)
+ /^data:image\/svg\+xml,%3c.*%3e#icon-heart-view$/,
+ )
+ })
+
+ test('url with an alias', async () => {
+ expect(await getBg('.icon-clock-alias')).toMatch(
+ /\.svg#icon-clock-view"\)$/,
+ )
+ })
+})
+
+test('Unknown extension assets import', async () => {
+ expect(await page.textContent('.unknown-ext')).toMatch(
+ isBuild ? 'data:application/octet-stream;' : '/nested/foo.unknown',
+ )
+})
+
+test('?raw import', async () => {
+ expect(await page.textContent('.raw')).toMatch('SVG')
+})
+
+test('?no-inline svg import', async () => {
+ expect(await page.textContent('.no-inline-svg')).toMatch(
+ isBuild
+ ? /\/foo\/bar\/assets\/fragment-[-\w]{8}\.svg/
+ : '/foo/bar/nested/fragment.svg?no-inline',
+ )
+})
+
+test('?no-inline svg import -- multiple postfix', async () => {
+ expect(await page.textContent('.no-inline-svg-mp')).toMatch(
+ isBuild
+ ? /\/foo\/bar\/assets\/fragment-[-\w]{8}\.svg\?foo=bar/
+ : '/foo/bar/nested/fragment.svg?no-inline&foo=bar',
+ )
+})
+
+test('?inline png import', async () => {
+ expect(await page.textContent('.inline-png')).toMatch(
+ /^data:image\/png;base64,/,
+ )
+})
+
+test('?inline public png import', async () => {
+ expect(await page.textContent('.inline-public-png')).toMatch(
+ /^data:image\/png;base64,/,
+ )
+})
+
+test('?inline public json import', async () => {
+ expect(await page.textContent('.inline-public-json')).toMatch(
+ /^data:application\/json;base64,/,
+ )
+})
+
+test('?url import', async () => {
+ const src = readFile('foo.js')
+ expect(await page.textContent('.url')).toMatch(
+ isBuild
+ ? `data:text/javascript;base64,${Buffer.from(src).toString('base64')}`
+ : `/foo/bar/foo.js`,
+ )
+})
+
+test('?url import on css', async () => {
+ const txt = await page.textContent('.url-css')
+ expect(txt).toMatch(
+ isBuild
+ ? /\/foo\/bar\/assets\/icons-[-\w]{8}\.css/
+ : '/foo/bar/css/icons.css',
+ )
+})
+
+describe('unicode url', () => {
+ test('from js import', async () => {
+ const src = readFile('テスト-測試-white space.js')
+ expect(await page.textContent('.unicode-url')).toMatch(
+ isBuild
+ ? `data:text/javascript;base64,${Buffer.from(src).toString('base64')}`
+ : encodeURI(`/foo/bar/テスト-測試-white space.js`),
+ )
+ })
+})
+
+describe.runIf(isBuild)('encodeURI', () => {
+ test('img src with encodeURI', async () => {
+ const img = await page.$('.encodeURI')
+ expect(await img.getAttribute('src')).toMatch(/^data:image\/png;base64,/)
+ })
+})
+
+test('new URL(..., import.meta.url)', async () => {
+ expect(await page.textContent('.import-meta-url')).toMatch(assetMatch)
+})
+
+test('new URL("@/...", import.meta.url)', async () => {
+ expect(await page.textContent('.import-meta-url-dep')).toMatch(assetMatch)
+})
+
+test('new URL("/...", import.meta.url)', async () => {
+ expect(await page.textContent('.import-meta-url-base-path')).toMatch(
+ iconMatch,
+ )
+})
+
+test('new URL("data:...", import.meta.url)', async () => {
+ const img = await page.$('.import-meta-url-data-uri-img')
+ expect(await img.getAttribute('src')).toMatch(/^data:image\/png;base64,/)
+ expect(await page.textContent('.import-meta-url-data-uri')).toMatch(
+ /^data:image\/png;base64,/,
+ )
+})
+
+test('new URL(..., import.meta.url) without extension', async () => {
+ expect(await page.textContent('.import-meta-url-without-extension')).toMatch(
+ isBuild ? 'data:text/javascript' : 'nested/test.js',
+ )
+ expect(
+ await page.textContent('.import-meta-url-content-without-extension'),
+ ).toContain('export default class')
+})
+
+test('new URL(`${dynamic}`, import.meta.url)', async () => {
+ expect(await page.textContent('.dynamic-import-meta-url-1')).toMatch(
+ isBuild ? 'data:image/png;base64' : '/foo/bar/nested/icon.png',
+ )
+ expect(await page.textContent('.dynamic-import-meta-url-2')).toMatch(
+ assetMatch,
+ )
+ expect(await page.textContent('.dynamic-import-meta-url-js')).toMatch(
+ isBuild ? 'data:text/javascript;base64' : '/foo/bar/nested/test.js',
+ )
+})
+
+test('new URL(`./${dynamic}?abc`, import.meta.url)', async () => {
+ expect(await page.textContent('.dynamic-import-meta-url-1-query')).toMatch(
+ isBuild ? 'data:image/png;base64' : '/foo/bar/nested/icon.png?abc',
+ )
+ expect(await page.textContent('.dynamic-import-meta-url-2-query')).toMatch(
+ isBuild
+ ? /\/foo\/bar\/assets\/asset-[-\w]{8}\.png\?abc/
+ : '/foo/bar/nested/asset.png?abc',
+ )
+})
+
+test('new URL(`./${1 === 0 ? static : dynamic}?abc`, import.meta.url)', async () => {
+ expect(await page.textContent('.dynamic-import-meta-url-1-ternary')).toMatch(
+ isBuild ? 'data:image/png;base64' : '/foo/bar/nested/icon.png?abc',
+ )
+ expect(await page.textContent('.dynamic-import-meta-url-2-ternary')).toMatch(
+ isBuild
+ ? /\/foo\/bar\/assets\/asset-[-\w]{8}\.png\?abc/
+ : '/foo/bar/nested/asset.png?abc',
+ )
+})
+
+test("new URL(/* @vite-ignore */ 'non-existent', import.meta.url)", async () => {
+ // the inlined script tag is extracted in a separate file
+ const importMetaUrl = new URL(
+ isBuild ? '/foo/bar/assets/index.js' : '/foo/bar/index.html',
+ page.url(),
+ )
+ expect(await page.textContent('.non-existent-import-meta-url')).toMatch(
+ new URL('non-existent', importMetaUrl).pathname,
+ )
+ expect(serverLogs).not.toContainEqual(
+ expect.stringContaining("doesn't exist at build time"),
+ )
+})
+
+test.runIf(isBuild)('manifest', async () => {
+ const manifest = readManifest('foo')
+ const entry = manifest['index.html']
+
+ for (const file of listAssets('foo')) {
+ if (file.endsWith('.css')) {
+ // ignore icons-*.css and css-url-url-*.css as it's imported with ?url
+ if (file.includes('icons-') || file.includes('css-url-url-')) continue
+ expect(entry.css).toContain(`assets/${file}`)
+ } else if (!file.endsWith('.js')) {
+ expect(entry.assets).toContain(`assets/${file}`)
+ }
+ }
+})
+
+describe.runIf(isBuild)('css and assets in css in build watch', () => {
+ test('css will not be lost and css does not contain undefined', async () => {
+ editFile('index.html', (code) => code.replace('Assets', 'assets'))
+ await notifyRebuildComplete(watcher)
+ const cssFile = findAssetFile(/index-[-\w]+\.css$/, 'foo')
+ expect(cssFile).not.toBe('')
+ expect(cssFile).not.toMatch(/undefined/)
+ })
+
+ test('import module.css', async () => {
+ expect(await getColor('#foo')).toBe('red')
+ editFile('css/foo.module.css', (code) => code.replace('red', 'blue'))
+ await notifyRebuildComplete(watcher)
+ await page.reload()
+ expect(await getColor('#foo')).toBe('blue')
+ })
+
+ test('import with raw query', async () => {
+ expect(await page.textContent('.raw-query')).toBe('foo')
+ editFile('static/foo.txt', (code) => code.replace('foo', 'zoo'))
+ await notifyRebuildComplete(watcher)
+ await page.reload()
+ expect(await page.textContent('.raw-query')).toBe('zoo')
+ })
+})
+
+test('inline style test', async () => {
+ expect(await getBg('.inline-style')).toMatch(assetMatch)
+ expect(await getBg('.style-url-assets')).toMatch(assetMatch)
+})
+
+if (!isBuild) {
+ test('@import in html style tag hmr', async () => {
+ await untilUpdated(() => getColor('.import-css'), 'rgb(0, 136, 255)')
+ const loadPromise = page.waitForEvent('load')
+ editFile('./css/import.css', (code) => code.replace('#0088ff', '#00ff88'))
+ await loadPromise
+ await untilUpdated(() => getColor('.import-css'), 'rgb(0, 255, 136)')
+ })
+}
+
+test('html import word boundary', async () => {
+ expect(await page.textContent('.obj-import-express')).toMatch(
+ 'ignore object import prop',
+ )
+ expect(await page.textContent('.string-import-express')).toMatch('no load')
+})
+
+test('relative path in html asset', async () => {
+ expect(await page.textContent('.relative-js')).toMatch('hello')
+ expect(await getColor('.relative-css')).toMatch('red')
+})
+
+test('url() contains file in publicDir, in
+
+ inline style
+
+use style class
+
+base64
+
+
+ inline style
+
+use style class
+from publicDir
+
+
+ inline style
+
+use style class
+
+@import
+
+
+
+ @import CSS from publicDir should load (this should be red)
+
+import module css
+
+
+
+style in svg
+
+
+
+
+
+
+
+
+assets in noscript
+
+
+
+
+assets in template
+
+
+
+
+
+link style
+
+
+
+
+
diff --git a/packages/playground/dynamic-import/mxd.json b/playground/assets/manifest.json
similarity index 100%
rename from packages/playground/dynamic-import/mxd.json
rename to playground/assets/manifest.json
diff --git a/packages/playground/assets/nested/asset.png b/playground/assets/nested/asset.png
similarity index 100%
rename from packages/playground/assets/nested/asset.png
rename to playground/assets/nested/asset.png
diff --git a/playground/assets/nested/asset[small].png b/playground/assets/nested/asset[small].png
new file mode 100644
index 00000000000000..cf5a52d15fc95e
Binary files /dev/null and b/playground/assets/nested/asset[small].png differ
diff --git a/playground/assets/nested/foo.unknown b/playground/assets/nested/foo.unknown
new file mode 100644
index 00000000000000..e24f83b664c55c
--- /dev/null
+++ b/playground/assets/nested/foo.unknown
@@ -0,0 +1 @@
+custom file
diff --git a/playground/assets/nested/fragment-bg-hmr.svg b/playground/assets/nested/fragment-bg-hmr.svg
new file mode 100644
index 00000000000000..44e4248f924d70
--- /dev/null
+++ b/playground/assets/nested/fragment-bg-hmr.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/assets/nested/fragment-bg-hmr2.svg b/playground/assets/nested/fragment-bg-hmr2.svg
new file mode 100644
index 00000000000000..44e4248f924d70
--- /dev/null
+++ b/playground/assets/nested/fragment-bg-hmr2.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/playground/assets/nested/fragment-bg.svg b/playground/assets/nested/fragment-bg.svg
similarity index 100%
rename from packages/playground/assets/nested/fragment-bg.svg
rename to playground/assets/nested/fragment-bg.svg
diff --git a/packages/playground/assets/nested/fragment.svg b/playground/assets/nested/fragment.svg
similarity index 100%
rename from packages/playground/assets/nested/fragment.svg
rename to playground/assets/nested/fragment.svg
diff --git a/playground/assets/nested/html-only-asset.jpg b/playground/assets/nested/html-only-asset.jpg
new file mode 100644
index 00000000000000..289e5ae196ad0e
Binary files /dev/null and b/playground/assets/nested/html-only-asset.jpg differ
diff --git a/packages/playground/assets/nested/icon.png b/playground/assets/nested/icon.png
similarity index 100%
rename from packages/playground/assets/nested/icon.png
rename to playground/assets/nested/icon.png
diff --git a/playground/assets/nested/inlined.svg b/playground/assets/nested/inlined.svg
new file mode 100644
index 00000000000000..e00a25209ebd4e
--- /dev/null
+++ b/playground/assets/nested/inlined.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/playground/assets/nested/test.js b/playground/assets/nested/test.js
new file mode 100644
index 00000000000000..1a292f36ac7916
--- /dev/null
+++ b/playground/assets/nested/test.js
@@ -0,0 +1,3 @@
+export default class a {
+ name = 'a'
+}
diff --git a/playground/assets/nested/with-single'quote.png b/playground/assets/nested/with-single'quote.png
new file mode 100644
index 00000000000000..cb1c88d48c090a
Binary files /dev/null and b/playground/assets/nested/with-single'quote.png differ
diff --git a/packages/playground/css/ok.png "b/playground/assets/nested/\343\203\206\343\202\271\343\203\210-\346\270\254\350\251\246-white space.png"
similarity index 100%
rename from packages/playground/css/ok.png
rename to "playground/assets/nested/\343\203\206\343\202\271\343\203\210-\346\270\254\350\251\246-white space.png"
diff --git a/playground/assets/package.json b/playground/assets/package.json
new file mode 100644
index 00000000000000..e90a090605bd61
--- /dev/null
+++ b/playground/assets/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@vitejs/test-assets",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "dev:encoded-base": "vite --config ./vite.config-encoded-base.js dev",
+ "build:encoded-base": "vite --config ./vite.config-encoded-base.js build",
+ "preview:encoded-base": "vite --config ./vite.config-encoded-base.js preview",
+ "dev:relative-base": "vite --config ./vite.config-relative-base.js dev",
+ "build:relative-base": "vite --config ./vite.config-relative-base.js build",
+ "preview:relative-base": "vite --config ./vite.config-relative-base.js preview",
+ "dev:runtime-base": "vite --config ./vite.config-runtime-base.js dev",
+ "build:runtime-base": "vite --config ./vite.config-runtime-base.js build",
+ "preview:runtime-base": "vite --config ./vite.config-runtime-base.js preview",
+ "dev:url-base": "vite --config ./vite.config-url-base.js dev",
+ "build:url-base": "vite --config ./vite.config-url-base.js build",
+ "preview:url-base": "vite --config ./vite.config-url-base.js preview"
+ }
+}
diff --git a/playground/assets/static/bar b/playground/assets/static/bar
new file mode 100644
index 00000000000000..5716ca5987cbf9
--- /dev/null
+++ b/playground/assets/static/bar
@@ -0,0 +1 @@
+bar
diff --git a/packages/playground/assets/static/foo.css b/playground/assets/static/foo.css
similarity index 100%
rename from packages/playground/assets/static/foo.css
rename to playground/assets/static/foo.css
diff --git a/playground/assets/static/foo.json b/playground/assets/static/foo.json
new file mode 100644
index 00000000000000..c8c4105eb57cda
--- /dev/null
+++ b/playground/assets/static/foo.json
@@ -0,0 +1,3 @@
+{
+ "foo": "bar"
+}
diff --git a/playground/assets/static/foo.txt b/playground/assets/static/foo.txt
new file mode 100644
index 00000000000000..19102815663d23
--- /dev/null
+++ b/playground/assets/static/foo.txt
@@ -0,0 +1 @@
+foo
\ No newline at end of file
diff --git a/packages/playground/assets/static/icon.png b/playground/assets/static/icon.png
similarity index 100%
rename from packages/playground/assets/static/icon.png
rename to playground/assets/static/icon.png
diff --git a/packages/playground/assets/static/import-expression.js b/playground/assets/static/import-expression.js
similarity index 100%
rename from packages/playground/assets/static/import-expression.js
rename to playground/assets/static/import-expression.js
diff --git a/packages/playground/assets/static/raw.css b/playground/assets/static/raw.css
similarity index 100%
rename from packages/playground/assets/static/raw.css
rename to playground/assets/static/raw.css
diff --git a/packages/playground/assets/static/raw.js b/playground/assets/static/raw.js
similarity index 100%
rename from packages/playground/assets/static/raw.js
rename to playground/assets/static/raw.js
diff --git a/playground/assets/static/raw.mts b/playground/assets/static/raw.mts
new file mode 100644
index 00000000000000..6719e0c239a418
--- /dev/null
+++ b/playground/assets/static/raw.mts
@@ -0,0 +1,3 @@
+export default function foobar() {
+ return 1 + 2
+}
diff --git a/playground/assets/static/raw.ts b/playground/assets/static/raw.ts
new file mode 100644
index 00000000000000..7faded7a1614a6
--- /dev/null
+++ b/playground/assets/static/raw.ts
@@ -0,0 +1,3 @@
+export default function other() {
+ return 1 + 2
+}
diff --git a/playground/assets/vite.config-encoded-base.js b/playground/assets/vite.config-encoded-base.js
new file mode 100644
index 00000000000000..70e86eeb90eb37
--- /dev/null
+++ b/playground/assets/vite.config-encoded-base.js
@@ -0,0 +1,34 @@
+import { defineConfig } from 'vite'
+import baseConfig from './vite.config.js'
+
+/** see `ports` variable in test-utils.ts */
+const port = 9524
+
+export default defineConfig({
+ ...baseConfig,
+ // Vite should auto-encode this as `/foo%20bar/` internally
+ base: '/foo bar/',
+ server: {
+ port,
+ strictPort: true,
+ },
+ build: {
+ ...baseConfig.build,
+ outDir: 'dist/encoded-base',
+ watch: null,
+ minify: false,
+ assetsInlineLimit: 0,
+ rollupOptions: {
+ output: {
+ entryFileNames: 'entries/[name].js',
+ chunkFileNames: 'chunks/[name]-[hash].js',
+ assetFileNames: 'other-assets/[name]-[hash][extname]',
+ },
+ },
+ },
+ preview: {
+ port,
+ strictPort: true,
+ },
+ cacheDir: 'node_modules/.vite-encoded-base',
+})
diff --git a/playground/assets/vite.config-relative-base.js b/playground/assets/vite.config-relative-base.js
new file mode 100644
index 00000000000000..1e25168e1c0aff
--- /dev/null
+++ b/playground/assets/vite.config-relative-base.js
@@ -0,0 +1,27 @@
+import { defineConfig } from 'vite'
+import baseConfig from './vite.config.js'
+
+export default defineConfig(({ isPreview }) => ({
+ ...baseConfig,
+ base: !isPreview ? './' : '/relative-base/', // relative base to make dist portable
+ build: {
+ ...baseConfig.build,
+ outDir: 'dist/relative-base',
+ watch: null,
+ minify: false,
+ assetsInlineLimit: 0,
+ rollupOptions: {
+ output: {
+ entryFileNames: 'entries/[name].js',
+ chunkFileNames: 'chunks/[name]-[hash].js',
+ assetFileNames: 'other-assets/[name]-[hash][extname]',
+ manualChunks(id) {
+ if (id.includes('css/manual-chunks.css')) {
+ return 'css/manual-chunks'
+ }
+ },
+ },
+ },
+ },
+ cacheDir: 'node_modules/.vite-relative-base',
+}))
diff --git a/playground/assets/vite.config-runtime-base.js b/playground/assets/vite.config-runtime-base.js
new file mode 100644
index 00000000000000..5113ccebc68c3d
--- /dev/null
+++ b/playground/assets/vite.config-runtime-base.js
@@ -0,0 +1,61 @@
+import { defineConfig } from 'vite'
+import baseConfig from './vite.config.js'
+
+const dynamicBaseAssetsCode = `
+globalThis.__toAssetUrl = url => '/' + url
+globalThis.__publicBase = '/'
+`
+
+export default defineConfig({
+ ...baseConfig,
+ base: './', // overwrite the original base: '/foo/'
+ build: {
+ ...baseConfig.build,
+ outDir: 'dist/runtime-base',
+ watch: null,
+ minify: false,
+ assetsInlineLimit: 0,
+ rollupOptions: {
+ output: {
+ entryFileNames: 'entries/[name].js',
+ chunkFileNames: 'chunks/[name]-[hash].js',
+ assetFileNames: 'other-assets/[name]-[hash][extname]',
+ },
+ },
+ },
+ plugins: [
+ {
+ name: 'dynamic-base-assets-globals',
+ transformIndexHtml(_, ctx) {
+ if (ctx.bundle) {
+ // Only inject during build
+ return [
+ {
+ tag: 'script',
+ attrs: { type: 'module' },
+ children: dynamicBaseAssetsCode,
+ },
+ ]
+ }
+ },
+ },
+ ],
+ experimental: {
+ renderBuiltUrl(filename, { hostType, type }) {
+ if (type === 'asset') {
+ if (hostType === 'js') {
+ return {
+ runtime: `globalThis.__toAssetUrl(${JSON.stringify(filename)})`,
+ }
+ }
+ } else if (type === 'public') {
+ if (hostType === 'js') {
+ return {
+ runtime: `globalThis.__publicBase+${JSON.stringify(filename)}`,
+ }
+ }
+ }
+ },
+ },
+ cacheDir: 'node_modules/.vite-runtime-base',
+})
diff --git a/playground/assets/vite.config-url-base.js b/playground/assets/vite.config-url-base.js
new file mode 100644
index 00000000000000..14d24feae4298d
--- /dev/null
+++ b/playground/assets/vite.config-url-base.js
@@ -0,0 +1,33 @@
+import { defineConfig } from 'vite'
+import baseConfig from './vite.config.js'
+
+/** see `ports` variable in test-utils.ts */
+const port = 9525
+
+export default defineConfig({
+ ...baseConfig,
+ base: `http://localhost:${port}/`,
+ server: {
+ port,
+ strictPort: true,
+ },
+ build: {
+ ...baseConfig.build,
+ outDir: 'dist/url-base',
+ watch: null,
+ minify: false,
+ assetsInlineLimit: 0,
+ rollupOptions: {
+ output: {
+ entryFileNames: 'entries/[name].js',
+ chunkFileNames: 'chunks/[name]-[hash].js',
+ assetFileNames: 'other-assets/[name]-[hash][extname]',
+ },
+ },
+ },
+ preview: {
+ port,
+ strictPort: true,
+ },
+ cacheDir: 'node_modules/.vite-url-base',
+})
diff --git a/playground/assets/vite.config.js b/playground/assets/vite.config.js
new file mode 100644
index 00000000000000..e0ba18d5e69567
--- /dev/null
+++ b/playground/assets/vite.config.js
@@ -0,0 +1,20 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ base: '/foo/bar',
+ publicDir: 'static',
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'nested'),
+ fragment: path.resolve(__dirname, 'nested/fragment-bg.svg'),
+ },
+ },
+ assetsInclude: ['**/*.unknown'],
+ build: {
+ outDir: 'dist/foo',
+ assetsInlineLimit: 8000, // 8 kB
+ manifest: true,
+ watch: {},
+ },
+})
diff --git "a/playground/assets/\343\203\206\343\202\271\343\203\210-\346\270\254\350\251\246-white space.js" "b/playground/assets/\343\203\206\343\202\271\343\203\210-\346\270\254\350\251\246-white space.js"
new file mode 100644
index 00000000000000..81de0358864984
--- /dev/null
+++ "b/playground/assets/\343\203\206\343\202\271\343\203\210-\346\270\254\350\251\246-white space.js"
@@ -0,0 +1 @@
+console.log('test Unicode')
diff --git a/playground/backend-integration/__tests__/backend-integration.spec.ts b/playground/backend-integration/__tests__/backend-integration.spec.ts
new file mode 100644
index 00000000000000..2e69f77f5e9d4f
--- /dev/null
+++ b/playground/backend-integration/__tests__/backend-integration.spec.ts
@@ -0,0 +1,132 @@
+import { describe, expect, test, vi } from 'vitest'
+import {
+ browserErrors,
+ browserLogs,
+ editFile,
+ getColor,
+ isBuild,
+ isServe,
+ listAssets,
+ page,
+ ports,
+ readManifest,
+ serverLogs,
+ untilBrowserLogAfter,
+ untilUpdated,
+} from '~utils'
+
+test('should have no 404s', () => {
+ browserLogs.forEach((msg) => {
+ expect(msg).not.toMatch('404')
+ })
+})
+
+describe('asset imports from js', () => {
+ test('file outside root', async () => {
+ // assert valid image src https://github.com/microsoft/playwright/issues/6046#issuecomment-1799585719
+ await vi.waitUntil(() =>
+ page
+ .locator('.asset-reference.outside-root .asset-preview')
+ .evaluate((el: HTMLImageElement) => el.naturalWidth > 0),
+ )
+
+ const text = await page.textContent(
+ '.asset-reference.outside-root .asset-url',
+ )
+ if (isBuild) {
+ expect(text).toMatch(/\/dev\/assets\/logo-[-\w]{8}\.png/)
+ } else {
+ // asset url is prefixed with server.origin
+ expect(text).toMatch(
+ `http://localhost:${ports['backend-integration']}/dev/@fs/`,
+ )
+ expect(text).toMatch(/\/dev\/@fs\/.+?\/images\/logo\.png/)
+ }
+ })
+})
+
+describe.runIf(isBuild)('build', () => {
+ test('manifest', async () => {
+ const manifest = readManifest('dev')
+ const htmlEntry = manifest['index.html']
+ const mainTsEntry = manifest['main.ts']
+ const cssAssetEntry = manifest['global.css']
+ const pcssAssetEntry = manifest['foo.pcss']
+ const scssAssetEntry = manifest['nested/blue.scss']
+ const imgAssetEntry = manifest['../images/logo.png']
+ const dirFooAssetEntry = manifest['../../dir/foo.css']
+ const iconEntrypointEntry = manifest['icon.png']
+ const waterContainerEntry = manifest['water-container.svg']
+ expect(htmlEntry.css.length).toEqual(1)
+ expect(htmlEntry.assets.length).toEqual(1)
+ expect(mainTsEntry.assets?.length ?? 0).toBeGreaterThanOrEqual(1)
+ expect(mainTsEntry.assets).toContainEqual(
+ expect.stringMatching(/assets\/url-[-\w]{8}\.css/),
+ )
+ expect(cssAssetEntry?.file).not.toBeUndefined()
+ expect(cssAssetEntry?.isEntry).toEqual(true)
+ expect(pcssAssetEntry?.file).not.toBeUndefined()
+ expect(pcssAssetEntry?.isEntry).toEqual(true)
+ expect(scssAssetEntry?.file).not.toBeUndefined()
+ expect(scssAssetEntry?.src).toEqual('nested/blue.scss')
+ expect(scssAssetEntry?.isEntry).toEqual(true)
+ expect(imgAssetEntry?.file).not.toBeUndefined()
+ expect(imgAssetEntry?.isEntry).toBeUndefined()
+ expect(dirFooAssetEntry).not.toBeUndefined() // '\\' should not be used even on windows
+ // use the entry name
+ expect(dirFooAssetEntry.file).toMatch('assets/bar-')
+ expect(dirFooAssetEntry.names).toStrictEqual(['bar.css'])
+ expect(iconEntrypointEntry?.file).not.toBeUndefined()
+ expect(waterContainerEntry?.file).not.toBeUndefined()
+ })
+
+ test('CSS imported from JS entry should have a non-nested chunk name', () => {
+ const manifest = readManifest('dev')
+ const mainTsEntryCss = manifest['nested/sub.ts'].css
+ expect(mainTsEntryCss.length).toBe(1)
+ expect(mainTsEntryCss[0].replace('assets/', '')).not.toContain('/')
+ })
+
+ test('entrypoint assets should not generate empty JS file', () => {
+ expect(serverLogs).not.toContainEqual(
+ 'Generated an empty chunk: "icon.png".',
+ )
+
+ const assets = listAssets('dev')
+ expect(assets).not.toContainEqual(
+ expect.stringMatching(/icon.png-[-\w]{8}\.js$/),
+ )
+ })
+})
+
+describe.runIf(isServe)('serve', () => {
+ test('No ReferenceError', async () => {
+ browserErrors.forEach((error) => {
+ expect(error.name).not.toBe('ReferenceError')
+ })
+ })
+
+ test('preserve the base in CSS HMR', async () => {
+ await untilUpdated(() => getColor('body'), 'black') // sanity check
+ editFile('frontend/entrypoints/global.css', (code) =>
+ code.replace('black', 'red'),
+ )
+ await untilUpdated(() => getColor('body'), 'red') // successful HMR
+
+ // Verify that the base (/dev/) was added during the css-update
+ const link = await page.$('link[rel="stylesheet"]:last-of-type')
+ expect(await link.getAttribute('href')).toContain('/dev/global.css?t=')
+ })
+
+ test('CSS dependencies are tracked for HMR', async () => {
+ const el = await page.$('h1')
+ await untilBrowserLogAfter(
+ () =>
+ editFile('frontend/entrypoints/main.ts', (code) =>
+ code.replace('text-black', 'text-[rgb(204,0,0)]'),
+ ),
+ '[vite] css hot updated: /global.css',
+ )
+ await untilUpdated(() => getColor(el), 'rgb(204, 0, 0)')
+ })
+})
diff --git a/playground/backend-integration/dir/foo.css b/playground/backend-integration/dir/foo.css
new file mode 100644
index 00000000000000..c2fad7486d3ab6
--- /dev/null
+++ b/playground/backend-integration/dir/foo.css
@@ -0,0 +1,3 @@
+.windows-path-foo {
+ color: blue;
+}
diff --git a/playground/backend-integration/frontend/entrypoints/foo.pcss b/playground/backend-integration/frontend/entrypoints/foo.pcss
new file mode 100644
index 00000000000000..a49e02c1cc73bf
--- /dev/null
+++ b/playground/backend-integration/frontend/entrypoints/foo.pcss
@@ -0,0 +1,3 @@
+.foo_pcss {
+ color: blue;
+}
diff --git a/packages/playground/backend-integration/frontend/entrypoints/global.css b/playground/backend-integration/frontend/entrypoints/global.css
similarity index 100%
rename from packages/playground/backend-integration/frontend/entrypoints/global.css
rename to playground/backend-integration/frontend/entrypoints/global.css
diff --git a/packages/playground/css/nested/icon.png b/playground/backend-integration/frontend/entrypoints/icon.png
similarity index 100%
rename from packages/playground/css/nested/icon.png
rename to playground/backend-integration/frontend/entrypoints/icon.png
diff --git a/playground/backend-integration/frontend/entrypoints/index.html b/playground/backend-integration/frontend/entrypoints/index.html
new file mode 100644
index 00000000000000..bc75dbc4b898e9
--- /dev/null
+++ b/playground/backend-integration/frontend/entrypoints/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+Backend Integration
+
+
+ This test configures the root
to simulate a Laravel/Rails setup.
+
+
+JS Asset References
+
+
+
+CSS Asset References
+
+
+
+ Background URL with Alias:
+
+
+
+ Background URL with Relative Path:
+
+
+
+
+CSS imported from JS
+
+text
+
+
+
+
diff --git a/playground/backend-integration/frontend/entrypoints/main.ts b/playground/backend-integration/frontend/entrypoints/main.ts
new file mode 100644
index 00000000000000..f32a8084f2a90d
--- /dev/null
+++ b/playground/backend-integration/frontend/entrypoints/main.ts
@@ -0,0 +1,25 @@
+import 'vite/modulepreload-polyfill'
+import cssUrl from '../styles/url.css?url'
+import waterContainer from './water-container.svg'
+
+const cssLink = document.createElement('link')
+cssLink.rel = 'stylesheet'
+cssLink.href = cssUrl
+document.querySelector('head').prepend(cssLink)
+
+const dummyMeta = document.createElement('meta')
+dummyMeta.name = 'dummy'
+dummyMeta.content = waterContainer
+document.querySelector('head').append(dummyMeta)
+
+export const colorClass = 'text-black'
+
+export function colorHeading() {
+ document.querySelector('h1').className = colorClass
+}
+
+colorHeading()
+
+if (import.meta.hot) {
+ import.meta.hot.accept()
+}
diff --git a/playground/backend-integration/frontend/entrypoints/nested/blue.scss b/playground/backend-integration/frontend/entrypoints/nested/blue.scss
new file mode 100644
index 00000000000000..67be4f93044cce
--- /dev/null
+++ b/playground/backend-integration/frontend/entrypoints/nested/blue.scss
@@ -0,0 +1,5 @@
+$primary: #cc0000;
+
+.text-primary {
+ color: $primary;
+}
diff --git a/playground/backend-integration/frontend/entrypoints/nested/sub.ts b/playground/backend-integration/frontend/entrypoints/nested/sub.ts
new file mode 100644
index 00000000000000..9f9f2d70dc674a
--- /dev/null
+++ b/playground/backend-integration/frontend/entrypoints/nested/sub.ts
@@ -0,0 +1 @@
+import '../../styles/imported.css'
diff --git a/playground/backend-integration/frontend/entrypoints/water-container.svg b/playground/backend-integration/frontend/entrypoints/water-container.svg
new file mode 100644
index 00000000000000..39e4aaaf282cb7
--- /dev/null
+++ b/playground/backend-integration/frontend/entrypoints/water-container.svg
@@ -0,0 +1 @@
+
diff --git a/packages/playground/backend-integration/frontend/images/logo.png b/playground/backend-integration/frontend/images/logo.png
similarity index 100%
rename from packages/playground/backend-integration/frontend/images/logo.png
rename to playground/backend-integration/frontend/images/logo.png
diff --git a/packages/playground/backend-integration/frontend/styles/background.css b/playground/backend-integration/frontend/styles/background.css
similarity index 100%
rename from packages/playground/backend-integration/frontend/styles/background.css
rename to playground/backend-integration/frontend/styles/background.css
diff --git a/playground/backend-integration/frontend/styles/imported.css b/playground/backend-integration/frontend/styles/imported.css
new file mode 100644
index 00000000000000..65af30a2064c86
--- /dev/null
+++ b/playground/backend-integration/frontend/styles/imported.css
@@ -0,0 +1,3 @@
+.imported {
+ color: green;
+}
diff --git a/playground/backend-integration/frontend/styles/tailwind.css b/playground/backend-integration/frontend/styles/tailwind.css
new file mode 100644
index 00000000000000..d4b5078586e291
--- /dev/null
+++ b/playground/backend-integration/frontend/styles/tailwind.css
@@ -0,0 +1 @@
+@import 'tailwindcss';
diff --git a/playground/backend-integration/frontend/styles/url.css b/playground/backend-integration/frontend/styles/url.css
new file mode 100644
index 00000000000000..6c9daf3ed51d1f
--- /dev/null
+++ b/playground/backend-integration/frontend/styles/url.css
@@ -0,0 +1,3 @@
+.url {
+ color: red;
+}
diff --git a/playground/backend-integration/package.json b/playground/backend-integration/package.json
new file mode 100644
index 00000000000000..180c51667c42a3
--- /dev/null
+++ b/playground/backend-integration/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@vitejs/test-backend-integration",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.1.6",
+ "sass": "^1.88.0",
+ "tailwindcss": "^4.1.6",
+ "tinyglobby": "^0.2.13"
+ }
+}
diff --git a/packages/playground/backend-integration/references.css b/playground/backend-integration/references.css
similarity index 100%
rename from packages/playground/backend-integration/references.css
rename to playground/backend-integration/references.css
diff --git a/playground/backend-integration/vite.config.js b/playground/backend-integration/vite.config.js
new file mode 100644
index 00000000000000..f432f139a4f6d9
--- /dev/null
+++ b/playground/backend-integration/vite.config.js
@@ -0,0 +1,58 @@
+import path from 'node:path'
+import { globSync } from 'tinyglobby'
+import { defineConfig, normalizePath } from 'vite'
+import tailwind from '@tailwindcss/vite'
+
+/**
+ * @returns {import('vite').Plugin}
+ */
+function BackendIntegrationExample() {
+ return {
+ name: 'backend-integration',
+ config() {
+ const projectRoot = __dirname
+ const sourceCodeDir = path.join(projectRoot, 'frontend')
+ const root = path.join(sourceCodeDir, 'entrypoints')
+ const outDir = path.relative(root, path.join(projectRoot, 'dist/dev'))
+
+ const entrypoints = globSync(`${normalizePath(root)}/**/*`, {
+ absolute: true,
+ expandDirectories: false,
+ onlyFiles: true,
+ }).map((filename) => [path.relative(root, filename), filename])
+
+ entrypoints.push(['tailwindcss-colors', 'tailwindcss/colors.js'])
+ entrypoints.push(['bar.css', path.resolve(__dirname, './dir/foo.css')])
+
+ return {
+ server: {
+ // same port in playground/test-utils.ts
+ port: 5009,
+ strictPort: true,
+ origin: 'http://localhost:5009',
+ },
+ preview: {
+ port: 5009,
+ },
+ build: {
+ manifest: true,
+ outDir,
+ rollupOptions: {
+ input: Object.fromEntries(entrypoints),
+ },
+ },
+ root,
+ resolve: {
+ alias: {
+ '~': sourceCodeDir,
+ },
+ },
+ }
+ },
+ }
+}
+
+export default defineConfig({
+ base: '/dev/',
+ plugins: [BackendIntegrationExample(), tailwind()],
+})
diff --git a/playground/build-old/__tests__/build-old.spec.ts b/playground/build-old/__tests__/build-old.spec.ts
new file mode 100644
index 00000000000000..517b204e7dde82
--- /dev/null
+++ b/playground/build-old/__tests__/build-old.spec.ts
@@ -0,0 +1,11 @@
+import { describe, expect, test } from 'vitest'
+import { page } from '~utils'
+
+describe('syntax preserve', () => {
+ test('import.meta.url', async () => {
+ expect(await page.textContent('.import-meta-url')).toBe('string')
+ })
+ test('dynamic import', async () => {
+ expect(await page.textContent('.dynamic-import')).toBe('success')
+ })
+})
diff --git a/playground/build-old/dynamic.js b/playground/build-old/dynamic.js
new file mode 100644
index 00000000000000..739bb26e01b765
--- /dev/null
+++ b/playground/build-old/dynamic.js
@@ -0,0 +1 @@
+export default 'success'
diff --git a/playground/build-old/index.html b/playground/build-old/index.html
new file mode 100644
index 00000000000000..118332ac0a42fc
--- /dev/null
+++ b/playground/build-old/index.html
@@ -0,0 +1,19 @@
+Build Old
+
+import meta url
+
+
+dynamic import
+
+
+
diff --git a/playground/build-old/package.json b/playground/build-old/package.json
new file mode 100644
index 00000000000000..695d5e6f28fbc7
--- /dev/null
+++ b/playground/build-old/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-build-old",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/build-old/vite.config.js b/playground/build-old/vite.config.js
new file mode 100644
index 00000000000000..6c5d26db8c0ee7
--- /dev/null
+++ b/playground/build-old/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ // old browsers only
+ target: ['chrome60'],
+ },
+})
diff --git a/playground/cli-module/__tests__/cli-module.spec.ts b/playground/cli-module/__tests__/cli-module.spec.ts
new file mode 100644
index 00000000000000..7eccaaa74b8578
--- /dev/null
+++ b/playground/cli-module/__tests__/cli-module.spec.ts
@@ -0,0 +1,26 @@
+import { expect, test } from 'vitest'
+import { port } from './serve'
+import { page } from '~utils'
+
+test('cli should work in "type":"module" package', async () => {
+ // this test uses a custom serve implementation, so regular helpers for browserLogs and goto don't work
+ // do the same thing manually
+ const logs = []
+ const onConsole = (msg) => {
+ logs.push(msg.text())
+ }
+ try {
+ page.on('console', onConsole)
+ await page.goto(`http://localhost:${port}/`)
+ expect(await page.textContent('.app')).toBe(
+ 'vite cli in "type":"module" package works!',
+ )
+ expect(
+ logs.some((msg) =>
+ msg.match('vite cli in "type":"module" package works!'),
+ ),
+ ).toBe(true)
+ } finally {
+ page.off('console', onConsole)
+ }
+})
diff --git a/playground/cli-module/__tests__/serve.ts b/playground/cli-module/__tests__/serve.ts
new file mode 100644
index 00000000000000..b19bdb62701a7d
--- /dev/null
+++ b/playground/cli-module/__tests__/serve.ts
@@ -0,0 +1,157 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+import { execaCommand } from 'execa'
+import kill from 'kill-port'
+import {
+ isBuild,
+ isWindows,
+ killProcess,
+ ports,
+ rootDir,
+ viteBinPath,
+} from '~utils'
+
+export const port = ports['cli-module']
+
+export async function serve() {
+ // collect stdout and stderr streams from child processes here to avoid interfering with regular vitest output
+ const streams = {
+ build: { out: [], err: [] },
+ server: { out: [], err: [] },
+ }
+ // helpers to collect streams
+ const collectStreams = (name, process) => {
+ process.stdout.on('data', (d) => streams[name].out.push(d.toString()))
+ process.stderr.on('data', (d) => streams[name].err.push(d.toString()))
+ }
+ const collectErrorStreams = (name, e) => {
+ e.stdout && streams[name].out.push(e.stdout)
+ e.stderr && streams[name].err.push(e.stderr)
+ }
+
+ // helper to output stream content on error
+ const printStreamsToConsole = async (name) => {
+ const std = streams[name]
+ if (std.out && std.out.length > 0) {
+ console.log(`stdout of ${name}\n${std.out.join('\n')}\n`)
+ }
+ if (std.err && std.err.length > 0) {
+ console.log(`stderr of ${name}\n${std.err.join('\n')}\n`)
+ }
+ }
+
+ // only run `vite build` when needed
+ if (isBuild) {
+ const buildCommand = `${viteBinPath} build`
+ try {
+ const buildProcess = execaCommand(buildCommand, {
+ cwd: rootDir,
+ stdio: 'pipe',
+ })
+ collectStreams('build', buildProcess)
+ await buildProcess
+ } catch (e) {
+ console.error(`error while executing cli command "${buildCommand}":`, e)
+ collectErrorStreams('build', e)
+ await printStreamsToConsole('build')
+ throw e
+ }
+ }
+
+ await kill(port)
+
+ // run `vite --port x` or `vite preview --port x` to start server
+ const viteServerArgs = ['--port', `${port}`, '--strict-port']
+ if (isBuild) {
+ viteServerArgs.unshift('preview')
+ }
+ const serverCommand = `${viteBinPath} ${viteServerArgs.join(' ')}`
+ const serverProcess = execaCommand(serverCommand, {
+ cwd: rootDir,
+ stdio: 'pipe',
+ forceKillAfterDelay: 3000,
+ })
+ collectStreams('server', serverProcess)
+
+ // close server helper, send SIGTERM followed by SIGKILL if needed, give up after 3sec
+ const close = async () => {
+ if (serverProcess) {
+ const timeoutError = `server process still alive after 3s`
+ try {
+ await killProcess(serverProcess)
+ await resolvedOrTimeout(serverProcess, 10000, timeoutError)
+ } catch (e) {
+ if (e === timeoutError || (!serverProcess.killed && !isWindows)) {
+ collectErrorStreams('server', e)
+ console.error(
+ `error while killing cli command "${serverCommand}":`,
+ e,
+ )
+ await printStreamsToConsole('server')
+ }
+ }
+ }
+ }
+
+ try {
+ await startedOnPort(serverProcess, port, 5173)
+ return { close }
+ } catch (e) {
+ collectErrorStreams('server', e)
+ console.error(`error while executing cli command "${serverCommand}":`, e)
+ await printStreamsToConsole('server')
+ try {
+ await close()
+ } catch (e1) {
+ console.error(
+ `error while killing cli command after failed execute "${serverCommand}":`,
+ e1,
+ )
+ }
+ }
+}
+
+// helper to validate that server was started on the correct port
+async function startedOnPort(serverProcess, port, timeout) {
+ let checkPort
+ const startedPromise = new Promise((resolve, reject) => {
+ checkPort = (data) => {
+ const str = data.toString()
+ // hack, console output may contain color code gibberish
+ // skip gibberish between localhost: and port number
+ const match = str.match(
+ /(http:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):).*(\d{4})/,
+ )
+ if (match) {
+ const startedPort = parseInt(match[2], 10)
+ if (startedPort === port) {
+ resolve()
+ } else {
+ const msg = `server listens on port ${startedPort} instead of ${port}`
+ reject(msg)
+ }
+ }
+ }
+ serverProcess.stdout.on('data', checkPort)
+ })
+ return resolvedOrTimeout(
+ startedPromise,
+ timeout,
+ `failed to start within ${timeout}ms`,
+ ).finally(() => serverProcess.stdout.off('data', checkPort))
+}
+
+// helper function that rejects with errorMessage if promise isn't settled within ms
+async function resolvedOrTimeout(promise, ms, errorMessage) {
+ let timer
+ return Promise.race([
+ promise,
+ new Promise((_, reject) => {
+ timer = setTimeout(() => reject(errorMessage), ms)
+ }),
+ ]).finally(() => {
+ clearTimeout(timer)
+ timer = null
+ })
+}
diff --git a/packages/playground/cli-module/index.html b/playground/cli-module/index.html
similarity index 100%
rename from packages/playground/cli-module/index.html
rename to playground/cli-module/index.html
diff --git a/packages/playground/cli-module/index.js b/playground/cli-module/index.js
similarity index 100%
rename from packages/playground/cli-module/index.js
rename to playground/cli-module/index.js
diff --git a/playground/cli-module/package.json b/playground/cli-module/package.json
new file mode 100644
index 00000000000000..e281a6ecfedb8b
--- /dev/null
+++ b/playground/cli-module/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-cli-module",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "serve": "vite preview"
+ },
+ "devDependencies": {
+ "url": "^0.11.4"
+ }
+}
diff --git a/playground/cli-module/vite.config.js b/playground/cli-module/vite.config.js
new file mode 100644
index 00000000000000..dedbf2ef658f86
--- /dev/null
+++ b/playground/cli-module/vite.config.js
@@ -0,0 +1,18 @@
+// eslint-disable-next-line import-x/no-nodejs-modules
+import { URL } from 'url'
+import { defineConfig } from 'vite'
+
+// make sure bundling works even if `url` refers to the locally installed
+// `url` package instead of the built-in `url` nodejs module
+globalThis.__test_url = URL
+
+export default defineConfig({
+ server: {
+ host: 'localhost',
+ },
+ build: {
+ //speed up build
+ minify: false,
+ target: 'esnext',
+ },
+})
diff --git a/playground/cli/__tests__/cli.spec.ts b/playground/cli/__tests__/cli.spec.ts
new file mode 100644
index 00000000000000..399a05115dfd77
--- /dev/null
+++ b/playground/cli/__tests__/cli.spec.ts
@@ -0,0 +1,39 @@
+import { expect, test } from 'vitest'
+import { port, streams } from './serve'
+import { editFile, isServe, page, withRetry } from '~utils'
+
+test('cli should work', async () => {
+ // this test uses a custom serve implementation, so regular helpers for browserLogs and goto don't work
+ // do the same thing manually
+ const logs = []
+ const onConsole = (msg) => {
+ logs.push(msg.text())
+ }
+ try {
+ page.on('console', onConsole)
+ await page.goto(`http://localhost:${port}/`)
+
+ expect(await page.textContent('.app')).toBe('vite cli works!')
+ expect(logs.some((msg) => msg.match('vite cli works!'))).toBe(true)
+ } finally {
+ page.off('console', onConsole)
+ }
+})
+
+test.runIf(isServe)('should restart', async () => {
+ const logsLengthBeforeEdit = streams.server.out.length
+ editFile('./vite.config.js', (content) => content)
+ await withRetry(async () => {
+ const logs = streams.server.out.slice(logsLengthBeforeEdit)
+ expect(logs).toEqual(
+ expect.arrayContaining([expect.stringMatching('server restarted')]),
+ )
+ // Don't reprint the server URLs as they are the same
+ expect(logs).not.toEqual(
+ expect.arrayContaining([expect.stringMatching('http://localhost')]),
+ )
+ expect(logs).not.toEqual(
+ expect.arrayContaining([expect.stringMatching('error')]),
+ )
+ })
+})
diff --git a/playground/cli/__tests__/serve.ts b/playground/cli/__tests__/serve.ts
new file mode 100644
index 00000000000000..5da61aec991109
--- /dev/null
+++ b/playground/cli/__tests__/serve.ts
@@ -0,0 +1,160 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+import { execaCommand } from 'execa'
+import kill from 'kill-port'
+import {
+ isBuild,
+ isWindows,
+ killProcess,
+ ports,
+ rootDir,
+ viteBinPath,
+} from '~utils'
+
+export const port = ports.cli
+export const streams = {} as {
+ build: { out: string[]; err: string[] }
+ server: { out: string[]; err: string[] }
+}
+export async function serve() {
+ // collect stdout and stderr streams from child processes here to avoid interfering with regular vitest output
+ Object.assign(streams, {
+ build: { out: [], err: [] },
+ server: { out: [], err: [] },
+ })
+ // helpers to collect streams
+ const collectStreams = (name, process) => {
+ process.stdout.on('data', (d) => streams[name].out.push(d.toString()))
+ process.stderr.on('data', (d) => streams[name].err.push(d.toString()))
+ }
+ const collectErrorStreams = (name, e) => {
+ e.stdout && streams[name].out.push(e.stdout)
+ e.stderr && streams[name].err.push(e.stderr)
+ }
+
+ // helper to output stream content on error
+ const printStreamsToConsole = async (name) => {
+ const std = streams[name]
+ if (std.out && std.out.length > 0) {
+ console.log(`stdout of ${name}\n${std.out.join('\n')}\n`)
+ }
+ if (std.err && std.err.length > 0) {
+ console.log(`stderr of ${name}\n${std.err.join('\n')}\n`)
+ }
+ }
+
+ // only run `vite build` when needed
+ if (isBuild) {
+ const buildCommand = `${viteBinPath} build`
+ try {
+ const buildProcess = execaCommand(buildCommand, {
+ cwd: rootDir,
+ stdio: 'pipe',
+ })
+ collectStreams('build', buildProcess)
+ await buildProcess
+ } catch (e) {
+ console.error(`error while executing cli command "${buildCommand}":`, e)
+ collectErrorStreams('build', e)
+ await printStreamsToConsole('build')
+ throw e
+ }
+ }
+
+ await kill(port)
+
+ // run `vite --port x` or `vite preview --port x` to start server
+ const viteServerArgs = ['--port', `${port}`, '--strict-port']
+ if (isBuild) {
+ viteServerArgs.unshift('preview')
+ }
+ const serverCommand = `${viteBinPath} ${viteServerArgs.join(' ')}`
+ const serverProcess = execaCommand(serverCommand, {
+ cwd: rootDir,
+ stdio: 'pipe',
+ forceKillAfterDelay: 3000,
+ })
+ collectStreams('server', serverProcess)
+
+ // close server helper, send SIGTERM followed by SIGKILL if needed, give up after 3sec
+ const close = async () => {
+ if (serverProcess) {
+ const timeoutError = `server process still alive after 3s`
+ try {
+ await killProcess(serverProcess)
+ await resolvedOrTimeout(serverProcess, 5173, timeoutError)
+ } catch (e) {
+ if (e === timeoutError || (!serverProcess.killed && !isWindows)) {
+ collectErrorStreams('server', e)
+ console.error(
+ `error while killing cli command "${serverCommand}":`,
+ e,
+ )
+ await printStreamsToConsole('server')
+ }
+ }
+ }
+ }
+
+ try {
+ await startedOnPort(serverProcess, port, 5173)
+ return { close }
+ } catch (e) {
+ collectErrorStreams('server', e)
+ console.error(`error while executing cli command "${serverCommand}":`, e)
+ await printStreamsToConsole('server')
+ try {
+ await close()
+ } catch (e1) {
+ console.error(
+ `error while killing cli command after failed execute "${serverCommand}":`,
+ e1,
+ )
+ }
+ }
+}
+
+// helper to validate that server was started on the correct port
+async function startedOnPort(serverProcess, port, timeout) {
+ let checkPort
+ const startedPromise = new Promise((resolve, reject) => {
+ checkPort = (data) => {
+ const str = data.toString()
+ // hack, console output may contain color code gibberish
+ // skip gibberish between localhost: and port number
+ const match = str.match(
+ /(http:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):).*(\d{4})/,
+ )
+ if (match) {
+ const startedPort = parseInt(match[2], 10)
+ if (startedPort === port) {
+ resolve()
+ } else {
+ const msg = `server listens on port ${startedPort} instead of ${port}`
+ reject(msg)
+ }
+ }
+ }
+ serverProcess.stdout.on('data', checkPort)
+ })
+ return resolvedOrTimeout(
+ startedPromise,
+ timeout,
+ `failed to start within ${timeout}ms`,
+ ).finally(() => serverProcess.stdout.off('data', checkPort))
+}
+
+// helper function that rejects with errorMessage if promise isn't settled within ms
+async function resolvedOrTimeout(promise, ms, errorMessage) {
+ let timer
+ return Promise.race([
+ promise,
+ new Promise((_, reject) => {
+ timer = setTimeout(() => reject(errorMessage), ms)
+ }),
+ ]).finally(() => {
+ clearTimeout(timer)
+ timer = null
+ })
+}
diff --git a/packages/playground/cli/index.html b/playground/cli/index.html
similarity index 100%
rename from packages/playground/cli/index.html
rename to playground/cli/index.html
diff --git a/packages/playground/cli/index.js b/playground/cli/index.js
similarity index 100%
rename from packages/playground/cli/index.js
rename to playground/cli/index.js
diff --git a/playground/cli/package.json b/playground/cli/package.json
new file mode 100644
index 00000000000000..09b97d52574733
--- /dev/null
+++ b/playground/cli/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-cli",
+ "private": true,
+ "version": "0.0.0",
+ "type": "commonjs",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/cli/vite.config.js b/playground/cli/vite.config.js
new file mode 100644
index 00000000000000..a5ffac7859b2f1
--- /dev/null
+++ b/playground/cli/vite.config.js
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ server: {
+ host: 'localhost',
+ headers: {
+ 'Cache-Control': 'no-store',
+ },
+ },
+ build: {
+ //speed up build
+ minify: false,
+ target: 'esnext',
+ },
+})
diff --git a/playground/client-reload/__tests__/client-reload.spec.ts b/playground/client-reload/__tests__/client-reload.spec.ts
new file mode 100644
index 00000000000000..0872a2e17d4294
--- /dev/null
+++ b/playground/client-reload/__tests__/client-reload.spec.ts
@@ -0,0 +1,75 @@
+import path from 'node:path'
+import { type ServerOptions, type ViteDevServer, createServer } from 'vite'
+import { afterEach, describe, expect, test } from 'vitest'
+import { hmrPorts, isServe, page, ports } from '~utils'
+
+let server: ViteDevServer
+
+afterEach(async () => {
+ await server?.close()
+})
+
+async function testClientReload(serverOptions: ServerOptions) {
+ // start server
+ server = await createServer({
+ root: path.resolve(import.meta.dirname, '..'),
+ logLevel: 'silent',
+ server: {
+ strictPort: true,
+ ...serverOptions,
+ },
+ })
+
+ await server.listen()
+ const serverUrl = server.resolvedUrls.local[0]
+
+ // open page and wait for connection
+ const connectedPromise = page.waitForEvent('console', {
+ predicate: (message) => message.text().includes('[vite] connected.'),
+ timeout: 5000,
+ })
+ await page.goto(serverUrl)
+ await connectedPromise
+
+ // input state
+ await page.locator('input').fill('hello')
+
+ // restart and wait for reconnection after reload
+ const reConnectedPromise = page.waitForEvent('console', {
+ predicate: (message) => message.text().includes('[vite] connected.'),
+ timeout: 5000,
+ })
+ await server.restart()
+ await reConnectedPromise
+ expect(await page.textContent('input')).toBe('')
+}
+
+describe.runIf(isServe)('client-reload', () => {
+ test('default', async () => {
+ await testClientReload({
+ port: ports['client-reload'],
+ })
+ })
+
+ test('custom hmr port', async () => {
+ await testClientReload({
+ port: ports['client-reload/hmr-port'],
+ hmr: {
+ port: hmrPorts['client-reload/hmr-port'],
+ },
+ })
+ })
+
+ test('custom hmr port and cross origin isolation', async () => {
+ await testClientReload({
+ port: ports['client-reload/cross-origin'],
+ hmr: {
+ port: hmrPorts['client-reload/cross-origin'],
+ },
+ headers: {
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
+ 'Cross-Origin-Opener-Policy': 'same-origin',
+ },
+ })
+ })
+})
diff --git a/playground/client-reload/__tests__/serve.ts b/playground/client-reload/__tests__/serve.ts
new file mode 100644
index 00000000000000..1d33d8064a44b4
--- /dev/null
+++ b/playground/client-reload/__tests__/serve.ts
@@ -0,0 +1,6 @@
+// do nothing here since server is managed inside spec
+export async function serve(): Promise<{ close(): Promise }> {
+ return {
+ close: () => Promise.resolve(),
+ }
+}
diff --git a/playground/client-reload/index.html b/playground/client-reload/index.html
new file mode 100644
index 00000000000000..7e78f23e2d5f54
--- /dev/null
+++ b/playground/client-reload/index.html
@@ -0,0 +1,4 @@
+
+ Test Client Reload
+
+
diff --git a/playground/client-reload/package.json b/playground/client-reload/package.json
new file mode 100644
index 00000000000000..a6fa570c64ffe7
--- /dev/null
+++ b/playground/client-reload/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-client-reload",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/client-reload/vite.config.ts b/playground/client-reload/vite.config.ts
new file mode 100644
index 00000000000000..4c9c4be6ba0c82
--- /dev/null
+++ b/playground/client-reload/vite.config.ts
@@ -0,0 +1,5 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ server: {},
+})
diff --git a/playground/csp/__tests__/csp.spec.ts b/playground/csp/__tests__/csp.spec.ts
new file mode 100644
index 00000000000000..93aa7cbbc1c278
--- /dev/null
+++ b/playground/csp/__tests__/csp.spec.ts
@@ -0,0 +1,47 @@
+import { expect, test } from 'vitest'
+import { expectWithRetry, getColor, page } from '~utils'
+
+test('linked css', async () => {
+ expect(await getColor('.linked')).toBe('blue')
+})
+
+test('inline style tag', async () => {
+ expect(await getColor('.inline')).toBe('green')
+})
+
+test('imported css', async () => {
+ expect(await getColor('.from-js')).toBe('blue')
+})
+
+test('dynamic css', async () => {
+ expect(await getColor('.dynamic')).toBe('red')
+})
+
+test('script tag', async () => {
+ await expectWithRetry(() => page.textContent('.js')).toBe('js: ok')
+})
+
+test('dynamic js', async () => {
+ await expectWithRetry(() => page.textContent('.dynamic-js')).toBe(
+ 'dynamic-js: ok',
+ )
+})
+
+test('inline js', async () => {
+ await expectWithRetry(() => page.textContent('.inline-js')).toBe(
+ 'inline-js: ok',
+ )
+})
+
+test('nonce attributes are not repeated', async () => {
+ const htmlSource = await page.content()
+ expect(htmlSource).not.toContain(/nonce=""[^>]*nonce=""/)
+ await expectWithRetry(() => page.textContent('.double-nonce-js')).toBe(
+ 'double-nonce-js: ok',
+ )
+})
+
+test('meta[property=csp-nonce] is injected', async () => {
+ const meta = await page.$('meta[property=csp-nonce]')
+ expect(await (await meta.getProperty('nonce')).jsonValue()).not.toBe('')
+})
diff --git a/playground/csp/dynamic.css b/playground/csp/dynamic.css
new file mode 100644
index 00000000000000..ca5140e1c23d94
--- /dev/null
+++ b/playground/csp/dynamic.css
@@ -0,0 +1,3 @@
+.dynamic {
+ color: red;
+}
diff --git a/playground/csp/dynamic.js b/playground/csp/dynamic.js
new file mode 100644
index 00000000000000..3d3e3a413e5677
--- /dev/null
+++ b/playground/csp/dynamic.js
@@ -0,0 +1,3 @@
+import './dynamic.css'
+
+document.querySelector('.dynamic-js').textContent = 'dynamic-js: ok'
diff --git a/playground/csp/from-js.css b/playground/csp/from-js.css
new file mode 100644
index 00000000000000..fb48429dc60ab4
--- /dev/null
+++ b/playground/csp/from-js.css
@@ -0,0 +1,3 @@
+.from-js {
+ color: blue;
+}
diff --git a/playground/csp/index.html b/playground/csp/index.html
new file mode 100644
index 00000000000000..45a508e76a2cd3
--- /dev/null
+++ b/playground/csp/index.html
@@ -0,0 +1,23 @@
+
+
+
+direct
+inline
+from-js
+dynamic
+js: error
+dynamic-js: error
+inline-js: error
+double-nonce-js: error
+
+
diff --git a/playground/csp/index.js b/playground/csp/index.js
new file mode 100644
index 00000000000000..465359baca8297
--- /dev/null
+++ b/playground/csp/index.js
@@ -0,0 +1,5 @@
+import './from-js.css'
+
+document.querySelector('.js').textContent = 'js: ok'
+
+import('./dynamic.js')
diff --git a/playground/csp/linked.css b/playground/csp/linked.css
new file mode 100644
index 00000000000000..51636e6cfad81f
--- /dev/null
+++ b/playground/csp/linked.css
@@ -0,0 +1,3 @@
+.linked {
+ color: blue;
+}
diff --git a/playground/csp/package.json b/playground/csp/package.json
new file mode 100644
index 00000000000000..e8a834d93abd25
--- /dev/null
+++ b/playground/csp/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-csp",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/csp/vite.config.js b/playground/csp/vite.config.js
new file mode 100644
index 00000000000000..84d6d92ba0d0bb
--- /dev/null
+++ b/playground/csp/vite.config.js
@@ -0,0 +1,67 @@
+import fs from 'node:fs/promises'
+import url from 'node:url'
+import path from 'node:path'
+import crypto from 'node:crypto'
+import { defineConfig } from 'vite'
+
+const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
+
+const noncePlaceholder = '#$NONCE$#'
+const createNonce = () => crypto.randomBytes(16).toString('base64')
+
+/**
+ * @param {import('node:http').ServerResponse} res
+ * @param {string} nonce
+ */
+const setNonceHeader = (res, nonce) => {
+ res.setHeader(
+ 'Content-Security-Policy',
+ `default-src 'nonce-${nonce}'; connect-src 'self'`,
+ )
+}
+
+/**
+ * @param {string} file
+ * @param {(input: string, originalUrl: string) => Promise} transform
+ * @returns {import('vite').Connect.NextHandleFunction}
+ */
+const createMiddleware = (file, transform) => async (req, res) => {
+ const nonce = createNonce()
+ setNonceHeader(res, nonce)
+ const content = await fs.readFile(path.join(__dirname, file), 'utf-8')
+ const transformedContent = await transform(content, req.originalUrl)
+ res.setHeader('Content-Type', 'text/html')
+ res.end(transformedContent.replaceAll(noncePlaceholder, nonce))
+}
+
+export default defineConfig({
+ plugins: [
+ {
+ name: 'nonce-inject',
+ config() {
+ return {
+ appType: 'custom',
+ html: {
+ cspNonce: noncePlaceholder,
+ },
+ }
+ },
+ configureServer({ transformIndexHtml, middlewares }) {
+ return () => {
+ middlewares.use(
+ createMiddleware('./index.html', (input, originalUrl) =>
+ transformIndexHtml(originalUrl, input),
+ ),
+ )
+ }
+ },
+ configurePreviewServer({ middlewares }) {
+ return () => {
+ middlewares.use(
+ createMiddleware('./dist/index.html', async (input) => input),
+ )
+ }
+ },
+ },
+ ],
+})
diff --git a/playground/css-codesplit-cjs/__tests__/css-codesplit-cjs.spec.ts b/playground/css-codesplit-cjs/__tests__/css-codesplit-cjs.spec.ts
new file mode 100644
index 00000000000000..69001c61bdb82b
--- /dev/null
+++ b/playground/css-codesplit-cjs/__tests__/css-codesplit-cjs.spec.ts
@@ -0,0 +1,21 @@
+import { describe, expect, test } from 'vitest'
+import { findAssetFile, getColor, isBuild, readManifest } from '~utils'
+
+test('should load both stylesheets', async () => {
+ expect(await getColor('h1')).toBe('red')
+ expect(await getColor('h2')).toBe('blue')
+})
+
+describe.runIf(isBuild)('build', () => {
+ test('should remove empty chunk', async () => {
+ expect(findAssetFile(/style.*\.js$/)).toBe('')
+ expect(findAssetFile('main.*.js$')).toMatch(`/* empty css`)
+ expect(findAssetFile('other.*.js$')).toMatch(`/* empty css`)
+ })
+
+ test('should generate correct manifest', async () => {
+ const manifest = readManifest()
+ expect(manifest['index.html'].css.length).toBe(2)
+ expect(manifest['other.js'].css.length).toBe(1)
+ })
+})
diff --git a/packages/playground/css-codesplit-cjs/index.html b/playground/css-codesplit-cjs/index.html
similarity index 100%
rename from packages/playground/css-codesplit-cjs/index.html
rename to playground/css-codesplit-cjs/index.html
diff --git a/packages/playground/css-codesplit-cjs/main.css b/playground/css-codesplit-cjs/main.css
similarity index 100%
rename from packages/playground/css-codesplit-cjs/main.css
rename to playground/css-codesplit-cjs/main.css
diff --git a/playground/css-codesplit-cjs/main.js b/playground/css-codesplit-cjs/main.js
new file mode 100644
index 00000000000000..766759f9bd79f4
--- /dev/null
+++ b/playground/css-codesplit-cjs/main.js
@@ -0,0 +1,5 @@
+import './style.css'
+import './main.css'
+
+document.getElementById('app').innerHTML =
+ `This should be red This should be blue `
diff --git a/packages/playground/css-codesplit-cjs/other.js b/playground/css-codesplit-cjs/other.js
similarity index 100%
rename from packages/playground/css-codesplit-cjs/other.js
rename to playground/css-codesplit-cjs/other.js
diff --git a/playground/css-codesplit-cjs/package.json b/playground/css-codesplit-cjs/package.json
new file mode 100644
index 00000000000000..e305007adfcb98
--- /dev/null
+++ b/playground/css-codesplit-cjs/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-css-codesplit-cjs",
+ "private": true,
+ "version": "0.0.0",
+ "type": "commonjs",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/packages/playground/css-codesplit-cjs/style.css b/playground/css-codesplit-cjs/style.css
similarity index 100%
rename from packages/playground/css-codesplit-cjs/style.css
rename to playground/css-codesplit-cjs/style.css
diff --git a/playground/css-codesplit-cjs/vite.config.js b/playground/css-codesplit-cjs/vite.config.js
new file mode 100644
index 00000000000000..d1353babe8336e
--- /dev/null
+++ b/playground/css-codesplit-cjs/vite.config.js
@@ -0,0 +1,21 @@
+import { resolve } from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ outDir: './dist',
+ manifest: true,
+ rollupOptions: {
+ input: {
+ main: resolve(__dirname, './index.html'),
+ other: resolve(__dirname, './other.js'),
+ },
+ treeshake: false,
+ output: {
+ format: 'cjs',
+ freeze: false,
+ externalLiveBindings: false,
+ },
+ },
+ },
+})
diff --git a/playground/css-codesplit/__tests__/css-codesplit-consistent.spec.ts b/playground/css-codesplit/__tests__/css-codesplit-consistent.spec.ts
new file mode 100644
index 00000000000000..4da121a652d0db
--- /dev/null
+++ b/playground/css-codesplit/__tests__/css-codesplit-consistent.spec.ts
@@ -0,0 +1,15 @@
+import { beforeEach, describe, expect, test } from 'vitest'
+import { findAssetFile, isBuild, startDefaultServe } from '~utils'
+
+beforeEach(async () => {
+ await startDefaultServe()
+})
+
+for (let i = 0; i < 5; i++) {
+ describe.runIf(isBuild)('css-codesplit build', () => {
+ test('should be consistent with same content', () => {
+ expect(findAssetFile(/style-.+\.css/)).toMatch('h2{color:#00f}')
+ expect(findAssetFile(/style2-.+\.css/)).toBe('')
+ })
+ })
+}
diff --git a/playground/css-codesplit/__tests__/css-codesplit.spec.ts b/playground/css-codesplit/__tests__/css-codesplit.spec.ts
new file mode 100644
index 00000000000000..cc54d865a6795e
--- /dev/null
+++ b/playground/css-codesplit/__tests__/css-codesplit.spec.ts
@@ -0,0 +1,70 @@
+import { describe, expect, test } from 'vitest'
+import {
+ findAssetFile,
+ getColor,
+ isBuild,
+ listAssets,
+ page,
+ readManifest,
+ untilUpdated,
+} from '~utils'
+
+test('should load all stylesheets', async () => {
+ expect(await getColor('h1')).toBe('red')
+ expect(await getColor('h2')).toBe('blue')
+ expect(await getColor('.dynamic')).toBe('green')
+ expect(await getColor('.async-js')).toBe('blue')
+ expect(await getColor('.chunk')).toBe('magenta')
+})
+
+test('should load dynamic import with inline', async () => {
+ const css = await page.textContent('.dynamic-inline')
+ expect(css).toMatch('.inline')
+
+ expect(await getColor('.inline')).not.toBe('yellow')
+})
+
+test('should load dynamic import with module', async () => {
+ const css = await page.textContent('.dynamic-module')
+ expect(css).toMatch('_mod_')
+
+ expect(await getColor('.mod')).toBe('yellow')
+})
+
+test('style order should be consistent when style tag is inserted by JS', async () => {
+ expect(await getColor('.order-bulk')).toBe('orange')
+ await page.click('.order-bulk-update')
+ await untilUpdated(() => getColor('.order-bulk'), 'green')
+})
+
+describe.runIf(isBuild)('build', () => {
+ test('should remove empty chunk', async () => {
+ expect(findAssetFile(/style-.*\.js$/)).toBe('')
+ expect(findAssetFile('main.*.js$')).toMatch(`/* empty css`)
+ expect(findAssetFile('other.*.js$')).toMatch(`/* empty css`)
+ expect(findAssetFile(/async-[-\w]{8}\.js$/)).toBe('')
+
+ const assets = listAssets()
+ expect(assets).not.toContainEqual(
+ expect.stringMatching(/async-js-[-\w]{8}\.js$/),
+ )
+ })
+
+ test('should remove empty chunk, HTML without JS', async () => {
+ const sharedCSSWithJSChunk = findAssetFile('shared-css-with-js.*.js$')
+ expect(sharedCSSWithJSChunk).toMatch(`/* empty css`)
+ // there are functions and modules in the src code that should be tree-shaken
+ expect(sharedCSSWithJSChunk).not.toMatch('function')
+ expect(sharedCSSWithJSChunk).not.toMatch(/import(?!".\/modulepreload)/)
+ })
+
+ test('should generate correct manifest', async () => {
+ const manifest = readManifest()
+ expect(manifest['index.html'].css.length).toBe(2)
+ expect(manifest['other.js'].css.length).toBe(1)
+ })
+
+ test('should not mark a css chunk with ?url and normal import as pure css chunk', () => {
+ expect(findAssetFile(/chunk-.*\.js$/)).toBeTruthy()
+ })
+})
diff --git a/playground/css-codesplit/async-js.css b/playground/css-codesplit/async-js.css
new file mode 100644
index 00000000000000..ed61a7f513c277
--- /dev/null
+++ b/playground/css-codesplit/async-js.css
@@ -0,0 +1,3 @@
+.async-js {
+ color: blue;
+}
diff --git a/playground/css-codesplit/async-js.js b/playground/css-codesplit/async-js.js
new file mode 100644
index 00000000000000..2ce31a1e741d2d
--- /dev/null
+++ b/playground/css-codesplit/async-js.js
@@ -0,0 +1,2 @@
+// a JS file that becomes an empty file but imports CSS files
+import './async-js.css'
diff --git a/playground/css-codesplit/async.css b/playground/css-codesplit/async.css
new file mode 100644
index 00000000000000..4902b2e7bee811
--- /dev/null
+++ b/playground/css-codesplit/async.css
@@ -0,0 +1,3 @@
+.dynamic {
+ color: green;
+}
diff --git a/playground/css-codesplit/chunk.css b/playground/css-codesplit/chunk.css
new file mode 100644
index 00000000000000..a8aa47c2d96134
--- /dev/null
+++ b/playground/css-codesplit/chunk.css
@@ -0,0 +1,3 @@
+.chunk {
+ color: magenta;
+}
diff --git a/playground/css-codesplit/index.html b/playground/css-codesplit/index.html
new file mode 100644
index 00000000000000..38885fa7ccb5ed
--- /dev/null
+++ b/playground/css-codesplit/index.html
@@ -0,0 +1,19 @@
+This should be red
+This should be blue
+
+This should be green
+This should be blue
+This should not be yellow
+
+This should be yellow
+
+
+
+ This should be orange
+ change to green
+
+
+This should be magenta
+
+
+
diff --git a/playground/css-codesplit/inline.css b/playground/css-codesplit/inline.css
new file mode 100644
index 00000000000000..b2a2b5f1ead51f
--- /dev/null
+++ b/playground/css-codesplit/inline.css
@@ -0,0 +1,3 @@
+.inline {
+ color: yellow;
+}
diff --git a/packages/playground/css-codesplit/main.css b/playground/css-codesplit/main.css
similarity index 100%
rename from packages/playground/css-codesplit/main.css
rename to playground/css-codesplit/main.css
diff --git a/playground/css-codesplit/main.js b/playground/css-codesplit/main.js
new file mode 100644
index 00000000000000..ec266fa003156d
--- /dev/null
+++ b/playground/css-codesplit/main.js
@@ -0,0 +1,23 @@
+import './style.css'
+import './main.css'
+import './order'
+
+import './chunk.css'
+import chunkCssUrl from './chunk.css?url'
+
+// use this to not treeshake
+globalThis.__test_chunkCssUrl = chunkCssUrl
+
+import('./async.css')
+import('./async-js')
+
+import('./inline.css?inline').then((css) => {
+ document.querySelector('.dynamic-inline').textContent = css.default
+})
+
+import('./mod.module.css').then((css) => {
+ document.querySelector('.dynamic-module').textContent = JSON.stringify(
+ css.default,
+ )
+ document.querySelector('.mod').classList.add(css.default.mod)
+})
diff --git a/playground/css-codesplit/mod.module.css b/playground/css-codesplit/mod.module.css
new file mode 100644
index 00000000000000..7f84410485a32c
--- /dev/null
+++ b/playground/css-codesplit/mod.module.css
@@ -0,0 +1,3 @@
+.mod {
+ color: yellow;
+}
diff --git a/playground/css-codesplit/order/base.css b/playground/css-codesplit/order/base.css
new file mode 100644
index 00000000000000..a08c84388f2079
--- /dev/null
+++ b/playground/css-codesplit/order/base.css
@@ -0,0 +1,3 @@
+.order-bulk {
+ color: blue;
+}
diff --git a/playground/css-codesplit/order/dynamic.css b/playground/css-codesplit/order/dynamic.css
new file mode 100644
index 00000000000000..f460d283759a88
--- /dev/null
+++ b/playground/css-codesplit/order/dynamic.css
@@ -0,0 +1,3 @@
+.order-bulk {
+ color: green;
+}
diff --git a/playground/css-codesplit/order/index.js b/playground/css-codesplit/order/index.js
new file mode 100644
index 00000000000000..dab4e8e5962b11
--- /dev/null
+++ b/playground/css-codesplit/order/index.js
@@ -0,0 +1,6 @@
+import './insert' // inserts "color: orange"
+import './base.css' // includes "color: blue"
+
+document.querySelector('.order-bulk-update').addEventListener('click', () => {
+ import('./dynamic.css') // includes "color: green"
+})
diff --git a/playground/css-codesplit/order/insert.js b/playground/css-codesplit/order/insert.js
new file mode 100644
index 00000000000000..2ccda105650412
--- /dev/null
+++ b/playground/css-codesplit/order/insert.js
@@ -0,0 +1,3 @@
+const style = document.createElement('style')
+style.textContent = '.order-bulk { color: orange; }'
+document.head.appendChild(style)
diff --git a/playground/css-codesplit/other.js b/playground/css-codesplit/other.js
new file mode 100644
index 00000000000000..4560c4d53c29ac
--- /dev/null
+++ b/playground/css-codesplit/other.js
@@ -0,0 +1,6 @@
+import './style.css'
+import './chunk.css'
+import chunkCssUrl from './chunk.css?url'
+
+// use this to not treeshake
+globalThis.__test_chunkCssUrl = chunkCssUrl
diff --git a/playground/css-codesplit/package.json b/playground/css-codesplit/package.json
new file mode 100644
index 00000000000000..f7edd868783ebf
--- /dev/null
+++ b/playground/css-codesplit/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-css-codesplit",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/css-codesplit/shared-css-empty-1.js b/playground/css-codesplit/shared-css-empty-1.js
new file mode 100644
index 00000000000000..80636d362c52d5
--- /dev/null
+++ b/playground/css-codesplit/shared-css-empty-1.js
@@ -0,0 +1,4 @@
+function shouldBeTreeshaken_1() {
+ // This function should be treeshaken, even if { moduleSideEffects: 'no-treeshake' }
+ // was used in the JS corresponding to the HTML entrypoint.
+}
diff --git a/playground/css-codesplit/shared-css-empty-2.js b/playground/css-codesplit/shared-css-empty-2.js
new file mode 100644
index 00000000000000..7ce6d30628268d
--- /dev/null
+++ b/playground/css-codesplit/shared-css-empty-2.js
@@ -0,0 +1,4 @@
+export default function shouldBeTreeshaken_2() {
+ // This function should be treeshaken, even if { moduleSideEffects: 'no-treeshake' }
+ // was used in the JS corresponding to the HTML entrypoint.
+}
diff --git a/playground/css-codesplit/shared-css-main.js b/playground/css-codesplit/shared-css-main.js
new file mode 100644
index 00000000000000..639861b66321f9
--- /dev/null
+++ b/playground/css-codesplit/shared-css-main.js
@@ -0,0 +1,10 @@
+import shouldTreeshake from './shared-css-empty-2.js'
+document.querySelector('#app').innerHTML = `
+
+
Shared CSS, with JS
+
+`
+function shouldBeTreeshaken_0() {
+ // This function should be treeshaken, even if { moduleSideEffects: 'no-treeshake' }
+ // was used in the JS corresponding to the HTML entrypoint.
+}
diff --git a/playground/css-codesplit/shared-css-no-js.html b/playground/css-codesplit/shared-css-no-js.html
new file mode 100644
index 00000000000000..27c666af881f15
--- /dev/null
+++ b/playground/css-codesplit/shared-css-no-js.html
@@ -0,0 +1,4 @@
+
+
+ Share CSS, no JS
+
diff --git a/packages/playground/html/nested/nested.css b/playground/css-codesplit/shared-css-theme.css
similarity index 100%
rename from packages/playground/html/nested/nested.css
rename to playground/css-codesplit/shared-css-theme.css
diff --git a/playground/css-codesplit/shared-css-with-js.html b/playground/css-codesplit/shared-css-with-js.html
new file mode 100644
index 00000000000000..aaa856f2c6c5ef
--- /dev/null
+++ b/playground/css-codesplit/shared-css-with-js.html
@@ -0,0 +1,6 @@
+
+
+
+
+ Replaced by shared-css-main.js
+
diff --git a/packages/playground/css-codesplit/style.css b/playground/css-codesplit/style.css
similarity index 100%
rename from packages/playground/css-codesplit/style.css
rename to playground/css-codesplit/style.css
diff --git a/playground/css-codesplit/style2.css b/playground/css-codesplit/style2.css
new file mode 100644
index 00000000000000..2b4bb3671e654b
--- /dev/null
+++ b/playground/css-codesplit/style2.css
@@ -0,0 +1,3 @@
+h2 {
+ color: blue;
+}
diff --git a/playground/css-codesplit/style2.js b/playground/css-codesplit/style2.js
new file mode 100644
index 00000000000000..ab7ebb9eb632a6
--- /dev/null
+++ b/playground/css-codesplit/style2.js
@@ -0,0 +1 @@
+import './style2.css'
diff --git a/playground/css-codesplit/vite.config.js b/playground/css-codesplit/vite.config.js
new file mode 100644
index 00000000000000..5042b6d9b9cab7
--- /dev/null
+++ b/playground/css-codesplit/vite.config.js
@@ -0,0 +1,25 @@
+import { resolve } from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ manifest: true,
+ rollupOptions: {
+ input: {
+ main: resolve(__dirname, './index.html'),
+ other: resolve(__dirname, './other.js'),
+ style2: resolve(__dirname, './style2.js'),
+ 'shared-css-with-js': resolve(__dirname, 'shared-css-with-js.html'),
+ 'shared-css-no-js': resolve(__dirname, 'shared-css-no-js.html'),
+ },
+ output: {
+ manualChunks(id) {
+ // make `chunk.css` it's own chunk for easier testing of pure css chunks
+ if (id.includes('chunk.css')) {
+ return 'chunk'
+ }
+ },
+ },
+ },
+ },
+})
diff --git a/playground/css-dynamic-import/__tests__/css-dynamic-import.spec.ts b/playground/css-dynamic-import/__tests__/css-dynamic-import.spec.ts
new file mode 100644
index 00000000000000..4b473d985f136a
--- /dev/null
+++ b/playground/css-dynamic-import/__tests__/css-dynamic-import.spec.ts
@@ -0,0 +1,93 @@
+import type { InlineConfig } from 'vite'
+import { build, createServer, preview } from 'vite'
+import { expect, test } from 'vitest'
+import { getColor, isBuild, isServe, page, ports, rootDir } from '~utils'
+
+const baseOptions = [
+ { base: '', label: 'relative' },
+ { base: '/', label: 'absolute' },
+]
+
+const getConfig = (base: string): InlineConfig => ({
+ base,
+ root: rootDir,
+ logLevel: 'silent',
+ server: { port: ports['css/dynamic-import'] },
+ preview: { port: ports['css/dynamic-import'] },
+ build: { assetsInlineLimit: 0 },
+})
+
+async function withBuild(base: string, fn: () => Promise) {
+ const config = getConfig(base)
+ await build(config)
+ const server = await preview(config)
+
+ try {
+ await page.goto(server.resolvedUrls.local[0])
+ await fn()
+ } finally {
+ server.httpServer.close()
+ }
+}
+
+async function withServe(base: string, fn: () => Promise) {
+ const config = getConfig(base)
+ const server = await createServer(config)
+ await server.listen()
+
+ try {
+ await page.goto(server.resolvedUrls.local[0])
+ await fn()
+ } finally {
+ await page.goto('about:blank') // move to a different page to avoid auto-refresh after server start
+ await server.close()
+ }
+}
+
+async function getLinks() {
+ const links = await page.$$('link')
+ return await Promise.all(
+ links.map((handle) => {
+ return handle.evaluate((link) => ({
+ pathname: new URL(link.href).pathname,
+ rel: link.rel,
+ as: link.as,
+ }))
+ }),
+ )
+}
+
+baseOptions.forEach(({ base, label }) => {
+ test.runIf(isBuild)(
+ `doesn't duplicate dynamically imported css files when built with ${label} base`,
+ async () => {
+ await withBuild(base, async () => {
+ await page.waitForSelector('.loaded', { state: 'attached' })
+
+ expect(await getColor('.css-dynamic-import')).toBe('green')
+ const linkUrls = (await getLinks()).map((link) => link.pathname)
+ const uniqueLinkUrls = [...new Set(linkUrls)]
+ expect(linkUrls).toStrictEqual(uniqueLinkUrls)
+ })
+ },
+ )
+
+ test.runIf(isServe)(
+ `doesn't duplicate dynamically imported css files when served with ${label} base`,
+ async () => {
+ await withServe(base, async () => {
+ await page.waitForSelector('.loaded', { state: 'attached' })
+
+ expect(await getColor('.css-dynamic-import')).toBe('green')
+ // in serve there is no preloading
+ expect(await getLinks()).toEqual([
+ {
+ pathname: '/dynamic.css',
+ rel: 'preload',
+ as: 'style',
+ },
+ ])
+ })
+ },
+ )
+})
diff --git a/playground/css-dynamic-import/__tests__/serve.ts b/playground/css-dynamic-import/__tests__/serve.ts
new file mode 100644
index 00000000000000..48f55eee4f48ec
--- /dev/null
+++ b/playground/css-dynamic-import/__tests__/serve.ts
@@ -0,0 +1,10 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+// The server is started in the test, so we need to have a custom serve
+// function or a default server will be created
+export async function serve() {
+ return {
+ close: () => Promise.resolve(),
+ }
+}
diff --git a/playground/css-dynamic-import/dynamic.css b/playground/css-dynamic-import/dynamic.css
new file mode 100644
index 00000000000000..6212a63c31fa19
--- /dev/null
+++ b/playground/css-dynamic-import/dynamic.css
@@ -0,0 +1,3 @@
+.css-dynamic-import {
+ color: green;
+}
diff --git a/playground/css-dynamic-import/dynamic.js b/playground/css-dynamic-import/dynamic.js
new file mode 100644
index 00000000000000..0d0aeb3aec229c
--- /dev/null
+++ b/playground/css-dynamic-import/dynamic.js
@@ -0,0 +1,6 @@
+import './dynamic.css'
+
+export const lazyLoad = async () => {
+ await import('./static.js')
+ document.body.classList.add('loaded')
+}
diff --git a/playground/css-dynamic-import/index.html b/playground/css-dynamic-import/index.html
new file mode 100644
index 00000000000000..d9f9fedbbda752
--- /dev/null
+++ b/playground/css-dynamic-import/index.html
@@ -0,0 +1,3 @@
+This should be green
+
+
diff --git a/playground/css-dynamic-import/index.js b/playground/css-dynamic-import/index.js
new file mode 100644
index 00000000000000..5a0c724da737db
--- /dev/null
+++ b/playground/css-dynamic-import/index.js
@@ -0,0 +1,10 @@
+import './static.js'
+
+const link = document.head.appendChild(document.createElement('link'))
+link.rel = 'preload'
+link.as = 'style'
+link.href = new URL('./dynamic.css', import.meta.url).href
+
+import('./dynamic.js').then(async ({ lazyLoad }) => {
+ await lazyLoad()
+})
diff --git a/playground/css-dynamic-import/package.json b/playground/css-dynamic-import/package.json
new file mode 100644
index 00000000000000..2b5339f8c72760
--- /dev/null
+++ b/playground/css-dynamic-import/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-css-dynamic-import",
+ "private": true,
+ "type": "module",
+ "version": "0.0.0"
+}
diff --git a/playground/css-dynamic-import/static.css b/playground/css-dynamic-import/static.css
new file mode 100644
index 00000000000000..4efb84fdfea550
--- /dev/null
+++ b/playground/css-dynamic-import/static.css
@@ -0,0 +1,3 @@
+.css-dynamic-import {
+ color: red;
+}
diff --git a/playground/css-dynamic-import/static.js b/playground/css-dynamic-import/static.js
new file mode 100644
index 00000000000000..1688198fba4227
--- /dev/null
+++ b/playground/css-dynamic-import/static.js
@@ -0,0 +1,3 @@
+import './static.css'
+
+export const foo = 'foo'
diff --git a/playground/css-lightningcss-proxy/__tests__/css-lightningcss-proxy.spec.ts b/playground/css-lightningcss-proxy/__tests__/css-lightningcss-proxy.spec.ts
new file mode 100644
index 00000000000000..3d5cbc2ebaf142
--- /dev/null
+++ b/playground/css-lightningcss-proxy/__tests__/css-lightningcss-proxy.spec.ts
@@ -0,0 +1,13 @@
+import { describe, expect, test } from 'vitest'
+import { port } from './serve'
+import { getColor, isServe, page } from '~utils'
+
+const url = `http://localhost:${port}`
+
+describe.runIf(isServe)('injected inline style', () => {
+ test('injected inline style is present', async () => {
+ await page.goto(url)
+ const el = await page.$('.ssr-proxy')
+ expect(await getColor(el)).toBe('coral')
+ })
+})
diff --git a/playground/css-lightningcss-proxy/__tests__/serve.ts b/playground/css-lightningcss-proxy/__tests__/serve.ts
new file mode 100644
index 00000000000000..ee933ecd507a8c
--- /dev/null
+++ b/playground/css-lightningcss-proxy/__tests__/serve.ts
@@ -0,0 +1,38 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+import path from 'node:path'
+import kill from 'kill-port'
+import { hmrPorts, ports, rootDir } from '~utils'
+
+export const port = ports['css/lightningcss-proxy']
+
+export async function serve(): Promise<{ close(): Promise }> {
+ await kill(port)
+
+ const { createServer } = await import(path.resolve(rootDir, 'server.js'))
+ const { app, vite } = await createServer(
+ rootDir,
+ hmrPorts['css/lightningcss-proxy'],
+ )
+
+ return new Promise((resolve, reject) => {
+ try {
+ const server = app.listen(port, () => {
+ resolve({
+ // for test teardown
+ async close() {
+ await new Promise((resolve) => {
+ server.close(resolve)
+ })
+ if (vite) {
+ await vite.close()
+ }
+ },
+ })
+ })
+ } catch (e) {
+ reject(e)
+ }
+ })
+}
diff --git a/playground/css-lightningcss-proxy/index.html b/playground/css-lightningcss-proxy/index.html
new file mode 100644
index 00000000000000..a017cc0d01b93c
--- /dev/null
+++ b/playground/css-lightningcss-proxy/index.html
@@ -0,0 +1,5 @@
+
+
+
Injected inline style with SSR Proxy
+
This should be coral
+
diff --git a/playground/css-lightningcss-proxy/package.json b/playground/css-lightningcss-proxy/package.json
new file mode 100644
index 00000000000000..aac00aececb311
--- /dev/null
+++ b/playground/css-lightningcss-proxy/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-css-lightningcss-proxy",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "node server",
+ "serve": "NODE_ENV=production node server",
+ "debug": "node --inspect-brk server",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "lightningcss": "^1.30.0",
+ "express": "^5.1.0"
+ }
+}
diff --git a/playground/css-lightningcss-proxy/server.js b/playground/css-lightningcss-proxy/server.js
new file mode 100644
index 00000000000000..63bc4aa2b1de5c
--- /dev/null
+++ b/playground/css-lightningcss-proxy/server.js
@@ -0,0 +1,86 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import express from 'express'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const isTest = process.env.VITEST
+
+const DYNAMIC_STYLES = `
+
+`
+
+export async function createServer(root = process.cwd(), hmrPort) {
+ const resolve = (p) => path.resolve(__dirname, p)
+
+ const app = express()
+
+ /**
+ * @type {import('vite').ViteDevServer}
+ */
+ const vite = await (
+ await import('vite')
+ ).createServer({
+ root,
+ logLevel: isTest ? 'error' : 'info',
+ css: {
+ transformer: 'lightningcss',
+ },
+ server: {
+ middlewareMode: true,
+ watch: {
+ // During tests we edit the files too fast and sometimes chokidar
+ // misses change events, so enforce polling for consistency
+ usePolling: true,
+ interval: 100,
+ },
+ hmr: {
+ port: hmrPort,
+ },
+ },
+ appType: 'custom',
+ })
+ // use vite's connect instance as middleware
+ app.use(vite.middlewares)
+
+ app.use('*all', async (req, res, next) => {
+ try {
+ let [url] = req.originalUrl.split('?')
+ if (url.endsWith('/')) url += 'index.html'
+
+ if (url.startsWith('/favicon.ico')) {
+ return res.status(404).end('404')
+ }
+
+ const htmlLoc = resolve(`.${url}`)
+ let template = fs.readFileSync(htmlLoc, 'utf-8')
+
+ template = template.replace('', DYNAMIC_STYLES)
+
+ // Force calling transformIndexHtml with url === '/', to simulate
+ // usage by ecosystem that was recommended in the SSR documentation
+ // as `const url = req.originalUrl`
+ const html = await vite.transformIndexHtml('/', template)
+
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
+ } catch (e) {
+ vite && vite.ssrFixStacktrace(e)
+ console.log(e.stack)
+ res.status(500).end(e.stack)
+ }
+ })
+
+ return { app, vite }
+}
+
+if (!isTest) {
+ createServer().then(({ app }) =>
+ app.listen(5173, () => {
+ console.log('http://localhost:5173')
+ }),
+ )
+}
diff --git a/playground/css-lightningcss-root/__tests__/css-lightningcss-root.spec.ts b/playground/css-lightningcss-root/__tests__/css-lightningcss-root.spec.ts
new file mode 100644
index 00000000000000..786d17f124ab87
--- /dev/null
+++ b/playground/css-lightningcss-root/__tests__/css-lightningcss-root.spec.ts
@@ -0,0 +1,9 @@
+import { expect, test } from 'vitest'
+import { getBg, isBuild, page, viteTestUrl } from '~utils'
+
+test('url dependency', async () => {
+ const css = await page.$('.url-dep')
+ expect(await getBg(css)).toMatch(
+ isBuild ? /ok-[-\w]+\.png/ : `${viteTestUrl}/ok.png`,
+ )
+})
diff --git a/playground/css-lightningcss-root/package.json b/playground/css-lightningcss-root/package.json
new file mode 100644
index 00000000000000..9cb46999d3b47e
--- /dev/null
+++ b/playground/css-lightningcss-root/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@vitejs/test-css-lightningcss-root",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "lightningcss": "^1.30.0"
+ }
+}
diff --git a/playground/css-lightningcss-root/root/index.html b/playground/css-lightningcss-root/root/index.html
new file mode 100644
index 00000000000000..df2820bbe74c59
--- /dev/null
+++ b/playground/css-lightningcss-root/root/index.html
@@ -0,0 +1,3 @@
+url() dependency
+
+
diff --git a/playground/css-lightningcss-root/root/main.js b/playground/css-lightningcss-root/root/main.js
new file mode 100644
index 00000000000000..fe93503f500dd8
--- /dev/null
+++ b/playground/css-lightningcss-root/root/main.js
@@ -0,0 +1 @@
+import './url-dep.css'
diff --git a/playground/css-lightningcss-root/root/ok.png b/playground/css-lightningcss-root/root/ok.png
new file mode 100644
index 00000000000000..a8d1e52510c41c
Binary files /dev/null and b/playground/css-lightningcss-root/root/ok.png differ
diff --git a/playground/css-lightningcss-root/root/url-dep.css b/playground/css-lightningcss-root/root/url-dep.css
new file mode 100644
index 00000000000000..f56470afc0bb51
--- /dev/null
+++ b/playground/css-lightningcss-root/root/url-dep.css
@@ -0,0 +1,7 @@
+.url-dep {
+ background-image: url('./ok.png');
+ background-size: cover;
+ width: 50px;
+ height: 50px;
+ border: 1px solid black;
+}
diff --git a/playground/css-lightningcss-root/vite.config.js b/playground/css-lightningcss-root/vite.config.js
new file mode 100644
index 00000000000000..6e42d3e20e28be
--- /dev/null
+++ b/playground/css-lightningcss-root/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ root: 'root',
+ css: {
+ transformer: 'lightningcss',
+ },
+})
diff --git a/playground/css-lightningcss/__tests__/css-lightningcss.spec.ts b/playground/css-lightningcss/__tests__/css-lightningcss.spec.ts
new file mode 100644
index 00000000000000..9dec9ebd992f68
--- /dev/null
+++ b/playground/css-lightningcss/__tests__/css-lightningcss.spec.ts
@@ -0,0 +1,95 @@
+import { expect, test } from 'vitest'
+import {
+ editFile,
+ findAssetFile,
+ getBg,
+ getColor,
+ isBuild,
+ page,
+ untilUpdated,
+ viteTestUrl,
+} from '~utils'
+
+// note: tests should retrieve the element at the beginning of test and reuse it
+// in later assertions to ensure CSS HMR doesn't reload the page
+test('linked css', async () => {
+ const linked = await page.$('.linked')
+ const atImport = await page.$('.linked-at-import')
+
+ expect(await getColor(linked)).toBe('blue')
+ expect(await getColor(atImport)).toBe('red')
+
+ if (isBuild) return
+ editFile('linked.css', (code) => code.replace('color: blue', 'color: red'))
+ await untilUpdated(() => getColor(linked), 'red')
+
+ editFile('linked-at-import.css', (code) =>
+ code.replace('color: red', 'color: blue'),
+ )
+ await untilUpdated(() => getColor(atImport), 'blue')
+})
+
+test('css import from js', async () => {
+ const imported = await page.$('.imported')
+ const atImport = await page.$('.imported-at-import')
+
+ expect(await getColor(imported)).toBe('green')
+ expect(await getColor(atImport)).toBe('purple')
+
+ if (isBuild) return
+ editFile('imported.css', (code) => code.replace('color: green', 'color: red'))
+ await untilUpdated(() => getColor(imported), 'red')
+
+ editFile('imported-at-import.css', (code) =>
+ code.replace('color: purple', 'color: blue'),
+ )
+ await untilUpdated(() => getColor(atImport), 'blue')
+})
+
+test('css modules', async () => {
+ const imported = await page.$('.modules')
+ expect(await getColor(imported)).toBe('turquoise')
+
+ expect(await imported.getAttribute('class')).toMatch(/\w{6}_apply-color/)
+
+ if (isBuild) return
+ editFile('mod.module.css', (code) =>
+ code.replace('color: turquoise', 'color: red'),
+ )
+ await untilUpdated(() => getColor(imported), 'red')
+})
+
+test('inline css modules', async () => {
+ const css = await page.textContent('.modules-inline')
+ expect(css).toMatch(/\._?\w{6}_apply-color-inline/)
+})
+
+test.runIf(isBuild)('minify css', async () => {
+ // should keep the rgba() syntax
+ const cssFile = findAssetFile(/index-[-\w]+\.css$/)
+ expect(cssFile).toMatch('rgba(')
+ expect(cssFile).not.toMatch('#ffff00b3')
+})
+
+test('css with external url', async () => {
+ const css = await page.$('.external')
+ expect(await getBg(css)).toMatch('url("https://vite.dev/logo.svg")')
+})
+
+test('nested css with relative asset', async () => {
+ const css = await page.$('.nested-css-relative-asset')
+ expect(await getBg(css)).toMatch(
+ isBuild ? /ok-[-\w]+\.png/ : `${viteTestUrl}/ok.png`,
+ )
+})
+
+test('aliased asset', async () => {
+ const bg = await getBg('.css-url-aliased')
+ expect(bg).toMatch('data:image/svg+xml,')
+})
+
+test('preinlined SVG', async () => {
+ expect(await getBg('.css-url-preinlined-svg')).toMatch(
+ /data:image\/svg\+xml,.+/,
+ )
+})
diff --git a/packages/playground/css/composed.module.css b/playground/css-lightningcss/composed.module.css
similarity index 100%
rename from packages/playground/css/composed.module.css
rename to playground/css-lightningcss/composed.module.css
diff --git a/playground/css-lightningcss/composes-path-resolving.module.css b/playground/css-lightningcss/composes-path-resolving.module.css
new file mode 100644
index 00000000000000..2873293f9c7605
--- /dev/null
+++ b/playground/css-lightningcss/composes-path-resolving.module.css
@@ -0,0 +1,3 @@
+.path-resolving-css {
+ composes: apply-color from './composed.module.css';
+}
diff --git a/playground/css-lightningcss/css-url.css b/playground/css-lightningcss/css-url.css
new file mode 100644
index 00000000000000..6695ad2b9b0bd3
--- /dev/null
+++ b/playground/css-lightningcss/css-url.css
@@ -0,0 +1,9 @@
+.css-url-aliased {
+ background: url('@/fragment.svg');
+ background-size: 10px;
+}
+
+.css-url-preinlined-svg {
+ background: url('data:image/svg+xml, ');
+ background-size: 20px;
+}
diff --git a/playground/css-lightningcss/external-url.css b/playground/css-lightningcss/external-url.css
new file mode 100644
index 00000000000000..d1c2ef28a6fe80
--- /dev/null
+++ b/playground/css-lightningcss/external-url.css
@@ -0,0 +1,7 @@
+.external {
+ background-image: url('https://vite.dev/logo.svg');
+ background-size: 100%;
+ width: 200px;
+ height: 200px;
+ background-color: #bed;
+}
diff --git a/packages/playground/css/imported-at-import.css b/playground/css-lightningcss/imported-at-import.css
similarity index 100%
rename from packages/playground/css/imported-at-import.css
rename to playground/css-lightningcss/imported-at-import.css
diff --git a/playground/css-lightningcss/imported.css b/playground/css-lightningcss/imported.css
new file mode 100644
index 00000000000000..929e8995d196af
--- /dev/null
+++ b/playground/css-lightningcss/imported.css
@@ -0,0 +1,13 @@
+@import url('./nested/nested.css');
+@import './imported-at-import.css';
+
+.imported {
+ color: green;
+}
+
+pre {
+ background-color: #eee;
+ width: 500px;
+ padding: 1em 1.5em;
+ border-radius: 10px;
+}
diff --git a/playground/css-lightningcss/index.html b/playground/css-lightningcss/index.html
new file mode 100644
index 00000000000000..c0756b11314831
--- /dev/null
+++ b/playground/css-lightningcss/index.html
@@ -0,0 +1,41 @@
+
+
+
+
Lightning CSS
+
+
<link>: This should be blue
+
@import in <link>: This should be red
+
+
import from js: This should be green
+
+ @import in import from js: This should be purple
+
+
+
CSS modules: this should be turquoise
+
Imported CSS module:
+
+
+
Imported compose/from CSS module:
+
+ CSS modules composes path resolving: this should be turquoise
+
+
+
+
Inline CSS module:
+
+
+
External URL
+
+
+
Assets relative to nested CSS
+
+
+
+ CSS background (aliased)
+
+
+ CSS background (pre inlined SVG)
+
+
+
+
diff --git a/playground/css-lightningcss/inline.module.css b/playground/css-lightningcss/inline.module.css
new file mode 100644
index 00000000000000..9566e21e2cd1af
--- /dev/null
+++ b/playground/css-lightningcss/inline.module.css
@@ -0,0 +1,3 @@
+.apply-color-inline {
+ color: turquoise;
+}
diff --git a/packages/playground/css/inlined.css b/playground/css-lightningcss/inlined.css
similarity index 100%
rename from packages/playground/css/inlined.css
rename to playground/css-lightningcss/inlined.css
diff --git a/packages/playground/css/linked-at-import.css b/playground/css-lightningcss/linked-at-import.css
similarity index 100%
rename from packages/playground/css/linked-at-import.css
rename to playground/css-lightningcss/linked-at-import.css
diff --git a/playground/css-lightningcss/linked.css b/playground/css-lightningcss/linked.css
new file mode 100644
index 00000000000000..49f677d1e6462a
--- /dev/null
+++ b/playground/css-lightningcss/linked.css
@@ -0,0 +1,8 @@
+@import './linked-at-import.css';
+
+/* test nesting */
+.wrapper {
+ .linked {
+ color: blue;
+ }
+}
diff --git a/playground/css-lightningcss/main.js b/playground/css-lightningcss/main.js
new file mode 100644
index 00000000000000..bc452b3cd42c2e
--- /dev/null
+++ b/playground/css-lightningcss/main.js
@@ -0,0 +1,33 @@
+import './minify.css'
+import './imported.css'
+import mod from './mod.module.css'
+import './external-url.css'
+import './css-url.css'
+
+document.querySelector('.modules').classList.add(mod['apply-color'])
+text('.modules-code', JSON.stringify(mod, null, 2))
+
+import composesPathResolvingMod from './composes-path-resolving.module.css'
+document
+ .querySelector('.path-resolved-modules-css')
+ .classList.add(...composesPathResolvingMod['path-resolving-css'].split(' '))
+text(
+ '.path-resolved-modules-code',
+ JSON.stringify(composesPathResolvingMod, null, 2),
+)
+
+import inlineMod from './inline.module.css?inline'
+text('.modules-inline', inlineMod)
+
+function text(el, text) {
+ document.querySelector(el).textContent = text
+}
+
+if (import.meta.hot) {
+ import.meta.hot.accept('./mod.module.css', (newMod) => {
+ const list = document.querySelector('.modules').classList
+ list.remove(mod.applyColor)
+ list.add(newMod.applyColor)
+ text('.modules-code', JSON.stringify(newMod.default, null, 2))
+ })
+}
diff --git a/packages/playground/css/minify.css b/playground/css-lightningcss/minify.css
similarity index 100%
rename from packages/playground/css/minify.css
rename to playground/css-lightningcss/minify.css
diff --git a/packages/playground/css/mod.module.css b/playground/css-lightningcss/mod.module.css
similarity index 100%
rename from packages/playground/css/mod.module.css
rename to playground/css-lightningcss/mod.module.css
diff --git a/packages/playground/vue/assets/fragment.svg b/playground/css-lightningcss/nested/fragment.svg
similarity index 100%
rename from packages/playground/vue/assets/fragment.svg
rename to playground/css-lightningcss/nested/fragment.svg
diff --git a/playground/css-lightningcss/nested/nested.css b/playground/css-lightningcss/nested/nested.css
new file mode 100644
index 00000000000000..7d123cdd106de1
--- /dev/null
+++ b/playground/css-lightningcss/nested/nested.css
@@ -0,0 +1,5 @@
+.nested-css-relative-asset {
+ background-image: url('../ok.png');
+ width: 50px;
+ height: 50px;
+}
diff --git a/playground/css-lightningcss/ok.png b/playground/css-lightningcss/ok.png
new file mode 100644
index 00000000000000..a8d1e52510c41c
Binary files /dev/null and b/playground/css-lightningcss/ok.png differ
diff --git a/playground/css-lightningcss/package.json b/playground/css-lightningcss/package.json
new file mode 100644
index 00000000000000..c1b8adbae2f633
--- /dev/null
+++ b/playground/css-lightningcss/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@vitejs/test-css-lightningcss",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "lightningcss": "^1.30.0"
+ }
+}
diff --git a/playground/css-lightningcss/vite.config.js b/playground/css-lightningcss/vite.config.js
new file mode 100644
index 00000000000000..fbefa0d4d4b99f
--- /dev/null
+++ b/playground/css-lightningcss/vite.config.js
@@ -0,0 +1,17 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ css: {
+ transformer: 'lightningcss',
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'nested'),
+ },
+ },
+ build: {
+ cssTarget: ['chrome61'],
+ cssMinify: 'lightningcss',
+ },
+})
diff --git a/playground/css-no-codesplit/__tests__/css-no-codesplit.spec.ts b/playground/css-no-codesplit/__tests__/css-no-codesplit.spec.ts
new file mode 100644
index 00000000000000..5110ef3a77ff7b
--- /dev/null
+++ b/playground/css-no-codesplit/__tests__/css-no-codesplit.spec.ts
@@ -0,0 +1,17 @@
+import { describe, expect, test } from 'vitest'
+import { expectWithRetry, getColor, isBuild, listAssets } from '~utils'
+
+test('should load all stylesheets', async () => {
+ expect(await getColor('.shared-linked')).toBe('blue')
+ await expectWithRetry(() => getColor('.async-js')).toBe('blue')
+})
+
+describe.runIf(isBuild)('build', () => {
+ test('should remove empty chunk', async () => {
+ const assets = listAssets()
+ expect(assets).not.toContainEqual(
+ expect.stringMatching(/shared-linked-.*\.js$/),
+ )
+ expect(assets).not.toContainEqual(expect.stringMatching(/async-js-.*\.js$/))
+ })
+})
diff --git a/playground/css-no-codesplit/async-js.css b/playground/css-no-codesplit/async-js.css
new file mode 100644
index 00000000000000..ed61a7f513c277
--- /dev/null
+++ b/playground/css-no-codesplit/async-js.css
@@ -0,0 +1,3 @@
+.async-js {
+ color: blue;
+}
diff --git a/playground/css-no-codesplit/async-js.js b/playground/css-no-codesplit/async-js.js
new file mode 100644
index 00000000000000..2ce31a1e741d2d
--- /dev/null
+++ b/playground/css-no-codesplit/async-js.js
@@ -0,0 +1,2 @@
+// a JS file that becomes an empty file but imports CSS files
+import './async-js.css'
diff --git a/playground/css-no-codesplit/index.html b/playground/css-no-codesplit/index.html
new file mode 100644
index 00000000000000..e7673c84e45933
--- /dev/null
+++ b/playground/css-no-codesplit/index.html
@@ -0,0 +1,5 @@
+
+
+
+shared linked: this should be blue
+async JS importing CSS: this should be blue
diff --git a/playground/css-no-codesplit/index.js b/playground/css-no-codesplit/index.js
new file mode 100644
index 00000000000000..44b33fda36a9cd
--- /dev/null
+++ b/playground/css-no-codesplit/index.js
@@ -0,0 +1 @@
+import('./async-js')
diff --git a/playground/css-no-codesplit/package.json b/playground/css-no-codesplit/package.json
new file mode 100644
index 00000000000000..61d806d3d264fa
--- /dev/null
+++ b/playground/css-no-codesplit/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-css-no-codesplit",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/css-no-codesplit/shared-linked.css b/playground/css-no-codesplit/shared-linked.css
new file mode 100644
index 00000000000000..51857a50efca1f
--- /dev/null
+++ b/playground/css-no-codesplit/shared-linked.css
@@ -0,0 +1,3 @@
+.shared-linked {
+ color: blue;
+}
diff --git a/playground/css-no-codesplit/sub.html b/playground/css-no-codesplit/sub.html
new file mode 100644
index 00000000000000..f535a771d06482
--- /dev/null
+++ b/playground/css-no-codesplit/sub.html
@@ -0,0 +1 @@
+
diff --git a/playground/css-no-codesplit/vite.config.js b/playground/css-no-codesplit/vite.config.js
new file mode 100644
index 00000000000000..f48d875832b928
--- /dev/null
+++ b/playground/css-no-codesplit/vite.config.js
@@ -0,0 +1,14 @@
+import { resolve } from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ cssCodeSplit: false,
+ rollupOptions: {
+ input: {
+ index: resolve(__dirname, './index.html'),
+ sub: resolve(__dirname, './sub.html'),
+ },
+ },
+ },
+})
diff --git a/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts b/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts
new file mode 100644
index 00000000000000..f943143187ee51
--- /dev/null
+++ b/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts
@@ -0,0 +1,251 @@
+import { URL } from 'node:url'
+import { describe, expect, test } from 'vitest'
+import {
+ extractSourcemap,
+ formatSourcemapForSnapshot,
+ isBuild,
+ isServe,
+ page,
+ serverLogs,
+} from '~utils'
+
+test.runIf(isBuild)('should not output sourcemap warning (#4939)', () => {
+ serverLogs.forEach((log) => {
+ expect(log).not.toMatch('Sourcemap is likely to be incorrect')
+ })
+})
+
+describe.runIf(isServe)('serve', () => {
+ const getStyleTagContentIncluding = async (content: string) => {
+ const styles = await page.$$('style')
+ for (const style of styles) {
+ const text = await style.textContent()
+ if (text.includes(content)) {
+ return text
+ }
+ }
+ throw new Error('Not found')
+ }
+
+ test('linked css', async () => {
+ const res = await page.request.get(
+ new URL('./linked.css', page.url()).href,
+ {
+ headers: {
+ accept: 'text/css',
+ },
+ },
+ )
+ const css = await res.text()
+ expect(css).not.toContain('sourceMappingURL')
+ })
+
+ test('linked css with import', async () => {
+ const res = await page.request.get(
+ new URL('./linked-with-import.css', page.url()).href,
+ {
+ headers: {
+ accept: 'text/css',
+ },
+ },
+ )
+ const css = await res.text()
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "AAAA;EACE,UAAU;AACZ;;ACAA;EACE,UAAU;AACZ",
+ "sources": [
+ "be-imported.css",
+ "linked-with-import.css",
+ ],
+ "sourcesContent": [
+ ".be-imported {
+ color: red;
+ }
+ ",
+ "@import '@/be-imported.css';
+
+ .linked-with-import {
+ color: red;
+ }
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test.runIf(isServe)(
+ 'js .css request does not include sourcemap',
+ async () => {
+ const res = await page.request.get(
+ new URL('./linked-with-import.css', page.url()).href,
+ )
+ const content = await res.text()
+ expect(content).not.toMatch('//#s*sourceMappingURL')
+ },
+ )
+
+ test('imported css', async () => {
+ const css = await getStyleTagContentIncluding('.imported ')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "AAAA,CAAC,QAAQ,CAAC;AACV,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG;AACZ;",
+ "sources": [
+ "/root/imported.css",
+ ],
+ "sourcesContent": [
+ ".imported {
+ color: red;
+ }
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('imported css with import', async () => {
+ const css = await getStyleTagContentIncluding('.imported-with-import ')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "AAAA;EACE,UAAU;AACZ;;ACAA;EACE,UAAU;AACZ",
+ "sources": [
+ "/root/be-imported.css",
+ "/root/imported-with-import.css",
+ ],
+ "sourcesContent": [
+ ".be-imported {
+ color: red;
+ }
+ ",
+ "@import '@/be-imported.css';
+
+ .imported-with-import {
+ color: red;
+ }
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('imported sass', async () => {
+ const css = await getStyleTagContentIncluding('.imported-sass ')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "AAGE;EACE,OCJM",
+ "sourceRoot": "",
+ "sources": [
+ "/root/imported.sass",
+ "/root/imported-nested.sass",
+ ],
+ "sourcesContent": [
+ "@use "/imported-nested.sass"
+
+ .imported
+ &-sass
+ color: imported-nested.$primary
+ ",
+ "$primary: red
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('imported sass module', async () => {
+ const css = await getStyleTagContentIncluding('._imported-sass-module_')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "ignoreList": [],
+ "mappings": "AACE;EACE",
+ "sources": [
+ "/root/imported.module.sass",
+ ],
+ "sourcesContent": [
+ ".imported
+ &-sass-module
+ color: red
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('imported less', async () => {
+ const css = await getStyleTagContentIncluding('.imported-less ')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "ignoreList": [],
+ "mappings": "AACE,SAAC;EACC",
+ "sources": [
+ "/root/imported.less",
+ ],
+ "sourcesContent": [
+ ".imported {
+ &-less {
+ color: @color;
+ }
+ }
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('imported stylus', async () => {
+ const css = await getStyleTagContentIncluding('.imported-stylus ')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "ignoreList": [],
+ "mappings": "AACE;EACE,OAAM,QAAN",
+ "sources": [
+ "/root/imported.styl",
+ ],
+ "sourcesContent": [
+ ".imported
+ &-stylus
+ color blue-red-mixed
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('imported sugarss', async () => {
+ const css = await getStyleTagContentIncluding('.imported-sugarss ')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "AAAA;EACE;AADe",
+ "sources": [
+ "/root/imported.sss",
+ ],
+ "sourcesContent": [
+ ".imported-sugarss
+ color: red
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('should not output missing source file warning', () => {
+ serverLogs.forEach((log) => {
+ expect(log).not.toMatch(/Sourcemap for .+ points to missing source files/)
+ })
+ })
+})
diff --git a/playground/css-sourcemap/__tests__/lib-entry/css-sourcemap-lib-entry.spec.ts b/playground/css-sourcemap/__tests__/lib-entry/css-sourcemap-lib-entry.spec.ts
new file mode 100644
index 00000000000000..d2fa5f7d2c31e7
--- /dev/null
+++ b/playground/css-sourcemap/__tests__/lib-entry/css-sourcemap-lib-entry.spec.ts
@@ -0,0 +1,8 @@
+import { describe, expect, test } from 'vitest'
+import { findAssetFile, isBuild } from '~utils'
+
+describe.runIf(isBuild)('css lib entry', () => {
+ test('remove useless js sourcemap', async () => {
+ expect(findAssetFile('linked.js.map', 'lib-entry', './')).toBe('')
+ })
+})
diff --git a/playground/css-sourcemap/__tests__/lightningcss/lightningcss.spec.ts b/playground/css-sourcemap/__tests__/lightningcss/lightningcss.spec.ts
new file mode 100644
index 00000000000000..98f128cdd4ff29
--- /dev/null
+++ b/playground/css-sourcemap/__tests__/lightningcss/lightningcss.spec.ts
@@ -0,0 +1,252 @@
+import { URL } from 'node:url'
+import { describe, expect, test } from 'vitest'
+import {
+ extractSourcemap,
+ formatSourcemapForSnapshot,
+ isBuild,
+ isServe,
+ page,
+ serverLogs,
+} from '~utils'
+
+test.runIf(isBuild)('should not output sourcemap warning (#4939)', () => {
+ serverLogs.forEach((log) => {
+ expect(log).not.toMatch('Sourcemap is likely to be incorrect')
+ })
+})
+
+describe.runIf(isServe)('serve', () => {
+ const getStyleTagContentIncluding = async (content: string) => {
+ const styles = await page.$$('style')
+ for (const style of styles) {
+ const text = await style.textContent()
+ if (text.includes(content)) {
+ return text
+ }
+ }
+ throw new Error('Not found')
+ }
+
+ test('linked css', async () => {
+ const res = await page.request.get(
+ new URL('./linked.css', page.url()).href,
+ {
+ headers: {
+ accept: 'text/css',
+ },
+ },
+ )
+ const css = await res.text()
+ expect(css).not.toContain('sourceMappingURL')
+ })
+
+ test('linked css with import', async () => {
+ const res = await page.request.get(
+ new URL('./linked-with-import.css', page.url()).href,
+ {
+ headers: {
+ accept: 'text/css',
+ },
+ },
+ )
+ const css = await res.text()
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "ACAA;;;;ADEA",
+ "sourceRoot": null,
+ "sources": [
+ "linked-with-import.css",
+ "be-imported.css",
+ ],
+ "sourcesContent": [
+ "@import '@/be-imported.css';
+
+ .linked-with-import {
+ color: red;
+ }
+ ",
+ ".be-imported {
+ color: red;
+ }
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test.runIf(isServe)(
+ 'js .css request does not include sourcemap',
+ async () => {
+ const res = await page.request.get(
+ new URL('./linked-with-import.css', page.url()).href,
+ )
+ const content = await res.text()
+ expect(content).not.toMatch('//#s*sourceMappingURL')
+ },
+ )
+
+ test('imported css', async () => {
+ const css = await getStyleTagContentIncluding('.imported ')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "AAAA",
+ "sourceRoot": null,
+ "sources": [
+ "imported.css",
+ ],
+ "sourcesContent": [
+ ".imported {
+ color: red;
+ }
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('imported css with import', async () => {
+ const css = await getStyleTagContentIncluding('.imported-with-import ')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "ACAA;;;;ADEA",
+ "sourceRoot": null,
+ "sources": [
+ "imported-with-import.css",
+ "be-imported.css",
+ ],
+ "sourcesContent": [
+ "@import '@/be-imported.css';
+
+ .imported-with-import {
+ color: red;
+ }
+ ",
+ ".be-imported {
+ color: red;
+ }
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('imported sass', async () => {
+ const css = await getStyleTagContentIncluding('.imported-sass ')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "ignoreList": [],
+ "mappings": "AAGE",
+ "sources": [
+ "/root/imported.sass",
+ ],
+ "sourcesContent": [
+ "@use "/imported-nested.sass"
+
+ .imported
+ &-sass
+ color: imported-nested.$primary
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('imported sass module', async () => {
+ const css = await getStyleTagContentIncluding('_imported-sass-module')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "ignoreList": [],
+ "mappings": "AACE",
+ "sources": [
+ "/root/imported.module.sass",
+ ],
+ "sourcesContent": [
+ ".imported
+ &-sass-module
+ color: red
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('imported less', async () => {
+ const css = await getStyleTagContentIncluding('.imported-less ')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "ignoreList": [],
+ "mappings": "AACE",
+ "sources": [
+ "/root/imported.less",
+ ],
+ "sourcesContent": [
+ ".imported {
+ &-less {
+ color: @color;
+ }
+ }
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('imported stylus', async () => {
+ const css = await getStyleTagContentIncluding('.imported-stylus ')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "ignoreList": [],
+ "mappings": "AACE",
+ "sources": [
+ "/root/imported.styl",
+ ],
+ "sourcesContent": [
+ ".imported
+ &-stylus
+ color blue-red-mixed
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('imported sugarss', async () => {
+ const css = await getStyleTagContentIncluding('.imported-sugarss ')
+ const map = extractSourcemap(css)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "ignoreList": [],
+ "mappings": "AAAA",
+ "sources": [
+ "/root/imported.sss",
+ ],
+ "sourcesContent": [
+ ".imported-sugarss
+ color: red
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('should not output missing source file warning', () => {
+ serverLogs.forEach((log) => {
+ expect(log).not.toMatch(/Sourcemap for .+ points to missing source files/)
+ })
+ })
+})
diff --git a/playground/css-sourcemap/__tests__/sass-modern/sass-modern.spec.ts b/playground/css-sourcemap/__tests__/sass-modern/sass-modern.spec.ts
new file mode 100644
index 00000000000000..69a693758c71cc
--- /dev/null
+++ b/playground/css-sourcemap/__tests__/sass-modern/sass-modern.spec.ts
@@ -0,0 +1,2 @@
+// NOTE: a separate directory from `playground/css-sourcemap` is created by playground/vitestGlobalSetup.ts
+import '../css-sourcemap.spec'
diff --git a/playground/css-sourcemap/be-imported.css b/playground/css-sourcemap/be-imported.css
new file mode 100644
index 00000000000000..a29e5f77e3cb5d
--- /dev/null
+++ b/playground/css-sourcemap/be-imported.css
@@ -0,0 +1,3 @@
+.be-imported {
+ color: red;
+}
diff --git a/playground/css-sourcemap/imported-nested.sass b/playground/css-sourcemap/imported-nested.sass
new file mode 100644
index 00000000000000..b5f10d672c1cc5
--- /dev/null
+++ b/playground/css-sourcemap/imported-nested.sass
@@ -0,0 +1 @@
+$primary: red
diff --git a/playground/css-sourcemap/imported-with-import.css b/playground/css-sourcemap/imported-with-import.css
new file mode 100644
index 00000000000000..6a1ed3c3772698
--- /dev/null
+++ b/playground/css-sourcemap/imported-with-import.css
@@ -0,0 +1,5 @@
+@import '@/be-imported.css';
+
+.imported-with-import {
+ color: red;
+}
diff --git a/playground/css-sourcemap/imported.css b/playground/css-sourcemap/imported.css
new file mode 100644
index 00000000000000..9c9b32924962dc
--- /dev/null
+++ b/playground/css-sourcemap/imported.css
@@ -0,0 +1,3 @@
+.imported {
+ color: red;
+}
diff --git a/playground/css-sourcemap/imported.less b/playground/css-sourcemap/imported.less
new file mode 100644
index 00000000000000..e71b15eb102441
--- /dev/null
+++ b/playground/css-sourcemap/imported.less
@@ -0,0 +1,5 @@
+.imported {
+ &-less {
+ color: @color;
+ }
+}
diff --git a/playground/css-sourcemap/imported.module.sass b/playground/css-sourcemap/imported.module.sass
new file mode 100644
index 00000000000000..448a5e7e31f75a
--- /dev/null
+++ b/playground/css-sourcemap/imported.module.sass
@@ -0,0 +1,3 @@
+.imported
+ &-sass-module
+ color: red
diff --git a/playground/css-sourcemap/imported.sass b/playground/css-sourcemap/imported.sass
new file mode 100644
index 00000000000000..e80f906f0e5f51
--- /dev/null
+++ b/playground/css-sourcemap/imported.sass
@@ -0,0 +1,5 @@
+@use "/imported-nested.sass"
+
+.imported
+ &-sass
+ color: imported-nested.$primary
diff --git a/playground/css-sourcemap/imported.sss b/playground/css-sourcemap/imported.sss
new file mode 100644
index 00000000000000..56084992472c47
--- /dev/null
+++ b/playground/css-sourcemap/imported.sss
@@ -0,0 +1,2 @@
+.imported-sugarss
+ color: red
diff --git a/playground/css-sourcemap/imported.styl b/playground/css-sourcemap/imported.styl
new file mode 100644
index 00000000000000..83c7cf517acf4d
--- /dev/null
+++ b/playground/css-sourcemap/imported.styl
@@ -0,0 +1,3 @@
+.imported
+ &-stylus
+ color blue-red-mixed
diff --git a/playground/css-sourcemap/index.html b/playground/css-sourcemap/index.html
new file mode 100644
index 00000000000000..8260ae75ed65ca
--- /dev/null
+++ b/playground/css-sourcemap/index.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
CSS Sourcemap
+
+
<inline>
+
+
<linked>: no import
+
<linked>: with import
+
+
<imported>: no import
+
<imported>: with import
+
+
<imported sass>
+
<imported sass> with module
+
+
<imported less> with string additionalData
+
+
<imported stylus>
+
+
+
+
<input source-map>
+
+
+
+
+
diff --git a/playground/css-sourcemap/index.js b/playground/css-sourcemap/index.js
new file mode 100644
index 00000000000000..c05f09558ddaf0
--- /dev/null
+++ b/playground/css-sourcemap/index.js
@@ -0,0 +1 @@
+export default 'hello'
diff --git a/playground/css-sourcemap/input-map.css b/playground/css-sourcemap/input-map.css
new file mode 100644
index 00000000000000..575a1751c2cbca
--- /dev/null
+++ b/playground/css-sourcemap/input-map.css
@@ -0,0 +1,4 @@
+.input-map {
+ color: #00f;
+}
+/*# sourceMappingURL=input-map.css.map */
diff --git a/playground/css-sourcemap/input-map.css.map b/playground/css-sourcemap/input-map.css.map
new file mode 100644
index 00000000000000..05502b8ce18685
--- /dev/null
+++ b/playground/css-sourcemap/input-map.css.map
@@ -0,0 +1,7 @@
+{
+ "version": 3,
+ "sources": ["input-map.src.css"],
+ "sourcesContent": [".input-map {\n color: blue;\n}"],
+ "mappings": "AAAA,WACE",
+ "names": []
+}
diff --git a/playground/css-sourcemap/input-map.src.css b/playground/css-sourcemap/input-map.src.css
new file mode 100644
index 00000000000000..90b9565e271633
--- /dev/null
+++ b/playground/css-sourcemap/input-map.src.css
@@ -0,0 +1,3 @@
+.input-map {
+ color: blue;
+}
diff --git a/playground/css-sourcemap/linked-with-import.css b/playground/css-sourcemap/linked-with-import.css
new file mode 100644
index 00000000000000..6f65d92441fa49
--- /dev/null
+++ b/playground/css-sourcemap/linked-with-import.css
@@ -0,0 +1,5 @@
+@import '@/be-imported.css';
+
+.linked-with-import {
+ color: red;
+}
diff --git a/playground/css-sourcemap/linked.css b/playground/css-sourcemap/linked.css
new file mode 100644
index 00000000000000..e3b67c83872ac0
--- /dev/null
+++ b/playground/css-sourcemap/linked.css
@@ -0,0 +1,3 @@
+.linked {
+ color: red;
+}
diff --git a/playground/css-sourcemap/package.json b/playground/css-sourcemap/package.json
new file mode 100644
index 00000000000000..89c092abe7ff99
--- /dev/null
+++ b/playground/css-sourcemap/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@vitejs/test-css-sourcemap",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "less": "^4.3.0",
+ "lightningcss": "^1.30.0",
+ "magic-string": "^0.30.17",
+ "sass": "^1.88.0",
+ "stylus": "^0.64.0",
+ "sugarss": "^5.0.0"
+ }
+}
diff --git a/playground/css-sourcemap/vite.config-lib-entry.js b/playground/css-sourcemap/vite.config-lib-entry.js
new file mode 100644
index 00000000000000..600b7414a48b75
--- /dev/null
+++ b/playground/css-sourcemap/vite.config-lib-entry.js
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ cssCodeSplit: true,
+ sourcemap: true,
+ outDir: 'dist/lib-entry',
+ lib: {
+ entry: ['./index.js', './linked.css'],
+ formats: ['es'],
+ },
+ },
+})
diff --git a/playground/css-sourcemap/vite.config-lightningcss.js b/playground/css-sourcemap/vite.config-lightningcss.js
new file mode 100644
index 00000000000000..cf987bc98f032e
--- /dev/null
+++ b/playground/css-sourcemap/vite.config-lightningcss.js
@@ -0,0 +1,11 @@
+import { defineConfig, mergeConfig } from 'vite'
+import baseConfig from './vite.config.js'
+
+export default mergeConfig(
+ baseConfig,
+ defineConfig({
+ css: {
+ transformer: 'lightningcss',
+ },
+ }),
+)
diff --git a/playground/css-sourcemap/vite.config-sass-modern.js b/playground/css-sourcemap/vite.config-sass-modern.js
new file mode 100644
index 00000000000000..739507a13caf27
--- /dev/null
+++ b/playground/css-sourcemap/vite.config-sass-modern.js
@@ -0,0 +1,15 @@
+import { defineConfig, mergeConfig } from 'vite'
+import baseConfig from './vite.config.js'
+
+export default mergeConfig(
+ baseConfig,
+ defineConfig({
+ css: {
+ preprocessorOptions: {
+ sass: {
+ api: 'modern',
+ },
+ },
+ },
+ }),
+)
diff --git a/playground/css-sourcemap/vite.config.js b/playground/css-sourcemap/vite.config.js
new file mode 100644
index 00000000000000..e51cf320ad76e1
--- /dev/null
+++ b/playground/css-sourcemap/vite.config.js
@@ -0,0 +1,59 @@
+import { defineConfig } from 'vite'
+import MagicString from 'magic-string'
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ '@': __dirname,
+ },
+ },
+ css: {
+ devSourcemap: true,
+ preprocessorOptions: {
+ less: {
+ additionalData: '@color: red;',
+ },
+ styl: {
+ additionalData: (content, filename) => {
+ const ms = new MagicString(content, { filename })
+
+ const willBeReplaced = 'blue-red-mixed'
+ const start = content.indexOf(willBeReplaced)
+ ms.overwrite(start, start + willBeReplaced.length, 'purple')
+
+ const map = ms.generateMap({ hires: 'boundary' })
+ map.file = filename
+ map.sources = [filename]
+
+ return {
+ content: ms.toString(),
+ map,
+ }
+ },
+ },
+ },
+ },
+ build: {
+ sourcemap: true,
+ },
+ plugins: [
+ {
+ name: 'virtual-html',
+ configureServer(server) {
+ server.middlewares.use(async (req, res, next) => {
+ if (req.url === '/virtual.html') {
+ const t = await server.transformIndexHtml(
+ '/virtual.html',
+ ' virtual html
',
+ )
+ res.setHeader('Content-Type', 'text/html')
+ res.statusCode = 200
+ res.end(t)
+ return
+ }
+ next()
+ })
+ },
+ },
+ ],
+})
diff --git a/playground/css/__tests__/css.spec.ts b/playground/css/__tests__/css.spec.ts
new file mode 100644
index 00000000000000..c5506838313f49
--- /dev/null
+++ b/playground/css/__tests__/css.spec.ts
@@ -0,0 +1,3 @@
+import { tests } from './tests'
+
+tests(false)
diff --git a/playground/css/__tests__/lightningcss/lightningcss.spec.ts b/playground/css/__tests__/lightningcss/lightningcss.spec.ts
new file mode 100644
index 00000000000000..9b9b35db56e605
--- /dev/null
+++ b/playground/css/__tests__/lightningcss/lightningcss.spec.ts
@@ -0,0 +1,4 @@
+// NOTE: a separate directory from `playground/css` is created by playground/vitestGlobalSetup.ts
+import { tests } from '../tests'
+
+tests(true)
diff --git a/playground/css/__tests__/no-css-minify/css-no-css-minify.spec.ts b/playground/css/__tests__/no-css-minify/css-no-css-minify.spec.ts
new file mode 100644
index 00000000000000..eb70df313e1670
--- /dev/null
+++ b/playground/css/__tests__/no-css-minify/css-no-css-minify.spec.ts
@@ -0,0 +1,14 @@
+import { describe, expect, test } from 'vitest'
+import { findAssetFile, isBuild } from '~utils'
+
+describe.runIf(isBuild)('no css minify', () => {
+ test('js minified but css not minified', () => {
+ expect(findAssetFile(/index-[-\w]+\.js$/, 'no-css-minify')).not.toMatch(
+ '(function polyfill() {',
+ )
+ expect(findAssetFile(/index-[-\w]+\.css$/, 'no-css-minify')).toMatch(`\
+.test-minify {
+ color: rgba(255, 255, 0, 0.7);
+}`)
+ })
+})
diff --git a/playground/css/__tests__/postcss-plugins-different-dir/css-postcss-plugins-different-dir.spec.ts b/playground/css/__tests__/postcss-plugins-different-dir/css-postcss-plugins-different-dir.spec.ts
new file mode 100644
index 00000000000000..e9a8e129e10850
--- /dev/null
+++ b/playground/css/__tests__/postcss-plugins-different-dir/css-postcss-plugins-different-dir.spec.ts
@@ -0,0 +1,30 @@
+import path from 'node:path'
+import { createServer } from 'vite'
+import { expect, test } from 'vitest'
+import { getBgColor, getColor, isServe, page, ports } from '~utils'
+
+// Regression test for https://github.com/vitejs/vite/issues/4000
+test.runIf(isServe)('postcss plugins in different dir', async () => {
+ const port = ports['css/postcss-plugins-different-dir']
+ const server = await createServer({
+ root: path.join(__dirname, '..', '..', '..', 'tailwind'),
+ logLevel: 'silent',
+ server: {
+ port,
+ strictPort: true,
+ },
+ build: {
+ // skip transpilation during tests to make it faster
+ target: 'esnext',
+ },
+ })
+ await server.listen()
+ try {
+ await page.goto(`http://localhost:${port}`)
+ const tailwindStyle = page.locator('#tailwind-style')
+ expect(await getBgColor(tailwindStyle)).toBe('oklch(0.936 0.032 17.717)')
+ expect(await getColor(tailwindStyle)).toBe('rgb(136, 136, 136)')
+ } finally {
+ await server.close()
+ }
+})
diff --git a/playground/css/__tests__/postcss-plugins-different-dir/serve.ts b/playground/css/__tests__/postcss-plugins-different-dir/serve.ts
new file mode 100644
index 00000000000000..195bb47d8520c7
--- /dev/null
+++ b/playground/css/__tests__/postcss-plugins-different-dir/serve.ts
@@ -0,0 +1,10 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+// The server is started in the test, so we need to have a custom serve
+// function or a default server will be created
+export async function serve(): Promise<{ close(): Promise }> {
+ return {
+ close: () => Promise.resolve(),
+ }
+}
diff --git a/playground/css/__tests__/same-file-name/css-same-file-name.spec.ts b/playground/css/__tests__/same-file-name/css-same-file-name.spec.ts
new file mode 100644
index 00000000000000..a0c9a115288135
--- /dev/null
+++ b/playground/css/__tests__/same-file-name/css-same-file-name.spec.ts
@@ -0,0 +1,19 @@
+import { beforeEach, describe, expect, test } from 'vitest'
+import { findAssetFile, isBuild, startDefaultServe } from '~utils'
+
+beforeEach(async () => {
+ await startDefaultServe()
+})
+
+for (let i = 0; i < 5; i++) {
+ describe.runIf(isBuild)('css files has same basename', () => {
+ test('emit file name should consistent', () => {
+ expect(findAssetFile('sub.css', 'same-file-name', '.')).toMatch(
+ '.sub1-sub',
+ )
+ expect(findAssetFile('sub2.css', 'same-file-name', '.')).toMatch(
+ '.sub2-sub',
+ )
+ })
+ })
+}
diff --git a/playground/css/__tests__/sass-modern-compiler-build/sass-modern-compiler.spec.ts b/playground/css/__tests__/sass-modern-compiler-build/sass-modern-compiler.spec.ts
new file mode 100644
index 00000000000000..98bba744b175d5
--- /dev/null
+++ b/playground/css/__tests__/sass-modern-compiler-build/sass-modern-compiler.spec.ts
@@ -0,0 +1,15 @@
+import { expect, test } from 'vitest'
+import { findAssetFile, isBuild } from '~utils'
+
+test.runIf(isBuild)('sass modern compiler build multiple entries', () => {
+ expect(findAssetFile(/entry1/, 'sass-modern-compiler-build'))
+ .toMatchInlineSnapshot(`
+ ".entry1{color:red}
+ "
+ `)
+ expect(findAssetFile(/entry2/, 'sass-modern-compiler-build'))
+ .toMatchInlineSnapshot(`
+ ".entry2{color:#00f}
+ "
+ `)
+})
diff --git a/playground/css/__tests__/sass-modern/sass-modern.spec.ts b/playground/css/__tests__/sass-modern/sass-modern.spec.ts
new file mode 100644
index 00000000000000..b2e4ca42c5118d
--- /dev/null
+++ b/playground/css/__tests__/sass-modern/sass-modern.spec.ts
@@ -0,0 +1,6 @@
+// NOTE: a separate directory from `playground/css` is created by playground/vitestGlobalSetup.ts
+import { sassModuleTests, sassOtherTests, sassTest } from '../sass-tests'
+
+sassTest()
+sassModuleTests()
+sassOtherTests()
diff --git a/playground/css/__tests__/sass-tests.ts b/playground/css/__tests__/sass-tests.ts
new file mode 100644
index 00000000000000..c9a3c9a5d42d07
--- /dev/null
+++ b/playground/css/__tests__/sass-tests.ts
@@ -0,0 +1,112 @@
+import { expect, test } from 'vitest'
+import {
+ editFile,
+ getBg,
+ getColor,
+ isBuild,
+ page,
+ untilUpdated,
+ viteTestUrl,
+} from '~utils'
+
+export const sassTest = () => {
+ test('sass', async () => {
+ const imported = await page.$('.sass')
+ const atImport = await page.$('.sass-at-import')
+ const atImportAlias = await page.$('.sass-at-import-alias')
+ const urlStartsWithVariable = await page.$('.sass-url-starts-with-variable')
+ const urlStartsWithFunctionCall = await page.$(
+ '.sass-url-starts-with-function-call',
+ )
+ const partialImport = await page.$('.sass-partial')
+
+ expect(await getColor(imported)).toBe('orange')
+ expect(await getColor(atImport)).toBe('olive')
+ expect(await getBg(atImport)).toMatch(
+ isBuild ? /base64/ : '/nested/icon.png',
+ )
+ expect(await getColor(atImportAlias)).toBe('olive')
+ expect(await getBg(atImportAlias)).toMatch(
+ isBuild ? /base64/ : '/nested/icon.png',
+ )
+ expect(await getBg(urlStartsWithVariable)).toMatch(
+ isBuild ? /ok-[-\w]+\.png/ : `${viteTestUrl}/ok.png`,
+ )
+ expect(await getBg(urlStartsWithFunctionCall)).toMatch(
+ isBuild ? /ok-[-\w]+\.png/ : `${viteTestUrl}/ok.png`,
+ )
+ expect(await getColor(partialImport)).toBe('orchid')
+ expect(await getColor(await page.$('.sass-file-absolute'))).toBe('orange')
+ expect(await getColor(await page.$('.sass-dir-index'))).toBe('orange')
+ expect(await getColor(await page.$('.sass-root-relative'))).toBe('orange')
+
+ if (isBuild) return
+
+ editFile('sass.scss', (code) =>
+ code.replace('color: $injectedColor', 'color: red'),
+ )
+ await untilUpdated(() => getColor(imported), 'red')
+
+ editFile('nested/_index.scss', (code) =>
+ code.replace('color: olive', 'color: blue'),
+ )
+ await untilUpdated(() => getColor(atImport), 'blue')
+
+ editFile('nested/_partial.scss', (code) =>
+ code.replace('color: orchid', 'color: green'),
+ )
+ await untilUpdated(() => getColor(partialImport), 'green')
+ })
+}
+
+export const sassModuleTests = (enableHmrTests = false) => {
+ test('sass modules composes/from path resolving', async () => {
+ const imported = await page.$('.path-resolved-modules-sass')
+ expect(await getColor(imported)).toBe('orangered')
+
+ // check if the generated CSS module class name is indeed using the
+ // format specified in vite.config.js
+ expect(await imported.getAttribute('class')).toMatch(
+ /.composed-module__apply-color___[\w-]{5}/,
+ )
+
+ expect(await imported.getAttribute('class')).toMatch(
+ /.composes-path-resolving-module__path-resolving-sass___[\w-]{5}/,
+ )
+
+ // @todo HMR is not working on this situation.
+ // editFile('composed.module.scss', (code) =>
+ // code.replace('color: orangered', 'color: red')
+ // )
+ // await untilUpdated(() => getColor(imported), 'red')
+ })
+
+ test('css modules w/ sass', async () => {
+ const imported = await page.$('.modules-sass')
+ expect(await getColor(imported)).toBe('orangered')
+ expect(await imported.getAttribute('class')).toMatch(
+ /.mod-module__apply-color___[\w-]{5}/,
+ )
+
+ if (isBuild) return
+
+ editFile('mod.module.scss', (code) =>
+ code.replace('color: orangered', 'color: blue'),
+ )
+ await untilUpdated(() => getColor(imported), 'blue')
+ })
+}
+
+export const sassOtherTests = () => {
+ test('@import dependency w/ sass entry', async () => {
+ expect(await getColor('.css-dep-sass')).toBe('orange')
+ })
+
+ test('@import dependency w/ sass export mapping', async () => {
+ expect(await getColor('.css-dep-exports-sass')).toBe('orange')
+ })
+
+ test('@import dependency w/out package scss', async () => {
+ expect(await getColor('.sass-dep')).toBe('lavender')
+ })
+}
diff --git a/playground/css/__tests__/tests.ts b/playground/css/__tests__/tests.ts
new file mode 100644
index 00000000000000..086180b9a64b44
--- /dev/null
+++ b/playground/css/__tests__/tests.ts
@@ -0,0 +1,543 @@
+import { readFileSync } from 'node:fs'
+import { expect, test } from 'vitest'
+import { sassModuleTests, sassOtherTests, sassTest } from './sass-tests'
+import {
+ editFile,
+ findAssetFile,
+ getBg,
+ getBgColor,
+ getColor,
+ isBuild,
+ page,
+ removeFile,
+ serverLogs,
+ untilUpdated,
+ viteTestUrl,
+ withRetry,
+} from '~utils'
+
+export const tests = (isLightningCSS: boolean) => {
+ // note: tests should retrieve the element at the beginning of test and reuse it
+ // in later assertions to ensure CSS HMR doesn't reload the page
+ test('imported css', async () => {
+ const glob = await page.textContent('.imported-css-glob')
+ expect(glob).toContain('.dir-import')
+ const globEager = await page.textContent('.imported-css-globEager')
+ expect(globEager).toContain('.dir-import')
+ })
+
+ test('linked css', async () => {
+ const linked = await page.$('.linked')
+ const atImport = await page.$('.linked-at-import')
+
+ expect(await getColor(linked)).toBe('blue')
+ expect(await getColor(atImport)).toBe('red')
+
+ if (isBuild) return
+
+ editFile('linked.css', (code) => code.replace('color: blue', 'color: red'))
+ await untilUpdated(() => getColor(linked), 'red')
+
+ editFile('linked-at-import.css', (code) =>
+ code.replace('color: red', 'color: blue'),
+ )
+ await untilUpdated(() => getColor(atImport), 'blue')
+ })
+
+ test('css import from js', async () => {
+ const imported = await page.$('.imported')
+ const atImport = await page.$('.imported-at-import')
+
+ expect(await getColor(imported)).toBe('green')
+ expect(await getColor(atImport)).toBe('purple')
+
+ if (isBuild) return
+
+ editFile('imported.css', (code) =>
+ code.replace('color: green', 'color: red'),
+ )
+ await untilUpdated(() => getColor(imported), 'red')
+
+ editFile('imported-at-import.css', (code) =>
+ code.replace('color: purple', 'color: blue'),
+ )
+ await untilUpdated(() => getColor(atImport), 'blue')
+ })
+
+ test('css import asset with space', async () => {
+ const importedWithSpace = await page.$('.import-with-space')
+
+ expect(await getBg(importedWithSpace)).toMatch(/.*\/ok.*\.png/)
+ })
+
+ test('postcss config', async () => {
+ const imported = await page.$('.postcss .nesting')
+ expect(await getColor(imported)).toBe('pink')
+
+ if (isBuild) return
+
+ editFile('imported.css', (code) =>
+ code.replace('color: pink', 'color: red'),
+ )
+ await untilUpdated(() => getColor(imported), 'red')
+ })
+
+ test('postcss plugin that injects url()', async () => {
+ const imported = await page.$('.inject-url')
+ // alias should be resolved
+ expect(await getBg(imported)).toMatch(
+ /localhost(?::\d+)?\/(?:assets\/)?ok.*\.png/,
+ )
+ })
+
+ sassTest()
+
+ test('less', async () => {
+ const imported = await page.$('.less')
+ const atImport = await page.$('.less-at-import')
+ const atImportAlias = await page.$('.less-at-import-alias')
+ const atImportUrlOmmer = await page.$('.less-at-import-url-ommer')
+ const urlStartsWithVariable = await page.$('.less-url-starts-with-variable')
+
+ expect(await getColor(imported)).toBe('blue')
+ expect(await getColor(atImport)).toBe('darkslateblue')
+ expect(await getBg(atImport)).toMatch(
+ isBuild ? /base64/ : '/nested/icon.png',
+ )
+ expect(await getColor(atImportAlias)).toBe('darkslateblue')
+ expect(await getBg(atImportAlias)).toMatch(
+ isBuild ? /base64/ : '/nested/icon.png',
+ )
+ expect(await getColor(atImportUrlOmmer)).toBe('darkorange')
+ expect(await getBg(urlStartsWithVariable)).toMatch(
+ isBuild ? /ok-[-\w]+\.png/ : `${viteTestUrl}/ok.png`,
+ )
+
+ if (isBuild) return
+
+ editFile('less.less', (code) => code.replace('@color: blue', '@color: red'))
+ await untilUpdated(() => getColor(imported), 'red')
+
+ editFile('nested/nested.less', (code) =>
+ code.replace('color: darkslateblue', 'color: blue'),
+ )
+ await untilUpdated(() => getColor(atImport), 'blue')
+ })
+
+ test('less-plugin', async () => {
+ const body = await page.$('.less-js-plugin')
+ expect(await getBg(body)).toBe(
+ 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGD4/x8AAwIB/8myre4AAAAASUVORK5CYII=")',
+ )
+ })
+
+ test('stylus', async () => {
+ const imported = await page.$('.stylus')
+ const additionalData = await page.$('.stylus-additional-data')
+ const relativeImport = await page.$('.stylus-import')
+ const relativeImportAlias = await page.$('.stylus-import-alias')
+ const optionsRelativeImport = await page.$(
+ '.stylus-options-relative-import',
+ )
+ const optionsAbsoluteImport = await page.$(
+ '.stylus-options-absolute-import',
+ )
+ const optionsDefineVar = await page.$('.stylus-options-define-var')
+ const optionsDefineFunc = await page.$('.stylus-options-define-func')
+
+ expect(await getColor(imported)).toBe('blue')
+ expect(await getColor(additionalData)).toBe('orange')
+ expect(await getColor(relativeImport)).toBe('darkslateblue')
+ expect(await getColor(relativeImportAlias)).toBe('darkslateblue')
+ expect(await getBg(relativeImportAlias)).toMatch(
+ isBuild ? /base64/ : '/nested/icon.png',
+ )
+ expect(await getColor(optionsRelativeImport)).toBe('green')
+ expect(await getColor(optionsAbsoluteImport)).toBe('red')
+ expect(await getColor(optionsDefineVar)).toBe('rgb(51, 197, 255)')
+ expect(await getColor(optionsDefineFunc)).toBe('rgb(255, 0, 98)')
+
+ if (isBuild) return
+
+ editFile('stylus.styl', (code) =>
+ code.replace('$color ?= blue', '$color ?= red'),
+ )
+ await untilUpdated(() => getColor(imported), 'red')
+
+ editFile('nested/nested.styl', (code) =>
+ code.replace('color: darkslateblue', 'color: blue'),
+ )
+ await untilUpdated(() => getColor(relativeImport), 'blue')
+ })
+
+ test('css modules', async () => {
+ const imported = await page.$('.modules')
+ expect(await getColor(imported)).toBe('turquoise')
+
+ // check if the generated CSS module class name is indeed using the
+ // format specified in vite.config.js
+ expect(await imported.getAttribute('class')).toMatch(
+ /.mod-module__apply-color___[\w-]{5}/,
+ )
+
+ if (isBuild) return
+
+ editFile('mod.module.css', (code) =>
+ code.replace('color: turquoise', 'color: red'),
+ )
+ await untilUpdated(() => getColor(imported), 'red')
+ })
+
+ test('css modules composes/from path resolving', async () => {
+ const imported = await page.$('.path-resolved-modules-css')
+ expect(await getColor(imported)).toBe('turquoise')
+
+ // check if the generated CSS module class name is indeed using the
+ // format specified in vite.config.js
+ expect(await imported.getAttribute('class')).toMatch(
+ /.composed-module__apply-color___[\w-]{5}/,
+ )
+
+ expect(await imported.getAttribute('class')).toMatch(
+ /.composes-path-resolving-module__path-resolving-css___[\w-]{5}/,
+ )
+
+ // @todo HMR is not working on this situation.
+ // editFile('composed.module.css', (code) =>
+ // code.replace('color: turquoise', 'color: red')
+ // )
+ // await untilUpdated(() => getColor(imported), 'red')
+ })
+
+ sassModuleTests()
+
+ test('less modules composes/from path resolving', async () => {
+ const imported = await page.$('.path-resolved-modules-less')
+ expect(await getColor(imported)).toBe('blue')
+
+ // check if the generated CSS module class name is indeed using the
+ // format specified in vite.config.js
+ expect(await imported.getAttribute('class')).toMatch(
+ /.composed-module__apply-color___[\w-]{5}/,
+ )
+
+ expect(await imported.getAttribute('class')).toMatch(
+ /.composes-path-resolving-module__path-resolving-less___[\w-]{5}/,
+ )
+
+ // @todo HMR is not working on this situation.
+ // editFile('composed.module.scss', (code) =>
+ // code.replace('color: orangered', 'color: red')
+ // )
+ // await untilUpdated(() => getColor(imported), 'red')
+ })
+
+ test('inline css modules', async () => {
+ const css = await page.textContent('.modules-inline')
+ expect(css).toMatch(/\.inline-module__apply-color-inline___[\w-]{5}/)
+ })
+
+ test.runIf(isBuild)('@charset hoist', async () => {
+ serverLogs.forEach((log) => {
+ // no warning from esbuild css minifier
+ expect(log).not.toMatch('"@charset" must be the first rule in the file')
+ })
+ })
+
+ test('layers', async () => {
+ expect(await getColor('.layers-blue')).toMatch('blue')
+ expect(await getColor('.layers-green')).toMatch('green')
+ })
+
+ test('@import dependency w/ style entry', async () => {
+ expect(await getColor('.css-dep')).toBe('purple')
+ })
+
+ test('@import dependency w/ style export mapping', async () => {
+ expect(await getColor('.css-dep-exports')).toBe('purple')
+ })
+
+ test('@import dependency that @import another dependency', async () => {
+ expect(await getColor('.css-proxy-dep')).toBe('purple')
+ })
+
+ test('@import scss dependency that has @import with a css extension pointing to another dependency', async () => {
+ expect(await getColor('.scss-proxy-dep')).toBe('purple')
+ })
+
+ sassOtherTests()
+
+ test('async chunk', async () => {
+ const el = await page.$('.async')
+ expect(await getColor(el)).toBe('teal')
+
+ if (isBuild) {
+ // assert that the css is extracted into its own file instead of in the
+ // main css file
+ expect(findAssetFile(/index-[-\w]{8}\.css$/)).not.toMatch('teal')
+ expect(findAssetFile(/async-[-\w]{8}\.css$/)).toMatch(
+ '.async{color:teal}',
+ )
+ } else {
+ // test hmr
+ editFile('async.css', (code) =>
+ code.replace('color: teal', 'color: blue'),
+ )
+ await untilUpdated(() => getColor(el), 'blue')
+ }
+ })
+
+ test('treeshaken async chunk', async () => {
+ if (isBuild) {
+ // should be absent in prod
+ expect(
+ await page.evaluate(() => {
+ return document.querySelector('.async-treeshaken')
+ }),
+ ).toBeNull()
+ // assert that the css is not present anywhere
+ expect(findAssetFile(/\.css$/)).not.toMatch('plum')
+ expect(findAssetFile(/index-[-\w]+\.js$/)).not.toMatch(
+ '.async{color:plum}',
+ )
+ expect(findAssetFile(/async-[-\w]+\.js$/)).not.toMatch(
+ '.async{color:plum}',
+ )
+ // should have no chunk!
+ expect(findAssetFile(/async-treeshaken/)).toBe('')
+ } else {
+ // should be present in dev
+ const el = await page.$('.async-treeshaken')
+ editFile('async-treeshaken.css', (code) =>
+ code.replace('color: plum', 'color: blue'),
+ )
+ await untilUpdated(() => getColor(el), 'blue')
+ }
+ })
+
+ test('PostCSS dir-dependency', async () => {
+ const el1 = await page.$('.dir-dep')
+ const el2 = await page.$('.dir-dep-2')
+ const el3 = await page.$('.dir-dep-3')
+
+ expect(await getColor(el1)).toBe('grey')
+ expect(await getColor(el2)).toBe('grey')
+ expect(await getColor(el3)).toBe('grey')
+
+ // NOTE: lightningcss does not support registering dependencies in plugins
+ if (!isBuild && !isLightningCSS) {
+ editFile('glob-dep/foo.css', (code) =>
+ code.replace('color: grey', 'color: blue'),
+ )
+ await untilUpdated(() => getColor(el1), 'blue')
+ expect(await getColor(el2)).toBe('grey')
+
+ editFile('glob-dep/bar.css', (code) =>
+ code.replace('color: grey', 'color: red'),
+ )
+ await untilUpdated(() => getColor(el2), 'red')
+ expect(await getColor(el1)).toBe('blue')
+
+ editFile('glob-dep/nested (dir)/baz.css', (code) =>
+ code.replace('color: grey', 'color: green'),
+ )
+ await untilUpdated(() => getColor(el3), 'green')
+ expect(await getColor(el1)).toBe('blue')
+ expect(await getColor(el2)).toBe('red')
+
+ // test add/remove
+ removeFile('glob-dep/bar.css')
+ await untilUpdated(() => getColor(el2), 'black')
+ }
+ })
+
+ test('import dependency includes css import', async () => {
+ expect(await getColor('.css-js-dep')).toBe('green')
+ expect(await getColor('.css-js-dep-module')).toBe('green')
+ })
+
+ test('URL separation', async () => {
+ const urlSeparated = await page.$('.url-separated')
+ const baseUrl = 'url(images/dog.webp)'
+ const cases = new Array(5)
+ .fill('')
+ .flatMap((_, i) =>
+ [',', ' ,', ', ', ' , '].map(
+ (sep) =>
+ `background-image:${new Array(i + 1).fill(baseUrl).join(sep)};`,
+ ),
+ )
+
+ // Insert the base case
+ cases.unshift('background-image:url(images/cat.webp),url(images/dog.webp)')
+
+ for (const [c, i] of cases.map((c, i) => [c, i]) as [string, number][]) {
+ // Replace the previous case
+ if (i > 0)
+ editFile('imported.css', (code) => code.replace(cases[i - 1], c))
+
+ expect(await getBg(urlSeparated)).toMatch(
+ /^url\(.+\)(?:\s*,\s*url\(.+\))*$/,
+ )
+ }
+ })
+
+ test('inlined', async () => {
+ // should not insert css
+ expect(await getColor('.inlined')).toBe('black')
+ })
+
+ test('inlined-code', async () => {
+ const code = await page.textContent('.inlined-code')
+ // should resolve assets
+ expect(code).toContain('background:')
+ expect(code).not.toContain('__VITE_ASSET__')
+
+ if (isBuild) {
+ expect(code.trim()).not.toContain('\n') // check minified
+ }
+ })
+
+ test('minify css', async () => {
+ if (!isBuild) {
+ return
+ }
+
+ // should keep the rgba() syntax
+ const cssFile = findAssetFile(/index-[-\w]+\.css$/)
+ expect(cssFile).toMatch('rgba(')
+ expect(cssFile).not.toMatch('#ffff00b3')
+ })
+
+ test('?url', async () => {
+ expect(await getColor('.url-imported-css')).toBe('yellow')
+ })
+
+ test('?raw', async () => {
+ const rawImportCss = await page.$('.raw-imported-css')
+
+ expect(await rawImportCss.textContent()).toBe(
+ readFileSync(require.resolve('../raw-imported.css'), 'utf-8'),
+ )
+
+ if (!isBuild) {
+ editFile('raw-imported.css', (code) =>
+ code.replace('color: yellow', 'color: blue'),
+ )
+ await untilUpdated(
+ () => page.textContent('.raw-imported-css'),
+ 'color: blue',
+ )
+ }
+ })
+
+ test('import css in less', async () => {
+ expect(await getColor('.css-in-less')).toBe('yellow')
+ expect(await getColor('.css-in-less-2')).toBe('blue')
+ })
+
+ test("relative path rewritten in Less's data-uri", async () => {
+ // relative path passed to Less's data-uri is rewritten to absolute,
+ // the Less inlines it
+ expect(await getBg('.form-box-data-uri')).toMatch(
+ /^url\("data:image\/svg\+xml,%3Csvg/,
+ )
+ })
+
+ test('PostCSS source.input.from includes query', async () => {
+ const code = await page.textContent('.postcss-source-input')
+ // should resolve assets
+ expect(code).toContain('/postcss-source-input.css?inline&query=foo')
+ })
+
+ test('aliased css has content', async () => {
+ expect(await getColor('.aliased')).toBe('blue')
+ // skipped: currently not supported see #8936
+ // expect(await page.textContent('.aliased-content')).toMatch('.aliased')
+ expect(await getColor('.aliased-module')).toBe('blue')
+ })
+
+ test('resolve imports field in CSS', async () => {
+ expect(await getColor('.imports-field')).toBe('red')
+ })
+
+ test.runIf(isBuild)(
+ 'warning can be suppressed by esbuild.logOverride',
+ () => {
+ serverLogs.forEach((log) => {
+ // no warning from esbuild css minifier
+ expect(log).not.toMatch('unsupported-css-property')
+ })
+ },
+ )
+
+ test('sugarss', async () => {
+ const imported = await page.$('.sugarss')
+ const atImport = await page.$('.sugarss-at-import')
+ const atImportAlias = await page.$('.sugarss-at-import-alias')
+
+ expect(await getColor(imported)).toBe('blue')
+ expect(await getColor(atImport)).toBe('darkslateblue')
+ expect(await getBg(atImport)).toMatch(
+ isBuild ? /base64/ : '/nested/icon.png',
+ )
+ expect(await getColor(atImportAlias)).toBe('darkslateblue')
+ expect(await getBg(atImportAlias)).toMatch(
+ isBuild ? /base64/ : '/nested/icon.png',
+ )
+
+ if (isBuild) return
+
+ editFile('sugarss.sss', (code) =>
+ code.replace('color: blue', 'color: coral'),
+ )
+ await untilUpdated(() => getColor(imported), 'coral')
+
+ editFile('nested/nested.sss', (code) =>
+ code.replace('color: darkslateblue', 'color: blue'),
+ )
+ await untilUpdated(() => getColor(atImport), 'blue')
+ })
+
+ // NOTE: the match inline snapshot should generate by build mode
+ test('async css order', async () => {
+ await withRetry(async () => {
+ expect(await getColor('.async-green')).toMatchInlineSnapshot('"green"')
+ expect(await getColor('.async-blue')).toMatchInlineSnapshot('"blue"')
+ })
+ })
+
+ test('async css order with css modules', async () => {
+ await withRetry(async () => {
+ expect(await getColor('.modules-pink')).toMatchInlineSnapshot('"pink"')
+ })
+ })
+
+ test('@import scss', async () => {
+ expect(await getColor('.at-import-scss')).toBe('red')
+ })
+
+ test.runIf(isBuild)('manual chunk path', async () => {
+ // assert that the manual-chunk css is output in the directory specified in manualChunk (#12072)
+ expect(
+ findAssetFile(/dir\/dir2\/manual-chunk-[-\w]{8}\.css$/),
+ ).not.toBeUndefined()
+ })
+
+ test.runIf(isBuild)('CSS modules should be treeshaken if not used', () => {
+ const css = findAssetFile(/\.css$/, undefined, undefined, true)
+ expect(css).not.toContain('treeshake-module-b')
+ })
+
+ test.runIf(isBuild)('Scoped CSS via cssScopeTo should be treeshaken', () => {
+ const css = findAssetFile(/\.css$/, undefined, undefined, true)
+ expect(css).not.toMatch(/\btreeshake-scoped-b\b/)
+ expect(css).not.toMatch(/\btreeshake-scoped-c\b/)
+ })
+
+ test('Scoped CSS should have a correct order', async () => {
+ await page.goto(viteTestUrl + '/treeshake-scoped/')
+ expect(await getColor('.treeshake-scoped-order')).toBe('red')
+ expect(await getBgColor('.treeshake-scoped-order')).toBe('blue')
+ })
+}
diff --git a/playground/css/aliased/bar.module.css b/playground/css/aliased/bar.module.css
new file mode 100644
index 00000000000000..e4e46f3306a02e
--- /dev/null
+++ b/playground/css/aliased/bar.module.css
@@ -0,0 +1,3 @@
+.aliasedModule {
+ color: blue;
+}
diff --git a/playground/css/aliased/foo.css b/playground/css/aliased/foo.css
new file mode 100644
index 00000000000000..7e32cb71a8f375
--- /dev/null
+++ b/playground/css/aliased/foo.css
@@ -0,0 +1,3 @@
+.aliased {
+ color: blue;
+}
diff --git a/packages/playground/css/async-treeshaken.css b/playground/css/async-treeshaken.css
similarity index 100%
rename from packages/playground/css/async-treeshaken.css
rename to playground/css/async-treeshaken.css
diff --git a/packages/playground/css/async-treeshaken.js b/playground/css/async-treeshaken.js
similarity index 100%
rename from packages/playground/css/async-treeshaken.js
rename to playground/css/async-treeshaken.js
diff --git a/packages/playground/css/async.css b/playground/css/async.css
similarity index 100%
rename from packages/playground/css/async.css
rename to playground/css/async.css
diff --git a/packages/playground/css/async.js b/playground/css/async.js
similarity index 100%
rename from packages/playground/css/async.js
rename to playground/css/async.js
diff --git a/playground/css/async/async-1.css b/playground/css/async/async-1.css
new file mode 100644
index 00000000000000..9af99eec7843fe
--- /dev/null
+++ b/playground/css/async/async-1.css
@@ -0,0 +1,3 @@
+.async-blue {
+ color: blue;
+}
diff --git a/playground/css/async/async-1.js b/playground/css/async/async-1.js
new file mode 100644
index 00000000000000..8187dc3b9307e7
--- /dev/null
+++ b/playground/css/async/async-1.js
@@ -0,0 +1,4 @@
+import { createButton } from './base'
+import './async-1.css'
+
+createButton('async-blue')
diff --git a/playground/css/async/async-2.css b/playground/css/async/async-2.css
new file mode 100644
index 00000000000000..941e034da37389
--- /dev/null
+++ b/playground/css/async/async-2.css
@@ -0,0 +1,3 @@
+.async-green {
+ color: green;
+}
diff --git a/playground/css/async/async-2.js b/playground/css/async/async-2.js
new file mode 100644
index 00000000000000..157eafdc4bff79
--- /dev/null
+++ b/playground/css/async/async-2.js
@@ -0,0 +1,4 @@
+import { createButton } from './base'
+import './async-2.css'
+
+createButton('async-green')
diff --git a/playground/css/async/async-3.js b/playground/css/async/async-3.js
new file mode 100644
index 00000000000000..b5dd6da1f326d2
--- /dev/null
+++ b/playground/css/async/async-3.js
@@ -0,0 +1,4 @@
+import { createButton } from './base'
+import styles from './async-3.module.css'
+
+createButton(`${styles['async-pink']} modules-pink`)
diff --git a/playground/css/async/async-3.module.css b/playground/css/async/async-3.module.css
new file mode 100644
index 00000000000000..7f43f88d754252
--- /dev/null
+++ b/playground/css/async/async-3.module.css
@@ -0,0 +1,3 @@
+.async-pink {
+ color: pink;
+}
diff --git a/playground/css/async/base.css b/playground/css/async/base.css
new file mode 100644
index 00000000000000..cc6f88ddccdf10
--- /dev/null
+++ b/playground/css/async/base.css
@@ -0,0 +1,3 @@
+.btn {
+ color: black;
+}
diff --git a/playground/css/async/base.js b/playground/css/async/base.js
new file mode 100644
index 00000000000000..1a409d7e32e4c9
--- /dev/null
+++ b/playground/css/async/base.js
@@ -0,0 +1,8 @@
+import './base.css'
+
+export function createButton(className) {
+ const button = document.createElement('button')
+ button.className = `btn ${className}`
+ document.body.appendChild(button)
+ button.textContent = `button ${getComputedStyle(button).color}`
+}
diff --git a/playground/css/async/index.js b/playground/css/async/index.js
new file mode 100644
index 00000000000000..20d6975ab9d23a
--- /dev/null
+++ b/playground/css/async/index.js
@@ -0,0 +1,3 @@
+import('./async-1.js')
+import('./async-2.js')
+import('./async-3.js')
diff --git a/playground/css/charset.css b/playground/css/charset.css
new file mode 100644
index 00000000000000..5c42b279f8404c
--- /dev/null
+++ b/playground/css/charset.css
@@ -0,0 +1,5 @@
+@charset "utf-8";
+
+.utf8 {
+ color: green;
+}
diff --git a/playground/css/composed.module.css b/playground/css/composed.module.css
new file mode 100644
index 00000000000000..b2ae0e967dced1
--- /dev/null
+++ b/playground/css/composed.module.css
@@ -0,0 +1,3 @@
+.apply-color {
+ color: turquoise;
+}
diff --git a/packages/playground/css/composed.module.less b/playground/css/composed.module.less
similarity index 100%
rename from packages/playground/css/composed.module.less
rename to playground/css/composed.module.less
diff --git a/packages/playground/css/composed.module.scss b/playground/css/composed.module.scss
similarity index 100%
rename from packages/playground/css/composed.module.scss
rename to playground/css/composed.module.scss
diff --git a/playground/css/composes-path-resolving.module.css b/playground/css/composes-path-resolving.module.css
new file mode 100644
index 00000000000000..a5a5172eb4104c
--- /dev/null
+++ b/playground/css/composes-path-resolving.module.css
@@ -0,0 +1,11 @@
+.path-resolving-css {
+ composes: apply-color from '=/composed.module.css';
+}
+
+.path-resolving-sass {
+ composes: apply-color from '=/composed.module.scss';
+}
+
+.path-resolving-less {
+ composes: apply-color from '=/composed.module.less';
+}
diff --git a/packages/playground/css/css-dep/index.js b/playground/css/css-dep-exports/index.js
similarity index 100%
rename from packages/playground/css/css-dep/index.js
rename to playground/css/css-dep-exports/index.js
diff --git a/playground/css/css-dep-exports/package.json b/playground/css/css-dep-exports/package.json
new file mode 100644
index 00000000000000..70cb0e17aad988
--- /dev/null
+++ b/playground/css/css-dep-exports/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-css-dep-exports",
+ "private": true,
+ "version": "1.0.0",
+ "exports": {
+ ".": {
+ "sass": "./style.scss",
+ "style": "./style.css",
+ "import": "./index.js"
+ }
+ }
+}
diff --git a/playground/css/css-dep-exports/style.css b/playground/css/css-dep-exports/style.css
new file mode 100644
index 00000000000000..838a8afbe4d435
--- /dev/null
+++ b/playground/css/css-dep-exports/style.css
@@ -0,0 +1,3 @@
+.css-dep-exports {
+ color: purple;
+}
diff --git a/playground/css/css-dep-exports/style.scss b/playground/css/css-dep-exports/style.scss
new file mode 100644
index 00000000000000..37df38d7d49d24
--- /dev/null
+++ b/playground/css/css-dep-exports/style.scss
@@ -0,0 +1,3 @@
+.css-dep-exports-sass {
+ color: orange;
+}
diff --git a/packages/playground/css/css-dep/index.css b/playground/css/css-dep/index.css
similarity index 100%
rename from packages/playground/css/css-dep/index.css
rename to playground/css/css-dep/index.css
diff --git a/playground/css/css-dep/index.js b/playground/css/css-dep/index.js
new file mode 100644
index 00000000000000..47b55353d03edb
--- /dev/null
+++ b/playground/css/css-dep/index.js
@@ -0,0 +1 @@
+throw new Error('should not be imported')
diff --git a/packages/playground/css/css-dep/index.scss b/playground/css/css-dep/index.scss
similarity index 100%
rename from packages/playground/css/css-dep/index.scss
rename to playground/css/css-dep/index.scss
diff --git a/packages/playground/css/css-dep/index.styl b/playground/css/css-dep/index.styl
similarity index 100%
rename from packages/playground/css/css-dep/index.styl
rename to playground/css/css-dep/index.styl
diff --git a/playground/css/css-dep/package.json b/playground/css/css-dep/package.json
new file mode 100644
index 00000000000000..204e8c71b0ebba
--- /dev/null
+++ b/playground/css/css-dep/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@vitejs/test-css-dep",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.js",
+ "style": "index.css",
+ "sass": "index.scss"
+}
diff --git a/playground/css/css-js-dep/bar.module.css b/playground/css/css-js-dep/bar.module.css
new file mode 100644
index 00000000000000..9d62f66761fa3d
--- /dev/null
+++ b/playground/css/css-js-dep/bar.module.css
@@ -0,0 +1,3 @@
+.cssJsDepModule {
+ color: green;
+}
diff --git a/playground/css/css-js-dep/foo.css b/playground/css/css-js-dep/foo.css
new file mode 100644
index 00000000000000..515ee7693bff3f
--- /dev/null
+++ b/playground/css/css-js-dep/foo.css
@@ -0,0 +1,3 @@
+.css-js-dep {
+ color: green;
+}
diff --git a/playground/css/css-js-dep/index.js b/playground/css/css-js-dep/index.js
new file mode 100644
index 00000000000000..853094b806fa97
--- /dev/null
+++ b/playground/css/css-js-dep/index.js
@@ -0,0 +1,4 @@
+import './foo.css'
+import barModuleClasses from './bar.module.css'
+
+export { barModuleClasses }
diff --git a/playground/css/css-js-dep/package.json b/playground/css/css-js-dep/package.json
new file mode 100644
index 00000000000000..ce96e1e3c2b3f9
--- /dev/null
+++ b/playground/css/css-js-dep/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-css-js-dep",
+ "private": true,
+ "type": "module",
+ "version": "1.0.0",
+ "main": "index.js"
+}
diff --git a/playground/css/css-proxy-dep-nested/index.css b/playground/css/css-proxy-dep-nested/index.css
new file mode 100644
index 00000000000000..ad0654a130d2e5
--- /dev/null
+++ b/playground/css/css-proxy-dep-nested/index.css
@@ -0,0 +1,3 @@
+.css-proxy-dep {
+ color: purple;
+}
diff --git a/playground/css/css-proxy-dep-nested/package.json b/playground/css/css-proxy-dep-nested/package.json
new file mode 100644
index 00000000000000..06cb23332c7c56
--- /dev/null
+++ b/playground/css/css-proxy-dep-nested/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-css-proxy-dep-nested",
+ "private": true,
+ "version": "1.0.0",
+ "style": "index.css"
+}
diff --git a/playground/css/css-proxy-dep/index.css b/playground/css/css-proxy-dep/index.css
new file mode 100644
index 00000000000000..9b31759a8a326d
--- /dev/null
+++ b/playground/css/css-proxy-dep/index.css
@@ -0,0 +1 @@
+@import '@vitejs/test-css-proxy-dep-nested';
diff --git a/playground/css/css-proxy-dep/package.json b/playground/css/css-proxy-dep/package.json
new file mode 100644
index 00000000000000..60256b6f4e2486
--- /dev/null
+++ b/playground/css/css-proxy-dep/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-css-proxy-dep",
+ "private": true,
+ "version": "1.0.0",
+ "style": "index.css",
+ "dependencies": {
+ "@vitejs/test-css-proxy-dep-nested": "file:../css-proxy-dep-nested"
+ }
+}
diff --git a/playground/css/dep.css b/playground/css/dep.css
new file mode 100644
index 00000000000000..2577e5b8cb3578
--- /dev/null
+++ b/playground/css/dep.css
@@ -0,0 +1,3 @@
+@import '@vitejs/test-css-dep';
+@import '@vitejs/test-css-dep-exports';
+@import '@vitejs/test-css-proxy-dep';
diff --git a/playground/css/file-absolute.scss b/playground/css/file-absolute.scss
new file mode 100644
index 00000000000000..508930e3678f6e
--- /dev/null
+++ b/playground/css/file-absolute.scss
@@ -0,0 +1,3 @@
+.sass-file-absolute {
+ color: orange;
+}
diff --git a/playground/css/folder with space/ok.png b/playground/css/folder with space/ok.png
new file mode 100644
index 00000000000000..a8d1e52510c41c
Binary files /dev/null and b/playground/css/folder with space/ok.png differ
diff --git a/playground/css/folder with space/space.css b/playground/css/folder with space/space.css
new file mode 100644
index 00000000000000..55a8532da32a94
--- /dev/null
+++ b/playground/css/folder with space/space.css
@@ -0,0 +1,5 @@
+.import-with-space {
+ color: green;
+ background: url(spacefolder/ok.png);
+ background-position: center;
+}
diff --git a/packages/playground/css/glob-dep.css b/playground/css/glob-dep.css
similarity index 100%
rename from packages/playground/css/glob-dep.css
rename to playground/css/glob-dep.css
diff --git a/packages/playground/css/glob-dep/bar.css b/playground/css/glob-dep/bar.css
similarity index 100%
rename from packages/playground/css/glob-dep/bar.css
rename to playground/css/glob-dep/bar.css
diff --git a/packages/playground/css/glob-dep/foo.css b/playground/css/glob-dep/foo.css
similarity index 100%
rename from packages/playground/css/glob-dep/foo.css
rename to playground/css/glob-dep/foo.css
diff --git a/playground/css/glob-dep/nested (dir)/baz.css b/playground/css/glob-dep/nested (dir)/baz.css
new file mode 100644
index 00000000000000..9a8b0f0ba47dc5
--- /dev/null
+++ b/playground/css/glob-dep/nested (dir)/baz.css
@@ -0,0 +1,3 @@
+.dir-dep-3 {
+ color: grey;
+}
diff --git a/packages/playground/css/glob-import/bar.css b/playground/css/glob-import/bar.css
similarity index 100%
rename from packages/playground/css/glob-import/bar.css
rename to playground/css/glob-import/bar.css
diff --git a/packages/playground/css/glob-import/foo.css b/playground/css/glob-import/foo.css
similarity index 100%
rename from packages/playground/css/glob-import/foo.css
rename to playground/css/glob-import/foo.css
diff --git a/playground/css/imported-at-import.css b/playground/css/imported-at-import.css
new file mode 100644
index 00000000000000..b71ef00e65b27a
--- /dev/null
+++ b/playground/css/imported-at-import.css
@@ -0,0 +1,3 @@
+.imported-at-import {
+ color: purple;
+}
diff --git a/playground/css/imported.css b/playground/css/imported.css
new file mode 100644
index 00000000000000..7d582995fab9fd
--- /dev/null
+++ b/playground/css/imported.css
@@ -0,0 +1,26 @@
+@import './imported-at-import.css';
+@import 'spacefolder/space.css';
+
+.imported {
+ color: green;
+}
+
+pre {
+ background-color: #eee;
+ width: 500px;
+ padding: 1em 1.5em;
+ border-radius: 10px;
+}
+
+/* test postcss nesting */
+.postcss {
+ .nesting {
+ color: pink;
+ }
+}
+
+/* test url comma separation */
+.url-separated {
+ /* prettier-ignore */
+ background-image:url(images/cat.webp),url(images/dog.webp);
+}
diff --git a/playground/css/imported.scss b/playground/css/imported.scss
new file mode 100644
index 00000000000000..eee442a32c9eb5
--- /dev/null
+++ b/playground/css/imported.scss
@@ -0,0 +1,5 @@
+$color: red;
+
+.at-import-scss {
+ color: $color;
+}
diff --git a/playground/css/imports-field.css b/playground/css/imports-field.css
new file mode 100644
index 00000000000000..9120b6c04f9150
--- /dev/null
+++ b/playground/css/imports-field.css
@@ -0,0 +1,3 @@
+.imports-field {
+ color: red;
+}
diff --git a/playground/css/imports-imports-field.css b/playground/css/imports-imports-field.css
new file mode 100644
index 00000000000000..2f4167c3033ec4
--- /dev/null
+++ b/playground/css/imports-imports-field.css
@@ -0,0 +1 @@
+@import '#imports';
diff --git a/playground/css/index.html b/playground/css/index.html
new file mode 100644
index 00000000000000..17e680d349a3dc
--- /dev/null
+++ b/playground/css/index.html
@@ -0,0 +1,222 @@
+
+
+
+
CSS
+
+
<link>: This should be blue
+
@import in <link>: This should be red
+
+
import from js: This should be green
+
+ @import in import from js: This should be purple
+
+
+ @import from file with space: This should be green and have a background
+ image
+
+
Imported css string:
+
+
+
+
Imported scoped CSS
+
+
+ PostCSS nesting plugin: this should be pink
+
+
PostCSS plugin: this should have a background image
+
+
SASS: This should be orange
+
+ @import from SASS _index: This should be olive and have bg image
+
+
+ @import from SASS _index: This should be olive and have bg image which url
+ contains alias
+
+
@import from SASS _partial: This should be orchid
+
url starts with variable
+
+ url starts with function call
+
+
Imported SASS string:
+
+ @import dependency w/ no scss entrypoint: this should be lavender
+
+
+ @import "file:///xxx/absolute-path.scss" should be orange
+
+
@import "./dir" should be orange
+
+ @import "/nested/root-relative.scss" should be orange
+
+
+
Less: This should be blue
+
+ @import from Less: This should be darkslateblue and have bg image
+
+
+ @import from Less: This should be darkslateblue and have bg image which url
+ contains alias
+
+
+ @import url() from Less: This should be darkorange
+
+
url starts with variable
+
+
+ tests Less's `data-uri()` function with relative image paths
+
+
+ url in Less's JS plugin: This should have a blue square below
+
+
+
+
Stylus: This should be blue
+
+ Stylus additionalData: This should be orange
+
+
@import from Stylus: This should be darkslateblue
+
+ @import from Stylus: This should be darkslateblue and have bg image which
+ url contains alias
+
+
+ Stylus import (relative path) via vite config preprocessor options: This
+ should be green
+
+
+ Stylus import (absolute path) via vite config preprocessor options: This
+ should be red
+
+
+ Stylus define variable via vite config preprocessor options: This should be
+ rgb(51, 197, 255)
+
+
+ Stylus define function via vite config preprocessor options: This should be
+ rgb(255, 0, 98)
+
+
+
+
+
+
+
CSS modules: this should be turquoise
+
Imported CSS module:
+
+
+
CSS modules w/ SASS: this should be orangered
+
Imported SASS module:
+
+
+
CSS modules should treeshake in build
+
+
Imported compose/from CSS/SASS module:
+
+ CSS modules composes path resolving: this should be turquoise
+
+
+ CSS modules composes path resolving: this should be orangered
+
+
+ CSS modules composes path resolving: this should be blue
+
+
+
+
Inline CSS module:
+
+
+
CSS with @charset:
+
+
+
+ @import with layers:
+ blue
+ green
+
+
+
+ @import dependency w/ style entrypoints: this should be purple
+
+
+ @import dependency w/ sass entrypoints: this should be orange
+
+
+
+ @import dependency w/ style export mapping: this should be purple
+
+
+ @import dependency w/ sass export mapping: this should be orange
+
+
+
+ @import dependency that @import another dependency: this should be purple
+
+
+ @import dependency that has @import with a css extension pointing to another
+ dependency: this should be purple
+
+
+
PostCSS dir-dependency: this should be grey
+
+ PostCSS dir-dependency (file 2): this should be grey too
+
+
+ PostCSS dir-dependency (file 3): this should be grey too
+
+
+
+ import dependency includes 'import "./foo.css"': this should be green
+
+
+ import dependency includes 'import "./bar.module.css"': this should be green
+
+
+
+ URL separation preservation: should have valid background-image
+
+
+
Inlined import - this should NOT be red.
+
+
+ test import css in less, this color will be yellow
+
+
+ test for import less in less, this color will be blue
+
+
+
+ test import css in scss, this color will be orange
+
+
+
+
+
URL Support
+
+
Raw Support
+
+
+
PostCSS source.input.from. Should include query
+
+
+
Import from jsfile.css.js without the extension
+
+
+
Aliased
+
import '#alias': this should be blue
+
+
import '#alias-module': this should be blue
+
+
Imports field
+
import '#imports': this should be red
+
+
+@import scss: this should be red
+
diff --git a/playground/css/inline.module.css b/playground/css/inline.module.css
new file mode 100644
index 00000000000000..9566e21e2cd1af
--- /dev/null
+++ b/playground/css/inline.module.css
@@ -0,0 +1,3 @@
+.apply-color-inline {
+ color: turquoise;
+}
diff --git a/playground/css/inlined.css b/playground/css/inlined.css
new file mode 100644
index 00000000000000..1b08950784ee76
--- /dev/null
+++ b/playground/css/inlined.css
@@ -0,0 +1,4 @@
+.inlined {
+ color: green;
+ background: url('./ok.png');
+}
diff --git a/playground/css/jsfile.css.js b/playground/css/jsfile.css.js
new file mode 100644
index 00000000000000..025674a66f3b16
--- /dev/null
+++ b/playground/css/jsfile.css.js
@@ -0,0 +1,2 @@
+const message = 'from jsfile.css.js'
+export default message
diff --git a/playground/css/layered/blue.css b/playground/css/layered/blue.css
new file mode 100644
index 00000000000000..faa644dd73ce2d
--- /dev/null
+++ b/playground/css/layered/blue.css
@@ -0,0 +1,5 @@
+@media screen {
+ .layers-blue {
+ color: blue;
+ }
+}
diff --git a/playground/css/layered/green.css b/playground/css/layered/green.css
new file mode 100644
index 00000000000000..15a762b7572e0b
--- /dev/null
+++ b/playground/css/layered/green.css
@@ -0,0 +1,5 @@
+@media screen {
+ .layers-green {
+ color: green;
+ }
+}
diff --git a/playground/css/layered/index.css b/playground/css/layered/index.css
new file mode 100644
index 00000000000000..49756673b674d4
--- /dev/null
+++ b/playground/css/layered/index.css
@@ -0,0 +1,13 @@
+@layer base;
+
+@import './blue.css' layer;
+@import './green.css' layer;
+
+@layer base {
+ .layers-blue {
+ color: black;
+ }
+ .layers-green {
+ color: black;
+ }
+}
diff --git a/playground/css/less-plugin.less b/playground/css/less-plugin.less
new file mode 100644
index 00000000000000..0b256dec683f1c
--- /dev/null
+++ b/playground/css/less-plugin.less
@@ -0,0 +1,7 @@
+@plugin "less-plugin/test.js";
+
+.less-js-plugin {
+ height: 1em;
+ width: 1em;
+ background-image: test();
+}
diff --git a/playground/css/less-plugin/test.js b/playground/css/less-plugin/test.js
new file mode 100644
index 00000000000000..e261eaf335e4da
--- /dev/null
+++ b/playground/css/less-plugin/test.js
@@ -0,0 +1,5 @@
+functions.add('test', function test() {
+ const transparentPng =
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGD4/x8AAwIB/8myre4AAAAASUVORK5CYII='
+ return `url(${transparentPng})`
+})
diff --git a/playground/css/less.less b/playground/css/less.less
new file mode 100644
index 00000000000000..f5f6fa52b36740
--- /dev/null
+++ b/playground/css/less.less
@@ -0,0 +1,11 @@
+@import '=/nested/nested';
+@import './nested/css-in-less.less';
+
+// Test data-uri calls with relative images.
+@import './less/components/form.less';
+
+@color: blue;
+
+.less {
+ color: @color;
+}
diff --git a/playground/css/less/components/form.less b/playground/css/less/components/form.less
new file mode 100644
index 00000000000000..99cdc4d5d1d118
--- /dev/null
+++ b/playground/css/less/components/form.less
@@ -0,0 +1,6 @@
+@import url('../../less/ommer.less');
+
+.form-box-data-uri {
+ // data-uri() calls with relative paths should be replaced just like urls.
+ background-image: data-uri('../images/backgrounds/form-select.svg');
+}
diff --git a/playground/css/less/images/backgrounds/form-select.svg b/playground/css/less/images/backgrounds/form-select.svg
new file mode 100644
index 00000000000000..8aaf69c09e03f4
--- /dev/null
+++ b/playground/css/less/images/backgrounds/form-select.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/playground/css/less/ommer.less b/playground/css/less/ommer.less
new file mode 100644
index 00000000000000..a0ef4ce9dda922
--- /dev/null
+++ b/playground/css/less/ommer.less
@@ -0,0 +1,3 @@
+.less-at-import-url-ommer {
+ color: darkorange;
+}
diff --git a/playground/css/lightningcss-plugins.js b/playground/css/lightningcss-plugins.js
new file mode 100644
index 00000000000000..4aa462d1e6db5b
--- /dev/null
+++ b/playground/css/lightningcss-plugins.js
@@ -0,0 +1,210 @@
+import path from 'node:path'
+import { normalizePath } from 'vite'
+import { bundle as bundleWithLightningCss } from 'lightningcss'
+import { globSync } from 'tinyglobby'
+
+/**
+ * @param {string} filename
+ * @returns {import('lightningcss').StyleSheet}
+ *
+ * based on https://github.com/sardinedev/lightningcss-plugins/blob/9fb379486e402a4b4b8950d09e655b4cbf8a118b/packages/global-custom-queries/src/globalCustomQueries.ts#L9-L29
+ * https://github.com/sardinedev/lightningcss-plugins/blob/main/LICENSE
+ */
+function obtainLightningCssAst(filename) {
+ let ast
+ try {
+ bundleWithLightningCss({
+ filename,
+ visitor: {
+ StyleSheet(stylesheet) {
+ ast = stylesheet
+ },
+ },
+ })
+ return ast
+ } catch (error) {
+ throw Error(`failed to obtain lightning css AST`, { cause: error })
+ }
+}
+
+/** @returns {import('lightningcss').Visitor} */
+export function testDirDep() {
+ /** @type {string[]} */
+ let currentStyleSheetSources
+ return {
+ StyleSheet(stylesheet) {
+ currentStyleSheetSources = stylesheet.sources
+ },
+ Rule: {
+ unknown: {
+ test(rule) {
+ const location = rule.loc
+ const from = currentStyleSheetSources[location.source_index]
+ const pattern = normalizePath(
+ path.resolve(path.dirname(from), './glob-dep/**/*.css'),
+ )
+ // FIXME: there's no way to add a dependency
+ const files = globSync(pattern, {
+ expandDirectories: false,
+ absolute: true,
+ })
+ return files.flatMap((file) => obtainLightningCssAst(file).rules)
+ },
+ },
+ },
+ }
+}
+
+/** @returns {import('lightningcss').Visitor} */
+export function testSourceInput() {
+ /** @type {string[]} */
+ let currentStyleSheetSources
+ return {
+ StyleSheet(stylesheet) {
+ currentStyleSheetSources = stylesheet.sources
+ },
+ Rule: {
+ unknown: {
+ 'source-input': (rule) => {
+ const location = rule.loc
+ const from = currentStyleSheetSources[location.source_index]
+ return [
+ {
+ type: 'style',
+ value: {
+ // .source-input::before
+ selectors: [
+ [
+ { type: 'class', name: 'source-input' },
+ { type: 'pseudo-element', kind: 'before' },
+ ],
+ ],
+ // content: ${JSON.stringify(from)};
+ declarations: {
+ declarations: [
+ {
+ property: 'custom',
+ value:
+ /** @satisfies {import('lightningcss').CustomProperty} */ ({
+ name: 'content',
+ value: [
+ {
+ type: 'token',
+ value: { type: 'string', value: from },
+ },
+ ],
+ }),
+ },
+ ],
+ },
+ loc: rule.loc,
+ },
+ },
+ ]
+ },
+ },
+ },
+ }
+}
+
+/**
+ * really simplified implementation of https://github.com/postcss/postcss-nested
+ *
+ * @returns {import('lightningcss').Visitor}
+ */
+export function nestedLikePlugin() {
+ return {
+ Rule: {
+ style(rule) {
+ // NOTE: multiple selectors are not supported
+ if (rule.value.selectors.length > 1) {
+ return
+ }
+ const parentSelector = rule.value.selectors[0]
+
+ const nestedRules = rule.value.rules
+ /** @type {import('lightningcss').Rule[]} */
+ const additionalRules = []
+ if (nestedRules) {
+ const filteredNestedRules = []
+ for (const nestedRule of nestedRules) {
+ if (nestedRule.type === 'style') {
+ const selectors = nestedRule.value.selectors
+ // NOTE: multiple selectors are not supported
+ if (selectors.length === 1) {
+ const selector = selectors[0]
+ if (
+ selector.length >= 2 &&
+ selector[0].type === 'nesting' &&
+ selector[1].type === 'type'
+ ) {
+ const lastParentSelectorComponent =
+ parentSelector[parentSelector.length - 1]
+ if ('name' in lastParentSelectorComponent) {
+ const newSelector = [
+ ...parentSelector.slice(0, -1),
+ {
+ ...lastParentSelectorComponent,
+ name:
+ lastParentSelectorComponent.name + selector[1].name,
+ },
+ ]
+ additionalRules.push({
+ type: 'style',
+ value: {
+ selectors: [newSelector],
+ declarations: nestedRule.value.declarations,
+ loc: nestedRule.value.loc,
+ },
+ })
+ continue
+ }
+ }
+ }
+ }
+ filteredNestedRules.push(nestedRule)
+ }
+ rule.value.rules = filteredNestedRules
+ }
+ return [rule, ...additionalRules]
+ },
+ },
+ }
+}
+
+/** @returns {import('lightningcss').Visitor} */
+export function testInjectUrl() {
+ return {
+ Rule: {
+ unknown: {
+ 'inject-url': (rule) => {
+ return [
+ {
+ type: 'style',
+ value: {
+ selectors: [[{ type: 'class', name: 'inject-url' }]],
+ declarations: {
+ declarations: [
+ {
+ property: 'background-image',
+ value: [
+ {
+ type: 'url',
+ value: {
+ url: '=/ok.png',
+ loc: rule.loc,
+ },
+ },
+ ],
+ },
+ ],
+ },
+ loc: rule.loc,
+ },
+ },
+ ]
+ },
+ },
+ },
+ }
+}
diff --git a/playground/css/linked-at-import.css b/playground/css/linked-at-import.css
new file mode 100644
index 00000000000000..a778a3c754bb1b
--- /dev/null
+++ b/playground/css/linked-at-import.css
@@ -0,0 +1,3 @@
+.linked-at-import {
+ color: red;
+}
diff --git a/playground/css/linked.css b/playground/css/linked.css
new file mode 100644
index 00000000000000..55b11f672fc500
--- /dev/null
+++ b/playground/css/linked.css
@@ -0,0 +1,8 @@
+@import '=/linked-at-import.css';
+
+/* test postcss nesting */
+.wrapper {
+ .linked {
+ color: blue;
+ }
+}
diff --git a/playground/css/main.js b/playground/css/main.js
new file mode 100644
index 00000000000000..aab2b499ec01b6
--- /dev/null
+++ b/playground/css/main.js
@@ -0,0 +1,140 @@
+import './minify.css'
+import './imported.css'
+import './sugarss.sss'
+import './sass.scss'
+import './less.less'
+import './less-plugin.less'
+import './stylus.styl'
+import './manual-chunk.css'
+import './postcss-inject-url.css'
+
+import urlCss from './url-imported.css?url'
+appendLinkStylesheet(urlCss)
+
+import rawCss from './raw-imported.css?raw'
+text('.raw-imported-css', rawCss)
+
+import { cUsed, a as treeshakeScopedA } from './treeshake-scoped/index.js'
+document.querySelector('.scoped').classList.add(treeshakeScopedA(), cUsed())
+
+import mod from './mod.module.css'
+document.querySelector('.modules').classList.add(mod['apply-color'])
+text('.modules-code', JSON.stringify(mod, null, 2))
+
+import sassMod from './mod.module.scss'
+document.querySelector('.modules-sass').classList.add(sassMod['apply-color'])
+text('.modules-sass-code', JSON.stringify(sassMod, null, 2))
+
+import { a as treeshakeMod } from './treeshake-module/index.js'
+document
+ .querySelector('.modules-treeshake')
+ .classList.add(treeshakeMod()['treeshake-module-a'])
+
+import composesPathResolvingMod from './composes-path-resolving.module.css'
+document
+ .querySelector('.path-resolved-modules-css')
+ .classList.add(...composesPathResolvingMod['path-resolving-css'].split(' '))
+document
+ .querySelector('.path-resolved-modules-sass')
+ .classList.add(...composesPathResolvingMod['path-resolving-sass'].split(' '))
+document
+ .querySelector('.path-resolved-modules-less')
+ .classList.add(...composesPathResolvingMod['path-resolving-less'].split(' '))
+text(
+ '.path-resolved-modules-code',
+ JSON.stringify(composesPathResolvingMod, null, 2),
+)
+
+import inlineMod from './inline.module.css?inline'
+text('.modules-inline', inlineMod)
+
+import charset from './charset.css?inline'
+text('.charset-css', charset)
+
+import './layered/index.css'
+
+import './dep.css'
+import './glob-dep.css'
+
+// eslint-disable-next-line import-x/order
+import { barModuleClasses } from '@vitejs/test-css-js-dep'
+document
+ .querySelector('.css-js-dep-module')
+ .classList.add(barModuleClasses.cssJsDepModule)
+
+function text(el, text) {
+ document.querySelector(el).textContent = text
+}
+
+function appendLinkStylesheet(href) {
+ const link = document.createElement('link')
+ link.rel = 'stylesheet'
+ link.href = href
+ document.head.appendChild(link)
+}
+
+if (import.meta.hot) {
+ import.meta.hot.accept('./mod.module.css', (newMod) => {
+ const list = document.querySelector('.modules').classList
+ list.remove(mod.applyColor)
+ list.add(newMod.applyColor)
+ text('.modules-code', JSON.stringify(newMod.default, null, 2))
+ })
+
+ import.meta.hot.accept('./mod.module.scss', (newMod) => {
+ const list = document.querySelector('.modules-sass').classList
+ list.remove(mod.applyColor)
+ list.add(newMod.applyColor)
+ text('.modules-sass-code', JSON.stringify(newMod.default, null, 2))
+ })
+}
+
+// async
+import('./async')
+
+if (import.meta.env.DEV) {
+ import('./async-treeshaken')
+}
+
+// inlined
+import inlined from './inlined.css?inline'
+text('.inlined-code', inlined)
+
+// glob
+const glob = import.meta.glob('./glob-import/*.css', { query: '?inline' })
+Promise.all(
+ Object.keys(glob).map((key) => glob[key]().then((i) => i.default)),
+).then((res) => {
+ text('.imported-css-glob', JSON.stringify(res, null, 2))
+})
+
+// globEager
+const globEager = import.meta.glob('./glob-import/*.css', {
+ eager: true,
+ query: '?inline',
+})
+text('.imported-css-globEager', JSON.stringify(globEager, null, 2))
+
+import postcssSourceInput from './postcss-source-input.css?inline&query=foo'
+text('.postcss-source-input', postcssSourceInput)
+
+// The file is jsfile.css.js, and we should be able to import it without extension
+import jsFileMessage from './jsfile.css'
+text('.jsfile-css-js', jsFileMessage)
+
+import '#alias'
+import aliasContent from '#alias?inline'
+text('.aliased-content', aliasContent)
+import aliasModule from '#alias-module'
+document
+ .querySelector('.aliased-module')
+ .classList.add(aliasModule.aliasedModule)
+
+import './unsupported.css'
+
+import './async/index'
+
+import('./same-name/sub1/sub')
+import('./same-name/sub2/sub')
+
+import './imports-imports-field.css'
diff --git a/playground/css/manual-chunk.css b/playground/css/manual-chunk.css
new file mode 100644
index 00000000000000..dc41883115cc1d
--- /dev/null
+++ b/playground/css/manual-chunk.css
@@ -0,0 +1,3 @@
+.manual-chunk {
+ color: blue;
+}
diff --git a/playground/css/minify.css b/playground/css/minify.css
new file mode 100644
index 00000000000000..ada062407cdb38
--- /dev/null
+++ b/playground/css/minify.css
@@ -0,0 +1,3 @@
+.test-minify {
+ color: rgba(255, 255, 0, 0.7);
+}
diff --git a/playground/css/mod.module.css b/playground/css/mod.module.css
new file mode 100644
index 00000000000000..b2ae0e967dced1
--- /dev/null
+++ b/playground/css/mod.module.css
@@ -0,0 +1,3 @@
+.apply-color {
+ color: turquoise;
+}
diff --git a/packages/playground/css/mod.module.scss b/playground/css/mod.module.scss
similarity index 100%
rename from packages/playground/css/mod.module.scss
rename to playground/css/mod.module.scss
diff --git a/playground/css/nested/_index.scss b/playground/css/nested/_index.scss
new file mode 100644
index 00000000000000..c0767a3f4431c6
--- /dev/null
+++ b/playground/css/nested/_index.scss
@@ -0,0 +1,27 @@
+@use 'sass:string';
+@use '/nested/root-relative'; // root relative path
+@use '../weapp.wxss'; // test user's custom importer in a file loaded by vite's custom importer
+
+@import './css-in-scss.css';
+
+.sass-at-import {
+ color: olive;
+ background: url(./icon.png) 10px no-repeat;
+}
+
+.sass-at-import-alias {
+ color: olive;
+ background: url(=/nested/icon.png) 10px no-repeat;
+}
+
+$var: '/ok.png';
+.sass-url-starts-with-variable {
+ background: url($var);
+ background-position: center;
+}
+
+$var2: '/OK.PNG';
+.sass-url-starts-with-function-call {
+ background: url(string.to-lower-case($var2));
+ background-position: center;
+}
diff --git a/packages/playground/css/nested/_partial.scss b/playground/css/nested/_partial.scss
similarity index 100%
rename from packages/playground/css/nested/_partial.scss
rename to playground/css/nested/_partial.scss
diff --git a/packages/playground/css/nested/css-in-less-2.less b/playground/css/nested/css-in-less-2.less
similarity index 100%
rename from packages/playground/css/nested/css-in-less-2.less
rename to playground/css/nested/css-in-less-2.less
diff --git a/packages/playground/css/nested/css-in-less.css b/playground/css/nested/css-in-less.css
similarity index 100%
rename from packages/playground/css/nested/css-in-less.css
rename to playground/css/nested/css-in-less.css
diff --git a/packages/playground/css/nested/css-in-less.less b/playground/css/nested/css-in-less.less
similarity index 100%
rename from packages/playground/css/nested/css-in-less.less
rename to playground/css/nested/css-in-less.less
diff --git a/packages/playground/css/nested/css-in-scss.css b/playground/css/nested/css-in-scss.css
similarity index 100%
rename from packages/playground/css/nested/css-in-scss.css
rename to playground/css/nested/css-in-scss.css
diff --git a/packages/playground/vue/public/icon.png b/playground/css/nested/icon.png
similarity index 100%
rename from packages/playground/vue/public/icon.png
rename to playground/css/nested/icon.png
diff --git a/playground/css/nested/nested.less b/playground/css/nested/nested.less
new file mode 100644
index 00000000000000..25aa1944d32c14
--- /dev/null
+++ b/playground/css/nested/nested.less
@@ -0,0 +1,15 @@
+.less-at-import {
+ color: darkslateblue;
+ background: url(./icon.png) 10px no-repeat;
+}
+
+.less-at-import-alias {
+ color: darkslateblue;
+ background: url(=/nested/icon.png) 10px no-repeat;
+}
+
+@var: '/ok.png';
+.less-url-starts-with-variable {
+ background: url('@{var}');
+ background-position: center;
+}
diff --git a/playground/css/nested/nested.sss b/playground/css/nested/nested.sss
new file mode 100644
index 00000000000000..9dc685cb3e50c3
--- /dev/null
+++ b/playground/css/nested/nested.sss
@@ -0,0 +1,8 @@
+.sugarss-at-import
+ color: darkslateblue
+ background: url(./icon.png) 10px no-repeat
+
+
+.sugarss-at-import-alias
+ color: darkslateblue
+ background: url(=/nested/icon.png) 10px no-repeat
diff --git a/playground/css/nested/nested.styl b/playground/css/nested/nested.styl
new file mode 100644
index 00000000000000..8a371948538de0
--- /dev/null
+++ b/playground/css/nested/nested.styl
@@ -0,0 +1,6 @@
+.stylus-import
+ color darkslateblue
+
+.stylus-import-alias
+ color darkslateblue
+ background url('=/nested/icon.png') 10px no-repeat
diff --git a/playground/css/nested/root-relative.scss b/playground/css/nested/root-relative.scss
new file mode 100644
index 00000000000000..775dca855743b3
--- /dev/null
+++ b/playground/css/nested/root-relative.scss
@@ -0,0 +1,3 @@
+.sass-root-relative {
+ color: orange;
+}
diff --git a/playground/css/ok.png b/playground/css/ok.png
new file mode 100644
index 00000000000000..a8d1e52510c41c
Binary files /dev/null and b/playground/css/ok.png differ
diff --git a/packages/playground/css/options/absolute-import.styl b/playground/css/options/absolute-import.styl
similarity index 100%
rename from packages/playground/css/options/absolute-import.styl
rename to playground/css/options/absolute-import.styl
diff --git a/packages/playground/css/options/relative-import.styl b/playground/css/options/relative-import.styl
similarity index 100%
rename from packages/playground/css/options/relative-import.styl
rename to playground/css/options/relative-import.styl
diff --git a/playground/css/package.json b/playground/css/package.json
new file mode 100644
index 00000000000000..1b323ed2b04c90
--- /dev/null
+++ b/playground/css/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@vitejs/test-css",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview",
+ "dev:relative-base": "vite --config ./vite.config-relative-base.js dev",
+ "build:relative-base": "vite --config ./vite.config-relative-base.js build",
+ "preview:relative-base": "vite --config ./vite.config-relative-base.js preview",
+ "dev:no-css-minify": "vite --config ./vite.config-no-css-minify.js dev",
+ "build:no-css-minify": "vite --config ./vite.config-no-css-minify.js build",
+ "preview:no-css-minify": "vite --config ./vite.config-no-css-minify.js preview"
+ },
+ "devDependencies": {
+ "@vitejs/test-css-dep": "link:./css-dep",
+ "@vitejs/test-css-dep-exports": "link:./css-dep-exports",
+ "@vitejs/test-css-js-dep": "file:./css-js-dep",
+ "@vitejs/test-css-proxy-dep": "file:./css-proxy-dep",
+ "@vitejs/test-scss-proxy-dep": "file:./scss-proxy-dep",
+ "less": "^4.3.0",
+ "lightningcss": "^1.30.0",
+ "postcss-nested": "^7.0.2",
+ "sass": "^1.88.0",
+ "stylus": "^0.64.0",
+ "sugarss": "^5.0.0",
+ "tinyglobby": "^0.2.13"
+ },
+ "imports": {
+ "#imports": "./imports-field.css"
+ }
+}
diff --git a/packages/playground/css/pkg-dep/_index.scss b/playground/css/pkg-dep/_index.scss
similarity index 100%
rename from packages/playground/css/pkg-dep/_index.scss
rename to playground/css/pkg-dep/_index.scss
diff --git a/playground/css/pkg-dep/index.js b/playground/css/pkg-dep/index.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/packages/playground/css/pkg-dep/package.json b/playground/css/pkg-dep/package.json
similarity index 100%
rename from packages/playground/css/pkg-dep/package.json
rename to playground/css/pkg-dep/package.json
diff --git a/packages/playground/css/postcss-caching/blue-app/imported.css b/playground/css/postcss-caching/blue-app/imported.css
similarity index 100%
rename from packages/playground/css/postcss-caching/blue-app/imported.css
rename to playground/css/postcss-caching/blue-app/imported.css
diff --git a/packages/playground/css/postcss-caching/blue-app/index.html b/playground/css/postcss-caching/blue-app/index.html
similarity index 100%
rename from packages/playground/css/postcss-caching/blue-app/index.html
rename to playground/css/postcss-caching/blue-app/index.html
diff --git a/playground/css/postcss-caching/blue-app/main.js b/playground/css/postcss-caching/blue-app/main.js
new file mode 100644
index 00000000000000..8556576f10e5f3
--- /dev/null
+++ b/playground/css/postcss-caching/blue-app/main.js
@@ -0,0 +1,7 @@
+import './imported.css'
+import css from './imported.css?inline'
+text('.imported-css', css)
+
+function text(el, text) {
+ document.querySelector(el).textContent = text
+}
diff --git a/playground/css/postcss-caching/blue-app/package.json b/playground/css/postcss-caching/blue-app/package.json
new file mode 100644
index 00000000000000..528263c4e60923
--- /dev/null
+++ b/playground/css/postcss-caching/blue-app/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "blue-app",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/css/postcss-caching/blue-app/postcss.config.js b/playground/css/postcss-caching/blue-app/postcss.config.js
new file mode 100644
index 00000000000000..679b801013ef1a
--- /dev/null
+++ b/playground/css/postcss-caching/blue-app/postcss.config.js
@@ -0,0 +1,15 @@
+export default {
+ plugins: [replacePinkWithBlue],
+}
+
+function replacePinkWithBlue() {
+ return {
+ postcssPlugin: 'replace-pink-with-blue',
+ Declaration(decl) {
+ if (decl.value === 'pink') {
+ decl.value = 'blue'
+ }
+ },
+ }
+}
+replacePinkWithBlue.postcss = true
diff --git a/playground/css/postcss-caching/css.spec.ts b/playground/css/postcss-caching/css.spec.ts
new file mode 100644
index 00000000000000..842d2726590d31
--- /dev/null
+++ b/playground/css/postcss-caching/css.spec.ts
@@ -0,0 +1,65 @@
+import path from 'node:path'
+import { createServer } from 'vite'
+import { expect, test } from 'vitest'
+import { getColor, isServe, page, ports } from '~utils'
+
+test.runIf(isServe)('postcss config', async () => {
+ const port = ports['css/postcss-caching']
+ const startServer = async (root) => {
+ const server = await createServer({
+ root,
+ logLevel: 'silent',
+ server: {
+ port,
+ strictPort: true,
+ },
+ build: {
+ // skip transpilation during tests to make it faster
+ target: 'esnext',
+ },
+ })
+ await server.listen()
+ return server
+ }
+
+ const blueAppDir = path.join(__dirname, 'blue-app')
+ const greenAppDir = path.join(__dirname, 'green-app')
+ let blueApp
+ let greenApp
+ try {
+ const hmrConnectionPromise = page.waitForEvent('console', (msg) =>
+ msg.text().includes('connected'),
+ )
+
+ blueApp = await startServer(blueAppDir)
+
+ await page.goto(`http://localhost:${port}`, { waitUntil: 'load' })
+ const blueA = await page.$('.postcss-a')
+ expect(await getColor(blueA)).toBe('blue')
+ const blueB = await page.$('.postcss-b')
+ expect(await getColor(blueB)).toBe('black')
+
+ // wait for hmr connection because: if server stops before connection, auto reload does not happen
+ await hmrConnectionPromise
+ await blueApp.close()
+ blueApp = null
+
+ const loadPromise = page.waitForEvent('load') // wait for server restart auto reload
+ greenApp = await startServer(greenAppDir)
+ await loadPromise
+
+ const greenA = await page.$('.postcss-a')
+ expect(await getColor(greenA)).toBe('black')
+ const greenB = await page.$('.postcss-b')
+ expect(await getColor(greenB)).toBe('green')
+ await greenApp.close()
+ greenApp = null
+ } finally {
+ if (blueApp) {
+ await blueApp.close()
+ }
+ if (greenApp) {
+ await greenApp.close()
+ }
+ }
+})
diff --git a/packages/playground/css/postcss-caching/green-app/imported.css b/playground/css/postcss-caching/green-app/imported.css
similarity index 100%
rename from packages/playground/css/postcss-caching/green-app/imported.css
rename to playground/css/postcss-caching/green-app/imported.css
diff --git a/packages/playground/css/postcss-caching/green-app/index.html b/playground/css/postcss-caching/green-app/index.html
similarity index 100%
rename from packages/playground/css/postcss-caching/green-app/index.html
rename to playground/css/postcss-caching/green-app/index.html
diff --git a/playground/css/postcss-caching/green-app/main.js b/playground/css/postcss-caching/green-app/main.js
new file mode 100644
index 00000000000000..8556576f10e5f3
--- /dev/null
+++ b/playground/css/postcss-caching/green-app/main.js
@@ -0,0 +1,7 @@
+import './imported.css'
+import css from './imported.css?inline'
+text('.imported-css', css)
+
+function text(el, text) {
+ document.querySelector(el).textContent = text
+}
diff --git a/playground/css/postcss-caching/green-app/package.json b/playground/css/postcss-caching/green-app/package.json
new file mode 100644
index 00000000000000..110ea74558435d
--- /dev/null
+++ b/playground/css/postcss-caching/green-app/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "green-app",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/css/postcss-caching/green-app/postcss.config.js b/playground/css/postcss-caching/green-app/postcss.config.js
new file mode 100644
index 00000000000000..c0a74e3676976d
--- /dev/null
+++ b/playground/css/postcss-caching/green-app/postcss.config.js
@@ -0,0 +1,15 @@
+export default {
+ plugins: [replacePinkWithGreen],
+}
+
+function replacePinkWithGreen() {
+ return {
+ postcssPlugin: 'replace-pink-with-green',
+ Declaration(decl) {
+ if (decl.value === 'pink') {
+ decl.value = 'green'
+ }
+ },
+ }
+}
+replacePinkWithGreen.postcss = true
diff --git a/playground/css/postcss-caching/serve.ts b/playground/css/postcss-caching/serve.ts
new file mode 100644
index 00000000000000..195bb47d8520c7
--- /dev/null
+++ b/playground/css/postcss-caching/serve.ts
@@ -0,0 +1,10 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+// The server is started in the test, so we need to have a custom serve
+// function or a default server will be created
+export async function serve(): Promise<{ close(): Promise }> {
+ return {
+ close: () => Promise.resolve(),
+ }
+}
diff --git a/playground/css/postcss-inject-url.css b/playground/css/postcss-inject-url.css
new file mode 100644
index 00000000000000..766ccc8838bf3d
--- /dev/null
+++ b/playground/css/postcss-inject-url.css
@@ -0,0 +1 @@
+@inject-url;
diff --git a/playground/css/postcss-source-input.css b/playground/css/postcss-source-input.css
new file mode 100644
index 00000000000000..c6c3cb0c16dece
--- /dev/null
+++ b/playground/css/postcss-source-input.css
@@ -0,0 +1 @@
+@source-input;
diff --git a/playground/css/postcss.config.js b/playground/css/postcss.config.js
new file mode 100644
index 00000000000000..0953807b15789a
--- /dev/null
+++ b/playground/css/postcss.config.js
@@ -0,0 +1,85 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { globSync } from 'tinyglobby'
+import { normalizePath } from 'vite'
+import postcssNested from 'postcss-nested'
+
+export default {
+ plugins: [postcssNested, testDirDep, testSourceInput, testInjectUrl],
+}
+
+/**
+ * A plugin for testing the `dir-dependency` message handling.
+ */
+function testDirDep() {
+ return {
+ postcssPlugin: 'dir-dep',
+ AtRule(atRule, { result, Comment }) {
+ if (atRule.name === 'test') {
+ const pattern = normalizePath(
+ path.resolve(path.dirname(result.opts.from), './glob-dep/**/*.css'),
+ )
+ const files = globSync(pattern, { expandDirectories: false })
+ const text = files.map((f) => fs.readFileSync(f, 'utf-8')).join('\n')
+ atRule.parent.insertAfter(atRule, text)
+ atRule.remove()
+
+ result.messages.push({
+ type: 'dir-dependency',
+ plugin: 'dir-dep',
+ dir: './glob-dep',
+ glob: '*.css',
+ parent: result.opts.from,
+ })
+
+ result.messages.push({
+ type: 'dir-dependency',
+ plugin: 'dir-dep',
+ dir: './glob-dep/nested (dir)', // includes special characters in glob
+ glob: '*.css',
+ parent: result.opts.from,
+ })
+ }
+ },
+ }
+}
+testDirDep.postcss = true
+
+function testSourceInput() {
+ return {
+ postcssPlugin: 'source-input',
+ AtRule(atRule) {
+ if (atRule.name === 'source-input') {
+ atRule.after(
+ `.source-input::before { content: ${JSON.stringify(
+ atRule.source.input.from,
+ )}; }`,
+ )
+ atRule.remove()
+ }
+ },
+ }
+}
+testSourceInput.postcss = true
+
+function testInjectUrl() {
+ return {
+ postcssPlugin: 'inject-url',
+ Once(root, { Rule }) {
+ root.walkAtRules('inject-url', (atRule) => {
+ const rule = new Rule({
+ selector: '.inject-url',
+ source: atRule.source,
+ })
+ rule.append({
+ prop: 'background',
+ value: "url('=/ok.png')",
+ source: atRule.source,
+ })
+ atRule.after(rule)
+ atRule.remove()
+ })
+ },
+ }
+}
+testInjectUrl.postcss = true
diff --git a/playground/css/raw-imported.css b/playground/css/raw-imported.css
new file mode 100644
index 00000000000000..ee681e650b0b47
--- /dev/null
+++ b/playground/css/raw-imported.css
@@ -0,0 +1,6 @@
+.raw {
+ /* should not be transformed by postcss */
+ &-imported {
+ color: yellow;
+ }
+}
diff --git a/playground/css/same-name/sub1/sub.css b/playground/css/same-name/sub1/sub.css
new file mode 100644
index 00000000000000..8eca4c3b9a7b6d
--- /dev/null
+++ b/playground/css/same-name/sub1/sub.css
@@ -0,0 +1,3 @@
+.sub1-sub {
+ color: red;
+}
diff --git a/playground/css/same-name/sub1/sub.js b/playground/css/same-name/sub1/sub.js
new file mode 100644
index 00000000000000..abe787e8e3c05d
--- /dev/null
+++ b/playground/css/same-name/sub1/sub.js
@@ -0,0 +1,3 @@
+import './sub.css'
+
+export default 'sub1-name'
diff --git a/playground/css/same-name/sub2/sub.css b/playground/css/same-name/sub2/sub.css
new file mode 100644
index 00000000000000..910bf3898e5bfb
--- /dev/null
+++ b/playground/css/same-name/sub2/sub.css
@@ -0,0 +1,3 @@
+.sub2-sub {
+ color: blue;
+}
diff --git a/playground/css/same-name/sub2/sub.js b/playground/css/same-name/sub2/sub.js
new file mode 100644
index 00000000000000..3d338a64d0649f
--- /dev/null
+++ b/playground/css/same-name/sub2/sub.js
@@ -0,0 +1,3 @@
+import './sub.css'
+
+export default 'sub2-name'
diff --git a/playground/css/sass-modern-compiler-build/entry1.scss b/playground/css/sass-modern-compiler-build/entry1.scss
new file mode 100644
index 00000000000000..e21334eb8337bc
--- /dev/null
+++ b/playground/css/sass-modern-compiler-build/entry1.scss
@@ -0,0 +1,3 @@
+.entry1 {
+ color: red;
+}
diff --git a/playground/css/sass-modern-compiler-build/entry2.scss b/playground/css/sass-modern-compiler-build/entry2.scss
new file mode 100644
index 00000000000000..eca3004c9d247f
--- /dev/null
+++ b/playground/css/sass-modern-compiler-build/entry2.scss
@@ -0,0 +1,3 @@
+.entry2 {
+ color: blue;
+}
diff --git a/playground/css/sass.scss b/playground/css/sass.scss
new file mode 100644
index 00000000000000..8d4bc5492e6299
--- /dev/null
+++ b/playground/css/sass.scss
@@ -0,0 +1,15 @@
+@use '=/nested'; // alias + custom index resolving -> /nested/_index.scss
+@use '=/nested/partial'; // sass convention: omitting leading _ for partials
+@use '@vitejs/test-css-dep'; // package w/ sass entry points
+@use '@vitejs/test-css-dep-exports'; // package with a sass export mapping
+@use '@vitejs/test-scss-proxy-dep'; // package with a sass proxy import
+@use 'virtual-dep'; // virtual file added through importer
+@use '=/pkg-dep'; // package w/out sass field
+@use '=/weapp.wxss'; // wxss file
+@use 'virtual-file-absolute';
+@use '=/scss-dir/main.scss'; // "./dir" reference from vite custom importer
+
+.sass {
+ /* injected via vite.config.js */
+ color: $injectedColor;
+}
diff --git a/playground/css/scss-dir/dir/index.scss b/playground/css/scss-dir/dir/index.scss
new file mode 100644
index 00000000000000..e6bcdc2166b8ab
--- /dev/null
+++ b/playground/css/scss-dir/dir/index.scss
@@ -0,0 +1,3 @@
+.sass-dir-index {
+ color: orange;
+}
diff --git a/playground/css/scss-dir/main.scss b/playground/css/scss-dir/main.scss
new file mode 100644
index 00000000000000..b661030297a451
--- /dev/null
+++ b/playground/css/scss-dir/main.scss
@@ -0,0 +1 @@
+@use './dir';
diff --git a/playground/css/scss-proxy-dep-nested/index.css b/playground/css/scss-proxy-dep-nested/index.css
new file mode 100644
index 00000000000000..4c7e3b16e62597
--- /dev/null
+++ b/playground/css/scss-proxy-dep-nested/index.css
@@ -0,0 +1,3 @@
+.scss-proxy-dep {
+ color: purple;
+}
diff --git a/playground/css/scss-proxy-dep-nested/package.json b/playground/css/scss-proxy-dep-nested/package.json
new file mode 100644
index 00000000000000..4f7a99bb559207
--- /dev/null
+++ b/playground/css/scss-proxy-dep-nested/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vitejs/test-scss-proxy-dep-nested",
+ "private": true,
+ "version": "1.0.0"
+}
diff --git a/playground/css/scss-proxy-dep/index.scss b/playground/css/scss-proxy-dep/index.scss
new file mode 100644
index 00000000000000..540353efe1030a
--- /dev/null
+++ b/playground/css/scss-proxy-dep/index.scss
@@ -0,0 +1 @@
+@use '@vitejs/test-scss-proxy-dep-nested/index.css';
diff --git a/playground/css/scss-proxy-dep/package.json b/playground/css/scss-proxy-dep/package.json
new file mode 100644
index 00000000000000..a2749e7ae3e957
--- /dev/null
+++ b/playground/css/scss-proxy-dep/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-scss-proxy-dep",
+ "private": true,
+ "version": "1.0.0",
+ "sass": "index.scss",
+ "dependencies": {
+ "@vitejs/test-scss-proxy-dep-nested": "file:../scss-proxy-dep-nested"
+ }
+}
diff --git a/playground/css/stylus.styl b/playground/css/stylus.styl
new file mode 100644
index 00000000000000..a4e61e6c06e2ec
--- /dev/null
+++ b/playground/css/stylus.styl
@@ -0,0 +1,18 @@
+@import './nested/nested'
+
+$color ?= blue
+
+.stylus
+ color $color
+
+.stylus-additional-data
+ /* injected via vite.config.js */
+ color $injectedColor
+
+.stylus-options-define-var
+ /* defined in vite.config.js */
+ color $definedColor
+
+.stylus-options-define-func
+ /* defined in vite.config.js */
+ color definedFunction()
diff --git a/playground/css/sugarss.sss b/playground/css/sugarss.sss
new file mode 100644
index 00000000000000..80cfc3861b9417
--- /dev/null
+++ b/playground/css/sugarss.sss
@@ -0,0 +1,4 @@
+@import '=/nested/nested.sss'
+
+.sugarss
+ color: blue
diff --git a/playground/css/treeshake-module/a.js b/playground/css/treeshake-module/a.js
new file mode 100644
index 00000000000000..7272fa1dc1d9c1
--- /dev/null
+++ b/playground/css/treeshake-module/a.js
@@ -0,0 +1,5 @@
+import style from './a.module.css'
+
+export function a() {
+ return style
+}
diff --git a/playground/css/treeshake-module/a.module.css b/playground/css/treeshake-module/a.module.css
new file mode 100644
index 00000000000000..72ab1a9fdb001a
--- /dev/null
+++ b/playground/css/treeshake-module/a.module.css
@@ -0,0 +1,3 @@
+.treeshake-module-a {
+ color: red;
+}
diff --git a/playground/css/treeshake-module/b.js b/playground/css/treeshake-module/b.js
new file mode 100644
index 00000000000000..b3db996f7f64cd
--- /dev/null
+++ b/playground/css/treeshake-module/b.js
@@ -0,0 +1,5 @@
+import style from './b.module.css'
+
+export function b() {
+ return style
+}
diff --git a/playground/css/treeshake-module/b.module.css b/playground/css/treeshake-module/b.module.css
new file mode 100644
index 00000000000000..5ad402ef7353e8
--- /dev/null
+++ b/playground/css/treeshake-module/b.module.css
@@ -0,0 +1,3 @@
+.treeshake-module-b {
+ color: red;
+}
diff --git a/playground/css/treeshake-module/index.js b/playground/css/treeshake-module/index.js
new file mode 100644
index 00000000000000..67332c5a21eb3d
--- /dev/null
+++ b/playground/css/treeshake-module/index.js
@@ -0,0 +1,2 @@
+export { a } from './a.js'
+export { b } from './b.js'
diff --git a/playground/css/treeshake-scoped/a-scoped.css b/playground/css/treeshake-scoped/a-scoped.css
new file mode 100644
index 00000000000000..e18cbb887f4637
--- /dev/null
+++ b/playground/css/treeshake-scoped/a-scoped.css
@@ -0,0 +1,3 @@
+.treeshake-scoped-a {
+ color: red;
+}
diff --git a/playground/css/treeshake-scoped/a.js b/playground/css/treeshake-scoped/a.js
new file mode 100644
index 00000000000000..819b7d3cf84e1d
--- /dev/null
+++ b/playground/css/treeshake-scoped/a.js
@@ -0,0 +1,5 @@
+import './a-scoped.css' // should be treeshaken away if `a` is not used
+
+export default function a() {
+ return 'treeshake-scoped-a'
+}
diff --git a/playground/css/treeshake-scoped/b-scoped.css b/playground/css/treeshake-scoped/b-scoped.css
new file mode 100644
index 00000000000000..9792a332519a81
--- /dev/null
+++ b/playground/css/treeshake-scoped/b-scoped.css
@@ -0,0 +1,3 @@
+.treeshake-scoped-b {
+ color: red;
+}
diff --git a/playground/css/treeshake-scoped/b.js b/playground/css/treeshake-scoped/b.js
new file mode 100644
index 00000000000000..798ec76741c429
--- /dev/null
+++ b/playground/css/treeshake-scoped/b.js
@@ -0,0 +1,5 @@
+import './b-scoped.css' // should be treeshaken away if `b` is not used
+
+export default function b() {
+ return 'treeshake-scoped-b'
+}
diff --git a/playground/css/treeshake-scoped/c-scoped.css b/playground/css/treeshake-scoped/c-scoped.css
new file mode 100644
index 00000000000000..8901f7303dc9d6
--- /dev/null
+++ b/playground/css/treeshake-scoped/c-scoped.css
@@ -0,0 +1,3 @@
+.treeshake-scoped-c {
+ color: red;
+}
diff --git a/playground/css/treeshake-scoped/c.js b/playground/css/treeshake-scoped/c.js
new file mode 100644
index 00000000000000..8a7e2fb89dbaa2
--- /dev/null
+++ b/playground/css/treeshake-scoped/c.js
@@ -0,0 +1,10 @@
+import './c-scoped.css' // should be treeshaken away if `b` is not used
+
+export default function c() {
+ return 'treeshake-scoped-c'
+}
+
+export function cUsed() {
+ // used but does not depend on scoped css
+ return 'c-used'
+}
diff --git a/playground/css/treeshake-scoped/d-scoped.css b/playground/css/treeshake-scoped/d-scoped.css
new file mode 100644
index 00000000000000..83c0b0ed176271
--- /dev/null
+++ b/playground/css/treeshake-scoped/d-scoped.css
@@ -0,0 +1,3 @@
+.treeshake-scoped-d {
+ color: red;
+}
diff --git a/playground/css/treeshake-scoped/d.js b/playground/css/treeshake-scoped/d.js
new file mode 100644
index 00000000000000..7581688476cf56
--- /dev/null
+++ b/playground/css/treeshake-scoped/d.js
@@ -0,0 +1,5 @@
+import './d-scoped.css' // should be treeshaken away if `d` is not used
+
+export default function d() {
+ return 'treeshake-scoped-d'
+}
diff --git a/playground/css/treeshake-scoped/index.html b/playground/css/treeshake-scoped/index.html
new file mode 100644
index 00000000000000..d5e17c9a6bd772
--- /dev/null
+++ b/playground/css/treeshake-scoped/index.html
@@ -0,0 +1,12 @@
+treeshake-scoped
+Imported scoped CSS
+
+ scoped CSS order (this should be red text with blue background)
+
+
+
diff --git a/playground/css/treeshake-scoped/index.js b/playground/css/treeshake-scoped/index.js
new file mode 100644
index 00000000000000..93bea696056968
--- /dev/null
+++ b/playground/css/treeshake-scoped/index.js
@@ -0,0 +1,4 @@
+export { default as a } from './a.js'
+export { default as b } from './b.js'
+export { default as c, cUsed } from './c.js'
+export { default as d } from './d.js'
diff --git a/playground/css/treeshake-scoped/order/a-scoped.css b/playground/css/treeshake-scoped/order/a-scoped.css
new file mode 100644
index 00000000000000..64b3725097079a
--- /dev/null
+++ b/playground/css/treeshake-scoped/order/a-scoped.css
@@ -0,0 +1,4 @@
+.treeshake-scoped-order {
+ color: red;
+ background: red;
+}
diff --git a/playground/css/treeshake-scoped/order/a.js b/playground/css/treeshake-scoped/order/a.js
new file mode 100644
index 00000000000000..2dfefad9c14b18
--- /dev/null
+++ b/playground/css/treeshake-scoped/order/a.js
@@ -0,0 +1,7 @@
+import './before.css'
+import './a-scoped.css'
+import './after.css'
+
+export default function a() {
+ return 'treeshake-scoped-order-a'
+}
diff --git a/playground/css/treeshake-scoped/order/after.css b/playground/css/treeshake-scoped/order/after.css
new file mode 100644
index 00000000000000..af41d370d9c45f
--- /dev/null
+++ b/playground/css/treeshake-scoped/order/after.css
@@ -0,0 +1,4 @@
+.treeshake-scoped-order {
+ color: red;
+ background: blue;
+}
diff --git a/playground/css/treeshake-scoped/order/before.css b/playground/css/treeshake-scoped/order/before.css
new file mode 100644
index 00000000000000..d5e6bdb1ee3d36
--- /dev/null
+++ b/playground/css/treeshake-scoped/order/before.css
@@ -0,0 +1,3 @@
+.treeshake-scoped-order {
+ color: blue;
+}
diff --git a/playground/css/unsupported.css b/playground/css/unsupported.css
new file mode 100644
index 00000000000000..c17818a3ab33d7
--- /dev/null
+++ b/playground/css/unsupported.css
@@ -0,0 +1,3 @@
+.unsupported {
+ overflow-x: hidden;
+}
diff --git a/playground/css/url-imported.css b/playground/css/url-imported.css
new file mode 100644
index 00000000000000..95fec50ab2c554
--- /dev/null
+++ b/playground/css/url-imported.css
@@ -0,0 +1,6 @@
+.url {
+ /* should be transformed by postcss */
+ &-imported-css {
+ color: yellow;
+ }
+}
diff --git a/playground/css/vite.config-lightningcss.js b/playground/css/vite.config-lightningcss.js
new file mode 100644
index 00000000000000..8d87c785a0a154
--- /dev/null
+++ b/playground/css/vite.config-lightningcss.js
@@ -0,0 +1,29 @@
+import { defineConfig } from 'vite'
+import { composeVisitors } from 'lightningcss'
+import baseConfig from './vite.config.js'
+import {
+ nestedLikePlugin,
+ testDirDep,
+ testInjectUrl,
+ testSourceInput,
+} from './lightningcss-plugins'
+
+export default defineConfig({
+ ...baseConfig,
+ css: {
+ ...baseConfig.css,
+ transformer: 'lightningcss',
+ lightningcss: {
+ cssModules: {
+ pattern: '[name]__[local]___[hash]',
+ },
+ visitor: composeVisitors([
+ nestedLikePlugin(),
+ testDirDep(),
+ testSourceInput(),
+ testInjectUrl(),
+ ]),
+ },
+ },
+ cacheDir: 'node_modules/.vite-no-css-minify',
+})
diff --git a/playground/css/vite.config-no-css-minify.js b/playground/css/vite.config-no-css-minify.js
new file mode 100644
index 00000000000000..dce4815a712b64
--- /dev/null
+++ b/playground/css/vite.config-no-css-minify.js
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite'
+import baseConfig from './vite.config.js'
+
+export default defineConfig({
+ ...baseConfig,
+ build: {
+ ...baseConfig.build,
+ outDir: 'dist/no-css-minify',
+ minify: true,
+ cssMinify: false,
+ },
+ cacheDir: 'node_modules/.vite-no-css-minify',
+})
diff --git a/playground/css/vite.config-relative-base.js b/playground/css/vite.config-relative-base.js
new file mode 100644
index 00000000000000..451ca7090d023a
--- /dev/null
+++ b/playground/css/vite.config-relative-base.js
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite'
+import baseConfig from './vite.config.js'
+
+export default defineConfig(({ isPreview }) => ({
+ ...baseConfig,
+ base: !isPreview ? './' : '/relative-base/', // relative base to make dist portable
+ build: {
+ ...baseConfig.build,
+ outDir: 'dist/relative-base',
+ watch: null,
+ minify: false,
+ assetsInlineLimit: 0,
+ rollupOptions: {
+ output: {
+ entryFileNames: 'entries/[name].js',
+ chunkFileNames: 'chunks/[name]-[hash].js',
+ assetFileNames: 'other-assets/[name]-[hash][extname]',
+ },
+ },
+ },
+ cacheDir: 'node_modules/.vite-relative-base',
+}))
diff --git a/playground/css/vite.config-same-file-name.js b/playground/css/vite.config-same-file-name.js
new file mode 100644
index 00000000000000..f8e88d23b973be
--- /dev/null
+++ b/playground/css/vite.config-same-file-name.js
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import baseConfig from './vite.config.js'
+
+export default defineConfig({
+ ...baseConfig,
+ build: {
+ ...baseConfig.build,
+ outDir: 'dist/same-file-name',
+ rollupOptions: {
+ output: {
+ entryFileNames: '[name].js',
+ chunkFileNames: '[name].[hash].js',
+ assetFileNames: '[name].[ext]',
+ },
+ },
+ },
+})
diff --git a/playground/css/vite.config-sass-modern-compiler-build.js b/playground/css/vite.config-sass-modern-compiler-build.js
new file mode 100644
index 00000000000000..b44ef1e354d4d9
--- /dev/null
+++ b/playground/css/vite.config-sass-modern-compiler-build.js
@@ -0,0 +1,27 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ outDir: 'dist/sass-modern-compiler-build',
+ rollupOptions: {
+ input: {
+ entry1: path.join(
+ import.meta.dirname,
+ 'sass-modern-compiler-build/entry1.scss',
+ ),
+ entry2: path.join(
+ import.meta.dirname,
+ 'sass-modern-compiler-build/entry2.scss',
+ ),
+ },
+ },
+ },
+ css: {
+ preprocessorOptions: {
+ scss: {
+ api: 'modern-compiler',
+ },
+ },
+ },
+})
diff --git a/playground/css/vite.config-sass-modern.js b/playground/css/vite.config-sass-modern.js
new file mode 100644
index 00000000000000..90855ac270b7d8
--- /dev/null
+++ b/playground/css/vite.config-sass-modern.js
@@ -0,0 +1,18 @@
+import { defineConfig } from 'vite'
+import baseConfig from './vite.config.js'
+
+export default defineConfig({
+ ...baseConfig,
+ css: {
+ ...baseConfig.css,
+ preprocessorOptions: {
+ ...baseConfig.css.preprocessorOptions,
+ scss: {
+ .../** @type {import('vite').SassPreprocessorOptions & { api?: undefined }} */ (
+ baseConfig.css.preprocessorOptions.scss
+ ),
+ api: 'modern',
+ },
+ },
+ },
+})
diff --git a/playground/css/vite.config.js b/playground/css/vite.config.js
new file mode 100644
index 00000000000000..8adb5163e365b2
--- /dev/null
+++ b/playground/css/vite.config.js
@@ -0,0 +1,141 @@
+import path from 'node:path'
+import { pathToFileURL } from 'node:url'
+import stylus from 'stylus'
+import { defineConfig } from 'vite'
+
+// trigger scss bug: https://github.com/sass/dart-sass/issues/710
+// make sure Vite handles safely
+// @ts-expect-error refer to https://github.com/vitejs/vite/pull/11079
+globalThis.window = {}
+// @ts-expect-error refer to https://github.com/vitejs/vite/pull/11079
+globalThis.location = new URL('http://localhost/')
+
+export default defineConfig({
+ plugins: [
+ {
+ // Emulate a UI framework component where a framework module would import
+ // scoped CSS files that should treeshake if the default export is not used.
+ name: 'treeshake-scoped-css',
+ enforce: 'pre',
+ async resolveId(id, importer) {
+ if (!importer || !id.endsWith('-scoped.css')) return
+
+ const resolved = await this.resolve(id, importer)
+ if (!resolved) return
+
+ return {
+ ...resolved,
+ meta: {
+ vite: {
+ cssScopeTo: [
+ importer,
+ resolved.id.includes('barrel') ? undefined : 'default',
+ ],
+ },
+ },
+ }
+ },
+ },
+ ],
+ build: {
+ cssTarget: 'chrome61',
+ rollupOptions: {
+ input: {
+ index: path.resolve(__dirname, './index.html'),
+ treeshakeScoped: path.resolve(
+ __dirname,
+ './treeshake-scoped/index.html',
+ ),
+ },
+ output: {
+ manualChunks(id) {
+ if (id.includes('manual-chunk.css')) {
+ return 'dir/dir2/manual-chunk'
+ }
+ },
+ },
+ },
+ },
+ esbuild: {
+ logOverride: {
+ 'unsupported-css-property': 'silent',
+ },
+ },
+ resolve: {
+ alias: {
+ '=': __dirname,
+ spacefolder: __dirname + '/folder with space',
+ '#alias': __dirname + '/aliased/foo.css',
+ '#alias?inline': __dirname + '/aliased/foo.css?inline',
+ '#alias-module': __dirname + '/aliased/bar.module.css',
+ },
+ },
+ css: {
+ modules: {
+ generateScopedName: '[name]__[local]___[hash:base64:5]',
+
+ // example of how getJSON can be used to generate
+ // typescript typings for css modules class names
+
+ // getJSON(cssFileName, json, _outputFileName) {
+ // let typings = 'declare const classNames: {\n'
+ // for (let className in json) {
+ // typings += ` "${className}": string;\n`
+ // }
+ // typings += '};\n'
+ // typings += 'export default classNames;\n'
+ // const { join, dirname, basename } = require('path')
+ // const typingsFile = join(
+ // dirname(cssFileName),
+ // basename(cssFileName) + '.d.ts'
+ // )
+ // require('fs').writeFileSync(typingsFile, typings)
+ // },
+ },
+ preprocessorOptions: {
+ scss: {
+ additionalData: `$injectedColor: orange;`,
+ importers: [
+ {
+ canonicalize(url) {
+ return url === 'virtual-dep' || url.endsWith('.wxss')
+ ? new URL('custom-importer:virtual-dep')
+ : null
+ },
+ load() {
+ return {
+ contents: ``,
+ syntax: 'scss',
+ }
+ },
+ },
+ {
+ canonicalize(url) {
+ return url === 'virtual-file-absolute'
+ ? new URL('custom-importer:virtual-file-absolute')
+ : null
+ },
+ load() {
+ return {
+ contents: `@use "${pathToFileURL(path.join(import.meta.dirname, 'file-absolute.scss')).href}"`,
+ syntax: 'scss',
+ }
+ },
+ },
+ ],
+ },
+ styl: {
+ additionalData: `$injectedColor ?= orange`,
+ imports: [
+ './options/relative-import.styl',
+ path.join(__dirname, 'options/absolute-import.styl'),
+ ],
+ define: {
+ $definedColor: new stylus.nodes.RGBA(51, 197, 255, 1),
+ definedFunction: () => new stylus.nodes.RGBA(255, 0, 98, 1),
+ },
+ },
+ },
+ preprocessorMaxWorkers: true,
+ },
+})
diff --git a/playground/css/weapp.wxss b/playground/css/weapp.wxss
new file mode 100644
index 00000000000000..7a6ada48018898
--- /dev/null
+++ b/playground/css/weapp.wxss
@@ -0,0 +1 @@
+this is not css
diff --git a/playground/data-uri/__tests__/data-uri.spec.ts b/playground/data-uri/__tests__/data-uri.spec.ts
new file mode 100644
index 00000000000000..b18012e59d380c
--- /dev/null
+++ b/playground/data-uri/__tests__/data-uri.spec.ts
@@ -0,0 +1,27 @@
+import { expect, test } from 'vitest'
+import { findAssetFile, isBuild, page } from '~utils'
+
+test('plain', async () => {
+ expect(await page.textContent('.plain')).toBe('hi')
+})
+
+test('base64', async () => {
+ expect(await page.textContent('.base64')).toBe('hi')
+})
+
+test('svg data uri minify', async () => {
+ const sqdqs = await page.getByTestId('sqdqs').boundingBox()
+ const sqsdqs = await page.getByTestId('sqsdqs').boundingBox()
+ const dqsqs = await page.getByTestId('dqsqs').boundingBox()
+ const dqssqs = await page.getByTestId('dqssqs').boundingBox()
+
+ expect(sqdqs.height).toBe(100)
+ expect(sqsdqs.height).toBe(100)
+ expect(dqsqs.height).toBe(100)
+ expect(dqssqs.height).toBe(100)
+})
+
+test.runIf(isBuild)('should compile away the import for build', async () => {
+ const file = findAssetFile('index')
+ expect(file).not.toMatch('import')
+})
diff --git a/playground/data-uri/double-quote-in-single-quotes.svg b/playground/data-uri/double-quote-in-single-quotes.svg
new file mode 100644
index 00000000000000..d3a5ffc19e3701
--- /dev/null
+++ b/playground/data-uri/double-quote-in-single-quotes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/playground/data-uri/double-quotes-in-single-quotes.svg b/playground/data-uri/double-quotes-in-single-quotes.svg
new file mode 100644
index 00000000000000..fb8f151a23a598
--- /dev/null
+++ b/playground/data-uri/double-quotes-in-single-quotes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/playground/data-uri/index.html b/playground/data-uri/index.html
new file mode 100644
index 00000000000000..3794f74db4ea3e
--- /dev/null
+++ b/playground/data-uri/index.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/data-uri/main.js b/playground/data-uri/main.js
new file mode 100644
index 00000000000000..63326fdb1b4781
--- /dev/null
+++ b/playground/data-uri/main.js
@@ -0,0 +1,18 @@
+import sqdqs from './single-quote-in-double-quotes.svg'
+import sqsdqs from './single-quotes-in-double-quotes.svg'
+import dqsqs from './double-quote-in-single-quotes.svg'
+import dqssqs from './double-quotes-in-single-quotes.svg'
+
+document.querySelector('#sqdqs').innerHTML = `
+
+`
+document.querySelector('#sqsdqs').innerHTML = `
+
+`
+
+document.querySelector('#dqsqs').innerHTML = `
+
+`
+document.querySelector('#dqssqs').innerHTML = `
+
+`
diff --git a/playground/data-uri/package.json b/playground/data-uri/package.json
new file mode 100644
index 00000000000000..4e8b2d699f2ac8
--- /dev/null
+++ b/playground/data-uri/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-data-uri",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/data-uri/single-quote-in-double-quotes.svg b/playground/data-uri/single-quote-in-double-quotes.svg
new file mode 100644
index 00000000000000..69974c97773921
--- /dev/null
+++ b/playground/data-uri/single-quote-in-double-quotes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/playground/data-uri/single-quotes-in-double-quotes.svg b/playground/data-uri/single-quotes-in-double-quotes.svg
new file mode 100644
index 00000000000000..0489e7b39e8b5a
--- /dev/null
+++ b/playground/data-uri/single-quotes-in-double-quotes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/playground/data-uri/vite.config.js b/playground/data-uri/vite.config.js
new file mode 100644
index 00000000000000..67d43b61015b13
--- /dev/null
+++ b/playground/data-uri/vite.config.js
@@ -0,0 +1,20 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ plugins: [
+ {
+ name: 'post-plugin',
+ enforce: 'post',
+ resolveId(id) {
+ if (id.replace(/\?.*$/, '') === 'comma/foo') {
+ return id
+ }
+ },
+ load(id) {
+ if (id.replace(/\?.*$/, '') === 'comma/foo') {
+ return `export const comma = 'hi'`
+ }
+ },
+ },
+ ],
+})
diff --git a/playground/define/__tests__/define.spec.ts b/playground/define/__tests__/define.spec.ts
new file mode 100644
index 00000000000000..71fa61035a8f31
--- /dev/null
+++ b/playground/define/__tests__/define.spec.ts
@@ -0,0 +1,111 @@
+import { expect, test } from 'vitest'
+import viteConfig from '../vite.config'
+import { page } from '~utils'
+
+const defines = viteConfig.define
+const envDefines = viteConfig.environments.client.define
+
+test('string', async () => {
+ expect(await page.textContent('.exp')).toBe(
+ String(typeof eval(defines.__EXP__)),
+ )
+ expect(await page.textContent('.string')).toBe(JSON.parse(defines.__STRING__))
+ expect(await page.textContent('.number')).toBe(String(defines.__NUMBER__))
+ expect(await page.textContent('.boolean')).toBe(String(defines.__BOOLEAN__))
+ expect(await page.textContent('.undefined')).toBe('')
+
+ expect(await page.textContent('.object')).toBe(
+ JSON.stringify(defines.__OBJ__, null, 2),
+ )
+ expect(await page.textContent('.process-node-env')).toBe(
+ JSON.parse(defines['process.env.NODE_ENV']),
+ )
+ expect(await page.textContent('.process-env')).toBe(
+ JSON.stringify(defines['process.env'], null, 2),
+ )
+ expect(await page.textContent('.env-var')).toBe(
+ JSON.parse(defines['process.env.SOMEVAR']),
+ )
+ expect(await page.textContent('.process-as-property')).toBe(
+ defines.__OBJ__.process.env.SOMEVAR,
+ )
+ expect(await page.textContent('.spread-object')).toBe(
+ JSON.stringify({ SOMEVAR: defines['process.env.SOMEVAR'] }),
+ )
+ expect(await page.textContent('.spread-array')).toBe(
+ JSON.stringify([...defines.__STRING__]),
+ )
+ expect(await page.textContent('.dollar-identifier')).toBe(
+ String(defines.$DOLLAR),
+ )
+ expect(await page.textContent('.unicode-identifier')).toBe(
+ String(defines.ÖUNICODE_LETTERɵ),
+ )
+ expect(await page.textContent('.no-identifier-substring')).toBe(String(true))
+ expect(await page.textContent('.no-property')).toBe(String(true))
+ // html wouldn't need to define replacement
+ expect(await page.textContent('.exp-define')).toBe('__EXP__')
+ expect(await page.textContent('.import-json')).toBe('__EXP__')
+ expect(await page.textContent('.define-in-dep')).toBe(
+ defines.__STRINGIFIED_OBJ__,
+ )
+ expect(await page.textContent('.define-in-environment')).toBe(
+ envDefines.__DEFINE_IN_ENVIRONMENT__,
+ )
+})
+
+test('ignores constants in string literals', async () => {
+ expect(
+ await page.textContent('.ignores-string-literals .process-env-dot'),
+ ).toBe('process.env.')
+ expect(
+ await page.textContent('.ignores-string-literals .global-process-env-dot'),
+ ).toBe('global.process.env.')
+ expect(
+ await page.textContent(
+ '.ignores-string-literals .globalThis-process-env-dot',
+ ),
+ ).toBe('globalThis.process.env.')
+ expect(
+ await page.textContent('.ignores-string-literals .process-env-NODE_ENV'),
+ ).toBe('process.env.NODE_ENV')
+ expect(
+ await page.textContent(
+ '.ignores-string-literals .global-process-env-NODE_ENV',
+ ),
+ ).toBe('global.process.env.NODE_ENV')
+ expect(
+ await page.textContent(
+ '.ignores-string-literals .globalThis-process-env-NODE_ENV',
+ ),
+ ).toBe('globalThis.process.env.NODE_ENV')
+ expect(
+ await page.textContent('.ignores-string-literals .import-meta-hot'),
+ ).toBe('import' + '.meta.hot')
+})
+
+test('replaces constants in template literal expressions', async () => {
+ expect(
+ await page.textContent(
+ '.replaces-constants-in-template-literal-expressions .process-env-dot',
+ ),
+ ).toBe(JSON.parse(defines['process.env.SOMEVAR']))
+ expect(
+ await page.textContent(
+ '.replaces-constants-in-template-literal-expressions .process-env-NODE_ENV',
+ ),
+ ).toBe('dev')
+})
+
+test('replace constants on import.meta.env when it is a invalid json', async () => {
+ expect(
+ await page.textContent(
+ '.replace-undefined-constants-on-import-meta-env .import-meta-env-UNDEFINED',
+ ),
+ ).toBe('undefined')
+ expect(
+ await page.textContent(
+ '.replace-undefined-constants-on-import-meta-env .import-meta-env-SOME_IDENTIFIER',
+ ),
+ ).toBe('true')
+})
diff --git a/playground/define/commonjs-dep/index.js b/playground/define/commonjs-dep/index.js
new file mode 100644
index 00000000000000..3525efcea4c5bf
--- /dev/null
+++ b/playground/define/commonjs-dep/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+ defined: __STRINGIFIED_OBJ__,
+}
diff --git a/playground/define/commonjs-dep/package.json b/playground/define/commonjs-dep/package.json
new file mode 100644
index 00000000000000..f8ac503baaf9a9
--- /dev/null
+++ b/playground/define/commonjs-dep/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-commonjs-dep",
+ "private": true,
+ "version": "1.0.0",
+ "type": "commonjs"
+}
diff --git a/packages/playground/define/data.json b/playground/define/data.json
similarity index 100%
rename from packages/playground/define/data.json
rename to playground/define/data.json
diff --git a/playground/define/index.html b/playground/define/index.html
new file mode 100644
index 00000000000000..7c8fc55ba302cf
--- /dev/null
+++ b/playground/define/index.html
@@ -0,0 +1,166 @@
+
+
+Define
+
+Raw Expression
+String
+Number
+Boolean
+Undefined
+Object
+Env Var
+process node env:
+process env:
+process as property:
+spread object:
+spread array:
+dollar identifier:
+unicode identifier:
+no property:
+no identifier substring:
+define variable in html: __EXP__
+import json:
+define in dep:
+define in environment:
+
+Define ignores string literals
+
+ process.env.
+ global.process.env.
+
+ globalThis.process.env.
+
+ process.env.NODE_ENV
+
+ global.process.env.NODE_ENV
+
+
+
+ globalThis.process.env.NODE_ENV
+
+
+ import.meta.hot
+
+
+Define replaces constants in template literal expressions
+
+ process.env.
+ global.process.env.
+
+ globalThis.process.env.
+
+ process.env.NODE_ENV
+
+ global.process.env.NODE_ENV
+
+
+
+ globalThis.process.env.NODE_ENV
+
+
+ import.meta.hot
+
+
+Define undefined constants on import.meta.env when it's a invalid json
+
+
+
+
+
+
diff --git a/playground/define/package.json b/playground/define/package.json
new file mode 100644
index 00000000000000..a65b36c1c3df67
--- /dev/null
+++ b/playground/define/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-define",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@vitejs/test-commonjs-dep": "file:./commonjs-dep"
+ }
+}
diff --git a/playground/define/vite.config.js b/playground/define/vite.config.js
new file mode 100644
index 00000000000000..0712c4d01179bb
--- /dev/null
+++ b/playground/define/vite.config.js
@@ -0,0 +1,41 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ define: {
+ __EXP__: 'false',
+ __STRING__: '"hello"',
+ __NUMBER__: 123,
+ __BOOLEAN__: true,
+ __UNDEFINED__: undefined,
+ __OBJ__: {
+ foo: 1,
+ bar: {
+ baz: 2,
+ },
+ process: {
+ env: {
+ SOMEVAR: '"PROCESS MAY BE PROPERTY"',
+ },
+ },
+ },
+ 'process.env.NODE_ENV': '"dev"',
+ 'process.env.SOMEVAR': '"SOMEVAR"',
+ 'process.env': {
+ NODE_ENV: 'dev',
+ SOMEVAR: 'SOMEVAR',
+ OTHER: 'works',
+ },
+ $DOLLAR: 456,
+ ÖUNICODE_LETTERɵ: 789,
+ __VAR_NAME__: false,
+ __STRINGIFIED_OBJ__: JSON.stringify({ foo: true }),
+ 'import.meta.env.SOME_IDENTIFIER': '__VITE_SOME_IDENTIFIER__',
+ },
+ environments: {
+ client: {
+ define: {
+ __DEFINE_IN_ENVIRONMENT__: '"defined only in client"',
+ },
+ },
+ },
+})
diff --git a/playground/dynamic-import-inline/__tests__/dynamic-import-inline.spec.ts b/playground/dynamic-import-inline/__tests__/dynamic-import-inline.spec.ts
new file mode 100644
index 00000000000000..6c2c0c447c472e
--- /dev/null
+++ b/playground/dynamic-import-inline/__tests__/dynamic-import-inline.spec.ts
@@ -0,0 +1,12 @@
+import { expect, test } from 'vitest'
+import { isBuild, serverLogs } from '~utils'
+
+test.runIf(isBuild)(
+ 'dont warn when inlineDynamicImports is set to true',
+ async () => {
+ const log = serverLogs.join('\n')
+ expect(log).not.toContain(
+ 'dynamic import will not move module into another chunk',
+ )
+ },
+)
diff --git a/playground/dynamic-import-inline/index.html b/playground/dynamic-import-inline/index.html
new file mode 100644
index 00000000000000..d86d5c08912184
--- /dev/null
+++ b/playground/dynamic-import-inline/index.html
@@ -0,0 +1 @@
+
diff --git a/playground/dynamic-import-inline/package.json b/playground/dynamic-import-inline/package.json
new file mode 100644
index 00000000000000..32a98927237fe7
--- /dev/null
+++ b/playground/dynamic-import-inline/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-dynamic-import-inline",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/dynamic-import-inline/src/foo.js b/playground/dynamic-import-inline/src/foo.js
new file mode 100644
index 00000000000000..483b987b3b1c9b
--- /dev/null
+++ b/playground/dynamic-import-inline/src/foo.js
@@ -0,0 +1,3 @@
+export default function foo() {
+ return 'foo'
+}
diff --git a/playground/dynamic-import-inline/src/index.js b/playground/dynamic-import-inline/src/index.js
new file mode 100644
index 00000000000000..a151a21a59d840
--- /dev/null
+++ b/playground/dynamic-import-inline/src/index.js
@@ -0,0 +1,9 @@
+import foo from './foo'
+
+const asyncImport = async () => {
+ const { foo } = await import('./foo.js')
+ foo()
+}
+
+foo()
+asyncImport()
diff --git a/playground/dynamic-import-inline/vite.config.js b/playground/dynamic-import-inline/vite.config.js
new file mode 100644
index 00000000000000..2d69e108f59e01
--- /dev/null
+++ b/playground/dynamic-import-inline/vite.config.js
@@ -0,0 +1,18 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'alias'),
+ },
+ },
+ build: {
+ sourcemap: true,
+ rollupOptions: {
+ output: {
+ inlineDynamicImports: true,
+ },
+ },
+ },
+})
diff --git a/playground/dynamic-import/(app)/main.js b/playground/dynamic-import/(app)/main.js
new file mode 100644
index 00000000000000..48f83a961a109a
--- /dev/null
+++ b/playground/dynamic-import/(app)/main.js
@@ -0,0 +1,3 @@
+export function hello() {
+ return 'dynamic-import-with-vars-contains-parenthesis'
+}
diff --git a/playground/dynamic-import/(app)/nest/index.js b/playground/dynamic-import/(app)/nest/index.js
new file mode 100644
index 00000000000000..f2e2ca17f05336
--- /dev/null
+++ b/playground/dynamic-import/(app)/nest/index.js
@@ -0,0 +1,6 @@
+const base = 'main'
+import(`../${base}.js`).then((mod) => {
+ document.querySelector(
+ '.dynamic-import-with-vars-contains-parenthesis',
+ ).textContent = mod.hello()
+})
diff --git a/playground/dynamic-import/__tests__/dynamic-import.spec.ts b/playground/dynamic-import/__tests__/dynamic-import.spec.ts
new file mode 100644
index 00000000000000..5f19984fcbcaec
--- /dev/null
+++ b/playground/dynamic-import/__tests__/dynamic-import.spec.ts
@@ -0,0 +1,206 @@
+import { expect, test } from 'vitest'
+import {
+ browserLogs,
+ findAssetFile,
+ getColor,
+ isBuild,
+ page,
+ serverLogs,
+ untilUpdated,
+} from '~utils'
+
+test('should load literal dynamic import', async () => {
+ await page.click('.baz')
+ await untilUpdated(() => page.textContent('.view'), 'Baz view')
+})
+
+test('should load full dynamic import from public', async () => {
+ await page.click('.qux')
+ await untilUpdated(() => page.textContent('.view'), 'Qux view')
+ // No warning should be logged as we are using @vite-ignore
+ expect(
+ serverLogs.some((log) => log.includes('cannot be analyzed by vite')),
+ ).toBe(false)
+})
+
+test('should load data URL of `blob:`', async () => {
+ await page.click('.issue-2658-1')
+ await untilUpdated(() => page.textContent('.view'), 'blob')
+})
+
+test('should load data URL of `data:`', async () => {
+ await page.click('.issue-2658-2')
+ await untilUpdated(() => page.textContent('.view'), 'data')
+})
+
+test('should have same reference on static and dynamic js import, .mxd', async () => {
+ await page.click('.mxd')
+ await untilUpdated(() => page.textContent('.view'), 'true')
+})
+
+// in this case, it is not possible to detect the correct module
+test('should have same reference on static and dynamic js import, .mxd2', async () => {
+ await page.click('.mxd2')
+ await untilUpdated(() => page.textContent('.view'), 'false')
+})
+
+test('should have same reference on static and dynamic js import, .mxdjson', async () => {
+ await page.click('.mxdjson')
+ await untilUpdated(() => page.textContent('.view'), 'true')
+})
+
+// since this test has a timeout, it should be put last so that it
+// does not bleed on the last
+test('should load dynamic import with vars', async () => {
+ await page.click('.foo')
+ await untilUpdated(() => page.textContent('.view'), 'Foo view')
+
+ await page.click('.bar')
+ await untilUpdated(() => page.textContent('.view'), 'Bar view')
+})
+
+// dynamic import css
+test('should load dynamic import with css', async () => {
+ await page.click('.css')
+ await untilUpdated(
+ () => page.$eval('.view', (node) => window.getComputedStyle(node).color),
+ 'rgb(255, 0, 0)',
+ )
+})
+
+test('should load dynamic import with vars', async () => {
+ await untilUpdated(
+ () => page.textContent('.dynamic-import-with-vars'),
+ 'hello',
+ )
+})
+
+test('should load dynamic import with vars ignored', async () => {
+ await untilUpdated(
+ () => page.textContent('.dynamic-import-with-vars-ignored'),
+ 'hello',
+ )
+ // No warning should be logged as we are using @vite-ignore
+ expect(
+ serverLogs.some((log) =>
+ log.includes('"https" has been externalized for browser compatibility'),
+ ),
+ ).toBe(false)
+})
+
+test('should load dynamic import with double slash ignored', async () => {
+ await untilUpdated(
+ () => page.textContent('.dynamic-import-with-double-slash-ignored'),
+ 'hello',
+ )
+})
+
+test('should load dynamic import with vars multiline', async () => {
+ await untilUpdated(
+ () => page.textContent('.dynamic-import-with-vars-multiline'),
+ 'hello',
+ )
+})
+
+test('should load dynamic import with vars alias', async () => {
+ await untilUpdated(
+ () => page.textContent('.dynamic-import-with-vars-alias'),
+ 'hi',
+ )
+})
+
+test('should load dynamic import with vars raw', async () => {
+ await untilUpdated(
+ () => page.textContent('.dynamic-import-with-vars-raw'),
+ 'export function hello()',
+ )
+})
+
+test('should load dynamic import with vars url', async () => {
+ await untilUpdated(
+ () => page.textContent('.dynamic-import-with-vars-url'),
+ isBuild ? 'data:text/javascript' : '/alias/url.js',
+ )
+})
+
+test('should load dynamic import with vars worker', async () => {
+ await untilUpdated(
+ () => page.textContent('.dynamic-import-with-vars-worker'),
+ 'load worker',
+ )
+})
+
+test('should load dynamic import with css in package', async () => {
+ await page.click('.pkg-css')
+ await untilUpdated(() => getColor('.pkg-css'), 'blue')
+})
+
+test('should work with load ../ and itself directory', async () => {
+ await untilUpdated(
+ () => page.textContent('.dynamic-import-self'),
+ 'dynamic-import-self-content',
+ )
+})
+
+test('should work with load ../ and contain itself directory', async () => {
+ await untilUpdated(
+ () => page.textContent('.dynamic-import-nested-self'),
+ 'dynamic-import-nested-self-content',
+ )
+})
+
+test('should work a load path that contains parentheses.', async () => {
+ await untilUpdated(
+ () => page.textContent('.dynamic-import-with-vars-contains-parenthesis'),
+ 'dynamic-import-with-vars-contains-parenthesis',
+ )
+})
+
+test.runIf(isBuild)(
+ 'should rollup warn when static and dynamic import a module in same chunk',
+ async () => {
+ const log = serverLogs.join('\n')
+ expect(log).toContain(
+ 'dynamic import will not move module into another chunk',
+ )
+ expect(log).toMatch(
+ /\(!\).*\/dynamic-import\/files\/mxd\.js is dynamically imported by/,
+ )
+ expect(log).toMatch(
+ /\(!\).*\/dynamic-import\/files\/mxd\.json is dynamically imported by/,
+ )
+ expect(log).not.toMatch(
+ /\(!\).*\/dynamic-import\/nested\/shared\.js is dynamically imported by/,
+ )
+ },
+)
+
+test('dynamic import treeshaken log', async () => {
+ const log = browserLogs.join('\n')
+ expect(log).toContain('treeshaken foo')
+ expect(log).toContain('treeshaken bar')
+ expect(log).toContain('treeshaken baz1')
+ expect(log).toContain('treeshaken baz2')
+ expect(log).toContain('treeshaken baz3')
+ expect(log).toContain('treeshaken baz4')
+ expect(log).toContain('treeshaken baz5')
+ expect(log).toContain('treeshaken default')
+
+ expect(log).not.toContain('treeshaken removed')
+})
+
+test('dynamic import syntax parsing', async () => {
+ const log = browserLogs.join('\n')
+ expect(log).toContain('treeshaken syntax foo')
+ expect(log).toContain('treeshaken syntax default')
+})
+
+test.runIf(isBuild)('dynamic import treeshaken file', async () => {
+ expect(findAssetFile(/treeshaken.+\.js$/)).not.toContain('treeshaken removed')
+})
+
+test.runIf(isBuild)('should not preload for non-analyzable urls', () => {
+ const js = findAssetFile(/index-[-\w]{8}\.js$/)
+ // should match e.g. await import(e.jss);o(".view",p===i)
+ expect(js).to.match(/\.jss\);/)
+})
diff --git a/playground/dynamic-import/alias/hello.js b/playground/dynamic-import/alias/hello.js
new file mode 100644
index 00000000000000..b10bde412dbbe1
--- /dev/null
+++ b/playground/dynamic-import/alias/hello.js
@@ -0,0 +1,4 @@
+export function hello() {
+ return 'hello'
+}
+console.log('hello.js')
diff --git a/playground/dynamic-import/alias/hi.js b/playground/dynamic-import/alias/hi.js
new file mode 100644
index 00000000000000..d2cfa4dc305c7b
--- /dev/null
+++ b/playground/dynamic-import/alias/hi.js
@@ -0,0 +1,4 @@
+export function hi() {
+ return 'hi'
+}
+console.log('hi.js')
diff --git a/playground/dynamic-import/alias/url.js b/playground/dynamic-import/alias/url.js
new file mode 100644
index 00000000000000..c9b0c79461d91e
--- /dev/null
+++ b/playground/dynamic-import/alias/url.js
@@ -0,0 +1 @@
+export const url = 'load url'
diff --git a/playground/dynamic-import/alias/worker.js b/playground/dynamic-import/alias/worker.js
new file mode 100644
index 00000000000000..2a8fc242aab315
--- /dev/null
+++ b/playground/dynamic-import/alias/worker.js
@@ -0,0 +1,5 @@
+self.onmessage = (event) => {
+ self.postMessage({
+ msg: 'load worker',
+ })
+}
diff --git a/packages/playground/dynamic-import/css/index.css b/playground/dynamic-import/css/index.css
similarity index 100%
rename from packages/playground/dynamic-import/css/index.css
rename to playground/dynamic-import/css/index.css
diff --git a/packages/playground/dynamic-import/mxd.js b/playground/dynamic-import/files/mxd.js
similarity index 100%
rename from packages/playground/dynamic-import/mxd.js
rename to playground/dynamic-import/files/mxd.js
diff --git a/playground/dynamic-import/files/mxd.json b/playground/dynamic-import/files/mxd.json
new file mode 100644
index 00000000000000..0967ef424bce67
--- /dev/null
+++ b/playground/dynamic-import/files/mxd.json
@@ -0,0 +1 @@
+{}
diff --git a/playground/dynamic-import/index.html b/playground/dynamic-import/index.html
new file mode 100644
index 00000000000000..1289efc3e1ac4e
--- /dev/null
+++ b/playground/dynamic-import/index.html
@@ -0,0 +1,54 @@
+Foo
+Bar
+Baz
+Qux
+Mxd
+Mxd2
+Mxdjson
+Issue 2658 - 1
+Issue 2658 - 2
+css
+pkg-css
+
+dynamic-import-with-vars
+todo
+
+dynamic-import-with-vars-ignored
+todo
+
+dynamic-import-with-double-slash-ignored
+todo
+
+dynamic-import-with-vars-multiline
+todo
+
+dynamic-import-with-vars-alias
+todo
+
+dynamic-import-with-vars-raw
+todo
+
+dynamic-import-with-vars-url
+todo
+
+dynamic-import-with-vars-worker
+todo
+
+dynamic-import-with-vars-contains-parenthesis
+todo
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/dynamic-import/nested/deps.js b/playground/dynamic-import/nested/deps.js
new file mode 100644
index 00000000000000..34668f67263505
--- /dev/null
+++ b/playground/dynamic-import/nested/deps.js
@@ -0,0 +1,3 @@
+/* don't include dynamic import inside this file */
+
+import '@vitejs/test-pkg'
diff --git a/playground/dynamic-import/nested/hello.js b/playground/dynamic-import/nested/hello.js
new file mode 100644
index 00000000000000..67900ef0999962
--- /dev/null
+++ b/playground/dynamic-import/nested/hello.js
@@ -0,0 +1,3 @@
+export function hello() {
+ return 'hello'
+}
diff --git a/playground/dynamic-import/nested/index.js b/playground/dynamic-import/nested/index.js
new file mode 100644
index 00000000000000..260f765787bdf3
--- /dev/null
+++ b/playground/dynamic-import/nested/index.js
@@ -0,0 +1,199 @@
+import mxdStatic from '../files/mxd'
+import mxdStaticJSON from '../files/mxd.json'
+
+async function setView(view) {
+ const { msg } = await import(`../views/${view}.js`)
+ text('.view', msg)
+}
+
+;['foo', 'bar'].forEach((id) => {
+ document.querySelector(`.${id}`).addEventListener('click', () => setView(id))
+})
+
+// literal dynamic
+document.querySelector('.baz').addEventListener('click', async () => {
+ const { msg } = await import('../views/baz.js')
+ text('.view', msg)
+})
+
+// full dynamic
+const arr = ['qux.js']
+const view = `/views/${arr[0]}`
+document.querySelector('.qux').addEventListener('click', async () => {
+ const { msg } = await import(/*@vite-ignore*/ view)
+ text('.view', msg)
+})
+
+// mixed static and dynamic
+document.querySelector('.mxd').addEventListener('click', async () => {
+ const view = 'mxd'
+ const { default: mxdDynamic } = await import(`../files/${view}.js`)
+ text('.view', mxdStatic === mxdDynamic)
+})
+
+document.querySelector('.mxd2').addEventListener('click', async () => {
+ const test = { jss: '../files/mxd.js' }
+ const ttest = test
+ const view = 'mxd'
+ const { default: mxdDynamic } = await import(/*@vite-ignore*/ test.jss)
+ text('.view', mxdStatic === mxdDynamic)
+})
+
+document.querySelector('.mxdjson').addEventListener('click', async () => {
+ const view = 'mxd'
+ const { default: mxdDynamicJSON } = await import(`../files/${view}.json`)
+ text('.view', mxdStaticJSON === mxdDynamicJSON)
+})
+
+// data URLs (`blob:`)
+const code1 = 'export const msg = "blob"'
+const blob = new Blob([code1], { type: 'text/javascript;charset=UTF-8' })
+const blobURL = URL.createObjectURL(blob)
+document.querySelector('.issue-2658-1').addEventListener('click', async () => {
+ const { msg } = await import(/*@vite-ignore*/ blobURL)
+ text('.view', msg)
+})
+
+// data URLs (`data:`)
+const code2 = 'export const msg = "data";'
+const dataURL = `data:text/javascript;charset=utf-8,${encodeURIComponent(
+ code2,
+)}`
+document.querySelector('.issue-2658-2').addEventListener('click', async () => {
+ const { msg } = await import(/*@vite-ignore*/ dataURL)
+ text('.view', msg)
+})
+
+document.querySelector('.css').addEventListener('click', async () => {
+ await import('../css/index.css')
+ text('.view', 'dynamic import css')
+})
+
+document.querySelector('.pkg-css').addEventListener('click', async () => {
+ await import('./deps')
+ text('.view', 'dynamic import css in package')
+})
+
+function text(el, text) {
+ document.querySelector(el).textContent = text
+}
+
+let base = 'hello'
+
+import(`../alias/${base}.js`).then((mod) => {
+ text('.dynamic-import-with-vars', mod.hello())
+})
+
+import(/*@vite-ignore*/ `https://localhost`).catch((mod) => {
+ console.log(mod)
+ text('.dynamic-import-with-vars-ignored', 'hello')
+})
+
+import(/*@vite-ignore*/ `https://localhost//${'test'}`).catch((mod) => {
+ console.log(mod)
+ text('.dynamic-import-with-double-slash-ignored', 'hello')
+})
+
+// prettier-ignore
+import(
+ /* this messes with */
+ `../alias/${base}.js`
+ /* es-module-lexer */
+).then((mod) => {
+ text('.dynamic-import-with-vars-multiline', mod.hello())
+})
+
+import(`../alias/${base}.js?raw`).then((mod) => {
+ text('.dynamic-import-with-vars-raw', JSON.stringify(mod))
+})
+
+base = 'url'
+import(`../alias/${base}.js?url`).then((mod) => {
+ text('.dynamic-import-with-vars-url', JSON.stringify(mod))
+})
+
+base = 'worker'
+import(`../alias/${base}.js?worker`).then((workerMod) => {
+ const worker = new workerMod.default()
+ worker.postMessage('1')
+ worker.addEventListener('message', (ev) => {
+ console.log(ev)
+ text('.dynamic-import-with-vars-worker', JSON.stringify(ev.data))
+ })
+})
+
+base = 'hi'
+import(`@/${base}.js`).then((mod) => {
+ text('.dynamic-import-with-vars-alias', mod.hi())
+})
+
+base = 'self'
+import(`../nested/${base}.js`).then((mod) => {
+ text('.dynamic-import-self', mod.self)
+})
+
+import(`../nested/nested/${base}.js`).then((mod) => {
+ text('.dynamic-import-nested-self', mod.self)
+})
+;(async function () {
+ const { foo } = await import('./treeshaken/treeshaken.js')
+ const { bar, default: tree } = await import('./treeshaken/treeshaken.js')
+ const default2 = (await import('./treeshaken/treeshaken.js')).default
+ const baz1 = (await import('./treeshaken/treeshaken.js')).baz1
+ const baz2 = (await import('./treeshaken/treeshaken.js')).baz2.log
+ const baz3 = (await import('./treeshaken/treeshaken.js')).baz3?.log
+ const baz4 = await import('./treeshaken/treeshaken.js').then(
+ ({ baz4 }) => baz4,
+ )
+ const baz5 = await import('./treeshaken/treeshaken.js').then(function ({
+ baz5,
+ }) {
+ return baz5
+ }),
+ { baz6 } = await import('./treeshaken/treeshaken.js')
+ foo()
+ bar()
+ tree()
+ ;(await import('./treeshaken/treeshaken.js')).default()
+ default2()
+ baz1()
+ baz2()
+ baz3()
+ baz4()
+ baz5()
+ baz6()
+})()
+// Test syntax parsing only
+;(async function () {
+ const default1 = await import('./treeshaken/syntax.js').then(
+ (mod) => mod.default,
+ )
+ const default2 = (await import('./treeshaken/syntax.js')).default,
+ other = () => {}
+ const foo = await import('./treeshaken/syntax.js').then((mod) => mod.foo)
+ const foo2 = await import('./treeshaken/syntax.js').then(
+ ({ foo = {} }) => foo,
+ )
+ await import('./treeshaken/syntax.js').then((mod) => mod.foo({ foo }))
+ const obj = [
+ '',
+ {
+ async lazy() {
+ const { foo } = await import('./treeshaken/treeshaken.js')
+ return { foo: aaa(foo) }
+ },
+ },
+ ]
+ default1()
+ default2()
+ other()
+ foo()
+ foo2()
+ obj[1].lazy()
+})()
+
+import(`../nested/static.js`).then((mod) => {
+ text('.dynamic-import-static', mod.self)
+})
+
+console.log('index.js')
diff --git a/playground/dynamic-import/nested/nested/self.js b/playground/dynamic-import/nested/nested/self.js
new file mode 100644
index 00000000000000..b18321b2044887
--- /dev/null
+++ b/playground/dynamic-import/nested/nested/self.js
@@ -0,0 +1 @@
+export const self = 'dynamic-import-nested-self-content'
diff --git a/playground/dynamic-import/nested/self.js b/playground/dynamic-import/nested/self.js
new file mode 100644
index 00000000000000..46e122535d86e4
--- /dev/null
+++ b/playground/dynamic-import/nested/self.js
@@ -0,0 +1 @@
+export const self = 'dynamic-import-self-content'
diff --git a/packages/playground/dynamic-import/nested/shared.js b/playground/dynamic-import/nested/shared.js
similarity index 100%
rename from packages/playground/dynamic-import/nested/shared.js
rename to playground/dynamic-import/nested/shared.js
diff --git a/playground/dynamic-import/nested/static.js b/playground/dynamic-import/nested/static.js
new file mode 100644
index 00000000000000..02dd476388a6e4
--- /dev/null
+++ b/playground/dynamic-import/nested/static.js
@@ -0,0 +1 @@
+export const self = 'dynamic-import-static'
diff --git a/playground/dynamic-import/nested/treeshaken/syntax.js b/playground/dynamic-import/nested/treeshaken/syntax.js
new file mode 100644
index 00000000000000..7ee55ddefc403d
--- /dev/null
+++ b/playground/dynamic-import/nested/treeshaken/syntax.js
@@ -0,0 +1,6 @@
+export const foo = () => {
+ console.log('treeshaken syntax foo')
+}
+export default () => {
+ console.log('treeshaken syntax default')
+}
diff --git a/playground/dynamic-import/nested/treeshaken/treeshaken.js b/playground/dynamic-import/nested/treeshaken/treeshaken.js
new file mode 100644
index 00000000000000..3fdc9ae7a7808f
--- /dev/null
+++ b/playground/dynamic-import/nested/treeshaken/treeshaken.js
@@ -0,0 +1,34 @@
+export const foo = () => {
+ console.log('treeshaken foo')
+}
+export const bar = () => {
+ console.log('treeshaken bar')
+}
+export const baz1 = () => {
+ console.log('treeshaken baz1')
+}
+export const baz2 = {
+ log: () => {
+ console.log('treeshaken baz2')
+ },
+}
+export const baz3 = {
+ log: () => {
+ console.log('treeshaken baz3')
+ },
+}
+export const baz4 = () => {
+ console.log('treeshaken baz4')
+}
+export const baz5 = () => {
+ console.log('treeshaken baz5')
+}
+export const baz6 = () => {
+ console.log('treeshaken baz6')
+}
+export const removed = () => {
+ console.log('treeshaken removed')
+}
+export default () => {
+ console.log('treeshaken default')
+}
diff --git a/playground/dynamic-import/package.json b/playground/dynamic-import/package.json
new file mode 100644
index 00000000000000..d3ab6846463268
--- /dev/null
+++ b/playground/dynamic-import/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-dynamic-import",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@vitejs/test-pkg": "file:./pkg"
+ }
+}
diff --git a/playground/dynamic-import/pkg/index.js b/playground/dynamic-import/pkg/index.js
new file mode 100644
index 00000000000000..20f705c0b4a8c9
--- /dev/null
+++ b/playground/dynamic-import/pkg/index.js
@@ -0,0 +1 @@
+import('./pkg.css')
diff --git a/playground/dynamic-import/pkg/package.json b/playground/dynamic-import/pkg/package.json
new file mode 100644
index 00000000000000..fb2e8c8815c8fa
--- /dev/null
+++ b/playground/dynamic-import/pkg/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-pkg",
+ "type": "module",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.js"
+}
diff --git a/playground/dynamic-import/pkg/pkg.css b/playground/dynamic-import/pkg/pkg.css
new file mode 100644
index 00000000000000..349d669b6829bf
--- /dev/null
+++ b/playground/dynamic-import/pkg/pkg.css
@@ -0,0 +1,3 @@
+.pkg-css {
+ color: blue;
+}
diff --git a/packages/playground/dynamic-import/views/bar.js b/playground/dynamic-import/views/bar.js
similarity index 100%
rename from packages/playground/dynamic-import/views/bar.js
rename to playground/dynamic-import/views/bar.js
diff --git a/packages/playground/dynamic-import/views/baz.js b/playground/dynamic-import/views/baz.js
similarity index 100%
rename from packages/playground/dynamic-import/views/baz.js
rename to playground/dynamic-import/views/baz.js
diff --git a/packages/playground/dynamic-import/views/foo.js b/playground/dynamic-import/views/foo.js
similarity index 100%
rename from packages/playground/dynamic-import/views/foo.js
rename to playground/dynamic-import/views/foo.js
diff --git a/packages/playground/dynamic-import/qux.js b/playground/dynamic-import/views/qux.js
similarity index 100%
rename from packages/playground/dynamic-import/qux.js
rename to playground/dynamic-import/views/qux.js
diff --git a/playground/dynamic-import/vite.config.js b/playground/dynamic-import/vite.config.js
new file mode 100644
index 00000000000000..dc8ecbd8bbdfe5
--- /dev/null
+++ b/playground/dynamic-import/vite.config.js
@@ -0,0 +1,35 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ plugins: [
+ {
+ name: 'copy',
+ writeBundle() {
+ fs.mkdirSync(path.resolve(__dirname, 'dist/views'))
+ fs.mkdirSync(path.resolve(__dirname, 'dist/files'))
+ fs.copyFileSync(
+ path.resolve(__dirname, 'views/qux.js'),
+ path.resolve(__dirname, 'dist/views/qux.js'),
+ )
+ fs.copyFileSync(
+ path.resolve(__dirname, 'files/mxd.js'),
+ path.resolve(__dirname, 'dist/files/mxd.js'),
+ )
+ fs.copyFileSync(
+ path.resolve(__dirname, 'files/mxd.json'),
+ path.resolve(__dirname, 'dist/files/mxd.json'),
+ )
+ },
+ },
+ ],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'alias'),
+ },
+ },
+ build: {
+ sourcemap: true,
+ },
+})
diff --git a/packages/playground/env-nested/.env b/playground/env-nested/.env
similarity index 100%
rename from packages/playground/env-nested/.env
rename to playground/env-nested/.env
diff --git a/packages/playground/env-nested/__tests__/env-nested.spec.ts b/playground/env-nested/__tests__/env-nested.spec.ts
similarity index 83%
rename from packages/playground/env-nested/__tests__/env-nested.spec.ts
rename to playground/env-nested/__tests__/env-nested.spec.ts
index 1ceebde7a044b7..ea4cdeb88e72c4 100644
--- a/packages/playground/env-nested/__tests__/env-nested.spec.ts
+++ b/playground/env-nested/__tests__/env-nested.spec.ts
@@ -1,4 +1,5 @@
-import { isBuild } from 'testUtils'
+import { expect, test } from 'vitest'
+import { isBuild, page } from '~utils'
const mode = isBuild ? `production` : `development`
diff --git a/packages/playground/env-nested/envs/.env.development b/playground/env-nested/envs/.env.development
similarity index 100%
rename from packages/playground/env-nested/envs/.env.development
rename to playground/env-nested/envs/.env.development
diff --git a/packages/playground/env-nested/envs/.env.production b/playground/env-nested/envs/.env.production
similarity index 100%
rename from packages/playground/env-nested/envs/.env.production
rename to playground/env-nested/envs/.env.production
diff --git a/packages/playground/env-nested/index.html b/playground/env-nested/index.html
similarity index 100%
rename from packages/playground/env-nested/index.html
rename to playground/env-nested/index.html
diff --git a/playground/env-nested/package.json b/playground/env-nested/package.json
new file mode 100644
index 00000000000000..060888e998c98d
--- /dev/null
+++ b/playground/env-nested/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-env-nested",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/env-nested/vite.config.js b/playground/env-nested/vite.config.js
new file mode 100644
index 00000000000000..dc79ce87dcc405
--- /dev/null
+++ b/playground/env-nested/vite.config.js
@@ -0,0 +1,5 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ envDir: './envs',
+})
diff --git a/playground/env/.env b/playground/env/.env
new file mode 100644
index 00000000000000..db7181dff7c1d3
--- /dev/null
+++ b/playground/env/.env
@@ -0,0 +1,11 @@
+VITE_CUSTOM_ENV_VARIABLE=1
+CUSTOM_PREFIX_ENV_VARIABLE=1
+VITE_EFFECTIVE_MODE_FILE_NAME=.env
+VITE_BOOL=true
+DEPEND_ENV=depend
+VITE_EXPAND_A=$EXPAND
+VITE_EXPAND_B=$DEPEND_ENV
+VITE_ESCAPE_A=escape\$
+VITE_ESCAPE_B=escape$
+IRRELEVANT_ENV=$DEPEND_ENV
+IRRELEVANT_ESCAPE_ENV=irrelevant$
diff --git a/packages/playground/env/.env.development b/playground/env/.env.development
similarity index 100%
rename from packages/playground/env/.env.development
rename to playground/env/.env.development
diff --git a/packages/playground/env/.env.production b/playground/env/.env.production
similarity index 100%
rename from packages/playground/env/.env.production
rename to playground/env/.env.production
diff --git a/playground/env/__tests__/env.spec.ts b/playground/env/__tests__/env.spec.ts
new file mode 100644
index 00000000000000..084d9d887e326d
--- /dev/null
+++ b/playground/env/__tests__/env.spec.ts
@@ -0,0 +1,124 @@
+import { expect, test } from 'vitest'
+import { isBuild, page } from '~utils'
+
+const mode = isBuild ? `production` : `development`
+
+test('base', async () => {
+ expect(await page.textContent('.base')).toBe('/env/')
+})
+
+test('mode', async () => {
+ expect(await page.textContent('.mode')).toBe(mode)
+})
+
+test('dev', async () => {
+ expect(await page.textContent('.dev')).toBe(String(!isBuild))
+})
+
+test('prod', async () => {
+ expect(await page.textContent('.prod')).toBe(String(isBuild))
+})
+
+test('custom', async () => {
+ expect(await page.textContent('.custom')).toBe('1')
+})
+
+test('custom in template literal expression', async () => {
+ expect(await page.textContent('.custom-template-literal-exp')).toBe('1')
+})
+
+test('custom-prefix', async () => {
+ expect(await page.textContent('.custom-prefix')).toBe('1')
+})
+
+test('mode file override', async () => {
+ expect(await page.textContent('.mode-file')).toBe(`.env.${mode}`)
+})
+
+test('inline variables', async () => {
+ expect(await page.textContent('.inline')).toBe(
+ isBuild ? `inline-build` : `inline-serve`,
+ )
+})
+
+test('define', async () => {
+ expect(await page.textContent('.bool')).toBe('boolean')
+ expect(await page.textContent('.number')).toBe('number')
+ expect(await page.textContent('.string')).toBe('string')
+ expect(await page.textContent('.stringify-object')).toBe('object')
+})
+
+test('NODE_ENV', async () => {
+ expect(await page.textContent('.node-env')).toBe(process.env.NODE_ENV)
+ expect(await page.textContent('.global-node-env')).toBe(process.env.NODE_ENV)
+ expect(await page.textContent('.global-this-node-env')).toBe(
+ process.env.NODE_ENV,
+ )
+})
+
+test('expand', async () => {
+ expect(await page.textContent('.expand-a')).toBe('expand')
+ expect(await page.textContent('.expand-b')).toBe('depend')
+})
+
+test('ssr', async () => {
+ expect(await page.textContent('.ssr')).toBe('false')
+})
+
+test('env object', async () => {
+ const env = JSON.parse(await page.textContent('.env-object'))
+ expect(env).not.toHaveProperty([
+ 'DEPEND_ENV',
+ 'IRRELEVANT_ENV',
+ 'IRRELEVANT_ESCAPE_ENV',
+ ])
+ expect(env).toMatchObject({
+ VITE_EFFECTIVE_MODE_FILE_NAME: `.env.${mode}`,
+ CUSTOM_PREFIX_ENV_VARIABLE: '1',
+ VITE_CUSTOM_ENV_VARIABLE: '1',
+ VITE_EXPAND_A: 'expand',
+ VITE_EXPAND_B: 'depend',
+ VITE_ESCAPE_A: 'escape$',
+ VITE_ESCAPE_B: 'escape$',
+ BASE_URL: '/env/',
+ VITE_BOOL: true,
+ SSR: false,
+ MODE: mode,
+ DEV: !isBuild,
+ PROD: isBuild,
+ VITE_NUMBER: 123,
+ VITE_STRING: '{"123",}',
+ VITE_STRINGIFY_OBJECT: {
+ a: '1',
+ b: '2',
+ },
+ })
+})
+
+test('env object in template literal expression', async () => {
+ const envText = await page.textContent('.env-object-in-template-literal-exp')
+ expect(JSON.parse(envText)).toMatchObject({
+ VITE_EFFECTIVE_MODE_FILE_NAME: `.env.${mode}`,
+ CUSTOM_PREFIX_ENV_VARIABLE: '1',
+ VITE_CUSTOM_ENV_VARIABLE: '1',
+ BASE_URL: '/env/',
+ MODE: mode,
+ DEV: !isBuild,
+ PROD: isBuild,
+ })
+})
+
+if (!isBuild) {
+ test('relative url import script return import.meta.url', async () => {
+ expect(await page.textContent('.url')).toMatch('/env/index.js')
+ })
+}
+
+test('ignores import' + '.meta.env in string literals', async () => {
+ expect(await page.textContent('.ignores-literal-import-meta-env-dot')).toBe(
+ 'import' + '.meta.env.',
+ )
+ expect(await page.textContent('.ignores-literal-import-meta-env')).toBe(
+ 'import' + '.meta.env',
+ )
+})
diff --git a/playground/env/index.html b/playground/env/index.html
new file mode 100644
index 00000000000000..442e5dd1fc7f3e
--- /dev/null
+++ b/playground/env/index.html
@@ -0,0 +1,89 @@
+Environment Variables
+import.meta.env.BASE_URL:
+import.meta.env.MODE:
+import.meta.env.DEV:
+import.meta.env.PROD:
+import.meta.env.VITE_CUSTOM_ENV_VARIABLE:
+
+ ${import.meta.env.VITE_CUSTOM_ENV_VARIABLE}:
+
+
+
+ import.meta.env.CUSTOM_PREFIX_ENV_VARIABLE:
+
+
+
+ import.meta.env.VITE_EFFECTIVE_MODE_FILE_NAME:
+
+import.meta.env.VITE_INLINE:
+typeof import.meta.env.VITE_BOOL:
+typeof import.meta.env.VITE_NUMBER:
+typeof import.meta.env.VITE_STRING:
+
+ typeof import.meta.env.VITE_STRINGIFY_OBJECT:
+
+
+process.env.NODE_ENV:
+global.process.env.NODE_ENV:
+
+ globalThis.process.env.NODE_ENV:
+
+import.meta.env.VITE_EXPAND_A:
+import.meta.env.VITE_EXPAND_B:
+import.meta.env.SSR:
+import.meta.env:
+
+ ${import.meta.env}:
+
+
+import.meta.url:
+
+ import.meta.env.
+
+import.meta.env
+
+
+
+
+
diff --git a/playground/env/index.js b/playground/env/index.js
new file mode 100644
index 00000000000000..35e23fd0d2b924
--- /dev/null
+++ b/playground/env/index.js
@@ -0,0 +1,5 @@
+text('.url', import.meta.url)
+
+function text(el, text) {
+ document.querySelector(el).textContent = text
+}
diff --git a/playground/env/package.json b/playground/env/package.json
new file mode 100644
index 00000000000000..a50e47e1c17313
--- /dev/null
+++ b/playground/env/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-env",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "VITE_INLINE=inline-serve vite",
+ "build": "VITE_INLINE=inline-build vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/env/vite.config.js b/playground/env/vite.config.js
new file mode 100644
index 00000000000000..bca10b34c4c6b5
--- /dev/null
+++ b/playground/env/vite.config.js
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+
+process.env.EXPAND = 'expand'
+
+export default defineConfig({
+ base: '/env/',
+ envPrefix: ['VITE_', 'CUSTOM_PREFIX_'],
+ build: {
+ outDir: 'dist/env',
+ },
+ define: {
+ 'import.meta.env.VITE_BOOL': true,
+ 'import.meta.env.VITE_NUMBER': '123',
+ 'import.meta.env.VITE_STRING': JSON.stringify('{"123",}'),
+ 'import.meta.env.VITE_STRINGIFY_OBJECT': JSON.stringify({ a: '1', b: '2' }),
+ },
+})
diff --git a/playground/environment-react-ssr/__tests__/environment-react-ssr.spec.ts b/playground/environment-react-ssr/__tests__/environment-react-ssr.spec.ts
new file mode 100644
index 00000000000000..247f980cab9944
--- /dev/null
+++ b/playground/environment-react-ssr/__tests__/environment-react-ssr.spec.ts
@@ -0,0 +1,100 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { stripVTControlCharacters } from 'node:util'
+import { describe, expect, onTestFinished, test } from 'vitest'
+import {
+ isBuild,
+ page,
+ readDepOptimizationMetadata,
+ readFile,
+ serverLogs,
+ testDir,
+ untilUpdated,
+} from '~utils'
+
+test('basic', async () => {
+ await page.getByText('hydrated: true').isVisible()
+ await page.getByText('Count: 0').isVisible()
+ await page.getByRole('button', { name: '+' }).click()
+ await page.getByText('Count: 1').isVisible()
+})
+
+describe.runIf(!isBuild)('pre-bundling', () => {
+ test('client', async () => {
+ const metaJson = readDepOptimizationMetadata()
+
+ expect(metaJson.optimized['react']).toBeTruthy()
+ expect(metaJson.optimized['react-dom/client']).toBeTruthy()
+ expect(metaJson.optimized['react/jsx-dev-runtime']).toBeTruthy()
+
+ expect(metaJson.optimized['react-dom/server']).toBeFalsy()
+ })
+
+ test('ssr', async () => {
+ const metaJson = readDepOptimizationMetadata('ssr')
+
+ expect(metaJson.optimized['react']).toBeTruthy()
+ expect(metaJson.optimized['react-dom/server']).toBeTruthy()
+ expect(metaJson.optimized['react/jsx-dev-runtime']).toBeTruthy()
+
+ expect(metaJson.optimized['react-dom/client']).toBeFalsy()
+
+ // process.env.NODE_ENV should be kept as keepProcessEnv is true
+ const depsFiles = fs
+ .readdirSync(path.resolve(testDir, 'node_modules/.vite/deps_ssr'), {
+ withFileTypes: true,
+ })
+ .filter((file) => file.isFile() && file.name.endsWith('.js'))
+ .map((file) => path.join(file.parentPath, file.name))
+ const depsFilesWithProcessEnvNodeEnv = depsFiles.filter((file) =>
+ fs.readFileSync(file, 'utf-8').includes('process.env.NODE_ENV'),
+ )
+
+ expect(depsFilesWithProcessEnvNodeEnv.length).toBeGreaterThan(0)
+ })
+
+ test('deps reload', async () => {
+ const envs = ['client', 'server'] as const
+
+ const clientMeta = readDepOptimizationMetadata('client')
+ const ssrMeta = readDepOptimizationMetadata('ssr')
+ expect(clientMeta.optimized['react-fake-client']).toBeFalsy()
+ expect(clientMeta.optimized['react-fake-server']).toBeFalsy()
+ expect(ssrMeta.optimized['react-fake-server']).toBeFalsy()
+ expect(ssrMeta.optimized['react-fake-client']).toBeFalsy()
+
+ envs.forEach((env) => {
+ const filePath = path.resolve(testDir, `src/entry-${env}.tsx`)
+ const originalContent = readFile(filePath)
+ fs.writeFileSync(
+ filePath,
+ `import 'react-fake-${env}'\n${originalContent}`,
+ 'utf-8',
+ )
+ onTestFinished(() => {
+ fs.writeFileSync(filePath, originalContent, 'utf-8')
+ })
+ })
+
+ await untilUpdated(
+ () =>
+ serverLogs
+ .map(
+ (log) =>
+ stripVTControlCharacters(log).match(
+ /new dependencies optimized: (react-fake-.*)/,
+ )?.[1],
+ )
+ .filter(Boolean)
+ .join(', '),
+ 'react-fake-server, react-fake-client',
+ )
+
+ const clientMetaNew = readDepOptimizationMetadata('client')
+ const ssrMetaNew = readDepOptimizationMetadata('ssr')
+ expect(clientMetaNew.optimized['react-fake-client']).toBeTruthy()
+ expect(clientMetaNew.optimized['react-fake-server']).toBeFalsy()
+ expect(ssrMetaNew.optimized['react-fake-server']).toBeTruthy()
+ expect(ssrMetaNew.optimized['react-fake-client']).toBeFalsy()
+ })
+})
diff --git a/playground/environment-react-ssr/index.html b/playground/environment-react-ssr/index.html
new file mode 100644
index 00000000000000..9f4d44a675c1b1
--- /dev/null
+++ b/playground/environment-react-ssr/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ environment-react-ssr
+
+
+
+
+
+
diff --git a/playground/environment-react-ssr/package.json b/playground/environment-react-ssr/package.json
new file mode 100644
index 00000000000000..a5b9989f3bafc3
--- /dev/null
+++ b/playground/environment-react-ssr/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@vitejs/test-environment-react-ssr",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build --app",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "@types/react": "^19.1.4",
+ "@types/react-dom": "^19.1.4",
+ "react": "^19.1.0",
+ "react-fake-client": "npm:react@^19.1.0",
+ "react-fake-server": "npm:react@^19.1.0",
+ "react-dom": "^19.1.0"
+ }
+}
diff --git a/playground/environment-react-ssr/src/entry-client.tsx b/playground/environment-react-ssr/src/entry-client.tsx
new file mode 100644
index 00000000000000..e33d677abfbab2
--- /dev/null
+++ b/playground/environment-react-ssr/src/entry-client.tsx
@@ -0,0 +1,12 @@
+import ReactDomClient from 'react-dom/client'
+import React from 'react'
+import Root from './root'
+
+async function main() {
+ const el = document.getElementById('root')
+ React.startTransition(() => {
+ ReactDomClient.hydrateRoot(el!, )
+ })
+}
+
+main()
diff --git a/playground/environment-react-ssr/src/entry-server.tsx b/playground/environment-react-ssr/src/entry-server.tsx
new file mode 100644
index 00000000000000..588d365a0ca996
--- /dev/null
+++ b/playground/environment-react-ssr/src/entry-server.tsx
@@ -0,0 +1,24 @@
+import ReactDomServer from 'react-dom/server'
+import type { Connect, ViteDevServer } from 'vite'
+import Root from './root'
+
+const handler: Connect.NextHandleFunction = async (_req, res) => {
+ const ssrHtml = ReactDomServer.renderToString( )
+ let html = await importHtml()
+ html = html.replace(//, `${ssrHtml}
`)
+ res.setHeader('content-type', 'text/html').end(html)
+}
+
+export default handler
+
+declare let __globalServer: ViteDevServer
+
+async function importHtml() {
+ if (import.meta.env.DEV) {
+ const mod = await import('/index.html?raw')
+ return __globalServer.transformIndexHtml('/', mod.default)
+ } else {
+ const mod = await import('/dist/client/index.html?raw')
+ return mod.default
+ }
+}
diff --git a/playground/environment-react-ssr/src/root.tsx b/playground/environment-react-ssr/src/root.tsx
new file mode 100644
index 00000000000000..3d077cafb892ba
--- /dev/null
+++ b/playground/environment-react-ssr/src/root.tsx
@@ -0,0 +1,19 @@
+import React from 'react'
+
+export default function Root() {
+ const [count, setCount] = React.useState(0)
+
+ const [hydrated, setHydrated] = React.useState(false)
+ React.useEffect(() => {
+ setHydrated(true)
+ }, [])
+
+ return (
+
+
hydrated: {String(hydrated)}
+
Count: {count}
+
setCount((v) => v - 1)}>-1
+
setCount((v) => v + 1)}>+1
+
+ )
+}
diff --git a/playground/environment-react-ssr/tsconfig.json b/playground/environment-react-ssr/tsconfig.json
new file mode 100644
index 00000000000000..be3ffda527ca91
--- /dev/null
+++ b/playground/environment-react-ssr/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../tsconfig.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "jsx": "react-jsx"
+ }
+}
diff --git a/playground/environment-react-ssr/vite.config.ts b/playground/environment-react-ssr/vite.config.ts
new file mode 100644
index 00000000000000..c2aa0734d85c3e
--- /dev/null
+++ b/playground/environment-react-ssr/vite.config.ts
@@ -0,0 +1,120 @@
+import {
+ type Connect,
+ type Plugin,
+ type PluginOption,
+ createServerModuleRunner,
+ defineConfig,
+} from 'vite'
+
+export default defineConfig((env) => ({
+ clearScreen: false,
+ appType: 'custom',
+ plugins: [
+ vitePluginSsrMiddleware({
+ entry: '/src/entry-server',
+ preview: new URL('./dist/server/index.js', import.meta.url).toString(),
+ }),
+ {
+ name: 'global-server',
+ configureServer(server) {
+ Object.assign(globalThis, { __globalServer: server })
+ },
+ },
+ {
+ name: 'build-client',
+ async buildApp(builder) {
+ await builder.build(builder.environments.client)
+ },
+ },
+ ],
+ resolve: {
+ noExternal: true,
+ },
+ environments: {
+ client: {
+ build: {
+ minify: false,
+ sourcemap: true,
+ outDir: 'dist/client',
+ },
+ },
+ ssr: {
+ optimizeDeps: {
+ noDiscovery: false,
+ },
+ build: {
+ outDir: 'dist/server',
+ // [feedback]
+ // is this still meant to be used?
+ // for example, `ssr: true` seems to make `minify: false` automatically
+ // and also externalization.
+ ssr: true,
+ rollupOptions: {
+ input: {
+ index: '/src/entry-server',
+ },
+ },
+ },
+ },
+ },
+
+ builder: {
+ async buildApp(builder) {
+ if (!builder.environments.client.isBuilt) {
+ throw new Error('Client environment should be built first')
+ }
+ await builder.build(builder.environments.ssr)
+ },
+ },
+}))
+
+// vavite-style ssr middleware plugin
+export function vitePluginSsrMiddleware({
+ entry,
+ preview,
+}: {
+ entry: string
+ preview?: string
+}): PluginOption {
+ const plugin: Plugin = {
+ name: vitePluginSsrMiddleware.name,
+
+ configureServer(server) {
+ const runner = createServerModuleRunner(server.environments.ssr, {
+ hmr: { logger: false },
+ })
+ const importWithRetry = async () => {
+ try {
+ return await runner.import(entry)
+ } catch (e) {
+ if (
+ e instanceof Error &&
+ (e as any).code === 'ERR_OUTDATED_OPTIMIZED_DEP'
+ ) {
+ runner.clearCache()
+ return await importWithRetry()
+ }
+ throw e
+ }
+ }
+ const handler: Connect.NextHandleFunction = async (req, res, next) => {
+ try {
+ const mod = await importWithRetry()
+ await mod['default'](req, res, next)
+ } catch (e) {
+ next(e)
+ }
+ }
+ return () => server.middlewares.use(handler)
+ },
+
+ async configurePreviewServer(server) {
+ if (preview) {
+ const mod = await import(preview)
+ return () => server.middlewares.use(mod.default)
+ }
+ return
+ },
+ }
+ return [plugin]
+}
diff --git a/playground/extensions/__tests__/extensions.spec.ts b/playground/extensions/__tests__/extensions.spec.ts
new file mode 100644
index 00000000000000..a2e229ffcd37f3
--- /dev/null
+++ b/playground/extensions/__tests__/extensions.spec.ts
@@ -0,0 +1,13 @@
+import { expect, test } from 'vitest'
+import { browserLogs, page } from '~utils'
+
+test('should have no 404s', () => {
+ browserLogs.forEach((msg) => {
+ expect(msg).not.toMatch('404')
+ })
+})
+
+test('not contain `.mjs`', async () => {
+ const appHtml = await page.content()
+ expect(appHtml).toMatch('Hello Vite!')
+})
diff --git a/packages/playground/extensions/index.html b/playground/extensions/index.html
similarity index 100%
rename from packages/playground/extensions/index.html
rename to playground/extensions/index.html
diff --git a/playground/extensions/package.json b/playground/extensions/package.json
new file mode 100644
index 00000000000000..67d502cb236204
--- /dev/null
+++ b/playground/extensions/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-extensions",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "vue": "^3.5.13"
+ }
+}
diff --git a/playground/extensions/vite.config.js b/playground/extensions/vite.config.js
new file mode 100644
index 00000000000000..5fdb2c721c7870
--- /dev/null
+++ b/playground/extensions/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ resolve: {
+ alias: [{ find: 'vue', replacement: 'vue/dist/vue.esm-bundler.js' }],
+ extensions: ['.js'],
+ },
+})
diff --git a/playground/external/__tests__/external.spec.ts b/playground/external/__tests__/external.spec.ts
new file mode 100644
index 00000000000000..af307a1ede2c38
--- /dev/null
+++ b/playground/external/__tests__/external.spec.ts
@@ -0,0 +1,26 @@
+import { describe, expect, test } from 'vitest'
+import { browserLogs, isBuild, page } from '~utils'
+
+test('importmap', () => {
+ expect(browserLogs).not.toContain(
+ 'An import map is added after module script load was triggered.',
+ )
+})
+
+test('should have default exports', async () => {
+ expect(await page.textContent('#imported-slash5-exists')).toBe('true')
+ expect(await page.textContent('#imported-slash3-exists')).toBe('true')
+ expect(await page.textContent('#required-slash3-exists')).toBe('true')
+})
+
+describe.runIf(isBuild)('build', () => {
+ test('should externalize imported packages', async () => {
+ // If `vue` is successfully externalized, the page should use the version from the import map
+ expect(await page.textContent('#imported-vue-version')).toBe('3.4.38')
+ })
+
+ test('should externalize required packages', async () => {
+ // If `vue` is successfully externalized, the page should use the version from the import map
+ expect(await page.textContent('#required-vue-version')).toBe('3.4.38')
+ })
+})
diff --git a/playground/external/dep-that-imports/index.js b/playground/external/dep-that-imports/index.js
new file mode 100644
index 00000000000000..096ea934cc4965
--- /dev/null
+++ b/playground/external/dep-that-imports/index.js
@@ -0,0 +1,9 @@
+import { version } from 'vue'
+import slash5 from 'slash5'
+import slash3 from 'slash3'
+
+document.querySelector('#imported-vue-version').textContent = version
+document.querySelector('#imported-slash5-exists').textContent =
+ !!slash5('foo/bar')
+document.querySelector('#imported-slash3-exists').textContent =
+ !!slash3('foo/bar')
diff --git a/playground/external/dep-that-imports/package.json b/playground/external/dep-that-imports/package.json
new file mode 100644
index 00000000000000..2943e16548d267
--- /dev/null
+++ b/playground/external/dep-that-imports/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-dep-that-imports",
+ "private": true,
+ "version": "0.0.0",
+ "dependencies": {
+ "slash3": "npm:slash@^3.0.0",
+ "slash5": "npm:slash@^5.1.0",
+ "vue": "^3.5.13"
+ }
+}
diff --git a/playground/external/dep-that-requires/index.js b/playground/external/dep-that-requires/index.js
new file mode 100644
index 00000000000000..6f0dd6124d829d
--- /dev/null
+++ b/playground/external/dep-that-requires/index.js
@@ -0,0 +1,7 @@
+const { version } = require('vue')
+// require('slash5') // cannot require ESM
+const slash3 = require('slash3')
+
+document.querySelector('#required-vue-version').textContent = version
+document.querySelector('#required-slash3-exists').textContent =
+ !!slash3('foo/bar')
diff --git a/playground/external/dep-that-requires/package.json b/playground/external/dep-that-requires/package.json
new file mode 100644
index 00000000000000..1ec250c5404bb7
--- /dev/null
+++ b/playground/external/dep-that-requires/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-dep-that-requires",
+ "private": true,
+ "version": "0.0.0",
+ "dependencies": {
+ "slash3": "npm:slash@^3.0.0",
+ "slash5": "npm:slash@^5.1.0",
+ "vue": "^3.5.13"
+ }
+}
diff --git a/playground/external/index.html b/playground/external/index.html
new file mode 100644
index 00000000000000..92f3b50e0b6d80
--- /dev/null
+++ b/playground/external/index.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Vite App
+
+
+
+ Imported Vue version:
+ Required Vue version:
+ Imported slash5 exists:
+ Imported slash3 exists:
+ Required slash3 exists:
+
+
+
diff --git a/playground/external/package.json b/playground/external/package.json
new file mode 100644
index 00000000000000..c5612d46f2f381
--- /dev/null
+++ b/playground/external/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@vitejs/test-external",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@vitejs/test-dep-that-imports": "file:./dep-that-imports",
+ "@vitejs/test-dep-that-requires": "file:./dep-that-requires"
+ },
+ "devDependencies": {
+ "slash3": "npm:slash@^3.0.0",
+ "slash5": "npm:slash@^5.1.0",
+ "vite": "workspace:*",
+ "vue": "^3.5.13",
+ "vue34": "npm:vue@~3.4.38"
+ }
+}
diff --git a/playground/external/public/slash@3.0.0.js b/playground/external/public/slash@3.0.0.js
new file mode 100644
index 00000000000000..754082e97c4f82
--- /dev/null
+++ b/playground/external/public/slash@3.0.0.js
@@ -0,0 +1,5 @@
+/* eslint-disable */
+// copied from https://esm.sh/v133/slash@3.0.0/es2022/slash.mjs to reduce network issues in CI
+
+/* esm.sh - esbuild bundle(slash@3.0.0) es2022 production */
+var a=Object.create;var d=Object.defineProperty;var m=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,p=Object.prototype.hasOwnProperty;var A=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),E=(e,t)=>{for(var r in t)d(e,r,{get:t[r],enumerable:!0})},u=(e,t,r,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of x(t))!p.call(e,n)&&n!==r&&d(e,n,{get:()=>t[n],enumerable:!(i=m(t,n))||i.enumerable});return e},o=(e,t,r)=>(u(e,t,"default"),r&&u(r,t,"default")),c=(e,t,r)=>(r=e!=null?a(g(e)):{},u(t||!e||!e.__esModule?d(r,"default",{value:e,enumerable:!0}):r,e));var f=A((h,_)=>{"use strict";_.exports=e=>{let t=/^\\\\\?\\/.test(e),r=/[^\u0000-\u0080]+/.test(e);return t||r?e:e.replace(/\\/g,"/")}});var s={};E(s,{default:()=>P});var L=c(f());o(s,c(f()));var{default:l,...N}=L,P=l!==void 0?l:N;export{P as default};
diff --git a/playground/external/src/main.js b/playground/external/src/main.js
new file mode 100644
index 00000000000000..46d97cebd47915
--- /dev/null
+++ b/playground/external/src/main.js
@@ -0,0 +1,2 @@
+import '@vitejs/test-dep-that-imports'
+import '@vitejs/test-dep-that-requires'
diff --git a/playground/external/vite.config.js b/playground/external/vite.config.js
new file mode 100644
index 00000000000000..4df027fe19431d
--- /dev/null
+++ b/playground/external/vite.config.js
@@ -0,0 +1,49 @@
+import fs from 'node:fs/promises'
+import { defineConfig } from 'vite'
+
+const npmDirectServeConfig = {
+ '/vue@3.4.38.js': 'vue34/dist/vue.runtime.esm-browser.js',
+ '/slash@5.js': 'slash5/index.js',
+}
+/** @type {import('vite').Connect.NextHandleFunction} */
+const serveNpmCodeDirectlyMiddleware = async (req, res, next) => {
+ for (const [url, file] of Object.entries(npmDirectServeConfig)) {
+ if (req.originalUrl === url) {
+ const code = await fs.readFile(
+ new URL(`./node_modules/${file}`, import.meta.url),
+ )
+ res.setHeader('Content-Type', 'text/javascript')
+ res.end(code)
+ return
+ }
+ }
+ next()
+}
+
+export default defineConfig({
+ optimizeDeps: {
+ include: ['dep-that-imports', 'dep-that-requires'],
+ exclude: ['vue', 'slash5'],
+ },
+ build: {
+ minify: false,
+ rollupOptions: {
+ external: ['vue', 'slash3', 'slash5'],
+ },
+ commonjsOptions: {
+ esmExternals: ['vue', 'slash5'],
+ dynamicRequireTargets: ['test-no-op-fdir-glob'],
+ },
+ },
+ plugins: [
+ {
+ name: 'serve-npm-code-directly',
+ configureServer({ middlewares }) {
+ middlewares.use(serveNpmCodeDirectlyMiddleware)
+ },
+ configurePreviewServer({ middlewares }) {
+ middlewares.use(serveNpmCodeDirectlyMiddleware)
+ },
+ },
+ ],
+})
diff --git a/playground/fs-serve/__tests__/base/fs-serve-base.spec.ts b/playground/fs-serve/__tests__/base/fs-serve-base.spec.ts
new file mode 100644
index 00000000000000..2bf1ce2bdb411a
--- /dev/null
+++ b/playground/fs-serve/__tests__/base/fs-serve-base.spec.ts
@@ -0,0 +1,113 @@
+import { beforeAll, describe, expect, test } from 'vitest'
+import testJSON from '../../safe.json'
+import { isServe, page, viteTestUrl } from '~utils'
+
+const stringified = JSON.stringify(testJSON)
+
+describe.runIf(isServe)('main', () => {
+ beforeAll(async () => {
+ const srcPrefix = viteTestUrl.endsWith('/') ? '' : '/'
+ await page.goto(viteTestUrl + srcPrefix + 'src/', {
+ // while networkidle is discouraged, we use here because we're not using playwright's retry-able assertions,
+ // and refactoring the code below to manually retry would be harder to read.
+ waitUntil: 'networkidle',
+ })
+ })
+
+ test('default import', async () => {
+ expect(await page.textContent('.full')).toBe(stringified)
+ })
+
+ test('named import', async () => {
+ expect(await page.textContent('.named')).toBe(testJSON.msg)
+ })
+
+ test('safe fetch', async () => {
+ expect(await page.textContent('.safe-fetch')).toMatch('KEY=safe')
+ expect(await page.textContent('.safe-fetch-status')).toBe('200')
+ })
+
+ test('safe fetch with query', async () => {
+ expect(await page.textContent('.safe-fetch-query')).toMatch('KEY=safe')
+ expect(await page.textContent('.safe-fetch-query-status')).toBe('200')
+ })
+
+ test('safe fetch with special characters', async () => {
+ expect(
+ await page.textContent('.safe-fetch-subdir-special-characters'),
+ ).toMatch('KEY=safe')
+ expect(
+ await page.textContent('.safe-fetch-subdir-special-characters-status'),
+ ).toBe('200')
+ })
+
+ test('unsafe fetch', async () => {
+ expect(await page.textContent('.unsafe-fetch')).toMatch('403 Restricted')
+ expect(await page.textContent('.unsafe-fetch-status')).toBe('403')
+ })
+
+ test('unsafe fetch with special characters (#8498)', async () => {
+ expect(await page.textContent('.unsafe-fetch-8498')).toBe('')
+ expect(await page.textContent('.unsafe-fetch-8498-status')).toBe('404')
+ })
+
+ test('unsafe fetch with special characters 2 (#8498)', async () => {
+ expect(await page.textContent('.unsafe-fetch-8498-2')).toBe('')
+ expect(await page.textContent('.unsafe-fetch-8498-2-status')).toBe('404')
+ })
+
+ test('safe fs fetch', async () => {
+ expect(await page.textContent('.safe-fs-fetch')).toBe(stringified)
+ expect(await page.textContent('.safe-fs-fetch-status')).toBe('200')
+ })
+
+ test('safe fs fetch', async () => {
+ expect(await page.textContent('.safe-fs-fetch-query')).toBe(stringified)
+ expect(await page.textContent('.safe-fs-fetch-query-status')).toBe('200')
+ })
+
+ test('safe fs fetch with special characters', async () => {
+ expect(await page.textContent('.safe-fs-fetch-special-characters')).toBe(
+ stringified,
+ )
+ expect(
+ await page.textContent('.safe-fs-fetch-special-characters-status'),
+ ).toBe('200')
+ })
+
+ test('unsafe fs fetch', async () => {
+ expect(await page.textContent('.unsafe-fs-fetch')).toBe('')
+ expect(await page.textContent('.unsafe-fs-fetch-status')).toBe('403')
+ })
+
+ test('unsafe fs fetch with special characters (#8498)', async () => {
+ expect(await page.textContent('.unsafe-fs-fetch-8498')).toBe('')
+ expect(await page.textContent('.unsafe-fs-fetch-8498-status')).toBe('404')
+ })
+
+ test('unsafe fs fetch with special characters 2 (#8498)', async () => {
+ expect(await page.textContent('.unsafe-fs-fetch-8498-2')).toBe('')
+ expect(await page.textContent('.unsafe-fs-fetch-8498-2-status')).toBe('404')
+ })
+
+ test('nested entry', async () => {
+ expect(await page.textContent('.nested-entry')).toBe('foobar')
+ })
+
+ test('denied', async () => {
+ expect(await page.textContent('.unsafe-dotenv')).toBe('403')
+ })
+
+ test('denied EnV casing', async () => {
+ // It is 403 in case insensitive system, 404 in others
+ const code = await page.textContent('.unsafe-dotEnV-casing')
+ expect(code === '403' || code === '404').toBeTruthy()
+ })
+})
+
+describe('fetch', () => {
+ test('serve with configured headers', async () => {
+ const res = await fetch(viteTestUrl + '/src/')
+ expect(res.headers.get('x-served-by')).toBe('vite')
+ })
+})
diff --git a/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts b/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts
new file mode 100644
index 00000000000000..fb60922e86e1ae
--- /dev/null
+++ b/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts
@@ -0,0 +1,17 @@
+import { describe, expect, test } from 'vitest'
+import { isServe, page, viteTestUrl } from '~utils'
+
+describe.runIf(isServe)('main', () => {
+ test('**/deny/** should deny src/deny/deny.txt', async () => {
+ const res = await page.request.fetch(
+ new URL('/src/deny/deny.txt', viteTestUrl).href,
+ )
+ expect(res.status()).toBe(403)
+ })
+ test('**/deny/** should deny src/deny/.deny', async () => {
+ const res = await page.request.fetch(
+ new URL('/src/deny/.deny', viteTestUrl).href,
+ )
+ expect(res.status()).toBe(403)
+ })
+})
diff --git a/playground/fs-serve/__tests__/fs-serve.spec.ts b/playground/fs-serve/__tests__/fs-serve.spec.ts
new file mode 100644
index 00000000000000..d88f3eb78d7e72
--- /dev/null
+++ b/playground/fs-serve/__tests__/fs-serve.spec.ts
@@ -0,0 +1,494 @@
+import net from 'node:net'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import http from 'node:http'
+import {
+ afterEach,
+ beforeAll,
+ beforeEach,
+ describe,
+ expect,
+ test,
+} from 'vitest'
+import type { Page } from 'playwright-chromium'
+import WebSocket from 'ws'
+import testJSON from '../safe.json'
+import { browser, isServe, page, viteServer, viteTestUrl } from '~utils'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+const getViteTestIndexHtmlUrl = () => {
+ const srcPrefix = viteTestUrl.endsWith('/') ? '' : '/'
+ // NOTE: viteTestUrl is set lazily
+ return viteTestUrl + srcPrefix + 'src/'
+}
+
+const stringified = JSON.stringify(testJSON)
+
+describe.runIf(isServe)('main', () => {
+ beforeAll(async () => {
+ await page.goto(getViteTestIndexHtmlUrl())
+ })
+
+ test('default import', async () => {
+ expect(await page.textContent('.full')).toBe(stringified)
+ })
+
+ test('named import', async () => {
+ expect(await page.textContent('.named')).toBe(testJSON.msg)
+ })
+
+ test('safe fetch', async () => {
+ expect(await page.textContent('.safe-fetch')).toMatch('KEY=safe')
+ expect(await page.textContent('.safe-fetch-status')).toBe('200')
+ })
+
+ test('safe fetch with query', async () => {
+ expect(await page.textContent('.safe-fetch-query')).toMatch('KEY=safe')
+ expect(await page.textContent('.safe-fetch-query-status')).toBe('200')
+ })
+
+ test('safe fetch with special characters', async () => {
+ expect(
+ await page.textContent('.safe-fetch-subdir-special-characters'),
+ ).toMatch('KEY=safe')
+ expect(
+ await page.textContent('.safe-fetch-subdir-special-characters-status'),
+ ).toBe('200')
+ })
+
+ test('unsafe fetch', async () => {
+ expect(await page.textContent('.unsafe-fetch')).toMatch('403 Restricted')
+ expect(await page.textContent('.unsafe-fetch-status')).toBe('403')
+ })
+
+ test('unsafe fetch with special characters (#8498)', async () => {
+ expect(await page.textContent('.unsafe-fetch-8498')).toBe('')
+ expect(await page.textContent('.unsafe-fetch-8498-status')).toBe('404')
+ })
+
+ test('unsafe fetch with special characters 2 (#8498)', async () => {
+ expect(await page.textContent('.unsafe-fetch-8498-2')).toBe('')
+ expect(await page.textContent('.unsafe-fetch-8498-2-status')).toBe('404')
+ })
+
+ test('unsafe fetch import inline', async () => {
+ expect(await page.textContent('.unsafe-fetch-import-inline-status')).toBe(
+ '403',
+ )
+ })
+
+ test('unsafe fetch raw query import', async () => {
+ expect(
+ await page.textContent('.unsafe-fetch-raw-query-import-status'),
+ ).toBe('403')
+ })
+
+ test('unsafe fetch ?.svg?import', async () => {
+ expect(
+ await page.textContent('.unsafe-fetch-query-dot-svg-import-status'),
+ ).toBe('403')
+ })
+
+ test('unsafe fetch .svg?import', async () => {
+ expect(await page.textContent('.unsafe-fetch-svg-status')).toBe('403')
+ })
+
+ test('safe fs fetch', async () => {
+ expect(await page.textContent('.safe-fs-fetch')).toBe(stringified)
+ expect(await page.textContent('.safe-fs-fetch-status')).toBe('200')
+ })
+
+ test('safe fs fetch', async () => {
+ expect(await page.textContent('.safe-fs-fetch-query')).toBe(stringified)
+ expect(await page.textContent('.safe-fs-fetch-query-status')).toBe('200')
+ })
+
+ test('safe fs fetch with special characters', async () => {
+ expect(await page.textContent('.safe-fs-fetch-special-characters')).toBe(
+ stringified,
+ )
+ expect(
+ await page.textContent('.safe-fs-fetch-special-characters-status'),
+ ).toBe('200')
+ })
+
+ test('unsafe fs fetch', async () => {
+ expect(await page.textContent('.unsafe-fs-fetch')).toBe('')
+ expect(await page.textContent('.unsafe-fs-fetch-status')).toBe('403')
+ })
+
+ test('unsafe fs fetch', async () => {
+ expect(await page.textContent('.unsafe-fs-fetch-raw')).toBe('')
+ expect(await page.textContent('.unsafe-fs-fetch-raw-status')).toBe('403')
+ })
+
+ test('unsafe fs fetch query 1', async () => {
+ expect(await page.textContent('.unsafe-fs-fetch-raw-query1')).toBe('')
+ expect(await page.textContent('.unsafe-fs-fetch-raw-query1-status')).toBe(
+ '403',
+ )
+ })
+
+ test('unsafe fs fetch query 2', async () => {
+ expect(await page.textContent('.unsafe-fs-fetch-raw-query2')).toBe('')
+ expect(await page.textContent('.unsafe-fs-fetch-raw-query2-status')).toBe(
+ '403',
+ )
+ })
+
+ test('unsafe fs fetch with special characters (#8498)', async () => {
+ expect(await page.textContent('.unsafe-fs-fetch-8498')).toBe('')
+ expect(await page.textContent('.unsafe-fs-fetch-8498-status')).toBe('404')
+ })
+
+ test('unsafe fs fetch with special characters 2 (#8498)', async () => {
+ expect(await page.textContent('.unsafe-fs-fetch-8498-2')).toBe('')
+ expect(await page.textContent('.unsafe-fs-fetch-8498-2-status')).toBe('404')
+ })
+
+ test('unsafe fs fetch import inline', async () => {
+ expect(
+ await page.textContent('.unsafe-fs-fetch-import-inline-status'),
+ ).toBe('403')
+ })
+
+ test('unsafe fs fetch import inline wasm init', async () => {
+ expect(
+ await page.textContent('.unsafe-fs-fetch-import-inline-wasm-init-status'),
+ ).toBe('403')
+ })
+
+ test('unsafe fs fetch with relative path after query status', async () => {
+ expect(
+ await page.textContent(
+ '.unsafe-fs-fetch-relative-path-after-query-status',
+ ),
+ ).toBe('403')
+ })
+
+ test('nested entry', async () => {
+ expect(await page.textContent('.nested-entry')).toBe('foobar')
+ })
+
+ test('denied', async () => {
+ expect(await page.textContent('.unsafe-dotenv')).toBe('403')
+ })
+
+ test('denied EnV casing', async () => {
+ // It is 403 in case insensitive system, 404 in others
+ const code = await page.textContent('.unsafe-dotEnV-casing')
+ expect(code === '403' || code === '404').toBeTruthy()
+ })
+
+ test('denied env with ?.svg?.wasm?init', async () => {
+ expect(
+ await page.textContent('.unsafe-dotenv-query-dot-svg-wasm-init'),
+ ).toBe('403')
+ })
+})
+
+describe('fetch', () => {
+ test('serve with configured headers', async () => {
+ const res = await fetch(viteTestUrl + '/src/')
+ expect(res.headers.get('x-served-by')).toBe('vite')
+ })
+})
+
+describe('cross origin', () => {
+ const fetchStatusFromPage = async (page: Page, url: string) => {
+ return await page.evaluate(async (url: string) => {
+ try {
+ const res = await globalThis.fetch(url)
+ return res.status
+ } catch {
+ return -1
+ }
+ }, url)
+ }
+
+ const connectWebSocketFromPage = async (page: Page, url: string) => {
+ return await page.evaluate(async (url: string) => {
+ try {
+ const ws = new globalThis.WebSocket(url, ['vite-hmr'])
+ await new Promise((resolve, reject) => {
+ ws.addEventListener('open', () => {
+ resolve()
+ ws.close()
+ })
+ ws.addEventListener('error', () => {
+ reject()
+ })
+ })
+ return true
+ } catch {
+ return false
+ }
+ }, url)
+ }
+
+ const connectWebSocketFromServer = async (
+ url: string,
+ host: string,
+ origin: string | undefined,
+ ) => {
+ try {
+ const ws = new WebSocket(url, ['vite-hmr'], {
+ headers: {
+ Host: host,
+ ...(origin ? { Origin: origin } : undefined),
+ },
+ })
+ await new Promise((resolve, reject) => {
+ ws.addEventListener('open', () => {
+ resolve()
+ ws.close()
+ })
+ ws.addEventListener('error', () => {
+ reject()
+ })
+ })
+ return true
+ } catch {
+ return false
+ }
+ }
+
+ describe('allowed for same origin', () => {
+ beforeEach(async () => {
+ await page.goto(getViteTestIndexHtmlUrl())
+ })
+
+ test('fetch HTML file', async () => {
+ const status = await fetchStatusFromPage(page, viteTestUrl + '/src/')
+ expect(status).toBe(200)
+ })
+
+ test.runIf(isServe)('fetch JS file', async () => {
+ const status = await fetchStatusFromPage(
+ page,
+ viteTestUrl + '/src/code.js',
+ )
+ expect(status).toBe(200)
+ })
+
+ test.runIf(isServe)('connect WebSocket with valid token', async () => {
+ const token = viteServer.config.webSocketToken
+ const result = await connectWebSocketFromPage(
+ page,
+ `${viteTestUrl}?token=${token}`,
+ )
+ expect(result).toBe(true)
+ })
+
+ test('fetch with allowed hosts', async () => {
+ const viteTestUrlUrl = new URL(viteTestUrl)
+ const res = await fetch(viteTestUrl + '/src/index.html', {
+ headers: { Host: viteTestUrlUrl.host },
+ })
+ expect(res.status).toBe(200)
+ })
+
+ test.runIf(isServe)(
+ 'connect WebSocket with valid token with allowed hosts',
+ async () => {
+ const viteTestUrlUrl = new URL(viteTestUrl)
+ const token = viteServer.config.webSocketToken
+ const result = await connectWebSocketFromServer(
+ `${viteTestUrl}?token=${token}`,
+ viteTestUrlUrl.host,
+ viteTestUrlUrl.origin,
+ )
+ expect(result).toBe(true)
+ },
+ )
+
+ test.runIf(isServe)(
+ 'connect WebSocket without a token without the origin header',
+ async () => {
+ const viteTestUrlUrl = new URL(viteTestUrl)
+ const result = await connectWebSocketFromServer(
+ viteTestUrl,
+ viteTestUrlUrl.host,
+ undefined,
+ )
+ expect(result).toBe(true)
+ },
+ )
+ })
+
+ describe('denied for different origin', async () => {
+ let page2: Page
+ beforeEach(async () => {
+ page2 = await browser.newPage()
+ await page2.goto('http://vite.dev/404')
+ })
+ afterEach(async () => {
+ await page2.close()
+ })
+
+ test('fetch HTML file', async () => {
+ const status = await fetchStatusFromPage(page2, viteTestUrl + '/src/')
+ expect(status).not.toBe(200)
+ })
+
+ test.runIf(isServe)('fetch JS file', async () => {
+ const status = await fetchStatusFromPage(
+ page2,
+ viteTestUrl + '/src/code.js',
+ )
+ expect(status).not.toBe(200)
+ })
+
+ test.runIf(isServe)('connect WebSocket without token', async () => {
+ const result = await connectWebSocketFromPage(page, viteTestUrl)
+ expect(result).toBe(false)
+
+ const result2 = await connectWebSocketFromPage(
+ page,
+ `${viteTestUrl}?token=`,
+ )
+ expect(result2).toBe(false)
+ })
+
+ test.runIf(isServe)('connect WebSocket with invalid token', async () => {
+ const token = viteServer.config.webSocketToken
+ const result = await connectWebSocketFromPage(
+ page,
+ `${viteTestUrl}?token=${'t'.repeat(token.length)}`,
+ )
+ expect(result).toBe(false)
+
+ const result2 = await connectWebSocketFromPage(
+ page,
+ `${viteTestUrl}?token=${'t'.repeat(token.length)}t`, // different length
+ )
+ expect(result2).toBe(false)
+ })
+
+ test('fetch with non-allowed hosts', async () => {
+ // NOTE: fetch cannot be used here as `fetch` sets the correct `Host` header
+ const res = await new Promise((resolve, reject) => {
+ http
+ .get(
+ viteTestUrl + '/src/index.html',
+ {
+ headers: {
+ Host: 'vite.dev',
+ },
+ },
+ (res) => {
+ resolve(res)
+ },
+ )
+ .on('error', (e) => {
+ reject(e)
+ })
+ })
+ expect(res.statusCode).toBe(403)
+ })
+
+ test.runIf(isServe)(
+ 'connect WebSocket with valid token with non-allowed hosts',
+ async () => {
+ const token = viteServer.config.webSocketToken
+ const result = await connectWebSocketFromServer(
+ `${viteTestUrl}?token=${token}`,
+ 'vite.dev',
+ 'http://vite.dev',
+ )
+ expect(result).toBe(false)
+
+ const result2 = await connectWebSocketFromServer(
+ `${viteTestUrl}?token=${token}`,
+ 'vite.dev',
+ undefined,
+ )
+ expect(result2).toBe(false)
+ },
+ )
+ })
+})
+
+describe.runIf(isServe)('invalid request', () => {
+ const sendRawRequest = async (baseUrl: string, requestTarget: string) => {
+ return new Promise((resolve, reject) => {
+ const parsedUrl = new URL(baseUrl)
+
+ const buf: Buffer[] = []
+ const client = net.createConnection(
+ { port: +parsedUrl.port, host: parsedUrl.hostname },
+ () => {
+ client.write(
+ [
+ `GET ${encodeURI(requestTarget)} HTTP/1.1`,
+ `Host: ${parsedUrl.host}`,
+ 'Connection: Close',
+ '\r\n',
+ ].join('\r\n'),
+ )
+ },
+ )
+ client.on('data', (data) => {
+ buf.push(data)
+ })
+ client.on('end', (hadError) => {
+ if (!hadError) {
+ resolve(Buffer.concat(buf).toString())
+ }
+ })
+ client.on('error', (err) => {
+ reject(err)
+ })
+ })
+ }
+
+ const root = path
+ .resolve(__dirname.replace('playground', 'playground-temp'), '..')
+ .replace(/\\/g, '/')
+
+ test('request with sendRawRequest should work', async () => {
+ const response = await sendRawRequest(viteTestUrl, '/src/safe.txt')
+ expect(response).toContain('HTTP/1.1 200 OK')
+ expect(response).toContain('KEY=safe')
+ })
+
+ test('request with sendRawRequest should work with /@fs/', async () => {
+ const response = await sendRawRequest(
+ viteTestUrl,
+ path.posix.join('/@fs/', root, 'root/src/safe.txt'),
+ )
+ expect(response).toContain('HTTP/1.1 200 OK')
+ expect(response).toContain('KEY=safe')
+ })
+
+ test('should reject request that has # in request-target', async () => {
+ const response = await sendRawRequest(
+ viteTestUrl,
+ '/src/safe.txt#/../../unsafe.txt',
+ )
+ expect(response).toContain('HTTP/1.1 400 Bad Request')
+ })
+
+ test('should reject request that has # in request-target with /@fs/', async () => {
+ const response = await sendRawRequest(
+ viteTestUrl,
+ path.posix.join('/@fs/', root, 'root/src/safe.txt') +
+ '#/../../unsafe.txt',
+ )
+ expect(response).toContain('HTTP/1.1 400 Bad Request')
+ })
+
+ test('should deny request to denied file when a request has /.', async () => {
+ const response = await sendRawRequest(viteTestUrl, '/src/dummy.crt/.')
+ expect(response).toContain('HTTP/1.1 403 Forbidden')
+ })
+
+ test('should deny request with /@fs/ to denied file when a request has /.', async () => {
+ const response = await sendRawRequest(
+ viteTestUrl,
+ path.posix.join('/@fs/', root, 'root/src/dummy.crt/') + '.',
+ )
+ expect(response).toContain('HTTP/1.1 403 Forbidden')
+ })
+})
diff --git a/packages/playground/fs-serve/entry.js b/playground/fs-serve/entry.js
similarity index 100%
rename from packages/playground/fs-serve/entry.js
rename to playground/fs-serve/entry.js
diff --git a/packages/playground/fs-serve/nested/foo.js b/playground/fs-serve/nested/foo.js
similarity index 100%
rename from packages/playground/fs-serve/nested/foo.js
rename to playground/fs-serve/nested/foo.js
diff --git a/playground/fs-serve/package.json b/playground/fs-serve/package.json
new file mode 100644
index 00000000000000..ff901dfc03e93f
--- /dev/null
+++ b/playground/fs-serve/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@vitejs/test-fs-serve",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite root",
+ "build": "vite build root",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview root",
+ "dev:base": "vite root --config ./root/vite.config-base.js",
+ "build:base": "vite build root --config ./root/vite.config-base.js",
+ "preview:base": "vite preview root --config ./root/vite.config-base.js",
+ "dev:deny": "vite root --config ./root/vite.config-deny.js",
+ "build:deny": "vite build root --config ./root/vite.config-deny.js",
+ "preview:deny": "vite preview root --config ./root/vite.config-deny.js"
+ },
+ "devDependencies": {
+ "ws": "^8.18.2"
+ }
+}
diff --git a/packages/playground/fs-serve/root/unsafe.txt b/playground/fs-serve/root/src/.env
similarity index 100%
rename from packages/playground/fs-serve/root/unsafe.txt
rename to playground/fs-serve/root/src/.env
diff --git a/playground/fs-serve/root/src/code.js b/playground/fs-serve/root/src/code.js
new file mode 100644
index 00000000000000..33fd8df878207b
--- /dev/null
+++ b/playground/fs-serve/root/src/code.js
@@ -0,0 +1 @@
+// code.js
diff --git a/playground/fs-serve/root/src/deny/.deny b/playground/fs-serve/root/src/deny/.deny
new file mode 100644
index 00000000000000..73bd3960853c61
--- /dev/null
+++ b/playground/fs-serve/root/src/deny/.deny
@@ -0,0 +1 @@
+.deny
diff --git a/playground/fs-serve/root/src/deny/deny.txt b/playground/fs-serve/root/src/deny/deny.txt
new file mode 100644
index 00000000000000..f9df83416f8a72
--- /dev/null
+++ b/playground/fs-serve/root/src/deny/deny.txt
@@ -0,0 +1 @@
+deny
diff --git a/playground/fs-serve/root/src/dummy.crt b/playground/fs-serve/root/src/dummy.crt
new file mode 100644
index 00000000000000..d97c5eada5d8c5
--- /dev/null
+++ b/playground/fs-serve/root/src/dummy.crt
@@ -0,0 +1 @@
+secret
diff --git a/playground/fs-serve/root/src/index.html b/playground/fs-serve/root/src/index.html
new file mode 100644
index 00000000000000..23be5e6b1bde17
--- /dev/null
+++ b/playground/fs-serve/root/src/index.html
@@ -0,0 +1,425 @@
+
+
+Normal Import
+
+
+
+Safe Fetch
+
+
+
+
+
+Safe Fetch Subdirectory
+
+
+
+
+
+Unsafe Fetch
+
+
+
+
+
+
+
+
+
+
+
+Safe /@fs/ Fetch
+
+
+
+
+
+
+
+Unsafe /@fs/ Fetch
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Nested Entry
+
+
+Denied
+
+
+
+
+
diff --git a/packages/playground/fs-serve/root/src/safe.txt b/playground/fs-serve/root/src/safe.txt
similarity index 100%
rename from packages/playground/fs-serve/root/src/safe.txt
rename to playground/fs-serve/root/src/safe.txt
diff --git a/packages/playground/fs-serve/safe.json "b/playground/fs-serve/root/src/special characters \303\245\303\244\303\266/safe.json"
similarity index 100%
rename from packages/playground/fs-serve/safe.json
rename to "playground/fs-serve/root/src/special characters \303\245\303\244\303\266/safe.json"
diff --git a/packages/playground/fs-serve/root/src/subdir/safe.txt "b/playground/fs-serve/root/src/special characters \303\245\303\244\303\266/safe.txt"
similarity index 100%
rename from packages/playground/fs-serve/root/src/subdir/safe.txt
rename to "playground/fs-serve/root/src/special characters \303\245\303\244\303\266/safe.txt"
diff --git a/playground/fs-serve/root/src/subdir/safe.txt b/playground/fs-serve/root/src/subdir/safe.txt
new file mode 100644
index 00000000000000..3f3d0607101642
--- /dev/null
+++ b/playground/fs-serve/root/src/subdir/safe.txt
@@ -0,0 +1 @@
+KEY=safe
diff --git a/playground/fs-serve/root/unsafe.svg b/playground/fs-serve/root/unsafe.svg
new file mode 100644
index 00000000000000..e80a9f4170bf1f
--- /dev/null
+++ b/playground/fs-serve/root/unsafe.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/playground/fs-serve/root/unsafe.txt b/playground/fs-serve/root/unsafe.txt
new file mode 100644
index 00000000000000..d0e0cfd28cbe57
--- /dev/null
+++ b/playground/fs-serve/root/unsafe.txt
@@ -0,0 +1 @@
+KEY=unsafe
diff --git a/playground/fs-serve/root/vite.config-base.js b/playground/fs-serve/root/vite.config-base.js
new file mode 100644
index 00000000000000..06573edb61854e
--- /dev/null
+++ b/playground/fs-serve/root/vite.config-base.js
@@ -0,0 +1,36 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+const BASE = '/base/'
+
+export default defineConfig({
+ base: BASE,
+ build: {
+ rollupOptions: {
+ input: {
+ main: path.resolve(__dirname, 'src/index.html'),
+ },
+ },
+ },
+ server: {
+ fs: {
+ strict: true,
+ allow: [path.resolve(__dirname, 'src')],
+ },
+ hmr: {
+ overlay: false,
+ },
+ headers: {
+ 'x-served-by': 'vite',
+ },
+ },
+ preview: {
+ headers: {
+ 'x-served-by': 'vite',
+ },
+ },
+ define: {
+ ROOT: JSON.stringify(path.dirname(__dirname).replace(/\\/g, '/')),
+ BASE: JSON.stringify(BASE),
+ },
+})
diff --git a/playground/fs-serve/root/vite.config-deny.js b/playground/fs-serve/root/vite.config-deny.js
new file mode 100644
index 00000000000000..27501c55f38180
--- /dev/null
+++ b/playground/fs-serve/root/vite.config-deny.js
@@ -0,0 +1,22 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ rollupOptions: {
+ input: {
+ main: path.resolve(__dirname, 'src/index.html'),
+ },
+ },
+ },
+ server: {
+ fs: {
+ strict: true,
+ allow: [path.resolve(__dirname, 'src')],
+ deny: ['**/deny/**'],
+ },
+ },
+ define: {
+ ROOT: JSON.stringify(path.dirname(__dirname).replace(/\\/g, '/')),
+ },
+})
diff --git a/playground/fs-serve/root/vite.config.js b/playground/fs-serve/root/vite.config.js
new file mode 100644
index 00000000000000..79d094d4925b8e
--- /dev/null
+++ b/playground/fs-serve/root/vite.config.js
@@ -0,0 +1,32 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ rollupOptions: {
+ input: {
+ main: path.resolve(__dirname, 'src/index.html'),
+ },
+ },
+ },
+ server: {
+ fs: {
+ strict: true,
+ allow: [path.resolve(__dirname, 'src')],
+ },
+ hmr: {
+ overlay: false,
+ },
+ headers: {
+ 'x-served-by': 'vite',
+ },
+ },
+ preview: {
+ headers: {
+ 'x-served-by': 'vite',
+ },
+ },
+ define: {
+ ROOT: JSON.stringify(path.dirname(__dirname).replace(/\\/g, '/')),
+ },
+})
diff --git a/playground/fs-serve/safe.json b/playground/fs-serve/safe.json
new file mode 100644
index 00000000000000..84f96593c10bad
--- /dev/null
+++ b/playground/fs-serve/safe.json
@@ -0,0 +1,3 @@
+{
+ "msg": "safe"
+}
diff --git a/packages/playground/fs-serve/unsafe.json b/playground/fs-serve/unsafe.json
similarity index 100%
rename from packages/playground/fs-serve/unsafe.json
rename to playground/fs-serve/unsafe.json
diff --git a/playground/glob-import/__tests__/glob-import.spec.ts b/playground/glob-import/__tests__/glob-import.spec.ts
new file mode 100644
index 00000000000000..2078a1ec1c4e79
--- /dev/null
+++ b/playground/glob-import/__tests__/glob-import.spec.ts
@@ -0,0 +1,249 @@
+import path from 'node:path'
+import { readdir } from 'node:fs/promises'
+import { expect, test } from 'vitest'
+import {
+ addFile,
+ editFile,
+ findAssetFile,
+ isBuild,
+ page,
+ removeFile,
+ untilUpdated,
+ withRetry,
+} from '~utils'
+
+const filteredResult = {
+ './alias.js': {
+ default: 'hi',
+ },
+ './foo.js': {
+ msg: 'foo',
+ },
+ "./quote'.js": {
+ msg: 'single-quote',
+ },
+}
+
+const json = {
+ msg: 'baz',
+ default: {
+ msg: 'baz',
+ },
+}
+
+const globWithAlias = {
+ '/dir/alias.js': {
+ default: 'hi',
+ },
+}
+
+const allResult = {
+ // JSON file should be properly transformed
+ '/dir/alias.js': {
+ default: 'hi',
+ },
+ '/dir/baz.json': json,
+ '/dir/foo.css': {},
+ '/dir/foo.js': {
+ msg: 'foo',
+ },
+ '/dir/index.js': isBuild
+ ? {
+ modules: filteredResult,
+ globWithAlias,
+ }
+ : {
+ globWithAlias,
+ modules: filteredResult,
+ },
+ '/dir/nested/bar.js': {
+ modules: {
+ '../baz.json': json,
+ },
+ msg: 'bar',
+ },
+ "/dir/quote'.js": {
+ msg: 'single-quote',
+ },
+}
+
+const nodeModulesResult = {
+ '/dir/node_modules/hoge.js': { msg: 'hoge' },
+}
+
+const rawResult = {
+ '/dir/baz.json': {
+ msg: 'baz',
+ },
+}
+
+const relativeRawResult = {
+ './dir/baz.json': {
+ msg: 'baz',
+ },
+}
+
+test('should work', async () => {
+ await withRetry(async () => {
+ const actual = await page.textContent('.result')
+ expect(JSON.parse(actual)).toStrictEqual(allResult)
+ })
+ await withRetry(async () => {
+ const actualEager = await page.textContent('.result-eager')
+ expect(JSON.parse(actualEager)).toStrictEqual(allResult)
+ })
+ await withRetry(async () => {
+ const actualNodeModules = await page.textContent('.result-node_modules')
+ expect(JSON.parse(actualNodeModules)).toStrictEqual(nodeModulesResult)
+ })
+})
+
+test('import glob raw', async () => {
+ expect(await page.textContent('.globraw')).toBe(
+ JSON.stringify(rawResult, null, 2),
+ )
+})
+
+test('import property access', async () => {
+ expect(await page.textContent('.property-access')).toBe(
+ JSON.stringify(rawResult['/dir/baz.json'], null, 2),
+ )
+})
+
+test('import relative glob raw', async () => {
+ expect(await page.textContent('.relative-glob-raw')).toBe(
+ JSON.stringify(relativeRawResult, null, 2),
+ )
+})
+
+test('unassigned import processes', async () => {
+ expect(await page.textContent('.side-effect-result')).toBe(
+ 'Hello from side effect',
+ )
+})
+
+test('import glob in package', async () => {
+ expect(await page.textContent('.in-package')).toBe(
+ JSON.stringify(['/pkg-pages/foo.js']),
+ )
+})
+
+if (!isBuild) {
+ test('hmr for adding/removing files', async () => {
+ const resultElement = page.locator('.result')
+
+ addFile('dir/a.js', '')
+ await withRetry(async () => {
+ const actualAdd = await resultElement.textContent()
+ expect(JSON.parse(actualAdd)).toStrictEqual({
+ '/dir/a.js': {},
+ ...allResult,
+ '/dir/index.js': {
+ ...allResult['/dir/index.js'],
+ modules: {
+ './a.js': {},
+ ...allResult['/dir/index.js'].modules,
+ },
+ },
+ })
+ })
+
+ // edit the added file
+ editFile('dir/a.js', () => 'export const msg ="a"')
+ await withRetry(async () => {
+ const actualEdit = await resultElement.textContent()
+ expect(JSON.parse(actualEdit)).toStrictEqual({
+ '/dir/a.js': {
+ msg: 'a',
+ },
+ ...allResult,
+ '/dir/index.js': {
+ ...allResult['/dir/index.js'],
+ modules: {
+ './a.js': {
+ msg: 'a',
+ },
+ ...allResult['/dir/index.js'].modules,
+ },
+ },
+ })
+ })
+
+ removeFile('dir/a.js')
+ await withRetry(async () => {
+ const actualRemove = await resultElement.textContent()
+ expect(JSON.parse(actualRemove)).toStrictEqual(allResult)
+ })
+ })
+
+ test('no hmr for adding/removing files', async () => {
+ let request = page.waitForResponse(/dir\/index\.js$/, { timeout: 200 })
+ addFile('nohmr.js', '')
+ let response = await request.catch(() => ({ status: () => -1 }))
+ expect(response.status()).toBe(-1)
+
+ request = page.waitForResponse(/dir\/index\.js$/, { timeout: 200 })
+ removeFile('nohmr.js')
+ response = await request.catch(() => ({ status: () => -1 }))
+ expect(response.status()).toBe(-1)
+ })
+
+ test('hmr for adding/removing files in package', async () => {
+ const resultElement = page.locator('.in-package')
+
+ addFile('pkg-pages/bar.js', '// empty')
+ await untilUpdated(
+ () => resultElement.textContent(),
+ JSON.stringify(['/pkg-pages/foo.js', '/pkg-pages/bar.js'].sort()),
+ )
+
+ removeFile('pkg-pages/bar.js')
+ await untilUpdated(
+ () => resultElement.textContent(),
+ JSON.stringify(['/pkg-pages/foo.js']),
+ )
+ })
+}
+
+test('tree-shake eager css', async () => {
+ expect(await page.textContent('.no-tree-shake-eager-css-result')).toMatch(
+ '.no-tree-shake-eager-css',
+ )
+
+ if (isBuild) {
+ const content = findAssetFile(/index-[-\w]+\.js/)
+ expect(content).not.toMatch('.tree-shake-eager-css')
+ }
+})
+
+test('escapes special chars in globs without mangling user supplied glob suffix', async () => {
+ // the escape dir contains subdirectories where each has a name that needs escaping for glob safety
+ // inside each of them is a glob.js that exports the result of a relative glob `./**/*.js`
+ // and an alias glob `@escape__mod/**/*.js`. The matching aliases are generated in vite.config.ts
+ // index.html has a script that loads all these glob.js files and prints the globs that returned the expected result
+ // this test finally compares the printed output of index.js with the list of directories with special chars,
+ // expecting that they all work
+ const files = await readdir(path.join(__dirname, '..', 'escape'), {
+ withFileTypes: true,
+ })
+ const expectedNames = files
+ .filter((f) => f.isDirectory())
+ .map((f) => `/escape/${f.name}/glob.js`)
+ .sort()
+ const foundRelativeNames = (await page.textContent('.escape-relative'))
+ .split('\n')
+ .sort()
+ expect(expectedNames).toEqual(foundRelativeNames)
+ const foundAliasNames = (await page.textContent('.escape-alias'))
+ .split('\n')
+ .sort()
+ expect(expectedNames).toEqual(foundAliasNames)
+})
+
+test('subpath imports', async () => {
+ expect(await page.textContent('.subpath-imports')).toMatch('bar foo')
+})
+
+test('#alias imports', async () => {
+ expect(await page.textContent('.hash-alias-imports')).toMatch('bar foo')
+})
diff --git a/packages/playground/glob-import/dir/alias.js b/playground/glob-import/dir/alias.js
similarity index 100%
rename from packages/playground/glob-import/dir/alias.js
rename to playground/glob-import/dir/alias.js
diff --git a/packages/playground/glob-import/dir/baz.json b/playground/glob-import/dir/baz.json
similarity index 100%
rename from packages/playground/glob-import/dir/baz.json
rename to playground/glob-import/dir/baz.json
diff --git a/playground/glob-import/dir/foo.css b/playground/glob-import/dir/foo.css
new file mode 100644
index 00000000000000..94ff8d4b4895c6
--- /dev/null
+++ b/playground/glob-import/dir/foo.css
@@ -0,0 +1,3 @@
+.foo {
+ color: blue;
+}
diff --git a/packages/playground/glob-import/dir/foo.js b/playground/glob-import/dir/foo.js
similarity index 100%
rename from packages/playground/glob-import/dir/foo.js
rename to playground/glob-import/dir/foo.js
diff --git a/playground/glob-import/dir/index.js b/playground/glob-import/dir/index.js
new file mode 100644
index 00000000000000..94ca66f1017093
--- /dev/null
+++ b/playground/glob-import/dir/index.js
@@ -0,0 +1,11 @@
+const modules = import.meta.glob('./*.(js|ts)', { eager: true })
+const globWithAlias = import.meta.glob('@dir/al*.js', { eager: true })
+
+// test negative glob
+import.meta.glob(['@dir/*.js', '!@dir/x.js'])
+import.meta.glob(['!@dir/x.js', '@dir/*.js'])
+
+// test for sourcemap
+console.log('hello')
+
+export { modules, globWithAlias }
diff --git a/playground/glob-import/dir/nested/bar.js b/playground/glob-import/dir/nested/bar.js
new file mode 100644
index 00000000000000..bb23a5a141de8e
--- /dev/null
+++ b/playground/glob-import/dir/nested/bar.js
@@ -0,0 +1,4 @@
+const modules = import.meta.glob('../*.json', { eager: true })
+
+export const msg = 'bar'
+export { modules }
diff --git a/packages/playground/glob-import/dir/node_modules/hoge.js b/playground/glob-import/dir/node_modules/hoge.js
similarity index 100%
rename from packages/playground/glob-import/dir/node_modules/hoge.js
rename to playground/glob-import/dir/node_modules/hoge.js
diff --git a/playground/glob-import/dir/quote'.js b/playground/glob-import/dir/quote'.js
new file mode 100644
index 00000000000000..deb63c3bcad695
--- /dev/null
+++ b/playground/glob-import/dir/quote'.js
@@ -0,0 +1 @@
+export const msg = 'single-quote'
diff --git a/playground/glob-import/escape/(parenthesis)/glob.js b/playground/glob-import/escape/(parenthesis)/glob.js
new file mode 100644
index 00000000000000..9e0d925c4d1026
--- /dev/null
+++ b/playground/glob-import/escape/(parenthesis)/glob.js
@@ -0,0 +1,5 @@
+const relative = import.meta.glob('./**/*.js', { eager: true })
+const alias = import.meta.glob('@escape_(parenthesis)_mod/**/*.js', {
+ eager: true,
+})
+export { relative, alias }
diff --git a/playground/glob-import/escape/(parenthesis)/mod/index.js b/playground/glob-import/escape/(parenthesis)/mod/index.js
new file mode 100644
index 00000000000000..4eeb2ac0e1dbb4
--- /dev/null
+++ b/playground/glob-import/escape/(parenthesis)/mod/index.js
@@ -0,0 +1 @@
+export const msg = 'foo'
diff --git a/playground/glob-import/escape/[brackets]/glob.js b/playground/glob-import/escape/[brackets]/glob.js
new file mode 100644
index 00000000000000..320cf021f9db77
--- /dev/null
+++ b/playground/glob-import/escape/[brackets]/glob.js
@@ -0,0 +1,5 @@
+const relative = import.meta.glob('./**/*.js', { eager: true })
+const alias = import.meta.glob('@escape_[brackets]_mod/**/*.js', {
+ eager: true,
+})
+export { relative, alias }
diff --git a/playground/glob-import/escape/[brackets]/mod/index.js b/playground/glob-import/escape/[brackets]/mod/index.js
new file mode 100644
index 00000000000000..4eeb2ac0e1dbb4
--- /dev/null
+++ b/playground/glob-import/escape/[brackets]/mod/index.js
@@ -0,0 +1 @@
+export const msg = 'foo'
diff --git a/playground/glob-import/escape/{curlies}/glob.js b/playground/glob-import/escape/{curlies}/glob.js
new file mode 100644
index 00000000000000..a6d286001567e9
--- /dev/null
+++ b/playground/glob-import/escape/{curlies}/glob.js
@@ -0,0 +1,3 @@
+const relative = import.meta.glob('./**/*.js', { eager: true })
+const alias = import.meta.glob('@escape_{curlies}_mod/**/*.js', { eager: true })
+export { relative, alias }
diff --git a/playground/glob-import/escape/{curlies}/mod/index.js b/playground/glob-import/escape/{curlies}/mod/index.js
new file mode 100644
index 00000000000000..4eeb2ac0e1dbb4
--- /dev/null
+++ b/playground/glob-import/escape/{curlies}/mod/index.js
@@ -0,0 +1 @@
+export const msg = 'foo'
diff --git a/playground/glob-import/import-meta-glob-pkg/index.js b/playground/glob-import/import-meta-glob-pkg/index.js
new file mode 100644
index 00000000000000..44705cf18f9f22
--- /dev/null
+++ b/playground/glob-import/import-meta-glob-pkg/index.js
@@ -0,0 +1,4 @@
+export const g = import.meta.glob('/pkg-pages/*.js')
+document.querySelector('.in-package').textContent = JSON.stringify(
+ Object.keys(g).sort(),
+)
diff --git a/playground/glob-import/import-meta-glob-pkg/package.json b/playground/glob-import/import-meta-glob-pkg/package.json
new file mode 100644
index 00000000000000..7138de851543cf
--- /dev/null
+++ b/playground/glob-import/import-meta-glob-pkg/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vitejs/test-import-meta-glob-pkg",
+ "type": "module",
+ "main": "./index.js"
+}
diff --git a/playground/glob-import/imports-path/bar.js b/playground/glob-import/imports-path/bar.js
new file mode 100644
index 00000000000000..4548a26ba14dc8
--- /dev/null
+++ b/playground/glob-import/imports-path/bar.js
@@ -0,0 +1 @@
+export default 'bar'
diff --git a/playground/glob-import/imports-path/foo.js b/playground/glob-import/imports-path/foo.js
new file mode 100644
index 00000000000000..7e942cf45c8a37
--- /dev/null
+++ b/playground/glob-import/imports-path/foo.js
@@ -0,0 +1 @@
+export default 'foo'
diff --git a/playground/glob-import/index.html b/playground/glob-import/index.html
new file mode 100644
index 00000000000000..8f8d833b56625b
--- /dev/null
+++ b/playground/glob-import/index.html
@@ -0,0 +1,169 @@
+Glob import
+Normal
+
+Eager
+
+node_modules
+
+Raw
+
+Property access
+
+Relative raw
+
+Side effect
+
+Tree shake Eager CSS
+Should be orange
+Should be orange
+
+Escape relative glob
+
+Escape alias glob
+
+Subpath imports
+
+#alias imports
+
+In package
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/glob-import/no-tree-shake.css b/playground/glob-import/no-tree-shake.css
new file mode 100644
index 00000000000000..9075733e138c5a
--- /dev/null
+++ b/playground/glob-import/no-tree-shake.css
@@ -0,0 +1,3 @@
+.no-tree-shake-eager-css {
+ color: orange;
+}
diff --git a/playground/glob-import/package.json b/playground/glob-import/package.json
new file mode 100644
index 00000000000000..d71d01109270f1
--- /dev/null
+++ b/playground/glob-import/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@vitejs/test-import-context",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "imports": {
+ "#imports/*": "./imports-path/*"
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@vitejs/test-import-meta-glob-pkg": "file:./import-meta-glob-pkg"
+ }
+}
diff --git a/playground/glob-import/pkg-pages/foo.js b/playground/glob-import/pkg-pages/foo.js
new file mode 100644
index 00000000000000..8b1a393741c96c
--- /dev/null
+++ b/playground/glob-import/pkg-pages/foo.js
@@ -0,0 +1 @@
+// empty
diff --git a/playground/glob-import/side-effect/writedom.js b/playground/glob-import/side-effect/writedom.js
new file mode 100644
index 00000000000000..e2ab04ba7f5cbe
--- /dev/null
+++ b/playground/glob-import/side-effect/writedom.js
@@ -0,0 +1,4 @@
+/* global document */
+document &&
+ (document.querySelector('.side-effect-result').textContent =
+ 'Hello from side effect')
diff --git a/playground/glob-import/side-effect/writetodom.js b/playground/glob-import/side-effect/writetodom.js
new file mode 100644
index 00000000000000..e2ab04ba7f5cbe
--- /dev/null
+++ b/playground/glob-import/side-effect/writetodom.js
@@ -0,0 +1,4 @@
+/* global document */
+document &&
+ (document.querySelector('.side-effect-result').textContent =
+ 'Hello from side effect')
diff --git a/playground/glob-import/tree-shake.css b/playground/glob-import/tree-shake.css
new file mode 100644
index 00000000000000..84f24297e4efce
--- /dev/null
+++ b/playground/glob-import/tree-shake.css
@@ -0,0 +1,3 @@
+.tree-shake-eager-css {
+ color: orange;
+}
diff --git a/playground/glob-import/vite.config.ts b/playground/glob-import/vite.config.ts
new file mode 100644
index 00000000000000..054fae9f8d4788
--- /dev/null
+++ b/playground/glob-import/vite.config.ts
@@ -0,0 +1,37 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+const escapeAliases = fs
+ .readdirSync(path.join(__dirname, 'escape'), { withFileTypes: true })
+ .filter((f) => f.isDirectory())
+ .map((f) => f.name)
+ .reduce((aliases: Record, dir) => {
+ aliases[`@escape_${dir}_mod`] = path.resolve(
+ __dirname,
+ `./escape/${dir}/mod`,
+ )
+ return aliases
+ }, {})
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ ...escapeAliases,
+ '@dir': path.resolve(__dirname, './dir/'),
+ '#alias': path.resolve(__dirname, './imports-path/'),
+ },
+ },
+ build: {
+ sourcemap: true,
+ rollupOptions: {
+ output: {
+ manualChunks(id) {
+ if (id.includes('foo.css')) {
+ return 'foo_css'
+ }
+ },
+ },
+ },
+ },
+})
diff --git a/playground/hmr-root/__tests__/hmr-root.spec.ts b/playground/hmr-root/__tests__/hmr-root.spec.ts
new file mode 100644
index 00000000000000..d38d761c998bd1
--- /dev/null
+++ b/playground/hmr-root/__tests__/hmr-root.spec.ts
@@ -0,0 +1,10 @@
+import { expect, test } from 'vitest'
+
+import { editFile, isServe, page, untilUpdated } from '~utils'
+
+test.runIf(isServe)('should watch files outside root', async () => {
+ expect(await page.textContent('#foo')).toBe('foo')
+ editFile('foo.js', (code) => code.replace("'foo'", "'foobar'"))
+ await page.waitForEvent('load')
+ await untilUpdated(async () => await page.textContent('#foo'), 'foobar')
+})
diff --git a/playground/hmr-root/foo.js b/playground/hmr-root/foo.js
new file mode 100644
index 00000000000000..cb356468240d50
--- /dev/null
+++ b/playground/hmr-root/foo.js
@@ -0,0 +1 @@
+export const foo = 'foo'
diff --git a/playground/hmr-root/root/index.html b/playground/hmr-root/root/index.html
new file mode 100644
index 00000000000000..ddf3514623d0b7
--- /dev/null
+++ b/playground/hmr-root/root/index.html
@@ -0,0 +1,7 @@
+
+
+
diff --git a/playground/hmr-root/vite.config.ts b/playground/hmr-root/vite.config.ts
new file mode 100644
index 00000000000000..8afcd8e3770834
--- /dev/null
+++ b/playground/hmr-root/vite.config.ts
@@ -0,0 +1,9 @@
+import path from 'node:path'
+import url from 'node:url'
+import { defineConfig } from 'vite'
+
+const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
+
+export default defineConfig({
+ root: path.join(__dirname, './root'),
+})
diff --git a/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts
new file mode 100644
index 00000000000000..e19ed5596b8d31
--- /dev/null
+++ b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts
@@ -0,0 +1,1149 @@
+import fs from 'node:fs'
+import { posix, resolve } from 'node:path'
+import EventEmitter from 'node:events'
+import {
+ afterAll,
+ beforeAll,
+ describe,
+ expect,
+ onTestFinished,
+ test,
+ vi,
+} from 'vitest'
+import type { InlineConfig, RunnableDevEnvironment, ViteDevServer } from 'vite'
+import { createRunnableDevEnvironment, createServer } from 'vite'
+import type { ModuleRunner } from 'vite/module-runner'
+import {
+ addFile,
+ createInMemoryLogger,
+ editFile,
+ isBuild,
+ promiseWithResolvers,
+ readFile,
+ removeFile,
+ slash,
+ testDir,
+ untilUpdated,
+} from '~utils'
+
+let server: ViteDevServer
+const clientLogs: string[] = []
+const serverLogs: string[] = []
+let runner: ModuleRunner
+
+const logsEmitter = new EventEmitter()
+
+afterAll(async () => {
+ await server?.close()
+})
+
+const hmr = (key: string) => (globalThis.__HMR__[key] as string) || ''
+
+const updated = (file: string, via?: string) => {
+ if (via) {
+ return `[vite] hot updated: ${file} via ${via}`
+ }
+ return `[vite] hot updated: ${file}`
+}
+
+if (!isBuild) {
+ describe('hmr works correctly', () => {
+ beforeAll(async () => {
+ await setupModuleRunner('/hmr.ts')
+ })
+
+ test('should connect', async () => {
+ expect(clientLogs).toContain('[vite] connected.')
+ })
+
+ test('self accept', async () => {
+ const el = () => hmr('.app')
+ await untilConsoleLogAfter(
+ () =>
+ editFile('hmr.ts', (code) =>
+ code.replace('const foo = 1', 'const foo = 2'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ 'foo was: 1',
+ '(self-accepting 1) foo is now: 2',
+ '(self-accepting 2) foo is now: 2',
+ updated('/hmr.ts'),
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el(), '2')
+
+ await untilConsoleLogAfter(
+ () =>
+ editFile('hmr.ts', (code) =>
+ code.replace('const foo = 2', 'const foo = 3'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ 'foo was: 2',
+ '(self-accepting 1) foo is now: 3',
+ '(self-accepting 2) foo is now: 3',
+ updated('/hmr.ts'),
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el(), '3')
+ })
+
+ test('accept dep', async () => {
+ const el = () => hmr('.dep')
+ await untilConsoleLogAfter(
+ () =>
+ editFile('hmrDep.js', (code) =>
+ code.replace('const foo = 1', 'const foo = 2'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ '(dep) foo was: 1',
+ '(dep) foo from dispose: 1',
+ '(single dep) foo is now: 2',
+ '(single dep) nested foo is now: 1',
+ '(multi deps) foo is now: 2',
+ '(multi deps) nested foo is now: 1',
+ updated('/hmrDep.js', '/hmr.ts'),
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el(), '2')
+
+ await untilConsoleLogAfter(
+ () =>
+ editFile('hmrDep.js', (code) =>
+ code.replace('const foo = 2', 'const foo = 3'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ '(dep) foo was: 2',
+ '(dep) foo from dispose: 2',
+ '(single dep) foo is now: 3',
+ '(single dep) nested foo is now: 1',
+ '(multi deps) foo is now: 3',
+ '(multi deps) nested foo is now: 1',
+ updated('/hmrDep.js', '/hmr.ts'),
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el(), '3')
+ })
+
+ test('nested dep propagation', async () => {
+ const el = () => hmr('.nested')
+ await untilConsoleLogAfter(
+ () =>
+ editFile('hmrNestedDep.js', (code) =>
+ code.replace('const foo = 1', 'const foo = 2'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ '(dep) foo was: 3',
+ '(dep) foo from dispose: 3',
+ '(single dep) foo is now: 3',
+ '(single dep) nested foo is now: 2',
+ '(multi deps) foo is now: 3',
+ '(multi deps) nested foo is now: 2',
+ updated('/hmrDep.js', '/hmr.ts'),
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el(), '2')
+
+ await untilConsoleLogAfter(
+ () =>
+ editFile('hmrNestedDep.js', (code) =>
+ code.replace('const foo = 2', 'const foo = 3'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ '(dep) foo was: 3',
+ '(dep) foo from dispose: 3',
+ '(single dep) foo is now: 3',
+ '(single dep) nested foo is now: 3',
+ '(multi deps) foo is now: 3',
+ '(multi deps) nested foo is now: 3',
+ updated('/hmrDep.js', '/hmr.ts'),
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el(), '3')
+ })
+
+ test('invalidate', async () => {
+ const el = () => hmr('.invalidation')
+ await untilConsoleLogAfter(
+ () =>
+ editFile('invalidation/child.js', (code) =>
+ code.replace('child', 'child updated'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ `>>> vite:invalidate -- /invalidation/child.js`,
+ '[vite] invalidate /invalidation/child.js',
+ updated('/invalidation/child.js'),
+ '>>> vite:afterUpdate -- update',
+ '>>> vite:beforeUpdate -- update',
+ '(invalidation) parent is executing',
+ updated('/invalidation/parent.js'),
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el(), 'child updated')
+ })
+
+ test('soft invalidate', async () => {
+ const el = () => hmr('.soft-invalidation')
+ expect(el()).toBe(
+ 'soft-invalidation/index.js is transformed 1 times. child is bar',
+ )
+ editFile('soft-invalidation/child.js', (code) =>
+ code.replace('bar', 'updated'),
+ )
+ await untilUpdated(
+ () => el(),
+ 'soft-invalidation/index.js is transformed 1 times. child is updated',
+ )
+ })
+
+ test('invalidate in circular dep should not trigger infinite HMR', async () => {
+ const el = () => hmr('.invalidation-circular-deps')
+ await untilUpdated(() => el(), 'child')
+ editFile(
+ 'invalidation-circular-deps/circular-invalidate/child.js',
+ (code) => code.replace('child', 'child updated'),
+ )
+ await untilUpdated(() => el(), 'child updated')
+ })
+
+ test('invalidate in circular dep should be hot updated if possible', async () => {
+ const el = () => hmr('.invalidation-circular-deps-handled')
+ await untilUpdated(() => el(), 'child')
+ editFile(
+ 'invalidation-circular-deps/invalidate-handled-in-circle/child.js',
+ (code) => code.replace('child', 'child updated'),
+ )
+ await untilUpdated(() => el(), 'child updated')
+ })
+
+ test('plugin hmr handler + custom event', async () => {
+ const el = () => hmr('.custom')
+ editFile('customFile.js', (code) => code.replace('custom', 'edited'))
+ await untilUpdated(() => el(), 'edited')
+ })
+
+ test('plugin hmr remove custom events', async () => {
+ const el = () => hmr('.toRemove')
+ editFile('customFile.js', (code) => code.replace('custom', 'edited'))
+ await untilUpdated(() => el(), 'edited')
+ editFile('customFile.js', (code) => code.replace('edited', 'custom'))
+ await untilUpdated(() => el(), 'edited')
+ })
+
+ test('plugin client-server communication', async () => {
+ const el = () => hmr('.custom-communication')
+ await untilUpdated(() => el(), '3')
+ })
+
+ test('queries are correctly resolved', async () => {
+ const query1 = () => hmr('query1')
+ const query2 = () => hmr('query2')
+
+ expect(query1()).toBe('query1')
+ expect(query2()).toBe('query2')
+
+ editFile('queries/multi-query.js', (code) => code + '//comment')
+ await untilUpdated(() => query1(), '//commentquery1')
+ await untilUpdated(() => query2(), '//commentquery2')
+ })
+ })
+
+ describe('self accept with different entry point formats', () => {
+ test.each(['./unresolved.ts', './unresolved', '/unresolved'])(
+ 'accepts if entry point is relative to root %s',
+ async (entrypoint) => {
+ await setupModuleRunner(entrypoint, {}, '/unresolved.ts')
+
+ const originalUnresolvedFile = readFile('unresolved.ts')
+ onTestFinished(() => {
+ const filepath = resolve(testDir, 'unresolved.ts')
+ fs.writeFileSync(filepath, originalUnresolvedFile, 'utf-8')
+ })
+
+ const el = () => hmr('.app')
+ await untilConsoleLogAfter(
+ () =>
+ editFile('unresolved.ts', (code) =>
+ code.replace('const foo = 1', 'const foo = 2'),
+ ),
+ [
+ 'foo was: 1',
+ '(self-accepting 1) foo is now: 2',
+ '(self-accepting 2) foo is now: 2',
+ updated(entrypoint),
+ ],
+ true,
+ )
+ await untilUpdated(() => el(), '2')
+
+ await untilConsoleLogAfter(
+ () =>
+ editFile('unresolved.ts', (code) =>
+ code.replace('const foo = 2', 'const foo = 3'),
+ ),
+ [
+ 'foo was: 2',
+ '(self-accepting 1) foo is now: 3',
+ '(self-accepting 2) foo is now: 3',
+ updated(entrypoint),
+ ],
+ true,
+ )
+ await untilUpdated(() => el(), '3')
+ },
+ )
+ })
+
+ describe('acceptExports', () => {
+ const HOT_UPDATED = /hot updated/
+ const CONNECTED = /connected/
+ const PROGRAM_RELOAD = /program reload/
+
+ const baseDir = 'accept-exports'
+
+ describe('when all used exports are accepted', () => {
+ const testDir = baseDir + '/main-accepted'
+
+ const fileName = 'target.ts'
+ const file = `${testDir}/${fileName}`
+ const url = `/${file}`
+
+ let dep = 'dep0'
+
+ beforeAll(async () => {
+ await untilConsoleLogAfter(
+ () => setupModuleRunner(`/${testDir}/index`),
+ [CONNECTED, />>>>>>/],
+ (logs) => {
+ expect(logs).toContain(`<<<<<< A0 B0 D0 ; ${dep}`)
+ expect(logs).toContain('>>>>>> A0 D0')
+ },
+ )
+ })
+
+ test('the callback is called with the new version the module', async () => {
+ const callbackFile = `${testDir}/callback.ts`
+ const callbackUrl = `/${callbackFile}`
+
+ await untilConsoleLogAfter(
+ () => {
+ editFile(callbackFile, (code) =>
+ code
+ .replace("x = 'X'", "x = 'Y'")
+ .replace('reloaded >>>', 'reloaded (2) >>>'),
+ )
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ 'reloaded >>> Y',
+ `[vite] hot updated: ${callbackUrl}`,
+ ])
+ },
+ )
+
+ await untilConsoleLogAfter(
+ () => {
+ editFile(callbackFile, (code) => code.replace("x = 'Y'", "x = 'Z'"))
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ 'reloaded (2) >>> Z',
+ `[vite] hot updated: ${callbackUrl}`,
+ ])
+ },
+ )
+ })
+
+ test('stops HMR bubble on dependency change', async () => {
+ const depFileName = 'dep.ts'
+ const depFile = `${testDir}/${depFileName}`
+
+ await untilConsoleLogAfter(
+ () => {
+ editFile(depFile, (code) => code.replace('dep0', (dep = 'dep1')))
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ `<<<<<< A0 B0 D0 ; ${dep}`,
+ `[vite] hot updated: ${url}`,
+ ])
+ },
+ )
+ })
+
+ test('accepts itself and refreshes on change', async () => {
+ await untilConsoleLogAfter(
+ () => {
+ editFile(file, (code) => code.replace(/(\b[A-Z])0/g, '$11'))
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ `<<<<<< A1 B1 D1 ; ${dep}`,
+ `[vite] hot updated: ${url}`,
+ ])
+ },
+ )
+ })
+
+ test('accepts itself and refreshes on 2nd change', async () => {
+ await untilConsoleLogAfter(
+ () => {
+ editFile(file, (code) =>
+ code
+ .replace(/(\b[A-Z])1/g, '$12')
+ .replace(
+ "acceptExports(['a', 'default']",
+ "acceptExports(['b', 'default']",
+ ),
+ )
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ `<<<<<< A2 B2 D2 ; ${dep}`,
+ `[vite] hot updated: ${url}`,
+ ])
+ },
+ )
+ })
+
+ test('does not accept itself anymore after acceptedExports change', async () => {
+ await untilConsoleLogAfter(
+ async () => {
+ editFile(file, (code) => code.replace(/(\b[A-Z])2/g, '$13'))
+ },
+ [PROGRAM_RELOAD, />>>>>>/],
+ (logs) => {
+ expect(logs).toContain(`<<<<<< A3 B3 D3 ; ${dep}`)
+ expect(logs).toContain('>>>>>> A3 D3')
+ },
+ )
+ })
+ })
+
+ describe('when some used exports are not accepted', () => {
+ const testDir = baseDir + '/main-non-accepted'
+
+ const namedFileName = 'named.ts'
+ const namedFile = `${testDir}/${namedFileName}`
+ const defaultFileName = 'default.ts'
+ const defaultFile = `${testDir}/${defaultFileName}`
+ const depFileName = 'dep.ts'
+ const depFile = `${testDir}/${depFileName}`
+
+ const a = 'A0'
+ let dep = 'dep0'
+
+ beforeAll(async () => {
+ await untilConsoleLogAfter(
+ () => setupModuleRunner(`/${testDir}/index`),
+ [CONNECTED, />>>>>>/],
+ (logs) => {
+ expect(logs).toContain(`<<< named: ${a} ; ${dep}`)
+ expect(logs).toContain(`<<< default: def0`)
+ expect(logs).toContain(`>>>>>> ${a} def0`)
+ },
+ )
+ })
+
+ test('does not stop the HMR bubble on change to dep', async () => {
+ await untilConsoleLogAfter(
+ async () => {
+ editFile(depFile, (code) => code.replace('dep0', (dep = 'dep1')))
+ },
+ [PROGRAM_RELOAD, />>>>>>/],
+ (logs) => {
+ expect(logs).toContain(`<<< named: ${a} ; ${dep}`)
+ },
+ )
+ })
+
+ describe('does not stop the HMR bubble on change to self', () => {
+ test('with named exports', async () => {
+ await untilConsoleLogAfter(
+ async () => {
+ editFile(namedFile, (code) => code.replace(a, 'A1'))
+ },
+ [PROGRAM_RELOAD, />>>>>>/],
+ (logs) => {
+ expect(logs).toContain(`<<< named: A1 ; ${dep}`)
+ },
+ )
+ })
+
+ test('with default export', async () => {
+ await untilConsoleLogAfter(
+ async () => {
+ editFile(defaultFile, (code) => code.replace('def0', 'def1'))
+ },
+ [PROGRAM_RELOAD, />>>>>>/],
+ (logs) => {
+ expect(logs).toContain(`<<< default: def1`)
+ },
+ )
+ })
+ })
+
+ describe("doesn't reload if files not in the the entrypoint importers chain is changed", async () => {
+ const testFile = 'non-tested/index.js'
+
+ beforeAll(async () => {
+ clientLogs.length = 0
+ // so it's in the module graph
+ const ssrEnvironment = server.environments.ssr
+ await ssrEnvironment.transformRequest(testFile)
+ await ssrEnvironment.transformRequest('non-tested/dep.js')
+ })
+
+ test('does not full reload', async () => {
+ editFile(
+ testFile,
+ (code) => code + '\n\nexport const query5 = "query5"',
+ )
+ const start = Date.now()
+ // for 2 seconds check that there is no log about the file being reloaded
+ while (Date.now() - start < 2000) {
+ if (
+ clientLogs.some(
+ (log) =>
+ log.match(PROGRAM_RELOAD) ||
+ log.includes('non-tested/index.js'),
+ )
+ ) {
+ throw new Error('File was reloaded')
+ }
+ await new Promise((r) => setTimeout(r, 100))
+ }
+ }, 5_000)
+
+ test('does not update', async () => {
+ editFile('non-tested/dep.js', (code) => code + '//comment')
+ const start = Date.now()
+ // for 2 seconds check that there is no log about the file being reloaded
+ while (Date.now() - start < 2000) {
+ if (
+ clientLogs.some(
+ (log) =>
+ log.match(PROGRAM_RELOAD) ||
+ log.includes('non-tested/dep.js'),
+ )
+ ) {
+ throw new Error('File was updated')
+ }
+ await new Promise((r) => setTimeout(r, 100))
+ }
+ }, 5_000)
+ })
+ })
+
+ test('accepts itself when imported for side effects only (no bindings imported)', async () => {
+ const testDir = baseDir + '/side-effects'
+ const file = 'side-effects.ts'
+
+ await untilConsoleLogAfter(
+ () => setupModuleRunner(`/${testDir}/index`),
+ [CONNECTED, />>>/],
+ (logs) => {
+ expect(logs).toContain('>>> side FX')
+ },
+ )
+
+ await untilConsoleLogAfter(
+ () => {
+ editFile(`${testDir}/${file}`, (code) =>
+ code.replace('>>> side FX', '>>> side FX !!'),
+ )
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ '>>> side FX !!',
+ updated(`/${testDir}/${file}`),
+ ])
+ },
+ )
+ })
+
+ describe('acceptExports([])', () => {
+ const testDir = baseDir + '/unused-exports'
+
+ test('accepts itself if no exports are imported', async () => {
+ const fileName = 'unused.ts'
+ const file = `${testDir}/${fileName}`
+ const url = '/' + file
+
+ await untilConsoleLogAfter(
+ () => setupModuleRunner(`/${testDir}/index`),
+ [CONNECTED, '-- unused --'],
+ (logs) => {
+ expect(logs).toContain('-- unused --')
+ },
+ )
+
+ await untilConsoleLogAfter(
+ () => {
+ editFile(file, (code) =>
+ code.replace('-- unused --', '-> unused <-'),
+ )
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual(['-> unused <-', updated(url)])
+ },
+ )
+ })
+
+ test("doesn't accept itself if any of its exports is imported", async () => {
+ const fileName = 'used.ts'
+ const file = `${testDir}/${fileName}`
+
+ await untilConsoleLogAfter(
+ () => setupModuleRunner(`/${testDir}/index`),
+ [CONNECTED, '-- used --', 'used:foo0'],
+ (logs) => {
+ expect(logs).toContain('-- used --')
+ expect(logs).toContain('used:foo0')
+ },
+ )
+
+ await untilConsoleLogAfter(
+ async () => {
+ editFile(file, (code) =>
+ code.replace('foo0', 'foo1').replace('-- used --', '-> used <-'),
+ )
+ },
+ [PROGRAM_RELOAD, /used:foo/],
+ (logs) => {
+ expect(logs).toContain('-> used <-')
+ expect(logs).toContain('used:foo1')
+ },
+ )
+ })
+ })
+
+ describe('indiscriminate imports: import *', () => {
+ const testStarExports = (testDirName: string) => {
+ const testDir = `${baseDir}/${testDirName}`
+
+ test('accepts itself if all its exports are accepted', async () => {
+ const fileName = 'deps-all-accepted.ts'
+ const file = `${testDir}/${fileName}`
+ const url = '/' + file
+
+ await untilConsoleLogAfter(
+ () => setupModuleRunner(`/${testDir}/index`),
+ [CONNECTED, '>>> ready <<<'],
+ (logs) => {
+ expect(logs).toContain('loaded:all:a0b0c0default0')
+ expect(logs).toContain('all >>>>>> a0, b0, c0')
+ },
+ )
+
+ await untilConsoleLogAfter(
+ () => {
+ editFile(file, (code) => code.replace(/([abc])0/g, '$11'))
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual(['all >>>>>> a1, b1, c1', updated(url)])
+ },
+ )
+
+ await untilConsoleLogAfter(
+ () => {
+ editFile(file, (code) => code.replace(/([abc])1/g, '$12'))
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual(['all >>>>>> a2, b2, c2', updated(url)])
+ },
+ )
+ })
+
+ test("doesn't accept itself if one export is not accepted", async () => {
+ const fileName = 'deps-some-accepted.ts'
+ const file = `${testDir}/${fileName}`
+
+ await untilConsoleLogAfter(
+ () => setupModuleRunner(`/${testDir}/index`),
+ [CONNECTED, '>>> ready <<<'],
+ (logs) => {
+ expect(logs).toContain('loaded:some:a0b0c0default0')
+ expect(logs).toContain('some >>>>>> a0, b0, c0')
+ },
+ )
+
+ await untilConsoleLogAfter(
+ async () => {
+ editFile(file, (code) => code.replace(/([abc])0/g, '$11'))
+ },
+ [PROGRAM_RELOAD, '>>> ready <<<'],
+ (logs) => {
+ expect(logs).toContain('loaded:some:a1b1c1default0')
+ expect(logs).toContain('some >>>>>> a1, b1, c1')
+ },
+ )
+ })
+ }
+
+ describe('import * from ...', () => testStarExports('star-imports'))
+
+ describe('dynamic import(...)', () => testStarExports('dynamic-imports'))
+ })
+ })
+
+ test('handle virtual module updates', async () => {
+ await setupModuleRunner('/hmr.ts')
+ const el = () => hmr('.virtual')
+ expect(el()).toBe('[success]0')
+ editFile('importedVirtual.js', (code) => code.replace('[success]', '[wow]'))
+ await untilUpdated(el, '[wow]')
+ })
+
+ test('invalidate virtual module', async () => {
+ await setupModuleRunner('/hmr.ts')
+ const el = () => hmr('.virtual')
+ expect(el()).toBe('[wow]0')
+ globalThis.__HMR__['virtual:increment']()
+ await untilUpdated(el, '[wow]1')
+ })
+
+ test('should hmr when file is deleted and restored', async () => {
+ await setupModuleRunner('/hmr.ts')
+
+ const parentFile = 'file-delete-restore/parent.js'
+ const childFile = 'file-delete-restore/child.js'
+
+ await untilUpdated(() => hmr('.file-delete-restore'), 'parent:child')
+
+ editFile(childFile, (code) =>
+ code.replace("value = 'child'", "value = 'child1'"),
+ )
+ await untilUpdated(() => hmr('.file-delete-restore'), 'parent:child1')
+
+ // delete the file
+ editFile(parentFile, (code) =>
+ code.replace(
+ "export { value as childValue } from './child'",
+ "export const childValue = 'not-child'",
+ ),
+ )
+ const originalChildFileCode = readFile(childFile)
+ removeFile(childFile)
+ await untilUpdated(() => hmr('.file-delete-restore'), 'parent:not-child')
+
+ // restore the file
+ addFile(childFile, originalChildFileCode)
+ editFile(parentFile, (code) =>
+ code.replace(
+ "export const childValue = 'not-child'",
+ "export { value as childValue } from './child'",
+ ),
+ )
+ await untilUpdated(() => hmr('.file-delete-restore'), 'parent:child')
+ })
+
+ test('delete file should not break hmr', async () => {
+ await setupModuleRunner('/hmr.ts', undefined, undefined, {
+ '.intermediate-file-delete-increment': '1',
+ })
+
+ await untilUpdated(
+ () => hmr('.intermediate-file-delete-display'),
+ 'count is 1',
+ )
+
+ // add state
+ globalThis.__HMR__['.delete-intermediate-file']()
+ await untilUpdated(
+ () => hmr('.intermediate-file-delete-display'),
+ 'count is 2',
+ )
+
+ // update import, hmr works
+ editFile('intermediate-file-delete/index.js', (code) =>
+ code.replace("from './re-export.js'", "from './display.js'"),
+ )
+ editFile('intermediate-file-delete/display.js', (code) =>
+ code.replace('count is ${count}', 'count is ${count}!'),
+ )
+ await untilUpdated(
+ () => hmr('.intermediate-file-delete-display'),
+ 'count is 2!',
+ )
+
+ // remove unused file
+ removeFile('intermediate-file-delete/re-export.js')
+ __HMR__['.intermediate-file-delete-increment'] = '1' // reset state
+ await untilUpdated(
+ () => hmr('.intermediate-file-delete-display'),
+ 'count is 1!',
+ )
+
+ // re-add state
+ globalThis.__HMR__['.delete-intermediate-file']()
+ await untilUpdated(
+ () => hmr('.intermediate-file-delete-display'),
+ 'count is 2!',
+ )
+
+ // hmr works after file deletion
+ editFile('intermediate-file-delete/display.js', (code) =>
+ code.replace('count is ${count}!', 'count is ${count}'),
+ )
+ await untilUpdated(
+ () => hmr('.intermediate-file-delete-display'),
+ 'count is 2',
+ )
+ })
+
+ test('deleted file should trigger dispose and prune callbacks', async () => {
+ await setupModuleRunner('/hmr.ts')
+
+ const parentFile = 'file-delete-restore/parent.js'
+ const childFile = 'file-delete-restore/child.js'
+
+ // delete the file
+ editFile(parentFile, (code) =>
+ code.replace(
+ "export { value as childValue } from './child'",
+ "export const childValue = 'not-child'",
+ ),
+ )
+ const originalChildFileCode = readFile(childFile)
+ removeFile(childFile)
+
+ await untilUpdated(() => hmr('.file-delete-restore'), 'parent:not-child')
+ expect(clientLogs).to.include('file-delete-restore/child.js is disposed')
+ expect(clientLogs).to.include('file-delete-restore/child.js is pruned')
+
+ // restore the file
+ addFile(childFile, originalChildFileCode)
+ editFile(parentFile, (code) =>
+ code.replace(
+ "export const childValue = 'not-child'",
+ "export { value as childValue } from './child'",
+ ),
+ )
+ await untilUpdated(() => hmr('.file-delete-restore'), 'parent:child')
+ })
+
+ test('import.meta.hot?.accept', async () => {
+ await setupModuleRunner('/hmr.ts')
+ await untilConsoleLogAfter(
+ () =>
+ editFile('optional-chaining/child.js', (code) =>
+ code.replace('const foo = 1', 'const foo = 2'),
+ ),
+ '(optional-chaining) child update',
+ )
+ await untilUpdated(() => hmr('.optional-chaining')?.toString(), '2')
+ })
+
+ // TODO: this is flaky due to https://github.com/vitejs/vite/issues/19804
+ test.skip('hmr works for self-accepted module within circular imported files', async () => {
+ await setupModuleRunner('/self-accept-within-circular/index')
+ const el = () => hmr('.self-accept-within-circular')
+ expect(el()).toBe('c')
+ editFile('self-accept-within-circular/c.js', (code) =>
+ code.replace(`export const c = 'c'`, `export const c = 'cc'`),
+ )
+ // it throws a same error as browser case,
+ // but it doesn't auto reload and it calls `hot.accept(nextExports)` with `nextExports = undefined`
+ await untilUpdated(() => el(), '')
+
+ // test reloading manually for now
+ server.moduleGraph.invalidateAll() // TODO: why is `runner.clearCache()` not enough?
+ await runner.import('/self-accept-within-circular/index')
+ await untilUpdated(() => el(), 'cc')
+ })
+
+ test('hmr should not reload if no accepted within circular imported files', async (ctx) => {
+ // TODO: Investigate race condition that causes an inconsistent behaviour for the last `untilUpdated`
+ // assertion where it'll sometimes receive "mod-a -> mod-b (edited) -> mod-c -> mod-a (expected no error)"
+ // This is probably related to https://github.com/vitejs/vite/issues/19804
+ ctx.skip()
+
+ await setupModuleRunner('/circular/index')
+ const el = () => hmr('.circular')
+ expect(el()).toBe(
+ // tests in the browser check that there is an error, but vite runtime just returns undefined in those cases
+ 'mod-a -> mod-b -> mod-c -> undefined (expected no error)',
+ )
+ editFile('circular/mod-b.js', (code) =>
+ code.replace(`mod-b ->`, `mod-b (edited) ->`),
+ )
+ await untilUpdated(
+ () => el(),
+ 'mod-a -> mod-b (edited) -> mod-c -> undefined (expected no error)',
+ )
+ })
+
+ test('not inlined assets HMR', async () => {
+ await setupModuleRunner('/hmr.ts')
+ const el = () => hmr('#logo-no-inline')
+ await untilConsoleLogAfter(
+ () =>
+ editFile('logo-no-inline.svg', (code) =>
+ code.replace('height="30px"', 'height="40px"'),
+ ),
+ /Logo-no-inline updated/,
+ )
+ await vi.waitUntil(() => el().includes('logo-no-inline.svg?t='))
+ })
+
+ test('inlined assets HMR', async () => {
+ await setupModuleRunner('/hmr.ts')
+ const el = () => hmr('#logo')
+ const initialLogoUrl = el()
+ expect(initialLogoUrl).toMatch(/^data:image\/svg\+xml/)
+ await untilConsoleLogAfter(
+ () =>
+ editFile('logo.svg', (code) =>
+ code.replace('height="30px"', 'height="40px"'),
+ ),
+ /Logo updated/,
+ )
+ // Should be updated with new data url
+ const updatedLogoUrl = el()
+ expect(updatedLogoUrl).toMatch(/^data:image\/svg\+xml/)
+ expect(updatedLogoUrl).not.toEqual(initialLogoUrl)
+ })
+} else {
+ test('this file only includes test for serve', () => {
+ expect(true).toBe(true)
+ })
+}
+
+type UntilBrowserLogAfterCallback = (logs: string[]) => PromiseLike | void
+
+export async function untilConsoleLogAfter(
+ operation: () => any,
+ target: string | RegExp | Array,
+ expectOrder?: boolean,
+ callback?: UntilBrowserLogAfterCallback,
+): Promise
+export async function untilConsoleLogAfter(
+ operation: () => any,
+ target: string | RegExp | Array,
+ callback?: UntilBrowserLogAfterCallback,
+): Promise
+export async function untilConsoleLogAfter(
+ operation: () => any,
+ target: string | RegExp | Array,
+ arg3?: boolean | UntilBrowserLogAfterCallback,
+ arg4?: UntilBrowserLogAfterCallback,
+): Promise {
+ const expectOrder = typeof arg3 === 'boolean' ? arg3 : false
+ const callback = typeof arg3 === 'boolean' ? arg4 : arg3
+
+ const promise = untilConsoleLog(target, expectOrder)
+ await operation()
+ const logs = await promise
+ if (callback) {
+ await callback(logs)
+ }
+ return logs
+}
+
+async function untilConsoleLog(
+ target?: string | RegExp | Array,
+ expectOrder = true,
+): Promise {
+ const { promise, resolve, reject } = promiseWithResolvers()
+
+ const logsMessages = []
+
+ try {
+ const isMatch = (matcher: string | RegExp) => (text: string) =>
+ typeof matcher === 'string' ? text === matcher : matcher.test(text)
+
+ let processMsg: (text: string) => boolean
+
+ if (!target) {
+ processMsg = () => true
+ } else if (Array.isArray(target)) {
+ if (expectOrder) {
+ const remainingTargets = [...target]
+ processMsg = (text: string) => {
+ const nextTarget = remainingTargets.shift()
+ expect(text).toMatch(nextTarget)
+ return remainingTargets.length === 0
+ }
+ } else {
+ const remainingMatchers = target.map(isMatch)
+ processMsg = (text: string) => {
+ const nextIndex = remainingMatchers.findIndex((matcher) =>
+ matcher(text),
+ )
+ if (nextIndex >= 0) {
+ remainingMatchers.splice(nextIndex, 1)
+ }
+ return remainingMatchers.length === 0
+ }
+ }
+ } else {
+ processMsg = isMatch(target)
+ }
+
+ const handleMsg = (text: string) => {
+ try {
+ text = text.replace(/\n$/, '')
+ logsMessages.push(text)
+ const done = processMsg(text)
+ if (done) {
+ resolve()
+ logsEmitter.off('log', handleMsg)
+ }
+ } catch (err) {
+ reject(err)
+ logsEmitter.off('log', handleMsg)
+ }
+ }
+
+ logsEmitter.on('log', handleMsg)
+ } catch (err) {
+ reject(err)
+ }
+
+ await promise
+
+ return logsMessages
+}
+
+function isWatched(server: ViteDevServer, watchedFile: string) {
+ const watched = server.watcher.getWatched()
+ for (const [dir, files] of Object.entries(watched)) {
+ const unixDir = slash(dir)
+ for (const file of files) {
+ const filePath = posix.join(unixDir, file)
+ if (filePath.includes(watchedFile)) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+function waitForWatcher(server: ViteDevServer, watched: string) {
+ return new Promise((resolve) => {
+ function checkWatched() {
+ if (isWatched(server, watched)) {
+ resolve()
+ } else {
+ setTimeout(checkWatched, 20)
+ }
+ }
+ checkWatched()
+ })
+}
+
+async function setupModuleRunner(
+ entrypoint: string,
+ serverOptions: InlineConfig = {},
+ waitForFile: string = entrypoint,
+ initHmrState: Record = {},
+) {
+ if (server) {
+ await server.close()
+ clientLogs.length = 0
+ serverLogs.length = 0
+ runner.clearCache()
+ }
+
+ globalThis.__HMR__ = initHmrState as any
+
+ const logger = new HMRMockLogger()
+ // @ts-expect-error not typed for HMR
+ globalThis.log = (...msg) => logger.log(...msg)
+
+ server = await createServer({
+ configFile: resolve(testDir, 'vite.config.ts'),
+ root: testDir,
+ customLogger: createInMemoryLogger(serverLogs),
+ server: {
+ middlewareMode: true,
+ watch: {
+ // During tests we edit the files too fast and sometimes chokidar
+ // misses change events, so enforce polling for consistency
+ usePolling: true,
+ interval: 100,
+ },
+ hmr: {
+ port: 9609,
+ },
+ preTransformRequests: false,
+ },
+ environments: {
+ ssr: {
+ dev: {
+ createEnvironment(name, config) {
+ return createRunnableDevEnvironment(name, config, {
+ runnerOptions: { hmr: { logger } },
+ })
+ },
+ },
+ },
+ },
+ optimizeDeps: {
+ disabled: true,
+ noDiscovery: true,
+ include: [],
+ },
+ ...serverOptions,
+ })
+
+ runner = (server.environments.ssr as RunnableDevEnvironment).runner
+
+ await waitForWatcher(server, waitForFile)
+
+ await runner.import(entrypoint)
+
+ return {
+ runtime: runner,
+ server,
+ }
+}
+
+class HMRMockLogger {
+ log(...msg: unknown[]) {
+ const log = msg.join(' ')
+ clientLogs.push(log)
+ logsEmitter.emit('log', log)
+ }
+
+ debug(...msg: unknown[]) {
+ const log = ['[vite]', ...msg].join(' ')
+ clientLogs.push(log)
+ logsEmitter.emit('log', log)
+ }
+ error(msg: string) {
+ const log = ['[vite]', msg].join(' ')
+ clientLogs.push(log)
+ logsEmitter.emit('log', log)
+ }
+}
diff --git a/playground/hmr-ssr/accept-exports/dynamic-imports/deps-all-accepted.ts b/playground/hmr-ssr/accept-exports/dynamic-imports/deps-all-accepted.ts
new file mode 100644
index 00000000000000..bf935ebc878609
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/dynamic-imports/deps-all-accepted.ts
@@ -0,0 +1,14 @@
+export const a = 'a0'
+
+export const b = 'b0'
+
+const aliased = 'c0'
+export { aliased as c }
+
+export default 'default0'
+
+log(`all >>>>>> ${a}, ${b}, ${aliased}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['a', 'b', 'c', 'default'])
+}
diff --git a/playground/hmr-ssr/accept-exports/dynamic-imports/deps-some-accepted.ts b/playground/hmr-ssr/accept-exports/dynamic-imports/deps-some-accepted.ts
new file mode 100644
index 00000000000000..04469868392dc3
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/dynamic-imports/deps-some-accepted.ts
@@ -0,0 +1,14 @@
+export const a = 'a0'
+
+export const b = 'b0'
+
+const aliased = 'c0'
+export { aliased as c }
+
+export default 'default0'
+
+log(`some >>>>>> ${a}, ${b}, ${aliased}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['a', 'b', 'default'])
+}
diff --git a/playground/hmr-ssr/accept-exports/dynamic-imports/dynamic-imports.ts b/playground/hmr-ssr/accept-exports/dynamic-imports/dynamic-imports.ts
new file mode 100644
index 00000000000000..a721c318f2ac6b
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/dynamic-imports/dynamic-imports.ts
@@ -0,0 +1,9 @@
+Promise.all([import('./deps-all-accepted'), import('./deps-some-accepted')])
+ .then(([all, some]) => {
+ log('loaded:all:' + all.a + all.b + all.c + all.default)
+ log('loaded:some:' + some.a + some.b + some.c + some.default)
+ log('>>> ready <<<')
+ })
+ .catch((err) => {
+ log(err)
+ })
diff --git a/playground/hmr-ssr/accept-exports/dynamic-imports/index.ts b/playground/hmr-ssr/accept-exports/dynamic-imports/index.ts
new file mode 100644
index 00000000000000..3e6d5d54db881e
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/dynamic-imports/index.ts
@@ -0,0 +1 @@
+import './dynamic-imports.ts'
diff --git a/playground/hmr-ssr/accept-exports/export-from/depA.ts b/playground/hmr-ssr/accept-exports/export-from/depA.ts
new file mode 100644
index 00000000000000..e2eda670ed0097
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/export-from/depA.ts
@@ -0,0 +1 @@
+export const a = 'Ax'
diff --git a/playground/hmr-ssr/accept-exports/export-from/export-from.ts b/playground/hmr-ssr/accept-exports/export-from/export-from.ts
new file mode 100644
index 00000000000000..49cc19fc3e9f86
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/export-from/export-from.ts
@@ -0,0 +1,8 @@
+import { a } from './hub'
+
+log(a)
+
+if (import.meta.hot) {
+ import.meta.hot.accept()
+} else {
+}
diff --git a/playground/hmr-ssr/accept-exports/export-from/hub.ts b/playground/hmr-ssr/accept-exports/export-from/hub.ts
new file mode 100644
index 00000000000000..5bd0dc05608909
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/export-from/hub.ts
@@ -0,0 +1 @@
+export * from './depA'
diff --git a/playground/hmr-ssr/accept-exports/export-from/index.html b/playground/hmr-ssr/accept-exports/export-from/index.html
new file mode 100644
index 00000000000000..0dde1345f085e2
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/export-from/index.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/playground/hmr-ssr/accept-exports/main-accepted/callback.ts b/playground/hmr-ssr/accept-exports/main-accepted/callback.ts
new file mode 100644
index 00000000000000..8dc4c42a24db99
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/main-accepted/callback.ts
@@ -0,0 +1,7 @@
+export const x = 'X'
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['x'], (m) => {
+ log(`reloaded >>> ${m.x}`)
+ })
+}
diff --git a/playground/hmr-ssr/accept-exports/main-accepted/dep.ts b/playground/hmr-ssr/accept-exports/main-accepted/dep.ts
new file mode 100644
index 00000000000000..b9f67fd33a75f8
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/main-accepted/dep.ts
@@ -0,0 +1 @@
+export default 'dep0'
diff --git a/playground/hmr-ssr/accept-exports/main-accepted/index.ts b/playground/hmr-ssr/accept-exports/main-accepted/index.ts
new file mode 100644
index 00000000000000..2e798337101607
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/main-accepted/index.ts
@@ -0,0 +1 @@
+import './main-accepted'
diff --git a/playground/hmr-ssr/accept-exports/main-accepted/main-accepted.ts b/playground/hmr-ssr/accept-exports/main-accepted/main-accepted.ts
new file mode 100644
index 00000000000000..74afdbfa7e378c
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/main-accepted/main-accepted.ts
@@ -0,0 +1,7 @@
+import def, { a } from './target'
+import { x } from './callback'
+
+// we don't want to pollute other checks' logs...
+if (0 > 1) log(x)
+
+log(`>>>>>> ${a} ${def}`)
diff --git a/playground/hmr-ssr/accept-exports/main-accepted/target.ts b/playground/hmr-ssr/accept-exports/main-accepted/target.ts
new file mode 100644
index 00000000000000..c4826524c3c83d
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/main-accepted/target.ts
@@ -0,0 +1,16 @@
+import dep from './dep'
+
+export const a = 'A0'
+
+const bValue = 'B0'
+export { bValue as b }
+
+const def = 'D0'
+
+export default def
+
+log(`<<<<<< ${a} ${bValue} ${def} ; ${dep}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['a', 'default'])
+}
diff --git a/playground/hmr-ssr/accept-exports/main-non-accepted/default.ts b/playground/hmr-ssr/accept-exports/main-non-accepted/default.ts
new file mode 100644
index 00000000000000..6ffaecaf43c588
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/main-non-accepted/default.ts
@@ -0,0 +1,11 @@
+export const x = 'y'
+
+const def = 'def0'
+
+export default def
+
+log(`<<< default: ${def}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['x'])
+}
diff --git a/playground/hmr-ssr/accept-exports/main-non-accepted/dep.ts b/playground/hmr-ssr/accept-exports/main-non-accepted/dep.ts
new file mode 100644
index 00000000000000..b9f67fd33a75f8
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/main-non-accepted/dep.ts
@@ -0,0 +1 @@
+export default 'dep0'
diff --git a/playground/hmr-ssr/accept-exports/main-non-accepted/index.ts b/playground/hmr-ssr/accept-exports/main-non-accepted/index.ts
new file mode 100644
index 00000000000000..3841d7997c4c26
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/main-non-accepted/index.ts
@@ -0,0 +1 @@
+import './main-non-accepted.ts'
diff --git a/playground/hmr-ssr/accept-exports/main-non-accepted/main-non-accepted.ts b/playground/hmr-ssr/accept-exports/main-non-accepted/main-non-accepted.ts
new file mode 100644
index 00000000000000..a159ced50a7f50
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/main-non-accepted/main-non-accepted.ts
@@ -0,0 +1,4 @@
+import { a } from './named'
+import def from './default'
+
+log(`>>>>>> ${a} ${def}`)
diff --git a/playground/hmr-ssr/accept-exports/main-non-accepted/named.ts b/playground/hmr-ssr/accept-exports/main-non-accepted/named.ts
new file mode 100644
index 00000000000000..435d3c8cb50ae8
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/main-non-accepted/named.ts
@@ -0,0 +1,11 @@
+import dep from './dep'
+
+export const a = 'A0'
+
+export const b = 'B0'
+
+log(`<<< named: ${a} ; ${dep}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['b'])
+}
diff --git a/playground/hmr-ssr/accept-exports/reexports.bak/accept-named.ts b/playground/hmr-ssr/accept-exports/reexports.bak/accept-named.ts
new file mode 100644
index 00000000000000..1c45a7c358452e
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/reexports.bak/accept-named.ts
@@ -0,0 +1,10 @@
+export { a, b } from './source'
+
+if (import.meta.hot) {
+ // import.meta.hot.accept('./source', (m) => {
+ // log(`accept-named reexport:${m.a},${m.b}`)
+ // })
+ import.meta.hot.acceptExports('a', (m) => {
+ log(`accept-named reexport:${m.a},${m.b}`)
+ })
+}
diff --git a/playground/hmr-ssr/accept-exports/reexports.bak/index.html b/playground/hmr-ssr/accept-exports/reexports.bak/index.html
new file mode 100644
index 00000000000000..241054bca8256f
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/reexports.bak/index.html
@@ -0,0 +1 @@
+
diff --git a/playground/hmr-ssr/accept-exports/reexports.bak/reexports.ts b/playground/hmr-ssr/accept-exports/reexports.bak/reexports.ts
new file mode 100644
index 00000000000000..659901c42c7149
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/reexports.bak/reexports.ts
@@ -0,0 +1,5 @@
+import { a } from './accept-named'
+
+log('accept-named:' + a)
+
+log('>>> ready')
diff --git a/playground/hmr-ssr/accept-exports/reexports.bak/source.ts b/playground/hmr-ssr/accept-exports/reexports.bak/source.ts
new file mode 100644
index 00000000000000..7f736004a8e9fa
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/reexports.bak/source.ts
@@ -0,0 +1,2 @@
+export const a = 'a0'
+export const b = 'b0'
diff --git a/playground/hmr-ssr/accept-exports/side-effects/index.ts b/playground/hmr-ssr/accept-exports/side-effects/index.ts
new file mode 100644
index 00000000000000..8a44ded37ba337
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/side-effects/index.ts
@@ -0,0 +1 @@
+import './side-effects.ts'
diff --git a/playground/hmr-ssr/accept-exports/side-effects/side-effects.ts b/playground/hmr-ssr/accept-exports/side-effects/side-effects.ts
new file mode 100644
index 00000000000000..f4abb02fb2b47e
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/side-effects/side-effects.ts
@@ -0,0 +1,13 @@
+export const x = 'x'
+
+export const y = 'y'
+
+export default 'z'
+
+log('>>> side FX')
+
+globalThis.__HMR__['.app'] = 'hey'
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['default'])
+}
diff --git a/playground/hmr-ssr/accept-exports/star-imports/deps-all-accepted.ts b/playground/hmr-ssr/accept-exports/star-imports/deps-all-accepted.ts
new file mode 100644
index 00000000000000..bf935ebc878609
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/star-imports/deps-all-accepted.ts
@@ -0,0 +1,14 @@
+export const a = 'a0'
+
+export const b = 'b0'
+
+const aliased = 'c0'
+export { aliased as c }
+
+export default 'default0'
+
+log(`all >>>>>> ${a}, ${b}, ${aliased}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['a', 'b', 'c', 'default'])
+}
diff --git a/playground/hmr-ssr/accept-exports/star-imports/deps-some-accepted.ts b/playground/hmr-ssr/accept-exports/star-imports/deps-some-accepted.ts
new file mode 100644
index 00000000000000..04469868392dc3
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/star-imports/deps-some-accepted.ts
@@ -0,0 +1,14 @@
+export const a = 'a0'
+
+export const b = 'b0'
+
+const aliased = 'c0'
+export { aliased as c }
+
+export default 'default0'
+
+log(`some >>>>>> ${a}, ${b}, ${aliased}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['a', 'b', 'default'])
+}
diff --git a/playground/hmr-ssr/accept-exports/star-imports/index.ts b/playground/hmr-ssr/accept-exports/star-imports/index.ts
new file mode 100644
index 00000000000000..d98700b239a3df
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/star-imports/index.ts
@@ -0,0 +1 @@
+import './star-imports.ts'
diff --git a/playground/hmr-ssr/accept-exports/star-imports/star-imports.ts b/playground/hmr-ssr/accept-exports/star-imports/star-imports.ts
new file mode 100644
index 00000000000000..228622f9ab85b3
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/star-imports/star-imports.ts
@@ -0,0 +1,6 @@
+import * as all from './deps-all-accepted'
+import * as some from './deps-some-accepted'
+
+log('loaded:all:' + all.a + all.b + all.c + all.default)
+log('loaded:some:' + some.a + some.b + some.c + some.default)
+log('>>> ready <<<')
diff --git a/playground/hmr-ssr/accept-exports/unused-exports/index.html b/playground/hmr-ssr/accept-exports/unused-exports/index.html
new file mode 100644
index 00000000000000..8998d3ce4581ee
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/unused-exports/index.html
@@ -0,0 +1 @@
+
diff --git a/playground/hmr-ssr/accept-exports/unused-exports/index.ts b/playground/hmr-ssr/accept-exports/unused-exports/index.ts
new file mode 100644
index 00000000000000..ffd430893843fd
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/unused-exports/index.ts
@@ -0,0 +1,4 @@
+import './unused'
+import { foo } from './used'
+
+log('used:' + foo)
diff --git a/playground/hmr-ssr/accept-exports/unused-exports/unused.ts b/playground/hmr-ssr/accept-exports/unused-exports/unused.ts
new file mode 100644
index 00000000000000..1462ed6101bba6
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/unused-exports/unused.ts
@@ -0,0 +1,11 @@
+export const x = 'x'
+
+export const y = 'y'
+
+export default 'z'
+
+log('-- unused --')
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports([])
+}
diff --git a/playground/hmr-ssr/accept-exports/unused-exports/used.ts b/playground/hmr-ssr/accept-exports/unused-exports/used.ts
new file mode 100644
index 00000000000000..a4a093f726e325
--- /dev/null
+++ b/playground/hmr-ssr/accept-exports/unused-exports/used.ts
@@ -0,0 +1,9 @@
+export const foo = 'foo0'
+
+export const bar = 'bar0'
+
+log('-- used --')
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports([])
+}
diff --git a/playground/hmr-ssr/circular/index.js b/playground/hmr-ssr/circular/index.js
new file mode 100644
index 00000000000000..a78188ea88f93c
--- /dev/null
+++ b/playground/hmr-ssr/circular/index.js
@@ -0,0 +1,7 @@
+import { msg } from './mod-a'
+
+globalThis.__HMR__['.circular'] = msg
+
+if (import.meta.hot) {
+ import.meta.hot.accept()
+}
diff --git a/playground/hmr-ssr/circular/mod-a.js b/playground/hmr-ssr/circular/mod-a.js
new file mode 100644
index 00000000000000..def8466da2e489
--- /dev/null
+++ b/playground/hmr-ssr/circular/mod-a.js
@@ -0,0 +1,5 @@
+export const value = 'mod-a'
+
+import { value as _value } from './mod-b'
+
+export const msg = `mod-a -> ${_value}`
diff --git a/playground/hmr-ssr/circular/mod-b.js b/playground/hmr-ssr/circular/mod-b.js
new file mode 100644
index 00000000000000..fe0125f33787b7
--- /dev/null
+++ b/playground/hmr-ssr/circular/mod-b.js
@@ -0,0 +1,3 @@
+import { value as _value } from './mod-c'
+
+export const value = `mod-b -> ${_value}`
diff --git a/playground/hmr-ssr/circular/mod-c.js b/playground/hmr-ssr/circular/mod-c.js
new file mode 100644
index 00000000000000..4f9de5b0efcc29
--- /dev/null
+++ b/playground/hmr-ssr/circular/mod-c.js
@@ -0,0 +1,11 @@
+import { value as _value } from './mod-a'
+
+// Should error as `_value` is not defined yet within the circular imports
+let __value
+try {
+ __value = `${_value} (expected no error)`
+} catch {
+ __value = 'mod-a (unexpected error)'
+}
+
+export const value = `mod-c -> ${__value}`
diff --git a/playground/hmr-ssr/counter/dep.ts b/playground/hmr-ssr/counter/dep.ts
new file mode 100644
index 00000000000000..e15e77f4e4743f
--- /dev/null
+++ b/playground/hmr-ssr/counter/dep.ts
@@ -0,0 +1,4 @@
+// This file is never loaded
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {})
+}
diff --git a/playground/hmr-ssr/counter/index.ts b/playground/hmr-ssr/counter/index.ts
new file mode 100644
index 00000000000000..66edcdbe737ed1
--- /dev/null
+++ b/playground/hmr-ssr/counter/index.ts
@@ -0,0 +1,11 @@
+let count = 0
+export function increment() {
+ count++
+}
+export function getCount() {
+ return count
+}
+// @ts-expect-error not used but this is to test that it works
+function neverCalled() {
+ import('./dep')
+}
diff --git a/packages/playground/hmr/customFile.js b/playground/hmr-ssr/customFile.js
similarity index 100%
rename from packages/playground/hmr/customFile.js
rename to playground/hmr-ssr/customFile.js
diff --git a/playground/hmr-ssr/event.d.ts b/playground/hmr-ssr/event.d.ts
new file mode 100644
index 00000000000000..1920d1e7aff076
--- /dev/null
+++ b/playground/hmr-ssr/event.d.ts
@@ -0,0 +1,17 @@
+import 'vite/types/customEvent'
+
+declare module 'vite/types/customEvent' {
+ interface CustomEventMap {
+ 'custom:foo': { msg: string }
+ 'custom:remote-add': { a: number; b: number }
+ 'custom:remote-add-result': { result: string }
+ }
+}
+
+declare global {
+ let log: (...msg: unknown[]) => void
+ let logger: {
+ error: (msg: string | Error) => void
+ debug: (...msg: unknown[]) => void
+ }
+}
diff --git a/playground/hmr-ssr/file-delete-restore/child.js b/playground/hmr-ssr/file-delete-restore/child.js
new file mode 100644
index 00000000000000..ddf10cd5a7a170
--- /dev/null
+++ b/playground/hmr-ssr/file-delete-restore/child.js
@@ -0,0 +1,19 @@
+import { rerender } from './runtime'
+
+export const value = 'child'
+
+if (import.meta.hot) {
+ import.meta.hot.accept((newMod) => {
+ if (!newMod) return
+
+ rerender({ child: newMod.value })
+ })
+
+ import.meta.hot.dispose(() => {
+ log('file-delete-restore/child.js is disposed')
+ })
+
+ import.meta.hot.prune(() => {
+ log('file-delete-restore/child.js is pruned')
+ })
+}
diff --git a/playground/hmr-ssr/file-delete-restore/index.js b/playground/hmr-ssr/file-delete-restore/index.js
new file mode 100644
index 00000000000000..fa4908a32662ac
--- /dev/null
+++ b/playground/hmr-ssr/file-delete-restore/index.js
@@ -0,0 +1,4 @@
+import { render } from './runtime'
+import { childValue, parentValue } from './parent'
+
+render({ parent: parentValue, child: childValue })
diff --git a/playground/hmr-ssr/file-delete-restore/parent.js b/playground/hmr-ssr/file-delete-restore/parent.js
new file mode 100644
index 00000000000000..050bfa6d49b4c0
--- /dev/null
+++ b/playground/hmr-ssr/file-delete-restore/parent.js
@@ -0,0 +1,12 @@
+import { rerender } from './runtime'
+
+export const parentValue = 'parent'
+export { value as childValue } from './child'
+
+if (import.meta.hot) {
+ import.meta.hot.accept((newMod) => {
+ if (!newMod) return
+
+ rerender({ child: newMod.childValue, parent: newMod.parentValue })
+ })
+}
diff --git a/playground/hmr-ssr/file-delete-restore/runtime.js b/playground/hmr-ssr/file-delete-restore/runtime.js
new file mode 100644
index 00000000000000..a3383fcf8ed777
--- /dev/null
+++ b/playground/hmr-ssr/file-delete-restore/runtime.js
@@ -0,0 +1,15 @@
+let state = {}
+
+export const render = (newState) => {
+ state = newState
+ apply()
+}
+
+export const rerender = (updates) => {
+ state = { ...state, ...updates }
+ apply()
+}
+
+const apply = () => {
+ globalThis.__HMR__['.file-delete-restore'] = Object.values(state).join(':')
+}
diff --git a/playground/hmr-ssr/hmr.ts b/playground/hmr-ssr/hmr.ts
new file mode 100644
index 00000000000000..f3efefdcb472d7
--- /dev/null
+++ b/playground/hmr-ssr/hmr.ts
@@ -0,0 +1,120 @@
+import { virtual } from 'virtual:file'
+import { foo as depFoo, nestedFoo } from './hmrDep'
+import './importing-updated'
+import './invalidation-circular-deps'
+import './invalidation/parent'
+import './file-delete-restore'
+import './optional-chaining/parent'
+import './intermediate-file-delete'
+import './circular'
+import './queries'
+import logo from './logo.svg'
+import logoNoInline from './logo-no-inline.svg'
+import { msg as softInvalidationMsg } from './soft-invalidation'
+
+export const foo = 1
+text('.app', foo)
+text('.dep', depFoo)
+text('.nested', nestedFoo)
+text('.virtual', virtual)
+text('.soft-invalidation', softInvalidationMsg)
+setImgSrc('#logo', logo)
+setImgSrc('#logo-no-inline', logoNoInline)
+
+globalThis.__HMR__['virtual:increment'] = () => {
+ if (import.meta.hot) {
+ import.meta.hot.send('virtual:increment')
+ }
+}
+
+if (import.meta.hot) {
+ import.meta.hot.accept(({ foo }) => {
+ log('(self-accepting 1) foo is now:', foo)
+ })
+
+ import.meta.hot.accept(({ foo }) => {
+ log('(self-accepting 2) foo is now:', foo)
+ })
+
+ const handleDep = (type, newFoo, newNestedFoo) => {
+ log(`(${type}) foo is now: ${newFoo}`)
+ log(`(${type}) nested foo is now: ${newNestedFoo}`)
+ text('.dep', newFoo)
+ text('.nested', newNestedFoo)
+ }
+
+ import.meta.hot.accept('./logo.svg', (newUrl) => {
+ setImgSrc('#logo', newUrl.default)
+ log('Logo updated', newUrl.default)
+ })
+
+ import.meta.hot.accept('./logo-no-inline.svg', (newUrl) => {
+ setImgSrc('#logo-no-inline', newUrl.default)
+ log('Logo-no-inline updated', newUrl.default)
+ })
+
+ import.meta.hot.accept('./hmrDep', ({ foo, nestedFoo }) => {
+ handleDep('single dep', foo, nestedFoo)
+ })
+
+ import.meta.hot.accept(['./hmrDep'], ([{ foo, nestedFoo }]) => {
+ handleDep('multi deps', foo, nestedFoo)
+ })
+
+ import.meta.hot.dispose(() => {
+ log(`foo was:`, foo)
+ })
+
+ import.meta.hot.on('vite:afterUpdate', (event) => {
+ log(`>>> vite:afterUpdate -- ${event.type}`)
+ })
+
+ import.meta.hot.on('vite:beforeUpdate', (event) => {
+ log(`>>> vite:beforeUpdate -- ${event.type}`)
+
+ const cssUpdate = event.updates.find(
+ (update) =>
+ update.type === 'css-update' && update.path.includes('global.css'),
+ )
+ if (cssUpdate) {
+ log('CSS updates are not supported in SSR')
+ }
+ })
+
+ import.meta.hot.on('vite:error', (event) => {
+ log(`>>> vite:error -- ${event.err.message}`)
+ })
+
+ import.meta.hot.on('vite:invalidate', ({ path }) => {
+ log(`>>> vite:invalidate -- ${path}`)
+ })
+
+ import.meta.hot.on('custom:foo', ({ msg }) => {
+ text('.custom', msg)
+ })
+
+ import.meta.hot.on('custom:remove', removeCb)
+
+ // send custom event to server to calculate 1 + 2
+ import.meta.hot.send('custom:remote-add', { a: 1, b: 2 })
+ import.meta.hot.on('custom:remote-add-result', ({ result }) => {
+ text('.custom-communication', result)
+ })
+}
+
+function text(el, text) {
+ hmr(el, text)
+}
+
+function setImgSrc(el, src) {
+ hmr(el, src)
+}
+
+function removeCb({ msg }) {
+ text('.toRemove', msg)
+ import.meta.hot.off('custom:remove', removeCb)
+}
+
+function hmr(key: string, value: unknown) {
+ ;(globalThis.__HMR__ as any)[key] = String(value)
+}
diff --git a/playground/hmr-ssr/hmrDep.js b/playground/hmr-ssr/hmrDep.js
new file mode 100644
index 00000000000000..c4c434146afc41
--- /dev/null
+++ b/playground/hmr-ssr/hmrDep.js
@@ -0,0 +1,14 @@
+export const foo = 1
+export { foo as nestedFoo } from './hmrNestedDep'
+
+if (import.meta.hot) {
+ const data = import.meta.hot.data
+ if ('fromDispose' in data) {
+ log(`(dep) foo from dispose: ${data.fromDispose}`)
+ }
+
+ import.meta.hot.dispose((data) => {
+ log(`(dep) foo was: ${foo}`)
+ data.fromDispose = foo
+ })
+}
diff --git a/packages/playground/hmr/hmrNestedDep.js b/playground/hmr-ssr/hmrNestedDep.js
similarity index 100%
rename from packages/playground/hmr/hmrNestedDep.js
rename to playground/hmr-ssr/hmrNestedDep.js
diff --git a/playground/hmr-ssr/importedVirtual.js b/playground/hmr-ssr/importedVirtual.js
new file mode 100644
index 00000000000000..8b0b417bc3113d
--- /dev/null
+++ b/playground/hmr-ssr/importedVirtual.js
@@ -0,0 +1 @@
+export const virtual = '[success]'
diff --git a/playground/hmr-ssr/importing-updated/a.js b/playground/hmr-ssr/importing-updated/a.js
new file mode 100644
index 00000000000000..e52ef8d3dce2d7
--- /dev/null
+++ b/playground/hmr-ssr/importing-updated/a.js
@@ -0,0 +1,9 @@
+const val = 'a0'
+globalThis.__HMR__['.importing-reloaded'] ??= ''
+globalThis.__HMR__['.importing-reloaded'] += `a.js: ${val} `
+
+export default val
+
+if (import.meta.hot) {
+ import.meta.hot.accept()
+}
diff --git a/playground/hmr-ssr/importing-updated/b.js b/playground/hmr-ssr/importing-updated/b.js
new file mode 100644
index 00000000000000..d309a396a3c56d
--- /dev/null
+++ b/playground/hmr-ssr/importing-updated/b.js
@@ -0,0 +1,10 @@
+import a from './a.js'
+
+const val = `b0,${a}`
+
+globalThis.__HMR__['.importing-reloaded'] ??= ''
+globalThis.__HMR__['.importing-reloaded'] += `b.js: ${val} `
+
+if (import.meta.hot) {
+ import.meta.hot.accept()
+}
diff --git a/playground/hmr-ssr/importing-updated/index.js b/playground/hmr-ssr/importing-updated/index.js
new file mode 100644
index 00000000000000..0cc74268d385de
--- /dev/null
+++ b/playground/hmr-ssr/importing-updated/index.js
@@ -0,0 +1,2 @@
+import './a'
+import './b'
diff --git a/playground/hmr-ssr/intermediate-file-delete/display.js b/playground/hmr-ssr/intermediate-file-delete/display.js
new file mode 100644
index 00000000000000..3ab1936b0c9009
--- /dev/null
+++ b/playground/hmr-ssr/intermediate-file-delete/display.js
@@ -0,0 +1 @@
+export const displayCount = (count) => `count is ${count}`
diff --git a/playground/hmr-ssr/intermediate-file-delete/index.js b/playground/hmr-ssr/intermediate-file-delete/index.js
new file mode 100644
index 00000000000000..30435b7606e273
--- /dev/null
+++ b/playground/hmr-ssr/intermediate-file-delete/index.js
@@ -0,0 +1,21 @@
+import { displayCount } from './re-export.js'
+
+const incrementValue = () =>
+ globalThis.__HMR__['.intermediate-file-delete-increment']
+
+const render = () => {
+ globalThis.__HMR__['.intermediate-file-delete-display'] = displayCount(
+ Number(incrementValue()),
+ )
+}
+
+render()
+
+globalThis.__HMR__['.delete-intermediate-file'] = () => {
+ globalThis.__HMR__['.intermediate-file-delete-increment'] = `${
+ Number(incrementValue()) + 1
+ }`
+ render()
+}
+
+if (import.meta.hot) import.meta.hot.accept()
diff --git a/playground/hmr-ssr/intermediate-file-delete/re-export.js b/playground/hmr-ssr/intermediate-file-delete/re-export.js
new file mode 100644
index 00000000000000..b2dade525c0675
--- /dev/null
+++ b/playground/hmr-ssr/intermediate-file-delete/re-export.js
@@ -0,0 +1 @@
+export * from './display.js'
diff --git a/playground/hmr-ssr/invalidation-circular-deps/circular-invalidate/child.js b/playground/hmr-ssr/invalidation-circular-deps/circular-invalidate/child.js
new file mode 100644
index 00000000000000..502aeedf296c8d
--- /dev/null
+++ b/playground/hmr-ssr/invalidation-circular-deps/circular-invalidate/child.js
@@ -0,0 +1,9 @@
+import './parent'
+
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {
+ import.meta.hot.invalidate()
+ })
+}
+
+export const value = 'child'
diff --git a/playground/hmr-ssr/invalidation-circular-deps/circular-invalidate/parent.js b/playground/hmr-ssr/invalidation-circular-deps/circular-invalidate/parent.js
new file mode 100644
index 00000000000000..c29064f0901acc
--- /dev/null
+++ b/playground/hmr-ssr/invalidation-circular-deps/circular-invalidate/parent.js
@@ -0,0 +1,12 @@
+import { value } from './child'
+
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {
+ import.meta.hot.invalidate()
+ })
+}
+
+log('(invalidation circular deps) parent is executing')
+setTimeout(() => {
+ globalThis.__HMR__['.invalidation-circular-deps'] = value
+})
diff --git a/playground/hmr-ssr/invalidation-circular-deps/index.js b/playground/hmr-ssr/invalidation-circular-deps/index.js
new file mode 100644
index 00000000000000..f45400604b138b
--- /dev/null
+++ b/playground/hmr-ssr/invalidation-circular-deps/index.js
@@ -0,0 +1,2 @@
+import './circular-invalidate/parent'
+import './invalidate-handled-in-circle/parent'
diff --git a/playground/hmr-ssr/invalidation-circular-deps/invalidate-handled-in-circle/child.js b/playground/hmr-ssr/invalidation-circular-deps/invalidate-handled-in-circle/child.js
new file mode 100644
index 00000000000000..502aeedf296c8d
--- /dev/null
+++ b/playground/hmr-ssr/invalidation-circular-deps/invalidate-handled-in-circle/child.js
@@ -0,0 +1,9 @@
+import './parent'
+
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {
+ import.meta.hot.invalidate()
+ })
+}
+
+export const value = 'child'
diff --git a/playground/hmr-ssr/invalidation-circular-deps/invalidate-handled-in-circle/parent.js b/playground/hmr-ssr/invalidation-circular-deps/invalidate-handled-in-circle/parent.js
new file mode 100644
index 00000000000000..28d2c5e0105145
--- /dev/null
+++ b/playground/hmr-ssr/invalidation-circular-deps/invalidate-handled-in-circle/parent.js
@@ -0,0 +1,10 @@
+import { value } from './child'
+
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {})
+}
+
+log('(invalidation circular deps handled) parent is executing')
+setTimeout(() => {
+ globalThis.__HMR__['.invalidation-circular-deps-handled'] = value
+})
diff --git a/playground/hmr-ssr/invalidation/child.js b/playground/hmr-ssr/invalidation/child.js
new file mode 100644
index 00000000000000..b424e2f83c3233
--- /dev/null
+++ b/playground/hmr-ssr/invalidation/child.js
@@ -0,0 +1,9 @@
+if (import.meta.hot) {
+ // Need to accept, to register a callback for HMR
+ import.meta.hot.accept(() => {
+ // Trigger HMR in importers
+ import.meta.hot.invalidate()
+ })
+}
+
+export const value = 'child'
diff --git a/playground/hmr-ssr/invalidation/parent.js b/playground/hmr-ssr/invalidation/parent.js
new file mode 100644
index 00000000000000..80f80e58348da8
--- /dev/null
+++ b/playground/hmr-ssr/invalidation/parent.js
@@ -0,0 +1,9 @@
+import { value } from './child'
+
+if (import.meta.hot) {
+ import.meta.hot.accept()
+}
+
+log('(invalidation) parent is executing')
+
+globalThis.__HMR__['.invalidation'] = value
diff --git a/playground/hmr-ssr/logo-no-inline.svg b/playground/hmr-ssr/logo-no-inline.svg
new file mode 100644
index 00000000000000..a85344da4790b2
--- /dev/null
+++ b/playground/hmr-ssr/logo-no-inline.svg
@@ -0,0 +1,3 @@
+
+ Vite
+
diff --git a/playground/hmr-ssr/logo.svg b/playground/hmr-ssr/logo.svg
new file mode 100644
index 00000000000000..a85344da4790b2
--- /dev/null
+++ b/playground/hmr-ssr/logo.svg
@@ -0,0 +1,3 @@
+
+ Vite
+
diff --git a/playground/hmr-ssr/missing-import/a.js b/playground/hmr-ssr/missing-import/a.js
new file mode 100644
index 00000000000000..fff5559cec149d
--- /dev/null
+++ b/playground/hmr-ssr/missing-import/a.js
@@ -0,0 +1,3 @@
+import 'missing-modules'
+
+log('missing test')
diff --git a/playground/hmr-ssr/missing-import/index.js b/playground/hmr-ssr/missing-import/index.js
new file mode 100644
index 00000000000000..5ad5ba12cc8619
--- /dev/null
+++ b/playground/hmr-ssr/missing-import/index.js
@@ -0,0 +1 @@
+import './main.js'
diff --git a/playground/hmr-ssr/missing-import/main.js b/playground/hmr-ssr/missing-import/main.js
new file mode 100644
index 00000000000000..999801e4dd1061
--- /dev/null
+++ b/playground/hmr-ssr/missing-import/main.js
@@ -0,0 +1 @@
+import './a.js'
diff --git a/playground/hmr-ssr/modules.d.ts b/playground/hmr-ssr/modules.d.ts
new file mode 100644
index 00000000000000..815c25568d5119
--- /dev/null
+++ b/playground/hmr-ssr/modules.d.ts
@@ -0,0 +1,11 @@
+declare module 'virtual:file' {
+ export const virtual: string
+}
+declare module '*?query1' {
+ const string: string
+ export default string
+}
+declare module '*?query2' {
+ const string: string
+ export default string
+}
diff --git a/playground/hmr-ssr/non-tested/dep.js b/playground/hmr-ssr/non-tested/dep.js
new file mode 100644
index 00000000000000..ed53b404c625a0
--- /dev/null
+++ b/playground/hmr-ssr/non-tested/dep.js
@@ -0,0 +1,3 @@
+export const test = 'true'
+
+import.meta.hot.accept()
diff --git a/playground/hmr-ssr/non-tested/index.js b/playground/hmr-ssr/non-tested/index.js
new file mode 100644
index 00000000000000..eeac757e0506eb
--- /dev/null
+++ b/playground/hmr-ssr/non-tested/index.js
@@ -0,0 +1,9 @@
+import { test } from './dep.js'
+
+function main() {
+ test()
+}
+
+main()
+
+import.meta.hot.accept('./dep.js')
diff --git a/playground/hmr-ssr/optional-chaining/child.js b/playground/hmr-ssr/optional-chaining/child.js
new file mode 100644
index 00000000000000..766766a6260612
--- /dev/null
+++ b/playground/hmr-ssr/optional-chaining/child.js
@@ -0,0 +1 @@
+export const foo = 1
diff --git a/playground/hmr-ssr/optional-chaining/parent.js b/playground/hmr-ssr/optional-chaining/parent.js
new file mode 100644
index 00000000000000..c4d9468bf67907
--- /dev/null
+++ b/playground/hmr-ssr/optional-chaining/parent.js
@@ -0,0 +1,8 @@
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+import { foo } from './child'
+
+import.meta.hot?.accept('./child', ({ foo }) => {
+ log('(optional-chaining) child update')
+ globalThis.__HMR__['.optional-chaining'] = foo
+})
diff --git a/playground/hmr-ssr/package.json b/playground/hmr-ssr/package.json
new file mode 100644
index 00000000000000..52a5646e2da7a4
--- /dev/null
+++ b/playground/hmr-ssr/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-hmr-ssr",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/hmr-ssr/queries/index.js b/playground/hmr-ssr/queries/index.js
new file mode 100644
index 00000000000000..113eb1a079af40
--- /dev/null
+++ b/playground/hmr-ssr/queries/index.js
@@ -0,0 +1,9 @@
+import query1 from './multi-query?query1'
+import query2 from './multi-query?query2'
+
+hmr('query1', query1)
+hmr('query2', query2)
+
+function hmr(key, value) {
+ globalThis.__HMR__[key] = String(value)
+}
diff --git a/playground/hmr-ssr/queries/multi-query.js b/playground/hmr-ssr/queries/multi-query.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/hmr-ssr/self-accept-within-circular/a.js b/playground/hmr-ssr/self-accept-within-circular/a.js
new file mode 100644
index 00000000000000..a559b739d9f253
--- /dev/null
+++ b/playground/hmr-ssr/self-accept-within-circular/a.js
@@ -0,0 +1,5 @@
+import { b } from './b'
+
+export const a = {
+ b,
+}
diff --git a/playground/hmr-ssr/self-accept-within-circular/b.js b/playground/hmr-ssr/self-accept-within-circular/b.js
new file mode 100644
index 00000000000000..4f5a135418728c
--- /dev/null
+++ b/playground/hmr-ssr/self-accept-within-circular/b.js
@@ -0,0 +1,7 @@
+import { c } from './c'
+
+const b = {
+ c,
+}
+
+export { b }
diff --git a/playground/hmr-ssr/self-accept-within-circular/c.js b/playground/hmr-ssr/self-accept-within-circular/c.js
new file mode 100644
index 00000000000000..7fe30c447965a2
--- /dev/null
+++ b/playground/hmr-ssr/self-accept-within-circular/c.js
@@ -0,0 +1,12 @@
+import './b'
+
+export const c = 'c'
+
+function render(content) {
+ globalThis.__HMR__['.self-accept-within-circular'] = content
+}
+render(c)
+
+import.meta.hot?.accept((nextExports) => {
+ render(nextExports?.c)
+})
diff --git a/playground/hmr-ssr/self-accept-within-circular/index.js b/playground/hmr-ssr/self-accept-within-circular/index.js
new file mode 100644
index 00000000000000..d826a1226a5e66
--- /dev/null
+++ b/playground/hmr-ssr/self-accept-within-circular/index.js
@@ -0,0 +1,3 @@
+import { a } from './a'
+
+log(a)
diff --git a/playground/hmr-ssr/soft-invalidation/child.js b/playground/hmr-ssr/soft-invalidation/child.js
new file mode 100644
index 00000000000000..21ec276fc7f825
--- /dev/null
+++ b/playground/hmr-ssr/soft-invalidation/child.js
@@ -0,0 +1 @@
+export const foo = 'bar'
diff --git a/playground/hmr-ssr/soft-invalidation/index.js b/playground/hmr-ssr/soft-invalidation/index.js
new file mode 100644
index 00000000000000..f236a2579b0c24
--- /dev/null
+++ b/playground/hmr-ssr/soft-invalidation/index.js
@@ -0,0 +1,4 @@
+import { foo } from './child'
+
+// @ts-expect-error global
+export const msg = `soft-invalidation/index.js is transformed ${__TRANSFORM_COUNT__} times. child is ${foo}`
diff --git a/playground/hmr-ssr/unresolved.ts b/playground/hmr-ssr/unresolved.ts
new file mode 100644
index 00000000000000..f260385025ac97
--- /dev/null
+++ b/playground/hmr-ssr/unresolved.ts
@@ -0,0 +1,20 @@
+export const foo = 1
+hmr('.app', foo)
+
+if (import.meta.hot) {
+ import.meta.hot.accept(({ foo }) => {
+ log('(self-accepting 1) foo is now:', foo)
+ })
+
+ import.meta.hot.accept(({ foo }) => {
+ log('(self-accepting 2) foo is now:', foo)
+ })
+
+ import.meta.hot.dispose(() => {
+ log(`foo was:`, foo)
+ })
+}
+
+function hmr(key: string, value: unknown) {
+ ;(globalThis.__HMR__ as any)[key] = String(value)
+}
diff --git a/playground/hmr-ssr/vite.config.ts b/playground/hmr-ssr/vite.config.ts
new file mode 100644
index 00000000000000..34bb3100b087d1
--- /dev/null
+++ b/playground/hmr-ssr/vite.config.ts
@@ -0,0 +1,97 @@
+import { defineConfig } from 'vite'
+import type { Plugin } from 'vite'
+
+export default defineConfig({
+ experimental: {
+ hmrPartialAccept: true,
+ },
+ build: {
+ assetsInlineLimit(filePath) {
+ if (filePath.endsWith('logo-no-inline.svg')) {
+ return false
+ }
+ },
+ },
+ plugins: [
+ {
+ name: 'mock-custom',
+ async hotUpdate({ file, read, server }) {
+ if (file.endsWith('customFile.js')) {
+ const content = await read()
+ const msg = content.match(/export const msg = '(\w+)'/)[1]
+ this.environment.hot.send('custom:foo', { msg })
+ this.environment.hot.send('custom:remove', { msg })
+ }
+ },
+ configureServer(server) {
+ server.environments.ssr.hot.on(
+ 'custom:remote-add',
+ ({ a, b }, client) => {
+ client.send('custom:remote-add-result', { result: a + b })
+ },
+ )
+ },
+ },
+ virtualPlugin(),
+ transformCountPlugin(),
+ queryPlugin(),
+ ],
+})
+
+function virtualPlugin(): Plugin {
+ let num = 0
+ return {
+ name: 'virtual-file',
+ resolveId(id, importer) {
+ if (id === 'virtual:file' || id === '\0virtual:file') {
+ return '\0virtual:file'
+ }
+ },
+ load(id) {
+ if (id === '\0virtual:file') {
+ return `\
+import { virtual as _virtual } from "/importedVirtual.js";
+export const virtual = _virtual + '${num}';`
+ }
+ },
+ configureServer(server) {
+ server.environments.ssr.hot.on('virtual:increment', async () => {
+ const mod =
+ await server.environments.ssr.moduleGraph.getModuleByUrl(
+ '\0virtual:file',
+ )
+ if (mod) {
+ num++
+ server.environments.ssr.reloadModule(mod)
+ }
+ })
+ },
+ }
+}
+
+function queryPlugin(): Plugin {
+ return {
+ name: 'query-resolver',
+ transform(code, id) {
+ if (id.includes('?query1')) {
+ return `export default ${JSON.stringify(code + 'query1')}`
+ }
+
+ if (id.includes('?query2')) {
+ return `export default ${JSON.stringify(code + 'query2')}`
+ }
+ },
+ }
+}
+
+function transformCountPlugin(): Plugin {
+ let num = 0
+ return {
+ name: 'transform-count',
+ transform(code) {
+ if (code.includes('__TRANSFORM_COUNT__')) {
+ return code.replace('__TRANSFORM_COUNT__', String(++num))
+ }
+ },
+ }
+}
diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts
new file mode 100644
index 00000000000000..19d4257fa17b26
--- /dev/null
+++ b/playground/hmr/__tests__/hmr.spec.ts
@@ -0,0 +1,1101 @@
+import { beforeAll, describe, expect, it, test } from 'vitest'
+import type { Page } from 'playwright-chromium'
+import {
+ addFile,
+ browser,
+ browserLogs,
+ editFile,
+ getBg,
+ getColor,
+ isBuild,
+ page,
+ readFile,
+ removeFile,
+ serverLogs,
+ untilBrowserLogAfter,
+ untilUpdated,
+ viteTestUrl,
+} from '~utils'
+
+test('should render', async () => {
+ expect(await page.textContent('.app')).toBe('1')
+ expect(await page.textContent('.dep')).toBe('1')
+ expect(await page.textContent('.nested')).toBe('1')
+})
+
+if (!isBuild) {
+ test('should connect', async () => {
+ expect(browserLogs.length).toBe(5)
+ expect(browserLogs.some((msg) => msg.includes('connected'))).toBe(true)
+ browserLogs.length = 0
+ })
+
+ test('self accept', async () => {
+ const el = await page.$('.app')
+ await untilBrowserLogAfter(
+ () =>
+ editFile('hmr.ts', (code) =>
+ code.replace('const foo = 1', 'const foo = 2'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ 'foo was: 1',
+ '(self-accepting 1) foo is now: 2',
+ '(self-accepting 2) foo is now: 2',
+ '[vite] hot updated: /hmr.ts',
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el.textContent(), '2')
+
+ await untilBrowserLogAfter(
+ () =>
+ editFile('hmr.ts', (code) =>
+ code.replace('const foo = 2', 'const foo = 3'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ 'foo was: 2',
+ '(self-accepting 1) foo is now: 3',
+ '(self-accepting 2) foo is now: 3',
+ '[vite] hot updated: /hmr.ts',
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el.textContent(), '3')
+ })
+
+ test('accept dep', async () => {
+ const el = await page.$('.dep')
+ await untilBrowserLogAfter(
+ () =>
+ editFile('hmrDep.js', (code) =>
+ code.replace('const foo = 1', 'const foo = 2'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ '(dep) foo was: 1',
+ '(dep) foo from dispose: 1',
+ '(single dep) foo is now: 2',
+ '(single dep) nested foo is now: 1',
+ '(multi deps) foo is now: 2',
+ '(multi deps) nested foo is now: 1',
+ '[vite] hot updated: /hmrDep.js via /hmr.ts',
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el.textContent(), '2')
+
+ await untilBrowserLogAfter(
+ () =>
+ editFile('hmrDep.js', (code) =>
+ code.replace('const foo = 2', 'const foo = 3'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ '(dep) foo was: 2',
+ '(dep) foo from dispose: 2',
+ '(single dep) foo is now: 3',
+ '(single dep) nested foo is now: 1',
+ '(multi deps) foo is now: 3',
+ '(multi deps) nested foo is now: 1',
+ '[vite] hot updated: /hmrDep.js via /hmr.ts',
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el.textContent(), '3')
+ })
+
+ test('nested dep propagation', async () => {
+ const el = await page.$('.nested')
+ await untilBrowserLogAfter(
+ () =>
+ editFile('hmrNestedDep.js', (code) =>
+ code.replace('const foo = 1', 'const foo = 2'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ '(dep) foo was: 3',
+ '(dep) foo from dispose: 3',
+ '(single dep) foo is now: 3',
+ '(single dep) nested foo is now: 2',
+ '(multi deps) foo is now: 3',
+ '(multi deps) nested foo is now: 2',
+ '[vite] hot updated: /hmrDep.js via /hmr.ts',
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el.textContent(), '2')
+
+ await untilBrowserLogAfter(
+ () =>
+ editFile('hmrNestedDep.js', (code) =>
+ code.replace('const foo = 2', 'const foo = 3'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ '(dep) foo was: 3',
+ '(dep) foo from dispose: 3',
+ '(single dep) foo is now: 3',
+ '(single dep) nested foo is now: 3',
+ '(multi deps) foo is now: 3',
+ '(multi deps) nested foo is now: 3',
+ '[vite] hot updated: /hmrDep.js via /hmr.ts',
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el.textContent(), '3')
+ })
+
+ test('invalidate', async () => {
+ const el = await page.$('.invalidation-parent')
+ await untilBrowserLogAfter(
+ () =>
+ editFile('invalidation/child.js', (code) =>
+ code.replace('child', 'child updated'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ '>>> vite:invalidate -- /invalidation/child.js',
+ '[vite] invalidate /invalidation/child.js',
+ '[vite] hot updated: /invalidation/child.js',
+ '>>> vite:afterUpdate -- update',
+ '>>> vite:beforeUpdate -- update',
+ '(invalidation) parent is executing',
+ '[vite] hot updated: /invalidation/parent.js',
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el.textContent(), 'child updated')
+ })
+
+ test('invalidate works with multiple tabs', async () => {
+ let page2: Page
+ try {
+ page2 = await browser.newPage()
+ await page2.goto(viteTestUrl)
+
+ const el = await page.$('.invalidation-parent')
+ await untilBrowserLogAfter(
+ () =>
+ editFile('invalidation/child.js', (code) =>
+ code.replace('child', 'child updated'),
+ ),
+ [
+ '>>> vite:beforeUpdate -- update',
+ '>>> vite:invalidate -- /invalidation/child.js',
+ '[vite] invalidate /invalidation/child.js',
+ '[vite] hot updated: /invalidation/child.js',
+ '>>> vite:afterUpdate -- update',
+ // if invalidate dedupe doesn't work correctly, this beforeUpdate will be called twice
+ '>>> vite:beforeUpdate -- update',
+ '(invalidation) parent is executing',
+ '[vite] hot updated: /invalidation/parent.js',
+ '>>> vite:afterUpdate -- update',
+ ],
+ true,
+ )
+ await untilUpdated(() => el.textContent(), 'child updated')
+ } finally {
+ await page2.close()
+ }
+ })
+
+ test('invalidate on root triggers page reload', async () => {
+ editFile('invalidation/root.js', (code) => code.replace('Init', 'Updated'))
+ await page.waitForEvent('load')
+ await untilUpdated(
+ async () => (await page.$('.invalidation-root')).textContent(),
+ 'Updated',
+ )
+ })
+
+ test('soft invalidate', async () => {
+ const el = await page.$('.soft-invalidation')
+ expect(await el.textContent()).toBe(
+ 'soft-invalidation/index.js is transformed 1 times. child is bar',
+ )
+ editFile('soft-invalidation/child.js', (code) =>
+ code.replace('bar', 'updated'),
+ )
+ await untilUpdated(
+ () => el.textContent(),
+ 'soft-invalidation/index.js is transformed 1 times. child is updated',
+ )
+
+ editFile('soft-invalidation/index.js', (code) =>
+ code.replace('child is', 'child is now'),
+ )
+ editFile('soft-invalidation/child.js', (code) =>
+ code.replace('updated', 'updated?'),
+ )
+ await untilUpdated(
+ () => el.textContent(),
+ 'soft-invalidation/index.js is transformed 2 times. child is now updated?',
+ )
+ })
+
+ test('invalidate in circular dep should not trigger infinite HMR', async () => {
+ const el = await page.$('.invalidation-circular-deps')
+ await untilUpdated(() => el.textContent(), 'child')
+ editFile(
+ 'invalidation-circular-deps/circular-invalidate/child.js',
+ (code) => code.replace('child', 'child updated'),
+ )
+ await page.waitForEvent('load')
+ await untilUpdated(
+ () => page.textContent('.invalidation-circular-deps'),
+ 'child updated',
+ )
+ })
+
+ test('invalidate in circular dep should be hot updated if possible', async () => {
+ const el = await page.$('.invalidation-circular-deps-handled')
+ await untilUpdated(() => el.textContent(), 'child')
+ editFile(
+ 'invalidation-circular-deps/invalidate-handled-in-circle/child.js',
+ (code) => code.replace('child', 'child updated'),
+ )
+ await untilUpdated(() => el.textContent(), 'child updated')
+ })
+
+ test('plugin hmr handler + custom event', async () => {
+ const el = await page.$('.custom')
+ editFile('customFile.js', (code) => code.replace('custom', 'edited'))
+ await untilUpdated(() => el.textContent(), 'edited')
+ })
+
+ test('plugin hmr remove custom events', async () => {
+ const el = await page.$('.toRemove')
+ editFile('customFile.js', (code) => code.replace('custom', 'edited'))
+ await untilUpdated(() => el.textContent(), 'edited')
+ editFile('customFile.js', (code) => code.replace('edited', 'custom'))
+ await untilUpdated(() => el.textContent(), 'edited')
+ })
+
+ test('plugin client-server communication', async () => {
+ const el = await page.$('.custom-communication')
+ await untilUpdated(() => el.textContent(), '3')
+ })
+
+ test('full-reload encodeURI path', async () => {
+ await page.goto(
+ viteTestUrl + '/unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html',
+ )
+ const el = await page.$('#app')
+ expect(await el.textContent()).toBe('title')
+ editFile('unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', (code) =>
+ code.replace('title', 'title2'),
+ )
+ await page.waitForEvent('load')
+ await untilUpdated(
+ async () => (await page.$('#app')).textContent(),
+ 'title2',
+ )
+ })
+
+ test('CSS update preserves query params', async () => {
+ await page.goto(viteTestUrl)
+
+ editFile('global.css', (code) => code.replace('white', 'tomato'))
+
+ const elprev = await page.$('.css-prev')
+ const elpost = await page.$('.css-post')
+ await untilUpdated(() => elprev.textContent(), 'param=required')
+ await untilUpdated(() => elpost.textContent(), 'param=required')
+ const textprev = await elprev.textContent()
+ const textpost = await elpost.textContent()
+ expect(textprev).not.toBe(textpost)
+ expect(textprev).not.toMatch('direct')
+ expect(textpost).not.toMatch('direct')
+ })
+
+ test('it swaps out link tags', async () => {
+ await page.goto(viteTestUrl)
+
+ editFile('global.css', (code) => code.replace('white', 'tomato'))
+
+ let el = await page.$('.link-tag-added')
+ await untilUpdated(() => el.textContent(), 'yes')
+
+ el = await page.$('.link-tag-removed')
+ await untilUpdated(() => el.textContent(), 'yes')
+
+ expect((await page.$$('link')).length).toBe(1)
+ })
+
+ test('not loaded dynamic import', async () => {
+ await page.goto(viteTestUrl + '/counter/index.html', { waitUntil: 'load' })
+
+ let btn = await page.$('button')
+ expect(await btn.textContent()).toBe('Counter 0')
+ await btn.click()
+ expect(await btn.textContent()).toBe('Counter 1')
+
+ // Modifying `index.ts` triggers a page reload, as expected
+ const indexTsLoadPromise = page.waitForEvent('load')
+ editFile('counter/index.ts', (code) => code)
+ await indexTsLoadPromise
+ btn = await page.$('button')
+ expect(await btn.textContent()).toBe('Counter 0')
+
+ await btn.click()
+ expect(await btn.textContent()).toBe('Counter 1')
+
+ // #7561
+ // `dep.ts` defines `import.module.hot.accept` and has not been loaded.
+ // Therefore, modifying it has no effect (doesn't trigger a page reload).
+ // (Note that, a dynamic import that is never loaded and that does not
+ // define `accept.module.hot.accept` may wrongfully trigger a full page
+ // reload, see discussion at #7561.)
+ const depTsLoadPromise = page.waitForEvent('load', { timeout: 1000 })
+ editFile('counter/dep.ts', (code) => code)
+ await expect(depTsLoadPromise).rejects.toThrow(
+ /page\.waitForEvent: Timeout \d+ms exceeded while waiting for event "load"/,
+ )
+
+ btn = await page.$('button')
+ expect(await btn.textContent()).toBe('Counter 1')
+ })
+
+ // #2255
+ test('importing reloaded', async () => {
+ await page.goto(viteTestUrl)
+ const outputEle = await page.$('.importing-reloaded')
+ const getOutput = () => {
+ return outputEle.innerHTML()
+ }
+
+ await untilUpdated(getOutput, ['a.js: a0', 'b.js: b0,a0'].join(' '))
+
+ editFile('importing-updated/a.js', (code) => code.replace("'a0'", "'a1'"))
+ await untilUpdated(
+ getOutput,
+ ['a.js: a0', 'b.js: b0,a0', 'a.js: a1'].join(' '),
+ )
+
+ editFile('importing-updated/b.js', (code) =>
+ code.replace('`b0,${a}`', '`b1,${a}`'),
+ )
+ // note that "a.js: a1" should not happen twice after "b.js: b0,a0'"
+ await untilUpdated(
+ getOutput,
+ ['a.js: a0', 'b.js: b0,a0', 'a.js: a1', 'b.js: b1,a1'].join(' '),
+ )
+ })
+
+ describe('acceptExports', () => {
+ const HOT_UPDATED = /hot updated/
+ const CONNECTED = /connected/
+
+ const baseDir = 'accept-exports'
+
+ describe('when all used exports are accepted', () => {
+ const testDir = baseDir + '/main-accepted'
+
+ const fileName = 'target.ts'
+ const file = `${testDir}/${fileName}`
+ const url = '/' + file
+
+ let dep = 'dep0'
+
+ beforeAll(async () => {
+ await untilBrowserLogAfter(
+ () => page.goto(`${viteTestUrl}/${testDir}/`),
+ [CONNECTED, />>>>>>/],
+ (logs) => {
+ expect(logs).toContain(`<<<<<< A0 B0 D0 ; ${dep}`)
+ expect(logs).toContain('>>>>>> A0 D0')
+ },
+ )
+ })
+
+ it('the callback is called with the new version the module', async () => {
+ const callbackFile = `${testDir}/callback.ts`
+ const callbackUrl = '/' + callbackFile
+
+ await untilBrowserLogAfter(
+ () => {
+ editFile(callbackFile, (code) =>
+ code
+ .replace("x = 'X'", "x = 'Y'")
+ .replace('reloaded >>>', 'reloaded (2) >>>'),
+ )
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ 'reloaded >>> Y',
+ `[vite] hot updated: ${callbackUrl}`,
+ ])
+ },
+ )
+
+ await untilBrowserLogAfter(
+ () => {
+ editFile(callbackFile, (code) => code.replace("x = 'Y'", "x = 'Z'"))
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ 'reloaded (2) >>> Z',
+ `[vite] hot updated: ${callbackUrl}`,
+ ])
+ },
+ )
+ })
+
+ it('stops HMR bubble on dependency change', async () => {
+ const depFileName = 'dep.ts'
+ const depFile = `${testDir}/${depFileName}`
+
+ await untilBrowserLogAfter(
+ () => {
+ editFile(depFile, (code) => code.replace('dep0', (dep = 'dep1')))
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ `<<<<<< A0 B0 D0 ; ${dep}`,
+ `[vite] hot updated: ${url}`,
+ ])
+ },
+ )
+ })
+
+ it('accepts itself and refreshes on change', async () => {
+ await untilBrowserLogAfter(
+ () => {
+ editFile(file, (code) => code.replace(/(\b[A-Z])0/g, '$11'))
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ `<<<<<< A1 B1 D1 ; ${dep}`,
+ `[vite] hot updated: ${url}`,
+ ])
+ },
+ )
+ })
+
+ it('accepts itself and refreshes on 2nd change', async () => {
+ await untilBrowserLogAfter(
+ () => {
+ editFile(file, (code) =>
+ code
+ .replace(/(\b[A-Z])1/g, '$12')
+ .replace(
+ "acceptExports(['a', 'default']",
+ "acceptExports(['b', 'default']",
+ ),
+ )
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ `<<<<<< A2 B2 D2 ; ${dep}`,
+ `[vite] hot updated: ${url}`,
+ ])
+ },
+ )
+ })
+
+ it('does not accept itself anymore after acceptedExports change', async () => {
+ await untilBrowserLogAfter(
+ async () => {
+ editFile(file, (code) => code.replace(/(\b[A-Z])2/g, '$13'))
+ await page.waitForEvent('load')
+ },
+ [CONNECTED, />>>>>>/],
+ (logs) => {
+ expect(logs).toContain(`<<<<<< A3 B3 D3 ; ${dep}`)
+ expect(logs).toContain('>>>>>> A3 D3')
+ },
+ )
+ })
+ })
+
+ describe('when some used exports are not accepted', () => {
+ const testDir = baseDir + '/main-non-accepted'
+
+ const namedFileName = 'named.ts'
+ const namedFile = `${testDir}/${namedFileName}`
+ const defaultFileName = 'default.ts'
+ const defaultFile = `${testDir}/${defaultFileName}`
+ const depFileName = 'dep.ts'
+ const depFile = `${testDir}/${depFileName}`
+
+ const a = 'A0'
+ let dep = 'dep0'
+
+ beforeAll(async () => {
+ await untilBrowserLogAfter(
+ () => page.goto(`${viteTestUrl}/${testDir}/`),
+ [CONNECTED, />>>>>>/],
+ (logs) => {
+ expect(logs).toContain(`<<< named: ${a} ; ${dep}`)
+ expect(logs).toContain(`<<< default: def0`)
+ expect(logs).toContain(`>>>>>> ${a} def0`)
+ },
+ )
+ })
+
+ it('does not stop the HMR bubble on change to dep', async () => {
+ await untilBrowserLogAfter(
+ async () => {
+ editFile(depFile, (code) => code.replace('dep0', (dep = 'dep1')))
+ await page.waitForEvent('load')
+ },
+ [CONNECTED, />>>>>>/],
+ (logs) => {
+ expect(logs).toContain(`<<< named: ${a} ; ${dep}`)
+ },
+ )
+ })
+
+ describe('does not stop the HMR bubble on change to self', () => {
+ it('with named exports', async () => {
+ await untilBrowserLogAfter(
+ async () => {
+ editFile(namedFile, (code) => code.replace(a, 'A1'))
+ await page.waitForEvent('load')
+ },
+ [CONNECTED, />>>>>>/],
+ (logs) => {
+ expect(logs).toContain(`<<< named: A1 ; ${dep}`)
+ },
+ )
+ })
+
+ it('with default export', async () => {
+ await untilBrowserLogAfter(
+ async () => {
+ editFile(defaultFile, (code) => code.replace('def0', 'def1'))
+ await page.waitForEvent('load')
+ },
+ [CONNECTED, />>>>>>/],
+ (logs) => {
+ expect(logs).toContain(`<<< default: def1`)
+ },
+ )
+ })
+ })
+ })
+
+ test('accepts itself when imported for side effects only (no bindings imported)', async () => {
+ const testDir = baseDir + '/side-effects'
+ const file = 'side-effects.ts'
+
+ await untilBrowserLogAfter(
+ () => page.goto(`${viteTestUrl}/${testDir}/`),
+ [CONNECTED, />>>/],
+ (logs) => {
+ expect(logs).toContain('>>> side FX')
+ },
+ )
+
+ await untilBrowserLogAfter(
+ () => {
+ editFile(`${testDir}/${file}`, (code) =>
+ code.replace('>>> side FX', '>>> side FX !!'),
+ )
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ '>>> side FX !!',
+ `[vite] hot updated: /${testDir}/${file}`,
+ ])
+ },
+ )
+ })
+
+ describe('acceptExports([])', () => {
+ const testDir = baseDir + '/unused-exports'
+
+ test('accepts itself if no exports are imported', async () => {
+ const fileName = 'unused.ts'
+ const file = `${testDir}/${fileName}`
+ const url = '/' + file
+
+ await untilBrowserLogAfter(
+ () => page.goto(`${viteTestUrl}/${testDir}/`),
+ [CONNECTED, '-- unused --'],
+ (logs) => {
+ expect(logs).toContain('-- unused --')
+ },
+ )
+
+ await untilBrowserLogAfter(
+ () => {
+ editFile(file, (code) =>
+ code.replace('-- unused --', '-> unused <-'),
+ )
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual(['-> unused <-', `[vite] hot updated: ${url}`])
+ },
+ )
+ })
+
+ test("doesn't accept itself if any of its exports is imported", async () => {
+ const fileName = 'used.ts'
+ const file = `${testDir}/${fileName}`
+
+ await untilBrowserLogAfter(
+ () => page.goto(`${viteTestUrl}/${testDir}/`),
+ [CONNECTED, '-- used --'],
+ (logs) => {
+ expect(logs).toContain('-- used --')
+ expect(logs).toContain('used:foo0')
+ },
+ )
+
+ await untilBrowserLogAfter(
+ async () => {
+ editFile(file, (code) =>
+ code.replace('foo0', 'foo1').replace('-- used --', '-> used <-'),
+ )
+ await page.waitForEvent('load')
+ },
+ [CONNECTED, /used:foo/],
+ (logs) => {
+ expect(logs).toContain('-> used <-')
+ expect(logs).toContain('used:foo1')
+ },
+ )
+ })
+ })
+
+ describe('indiscriminate imports: import *', () => {
+ const testStarExports = (testDirName: string) => {
+ const testDir = `${baseDir}/${testDirName}`
+
+ it('accepts itself if all its exports are accepted', async () => {
+ const fileName = 'deps-all-accepted.ts'
+ const file = `${testDir}/${fileName}`
+ const url = '/' + file
+
+ await untilBrowserLogAfter(
+ () => page.goto(`${viteTestUrl}/${testDir}/`),
+ [CONNECTED, '>>> ready <<<'],
+ (logs) => {
+ expect(logs).toContain('loaded:all:a0b0c0default0')
+ expect(logs).toContain('all >>>>>> a0, b0, c0')
+ },
+ )
+
+ await untilBrowserLogAfter(
+ () => {
+ editFile(file, (code) => code.replace(/([abc])0/g, '$11'))
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ 'all >>>>>> a1, b1, c1',
+ `[vite] hot updated: ${url}`,
+ ])
+ },
+ )
+
+ await untilBrowserLogAfter(
+ () => {
+ editFile(file, (code) => code.replace(/([abc])1/g, '$12'))
+ },
+ HOT_UPDATED,
+ (logs) => {
+ expect(logs).toEqual([
+ 'all >>>>>> a2, b2, c2',
+ `[vite] hot updated: ${url}`,
+ ])
+ },
+ )
+ })
+
+ it("doesn't accept itself if one export is not accepted", async () => {
+ const fileName = 'deps-some-accepted.ts'
+ const file = `${testDir}/${fileName}`
+
+ await untilBrowserLogAfter(
+ () => page.goto(`${viteTestUrl}/${testDir}/`),
+ [CONNECTED, '>>> ready <<<'],
+ (logs) => {
+ expect(logs).toContain('loaded:some:a0b0c0default0')
+ expect(logs).toContain('some >>>>>> a0, b0, c0')
+ },
+ )
+
+ await untilBrowserLogAfter(
+ async () => {
+ const loadPromise = page.waitForEvent('load')
+ editFile(file, (code) => code.replace(/([abc])0/g, '$11'))
+ await loadPromise
+ },
+ [CONNECTED, '>>> ready <<<'],
+ (logs) => {
+ expect(logs).toContain('loaded:some:a1b1c1default0')
+ expect(logs).toContain('some >>>>>> a1, b1, c1')
+ },
+ )
+ })
+ }
+
+ describe('import * from ...', () => testStarExports('star-imports'))
+
+ describe('dynamic import(...)', () => testStarExports('dynamic-imports'))
+ })
+ })
+
+ test('css in html hmr', async () => {
+ await page.goto(viteTestUrl)
+ expect(await getBg('.import-image')).toMatch('icon')
+ await page.goto(viteTestUrl + '/foo/', { waitUntil: 'load' })
+ expect(await getBg('.import-image')).toMatch('icon')
+
+ const loadPromise = page.waitForEvent('load')
+ editFile('index.html', (code) => code.replace('url("./icon.png")', ''))
+ await loadPromise
+ expect(await getBg('.import-image')).toMatch('')
+ })
+
+ test('HTML', async () => {
+ await page.goto(viteTestUrl + '/counter/index.html')
+ let btn = await page.$('button')
+ expect(await btn.textContent()).toBe('Counter 0')
+
+ const loadPromise = page.waitForEvent('load')
+ editFile('counter/index.html', (code) =>
+ code.replace('Counter', 'Compteur'),
+ )
+ await loadPromise
+ btn = await page.$('button')
+ expect(await btn.textContent()).toBe('Compteur 0')
+ })
+
+ test('handle virtual module updates', async () => {
+ await page.goto(viteTestUrl)
+ const el = await page.$('.virtual')
+ expect(await el.textContent()).toBe('[success]0')
+ editFile('importedVirtual.js', (code) => code.replace('[success]', '[wow]'))
+ await untilUpdated(async () => {
+ const el = await page.$('.virtual')
+ return await el.textContent()
+ }, '[wow]')
+ })
+
+ test('invalidate virtual module', async () => {
+ await page.goto(viteTestUrl)
+ const el = await page.$('.virtual')
+ expect(await el.textContent()).toBe('[wow]0')
+ const btn = await page.$('.virtual-update')
+ btn.click()
+ await untilUpdated(async () => {
+ const el = await page.$('.virtual')
+ return await el.textContent()
+ }, '[wow]1')
+ })
+
+ test('handle virtual module accept updates', async () => {
+ await page.goto(viteTestUrl)
+ const el = await page.$('.virtual-dep')
+ expect(await el.textContent()).toBe('0')
+ editFile('importedVirtual.js', (code) => code.replace('[success]', '[wow]'))
+ await untilUpdated(async () => {
+ const el = await page.$('.virtual-dep')
+ return await el.textContent()
+ }, '[wow]')
+ })
+
+ test('invalidate virtual module and accept', async () => {
+ await page.goto(viteTestUrl)
+ const el = await page.$('.virtual-dep')
+ expect(await el.textContent()).toBe('0')
+ const btn = await page.$('.virtual-update-dep')
+ btn.click()
+ await untilUpdated(async () => {
+ const el = await page.$('.virtual-dep')
+ return await el.textContent()
+ }, '[wow]2')
+ })
+
+ test('keep hmr reload after missing import on server startup', async () => {
+ const file = 'missing-import/a.js'
+ const importCode = "import 'missing-modules'"
+ const unImportCode = `// ${importCode}`
+
+ await untilBrowserLogAfter(
+ () =>
+ page.goto(viteTestUrl + '/missing-import/index.html', {
+ waitUntil: 'load',
+ }),
+ /connected/, // wait for HMR connection
+ )
+
+ await untilBrowserLogAfter(async () => {
+ const loadPromise = page.waitForEvent('load')
+ editFile(file, (code) => code.replace(importCode, unImportCode))
+ await loadPromise
+ }, ['missing test', /connected/])
+
+ await untilBrowserLogAfter(async () => {
+ const loadPromise = page.waitForEvent('load')
+ editFile(file, (code) => code.replace(unImportCode, importCode))
+ await loadPromise
+ }, [/500/, /connected/])
+ })
+
+ test('should hmr when file is deleted and restored', async () => {
+ await page.goto(viteTestUrl)
+
+ const parentFile = 'file-delete-restore/parent.js'
+ const childFile = 'file-delete-restore/child.js'
+
+ await untilUpdated(
+ () => page.textContent('.file-delete-restore'),
+ 'parent:child',
+ )
+
+ editFile(childFile, (code) =>
+ code.replace("value = 'child'", "value = 'child1'"),
+ )
+ await untilUpdated(
+ () => page.textContent('.file-delete-restore'),
+ 'parent:child1',
+ )
+
+ // delete the file
+ editFile(parentFile, (code) =>
+ code.replace(
+ "export { value as childValue } from './child'",
+ "export const childValue = 'not-child'",
+ ),
+ )
+ const originalChildFileCode = readFile(childFile)
+ await Promise.all([
+ untilBrowserLogAfter(
+ () => removeFile(childFile),
+ `${childFile} is disposed`,
+ ),
+ untilUpdated(
+ () => page.textContent('.file-delete-restore'),
+ 'parent:not-child',
+ ),
+ ])
+
+ await untilBrowserLogAfter(async () => {
+ const loadPromise = page.waitForEvent('load')
+ addFile(childFile, originalChildFileCode)
+ editFile(parentFile, (code) =>
+ code.replace(
+ "export const childValue = 'not-child'",
+ "export { value as childValue } from './child'",
+ ),
+ )
+ await loadPromise
+ }, [/connected/])
+ await untilUpdated(
+ () => page.textContent('.file-delete-restore'),
+ 'parent:child',
+ )
+ })
+
+ test('delete file should not break hmr', async () => {
+ await page.goto(viteTestUrl)
+
+ await untilUpdated(
+ () => page.textContent('.intermediate-file-delete-display'),
+ 'count is 1',
+ )
+
+ // add state
+ await page.click('.intermediate-file-delete-increment')
+ await untilUpdated(
+ () => page.textContent('.intermediate-file-delete-display'),
+ 'count is 2',
+ )
+
+ // update import, hmr works
+ editFile('intermediate-file-delete/index.js', (code) =>
+ code.replace("from './re-export.js'", "from './display.js'"),
+ )
+ editFile('intermediate-file-delete/display.js', (code) =>
+ code.replace('count is ${count}', 'count is ${count}!'),
+ )
+ await untilUpdated(
+ () => page.textContent('.intermediate-file-delete-display'),
+ 'count is 2!',
+ )
+
+ // remove unused file, page reload because it's considered entry point now
+ removeFile('intermediate-file-delete/re-export.js')
+ await untilUpdated(
+ () => page.textContent('.intermediate-file-delete-display'),
+ 'count is 1!',
+ )
+
+ // re-add state
+ await page.click('.intermediate-file-delete-increment')
+ await untilUpdated(
+ () => page.textContent('.intermediate-file-delete-display'),
+ 'count is 2!',
+ )
+
+ // hmr works after file deletion
+ editFile('intermediate-file-delete/display.js', (code) =>
+ code.replace('count is ${count}!', 'count is ${count}'),
+ )
+ await untilUpdated(
+ () => page.textContent('.intermediate-file-delete-display'),
+ 'count is 2',
+ )
+ })
+
+ test('deleted file should trigger dispose and prune callbacks', async () => {
+ await page.goto(viteTestUrl)
+
+ const parentFile = 'file-delete-restore/parent.js'
+ const childFile = 'file-delete-restore/child.js'
+ const originalChildFileCode = readFile(childFile)
+
+ await untilBrowserLogAfter(
+ () => {
+ // delete the file
+ editFile(parentFile, (code) =>
+ code.replace(
+ "export { value as childValue } from './child'",
+ "export const childValue = 'not-child'",
+ ),
+ )
+ removeFile(childFile)
+ },
+ [
+ 'file-delete-restore/child.js is disposed',
+ 'file-delete-restore/child.js is pruned',
+ ],
+ false,
+ )
+ await untilUpdated(
+ () => page.textContent('.file-delete-restore'),
+ 'parent:not-child',
+ )
+
+ // restore the file
+ addFile(childFile, originalChildFileCode)
+ editFile(parentFile, (code) =>
+ code.replace(
+ "export const childValue = 'not-child'",
+ "export { value as childValue } from './child'",
+ ),
+ )
+ await untilUpdated(
+ () => page.textContent('.file-delete-restore'),
+ 'parent:child',
+ )
+ })
+
+ test('import.meta.hot?.accept', async () => {
+ await page.goto(viteTestUrl)
+
+ const el = await page.$('.optional-chaining')
+ await untilBrowserLogAfter(
+ () =>
+ editFile('optional-chaining/child.js', (code) =>
+ code.replace('const foo = 1', 'const foo = 2'),
+ ),
+ '(optional-chaining) child update',
+ )
+ await untilUpdated(() => el.textContent(), '2')
+ })
+
+ test('hmr works for self-accepted module within circular imported files', async () => {
+ await page.goto(viteTestUrl + '/self-accept-within-circular/index.html')
+ const el = await page.$('.self-accept-within-circular')
+ expect(await el.textContent()).toBe('c')
+ editFile('self-accept-within-circular/c.js', (code) =>
+ code.replace(`export const c = 'c'`, `export const c = 'cc'`),
+ )
+ await untilUpdated(
+ () => page.textContent('.self-accept-within-circular'),
+ 'cc',
+ )
+ expect(serverLogs.length).greaterThanOrEqual(1)
+ // Should still keep hmr update, but it'll error on the browser-side and will refresh itself.
+ // Match on full log not possible because of color markers
+ expect(serverLogs.at(-1)!).toContain('hmr update')
+ })
+
+ test('hmr should not reload if no accepted within circular imported files', async () => {
+ await page.goto(viteTestUrl + '/circular/index.html')
+ const el = await page.$('.circular')
+ expect(await el.textContent()).toBe(
+ 'mod-a -> mod-b -> mod-c -> mod-a (expected error)',
+ )
+ editFile('circular/mod-b.js', (code) =>
+ code.replace(`mod-b ->`, `mod-b (edited) ->`),
+ )
+ await untilUpdated(
+ () => el.textContent(),
+ 'mod-a -> mod-b (edited) -> mod-c -> mod-a (expected error)',
+ )
+ })
+
+ test('not inlined assets HMR', async () => {
+ await page.goto(viteTestUrl)
+ const el = await page.$('#logo-no-inline')
+ await untilBrowserLogAfter(
+ () =>
+ editFile('logo-no-inline.svg', (code) =>
+ code.replace('height="30px"', 'height="40px"'),
+ ),
+ /Logo-no-inline updated/,
+ )
+ await untilUpdated(() => el.evaluate((it) => `${it.clientHeight}`), '40')
+ })
+
+ test('inlined assets HMR', async () => {
+ await page.goto(viteTestUrl)
+ const el = await page.$('#logo')
+ await untilBrowserLogAfter(
+ () =>
+ editFile('logo.svg', (code) =>
+ code.replace('height="30px"', 'height="40px"'),
+ ),
+ /Logo updated/,
+ )
+ await untilUpdated(() => el.evaluate((it) => `${it.clientHeight}`), '40')
+ })
+
+ test('CSS HMR with this.addWatchFile', async () => {
+ await page.goto(viteTestUrl + '/css-deps/index.html')
+ expect(await getColor('.css-deps')).toMatch('red')
+ editFile('css-deps/dep.js', (code) => code.replace(`red`, `green`))
+ await untilUpdated(() => getColor('.css-deps'), 'green')
+ })
+
+ test('hmr should happen after missing file is created', async () => {
+ const file = 'missing-file/a.js'
+ const code = 'console.log("a.js")'
+
+ await untilBrowserLogAfter(
+ () =>
+ page.goto(viteTestUrl + '/missing-file/index.html', {
+ waitUntil: 'load',
+ }),
+ /connected/, // wait for HMR connection
+ )
+
+ await untilBrowserLogAfter(async () => {
+ const loadPromise = page.waitForEvent('load')
+ addFile(file, code)
+ await loadPromise
+ }, [/connected/, 'a.js'])
+ })
+}
diff --git a/playground/hmr/accept-exports/dynamic-imports/deps-all-accepted.ts b/playground/hmr/accept-exports/dynamic-imports/deps-all-accepted.ts
new file mode 100644
index 00000000000000..b837884b0615ee
--- /dev/null
+++ b/playground/hmr/accept-exports/dynamic-imports/deps-all-accepted.ts
@@ -0,0 +1,14 @@
+export const a = 'a0'
+
+export const b = 'b0'
+
+const aliased = 'c0'
+export { aliased as c }
+
+export default 'default0'
+
+console.log(`all >>>>>> ${a}, ${b}, ${aliased}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['a', 'b', 'c', 'default'])
+}
diff --git a/playground/hmr/accept-exports/dynamic-imports/deps-some-accepted.ts b/playground/hmr/accept-exports/dynamic-imports/deps-some-accepted.ts
new file mode 100644
index 00000000000000..05fd5b9fb4e510
--- /dev/null
+++ b/playground/hmr/accept-exports/dynamic-imports/deps-some-accepted.ts
@@ -0,0 +1,14 @@
+export const a = 'a0'
+
+export const b = 'b0'
+
+const aliased = 'c0'
+export { aliased as c }
+
+export default 'default0'
+
+console.log(`some >>>>>> ${a}, ${b}, ${aliased}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['a', 'b', 'default'])
+}
diff --git a/playground/hmr/accept-exports/dynamic-imports/dynamic-imports.ts b/playground/hmr/accept-exports/dynamic-imports/dynamic-imports.ts
new file mode 100644
index 00000000000000..0314ff68b90134
--- /dev/null
+++ b/playground/hmr/accept-exports/dynamic-imports/dynamic-imports.ts
@@ -0,0 +1,9 @@
+Promise.all([import('./deps-all-accepted'), import('./deps-some-accepted')])
+ .then(([all, some]) => {
+ console.log('loaded:all:' + all.a + all.b + all.c + all.default)
+ console.log('loaded:some:' + some.a + some.b + some.c + some.default)
+ console.log('>>> ready <<<')
+ })
+ .catch((err) => {
+ console.error(err)
+ })
diff --git a/playground/hmr/accept-exports/dynamic-imports/index.html b/playground/hmr/accept-exports/dynamic-imports/index.html
new file mode 100644
index 00000000000000..18fc78767ede6b
--- /dev/null
+++ b/playground/hmr/accept-exports/dynamic-imports/index.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/playground/hmr/accept-exports/export-from/depA.ts b/playground/hmr/accept-exports/export-from/depA.ts
new file mode 100644
index 00000000000000..e2eda670ed0097
--- /dev/null
+++ b/playground/hmr/accept-exports/export-from/depA.ts
@@ -0,0 +1 @@
+export const a = 'Ax'
diff --git a/playground/hmr/accept-exports/export-from/export-from.ts b/playground/hmr/accept-exports/export-from/export-from.ts
new file mode 100644
index 00000000000000..991dee89fe8fb8
--- /dev/null
+++ b/playground/hmr/accept-exports/export-from/export-from.ts
@@ -0,0 +1,8 @@
+import { a } from './hub'
+
+console.log(a)
+
+if (import.meta.hot) {
+ import.meta.hot.accept()
+} else {
+}
diff --git a/playground/hmr/accept-exports/export-from/hub.ts b/playground/hmr/accept-exports/export-from/hub.ts
new file mode 100644
index 00000000000000..5bd0dc05608909
--- /dev/null
+++ b/playground/hmr/accept-exports/export-from/hub.ts
@@ -0,0 +1 @@
+export * from './depA'
diff --git a/playground/hmr/accept-exports/export-from/index.html b/playground/hmr/accept-exports/export-from/index.html
new file mode 100644
index 00000000000000..0dde1345f085e2
--- /dev/null
+++ b/playground/hmr/accept-exports/export-from/index.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/playground/hmr/accept-exports/main-accepted/callback.ts b/playground/hmr/accept-exports/main-accepted/callback.ts
new file mode 100644
index 00000000000000..3890c8d4a550f2
--- /dev/null
+++ b/playground/hmr/accept-exports/main-accepted/callback.ts
@@ -0,0 +1,7 @@
+export const x = 'X'
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['x'], (m) => {
+ console.log(`reloaded >>> ${m.x}`)
+ })
+}
diff --git a/playground/hmr/accept-exports/main-accepted/dep.ts b/playground/hmr/accept-exports/main-accepted/dep.ts
new file mode 100644
index 00000000000000..b9f67fd33a75f8
--- /dev/null
+++ b/playground/hmr/accept-exports/main-accepted/dep.ts
@@ -0,0 +1 @@
+export default 'dep0'
diff --git a/playground/hmr/accept-exports/main-accepted/index.html b/playground/hmr/accept-exports/main-accepted/index.html
new file mode 100644
index 00000000000000..8d576b0e135457
--- /dev/null
+++ b/playground/hmr/accept-exports/main-accepted/index.html
@@ -0,0 +1 @@
+
diff --git a/playground/hmr/accept-exports/main-accepted/main-accepted.ts b/playground/hmr/accept-exports/main-accepted/main-accepted.ts
new file mode 100644
index 00000000000000..0156fbf39d92e2
--- /dev/null
+++ b/playground/hmr/accept-exports/main-accepted/main-accepted.ts
@@ -0,0 +1,7 @@
+import def, { a } from './target'
+import { x } from './callback'
+
+// we don't want to pollute other checks' logs...
+if (0 > 1) console.log(x)
+
+console.log(`>>>>>> ${a} ${def}`)
diff --git a/playground/hmr/accept-exports/main-accepted/target.ts b/playground/hmr/accept-exports/main-accepted/target.ts
new file mode 100644
index 00000000000000..d34a7e261bbd13
--- /dev/null
+++ b/playground/hmr/accept-exports/main-accepted/target.ts
@@ -0,0 +1,16 @@
+import dep from './dep'
+
+export const a = 'A0'
+
+const bValue = 'B0'
+export { bValue as b }
+
+const def = 'D0'
+
+export default def
+
+console.log(`<<<<<< ${a} ${bValue} ${def} ; ${dep}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['a', 'default'])
+}
diff --git a/playground/hmr/accept-exports/main-non-accepted/default.ts b/playground/hmr/accept-exports/main-non-accepted/default.ts
new file mode 100644
index 00000000000000..7cb33058a0fa7a
--- /dev/null
+++ b/playground/hmr/accept-exports/main-non-accepted/default.ts
@@ -0,0 +1,11 @@
+export const x = 'y'
+
+const def = 'def0'
+
+export default def
+
+console.log(`<<< default: ${def}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['x'])
+}
diff --git a/playground/hmr/accept-exports/main-non-accepted/dep.ts b/playground/hmr/accept-exports/main-non-accepted/dep.ts
new file mode 100644
index 00000000000000..b9f67fd33a75f8
--- /dev/null
+++ b/playground/hmr/accept-exports/main-non-accepted/dep.ts
@@ -0,0 +1 @@
+export default 'dep0'
diff --git a/playground/hmr/accept-exports/main-non-accepted/index.html b/playground/hmr/accept-exports/main-non-accepted/index.html
new file mode 100644
index 00000000000000..8630d9a8739a3a
--- /dev/null
+++ b/playground/hmr/accept-exports/main-non-accepted/index.html
@@ -0,0 +1 @@
+
diff --git a/playground/hmr/accept-exports/main-non-accepted/main-non-accepted.ts b/playground/hmr/accept-exports/main-non-accepted/main-non-accepted.ts
new file mode 100644
index 00000000000000..99b41b1fd2d376
--- /dev/null
+++ b/playground/hmr/accept-exports/main-non-accepted/main-non-accepted.ts
@@ -0,0 +1,4 @@
+import { a } from './named'
+import def from './default'
+
+console.log(`>>>>>> ${a} ${def}`)
diff --git a/playground/hmr/accept-exports/main-non-accepted/named.ts b/playground/hmr/accept-exports/main-non-accepted/named.ts
new file mode 100644
index 00000000000000..847717b7982964
--- /dev/null
+++ b/playground/hmr/accept-exports/main-non-accepted/named.ts
@@ -0,0 +1,11 @@
+import dep from './dep'
+
+export const a = 'A0'
+
+export const b = 'B0'
+
+console.log(`<<< named: ${a} ; ${dep}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['b'])
+}
diff --git a/playground/hmr/accept-exports/reexports.bak/accept-named.ts b/playground/hmr/accept-exports/reexports.bak/accept-named.ts
new file mode 100644
index 00000000000000..e63b352a5247fc
--- /dev/null
+++ b/playground/hmr/accept-exports/reexports.bak/accept-named.ts
@@ -0,0 +1,10 @@
+export { a, b } from './source'
+
+if (import.meta.hot) {
+ // import.meta.hot.accept('./source', (m) => {
+ // console.log(`accept-named reexport:${m.a},${m.b}`)
+ // })
+ import.meta.hot.acceptExports('a', (m) => {
+ console.log(`accept-named reexport:${m.a},${m.b}`)
+ })
+}
diff --git a/playground/hmr/accept-exports/reexports.bak/index.html b/playground/hmr/accept-exports/reexports.bak/index.html
new file mode 100644
index 00000000000000..241054bca8256f
--- /dev/null
+++ b/playground/hmr/accept-exports/reexports.bak/index.html
@@ -0,0 +1 @@
+
diff --git a/playground/hmr/accept-exports/reexports.bak/reexports.ts b/playground/hmr/accept-exports/reexports.bak/reexports.ts
new file mode 100644
index 00000000000000..56fd0d32f3cbbc
--- /dev/null
+++ b/playground/hmr/accept-exports/reexports.bak/reexports.ts
@@ -0,0 +1,5 @@
+import { a } from './accept-named'
+
+console.log('accept-named:' + a)
+
+console.log('>>> ready')
diff --git a/playground/hmr/accept-exports/reexports.bak/source.ts b/playground/hmr/accept-exports/reexports.bak/source.ts
new file mode 100644
index 00000000000000..7f736004a8e9fa
--- /dev/null
+++ b/playground/hmr/accept-exports/reexports.bak/source.ts
@@ -0,0 +1,2 @@
+export const a = 'a0'
+export const b = 'b0'
diff --git a/playground/hmr/accept-exports/side-effects/index.html b/playground/hmr/accept-exports/side-effects/index.html
new file mode 100644
index 00000000000000..7b94f06a5a6a8a
--- /dev/null
+++ b/playground/hmr/accept-exports/side-effects/index.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/playground/hmr/accept-exports/side-effects/side-effects.ts b/playground/hmr/accept-exports/side-effects/side-effects.ts
new file mode 100644
index 00000000000000..a9c4644fdd656b
--- /dev/null
+++ b/playground/hmr/accept-exports/side-effects/side-effects.ts
@@ -0,0 +1,13 @@
+export const x = 'x'
+
+export const y = 'y'
+
+export default 'z'
+
+console.log('>>> side FX')
+
+document.querySelector('.app').textContent = 'hey'
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['default'])
+}
diff --git a/playground/hmr/accept-exports/star-imports/deps-all-accepted.ts b/playground/hmr/accept-exports/star-imports/deps-all-accepted.ts
new file mode 100644
index 00000000000000..b837884b0615ee
--- /dev/null
+++ b/playground/hmr/accept-exports/star-imports/deps-all-accepted.ts
@@ -0,0 +1,14 @@
+export const a = 'a0'
+
+export const b = 'b0'
+
+const aliased = 'c0'
+export { aliased as c }
+
+export default 'default0'
+
+console.log(`all >>>>>> ${a}, ${b}, ${aliased}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['a', 'b', 'c', 'default'])
+}
diff --git a/playground/hmr/accept-exports/star-imports/deps-some-accepted.ts b/playground/hmr/accept-exports/star-imports/deps-some-accepted.ts
new file mode 100644
index 00000000000000..05fd5b9fb4e510
--- /dev/null
+++ b/playground/hmr/accept-exports/star-imports/deps-some-accepted.ts
@@ -0,0 +1,14 @@
+export const a = 'a0'
+
+export const b = 'b0'
+
+const aliased = 'c0'
+export { aliased as c }
+
+export default 'default0'
+
+console.log(`some >>>>>> ${a}, ${b}, ${aliased}`)
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports(['a', 'b', 'default'])
+}
diff --git a/playground/hmr/accept-exports/star-imports/index.html b/playground/hmr/accept-exports/star-imports/index.html
new file mode 100644
index 00000000000000..742ddd4dd2a3f8
--- /dev/null
+++ b/playground/hmr/accept-exports/star-imports/index.html
@@ -0,0 +1 @@
+
diff --git a/playground/hmr/accept-exports/star-imports/star-imports.ts b/playground/hmr/accept-exports/star-imports/star-imports.ts
new file mode 100644
index 00000000000000..e65d00aaf389be
--- /dev/null
+++ b/playground/hmr/accept-exports/star-imports/star-imports.ts
@@ -0,0 +1,6 @@
+import * as all from './deps-all-accepted'
+import * as some from './deps-some-accepted'
+
+console.log('loaded:all:' + all.a + all.b + all.c + all.default)
+console.log('loaded:some:' + some.a + some.b + some.c + some.default)
+console.log('>>> ready <<<')
diff --git a/playground/hmr/accept-exports/unused-exports/index.html b/playground/hmr/accept-exports/unused-exports/index.html
new file mode 100644
index 00000000000000..8998d3ce4581ee
--- /dev/null
+++ b/playground/hmr/accept-exports/unused-exports/index.html
@@ -0,0 +1 @@
+
diff --git a/playground/hmr/accept-exports/unused-exports/index.ts b/playground/hmr/accept-exports/unused-exports/index.ts
new file mode 100644
index 00000000000000..e7dde82c9037f8
--- /dev/null
+++ b/playground/hmr/accept-exports/unused-exports/index.ts
@@ -0,0 +1,4 @@
+import './unused'
+import { foo } from './used'
+
+console.log('used:' + foo)
diff --git a/playground/hmr/accept-exports/unused-exports/unused.ts b/playground/hmr/accept-exports/unused-exports/unused.ts
new file mode 100644
index 00000000000000..e7ce31166f4437
--- /dev/null
+++ b/playground/hmr/accept-exports/unused-exports/unused.ts
@@ -0,0 +1,11 @@
+export const x = 'x'
+
+export const y = 'y'
+
+export default 'z'
+
+console.log('-- unused --')
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports([])
+}
diff --git a/playground/hmr/accept-exports/unused-exports/used.ts b/playground/hmr/accept-exports/unused-exports/used.ts
new file mode 100644
index 00000000000000..a62fd5afc9cebb
--- /dev/null
+++ b/playground/hmr/accept-exports/unused-exports/used.ts
@@ -0,0 +1,9 @@
+export const foo = 'foo0'
+
+export const bar = 'bar0'
+
+console.log('-- used --')
+
+if (import.meta.hot) {
+ import.meta.hot.acceptExports([])
+}
diff --git a/playground/hmr/circular/index.js b/playground/hmr/circular/index.js
new file mode 100644
index 00000000000000..4c67f476f4269d
--- /dev/null
+++ b/playground/hmr/circular/index.js
@@ -0,0 +1,7 @@
+import { msg } from './mod-a'
+
+document.querySelector('.circular').textContent = msg
+
+if (import.meta.hot) {
+ import.meta.hot.accept()
+}
diff --git a/playground/hmr/circular/mod-a.js b/playground/hmr/circular/mod-a.js
new file mode 100644
index 00000000000000..def8466da2e489
--- /dev/null
+++ b/playground/hmr/circular/mod-a.js
@@ -0,0 +1,5 @@
+export const value = 'mod-a'
+
+import { value as _value } from './mod-b'
+
+export const msg = `mod-a -> ${_value}`
diff --git a/playground/hmr/circular/mod-b.js b/playground/hmr/circular/mod-b.js
new file mode 100644
index 00000000000000..fe0125f33787b7
--- /dev/null
+++ b/playground/hmr/circular/mod-b.js
@@ -0,0 +1,3 @@
+import { value as _value } from './mod-c'
+
+export const value = `mod-b -> ${_value}`
diff --git a/playground/hmr/circular/mod-c.js b/playground/hmr/circular/mod-c.js
new file mode 100644
index 00000000000000..671d43fa32d46c
--- /dev/null
+++ b/playground/hmr/circular/mod-c.js
@@ -0,0 +1,11 @@
+import { value as _value } from './mod-a'
+
+// Should error as `_value` is not defined yet within the circular imports
+let __value
+try {
+ __value = `${_value} (unexpected no error)`
+} catch {
+ __value = 'mod-a (expected error)'
+}
+
+export const value = `mod-c -> ${__value}`
diff --git a/playground/hmr/counter/dep.ts b/playground/hmr/counter/dep.ts
new file mode 100644
index 00000000000000..e15e77f4e4743f
--- /dev/null
+++ b/playground/hmr/counter/dep.ts
@@ -0,0 +1,4 @@
+// This file is never loaded
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {})
+}
diff --git a/playground/hmr/counter/index.html b/playground/hmr/counter/index.html
new file mode 100644
index 00000000000000..f5290ad4f1e507
--- /dev/null
+++ b/playground/hmr/counter/index.html
@@ -0,0 +1,2 @@
+Counter 0
+
diff --git a/playground/hmr/counter/index.ts b/playground/hmr/counter/index.ts
new file mode 100644
index 00000000000000..0230140278989f
--- /dev/null
+++ b/playground/hmr/counter/index.ts
@@ -0,0 +1,12 @@
+const btn = document.querySelector('button')
+let count = 0
+const update = () => {
+ btn.textContent = `Counter ${count}`
+}
+btn.onclick = () => {
+ count++
+ update()
+}
+function neverCalled() {
+ import('./dep')
+}
diff --git a/playground/hmr/css-deps/dep.js b/playground/hmr/css-deps/dep.js
new file mode 100644
index 00000000000000..07f62c4241209d
--- /dev/null
+++ b/playground/hmr/css-deps/dep.js
@@ -0,0 +1,8 @@
+// This file is depended by main.css via this.addWatchFile
+export const color = 'red'
+
+// Self-accept so that updating this file would not trigger a page reload.
+// We only want to observe main.css updating itself.
+if (import.meta.hot) {
+ import.meta.hot.accept()
+}
diff --git a/playground/hmr/css-deps/index.html b/playground/hmr/css-deps/index.html
new file mode 100644
index 00000000000000..6ff27f56556e00
--- /dev/null
+++ b/playground/hmr/css-deps/index.html
@@ -0,0 +1,8 @@
+should be red
+
+
diff --git a/playground/hmr/css-deps/main.css b/playground/hmr/css-deps/main.css
new file mode 100644
index 00000000000000..65cea880bd580e
--- /dev/null
+++ b/playground/hmr/css-deps/main.css
@@ -0,0 +1,3 @@
+.css-deps {
+ color: replaced;
+}
diff --git a/playground/hmr/customFile.js b/playground/hmr/customFile.js
new file mode 100644
index 00000000000000..7c9069974578e0
--- /dev/null
+++ b/playground/hmr/customFile.js
@@ -0,0 +1 @@
+export const msg = 'custom'
diff --git a/playground/hmr/event.d.ts b/playground/hmr/event.d.ts
new file mode 100644
index 00000000000000..151a9cc3b861cd
--- /dev/null
+++ b/playground/hmr/event.d.ts
@@ -0,0 +1,9 @@
+import 'vite/types/customEvent'
+
+declare module 'vite/types/customEvent' {
+ interface CustomEventMap {
+ 'custom:foo': { msg: string }
+ 'custom:remote-add': { a: number; b: number }
+ 'custom:remote-add-result': { result: string }
+ }
+}
diff --git a/playground/hmr/file-delete-restore/child.js b/playground/hmr/file-delete-restore/child.js
new file mode 100644
index 00000000000000..7031ef7db067c3
--- /dev/null
+++ b/playground/hmr/file-delete-restore/child.js
@@ -0,0 +1,19 @@
+import { rerender } from './runtime'
+
+export const value = 'child'
+
+if (import.meta.hot) {
+ import.meta.hot.accept((newMod) => {
+ if (!newMod) return
+
+ rerender({ child: newMod.value })
+ })
+
+ import.meta.hot.dispose(() => {
+ console.log('file-delete-restore/child.js is disposed')
+ })
+
+ import.meta.hot.prune(() => {
+ console.log('file-delete-restore/child.js is pruned')
+ })
+}
diff --git a/playground/hmr/file-delete-restore/index.js b/playground/hmr/file-delete-restore/index.js
new file mode 100644
index 00000000000000..fa4908a32662ac
--- /dev/null
+++ b/playground/hmr/file-delete-restore/index.js
@@ -0,0 +1,4 @@
+import { render } from './runtime'
+import { childValue, parentValue } from './parent'
+
+render({ parent: parentValue, child: childValue })
diff --git a/playground/hmr/file-delete-restore/parent.js b/playground/hmr/file-delete-restore/parent.js
new file mode 100644
index 00000000000000..050bfa6d49b4c0
--- /dev/null
+++ b/playground/hmr/file-delete-restore/parent.js
@@ -0,0 +1,12 @@
+import { rerender } from './runtime'
+
+export const parentValue = 'parent'
+export { value as childValue } from './child'
+
+if (import.meta.hot) {
+ import.meta.hot.accept((newMod) => {
+ if (!newMod) return
+
+ rerender({ child: newMod.childValue, parent: newMod.parentValue })
+ })
+}
diff --git a/playground/hmr/file-delete-restore/runtime.js b/playground/hmr/file-delete-restore/runtime.js
new file mode 100644
index 00000000000000..c038eed173cdbf
--- /dev/null
+++ b/playground/hmr/file-delete-restore/runtime.js
@@ -0,0 +1,16 @@
+let state = {}
+
+export const render = (newState) => {
+ state = newState
+ apply()
+}
+
+export const rerender = (updates) => {
+ state = { ...state, ...updates }
+ apply()
+}
+
+const apply = () => {
+ document.querySelector('.file-delete-restore').textContent =
+ Object.values(state).join(':')
+}
diff --git a/packages/playground/hmr/global.css b/playground/hmr/global.css
similarity index 100%
rename from packages/playground/hmr/global.css
rename to playground/hmr/global.css
diff --git a/playground/hmr/hmr.ts b/playground/hmr/hmr.ts
new file mode 100644
index 00000000000000..57eb5df0ab30ea
--- /dev/null
+++ b/playground/hmr/hmr.ts
@@ -0,0 +1,166 @@
+import { virtual } from 'virtual:file'
+import { virtual as virtualDep } from 'virtual:file-dep'
+import { foo as depFoo, nestedFoo } from './hmrDep'
+import './importing-updated'
+import './invalidation-circular-deps'
+import './file-delete-restore'
+import './optional-chaining/parent'
+import './intermediate-file-delete'
+import './circular'
+import logo from './logo.svg'
+import logoNoInline from './logo-no-inline.svg'
+import { msg as softInvalidationMsg } from './soft-invalidation'
+
+export const foo = 1
+text('.app', foo)
+text('.dep', depFoo)
+text('.nested', nestedFoo)
+text('.virtual', virtual)
+text('.virtual-dep', virtualDep)
+text('.soft-invalidation', softInvalidationMsg)
+setImgSrc('#logo', logo)
+setImgSrc('#logo-no-inline', logoNoInline)
+
+text('.virtual-dep', 0)
+
+const btn = document.querySelector('.virtual-update') as HTMLButtonElement
+btn.onclick = () => {
+ if (import.meta.hot) {
+ import.meta.hot.send('virtual:increment')
+ }
+}
+
+const btnDep = document.querySelector(
+ '.virtual-update-dep',
+) as HTMLButtonElement
+btnDep.onclick = () => {
+ if (import.meta.hot) {
+ import.meta.hot.send('virtual:increment', '-dep')
+ }
+}
+
+if (import.meta.hot) {
+ import.meta.hot.accept(({ foo }) => {
+ console.log('(self-accepting 1) foo is now:', foo)
+ })
+
+ import.meta.hot.accept(({ foo }) => {
+ console.log('(self-accepting 2) foo is now:', foo)
+ })
+
+ const handleDep = (type, newFoo, newNestedFoo) => {
+ console.log(`(${type}) foo is now: ${newFoo}`)
+ console.log(`(${type}) nested foo is now: ${newNestedFoo}`)
+ text('.dep', newFoo)
+ text('.nested', newNestedFoo)
+ }
+
+ import.meta.hot.accept('./logo.svg', (newUrl) => {
+ setImgSrc('#logo', newUrl.default)
+ console.log('Logo updated', newUrl.default)
+ })
+
+ import.meta.hot.accept('./logo-no-inline.svg', (newUrl) => {
+ setImgSrc('#logo-no-inline', newUrl.default)
+ console.log('Logo-no-inline updated', newUrl.default)
+ })
+
+ import.meta.hot.accept('./hmrDep', ({ foo, nestedFoo }) => {
+ handleDep('single dep', foo, nestedFoo)
+ })
+
+ import.meta.hot.accept('virtual:file-dep', ({ virtual }) => {
+ text('.virtual-dep', virtual)
+ })
+
+ import.meta.hot.accept(['./hmrDep'], ([{ foo, nestedFoo }]) => {
+ handleDep('multi deps', foo, nestedFoo)
+ })
+
+ import.meta.hot.dispose(() => {
+ console.log(`foo was:`, foo)
+ })
+
+ import.meta.hot.on('vite:afterUpdate', (event) => {
+ console.log(`>>> vite:afterUpdate -- ${event.type}`)
+ })
+
+ import.meta.hot.on('vite:beforeUpdate', (event) => {
+ console.log(`>>> vite:beforeUpdate -- ${event.type}`)
+
+ const cssUpdate = event.updates.find(
+ (update) =>
+ update.type === 'css-update' && update.path.includes('global.css'),
+ )
+ if (cssUpdate) {
+ text(
+ '.css-prev',
+ (document.querySelector('.global-css') as HTMLLinkElement).href,
+ )
+
+ // Wait until the tag has been swapped out, which includes the time taken
+ // to download and parse the new stylesheet. Assert the swapped link.
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ mutation.addedNodes.forEach((node) => {
+ if (
+ node.nodeType === Node.ELEMENT_NODE &&
+ (node as Element).tagName === 'LINK'
+ ) {
+ text('.link-tag-added', 'yes')
+ }
+ })
+ mutation.removedNodes.forEach((node) => {
+ if (
+ node.nodeType === Node.ELEMENT_NODE &&
+ (node as Element).tagName === 'LINK'
+ ) {
+ text('.link-tag-removed', 'yes')
+ text(
+ '.css-post',
+ (document.querySelector('.global-css') as HTMLLinkElement).href,
+ )
+ }
+ })
+ })
+ })
+
+ observer.observe(document.querySelector('#style-tags-wrapper'), {
+ childList: true,
+ })
+ }
+ })
+
+ import.meta.hot.on('vite:error', (event) => {
+ console.log(`>>> vite:error -- ${event.err.message}`)
+ })
+
+ import.meta.hot.on('vite:invalidate', ({ path }) => {
+ console.log(`>>> vite:invalidate -- ${path}`)
+ })
+
+ import.meta.hot.on('custom:foo', ({ msg }) => {
+ text('.custom', msg)
+ })
+
+ import.meta.hot.on('custom:remove', removeCb)
+
+ // send custom event to server to calculate 1 + 2
+ import.meta.hot.send('custom:remote-add', { a: 1, b: 2 })
+ import.meta.hot.on('custom:remote-add-result', ({ result }) => {
+ text('.custom-communication', result)
+ })
+}
+
+function text(el, text) {
+ document.querySelector(el).textContent = text
+}
+
+function setImgSrc(el, src) {
+ ;(document.querySelector(el) as HTMLImageElement).src = src
+}
+
+function removeCb({ msg }) {
+ text('.toRemove', msg)
+ import.meta.hot.off('custom:remove', removeCb)
+}
diff --git a/packages/playground/hmr/hmrDep.js b/playground/hmr/hmrDep.js
similarity index 100%
rename from packages/playground/hmr/hmrDep.js
rename to playground/hmr/hmrDep.js
diff --git a/playground/hmr/hmrNestedDep.js b/playground/hmr/hmrNestedDep.js
new file mode 100644
index 00000000000000..766766a6260612
--- /dev/null
+++ b/playground/hmr/hmrNestedDep.js
@@ -0,0 +1 @@
+export const foo = 1
diff --git "a/packages/playground/assets/nested/\343\203\206\343\202\271\343\203\210-\346\270\254\350\251\246-white space.png" b/playground/hmr/icon.png
similarity index 100%
rename from "packages/playground/assets/nested/\343\203\206\343\202\271\343\203\210-\346\270\254\350\251\246-white space.png"
rename to playground/hmr/icon.png
diff --git a/playground/hmr/importedVirtual.js b/playground/hmr/importedVirtual.js
new file mode 100644
index 00000000000000..8b0b417bc3113d
--- /dev/null
+++ b/playground/hmr/importedVirtual.js
@@ -0,0 +1 @@
+export const virtual = '[success]'
diff --git a/playground/hmr/importing-updated/a.js b/playground/hmr/importing-updated/a.js
new file mode 100644
index 00000000000000..4b08229417e4c3
--- /dev/null
+++ b/playground/hmr/importing-updated/a.js
@@ -0,0 +1,8 @@
+const val = 'a0'
+document.querySelector('.importing-reloaded').innerHTML += `a.js: ${val} `
+
+export default val
+
+if (import.meta.hot) {
+ import.meta.hot.accept()
+}
diff --git a/playground/hmr/importing-updated/b.js b/playground/hmr/importing-updated/b.js
new file mode 100644
index 00000000000000..87c4a065061fea
--- /dev/null
+++ b/playground/hmr/importing-updated/b.js
@@ -0,0 +1,8 @@
+import a from './a.js'
+
+const val = `b0,${a}`
+document.querySelector('.importing-reloaded').innerHTML += `b.js: ${val} `
+
+if (import.meta.hot) {
+ import.meta.hot.accept()
+}
diff --git a/playground/hmr/importing-updated/index.js b/playground/hmr/importing-updated/index.js
new file mode 100644
index 00000000000000..0cc74268d385de
--- /dev/null
+++ b/playground/hmr/importing-updated/index.js
@@ -0,0 +1,2 @@
+import './a'
+import './b'
diff --git a/playground/hmr/index.html b/playground/hmr/index.html
new file mode 100644
index 00000000000000..8bd295beb73c95
--- /dev/null
+++ b/playground/hmr/index.html
@@ -0,0 +1,47 @@
+
+
+
+update virtual module
+update virtual module via accept
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+no
+no
+
+
+
+
+1
+
+
+
+
diff --git a/playground/hmr/intermediate-file-delete/display.js b/playground/hmr/intermediate-file-delete/display.js
new file mode 100644
index 00000000000000..3ab1936b0c9009
--- /dev/null
+++ b/playground/hmr/intermediate-file-delete/display.js
@@ -0,0 +1 @@
+export const displayCount = (count) => `count is ${count}`
diff --git a/playground/hmr/intermediate-file-delete/index.js b/playground/hmr/intermediate-file-delete/index.js
new file mode 100644
index 00000000000000..4137a300f2be32
--- /dev/null
+++ b/playground/hmr/intermediate-file-delete/index.js
@@ -0,0 +1,17 @@
+import { displayCount } from './re-export.js'
+
+const button = document.querySelector('.intermediate-file-delete-increment')
+
+const render = () => {
+ document.querySelector('.intermediate-file-delete-display').textContent =
+ displayCount(Number(button.textContent))
+}
+
+render()
+
+button.addEventListener('click', () => {
+ button.textContent = `${Number(button.textContent) + 1}`
+ render()
+})
+
+if (import.meta.hot) import.meta.hot.accept()
diff --git a/playground/hmr/intermediate-file-delete/re-export.js b/playground/hmr/intermediate-file-delete/re-export.js
new file mode 100644
index 00000000000000..b2dade525c0675
--- /dev/null
+++ b/playground/hmr/intermediate-file-delete/re-export.js
@@ -0,0 +1 @@
+export * from './display.js'
diff --git a/playground/hmr/invalidation-circular-deps/circular-invalidate/child.js b/playground/hmr/invalidation-circular-deps/circular-invalidate/child.js
new file mode 100644
index 00000000000000..502aeedf296c8d
--- /dev/null
+++ b/playground/hmr/invalidation-circular-deps/circular-invalidate/child.js
@@ -0,0 +1,9 @@
+import './parent'
+
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {
+ import.meta.hot.invalidate()
+ })
+}
+
+export const value = 'child'
diff --git a/playground/hmr/invalidation-circular-deps/circular-invalidate/parent.js b/playground/hmr/invalidation-circular-deps/circular-invalidate/parent.js
new file mode 100644
index 00000000000000..13ca6287e048aa
--- /dev/null
+++ b/playground/hmr/invalidation-circular-deps/circular-invalidate/parent.js
@@ -0,0 +1,12 @@
+import { value } from './child'
+
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {
+ import.meta.hot.invalidate()
+ })
+}
+
+console.log('(invalidation circular deps) parent is executing')
+setTimeout(() => {
+ document.querySelector('.invalidation-circular-deps').innerHTML = value
+})
diff --git a/playground/hmr/invalidation-circular-deps/index.js b/playground/hmr/invalidation-circular-deps/index.js
new file mode 100644
index 00000000000000..f45400604b138b
--- /dev/null
+++ b/playground/hmr/invalidation-circular-deps/index.js
@@ -0,0 +1,2 @@
+import './circular-invalidate/parent'
+import './invalidate-handled-in-circle/parent'
diff --git a/playground/hmr/invalidation-circular-deps/invalidate-handled-in-circle/child.js b/playground/hmr/invalidation-circular-deps/invalidate-handled-in-circle/child.js
new file mode 100644
index 00000000000000..502aeedf296c8d
--- /dev/null
+++ b/playground/hmr/invalidation-circular-deps/invalidate-handled-in-circle/child.js
@@ -0,0 +1,9 @@
+import './parent'
+
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {
+ import.meta.hot.invalidate()
+ })
+}
+
+export const value = 'child'
diff --git a/playground/hmr/invalidation-circular-deps/invalidate-handled-in-circle/parent.js b/playground/hmr/invalidation-circular-deps/invalidate-handled-in-circle/parent.js
new file mode 100644
index 00000000000000..db9be83c2b61af
--- /dev/null
+++ b/playground/hmr/invalidation-circular-deps/invalidate-handled-in-circle/parent.js
@@ -0,0 +1,11 @@
+import { value } from './child'
+
+if (import.meta.hot) {
+ import.meta.hot.accept(() => {})
+}
+
+console.log('(invalidation circular deps handled) parent is executing')
+setTimeout(() => {
+ document.querySelector('.invalidation-circular-deps-handled').innerHTML =
+ value
+})
diff --git a/playground/hmr/invalidation/child.js b/playground/hmr/invalidation/child.js
new file mode 100644
index 00000000000000..b424e2f83c3233
--- /dev/null
+++ b/playground/hmr/invalidation/child.js
@@ -0,0 +1,9 @@
+if (import.meta.hot) {
+ // Need to accept, to register a callback for HMR
+ import.meta.hot.accept(() => {
+ // Trigger HMR in importers
+ import.meta.hot.invalidate()
+ })
+}
+
+export const value = 'child'
diff --git a/playground/hmr/invalidation/parent.js b/playground/hmr/invalidation/parent.js
new file mode 100644
index 00000000000000..e4190b4e4fd1a5
--- /dev/null
+++ b/playground/hmr/invalidation/parent.js
@@ -0,0 +1,9 @@
+import { value } from './child'
+
+if (import.meta.hot) {
+ import.meta.hot.accept()
+}
+
+console.log('(invalidation) parent is executing')
+
+document.querySelector('.invalidation-parent').innerHTML = value
diff --git a/playground/hmr/invalidation/root.js b/playground/hmr/invalidation/root.js
new file mode 100644
index 00000000000000..c2946bc654a6f5
--- /dev/null
+++ b/playground/hmr/invalidation/root.js
@@ -0,0 +1,16 @@
+import './parent.js'
+
+if (import.meta.hot) {
+ // Need to accept, to register a callback for HMR
+ import.meta.hot.accept(() => {
+ // Triggers full page reload because no importers
+ import.meta.hot.invalidate()
+ })
+}
+
+const root = document.querySelector('.invalidation-root')
+
+// Non HMR-able behaviour
+if (!root.innerHTML) {
+ root.innerHTML = 'Init'
+}
diff --git a/playground/hmr/logo-no-inline.svg b/playground/hmr/logo-no-inline.svg
new file mode 100644
index 00000000000000..a85344da4790b2
--- /dev/null
+++ b/playground/hmr/logo-no-inline.svg
@@ -0,0 +1,3 @@
+
+ Vite
+
diff --git a/playground/hmr/logo.svg b/playground/hmr/logo.svg
new file mode 100644
index 00000000000000..a85344da4790b2
--- /dev/null
+++ b/playground/hmr/logo.svg
@@ -0,0 +1,3 @@
+
+ Vite
+
diff --git a/playground/hmr/missing-file/index.html b/playground/hmr/missing-file/index.html
new file mode 100644
index 00000000000000..cfbd07a1e44286
--- /dev/null
+++ b/playground/hmr/missing-file/index.html
@@ -0,0 +1,2 @@
+Page
+
diff --git a/playground/hmr/missing-file/main.js b/playground/hmr/missing-file/main.js
new file mode 100644
index 00000000000000..999801e4dd1061
--- /dev/null
+++ b/playground/hmr/missing-file/main.js
@@ -0,0 +1 @@
+import './a.js'
diff --git a/playground/hmr/missing-import/a.js b/playground/hmr/missing-import/a.js
new file mode 100644
index 00000000000000..886fc4ff2ef541
--- /dev/null
+++ b/playground/hmr/missing-import/a.js
@@ -0,0 +1,3 @@
+import 'missing-modules'
+
+console.log('missing test')
diff --git a/playground/hmr/missing-import/index.html b/playground/hmr/missing-import/index.html
new file mode 100644
index 00000000000000..cfbd07a1e44286
--- /dev/null
+++ b/playground/hmr/missing-import/index.html
@@ -0,0 +1,2 @@
+Page
+
diff --git a/playground/hmr/missing-import/main.js b/playground/hmr/missing-import/main.js
new file mode 100644
index 00000000000000..999801e4dd1061
--- /dev/null
+++ b/playground/hmr/missing-import/main.js
@@ -0,0 +1 @@
+import './a.js'
diff --git a/playground/hmr/modules.d.ts b/playground/hmr/modules.d.ts
new file mode 100644
index 00000000000000..e880082076b638
--- /dev/null
+++ b/playground/hmr/modules.d.ts
@@ -0,0 +1,7 @@
+declare module 'virtual:file' {
+ export const virtual: string
+}
+
+declare module 'virtual:file-dep' {
+ export const virtual: string
+}
diff --git a/playground/hmr/optional-chaining/child.js b/playground/hmr/optional-chaining/child.js
new file mode 100644
index 00000000000000..766766a6260612
--- /dev/null
+++ b/playground/hmr/optional-chaining/child.js
@@ -0,0 +1 @@
+export const foo = 1
diff --git a/playground/hmr/optional-chaining/parent.js b/playground/hmr/optional-chaining/parent.js
new file mode 100644
index 00000000000000..d484884cc04c2d
--- /dev/null
+++ b/playground/hmr/optional-chaining/parent.js
@@ -0,0 +1,8 @@
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
+import { foo } from './child'
+
+import.meta.hot?.accept('./child', ({ foo }) => {
+ console.log('(optional-chaining) child update')
+ document.querySelector('.optional-chaining').textContent = foo
+})
diff --git a/playground/hmr/package.json b/playground/hmr/package.json
new file mode 100644
index 00000000000000..35e0799c262738
--- /dev/null
+++ b/playground/hmr/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-hmr",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/hmr/self-accept-within-circular/a.js b/playground/hmr/self-accept-within-circular/a.js
new file mode 100644
index 00000000000000..a559b739d9f253
--- /dev/null
+++ b/playground/hmr/self-accept-within-circular/a.js
@@ -0,0 +1,5 @@
+import { b } from './b'
+
+export const a = {
+ b,
+}
diff --git a/playground/hmr/self-accept-within-circular/b.js b/playground/hmr/self-accept-within-circular/b.js
new file mode 100644
index 00000000000000..4f5a135418728c
--- /dev/null
+++ b/playground/hmr/self-accept-within-circular/b.js
@@ -0,0 +1,7 @@
+import { c } from './c'
+
+const b = {
+ c,
+}
+
+export { b }
diff --git a/playground/hmr/self-accept-within-circular/c.js b/playground/hmr/self-accept-within-circular/c.js
new file mode 100644
index 00000000000000..2d0599a3836a43
--- /dev/null
+++ b/playground/hmr/self-accept-within-circular/c.js
@@ -0,0 +1,12 @@
+import './b'
+
+export const c = 'c'
+
+function render(content) {
+ document.querySelector('.self-accept-within-circular').textContent = content
+}
+render(c)
+
+import.meta.hot?.accept((nextExports) => {
+ render(nextExports.c)
+})
diff --git a/playground/hmr/self-accept-within-circular/index.html b/playground/hmr/self-accept-within-circular/index.html
new file mode 100644
index 00000000000000..a15fe3cec6541c
--- /dev/null
+++ b/playground/hmr/self-accept-within-circular/index.html
@@ -0,0 +1,2 @@
+
+
diff --git a/playground/hmr/self-accept-within-circular/index.js b/playground/hmr/self-accept-within-circular/index.js
new file mode 100644
index 00000000000000..604c0a3518932b
--- /dev/null
+++ b/playground/hmr/self-accept-within-circular/index.js
@@ -0,0 +1,3 @@
+import { a } from './a'
+
+console.log(a)
diff --git a/playground/hmr/soft-invalidation/child.js b/playground/hmr/soft-invalidation/child.js
new file mode 100644
index 00000000000000..21ec276fc7f825
--- /dev/null
+++ b/playground/hmr/soft-invalidation/child.js
@@ -0,0 +1 @@
+export const foo = 'bar'
diff --git a/playground/hmr/soft-invalidation/index.js b/playground/hmr/soft-invalidation/index.js
new file mode 100644
index 00000000000000..f236a2579b0c24
--- /dev/null
+++ b/playground/hmr/soft-invalidation/index.js
@@ -0,0 +1,4 @@
+import { foo } from './child'
+
+// @ts-expect-error global
+export const msg = `soft-invalidation/index.js is transformed ${__TRANSFORM_COUNT__} times. child is ${foo}`
diff --git "a/packages/playground/hmr/unicode-path/\344\270\255\346\226\207-\343\201\253\343\201\273\343\202\223\343\201\224-\355\225\234\352\270\200-\360\237\214\225\360\237\214\226\360\237\214\227/index.html" "b/playground/hmr/unicode-path/\344\270\255\346\226\207-\343\201\253\343\201\273\343\202\223\343\201\224-\355\225\234\352\270\200-\360\237\214\225\360\237\214\226\360\237\214\227/index.html"
similarity index 100%
rename from "packages/playground/hmr/unicode-path/\344\270\255\346\226\207-\343\201\253\343\201\273\343\202\223\343\201\224-\355\225\234\352\270\200-\360\237\214\225\360\237\214\226\360\237\214\227/index.html"
rename to "playground/hmr/unicode-path/\344\270\255\346\226\207-\343\201\253\343\201\273\343\202\223\343\201\224-\355\225\234\352\270\200-\360\237\214\225\360\237\214\226\360\237\214\227/index.html"
diff --git a/playground/hmr/vite.config.ts b/playground/hmr/vite.config.ts
new file mode 100644
index 00000000000000..9ee8024ee2bf44
--- /dev/null
+++ b/playground/hmr/vite.config.ts
@@ -0,0 +1,100 @@
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import { defineConfig } from 'vite'
+import type { Plugin } from 'vite'
+
+export default defineConfig({
+ experimental: {
+ hmrPartialAccept: true,
+ },
+ build: {
+ assetsInlineLimit(filePath) {
+ if (filePath.endsWith('logo-no-inline.svg')) {
+ return false
+ }
+ },
+ },
+ plugins: [
+ {
+ name: 'mock-custom',
+ async hotUpdate({ file, read }) {
+ if (file.endsWith('customFile.js')) {
+ const content = await read()
+ const msg = content.match(/export const msg = '(\w+)'/)[1]
+ this.environment.hot.send('custom:foo', { msg })
+ this.environment.hot.send('custom:remove', { msg })
+ }
+ },
+ configureServer(server) {
+ server.environments.client.hot.on(
+ 'custom:remote-add',
+ ({ a, b }, client) => {
+ client.send('custom:remote-add-result', { result: a + b })
+ },
+ )
+ },
+ },
+ virtualPlugin(),
+ transformCountPlugin(),
+ watchCssDepsPlugin(),
+ ],
+})
+
+function virtualPlugin(): Plugin {
+ let num = 0
+ return {
+ name: 'virtual-file',
+ resolveId(id) {
+ if (id.startsWith('virtual:file')) {
+ return '\0' + id
+ }
+ },
+ load(id) {
+ if (id.startsWith('\0virtual:file')) {
+ return `\
+import { virtual as _virtual } from "/importedVirtual.js";
+export const virtual = _virtual + '${num}';`
+ }
+ },
+ configureServer(server) {
+ server.environments.client.hot.on('virtual:increment', async (suffix) => {
+ const mod = await server.environments.client.moduleGraph.getModuleById(
+ '\0virtual:file' + (suffix || ''),
+ )
+ if (mod) {
+ num++
+ server.environments.client.reloadModule(mod)
+ }
+ })
+ },
+ }
+}
+
+function transformCountPlugin(): Plugin {
+ let num = 0
+ return {
+ name: 'transform-count',
+ transform(code) {
+ if (code.includes('__TRANSFORM_COUNT__')) {
+ return code.replace('__TRANSFORM_COUNT__', String(++num))
+ }
+ },
+ }
+}
+
+function watchCssDepsPlugin(): Plugin {
+ return {
+ name: 'watch-css-deps',
+ async transform(code, id) {
+ // replace the `replaced` identifier in the CSS file with the adjacent
+ // `dep.js` file's `color` variable.
+ if (id.includes('css-deps/main.css')) {
+ const depPath = path.resolve(__dirname, './css-deps/dep.js')
+ const dep = await fs.readFile(depPath, 'utf-8')
+ const color = dep.match(/color = '(.+?)'/)[1]
+ this.addWatchFile(depPath)
+ return code.replace('replaced', color)
+ }
+ },
+ }
+}
diff --git a/playground/html/.env b/playground/html/.env
new file mode 100644
index 00000000000000..a94d8ee1e130c5
--- /dev/null
+++ b/playground/html/.env
@@ -0,0 +1,2 @@
+VITE_FOO=bar
+VITE_FAVICON_URL=/sprite.svg
diff --git a/playground/html/__tests__/html.spec.ts b/playground/html/__tests__/html.spec.ts
new file mode 100644
index 00000000000000..3d198d674db737
--- /dev/null
+++ b/playground/html/__tests__/html.spec.ts
@@ -0,0 +1,521 @@
+import { beforeAll, describe, expect, test } from 'vitest'
+import {
+ browserLogs,
+ editFile,
+ expectWithRetry,
+ getColor,
+ isBuild,
+ isServe,
+ page,
+ serverLogs,
+ untilBrowserLogAfter,
+ viteServer,
+ viteTestUrl,
+ withRetry,
+} from '~utils'
+
+function fetchHtml(p: string) {
+ return fetch(viteTestUrl + p, {
+ headers: { Accept: 'text/html,*/*' },
+ })
+}
+
+function testPage(isNested: boolean) {
+ test('pre transform', async () => {
+ expect(await page.$('head meta[name=viewport]')).toBeTruthy()
+ })
+
+ test('string transform', async () => {
+ expect(await page.textContent('h1')).toBe(
+ isNested ? 'Nested' : 'Transformed',
+ )
+ })
+
+ test('tags transform', async () => {
+ const el = await page.$('head meta[name=description]')
+ expect(await el.getAttribute('content')).toBe('a vite app')
+
+ const kw = await page.$('head meta[name=keywords]')
+ expect(await kw.getAttribute('content')).toBe('es modules')
+ })
+
+ test('combined transform', async () => {
+ expect(await page.title()).toBe('Test HTML transforms')
+ // the p should be injected to body
+ expect(await page.textContent('body p.inject')).toBe('This is injected')
+ })
+
+ test('server only transform', async () => {
+ if (!isBuild) {
+ expect(await page.textContent('body p.server')).toMatch(
+ 'injected only during dev',
+ )
+ } else {
+ expect(await page.innerHTML('body')).not.toMatch('p class="server"')
+ }
+ })
+
+ test('build only transform', async () => {
+ if (isBuild) {
+ expect(await page.textContent('body p.build')).toMatch(
+ 'injected only during build',
+ )
+ } else {
+ expect(await page.innerHTML('body')).not.toMatch('p class="build"')
+ }
+ })
+
+ test('conditional transform', async () => {
+ if (isNested) {
+ expect(await page.textContent('body p.conditional')).toMatch(
+ 'injected only for /nested/',
+ )
+ } else {
+ expect(await page.innerHTML('body')).not.toMatch('p class="conditional"')
+ }
+ })
+
+ test('body prepend/append transform', async () => {
+ expect(await page.innerHTML('body')).toMatch(
+ /prepended to body(.*)appended to body/s,
+ )
+ })
+
+ test('css', async () => {
+ expect(await getColor('h1')).toBe(isNested ? 'red' : 'blue')
+ expect(await getColor('p')).toBe('grey')
+ })
+
+ if (isNested) {
+ test('relative path in html asset', async () => {
+ expect(await page.textContent('.relative-js')).toMatch('hello')
+ expect(await getColor('.relative-css')).toMatch('red')
+ })
+ }
+}
+
+describe('main', () => {
+ testPage(false)
+
+ test('preserve comments', async () => {
+ const html = await page.innerHTML('body')
+ expect(html).toMatch(``)
+ expect(html).toMatch(``)
+ })
+
+ test('external paths works with vite-ignore attribute', async () => {
+ expect(await page.textContent('.external-path')).toBe('works')
+ expect(await page.getAttribute('.external-path', 'vite-ignore')).toBe(null)
+ expect(await getColor('.external-path')).toBe('red')
+ if (isServe) {
+ expect(serverLogs).not.toEqual(
+ expect.arrayContaining([
+ expect.stringMatching('Failed to load url /external-path.js'),
+ ]),
+ )
+ } else {
+ expect(serverLogs).not.toEqual(
+ expect.arrayContaining([
+ expect.stringMatching(
+ /"\/external-path\.js".*can't be bundled without type="module" attribute/,
+ ),
+ ]),
+ )
+ }
+ })
+
+ test.runIf(isBuild)(
+ 'external paths by rollupOptions.external works',
+ async () => {
+ expect(await page.textContent('.external-path-by-rollup-options')).toBe(
+ 'works',
+ )
+ expect(serverLogs).not.toEqual(
+ expect.arrayContaining([expect.stringContaining('Could not load')]),
+ )
+ },
+ )
+})
+
+describe('nested', () => {
+ beforeAll(async () => {
+ await page.goto(viteTestUrl + '/nested/')
+ })
+
+ testPage(true)
+})
+
+describe('nested w/ query', () => {
+ beforeAll(async () => {
+ await page.goto(viteTestUrl + '/nested/index.html?v=1')
+ })
+
+ testPage(true)
+})
+
+describe.runIf(isBuild)('build', () => {
+ describe('scriptAsync', () => {
+ beforeAll(async () => {
+ await page.goto(viteTestUrl + '/scriptAsync.html')
+ })
+
+ test('script is async', async () => {
+ expect(await page.$('head script[type=module][async]')).toBeTruthy()
+ expect(await page.$('head script[type=module]:not([async])')).toBeNull()
+ })
+ })
+
+ describe('scriptMixed', () => {
+ beforeAll(async () => {
+ await page.goto(viteTestUrl + '/scriptMixed.html')
+ })
+
+ test('script is mixed', async () => {
+ expect(await page.$('head script[type=module][async]')).toBeNull()
+ expect(await page.$('head script[type=module]:not([async])')).toBeTruthy()
+ })
+ })
+
+ describe('zeroJS', () => {
+ // Ensure that the modulePreload polyfill is discarded in this case
+
+ beforeAll(async () => {
+ await page.goto(viteTestUrl + '/zeroJS.html')
+ })
+
+ test('zeroJS', async () => {
+ expect(await page.$('head script[type=module]')).toBeNull()
+ })
+ })
+
+ describe('inline entry', () => {
+ const _countTags = (selector) => page.$$eval(selector, (t) => t.length)
+ const countScriptTags = _countTags.bind(this, 'script[type=module]')
+ const countPreloadTags = _countTags.bind(this, 'link[rel=modulepreload]')
+
+ test('is inlined', async () => {
+ await page.goto(viteTestUrl + '/inline/shared-2.html?v=1')
+ expect(await countScriptTags()).toBeGreaterThan(1)
+ expect(await countPreloadTags()).toBe(0)
+ })
+
+ test('is not inlined', async () => {
+ await page.goto(viteTestUrl + '/inline/unique.html?v=1')
+ expect(await countScriptTags()).toBe(1)
+ expect(await countPreloadTags()).toBeGreaterThan(0)
+ })
+
+ test('execution order when inlined', async () => {
+ await page.goto(viteTestUrl + '/inline/shared-1.html?v=1')
+ expect((await page.textContent('#output')).trim()).toBe(
+ 'dep1 common dep2 dep3 shared',
+ )
+ await page.goto(viteTestUrl + '/inline/shared-2.html?v=1')
+ expect((await page.textContent('#output')).trim()).toBe(
+ 'dep1 common dep2 dep3 shared',
+ )
+ })
+
+ test('execution order when not inlined', async () => {
+ await page.goto(viteTestUrl + '/inline/unique.html?v=1')
+ expect((await page.textContent('#output')).trim()).toBe(
+ 'dep1 common dep2 unique',
+ )
+ })
+ })
+})
+
+describe('noHead', () => {
+ beforeAll(async () => {
+ await page.goto(viteTestUrl + '/noHead.html')
+ })
+
+ test('noHead tags injection', async () => {
+ const el = await page.$('html meta[name=description]')
+ expect(await el.getAttribute('content')).toBe('a vite app')
+
+ const kw = await page.$('html meta[name=keywords]')
+ expect(await kw.getAttribute('content')).toBe('es modules')
+ })
+})
+
+describe('noBody', () => {
+ beforeAll(async () => {
+ await page.goto(viteTestUrl + '/noBody.html')
+ })
+
+ test('noBody tags injection', async () => {
+ // this selects the first noscript in body, even without a body tag
+ const el = await page.$('body noscript')
+ expect(await el.innerHTML()).toMatch(``)
+
+ const kw = await page.$('html:last-child')
+ expect(await kw.innerHTML()).toMatch(``)
+ })
+})
+
+describe('Unicode path', () => {
+ test('direct access', async () => {
+ await page.goto(
+ viteTestUrl + '/unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html',
+ )
+ expect(await page.textContent('h1')).toBe('Unicode path')
+ })
+
+ test('spa fallback', async () => {
+ await page.goto(viteTestUrl + '/unicode-path/中文-にほんご-한글-🌕🌖🌗/')
+ expect(await page.textContent('h1')).toBe('Unicode path')
+ })
+})
+
+describe('link with props', () => {
+ test('separate links with different media props', async () => {
+ await page.goto(viteTestUrl + '/link-props/index.html')
+ expect(await getColor('h1')).toBe('red')
+ })
+})
+
+describe.runIf(isServe)('invalid', () => {
+ test('should be 500 with overlay', async () => {
+ const response = await page.goto(viteTestUrl + '/invalid.html')
+ expect(response.status()).toBe(500)
+
+ const errorOverlay = await page.waitForSelector('vite-error-overlay')
+ expect(errorOverlay).toBeTruthy()
+
+ const message = await errorOverlay.$$eval('.message-body', (m) => {
+ return m[0].innerHTML
+ })
+ expect(message).toMatch(/^Unable to parse HTML/)
+ })
+
+ test('should close overlay when clicked away', async () => {
+ await page.goto(viteTestUrl + '/invalid.html')
+ const errorOverlay = await page.waitForSelector('vite-error-overlay')
+ expect(errorOverlay).toBeTruthy()
+
+ await page.click('html')
+ const isVisibleOverlay = await errorOverlay.isVisible()
+ expect(isVisibleOverlay).toBeFalsy()
+ })
+
+ test('should close overlay when escape key is pressed', async () => {
+ await page.goto(viteTestUrl + '/invalid.html')
+ const errorOverlay = await page.waitForSelector('vite-error-overlay')
+ expect(errorOverlay).toBeTruthy()
+
+ await page.keyboard.press('Escape')
+ const isVisibleOverlay = await errorOverlay.isVisible()
+ expect(isVisibleOverlay).toBeFalsy()
+ })
+
+ test('stack is updated', async () => {
+ await page.goto(viteTestUrl + '/invalid.html')
+
+ const errorOverlay = await page.waitForSelector('vite-error-overlay')
+ const hiddenPromise = errorOverlay.waitForElementState('hidden')
+ await page.keyboard.press('Escape')
+ await hiddenPromise
+
+ viteServer.environments.client.hot.send({
+ type: 'error',
+ err: {
+ message: 'someError',
+ stack: [
+ 'Error: someError',
+ ' at someMethod (/some/file.ts:1:2)',
+ ].join('\n'),
+ },
+ })
+ const newErrorOverlay = await page.waitForSelector('vite-error-overlay')
+ const stack = await newErrorOverlay.$$eval('.stack', (m) => m[0].innerHTML)
+ expect(stack).toMatch(/^Error: someError/)
+ })
+
+ test('should reload when fixed', async () => {
+ await untilBrowserLogAfter(
+ () => page.goto(viteTestUrl + '/invalid.html'),
+ /connected/, // wait for HMR connection
+ )
+ editFile('invalid.html', (content) => {
+ return content.replace(' Good')
+ })
+ const content = await page.waitForSelector('text=Good HTML')
+ expect(content).toBeTruthy()
+ })
+})
+
+describe('Valid HTML', () => {
+ test('valid HTML is parsed', async () => {
+ await page.goto(viteTestUrl + '/valid.html')
+ expect(await page.textContent('#no-quotes-on-attr')).toBe(
+ 'No quotes on Attr working',
+ )
+
+ expect(await getColor('#duplicated-attrs')).toBe('green')
+ })
+})
+
+describe('env', () => {
+ beforeAll(async () => {
+ await page.goto(viteTestUrl + '/env.html')
+ })
+
+ test('env works', async () => {
+ expect(await page.textContent('.env')).toBe('bar')
+ expect(await page.textContent('.env-define')).toBe('5173')
+ expect(await page.textContent('.env-define-string')).toBe('string')
+ expect(await page.textContent('.env-define-object-string')).toBe(
+ '{ "foo": "bar" }',
+ )
+ expect(await page.textContent('.env-define-null-string')).toBe('null')
+ expect(await page.textContent('.env-bar')).toBeTruthy()
+ expect(await page.textContent('.env-prod')).toBe(isBuild + '')
+ expect(await page.textContent('.env-dev')).toBe(isServe + '')
+
+ const iconLink = await page.$('link[rel=icon]')
+ expect(await iconLink.getAttribute('href')).toBe(
+ `${isBuild ? './' : '/'}sprite.svg`,
+ )
+ })
+})
+
+describe('importmap', () => {
+ beforeAll(async () => {
+ await page.goto(viteTestUrl + '/importmapOrder.html')
+ })
+
+ // Should put this test at the end to get all browser logs above
+ test('importmap should be prepended', async () => {
+ expect(browserLogs).not.toContain(
+ 'An import map is added after module script load was triggered.',
+ )
+ })
+})
+
+describe('side-effects', () => {
+ beforeAll(async () => {
+ await page.goto(viteTestUrl + '/side-effects/')
+ })
+
+ test('console.log is not tree-shaken', async () => {
+ expect(browserLogs).toContain('message from sideEffects script')
+ })
+})
+
+describe('special character', () => {
+ beforeAll(async () => {
+ await page.goto(viteTestUrl + '/a á.html')
+ })
+
+ test('should fetch html proxy', async () => {
+ expect(browserLogs).toContain('special character')
+ })
+})
+
+describe('relative input', () => {
+ beforeAll(async () => {
+ await page.goto(viteTestUrl + '/relative-input.html')
+ })
+
+ test('passing relative path to rollupOptions.input works', async () => {
+ await expectWithRetry(() => page.textContent('.relative-input')).toBe('OK')
+ })
+})
+
+describe.runIf(isServe)('warmup', () => {
+ test('should warmup /warmup/warm.js', async () => {
+ // warmup transform files async during server startup, so the module check
+ // here might take a while to load
+ await withRetry(async () => {
+ const mod =
+ await viteServer.environments.client.moduleGraph.getModuleByUrl(
+ '/warmup/warm.js',
+ )
+ expect(mod).toBeTruthy()
+ })
+ })
+})
+
+test('html serve behavior', async () => {
+ const [
+ file,
+ fileSlash,
+ fileDotHtml,
+
+ folder,
+ folderSlash,
+ folderSlashIndexHtml,
+
+ both,
+ bothSlash,
+ bothDotHtml,
+ bothSlashIndexHtml,
+ ] = await Promise.all([
+ fetchHtml('/serve/file'), // -> serve/file.html
+ fetchHtml('/serve/file/'), // -> index.html (404 in mpa)
+ fetchHtml('/serve/file.html'), // -> serve/file.html
+
+ fetchHtml('/serve/folder'), // -> index.html (404 in mpa)
+ fetchHtml('/serve/folder/'), // -> serve/folder/index.html
+ fetchHtml('/serve/folder/index.html'), // -> serve/folder/index.html
+
+ fetchHtml('/serve/both'), // -> serve/both.html
+ fetchHtml('/serve/both/'), // -> serve/both/index.html
+ fetchHtml('/serve/both.html'), // -> serve/both.html
+ fetchHtml('/serve/both/index.html'), // -> serve/both/index.html
+ ])
+
+ expect(file.status).toBe(200)
+ expect(await file.text()).toContain('file.html')
+ expect(fileSlash.status).toBe(200)
+ expect(await fileSlash.text()).toContain('index.html (fallback)')
+ expect(fileDotHtml.status).toBe(200)
+ expect(await fileDotHtml.text()).toContain('file.html')
+
+ expect(folder.status).toBe(200)
+ expect(await folder.text()).toContain('index.html (fallback)')
+ expect(folderSlash.status).toBe(200)
+ expect(await folderSlash.text()).toContain('folder/index.html')
+ expect(folderSlashIndexHtml.status).toBe(200)
+ expect(await folderSlashIndexHtml.text()).toContain('folder/index.html')
+
+ expect(both.status).toBe(200)
+ expect(await both.text()).toContain('both.html')
+ expect(bothSlash.status).toBe(200)
+ expect(await bothSlash.text()).toContain('both/index.html')
+ expect(bothDotHtml.status).toBe(200)
+ expect(await bothDotHtml.text()).toContain('both.html')
+ expect(bothSlashIndexHtml.status).toBe(200)
+ expect(await bothSlashIndexHtml.text()).toContain('both/index.html')
+})
+
+test('html fallback works non browser accept header', async () => {
+ expect((await fetch(viteTestUrl, { headers: { Accept: '' } })).status).toBe(
+ 200,
+ )
+ // defaults to "Accept: */*"
+ expect((await fetch(viteTestUrl)).status).toBe(200)
+ // wait-on uses axios and axios sends this accept header
+ expect(
+ (
+ await fetch(viteTestUrl, {
+ headers: { Accept: 'application/json, text/plain, */*' },
+ })
+ ).status,
+ ).toBe(200)
+})
+
+test('escape html attribute', async () => {
+ const el = await page.$('.unescape-div')
+ expect(el).toBeNull()
+})
+
+test('invalidate inline proxy module on reload', async () => {
+ await page.goto(`${viteTestUrl}/transform-inline-js`)
+ expect(await page.textContent('.test')).toContain('ok')
+ await page.reload()
+ expect(await page.textContent('.test')).toContain('ok')
+ await page.reload()
+ expect(await page.textContent('.test')).toContain('ok')
+})
diff --git "a/playground/html/a \303\241.html" "b/playground/html/a \303\241.html"
new file mode 100644
index 00000000000000..5a3c9ae4e1a28a
--- /dev/null
+++ "b/playground/html/a \303\241.html"
@@ -0,0 +1,5 @@
+
Special Character
+
+
diff --git a/packages/playground/html/common.css b/playground/html/common.css
similarity index 100%
rename from packages/playground/html/common.css
rename to playground/html/common.css
diff --git a/packages/playground/html/emptyAttr.html b/playground/html/emptyAttr.html
similarity index 94%
rename from packages/playground/html/emptyAttr.html
rename to playground/html/emptyAttr.html
index 30c647017690dd..9aac52e04f88d4 100644
--- a/packages/playground/html/emptyAttr.html
+++ b/playground/html/emptyAttr.html
@@ -1,4 +1,4 @@
-
+
diff --git a/playground/html/env.html b/playground/html/env.html
new file mode 100644
index 00000000000000..63286ea70aa9cc
--- /dev/null
+++ b/playground/html/env.html
@@ -0,0 +1,9 @@
+
%VITE_FOO%
+
%VITE_NUMBER%
+
%VITE_STRING%
+
%VITE_OBJECT_STRING%
+
%VITE_NULL_STRING%
+
class name should be env-bar
+
%PROD%
+
%DEV%
+
diff --git a/packages/playground/html/foo.html b/playground/html/foo.html
similarity index 95%
rename from packages/playground/html/foo.html
rename to playground/html/foo.html
index 0c580cdd508517..721c40a4efd9e1 100644
--- a/packages/playground/html/foo.html
+++ b/playground/html/foo.html
@@ -1,4 +1,4 @@
-
+
diff --git a/playground/html/importmapOrder.html b/playground/html/importmapOrder.html
new file mode 100644
index 00000000000000..3256bd4e39dd8c
--- /dev/null
+++ b/playground/html/importmapOrder.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/playground/html/index.html b/playground/html/index.html
new file mode 100644
index 00000000000000..37554e86e7cd76
--- /dev/null
+++ b/playground/html/index.html
@@ -0,0 +1,20 @@
+
+
+
+
Hello
+
+
+
+
+
+
+
index.html (fallback)
+
+
External path:
+
+
+
+
+ External path by rollupOptions.external (build only):
+
+
diff --git a/packages/playground/html/inline/common.js b/playground/html/inline/common.js
similarity index 100%
rename from packages/playground/html/inline/common.js
rename to playground/html/inline/common.js
diff --git a/packages/playground/html/inline/dep1.js b/playground/html/inline/dep1.js
similarity index 100%
rename from packages/playground/html/inline/dep1.js
rename to playground/html/inline/dep1.js
diff --git a/packages/playground/html/inline/dep2.js b/playground/html/inline/dep2.js
similarity index 100%
rename from packages/playground/html/inline/dep2.js
rename to playground/html/inline/dep2.js
diff --git a/packages/playground/html/inline/dep3.js b/playground/html/inline/dep3.js
similarity index 100%
rename from packages/playground/html/inline/dep3.js
rename to playground/html/inline/dep3.js
diff --git a/packages/playground/html/inline/module-graph.dot b/playground/html/inline/module-graph.dot
similarity index 100%
rename from packages/playground/html/inline/module-graph.dot
rename to playground/html/inline/module-graph.dot
diff --git a/packages/playground/html/inline/shared-1.html b/playground/html/inline/shared-1.html
similarity index 100%
rename from packages/playground/html/inline/shared-1.html
rename to playground/html/inline/shared-1.html
diff --git a/packages/playground/html/inline/shared-2.html b/playground/html/inline/shared-2.html
similarity index 100%
rename from packages/playground/html/inline/shared-2.html
rename to playground/html/inline/shared-2.html
diff --git a/packages/playground/html/inline/shared.js b/playground/html/inline/shared.js
similarity index 100%
rename from packages/playground/html/inline/shared.js
rename to playground/html/inline/shared.js
diff --git a/playground/html/inline/shared_a.html b/playground/html/inline/shared_a.html
new file mode 100644
index 00000000000000..31fbd8fcc34bdf
--- /dev/null
+++ b/playground/html/inline/shared_a.html
@@ -0,0 +1 @@
+
inline a
diff --git a/packages/playground/html/inline/unique.html b/playground/html/inline/unique.html
similarity index 100%
rename from packages/playground/html/inline/unique.html
rename to playground/html/inline/unique.html
diff --git a/packages/playground/html/inline/unique.js b/playground/html/inline/unique.js
similarity index 100%
rename from packages/playground/html/inline/unique.js
rename to playground/html/inline/unique.js
diff --git a/playground/html/invalid.html b/playground/html/invalid.html
new file mode 100644
index 00000000000000..8acea73f16bdad
--- /dev/null
+++ b/playground/html/invalid.html
@@ -0,0 +1 @@
+
diff --git a/playground/html/link-props/index.html b/playground/html/link-props/index.html
new file mode 100644
index 00000000000000..c48e0af202711c
--- /dev/null
+++ b/playground/html/link-props/index.html
@@ -0,0 +1,4 @@
+
+
+
+
test color
diff --git a/playground/html/link-props/print.css b/playground/html/link-props/print.css
new file mode 100644
index 00000000000000..1fd8299149a001
--- /dev/null
+++ b/playground/html/link-props/print.css
@@ -0,0 +1,3 @@
+#link-props {
+ color: green;
+}
diff --git a/playground/html/link-props/screen.css b/playground/html/link-props/screen.css
new file mode 100644
index 00000000000000..d3ef4a56058bf3
--- /dev/null
+++ b/playground/html/link-props/screen.css
@@ -0,0 +1,3 @@
+#link-props {
+ color: red;
+}
diff --git a/packages/playground/html/link.html b/playground/html/link.html
similarity index 95%
rename from packages/playground/html/link.html
rename to playground/html/link.html
index 1ec95eedeab182..5fd9b380fa68b0 100644
--- a/packages/playground/html/link.html
+++ b/playground/html/link.html
@@ -1,4 +1,4 @@
-
+
diff --git a/packages/playground/html/main.css b/playground/html/main.css
similarity index 100%
rename from packages/playground/html/main.css
rename to playground/html/main.css
diff --git a/packages/playground/html/main.js b/playground/html/main.js
similarity index 100%
rename from packages/playground/html/main.js
rename to playground/html/main.js
diff --git a/playground/html/nested/asset/main.js b/playground/html/nested/asset/main.js
new file mode 100644
index 00000000000000..e889874ff000d2
--- /dev/null
+++ b/playground/html/nested/asset/main.js
@@ -0,0 +1,4 @@
+function text(el, text) {
+ document.querySelector(el).textContent = text
+}
+text('.relative-js', 'hello')
diff --git a/playground/html/nested/asset/style.css b/playground/html/nested/asset/style.css
new file mode 100644
index 00000000000000..bafc1d8f12badb
--- /dev/null
+++ b/playground/html/nested/asset/style.css
@@ -0,0 +1,3 @@
+.relative-css {
+ color: red;
+}
diff --git a/playground/html/nested/index.html b/playground/html/nested/index.html
new file mode 100644
index 00000000000000..2103a4b3bed77f
--- /dev/null
+++ b/playground/html/nested/index.html
@@ -0,0 +1,9 @@
+
+
Nested
+
+
+
no base path nested
+
+
link style
+
+
diff --git a/playground/html/nested/nested.css b/playground/html/nested/nested.css
new file mode 100644
index 00000000000000..adc68fa6a4dfa0
--- /dev/null
+++ b/playground/html/nested/nested.css
@@ -0,0 +1,3 @@
+h1 {
+ color: red;
+}
diff --git a/packages/playground/html/nested/nested.js b/playground/html/nested/nested.js
similarity index 100%
rename from packages/playground/html/nested/nested.js
rename to playground/html/nested/nested.js
diff --git a/packages/playground/html/noBody.html b/playground/html/noBody.html
similarity index 100%
rename from packages/playground/html/noBody.html
rename to playground/html/noBody.html
diff --git a/packages/playground/html/noHead.html b/playground/html/noHead.html
similarity index 100%
rename from packages/playground/html/noHead.html
rename to playground/html/noHead.html
diff --git a/playground/html/package.json b/playground/html/package.json
new file mode 100644
index 00000000000000..6bdb23d06194a4
--- /dev/null
+++ b/playground/html/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-html",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/html/public/sprite.svg b/playground/html/public/sprite.svg
new file mode 100644
index 00000000000000..b948cff92b6e39
--- /dev/null
+++ b/playground/html/public/sprite.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/playground/html/relative-input.html b/playground/html/relative-input.html
new file mode 100644
index 00000000000000..b18dcea836a1d4
--- /dev/null
+++ b/playground/html/relative-input.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/playground/html/relative-input/main.js b/playground/html/relative-input/main.js
new file mode 100644
index 00000000000000..b60f93e96854ee
--- /dev/null
+++ b/playground/html/relative-input/main.js
@@ -0,0 +1 @@
+document.querySelector('.relative-input').textContent = 'OK'
diff --git a/packages/playground/html/scriptAsync.html b/playground/html/scriptAsync.html
similarity index 100%
rename from packages/playground/html/scriptAsync.html
rename to playground/html/scriptAsync.html
diff --git a/packages/playground/html/scriptMixed.html b/playground/html/scriptMixed.html
similarity index 100%
rename from packages/playground/html/scriptMixed.html
rename to playground/html/scriptMixed.html
diff --git a/playground/html/serve/both.html b/playground/html/serve/both.html
new file mode 100644
index 00000000000000..9eebf466b653b5
--- /dev/null
+++ b/playground/html/serve/both.html
@@ -0,0 +1 @@
+
both.html
diff --git a/playground/html/serve/both/index.html b/playground/html/serve/both/index.html
new file mode 100644
index 00000000000000..00a4791ad0a8b8
--- /dev/null
+++ b/playground/html/serve/both/index.html
@@ -0,0 +1 @@
+
both/index.html
diff --git a/playground/html/serve/file.html b/playground/html/serve/file.html
new file mode 100644
index 00000000000000..f956e1216f1a1f
--- /dev/null
+++ b/playground/html/serve/file.html
@@ -0,0 +1 @@
+
file.html
diff --git a/playground/html/serve/folder/index.html b/playground/html/serve/folder/index.html
new file mode 100644
index 00000000000000..d66d0959648da4
--- /dev/null
+++ b/playground/html/serve/folder/index.html
@@ -0,0 +1 @@
+
folder/index.html
diff --git a/packages/playground/html/shared.js b/playground/html/shared.js
similarity index 100%
rename from packages/playground/html/shared.js
rename to playground/html/shared.js
diff --git a/playground/html/side-effects/index.html b/playground/html/side-effects/index.html
new file mode 100644
index 00000000000000..cca7443bd8eedb
--- /dev/null
+++ b/playground/html/side-effects/index.html
@@ -0,0 +1,2 @@
+
sideEffects false
+
diff --git a/playground/html/side-effects/package.json b/playground/html/side-effects/package.json
new file mode 100644
index 00000000000000..72a1449164f79b
--- /dev/null
+++ b/playground/html/side-effects/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-html-side-effects",
+ "private": true,
+ "version": "0.0.0",
+ "sideEffects": false
+}
diff --git a/playground/html/side-effects/sideEffects.js b/playground/html/side-effects/sideEffects.js
new file mode 100644
index 00000000000000..6bfb77c68206c0
--- /dev/null
+++ b/playground/html/side-effects/sideEffects.js
@@ -0,0 +1 @@
+console.log('message from sideEffects script')
diff --git a/playground/html/transform-inline-js.html b/playground/html/transform-inline-js.html
new file mode 100644
index 00000000000000..bfe65686e6970d
--- /dev/null
+++ b/playground/html/transform-inline-js.html
@@ -0,0 +1,5 @@
+
id: {{ id }}
+
test: ???
+
diff --git "a/playground/html/unicode-path/\344\270\255\346\226\207-\343\201\253\343\201\273\343\202\223\343\201\224-\355\225\234\352\270\200-\360\237\214\225\360\237\214\226\360\237\214\227/index.html" "b/playground/html/unicode-path/\344\270\255\346\226\207-\343\201\253\343\201\273\343\202\223\343\201\224-\355\225\234\352\270\200-\360\237\214\225\360\237\214\226\360\237\214\227/index.html"
new file mode 100644
index 00000000000000..8b401b4c85e7bf
--- /dev/null
+++ "b/playground/html/unicode-path/\344\270\255\346\226\207-\343\201\253\343\201\273\343\202\223\343\201\224-\355\225\234\352\270\200-\360\237\214\225\360\237\214\226\360\237\214\227/index.html"
@@ -0,0 +1 @@
+
Unicode path
diff --git a/playground/html/valid.html b/playground/html/valid.html
new file mode 100644
index 00000000000000..66193ba68fd27e
--- /dev/null
+++ b/playground/html/valid.html
@@ -0,0 +1,18 @@
+
+
Accept duplicated attribute
+
+
+
+
+
+
+
No quotes on Attr
+
+
+
+
+
+
+
+
+
diff --git a/playground/html/valid.js b/playground/html/valid.js
new file mode 100644
index 00000000000000..7b6a2386a07931
--- /dev/null
+++ b/playground/html/valid.js
@@ -0,0 +1,2 @@
+document.getElementById(`no-quotes-on-attr`).innerHTML =
+ `No quotes on Attr working`
diff --git a/playground/html/vite.config.js b/playground/html/vite.config.js
new file mode 100644
index 00000000000000..81b542f85f0940
--- /dev/null
+++ b/playground/html/vite.config.js
@@ -0,0 +1,297 @@
+import { relative, resolve } from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ base: './',
+ build: {
+ rollupOptions: {
+ input: {
+ main: resolve(__dirname, 'index.html'),
+ nested: resolve(__dirname, 'nested/index.html'),
+ scriptAsync: resolve(__dirname, 'scriptAsync.html'),
+ scriptMixed: resolve(__dirname, 'scriptMixed.html'),
+ emptyAttr: resolve(__dirname, 'emptyAttr.html'),
+ link: resolve(__dirname, 'link.html'),
+ 'link/target': resolve(__dirname, 'index.html'),
+ zeroJS: resolve(__dirname, 'zeroJS.html'),
+ noHead: resolve(__dirname, 'noHead.html'),
+ noBody: resolve(__dirname, 'noBody.html'),
+ inlinea: resolve(__dirname, 'inline/shared_a.html'),
+ inline1: resolve(__dirname, 'inline/shared-1.html'),
+ inline2: resolve(__dirname, 'inline/shared-2.html'),
+ inline3: resolve(__dirname, 'inline/unique.html'),
+ unicodePath: resolve(
+ __dirname,
+ 'unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html',
+ ),
+ linkProps: resolve(__dirname, 'link-props/index.html'),
+ valid: resolve(__dirname, 'valid.html'),
+ importmapOrder: resolve(__dirname, 'importmapOrder.html'),
+ env: resolve(__dirname, 'env.html'),
+ sideEffects: resolve(__dirname, 'side-effects/index.html'),
+ 'a á': resolve(__dirname, 'a á.html'),
+ serveFile: resolve(__dirname, 'serve/file.html'),
+ serveFolder: resolve(__dirname, 'serve/folder/index.html'),
+ serveBothFile: resolve(__dirname, 'serve/both.html'),
+ serveBothFolder: resolve(__dirname, 'serve/both/index.html'),
+ write: resolve(__dirname, 'write.html'),
+ 'transform-inline-js': resolve(__dirname, 'transform-inline-js.html'),
+ relativeInput: relative(
+ process.cwd(),
+ resolve(__dirname, 'relative-input.html'),
+ ),
+ },
+ external: ['/external-path-by-rollup-options.js'],
+ },
+ },
+
+ server: {
+ warmup: {
+ clientFiles: ['./warmup/*'],
+ },
+ },
+
+ define: {
+ 'import.meta.env.VITE_NUMBER': 5173,
+ 'import.meta.env.VITE_STRING': JSON.stringify('string'),
+ 'import.meta.env.VITE_OBJECT_STRING': '{ "foo": "bar" }',
+ 'import.meta.env.VITE_NULL_STRING': 'null',
+ },
+
+ plugins: [
+ {
+ name: 'pre-transform',
+ transformIndexHtml: {
+ order: 'pre',
+ handler(html, { filename }) {
+ if (html.includes('/@vite/client')) {
+ throw new Error('pre transform applied at wrong time!')
+ }
+
+ const doctypeRE = //i
+ if (doctypeRE.test(html)) return
+
+ const head = `
+
+
+
+
{{ title }}
+ `
+ return `
+${filename.includes('noHead') ? '' : head}
+${
+ filename.includes('noBody')
+ ? html
+ : `
+ ${html}
+`
+}
+
+ `
+ },
+ },
+ },
+ {
+ name: 'string-transform',
+ transformIndexHtml(html) {
+ return html.replace('Hello', 'Transformed')
+ },
+ },
+ {
+ name: 'tags-transform',
+ transformIndexHtml() {
+ return [
+ {
+ tag: 'meta',
+ attrs: { name: 'description', content: 'a vite app' },
+ // default injection is head-prepend
+ },
+ {
+ tag: 'meta',
+ attrs: { name: 'keywords', content: 'es modules' },
+ injectTo: 'head',
+ },
+ ]
+ },
+ },
+ {
+ name: 'combined-transform',
+ transformIndexHtml(html) {
+ return {
+ html: html.replace('{{ title }}', 'Test HTML transforms'),
+ tags: [
+ {
+ tag: 'p',
+ attrs: { class: 'inject' },
+ children: 'This is injected',
+ injectTo: 'body',
+ },
+ ],
+ }
+ },
+ },
+ {
+ name: 'serve-only-transform',
+ transformIndexHtml(_, ctx) {
+ if (ctx.server) {
+ return [
+ {
+ tag: 'p',
+ attrs: { class: 'server' },
+ children: 'This is injected only during dev',
+ injectTo: 'body',
+ },
+ ]
+ }
+ },
+ },
+ {
+ name: 'build-only-transform',
+ transformIndexHtml(_, ctx) {
+ if (ctx.bundle) {
+ return [
+ {
+ tag: 'p',
+ attrs: { class: 'build' },
+ children: 'This is injected only during build',
+ injectTo: 'body',
+ },
+ ]
+ }
+ },
+ },
+ {
+ name: 'path-conditional-transform',
+ transformIndexHtml(_, ctx) {
+ if (ctx.path.includes('nested')) {
+ return [
+ {
+ tag: 'p',
+ attrs: { class: 'conditional' },
+ children: 'This is injected only for /nested/index.html',
+ injectTo: 'body',
+ },
+ ]
+ }
+ },
+ },
+ {
+ name: 'body-prepend-transform',
+ transformIndexHtml() {
+ return [
+ {
+ tag: 'noscript',
+ children: '',
+ injectTo: 'body',
+ },
+ {
+ tag: 'noscript',
+ children: '',
+ injectTo: 'body-prepend',
+ },
+ ]
+ },
+ },
+ {
+ name: 'head-prepend-importmap',
+ transformIndexHtml(_, ctx) {
+ if (ctx.path.includes('importmapOrder')) return
+
+ return [
+ {
+ tag: 'script',
+ attrs: { type: 'importmap' },
+ children: `
+ {
+ "imports": {
+ "vue": "https://unpkg.com/vue@3.4.38/dist/vue.runtime.esm-browser.js"
+ }
+ }
+ `,
+ injectTo: 'head',
+ },
+ ]
+ },
+ },
+ {
+ name: 'escape-html-attribute',
+ transformIndexHtml: {
+ order: 'post',
+ handler() {
+ return [
+ {
+ tag: 'link',
+ attrs: {
+ href: `">
extra content
`,
+ },
+ injectTo: 'body',
+ },
+ ]
+ },
+ },
+ },
+ {
+ name: 'append-external-path-by-rollup-options',
+ apply: 'build', // this does not work in serve
+ transformIndexHtml: {
+ order: 'pre',
+ handler(_, ctx) {
+ if (!ctx.filename.endsWith('html/index.html')) return
+ return [
+ {
+ tag: 'script',
+ attrs: {
+ type: 'module',
+ src: '/external-path-by-rollup-options.js',
+ },
+ injectTo: 'body',
+ },
+ ]
+ },
+ },
+ },
+ {
+ name: 'transform-inline-js',
+ transformIndexHtml: {
+ order: 'pre',
+ handler(html, ctx) {
+ if (!ctx.filename.endsWith('html/transform-inline-js.html')) return
+ return html.replaceAll(
+ '{{ id }}',
+ Math.random().toString(36).slice(2),
+ )
+ },
+ },
+ },
+ serveExternalPathPlugin(),
+ ],
+})
+
+/** @returns {import('vite').Plugin} */
+function serveExternalPathPlugin() {
+ const handler = (req, res, next) => {
+ if (req.url === '/external-path.js') {
+ res.setHeader('Content-Type', 'application/javascript')
+ res.end('document.querySelector(".external-path").textContent = "works"')
+ } else if (req.url === '/external-path.css') {
+ res.setHeader('Content-Type', 'text/css')
+ res.end('.external-path{color:red}')
+ } else if (req.url === '/external-path-by-rollup-options.js') {
+ res.setHeader('Content-Type', 'application/javascript')
+ res.end(
+ 'document.querySelector(".external-path-by-rollup-options").textContent = "works"',
+ )
+ } else {
+ next()
+ }
+ }
+ return {
+ name: 'serve-external-path',
+ configureServer(server) {
+ server.middlewares.use(handler)
+ },
+ configurePreviewServer(server) {
+ server.middlewares.use(handler)
+ },
+ }
+}
diff --git a/playground/html/warmup/warm.js b/playground/html/warmup/warm.js
new file mode 100644
index 00000000000000..07ef537e41adcb
--- /dev/null
+++ b/playground/html/warmup/warm.js
@@ -0,0 +1 @@
+console.log('From warm.js')
diff --git a/playground/html/write.html b/playground/html/write.html
new file mode 100644
index 00000000000000..f85ce49c08dc8c
--- /dev/null
+++ b/playground/html/write.html
@@ -0,0 +1,3 @@
+
+
diff --git a/packages/playground/html/zeroJS.html b/playground/html/zeroJS.html
similarity index 100%
rename from packages/playground/html/zeroJS.html
rename to playground/html/zeroJS.html
diff --git a/playground/import-assertion/__tests__/import-assertion.spec.ts b/playground/import-assertion/__tests__/import-assertion.spec.ts
new file mode 100644
index 00000000000000..796011926f0e7d
--- /dev/null
+++ b/playground/import-assertion/__tests__/import-assertion.spec.ts
@@ -0,0 +1,10 @@
+import { expect, test } from 'vitest'
+import { page } from '~utils'
+
+test('from source code', async () => {
+ expect(await page.textContent('.src'), 'bar')
+})
+
+test('from dependency', async () => {
+ expect(await page.textContent('.dep'), 'world')
+})
diff --git a/playground/import-assertion/data.json b/playground/import-assertion/data.json
new file mode 100644
index 00000000000000..c8c4105eb57cda
--- /dev/null
+++ b/playground/import-assertion/data.json
@@ -0,0 +1,3 @@
+{
+ "foo": "bar"
+}
diff --git a/playground/import-assertion/import-assertion-dep/data.json b/playground/import-assertion/import-assertion-dep/data.json
new file mode 100644
index 00000000000000..f2a886f39de7d3
--- /dev/null
+++ b/playground/import-assertion/import-assertion-dep/data.json
@@ -0,0 +1,3 @@
+{
+ "hello": "world"
+}
diff --git a/playground/import-assertion/import-assertion-dep/index.js b/playground/import-assertion/import-assertion-dep/index.js
new file mode 100644
index 00000000000000..a6dd471aa74a76
--- /dev/null
+++ b/playground/import-assertion/import-assertion-dep/index.js
@@ -0,0 +1,3 @@
+import json from './data.json' assert { type: 'json' }
+
+export const hello = json.hello
diff --git a/playground/import-assertion/import-assertion-dep/package.json b/playground/import-assertion/import-assertion-dep/package.json
new file mode 100644
index 00000000000000..9d03675fb4a5da
--- /dev/null
+++ b/playground/import-assertion/import-assertion-dep/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-import-assertion-dep",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "exports": "./index.js"
+}
diff --git a/playground/import-assertion/index.html b/playground/import-assertion/index.html
new file mode 100644
index 00000000000000..1135de1ff891e0
--- /dev/null
+++ b/playground/import-assertion/index.html
@@ -0,0 +1,19 @@
+
Import assertion
+
+
From source code
+
+
+
From dependency
+
+
+
diff --git a/playground/import-assertion/package.json b/playground/import-assertion/package.json
new file mode 100644
index 00000000000000..04ecce34c1f5ec
--- /dev/null
+++ b/playground/import-assertion/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-import-assertion",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@vitejs/test-import-assertion-dep": "file:./import-assertion-dep"
+ }
+}
diff --git a/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts
new file mode 100644
index 00000000000000..510b64d65921cb
--- /dev/null
+++ b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts
@@ -0,0 +1,244 @@
+import { URL, fileURLToPath } from 'node:url'
+import { promisify } from 'node:util'
+import { execFile } from 'node:child_process'
+import { describe, expect, test } from 'vitest'
+import { mapFileCommentRegex } from 'convert-source-map'
+import { commentSourceMap } from '../foo-with-sourcemap-plugin'
+import {
+ extractSourcemap,
+ findAssetFile,
+ formatSourcemapForSnapshot,
+ isBuild,
+ listAssets,
+ page,
+ readFile,
+ serverLogs,
+} from '~utils'
+
+if (!isBuild) {
+ test('js', async () => {
+ const res = await page.request.get(new URL('./foo.js', page.url()).href)
+ const js = await res.text()
+ const map = extractSourcemap(js)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "AAAA,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG;",
+ "sources": [
+ "foo.js",
+ ],
+ "sourcesContent": [
+ "export const foo = 'foo'
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('plugin return sourcemap with `sources: [""]`', async () => {
+ const res = await page.request.get(new URL('./zoo.js', page.url()).href)
+ const js = await res.text()
+ expect(js).toContain('// add comment')
+
+ const map = extractSourcemap(js)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "AAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;",
+ "sources": [
+ "zoo.js",
+ ],
+ "sourcesContent": [
+ "export const zoo = 'zoo'
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('js with inline sourcemap injected by a plugin', async () => {
+ const res = await page.request.get(
+ new URL('./foo-with-sourcemap.js', page.url()).href,
+ )
+ const js = await res.text()
+
+ expect(js).toContain(commentSourceMap)
+ const sourcemapComments = js.match(mapFileCommentRegex).length
+ expect(sourcemapComments).toBe(1)
+
+ const map = extractSourcemap(js)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "AAAA,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG",
+ "sources": [
+ "",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('ts', async () => {
+ const res = await page.request.get(new URL('./bar.ts', page.url()).href)
+ const js = await res.text()
+ const map = extractSourcemap(js)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "AAAO,aAAM,MAAM;",
+ "sources": [
+ "bar.ts",
+ ],
+ "sourcesContent": [
+ "export const bar = 'bar'
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('multiline import', async () => {
+ const res = await page.request.get(
+ new URL('./with-multiline-import.ts', page.url()).href,
+ )
+ const multi = await res.text()
+ const map = extractSourcemap(multi)
+ expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(`
+ {
+ "mappings": "AACA;AAAA,EACE;AAAA,OACK;AAEP,QAAQ,IAAI,yBAAyB,GAAG;",
+ "sources": [
+ "with-multiline-import.ts",
+ ],
+ "sourcesContent": [
+ "// prettier-ignore
+ import {
+ foo
+ } from '@vitejs/test-importee-pkg'
+
+ console.log('with-multiline-import', foo)
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('should not output missing source file warning', () => {
+ serverLogs.forEach((log) => {
+ expect(log).not.toMatch(/Sourcemap for .+ points to missing source files/)
+ })
+ })
+}
+
+describe.runIf(isBuild)('build tests', () => {
+ test('should not output sourcemap warning (#4939)', () => {
+ serverLogs.forEach((log) => {
+ expect(log).not.toMatch('Sourcemap is likely to be incorrect')
+ })
+ })
+
+ test('sourcemap is correct when preload information is injected', async () => {
+ const map = findAssetFile(/after-preload-dynamic-[-\w]{8}\.js\.map/)
+ expect(formatSourcemapForSnapshot(JSON.parse(map))).toMatchInlineSnapshot(`
+ {
+ "debugId": "00000000-0000-0000-0000-000000000000",
+ "ignoreList": [],
+ "mappings": ";4kCAAA,OAAO,2BAAuB,0BAE9B,QAAQ,IAAI,uBAAuB",
+ "sources": [
+ "../../after-preload-dynamic.js",
+ ],
+ "sourcesContent": [
+ "import('./dynamic/dynamic-foo')
+
+ console.log('after preload dynamic')
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ // verify sourcemap comment is preserved at the last line
+ const js = findAssetFile(/after-preload-dynamic-[-\w]{8}\.js$/)
+ expect(js).toMatch(
+ /\n\/\/# sourceMappingURL=after-preload-dynamic-[-\w]{8}\.js\.map\n$/,
+ )
+ })
+
+ test('__vite__mapDeps injected after banner', async () => {
+ const js = findAssetFile(/after-preload-dynamic-hashbang-[-\w]{8}\.js$/)
+ expect(js.split('\n').slice(0, 2)).toEqual([
+ '#!/usr/bin/env node',
+ expect.stringContaining('const __vite__mapDeps=(i'),
+ ])
+ })
+
+ test('no unused __vite__mapDeps', async () => {
+ const js = findAssetFile(/after-preload-dynamic-no-dep-[-\w]{8}\.js$/)
+ expect(js).not.toMatch(/__vite__mapDeps/)
+ })
+
+ test('sourcemap is correct when using object as "define" value', async () => {
+ const map = findAssetFile(/with-define-object.*\.js\.map/)
+ expect(formatSourcemapForSnapshot(JSON.parse(map))).toMatchInlineSnapshot(`
+ {
+ "debugId": "00000000-0000-0000-0000-000000000000",
+ "mappings": "qBAEA,SAASA,GAAO,CACJC,EAAA,CACZ,CAEA,SAASA,GAAY,CAEX,QAAA,MAAM,qBAAsBC,CAAkB,CACxD,CAEAF,EAAK",
+ "sources": [
+ "../../with-define-object.ts",
+ ],
+ "sourcesContent": [
+ "// test complicated stack since broken sourcemap
+ // might still look correct with a simple case
+ function main() {
+ mainInner()
+ }
+
+ function mainInner() {
+ // @ts-expect-error "define"
+ console.trace('with-define-object', __testDefineObject)
+ }
+
+ main()
+ ",
+ ],
+ "version": 3,
+ }
+ `)
+ })
+
+ test('correct sourcemap during ssr dev when using object as "define" value', async () => {
+ const execFileAsync = promisify(execFile)
+ await execFileAsync('node', ['test-ssr-dev.js'], {
+ cwd: fileURLToPath(new URL('..', import.meta.url)),
+ })
+ })
+
+ test('source and sourcemap contain matching debug IDs', () => {
+ function getDebugIdFromString(input: string): string | undefined {
+ const match = input.match(/\/\/# debugId=([a-fA-F0-9-]+)/)
+ return match ? match[1] : undefined
+ }
+
+ const assets = listAssets().map((asset) => `dist/assets/${asset}`)
+ const jsAssets = assets.filter((asset) => asset.endsWith('.js'))
+
+ for (const jsAsset of jsAssets) {
+ const jsContent = readFile(jsAsset)
+ const sourceDebugId = getDebugIdFromString(jsContent)
+ expect(
+ sourceDebugId,
+ `Asset '${jsAsset}' did not contain a debug ID`,
+ ).toBeDefined()
+
+ const mapFile = jsAsset + '.map'
+ const mapContent = readFile(mapFile)
+
+ const mapObj = JSON.parse(mapContent)
+ const mapDebugId = mapObj.debugId
+
+ expect(
+ sourceDebugId,
+ 'Debug ID in source didnt match debug ID in sourcemap',
+ ).toEqual(mapDebugId)
+ }
+ })
+})
diff --git a/playground/js-sourcemap/after-preload-dynamic-hashbang.js b/playground/js-sourcemap/after-preload-dynamic-hashbang.js
new file mode 100644
index 00000000000000..918cdeff7932a4
--- /dev/null
+++ b/playground/js-sourcemap/after-preload-dynamic-hashbang.js
@@ -0,0 +1,5 @@
+// hashbang is injected via rollupOptions.output.banner
+
+import('./dynamic/dynamic-foo')
+
+console.log('after preload dynamic hashbang')
diff --git a/playground/js-sourcemap/after-preload-dynamic-no-dep.js b/playground/js-sourcemap/after-preload-dynamic-no-dep.js
new file mode 100644
index 00000000000000..55f150702d02dc
--- /dev/null
+++ b/playground/js-sourcemap/after-preload-dynamic-no-dep.js
@@ -0,0 +1,3 @@
+import('./dynamic/dynamic-no-dep')
+
+console.log('after preload dynamic no dep')
diff --git a/playground/js-sourcemap/after-preload-dynamic.js b/playground/js-sourcemap/after-preload-dynamic.js
new file mode 100644
index 00000000000000..c11701f2320efc
--- /dev/null
+++ b/playground/js-sourcemap/after-preload-dynamic.js
@@ -0,0 +1,3 @@
+import('./dynamic/dynamic-foo')
+
+console.log('after preload dynamic')
diff --git a/playground/js-sourcemap/bar.ts b/playground/js-sourcemap/bar.ts
new file mode 100644
index 00000000000000..1fc11814f22e80
--- /dev/null
+++ b/playground/js-sourcemap/bar.ts
@@ -0,0 +1 @@
+export const bar = 'bar'
diff --git a/playground/js-sourcemap/dynamic/dynamic-foo.css b/playground/js-sourcemap/dynamic/dynamic-foo.css
new file mode 100644
index 00000000000000..0f85e3797cbb65
--- /dev/null
+++ b/playground/js-sourcemap/dynamic/dynamic-foo.css
@@ -0,0 +1,3 @@
+.dynamic-foo {
+ color: red;
+}
diff --git a/playground/js-sourcemap/dynamic/dynamic-foo.js b/playground/js-sourcemap/dynamic/dynamic-foo.js
new file mode 100644
index 00000000000000..32bcdeeb03683f
--- /dev/null
+++ b/playground/js-sourcemap/dynamic/dynamic-foo.js
@@ -0,0 +1,3 @@
+import './dynamic-foo.css'
+
+console.log('dynamic/dynamic-foo')
diff --git a/playground/js-sourcemap/dynamic/dynamic-no-dep.js b/playground/js-sourcemap/dynamic/dynamic-no-dep.js
new file mode 100644
index 00000000000000..d2291ff747319c
--- /dev/null
+++ b/playground/js-sourcemap/dynamic/dynamic-no-dep.js
@@ -0,0 +1 @@
+console.log('dynamic/dynamic-no-dep')
diff --git a/playground/js-sourcemap/foo-with-sourcemap-plugin.ts b/playground/js-sourcemap/foo-with-sourcemap-plugin.ts
new file mode 100644
index 00000000000000..a181c39eaf6036
--- /dev/null
+++ b/playground/js-sourcemap/foo-with-sourcemap-plugin.ts
@@ -0,0 +1,17 @@
+import type { Plugin } from 'vite'
+
+export const commentSourceMap = [
+ '// default boundary sourcemap with magic-string',
+ '//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxNQUFNLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHIn0=',
+].join('\n')
+
+export default function transformFooWithInlineSourceMap(): Plugin {
+ return {
+ name: 'transform-foo-with-inline-sourcemap',
+ transform(code, id) {
+ if (id.includes('foo-with-sourcemap.js')) {
+ return `${code}${commentSourceMap}`
+ }
+ },
+ }
+}
diff --git a/playground/js-sourcemap/foo-with-sourcemap.js b/playground/js-sourcemap/foo-with-sourcemap.js
new file mode 100644
index 00000000000000..cb356468240d50
--- /dev/null
+++ b/playground/js-sourcemap/foo-with-sourcemap.js
@@ -0,0 +1 @@
+export const foo = 'foo'
diff --git a/playground/js-sourcemap/foo.js b/playground/js-sourcemap/foo.js
new file mode 100644
index 00000000000000..cb356468240d50
--- /dev/null
+++ b/playground/js-sourcemap/foo.js
@@ -0,0 +1 @@
+export const foo = 'foo'
diff --git a/playground/js-sourcemap/importee-pkg/index.js b/playground/js-sourcemap/importee-pkg/index.js
new file mode 100644
index 00000000000000..95403a93f0d308
--- /dev/null
+++ b/playground/js-sourcemap/importee-pkg/index.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import-x/no-commonjs
+exports.foo = 'foo'
diff --git a/playground/js-sourcemap/importee-pkg/package.json b/playground/js-sourcemap/importee-pkg/package.json
new file mode 100644
index 00000000000000..2bc76d5bb50b39
--- /dev/null
+++ b/playground/js-sourcemap/importee-pkg/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-importee-pkg",
+ "private": true,
+ "version": "0.0.0",
+ "main": "./index.js"
+}
diff --git a/playground/js-sourcemap/index.html b/playground/js-sourcemap/index.html
new file mode 100644
index 00000000000000..7a91852f4ebe18
--- /dev/null
+++ b/playground/js-sourcemap/index.html
@@ -0,0 +1,13 @@
+
+
JS Sourcemap
+
dynamic
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/js-sourcemap/package.json b/playground/js-sourcemap/package.json
new file mode 100644
index 00000000000000..d7c724d46138eb
--- /dev/null
+++ b/playground/js-sourcemap/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@vitejs/test-js-sourcemap",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@vitejs/test-importee-pkg": "file:importee-pkg",
+ "magic-string": "^0.30.17"
+ }
+}
diff --git a/playground/js-sourcemap/plugin-foo.js b/playground/js-sourcemap/plugin-foo.js
new file mode 100644
index 00000000000000..cb356468240d50
--- /dev/null
+++ b/playground/js-sourcemap/plugin-foo.js
@@ -0,0 +1 @@
+export const foo = 'foo'
diff --git a/playground/js-sourcemap/test-ssr-dev.js b/playground/js-sourcemap/test-ssr-dev.js
new file mode 100644
index 00000000000000..0e21a0ae80fe17
--- /dev/null
+++ b/playground/js-sourcemap/test-ssr-dev.js
@@ -0,0 +1,39 @@
+import assert from 'node:assert'
+import { fileURLToPath } from 'node:url'
+import { createServer } from 'vite'
+
+async function runTest() {
+ const server = await createServer({
+ root: fileURLToPath(new URL('.', import.meta.url)),
+ configFile: false,
+ optimizeDeps: {
+ noDiscovery: true,
+ },
+ server: {
+ middlewareMode: true,
+ hmr: false,
+ ws: false,
+ },
+ define: {
+ __testDefineObject: '{ "hello": "test" }',
+ },
+ })
+ const mod = await server.ssrLoadModule('/with-define-object-ssr.ts')
+ const error = await getError(() => mod.error())
+ server.ssrFixStacktrace(error)
+ assert.match(error.stack, /at errorInner (.*with-define-object-ssr.ts:7:9)/)
+ await server.close()
+}
+
+async function getError(f) {
+ let error
+ try {
+ await f()
+ } catch (e) {
+ error = e
+ }
+ assert.ok(error)
+ return error
+}
+
+runTest()
diff --git a/playground/js-sourcemap/vite.config.js b/playground/js-sourcemap/vite.config.js
new file mode 100644
index 00000000000000..4d3d5818b5dde9
--- /dev/null
+++ b/playground/js-sourcemap/vite.config.js
@@ -0,0 +1,40 @@
+import { defineConfig } from 'vite'
+import transformFooWithInlineSourceMap from './foo-with-sourcemap-plugin'
+import { transformZooWithSourcemapPlugin } from './zoo-with-sourcemap-plugin'
+
+export default defineConfig({
+ plugins: [
+ transformFooWithInlineSourceMap(),
+ transformZooWithSourcemapPlugin(),
+ ],
+ build: {
+ sourcemap: true,
+ rollupOptions: {
+ output: {
+ manualChunks(name) {
+ if (name.endsWith('after-preload-dynamic.js')) {
+ return 'after-preload-dynamic'
+ }
+ if (name.endsWith('after-preload-dynamic-hashbang.js')) {
+ return 'after-preload-dynamic-hashbang'
+ }
+ if (name.endsWith('after-preload-dynamic-no-dep.js')) {
+ return 'after-preload-dynamic-no-dep'
+ }
+ if (name.includes('with-define-object')) {
+ return 'with-define-object'
+ }
+ },
+ banner(chunk) {
+ if (chunk.name.endsWith('after-preload-dynamic-hashbang')) {
+ return '#!/usr/bin/env node'
+ }
+ },
+ sourcemapDebugIds: true,
+ },
+ },
+ },
+ define: {
+ __testDefineObject: '{ "hello": "test" }',
+ },
+})
diff --git a/playground/js-sourcemap/with-define-object-ssr.ts b/playground/js-sourcemap/with-define-object-ssr.ts
new file mode 100644
index 00000000000000..9ff85230025e2d
--- /dev/null
+++ b/playground/js-sourcemap/with-define-object-ssr.ts
@@ -0,0 +1,8 @@
+export function error() {
+ errorInner()
+}
+
+function errorInner() {
+ // @ts-expect-error "define"
+ throw new Error('with-define-object: ' + JSON.stringify(__testDefineObject))
+}
diff --git a/playground/js-sourcemap/with-define-object.ts b/playground/js-sourcemap/with-define-object.ts
new file mode 100644
index 00000000000000..5a9f8e2ddd43d9
--- /dev/null
+++ b/playground/js-sourcemap/with-define-object.ts
@@ -0,0 +1,12 @@
+// test complicated stack since broken sourcemap
+// might still look correct with a simple case
+function main() {
+ mainInner()
+}
+
+function mainInner() {
+ // @ts-expect-error "define"
+ console.trace('with-define-object', __testDefineObject)
+}
+
+main()
diff --git a/playground/js-sourcemap/with-multiline-import.ts b/playground/js-sourcemap/with-multiline-import.ts
new file mode 100644
index 00000000000000..5bf8aa53214384
--- /dev/null
+++ b/playground/js-sourcemap/with-multiline-import.ts
@@ -0,0 +1,6 @@
+// prettier-ignore
+import {
+ foo
+} from '@vitejs/test-importee-pkg'
+
+console.log('with-multiline-import', foo)
diff --git a/playground/js-sourcemap/zoo-with-sourcemap-plugin.ts b/playground/js-sourcemap/zoo-with-sourcemap-plugin.ts
new file mode 100644
index 00000000000000..6c493278d166c8
--- /dev/null
+++ b/playground/js-sourcemap/zoo-with-sourcemap-plugin.ts
@@ -0,0 +1,18 @@
+import MagicString from 'magic-string'
+import type { Plugin } from 'vite'
+
+export const transformZooWithSourcemapPlugin: () => Plugin = () => ({
+ name: 'sourcemap',
+ transform(code, id) {
+ if (id.includes('zoo.js')) {
+ const ms = new MagicString(code)
+ ms.append('// add comment')
+ return {
+ code: ms.toString(),
+ // NOTE: MagicString without `filename` option generates
+ // a sourcemap with `sources: ['']` or `sources: [null]`
+ map: ms.generateMap({ hires: true }),
+ }
+ }
+ },
+})
diff --git a/playground/js-sourcemap/zoo.js b/playground/js-sourcemap/zoo.js
new file mode 100644
index 00000000000000..286343f930d3c3
--- /dev/null
+++ b/playground/js-sourcemap/zoo.js
@@ -0,0 +1 @@
+export const zoo = 'zoo'
diff --git a/playground/json/__tests__/csr/json-csr.spec.ts b/playground/json/__tests__/csr/json-csr.spec.ts
new file mode 100644
index 00000000000000..cec1fb5f0eb0ba
--- /dev/null
+++ b/playground/json/__tests__/csr/json-csr.spec.ts
@@ -0,0 +1,62 @@
+import { readFileSync } from 'node:fs'
+import { expect, test } from 'vitest'
+import deepJson from 'vue/package.json'
+import testJson from '../../test.json'
+import hmrJson from '../../hmr.json'
+import { editFile, isBuild, isServe, page, untilUpdated } from '~utils'
+
+const stringified = JSON.stringify(testJson)
+const deepStringified = JSON.stringify(deepJson)
+const hmrStringified = JSON.stringify(hmrJson)
+
+test('default import', async () => {
+ expect(await page.textContent('.full')).toBe(stringified)
+})
+
+test('named import', async () => {
+ expect(await page.textContent('.named')).toBe(testJson.hello)
+})
+
+test('deep import', async () => {
+ expect(await page.textContent('.deep-full')).toBe(deepStringified)
+})
+
+test('named deep import', async () => {
+ expect(await page.textContent('.deep-named')).toBe(deepJson.name)
+})
+
+test('dynamic import', async () => {
+ expect(await page.textContent('.dynamic')).toBe(stringified)
+})
+
+test('dynamic import, named', async () => {
+ expect(await page.textContent('.dynamic-named')).toBe(testJson.hello)
+})
+
+test('fetch', async () => {
+ expect(await page.textContent('.fetch')).toBe(stringified)
+})
+
+test('?url', async () => {
+ expect(await page.textContent('.url')).toMatch(
+ isBuild ? 'data:application/json' : '/test.json',
+ )
+})
+
+test('?raw', async () => {
+ expect(await page.textContent('.raw')).toBe(
+ readFileSync(require.resolve('../../test.json'), 'utf-8'),
+ )
+})
+
+test.runIf(isServe)('should full reload', async () => {
+ expect(await page.textContent('.hmr')).toBe(hmrStringified)
+
+ editFile('hmr.json', (code) =>
+ code.replace('"this is hmr json"', '"this is hmr update json"'),
+ )
+ await untilUpdated(
+ () => page.textContent('.hmr'),
+ '"this is hmr update json"',
+ )
+})
diff --git a/playground/json/hmr.json b/playground/json/hmr.json
new file mode 100644
index 00000000000000..2dc497c5321ac4
--- /dev/null
+++ b/playground/json/hmr.json
@@ -0,0 +1,3 @@
+{
+ "hmr": "this is hmr json"
+}
diff --git a/playground/json/index.html b/playground/json/index.html
new file mode 100644
index 00000000000000..ee076836f10b53
--- /dev/null
+++ b/playground/json/index.html
@@ -0,0 +1,70 @@
+
Normal Import
+
+
+
+
Deep Import
+
+
+
+
Dynamic Import
+
+
+
+
fetch
+
+
+
Importing as URL
+
+
+
Raw Import
+
+
+
JSON Module
+
+
+
Has BOM Tag
+
+
+
HMR
+
+
+
diff --git a/playground/json/json-bom/has-bom.json b/playground/json/json-bom/has-bom.json
new file mode 100644
index 00000000000000..8145b3da51e442
--- /dev/null
+++ b/playground/json/json-bom/has-bom.json
@@ -0,0 +1,4 @@
+{
+ "description": "This file is marked with BOM.",
+ "message": "If the parsing is successful, the BOM tag has been removed."
+}
diff --git a/packages/playground/json/json-module/index.json b/playground/json/json-module/index.json
similarity index 100%
rename from packages/playground/json/json-module/index.json
rename to playground/json/json-module/index.json
diff --git a/playground/json/json-module/package.json b/playground/json/json-module/package.json
new file mode 100644
index 00000000000000..a02c431845033f
--- /dev/null
+++ b/playground/json/json-module/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "@vitejs/test-json-module",
+ "version": "0.0.0"
+}
diff --git a/playground/json/package.json b/playground/json/package.json
new file mode 100644
index 00000000000000..388421a1da2400
--- /dev/null
+++ b/playground/json/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@vitejs/test-json",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "@vitejs/test-json-module": "file:./json-module",
+ "vue": "^3.5.13"
+ }
+}
diff --git a/packages/playground/json/public/public.json b/playground/json/public/public.json
similarity index 100%
rename from packages/playground/json/public/public.json
rename to playground/json/public/public.json
diff --git a/playground/json/test.json b/playground/json/test.json
new file mode 100644
index 00000000000000..bacbe05617617e
--- /dev/null
+++ b/playground/json/test.json
@@ -0,0 +1,3 @@
+{
+ "hello": "this is json"
+}
diff --git a/playground/legacy/__tests__/client-and-ssr/legacy-client-legacy-ssr-sequential-builds.spec.ts b/playground/legacy/__tests__/client-and-ssr/legacy-client-legacy-ssr-sequential-builds.spec.ts
new file mode 100644
index 00000000000000..0980d722605347
--- /dev/null
+++ b/playground/legacy/__tests__/client-and-ssr/legacy-client-legacy-ssr-sequential-builds.spec.ts
@@ -0,0 +1,17 @@
+import { describe, expect, test } from 'vitest'
+import { port } from './serve'
+import { isBuild, page } from '~utils'
+
+const url = `http://localhost:${port}`
+
+describe.runIf(isBuild)('client-legacy-ssr-sequential-builds', () => {
+ test('should work', async () => {
+ await page.goto(url)
+ expect(await page.textContent('#app')).toMatch('Hello')
+ })
+
+ test('import.meta.env.MODE', async () => {
+ // SSR build is always modern
+ expect(await page.textContent('#mode')).toMatch('test')
+ })
+})
diff --git a/playground/legacy/__tests__/client-and-ssr/serve.ts b/playground/legacy/__tests__/client-and-ssr/serve.ts
new file mode 100644
index 00000000000000..46a075549db53a
--- /dev/null
+++ b/playground/legacy/__tests__/client-and-ssr/serve.ts
@@ -0,0 +1,59 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+import path from 'node:path'
+import { ports, rootDir } from '~utils'
+
+export const port = ports['legacy/client-and-ssr']
+
+export async function serve(): Promise<{ close(): Promise
}> {
+ const { build } = await import('vite')
+
+ await build({
+ mode: 'test',
+ root: rootDir,
+ logLevel: 'silent',
+ build: {
+ target: 'esnext',
+ outDir: 'dist/client',
+ },
+ })
+
+ await build({
+ mode: 'test',
+ root: rootDir,
+ logLevel: 'silent',
+ build: {
+ target: 'esnext',
+ ssr: 'entry-server-sequential.js',
+ outDir: 'dist/server',
+ },
+ })
+
+ const { default: express } = await import('express')
+ const app = express()
+
+ app.use('/', async (_req, res) => {
+ const { render } = await import(
+ path.resolve(rootDir, './dist/server/entry-server-sequential.js')
+ )
+ const html = await render()
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
+ })
+
+ return new Promise((resolve, reject) => {
+ try {
+ const server = app.listen(port, () => {
+ resolve({
+ // for test teardown
+ async close() {
+ await new Promise((resolve) => {
+ server.close(resolve)
+ })
+ },
+ })
+ })
+ } catch (e) {
+ reject(e)
+ }
+ })
+}
diff --git a/playground/legacy/__tests__/legacy.spec.ts b/playground/legacy/__tests__/legacy.spec.ts
new file mode 100644
index 00000000000000..64db58a8886016
--- /dev/null
+++ b/playground/legacy/__tests__/legacy.spec.ts
@@ -0,0 +1,161 @@
+import { describe, expect, test } from 'vitest'
+import {
+ findAssetFile,
+ getColor,
+ isBuild,
+ listAssets,
+ page,
+ readManifest,
+ untilUpdated,
+} from '~utils'
+
+test('should load the worker', async () => {
+ await untilUpdated(() => page.textContent('.worker-message'), 'module')
+})
+
+test('should work', async () => {
+ await untilUpdated(() => page.textContent('#app'), 'Hello')
+})
+
+test('import.meta.env.LEGACY', async () => {
+ await untilUpdated(() => page.textContent('#env'), isBuild ? 'true' : 'false')
+ await untilUpdated(() => page.textContent('#env-equal'), 'true')
+})
+
+// https://github.com/vitejs/vite/issues/3400
+test('transpiles down iterators correctly', async () => {
+ await untilUpdated(() => page.textContent('#iterators'), 'hello')
+})
+
+test('async generator', async () => {
+ await untilUpdated(() => page.textContent('#async-generator'), '[0,1,2]')
+})
+
+test('wraps with iife', async () => {
+ await untilUpdated(
+ () => page.textContent('#babel-helpers'),
+ 'exposed babel helpers: false',
+ )
+})
+
+test('generates assets', async () => {
+ await untilUpdated(
+ () => page.textContent('#assets'),
+ isBuild
+ ? [
+ 'index: text/html',
+ 'index-legacy: text/html',
+ 'chunk-async: text/html',
+ 'chunk-async-legacy: text/html',
+ 'immutable-chunk: text/javascript',
+ 'immutable-chunk-legacy: text/javascript',
+ 'polyfills-legacy: text/html',
+ ].join('\n')
+ : [
+ 'index: text/html',
+ 'index-legacy: text/html',
+ 'chunk-async: text/html',
+ 'chunk-async-legacy: text/html',
+ 'immutable-chunk: text/html',
+ 'immutable-chunk-legacy: text/html',
+ 'polyfills-legacy: text/html',
+ ].join('\n'),
+ )
+})
+
+test('correctly emits styles', async () => {
+ expect(await getColor('#app')).toBe('red')
+})
+
+// dynamic import css
+test('should load dynamic import with css', async () => {
+ await page.click('#dynamic-css-button')
+ await untilUpdated(() => getColor('#dynamic-css'), 'red')
+})
+
+test('asset url', async () => {
+ expect(await page.textContent('#asset-path')).toMatch(
+ isBuild ? /\/assets\/vite-[-\w]+\.svg/ : '/vite.svg',
+ )
+})
+
+describe.runIf(isBuild)('build', () => {
+ test('should generate correct manifest', async () => {
+ const manifest = readManifest()
+ // legacy polyfill
+ expect(manifest['../../vite/legacy-polyfills-legacy']).toBeDefined()
+ expect(manifest['../../vite/legacy-polyfills-legacy'].src).toBe(
+ '../../vite/legacy-polyfills-legacy',
+ )
+ expect(manifest['custom0-legacy.js'].file).toMatch(
+ /chunk-X-legacy\.[-\w]{8}.js/,
+ )
+ expect(manifest['custom1-legacy.js'].file).toMatch(
+ /chunk-X-legacy-[-\w]{8}.js/,
+ )
+ expect(manifest['custom2-legacy.js'].file).toMatch(
+ /chunk-X-legacy[-\w]{8}.js/,
+ )
+ // modern polyfill
+ expect(manifest['../../vite/legacy-polyfills']).toBeDefined()
+ expect(manifest['../../vite/legacy-polyfills'].src).toBe(
+ '../../vite/legacy-polyfills',
+ )
+ })
+
+ test('should minify legacy chunks with terser', async () => {
+ // This is a ghetto heuristic, but terser output seems to reliably start
+ // with one of the following, and non-terser output (including unminified or
+ // esbuild-minified) does not!
+ const terserPattern = /^(?:!function|System.register)/
+
+ expect(findAssetFile(/chunk-async-legacy/)).toMatch(terserPattern)
+ expect(findAssetFile(/chunk-async(?!-legacy)/)).not.toMatch(terserPattern)
+ expect(findAssetFile(/immutable-chunk-legacy/)).toMatch(terserPattern)
+ expect(findAssetFile(/immutable-chunk(?!-legacy)/)).not.toMatch(
+ terserPattern,
+ )
+ expect(findAssetFile(/index-legacy/)).toMatch(terserPattern)
+ expect(findAssetFile(/index(?!-legacy)/)).not.toMatch(terserPattern)
+ expect(findAssetFile(/polyfills-legacy/)).toMatch(terserPattern)
+ })
+
+ test('should emit css file', async () => {
+ expect(
+ listAssets().some((filename) => filename.endsWith('.css')),
+ ).toBeTruthy()
+ })
+
+ test('includes structuredClone polyfill which is supported after core-js v3', () => {
+ expect(findAssetFile(/polyfills-legacy/)).toMatch('"structuredClone"')
+ expect(findAssetFile(/polyfills-[-\w]{8}\./)).toMatch('"structuredClone"')
+ })
+
+ test('should generate legacy sourcemap file', async () => {
+ expect(
+ listAssets().some((filename) =>
+ /index-legacy-[-\w]{8}\.js\.map$/.test(filename),
+ ),
+ ).toBeTruthy()
+ expect(
+ listAssets().some((filename) =>
+ /polyfills-legacy-[-\w]{8}\.js\.map$/.test(filename),
+ ),
+ ).toBeTruthy()
+ // also for modern polyfills
+ expect(
+ listAssets().some((filename) =>
+ /polyfills-[-\w]{8}\.js\.map$/.test(filename),
+ ),
+ ).toBeTruthy()
+ })
+
+ test('should have only modern entry files guarded', async () => {
+ const guard = /(import\s*\()|(import.meta)|(async\s*function\*)/
+ expect(findAssetFile(/index(?!-legacy)/)).toMatch(guard)
+ expect(findAssetFile(/polyfills(?!-legacy)/)).toMatch(guard)
+
+ expect(findAssetFile(/chunk-async(?!-legacy)/)).not.toMatch(guard)
+ expect(findAssetFile(/index-legacy/)).not.toMatch(guard)
+ })
+})
diff --git a/playground/legacy/__tests__/no-polyfills-no-systemjs/legacy-no-polyfills-no-systemjs.spec.ts b/playground/legacy/__tests__/no-polyfills-no-systemjs/legacy-no-polyfills-no-systemjs.spec.ts
new file mode 100644
index 00000000000000..c9d6a027d8dddd
--- /dev/null
+++ b/playground/legacy/__tests__/no-polyfills-no-systemjs/legacy-no-polyfills-no-systemjs.spec.ts
@@ -0,0 +1,15 @@
+import { expect, test } from 'vitest'
+import { isBuild, page, untilUpdated, viteTestUrl } from '~utils'
+
+test.runIf(isBuild)('includes only a single script tag', async () => {
+ await page.goto(viteTestUrl + '/no-polyfills-no-systemjs.html')
+
+ await untilUpdated(
+ () => page.getAttribute('#vite-legacy-entry', 'data-src'),
+ /.\/assets\/index-legacy-(.+)\.js/,
+ )
+
+ expect(await page.locator('script').count()).toBe(1)
+ expect(await page.locator('#vite-legacy-polyfill').count()).toBe(0)
+ expect(await page.locator('#vite-legacy-entry').count()).toBe(1)
+})
diff --git a/playground/legacy/__tests__/no-polyfills/legacy-no-polyfills.spec.ts b/playground/legacy/__tests__/no-polyfills/legacy-no-polyfills.spec.ts
new file mode 100644
index 00000000000000..e2bf95ca478868
--- /dev/null
+++ b/playground/legacy/__tests__/no-polyfills/legacy-no-polyfills.spec.ts
@@ -0,0 +1,18 @@
+import { test } from 'vitest'
+import { isBuild, page, untilUpdated, viteTestUrl } from '~utils'
+
+test('should load and execute the JS file', async () => {
+ await page.goto(viteTestUrl + '/no-polyfills.html')
+ await untilUpdated(() => page.textContent('main'), '👋')
+})
+
+test.runIf(isBuild)('includes a script tag for SystemJS', async () => {
+ await untilUpdated(
+ () => page.getAttribute('#vite-legacy-polyfill', 'src'),
+ /.\/assets\/polyfills-legacy-(.+)\.js/,
+ )
+ await untilUpdated(
+ () => page.getAttribute('#vite-legacy-entry', 'data-src'),
+ /.\/assets\/index-legacy-(.+)\.js/,
+ )
+})
diff --git a/playground/legacy/__tests__/ssr/legacy-ssr.spec.ts b/playground/legacy/__tests__/ssr/legacy-ssr.spec.ts
new file mode 100644
index 00000000000000..b42c268daf8b80
--- /dev/null
+++ b/playground/legacy/__tests__/ssr/legacy-ssr.spec.ts
@@ -0,0 +1,17 @@
+import { describe, expect, test } from 'vitest'
+import { port } from './serve'
+import { isBuild, page } from '~utils'
+
+const url = `http://localhost:${port}`
+
+describe.runIf(isBuild)('legacy-ssr', () => {
+ test('should work', async () => {
+ await page.goto(url)
+ expect(await page.textContent('#app')).toMatch('Hello')
+ })
+
+ test('import.meta.env.LEGACY', async () => {
+ // SSR build is always modern
+ expect(await page.textContent('#env')).toMatch('false')
+ })
+})
diff --git a/playground/legacy/__tests__/ssr/serve.ts b/playground/legacy/__tests__/ssr/serve.ts
new file mode 100644
index 00000000000000..2b20dfc8455d69
--- /dev/null
+++ b/playground/legacy/__tests__/ssr/serve.ts
@@ -0,0 +1,47 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+import path from 'node:path'
+import { ports, rootDir } from '~utils'
+
+export const port = ports['legacy/ssr']
+
+export async function serve(): Promise<{ close(): Promise }> {
+ const { build } = await import('vite')
+ await build({
+ root: rootDir,
+ logLevel: 'silent',
+ build: {
+ target: 'esnext',
+ ssr: 'entry-server.js',
+ outDir: 'dist/server',
+ },
+ })
+
+ const { default: express } = await import('express')
+ const app = express()
+
+ app.use('/', async (_req, res) => {
+ const { render } = await import(
+ path.resolve(rootDir, './dist/server/entry-server.js')
+ )
+ const html = await render()
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
+ })
+
+ return new Promise((resolve, reject) => {
+ try {
+ const server = app.listen(port, () => {
+ resolve({
+ // for test teardown
+ async close() {
+ await new Promise((resolve) => {
+ server.close(resolve)
+ })
+ },
+ })
+ })
+ } catch (e) {
+ reject(e)
+ }
+ })
+}
diff --git a/playground/legacy/__tests__/watch/legacy-styles-only-entry-watch.spec.ts b/playground/legacy/__tests__/watch/legacy-styles-only-entry-watch.spec.ts
new file mode 100644
index 00000000000000..20a62b5855411f
--- /dev/null
+++ b/playground/legacy/__tests__/watch/legacy-styles-only-entry-watch.spec.ts
@@ -0,0 +1,45 @@
+import { expect, test } from 'vitest'
+import {
+ editFile,
+ findAssetFile,
+ isBuild,
+ notifyRebuildComplete,
+ readManifest,
+ watcher,
+} from '~utils'
+
+test.runIf(isBuild)('rebuilds styles only entry on change', async () => {
+ expect(findAssetFile(/style-only-entry-.+\.css/, 'watch')).toContain(
+ '#ff69b4',
+ )
+ expect(findAssetFile(/style-only-entry-legacy-.+\.js/, 'watch')).toContain(
+ '#ff69b4',
+ )
+ expect(findAssetFile(/polyfills-legacy-.+\.js/, 'watch')).toBeTruthy()
+ const numberOfManifestEntries = Object.keys(readManifest('watch')).length
+ expect(numberOfManifestEntries).toBe(3)
+
+ editFile('style-only-entry.css', (originalContents) =>
+ originalContents.replace('#ff69b4', '#ffb6c1'),
+ )
+ await notifyRebuildComplete(watcher)
+
+ const updatedManifest = readManifest('watch')
+ expect(Object.keys(updatedManifest)).toHaveLength(numberOfManifestEntries)
+
+ // We must use the file referenced in the manifest here,
+ // since there'll be different versions of the file with different hashes.
+ const reRenderedCssFile = findAssetFile(
+ updatedManifest['style-only-entry.css']!.file.substring('assets/'.length),
+ 'watch',
+ )
+ expect(reRenderedCssFile).toContain('#ffb6c1')
+ const reRenderedCssLegacyFile = findAssetFile(
+ updatedManifest['style-only-entry-legacy.css']!.file.substring(
+ 'assets/'.length,
+ ),
+ 'watch',
+ )
+ expect(reRenderedCssLegacyFile).toContain('#ffb6c1')
+ expect(findAssetFile(/polyfills-legacy-.+\.js/, 'watch')).toBeTruthy()
+})
diff --git a/packages/playground/legacy/async.js b/playground/legacy/async.js
similarity index 100%
rename from packages/playground/legacy/async.js
rename to playground/legacy/async.js
diff --git a/playground/legacy/custom0.js b/playground/legacy/custom0.js
new file mode 100644
index 00000000000000..21ec276fc7f825
--- /dev/null
+++ b/playground/legacy/custom0.js
@@ -0,0 +1 @@
+export const foo = 'bar'
diff --git a/playground/legacy/custom1.js b/playground/legacy/custom1.js
new file mode 100644
index 00000000000000..21ec276fc7f825
--- /dev/null
+++ b/playground/legacy/custom1.js
@@ -0,0 +1 @@
+export const foo = 'bar'
diff --git a/playground/legacy/custom2.js b/playground/legacy/custom2.js
new file mode 100644
index 00000000000000..21ec276fc7f825
--- /dev/null
+++ b/playground/legacy/custom2.js
@@ -0,0 +1 @@
+export const foo = 'bar'
diff --git a/playground/legacy/dynamic.css b/playground/legacy/dynamic.css
new file mode 100644
index 00000000000000..160ee45a8a850a
--- /dev/null
+++ b/playground/legacy/dynamic.css
@@ -0,0 +1,3 @@
+#dynamic-css {
+ color: red;
+}
diff --git a/playground/legacy/entry-server-sequential.js b/playground/legacy/entry-server-sequential.js
new file mode 100644
index 00000000000000..718dc84b8df6b0
--- /dev/null
+++ b/playground/legacy/entry-server-sequential.js
@@ -0,0 +1,7 @@
+// This counts as 'server-side' rendering, yes?
+export async function render() {
+ return /* html */ `
+ Hello
+ ${import.meta.env.MODE}
+ `
+}
diff --git a/packages/playground/legacy/entry-server.js b/playground/legacy/entry-server.js
similarity index 100%
rename from packages/playground/legacy/entry-server.js
rename to playground/legacy/entry-server.js
diff --git a/playground/legacy/immutable-chunk.js b/playground/legacy/immutable-chunk.js
new file mode 100644
index 00000000000000..f8a06b820eb7b2
--- /dev/null
+++ b/playground/legacy/immutable-chunk.js
@@ -0,0 +1,18 @@
+const chunks = [
+ 'index',
+ 'index-legacy',
+ 'chunk-async',
+ 'chunk-async-legacy',
+ 'immutable-chunk',
+ 'immutable-chunk-legacy',
+ 'polyfills-legacy',
+]
+
+export function fn() {
+ return Promise.all(
+ chunks.map(async (name) => {
+ const response = await fetch(`/assets/${name}.js`)
+ return `${name}: ${response.headers.get('Content-Type')}`
+ }),
+ )
+}
diff --git a/playground/legacy/index.html b/playground/legacy/index.html
new file mode 100644
index 00000000000000..26a22ad0846eea
--- /dev/null
+++ b/playground/legacy/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+dynamic css
+
+## worker message:
+
+
+
diff --git a/playground/legacy/main.js b/playground/legacy/main.js
new file mode 100644
index 00000000000000..e34b5835c82b9d
--- /dev/null
+++ b/playground/legacy/main.js
@@ -0,0 +1,86 @@
+import './style.css'
+import viteSvgPath from './vite.svg'
+import MyWorker from './worker?worker'
+
+async function run() {
+ await import('./custom0.js')
+ await import('./custom1.js')
+ await import('./custom2.js')
+ const { fn } = await import('./async.js')
+ fn()
+}
+
+run()
+
+let isLegacy
+
+// make sure that branching works despite esbuild's constant folding (#1999)
+if (import.meta.env.LEGACY) {
+ if (import.meta.env.LEGACY === true) isLegacy = true
+} else {
+ if (import.meta.env.LEGACY === false) isLegacy = false
+}
+
+text('#env', `is legacy: ${isLegacy}`)
+
+const metaEnvObj = import.meta.env
+text('#env-equal', import.meta.env.LEGACY === metaEnvObj.LEGACY)
+
+// Iterators
+text('#iterators', [...new Set(['hello'])].join(''))
+
+// structuredClone is supported core.js v3.20.0+
+text(
+ '#features-after-corejs-3',
+ JSON.stringify(structuredClone({ foo: 'foo' })),
+)
+
+// async generator
+async function* asyncGenerator() {
+ for (let i = 0; i < 3; i++) {
+ await new Promise((resolve) => setTimeout(resolve, 10))
+ yield i
+ }
+}
+;(async () => {
+ const result = []
+ for await (const i of asyncGenerator()) {
+ result.push(i)
+ }
+ text('#async-generator', JSON.stringify(result))
+})()
+
+// babel-helpers
+// Using `String.raw` to inject `@babel/plugin-transform-template-literals`
+// helpers.
+text(
+ '#babel-helpers',
+ String.raw`exposed babel helpers: ${window._templateObject != null}`,
+)
+
+// dynamic chunk names
+import('./immutable-chunk.js')
+ .then(({ fn }) => fn())
+ .then((assets) => {
+ text('#assets', assets.join('\n'))
+ })
+
+// dynamic css
+document
+ .querySelector('#dynamic-css-button')
+ .addEventListener('click', async () => {
+ await import('./dynamic.css')
+ text('#dynamic-css', 'dynamic import css')
+ })
+
+text('#asset-path', viteSvgPath)
+
+function text(el, text) {
+ document.querySelector(el).textContent = text
+}
+
+const worker = new MyWorker()
+worker.postMessage('ping')
+worker.addEventListener('message', (ev) => {
+ text('.worker-message', JSON.stringify(ev.data))
+})
diff --git a/playground/legacy/module.js b/playground/legacy/module.js
new file mode 100644
index 00000000000000..8080ab24c9a7f6
--- /dev/null
+++ b/playground/legacy/module.js
@@ -0,0 +1 @@
+export const module = 'module'
diff --git a/playground/legacy/nested/index.html b/playground/legacy/nested/index.html
new file mode 100644
index 00000000000000..dd5edae938288c
--- /dev/null
+++ b/playground/legacy/nested/index.html
@@ -0,0 +1,2 @@
+
+
diff --git a/playground/legacy/no-polyfills-no-systemjs.html b/playground/legacy/no-polyfills-no-systemjs.html
new file mode 100644
index 00000000000000..878b86e5027e0c
--- /dev/null
+++ b/playground/legacy/no-polyfills-no-systemjs.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/playground/legacy/no-polyfills-no-systemjs.js b/playground/legacy/no-polyfills-no-systemjs.js
new file mode 100644
index 00000000000000..c806e68363501b
--- /dev/null
+++ b/playground/legacy/no-polyfills-no-systemjs.js
@@ -0,0 +1 @@
+document.querySelector('main').innerHTML = '👋'
diff --git a/playground/legacy/no-polyfills.html b/playground/legacy/no-polyfills.html
new file mode 100644
index 00000000000000..a91e4764055276
--- /dev/null
+++ b/playground/legacy/no-polyfills.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/playground/legacy/no-polyfills.js b/playground/legacy/no-polyfills.js
new file mode 100644
index 00000000000000..c806e68363501b
--- /dev/null
+++ b/playground/legacy/no-polyfills.js
@@ -0,0 +1 @@
+document.querySelector('main').innerHTML = '👋'
diff --git a/playground/legacy/package.json b/playground/legacy/package.json
new file mode 100644
index 00000000000000..7e3c9c9d6415da
--- /dev/null
+++ b/playground/legacy/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@vitejs/test-legacy",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build --debug legacy",
+ "build:custom-filename": "vite --config ./vite.config-custom-filename.js build --debug legacy",
+ "build:multiple-output": "vite --config ./vite.config-multiple-output.js build",
+ "build:no-polyfills": "vite --config ./vite.config-no-polyfills.js build",
+ "build:no-polyfills-no-systemjs": "vite --config ./vite.config-no-polyfills-no-systemjs.js build",
+ "build:watch": "vite --config ./vite.config-watch.js build --debug legacy",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "vite": "workspace:*",
+ "@vitejs/plugin-legacy": "workspace:*",
+ "express": "^5.1.0",
+ "terser": "^5.39.0"
+ }
+}
diff --git a/playground/legacy/style-only-entry.css b/playground/legacy/style-only-entry.css
new file mode 100644
index 00000000000000..2212f7a84ad32f
--- /dev/null
+++ b/playground/legacy/style-only-entry.css
@@ -0,0 +1,3 @@
+:root {
+ background: #ff69b4;
+}
diff --git a/packages/playground/legacy/style.css b/playground/legacy/style.css
similarity index 100%
rename from packages/playground/legacy/style.css
rename to playground/legacy/style.css
diff --git a/playground/legacy/vite.config-custom-filename.js b/playground/legacy/vite.config-custom-filename.js
new file mode 100644
index 00000000000000..a4d14c8415d8fd
--- /dev/null
+++ b/playground/legacy/vite.config-custom-filename.js
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite'
+import legacy from '@vitejs/plugin-legacy'
+
+export default defineConfig({
+ plugins: [legacy({ modernPolyfills: true })],
+ build: {
+ manifest: true,
+ minify: false,
+ rollupOptions: {
+ output: {
+ entryFileNames: `assets/[name].js`,
+ chunkFileNames: `assets/[name].js`,
+ },
+ },
+ },
+})
diff --git a/playground/legacy/vite.config-multiple-output.js b/playground/legacy/vite.config-multiple-output.js
new file mode 100644
index 00000000000000..ae4d7d530adc8e
--- /dev/null
+++ b/playground/legacy/vite.config-multiple-output.js
@@ -0,0 +1,28 @@
+import { defineConfig } from 'vite'
+import legacy from '@vitejs/plugin-legacy'
+
+export default defineConfig({
+ plugins: [legacy({ modernPolyfills: true })],
+ build: {
+ manifest: true,
+ minify: false,
+ rollupOptions: {
+ output: [
+ {
+ assetFileNames() {
+ return 'assets/subdir/[name]-[hash][extname]'
+ },
+ entryFileNames: `assets/subdir/[name].js`,
+ chunkFileNames: `assets/subdir/[name].js`,
+ },
+ {
+ assetFileNames() {
+ return 'assets/subdir/[name]-[hash][extname]'
+ },
+ entryFileNames: `assets/anotherSubdir/[name].js`,
+ chunkFileNames: `assets/anotherSubdir/[name].js`,
+ },
+ ],
+ },
+ },
+})
diff --git a/playground/legacy/vite.config-no-polyfills-no-systemjs.js b/playground/legacy/vite.config-no-polyfills-no-systemjs.js
new file mode 100644
index 00000000000000..388ee5a3ba71a4
--- /dev/null
+++ b/playground/legacy/vite.config-no-polyfills-no-systemjs.js
@@ -0,0 +1,28 @@
+import path from 'node:path'
+import legacy from '@vitejs/plugin-legacy'
+import { defineConfig } from 'vite'
+
+export default defineConfig(({ isPreview }) => ({
+ base: !isPreview ? './' : '/no-polyfills-no-systemjs/',
+ plugins: [
+ legacy({
+ renderModernChunks: false,
+ polyfills: false,
+ externalSystemJS: true,
+ }),
+ {
+ name: 'remove crossorigin attribute',
+ transformIndexHtml: (html) => html.replaceAll('crossorigin', ''),
+ enforce: 'post',
+ },
+ ],
+
+ build: {
+ outDir: 'dist/no-polyfills-no-systemjs',
+ rollupOptions: {
+ input: {
+ index: path.resolve(__dirname, 'no-polyfills-no-systemjs.html'),
+ },
+ },
+ },
+}))
diff --git a/playground/legacy/vite.config-no-polyfills.js b/playground/legacy/vite.config-no-polyfills.js
new file mode 100644
index 00000000000000..a33a34aa20e556
--- /dev/null
+++ b/playground/legacy/vite.config-no-polyfills.js
@@ -0,0 +1,27 @@
+import path from 'node:path'
+import legacy from '@vitejs/plugin-legacy'
+import { defineConfig } from 'vite'
+
+export default defineConfig(({ isPreview }) => ({
+ base: !isPreview ? './' : '/no-polyfills/',
+ plugins: [
+ legacy({
+ renderModernChunks: false,
+ polyfills: false,
+ }),
+ {
+ name: 'remove crossorigin attribute',
+ transformIndexHtml: (html) => html.replaceAll('crossorigin', ''),
+ enforce: 'post',
+ },
+ ],
+
+ build: {
+ outDir: 'dist/no-polyfills',
+ rollupOptions: {
+ input: {
+ index: path.resolve(__dirname, 'no-polyfills.html'),
+ },
+ },
+ },
+}))
diff --git a/playground/legacy/vite.config-watch.js b/playground/legacy/vite.config-watch.js
new file mode 100644
index 00000000000000..48d57f3e988edc
--- /dev/null
+++ b/playground/legacy/vite.config-watch.js
@@ -0,0 +1,17 @@
+import { resolve } from 'node:path'
+import legacy from '@vitejs/plugin-legacy'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ plugins: [legacy()],
+ build: {
+ manifest: true,
+ rollupOptions: {
+ input: {
+ 'style-only-entry': resolve(__dirname, 'style-only-entry.css'),
+ },
+ },
+ watch: {},
+ outDir: 'dist/watch',
+ },
+})
diff --git a/playground/legacy/vite.config.js b/playground/legacy/vite.config.js
new file mode 100644
index 00000000000000..d07d295a0a27d8
--- /dev/null
+++ b/playground/legacy/vite.config.js
@@ -0,0 +1,50 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import legacy from '@vitejs/plugin-legacy'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ base: './',
+ plugins: [
+ legacy({
+ targets: 'IE 11',
+ modernPolyfills: true,
+ }),
+ ],
+
+ build: {
+ cssCodeSplit: false,
+ manifest: true,
+ sourcemap: true,
+ assetsInlineLimit: 100, // keep SVG as assets URL
+ rollupOptions: {
+ input: {
+ index: path.resolve(__dirname, 'index.html'),
+ nested: path.resolve(__dirname, 'nested/index.html'),
+ },
+ output: {
+ chunkFileNames(chunkInfo) {
+ if (chunkInfo.name === 'immutable-chunk') {
+ return `assets/${chunkInfo.name}.js`
+ } else if (/custom\d/.test(chunkInfo.name)) {
+ return `assets/chunk-X${
+ ['.', '-', ''][/custom(\d)/.exec(chunkInfo.name)[1]]
+ }[hash].js`
+ }
+ return `assets/chunk-[name].[hash].js`
+ },
+ },
+ },
+ },
+
+ // for tests, remove `
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/playground/lib/index.html b/playground/lib/index.html
similarity index 100%
rename from packages/playground/lib/index.html
rename to playground/lib/index.html
diff --git a/playground/lib/package.json b/playground/lib/package.json
new file mode 100644
index 00000000000000..0db97d117e49be
--- /dev/null
+++ b/playground/lib/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-my-lib",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "sirv": "^3.0.1"
+ }
+}
diff --git a/playground/lib/src/css-entry-1.js b/playground/lib/src/css-entry-1.js
new file mode 100644
index 00000000000000..b94766d8902a9e
--- /dev/null
+++ b/playground/lib/src/css-entry-1.js
@@ -0,0 +1,3 @@
+import './entry-1.css'
+
+export default 'css-entry-1'
diff --git a/playground/lib/src/css-entry-2.js b/playground/lib/src/css-entry-2.js
new file mode 100644
index 00000000000000..fdb7d8ae56e453
--- /dev/null
+++ b/playground/lib/src/css-entry-2.js
@@ -0,0 +1,3 @@
+import './entry-2.css'
+
+export default 'css-entry-2'
diff --git a/playground/lib/src/dynamic.css b/playground/lib/src/dynamic.css
new file mode 100644
index 00000000000000..4378c8d328cfbe
--- /dev/null
+++ b/playground/lib/src/dynamic.css
@@ -0,0 +1,4 @@
+@import 'https://cdn.jsdelivr.net/npm/@mdi/font@5.9.55/css/materialdesignicons.min.css';
+.dynamic {
+ color: red;
+}
diff --git a/playground/lib/src/entry-1.css b/playground/lib/src/entry-1.css
new file mode 100644
index 00000000000000..2587b2057f1013
--- /dev/null
+++ b/playground/lib/src/entry-1.css
@@ -0,0 +1,3 @@
+h1 {
+ content: 'entry-1.css';
+}
diff --git a/playground/lib/src/entry-2.css b/playground/lib/src/entry-2.css
new file mode 100644
index 00000000000000..e2c1e61158a22f
--- /dev/null
+++ b/playground/lib/src/entry-2.css
@@ -0,0 +1,3 @@
+h2 {
+ content: 'entry-2.css';
+}
diff --git a/playground/lib/src/index.css b/playground/lib/src/index.css
new file mode 100644
index 00000000000000..b0bd670bd9ecad
--- /dev/null
+++ b/playground/lib/src/index.css
@@ -0,0 +1,3 @@
+.index {
+ color: blue;
+}
diff --git a/playground/lib/src/main-helpers-injection.js b/playground/lib/src/main-helpers-injection.js
new file mode 100644
index 00000000000000..5a7fce534d2db9
--- /dev/null
+++ b/playground/lib/src/main-helpers-injection.js
@@ -0,0 +1,11 @@
+// Trigger wrong helpers injection if not properly constrained
+;(function () {
+ var getEvalledConstructor = function (expressionSyntax) {
+ try {
+ return $Function(
+ '"use strict"; return (' + expressionSyntax + ').constructor;',
+ )()
+ } catch (e) {}
+ }
+ console.log(getEvalledConstructor(0))
+})()
diff --git a/playground/lib/src/main-multiple-output.js b/playground/lib/src/main-multiple-output.js
new file mode 100644
index 00000000000000..78e22283ccbc1b
--- /dev/null
+++ b/playground/lib/src/main-multiple-output.js
@@ -0,0 +1,6 @@
+// import file to test css build handling
+import './index.css'
+
+export default async function message(sel) {
+ document.querySelector(sel).textContent = 'success'
+}
diff --git a/playground/lib/src/main-named.js b/playground/lib/src/main-named.js
new file mode 100644
index 00000000000000..238bedb07c3507
--- /dev/null
+++ b/playground/lib/src/main-named.js
@@ -0,0 +1,4 @@
+export const foo = 'foo'
+
+// Force esbuild spread helpers
+console.log({ ...foo })
diff --git a/playground/lib/src/main.js b/playground/lib/src/main.js
new file mode 100644
index 00000000000000..8be8ec37e635ee
--- /dev/null
+++ b/playground/lib/src/main.js
@@ -0,0 +1,15 @@
+export default function myLib(sel) {
+ // Force esbuild spread helpers (https://github.com/evanw/esbuild/issues/951)
+ console.log({ ...'foo' })
+
+ document.querySelector(sel).textContent = 'It works'
+
+ // Env vars should not be replaced
+ console.log(process.env.NODE_ENV)
+
+ // make sure umd helper has been moved to the right position
+ console.log(`amd function(){ "use strict"; }`)
+}
+
+// For triggering unhandled global esbuild helpers in previous regex-based implementation for injection
+;(function () {})()?.foo
diff --git a/playground/lib/src/main2.js b/playground/lib/src/main2.js
new file mode 100644
index 00000000000000..f19a16bb128949
--- /dev/null
+++ b/playground/lib/src/main2.js
@@ -0,0 +1,9 @@
+import './index.css'
+
+export default async function message(sel) {
+ const message = await import('./message.js')
+
+ await import('./dynamic.css')
+
+ document.querySelector(sel).textContent = message.default
+}
diff --git a/packages/playground/lib/src/message.js b/playground/lib/src/message.js
similarity index 100%
rename from packages/playground/lib/src/message.js
rename to playground/lib/src/message.js
diff --git a/playground/lib/src/sub-multiple-output.js b/playground/lib/src/sub-multiple-output.js
new file mode 100644
index 00000000000000..78e22283ccbc1b
--- /dev/null
+++ b/playground/lib/src/sub-multiple-output.js
@@ -0,0 +1,6 @@
+// import file to test css build handling
+import './index.css'
+
+export default async function message(sel) {
+ document.querySelector(sel).textContent = 'success'
+}
diff --git a/playground/lib/vite.config.js b/playground/lib/vite.config.js
new file mode 100644
index 00000000000000..84612ba1f65306
--- /dev/null
+++ b/playground/lib/vite.config.js
@@ -0,0 +1,41 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ esbuild: {
+ supported: {
+ // Force esbuild inject helpers to test regex
+ 'object-rest-spread': false,
+ 'optional-chain': false,
+ },
+ },
+ build: {
+ rollupOptions: {
+ output: {
+ banner: `/*!\nMayLib\n*/`,
+ },
+ },
+ lib: {
+ entry: path.resolve(__dirname, 'src/main.js'),
+ name: 'MyLib',
+ formats: ['es', 'umd', 'iife'],
+ fileName: 'my-lib-custom-filename',
+ },
+ },
+ plugins: [
+ {
+ name: 'emit-index',
+ generateBundle() {
+ this.emitFile({
+ type: 'asset',
+ fileName: 'index.html',
+ source: fs.readFileSync(
+ path.resolve(__dirname, 'index.dist.html'),
+ 'utf-8',
+ ),
+ })
+ },
+ },
+ ],
+})
diff --git a/playground/lib/vite.css-code-split.config.js b/playground/lib/vite.css-code-split.config.js
new file mode 100644
index 00000000000000..140d6358abba47
--- /dev/null
+++ b/playground/lib/vite.css-code-split.config.js
@@ -0,0 +1,16 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ cssCodeSplit: true,
+ lib: {
+ entry: [
+ fileURLToPath(new URL('src/css-entry-1.js', import.meta.url)),
+ fileURLToPath(new URL('src/css-entry-2.js', import.meta.url)),
+ ],
+ name: 'css-code-split',
+ },
+ outDir: 'dist/css-code-split',
+ },
+})
diff --git a/playground/lib/vite.css-multi-entry.config.js b/playground/lib/vite.css-multi-entry.config.js
new file mode 100644
index 00000000000000..9296799268ea60
--- /dev/null
+++ b/playground/lib/vite.css-multi-entry.config.js
@@ -0,0 +1,15 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ lib: {
+ entry: [
+ fileURLToPath(new URL('src/css-entry-1.js', import.meta.url)),
+ fileURLToPath(new URL('src/css-entry-2.js', import.meta.url)),
+ ],
+ name: 'css-multi-entry',
+ },
+ outDir: 'dist/css-multi-entry',
+ },
+})
diff --git a/playground/lib/vite.css-single-entry.config.js b/playground/lib/vite.css-single-entry.config.js
new file mode 100644
index 00000000000000..7e16cfe42205a4
--- /dev/null
+++ b/playground/lib/vite.css-single-entry.config.js
@@ -0,0 +1,12 @@
+import { fileURLToPath } from 'node:url'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ lib: {
+ entry: fileURLToPath(new URL('src/css-entry-1.js', import.meta.url)),
+ name: 'css-single-entry',
+ },
+ outDir: 'dist/css-single-entry',
+ },
+})
diff --git a/playground/lib/vite.dyimport.config.js b/playground/lib/vite.dyimport.config.js
new file mode 100644
index 00000000000000..dcd9588b555896
--- /dev/null
+++ b/playground/lib/vite.dyimport.config.js
@@ -0,0 +1,15 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ lib: {
+ entry: path.resolve(__dirname, 'src/main2.js'),
+ formats: ['es', 'iife'],
+ name: 'message',
+ fileName: (format) => `dynamic-import-message.${format}.mjs`,
+ },
+ outDir: 'dist/lib',
+ },
+ cacheDir: 'node_modules/.vite-dyimport',
+})
diff --git a/playground/lib/vite.helpers-injection.config.js b/playground/lib/vite.helpers-injection.config.js
new file mode 100644
index 00000000000000..540134d30d57b5
--- /dev/null
+++ b/playground/lib/vite.helpers-injection.config.js
@@ -0,0 +1,19 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+// Check that helpers injection is properly constrained
+
+export default defineConfig({
+ build: {
+ lib: {
+ entry: path.resolve(__dirname, 'src/main-helpers-injection.js'),
+ name: 'MyLib',
+ formats: ['iife'],
+ fileName: 'my-lib-custom-filename',
+ },
+ minify: false,
+ outDir: 'dist/helpers-injection',
+ },
+ plugins: [],
+ cacheDir: 'node_modules/.vite-helpers-injection',
+})
diff --git a/playground/lib/vite.multiple-output.config.js b/playground/lib/vite.multiple-output.config.js
new file mode 100644
index 00000000000000..e986221ad6ca9b
--- /dev/null
+++ b/playground/lib/vite.multiple-output.config.js
@@ -0,0 +1,39 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+const root = process.env.VITEST
+ ? path.resolve(__dirname, '../../playground-temp/lib')
+ : __dirname
+
+export default defineConfig({
+ build: {
+ lib: {
+ // set multiple entrypoint to trigger css chunking
+ entry: {
+ main: path.resolve(__dirname, 'src/main-multiple-output.js'),
+ sub: path.resolve(__dirname, 'src/sub-multiple-output.js'),
+ },
+ name: 'MyLib',
+ },
+ outDir: 'dist/multiple-output',
+ rollupOptions: {
+ // due to playground-temp, the `dir` needs to be relative to the resolvedRoot
+ output: [
+ {
+ dir: path.resolve(root, 'dist/multiple-output/es'),
+ format: 'es',
+ entryFileNames: 'index.mjs',
+ assetFileNames: 'assets/mylib.css',
+ },
+ {
+ dir: path.resolve(root, 'dist/multiple-output/cjs'),
+ format: 'cjs',
+ entryFileNames: 'index.cjs',
+ assetFileNames: 'assets/mylib.css',
+ },
+ ],
+ },
+ cssCodeSplit: true,
+ },
+ cacheDir: 'node_modules/.vite-multiple-output',
+})
diff --git a/playground/lib/vite.named-exports.config.js b/playground/lib/vite.named-exports.config.js
new file mode 100644
index 00000000000000..5c2f3bdb7f3db9
--- /dev/null
+++ b/playground/lib/vite.named-exports.config.js
@@ -0,0 +1,20 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ esbuild: {
+ supported: {
+ // Force esbuild inject helpers to test regex
+ 'object-rest-spread': false,
+ },
+ },
+ build: {
+ lib: {
+ entry: path.resolve(__dirname, 'src/main-named.js'),
+ name: 'MyLibNamed',
+ formats: ['umd', 'iife'],
+ fileName: 'my-lib-named',
+ },
+ outDir: 'dist/named',
+ },
+})
diff --git a/playground/lib/vite.nominify.config.js b/playground/lib/vite.nominify.config.js
new file mode 100644
index 00000000000000..9090ff9226e53c
--- /dev/null
+++ b/playground/lib/vite.nominify.config.js
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite'
+import baseConfig from './vite.config'
+
+export default defineConfig({
+ ...baseConfig,
+ build: {
+ ...baseConfig.build,
+ minify: false,
+ outDir: 'dist/nominify',
+ },
+ plugins: [],
+ cacheDir: 'node_modules/.vite-nominify',
+})
diff --git a/playground/minify/__tests__/minify.spec.ts b/playground/minify/__tests__/minify.spec.ts
new file mode 100644
index 00000000000000..7b672d21134257
--- /dev/null
+++ b/playground/minify/__tests__/minify.spec.ts
@@ -0,0 +1,21 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { expect, test } from 'vitest'
+import { isBuild, readFile, testDir } from '~utils'
+
+test.runIf(isBuild)('no minifySyntax', () => {
+ const assetsDir = path.resolve(testDir, 'dist/assets')
+ const files = fs.readdirSync(assetsDir)
+
+ const jsFile = files.find((f) => f.endsWith('.js'))
+ const jsContent = readFile(path.resolve(assetsDir, jsFile))
+
+ const cssFile = files.find((f) => f.endsWith('.css'))
+ const cssContent = readFile(path.resolve(assetsDir, cssFile))
+
+ expect(jsContent).toContain('{console.log("hello world")}')
+ expect(jsContent).not.toContain('/*! explicit comment */')
+
+ expect(cssContent).toContain('color:#ff0000')
+ expect(cssContent).not.toContain('/*! explicit comment */')
+})
diff --git a/playground/minify/dir/module/index.css b/playground/minify/dir/module/index.css
new file mode 100644
index 00000000000000..16b551d967397c
--- /dev/null
+++ b/playground/minify/dir/module/index.css
@@ -0,0 +1,4 @@
+/*! explicit comment */
+h2 {
+ color: #ff00ff;
+}
diff --git a/playground/minify/dir/module/index.js b/playground/minify/dir/module/index.js
new file mode 100644
index 00000000000000..e76258a228a47c
--- /dev/null
+++ b/playground/minify/dir/module/index.js
@@ -0,0 +1,2 @@
+/*! explicit comment */
+export const msg = `[success] minified module`
diff --git a/playground/minify/dir/module/package.json b/playground/minify/dir/module/package.json
new file mode 100644
index 00000000000000..037e7534a446e4
--- /dev/null
+++ b/playground/minify/dir/module/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-minify",
+ "private": true,
+ "type": "module",
+ "version": "0.0.0"
+}
diff --git a/playground/minify/index.html b/playground/minify/index.html
new file mode 100644
index 00000000000000..1b599018cd92b6
--- /dev/null
+++ b/playground/minify/index.html
@@ -0,0 +1,3 @@
+Minify
+
+
diff --git a/playground/minify/main.js b/playground/minify/main.js
new file mode 100644
index 00000000000000..c732f9cab794ac
--- /dev/null
+++ b/playground/minify/main.js
@@ -0,0 +1,8 @@
+import './test.css'
+import { msg } from 'minified-module'
+
+console.log(msg)
+
+if (window) {
+ console.log('hello world')
+}
diff --git a/playground/minify/package.json b/playground/minify/package.json
new file mode 100644
index 00000000000000..9ff696c8900cd8
--- /dev/null
+++ b/playground/minify/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-minify",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "minified-module": "file:./dir/module"
+ }
+}
diff --git a/playground/minify/test.css b/playground/minify/test.css
new file mode 100644
index 00000000000000..23f46306b712bd
--- /dev/null
+++ b/playground/minify/test.css
@@ -0,0 +1,6 @@
+@import 'minified-module/index.css';
+
+h1 {
+ /* do not minify as red text */
+ color: #ff0000;
+}
diff --git a/playground/minify/vite.config.js b/playground/minify/vite.config.js
new file mode 100644
index 00000000000000..018cc196e60707
--- /dev/null
+++ b/playground/minify/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ esbuild: {
+ legalComments: 'none',
+ minifySyntax: false,
+ },
+})
diff --git a/playground/module-graph/__tests__/module-graph.spec.ts b/playground/module-graph/__tests__/module-graph.spec.ts
new file mode 100644
index 00000000000000..20492e968c674f
--- /dev/null
+++ b/playground/module-graph/__tests__/module-graph.spec.ts
@@ -0,0 +1,12 @@
+import { expect, test } from 'vitest'
+import { isServe, page, viteServer } from '~utils'
+
+test.runIf(isServe)('importedUrls order is preserved', async () => {
+ const el = page.locator('.imported-urls-order')
+ expect(await el.textContent()).toBe('[success]')
+ const mod = await viteServer.environments.client.moduleGraph.getModuleByUrl(
+ '/imported-urls-order.js',
+ )
+ const importedModuleIds = [...mod.importedModules].map((m) => m.url)
+ expect(importedModuleIds).toEqual(['\x00virtual:slow-module', '/empty.js'])
+})
diff --git a/playground/module-graph/empty.js b/playground/module-graph/empty.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/module-graph/imported-urls-order.js b/playground/module-graph/imported-urls-order.js
new file mode 100644
index 00000000000000..9ccf4527e6665d
--- /dev/null
+++ b/playground/module-graph/imported-urls-order.js
@@ -0,0 +1,7 @@
+import { msg } from 'virtual:slow-module'
+import './empty.js'
+
+export default msg
+
+// This module tests that the import order is preserved in this module's `importedUrls` property
+// as the imports can be processed in parallel
diff --git a/playground/module-graph/index.html b/playground/module-graph/index.html
new file mode 100644
index 00000000000000..663a7d7ed0066a
--- /dev/null
+++ b/playground/module-graph/index.html
@@ -0,0 +1,10 @@
+
+
+
diff --git a/playground/module-graph/package.json b/playground/module-graph/package.json
new file mode 100644
index 00000000000000..35e0799c262738
--- /dev/null
+++ b/playground/module-graph/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-hmr",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/module-graph/vite.config.ts b/playground/module-graph/vite.config.ts
new file mode 100644
index 00000000000000..53e07ff3bfd483
--- /dev/null
+++ b/playground/module-graph/vite.config.ts
@@ -0,0 +1,23 @@
+import { defineConfig } from 'vite'
+import type { Plugin } from 'vite'
+
+export default defineConfig({
+ plugins: [slowModulePlugin()],
+})
+
+function slowModulePlugin(): Plugin {
+ return {
+ name: 'slow-module',
+ resolveId(id) {
+ if (id === 'virtual:slow-module') {
+ return '\0virtual:slow-module'
+ }
+ },
+ async load(id) {
+ if (id === '\0virtual:slow-module') {
+ await new Promise((resolve) => setTimeout(resolve, 500))
+ return `export const msg = '[success]'`
+ }
+ },
+ }
+}
diff --git a/playground/multiple-entrypoints/__tests__/multiple-entrypoints.spec.ts b/playground/multiple-entrypoints/__tests__/multiple-entrypoints.spec.ts
new file mode 100644
index 00000000000000..653d9b84212e9e
--- /dev/null
+++ b/playground/multiple-entrypoints/__tests__/multiple-entrypoints.spec.ts
@@ -0,0 +1,10 @@
+import { expect, test } from 'vitest'
+import { getColor, page, untilUpdated } from '~utils'
+
+test('should have css applied on second dynamic import', async () => {
+ await untilUpdated(() => page.textContent('.content'), 'Initial')
+ await page.click('.b')
+
+ await untilUpdated(() => page.textContent('.content'), 'Reference')
+ expect(await getColor('.content')).toBe('red')
+})
diff --git a/packages/playground/multiple-entrypoints/deps.json b/playground/multiple-entrypoints/deps.json
similarity index 100%
rename from packages/playground/multiple-entrypoints/deps.json
rename to playground/multiple-entrypoints/deps.json
diff --git a/packages/playground/multiple-entrypoints/dynamic-a.js b/playground/multiple-entrypoints/dynamic-a.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/dynamic-a.js
rename to playground/multiple-entrypoints/dynamic-a.js
diff --git a/packages/playground/multiple-entrypoints/dynamic-b.js b/playground/multiple-entrypoints/dynamic-b.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/dynamic-b.js
rename to playground/multiple-entrypoints/dynamic-b.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a0.js b/playground/multiple-entrypoints/entrypoints/a0.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a0.js
rename to playground/multiple-entrypoints/entrypoints/a0.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a1.js b/playground/multiple-entrypoints/entrypoints/a1.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a1.js
rename to playground/multiple-entrypoints/entrypoints/a1.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a10.js b/playground/multiple-entrypoints/entrypoints/a10.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a10.js
rename to playground/multiple-entrypoints/entrypoints/a10.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a11.js b/playground/multiple-entrypoints/entrypoints/a11.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a11.js
rename to playground/multiple-entrypoints/entrypoints/a11.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a12.js b/playground/multiple-entrypoints/entrypoints/a12.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a12.js
rename to playground/multiple-entrypoints/entrypoints/a12.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a13.js b/playground/multiple-entrypoints/entrypoints/a13.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a13.js
rename to playground/multiple-entrypoints/entrypoints/a13.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a14.js b/playground/multiple-entrypoints/entrypoints/a14.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a14.js
rename to playground/multiple-entrypoints/entrypoints/a14.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a15.js b/playground/multiple-entrypoints/entrypoints/a15.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a15.js
rename to playground/multiple-entrypoints/entrypoints/a15.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a16.js b/playground/multiple-entrypoints/entrypoints/a16.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a16.js
rename to playground/multiple-entrypoints/entrypoints/a16.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a17.js b/playground/multiple-entrypoints/entrypoints/a17.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a17.js
rename to playground/multiple-entrypoints/entrypoints/a17.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a18.js b/playground/multiple-entrypoints/entrypoints/a18.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a18.js
rename to playground/multiple-entrypoints/entrypoints/a18.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a19.js b/playground/multiple-entrypoints/entrypoints/a19.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a19.js
rename to playground/multiple-entrypoints/entrypoints/a19.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a2.js b/playground/multiple-entrypoints/entrypoints/a2.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a2.js
rename to playground/multiple-entrypoints/entrypoints/a2.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a20.js b/playground/multiple-entrypoints/entrypoints/a20.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a20.js
rename to playground/multiple-entrypoints/entrypoints/a20.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a21.js b/playground/multiple-entrypoints/entrypoints/a21.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a21.js
rename to playground/multiple-entrypoints/entrypoints/a21.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a22.js b/playground/multiple-entrypoints/entrypoints/a22.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a22.js
rename to playground/multiple-entrypoints/entrypoints/a22.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a23.js b/playground/multiple-entrypoints/entrypoints/a23.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a23.js
rename to playground/multiple-entrypoints/entrypoints/a23.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a24.js b/playground/multiple-entrypoints/entrypoints/a24.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a24.js
rename to playground/multiple-entrypoints/entrypoints/a24.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a3.js b/playground/multiple-entrypoints/entrypoints/a3.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a3.js
rename to playground/multiple-entrypoints/entrypoints/a3.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a4.js b/playground/multiple-entrypoints/entrypoints/a4.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a4.js
rename to playground/multiple-entrypoints/entrypoints/a4.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a5.js b/playground/multiple-entrypoints/entrypoints/a5.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a5.js
rename to playground/multiple-entrypoints/entrypoints/a5.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a6.js b/playground/multiple-entrypoints/entrypoints/a6.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a6.js
rename to playground/multiple-entrypoints/entrypoints/a6.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a7.js b/playground/multiple-entrypoints/entrypoints/a7.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a7.js
rename to playground/multiple-entrypoints/entrypoints/a7.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a8.js b/playground/multiple-entrypoints/entrypoints/a8.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a8.js
rename to playground/multiple-entrypoints/entrypoints/a8.js
diff --git a/packages/playground/multiple-entrypoints/entrypoints/a9.js b/playground/multiple-entrypoints/entrypoints/a9.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/entrypoints/a9.js
rename to playground/multiple-entrypoints/entrypoints/a9.js
diff --git a/packages/playground/multiple-entrypoints/index.html b/playground/multiple-entrypoints/index.html
similarity index 100%
rename from packages/playground/multiple-entrypoints/index.html
rename to playground/multiple-entrypoints/index.html
diff --git a/packages/playground/multiple-entrypoints/index.js b/playground/multiple-entrypoints/index.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/index.js
rename to playground/multiple-entrypoints/index.js
diff --git a/playground/multiple-entrypoints/package.json b/playground/multiple-entrypoints/package.json
new file mode 100644
index 00000000000000..b28f5764d5d466
--- /dev/null
+++ b/playground/multiple-entrypoints/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-multiple-entrypoints",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "sass": "^1.88.0"
+ }
+}
diff --git a/packages/playground/multiple-entrypoints/reference.js b/playground/multiple-entrypoints/reference.js
similarity index 100%
rename from packages/playground/multiple-entrypoints/reference.js
rename to playground/multiple-entrypoints/reference.js
diff --git a/packages/playground/multiple-entrypoints/reference.scss b/playground/multiple-entrypoints/reference.scss
similarity index 100%
rename from packages/playground/multiple-entrypoints/reference.scss
rename to playground/multiple-entrypoints/reference.scss
diff --git a/playground/multiple-entrypoints/vite.config.js b/playground/multiple-entrypoints/vite.config.js
new file mode 100644
index 00000000000000..3202cebc0ce4aa
--- /dev/null
+++ b/playground/multiple-entrypoints/vite.config.js
@@ -0,0 +1,40 @@
+import { resolve } from 'node:path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ outDir: './dist',
+ emptyOutDir: true,
+ rollupOptions: {
+ preserveEntrySignatures: 'strict',
+ input: {
+ a0: resolve(__dirname, 'entrypoints/a0.js'),
+ a1: resolve(__dirname, 'entrypoints/a1.js'),
+ a2: resolve(__dirname, 'entrypoints/a2.js'),
+ a3: resolve(__dirname, 'entrypoints/a3.js'),
+ a4: resolve(__dirname, 'entrypoints/a4.js'),
+ a5: resolve(__dirname, 'entrypoints/a5.js'),
+ a6: resolve(__dirname, 'entrypoints/a6.js'),
+ a7: resolve(__dirname, 'entrypoints/a7.js'),
+ a8: resolve(__dirname, 'entrypoints/a8.js'),
+ a9: resolve(__dirname, 'entrypoints/a9.js'),
+ a10: resolve(__dirname, 'entrypoints/a10.js'),
+ a11: resolve(__dirname, 'entrypoints/a11.js'),
+ a12: resolve(__dirname, 'entrypoints/a12.js'),
+ a13: resolve(__dirname, 'entrypoints/a13.js'),
+ a14: resolve(__dirname, 'entrypoints/a14.js'),
+ a15: resolve(__dirname, 'entrypoints/a15.js'),
+ a16: resolve(__dirname, 'entrypoints/a16.js'),
+ a17: resolve(__dirname, 'entrypoints/a17.js'),
+ a18: resolve(__dirname, 'entrypoints/a18.js'),
+ a19: resolve(__dirname, 'entrypoints/a19.js'),
+ a20: resolve(__dirname, 'entrypoints/a20.js'),
+ a21: resolve(__dirname, 'entrypoints/a21.js'),
+ a22: resolve(__dirname, 'entrypoints/a22.js'),
+ a23: resolve(__dirname, 'entrypoints/a23.js'),
+ a24: resolve(__dirname, 'entrypoints/a24.js'),
+ index: resolve(__dirname, './index.html'),
+ },
+ },
+ },
+})
diff --git a/playground/nested-deps/__tests__/nested-deps.spec.ts b/playground/nested-deps/__tests__/nested-deps.spec.ts
new file mode 100644
index 00000000000000..ebdc413ae54b55
--- /dev/null
+++ b/playground/nested-deps/__tests__/nested-deps.spec.ts
@@ -0,0 +1,17 @@
+import { expect, test } from 'vitest'
+import { page } from '~utils'
+
+test('handle nested package', async () => {
+ expect(await page.textContent('.a')).toBe('A@2.0.0')
+ expect(await page.textContent('.b')).toBe('B@1.0.0')
+ expect(await page.textContent('.nested-a')).toBe('A@1.0.0')
+ const c = await page.textContent('.c')
+ expect(c).toBe('es-C@1.0.0')
+ expect(await page.textContent('.side-c')).toBe(c)
+ expect(await page.textContent('.d')).toBe('D@1.0.0')
+ expect(await page.textContent('.nested-d')).toBe('D-nested@1.0.0')
+ expect(await page.textContent('.nested-e')).toBe('1')
+
+ expect(await page.textContent('.absolute-f')).toBe('F@2.0.0')
+ expect(await page.textContent('.self-referencing')).toBe('true')
+})
diff --git a/playground/nested-deps/index.html b/playground/nested-deps/index.html
new file mode 100644
index 00000000000000..21948b1d0c10e0
--- /dev/null
+++ b/playground/nested-deps/index.html
@@ -0,0 +1,59 @@
+direct dependency A
+
+
+direct dependency B
+
+
+nested dependency A
+
+
+direct dependency C
+
+
+side dependency C
+
+
+direct dependency D
+
+
+nested dependency nested-D (dep of D)
+
+
+exclude dependency of pre-bundled dependency
+nested module instance count:
+
+absolute dependency path:
+
+self referencing
+
+
+
diff --git a/playground/nested-deps/package.json b/playground/nested-deps/package.json
new file mode 100644
index 00000000000000..b01ec81792bdb4
--- /dev/null
+++ b/playground/nested-deps/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@vitejs/test-nested-deps",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@vitejs/test-package-a": "link:./test-package-a",
+ "@vitejs/test-package-b": "link:./test-package-b",
+ "@vitejs/test-package-c": "link:./test-package-c",
+ "@vitejs/test-package-d": "link:./test-package-d",
+ "@vitejs/test-package-e": "link:./test-package-e",
+ "@vitejs/test-package-f": "link:./test-package-f",
+ "@vitejs/self-referencing": "link:../self-referencing"
+ }
+}
diff --git a/packages/playground/nested-deps/test-package-a/index.js b/playground/nested-deps/test-package-a/index.js
similarity index 100%
rename from packages/playground/nested-deps/test-package-a/index.js
rename to playground/nested-deps/test-package-a/index.js
diff --git a/playground/nested-deps/test-package-a/package.json b/playground/nested-deps/test-package-a/package.json
new file mode 100644
index 00000000000000..f77fc3ee219989
--- /dev/null
+++ b/playground/nested-deps/test-package-a/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-package-a",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "main": "index.js"
+}
diff --git a/packages/playground/nested-deps/test-package-b/index.js b/playground/nested-deps/test-package-b/index.js
similarity index 100%
rename from packages/playground/nested-deps/test-package-b/index.js
rename to playground/nested-deps/test-package-b/index.js
diff --git a/packages/playground/nested-deps/test-package-b/node_modules/test-package-a/index.js b/playground/nested-deps/test-package-b/node_modules/test-package-a/index.js
similarity index 100%
rename from packages/playground/nested-deps/test-package-b/node_modules/test-package-a/index.js
rename to playground/nested-deps/test-package-b/node_modules/test-package-a/index.js
diff --git a/packages/playground/nested-deps/test-package-b/node_modules/test-package-a/package.json b/playground/nested-deps/test-package-b/node_modules/test-package-a/package.json
similarity index 100%
rename from packages/playground/nested-deps/test-package-b/node_modules/test-package-a/package.json
rename to playground/nested-deps/test-package-b/node_modules/test-package-a/package.json
diff --git a/playground/nested-deps/test-package-b/package.json b/playground/nested-deps/test-package-b/package.json
new file mode 100644
index 00000000000000..a308a70e598102
--- /dev/null
+++ b/playground/nested-deps/test-package-b/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-package-b",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "main": "index.js"
+}
diff --git a/packages/playground/nested-deps/test-package-c/index-es.js b/playground/nested-deps/test-package-c/index-es.js
similarity index 100%
rename from packages/playground/nested-deps/test-package-c/index-es.js
rename to playground/nested-deps/test-package-c/index-es.js
diff --git a/packages/playground/nested-deps/test-package-c/index.js b/playground/nested-deps/test-package-c/index.js
similarity index 100%
rename from packages/playground/nested-deps/test-package-c/index.js
rename to playground/nested-deps/test-package-c/index.js
diff --git a/playground/nested-deps/test-package-c/package.json b/playground/nested-deps/test-package-c/package.json
new file mode 100644
index 00000000000000..72849bfe438d2e
--- /dev/null
+++ b/playground/nested-deps/test-package-c/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@vitejs/test-package-c",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "main": "index.js",
+ "module": "index-es.js"
+}
diff --git a/playground/nested-deps/test-package-c/side.js b/playground/nested-deps/test-package-c/side.js
new file mode 100644
index 00000000000000..037313ec646ad5
--- /dev/null
+++ b/playground/nested-deps/test-package-c/side.js
@@ -0,0 +1 @@
+export { default as C } from '@vitejs/test-package-c'
diff --git a/playground/nested-deps/test-package-d/index.js b/playground/nested-deps/test-package-d/index.js
new file mode 100644
index 00000000000000..0bcf8f7f466eba
--- /dev/null
+++ b/playground/nested-deps/test-package-d/index.js
@@ -0,0 +1,3 @@
+export { default as nestedD } from '@vitejs/test-package-d-nested'
+
+export default 'D@1.0.0'
diff --git a/playground/nested-deps/test-package-d/package.json b/playground/nested-deps/test-package-d/package.json
new file mode 100644
index 00000000000000..0b90182dbc6cd2
--- /dev/null
+++ b/playground/nested-deps/test-package-d/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-package-d",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "main": "index.js",
+ "dependencies": {
+ "@vitejs/test-package-d-nested": "link:./test-package-d-nested"
+ }
+}
diff --git a/packages/playground/nested-deps/test-package-d/test-package-d-nested/index.js b/playground/nested-deps/test-package-d/test-package-d-nested/index.js
similarity index 100%
rename from packages/playground/nested-deps/test-package-d/test-package-d-nested/index.js
rename to playground/nested-deps/test-package-d/test-package-d-nested/index.js
diff --git a/playground/nested-deps/test-package-d/test-package-d-nested/package.json b/playground/nested-deps/test-package-d/test-package-d-nested/package.json
new file mode 100644
index 00000000000000..369654d6a00839
--- /dev/null
+++ b/playground/nested-deps/test-package-d/test-package-d-nested/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-package-d-nested",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.js"
+}
diff --git a/playground/nested-deps/test-package-e/index.js b/playground/nested-deps/test-package-e/index.js
new file mode 100644
index 00000000000000..67bc92858ce469
--- /dev/null
+++ b/playground/nested-deps/test-package-e/index.js
@@ -0,0 +1,2 @@
+export { testIncluded } from '@vitejs/test-package-e-included'
+export { testExcluded } from '@vitejs/test-package-e-excluded'
diff --git a/playground/nested-deps/test-package-e/package.json b/playground/nested-deps/test-package-e/package.json
new file mode 100644
index 00000000000000..02a34294330b6e
--- /dev/null
+++ b/playground/nested-deps/test-package-e/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-package-e",
+ "private": true,
+ "version": "0.1.0",
+ "main": "index.js",
+ "dependencies": {
+ "@vitejs/test-package-e-excluded": "link:./test-package-e-excluded",
+ "@vitejs/test-package-e-included": "link:./test-package-e-included"
+ }
+}
diff --git a/packages/playground/nested-deps/test-package-e/test-package-e-excluded/index.js b/playground/nested-deps/test-package-e/test-package-e-excluded/index.js
similarity index 100%
rename from packages/playground/nested-deps/test-package-e/test-package-e-excluded/index.js
rename to playground/nested-deps/test-package-e/test-package-e-excluded/index.js
diff --git a/playground/nested-deps/test-package-e/test-package-e-excluded/package.json b/playground/nested-deps/test-package-e/test-package-e-excluded/package.json
new file mode 100644
index 00000000000000..c4698332a8c487
--- /dev/null
+++ b/playground/nested-deps/test-package-e/test-package-e-excluded/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-package-e-excluded",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "main": "index.js"
+}
diff --git a/playground/nested-deps/test-package-e/test-package-e-included/index.js b/playground/nested-deps/test-package-e/test-package-e-included/index.js
new file mode 100644
index 00000000000000..d2ff72fa7943fd
--- /dev/null
+++ b/playground/nested-deps/test-package-e/test-package-e-included/index.js
@@ -0,0 +1,5 @@
+import { testExcluded } from '@vitejs/test-package-e-excluded'
+
+export function testIncluded() {
+ return testExcluded()
+}
diff --git a/playground/nested-deps/test-package-e/test-package-e-included/package.json b/playground/nested-deps/test-package-e/test-package-e-included/package.json
new file mode 100644
index 00000000000000..67b8fec37b3014
--- /dev/null
+++ b/playground/nested-deps/test-package-e/test-package-e-included/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-package-e-included",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "main": "index.js",
+ "dependencies": {
+ "@vitejs/test-package-e-excluded": "link:../test-package-e-excluded"
+ }
+}
diff --git a/playground/nested-deps/test-package-f/index.js b/playground/nested-deps/test-package-f/index.js
new file mode 100644
index 00000000000000..f10d4e1f0b81be
--- /dev/null
+++ b/playground/nested-deps/test-package-f/index.js
@@ -0,0 +1 @@
+export default 'F@2.0.0'
diff --git a/playground/nested-deps/test-package-f/package.json b/playground/nested-deps/test-package-f/package.json
new file mode 100644
index 00000000000000..ea51dcbdf20b07
--- /dev/null
+++ b/playground/nested-deps/test-package-f/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-package-f",
+ "private": true,
+ "version": "2.0.0",
+ "type": "module",
+ "main": "index.js"
+}
diff --git a/playground/nested-deps/vite.config.js b/playground/nested-deps/vite.config.js
new file mode 100644
index 00000000000000..f79110856fcb8b
--- /dev/null
+++ b/playground/nested-deps/vite.config.js
@@ -0,0 +1,24 @@
+import path from 'node:path'
+import { defineConfig } from 'vite'
+
+const packageFPath = path.resolve(__dirname, 'test-package-f')
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ __F_ABSOLUTE_PACKAGE_PATH__: packageFPath,
+ },
+ },
+ optimizeDeps: {
+ include: [
+ '@vitejs/test-package-a',
+ '@vitejs/test-package-b',
+ '@vitejs/test-package-c',
+ '@vitejs/test-package-c/side',
+ '@vitejs/test-package-d > @vitejs/test-package-d-nested',
+ '@vitejs/test-package-e > @vitejs/test-package-e-included',
+ '@vitejs/test-package-f',
+ ],
+ exclude: ['@vitejs/test-package-d', '@vitejs/test-package-e-excluded'],
+ },
+})
diff --git a/playground/object-hooks/__tests__/object-hooks.spec.ts b/playground/object-hooks/__tests__/object-hooks.spec.ts
new file mode 100644
index 00000000000000..8e6de2d23025f6
--- /dev/null
+++ b/playground/object-hooks/__tests__/object-hooks.spec.ts
@@ -0,0 +1,6 @@
+import { expect, test } from 'vitest'
+import { page } from '~utils'
+
+test('object hooks', async () => {
+ expect(await page.textContent('#transform')).toMatch('ok')
+})
diff --git a/playground/object-hooks/index.html b/playground/object-hooks/index.html
new file mode 100644
index 00000000000000..008ef32c159a6d
--- /dev/null
+++ b/playground/object-hooks/index.html
@@ -0,0 +1,4 @@
+Transform Hook order
+
+
+
diff --git a/playground/object-hooks/main.ts b/playground/object-hooks/main.ts
new file mode 100644
index 00000000000000..8e4878d209bf9e
--- /dev/null
+++ b/playground/object-hooks/main.ts
@@ -0,0 +1,2 @@
+const app = document.getElementById('transform')
+app.innerText = '__TRANSFORM__'
diff --git a/playground/object-hooks/package.json b/playground/object-hooks/package.json
new file mode 100644
index 00000000000000..684bc1d314078f
--- /dev/null
+++ b/playground/object-hooks/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-object-hooks",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/object-hooks/vite.config.ts b/playground/object-hooks/vite.config.ts
new file mode 100644
index 00000000000000..ac25d69ba85a17
--- /dev/null
+++ b/playground/object-hooks/vite.config.ts
@@ -0,0 +1,69 @@
+/* eslint-disable import-x/no-nodejs-modules */
+import assert from 'assert'
+import { defineConfig } from 'vite'
+
+let count = 0
+export default defineConfig({
+ plugins: [
+ {
+ name: 'plugin1',
+ buildStart: {
+ async handler() {
+ await new Promise((r) => setTimeout(r, 100))
+ count += 1
+ },
+ },
+ transform: {
+ order: 'post',
+ handler(code) {
+ return code.replace('__TRANSFORM3__', 'ok')
+ },
+ },
+ },
+ {
+ name: 'plugin2',
+ buildStart: {
+ sequential: true,
+ async handler() {
+ assert(count === 1)
+ await new Promise((r) => setTimeout(r, 100))
+ count += 1
+ },
+ },
+ transform: {
+ handler(code) {
+ return code.replace('__TRANSFORM1__', '__TRANSFORM2__')
+ },
+ },
+ },
+ {
+ name: 'plugin3',
+ buildStart: {
+ async handler() {
+ assert(count === 2)
+ await new Promise((r) => setTimeout(r, 100))
+ count += 1
+ },
+ },
+ transform: {
+ order: 'pre',
+ handler(code) {
+ return code.replace('__TRANSFORM__', '__TRANSFORM1__')
+ },
+ },
+ },
+ {
+ name: 'plugin4',
+ buildStart: {
+ async handler() {
+ assert(count === 2)
+ },
+ },
+ transform: {
+ handler(code) {
+ return code.replace('__TRANSFORM2__', '__TRANSFORM3__')
+ },
+ },
+ },
+ ],
+})
diff --git a/playground/optimize-deps-no-discovery/__tests__/optimize-deps-no-discovery.spec.ts b/playground/optimize-deps-no-discovery/__tests__/optimize-deps-no-discovery.spec.ts
new file mode 100644
index 00000000000000..19826f82d59a9f
--- /dev/null
+++ b/playground/optimize-deps-no-discovery/__tests__/optimize-deps-no-discovery.spec.ts
@@ -0,0 +1,17 @@
+import { expect, test } from 'vitest'
+import { isBuild, page, readDepOptimizationMetadata } from '~utils'
+
+test('optimized dep', async () => {
+ expect(await page.textContent('.optimized-dep')).toBe('[success]')
+})
+
+test('vue + vuex', async () => {
+ expect(await page.textContent('.vue')).toMatch(`[success]`)
+})
+
+test.runIf(!isBuild)('metadata', async () => {
+ const meta = readDepOptimizationMetadata()
+ expect(Object.keys(meta.optimized)).toContain('@vitejs/test-dep-no-discovery')
+ expect(Object.keys(meta.optimized)).not.toContain('vue')
+ expect(Object.keys(meta.optimized)).not.toContain('vuex')
+})
diff --git a/playground/optimize-deps-no-discovery/dep-no-discovery/index.js b/playground/optimize-deps-no-discovery/dep-no-discovery/index.js
new file mode 100644
index 00000000000000..8096b3eca30104
--- /dev/null
+++ b/playground/optimize-deps-no-discovery/dep-no-discovery/index.js
@@ -0,0 +1,2 @@
+// written in cjs, optimization should convert this to esm
+module.exports = '[success]'
diff --git a/playground/optimize-deps-no-discovery/dep-no-discovery/package.json b/playground/optimize-deps-no-discovery/dep-no-discovery/package.json
new file mode 100644
index 00000000000000..00c68ab6f39275
--- /dev/null
+++ b/playground/optimize-deps-no-discovery/dep-no-discovery/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-no-discovery",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.js"
+}
diff --git a/playground/optimize-deps-no-discovery/index.html b/playground/optimize-deps-no-discovery/index.html
new file mode 100644
index 00000000000000..1d0ccb72106bcb
--- /dev/null
+++ b/playground/optimize-deps-no-discovery/index.html
@@ -0,0 +1,24 @@
+Optimize Deps
+
+Optimized Dep
+
+
+Vue & Vuex
+
+
+
+
+
diff --git a/playground/optimize-deps-no-discovery/package.json b/playground/optimize-deps-no-discovery/package.json
new file mode 100644
index 00000000000000..24b324d19b4637
--- /dev/null
+++ b/playground/optimize-deps-no-discovery/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@vitejs/test-optimize-deps-no-discovery",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@vitejs/test-dep-no-discovery": "file:./dep-no-discovery",
+ "vue": "^3.5.13",
+ "vuex": "^4.1.0"
+ }
+}
diff --git a/playground/optimize-deps-no-discovery/vite.config.js b/playground/optimize-deps-no-discovery/vite.config.js
new file mode 100644
index 00000000000000..445f53fd121b0e
--- /dev/null
+++ b/playground/optimize-deps-no-discovery/vite.config.js
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite'
+
+// Overriding the NODE_ENV set by vitest
+process.env.NODE_ENV = ''
+
+export default defineConfig({
+ optimizeDeps: {
+ noDiscovery: true,
+ include: ['@vitejs/test-dep-no-discovery'],
+ },
+
+ build: {
+ // to make tests faster
+ minify: false,
+ },
+})
diff --git a/packages/playground/optimize-deps/.hidden-dir/foo.js b/playground/optimize-deps/.hidden-dir/foo.js
similarity index 100%
rename from packages/playground/optimize-deps/.hidden-dir/foo.js
rename to playground/optimize-deps/.hidden-dir/foo.js
diff --git a/playground/optimize-deps/__tests__/optimize-deps.spec.ts b/playground/optimize-deps/__tests__/optimize-deps.spec.ts
new file mode 100644
index 00000000000000..f9f34d50bcbb45
--- /dev/null
+++ b/playground/optimize-deps/__tests__/optimize-deps.spec.ts
@@ -0,0 +1,372 @@
+import { describe, expect, test } from 'vitest'
+import {
+ browserErrors,
+ browserLogs,
+ expectWithRetry,
+ getColor,
+ isBuild,
+ isServe,
+ page,
+ readDepOptimizationMetadata,
+ serverLogs,
+ viteTestUrl,
+} from '~utils'
+
+test('default + named imports from cjs dep (react)', async () => {
+ await expectWithRetry(() => page.textContent('.cjs button')).toBe(
+ 'count is 0',
+ )
+ await page.click('.cjs button')
+ await expectWithRetry(() => page.textContent('.cjs button')).toBe(
+ 'count is 1',
+ )
+})
+
+test('named imports from webpacked cjs (phoenix)', async () => {
+ await expectWithRetry(() => page.textContent('.cjs-phoenix')).toBe('ok')
+})
+
+test('default import from webpacked cjs (clipboard)', async () => {
+ await expectWithRetry(() => page.textContent('.cjs-clipboard')).toBe('ok')
+})
+
+test('default import from cjs (cjs-dep-cjs-compiled-from-esm)', async () => {
+ await expectWithRetry(() =>
+ page.textContent('.cjs-dep-cjs-compiled-from-esm'),
+ ).toBe('ok')
+})
+
+test('default import from cjs (cjs-dep-cjs-compiled-from-cjs)', async () => {
+ await expectWithRetry(() =>
+ page.textContent('.cjs-dep-cjs-compiled-from-cjs'),
+ ).toBe('ok')
+})
+
+test('dynamic imports from cjs dep (react)', async () => {
+ await expectWithRetry(() => page.textContent('.cjs-dynamic button')).toBe(
+ 'count is 0',
+ )
+ await page.click('.cjs-dynamic button')
+ await expectWithRetry(() => page.textContent('.cjs-dynamic button')).toBe(
+ 'count is 1',
+ )
+})
+
+test('dynamic named imports from webpacked cjs (phoenix)', async () => {
+ await expectWithRetry(() => page.textContent('.cjs-dynamic-phoenix')).toBe(
+ 'ok',
+ )
+})
+
+test('dynamic default import from webpacked cjs (clipboard)', async () => {
+ await expectWithRetry(() => page.textContent('.cjs-dynamic-clipboard')).toBe(
+ 'ok',
+ )
+})
+
+test('dynamic default import from cjs (cjs-dynamic-dep-cjs-compiled-from-esm)', async () => {
+ await expectWithRetry(() =>
+ page.textContent('.cjs-dynamic-dep-cjs-compiled-from-esm'),
+ ).toBe('ok')
+})
+
+test('dynamic default import from cjs (cjs-dynamic-dep-cjs-compiled-from-cjs)', async () => {
+ await expectWithRetry(() =>
+ page.textContent('.cjs-dynamic-dep-cjs-compiled-from-cjs'),
+ ).toBe('ok')
+})
+
+test('dedupe', async () => {
+ await expectWithRetry(() => page.textContent('.dedupe button')).toBe(
+ 'count is 0',
+ )
+ await page.click('.dedupe button')
+ await expectWithRetry(() => page.textContent('.dedupe button')).toBe(
+ 'count is 1',
+ )
+})
+
+test('cjs browser field (axios)', async () => {
+ await expectWithRetry(() => page.textContent('.cjs-browser-field')).toBe(
+ 'pong',
+ )
+})
+
+test('cjs browser field bare', async () => {
+ await expectWithRetry(() => page.textContent('.cjs-browser-field-bare')).toBe(
+ 'pong',
+ )
+})
+
+test('dep from linked dep (lodash-es)', async () => {
+ await expectWithRetry(() => page.textContent('.deps-linked')).toBe(
+ 'fooBarBaz',
+ )
+})
+
+test('forced include', async () => {
+ await expectWithRetry(() => page.textContent('.force-include')).toMatch(
+ `[success]`,
+ )
+})
+
+test('import * from optimized dep', async () => {
+ await expectWithRetry(() => page.textContent('.import-star')).toMatch(
+ `[success]`,
+ )
+})
+
+test('import from dep with process.env.NODE_ENV', async () => {
+ await expectWithRetry(() => page.textContent('.node-env')).toMatch(
+ isBuild ? 'prod' : 'dev',
+ )
+})
+
+test('import from dep with .notjs files', async () => {
+ await expectWithRetry(() => page.textContent('.not-js')).toMatch(`[success]`)
+})
+
+test('Import from dependency which uses relative path which needs to be resolved by main field', async () => {
+ await expectWithRetry(() => page.textContent('.relative-to-main')).toMatch(
+ `[success]`,
+ )
+})
+
+test('dep with dynamic import', async () => {
+ await expectWithRetry(() =>
+ page.textContent('.dep-with-dynamic-import'),
+ ).toMatch(`[success]`)
+})
+
+test('dep with optional peer dep', async () => {
+ await expectWithRetry(() =>
+ page.textContent('.dep-with-optional-peer-dep'),
+ ).toMatch(`[success]`)
+ if (isServe) {
+ expect(browserErrors.map((error) => error.message)).toEqual(
+ expect.arrayContaining([
+ 'Could not resolve "foobar" imported by "@vitejs/test-dep-with-optional-peer-dep". Is it installed?',
+ ]),
+ )
+ }
+})
+
+test('dep with optional peer dep submodule', async () => {
+ await expectWithRetry(() =>
+ page.textContent('.dep-with-optional-peer-dep-submodule'),
+ ).toMatch(`[success]`)
+ if (isServe) {
+ expect(browserErrors.map((error) => error.message)).toEqual(
+ expect.arrayContaining([
+ 'Could not resolve "foobar/baz" imported by "@vitejs/test-dep-with-optional-peer-dep-submodule". Is it installed?',
+ ]),
+ )
+ }
+})
+
+test('dep with css import', async () => {
+ await expectWithRetry(() => getColor('.dep-linked-include')).toBe('red')
+})
+
+test('CJS dep with css import', async () => {
+ await expectWithRetry(() => getColor('.cjs-with-assets')).toBe('blue')
+})
+
+test('externalize known non-js files in optimize included dep', async () => {
+ await expectWithRetry(() =>
+ page.textContent('.externalize-known-non-js'),
+ ).toMatch(`[success]`)
+})
+
+test('vue + vuex', async () => {
+ await expectWithRetry(() => page.textContent('.vue')).toMatch(`[success]`)
+})
+
+// When we use the Rollup CommonJS plugin instead of esbuild prebundling,
+// the esbuild plugins won't apply to dependencies
+test.runIf(isServe)('esbuild-plugin', async () => {
+ await expectWithRetry(() => page.textContent('.esbuild-plugin')).toMatch(
+ `Hello from an esbuild plugin`,
+ )
+})
+
+test('import from hidden dir', async () => {
+ await expectWithRetry(() => page.textContent('.hidden-dir')).toBe('hello!')
+})
+
+test('import optimize-excluded package that imports optimized-included package', async () => {
+ await expectWithRetry(() => page.textContent('.nested-include')).toBe(
+ 'nested-include',
+ )
+})
+
+test('import aliased package with colon', async () => {
+ await expectWithRetry(() => page.textContent('.url')).toBe('vite.dev')
+})
+
+test('import aliased package using absolute path', async () => {
+ await expectWithRetry(() =>
+ page.textContent('.alias-using-absolute-path'),
+ ).toBe('From dep-alias-using-absolute-path')
+})
+
+test('variable names are reused in different scripts', async () => {
+ await expectWithRetry(() => page.textContent('.reused-variable-names')).toBe(
+ 'reused',
+ )
+})
+
+test('flatten id should generate correctly', async () => {
+ await expectWithRetry(() => page.textContent('.clonedeep-slash')).toBe(
+ 'clonedeep-slash',
+ )
+ await expectWithRetry(() => page.textContent('.clonedeep-dot')).toBe(
+ 'clonedeep-dot',
+ )
+})
+
+test('non optimized module is not duplicated', async () => {
+ await expectWithRetry(() =>
+ page.textContent('.non-optimized-module-is-not-duplicated'),
+ ).toBe('from-absolute-path, from-relative-path')
+})
+
+test.runIf(isServe)('error on builtin modules usage', () => {
+ expect(browserLogs).toEqual(
+ expect.arrayContaining([
+ // from dep-with-builtin-module-esm
+ expect.stringMatching(/dep-with-builtin-module-esm.*is not a function/),
+ // dep-with-builtin-module-esm warnings
+ expect.stringContaining(
+ 'Module "fs" has been externalized for browser compatibility. Cannot access "fs.readFileSync" in client code.',
+ ),
+ expect.stringContaining(
+ 'Module "path" has been externalized for browser compatibility. Cannot access "path.join" in client code.',
+ ),
+ // from dep-with-builtin-module-cjs
+ expect.stringMatching(/dep-with-builtin-module-cjs.*is not a function/),
+ // dep-with-builtin-module-cjs warnings
+ expect.stringContaining(
+ 'Module "fs" has been externalized for browser compatibility. Cannot access "fs.readFileSync" in client code.',
+ ),
+ expect.stringContaining(
+ 'Module "path" has been externalized for browser compatibility. Cannot access "path.join" in client code.',
+ ),
+ ]),
+ )
+
+ expect(browserErrors.map((error) => error.message)).toEqual(
+ expect.arrayContaining([
+ // from user source code
+ expect.stringContaining(
+ 'Module "buffer" has been externalized for browser compatibility. Cannot access "buffer.Buffer" in client code.',
+ ),
+ expect.stringContaining(
+ 'Module "child_process" has been externalized for browser compatibility. Cannot access "child_process.execSync" in client code.',
+ ),
+ ]),
+ )
+})
+
+test('pre bundle css require', async () => {
+ if (isServe) {
+ const response = page.waitForResponse(/@vitejs_test-dep-css-require\.js/)
+ await page.goto(viteTestUrl)
+ const content = await (await response).text()
+ expect(content).toMatch(
+ /import\s"\/@fs.+@vitejs\/test-dep-css-require\/style\.css"/,
+ )
+ }
+
+ await expectWithRetry(() => getColor('.css-require')).toBe('red')
+ await expectWithRetry(() => getColor('.css-module-require')).toBe('red')
+})
+
+test.runIf(isBuild)('no missing deps during build', async () => {
+ serverLogs.forEach((log) => {
+ // no warning from esbuild css minifier
+ expect(log).not.toMatch('Missing dependency found after crawling ended')
+ })
+})
+
+test('name file limit is 170 characters', async () => {
+ if (isServe) {
+ const response = page.waitForResponse(
+ /@vitejs_longfilename-\w+_[a-zA-Z\d]+\.js\?v=[a-zA-Z\d]+/,
+ )
+ await page.goto(viteTestUrl)
+ const content = await response
+
+ const fromUrl = content.url()
+ const stripFolderPart = fromUrl.split('/').at(-1)
+ const onlyTheFilePart = stripFolderPart.split('.')[0]
+ expect(onlyTheFilePart).toHaveLength(170)
+ }
+})
+
+describe.runIf(isServe)('optimizeDeps config', () => {
+ test('supports include glob syntax', () => {
+ const metadata = readDepOptimizationMetadata()
+ expect(Object.keys(metadata.optimized)).to.include.members([
+ '@vitejs/test-dep-optimize-exports-with-glob',
+ '@vitejs/test-dep-optimize-exports-with-glob/named',
+ '@vitejs/test-dep-optimize-exports-with-glob/glob-dir/foo',
+ '@vitejs/test-dep-optimize-exports-with-glob/glob-dir/bar',
+ '@vitejs/test-dep-optimize-exports-with-glob/glob-dir/nested/baz',
+ '@vitejs/test-dep-optimize-exports-with-root-glob',
+ '@vitejs/test-dep-optimize-exports-with-root-glob/file1.js',
+ '@vitejs/test-dep-optimize-exports-with-root-glob/index.js',
+ '@vitejs/test-dep-optimize-exports-with-root-glob/dir/file2.js',
+ '@vitejs/test-dep-optimize-with-glob',
+ '@vitejs/test-dep-optimize-with-glob/index.js',
+ '@vitejs/test-dep-optimize-with-glob/named.js',
+ '@vitejs/test-dep-optimize-with-glob/glob/foo.js',
+ '@vitejs/test-dep-optimize-with-glob/glob/bar.js',
+ '@vitejs/test-dep-optimize-with-glob/glob/nested/baz.js',
+ ])
+ })
+})
+
+test('long file name should work', async () => {
+ await expectWithRetry(() => page.textContent('.long-file-name')).toMatch(
+ `hello world`,
+ )
+})
+
+test.runIf(isServe)('warn on incompatible dependency', () => {
+ expect(serverLogs).toContainEqual(
+ expect.stringContaining(
+ 'The dependency might be incompatible with the dep optimizer.',
+ ),
+ )
+})
+
+test('import the CommonJS external package that omits the js suffix', async () => {
+ await expectWithRetry(() => page.textContent('.external-package-js')).toBe(
+ 'okay',
+ )
+ await expectWithRetry(() =>
+ page.textContent('.external-package-scss-js'),
+ ).toBe('scss')
+ await expectWithRetry(() =>
+ page.textContent('.external-package-astro-js'),
+ ).toBe('astro')
+ await expectWithRetry(() =>
+ page.textContent('.external-package-tsx-js'),
+ ).toBe('tsx')
+})
+
+test('external package name with asset extension', async () => {
+ await expectWithRetry(() =>
+ page.textContent('.dep-with-asset-ext-no-dual-package'),
+ ).toBe('true')
+ await expectWithRetry(() =>
+ page.textContent('.dep-with-asset-ext-prebundled'),
+ ).toBe(String(isServe))
+})
+
+test('dependency with external sub-dependency', async () => {
+ await expectWithRetry(() =>
+ page.textContent('.dep-cjs-with-external-dep'),
+ ).toBe('ok')
+})
diff --git a/playground/optimize-deps/added-in-entries/index.js b/playground/optimize-deps/added-in-entries/index.js
new file mode 100644
index 00000000000000..f660e74d01dd01
--- /dev/null
+++ b/playground/optimize-deps/added-in-entries/index.js
@@ -0,0 +1,2 @@
+// written in cjs, optimization should convert this to esm
+module.exports = 'added-in-entries'
diff --git a/playground/optimize-deps/added-in-entries/package.json b/playground/optimize-deps/added-in-entries/package.json
new file mode 100644
index 00000000000000..315b8f2ebacf3b
--- /dev/null
+++ b/playground/optimize-deps/added-in-entries/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-added-in-entries",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.js"
+}
diff --git a/playground/optimize-deps/cjs-dynamic.js b/playground/optimize-deps/cjs-dynamic.js
new file mode 100644
index 00000000000000..616dffae4d6c0c
--- /dev/null
+++ b/playground/optimize-deps/cjs-dynamic.js
@@ -0,0 +1,50 @@
+// test dynamic import to cjs deps
+// mostly ensuring consistency between dev server behavior and build behavior
+// of @rollup/plugin-commonjs
+;(async () => {
+ const { useState } = await import('react')
+ const React = (await import('react')).default
+ const ReactDOM = await import('react-dom/client')
+
+ const clip = await import('clipboard')
+ if (typeof clip.default === 'function') {
+ text('.cjs-dynamic-clipboard', 'ok')
+ }
+
+ const { Socket } = await import('phoenix')
+ if (typeof Socket === 'function') {
+ text('.cjs-dynamic-phoenix', 'ok')
+ }
+
+ const cjsFromESM = await import('@vitejs/test-dep-cjs-compiled-from-esm')
+ if (typeof cjsFromESM.default === 'function') {
+ text('.cjs-dynamic-dep-cjs-compiled-from-esm', 'ok')
+ }
+
+ const cjsFromCJS = await import('@vitejs/test-dep-cjs-compiled-from-cjs')
+ if (typeof cjsFromCJS.default === 'function') {
+ text('.cjs-dynamic-dep-cjs-compiled-from-cjs', 'ok')
+ }
+
+ function App() {
+ const [count, setCount] = useState(0)
+
+ return React.createElement(
+ 'button',
+ {
+ onClick() {
+ setCount(count + 1)
+ },
+ },
+ `count is ${count}`,
+ )
+ }
+
+ ReactDOM.createRoot(document.querySelector('.cjs-dynamic')).render(
+ React.createElement(App),
+ )
+
+ function text(el, text) {
+ document.querySelector(el).textContent = text
+ }
+})()
diff --git a/playground/optimize-deps/cjs.js b/playground/optimize-deps/cjs.js
new file mode 100644
index 00000000000000..f0201008647889
--- /dev/null
+++ b/playground/optimize-deps/cjs.js
@@ -0,0 +1,52 @@
+// test importing both default and named exports from a CommonJS module
+// React is the ultimate test of this because its dynamic exports assignments
+// are not statically detectable by @rollup/plugin-commonjs.
+import React, { useState } from 'react'
+import ReactDOM from 'react-dom/client'
+import { Socket } from 'phoenix'
+import clip from 'clipboard'
+import cjsFromESM from '@vitejs/test-dep-cjs-compiled-from-esm'
+import cjsFromCJS from '@vitejs/test-dep-cjs-compiled-from-cjs'
+
+// Test exporting a name that was already imported
+export { useState } from 'react'
+export { useState as anotherNameForUseState } from 'react'
+export { default as React } from 'react'
+
+if (typeof clip === 'function') {
+ text('.cjs-clipboard', 'ok')
+}
+
+if (typeof Socket === 'function') {
+ text('.cjs-phoenix', 'ok')
+}
+
+if (typeof cjsFromESM === 'function') {
+ text('.cjs-dep-cjs-compiled-from-esm', 'ok')
+}
+
+if (typeof cjsFromCJS === 'function') {
+ text('.cjs-dep-cjs-compiled-from-cjs', 'ok')
+}
+
+function App() {
+ const [count, setCount] = useState(0)
+
+ return React.createElement(
+ 'button',
+ {
+ onClick() {
+ setCount(count + 1)
+ },
+ },
+ `count is ${count}`,
+ )
+}
+
+ReactDOM.createRoot(document.querySelector('.cjs')).render(
+ React.createElement(App),
+)
+
+function text(el, text) {
+ document.querySelector(el).textContent = text
+}
diff --git a/playground/optimize-deps/dedupe.js b/playground/optimize-deps/dedupe.js
new file mode 100644
index 00000000000000..b62af42e0415aa
--- /dev/null
+++ b/playground/optimize-deps/dedupe.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+
+// #1302: The linked package has a different version of React in its deps
+// and is itself optimized. Without `dedupe`, the linked package is optimized
+// with a separate copy of React included, and results in runtime errors.
+import { useCount } from '@vitejs/test-dep-linked-include/index.mjs'
+
+function App() {
+ const [count, setCount] = useCount()
+
+ return React.createElement(
+ 'button',
+ {
+ onClick() {
+ setCount(count + 1)
+ },
+ },
+ `count is ${count}`,
+ )
+}
+
+ReactDOM.createRoot(document.querySelector('.dedupe')).render(
+ React.createElement(App),
+)
diff --git a/playground/optimize-deps/dep-alias-using-absolute-path/index.js b/playground/optimize-deps/dep-alias-using-absolute-path/index.js
new file mode 100644
index 00000000000000..82b1f4d1f0874f
--- /dev/null
+++ b/playground/optimize-deps/dep-alias-using-absolute-path/index.js
@@ -0,0 +1,13 @@
+// Importing a shared dependency used by other modules,
+// so dependency optimizer creates a common chunk.
+// This is used to setup a test scenario, where dep scanner
+// could not determine all of the used dependencies on first
+// pass, e.g., a dependency that is aliased using an absolute
+// path, in which case it used to trigger unnecessary "full
+// reloads" invalidating all modules in a module graph.
+const cloneDeep = require('lodash/cloneDeep')
+
+// no-op, using imported module for sake of completeness
+module.exports = cloneDeep({
+ message: 'From dep-alias-using-absolute-path',
+}).message
diff --git a/playground/optimize-deps/dep-alias-using-absolute-path/package.json b/playground/optimize-deps/dep-alias-using-absolute-path/package.json
new file mode 100644
index 00000000000000..3a43490c40ba48
--- /dev/null
+++ b/playground/optimize-deps/dep-alias-using-absolute-path/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-dep-alias-using-absolute-path",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.js",
+ "dependencies": {
+ "lodash": "^4.17.21"
+ }
+}
diff --git a/playground/optimize-deps/dep-cjs-browser-field-bare/events-shim.js b/playground/optimize-deps/dep-cjs-browser-field-bare/events-shim.js
new file mode 100644
index 00000000000000..964e815b7a7bcf
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-browser-field-bare/events-shim.js
@@ -0,0 +1,3 @@
+module.exports = {
+ foo: 'foo',
+}
diff --git a/playground/optimize-deps/dep-cjs-browser-field-bare/index.js b/playground/optimize-deps/dep-cjs-browser-field-bare/index.js
new file mode 100644
index 00000000000000..5ea984fdc8a39c
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-browser-field-bare/index.js
@@ -0,0 +1,5 @@
+'use strict'
+
+const internal = require('./internal')
+
+module.exports = internal
diff --git a/playground/optimize-deps/dep-cjs-browser-field-bare/internal.js b/playground/optimize-deps/dep-cjs-browser-field-bare/internal.js
new file mode 100644
index 00000000000000..5bd55071bfcc48
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-browser-field-bare/internal.js
@@ -0,0 +1,6 @@
+'use strict'
+
+// eslint-disable-next-line import-x/no-nodejs-modules
+const events = require('events')
+
+module.exports = 'foo' in events ? 'pong' : ''
diff --git a/playground/optimize-deps/dep-cjs-browser-field-bare/package.json b/playground/optimize-deps/dep-cjs-browser-field-bare/package.json
new file mode 100644
index 00000000000000..d78987e86ed2af
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-browser-field-bare/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-dep-cjs-browser-field-bare",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js",
+ "browser": {
+ "events": "./events-shim.js"
+ }
+}
diff --git a/packages/playground/optimize-deps/dep-cjs-compiled-from-cjs/index.js b/playground/optimize-deps/dep-cjs-compiled-from-cjs/index.js
similarity index 100%
rename from packages/playground/optimize-deps/dep-cjs-compiled-from-cjs/index.js
rename to playground/optimize-deps/dep-cjs-compiled-from-cjs/index.js
diff --git a/playground/optimize-deps/dep-cjs-compiled-from-cjs/package.json b/playground/optimize-deps/dep-cjs-compiled-from-cjs/package.json
new file mode 100644
index 00000000000000..19a9800c5de17c
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-compiled-from-cjs/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-cjs-compiled-from-cjs",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js"
+}
diff --git a/packages/playground/optimize-deps/dep-cjs-compiled-from-esm/index.js b/playground/optimize-deps/dep-cjs-compiled-from-esm/index.js
similarity index 100%
rename from packages/playground/optimize-deps/dep-cjs-compiled-from-esm/index.js
rename to playground/optimize-deps/dep-cjs-compiled-from-esm/index.js
diff --git a/playground/optimize-deps/dep-cjs-compiled-from-esm/package.json b/playground/optimize-deps/dep-cjs-compiled-from-esm/package.json
new file mode 100644
index 00000000000000..a4d7117670a7eb
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-compiled-from-esm/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-cjs-compiled-from-esm",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js"
+}
diff --git a/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/index.js b/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/index.js
new file mode 100644
index 00000000000000..c3bcc276762093
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/index.js
@@ -0,0 +1,6 @@
+const { okay } = require('./test.okay')
+const { scss } = require('./test.scss')
+const { astro } = require('./test.astro')
+const { tsx } = require('./test.tsx')
+
+module.exports = { okay, scss, astro, tsx }
diff --git a/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/package.json b/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/package.json
new file mode 100644
index 00000000000000..c0e4afb3259b07
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-cjs-external-package-omit-js-suffix",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js"
+}
diff --git a/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/test.astro.js b/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/test.astro.js
new file mode 100644
index 00000000000000..337124ff2857bc
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/test.astro.js
@@ -0,0 +1,5 @@
+function astro() {
+ return 'astro'
+}
+
+module.exports = { astro }
diff --git a/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/test.okay.js b/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/test.okay.js
new file mode 100644
index 00000000000000..b8913f7c835d9c
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/test.okay.js
@@ -0,0 +1,5 @@
+function okay() {
+ return 'okay'
+}
+
+module.exports = { okay }
diff --git a/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/test.scss.js b/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/test.scss.js
new file mode 100644
index 00000000000000..507ebe60f429d3
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/test.scss.js
@@ -0,0 +1,5 @@
+function scss() {
+ return 'scss'
+}
+
+module.exports = { scss }
diff --git a/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/test.tsx.js b/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/test.tsx.js
new file mode 100644
index 00000000000000..6edc349a1880c3
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-external-package-omit-js-suffix/test.tsx.js
@@ -0,0 +1,5 @@
+function tsx() {
+ return 'tsx'
+}
+
+module.exports = { tsx }
diff --git a/playground/optimize-deps/dep-cjs-with-assets/foo.css b/playground/optimize-deps/dep-cjs-with-assets/foo.css
new file mode 100644
index 00000000000000..8347f9fb0c358e
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-with-assets/foo.css
@@ -0,0 +1,3 @@
+.cjs-with-assets {
+ color: blue;
+}
diff --git a/playground/optimize-deps/dep-cjs-with-assets/index.js b/playground/optimize-deps/dep-cjs-with-assets/index.js
new file mode 100644
index 00000000000000..26b296af650d88
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-with-assets/index.js
@@ -0,0 +1,3 @@
+require('./foo.css')
+
+exports.a = 11
diff --git a/playground/optimize-deps/dep-cjs-with-assets/package.json b/playground/optimize-deps/dep-cjs-with-assets/package.json
new file mode 100644
index 00000000000000..25dee845ba8fd9
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-with-assets/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-cjs-with-assets",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js"
+}
diff --git a/playground/optimize-deps/dep-cjs-with-external-dep/index.js b/playground/optimize-deps/dep-cjs-with-external-dep/index.js
new file mode 100644
index 00000000000000..18b7d613194bae
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-with-external-dep/index.js
@@ -0,0 +1,4 @@
+const external = require('@vitejs/test-dep-esm-external')
+// eslint-disable-next-line no-prototype-builtins
+const result = external.hasOwnProperty('foo') ? 'ok' : 'error'
+module.exports = { result }
diff --git a/playground/optimize-deps/dep-cjs-with-external-dep/package.json b/playground/optimize-deps/dep-cjs-with-external-dep/package.json
new file mode 100644
index 00000000000000..39e8bf01d732d0
--- /dev/null
+++ b/playground/optimize-deps/dep-cjs-with-external-dep/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-dep-cjs-with-external-dep",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js",
+ "dependencies": {
+ "@vitejs/test-dep-esm-external": "file:../dep-esm-external"
+ }
+}
diff --git a/playground/optimize-deps/dep-css-require/index.cjs b/playground/optimize-deps/dep-css-require/index.cjs
new file mode 100644
index 00000000000000..f3f8dd4ea41465
--- /dev/null
+++ b/playground/optimize-deps/dep-css-require/index.cjs
@@ -0,0 +1 @@
+require('./style.css')
diff --git a/playground/optimize-deps/dep-css-require/mod.cjs b/playground/optimize-deps/dep-css-require/mod.cjs
new file mode 100644
index 00000000000000..b2428066205643
--- /dev/null
+++ b/playground/optimize-deps/dep-css-require/mod.cjs
@@ -0,0 +1,2 @@
+const style = require('./mod.module.css')
+module.exports = style
diff --git a/playground/optimize-deps/dep-css-require/mod.module.css b/playground/optimize-deps/dep-css-require/mod.module.css
new file mode 100644
index 00000000000000..d27a321f0a48a8
--- /dev/null
+++ b/playground/optimize-deps/dep-css-require/mod.module.css
@@ -0,0 +1,3 @@
+.cssModuleRequire {
+ color: red;
+}
diff --git a/playground/optimize-deps/dep-css-require/package.json b/playground/optimize-deps/dep-css-require/package.json
new file mode 100644
index 00000000000000..8e4a0aaf9f14ba
--- /dev/null
+++ b/playground/optimize-deps/dep-css-require/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-css-require",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.cjs"
+}
diff --git a/playground/optimize-deps/dep-css-require/style.css b/playground/optimize-deps/dep-css-require/style.css
new file mode 100644
index 00000000000000..e88cf23879cc71
--- /dev/null
+++ b/playground/optimize-deps/dep-css-require/style.css
@@ -0,0 +1,3 @@
+.css-require {
+ color: red;
+}
diff --git a/packages/playground/optimize-deps/dep-esbuild-plugin-transform/index.js b/playground/optimize-deps/dep-esbuild-plugin-transform/index.js
similarity index 100%
rename from packages/playground/optimize-deps/dep-esbuild-plugin-transform/index.js
rename to playground/optimize-deps/dep-esbuild-plugin-transform/index.js
diff --git a/playground/optimize-deps/dep-esbuild-plugin-transform/package.json b/playground/optimize-deps/dep-esbuild-plugin-transform/package.json
new file mode 100644
index 00000000000000..fe4c7cdc844d09
--- /dev/null
+++ b/playground/optimize-deps/dep-esbuild-plugin-transform/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-dep-esbuild-plugin-transform",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "index.js"
+}
diff --git a/playground/optimize-deps/dep-esm-external/index.js b/playground/optimize-deps/dep-esm-external/index.js
new file mode 100644
index 00000000000000..6de1f5a8d64dcf
--- /dev/null
+++ b/playground/optimize-deps/dep-esm-external/index.js
@@ -0,0 +1,3 @@
+export function foo() {
+ return 'foo'
+}
diff --git a/playground/optimize-deps/dep-esm-external/package.json b/playground/optimize-deps/dep-esm-external/package.json
new file mode 100644
index 00000000000000..83cf0e23537ab5
--- /dev/null
+++ b/playground/optimize-deps/dep-esm-external/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-dep-esm-external",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js",
+ "type": "module"
+}
diff --git a/playground/optimize-deps/dep-incompatible/index.js b/playground/optimize-deps/dep-incompatible/index.js
new file mode 100644
index 00000000000000..6d67368a1d4df7
--- /dev/null
+++ b/playground/optimize-deps/dep-incompatible/index.js
@@ -0,0 +1,3 @@
+const subUrl = new URL('./sub.js', import.meta.url)
+
+export default () => import(subUrl)
diff --git a/playground/optimize-deps/dep-incompatible/package.json b/playground/optimize-deps/dep-incompatible/package.json
new file mode 100644
index 00000000000000..1d67c51eb4cbcc
--- /dev/null
+++ b/playground/optimize-deps/dep-incompatible/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-dep-incompatible",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "index.js"
+}
diff --git a/playground/optimize-deps/dep-incompatible/sub.js b/playground/optimize-deps/dep-incompatible/sub.js
new file mode 100644
index 00000000000000..26f9a8a9430bf8
--- /dev/null
+++ b/playground/optimize-deps/dep-incompatible/sub.js
@@ -0,0 +1 @@
+export default 'sub'
diff --git a/packages/playground/optimize-deps/dep-linked-include/Test.vue b/playground/optimize-deps/dep-linked-include/Test.vue
similarity index 100%
rename from packages/playground/optimize-deps/dep-linked-include/Test.vue
rename to playground/optimize-deps/dep-linked-include/Test.vue
diff --git a/packages/playground/optimize-deps/dep-linked-include/foo.js b/playground/optimize-deps/dep-linked-include/foo.js
similarity index 100%
rename from packages/playground/optimize-deps/dep-linked-include/foo.js
rename to playground/optimize-deps/dep-linked-include/foo.js
diff --git a/playground/optimize-deps/dep-linked-include/index.mjs b/playground/optimize-deps/dep-linked-include/index.mjs
new file mode 100644
index 00000000000000..c5d1e2f3f1f0a3
--- /dev/null
+++ b/playground/optimize-deps/dep-linked-include/index.mjs
@@ -0,0 +1,21 @@
+export { msg } from './foo.js'
+
+// test importing node built-ins
+import fs from 'node:fs'
+
+import { useState } from 'react'
+
+export function useCount() {
+ return useState(0)
+}
+
+// test dep with css/asset imports
+import './test.css'
+
+if (false) {
+ fs.readFileSync()
+} else {
+ console.log('ok')
+}
+
+export { default as VueSFC } from './Test.vue'
diff --git a/playground/optimize-deps/dep-linked-include/package.json b/playground/optimize-deps/dep-linked-include/package.json
new file mode 100644
index 00000000000000..e2d233a277167a
--- /dev/null
+++ b/playground/optimize-deps/dep-linked-include/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-dep-linked-include",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.mjs",
+ "dependencies": {
+ "react": "19.1.0"
+ }
+}
diff --git a/playground/optimize-deps/dep-linked-include/test.css b/playground/optimize-deps/dep-linked-include/test.css
new file mode 100644
index 00000000000000..20ae3263941d1e
--- /dev/null
+++ b/playground/optimize-deps/dep-linked-include/test.css
@@ -0,0 +1,3 @@
+.dep-linked-include {
+ color: red;
+}
diff --git a/packages/playground/optimize-deps/dep-linked/index.js b/playground/optimize-deps/dep-linked/index.js
similarity index 100%
rename from packages/playground/optimize-deps/dep-linked/index.js
rename to playground/optimize-deps/dep-linked/index.js
diff --git a/playground/optimize-deps/dep-linked/package.json b/playground/optimize-deps/dep-linked/package.json
new file mode 100644
index 00000000000000..711e5ee1df20d2
--- /dev/null
+++ b/playground/optimize-deps/dep-linked/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-dep-linked",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "index.js",
+ "dependencies": {
+ "lodash-es": "^4.17.21"
+ }
+}
diff --git a/playground/optimize-deps/dep-node-env/index.js b/playground/optimize-deps/dep-node-env/index.js
new file mode 100644
index 00000000000000..8548c37894539f
--- /dev/null
+++ b/playground/optimize-deps/dep-node-env/index.js
@@ -0,0 +1 @@
+export const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev'
diff --git a/playground/optimize-deps/dep-node-env/package.json b/playground/optimize-deps/dep-node-env/package.json
new file mode 100644
index 00000000000000..49cc0186499f73
--- /dev/null
+++ b/playground/optimize-deps/dep-node-env/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-node-env",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module"
+}
diff --git a/playground/optimize-deps/dep-non-optimized/index.js b/playground/optimize-deps/dep-non-optimized/index.js
new file mode 100644
index 00000000000000..51b048a657d5c9
--- /dev/null
+++ b/playground/optimize-deps/dep-non-optimized/index.js
@@ -0,0 +1,6 @@
+// Scheme check that imports from different paths are resolved to the same module
+const messages = []
+export const add = (message) => {
+ messages.push(message)
+}
+export const get = () => messages
diff --git a/playground/optimize-deps/dep-non-optimized/package.json b/playground/optimize-deps/dep-non-optimized/package.json
new file mode 100644
index 00000000000000..e07627f09464ce
--- /dev/null
+++ b/playground/optimize-deps/dep-non-optimized/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-non-optimized",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module"
+}
diff --git a/packages/playground/optimize-deps/dep-not-js/foo.js b/playground/optimize-deps/dep-not-js/foo.js
similarity index 100%
rename from packages/playground/optimize-deps/dep-not-js/foo.js
rename to playground/optimize-deps/dep-not-js/foo.js
diff --git a/packages/playground/optimize-deps/dep-not-js/index.notjs b/playground/optimize-deps/dep-not-js/index.notjs
similarity index 100%
rename from packages/playground/optimize-deps/dep-not-js/index.notjs
rename to playground/optimize-deps/dep-not-js/index.notjs
diff --git a/playground/optimize-deps/dep-not-js/package.json b/playground/optimize-deps/dep-not-js/package.json
new file mode 100644
index 00000000000000..ed4de5ee7ce788
--- /dev/null
+++ b/playground/optimize-deps/dep-not-js/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-not-js",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.notjs"
+}
diff --git a/playground/optimize-deps/dep-optimize-exports-with-glob/glob/bar.js b/playground/optimize-deps/dep-optimize-exports-with-glob/glob/bar.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-exports-with-glob/glob/foo.js b/playground/optimize-deps/dep-optimize-exports-with-glob/glob/foo.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-exports-with-glob/glob/nested/baz.js b/playground/optimize-deps/dep-optimize-exports-with-glob/glob/nested/baz.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-exports-with-glob/index.js b/playground/optimize-deps/dep-optimize-exports-with-glob/index.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-exports-with-glob/named.js b/playground/optimize-deps/dep-optimize-exports-with-glob/named.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-exports-with-glob/package.json b/playground/optimize-deps/dep-optimize-exports-with-glob/package.json
new file mode 100644
index 00000000000000..3a5936bc882613
--- /dev/null
+++ b/playground/optimize-deps/dep-optimize-exports-with-glob/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@vitejs/test-dep-optimize-exports-with-glob",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "exports": {
+ ".": "./index.js",
+ "./named": "./named.js",
+ "./glob-dir/*": "./glob/*.js"
+ }
+}
diff --git a/playground/optimize-deps/dep-optimize-exports-with-root-glob/dir/file2.js b/playground/optimize-deps/dep-optimize-exports-with-root-glob/dir/file2.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-exports-with-root-glob/file1.js b/playground/optimize-deps/dep-optimize-exports-with-root-glob/file1.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-exports-with-root-glob/index.js b/playground/optimize-deps/dep-optimize-exports-with-root-glob/index.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-exports-with-root-glob/package.json b/playground/optimize-deps/dep-optimize-exports-with-root-glob/package.json
new file mode 100644
index 00000000000000..4594a7bbcc4cf7
--- /dev/null
+++ b/playground/optimize-deps/dep-optimize-exports-with-root-glob/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-dep-optimize-exports-with-root-glob",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "exports": {
+ ".": "./index.js",
+ "./*": "./*"
+ }
+}
diff --git a/playground/optimize-deps/dep-optimize-with-glob/glob/bar.js b/playground/optimize-deps/dep-optimize-with-glob/glob/bar.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-with-glob/glob/foo.js b/playground/optimize-deps/dep-optimize-with-glob/glob/foo.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-with-glob/glob/nested/baz.js b/playground/optimize-deps/dep-optimize-with-glob/glob/nested/baz.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-with-glob/index.js b/playground/optimize-deps/dep-optimize-with-glob/index.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-with-glob/named.js b/playground/optimize-deps/dep-optimize-with-glob/named.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/optimize-deps/dep-optimize-with-glob/package.json b/playground/optimize-deps/dep-optimize-with-glob/package.json
new file mode 100644
index 00000000000000..b640f9453e3a9f
--- /dev/null
+++ b/playground/optimize-deps/dep-optimize-with-glob/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-optimize-with-glob",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module"
+}
diff --git a/playground/optimize-deps/dep-relative-to-main/entry.js b/playground/optimize-deps/dep-relative-to-main/entry.js
new file mode 100644
index 00000000000000..d70a1adf8fee78
--- /dev/null
+++ b/playground/optimize-deps/dep-relative-to-main/entry.js
@@ -0,0 +1 @@
+module.exports = require('./')
diff --git a/playground/optimize-deps/dep-relative-to-main/lib/main.js b/playground/optimize-deps/dep-relative-to-main/lib/main.js
new file mode 100644
index 00000000000000..d27286071c483f
--- /dev/null
+++ b/playground/optimize-deps/dep-relative-to-main/lib/main.js
@@ -0,0 +1 @@
+module.exports = '[success] imported from main'
diff --git a/playground/optimize-deps/dep-relative-to-main/package.json b/playground/optimize-deps/dep-relative-to-main/package.json
new file mode 100644
index 00000000000000..46e1715143713e
--- /dev/null
+++ b/playground/optimize-deps/dep-relative-to-main/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-relative-to-main",
+ "private": true,
+ "version": "1.0.0",
+ "main": "lib/main.js"
+}
diff --git a/playground/optimize-deps/dep-source-map-no-sources/all.js b/playground/optimize-deps/dep-source-map-no-sources/all.js
new file mode 100644
index 00000000000000..33b7c388d5f003
--- /dev/null
+++ b/playground/optimize-deps/dep-source-map-no-sources/all.js
@@ -0,0 +1,2 @@
+export const all = 'all'
+export { sub } from './sub.js'
diff --git a/playground/optimize-deps/dep-source-map-no-sources/package.json b/playground/optimize-deps/dep-source-map-no-sources/package.json
new file mode 100644
index 00000000000000..cd0db894af0ceb
--- /dev/null
+++ b/playground/optimize-deps/dep-source-map-no-sources/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-dep-source-map-no-sources",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "exports": {
+ "./*": "./*"
+ }
+}
diff --git a/playground/optimize-deps/dep-source-map-no-sources/sub.js b/playground/optimize-deps/dep-source-map-no-sources/sub.js
new file mode 100644
index 00000000000000..125abf53cf2920
--- /dev/null
+++ b/playground/optimize-deps/dep-source-map-no-sources/sub.js
@@ -0,0 +1 @@
+export const sub = 'sub'
diff --git a/playground/optimize-deps/dep-with-asset-ext/dep1/index.mjs b/playground/optimize-deps/dep-with-asset-ext/dep1/index.mjs
new file mode 100644
index 00000000000000..0c18cd78e5a5ac
--- /dev/null
+++ b/playground/optimize-deps/dep-with-asset-ext/dep1/index.mjs
@@ -0,0 +1,3 @@
+export default { random: Math.random() }
+
+export const isPreBundled = import.meta.url.includes('/.vite/deps/')
diff --git a/playground/optimize-deps/dep-with-asset-ext/dep1/package.json b/playground/optimize-deps/dep-with-asset-ext/dep1/package.json
new file mode 100644
index 00000000000000..cec7eb4442bec8
--- /dev/null
+++ b/playground/optimize-deps/dep-with-asset-ext/dep1/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-with-asset-ext1.pdf",
+ "private": true,
+ "type": "module",
+ "exports": "./index.mjs"
+}
diff --git a/playground/optimize-deps/dep-with-asset-ext/dep2/index.js b/playground/optimize-deps/dep-with-asset-ext/dep2/index.js
new file mode 100644
index 00000000000000..51eb63870348db
--- /dev/null
+++ b/playground/optimize-deps/dep-with-asset-ext/dep2/index.js
@@ -0,0 +1 @@
+export { default } from '@vitejs/test-dep-with-asset-ext1.pdf'
diff --git a/playground/optimize-deps/dep-with-asset-ext/dep2/package.json b/playground/optimize-deps/dep-with-asset-ext/dep2/package.json
new file mode 100644
index 00000000000000..2134bc6de3eabb
--- /dev/null
+++ b/playground/optimize-deps/dep-with-asset-ext/dep2/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-dep-with-asset-ext2.pdf",
+ "private": true,
+ "type": "module",
+ "exports": "./index.js",
+ "dependencies": {
+ "@vitejs/test-dep-with-asset-ext1.pdf": "file:../dep1"
+ }
+}
diff --git a/playground/optimize-deps/dep-with-builtin-module-cjs/index.js b/playground/optimize-deps/dep-with-builtin-module-cjs/index.js
new file mode 100644
index 00000000000000..17c7253a0d842e
--- /dev/null
+++ b/playground/optimize-deps/dep-with-builtin-module-cjs/index.js
@@ -0,0 +1,21 @@
+// no node: protocol intentionally
+// eslint-disable-next-line import-x/no-nodejs-modules
+const fs = require('fs')
+// eslint-disable-next-line import-x/no-nodejs-modules
+const path = require('path')
+
+// NOTE: require destructure would error immediately because of how esbuild
+// compiles it. There's no way around it as it's direct property access, which
+// triggers the Proxy get trap.
+
+// access from default import
+try {
+ path.join()
+} catch (e) {
+ console.log('dep-with-builtin-module-cjs', e)
+}
+
+// access from function
+module.exports.read = () => {
+ return fs.readFileSync('test')
+}
diff --git a/playground/optimize-deps/dep-with-builtin-module-cjs/package.json b/playground/optimize-deps/dep-with-builtin-module-cjs/package.json
new file mode 100644
index 00000000000000..3bf66569faa509
--- /dev/null
+++ b/playground/optimize-deps/dep-with-builtin-module-cjs/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-dep-with-builtin-module-cjs",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js"
+}
diff --git a/playground/optimize-deps/dep-with-builtin-module-esm/index.js b/playground/optimize-deps/dep-with-builtin-module-esm/index.js
new file mode 100644
index 00000000000000..f2d5fbc2480353
--- /dev/null
+++ b/playground/optimize-deps/dep-with-builtin-module-esm/index.js
@@ -0,0 +1,24 @@
+// no node: protocol intentionally
+// eslint-disable-next-line import-x/no-nodejs-modules
+import { readFileSync } from 'fs'
+// eslint-disable-next-line import-x/no-nodejs-modules
+import path from 'path'
+
+// access from named import
+try {
+ readFileSync()
+} catch (e) {
+ console.log('dep-with-builtin-module-esm', e)
+}
+
+// access from default import
+try {
+ path.join()
+} catch (e) {
+ console.log('dep-with-builtin-module-esm', e)
+}
+
+// access from function
+export function read() {
+ return readFileSync('test')
+}
diff --git a/playground/optimize-deps/dep-with-builtin-module-esm/package.json b/playground/optimize-deps/dep-with-builtin-module-esm/package.json
new file mode 100644
index 00000000000000..817d5153324067
--- /dev/null
+++ b/playground/optimize-deps/dep-with-builtin-module-esm/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-dep-with-builtin-module-esm",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js",
+ "type": "module"
+}
diff --git a/packages/playground/optimize-deps/dep-with-dynamic-import/dynamic.js b/playground/optimize-deps/dep-with-dynamic-import/dynamic.js
similarity index 100%
rename from packages/playground/optimize-deps/dep-with-dynamic-import/dynamic.js
rename to playground/optimize-deps/dep-with-dynamic-import/dynamic.js
diff --git a/packages/playground/optimize-deps/dep-with-dynamic-import/index.js b/playground/optimize-deps/dep-with-dynamic-import/index.js
similarity index 100%
rename from packages/playground/optimize-deps/dep-with-dynamic-import/index.js
rename to playground/optimize-deps/dep-with-dynamic-import/index.js
diff --git a/playground/optimize-deps/dep-with-dynamic-import/package.json b/playground/optimize-deps/dep-with-dynamic-import/package.json
new file mode 100644
index 00000000000000..73db7f8bd9cbd5
--- /dev/null
+++ b/playground/optimize-deps/dep-with-dynamic-import/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-dep-with-dynamic-import",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "index.js"
+}
diff --git a/playground/optimize-deps/dep-with-optional-peer-dep-submodule/index.js b/playground/optimize-deps/dep-with-optional-peer-dep-submodule/index.js
new file mode 100644
index 00000000000000..d2ace777ebf930
--- /dev/null
+++ b/playground/optimize-deps/dep-with-optional-peer-dep-submodule/index.js
@@ -0,0 +1,7 @@
+export function callItself() {
+ return '[success]'
+}
+
+export async function callPeerDepSubmodule() {
+ return await import('foobar/baz')
+}
diff --git a/playground/optimize-deps/dep-with-optional-peer-dep-submodule/package.json b/playground/optimize-deps/dep-with-optional-peer-dep-submodule/package.json
new file mode 100644
index 00000000000000..82dcdff5dea262
--- /dev/null
+++ b/playground/optimize-deps/dep-with-optional-peer-dep-submodule/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-dep-with-optional-peer-dep-submodule",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js",
+ "type": "module",
+ "peerDependencies": {
+ "foobar": "0.0.0"
+ },
+ "peerDependenciesMeta": {
+ "foobar": {
+ "optional": true
+ }
+ }
+}
diff --git a/playground/optimize-deps/dep-with-optional-peer-dep/index.js b/playground/optimize-deps/dep-with-optional-peer-dep/index.js
new file mode 100644
index 00000000000000..bce89ca18f3ad7
--- /dev/null
+++ b/playground/optimize-deps/dep-with-optional-peer-dep/index.js
@@ -0,0 +1,7 @@
+export function callItself() {
+ return '[success]'
+}
+
+export async function callPeerDep() {
+ return await import('foobar')
+}
diff --git a/playground/optimize-deps/dep-with-optional-peer-dep/package.json b/playground/optimize-deps/dep-with-optional-peer-dep/package.json
new file mode 100644
index 00000000000000..2e8ca2d85a47fc
--- /dev/null
+++ b/playground/optimize-deps/dep-with-optional-peer-dep/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-dep-with-optional-peer-dep",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js",
+ "type": "module",
+ "peerDependencies": {
+ "foobar": "0.0.0"
+ },
+ "peerDependenciesMeta": {
+ "foobar": {
+ "optional": true
+ }
+ }
+}
diff --git a/playground/optimize-deps/dynamic-use-dep-alias-using-absolute-path.js b/playground/optimize-deps/dynamic-use-dep-alias-using-absolute-path.js
new file mode 100644
index 00000000000000..784123a81ec685
--- /dev/null
+++ b/playground/optimize-deps/dynamic-use-dep-alias-using-absolute-path.js
@@ -0,0 +1,6 @@
+// This is used to setup a test scenario, where dep scanner
+// could not determine all of the used dependencies on first
+// pass, e.g., a dependency that is aliased using an absolute
+// path, in which case it used to trigger unnecessary "full
+// reloads" invalidating all modules in a module graph.
+export { default } from '@vitejs/test-dep-alias-using-absolute-path'
diff --git a/playground/optimize-deps/generics.vue b/playground/optimize-deps/generics.vue
new file mode 100644
index 00000000000000..13e17ce1c12d6a
--- /dev/null
+++ b/playground/optimize-deps/generics.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+{{ items }}
diff --git a/packages/playground/optimize-deps/glob/foo.js b/playground/optimize-deps/glob/foo.js
similarity index 100%
rename from packages/playground/optimize-deps/glob/foo.js
rename to playground/optimize-deps/glob/foo.js
diff --git a/playground/optimize-deps/index.astro b/playground/optimize-deps/index.astro
new file mode 100644
index 00000000000000..4425a8b0f6b1ee
--- /dev/null
+++ b/playground/optimize-deps/index.astro
@@ -0,0 +1,6 @@
+
diff --git a/playground/optimize-deps/index.html b/playground/optimize-deps/index.html
new file mode 100644
index 00000000000000..c3493cc412baec
--- /dev/null
+++ b/playground/optimize-deps/index.html
@@ -0,0 +1,321 @@
+Optimize Deps
+
+CommonJS w/ named imports (react)
+
+CommonJS w/ named imports (phoenix)
+fail
+CommonJS w/ default export (clipboard)
+fail
+CommonJS import default (dep-cjs-compiled-from-esm)
+
+CommonJS import default (dep-cjs-compiled-from-cjs)
+
+
+
+
+CommonJS dynamic import default + named (react)
+
+CommonJS dynamic import named (phoenix)
+
+CommonJS dynamic import default (clipboard)
+
+CommonJS dynamic import default (dep-cjs-compiled-from-esm)
+
+CommonJS dynamic import default (dep-cjs-compiled-from-cjs)
+
+
+
+
+Dedupe (dep in linked & optimized package)
+
+
+
+CommonJS w/ browser field mapping (axios)
+This should show pong:
+
+CommonJS w/ bare id browser field mapping
+This should show pong:
+
+Detecting linked src package and optimizing its deps (lodash-es)
+This should show fooBarBaz:
+
+Optimizing force included dep even when it's linked
+
+
+Dep with CSS
+This should be red
+
+CJS Dep with CSS
+This should be blue
+
+import * as ...
+
+
+Import from dependency with process.env.NODE_ENV
+
+
+Import from dependency with .notjs files
+
+
+
+ Import from dependency which uses relative path which needs to be resolved by
+ main field
+
+
+
+Import from dependency with dynamic import
+
+
+Import from dependency with optional peer dep
+
+
+Import from dependency with optional peer dep submodule
+
+
+Externalize known non-js files in optimize included dep
+
+
+Vue & Vuex
+
+
+Dep with changes from esbuild plugin
+This should show a greeting:
+
+Dep from hidden dir
+This should show hello!:
+
+Nested include
+Module path:
+
+Alias with colon
+URL:
+
+Alias using absolute path
+
+
+Reused variable names
+This should show reused:
+
+Flatten Id
+
+
+
+Non Optimized Module isn't duplicated
+
+
+Pre bundle css require
+css require
+
+Pre bundle css modules require
+This should be red
+
+Long file name import works
+
+
+
+
+Import the CommonJS external package that omits the js suffix
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Pre-bundle transitive dependency 'some-package.pdf'
+prebundled: ???
+
+ no dual package: ???
+
+
+
+
+
+Pre-bundle dependency with external sub-dependency
+
+ require('some-external-sub-dependency') returns a plain object:
+ ???
+
+
diff --git a/playground/optimize-deps/long-file-name.js b/playground/optimize-deps/long-file-name.js
new file mode 100644
index 00000000000000..5717e18ec0d536
--- /dev/null
+++ b/playground/optimize-deps/long-file-name.js
@@ -0,0 +1,4 @@
+import test from '@vitejs/longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/index'
+
+const main = document.querySelector('.long-file-name')
+main.innerHTML = test ?? 'failed to import'
diff --git a/playground/optimize-deps/longfilename/index.js b/playground/optimize-deps/longfilename/index.js
new file mode 100644
index 00000000000000..7f0f7ce6d2e4a0
--- /dev/null
+++ b/playground/optimize-deps/longfilename/index.js
@@ -0,0 +1,3 @@
+const test = 'hello world'
+
+export default test
diff --git a/playground/optimize-deps/longfilename/package.json b/playground/optimize-deps/longfilename/package.json
new file mode 100644
index 00000000000000..1bae62210b0cf9
--- /dev/null
+++ b/playground/optimize-deps/longfilename/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js",
+ "type": "module"
+}
diff --git a/playground/optimize-deps/nested-exclude/index.js b/playground/optimize-deps/nested-exclude/index.js
new file mode 100644
index 00000000000000..c3dc5c01b6d886
--- /dev/null
+++ b/playground/optimize-deps/nested-exclude/index.js
@@ -0,0 +1,3 @@
+export { default as nestedInclude } from '@vitejs/test-nested-include'
+
+export default 'nested-exclude'
diff --git a/playground/optimize-deps/nested-exclude/package.json b/playground/optimize-deps/nested-exclude/package.json
new file mode 100644
index 00000000000000..3e12e0111fe31d
--- /dev/null
+++ b/playground/optimize-deps/nested-exclude/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-nested-exclude",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "main": "index.js",
+ "dependencies": {
+ "@vitejs/test-nested-include": "file:../nested-include"
+ }
+}
diff --git a/packages/playground/optimize-deps/nested-exclude/nested-include/index.js b/playground/optimize-deps/nested-include/index.js
similarity index 100%
rename from packages/playground/optimize-deps/nested-exclude/nested-include/index.js
rename to playground/optimize-deps/nested-include/index.js
diff --git a/playground/optimize-deps/nested-include/package.json b/playground/optimize-deps/nested-include/package.json
new file mode 100644
index 00000000000000..d4b97a20651d1c
--- /dev/null
+++ b/playground/optimize-deps/nested-include/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-nested-include",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.js"
+}
diff --git a/playground/optimize-deps/non-optimizable-include/index.css b/playground/optimize-deps/non-optimizable-include/index.css
new file mode 100644
index 00000000000000..161363ab3641a8
--- /dev/null
+++ b/playground/optimize-deps/non-optimizable-include/index.css
@@ -0,0 +1,4 @@
+@font-face {
+ font-family: 'Not Real Sans';
+ src: url('./i-throw-if-you-optimize-this-file.woff') format('woff');
+}
diff --git a/playground/optimize-deps/non-optimizable-include/package.json b/playground/optimize-deps/non-optimizable-include/package.json
new file mode 100644
index 00000000000000..6ae140f15956ee
--- /dev/null
+++ b/playground/optimize-deps/non-optimizable-include/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-non-optimizable-include",
+ "private": true,
+ "type": "module",
+ "version": "0.0.0",
+ "exports": {
+ ".": "./index.css"
+ }
+}
diff --git a/playground/optimize-deps/package.json b/playground/optimize-deps/package.json
new file mode 100644
index 00000000000000..ce6645567c514b
--- /dev/null
+++ b/playground/optimize-deps/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@vitejs/test-optimize-deps",
+ "private": true,
+ "type": "module",
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.9.0",
+ "clipboard": "^2.0.11",
+ "@vitejs/longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": "file:./longfilename",
+ "@vitejs/test-dep-alias-using-absolute-path": "file:./dep-alias-using-absolute-path",
+ "@vitejs/test-dep-cjs-browser-field-bare": "file:./dep-cjs-browser-field-bare",
+ "@vitejs/test-dep-cjs-compiled-from-cjs": "file:./dep-cjs-compiled-from-cjs",
+ "@vitejs/test-dep-cjs-compiled-from-esm": "file:./dep-cjs-compiled-from-esm",
+ "@vitejs/test-dep-cjs-with-assets": "file:./dep-cjs-with-assets",
+ "@vitejs/test-dep-cjs-with-external-dep": "file:./dep-cjs-with-external-dep",
+ "@vitejs/test-dep-css-require": "file:./dep-css-require",
+ "@vitejs/test-dep-esbuild-plugin-transform": "file:./dep-esbuild-plugin-transform",
+ "@vitejs/test-dep-incompatible": "file:./dep-incompatible",
+ "@vitejs/test-dep-linked": "link:./dep-linked",
+ "@vitejs/test-dep-linked-include": "link:./dep-linked-include",
+ "@vitejs/test-dep-node-env": "file:./dep-node-env",
+ "@vitejs/test-dep-not-js": "file:./dep-not-js",
+ "@vitejs/test-dep-optimize-exports-with-glob": "file:./dep-optimize-exports-with-glob",
+ "@vitejs/test-dep-optimize-exports-with-root-glob": "file:./dep-optimize-exports-with-root-glob",
+ "@vitejs/test-dep-optimize-with-glob": "file:./dep-optimize-with-glob",
+ "@vitejs/test-dep-relative-to-main": "file:./dep-relative-to-main",
+ "@vitejs/test-dep-source-map-no-sources": "file:./dep-source-map-no-sources",
+ "@vitejs/test-dep-with-asset-ext1.pdf": "file:./dep-with-asset-ext/dep1",
+ "@vitejs/test-dep-with-asset-ext2.pdf": "file:./dep-with-asset-ext/dep2",
+ "@vitejs/test-dep-with-builtin-module-cjs": "file:./dep-with-builtin-module-cjs",
+ "@vitejs/test-dep-with-builtin-module-esm": "file:./dep-with-builtin-module-esm",
+ "@vitejs/test-dep-with-dynamic-import": "file:./dep-with-dynamic-import",
+ "@vitejs/test-dep-with-optional-peer-dep": "file:./dep-with-optional-peer-dep",
+ "@vitejs/test-dep-with-optional-peer-dep-submodule": "file:./dep-with-optional-peer-dep-submodule",
+ "@vitejs/test-dep-non-optimized": "file:./dep-non-optimized",
+ "@vitejs/test-added-in-entries": "file:./added-in-entries",
+ "@vitejs/test-dep-cjs-external-package-omit-js-suffix": "file:./dep-cjs-external-package-omit-js-suffix",
+ "lodash-es": "^4.17.21",
+ "@vitejs/test-nested-exclude": "file:./nested-exclude",
+ "phoenix": "^1.7.21",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
+ "@vitejs/test-resolve-linked": "workspace:0.0.0",
+ "url": "^0.11.4",
+ "vue": "^3.5.13",
+ "vuex": "^4.1.0",
+ "lodash": "^4.17.21",
+ "lodash.clonedeep": "^4.5.0"
+ }
+}
diff --git a/playground/optimize-deps/unused-split-entry.js b/playground/optimize-deps/unused-split-entry.js
new file mode 100644
index 00000000000000..0d15b94afe07c3
--- /dev/null
+++ b/playground/optimize-deps/unused-split-entry.js
@@ -0,0 +1,11 @@
+import msg from '@vitejs/test-added-in-entries'
+
+// This is an entry file that is added to optimizeDeps.entries
+// When the deps aren't cached, these entries are also processed
+// to discover dependencies in them. This should only be needed
+// for code split sections that are commonly visited after
+// first load where a full-reload wants to be avoided at the expense
+// of extra processing on cold start. Another option is to add
+// the missing dependencies to optimizeDeps.include directly
+
+console.log(msg)
diff --git a/playground/optimize-deps/vite.config.js b/playground/optimize-deps/vite.config.js
new file mode 100644
index 00000000000000..4e6f1c7fc16ecc
--- /dev/null
+++ b/playground/optimize-deps/vite.config.js
@@ -0,0 +1,170 @@
+import fs from 'node:fs'
+import module from 'node:module'
+import { defineConfig } from 'vite'
+const require = module.createRequire(import.meta.url)
+
+export default defineConfig({
+ resolve: {
+ dedupe: ['react'],
+ alias: {
+ 'node:url': 'url',
+ '@vitejs/test-dep-alias-using-absolute-path': require.resolve(
+ '@vitejs/test-dep-alias-using-absolute-path',
+ ),
+ },
+ },
+ optimizeDeps: {
+ include: [
+ '@vitejs/test-dep-linked-include',
+ '@vitejs/test-nested-exclude > @vitejs/test-nested-include',
+ '@vitejs/test-dep-cjs-external-package-omit-js-suffix',
+ // will throw if optimized (should log warning instead)
+ '@vitejs/test-non-optimizable-include',
+ '@vitejs/test-dep-optimize-exports-with-glob/**/*',
+ '@vitejs/test-dep-optimize-exports-with-root-glob/**/*.js',
+ '@vitejs/test-dep-optimize-with-glob/**/*.js',
+ '@vitejs/test-dep-cjs-with-external-dep',
+ ],
+ exclude: [
+ '@vitejs/test-nested-exclude',
+ '@vitejs/test-dep-non-optimized',
+ '@vitejs/test-dep-esm-external',
+ ],
+ esbuildOptions: {
+ plugins: [
+ {
+ name: 'replace-a-file',
+ setup(build) {
+ build.onLoad(
+ { filter: /dep-esbuild-plugin-transform(\\|\/)index\.js$/ },
+ () => ({
+ contents: `export const hello = () => 'Hello from an esbuild plugin'`,
+ loader: 'js',
+ }),
+ )
+ },
+ },
+ ],
+ },
+ entries: ['index.html', 'unused-split-entry.js'],
+ },
+
+ build: {
+ // to make tests faster
+ minify: false,
+ rollupOptions: {
+ onwarn(msg, warn) {
+ // filter `"Buffer" is not exported by "__vite-browser-external"` warning
+ if (msg.message.includes('Buffer')) return
+ warn(msg)
+ },
+ },
+ },
+
+ plugins: [
+ testVue(),
+ notjs(),
+ // for axios request test
+ {
+ name: 'mock',
+ configureServer({ middlewares }) {
+ middlewares.use('/ping', (_, res) => {
+ res.statusCode = 200
+ res.end('pong')
+ })
+ },
+ configurePreviewServer({ middlewares }) {
+ middlewares.use('/ping', (_, res) => {
+ res.statusCode = 200
+ res.end('pong')
+ })
+ },
+ },
+ {
+ name: 'test-astro',
+ transform(code, id) {
+ if (id.endsWith('.astro')) {
+ code = `export default {}`
+ return { code }
+ }
+ },
+ },
+ // TODO: Remove this one support for prebundling in build lands.
+ // It is expected that named importing in build doesn't work
+ // as it incurs a lot of overhead in build.
+ {
+ name: 'polyfill-named-fs-build',
+ apply: 'build',
+ enforce: 'pre',
+ load(id) {
+ if (id === '__vite-browser-external') {
+ return `export default {}; export function readFileSync() {}`
+ }
+ },
+ },
+ ],
+})
+
+// Handles Test.vue in dep-linked-include package
+function testVue() {
+ return {
+ name: 'testvue',
+ transform(code, id) {
+ if (id.includes('dep-linked-include/Test.vue')) {
+ return {
+ code: `
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+ name: 'Test',
+ render() {
+ return '[success] rendered from Vue'
+ }
+})
+`.trim(),
+ }
+ }
+
+ // fallback to empty module for other vue files
+ if (id.endsWith('.vue')) {
+ return { code: `export default {}` }
+ }
+ },
+ }
+}
+
+// Handles .notjs file, basically remove wrapping and tags
+function notjs() {
+ return {
+ name: 'notjs',
+ config() {
+ return {
+ optimizeDeps: {
+ extensions: ['.notjs'],
+ esbuildOptions: {
+ plugins: [
+ {
+ name: 'esbuild-notjs',
+ setup(build) {
+ build.onLoad({ filter: /\.notjs$/ }, ({ path }) => {
+ let contents = fs.readFileSync(path, 'utf-8')
+ contents = contents
+ .replace('', '')
+ .replace(' ', '')
+ return { contents, loader: 'js' }
+ })
+ },
+ },
+ ],
+ },
+ },
+ }
+ },
+ transform(code, id) {
+ if (id.endsWith('.notjs')) {
+ code = code.replace('', '').replace(' ', '')
+ return { code }
+ }
+ },
+ }
+}
diff --git a/playground/optimize-missing-deps/__test__/optimize-missing-deps.spec.ts b/playground/optimize-missing-deps/__test__/optimize-missing-deps.spec.ts
new file mode 100644
index 00000000000000..75d7569ed0d135
--- /dev/null
+++ b/playground/optimize-missing-deps/__test__/optimize-missing-deps.spec.ts
@@ -0,0 +1,16 @@
+import { expect, test } from 'vitest'
+import { port } from './serve'
+import { isBuild, page, untilUpdated } from '~utils'
+
+const url = `http://localhost:${port}/`
+
+test.runIf(!isBuild)('optimize', async () => {
+ await page.goto(url)
+ // reload page to get optimized missing deps
+ await page.reload()
+ await untilUpdated(() => page.textContent('div'), 'Client')
+
+ // raw http request
+ const aboutHtml = await (await fetch(url)).text()
+ expect(aboutHtml).toContain('Server')
+})
diff --git a/playground/optimize-missing-deps/__test__/serve.ts b/playground/optimize-missing-deps/__test__/serve.ts
new file mode 100644
index 00000000000000..2472e170236d5d
--- /dev/null
+++ b/playground/optimize-missing-deps/__test__/serve.ts
@@ -0,0 +1,35 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+import path from 'node:path'
+import { hmrPorts, ports, rootDir } from '~utils'
+
+export const port = ports['optimize-missing-deps']
+
+export async function serve(): Promise<{ close(): Promise }> {
+ const { createServer } = await import(path.resolve(rootDir, 'server.js'))
+ const { app, vite } = await createServer(
+ rootDir,
+ hmrPorts['optimize-missing-deps'],
+ )
+
+ return new Promise((resolve, reject) => {
+ try {
+ const server = app.listen(port, () => {
+ resolve({
+ // for test teardown
+ async close() {
+ await new Promise((resolve) => {
+ server.close(resolve)
+ })
+ if (vite) {
+ await vite.close()
+ }
+ },
+ })
+ })
+ } catch (e) {
+ reject(e)
+ }
+ })
+}
diff --git a/playground/optimize-missing-deps/index.html b/playground/optimize-missing-deps/index.html
new file mode 100644
index 00000000000000..90a9a72f333da4
--- /dev/null
+++ b/playground/optimize-missing-deps/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
diff --git a/playground/optimize-missing-deps/main.js b/playground/optimize-missing-deps/main.js
new file mode 100644
index 00000000000000..bc573a90b845bf
--- /dev/null
+++ b/playground/optimize-missing-deps/main.js
@@ -0,0 +1,3 @@
+import { sayName } from '@vitejs/test-missing-dep'
+
+export const name = sayName()
diff --git a/playground/optimize-missing-deps/missing-dep/index.js b/playground/optimize-missing-deps/missing-dep/index.js
new file mode 100644
index 00000000000000..b2f333b1b86745
--- /dev/null
+++ b/playground/optimize-missing-deps/missing-dep/index.js
@@ -0,0 +1,5 @@
+import { name } from '@vitejs/test-multi-entry-dep'
+
+export function sayName() {
+ return name
+}
diff --git a/playground/optimize-missing-deps/missing-dep/package.json b/playground/optimize-missing-deps/missing-dep/package.json
new file mode 100644
index 00000000000000..0f5003771401c8
--- /dev/null
+++ b/playground/optimize-missing-deps/missing-dep/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-missing-dep",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "index.js",
+ "dependencies": {
+ "@vitejs/test-multi-entry-dep": "file:../multi-entry-dep"
+ }
+}
diff --git a/packages/playground/optimize-missing-deps/multi-entry-dep/index.browser.js b/playground/optimize-missing-deps/multi-entry-dep/index.browser.js
similarity index 100%
rename from packages/playground/optimize-missing-deps/multi-entry-dep/index.browser.js
rename to playground/optimize-missing-deps/multi-entry-dep/index.browser.js
diff --git a/playground/optimize-missing-deps/multi-entry-dep/index.js b/playground/optimize-missing-deps/multi-entry-dep/index.js
new file mode 100644
index 00000000000000..4b214df5d30765
--- /dev/null
+++ b/playground/optimize-missing-deps/multi-entry-dep/index.js
@@ -0,0 +1,3 @@
+const path = require('node:path')
+
+exports.name = path.normalize('./Server')
diff --git a/playground/optimize-missing-deps/multi-entry-dep/package.json b/playground/optimize-missing-deps/multi-entry-dep/package.json
new file mode 100644
index 00000000000000..d0cf88f64307ce
--- /dev/null
+++ b/playground/optimize-missing-deps/multi-entry-dep/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-multi-entry-dep",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js",
+ "browser": {
+ "./index.js": "./index.browser.js"
+ }
+}
diff --git a/playground/optimize-missing-deps/package.json b/playground/optimize-missing-deps/package.json
new file mode 100644
index 00000000000000..1ce128ee4f548d
--- /dev/null
+++ b/playground/optimize-missing-deps/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-optimize-missing-deps",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "node server"
+ },
+ "dependencies": {
+ "@vitejs/test-missing-dep": "file:./missing-dep"
+ },
+ "devDependencies": {
+ "express": "^5.1.0"
+ }
+}
diff --git a/playground/optimize-missing-deps/server.js b/playground/optimize-missing-deps/server.js
new file mode 100644
index 00000000000000..165e3786ef182b
--- /dev/null
+++ b/playground/optimize-missing-deps/server.js
@@ -0,0 +1,68 @@
+// @ts-check
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import express from 'express'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const isTest = process.env.VITEST
+
+export async function createServer(root = process.cwd(), hmrPort) {
+ const resolve = (p) => path.resolve(__dirname, p)
+
+ const app = express()
+
+ /**
+ * @type {import('vite').ViteDevServer}
+ */
+ const vite = await (
+ await import('vite')
+ ).createServer({
+ root,
+ logLevel: isTest ? 'error' : 'info',
+ server: {
+ middlewareMode: true,
+ hmr: {
+ port: hmrPort,
+ },
+ },
+ appType: 'custom',
+ })
+ app.use(vite.middlewares)
+
+ app.use('*all', async (req, res) => {
+ try {
+ let template = fs.readFileSync(resolve('index.html'), 'utf-8')
+ template = await vite.transformIndexHtml(req.originalUrl, template)
+
+ // `main.js` imports dependencies that are yet to be discovered and optimized, aka "missing" deps.
+ // Loading `main.js` in SSR should not trigger optimizing the "missing" deps
+ const { name } = await vite.ssrLoadModule('./main.js')
+
+ // Loading `main.js` in the client should trigger optimizing the "missing" deps
+ const appHtml = `${name}
+`
+
+ const html = template.replace(``, appHtml)
+
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
+ } catch (e) {
+ vite.ssrFixStacktrace(e)
+ console.log(e.stack)
+ res.status(500).end(e.stack)
+ }
+ })
+
+ return { app, vite }
+}
+
+if (!isTest) {
+ createServer().then(({ app }) =>
+ app.listen(5173, () => {
+ console.log('http://localhost:5173')
+ }),
+ )
+}
diff --git a/playground/package.json b/playground/package.json
new file mode 100644
index 00000000000000..3d22afa4721375
--- /dev/null
+++ b/playground/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@vitejs/vite-playground",
+ "private": true,
+ "type": "module",
+ "version": "1.0.0",
+ "scripts": {
+ "typecheck": "tsc --noEmit"
+ },
+ "devDependencies": {
+ "convert-source-map": "^2.0.0",
+ "css-color-names": "^1.0.1",
+ "kill-port": "^1.6.1"
+ }
+}
diff --git a/playground/preload/__tests__/preload-disabled/preload-disabled.spec.ts b/playground/preload/__tests__/preload-disabled/preload-disabled.spec.ts
new file mode 100644
index 00000000000000..ea1fc220795043
--- /dev/null
+++ b/playground/preload/__tests__/preload-disabled/preload-disabled.spec.ts
@@ -0,0 +1,27 @@
+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 comments', async () => {
+ await page.click('#hello .load')
+ await page.waitForSelector('#hello output')
+
+ const html = await page.content()
+ expect(html).not.toMatch(/link rel="modulepreload"/)
+
+ expect(html).toMatch(
+ /link rel="stylesheet".*?href=".*?\/assets\/hello-[-\w]{8}\.css"/,
+ )
+ })
+})
diff --git a/playground/preload/__tests__/preload.spec.ts b/playground/preload/__tests__/preload.spec.ts
new file mode 100644
index 00000000000000..4c055d01e770e0
--- /dev/null
+++ b/playground/preload/__tests__/preload.spec.ts
@@ -0,0 +1,28 @@
+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 comments', async () => {
+ await page.click('#hello .load')
+ await page.waitForSelector('#hello output')
+
+ const html = await page.content()
+ expect(html).toMatch(
+ /link rel="modulepreload".*?href=".*?\/assets\/hello-[-\w]{8}\.js"/,
+ )
+ expect(html).toMatch(
+ /link rel="stylesheet".*?href=".*?\/assets\/hello-[-\w]{8}\.css"/,
+ )
+ })
+})
diff --git a/playground/preload/__tests__/resolve-deps/preload-resolve-deps.spec.ts b/playground/preload/__tests__/resolve-deps/preload-resolve-deps.spec.ts
new file mode 100644
index 00000000000000..41a61460807c1f
--- /dev/null
+++ b/playground/preload/__tests__/resolve-deps/preload-resolve-deps.spec.ts
@@ -0,0 +1,31 @@
+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 comments', async () => {
+ await page.click('#hello .load')
+ await page.waitForSelector('#hello output')
+
+ const html = await page.content()
+ expect(html).toMatch(
+ /link rel="modulepreload".*?href="http.*?\/hello-[-\w]{8}\.js"/,
+ )
+ expect(html).toMatch(
+ /link rel="modulepreload".*?href="http.*?\/preloaded.js"/,
+ )
+ expect(html).toMatch(
+ /link rel="stylesheet".*?href="http.*?\/hello-[-\w]{8}\.css"/,
+ )
+ })
+})
diff --git a/playground/preload/dep-a/index.js b/playground/preload/dep-a/index.js
new file mode 100644
index 00000000000000..42c97c2d826ec6
--- /dev/null
+++ b/playground/preload/dep-a/index.js
@@ -0,0 +1 @@
+export const msgFromA = 'From dep-a'
diff --git a/playground/preload/dep-a/package.json b/playground/preload/dep-a/package.json
new file mode 100644
index 00000000000000..27666af4c587b6
--- /dev/null
+++ b/playground/preload/dep-a/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-dep-a",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "index.js"
+}
diff --git a/playground/preload/dep-including-a/index.js b/playground/preload/dep-including-a/index.js
new file mode 100644
index 00000000000000..a8f4f26d79aad7
--- /dev/null
+++ b/playground/preload/dep-including-a/index.js
@@ -0,0 +1,3 @@
+export { msgFromA } from '@vitejs/test-dep-a'
+
+export const msg = 'From dep-including-a'
diff --git a/playground/preload/dep-including-a/package.json b/playground/preload/dep-including-a/package.json
new file mode 100644
index 00000000000000..49068456a8e1c1
--- /dev/null
+++ b/playground/preload/dep-including-a/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-dep-including-a",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "index.js",
+ "dependencies": {
+ "@vitejs/test-dep-a": "file:../dep-a"
+ }
+}
diff --git a/playground/preload/index.html b/playground/preload/index.html
new file mode 100644
index 00000000000000..363b9993fe4d01
--- /dev/null
+++ b/playground/preload/index.html
@@ -0,0 +1,20 @@
+preload
+
+
+
+
+
diff --git a/playground/preload/package.json b/playground/preload/package.json
new file mode 100644
index 00000000000000..1f8bbca51f94d4
--- /dev/null
+++ b/playground/preload/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@vitejs/test-preload",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview",
+ "dev:resolve-deps": "vite --config vite.config-resolve-deps.js",
+ "build:resolve-deps": "vite build --config vite.config-resolve-deps.js",
+ "debug:resolve-deps": "node --inspect-brk ../../packages/vite/bin/vite --config vite.config-resolve-deps.js",
+ "preview:resolve-deps": "vite preview --config vite.config-resolve-deps.js",
+ "dev:preload-disabled": "vite --config vite.config-preload-disabled.js",
+ "build:preload-disabled": "vite build --config vite.config-preload-disabled.js",
+ "debug:preload-disabled": "node --inspect-brk ../../packages/vite/bin/vite --config vite.config-preload-disabled.js",
+ "preview:preload-disabled": "vite preview --config vite.config-preload-disabled.js"
+ },
+ "devDependencies": {
+ "terser": "^5.39.0",
+ "@vitejs/test-dep-a": "file:./dep-a",
+ "@vitejs/test-dep-including-a": "file:./dep-including-a"
+ }
+}
diff --git a/playground/preload/public/preloaded.js b/playground/preload/public/preloaded.js
new file mode 100644
index 00000000000000..f6e428aca78ade
--- /dev/null
+++ b/playground/preload/public/preloaded.js
@@ -0,0 +1 @@
+console.log('preloaded')
diff --git a/playground/preload/src/about.js b/playground/preload/src/about.js
new file mode 100644
index 00000000000000..56b5fd6e8bcfe1
--- /dev/null
+++ b/playground/preload/src/about.js
@@ -0,0 +1,3 @@
+import { msg } from '@vitejs/test-dep-including-a'
+
+document.querySelector('#about .msg').textContent = msg
diff --git a/playground/preload/src/chunk.js b/playground/preload/src/chunk.js
new file mode 100644
index 00000000000000..b0ccae69cf3275
--- /dev/null
+++ b/playground/preload/src/chunk.js
@@ -0,0 +1 @@
+export default '[success] message from chunk.js'
diff --git a/playground/preload/src/hello.js b/playground/preload/src/hello.js
new file mode 100644
index 00000000000000..d2c5ee4ff3f9e5
--- /dev/null
+++ b/playground/preload/src/hello.js
@@ -0,0 +1,5 @@
+import style from './hello.module.css'
+
+const msg = document.querySelector('#hello .msg')
+msg.textContent = 'hello'
+msg.classList.add(style.h1)
diff --git a/playground/preload/src/hello.module.css b/playground/preload/src/hello.module.css
new file mode 100644
index 00000000000000..23910a7820b889
--- /dev/null
+++ b/playground/preload/src/hello.module.css
@@ -0,0 +1,3 @@
+.h1 {
+ color: red;
+}
diff --git a/playground/preload/src/main.js b/playground/preload/src/main.js
new file mode 100644
index 00000000000000..d5004c02610090
--- /dev/null
+++ b/playground/preload/src/main.js
@@ -0,0 +1,20 @@
+import chunkMsg from './chunk'
+
+document.querySelector('.chunk').textContent = chunkMsg
+
+const ids = {
+ hello: async () => {
+ await import(/* a comment */ './hello.js')
+ },
+ about: async () => {
+ await import('./about.js') // lazy load
+ },
+}
+
+for (const [id, loader] of Object.entries(ids)) {
+ const loadButton = document.querySelector(`#${id} .load`)
+ loadButton.addEventListener('click', async () => {
+ await loader()
+ loadButton.insertAdjacentHTML('afterend', 'loaded ')
+ })
+}
diff --git a/playground/preload/vite.config-preload-disabled.js b/playground/preload/vite.config-preload-disabled.js
new file mode 100644
index 00000000000000..e1a9f4f17d3df2
--- /dev/null
+++ b/playground/preload/vite.config-preload-disabled.js
@@ -0,0 +1,27 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ outDir: 'dist/preload-disabled',
+ minify: 'terser',
+ terserOptions: {
+ format: {
+ beautify: true,
+ },
+ compress: {
+ passes: 3,
+ },
+ },
+ rollupOptions: {
+ output: {
+ manualChunks(id) {
+ if (id.includes('chunk.js')) {
+ return 'chunk'
+ }
+ },
+ },
+ },
+ modulePreload: false,
+ },
+ cacheDir: 'node_modules/.vite-preload-disabled',
+})
diff --git a/playground/preload/vite.config-resolve-deps.js b/playground/preload/vite.config-resolve-deps.js
new file mode 100644
index 00000000000000..accc72a6e1d42d
--- /dev/null
+++ b/playground/preload/vite.config-resolve-deps.js
@@ -0,0 +1,42 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ outDir: 'dist/resolve-deps',
+ minify: 'terser',
+ terserOptions: {
+ format: {
+ beautify: true,
+ },
+ compress: {
+ passes: 3,
+ },
+ },
+ rollupOptions: {
+ output: {
+ manualChunks(id) {
+ if (id.includes('chunk.js')) {
+ return 'chunk'
+ }
+ },
+ },
+ },
+ modulePreload: {
+ resolveDependencies(filename, deps, { hostId, hostType }) {
+ if (filename.includes('hello')) {
+ return [...deps, 'preloaded.js']
+ }
+ return deps
+ },
+ },
+ },
+ experimental: {
+ renderBuiltUrl(filename, { hostId, hostType }) {
+ if (filename.includes('preloaded')) {
+ return { runtime: `""+${JSON.stringify('/' + filename)}` }
+ }
+ return { relative: true }
+ },
+ },
+ cacheDir: 'node_modules/.vite-resolve-deps',
+})
diff --git a/playground/preload/vite.config.ts b/playground/preload/vite.config.ts
new file mode 100644
index 00000000000000..6ec8716860526a
--- /dev/null
+++ b/playground/preload/vite.config.ts
@@ -0,0 +1,25 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ outDir: 'dist/normal',
+ minify: 'terser',
+ terserOptions: {
+ format: {
+ beautify: true,
+ },
+ compress: {
+ passes: 3,
+ },
+ },
+ rollupOptions: {
+ output: {
+ manualChunks(id) {
+ if (id.includes('chunk.js')) {
+ return 'chunk'
+ }
+ },
+ },
+ },
+ },
+})
diff --git a/playground/preserve-symlinks/__tests__/preserve-symlinks.spec.ts b/playground/preserve-symlinks/__tests__/preserve-symlinks.spec.ts
new file mode 100644
index 00000000000000..0748841a2ad64d
--- /dev/null
+++ b/playground/preserve-symlinks/__tests__/preserve-symlinks.spec.ts
@@ -0,0 +1,12 @@
+import { expect, test } from 'vitest'
+import { browserLogs, page } from '~utils'
+
+test('should have no 404s', () => {
+ browserLogs.forEach((msg) => {
+ expect(msg).not.toMatch('404')
+ })
+})
+
+test('not-preserve-symlinks', async () => {
+ expect(await page.textContent('#root')).toBe('hello vite')
+})
diff --git a/playground/preserve-symlinks/index.html b/playground/preserve-symlinks/index.html
new file mode 100644
index 00000000000000..afd943b88156ae
--- /dev/null
+++ b/playground/preserve-symlinks/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/packages/playground/preserve-symlinks/moduleA/linked.js b/playground/preserve-symlinks/module-a/linked.js
similarity index 100%
rename from packages/playground/preserve-symlinks/moduleA/linked.js
rename to playground/preserve-symlinks/module-a/linked.js
diff --git a/playground/preserve-symlinks/module-a/package.json b/playground/preserve-symlinks/module-a/package.json
new file mode 100644
index 00000000000000..1815df52195607
--- /dev/null
+++ b/playground/preserve-symlinks/module-a/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-module-a",
+ "private": true,
+ "version": "0.0.0",
+ "main": "linked.js"
+}
diff --git a/playground/preserve-symlinks/module-a/src/data.js b/playground/preserve-symlinks/module-a/src/data.js
new file mode 100644
index 00000000000000..5e21c75a71d952
--- /dev/null
+++ b/playground/preserve-symlinks/module-a/src/data.js
@@ -0,0 +1,3 @@
+export const data = {
+ msg: 'hello vite',
+}
diff --git a/packages/playground/preserve-symlinks/moduleA/src/index.js b/playground/preserve-symlinks/module-a/src/index.js
similarity index 100%
rename from packages/playground/preserve-symlinks/moduleA/src/index.js
rename to playground/preserve-symlinks/module-a/src/index.js
diff --git a/playground/preserve-symlinks/package.json b/playground/preserve-symlinks/package.json
new file mode 100644
index 00000000000000..8a68c00cc82ddf
--- /dev/null
+++ b/playground/preserve-symlinks/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@vitejs/test-preserve-symlinks",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --force",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@vitejs/test-module-a": "link:./module-a"
+ }
+}
diff --git a/playground/preserve-symlinks/src/main.js b/playground/preserve-symlinks/src/main.js
new file mode 100644
index 00000000000000..346cefae5b84d3
--- /dev/null
+++ b/playground/preserve-symlinks/src/main.js
@@ -0,0 +1,3 @@
+import { sayHi } from '@vitejs/test-module-a'
+
+document.getElementById('root').innerText = sayHi().msg
diff --git a/playground/proxy-bypass/__tests__/proxy-bypass.spec.ts b/playground/proxy-bypass/__tests__/proxy-bypass.spec.ts
new file mode 100644
index 00000000000000..b133f543f2e813
--- /dev/null
+++ b/playground/proxy-bypass/__tests__/proxy-bypass.spec.ts
@@ -0,0 +1,19 @@
+import { expect, test, vi } from 'vitest'
+import { browserLogs, page, serverLogs } from '~utils'
+
+test('proxy-bypass', async () => {
+ await vi.waitFor(() => {
+ expect(browserLogs.join('\n')).toContain('status of 404 (Not Found)')
+ })
+})
+
+test('async-proxy-bypass', async () => {
+ const content = await page.frame('async-response').content()
+ expect(content).toContain('Hello after 4 ms (async timeout)')
+})
+
+test('async-proxy-bypass-with-error', async () => {
+ await vi.waitFor(() => {
+ expect(serverLogs.join('\n')).toContain('bypass error')
+ })
+})
diff --git a/playground/proxy-bypass/index.html b/playground/proxy-bypass/index.html
new file mode 100644
index 00000000000000..e41b3708cfa0e3
--- /dev/null
+++ b/playground/proxy-bypass/index.html
@@ -0,0 +1,4 @@
+root app
+
+
+
diff --git a/playground/proxy-bypass/package.json b/playground/proxy-bypass/package.json
new file mode 100644
index 00000000000000..fa9ecab396977a
--- /dev/null
+++ b/playground/proxy-bypass/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@vitejs/test-proxy-bypass",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/proxy-bypass/vite.config.js b/playground/proxy-bypass/vite.config.js
new file mode 100644
index 00000000000000..1b40aebe5b2968
--- /dev/null
+++ b/playground/proxy-bypass/vite.config.js
@@ -0,0 +1,47 @@
+import { defineConfig } from 'vite'
+
+const timeout = (ms) => new Promise((r) => setTimeout(r, ms))
+
+export default defineConfig({
+ server: {
+ port: 9606,
+ proxy: {
+ '/nonExistentApp': {
+ target: 'http://localhost:9607',
+ bypass: () => {
+ return false
+ },
+ },
+ '/asyncResponse': {
+ bypass: async (_, res) => {
+ await timeout(4)
+ res.writeHead(200, {
+ 'Content-Type': 'text/plain',
+ })
+ res.end('Hello after 4 ms (async timeout)')
+ return '/asyncResponse'
+ },
+ },
+ '/asyncThrowingError': {
+ bypass: async () => {
+ await timeout(4)
+ throw new Error('bypass error')
+ },
+ },
+ },
+ },
+ plugins: [
+ {
+ name: 'handle-error-in-preview',
+ configurePreviewServer({ config, middlewares }) {
+ return () => {
+ middlewares.use((err, _req, res, _next) => {
+ config.logger.error(err.message, { error: err })
+ res.statusCode = 500
+ res.end()
+ })
+ }
+ },
+ },
+ ],
+})
diff --git a/playground/proxy-hmr/__tests__/proxy-hmr.spec.ts b/playground/proxy-hmr/__tests__/proxy-hmr.spec.ts
new file mode 100644
index 00000000000000..ca3edd739de9c6
--- /dev/null
+++ b/playground/proxy-hmr/__tests__/proxy-hmr.spec.ts
@@ -0,0 +1,27 @@
+import { test } from 'vitest'
+import {
+ editFile,
+ isBuild,
+ page,
+ untilBrowserLogAfter,
+ untilUpdated,
+ viteTestUrl,
+} from '~utils'
+
+test.runIf(!isBuild)('proxy-hmr', async () => {
+ await untilBrowserLogAfter(
+ () => page.goto(viteTestUrl),
+ // wait for both main and sub app HMR connection
+ [/connected/, /connected/],
+ )
+
+ const otherAppTextLocator = page.frameLocator('iframe').locator('.content')
+ await untilUpdated(() => otherAppTextLocator.textContent(), 'other app')
+ editFile('other-app/index.html', (code) =>
+ code.replace('app', 'modified app'),
+ )
+ await untilUpdated(
+ () => otherAppTextLocator.textContent(),
+ 'other modified app',
+ )
+})
diff --git a/playground/proxy-hmr/__tests__/serve.ts b/playground/proxy-hmr/__tests__/serve.ts
new file mode 100644
index 00000000000000..9cbeef96edcbb2
--- /dev/null
+++ b/playground/proxy-hmr/__tests__/serve.ts
@@ -0,0 +1,27 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+import path from 'node:path'
+import { rootDir, setViteUrl } from '~utils'
+
+export async function serve(): Promise<{ close(): Promise }> {
+ const vite = await import('vite')
+ const rootServer = await vite.createServer({
+ root: rootDir,
+ logLevel: 'silent',
+ })
+ const otherServer = await vite.createServer({
+ root: path.join(rootDir, 'other-app'),
+ logLevel: 'silent',
+ })
+
+ await Promise.all([rootServer.listen(), otherServer.listen()])
+ const viteUrl = rootServer.resolvedUrls.local[0]
+ setViteUrl(viteUrl)
+
+ return {
+ async close() {
+ await Promise.all([rootServer.close(), otherServer.close()])
+ },
+ }
+}
diff --git a/playground/proxy-hmr/index.html b/playground/proxy-hmr/index.html
new file mode 100644
index 00000000000000..f14fde8e428635
--- /dev/null
+++ b/playground/proxy-hmr/index.html
@@ -0,0 +1,2 @@
+root app
+
diff --git a/playground/proxy-hmr/other-app/index.html b/playground/proxy-hmr/other-app/index.html
new file mode 100644
index 00000000000000..18f42b0b93d11c
--- /dev/null
+++ b/playground/proxy-hmr/other-app/index.html
@@ -0,0 +1 @@
+other app
diff --git a/playground/proxy-hmr/other-app/package.json b/playground/proxy-hmr/other-app/package.json
new file mode 100644
index 00000000000000..4457d4da6ae217
--- /dev/null
+++ b/playground/proxy-hmr/other-app/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@vitejs/test-other-app",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/proxy-hmr/other-app/vite.config.js b/playground/proxy-hmr/other-app/vite.config.js
new file mode 100644
index 00000000000000..9fb3ece5eb9c3c
--- /dev/null
+++ b/playground/proxy-hmr/other-app/vite.config.js
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ base: '/anotherApp',
+ server: {
+ port: 9617,
+ strictPort: true,
+ },
+})
diff --git a/playground/proxy-hmr/package.json b/playground/proxy-hmr/package.json
new file mode 100644
index 00000000000000..3aff8b1c8936fd
--- /dev/null
+++ b/playground/proxy-hmr/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@vitejs/test-proxy-hmr",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ }
+}
diff --git a/playground/proxy-hmr/vite.config.js b/playground/proxy-hmr/vite.config.js
new file mode 100644
index 00000000000000..ef57a85757a82a
--- /dev/null
+++ b/playground/proxy-hmr/vite.config.js
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ server: {
+ port: 9616,
+ proxy: {
+ '/anotherApp': {
+ target: 'http://localhost:9617',
+ ws: true,
+ },
+ },
+ },
+})
diff --git a/packages/playground/resolve-linked/dep.js b/playground/resolve-linked/dep.js
similarity index 100%
rename from packages/playground/resolve-linked/dep.js
rename to playground/resolve-linked/dep.js
diff --git a/playground/resolve-linked/package.json b/playground/resolve-linked/package.json
new file mode 100644
index 00000000000000..a95a9ff5562bc0
--- /dev/null
+++ b/playground/resolve-linked/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-resolve-linked",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "src/index.js"
+}
diff --git a/packages/playground/resolve-linked/src/index.js b/playground/resolve-linked/src/index.js
similarity index 100%
rename from packages/playground/resolve-linked/src/index.js
rename to playground/resolve-linked/src/index.js
diff --git a/playground/resolve/__tests__/mainfields-custom-first/resolve-mainfields-custom-first.spec.ts b/playground/resolve/__tests__/mainfields-custom-first/resolve-mainfields-custom-first.spec.ts
new file mode 100644
index 00000000000000..c15f0f56d72375
--- /dev/null
+++ b/playground/resolve/__tests__/mainfields-custom-first/resolve-mainfields-custom-first.spec.ts
@@ -0,0 +1,8 @@
+import { expect, test } from 'vitest'
+import { page } from '~utils'
+
+test('resolve.mainFields.custom-first', async () => {
+ expect(await page.textContent('.custom-browser-main-field')).toBe(
+ 'resolved custom field',
+ )
+})
diff --git a/playground/resolve/__tests__/resolve.spec.ts b/playground/resolve/__tests__/resolve.spec.ts
new file mode 100644
index 00000000000000..d5d11f4a7b08ce
--- /dev/null
+++ b/playground/resolve/__tests__/resolve.spec.ts
@@ -0,0 +1,258 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { expect, test } from 'vitest'
+import { isBuild, isWindows, page, testDir, viteTestUrl } from '~utils'
+
+test('bom import', async () => {
+ expect(await page.textContent('.utf8-bom')).toMatch('[success]')
+})
+
+test('deep import', async () => {
+ expect(await page.textContent('.deep-import')).toMatch('[2,4]')
+})
+
+test('exports and a nested package scope with a different type', async () => {
+ expect(await page.textContent('.exports-and-nested-scope')).toMatch(
+ '[success]',
+ )
+})
+
+test('entry with exports field', async () => {
+ expect(await page.textContent('.exports-entry')).toMatch('[success]')
+})
+
+test('deep import with exports field', async () => {
+ expect(await page.textContent('.exports-deep')).toMatch('[success]')
+})
+
+test('deep import with query with exports field', async () => {
+ // since it is imported with `?url` it should return a URL
+ expect(await page.textContent('.exports-deep-query')).toMatch(
+ isBuild ? /base64/ : '/exports-path/deep.json',
+ )
+})
+
+test('deep import with exports field + exposed dir', async () => {
+ expect(await page.textContent('.exports-deep-exposed-dir')).toMatch(
+ '[success]',
+ )
+})
+
+test('deep import with exports field + mapped dir', async () => {
+ expect(await page.textContent('.exports-deep-mapped-dir')).toMatch(
+ '[success]',
+ )
+})
+
+test('exports read from the root package.json', async () => {
+ expect(await page.textContent('.exports-from-root')).toMatch('[success]')
+})
+
+// this is how Svelte 3 is packaged
+test('deep import with exports and legacy fallback', async () => {
+ expect(await page.textContent('.exports-legacy-fallback')).toMatch(
+ '[success]',
+ )
+})
+
+test('Respect exports field env key priority', async () => {
+ expect(await page.textContent('.exports-env')).toMatch('[success]')
+})
+
+test('Respect production/development conditionals', async () => {
+ expect(await page.textContent('.exports-env')).toMatch(
+ isBuild ? `browser.prod.mjs` : `browser.mjs`,
+ )
+})
+
+test('Respect exports to take precedence over mainFields', async () => {
+ expect(await page.textContent('.exports-with-module')).toMatch('[success]')
+})
+
+test('import and require resolve using module condition', async () => {
+ expect(await page.textContent('.exports-with-module-condition')).toMatch(
+ '[success]',
+ )
+ expect(
+ await page.textContent('.exports-with-module-condition-required'),
+ ).toMatch('[success]')
+})
+
+test('implicit dir/index.js', async () => {
+ expect(await page.textContent('.index')).toMatch('[success]')
+})
+
+test('implicit dir/index.js vs explicit file', async () => {
+ expect(await page.textContent('.dir-vs-file')).toMatch('[success]')
+})
+
+test('nested extension', async () => {
+ expect(await page.textContent('.nested-extension')).toMatch(
+ '[success] file.json.js',
+ )
+})
+
+test('exact extension vs. duplicated (.js.js)', async () => {
+ expect(await page.textContent('.exact-extension')).toMatch('[success]')
+})
+
+test('dont add extension to directory name (./dir-with-ext.js/index.js)', async () => {
+ expect(await page.textContent('.dir-with-ext')).toMatch('[success]')
+})
+
+test('do not resolve to the `module` field if the importer is a `require` call', async () => {
+ expect(await page.textContent('.require-pkg-with-module-field')).toMatch(
+ '[success]',
+ )
+})
+
+test('a ts module can import another ts module using its corresponding js file name', async () => {
+ expect(await page.textContent('.ts-extension')).toMatch('[success]')
+})
+
+test('a js module can import another ts module using its corresponding js file name', async () => {
+ expect(await page.textContent('.js-ts-extension')).toMatch('[success]')
+})
+
+test('filename with dot', async () => {
+ expect(await page.textContent('.dot')).toMatch('[success]')
+})
+
+test.runIf(isWindows)('drive-relative path', async () => {
+ expect(await page.textContent('.drive-relative')).toMatch('[success]')
+})
+
+test('absolute path', async () => {
+ expect(await page.textContent('.absolute')).toMatch('[success]')
+})
+
+test('file url', async () => {
+ expect(await page.textContent('.file-url')).toMatch('[success]')
+})
+
+test('browser field', async () => {
+ expect(await page.textContent('.browser')).toMatch('[success]')
+})
+
+test('Resolve browser field even if module field exists', async () => {
+ expect(await page.textContent('.browser-module1')).toMatch('[success]')
+})
+
+test('Resolve module field if browser field is likely UMD or CJS', async () => {
+ expect(await page.textContent('.browser-module2')).toMatch('[success]')
+})
+
+test('Resolve module field if browser field is likely IIFE', async () => {
+ expect(await page.textContent('.browser-module3')).toMatch('[success]')
+})
+
+test('css entry', async () => {
+ expect(await page.textContent('.css')).toMatch('[success]')
+})
+
+test('monorepo linked dep', async () => {
+ expect(await page.textContent('.monorepo')).toMatch('[success]')
+})
+
+test('plugin resolved virtual file', async () => {
+ expect(await page.textContent('.virtual')).toMatch('[success]')
+})
+
+test('plugin resolved custom virtual file', async () => {
+ expect(await page.textContent('.custom-virtual')).toMatch('[success]')
+})
+
+test('resolve inline package', async () => {
+ expect(await page.textContent('.inline-pkg')).toMatch('[success]')
+})
+
+test('resolve.extensions', async () => {
+ expect(await page.textContent('.custom-ext')).toMatch('[success]')
+})
+
+test('resolve.mainFields', async () => {
+ expect(await page.textContent('.custom-main-fields')).toMatch('[success]')
+})
+
+test('resolve.mainFields.browser-first', async () => {
+ expect(await page.textContent('.custom-browser-main-field')).toBe(
+ 'resolved browser field',
+ )
+})
+
+test('resolve.conditions', async () => {
+ expect(await page.textContent('.custom-condition')).toMatch('[success]')
+})
+
+test('resolve package that contains # in path', async () => {
+ expect(await page.textContent('.path-contains-sharp-symbol')).toMatch(
+ '[success] true #',
+ )
+})
+
+test('Resolving top level with imports field', async () => {
+ expect(await page.textContent('.imports-top-level')).toMatch('[success]')
+})
+
+test('Resolving same level with imports field', async () => {
+ expect(await page.textContent('.imports-same-level')).toMatch(
+ await page.textContent('.imports-top-level'),
+ )
+})
+
+test('Resolving nested path with imports field', async () => {
+ expect(await page.textContent('.imports-nested')).toMatch('[success]')
+})
+
+test('Resolving star with imports filed', async () => {
+ expect(await page.textContent('.imports-star')).toMatch('[success]')
+})
+
+test('Resolving slash with imports filed', async () => {
+ expect(await page.textContent('.imports-slash')).toMatch('[success]')
+})
+
+test('Resolving from other package with imports field', async () => {
+ expect(await page.textContent('.imports-pkg-slash')).toMatch('[success]')
+})
+
+test('Resolving with query with imports field', async () => {
+ // since it is imported with `?url` it should return a URL
+ expect(await page.textContent('.imports-query')).toMatch(
+ isBuild ? /base64/ : '/imports-path/query.json',
+ )
+})
+
+test('Resolve doesnt interrupt page request with trailing query and .css', async () => {
+ await page.goto(viteTestUrl + '/?test.css')
+ expect(await page.locator('vite-error-overlay').count()).toBe(0)
+ expect(await page.textContent('h1')).toBe('Resolve')
+})
+
+test('resolve non-normalized absolute path', async () => {
+ expect(await page.textContent('.non-normalized')).toMatch('[success]')
+})
+
+test.runIf(!isWindows)(
+ 'Resolve doesnt interrupt page request that clashes with local project package.json',
+ async () => {
+ // Sometimes request path may point to a different project's package.json, but for testing
+ // we point to Vite's own monorepo which always exists, and the package.json is not a library
+ const pathToViteMonorepoRoot = new URL('../../../', import.meta.url)
+ const urlPath = fileURLToPath(pathToViteMonorepoRoot).replace(/\/$/, '')
+ await page.goto(viteTestUrl + urlPath)
+ expect(await page.locator('vite-error-overlay').count()).toBe(0)
+ expect(await page.textContent('h1')).toBe('Resolve')
+ },
+)
+
+test.runIf(isBuild)('public dir is not copied', async () => {
+ expect(
+ fs.existsSync(path.resolve(testDir, 'dist/should-not-be-copied')),
+ ).toBe(false)
+})
+
+test('import utf8-bom package', async () => {
+ expect(await page.textContent('.utf8-bom-package')).toMatch('[success]')
+})
diff --git a/playground/resolve/absolute.js b/playground/resolve/absolute.js
new file mode 100644
index 00000000000000..c0581888ebb90b
--- /dev/null
+++ b/playground/resolve/absolute.js
@@ -0,0 +1 @@
+export default '[success] absolute'
diff --git a/playground/resolve/browser-field-bare-import-fail/main.js b/playground/resolve/browser-field-bare-import-fail/main.js
new file mode 100644
index 00000000000000..24175aeca1ad0a
--- /dev/null
+++ b/playground/resolve/browser-field-bare-import-fail/main.js
@@ -0,0 +1 @@
+export default '[fail]'
diff --git a/playground/resolve/browser-field-bare-import-fail/module.js b/playground/resolve/browser-field-bare-import-fail/module.js
new file mode 100644
index 00000000000000..24175aeca1ad0a
--- /dev/null
+++ b/playground/resolve/browser-field-bare-import-fail/module.js
@@ -0,0 +1 @@
+export default '[fail]'
diff --git a/playground/resolve/browser-field-bare-import-fail/package.json b/playground/resolve/browser-field-bare-import-fail/package.json
new file mode 100644
index 00000000000000..a983eeb1b2ea82
--- /dev/null
+++ b/playground/resolve/browser-field-bare-import-fail/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@vitejs/test-resolve-browser-field-bare-import-fail",
+ "private": true,
+ "version": "1.0.0",
+ "main": "main.js",
+ "module": "module.js",
+ "type": "module"
+}
diff --git a/playground/resolve/browser-field-bare-import-success/main.js b/playground/resolve/browser-field-bare-import-success/main.js
new file mode 100644
index 00000000000000..24175aeca1ad0a
--- /dev/null
+++ b/playground/resolve/browser-field-bare-import-success/main.js
@@ -0,0 +1 @@
+export default '[fail]'
diff --git a/playground/resolve/browser-field-bare-import-success/module.js b/playground/resolve/browser-field-bare-import-success/module.js
new file mode 100644
index 00000000000000..2ecbfe1a42cf13
--- /dev/null
+++ b/playground/resolve/browser-field-bare-import-success/module.js
@@ -0,0 +1 @@
+export default '[success]'
diff --git a/playground/resolve/browser-field-bare-import-success/package.json b/playground/resolve/browser-field-bare-import-success/package.json
new file mode 100644
index 00000000000000..d4e19ab1a0facc
--- /dev/null
+++ b/playground/resolve/browser-field-bare-import-success/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@vitejs/test-resolve-browser-field-bare-import-success",
+ "private": true,
+ "version": "1.0.0",
+ "main": "main.js",
+ "module": "module.js",
+ "type": "module"
+}
diff --git a/playground/resolve/browser-field/bare-import.js b/playground/resolve/browser-field/bare-import.js
new file mode 100644
index 00000000000000..9c86e347424176
--- /dev/null
+++ b/playground/resolve/browser-field/bare-import.js
@@ -0,0 +1,2 @@
+import message from '@vitejs/test-resolve-browser-field-bare-import-fail'
+export default message
diff --git a/playground/resolve/browser-field/multiple.dot.path.js b/playground/resolve/browser-field/multiple.dot.path.js
new file mode 100644
index 00000000000000..37bd1099aecc0e
--- /dev/null
+++ b/playground/resolve/browser-field/multiple.dot.path.js
@@ -0,0 +1,2 @@
+const fs = require('node:fs')
+console.log('this should not run in the browser')
diff --git a/playground/resolve/browser-field/no-ext-index/index.js b/playground/resolve/browser-field/no-ext-index/index.js
new file mode 100644
index 00000000000000..cfce659df53665
--- /dev/null
+++ b/playground/resolve/browser-field/no-ext-index/index.js
@@ -0,0 +1,2 @@
+import jsdom from 'jsdom' // should be redirected to empty module
+export default ''
diff --git a/playground/resolve/browser-field/no-ext.js b/playground/resolve/browser-field/no-ext.js
new file mode 100644
index 00000000000000..cfce659df53665
--- /dev/null
+++ b/playground/resolve/browser-field/no-ext.js
@@ -0,0 +1,2 @@
+import jsdom from 'jsdom' // should be redirected to empty module
+export default ''
diff --git a/playground/resolve/browser-field/not-browser.js b/playground/resolve/browser-field/not-browser.js
new file mode 100644
index 00000000000000..37bd1099aecc0e
--- /dev/null
+++ b/playground/resolve/browser-field/not-browser.js
@@ -0,0 +1,2 @@
+const fs = require('node:fs')
+console.log('this should not run in the browser')
diff --git a/packages/playground/resolve/browser-field/out/cjs.node.js b/playground/resolve/browser-field/out/cjs.node.js
similarity index 100%
rename from packages/playground/resolve/browser-field/out/cjs.node.js
rename to playground/resolve/browser-field/out/cjs.node.js
diff --git a/playground/resolve/browser-field/out/esm.browser.js b/playground/resolve/browser-field/out/esm.browser.js
new file mode 100644
index 00000000000000..b473ac9059ee74
--- /dev/null
+++ b/playground/resolve/browser-field/out/esm.browser.js
@@ -0,0 +1,2 @@
+import jsdom from 'jsdom' // should be redirected to empty module
+export default '[success] resolve browser field'
diff --git a/playground/resolve/browser-field/package.json b/playground/resolve/browser-field/package.json
new file mode 100644
index 00000000000000..76b67bbaf75ddf
--- /dev/null
+++ b/playground/resolve/browser-field/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@vitejs/test-resolve-browser-field",
+ "private": true,
+ "version": "1.0.0",
+ "//": "real world example: https://github.com/axios/axios/blob/3f2ef030e001547eb06060499f8a2e3f002b5a14/package.json#L71-L73",
+ "main": "out/cjs.node.js",
+ "browser": {
+ "./out/cjs.node.js": "./out/esm.browser.js",
+ "./no-ext": "./out/esm.browser.js",
+ "./ext.js": "./out/esm.browser.js",
+ "./ext-index/index.js": "./out/esm.browser.js",
+ "./no-ext-index": "./out/esm.browser.js",
+ "./bare-import": "./bare-import.js",
+ "./not-browser.js": false,
+ "./multiple.dot.path.js": false,
+ "jsdom": false,
+ "@vitejs/test-resolve-browser-field-bare-import-fail": "@vitejs/test-resolve-browser-field-bare-import-success"
+ },
+ "dependencies": {
+ "@vitejs/test-resolve-browser-field-bare-import-fail": "link:../browser-field-bare-import-fail",
+ "@vitejs/test-resolve-browser-field-bare-import-success": "link:../browser-field-bare-import-success"
+ }
+}
diff --git a/packages/playground/resolve/browser-field/relative.js b/playground/resolve/browser-field/relative.js
similarity index 86%
rename from packages/playground/resolve/browser-field/relative.js
rename to playground/resolve/browser-field/relative.js
index bbf6a2c74a10b5..660d6be578a728 100644
--- a/packages/playground/resolve/browser-field/relative.js
+++ b/playground/resolve/browser-field/relative.js
@@ -1,3 +1,4 @@
+/* eslint-disable import-x/no-duplicates */
import ra from './no-ext'
import rb from './no-ext.js' // no substitution
import rc from './ext'
diff --git a/playground/resolve/browser-module-field1/index.js b/playground/resolve/browser-module-field1/index.js
new file mode 100644
index 00000000000000..ce45a76e78514d
--- /dev/null
+++ b/playground/resolve/browser-module-field1/index.js
@@ -0,0 +1 @@
+export default '[fail] this should not run in the browser'
diff --git a/playground/resolve/browser-module-field1/index.web.js b/playground/resolve/browser-module-field1/index.web.js
new file mode 100644
index 00000000000000..99af62f8e3700e
--- /dev/null
+++ b/playground/resolve/browser-module-field1/index.web.js
@@ -0,0 +1 @@
+export default '[success] this should run in browser'
diff --git a/playground/resolve/browser-module-field1/package.json b/playground/resolve/browser-module-field1/package.json
new file mode 100644
index 00000000000000..f9f9bd2f41464f
--- /dev/null
+++ b/playground/resolve/browser-module-field1/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@vitejs/test-resolve-browser-module-field1",
+ "private": true,
+ "version": "1.0.0",
+ "//": "real world example: https://github.com/aws/aws-sdk-js-v3/blob/59cdfd81452bce16bb26d07668e5550ed05d9d06/packages/credential-providers/package.json#L6-L7",
+ "module": "index.js",
+ "browser": "index.web.js"
+}
diff --git a/playground/resolve/browser-module-field2/index.js b/playground/resolve/browser-module-field2/index.js
new file mode 100644
index 00000000000000..99af62f8e3700e
--- /dev/null
+++ b/playground/resolve/browser-module-field2/index.js
@@ -0,0 +1 @@
+export default '[success] this should run in browser'
diff --git a/playground/resolve/browser-module-field2/index.web.js b/playground/resolve/browser-module-field2/index.web.js
new file mode 100644
index 00000000000000..172aa9928c86ae
--- /dev/null
+++ b/playground/resolve/browser-module-field2/index.web.js
@@ -0,0 +1 @@
+module.exports = '[fail] this should not run in the browser'
diff --git a/playground/resolve/browser-module-field2/package.json b/playground/resolve/browser-module-field2/package.json
new file mode 100644
index 00000000000000..f496de2b9cbcf4
--- /dev/null
+++ b/playground/resolve/browser-module-field2/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-resolve-browser-module-field2",
+ "private": true,
+ "version": "1.0.0",
+ "module": "index.js",
+ "browser": "index.web.js"
+}
diff --git a/playground/resolve/browser-module-field3/index.js b/playground/resolve/browser-module-field3/index.js
new file mode 100644
index 00000000000000..99af62f8e3700e
--- /dev/null
+++ b/playground/resolve/browser-module-field3/index.js
@@ -0,0 +1 @@
+export default '[success] this should run in browser'
diff --git a/playground/resolve/browser-module-field3/index.web.js b/playground/resolve/browser-module-field3/index.web.js
new file mode 100644
index 00000000000000..843b376e2c4daa
--- /dev/null
+++ b/playground/resolve/browser-module-field3/index.web.js
@@ -0,0 +1,7 @@
+var browserModuleField3 = (function () {
+ 'use strict'
+
+ var main = '[fail] this should not run in the browser'
+
+ return main
+})()
diff --git a/playground/resolve/browser-module-field3/package.json b/playground/resolve/browser-module-field3/package.json
new file mode 100644
index 00000000000000..db4afca86dc5cd
--- /dev/null
+++ b/playground/resolve/browser-module-field3/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-resolve-browser-module-field3",
+ "private": true,
+ "version": "1.0.0",
+ "module": "index.js",
+ "browser": "index.web.js"
+}
diff --git a/playground/resolve/config-dep.cjs b/playground/resolve/config-dep.cjs
new file mode 100644
index 00000000000000..167d270797e4c2
--- /dev/null
+++ b/playground/resolve/config-dep.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+ a: 1,
+}
diff --git a/playground/resolve/custom-browser-main-field/index.browser.js b/playground/resolve/custom-browser-main-field/index.browser.js
new file mode 100644
index 00000000000000..a12f4a603c068d
--- /dev/null
+++ b/playground/resolve/custom-browser-main-field/index.browser.js
@@ -0,0 +1 @@
+export const msg = 'resolved browser field'
diff --git a/playground/resolve/custom-browser-main-field/index.custom.js b/playground/resolve/custom-browser-main-field/index.custom.js
new file mode 100644
index 00000000000000..01ea529fad19b5
--- /dev/null
+++ b/playground/resolve/custom-browser-main-field/index.custom.js
@@ -0,0 +1 @@
+export const msg = 'resolved custom field'
diff --git a/playground/resolve/custom-browser-main-field/index.js b/playground/resolve/custom-browser-main-field/index.js
new file mode 100644
index 00000000000000..27f9fcfd43658b
--- /dev/null
+++ b/playground/resolve/custom-browser-main-field/index.js
@@ -0,0 +1 @@
+export const msg = '[fail] resolved main field'
diff --git a/playground/resolve/custom-browser-main-field/package.json b/playground/resolve/custom-browser-main-field/package.json
new file mode 100644
index 00000000000000..0d372bc5eba9fc
--- /dev/null
+++ b/playground/resolve/custom-browser-main-field/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@vitejs/test-resolve-custom-browser-main-field",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.js",
+ "browser": "index.browser.js",
+ "custom": "index.custom.js"
+}
diff --git a/packages/playground/resolve/custom-condition/index.custom.js b/playground/resolve/custom-condition/index.custom.js
similarity index 100%
rename from packages/playground/resolve/custom-condition/index.custom.js
rename to playground/resolve/custom-condition/index.custom.js
diff --git a/packages/playground/resolve/custom-condition/index.js b/playground/resolve/custom-condition/index.js
similarity index 100%
rename from packages/playground/resolve/custom-condition/index.js
rename to playground/resolve/custom-condition/index.js
diff --git a/playground/resolve/custom-condition/package.json b/playground/resolve/custom-condition/package.json
new file mode 100644
index 00000000000000..1a4b21fdf63ac0
--- /dev/null
+++ b/playground/resolve/custom-condition/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@vitejs/test-resolve-custom-condition",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.js",
+ "exports": {
+ ".": {
+ "custom": "./index.custom.js",
+ "import": "./index.js",
+ "require": "./index.js"
+ }
+ }
+}
diff --git a/packages/playground/resolve/custom-ext.es b/playground/resolve/custom-ext.es
similarity index 100%
rename from packages/playground/resolve/custom-ext.es
rename to playground/resolve/custom-ext.es
diff --git a/packages/playground/resolve/custom-main-field/index.custom.js b/playground/resolve/custom-main-field/index.custom.js
similarity index 100%
rename from packages/playground/resolve/custom-main-field/index.custom.js
rename to playground/resolve/custom-main-field/index.custom.js
diff --git a/packages/playground/resolve/custom-main-field/index.js b/playground/resolve/custom-main-field/index.js
similarity index 100%
rename from packages/playground/resolve/custom-main-field/index.js
rename to playground/resolve/custom-main-field/index.js
diff --git a/playground/resolve/custom-main-field/package.json b/playground/resolve/custom-main-field/package.json
new file mode 100644
index 00000000000000..9159d350a83646
--- /dev/null
+++ b/playground/resolve/custom-main-field/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-resolve-custom-main-field",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.js",
+ "custom": "index.custom.js"
+}
diff --git a/playground/resolve/dir-with-ext.js/empty b/playground/resolve/dir-with-ext.js/empty
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/packages/playground/resolve/dir-with-ext/index.js b/playground/resolve/dir-with-ext/index.js
similarity index 100%
rename from packages/playground/resolve/dir-with-ext/index.js
rename to playground/resolve/dir-with-ext/index.js
diff --git a/packages/playground/resolve/dir.js b/playground/resolve/dir.js
similarity index 100%
rename from packages/playground/resolve/dir.js
rename to playground/resolve/dir.js
diff --git a/packages/playground/resolve/dir/index.js b/playground/resolve/dir/index.js
similarity index 100%
rename from packages/playground/resolve/dir/index.js
rename to playground/resolve/dir/index.js
diff --git a/playground/resolve/drive-relative.js b/playground/resolve/drive-relative.js
new file mode 100644
index 00000000000000..188ac36e661b35
--- /dev/null
+++ b/playground/resolve/drive-relative.js
@@ -0,0 +1 @@
+export default '[success] drive relative'
diff --git a/packages/playground/resolve/exact-extension/file.js b/playground/resolve/exact-extension/file.js
similarity index 100%
rename from packages/playground/resolve/exact-extension/file.js
rename to playground/resolve/exact-extension/file.js
diff --git a/packages/playground/resolve/exact-extension/file.js.js b/playground/resolve/exact-extension/file.js.js
similarity index 100%
rename from packages/playground/resolve/exact-extension/file.js.js
rename to playground/resolve/exact-extension/file.js.js
diff --git a/playground/resolve/exact-extension/file.json.js b/playground/resolve/exact-extension/file.json.js
new file mode 100644
index 00000000000000..a22e93aaefa525
--- /dev/null
+++ b/playground/resolve/exact-extension/file.json.js
@@ -0,0 +1 @@
+export const file = '[success] file.json.js'
diff --git a/playground/resolve/exports-and-nested-scope/index.js b/playground/resolve/exports-and-nested-scope/index.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/resolve/exports-and-nested-scope/nested-scope/file.js b/playground/resolve/exports-and-nested-scope/nested-scope/file.js
new file mode 100644
index 00000000000000..b183d5649f6ece
--- /dev/null
+++ b/playground/resolve/exports-and-nested-scope/nested-scope/file.js
@@ -0,0 +1,4 @@
+'use strict'
+
+// intentionally use the default export here since default import from CJS has different semantics in node
+export default '[success] ES .js file within root that has type: commonjs (thanks to a package scope)'
diff --git a/playground/resolve/exports-and-nested-scope/nested-scope/package.json b/playground/resolve/exports-and-nested-scope/nested-scope/package.json
new file mode 100644
index 00000000000000..e986b24bbae58b
--- /dev/null
+++ b/playground/resolve/exports-and-nested-scope/nested-scope/package.json
@@ -0,0 +1,4 @@
+{
+ "private": true,
+ "type": "module"
+}
diff --git a/playground/resolve/exports-and-nested-scope/package.json b/playground/resolve/exports-and-nested-scope/package.json
new file mode 100644
index 00000000000000..7d40b793f6c055
--- /dev/null
+++ b/playground/resolve/exports-and-nested-scope/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-resolve-exports-and-nested-scope",
+ "private": true,
+ "version": "1.0.0",
+ "type": "commonjs",
+ "exports": {
+ ".": "./index.js",
+ "./nested": "./nested-scope/file.js"
+ }
+}
diff --git a/packages/playground/resolve/exports-env/browser.js b/playground/resolve/exports-env/browser.js
similarity index 100%
rename from packages/playground/resolve/exports-env/browser.js
rename to playground/resolve/exports-env/browser.js
diff --git a/packages/playground/resolve/exports-env/browser.mjs b/playground/resolve/exports-env/browser.mjs
similarity index 100%
rename from packages/playground/resolve/exports-env/browser.mjs
rename to playground/resolve/exports-env/browser.mjs
diff --git a/packages/playground/resolve/exports-env/browser.prod.mjs b/playground/resolve/exports-env/browser.prod.mjs
similarity index 100%
rename from packages/playground/resolve/exports-env/browser.prod.mjs
rename to playground/resolve/exports-env/browser.prod.mjs
diff --git a/packages/playground/resolve/exports-env/fallback.umd.js b/playground/resolve/exports-env/fallback.umd.js
similarity index 100%
rename from packages/playground/resolve/exports-env/fallback.umd.js
rename to playground/resolve/exports-env/fallback.umd.js
diff --git a/playground/resolve/exports-env/package.json b/playground/resolve/exports-env/package.json
new file mode 100644
index 00000000000000..5db008e4bbb1b5
--- /dev/null
+++ b/playground/resolve/exports-env/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-resolve-exports-env",
+ "private": true,
+ "version": "1.0.0",
+ "exports": {
+ "import": {
+ "browser": {
+ "production": "./browser.prod.mjs",
+ "development": "./browser.mjs"
+ }
+ },
+ "browser": "./browser.js",
+ "default": "./fallback.umd.js"
+ }
+}
diff --git a/playground/resolve/exports-from-root/file.js b/playground/resolve/exports-from-root/file.js
new file mode 100644
index 00000000000000..c97992c496dde1
--- /dev/null
+++ b/playground/resolve/exports-from-root/file.js
@@ -0,0 +1 @@
+export const msg = '[success] exports from root (./file.js)'
diff --git a/playground/resolve/exports-from-root/index.js b/playground/resolve/exports-from-root/index.js
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/resolve/exports-from-root/nested/file.js b/playground/resolve/exports-from-root/nested/file.js
new file mode 100644
index 00000000000000..78e96789d86b8b
--- /dev/null
+++ b/playground/resolve/exports-from-root/nested/file.js
@@ -0,0 +1 @@
+export const msg = 'fail exports from root (./nested/file.js)'
diff --git a/playground/resolve/exports-from-root/nested/package.json b/playground/resolve/exports-from-root/nested/package.json
new file mode 100644
index 00000000000000..0908b494205729
--- /dev/null
+++ b/playground/resolve/exports-from-root/nested/package.json
@@ -0,0 +1,5 @@
+{
+ "exports": {
+ ".": "./file.js"
+ }
+}
diff --git a/playground/resolve/exports-from-root/package.json b/playground/resolve/exports-from-root/package.json
new file mode 100644
index 00000000000000..1ec99410519211
--- /dev/null
+++ b/playground/resolve/exports-from-root/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-resolve-exports-from-root",
+ "private": true,
+ "version": "1.0.0",
+ "exports": {
+ ".": "./index.js",
+ "./nested": "./file.js"
+ }
+}
diff --git a/playground/resolve/exports-legacy-fallback/dir/index.js b/playground/resolve/exports-legacy-fallback/dir/index.js
new file mode 100644
index 00000000000000..9c02b83a60f172
--- /dev/null
+++ b/playground/resolve/exports-legacy-fallback/dir/index.js
@@ -0,0 +1 @@
+export const msg = '[fail] mapped js file'
diff --git a/playground/resolve/exports-legacy-fallback/dir/index.mjs b/playground/resolve/exports-legacy-fallback/dir/index.mjs
new file mode 100644
index 00000000000000..fa498c78684744
--- /dev/null
+++ b/playground/resolve/exports-legacy-fallback/dir/index.mjs
@@ -0,0 +1 @@
+export const msg = '[success] mapped mjs file'
diff --git a/playground/resolve/exports-legacy-fallback/dir/package.json b/playground/resolve/exports-legacy-fallback/dir/package.json
new file mode 100644
index 00000000000000..4cf44a46cb30d2
--- /dev/null
+++ b/playground/resolve/exports-legacy-fallback/dir/package.json
@@ -0,0 +1,4 @@
+{
+ "main": "index.js",
+ "module": "index.mjs"
+}
diff --git a/playground/resolve/exports-legacy-fallback/index.js b/playground/resolve/exports-legacy-fallback/index.js
new file mode 100644
index 00000000000000..03677be8580ef6
--- /dev/null
+++ b/playground/resolve/exports-legacy-fallback/index.js
@@ -0,0 +1 @@
+export default 5
diff --git a/playground/resolve/exports-legacy-fallback/package.json b/playground/resolve/exports-legacy-fallback/package.json
new file mode 100644
index 00000000000000..6fc775cf65dfc1
--- /dev/null
+++ b/playground/resolve/exports-legacy-fallback/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-resolve-exports-legacy-fallback",
+ "private": true,
+ "version": "1.0.0",
+ "exports": {
+ "./dir": {
+ "import": "./dir/index.mjs",
+ "require": "./dir/index.js"
+ },
+ ".": "index.js"
+ }
+}
diff --git a/packages/playground/resolve/exports-path/cjs.js b/playground/resolve/exports-path/cjs.js
similarity index 100%
rename from packages/playground/resolve/exports-path/cjs.js
rename to playground/resolve/exports-path/cjs.js
diff --git a/packages/playground/resolve/exports-path/deep.js b/playground/resolve/exports-path/deep.js
similarity index 100%
rename from packages/playground/resolve/exports-path/deep.js
rename to playground/resolve/exports-path/deep.js
diff --git a/packages/playground/resolve/exports-path/deep.json b/playground/resolve/exports-path/deep.json
similarity index 100%
rename from packages/playground/resolve/exports-path/deep.json
rename to playground/resolve/exports-path/deep.json
diff --git a/packages/playground/resolve/exports-path/dir/dir.js b/playground/resolve/exports-path/dir/dir.js
similarity index 100%
rename from packages/playground/resolve/exports-path/dir/dir.js
rename to playground/resolve/exports-path/dir/dir.js
diff --git a/packages/playground/resolve/exports-path/main.js b/playground/resolve/exports-path/main.js
similarity index 100%
rename from packages/playground/resolve/exports-path/main.js
rename to playground/resolve/exports-path/main.js
diff --git a/playground/resolve/exports-path/package.json b/playground/resolve/exports-path/package.json
new file mode 100644
index 00000000000000..55098bd0867c7c
--- /dev/null
+++ b/playground/resolve/exports-path/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@vitejs/test-resolve-exports-path",
+ "private": true,
+ "version": "1.0.0",
+ "exports": {
+ ".": {
+ "import": "./main.js",
+ "require": "./cjs.js"
+ },
+ "./deep.js": "./deep.js",
+ "./deep.json": "./deep.json",
+ "./dir/": "./dir/",
+ "./dir-mapped/*": {
+ "import": "./dir/*",
+ "require": "./dir-cjs/*"
+ }
+ }
+}
diff --git a/playground/resolve/exports-with-module-condition-required/index.cjs b/playground/resolve/exports-with-module-condition-required/index.cjs
new file mode 100644
index 00000000000000..ad43e63153349d
--- /dev/null
+++ b/playground/resolve/exports-with-module-condition-required/index.cjs
@@ -0,0 +1,2 @@
+const { msg } = require('@vitejs/test-resolve-exports-with-module-condition')
+module.exports = { msg }
diff --git a/playground/resolve/exports-with-module-condition-required/package.json b/playground/resolve/exports-with-module-condition-required/package.json
new file mode 100644
index 00000000000000..8e1b2969fbf7f2
--- /dev/null
+++ b/playground/resolve/exports-with-module-condition-required/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-resolve-exports-with-module-condition-required",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.cjs",
+ "dependencies": {
+ "@vitejs/test-resolve-exports-with-module-condition": "link:../exports-with-module-condition"
+ }
+}
diff --git a/playground/resolve/exports-with-module-condition/index.esm.js b/playground/resolve/exports-with-module-condition/index.esm.js
new file mode 100644
index 00000000000000..f1e0b54ee0c7ac
--- /dev/null
+++ b/playground/resolve/exports-with-module-condition/index.esm.js
@@ -0,0 +1 @@
+export const msg = '[success] exports with module condition (index.esm.js)'
diff --git a/playground/resolve/exports-with-module-condition/index.js b/playground/resolve/exports-with-module-condition/index.js
new file mode 100644
index 00000000000000..d38a0e272c457d
--- /dev/null
+++ b/playground/resolve/exports-with-module-condition/index.js
@@ -0,0 +1,2 @@
+/* eslint-disable import-x/no-commonjs */
+module.exports.msg = '[fail] exports with module condition (index.js)'
diff --git a/playground/resolve/exports-with-module-condition/index.mjs b/playground/resolve/exports-with-module-condition/index.mjs
new file mode 100644
index 00000000000000..1696a05425e35f
--- /dev/null
+++ b/playground/resolve/exports-with-module-condition/index.mjs
@@ -0,0 +1 @@
+export const msg = '[fail] exports with module condition (index.mjs)'
diff --git a/playground/resolve/exports-with-module-condition/package.json b/playground/resolve/exports-with-module-condition/package.json
new file mode 100644
index 00000000000000..92a8f2dd2d1f80
--- /dev/null
+++ b/playground/resolve/exports-with-module-condition/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-resolve-exports-with-module-condition",
+ "private": true,
+ "version": "1.0.0",
+ "exports": {
+ "module": "./index.esm.js",
+ "import": "./index.mjs",
+ "require": "./index.js"
+ }
+}
diff --git a/playground/resolve/exports-with-module/import.mjs b/playground/resolve/exports-with-module/import.mjs
new file mode 100644
index 00000000000000..d97c695169a575
--- /dev/null
+++ b/playground/resolve/exports-with-module/import.mjs
@@ -0,0 +1,2 @@
+// import.mjs should take precedence
+export const msg = '[success] exports with module (import.mjs)'
diff --git a/playground/resolve/exports-with-module/module.mjs b/playground/resolve/exports-with-module/module.mjs
new file mode 100644
index 00000000000000..f9392192058822
--- /dev/null
+++ b/playground/resolve/exports-with-module/module.mjs
@@ -0,0 +1,2 @@
+// import.mjs should take precedence
+export const msg = '[fail] exports with module (module.mjs)'
diff --git a/playground/resolve/exports-with-module/package.json b/playground/resolve/exports-with-module/package.json
new file mode 100644
index 00000000000000..b3d9f7e999ca75
--- /dev/null
+++ b/playground/resolve/exports-with-module/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-resolve-exports-with-module",
+ "private": true,
+ "version": "1.0.0",
+ "type": "commonjs",
+ "module": "./module.mjs",
+ "exports": {
+ "import": "./import.mjs"
+ }
+}
diff --git a/playground/resolve/file-url.js b/playground/resolve/file-url.js
new file mode 100644
index 00000000000000..3108786390cc71
--- /dev/null
+++ b/playground/resolve/file-url.js
@@ -0,0 +1 @@
+export default '[success] file-url'
diff --git a/playground/resolve/imports-path/nested-path.js b/playground/resolve/imports-path/nested-path.js
new file mode 100644
index 00000000000000..a191fab4e01906
--- /dev/null
+++ b/playground/resolve/imports-path/nested-path.js
@@ -0,0 +1 @@
+export const msg = '[success] nested path subpath imports'
diff --git a/playground/resolve/imports-path/other-pkg/nest/index.js b/playground/resolve/imports-path/other-pkg/nest/index.js
new file mode 100644
index 00000000000000..e333ed38cad6df
--- /dev/null
+++ b/playground/resolve/imports-path/other-pkg/nest/index.js
@@ -0,0 +1 @@
+export const msg = '[success] subpath imports from other package'
diff --git a/playground/resolve/imports-path/other-pkg/package.json b/playground/resolve/imports-path/other-pkg/package.json
new file mode 100644
index 00000000000000..6ba894e8ba3e49
--- /dev/null
+++ b/playground/resolve/imports-path/other-pkg/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "@vitejs/test-resolve-imports-pkg",
+ "private": true
+}
diff --git a/playground/resolve/imports-path/query.json b/playground/resolve/imports-path/query.json
new file mode 100644
index 00000000000000..97e19265d6c843
--- /dev/null
+++ b/playground/resolve/imports-path/query.json
@@ -0,0 +1,3 @@
+{
+ "foo": "json"
+}
diff --git a/playground/resolve/imports-path/same-level.js b/playground/resolve/imports-path/same-level.js
new file mode 100644
index 00000000000000..2f8b67543787f9
--- /dev/null
+++ b/playground/resolve/imports-path/same-level.js
@@ -0,0 +1 @@
+export * from '#top-level'
diff --git a/playground/resolve/imports-path/slash/index.js b/playground/resolve/imports-path/slash/index.js
new file mode 100644
index 00000000000000..54eb4b30252bb8
--- /dev/null
+++ b/playground/resolve/imports-path/slash/index.js
@@ -0,0 +1 @@
+export const msg = '[success] subpath imports with slash'
diff --git a/playground/resolve/imports-path/star/index.js b/playground/resolve/imports-path/star/index.js
new file mode 100644
index 00000000000000..119e154dda3fd6
--- /dev/null
+++ b/playground/resolve/imports-path/star/index.js
@@ -0,0 +1 @@
+export const msg = '[success] subpath imports with star'
diff --git a/playground/resolve/imports-path/top-level.js b/playground/resolve/imports-path/top-level.js
new file mode 100644
index 00000000000000..861f792284931d
--- /dev/null
+++ b/playground/resolve/imports-path/top-level.js
@@ -0,0 +1 @@
+export const msg = '[success] top level subpath imports'
diff --git a/playground/resolve/index.html b/playground/resolve/index.html
new file mode 100644
index 00000000000000..1b5cd5ae76a3fd
--- /dev/null
+++ b/playground/resolve/index.html
@@ -0,0 +1,423 @@
+Resolve
+
+Utf8-bom import
+fail
+
+Deep import
+Should show [2,4]:fail
+
+Exports and a nested package scope with a different type
+fail
+
+Entry resolving with exports field
+fail
+
+Deep import with exports field
+fail
+
+Deep import with query with exports field
+fail
+
+Deep import with exports field + exposed directory
+fail
+
+Deep import with exports field + mapped directory
+fail
+
+Exports field env priority
+fail
+
+Exports field read only from the root package.json
+fail
+
+Exports with legacy fallback
+fail
+
+Exports with module
+fail
+
+
+ Both import and require resolve using module condition (avoids dual package
+ hazard)
+
+fail
+fail
+
+Resolving top level with imports field
+fail
+
+Resolving same level with imports field
+fail
+
+Resolving nested path with imports field
+fail
+
+Resolving star with imports filed
+fail
+
+Resolving slash with imports filed
+fail
+
+Resolving from other package with imports field
+fail
+
+Resolving with query with imports field
+fail
+
+Resolve /index.*
+fail
+
+Resolve dir and file of the same name (should prioritize file)
+fail
+
+Resolve to non-duplicated file extension
+fail
+
+Resolve nested file extension
+fail
+
+Don't add extensions to directory names
+fail
+
+
+ A ts module can import another ts module using its corresponding js file name
+
+fail
+
+
+ A ts module can import another tsx module using its corresponding jsx file
+ name
+
+fail
+
+
+ A ts module can import another tsx module using its corresponding js file name
+
+fail
+
+
+ A ts module can import another ESM module using its corresponding mjs file
+ name
+
+fail
+
+
+ A ts module can import another ESM module using its corresponding mjs file
+ name with query parameters
+
+fail
+
+
+ A ts module can import another CommonJS module using its corresponding cjs
+ file name
+
+fail
+
+A js module can import TS modules using its corresponding js file name
+fail
+
+Resolve file name containing dot
+fail
+
+Resolve drive-relative path (Windows only)
+fail
+
+Resolve absolute path
+fail
+
+Resolve file url
+fail
+
+Browser Field
+fail
+
+Resolve browser field even if module field exists
+fail
+
+Resolve module field if browser field is likely UMD or CJS
+fail
+
+Resolve module field if browser field is likely IIFE
+fail
+
+Don't resolve to the `module` field if the importer is a `require` call
+fail
+
+CSS Entry
+
+
+Monorepo linked dep
+
+
+Plugin resolved virtual file
+
+
+Plugin resolved virtual file (#9036)
+
+
+Plugin resolved custom virtual file
+
+
+Inline package
+
+
+resolve.extensions
+
+
+resolve.mainFields
+
+
+resolve.mainFields.custom-browser-main
+
+
+resolve.conditions
+
+
+resolve package that contains # in path
+
+
+resolve non normalized absolute path
+
+
+utf8-bom-package
+fail
+
+
+
+
diff --git a/packages/playground/resolve/inline-package/inline.js b/playground/resolve/inline-package/inline.js
similarity index 100%
rename from packages/playground/resolve/inline-package/inline.js
rename to playground/resolve/inline-package/inline.js
diff --git a/playground/resolve/inline-package/package.json b/playground/resolve/inline-package/package.json
new file mode 100644
index 00000000000000..c254a2b9c4df18
--- /dev/null
+++ b/playground/resolve/inline-package/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-inline-package",
+ "private": true,
+ "version": "0.0.0",
+ "sideEffects": false,
+ "main": "./inline"
+}
diff --git a/playground/resolve/non-normalized.js b/playground/resolve/non-normalized.js
new file mode 100644
index 00000000000000..eee257d6c8e33c
--- /dev/null
+++ b/playground/resolve/non-normalized.js
@@ -0,0 +1 @@
+export default '[success] non normalized absolute path'
diff --git a/playground/resolve/package.json b/playground/resolve/package.json
new file mode 100644
index 00000000000000..c2a611e46bcacf
--- /dev/null
+++ b/playground/resolve/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@vitejs/test-resolve",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite",
+ "preview": "vite preview"
+ },
+ "imports": {
+ "#top-level": "./imports-path/top-level.js",
+ "#same-level": "./imports-path/same-level.js",
+ "#nested/path.js": "./imports-path/nested-path.js",
+ "#star/*": "./imports-path/star/*",
+ "#slash/": "./imports-path/slash/",
+ "#other-pkg-slash/": "@vitejs/test-resolve-imports-pkg/nest/",
+ "#query": "./imports-path/query.json"
+ },
+ "dependencies": {
+ "@babel/runtime": "^7.27.1",
+ "es5-ext": "0.10.64",
+ "normalize.css": "^8.0.1",
+ "@vitejs/test-require-pkg-with-module-field": "link:./require-pkg-with-module-field",
+ "@vitejs/test-resolve-browser-field": "link:./browser-field",
+ "@vitejs/test-resolve-browser-module-field1": "link:./browser-module-field1",
+ "@vitejs/test-resolve-browser-module-field2": "link:./browser-module-field2",
+ "@vitejs/test-resolve-browser-module-field3": "link:./browser-module-field3",
+ "@vitejs/test-resolve-custom-condition": "link:./custom-condition",
+ "@vitejs/test-resolve-custom-main-field": "link:./custom-main-field",
+ "@vitejs/test-resolve-custom-browser-main-field": "link:./custom-browser-main-field",
+ "@vitejs/test-resolve-exports-and-nested-scope": "link:./exports-and-nested-scope",
+ "@vitejs/test-resolve-exports-env": "link:./exports-env",
+ "@vitejs/test-resolve-exports-from-root": "link:./exports-from-root",
+ "@vitejs/test-resolve-exports-legacy-fallback": "link:./exports-legacy-fallback",
+ "@vitejs/test-resolve-exports-path": "link:./exports-path",
+ "@vitejs/test-resolve-exports-with-module": "link:./exports-with-module",
+ "@vitejs/test-resolve-exports-with-module-condition": "link:./exports-with-module-condition",
+ "@vitejs/test-resolve-exports-with-module-condition-required": "link:./exports-with-module-condition-required",
+ "@vitejs/test-resolve-linked": "workspace:*",
+ "@vitejs/test-resolve-imports-pkg": "link:./imports-path/other-pkg",
+ "@vitejs/test-resolve-sharp-dir": "link:./sharp-dir",
+ "@vitejs/test-utf8-bom-package": "link:./utf8-bom-package"
+ }
+}
diff --git a/playground/resolve/public/should-not-be-copied b/playground/resolve/public/should-not-be-copied
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/playground/resolve/require-pkg-with-module-field/dep.cjs b/playground/resolve/require-pkg-with-module-field/dep.cjs
new file mode 100644
index 00000000000000..3fb20b76d48b79
--- /dev/null
+++ b/playground/resolve/require-pkg-with-module-field/dep.cjs
@@ -0,0 +1,5 @@
+const BigNumber = require('bignumber.js')
+
+const x = new BigNumber('1111222233334444555566')
+
+module.exports = x.toString()
diff --git a/playground/resolve/require-pkg-with-module-field/index.cjs b/playground/resolve/require-pkg-with-module-field/index.cjs
new file mode 100644
index 00000000000000..da215f306d1ac1
--- /dev/null
+++ b/playground/resolve/require-pkg-with-module-field/index.cjs
@@ -0,0 +1,8 @@
+const dep = require('./dep.cjs')
+
+const msg =
+ dep === '1.111222233334444555566e+21'
+ ? '[success] require-pkg-with-module-field'
+ : '[failed] require-pkg-with-module-field'
+
+exports.msg = msg
diff --git a/playground/resolve/require-pkg-with-module-field/package.json b/playground/resolve/require-pkg-with-module-field/package.json
new file mode 100644
index 00000000000000..3d429aab7f9a45
--- /dev/null
+++ b/playground/resolve/require-pkg-with-module-field/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-require-pkg-with-module-field",
+ "private": true,
+ "version": "1.0.0",
+ "main": "./index.cjs",
+ "dependencies": {
+ "bignumber.js": "9.3.0"
+ }
+}
diff --git a/playground/resolve/sharp-dir/index.cjs b/playground/resolve/sharp-dir/index.cjs
new file mode 100644
index 00000000000000..58495ca13f0d4b
--- /dev/null
+++ b/playground/resolve/sharp-dir/index.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+ last: require('es5-ext/string/#/last.js'),
+}
diff --git a/playground/resolve/sharp-dir/package.json b/playground/resolve/sharp-dir/package.json
new file mode 100644
index 00000000000000..a7945b3a95f895
--- /dev/null
+++ b/playground/resolve/sharp-dir/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-resolve-sharp-dir",
+ "private": true,
+ "version": "1.0.0",
+ "main": "./index.cjs",
+ "dependencies": {
+ "es5-ext": "0.10.64"
+ }
+}
diff --git a/packages/playground/resolve/ts-extension/hello.ts b/playground/resolve/ts-extension/hello.ts
similarity index 100%
rename from packages/playground/resolve/ts-extension/hello.ts
rename to playground/resolve/ts-extension/hello.ts
diff --git a/playground/resolve/ts-extension/hellocjs.cts b/playground/resolve/ts-extension/hellocjs.cts
new file mode 100644
index 00000000000000..8bfd9caa7407ca
--- /dev/null
+++ b/playground/resolve/ts-extension/hellocjs.cts
@@ -0,0 +1 @@
+export const msgCjs = '[success] use .cjs extension to import a CommonJS module'
diff --git a/packages/playground/resolve/ts-extension/hellojsx.tsx b/playground/resolve/ts-extension/hellojsx.tsx
similarity index 100%
rename from packages/playground/resolve/ts-extension/hellojsx.tsx
rename to playground/resolve/ts-extension/hellojsx.tsx
diff --git a/playground/resolve/ts-extension/hellomjs.mts b/playground/resolve/ts-extension/hellomjs.mts
new file mode 100644
index 00000000000000..501637c83f1257
--- /dev/null
+++ b/playground/resolve/ts-extension/hellomjs.mts
@@ -0,0 +1 @@
+export const msgMjs = '[success] use .mjs extension to import an ESM module'
diff --git a/packages/playground/resolve/ts-extension/hellotsx.tsx b/playground/resolve/ts-extension/hellotsx.tsx
similarity index 100%
rename from packages/playground/resolve/ts-extension/hellotsx.tsx
rename to playground/resolve/ts-extension/hellotsx.tsx
diff --git a/playground/resolve/ts-extension/index-js.js b/playground/resolve/ts-extension/index-js.js
new file mode 100644
index 00000000000000..0a1d17d4b10ea1
--- /dev/null
+++ b/playground/resolve/ts-extension/index-js.js
@@ -0,0 +1,10 @@
+import { msg as msgJs } from './hello.js'
+import { msgJsx } from './hellojsx.jsx'
+import { msgTsx } from './hellotsx.js'
+import { msgCjs } from './hellocjs.cjs'
+import { msgMjs } from './hellomjs.mjs'
+
+export const msg =
+ msgJs && msgJsx && msgTsx && msgCjs && msgMjs
+ ? '[success] use .js / .jsx / .cjs / .mjs extension to import a TS modules'
+ : '[fail]'
diff --git a/playground/resolve/ts-extension/index.ts b/playground/resolve/ts-extension/index.ts
new file mode 100644
index 00000000000000..60612037e02fbe
--- /dev/null
+++ b/playground/resolve/ts-extension/index.ts
@@ -0,0 +1,7 @@
+import { msg } from './hello.js'
+import { msgJsx } from './hellojsx.jsx'
+import { msgTsx } from './hellotsx.js'
+import { msgCjs } from './hellocjs.cjs'
+import { msgMjs } from './hellomjs.mjs'
+
+export { msg, msgJsx, msgTsx, msgCjs, msgMjs }
diff --git a/playground/resolve/utf8-bom-package/index.mjs b/playground/resolve/utf8-bom-package/index.mjs
new file mode 100644
index 00000000000000..8df4477e58038e
--- /dev/null
+++ b/playground/resolve/utf8-bom-package/index.mjs
@@ -0,0 +1 @@
+export const msg = '[success]'
diff --git a/playground/resolve/utf8-bom-package/package.json b/playground/resolve/utf8-bom-package/package.json
new file mode 100644
index 00000000000000..7a3d4ac44434a6
--- /dev/null
+++ b/playground/resolve/utf8-bom-package/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@vitejs/test-utf8-bom-package",
+ "private": true,
+ "version": "1.0.0",
+ "exports": {
+ ".": "./index.mjs"
+ }
+}
diff --git a/packages/playground/resolve/utf8-bom/main.js b/playground/resolve/utf8-bom/main.js
similarity index 100%
rename from packages/playground/resolve/utf8-bom/main.js
rename to playground/resolve/utf8-bom/main.js
diff --git a/packages/playground/resolve/util/bar.util.js b/playground/resolve/util/bar.util.js
similarity index 100%
rename from packages/playground/resolve/util/bar.util.js
rename to playground/resolve/util/bar.util.js
diff --git a/packages/playground/resolve/util/index.js b/playground/resolve/util/index.js
similarity index 100%
rename from packages/playground/resolve/util/index.js
rename to playground/resolve/util/index.js
diff --git a/playground/resolve/vite.config-mainfields-custom-first.js b/playground/resolve/vite.config-mainfields-custom-first.js
new file mode 100644
index 00000000000000..96279c461edd5b
--- /dev/null
+++ b/playground/resolve/vite.config-mainfields-custom-first.js
@@ -0,0 +1,6 @@
+import config from './vite.config.js'
+config.resolve.mainFields = [
+ 'custom',
+ ...config.resolve.mainFields.filter((f) => f !== 'custom'),
+]
+export default config
diff --git a/playground/resolve/vite.config.js b/playground/resolve/vite.config.js
new file mode 100644
index 00000000000000..9216e5db62e818
--- /dev/null
+++ b/playground/resolve/vite.config.js
@@ -0,0 +1,124 @@
+import path from 'node:path'
+import { defaultClientConditions, defineConfig, normalizePath } from 'vite'
+import { a } from './config-dep.cjs'
+
+const virtualFile = '@virtual-file'
+const virtualId = '\0' + virtualFile
+
+const virtualFile9036 = 'virtual:file-9036.js'
+const virtualId9036 = '\0' + virtualFile9036
+
+const customVirtualFile = '@custom-virtual-file'
+
+const generatedContentVirtualFile = '@generated-content-virtual-file'
+const generatedContentImports = [
+ {
+ specifier: normalizePath(
+ path.resolve(__dirname, './drive-relative.js').replace(/^[a-zA-Z]:/, ''),
+ ),
+ elementQuery: '.drive-relative',
+ },
+ {
+ specifier: normalizePath(path.resolve(__dirname, './absolute.js')),
+ elementQuery: '.absolute',
+ },
+ {
+ specifier: new URL('file-url.js', import.meta.url),
+ elementQuery: '.file-url',
+ },
+]
+
+export default defineConfig({
+ resolve: {
+ extensions: ['.mjs', '.js', '.es', '.ts'],
+ mainFields: ['browser', 'custom', 'module'],
+ conditions: [...defaultClientConditions, 'custom'],
+ },
+ define: {
+ VITE_CONFIG_DEP_TEST: a,
+ },
+ plugins: [
+ {
+ name: 'virtual-module',
+ resolveId(id) {
+ if (id === virtualFile) {
+ return virtualId
+ }
+ },
+ load(id) {
+ if (id === virtualId) {
+ return `export const msg = "[success] from conventional virtual file"`
+ }
+ },
+ },
+ {
+ name: 'virtual-module-9036',
+ resolveId(id) {
+ if (id === virtualFile9036) {
+ return virtualId9036
+ }
+ },
+ load(id) {
+ if (id === virtualId9036) {
+ return `export const msg = "[success] from virtual file #9036"`
+ }
+ },
+ },
+ {
+ name: 'custom-resolve',
+ resolveId(id) {
+ if (id === customVirtualFile) {
+ return id
+ }
+ },
+ load(id) {
+ if (id === customVirtualFile) {
+ return `export const msg = "[success] from custom virtual file"`
+ }
+ },
+ },
+ {
+ name: 'generated-content',
+ resolveId(id) {
+ if (id === generatedContentVirtualFile) {
+ return id
+ }
+ },
+ load(id) {
+ if (id === generatedContentVirtualFile) {
+ const tests = generatedContentImports
+ .map(
+ ({ specifier, elementQuery }, i) =>
+ `import content${i} from ${JSON.stringify(specifier)}\n` +
+ `text(${JSON.stringify(elementQuery)}, content${i})`,
+ )
+ .join('\n')
+
+ return (
+ 'function text(selector, text) {\n' +
+ ' document.querySelector(selector).textContent = text\n' +
+ '}\n\n' +
+ tests
+ )
+ }
+ },
+ },
+ {
+ name: 'resolve to non normalized absolute',
+ async resolveId(id) {
+ if (id !== '@non-normalized') return
+ return this.resolve(__dirname + '//non-normalized')
+ },
+ },
+ ],
+ optimizeDeps: {
+ include: [
+ '@vitejs/test-resolve-exports-with-module-condition-required',
+ '@vitejs/test-require-pkg-with-module-field',
+ '@vitejs/test-resolve-sharp-dir',
+ ],
+ },
+ build: {
+ copyPublicDir: false,
+ },
+})
diff --git a/playground/self-referencing/index.js b/playground/self-referencing/index.js
new file mode 100644
index 00000000000000..677689175daccb
--- /dev/null
+++ b/playground/self-referencing/index.js
@@ -0,0 +1 @@
+export const isSelfReference = true
diff --git a/playground/self-referencing/package.json b/playground/self-referencing/package.json
new file mode 100644
index 00000000000000..d0022da4e4ab17
--- /dev/null
+++ b/playground/self-referencing/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@vitejs/self-referencing",
+ "type": "module",
+ "exports": {
+ ".": "./index.js",
+ "./test": "./test/index.js"
+ }
+}
diff --git a/playground/self-referencing/test/index.js b/playground/self-referencing/test/index.js
new file mode 100644
index 00000000000000..2192a75f466e22
--- /dev/null
+++ b/playground/self-referencing/test/index.js
@@ -0,0 +1 @@
+export { isSelfReference } from '@vitejs/self-referencing'
diff --git a/playground/shims.d.ts b/playground/shims.d.ts
new file mode 100644
index 00000000000000..7239629936a7db
--- /dev/null
+++ b/playground/shims.d.ts
@@ -0,0 +1,15 @@
+declare module 'css-color-names' {
+ const colors: Record
+ export default colors
+}
+
+declare module 'kill-port' {
+ const kill: (port: number) => Promise
+ export default kill
+}
+
+declare module '*.vue' {
+ import type { ComponentOptions } from 'vue'
+ const component: ComponentOptions
+ export default component
+}
diff --git a/playground/ssr-alias/__tests__/ssr-alias.spec.ts b/playground/ssr-alias/__tests__/ssr-alias.spec.ts
new file mode 100644
index 00000000000000..93001865ce84e5
--- /dev/null
+++ b/playground/ssr-alias/__tests__/ssr-alias.spec.ts
@@ -0,0 +1,20 @@
+import { expect, test } from 'vitest'
+import { isServe, testDir, viteServer } from '~utils'
+
+test.runIf(isServe)('dev', async () => {
+ const mod = await viteServer.ssrLoadModule('/src/main.js')
+ expect(mod.default).toEqual({
+ dep: 'ok',
+ nonDep: 'ok',
+ builtin: 'ok',
+ })
+})
+
+test.runIf(!isServe)('build', async () => {
+ const mod = await import(`${testDir}/dist/main.js`)
+ expect(mod.default).toEqual({
+ dep: 'ok',
+ nonDep: 'ok',
+ builtin: 'ok',
+ })
+})
diff --git a/playground/ssr-alias/alias-original/index.js b/playground/ssr-alias/alias-original/index.js
new file mode 100644
index 00000000000000..cc9a88ac598de3
--- /dev/null
+++ b/playground/ssr-alias/alias-original/index.js
@@ -0,0 +1 @@
+export default 'original'
diff --git a/playground/ssr-alias/alias-original/package.json b/playground/ssr-alias/alias-original/package.json
new file mode 100644
index 00000000000000..a8a86b500b90fa
--- /dev/null
+++ b/playground/ssr-alias/alias-original/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-alias-original",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "exports": {
+ ".": "./index.js"
+ }
+}
diff --git a/playground/ssr-alias/package.json b/playground/ssr-alias/package.json
new file mode 100644
index 00000000000000..f765f8ac70d60b
--- /dev/null
+++ b/playground/ssr-alias/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-ssr-html",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "build": "vite build"
+ },
+ "dependencies": {
+ "@vitejs/test-alias-original": "file:./alias-original"
+ }
+}
diff --git a/playground/ssr-alias/src/alias-process.js b/playground/ssr-alias/src/alias-process.js
new file mode 100644
index 00000000000000..9cd9e7ddeb637d
--- /dev/null
+++ b/playground/ssr-alias/src/alias-process.js
@@ -0,0 +1,3 @@
+export default {
+ env: { __TEST_ALIAS__: 'ok' },
+}
diff --git a/playground/ssr-alias/src/alias-replaced.js b/playground/ssr-alias/src/alias-replaced.js
new file mode 100644
index 00000000000000..60c71f346d9a3e
--- /dev/null
+++ b/playground/ssr-alias/src/alias-replaced.js
@@ -0,0 +1 @@
+export default 'ok'
diff --git a/playground/ssr-alias/src/main.js b/playground/ssr-alias/src/main.js
new file mode 100644
index 00000000000000..14ded7d14b17ff
--- /dev/null
+++ b/playground/ssr-alias/src/main.js
@@ -0,0 +1,9 @@
+import process from 'node:process'
+import dep from '@vitejs/test-alias-original'
+import nonDep from '@vitejs/test-alias-non-dep'
+
+export default {
+ dep,
+ nonDep,
+ builtin: process.env['__TEST_ALIAS__'],
+}
diff --git a/playground/ssr-alias/vite.config.js b/playground/ssr-alias/vite.config.js
new file mode 100644
index 00000000000000..deb71aee5714a9
--- /dev/null
+++ b/playground/ssr-alias/vite.config.js
@@ -0,0 +1,14 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ ssr: './src/main.js',
+ },
+ resolve: {
+ alias: {
+ '@vitejs/test-alias-original': '/src/alias-replaced.js',
+ '@vitejs/test-alias-non-dep': '/src/alias-replaced.js',
+ 'node:process': '/src/alias-process.js',
+ },
+ },
+})
diff --git a/playground/ssr-conditions/__tests__/serve.ts b/playground/ssr-conditions/__tests__/serve.ts
new file mode 100644
index 00000000000000..ff8a5fd6785d08
--- /dev/null
+++ b/playground/ssr-conditions/__tests__/serve.ts
@@ -0,0 +1,62 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+import path from 'node:path'
+import kill from 'kill-port'
+import { hmrPorts, isBuild, ports, rootDir } from '~utils'
+
+export const port = ports['ssr-conditions']
+
+export async function serve(): Promise<{ close(): Promise }> {
+ if (isBuild) {
+ // build first
+ const { build } = await import('vite')
+ // client build
+ await build({
+ root: rootDir,
+ logLevel: 'silent', // exceptions are logged by Vitest
+ build: {
+ minify: false,
+ outDir: 'dist/client',
+ },
+ })
+ // server build
+ await build({
+ root: rootDir,
+ logLevel: 'silent',
+ build: {
+ ssr: 'src/app.js',
+ outDir: 'dist/server',
+ },
+ })
+ }
+
+ await kill(port)
+
+ const { createServer } = await import(path.resolve(rootDir, 'server.js'))
+ const { app, vite } = await createServer(
+ rootDir,
+ isBuild,
+ hmrPorts['ssr-conditions'],
+ )
+
+ return new Promise((resolve, reject) => {
+ try {
+ const server = app.listen(port, () => {
+ resolve({
+ // for test teardown
+ async close() {
+ await new Promise((resolve) => {
+ server.close(resolve)
+ })
+ if (vite) {
+ await vite.close()
+ }
+ },
+ })
+ })
+ } catch (e) {
+ reject(e)
+ }
+ })
+}
diff --git a/playground/ssr-conditions/__tests__/ssr-conditions.spec.ts b/playground/ssr-conditions/__tests__/ssr-conditions.spec.ts
new file mode 100644
index 00000000000000..078c6357e9d888
--- /dev/null
+++ b/playground/ssr-conditions/__tests__/ssr-conditions.spec.ts
@@ -0,0 +1,35 @@
+import { expect, test } from 'vitest'
+import { port } from './serve'
+import { isServe, page, withRetry } from '~utils'
+
+const url = `http://localhost:${port}`
+
+test('ssr.resolve.conditions affect non-externalized imports during ssr', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.no-external-react-server')).toMatch(
+ 'node.unbundled.js',
+ )
+})
+
+// externalConditions is only used for dev
+test.runIf(isServe)(
+ 'ssr.resolve.externalConditions affect externalized imports during ssr',
+ async () => {
+ await page.goto(url)
+ expect(await page.textContent('.external-react-server')).toMatch('edge.js')
+ },
+)
+
+test('ssr.resolve settings do not affect non-ssr imports', async () => {
+ await page.goto(url)
+ await withRetry(async () => {
+ expect(await page.textContent('.browser-no-external-react-server')).toMatch(
+ 'default.js',
+ )
+ })
+ await withRetry(async () => {
+ expect(await page.textContent('.browser-external-react-server')).toMatch(
+ 'default.js',
+ )
+ })
+})
diff --git a/playground/ssr-conditions/external/browser.js b/playground/ssr-conditions/external/browser.js
new file mode 100644
index 00000000000000..a02fa8e63243a7
--- /dev/null
+++ b/playground/ssr-conditions/external/browser.js
@@ -0,0 +1 @@
+export default 'browser.js'
diff --git a/playground/ssr-conditions/external/default.js b/playground/ssr-conditions/external/default.js
new file mode 100644
index 00000000000000..6d647f59df1b54
--- /dev/null
+++ b/playground/ssr-conditions/external/default.js
@@ -0,0 +1 @@
+export default 'default.js'
diff --git a/playground/ssr-conditions/external/edge.js b/playground/ssr-conditions/external/edge.js
new file mode 100644
index 00000000000000..358f21d00905b2
--- /dev/null
+++ b/playground/ssr-conditions/external/edge.js
@@ -0,0 +1 @@
+export default 'edge.js'
diff --git a/playground/ssr-conditions/external/node.js b/playground/ssr-conditions/external/node.js
new file mode 100644
index 00000000000000..758a33e2709d32
--- /dev/null
+++ b/playground/ssr-conditions/external/node.js
@@ -0,0 +1 @@
+export default 'node.js'
diff --git a/playground/ssr-conditions/external/node.unbundled.js b/playground/ssr-conditions/external/node.unbundled.js
new file mode 100644
index 00000000000000..5ea769de6bd97f
--- /dev/null
+++ b/playground/ssr-conditions/external/node.unbundled.js
@@ -0,0 +1 @@
+export default 'node.unbundled.js'
diff --git a/playground/ssr-conditions/external/package.json b/playground/ssr-conditions/external/package.json
new file mode 100644
index 00000000000000..55b77e3670c375
--- /dev/null
+++ b/playground/ssr-conditions/external/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@vitejs/test-ssr-conditions-external",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "exports": {
+ "./server": {
+ "react-server": {
+ "workerd": "./edge.js",
+ "deno": "./browser.js",
+ "node": {
+ "webpack": "./node.js",
+ "default": "./node.unbundled.js"
+ },
+ "edge-light": "./edge.js",
+ "browser": "./browser.js"
+ },
+ "default": "./default.js"
+ }
+ }
+}
diff --git a/playground/ssr-conditions/index.html b/playground/ssr-conditions/index.html
new file mode 100644
index 00000000000000..d2fec18916c81f
--- /dev/null
+++ b/playground/ssr-conditions/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+ SSR Resolve Conditions
+
+
+ SSR Resolve Conditions
+
+
+
+
+
diff --git a/playground/ssr-conditions/no-external/browser.js b/playground/ssr-conditions/no-external/browser.js
new file mode 100644
index 00000000000000..a02fa8e63243a7
--- /dev/null
+++ b/playground/ssr-conditions/no-external/browser.js
@@ -0,0 +1 @@
+export default 'browser.js'
diff --git a/playground/ssr-conditions/no-external/default.js b/playground/ssr-conditions/no-external/default.js
new file mode 100644
index 00000000000000..6d647f59df1b54
--- /dev/null
+++ b/playground/ssr-conditions/no-external/default.js
@@ -0,0 +1 @@
+export default 'default.js'
diff --git a/playground/ssr-conditions/no-external/edge.js b/playground/ssr-conditions/no-external/edge.js
new file mode 100644
index 00000000000000..358f21d00905b2
--- /dev/null
+++ b/playground/ssr-conditions/no-external/edge.js
@@ -0,0 +1 @@
+export default 'edge.js'
diff --git a/playground/ssr-conditions/no-external/node.js b/playground/ssr-conditions/no-external/node.js
new file mode 100644
index 00000000000000..758a33e2709d32
--- /dev/null
+++ b/playground/ssr-conditions/no-external/node.js
@@ -0,0 +1 @@
+export default 'node.js'
diff --git a/playground/ssr-conditions/no-external/node.unbundled.js b/playground/ssr-conditions/no-external/node.unbundled.js
new file mode 100644
index 00000000000000..5ea769de6bd97f
--- /dev/null
+++ b/playground/ssr-conditions/no-external/node.unbundled.js
@@ -0,0 +1 @@
+export default 'node.unbundled.js'
diff --git a/playground/ssr-conditions/no-external/package.json b/playground/ssr-conditions/no-external/package.json
new file mode 100644
index 00000000000000..45b1d5f706a286
--- /dev/null
+++ b/playground/ssr-conditions/no-external/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@vitejs/test-ssr-conditions-no-external",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "exports": {
+ "./server": {
+ "react-server": {
+ "workerd": "./edge.js",
+ "deno": "./browser.js",
+ "node": {
+ "webpack": "./node.js",
+ "default": "./node.unbundled.js"
+ },
+ "edge-light": "./edge.js",
+ "browser": "./browser.js"
+ },
+ "default": "./default.js"
+ }
+ }
+}
diff --git a/playground/ssr-conditions/package.json b/playground/ssr-conditions/package.json
new file mode 100644
index 00000000000000..96c9ef988f226b
--- /dev/null
+++ b/playground/ssr-conditions/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@vitejs/test-ssr-conditions",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "node server",
+ "build": "npm run build:client && npm run build:server",
+ "build:client": "vite build --outDir dist/client",
+ "build:server": "vite build --ssr src/app.js --outDir dist/server",
+ "serve": "NODE_ENV=production node server",
+ "debug": "node --inspect-brk server"
+ },
+ "dependencies": {
+ "@vitejs/test-ssr-conditions-external": "file:./external",
+ "@vitejs/test-ssr-conditions-no-external": "file:./no-external"
+ },
+ "devDependencies": {
+ "express": "^5.1.0",
+ "sirv": "^3.0.1"
+ }
+}
diff --git a/playground/ssr-conditions/server.js b/playground/ssr-conditions/server.js
new file mode 100644
index 00000000000000..00b0336d66a949
--- /dev/null
+++ b/playground/ssr-conditions/server.js
@@ -0,0 +1,88 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import express from 'express'
+import sirv from 'sirv'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+const isTest = process.env.VITEST
+
+export async function createServer(
+ root = process.cwd(),
+ isProd = process.env.NODE_ENV === 'production',
+ hmrPort,
+) {
+ const resolve = (p) => path.resolve(__dirname, p)
+
+ const indexProd = isProd
+ ? fs.readFileSync(resolve('dist/client/index.html'), 'utf-8')
+ : ''
+
+ const app = express()
+
+ /**
+ * @type {import('vite').ViteDevServer}
+ */
+ let vite
+ if (!isProd) {
+ vite = await (
+ await import('vite')
+ ).createServer({
+ root,
+ logLevel: isTest ? 'error' : 'info',
+ server: {
+ middlewareMode: true,
+ watch: {
+ // During tests we edit the files too fast and sometimes chokidar
+ // misses change events, so enforce polling for consistency
+ usePolling: true,
+ interval: 100,
+ },
+ hmr: {
+ port: hmrPort,
+ },
+ },
+ appType: 'custom',
+ })
+ app.use(vite.middlewares)
+ } else {
+ app.use(sirv(resolve('dist/client'), { extensions: [] }))
+ }
+
+ app.use('*all', async (req, res) => {
+ try {
+ const url = req.originalUrl
+
+ let template, render
+ if (!isProd) {
+ template = fs.readFileSync(resolve('index.html'), 'utf-8')
+ template = await vite.transformIndexHtml(url, template)
+ render = (await vite.ssrLoadModule('/src/app.js')).render
+ } else {
+ template = indexProd
+ render = (await import('./dist/server/app.js')).render
+ }
+
+ const appHtml = await render(url, __dirname)
+
+ const html = template.replace(``, appHtml)
+
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
+ } catch (e) {
+ vite && vite.ssrFixStacktrace(e)
+ console.log(e.stack)
+ res.status(500).end(e.stack)
+ }
+ })
+
+ return { app, vite }
+}
+
+if (!isTest) {
+ createServer().then(({ app }) =>
+ app.listen(5173, () => {
+ console.log('http://localhost:5173')
+ }),
+ )
+}
diff --git a/playground/ssr-conditions/src/app.js b/playground/ssr-conditions/src/app.js
new file mode 100644
index 00000000000000..43f109c37ea7a9
--- /dev/null
+++ b/playground/ssr-conditions/src/app.js
@@ -0,0 +1,16 @@
+import noExternalReactServerMessage from '@vitejs/test-ssr-conditions-no-external/server'
+import externalReactServerMessage from '@vitejs/test-ssr-conditions-external/server'
+
+export async function render(url) {
+ let html = ''
+
+ html += `\n${noExternalReactServerMessage}
`
+
+ html += `\n
`
+
+ html += `\n${externalReactServerMessage}
`
+
+ html += `\n
`
+
+ return html + '\n'
+}
diff --git a/playground/ssr-conditions/vite.config.js b/playground/ssr-conditions/vite.config.js
new file mode 100644
index 00000000000000..78eb0df4d35fe1
--- /dev/null
+++ b/playground/ssr-conditions/vite.config.js
@@ -0,0 +1,12 @@
+import { defaultServerConditions, defineConfig } from 'vite'
+
+export default defineConfig({
+ ssr: {
+ external: ['@vitejs/test-ssr-conditions-external'],
+ noExternal: ['@vitejs/test-ssr-conditions-no-external'],
+ resolve: {
+ conditions: [...defaultServerConditions, 'react-server'],
+ externalConditions: ['node', 'workerd', 'react-server'],
+ },
+ },
+})
diff --git a/playground/ssr-deps/__tests__/serve.ts b/playground/ssr-deps/__tests__/serve.ts
new file mode 100644
index 00000000000000..d7502f5542a356
--- /dev/null
+++ b/playground/ssr-deps/__tests__/serve.ts
@@ -0,0 +1,35 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+import path from 'node:path'
+import kill from 'kill-port'
+import { hmrPorts, ports, rootDir } from '~utils'
+
+export const port = ports['ssr-deps']
+
+export async function serve(): Promise<{ close(): Promise }> {
+ await kill(port)
+
+ const { createServer } = await import(path.resolve(rootDir, 'server.js'))
+ const { app, vite } = await createServer(rootDir, hmrPorts['ssr-deps'])
+
+ return new Promise((resolve, reject) => {
+ try {
+ const server = app.listen(port, () => {
+ resolve({
+ // for test teardown
+ async close() {
+ await new Promise((resolve) => {
+ server.close(resolve)
+ })
+ if (vite) {
+ await vite.close()
+ }
+ },
+ })
+ })
+ } catch (e) {
+ reject(e)
+ }
+ })
+}
diff --git a/playground/ssr-deps/__tests__/ssr-deps.spec.ts b/playground/ssr-deps/__tests__/ssr-deps.spec.ts
new file mode 100644
index 00000000000000..0d09facb91bec1
--- /dev/null
+++ b/playground/ssr-deps/__tests__/ssr-deps.spec.ts
@@ -0,0 +1,162 @@
+import { describe, expect, test } from 'vitest'
+import { port } from './serve'
+import { editFile, getColor, isServe, page, untilUpdated } from '~utils'
+
+const url = `http://localhost:${port}`
+
+/**
+ * test for #5809
+ */
+test('msg should be encrypted', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.encrypted-msg')).not.toMatch(
+ 'Secret Message!',
+ )
+})
+
+test('msg read by fs/promises', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.file-message')).toMatch('File Content!')
+})
+
+test('msg from primitive export', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.primitive-export-message')).toMatch(
+ 'Hello World!',
+ )
+})
+
+test('msg from TS transpiled exports', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.ts-default-export-message')).toMatch(
+ 'Hello World!',
+ )
+ expect(await page.textContent('.ts-named-export-message')).toMatch(
+ 'Hello World!',
+ )
+})
+
+test('msg from Object.assign exports', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.object-assigned-exports-message')).toMatch(
+ 'Hello World!',
+ )
+})
+
+test('msg from forwarded exports', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.forwarded-export-message')).toMatch(
+ 'Hello World!',
+ )
+})
+
+test('msg from define properties exports', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.define-properties-exports-msg')).toMatch(
+ 'Hello World!',
+ )
+})
+
+test('msg from define property exports', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.define-property-exports-msg')).toMatch(
+ 'Hello World!',
+ )
+})
+
+test('msg from only object assigned exports', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.only-object-assigned-exports-msg')).toMatch(
+ 'Hello World!',
+ )
+})
+
+test('msg from no external cjs', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.no-external-cjs-msg')).toMatch('Hello World!')
+})
+
+test('msg from optimized with nested external', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.optimized-with-nested-external')).toMatch(
+ 'Hello World!',
+ )
+})
+
+test('msg from optimized cjs with nested external', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.optimized-cjs-with-nested-external')).toMatch(
+ 'Hello World!',
+ )
+})
+
+test('msg from external using external entry', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.external-using-external-entry')).toMatch(
+ 'Hello World!',
+ )
+})
+
+test('msg from linked no external', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.linked-no-external')).toMatch(
+ `Hello World from ${process.env.NODE_ENV}!`,
+ )
+})
+
+test('msg from linked no external', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.dep-virtual')).toMatch('[success]')
+})
+
+test('import css library', async () => {
+ await page.goto(url)
+ expect(await getColor('.css-lib')).toBe('blue')
+})
+
+test('import css library', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.module-condition')).toMatch('[success]')
+})
+
+test('optimize-deps-nested-include', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.optimize-deps-nested-include')).toMatch(
+ 'nested-include',
+ )
+})
+
+describe.runIf(isServe)('hmr', () => {
+ // TODO: the server file is not imported on the client at all
+ // so it's not present in the client moduleGraph anymore
+ // we need to decide if we want to support a usecase when ssr change
+ // affects the client in any way
+ test.skip('handle isomorphic module updates', async () => {
+ await page.goto(url)
+
+ expect(await page.textContent('.isomorphic-module-server')).toMatch(
+ '[server]',
+ )
+ // Allowing additional time for this element to be filled in
+ // by a client script that is loaded using dynamic import
+ await untilUpdated(async () => {
+ return page.textContent('.isomorphic-module-browser')
+ }, '[browser]')
+
+ editFile('src/isomorphic-module-browser.js', (code) =>
+ code.replace('[browser]', '[browser-hmr]'),
+ )
+ await page.waitForNavigation()
+ await untilUpdated(async () => {
+ return page.textContent('.isomorphic-module-browser')
+ }, '[browser-hmr]')
+
+ editFile('src/isomorphic-module-server.js', (code) =>
+ code.replace('[server]', '[server-hmr]'),
+ )
+ await page.waitForNavigation()
+ await untilUpdated(async () => {
+ return page.textContent('.isomorphic-module-server')
+ }, '[server-hmr]')
+ })
+})
diff --git a/playground/ssr-deps/css-lib/index.css b/playground/ssr-deps/css-lib/index.css
new file mode 100644
index 00000000000000..d3974e432dc451
--- /dev/null
+++ b/playground/ssr-deps/css-lib/index.css
@@ -0,0 +1,3 @@
+.css-lib {
+ color: blue;
+}
diff --git a/playground/ssr-deps/css-lib/package.json b/playground/ssr-deps/css-lib/package.json
new file mode 100644
index 00000000000000..1d9f1e4b044c69
--- /dev/null
+++ b/playground/ssr-deps/css-lib/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-css-lib",
+ "private": true,
+ "version": "0.0.0",
+ "main": "./index.css"
+}
diff --git a/packages/playground/ssr-deps/define-properties-exports/index.js b/playground/ssr-deps/define-properties-exports/index.js
similarity index 100%
rename from packages/playground/ssr-deps/define-properties-exports/index.js
rename to playground/ssr-deps/define-properties-exports/index.js
diff --git a/playground/ssr-deps/define-properties-exports/package.json b/playground/ssr-deps/define-properties-exports/package.json
new file mode 100644
index 00000000000000..f424a352a6a5c8
--- /dev/null
+++ b/playground/ssr-deps/define-properties-exports/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vitejs/test-define-properties-exports",
+ "private": true,
+ "version": "0.0.0"
+}
diff --git a/playground/ssr-deps/define-property-exports/index.js b/playground/ssr-deps/define-property-exports/index.js
new file mode 100644
index 00000000000000..d9708c8a5fed60
--- /dev/null
+++ b/playground/ssr-deps/define-property-exports/index.js
@@ -0,0 +1,5 @@
+Object.defineProperty(exports, 'hello', {
+ value() {
+ return 'Hello World!'
+ },
+})
diff --git a/playground/ssr-deps/define-property-exports/package.json b/playground/ssr-deps/define-property-exports/package.json
new file mode 100644
index 00000000000000..2433a7f1f7b084
--- /dev/null
+++ b/playground/ssr-deps/define-property-exports/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vitejs/test-define-property-exports",
+ "private": true,
+ "version": "0.0.0"
+}
diff --git a/playground/ssr-deps/external-entry/entry.js b/playground/ssr-deps/external-entry/entry.js
new file mode 100644
index 00000000000000..1e211c295b64ef
--- /dev/null
+++ b/playground/ssr-deps/external-entry/entry.js
@@ -0,0 +1,9 @@
+// Module with state, to check that it is properly externalized and
+// not bundled in the optimized deps
+let msg
+export function setMessage(externalMsg) {
+ msg = externalMsg
+}
+export default function getMessage() {
+ return msg
+}
diff --git a/playground/ssr-deps/external-entry/index.js b/playground/ssr-deps/external-entry/index.js
new file mode 100644
index 00000000000000..edb72725b9e7c6
--- /dev/null
+++ b/playground/ssr-deps/external-entry/index.js
@@ -0,0 +1 @@
+export default undefined
diff --git a/playground/ssr-deps/external-entry/package.json b/playground/ssr-deps/external-entry/package.json
new file mode 100644
index 00000000000000..c4c8a60c87f3db
--- /dev/null
+++ b/playground/ssr-deps/external-entry/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-external-entry",
+ "private": true,
+ "version": "0.0.0",
+ "exports": {
+ ".": "./index.js",
+ "./entry": "./entry.js"
+ },
+ "type": "module"
+}
diff --git a/playground/ssr-deps/external-using-external-entry/index.js b/playground/ssr-deps/external-using-external-entry/index.js
new file mode 100644
index 00000000000000..e8160fcd36d176
--- /dev/null
+++ b/playground/ssr-deps/external-using-external-entry/index.js
@@ -0,0 +1,7 @@
+import getMessage from 'external-entry/entry'
+
+export default {
+ hello() {
+ return getMessage()
+ },
+}
diff --git a/playground/ssr-deps/external-using-external-entry/package.json b/playground/ssr-deps/external-using-external-entry/package.json
new file mode 100644
index 00000000000000..8ac14880e872a3
--- /dev/null
+++ b/playground/ssr-deps/external-using-external-entry/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-external-using-external-entry",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "index.js",
+ "dependencies": {
+ "external-entry": "file:../external-entry"
+ }
+}
diff --git a/packages/playground/ssr-deps/forwarded-export/index.js b/playground/ssr-deps/forwarded-export/index.js
similarity index 100%
rename from packages/playground/ssr-deps/forwarded-export/index.js
rename to playground/ssr-deps/forwarded-export/index.js
diff --git a/playground/ssr-deps/forwarded-export/package.json b/playground/ssr-deps/forwarded-export/package.json
new file mode 100644
index 00000000000000..74760eeb6f085b
--- /dev/null
+++ b/playground/ssr-deps/forwarded-export/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@vitejs/test-forwarded-export",
+ "private": true,
+ "version": "0.0.0",
+ "dependencies": {
+ "object-assigned-exports": "file:../object-assigned-exports"
+ }
+}
diff --git a/playground/ssr-deps/import-builtin-cjs/index.js b/playground/ssr-deps/import-builtin-cjs/index.js
new file mode 100644
index 00000000000000..f3e8d6a4b9031f
--- /dev/null
+++ b/playground/ssr-deps/import-builtin-cjs/index.js
@@ -0,0 +1,5 @@
+exports.stream = require('node:stream')
+
+exports.hello = function () {
+ return 'Hello World!'
+}
diff --git a/playground/ssr-deps/import-builtin-cjs/package.json b/playground/ssr-deps/import-builtin-cjs/package.json
new file mode 100644
index 00000000000000..d743aa9a4debb7
--- /dev/null
+++ b/playground/ssr-deps/import-builtin-cjs/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-import-builtin",
+ "private": true,
+ "type": "commonjs",
+ "version": "0.0.0"
+}
diff --git a/playground/ssr-deps/index.html b/playground/ssr-deps/index.html
new file mode 100644
index 00000000000000..6162c03e6f0f85
--- /dev/null
+++ b/playground/ssr-deps/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+ SSR Dependencies
+
+
+ SSR Dependencies
+
+
+
+
+
diff --git a/playground/ssr-deps/linked-no-external/index.js b/playground/ssr-deps/linked-no-external/index.js
new file mode 100644
index 00000000000000..fd63958570d556
--- /dev/null
+++ b/playground/ssr-deps/linked-no-external/index.js
@@ -0,0 +1,7 @@
+export const hello = function () {
+ // make sure linked package is not externalized so Vite features like
+ // import.meta.env works (or handling TS files)
+ return `Hello World from ${
+ import.meta.env.DEV ? 'development' : 'production'
+ }!`
+}
diff --git a/playground/ssr-deps/linked-no-external/package.json b/playground/ssr-deps/linked-no-external/package.json
new file mode 100644
index 00000000000000..3957ca1053fe51
--- /dev/null
+++ b/playground/ssr-deps/linked-no-external/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-linked-no-external",
+ "private": true,
+ "type": "module",
+ "version": "0.0.0"
+}
diff --git a/packages/playground/ssr-deps/message b/playground/ssr-deps/message
similarity index 100%
rename from packages/playground/ssr-deps/message
rename to playground/ssr-deps/message
diff --git a/playground/ssr-deps/module-condition/import.mjs b/playground/ssr-deps/module-condition/import.mjs
new file mode 100644
index 00000000000000..2ecbfe1a42cf13
--- /dev/null
+++ b/playground/ssr-deps/module-condition/import.mjs
@@ -0,0 +1 @@
+export default '[success]'
diff --git a/playground/ssr-deps/module-condition/module.js b/playground/ssr-deps/module-condition/module.js
new file mode 100644
index 00000000000000..2a5cbd94bcad9b
--- /dev/null
+++ b/playground/ssr-deps/module-condition/module.js
@@ -0,0 +1,4 @@
+// this is written in ESM but the file extension implies this is evaluated as CJS.
+// BUT this doesn't matter in practice as the `module` condition is not used in node.
+// hence SSR should not load this file.
+export default '[fail] should not load me'
diff --git a/playground/ssr-deps/module-condition/package.json b/playground/ssr-deps/module-condition/package.json
new file mode 100644
index 00000000000000..e7809eced127bb
--- /dev/null
+++ b/playground/ssr-deps/module-condition/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@vitejs/test-module-condition",
+ "private": true,
+ "version": "0.0.0",
+ "exports": {
+ ".": {
+ "module": "./module.js",
+ "import": "./import.mjs"
+ }
+ }
+}
diff --git a/playground/ssr-deps/nested-exclude/index.js b/playground/ssr-deps/nested-exclude/index.js
new file mode 100644
index 00000000000000..c3dc5c01b6d886
--- /dev/null
+++ b/playground/ssr-deps/nested-exclude/index.js
@@ -0,0 +1,3 @@
+export { default as nestedInclude } from '@vitejs/test-nested-include'
+
+export default 'nested-exclude'
diff --git a/playground/ssr-deps/nested-exclude/package.json b/playground/ssr-deps/nested-exclude/package.json
new file mode 100644
index 00000000000000..3e12e0111fe31d
--- /dev/null
+++ b/playground/ssr-deps/nested-exclude/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-nested-exclude",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "main": "index.js",
+ "dependencies": {
+ "@vitejs/test-nested-include": "file:../nested-include"
+ }
+}
diff --git a/playground/ssr-deps/nested-external-cjs/index.js b/playground/ssr-deps/nested-external-cjs/index.js
new file mode 100644
index 00000000000000..64f8e0be4047ae
--- /dev/null
+++ b/playground/ssr-deps/nested-external-cjs/index.js
@@ -0,0 +1,12 @@
+// Module with state, to check that it is properly externalized and
+// not bundled in the optimized deps
+let msg
+
+module.exports = {
+ setMessage(externalMsg) {
+ msg = externalMsg
+ },
+ getMessage() {
+ return msg
+ },
+}
diff --git a/playground/ssr-deps/nested-external-cjs/package.json b/playground/ssr-deps/nested-external-cjs/package.json
new file mode 100644
index 00000000000000..9a91fc38dfcf44
--- /dev/null
+++ b/playground/ssr-deps/nested-external-cjs/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "nested-external-cjs",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js",
+ "type": "commonjs"
+}
diff --git a/playground/ssr-deps/nested-external/index.js b/playground/ssr-deps/nested-external/index.js
new file mode 100644
index 00000000000000..1e211c295b64ef
--- /dev/null
+++ b/playground/ssr-deps/nested-external/index.js
@@ -0,0 +1,9 @@
+// Module with state, to check that it is properly externalized and
+// not bundled in the optimized deps
+let msg
+export function setMessage(externalMsg) {
+ msg = externalMsg
+}
+export default function getMessage() {
+ return msg
+}
diff --git a/playground/ssr-deps/nested-external/package.json b/playground/ssr-deps/nested-external/package.json
new file mode 100644
index 00000000000000..84b5bd709fb5cf
--- /dev/null
+++ b/playground/ssr-deps/nested-external/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-nested-external",
+ "private": true,
+ "version": "0.0.0",
+ "main": "index.js",
+ "type": "module"
+}
diff --git a/playground/ssr-deps/nested-include/index.js b/playground/ssr-deps/nested-include/index.js
new file mode 100644
index 00000000000000..547417fcf2e4d4
--- /dev/null
+++ b/playground/ssr-deps/nested-include/index.js
@@ -0,0 +1,2 @@
+// written in cjs, optimization should convert this to esm
+module.exports = 'nested-include'
diff --git a/playground/ssr-deps/nested-include/package.json b/playground/ssr-deps/nested-include/package.json
new file mode 100644
index 00000000000000..d4b97a20651d1c
--- /dev/null
+++ b/playground/ssr-deps/nested-include/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-nested-include",
+ "private": true,
+ "version": "1.0.0",
+ "main": "index.js"
+}
diff --git a/playground/ssr-deps/no-external-cjs/index.js b/playground/ssr-deps/no-external-cjs/index.js
new file mode 100644
index 00000000000000..d4fc9da147c88f
--- /dev/null
+++ b/playground/ssr-deps/no-external-cjs/index.js
@@ -0,0 +1,3 @@
+exports.hello = function () {
+ return 'Hello World!'
+}
diff --git a/playground/ssr-deps/no-external-cjs/package.json b/playground/ssr-deps/no-external-cjs/package.json
new file mode 100644
index 00000000000000..a86956d8eefc2e
--- /dev/null
+++ b/playground/ssr-deps/no-external-cjs/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@vitejs/test-no-external-cjs",
+ "private": true,
+ "type": "commonjs",
+ "version": "0.0.0"
+}
diff --git a/playground/ssr-deps/no-external-css/index.css b/playground/ssr-deps/no-external-css/index.css
new file mode 100644
index 00000000000000..161363ab3641a8
--- /dev/null
+++ b/playground/ssr-deps/no-external-css/index.css
@@ -0,0 +1,4 @@
+@font-face {
+ font-family: 'Not Real Sans';
+ src: url('./i-throw-if-you-optimize-this-file.woff') format('woff');
+}
diff --git a/playground/ssr-deps/no-external-css/package.json b/playground/ssr-deps/no-external-css/package.json
new file mode 100644
index 00000000000000..45c69024683f3b
--- /dev/null
+++ b/playground/ssr-deps/no-external-css/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-no-external-css",
+ "private": true,
+ "type": "module",
+ "version": "0.0.0",
+ "exports": {
+ ".": "./index.css"
+ }
+}
diff --git a/playground/ssr-deps/non-optimized-with-nested-external/index.js b/playground/ssr-deps/non-optimized-with-nested-external/index.js
new file mode 100644
index 00000000000000..45eb27c4ad9565
--- /dev/null
+++ b/playground/ssr-deps/non-optimized-with-nested-external/index.js
@@ -0,0 +1,5 @@
+import { setMessage } from 'nested-external'
+import external from 'nested-external-cjs'
+
+setMessage('Hello World!')
+external.setMessage('Hello World!')
diff --git a/playground/ssr-deps/non-optimized-with-nested-external/package.json b/playground/ssr-deps/non-optimized-with-nested-external/package.json
new file mode 100644
index 00000000000000..04a4ad27ca4d5e
--- /dev/null
+++ b/playground/ssr-deps/non-optimized-with-nested-external/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@vitejs/test-non-optimized-with-nested-external",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "index.js",
+ "dependencies": {
+ "nested-external": "file:../nested-external",
+ "nested-external-cjs": "file:../nested-external-cjs"
+ }
+}
diff --git a/playground/ssr-deps/object-assigned-exports/index.js b/playground/ssr-deps/object-assigned-exports/index.js
new file mode 100644
index 00000000000000..e0e61fcd260e4e
--- /dev/null
+++ b/playground/ssr-deps/object-assigned-exports/index.js
@@ -0,0 +1,9 @@
+Object.defineProperty(exports, '__esModule', { value: true })
+
+const obj = {
+ hello() {
+ return 'Hello World!'
+ },
+}
+
+Object.assign(exports, obj)
diff --git a/playground/ssr-deps/object-assigned-exports/package.json b/playground/ssr-deps/object-assigned-exports/package.json
new file mode 100644
index 00000000000000..9f0a13f129721c
--- /dev/null
+++ b/playground/ssr-deps/object-assigned-exports/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vitejs/test-object-assigned-exports",
+ "private": true,
+ "version": "0.0.0"
+}
diff --git a/playground/ssr-deps/only-object-assigned-exports/index.js b/playground/ssr-deps/only-object-assigned-exports/index.js
new file mode 100644
index 00000000000000..8d268027ec7f0c
--- /dev/null
+++ b/playground/ssr-deps/only-object-assigned-exports/index.js
@@ -0,0 +1,5 @@
+Object.assign(exports, {
+ hello() {
+ return 'Hello World!'
+ },
+})
diff --git a/playground/ssr-deps/only-object-assigned-exports/package.json b/playground/ssr-deps/only-object-assigned-exports/package.json
new file mode 100644
index 00000000000000..d87ce4fc571f34
--- /dev/null
+++ b/playground/ssr-deps/only-object-assigned-exports/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vitejs/test-only-object-assigned-exports",
+ "private": true,
+ "version": "0.0.0"
+}
diff --git a/playground/ssr-deps/optimized-cjs-with-nested-external/index.js b/playground/ssr-deps/optimized-cjs-with-nested-external/index.js
new file mode 100644
index 00000000000000..6fc7c0c31a8324
--- /dev/null
+++ b/playground/ssr-deps/optimized-cjs-with-nested-external/index.js
@@ -0,0 +1,5 @@
+const getMessage = require('nested-external')
+
+module.exports = {
+ hello: getMessage,
+}
diff --git a/playground/ssr-deps/optimized-cjs-with-nested-external/package.json b/playground/ssr-deps/optimized-cjs-with-nested-external/package.json
new file mode 100644
index 00000000000000..806cdd316c8db0
--- /dev/null
+++ b/playground/ssr-deps/optimized-cjs-with-nested-external/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@vitejs/test-optimized-cjs-with-nested-external",
+ "private": true,
+ "version": "0.0.0",
+ "dependencies": {
+ "nested-external": "file:../nested-external"
+ }
+}
diff --git a/playground/ssr-deps/optimized-with-nested-external/index.js b/playground/ssr-deps/optimized-with-nested-external/index.js
new file mode 100644
index 00000000000000..3b0d9135b3e5b1
--- /dev/null
+++ b/playground/ssr-deps/optimized-with-nested-external/index.js
@@ -0,0 +1,5 @@
+import getMessage from 'nested-external'
+
+export function hello() {
+ return getMessage()
+}
diff --git a/playground/ssr-deps/optimized-with-nested-external/package.json b/playground/ssr-deps/optimized-with-nested-external/package.json
new file mode 100644
index 00000000000000..bc2580d5eba282
--- /dev/null
+++ b/playground/ssr-deps/optimized-with-nested-external/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-optimized-with-nested-external",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "index.js",
+ "dependencies": {
+ "nested-external": "file:../nested-external"
+ }
+}
diff --git a/playground/ssr-deps/package.json b/playground/ssr-deps/package.json
new file mode 100644
index 00000000000000..d0d5a9f72de643
--- /dev/null
+++ b/playground/ssr-deps/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@vitejs/test-ssr-deps",
+ "private": true,
+ "type": "module",
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "node server",
+ "serve": "NODE_ENV=production node server",
+ "debug": "node --inspect-brk server"
+ },
+ "dependencies": {
+ "@node-rs/bcrypt": "^1.10.7",
+ "@vitejs/test-css-lib": "file:./css-lib",
+ "@vitejs/test-define-properties-exports": "file:./define-properties-exports",
+ "@vitejs/test-define-property-exports": "file:./define-property-exports",
+ "@vitejs/test-external-entry": "file:./external-entry",
+ "@vitejs/test-external-using-external-entry": "file:./external-using-external-entry",
+ "@vitejs/test-forwarded-export": "file:./forwarded-export",
+ "@vitejs/test-import-builtin-cjs": "file:./import-builtin-cjs",
+ "@vitejs/test-linked-no-external": "link:./linked-no-external",
+ "@vitejs/test-module-condition": "file:./module-condition",
+ "@vitejs/test-nested-exclude": "file:./nested-exclude",
+ "@vitejs/test-no-external-cjs": "file:./no-external-cjs",
+ "@vitejs/test-no-external-css": "file:./no-external-css",
+ "@vitejs/test-non-optimized-with-nested-external": "workspace:*",
+ "@vitejs/test-object-assigned-exports": "file:./object-assigned-exports",
+ "@vitejs/test-only-object-assigned-exports": "file:./only-object-assigned-exports",
+ "@vitejs/test-optimized-cjs-with-nested-external": "file:./optimized-with-nested-external",
+ "@vitejs/test-optimized-with-nested-external": "file:./optimized-with-nested-external",
+ "@vitejs/test-pkg-exports": "file:./pkg-exports",
+ "@vitejs/test-primitive-export": "file:./primitive-export",
+ "@vitejs/test-read-file-content": "file:./read-file-content",
+ "@vitejs/test-require-absolute": "file:./require-absolute",
+ "@vitejs/test-ts-transpiled-exports": "file:./ts-transpiled-exports"
+ },
+ "devDependencies": {
+ "express": "^5.1.0"
+ }
+}
diff --git a/playground/ssr-deps/pkg-exports/index.js b/playground/ssr-deps/pkg-exports/index.js
new file mode 100644
index 00000000000000..edb72725b9e7c6
--- /dev/null
+++ b/playground/ssr-deps/pkg-exports/index.js
@@ -0,0 +1 @@
+export default undefined
diff --git a/playground/ssr-deps/pkg-exports/package.json b/playground/ssr-deps/pkg-exports/package.json
new file mode 100644
index 00000000000000..32b164a5a1cfe2
--- /dev/null
+++ b/playground/ssr-deps/pkg-exports/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-pkg-exports",
+ "private": true,
+ "version": "0.0.0",
+ "exports": {
+ ".": "./index.js"
+ },
+ "type": "module"
+}
diff --git a/packages/playground/ssr-deps/primitive-export/index.js b/playground/ssr-deps/primitive-export/index.js
similarity index 100%
rename from packages/playground/ssr-deps/primitive-export/index.js
rename to playground/ssr-deps/primitive-export/index.js
diff --git a/playground/ssr-deps/primitive-export/package.json b/playground/ssr-deps/primitive-export/package.json
new file mode 100644
index 00000000000000..1588eb739c938a
--- /dev/null
+++ b/playground/ssr-deps/primitive-export/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vitejs/test-primitive-export",
+ "private": true,
+ "version": "0.0.0"
+}
diff --git a/playground/ssr-deps/read-file-content/index.js b/playground/ssr-deps/read-file-content/index.js
new file mode 100644
index 00000000000000..03b2e199b61f52
--- /dev/null
+++ b/playground/ssr-deps/read-file-content/index.js
@@ -0,0 +1,6 @@
+const path = require('node:path')
+
+module.exports = async function readFileContent(filePath) {
+ const fs = require('node:fs/promises')
+ return await fs.readFile(path.resolve(filePath), 'utf-8')
+}
diff --git a/playground/ssr-deps/read-file-content/package.json b/playground/ssr-deps/read-file-content/package.json
new file mode 100644
index 00000000000000..3e02c6ae933f41
--- /dev/null
+++ b/playground/ssr-deps/read-file-content/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vitejs/test-read-file-content",
+ "private": true,
+ "version": "0.0.0"
+}
diff --git a/packages/playground/ssr-deps/require-absolute/foo.js b/playground/ssr-deps/require-absolute/foo.js
similarity index 100%
rename from packages/playground/ssr-deps/require-absolute/foo.js
rename to playground/ssr-deps/require-absolute/foo.js
diff --git a/playground/ssr-deps/require-absolute/index.js b/playground/ssr-deps/require-absolute/index.js
new file mode 100644
index 00000000000000..18b3aa936629dc
--- /dev/null
+++ b/playground/ssr-deps/require-absolute/index.js
@@ -0,0 +1,3 @@
+const path = require('node:path')
+
+module.exports.hello = () => require(path.resolve(__dirname, './foo.js')).hello
diff --git a/playground/ssr-deps/require-absolute/package.json b/playground/ssr-deps/require-absolute/package.json
new file mode 100644
index 00000000000000..86b28ee403ffb0
--- /dev/null
+++ b/playground/ssr-deps/require-absolute/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vitejs/test-require-absolute",
+ "private": true,
+ "version": "0.0.0"
+}
diff --git a/playground/ssr-deps/server.js b/playground/ssr-deps/server.js
new file mode 100644
index 00000000000000..c9b6230ab875df
--- /dev/null
+++ b/playground/ssr-deps/server.js
@@ -0,0 +1,128 @@
+// @ts-check
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import express from 'express'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+const isTest = process.env.VITEST
+
+const noExternal = [
+ '@vitejs/test-no-external-cjs',
+ '@vitejs/test-import-builtin-cjs',
+ '@vitejs/test-no-external-css',
+ '@vitejs/test-external-entry',
+]
+
+export async function createServer(root = process.cwd(), hmrPort) {
+ const resolve = (p) => path.resolve(__dirname, p)
+
+ const app = express()
+
+ /**
+ * @type {import('vite').ViteDevServer}
+ */
+ const vite = await (
+ await import('vite')
+ ).createServer({
+ root,
+ logLevel: isTest ? 'error' : 'info',
+ server: {
+ middlewareMode: true,
+ watch: {
+ // During tests we edit the files too fast and sometimes chokidar
+ // misses change events, so enforce polling for consistency
+ usePolling: true,
+ interval: 100,
+ },
+ hmr: {
+ port: hmrPort,
+ },
+ },
+ appType: 'custom',
+ ssr: {
+ noExternal: [
+ ...noExternal,
+ '@vitejs/test-nested-exclude',
+ '@vitejs/test-nested-include',
+ ],
+ external: [
+ '@vitejs/test-nested-external',
+ '@vitejs/test-external-entry/entry',
+ ],
+ optimizeDeps: {
+ include: [
+ ...noExternal,
+ '@vitejs/test-nested-exclude > @vitejs/test-nested-include',
+ ],
+ },
+ },
+ plugins: [
+ {
+ name: 'dep-virtual',
+ enforce: 'pre',
+ resolveId(id) {
+ if (id === '@vitejs/test-pkg-exports/virtual') {
+ return '@vitejs/test-pkg-exports/virtual'
+ }
+ },
+ load(id) {
+ if (id === '@vitejs/test-pkg-exports/virtual') {
+ return 'export default "[success]"'
+ }
+ },
+ },
+ {
+ name: 'virtual-isomorphic-module',
+ resolveId(id) {
+ if (id === 'virtual:isomorphic-module') {
+ return '\0virtual:isomorphic-module'
+ }
+ },
+ load(id, { ssr }) {
+ if (id === '\0virtual:isomorphic-module') {
+ if (ssr) {
+ return 'export { default } from "/src/isomorphic-module-server.js";'
+ } else {
+ return 'export { default } from "/src/isomorphic-module-browser.js";'
+ }
+ }
+ },
+ },
+ ],
+ })
+ // use vite's connect instance as middleware
+ app.use(vite.middlewares)
+
+ app.use('*all', async (req, res) => {
+ try {
+ const url = req.originalUrl
+
+ let template
+ template = fs.readFileSync(resolve('index.html'), 'utf-8')
+ template = await vite.transformIndexHtml(url, template)
+ const render = (await vite.ssrLoadModule('/src/app.js')).render
+
+ const appHtml = await render(url, __dirname)
+
+ const html = template.replace(``, appHtml)
+
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
+ } catch (e) {
+ vite && vite.ssrFixStacktrace(e)
+ console.log(e.stack)
+ res.status(500).end(e.stack)
+ }
+ })
+
+ return { app, vite }
+}
+
+if (!isTest) {
+ createServer().then(({ app }) =>
+ app.listen(5173, () => {
+ console.log('http://localhost:5173')
+ }),
+ )
+}
diff --git a/playground/ssr-deps/src/app.js b/playground/ssr-deps/src/app.js
new file mode 100644
index 00000000000000..1663979273a9a1
--- /dev/null
+++ b/playground/ssr-deps/src/app.js
@@ -0,0 +1,101 @@
+import path from 'node:path'
+import readFileContent from '@vitejs/test-read-file-content'
+import primitiveExport from '@vitejs/test-primitive-export'
+import tsDefaultExport, {
+ hello as tsNamedExport,
+} from '@vitejs/test-ts-transpiled-exports'
+import objectAssignedExports from '@vitejs/test-object-assigned-exports'
+import forwardedExport from '@vitejs/test-forwarded-export'
+import bcrypt from '@node-rs/bcrypt'
+import definePropertiesExports from '@vitejs/test-define-properties-exports'
+import definePropertyExports from '@vitejs/test-define-property-exports'
+import onlyObjectAssignedExports from '@vitejs/test-only-object-assigned-exports'
+import requireAbsolute from '@vitejs/test-require-absolute'
+import noExternalCjs from '@vitejs/test-no-external-cjs'
+import importBuiltinCjs from '@vitejs/test-import-builtin-cjs'
+import { hello as linkedNoExternal } from '@vitejs/test-linked-no-external'
+import virtualMessage from '@vitejs/test-pkg-exports/virtual'
+import moduleConditionMessage from '@vitejs/test-module-condition'
+import '@vitejs/test-css-lib'
+
+// This import will set a 'Hello World!" message in the nested-external non-entry dependency
+import '@vitejs/test-non-optimized-with-nested-external'
+
+import * as optimizedWithNestedExternal from '@vitejs/test-optimized-with-nested-external'
+import * as optimizedCjsWithNestedExternal from '@vitejs/test-optimized-cjs-with-nested-external'
+import * as optimizeDepsNestedInclude from '@vitejs/test-nested-exclude'
+
+import { setMessage } from '@vitejs/test-external-entry/entry'
+setMessage('Hello World!')
+import externalUsingExternalEntry from '@vitejs/test-external-using-external-entry'
+import isomorphicModuleMessage from 'virtual:isomorphic-module'
+
+export async function render(url, rootDir) {
+ let html = ''
+
+ const encryptedMsg = await bcrypt.hash('Secret Message!', 10)
+ html += `\nencrypted message: ${encryptedMsg}
`
+
+ const fileContent = await readFileContent(path.resolve(rootDir, 'message'))
+ html += `\nmsg read via fs/promises: ${fileContent}
`
+
+ html += `\nmessage from primitive export: ${primitiveExport}
`
+
+ // `.default()` as incorrectly packaged
+ const tsDefaultExportMessage = tsDefaultExport.default()
+ html += `\nmessage from ts-default-export: ${tsDefaultExportMessage}
`
+
+ const tsNamedExportMessage = tsNamedExport()
+ html += `\nmessage from ts-named-export: ${tsNamedExportMessage}
`
+
+ const objectAssignedExportsMessage = objectAssignedExports.hello()
+ html += `\nmessage from object-assigned-exports: ${objectAssignedExportsMessage}
`
+
+ const forwardedExportMessage = forwardedExport.hello()
+ html += `\nmessage from forwarded-export: ${forwardedExportMessage}
`
+
+ const definePropertiesExportsMsg = definePropertiesExports.hello()
+ html += `\nmessage from define-properties-exports: ${definePropertiesExportsMsg}
`
+
+ const definePropertyExportsMsg = definePropertyExports.hello()
+ html += `\nmessage from define-property-exports: ${definePropertyExportsMsg}
`
+
+ const onlyObjectAssignedExportsMessage = onlyObjectAssignedExports.hello()
+ html += `\nmessage from only-object-assigned-exports: ${onlyObjectAssignedExportsMessage}
`
+
+ const requireAbsoluteMessage = requireAbsolute.hello()
+ html += `\nmessage from require-absolute: ${requireAbsoluteMessage}
`
+
+ const noExternalCjsMessage = noExternalCjs.hello()
+ html += `\nmessage from no-external-cjs: ${noExternalCjsMessage}
`
+
+ const importBuiltinCjsMessage = importBuiltinCjs.hello()
+ html += `\nmessage from import-builtin-cjs: ${importBuiltinCjsMessage}
`
+
+ const optimizedWithNestedExternalMessage = optimizedWithNestedExternal.hello()
+ html += `\nmessage from optimized-with-nested-external: ${optimizedWithNestedExternalMessage}
`
+
+ const optimizedCjsWithNestedExternalMessage =
+ optimizedCjsWithNestedExternal.hello()
+ html += `\nmessage from optimized-cjs-with-nested-external: ${optimizedCjsWithNestedExternalMessage}
`
+
+ const externalUsingExternalEntryMessage = externalUsingExternalEntry.hello()
+ html += `\nmessage from external-using-external-entry: ${externalUsingExternalEntryMessage}
`
+
+ const linkedNoExternalMessage = linkedNoExternal()
+ html += `\nlinked-no-external msg: ${linkedNoExternalMessage}
`
+
+ html += `\nmessage from dep-virtual: ${virtualMessage}
`
+
+ html += `\nI should be blue
`
+
+ html += `\n${moduleConditionMessage}
`
+
+ html += `\n${isomorphicModuleMessage}
`
+
+ html += `\n
`
+
+ html += `\nmessage from optimize-deps-nested-include: ${optimizeDepsNestedInclude.nestedInclude}
`
+
+ return html + '\n'
+}
diff --git a/playground/ssr-deps/src/isomorphic-module-browser.js b/playground/ssr-deps/src/isomorphic-module-browser.js
new file mode 100644
index 00000000000000..95ab03d26f7d7b
--- /dev/null
+++ b/playground/ssr-deps/src/isomorphic-module-browser.js
@@ -0,0 +1,3 @@
+const message = 'message from isomorphic-module (browser): [browser]'
+
+export default message
diff --git a/playground/ssr-deps/src/isomorphic-module-server.js b/playground/ssr-deps/src/isomorphic-module-server.js
new file mode 100644
index 00000000000000..def23eb4caa384
--- /dev/null
+++ b/playground/ssr-deps/src/isomorphic-module-server.js
@@ -0,0 +1,3 @@
+const message = 'message from isomorphic-module (server): [server]'
+
+export default message
diff --git a/packages/playground/ssr-deps/ts-transpiled-exports/index.js b/playground/ssr-deps/ts-transpiled-exports/index.js
similarity index 100%
rename from packages/playground/ssr-deps/ts-transpiled-exports/index.js
rename to playground/ssr-deps/ts-transpiled-exports/index.js
diff --git a/playground/ssr-deps/ts-transpiled-exports/package.json b/playground/ssr-deps/ts-transpiled-exports/package.json
new file mode 100644
index 00000000000000..326037b5ca9f4a
--- /dev/null
+++ b/playground/ssr-deps/ts-transpiled-exports/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vitejs/test-ts-transpiled-exports",
+ "private": true,
+ "version": "0.0.0"
+}
diff --git a/playground/ssr-html/__tests__/serve.ts b/playground/ssr-html/__tests__/serve.ts
new file mode 100644
index 00000000000000..4886414606b6b6
--- /dev/null
+++ b/playground/ssr-html/__tests__/serve.ts
@@ -0,0 +1,35 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+import path from 'node:path'
+import kill from 'kill-port'
+import { hmrPorts, ports, rootDir } from '~utils'
+
+export const port = ports['ssr-html']
+
+export async function serve(): Promise<{ close(): Promise }> {
+ await kill(port)
+
+ const { createServer } = await import(path.resolve(rootDir, 'server.js'))
+ const { app, vite } = await createServer(rootDir, hmrPorts['ssr-html'])
+
+ return new Promise((resolve, reject) => {
+ try {
+ const server = app.listen(port, () => {
+ resolve({
+ // for test teardown
+ async close() {
+ await new Promise((resolve) => {
+ server.close(resolve)
+ })
+ if (vite) {
+ await vite.close()
+ }
+ },
+ })
+ })
+ } catch (e) {
+ reject(e)
+ }
+ })
+}
diff --git a/playground/ssr-html/__tests__/ssr-html.spec.ts b/playground/ssr-html/__tests__/ssr-html.spec.ts
new file mode 100644
index 00000000000000..d47272f50b8397
--- /dev/null
+++ b/playground/ssr-html/__tests__/ssr-html.spec.ts
@@ -0,0 +1,136 @@
+import { execFile } from 'node:child_process'
+import { promisify } from 'node:util'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { describe, expect, test } from 'vitest'
+import { port } from './serve'
+import { editFile, isServe, page, untilUpdated } from '~utils'
+
+const url = `http://localhost:${port}`
+
+describe.runIf(isServe)('injected inline scripts', () => {
+ test('no injected inline scripts are present', async () => {
+ await page.goto(url)
+ const inlineScripts = await page.$$eval('script', (nodes) =>
+ nodes.filter((n) => !n.getAttribute('src') && n.innerHTML),
+ )
+ expect(inlineScripts).toHaveLength(0)
+ })
+
+ test('injected script proxied correctly', async () => {
+ await page.goto(url)
+ const proxiedScripts = await page.$$eval('script', (nodes) =>
+ nodes
+ .filter((n) => {
+ const src = n.getAttribute('src')
+ if (!src) return false
+ return src.includes('?html-proxy&index')
+ })
+ .map((n) => n.getAttribute('src')),
+ )
+
+ // assert at least 1 proxied script exists
+ expect(proxiedScripts).not.toHaveLength(0)
+
+ const scriptContents = await Promise.all(
+ proxiedScripts.map((src) => fetch(url + src).then((res) => res.text())),
+ )
+
+ // all proxied scripts return code
+ for (const code of scriptContents) {
+ expect(code).toBeTruthy()
+ }
+ })
+})
+
+describe.runIf(isServe)('hmr', () => {
+ test('handle virtual module updates', async () => {
+ await page.goto(url)
+ const el = await page.$('.virtual')
+ expect(await el.textContent()).toBe('[success]')
+
+ const loadPromise = page.waitForEvent('load')
+ editFile('src/importedVirtual.js', (code) =>
+ code.replace('[success]', '[wow]'),
+ )
+ await loadPromise
+
+ await untilUpdated(async () => {
+ const el = await page.$('.virtual')
+ return await el.textContent()
+ }, '[wow]')
+ })
+})
+
+const execFileAsync = promisify(execFile)
+
+describe.runIf(isServe)('stacktrace', () => {
+ for (const ext of ['js', 'ts']) {
+ for (const sourcemapsEnabled of [false, true]) {
+ test(`stacktrace of ${ext} is correct when sourcemaps is${
+ sourcemapsEnabled ? '' : ' not'
+ } enabled in Node.js`, async () => {
+ const testStacktraceFile = path.resolve(
+ __dirname,
+ '../test-stacktrace.js',
+ )
+
+ const p = await execFileAsync('node', [
+ testStacktraceFile,
+ '' + sourcemapsEnabled,
+ ext,
+ ])
+ const lines = p.stdout
+ .split('\n')
+ .filter((line) => line.includes('Module.error'))
+
+ const reg = new RegExp(
+ path
+ .resolve(__dirname, '../src', `error-${ext}.${ext}`)
+ .replace(/\\/g, '\\\\') + ':2:9',
+ 'i',
+ )
+
+ lines.forEach((line) => {
+ expect(line.trim()).toMatch(reg)
+ })
+ })
+ }
+ }
+
+ test('with Vite runtime', async () => {
+ await execFileAsync('node', ['test-stacktrace-runtime.js'], {
+ cwd: fileURLToPath(new URL('..', import.meta.url)),
+ })
+ })
+})
+
+// --experimental-network-imports is going to be dropped
+// https://github.com/nodejs/node/pull/53822
+const noNetworkImports = Number(process.version.match(/^v(\d+)\./)[1]) >= 22
+
+describe.runIf(isServe && !noNetworkImports)('network-imports', () => {
+ test('with Vite SSR', async () => {
+ await execFileAsync(
+ 'node',
+ ['--experimental-network-imports', 'test-network-imports.js'],
+ {
+ cwd: fileURLToPath(new URL('..', import.meta.url)),
+ },
+ )
+ })
+
+ test('with Vite runtime', async () => {
+ await execFileAsync(
+ 'node',
+ [
+ '--experimental-network-imports',
+ 'test-network-imports.js',
+ '--module-runner',
+ ],
+ {
+ cwd: fileURLToPath(new URL('..', import.meta.url)),
+ },
+ )
+ })
+})
diff --git a/playground/ssr-html/index.html b/playground/ssr-html/index.html
new file mode 100644
index 00000000000000..afe1a3271bbe5c
--- /dev/null
+++ b/playground/ssr-html/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ SSR HTML
+
+
+
+ SSR Dynamic HTML
+
+
+
diff --git a/playground/ssr-html/package.json b/playground/ssr-html/package.json
new file mode 100644
index 00000000000000..a621a74567a369
--- /dev/null
+++ b/playground/ssr-html/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@vitejs/test-ssr-html",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "node server",
+ "serve": "NODE_ENV=production node server",
+ "debug": "node --inspect-brk server",
+ "test-stacktrace:off": "node test-stacktrace false",
+ "test-stacktrace:on": "node test-stacktrace true"
+ },
+ "dependencies": {},
+ "devDependencies": {
+ "express": "^5.1.0"
+ }
+}
diff --git a/playground/ssr-html/public/slash@3.0.0.js b/playground/ssr-html/public/slash@3.0.0.js
new file mode 100644
index 00000000000000..754082e97c4f82
--- /dev/null
+++ b/playground/ssr-html/public/slash@3.0.0.js
@@ -0,0 +1,5 @@
+/* eslint-disable */
+// copied from https://esm.sh/v133/slash@3.0.0/es2022/slash.mjs to reduce network issues in CI
+
+/* esm.sh - esbuild bundle(slash@3.0.0) es2022 production */
+var a=Object.create;var d=Object.defineProperty;var m=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var g=Object.getPrototypeOf,p=Object.prototype.hasOwnProperty;var A=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),E=(e,t)=>{for(var r in t)d(e,r,{get:t[r],enumerable:!0})},u=(e,t,r,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of x(t))!p.call(e,n)&&n!==r&&d(e,n,{get:()=>t[n],enumerable:!(i=m(t,n))||i.enumerable});return e},o=(e,t,r)=>(u(e,t,"default"),r&&u(r,t,"default")),c=(e,t,r)=>(r=e!=null?a(g(e)):{},u(t||!e||!e.__esModule?d(r,"default",{value:e,enumerable:!0}):r,e));var f=A((h,_)=>{"use strict";_.exports=e=>{let t=/^\\\\\?\\/.test(e),r=/[^\u0000-\u0080]+/.test(e);return t||r?e:e.replace(/\\/g,"/")}});var s={};E(s,{default:()=>P});var L=c(f());o(s,c(f()));var{default:l,...N}=L,P=l!==void 0?l:N;export{P as default};
diff --git a/playground/ssr-html/server.js b/playground/ssr-html/server.js
new file mode 100644
index 00000000000000..80531bea49418f
--- /dev/null
+++ b/playground/ssr-html/server.js
@@ -0,0 +1,113 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import express from 'express'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const isTest = process.env.VITEST
+
+const DYNAMIC_SCRIPTS = `
+
+
+`
+
+const DYNAMIC_STYLES = `
+
+`
+
+export async function createServer(root = process.cwd(), hmrPort) {
+ const resolve = (p) => path.resolve(__dirname, p)
+
+ const app = express()
+
+ /**
+ * @type {import('vite').ViteDevServer}
+ */
+ const vite = await (
+ await import('vite')
+ ).createServer({
+ root,
+ logLevel: isTest ? 'error' : 'info',
+ server: {
+ middlewareMode: true,
+ watch: {
+ // During tests we edit the files too fast and sometimes chokidar
+ // misses change events, so enforce polling for consistency
+ usePolling: true,
+ interval: 100,
+ },
+ hmr: {
+ port: hmrPort,
+ },
+ },
+ appType: 'custom',
+ plugins: [
+ {
+ name: 'virtual-file',
+ resolveId(id) {
+ if (id === 'virtual:file') {
+ return '\0virtual:file'
+ }
+ },
+ load(id) {
+ if (id === '\0virtual:file') {
+ return 'import { virtual } from "/src/importedVirtual.js"; export { virtual };'
+ }
+ },
+ },
+ ],
+ })
+ // use vite's connect instance as middleware
+ app.use(vite.middlewares)
+
+ app.use('*all', async (req, res, next) => {
+ try {
+ let [url] = req.originalUrl.split('?')
+ if (url.endsWith('/')) url += 'index.html'
+
+ if (url.startsWith('/favicon.ico')) {
+ return res.status(404).end('404')
+ }
+ if (url.startsWith('/@id/__x00__')) {
+ return next()
+ }
+
+ const htmlLoc = resolve(`.${url}`)
+ let template = fs.readFileSync(htmlLoc, 'utf-8')
+
+ template = template.replace(
+ '',
+ `${DYNAMIC_SCRIPTS}${DYNAMIC_STYLES}`,
+ )
+
+ // Force calling transformIndexHtml with url === '/', to simulate
+ // usage by ecosystem that was recommended in the SSR documentation
+ // as `const url = req.originalUrl`
+ const html = await vite.transformIndexHtml('/', template)
+
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
+ } catch (e) {
+ vite && vite.ssrFixStacktrace(e)
+ console.log(e.stack)
+ res.status(500).end(e.stack)
+ }
+ })
+
+ return { app, vite }
+}
+
+if (!isTest) {
+ createServer().then(({ app }) =>
+ app.listen(5173, () => {
+ console.log('http://localhost:5173')
+ }),
+ )
+}
diff --git a/playground/ssr-html/src/app.js b/playground/ssr-html/src/app.js
new file mode 100644
index 00000000000000..8612afffaea5ba
--- /dev/null
+++ b/playground/ssr-html/src/app.js
@@ -0,0 +1,11 @@
+import { virtual } from 'virtual:file'
+
+const p = document.createElement('p')
+p.innerHTML = '✅ Dynamically injected script from file'
+document.body.appendChild(p)
+
+text('.virtual', virtual)
+
+function text(el, text) {
+ document.querySelector(el).textContent = text
+}
diff --git a/playground/ssr-html/src/error-js.js b/playground/ssr-html/src/error-js.js
new file mode 100644
index 00000000000000..fe8eeb20af8f8a
--- /dev/null
+++ b/playground/ssr-html/src/error-js.js
@@ -0,0 +1,3 @@
+export function error() {
+ throw new Error('e')
+}
diff --git a/playground/ssr-html/src/error-ts.ts b/playground/ssr-html/src/error-ts.ts
new file mode 100644
index 00000000000000..fe8eeb20af8f8a
--- /dev/null
+++ b/playground/ssr-html/src/error-ts.ts
@@ -0,0 +1,3 @@
+export function error() {
+ throw new Error('e')
+}
diff --git a/playground/ssr-html/src/has-error-deep.ts b/playground/ssr-html/src/has-error-deep.ts
new file mode 100644
index 00000000000000..8da094a3fa4800
--- /dev/null
+++ b/playground/ssr-html/src/has-error-deep.ts
@@ -0,0 +1,7 @@
+function crash(message: string) {
+ throw new Error(message)
+}
+
+export function main(): void {
+ crash('crash')
+}
diff --git a/playground/ssr-html/src/importedVirtual.js b/playground/ssr-html/src/importedVirtual.js
new file mode 100644
index 00000000000000..8b0b417bc3113d
--- /dev/null
+++ b/playground/ssr-html/src/importedVirtual.js
@@ -0,0 +1 @@
+export const virtual = '[success]'
diff --git a/playground/ssr-html/src/network-imports.js b/playground/ssr-html/src/network-imports.js
new file mode 100644
index 00000000000000..770fb6e57b610d
--- /dev/null
+++ b/playground/ssr-html/src/network-imports.js
@@ -0,0 +1,7 @@
+// same port as `ports["ssr-html"]` in playground/test-utils.ts
+import slash from 'http://localhost:9602/slash@3.0.0.js'
+
+// or test without local server
+// import slash from 'https://esm.sh/slash@3.0.0'
+
+export { slash }
diff --git a/playground/ssr-html/test-network-imports.js b/playground/ssr-html/test-network-imports.js
new file mode 100644
index 00000000000000..d205acb12ee138
--- /dev/null
+++ b/playground/ssr-html/test-network-imports.js
@@ -0,0 +1,27 @@
+import assert from 'node:assert'
+import { fileURLToPath } from 'node:url'
+import { createServer, createServerModuleRunner } from 'vite'
+
+async function runTest(userRunner) {
+ const server = await createServer({
+ configFile: false,
+ root: fileURLToPath(new URL('.', import.meta.url)),
+ server: {
+ middlewareMode: true,
+ ws: false,
+ },
+ })
+ let mod
+ if (userRunner) {
+ const runner = await createServerModuleRunner(server.environments.ssr, {
+ hmr: false,
+ })
+ mod = await runner.import('/src/network-imports.js')
+ } else {
+ mod = await server.ssrLoadModule('/src/network-imports.js')
+ }
+ assert.equal(mod.slash('foo\\bar'), 'foo/bar')
+ await server.close()
+}
+
+runTest(process.argv.includes('--module-runner'))
diff --git a/playground/ssr-html/test-stacktrace-runtime.js b/playground/ssr-html/test-stacktrace-runtime.js
new file mode 100644
index 00000000000000..a53b7fbcdf8769
--- /dev/null
+++ b/playground/ssr-html/test-stacktrace-runtime.js
@@ -0,0 +1,30 @@
+import { fileURLToPath } from 'node:url'
+import assert from 'node:assert'
+import { createServer, createServerModuleRunner } from 'vite'
+
+// same test case as packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts
+// implemented for e2e to catch build specific behavior
+
+const server = await createServer({
+ configFile: false,
+ root: fileURLToPath(new URL('.', import.meta.url)),
+ server: {
+ middlewareMode: true,
+ ws: false,
+ },
+})
+
+const runner = await createServerModuleRunner(server.environments.ssr, {
+ sourcemapInterceptor: 'prepareStackTrace',
+})
+
+const mod = await runner.import('/src/has-error-deep.ts')
+let error
+try {
+ mod.main()
+} catch (e) {
+ error = e
+} finally {
+ await server.close()
+}
+assert.match(error?.stack, /has-error-deep.ts:6:3/)
diff --git a/playground/ssr-html/test-stacktrace.js b/playground/ssr-html/test-stacktrace.js
new file mode 100644
index 00000000000000..d8c0f1b768a0bc
--- /dev/null
+++ b/playground/ssr-html/test-stacktrace.js
@@ -0,0 +1,46 @@
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { createServer } from 'vite'
+
+const isSourceMapEnabled = process.argv[2] === 'true'
+const ext = process.argv[3]
+process.setSourceMapsEnabled(isSourceMapEnabled)
+console.log('# sourcemaps enabled:', isSourceMapEnabled)
+console.log('# source file extension:', ext)
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const isTest = process.env.VITEST
+
+const vite = await createServer({
+ root: __dirname,
+ logLevel: isTest ? 'error' : 'info',
+ server: {
+ middlewareMode: true,
+ ws: false,
+ },
+ appType: 'custom',
+})
+
+const dir = path.dirname(fileURLToPath(import.meta.url))
+
+const abs1 = await vite.ssrLoadModule(`/src/error-${ext}.${ext}`)
+const abs2 = await vite.ssrLoadModule(
+ path.resolve(dir, `./src/error-${ext}.${ext}`),
+)
+const relative = await vite.ssrLoadModule(`./src/error-${ext}.${ext}`)
+
+for (const mod of [abs1, abs2, relative]) {
+ try {
+ mod.error()
+ } catch (e) {
+ // this should not be called
+ // when sourcemap support for `new Function` is supported and sourcemap is enabled
+ // because the stacktrace is already rewritten by Node.js
+ if (!isSourceMapEnabled) {
+ vite.ssrFixStacktrace(e)
+ }
+ console.log(e)
+ }
+}
+
+await vite.close()
diff --git a/playground/ssr-noexternal/__tests__/serve.ts b/playground/ssr-noexternal/__tests__/serve.ts
new file mode 100644
index 00000000000000..47bb4c5236294f
--- /dev/null
+++ b/playground/ssr-noexternal/__tests__/serve.ts
@@ -0,0 +1,52 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+import path from 'node:path'
+import kill from 'kill-port'
+import { hmrPorts, isBuild, ports, rootDir } from '~utils'
+
+export const port = ports['ssr-noexternal']
+
+export async function serve(): Promise<{ close(): Promise }> {
+ if (isBuild) {
+ // build first
+ const { build } = await import('vite')
+ // server build
+ await build({
+ root: rootDir,
+ logLevel: 'silent',
+ build: {
+ ssr: 'src/entry-server.js',
+ },
+ })
+ }
+
+ await kill(port)
+
+ const { createServer } = await import(path.resolve(rootDir, 'server.js'))
+ const { app, vite } = await createServer(
+ rootDir,
+ isBuild,
+ hmrPorts['ssr-noexternal'],
+ )
+
+ return new Promise((resolve, reject) => {
+ try {
+ const server = app.listen(port, () => {
+ resolve({
+ // for test teardown
+ async close() {
+ await new Promise((resolve) => {
+ server.close(resolve)
+ })
+ if (vite) {
+ await vite.close()
+ }
+ },
+ })
+ })
+ } catch (e) {
+ reject(e)
+ }
+ })
+}
diff --git a/playground/ssr-noexternal/__tests__/ssr-noexternal.spec.ts b/playground/ssr-noexternal/__tests__/ssr-noexternal.spec.ts
new file mode 100644
index 00000000000000..ab41f0a4602454
--- /dev/null
+++ b/playground/ssr-noexternal/__tests__/ssr-noexternal.spec.ts
@@ -0,0 +1,10 @@
+import { expect, test } from 'vitest'
+import { port } from './serve'
+import { isBuild, page } from '~utils'
+
+const url = `http://localhost:${port}`
+
+test.runIf(!isBuild)('message from require-external-cjs', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.require-external-cjs')).toMatch('foo')
+})
diff --git a/playground/ssr-noexternal/external-cjs/import.mjs b/playground/ssr-noexternal/external-cjs/import.mjs
new file mode 100644
index 00000000000000..01ddf7976bf76d
--- /dev/null
+++ b/playground/ssr-noexternal/external-cjs/import.mjs
@@ -0,0 +1 @@
+throw new Error('shouldnt be loaded')
diff --git a/playground/ssr-noexternal/external-cjs/package.json b/playground/ssr-noexternal/external-cjs/package.json
new file mode 100644
index 00000000000000..dee5a252b94259
--- /dev/null
+++ b/playground/ssr-noexternal/external-cjs/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-external-cjs",
+ "private": true,
+ "version": "0.0.0",
+ "exports": {
+ "require": "./require.cjs",
+ "import": "./import.mjs"
+ }
+}
diff --git a/playground/ssr-noexternal/external-cjs/require.cjs b/playground/ssr-noexternal/external-cjs/require.cjs
new file mode 100644
index 00000000000000..3b8256e4e0efb9
--- /dev/null
+++ b/playground/ssr-noexternal/external-cjs/require.cjs
@@ -0,0 +1 @@
+module.exports = 'foo'
diff --git a/playground/ssr-noexternal/index.html b/playground/ssr-noexternal/index.html
new file mode 100644
index 00000000000000..eb36840fb1ed71
--- /dev/null
+++ b/playground/ssr-noexternal/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
diff --git a/playground/ssr-noexternal/package.json b/playground/ssr-noexternal/package.json
new file mode 100644
index 00000000000000..bee896d2d0846a
--- /dev/null
+++ b/playground/ssr-noexternal/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@vitejs/test-ssr-noexternal",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "node server",
+ "build": "vite build --ssr src/entry-server.js",
+ "serve": "NODE_ENV=production node server",
+ "debug": "node --inspect-brk server"
+ },
+ "dependencies": {
+ "@vitejs/test-external-cjs": "file:./external-cjs",
+ "@vitejs/test-require-external-cjs": "file:./require-external-cjs",
+ "express": "^5.1.0"
+ }
+}
diff --git a/playground/ssr-noexternal/require-external-cjs/main.js b/playground/ssr-noexternal/require-external-cjs/main.js
new file mode 100644
index 00000000000000..7eef42446ca6db
--- /dev/null
+++ b/playground/ssr-noexternal/require-external-cjs/main.js
@@ -0,0 +1 @@
+module.exports = require('@vitejs/test-external-cjs')
diff --git a/playground/ssr-noexternal/require-external-cjs/package.json b/playground/ssr-noexternal/require-external-cjs/package.json
new file mode 100644
index 00000000000000..f6c44ae0180847
--- /dev/null
+++ b/playground/ssr-noexternal/require-external-cjs/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@vitejs/test-require-external-cjs",
+ "type": "commonjs",
+ "private": true,
+ "version": "0.0.0",
+ "main": "main.js",
+ "dependencies": {
+ "@vitejs/test-external-cjs": "file:../external-cjs"
+ }
+}
diff --git a/playground/ssr-noexternal/server.js b/playground/ssr-noexternal/server.js
new file mode 100644
index 00000000000000..73cee5cfe8fa59
--- /dev/null
+++ b/playground/ssr-noexternal/server.js
@@ -0,0 +1,86 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import express from 'express'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+const isTest = process.env.VITEST
+
+export async function createServer(
+ root = process.cwd(),
+ isProd = process.env.NODE_ENV === 'production',
+ hmrPort,
+) {
+ const resolve = (p) => path.resolve(__dirname, p)
+
+ const indexProd = isProd
+ ? fs.readFileSync(resolve('index.html'), 'utf-8')
+ : ''
+
+ const app = express()
+
+ /**
+ * @type {import('vite').ViteDevServer}
+ */
+ let vite
+ if (!isProd) {
+ vite = await (
+ await import('vite')
+ ).createServer({
+ root,
+ logLevel: isTest ? 'error' : 'info',
+ server: {
+ middlewareMode: true,
+ watch: {
+ // During tests we edit the files too fast and sometimes chokidar
+ // misses change events, so enforce polling for consistency
+ usePolling: true,
+ interval: 100,
+ },
+ hmr: {
+ port: hmrPort,
+ },
+ },
+ appType: 'custom',
+ })
+ app.use(vite.middlewares)
+ }
+
+ app.use('*all', async (req, res) => {
+ try {
+ const url = req.originalUrl
+
+ let template, render
+ if (!isProd) {
+ // always read fresh template in dev
+ template = fs.readFileSync(resolve('index.html'), 'utf-8')
+ template = await vite.transformIndexHtml(url, template)
+ render = (await vite.ssrLoadModule('/src/entry-server.js')).render
+ } else {
+ template = indexProd
+ render = (await import('./dist/entry-server.js')).render
+ }
+
+ const appHtml = await render(url)
+
+ const html = template.replace(``, appHtml)
+
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
+ } catch (e) {
+ !isProd && vite.ssrFixStacktrace(e)
+ console.log(e.stack)
+ res.status(500).end(e.stack)
+ }
+ })
+
+ return { app, vite }
+}
+
+if (!isTest) {
+ createServer().then(({ app }) =>
+ app.listen(5173, () => {
+ console.log('http://localhost:5173')
+ }),
+ )
+}
diff --git a/playground/ssr-noexternal/src/entry-server.js b/playground/ssr-noexternal/src/entry-server.js
new file mode 100644
index 00000000000000..2b69eaa2645244
--- /dev/null
+++ b/playground/ssr-noexternal/src/entry-server.js
@@ -0,0 +1,9 @@
+import requireExternalCjs from '@vitejs/test-require-external-cjs'
+
+export async function render(url) {
+ let html = ''
+
+ html += `\nmessage from require-external-cjs: ${requireExternalCjs}
`
+
+ return html + '\n'
+}
diff --git a/playground/ssr-noexternal/vite.config.js b/playground/ssr-noexternal/vite.config.js
new file mode 100644
index 00000000000000..1109bddd187001
--- /dev/null
+++ b/playground/ssr-noexternal/vite.config.js
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite'
+
+const noExternal = ['@vitejs/test-require-external-cjs']
+export default defineConfig({
+ ssr: {
+ noExternal,
+ external: ['@vitejs/test-external-cjs'],
+ optimizeDeps: {
+ include: noExternal,
+ },
+ },
+ build: {
+ target: 'esnext',
+ minify: false,
+ rollupOptions: {
+ external: ['@vitejs/test-external-cjs'],
+ },
+ },
+})
diff --git a/playground/ssr-pug/__tests__/serve.ts b/playground/ssr-pug/__tests__/serve.ts
new file mode 100644
index 00000000000000..715c38e99801ad
--- /dev/null
+++ b/playground/ssr-pug/__tests__/serve.ts
@@ -0,0 +1,35 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+import path from 'node:path'
+import kill from 'kill-port'
+import { hmrPorts, ports, rootDir } from '~utils'
+
+export const port = ports['ssr-pug']
+
+export async function serve(): Promise<{ close(): Promise }> {
+ await kill(port)
+
+ const { createServer } = await import(path.resolve(rootDir, 'server.js'))
+ const { app, vite } = await createServer(rootDir, hmrPorts['ssr-pug'])
+
+ return new Promise((resolve, reject) => {
+ try {
+ const server = app.listen(port, () => {
+ resolve({
+ // for test teardown
+ async close() {
+ await new Promise((resolve) => {
+ server.close(resolve)
+ })
+ if (vite) {
+ await vite.close()
+ }
+ },
+ })
+ })
+ } catch (e) {
+ reject(e)
+ }
+ })
+}
diff --git a/packages/playground/ssr-pug/__tests__/ssr-pug.spec.ts b/playground/ssr-pug/__tests__/ssr-pug.spec.ts
similarity index 88%
rename from packages/playground/ssr-pug/__tests__/ssr-pug.spec.ts
rename to playground/ssr-pug/__tests__/ssr-pug.spec.ts
index e34b8a91fc3421..6689a6c4694c9c 100644
--- a/packages/playground/ssr-pug/__tests__/ssr-pug.spec.ts
+++ b/playground/ssr-pug/__tests__/ssr-pug.spec.ts
@@ -1,5 +1,6 @@
+import { describe, expect, test } from 'vitest'
import { port } from './serve'
-import fetch from 'node-fetch'
+import { page } from '~utils'
const url = `http://localhost:${port}`
@@ -7,7 +8,7 @@ describe('injected inline scripts', () => {
test('no injected inline scripts are present', async () => {
await page.goto(url)
const inlineScripts = await page.$$eval('script', (nodes) =>
- nodes.filter((n) => !n.getAttribute('src') && n.innerHTML)
+ nodes.filter((n) => !n.getAttribute('src') && n.innerHTML),
)
expect(inlineScripts).toHaveLength(0)
})
@@ -21,14 +22,14 @@ describe('injected inline scripts', () => {
if (!src) return false
return src.includes('?html-proxy&index')
})
- .map((n) => n.getAttribute('src'))
+ .map((n) => n.getAttribute('src')),
)
// assert at least 1 proxied script exists
expect(proxiedScripts).not.toHaveLength(0)
const scriptContents = await Promise.all(
- proxiedScripts.map((src) => fetch(url + src).then((res) => res.text()))
+ proxiedScripts.map((src) => fetch(url + src).then((res) => res.text())),
)
// all proxied scripts return code
diff --git a/packages/playground/ssr-pug/index.pug b/playground/ssr-pug/index.pug
similarity index 100%
rename from packages/playground/ssr-pug/index.pug
rename to playground/ssr-pug/index.pug
diff --git a/playground/ssr-pug/package.json b/playground/ssr-pug/package.json
new file mode 100644
index 00000000000000..2e7e9b2f2a942b
--- /dev/null
+++ b/playground/ssr-pug/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-ssr-pug",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "node server",
+ "serve": "NODE_ENV=production node server",
+ "debug": "node --inspect-brk server"
+ },
+ "devDependencies": {
+ "express": "^5.1.0",
+ "pug": "^3.0.3"
+ }
+}
diff --git a/playground/ssr-pug/server.js b/playground/ssr-pug/server.js
new file mode 100644
index 00000000000000..18f3c1ed151afe
--- /dev/null
+++ b/playground/ssr-pug/server.js
@@ -0,0 +1,78 @@
+// @ts-check
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import pug from 'pug'
+import express from 'express'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+const isTest = process.env.VITEST
+
+const DYNAMIC_SCRIPTS = `
+
+
+`
+
+export async function createServer(root = process.cwd(), hmrPort) {
+ const resolve = (p) => path.resolve(__dirname, p)
+
+ const app = express()
+
+ /**
+ * @type {import('vite').ViteDevServer}
+ */
+ const vite = await (
+ await import('vite')
+ ).createServer({
+ root,
+ logLevel: isTest ? 'error' : 'info',
+ server: {
+ middlewareMode: true,
+ watch: {
+ // During tests we edit the files too fast and sometimes chokidar
+ // misses change events, so enforce polling for consistency
+ usePolling: true,
+ interval: 100,
+ },
+ hmr: {
+ port: hmrPort,
+ },
+ },
+ appType: 'custom',
+ })
+ // use vite's connect instance as middleware
+ app.use(vite.middlewares)
+
+ app.use('*all', async (req, res) => {
+ try {
+ let [url] = req.originalUrl.split('?')
+ url = url.replace(/\.html$/, '.pug')
+ if (url.endsWith('/')) url += 'index.pug'
+
+ const htmlLoc = resolve(`.${url}`)
+ let html = pug.renderFile(htmlLoc)
+ html = html.replace('', `${DYNAMIC_SCRIPTS}`)
+ html = await vite.transformIndexHtml(url, html)
+
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
+ } catch (e) {
+ vite && vite.ssrFixStacktrace(e)
+ console.log(e.stack)
+ res.status(500).end(e.stack)
+ }
+ })
+
+ return { app, vite }
+}
+
+if (!isTest) {
+ createServer().then(({ app }) =>
+ app.listen(5173, () => {
+ console.log('http://localhost:5173')
+ }),
+ )
+}
diff --git a/packages/playground/ssr-html/src/app.js b/playground/ssr-pug/src/app.js
similarity index 100%
rename from packages/playground/ssr-html/src/app.js
rename to playground/ssr-pug/src/app.js
diff --git a/playground/ssr-resolve/__tests__/ssr-resolve.spec.ts b/playground/ssr-resolve/__tests__/ssr-resolve.spec.ts
new file mode 100644
index 00000000000000..3920d4ccbd052e
--- /dev/null
+++ b/playground/ssr-resolve/__tests__/ssr-resolve.spec.ts
@@ -0,0 +1,35 @@
+import { expect, test } from 'vitest'
+import { isBuild, readFile, testDir } from '~utils'
+
+test.runIf(isBuild)('correctly resolve entrypoints', async () => {
+ const contents = readFile('dist/main.mjs')
+
+ const _ = `['"]`
+ expect(contents).toMatch(
+ new RegExp(`from ${_}@vitejs/test-entries/dir/index.js${_}`),
+ )
+ expect(contents).toMatch(
+ new RegExp(`from ${_}@vitejs/test-entries/file.js${_}`),
+ )
+ expect(contents).toMatch(
+ new RegExp(`from ${_}@vitejs/test-resolve-pkg-exports/entry${_}`),
+ )
+
+ expect(contents).toMatch(
+ new RegExp(`from ${_}@vitejs/test-deep-import/foo/index.js${_}`),
+ )
+
+ expect(contents).toMatch(
+ new RegExp(`from ${_}@vitejs/test-deep-import/bar${_}`),
+ )
+
+ await expect(import(`${testDir}/dist/main.mjs`)).resolves.toBeTruthy()
+})
+
+test.runIf(isBuild)(
+ 'node builtins should not be bundled if not used',
+ async () => {
+ const contents = readFile('dist/main.mjs')
+ expect(contents).not.include(`node:url`)
+ },
+)
diff --git a/playground/ssr-resolve/deep-import/bar/package.json b/playground/ssr-resolve/deep-import/bar/package.json
new file mode 100644
index 00000000000000..9c28d3fd659022
--- /dev/null
+++ b/playground/ssr-resolve/deep-import/bar/package.json
@@ -0,0 +1,7 @@
+{
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "../utils/bar.js",
+ "module": "../utils/bar.js"
+}
diff --git a/playground/ssr-resolve/deep-import/foo/index.js b/playground/ssr-resolve/deep-import/foo/index.js
new file mode 100644
index 00000000000000..7e942cf45c8a37
--- /dev/null
+++ b/playground/ssr-resolve/deep-import/foo/index.js
@@ -0,0 +1 @@
+export default 'foo'
diff --git a/playground/ssr-resolve/deep-import/foo/package.json b/playground/ssr-resolve/deep-import/foo/package.json
new file mode 100644
index 00000000000000..23c18d67103169
--- /dev/null
+++ b/playground/ssr-resolve/deep-import/foo/package.json
@@ -0,0 +1,6 @@
+{
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "module": "./index.js"
+}
diff --git a/playground/ssr-resolve/deep-import/index.js b/playground/ssr-resolve/deep-import/index.js
new file mode 100644
index 00000000000000..d397f45e627464
--- /dev/null
+++ b/playground/ssr-resolve/deep-import/index.js
@@ -0,0 +1,3 @@
+export { default as foo } from './foo'
+export { default as bar } from './bar'
+export default 'external-nested'
diff --git a/playground/ssr-resolve/deep-import/package.json b/playground/ssr-resolve/deep-import/package.json
new file mode 100644
index 00000000000000..373581df14f471
--- /dev/null
+++ b/playground/ssr-resolve/deep-import/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@vitejs/test-deep-import",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "module": "index.js"
+}
diff --git a/playground/ssr-resolve/deep-import/utils/bar.js b/playground/ssr-resolve/deep-import/utils/bar.js
new file mode 100644
index 00000000000000..4548a26ba14dc8
--- /dev/null
+++ b/playground/ssr-resolve/deep-import/utils/bar.js
@@ -0,0 +1 @@
+export default 'bar'
diff --git a/playground/ssr-resolve/entries/dir/index.js b/playground/ssr-resolve/entries/dir/index.js
new file mode 100644
index 00000000000000..29a5acb7305fc0
--- /dev/null
+++ b/playground/ssr-resolve/entries/dir/index.js
@@ -0,0 +1 @@
+module.exports = __filename.slice(__filename.lastIndexOf('entries'))
diff --git a/playground/ssr-resolve/entries/file.js b/playground/ssr-resolve/entries/file.js
new file mode 100644
index 00000000000000..29a5acb7305fc0
--- /dev/null
+++ b/playground/ssr-resolve/entries/file.js
@@ -0,0 +1 @@
+module.exports = __filename.slice(__filename.lastIndexOf('entries'))
diff --git a/playground/ssr-resolve/entries/package.json b/playground/ssr-resolve/entries/package.json
new file mode 100644
index 00000000000000..731bfeeb1a016e
--- /dev/null
+++ b/playground/ssr-resolve/entries/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@vitejs/test-entries",
+ "private": true,
+ "version": "0.0.0"
+}
diff --git a/playground/ssr-resolve/main.js b/playground/ssr-resolve/main.js
new file mode 100644
index 00000000000000..699ca580034a28
--- /dev/null
+++ b/playground/ssr-resolve/main.js
@@ -0,0 +1,18 @@
+// no `exports` key, should resolve to entries/dir/index.js
+import dirEntry from '@vitejs/test-entries/dir'
+// no `exports` key, should resolve to entries/file.js
+import fileEntry from '@vitejs/test-entries/file'
+// has `exports` key, should resolve to pkg-exports/entry
+import pkgExportsEntry from '@vitejs/test-resolve-pkg-exports/entry'
+import deepFoo from '@vitejs/test-deep-import/foo'
+import deepBar from '@vitejs/test-deep-import/bar'
+import { used } from './util'
+
+export default `
+ entries/dir: ${dirEntry}
+ entries/file: ${fileEntry}
+ pkg-exports/entry: ${pkgExportsEntry}
+ deep-import/foo: ${deepFoo}
+ deep-import/bar: ${deepBar}
+ util: ${used(['[success]'])}
+`
diff --git a/playground/ssr-resolve/package.json b/playground/ssr-resolve/package.json
new file mode 100644
index 00000000000000..a764b385967ed2
--- /dev/null
+++ b/playground/ssr-resolve/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-ssr-resolve",
+ "private": true,
+ "version": "0.0.0",
+ "type": "commonjs",
+ "scripts": {
+ "build": "vite build",
+ "debug": "node --inspect-brk ../../packages/vite/bin/vite build"
+ },
+ "dependencies": {
+ "@vitejs/test-entries": "file:./entries",
+ "@vitejs/test-resolve-pkg-exports": "file:./pkg-exports",
+ "@vitejs/test-deep-import": "file:./deep-import"
+ }
+}
diff --git a/playground/ssr-resolve/pkg-exports/entry.js b/playground/ssr-resolve/pkg-exports/entry.js
new file mode 100644
index 00000000000000..880189c611bf02
--- /dev/null
+++ b/playground/ssr-resolve/pkg-exports/entry.js
@@ -0,0 +1 @@
+module.exports = 'pkg-exports entry'
diff --git a/playground/ssr-resolve/pkg-exports/index.js b/playground/ssr-resolve/pkg-exports/index.js
new file mode 100644
index 00000000000000..c5f2ccf114fb46
--- /dev/null
+++ b/playground/ssr-resolve/pkg-exports/index.js
@@ -0,0 +1 @@
+module.exports = undefined
diff --git a/playground/ssr-resolve/pkg-exports/package.json b/playground/ssr-resolve/pkg-exports/package.json
new file mode 100644
index 00000000000000..9f1227fb21ebe2
--- /dev/null
+++ b/playground/ssr-resolve/pkg-exports/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@vitejs/test-resolve-pkg-exports",
+ "private": true,
+ "version": "0.0.0",
+ "exports": {
+ ".": "./index.js",
+ "./entry": "./entry.js"
+ }
+}
diff --git a/playground/ssr-resolve/util.js b/playground/ssr-resolve/util.js
new file mode 100644
index 00000000000000..c3234a470328cf
--- /dev/null
+++ b/playground/ssr-resolve/util.js
@@ -0,0 +1,10 @@
+import { pathToFileURL } from 'node:url'
+
+export function used(s) {
+ return s
+}
+
+// This is not used, so `node:url` should not be bundled
+export function treeshaken(s) {
+ return pathToFileURL(s)
+}
diff --git a/playground/ssr-resolve/vite.config.js b/playground/ssr-resolve/vite.config.js
new file mode 100644
index 00000000000000..206485d1088b8c
--- /dev/null
+++ b/playground/ssr-resolve/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ ssr: './main.js',
+ },
+})
diff --git a/playground/ssr-webworker/__tests__/serve.ts b/playground/ssr-webworker/__tests__/serve.ts
new file mode 100644
index 00000000000000..8d384a696eeaf8
--- /dev/null
+++ b/playground/ssr-webworker/__tests__/serve.ts
@@ -0,0 +1,37 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+import path from 'node:path'
+import kill from 'kill-port'
+import { ports, rootDir } from '~utils'
+
+export const port = ports['ssr-webworker']
+
+export async function serve(): Promise<{ close(): Promise }> {
+ await kill(port)
+
+ // we build first, regardless of whether it's prod/build mode
+ // because Vite doesn't support the concept of a "webworker server"
+ const { build } = await import('vite')
+
+ // worker build
+ await build({
+ root: rootDir,
+ logLevel: 'silent',
+ build: {
+ target: 'esnext',
+ ssr: 'src/entry-worker.jsx',
+ outDir: 'dist/worker',
+ },
+ })
+
+ const { createServer } = await import(path.resolve(rootDir, 'worker.js'))
+ const { mf } = await createServer(port)
+
+ return {
+ // for test teardown
+ async close() {
+ await mf.dispose()
+ },
+ }
+}
diff --git a/playground/ssr-webworker/__tests__/ssr-webworker.spec.ts b/playground/ssr-webworker/__tests__/ssr-webworker.spec.ts
new file mode 100644
index 00000000000000..65a01f196df295
--- /dev/null
+++ b/playground/ssr-webworker/__tests__/ssr-webworker.spec.ts
@@ -0,0 +1,36 @@
+import { expect, test } from 'vitest'
+import { port } from './serve'
+import { findAssetFile, isBuild, page } from '~utils'
+
+const url = `http://localhost:${port}`
+
+test('/', async () => {
+ await page.goto(url + '/')
+ expect(await page.textContent('h1')).toMatch('hello from webworker')
+ expect(await page.textContent('.linked')).toMatch('dep from upper directory')
+ expect(await page.textContent('.external')).toMatch('object')
+})
+
+test('supports resolve.conditions', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.worker-exports')).toMatch('[success] worker')
+})
+
+test('respects browser export', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.browser-exports')).toMatch(
+ '[success] browser',
+ )
+})
+
+test('supports nodejs_compat', async () => {
+ await page.goto(url)
+ expect(await page.textContent('.nodejs-compat')).toMatch(
+ '[success] nodejs compat',
+ )
+})
+
+test.runIf(isBuild)('inlineDynamicImports', () => {
+ const dynamicJsContent = findAssetFile(/dynamic-[-\w]+\.js/, 'worker')
+ expect(dynamicJsContent).toBe('')
+})
diff --git a/playground/ssr-webworker/browser-exports/browser.js b/playground/ssr-webworker/browser-exports/browser.js
new file mode 100644
index 00000000000000..a2f54551f2dc4a
--- /dev/null
+++ b/playground/ssr-webworker/browser-exports/browser.js
@@ -0,0 +1 @@
+export default '[success] browser'
diff --git a/playground/ssr-webworker/browser-exports/node.js b/playground/ssr-webworker/browser-exports/node.js
new file mode 100644
index 00000000000000..b55d59fc6c4b95
--- /dev/null
+++ b/playground/ssr-webworker/browser-exports/node.js
@@ -0,0 +1 @@
+export default '[fail] should not load me'
diff --git a/playground/ssr-webworker/browser-exports/package.json b/playground/ssr-webworker/browser-exports/package.json
new file mode 100644
index 00000000000000..daabadafafc095
--- /dev/null
+++ b/playground/ssr-webworker/browser-exports/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@vitejs/test-browser-exports",
+ "private": true,
+ "version": "0.0.0",
+ "exports": {
+ ".": {
+ "browser": "./browser.js",
+ "node": "./node.js",
+ "default": "./node.js"
+ }
+ }
+}
diff --git a/playground/ssr-webworker/package.json b/playground/ssr-webworker/package.json
new file mode 100644
index 00000000000000..05bb4c3b230878
--- /dev/null
+++ b/playground/ssr-webworker/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@vitejs/test-ssr-webworker",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "DEV=1 node worker",
+ "build:worker": "vite build --ssr src/entry-worker.jsx --outDir dist/worker"
+ },
+ "dependencies": {
+ "react": "^19.1.0",
+ "@vitejs/test-browser-exports": "file:./browser-exports",
+ "@vitejs/test-worker-exports": "file:./worker-exports"
+ },
+ "devDependencies": {
+ "miniflare": "^4.20250507.0",
+ "@vitejs/test-resolve-linked": "workspace:*"
+ }
+}
diff --git a/playground/ssr-webworker/src/dynamic.js b/playground/ssr-webworker/src/dynamic.js
new file mode 100644
index 00000000000000..cb356468240d50
--- /dev/null
+++ b/playground/ssr-webworker/src/dynamic.js
@@ -0,0 +1 @@
+export const foo = 'foo'
diff --git a/playground/ssr-webworker/src/entry-worker.jsx b/playground/ssr-webworker/src/entry-worker.jsx
new file mode 100644
index 00000000000000..5068c796481df2
--- /dev/null
+++ b/playground/ssr-webworker/src/entry-worker.jsx
@@ -0,0 +1,31 @@
+import { equal } from 'node:assert'
+import { msg as linkedMsg } from '@vitejs/test-resolve-linked'
+import browserExportsMessage from '@vitejs/test-browser-exports'
+import workerExportsMessage from '@vitejs/test-worker-exports'
+import React from 'react'
+
+let loaded = false
+import('./dynamic').then(({ foo }) => {
+ loaded = !!foo
+})
+
+addEventListener('fetch', function (event) {
+ return event.respondWith(
+ new Response(
+ `
+ hello from webworker
+ ${linkedMsg}
+ ${typeof React}
+ dynamic: ${loaded}
+ ${browserExportsMessage}
+ ${workerExportsMessage}
+ ${equal('a', 'a') || '[success] nodejs compat'}
+ `,
+ {
+ headers: {
+ 'content-type': 'text/html',
+ },
+ },
+ ),
+ )
+})
diff --git a/playground/ssr-webworker/vite.config.js b/playground/ssr-webworker/vite.config.js
new file mode 100644
index 00000000000000..a2542d7d422c07
--- /dev/null
+++ b/playground/ssr-webworker/vite.config.js
@@ -0,0 +1,42 @@
+import { defaultClientConditions, defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ minify: false,
+ },
+ resolve: {
+ dedupe: ['react'],
+ },
+ ssr: {
+ target: 'webworker',
+ noExternal: ['this-should-be-replaced-by-the-boolean'],
+ // Some webworker builds may choose to externalize node builtins as they may be implemented
+ // in the runtime, and so we can externalize it when bundling.
+ external: ['node:assert'],
+ resolve: {
+ conditions: [...defaultClientConditions, 'worker'],
+ },
+ },
+ plugins: [
+ {
+ name: '@vitejs/test-ssr-webworker/no-external',
+ config() {
+ return {
+ ssr: {
+ noExternal: true,
+ },
+ }
+ },
+ },
+ {
+ name: '@vitejs/test-ssr-webworker/no-external-array',
+ config() {
+ return {
+ ssr: {
+ noExternal: ['this-should-not-replace-the-boolean'],
+ },
+ }
+ },
+ },
+ ],
+})
diff --git a/playground/ssr-webworker/worker-exports/browser.js b/playground/ssr-webworker/worker-exports/browser.js
new file mode 100644
index 00000000000000..819b0ae6e9556f
--- /dev/null
+++ b/playground/ssr-webworker/worker-exports/browser.js
@@ -0,0 +1,2 @@
+// conditions are set to worker, and worker is higher up in the exports object in package.json, so should be preferred
+export default '[fail] should not load me'
diff --git a/playground/ssr-webworker/worker-exports/node.js b/playground/ssr-webworker/worker-exports/node.js
new file mode 100644
index 00000000000000..b55d59fc6c4b95
--- /dev/null
+++ b/playground/ssr-webworker/worker-exports/node.js
@@ -0,0 +1 @@
+export default '[fail] should not load me'
diff --git a/playground/ssr-webworker/worker-exports/package.json b/playground/ssr-webworker/worker-exports/package.json
new file mode 100644
index 00000000000000..c8e1a93163d5c6
--- /dev/null
+++ b/playground/ssr-webworker/worker-exports/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@vitejs/test-worker-exports",
+ "private": true,
+ "version": "0.0.0",
+ "exports": {
+ ".": {
+ "worker": "./worker.js",
+ "browser": "./browser.js",
+ "node": "./node.js",
+ "default": "./node.js"
+ }
+ }
+}
diff --git a/playground/ssr-webworker/worker-exports/worker.js b/playground/ssr-webworker/worker-exports/worker.js
new file mode 100644
index 00000000000000..8cd0fa65046d76
--- /dev/null
+++ b/playground/ssr-webworker/worker-exports/worker.js
@@ -0,0 +1 @@
+export default '[success] worker'
diff --git a/playground/ssr-webworker/worker.js b/playground/ssr-webworker/worker.js
new file mode 100644
index 00000000000000..a910cfc02907e7
--- /dev/null
+++ b/playground/ssr-webworker/worker.js
@@ -0,0 +1,22 @@
+import { fileURLToPath } from 'node:url'
+import path from 'node:path'
+import { Miniflare } from 'miniflare'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+const isTest = !!process.env.TEST
+
+export async function createServer(port) {
+ const mf = new Miniflare({
+ scriptPath: path.resolve(__dirname, 'dist/worker/entry-worker.js'),
+ port,
+ modules: true,
+ compatibilityFlags: ['nodejs_compat'],
+ })
+ await mf.ready
+ return { mf }
+}
+
+if (!isTest) {
+ createServer(5173).then(() => console.log('http://localhost:5173'))
+}
diff --git a/playground/ssr/__tests__/serve.ts b/playground/ssr/__tests__/serve.ts
new file mode 100644
index 00000000000000..96e2044aa8b1dd
--- /dev/null
+++ b/playground/ssr/__tests__/serve.ts
@@ -0,0 +1,41 @@
+// this is automatically detected by playground/vitestSetup.ts and will replace
+// the default e2e test serve behavior
+
+import path from 'node:path'
+import kill from 'kill-port'
+import { createInMemoryLogger, hmrPorts, ports, rootDir } from '~utils'
+
+export const port = ports.ssr
+
+export const serverLogs = []
+
+export async function serve(): Promise<{ close(): Promise }> {
+ await kill(port)
+
+ const { createServer } = await import(path.resolve(rootDir, 'server.js'))
+ const { app, vite } = await createServer(
+ rootDir,
+ hmrPorts.ssr,
+ createInMemoryLogger(serverLogs),
+ )
+
+ return new Promise((resolve, reject) => {
+ try {
+ const server = app.listen(port, () => {
+ resolve({
+ // for test teardown
+ async close() {
+ await new Promise((resolve) => {
+ server.close(resolve)
+ })
+ if (vite) {
+ await vite.close()
+ }
+ },
+ })
+ })
+ } catch (e) {
+ reject(e)
+ }
+ })
+}
diff --git a/playground/ssr/__tests__/ssr.spec.ts b/playground/ssr/__tests__/ssr.spec.ts
new file mode 100644
index 00000000000000..0e4cb525ac9b4c
--- /dev/null
+++ b/playground/ssr/__tests__/ssr.spec.ts
@@ -0,0 +1,66 @@
+import { expect, test } from 'vitest'
+import { port, serverLogs } from './serve'
+import { browserLogs, editFile, isServe, page, withRetry } from '~utils'
+
+const url = `http://localhost:${port}`
+
+test(`circular dependencies modules doesn't throw`, async () => {
+ await page.goto(`${url}/circular-dep`)
+
+ expect(await page.textContent('.circ-dep-init')).toMatch(
+ 'circ-dep-init-a circ-dep-init-b',
+ )
+})
+
+test(`circular import doesn't throw (1)`, async () => {
+ await page.goto(`${url}/circular-import`)
+
+ expect(await page.textContent('.circ-import')).toMatchInlineSnapshot(
+ '"A is: __A__"',
+ )
+})
+
+test(`circular import doesn't throw (2)`, async () => {
+ await page.goto(`${url}/circular-import2`)
+
+ expect(await page.textContent('.circ-import')).toMatchInlineSnapshot(
+ '"A is: __A__"',
+ )
+})
+
+test(`deadlock doesn't happen for static imports`, async () => {
+ await page.goto(`${url}/forked-deadlock-static-imports`)
+
+ expect(await page.textContent('.forked-deadlock-static-imports')).toMatch(
+ 'rendered',
+ )
+})
+
+test(`deadlock doesn't happen for dynamic imports`, async () => {
+ await page.goto(`${url}/forked-deadlock-dynamic-imports`)
+
+ expect(await page.textContent('.forked-deadlock-dynamic-imports')).toMatch(
+ 'rendered',
+ )
+})
+
+test.runIf(isServe)('html proxy is encoded', async () => {
+ await page.goto(
+ `${url}?%22%3E%3C/script%3E%3Cscript%3Econsole.log(%27html%20proxy%20is%20not%20encoded%27)%3C/script%3E`,
+ )
+
+ expect(browserLogs).not.toContain('html proxy is not encoded')
+})
+
+// run this at the end to reduce flakiness
+test.runIf(isServe)('should restart ssr', async () => {
+ editFile('./vite.config.ts', (content) => content)
+ await withRetry(async () => {
+ expect(serverLogs).toEqual(
+ expect.arrayContaining([expect.stringMatching('server restarted')]),
+ )
+ expect(serverLogs).not.toEqual(
+ expect.arrayContaining([expect.stringMatching('error')]),
+ )
+ })
+})
diff --git a/playground/ssr/index.html b/playground/ssr/index.html
new file mode 100644
index 00000000000000..1b901ee15cf153
--- /dev/null
+++ b/playground/ssr/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ SSR
+
+
+
+ SSR
+
+
+
diff --git a/playground/ssr/package.json b/playground/ssr/package.json
new file mode 100644
index 00000000000000..986f07182fd563
--- /dev/null
+++ b/playground/ssr/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@vitejs/test-ssr",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "node server",
+ "serve": "NODE_ENV=production node server",
+ "debug": "node --inspect-brk server"
+ },
+ "dependencies": {},
+ "devDependencies": {
+ "express": "^5.1.0"
+ }
+}
diff --git a/playground/ssr/server.js b/playground/ssr/server.js
new file mode 100644
index 00000000000000..6e8763e02208a7
--- /dev/null
+++ b/playground/ssr/server.js
@@ -0,0 +1,75 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import express from 'express'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const isTest = process.env.VITEST
+
+export async function createServer(
+ root = process.cwd(),
+ hmrPort,
+ customLogger,
+) {
+ const resolve = (p) => path.resolve(__dirname, p)
+
+ const app = express()
+
+ /**
+ * @type {import('vite').ViteDevServer}
+ */
+ const vite = await (
+ await import('vite')
+ ).createServer({
+ root,
+ logLevel: isTest ? 'error' : 'info',
+ server: {
+ middlewareMode: true,
+ watch: {
+ // During tests we edit the files too fast and sometimes chokidar
+ // misses change events, so enforce polling for consistency
+ usePolling: true,
+ interval: 100,
+ },
+ hmr: {
+ port: hmrPort,
+ },
+ },
+ appType: 'custom',
+ customLogger,
+ })
+ // use vite's connect instance as middleware
+ app.use(vite.middlewares)
+
+ app.use('*all', async (req, res, next) => {
+ try {
+ const url = req.originalUrl
+
+ let template
+ template = fs.readFileSync(resolve('index.html'), 'utf-8')
+ template = await vite.transformIndexHtml(url, template)
+ const render = (await vite.ssrLoadModule('/src/app.js')).render
+
+ const appHtml = await render(url, __dirname)
+
+ const html = template.replace(``, appHtml)
+
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
+ } catch (e) {
+ vite && vite.ssrFixStacktrace(e)
+ if (isTest) throw e
+ console.log(e.stack)
+ res.status(500).end(e.stack)
+ }
+ })
+
+ return { app, vite }
+}
+
+if (!isTest) {
+ createServer().then(({ app }) =>
+ app.listen(5173, () => {
+ console.log('http://localhost:5173')
+ }),
+ )
+}
diff --git a/playground/ssr/src/app.js b/playground/ssr/src/app.js
new file mode 100644
index 00000000000000..5124b45b06075b
--- /dev/null
+++ b/playground/ssr/src/app.js
@@ -0,0 +1,62 @@
+import { escapeHtml } from './utils'
+
+const pathRenderers = {
+ '/': renderRoot,
+ '/circular-dep': renderCircularDep,
+ '/circular-import': renderCircularImport,
+ '/circular-import2': renderCircularImport2,
+ '/forked-deadlock-static-imports': renderForkedDeadlockStaticImports,
+ '/forked-deadlock-dynamic-imports': renderForkedDeadlockDynamicImports,
+}
+
+export async function render(url, rootDir) {
+ const pathname = url.replace(/#[^#]*$/, '').replace(/\?[^?]*$/, '')
+ const renderer = pathRenderers[pathname]
+ if (renderer) {
+ return await renderer(rootDir)
+ }
+ return '404'
+}
+
+async function renderRoot(rootDir) {
+ const paths = Object.keys(pathRenderers).filter((key) => key !== '/')
+ return `
+
+ ${paths
+ .map(
+ (path) =>
+ `