Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: use builtin css handling #59

Draft
wants to merge 46 commits into
base: rolldown-v6
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
0eddb11
feat: use rolldown in the dep optimizer
sapphi-red Sep 11, 2024
37ee073
feat: use rolldown for build
sapphi-red Sep 12, 2024
eaf89b8
chore: skip plugin-legacy build
sapphi-red Sep 12, 2024
bbbd83a
chore: skip typecheck for now
sapphi-red Sep 12, 2024
6bfed2d
chore: 🤖 add justfile for faster dev
IWANABETHATGUY Aug 15, 2024
c3576f6
feat: add experimental.enableNativePlugin (#41)
IWANABETHATGUY Sep 10, 2024
801bfab
feat: clone class plugins correctly
IWANABETHATGUY Sep 13, 2024
10bcd24
feat: native build plugins (#45) (#46)
IWANABETHATGUY Sep 12, 2024
095196a
perf: reduce pre alias plugin in build mode
IWANABETHATGUY Sep 12, 2024
35abe57
feat: 🎸 add an new option to disable build report
IWANABETHATGUY Sep 13, 2024
2dd31fa
feat: support native define
sapphi-red Sep 18, 2024
c4f89cf
feat: use filter for plugins (#49) (#50) (#51) (#52) (#53)
sapphi-red Sep 18, 2024
dbf006f
perf: ⚡️ use default resolver (#56)
IWANABETHATGUY Sep 14, 2024
1b9602c
fix: 🐛 lint (#58)
IWANABETHATGUY Sep 20, 2024
049058d
chore: bump rolldown
sapphi-red Sep 24, 2024
b68f46f
chore: skip data uri by load fallback plugin for native data uri hand…
sapphi-red Sep 24, 2024
477701b
feat: handle non-relative paths by Vite's resolver for enableNativePl…
sapphi-red Sep 19, 2024
68a94bc
chore: remove browser field edge case test
sapphi-red Sep 25, 2024
93fd15f
chore: use advancedChunks instead of manualChunks
sapphi-red Oct 1, 2024
404fccc
feat: convert `optimizeDeps.esbuildOptions` to `optimizeDeps.rollupOp…
sapphi-red Oct 4, 2024
72fb859
fix: optimizeDeps test pass on windows
sapphi-red Oct 9, 2024
b5034b2
chore: make some tests passing
sapphi-red Oct 9, 2024
0b9ce31
chore: make glob-import test pass with workarounds
sapphi-red Oct 9, 2024
b91ddef
chore: run worker-es tests
sapphi-red Oct 9, 2024
732d0eb
fix: use preliminaryFileName for chunkMetadata key
sapphi-red Oct 10, 2024
5f46885
feat: oxc tranformer (#60)
underfin Oct 11, 2024
d9e05ed
feat: use `onlyRemoveTypeImports`
sapphi-red Oct 11, 2024
4595c00
chore: enable continuous release under rolldown-vite (#61)
yyx990803 Oct 12, 2024
708eef3
feat: environment aware native plugins
sapphi-red Oct 15, 2024
bf628df
feat: export transformWithOxc (#62)
underfin Oct 17, 2024
19acd22
feat: add oxc jsxInclude and jsxExclude (#63)
underfin Oct 18, 2024
5705e75
chore: update some tests that relies on rollup's behavior
sapphi-red Oct 24, 2024
30b40e4
chore: skip environment-react-ssr for now
sapphi-red Oct 24, 2024
8356aaa
chore: bump rolldown
sapphi-red Oct 24, 2024
ae20803
chore: skip minifySyntax test as it relies on specific esbuild option
sapphi-red Oct 24, 2024
74b287e
chore: fix type errors
sapphi-red Oct 24, 2024
cd614c5
chore: skip sideeffects set for HTML scripts for now
sapphi-red Nov 6, 2024
574ccb4
chore: partial revert "refactor: use `originalFileNames`/`names` (#18…
sapphi-red Nov 6, 2024
f91e197
chore: tweak tests
sapphi-red Nov 6, 2024
c197275
chore: bump rolldown
sapphi-red Nov 6, 2024
9262331
chore: enable backend-integration test
sapphi-red Nov 6, 2024
c4be548
chore: skip error message test
sapphi-red Nov 13, 2024
487905e
chore: bump rolldown
sapphi-red Nov 13, 2024
b51ee7f
chore: bump rolldown
sapphi-red Nov 13, 2024
f39f6c0
feat: enable lib mode tests (#64)
sapphi-red Nov 13, 2024
2ff913e
wip: use builtin css handling
sapphi-red Sep 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -173,8 +173,8 @@ jobs:
- name: Check formatting
run: pnpm prettier --write --log-level=warn . && git diff --exit-code

- name: Typecheck
run: pnpm run typecheck
# - name: Typecheck
# run: pnpm run typecheck

- name: Test docs
run: pnpm run test-docs
6 changes: 3 additions & 3 deletions .github/workflows/preview-release.yml
Original file line number Diff line number Diff line change
@@ -10,14 +10,14 @@ permissions:
on:
push:
branches:
- main
- rolldown-v6
pull_request:
types: [opened, synchronize, labeled]

jobs:
preview:
if: >
github.repository == 'vitejs/vite' &&
github.repository == 'rolldown/vite' &&
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger: preview')))
runs-on: ubuntu-latest
@@ -35,4 +35,4 @@ jobs:
working-directory: ./packages/vite
run: pnpm build

- run: pnpm dlx [email protected] publish --compact --pnpm ./packages/vite
- run: pnpm dlx [email protected] publish --pnpm ./packages/vite
13 changes: 13 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
build-vite:
pnpm --filter vite run build-bundle

test-serve:
pnpm run test-serve

test-build:
pnpm run test-build

test: test-serve test-build

fmt:
pnpm --filter vite run format
1 change: 0 additions & 1 deletion packages/plugin-legacy/package.json
Original file line number Diff line number Diff line change
@@ -24,7 +24,6 @@
},
"scripts": {
"dev": "unbuild --stub",
"build": "unbuild && pnpm run patch-cjs",
"patch-cjs": "tsx ../../scripts/patchCJS.ts",
"prepublishOnly": "npm run build"
},
754 changes: 375 additions & 379 deletions packages/vite/LICENSE.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/vite/index.cjs
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ const asyncFunctions = [
'createServer',
'preview',
'transformWithEsbuild',
'transformWithOxc',
'resolveConfig',
'optimizeDeps',
'formatPostcssSourceMap',
3 changes: 2 additions & 1 deletion packages/vite/package.json
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
"type": "module",
"license": "MIT",
"author": "Evan You",
"description": "Native-ESM powered web dev build tool",
"description": "Vite on Rolldown preview",
"bin": {
"vite": "bin/vite.js"
},
@@ -88,6 +88,7 @@
"dependencies": {
"esbuild": "^0.24.0",
"postcss": "^8.4.48",
"rolldown": "https://pkg.pr.new/rolldown@3d47bd0",
"rollup": "^4.23.0"
},
"optionalDependencies": {
1 change: 1 addition & 0 deletions packages/vite/rollup.config.ts
Original file line number Diff line number Diff line change
@@ -112,6 +112,7 @@ const nodeConfig = defineConfig({
'fsevents',
'rollup/parseAst',
/^tsx\//,
'rolldown/experimental',
...Object.keys(pkg.dependencies),
...Object.keys(pkg.peerDependencies),
],
15 changes: 10 additions & 5 deletions packages/vite/rollup.dts.config.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ const external = [
/^node:*/,
/^vite\//,
'rollup/parseAst',
'rolldown/experimental',
...Object.keys(pkg.dependencies),
...Object.keys(pkg.peerDependencies),
...Object.keys(pkg.devDependencies),
@@ -46,11 +47,15 @@ const identifierWithTrailingDollarRE = /\b(\w+)\$\d+\b/g
* the module that imports the identifer as a named import alias
*/
const identifierReplacements: Record<string, Record<string, string>> = {
rollup: {
Plugin$1: 'rollup.Plugin',
PluginContext$1: 'rollup.PluginContext',
TransformPluginContext$1: 'rollup.TransformPluginContext',
TransformResult$2: 'rollup.TransformResult',
rolldown: {
Plugin$1: 'rolldown.Plugin',
PluginContext$1: 'rolldown.PluginContext',
TransformPluginContext$1: 'rolldown.TransformPluginContext',
TransformResult$3: 'rolldown.TransformResult',
},
'rolldown/experimental': {
TransformOptions$2: 'rolldown_experimental_TransformOptions',
TransformResult$2: 'rolldown_experimental_TransformResult',
},
esbuild: {
TransformResult$1: 'esbuild_TransformResult',
4 changes: 2 additions & 2 deletions packages/vite/src/node/__tests__/build.spec.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { basename, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import colors from 'picocolors'
import { describe, expect, test, vi } from 'vitest'
import type { OutputChunk, OutputOptions, RollupOutput } from 'rollup'
import type { OutputChunk, OutputOptions, RollupOutput } from 'rolldown'
import type { LibraryFormats, LibraryOptions } from '../build'
import {
build,
@@ -707,7 +707,7 @@ test('default sharedConfigBuild true on build api', async () => {
expect(counter).toBe(1)
})

test('adjust worker build error for worker.format', async () => {
test.skip('adjust worker build error for worker.format', async () => {
try {
await build({
root: resolve(__dirname, 'fixtures/worker-dynamic'),
Original file line number Diff line number Diff line change
@@ -10,8 +10,8 @@ async function createAssetImportMetaurlPluginTransform() {
const environment = new PartialEnvironment('client', config)

return async (code: string) => {
// @ts-expect-error transform should exist
const result = await instance.transform.call(
// @ts-expect-error transform.handler should exist
const result = await instance.transform.handler.call(
{ environment, parse: parseAst },
code,
'foo.ts',
7 changes: 4 additions & 3 deletions packages/vite/src/node/__tests__/plugins/css.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, test } from 'vitest'
import type { Plugin } from 'rolldown'
import { resolveConfig } from '../../config'
import type { InlineConfig } from '../../config'
import {
@@ -210,15 +211,15 @@ async function createCssPluginTransform(inlineConfig: InlineConfig = {}) {
const config = await resolveConfig(inlineConfig, 'serve')
const environment = new PartialEnvironment('client', config)

const { transform, buildStart } = cssPlugin(config)
const { transform, buildStart } = cssPlugin(config) as Plugin

// @ts-expect-error buildStart is function
await buildStart.call({})

return {
async transform(code: string, id: string) {
// @ts-expect-error transform is function
return await transform.call(
// @ts-expect-error transform.handler is function
return await transform.handler.call(
{
addWatchFile() {
return
10 changes: 7 additions & 3 deletions packages/vite/src/node/__tests__/plugins/import.spec.ts
Original file line number Diff line number Diff line change
@@ -73,9 +73,13 @@ describe('transformCjsImport', () => {
'',
config,
),
).toBe(
'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)`,
).toMatchInlineSnapshot(
`
"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)"
`,
)
})

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it } from 'vitest'
import type { ModuleFormat, RollupOutput } from 'rollup'
import type { ModuleFormat, RollupOutput } from 'rolldown'
import { build } from '../../../build'
import { modulePreloadPolyfillId } from '../../../plugins/modulePreloadPolyfill'

@@ -37,7 +37,7 @@ const buildProject = ({ format = 'es' as ModuleFormat } = {}) =>
}) as Promise<RollupOutput>

describe('load', () => {
it('loads modulepreload polyfill', async ({ expect }) => {
it.skip('loads modulepreload polyfill', async ({ expect }) => {
const { output } = await buildProject()
expect(output).toHaveLength(1)
expect(output[0].code).toMatchSnapshot()
2 changes: 1 addition & 1 deletion packages/vite/src/node/__tests_dts__/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* This is a development only file for testing types.
*/
import type { Plugin as RollupPlugin } from 'rollup'
import type { Plugin as RollupPlugin } from 'rolldown'
import type { Equal, ExpectExtends, ExpectTrue } from '@type-challenges/utils'
import type { Plugin, PluginContextExtension } from '../plugin'
import type { ROLLUP_HOOKS } from '../constants'
295 changes: 170 additions & 125 deletions packages/vite/src/node/build.ts

Large diffs are not rendered by default.

162 changes: 155 additions & 7 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import { builtinModules, createRequire } from 'node:module'
import colors from 'picocolors'
import type { Alias, AliasOptions } from 'dep-types/alias'
import { build } from 'esbuild'
import type { RollupOptions } from 'rollup'
import type { RollupOptions } from 'rolldown'
import picomatch from 'picomatch'
import type { AnymatchFn } from '../types/anymatch'
import { withTrailingSlash } from '../shared/utils'
@@ -98,6 +98,7 @@ import type { ResolvedSSROptions, SSROptions } from './ssr'
import { resolveSSROptions, ssrConfigDefaults } from './ssr'
import { PartialEnvironment } from './baseEnvironment'
import { createIdResolver } from './idResolver'
import { type OxcOptions, convertEsbuildConfigToOxcConfig } from './plugins/oxc'

const debug = createDebugger('vite:config', { depth: 10 })
const promisifiedRealpath = promisify(fs.realpath)
@@ -349,6 +350,11 @@ export interface UserConfig extends DefaultEnvironmentOptions {
* Or set to `false` to disable esbuild.
*/
esbuild?: ESBuildOptions | false
/**
* Transform options to pass to esbuild.
* Or set to `false` to disable esbuild.
*/
oxc?: OxcOptions | false
/**
* Specify additional picomatch patterns to be treated as static assets.
*/
@@ -504,6 +510,14 @@ export interface ExperimentalOptions {
* @default false
*/
skipSsrTransform?: boolean

/**
* Enable builtin plugin that written by rust, which is faster than js plugin.
*
* @experimental
* @default true
*/
enableNativePlugin?: boolean
}

export interface LegacyOptions {
@@ -576,7 +590,8 @@ export type ResolvedConfig = Readonly<
plugins: readonly Plugin[]
css: ResolvedCSSOptions
json: Required<JsonOptions>
esbuild: ESBuildOptions | false
// esbuild: ESBuildOptions | false
oxc: OxcOptions | false
server: ResolvedServerOptions
dev: ResolvedDevEnvironmentOptions
/** @experimental */
@@ -690,6 +705,7 @@ export const configDefaults = Object.freeze({
exclude: [],
needsInterop: [],
// esbuildOptions
rollupOptions: {},
/** @experimental */
extensions: [],
/** @deprecated @experimental */
@@ -773,6 +789,7 @@ function resolveEnvironmentOptions(
options.optimizeDeps,
resolve.preserveSymlinks,
consumer,
logger,
),
dev: resolveDevEnvironmentOptions(
options.dev,
@@ -944,7 +961,118 @@ function resolveDepOptimizationOptions(
optimizeDeps: DepOptimizationOptions | undefined,
preserveSymlinks: boolean,
consumer: 'client' | 'server' | undefined,
logger: Logger,
): DepOptimizationOptions {
if (optimizeDeps?.esbuildOptions) {
logger.warn(
colors.yellow(
`You have set \`optimizeDeps.esbuildOptions\` but this options is now deprecated. ` +
`Vite now uses Rolldown to optimize the dependencies. ` +
`Please use \`optimizeDeps.rollupOptions\` instead.`,
),
)

optimizeDeps.rollupOptions ??= {}
optimizeDeps.rollupOptions.resolve ??= {}
optimizeDeps.rollupOptions.output ??= {}

const setResolveOptions = <
T extends keyof Exclude<RollupOptions['resolve'], undefined>,
>(
key: T,
value: Exclude<RollupOptions['resolve'], undefined>[T],
) => {
if (
value !== undefined &&
optimizeDeps.rollupOptions!.resolve![key] === undefined
) {
optimizeDeps.rollupOptions!.resolve![key] = value
}
}

if (
optimizeDeps.esbuildOptions.minify !== undefined &&
optimizeDeps.rollupOptions.output.minify === undefined
) {
optimizeDeps.rollupOptions.output.minify =
optimizeDeps.esbuildOptions.minify
}
if (
optimizeDeps.esbuildOptions.treeShaking !== undefined &&
optimizeDeps.rollupOptions.treeshake === undefined
) {
optimizeDeps.rollupOptions.treeshake =
optimizeDeps.esbuildOptions.treeShaking
}
if (
optimizeDeps.esbuildOptions.define !== undefined &&
optimizeDeps.rollupOptions.define === undefined
) {
optimizeDeps.rollupOptions.define = optimizeDeps.esbuildOptions.define
}
if (optimizeDeps.esbuildOptions.loader !== undefined) {
const loader = optimizeDeps.esbuildOptions.loader
optimizeDeps.rollupOptions.moduleTypes ??= {}
for (const [key, value] of Object.entries(loader)) {
if (
optimizeDeps.rollupOptions.moduleTypes[key] === undefined &&
value !== 'copy' &&
value !== 'css' &&
value !== 'default' &&
value !== 'file' &&
value !== 'local-css'
) {
optimizeDeps.rollupOptions.moduleTypes[key] = value
}
}
}
setResolveOptions('symlinks', optimizeDeps.esbuildOptions.preserveSymlinks)
setResolveOptions(
'extensions',
optimizeDeps.esbuildOptions.resolveExtensions,
)
setResolveOptions('mainFields', optimizeDeps.esbuildOptions.mainFields)
setResolveOptions('conditionNames', optimizeDeps.esbuildOptions.conditions)

// NOTE: the following options cannot be converted
// - legalComments
// - target, supported (Vite used to transpile down to `ESBUILD_MODULES_TARGET`)
// - ignoreAnnotations
// - jsx, jsxFactory, jsxFragment, jsxImportSource, jsxDev, jsxSideEffects
// - tsconfigRaw, tsconfig

// NOTE: the following options can be converted but probably not worth it
// - sourceRoot
// - sourcesContent (`output.sourcemapExcludeSources` is not supported by rolldown)
// - drop
// - dropLabels
// - mangleProps, reserveProps, mangleQuoted, mangleCache
// - minifyWhitespace, minifyIdentifiers, minifySyntax
// - lineLimit
// - charset
// - pure (`treeshake.manualPureFunctions` is not supported by rolldown)
// - alias (it probably does not work the same with `resolve.alias`)
// - inject
// - banner, footer
// - plugins (not sure if it's possible and need to check if it's worth it before)
// - nodePaths

// NOTE: the following options does not make sense to set / convert it
// - globalName (we only use ESM format)
// - keepNames (probably rolldown does not need it? not sure)
// - color
// - logLimit
// - logOverride
// - splitting
// - outbase
// - packages (this should not be set)
// - allowOverwrite
// - publicPath (`file` loader is not supported by rolldown)
// - entryNames, chunkNames, assetNames (Vite does not support changing these options)
// - stdin
// - absWorkingDir
}

return mergeWithDefaults(
{
...configDefaults.optimizeDeps,
@@ -1351,6 +1479,18 @@ export async function resolveConfig(

const base = withTrailingSlash(resolvedBase)

let oxc: OxcOptions | false | undefined = config.oxc

if (config.esbuild) {
if (config.oxc) {
logger.warn(
`Found esbuild and oxc options, will use oxc and ignore esbuild at transformer.`,
)
} else {
oxc = convertEsbuildConfigToOxcConfig(config.esbuild, logger)
}
}

resolved = {
configFile: configFile ? normalizePath(configFile) : undefined,
configFileDependencies: configFileDependencies.map((name) =>
@@ -1372,12 +1512,17 @@ export async function resolveConfig(
plugins: userPlugins, // placeholder to be replaced
css: resolveCSSOptions(config.css),
json: mergeWithDefaults(configDefaults.json, config.json ?? {}),
esbuild:
config.esbuild === false
// preserve esbuild for buildEsbuildPlugin
esbuild: config.esbuild ?? {},
oxc:
oxc === false
? false
: {
jsxDev: !isProduction,
...config.esbuild,
...oxc,
jsx: {
development: !isProduction,
...oxc?.jsx,
},
},
server,
builder,
@@ -1400,6 +1545,7 @@ export async function resolveConfig(
experimental: {
importGlobRestoreExtension: false,
hmrPartialAccept: false,
enableNativePlugin: false,
...config.experimental,
},
future: config.future,
@@ -1514,7 +1660,9 @@ export async function resolveConfig(

// Check if all assetFileNames have the same reference.
// If not, display a warn for user.
const outputOption = config.build?.rollupOptions?.output ?? []

// Note: the rolldown `output` option is object.
const outputOption = config.build?.rollupOptions?.output ?? {}
// Use isArray to narrow its type to array
if (Array.isArray(outputOption)) {
const assetFileNamesList = outputOption.map(
10 changes: 5 additions & 5 deletions packages/vite/src/node/constants.ts
Original file line number Diff line number Diff line change
@@ -19,10 +19,10 @@ export const ROLLUP_HOOKS = [
'banner',
'footer',
'augmentChunkHash',
'outputOptions',
'renderDynamicImport',
'resolveFileUrl',
'resolveImportMeta',
// 'outputOptions',
// 'renderDynamicImport',
// 'resolveFileUrl',
// 'resolveImportMeta',
'intro',
'outro',
'closeBundle',
@@ -32,7 +32,7 @@ export const ROLLUP_HOOKS = [
'watchChange',
'resolveDynamicImport',
'resolveId',
'shouldTransformCachedModule',
// 'shouldTransformCachedModule',
'transform',
'onLog',
] satisfies RollupPluginHooks[]
4 changes: 3 additions & 1 deletion packages/vite/src/node/idResolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PartialResolvedId } from 'rollup'
import type { PartialResolvedId } from 'rolldown'
import aliasPlugin from '@rollup/plugin-alias'
import type { ResolvedConfig } from './config'
import type { EnvironmentPluginContainer } from './server/pluginContainer'
@@ -59,6 +59,7 @@ export function createIdResolver(
pluginContainer = await createEnvironmentPluginContainer(
environment as Environment,
[
// @ts-expect-error the aliasPlugin uses rollup types
aliasPlugin({ entries: environment.config.resolve.alias }),
resolvePlugin({
root: config.root,
@@ -91,6 +92,7 @@ export function createIdResolver(
if (!pluginContainer) {
pluginContainer = await createEnvironmentPluginContainer(
environment as Environment,
// @ts-expect-error the aliasPlugin uses rollup types
[aliasPlugin({ entries: environment.config.resolve.alias })],
)
aliasOnlyPluginContainerMap.set(environment, pluginContainer)
3 changes: 2 additions & 1 deletion packages/vite/src/node/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type * as Rollup from 'rollup'
import type * as Rollup from 'rolldown'

export type { Rollup }
export { parseAst, parseAstAsync } from 'rollup/parseAst'
@@ -19,6 +19,7 @@ export { createIdResolver } from './idResolver'

export { formatPostcssSourceMap, preprocessCSS } from './plugins/css'
export { transformWithEsbuild } from './plugins/esbuild'
export { transformWithOxc } from './plugins/oxc'
export { buildErrorMessage } from './server/middlewares/error'

export {
2 changes: 1 addition & 1 deletion packages/vite/src/node/logger.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

import readline from 'node:readline'
import colors from 'picocolors'
import type { RollupError } from 'rollup'
import type { RollupError } from 'rolldown'
import type { ResolvedServerUrls } from './server'

export type LogType = 'error' | 'warn' | 'info'
347 changes: 0 additions & 347 deletions packages/vite/src/node/optimizer/esbuildDepPlugin.ts

This file was deleted.

291 changes: 150 additions & 141 deletions packages/vite/src/node/optimizer/index.ts

Large diffs are not rendered by default.

338 changes: 338 additions & 0 deletions packages/vite/src/node/optimizer/rolldownDepPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
import path from 'node:path'
import type { ImportKind, Plugin } from 'rolldown'
import { JS_TYPES_RE, KNOWN_ASSET_TYPES } from '../constants'
import type { PackageCache } from '../packages'
import {
escapeRegex,
flattenId,
isBuiltin,
isExternalUrl,
moduleListContains,
normalizePath,
} from '../utils'
import { browserExternalId, optionalPeerDepId } from '../plugins/resolve'
import { isCSSRequest, isModuleCSSRequest } from '../plugins/css'
import type { Environment } from '../environment'
import { createBackCompatIdResolver } from '../idResolver'
import { isWindows } from '../../shared/utils'

const externalWithConversionNamespace =
'vite:dep-pre-bundle:external-conversion'
const convertedExternalPrefix = 'vite-dep-pre-bundle-external:'

const cjsExternalFacadeNamespace = 'vite:cjs-external-facade'
const nonFacadePrefix = 'vite-cjs-external-facade:'

const externalTypes = [
'css',
// supported pre-processor types
'less',
'sass',
'scss',
'styl',
'stylus',
'pcss',
'postcss',
// wasm
'wasm',
// known SFC types
'vue',
'svelte',
'marko',
'astro',
'imba',
// JSX/TSX may be configured to be compiled differently from how esbuild
// handles it by default, so exclude them as well
'jsx',
'tsx',
...KNOWN_ASSET_TYPES,
]

const optionalPeerDepNamespace = 'optional-peer-dep:'
const browserExternalNamespace = 'browser-external:'

export function rolldownDepPlugin(
environment: Environment,
qualified: Record<string, string>,
external: string[],
): Plugin {
const { isProduction } = environment.config
const { extensions } = environment.config.optimizeDeps

// remove optimizable extensions from `externalTypes` list
const allExternalTypes = extensions
? externalTypes.filter((type) => !extensions?.includes('.' + type))
: externalTypes

// use separate package cache for optimizer as it caches paths around node_modules
// and it's unlikely for the core Vite process to traverse into node_modules again
const esmPackageCache: PackageCache = new Map()
const cjsPackageCache: PackageCache = new Map()

// default resolver which prefers ESM
const _resolve = createBackCompatIdResolver(environment.getTopLevelConfig(), {
asSrc: false,
scan: true,
packageCache: esmPackageCache,
})

// cjs resolver that prefers Node
const _resolveRequire = createBackCompatIdResolver(
environment.getTopLevelConfig(),
{
asSrc: false,
isRequire: true,
scan: true,
packageCache: cjsPackageCache,
},
)

const resolve = (
id: string,
importer: string | undefined,
kind: ImportKind,
resolveDir?: string,
): Promise<string | undefined> => {
let _importer: string | undefined
// explicit resolveDir - this is passed only during yarn pnp resolve for
// entries
if (resolveDir) {
_importer = normalizePath(path.join(resolveDir, '*'))
} else if (importer) {
// map importer ids to file paths for correct resolution
_importer = importer in qualified ? qualified[importer] : importer
}
const resolver = kind.startsWith('require') ? _resolveRequire : _resolve
return resolver(environment, id, _importer)
}

const resolveResult = (id: string, resolved: string) => {
if (resolved.startsWith(browserExternalId)) {
return {
id: browserExternalNamespace + id,
}
}
if (resolved.startsWith(optionalPeerDepId)) {
return {
id: optionalPeerDepNamespace + resolved,
}
}
if (environment.config.consumer === 'server' && isBuiltin(resolved)) {
return
}
if (isExternalUrl(resolved)) {
return {
id: resolved,
external: true,
}
}
return {
id: path.resolve(resolved),
}
}

const allExternalTypesReg = new RegExp(
`\\.(` + allExternalTypes.join('|') + `)(\\?.*)?$`,
)

function resolveEntry(id: string) {
const flatId = flattenId(id)
if (flatId in qualified) {
return {
id: qualified[flatId],
}
}
}

return {
name: 'vite:dep-pre-bundle',
// clear package cache when build is finished
buildEnd() {
esmPackageCache.clear()
cjsPackageCache.clear()
},
resolveId: async function (id, importer, options) {
const kind = options.kind
// externalize assets and commonly known non-js file types
// See #8459 for more details about this require-import conversion
if (allExternalTypesReg.test(id)) {
// if the prefix exist, it is already converted to `import`, so set `external: true`
if (id.startsWith(convertedExternalPrefix)) {
return {
id: id.slice(convertedExternalPrefix.length),
external: true,
}
}

const resolved = await resolve(id, importer, kind)
if (resolved) {
// `resolved` can be javascript even when `id` matches `allExternalTypes`
// due to cjs resolution (e.g. require("./test.pdf") for "./test.pdf.js")
// or package name (e.g. import "some-package.pdf")
if (JS_TYPES_RE.test(resolved)) {
return {
// normalize to \\ on windows for esbuild/rolldown behavior difference: https://github.com/sapphi-red-repros/rolldown-esbuild-path-normalization
id: isWindows ? resolved.replaceAll('/', '\\') : resolved,
external: false,
}
}

if (kind === 'require-call') {
// here it is not set to `external: true` to convert `require` to `import`
return {
id: externalWithConversionNamespace + resolved,
}
}
return {
id: resolved,
external: true,
}
}
}

if (/^[\w@][^:]/.test(id)) {
if (moduleListContains(external, id)) {
return {
id: id,
external: true,
}
}

// ensure esbuild uses our resolved entries
let entry: { id: string } | undefined
// if this is an entry, return entry namespace resolve result
if (!importer) {
if ((entry = resolveEntry(id))) return entry
// check if this is aliased to an entry - also return entry namespace
const aliased = await _resolve(environment, id, undefined, true)
if (aliased && (entry = resolveEntry(aliased))) {
return entry
}
}

// use vite's own resolver
const resolved = await resolve(id, importer, kind)
if (resolved) {
return resolveResult(id, resolved)
}
}
},
load(id) {
if (id.startsWith(externalWithConversionNamespace)) {
const path = id.slice(externalWithConversionNamespace.length)
// import itself with prefix (this is the actual part of require-import conversion)
const modulePath = `"${convertedExternalPrefix}${path}"`
return {
code:
isCSSRequest(path) && !isModuleCSSRequest(path)
? `import ${modulePath};`
: `export { default } from ${modulePath};` +
`export * from ${modulePath};`,
}
}

if (id.startsWith(browserExternalNamespace)) {
const path = id.slice(browserExternalNamespace.length)
if (isProduction) {
return {
code: 'module.exports = {}',
}
} else {
return {
// Return in CJS to intercept named imports. Use `Object.create` to
// create the Proxy in the prototype to workaround esbuild issue. Why?
//
// In short, esbuild cjs->esm flow:
// 1. Create empty object using `Object.create(Object.getPrototypeOf(module.exports))`.
// 2. Assign props of `module.exports` to the object.
// 3. Return object for ESM use.
//
// If we do `module.exports = new Proxy({}, {})`, step 1 returns empty object,
// step 2 does nothing as there's no props for `module.exports`. The final object
// is just an empty object.
//
// Creating the Proxy in the prototype satisfies step 1 immediately, which means
// the returned object is a Proxy that we can intercept.
//
// Note: Skip keys that are accessed by esbuild and browser devtools.
code: `\
module.exports = Object.create(new Proxy({}, {
get(_, key) {
if (
key !== '__esModule' &&
key !== '__proto__' &&
key !== 'constructor' &&
key !== 'splice'
) {
console.warn(\`Module "${path}" has been externalized for browser compatibility. Cannot access "${path}.\${key}" in client code. See http://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\`)
}
}
}))`,
}
}
}

if (id.startsWith(optionalPeerDepNamespace)) {
if (isProduction) {
return {
code: 'module.exports = {}',
}
} else {
const path = id.slice(externalWithConversionNamespace.length)
const [, peerDep, parentDep] = path.split(':')
return {
code: `throw new Error(\`Could not resolve "${peerDep}" imported by "${parentDep}". Is it installed?\`)`,
}
}
}
},
}
}

const matchesEntireLine = (text: string) => `^${escapeRegex(text)}$`

// esbuild doesn't transpile `require('foo')` into `import` statements if 'foo' is externalized
// https://github.com/evanw/esbuild/issues/566#issuecomment-735551834
export function rolldownCjsExternalPlugin(
externals: string[],
platform: 'node' | 'browser' | 'neutral',
): Plugin {
const filter = new RegExp(externals.map(matchesEntireLine).join('|'))

return {
name: 'cjs-external',
resolveId(id, _importer, options) {
if (id.startsWith(nonFacadePrefix)) {
return {
id: id.slice(nonFacadePrefix.length),
external: true,
}
}

if (filter.test(id)) {
const kind = options.kind
// preserve `require` for node because it's more accurate than converting it to import
if (kind === 'require-call' && platform !== 'node') {
return {
id: cjsExternalFacadeNamespace + id,
}
}

return {
id,
external: true,
}
}
},
load(id) {
if (id.startsWith(cjsExternalFacadeNamespace)) {
return {
code:
`import * as m from ${JSON.stringify(
nonFacadePrefix + id.slice(cjsExternalFacadeNamespace.length),
)};` + `module.exports = m;`,
}
}
},
}
}
643 changes: 279 additions & 364 deletions packages/vite/src/node/optimizer/scan.ts

Large diffs are not rendered by default.

20 changes: 16 additions & 4 deletions packages/vite/src/node/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type {
CustomPluginOptions,
LoadResult,
ModuleType,
ObjectHook,
ResolveIdResult,
MinimalPluginContext as RollupMinimalPluginContext,
Plugin as RollupPlugin,
PluginContext as RollupPluginContext,
TransformPluginContext as RollupTransformPluginContext,
SourceMap,
TransformResult,
} from 'rollup'
} from 'rolldown'
import type {
ConfigEnv,
EnvironmentOptions,
@@ -60,6 +62,11 @@ export interface PluginContextExtension {
environment: Environment
}

export interface TransformPluginContextExtension {
// TODO: rolldown does not support this yet: https://github.com/rolldown/rolldown/pull/1121, https://github.com/rolldown/rolldown/pull/1426
getCombinedSourcemap: () => SourceMap
}

export interface HotUpdatePluginContext {
environment: DevEnvironment
}
@@ -78,11 +85,14 @@ export interface ResolveIdPluginContext

export interface TransformPluginContext
extends RollupTransformPluginContext,
PluginContextExtension {}
PluginContextExtension,
TransformPluginContextExtension {}

// Argument Rollup types to have the PluginContextExtension
declare module 'rollup' {
declare module 'rolldown' {
export interface MinimalPluginContext extends PluginContextExtension {}
export interface TransformPluginContext
extends TransformPluginContextExtension {}
}

/**
@@ -130,14 +140,15 @@ export interface Plugin<A = any> extends RollupPlugin<A> {
source: string,
importer: string | undefined,
options: {
attributes: Record<string, string>
// attributes: Record<string, string>
custom?: CustomPluginOptions
ssr?: boolean
/**
* @internal
*/
scan?: boolean
isEntry: boolean
kind?: 'import' | 'dynamic-import' | 'require-call'
},
) => Promise<ResolveIdResult> | ResolveIdResult
>
@@ -160,6 +171,7 @@ export interface Plugin<A = any> extends RollupPlugin<A> {
code: string,
id: string,
options?: {
moduleType: ModuleType
ssr?: boolean
},
) => Promise<TransformResult> | TransformResult
41 changes: 32 additions & 9 deletions packages/vite/src/node/plugins/asset.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import path from 'node:path'
import fsp from 'node:fs/promises'
import { Buffer } from 'node:buffer'
import * as mrmime from 'mrmime'
import type { NormalizedOutputOptions, RenderedChunk } from 'rollup'
import type { NormalizedOutputOptions, RenderedChunk } from 'rolldown'
import MagicString from 'magic-string'
import colors from 'picocolors'
import {
@@ -30,6 +30,7 @@ import {
withTrailingSlash,
} from '../../shared/utils'
import type { Environment } from '../environment'
import { getChunkMetadata } from './metadata'

// referenceId is base64url but replaces - with $
export const assetUrlRE = /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g
@@ -42,8 +43,17 @@ const svgExtRE = /\.svg(?:$|\?)/

const assetCache = new WeakMap<Environment, Map<string, string>>()

/** a set of referenceId for entry CSS assets for each environment */
export const cssEntriesMap = new WeakMap<Environment, Set<string>>()
// chunk.name is the basename for the asset ignoring the directory structure
// For the manifest, we need to preserve the original file path and isEntry
// for CSS assets. We keep a map from referenceId to this information.
export interface GeneratedAssetMeta {
originalFileName: string | undefined
isEntry?: boolean
}
export const generatedAssetsMap = new WeakMap<
Environment,
Map<string, GeneratedAssetMeta>
>()

// add own dictionary entry by directly assigning mrmime
export function registerCustomMime(): void {
@@ -83,7 +93,7 @@ export function renderAssetUrlInJS(
s ||= new MagicString(code)
const [full, referenceId, postfix = ''] = match
const file = pluginContext.getFileName(referenceId)
chunk.viteMetadata!.importedAssets.add(cleanUrl(file))
getChunkMetadata(chunk)!.importedAssets.add(cleanUrl(file))
const filename = file + postfix
const replacement = toOutputFilePathInJS(
environment,
@@ -134,14 +144,16 @@ export function renderAssetUrlInJS(
export function assetPlugin(config: ResolvedConfig): Plugin {
registerCustomMime()

const assetModuleId = new Set<string>()

return {
name: 'vite:asset',

perEnvironmentStartEndDuringDev: true,

buildStart() {
assetCache.set(this.environment, new Map())
cssEntriesMap.set(this.environment, new Set())
generatedAssetsMap.set(this.environment, new Map())
},

resolveId(id) {
@@ -168,9 +180,12 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
const file = checkPublicFile(id, config) || cleanUrl(id)
this.addWatchFile(file)
// raw query, read file and return as string
return `export default ${JSON.stringify(
await fsp.readFile(file, 'utf-8'),
)}`
return {
code: `export default ${JSON.stringify(
await fsp.readFile(file, 'utf-8'),
)}`,
moduleType: 'js', // TODO: remove later when not needed
}
}

if (!urlRE.test(id) && !config.assetsInclude(cleanUrl(id))) {
@@ -188,6 +203,10 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
}
}

// Note: rolldown does not support meta, use a Set instead of it for now
if (config.command === 'build') {
assetModuleId.add(id)
}
return {
code: `export default ${JSON.stringify(encodeURIPath(url))}`,
// Force rollup to keep this module from being shared between other entry points if it's an entrypoint.
@@ -197,6 +216,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
? 'no-treeshake'
: false,
meta: config.command === 'build' ? { 'vite:asset': true } : undefined,
moduleType: 'js', // TODO: remove later when not needed
}
},

@@ -224,7 +244,8 @@ export function assetPlugin(config: ResolvedConfig): Plugin {
chunk.isEntry &&
chunk.moduleIds.length === 1 &&
config.assetsInclude(chunk.moduleIds[0]) &&
this.getModuleInfo(chunk.moduleIds[0])?.meta['vite:asset']
assetModuleId.has(chunk.moduleIds[0])
// this.getModuleInfo(chunk.moduleIds[0])?.meta['vite:asset']
) {
delete bundle[file]
}
@@ -397,6 +418,8 @@ async function fileToBuiltUrl(
originalFileName,
source: content,
})
generatedAssetsMap.get(environment)!.set(referenceId, { originalFileName })

url = `__VITE_ASSET__${referenceId}__${postfix ? `$_${postfix}__` : ``}`
}

220 changes: 115 additions & 105 deletions packages/vite/src/node/plugins/assetImportMetaUrl.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import path from 'node:path'
import MagicString from 'magic-string'
import { stripLiteral } from 'strip-literal'
import type { Plugin } from '../plugin'
import { parseAst } from 'rollup/parseAst'
import type { RolldownPlugin } from 'rolldown'
import type { ResolvedConfig } from '../config'
import {
injectQuery,
@@ -29,7 +30,9 @@ import { hasViteIgnoreRE } from './importAnalysis'
* import.meta.glob('./dir/**.png', { eager: true, import: 'default' })[`./dir/${name}.png`]
* ```
*/
export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {
export function assetImportMetaUrlPlugin(
config: ResolvedConfig,
): RolldownPlugin {
const { publicDir } = config
let assetResolver: ResolveIdFn

@@ -44,124 +47,131 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin {

return {
name: 'vite:asset-import-meta-url',
async transform(code, id) {
const { environment } = this
if (
environment.config.consumer === 'client' &&
id !== preloadHelperId &&
id !== CLIENT_ENTRY &&
code.includes('new URL') &&
code.includes(`import.meta.url`)
) {
let s: MagicString | undefined
const assetImportMetaUrlRE =
/\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)/dg
const cleanString = stripLiteral(code)
transform: {
filter: {
code: {
include: ['new URL', 'import.meta.url'],
},
},
async handler(code, id) {
const { environment } = this
if (
environment.config.consumer === 'client' &&
id !== preloadHelperId &&
id !== CLIENT_ENTRY &&
code.includes('new URL') &&
code.includes(`import.meta.url`)
) {
let s: MagicString | undefined
const assetImportMetaUrlRE =
/\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)/dg
const cleanString = stripLiteral(code)

let match: RegExpExecArray | null
while ((match = assetImportMetaUrlRE.exec(cleanString))) {
const [[startIndex, endIndex], [urlStart, urlEnd]] = match.indices!
if (hasViteIgnoreRE.test(code.slice(startIndex, urlStart))) continue
let match: RegExpExecArray | null
while ((match = assetImportMetaUrlRE.exec(cleanString))) {
const [[startIndex, endIndex], [urlStart, urlEnd]] = match.indices!
if (hasViteIgnoreRE.test(code.slice(startIndex, urlStart))) continue

const rawUrl = code.slice(urlStart, urlEnd)
const rawUrl = code.slice(urlStart, urlEnd)

if (!s) s = new MagicString(code)
if (!s) s = new MagicString(code)

// potential dynamic template string
if (rawUrl[0] === '`' && rawUrl.includes('${')) {
const queryDelimiterIndex = getQueryDelimiterIndex(rawUrl)
const hasQueryDelimiter = queryDelimiterIndex !== -1
const pureUrl = hasQueryDelimiter
? rawUrl.slice(0, queryDelimiterIndex) + '`'
: rawUrl
const queryString = hasQueryDelimiter
? rawUrl.slice(queryDelimiterIndex, -1)
: ''
const ast = this.parse(pureUrl)
const templateLiteral = (ast as any).body[0].expression
if (templateLiteral.expressions.length) {
const pattern = buildGlobPattern(templateLiteral)
if (pattern.startsWith('*')) {
// don't transform for patterns like this
// because users won't intend to do that in most cases
// potential dynamic template string
if (rawUrl[0] === '`' && rawUrl.includes('${')) {
const queryDelimiterIndex = getQueryDelimiterIndex(rawUrl)
const hasQueryDelimiter = queryDelimiterIndex !== -1
const pureUrl = hasQueryDelimiter
? rawUrl.slice(0, queryDelimiterIndex) + '`'
: rawUrl
const queryString = hasQueryDelimiter
? rawUrl.slice(queryDelimiterIndex, -1)
: ''
const ast = parseAst(pureUrl)
const templateLiteral = (ast as any).body[0].expression
if (templateLiteral.expressions.length) {
const pattern = buildGlobPattern(templateLiteral)
if (pattern.startsWith('*')) {
// don't transform for patterns like this
// because users won't intend to do that in most cases
continue
}

const globOptions = {
eager: true,
import: 'default',
// A hack to allow 'as' & 'query' exist at the same time
query: injectQuery(queryString, 'url'),
}
s.update(
startIndex,
endIndex,
`new URL((import.meta.glob(${JSON.stringify(
pattern,
)}, ${JSON.stringify(
globOptions,
)}))[${pureUrl}], import.meta.url)`,
)
continue
}
}

const globOptions = {
eager: true,
import: 'default',
// A hack to allow 'as' & 'query' exist at the same time
query: injectQuery(queryString, 'url'),
}
s.update(
startIndex,
endIndex,
`new URL((import.meta.glob(${JSON.stringify(
pattern,
)}, ${JSON.stringify(
globOptions,
)}))[${pureUrl}], import.meta.url)`,
)
const url = rawUrl.slice(1, -1)
if (isDataUrl(url)) {
continue
}
}

const url = rawUrl.slice(1, -1)
if (isDataUrl(url)) {
continue
}
let file: string | undefined
if (url[0] === '.') {
file = slash(path.resolve(path.dirname(id), url))
file = tryFsResolve(file, fsResolveOptions) ?? file
} else {
assetResolver ??= createBackCompatIdResolver(config, {
extensions: [],
mainFields: [],
tryIndex: false,
preferRelative: true,
})
file = await assetResolver(environment, url, id)
file ??=
url[0] === '/'
? slash(path.join(publicDir, url))
: slash(path.resolve(path.dirname(id), url))
}
let file: string | undefined
if (url[0] === '.') {
file = slash(path.resolve(path.dirname(id), url))
file = tryFsResolve(file, fsResolveOptions) ?? file
} else {
assetResolver ??= createBackCompatIdResolver(config, {
extensions: [],
mainFields: [],
tryIndex: false,
preferRelative: true,
})
file = await assetResolver(environment, url, id)
file ??=
url[0] === '/'
? slash(path.join(publicDir, url))
: slash(path.resolve(path.dirname(id), url))
}

// Get final asset URL. If the file does not exist,
// we fall back to the initial URL and let it resolve in runtime
let builtUrl: string | undefined
if (file) {
try {
if (publicDir && isParentDirectory(publicDir, file)) {
const publicPath = '/' + path.posix.relative(publicDir, file)
builtUrl = await fileToUrl(this, publicPath)
} else {
builtUrl = await fileToUrl(this, file)
// Get final asset URL. If the file does not exist,
// we fall back to the initial URL and let it resolve in runtime
let builtUrl: string | undefined
if (file) {
try {
if (publicDir && isParentDirectory(publicDir, file)) {
const publicPath = '/' + path.posix.relative(publicDir, file)
builtUrl = await fileToUrl(this, publicPath)
} else {
builtUrl = await fileToUrl(this, file)
}
} catch {
// do nothing, we'll log a warning after this
}
} catch {
// do nothing, we'll log a warning after this
}
}
if (!builtUrl) {
const rawExp = code.slice(startIndex, endIndex)
config.logger.warnOnce(
`\n${rawExp} doesn't exist at build time, it will remain unchanged to be resolved at runtime. ` +
`If this is intended, you can use the /* @vite-ignore */ comment to suppress this warning.`,
if (!builtUrl) {
const rawExp = code.slice(startIndex, endIndex)
config.logger.warnOnce(
`\n${rawExp} doesn't exist at build time, it will remain unchanged to be resolved at runtime. ` +
`If this is intended, you can use the /* @vite-ignore */ comment to suppress this warning.`,
)
builtUrl = url
}
s.update(
startIndex,
endIndex,
`new URL(${JSON.stringify(builtUrl)}, import.meta.url)`,
)
builtUrl = url
}
s.update(
startIndex,
endIndex,
`new URL(${JSON.stringify(builtUrl)}, import.meta.url)`,
)
}
if (s) {
return transformStableResult(s, id, config)
if (s) {
return transformStableResult(s, id, config)
}
}
}
return null
return null
},
},
}
}
22 changes: 11 additions & 11 deletions packages/vite/src/node/plugins/completeSystemWrap.ts
Original file line number Diff line number Diff line change
@@ -4,20 +4,20 @@ import type { Plugin } from '../plugin'
* make sure systemjs register wrap to had complete parameters in system format
*/
export function completeSystemWrapPlugin(): Plugin {
const SystemJSWrapRE = /System.register\(.*?(\(exports\)|\(\))/g
// const SystemJSWrapRE = /System.register\(.*?(\(exports\)|\(\))/g

return {
name: 'vite:force-systemjs-wrap-complete',

renderChunk(code, _chunk, opts) {
if (opts.format === 'system') {
return {
code: code.replace(SystemJSWrapRE, (s, s1) =>
s.replace(s1, '(exports, module)'),
),
map: null,
}
}
},
// renderChunk(code, _chunk, opts) {
// if (opts.format === 'system') {
// return {
// code: code.replace(SystemJSWrapRE, (s, s1) =>
// s.replace(s1, '(exports, module)'),
// ),
// map: null,
// }
// }
// },
}
}
555 changes: 299 additions & 256 deletions packages/vite/src/node/plugins/css.ts

Large diffs are not rendered by default.

16 changes: 13 additions & 3 deletions packages/vite/src/node/plugins/define.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { transform } from 'esbuild'
import { TraceMap, decodedMap, encodedMap } from '@jridgewell/trace-mapping'
import type { RolldownPlugin } from 'rolldown'
import type { ResolvedConfig } from '../config'
import type { Plugin } from '../plugin'
import { escapeRegex } from '../utils'
import type { Environment } from '../environment'
import { isCSSRequest } from './css'
@@ -12,7 +12,7 @@ const isNonJsRequest = (request: string): boolean => nonJsRe.test(request)
const importMetaEnvMarker = '__vite_import_meta_env__'
const importMetaEnvKeyReCache = new Map<string, RegExp>()

export function definePlugin(config: ResolvedConfig): Plugin {
export function definePlugin(config: ResolvedConfig): RolldownPlugin {
const isBuild = config.command === 'build'
const isBuildLib = isBuild && config.build.lib

@@ -111,7 +111,7 @@ export function definePlugin(config: ResolvedConfig): Plugin {
return pattern
}

return {
const plugin: RolldownPlugin = {
name: 'vite:define',

async transform(code, id) {
@@ -179,6 +179,16 @@ export function definePlugin(config: ResolvedConfig): Plugin {
return result
},
}
const enableNativePlugin = config.experimental.enableNativePlugin
if (enableNativePlugin) {
delete plugin.transform
plugin.options = function (option) {
const [define, _pattern, importMetaEnvVal] = getPattern(this.environment)
define['import.meta.env'] = importMetaEnvVal
option.define = define
}
}
return plugin
}

export async function replaceDefine(
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/esbuild.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import type {
} from 'esbuild'
import { transform } from 'esbuild'
import type { RawSourceMap } from '@ampproject/remapping'
import type { InternalModuleFormat, SourceMap } from 'rollup'
import type { InternalModuleFormat, SourceMap } from 'rolldown'
import type { TSConfckParseResult } from 'tsconfck'
import { TSConfckCache, TSConfckParseError, parse } from 'tsconfck'
import type { FSWatcher } from 'dep-types/chokidar'
734 changes: 380 additions & 354 deletions packages/vite/src/node/plugins/html.ts

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
@@ -58,7 +58,10 @@ import type { ResolvedConfig } from '../config'
import type { Plugin } from '../plugin'
import type { DevEnvironment } from '../server/environment'
import { shouldExternalize } from '../external'
import { optimizedDepNeedsInterop } from '../optimizer'
import {
optimizedDepInfoFromFile,
optimizedDepNeedsInterop,
} from '../optimizer'
import {
cleanUrl,
unwrapId,
@@ -82,7 +85,6 @@ export const canSkipImportAnalysis = (id: string): boolean =>
skipRE.test(id) || isDirectCSSRequest(id)

const optimizedDepChunkRE = /\/chunk-[A-Z\d]{8}\.js/
const optimizedDepDynamicRE = /-[A-Z\d]{8}\.js/

export const hasViteIgnoreRE = /\/\*\s*@vite-ignore\s*\*\//

@@ -354,6 +356,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
throw e
})

// TODO: resolved.meta is not supported
if (!resolved || resolved.meta?.['vite:alias']?.noResolved) {
// in ssr, we should let node handle the missing modules
if (ssr) {
@@ -569,6 +572,10 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
// page reload. We could return a 404 in that case but it is safe to return the request
const file = cleanUrl(resolvedId) // Remove ?v={hash}

const depInfo = optimizedDepInfoFromFile(
depsOptimizer.metadata,
file,
)
const needsInterop = await optimizedDepNeedsInterop(
environment,
depsOptimizer.metadata,
@@ -579,7 +586,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
// Non-entry dynamic imports from dependencies will reach here as there isn't
// optimize info for them, but they don't need es interop. If the request isn't
// a dynamic import, then it is an internal Vite error
if (!optimizedDepDynamicRE.test(file)) {
if (depInfo?.isDynamicEntry) {
config.logger.error(
colors.red(
`Vite Error, ${url} optimized info should be defined`,
113 changes: 78 additions & 35 deletions packages/vite/src/node/plugins/importAnalysisBuild.ts
Original file line number Diff line number Diff line change
@@ -5,22 +5,24 @@ import type {
ImportSpecifier,
} from 'es-module-lexer'
import { init, parse as parseImports } from 'es-module-lexer'
import type { SourceMap } from 'rollup'
import type { SourceMap } from 'rolldown'
import type { RawSourceMap } from '@ampproject/remapping'
import convertSourceMap from 'convert-source-map'
import { buildImportAnalysisPlugin as nativeBuildImportAnalysisPlugin } from 'rolldown/experimental'
import {
combineSourcemaps,
generateCodeFrame,
isInNodeModules,
numberToPos,
} from '../utils'
import type { Plugin } from '../plugin'
import { type Plugin, perEnvironmentPlugin } from '../plugin'
import type { ResolvedConfig } from '../config'
import { toOutputFilePathInJS } from '../build'
import { genSourceMapUrl } from '../server/sourcemap'
import type { Environment } from '../environment'
import type { PartialEnvironment } from '../baseEnvironment'
import { removedPureCssFilesCache } from './css'
import { createParseErrorInfo } from './importAnalysis'
import { getChunkMetadata } from './metadata'

type FileDep = {
url: string
@@ -166,19 +168,52 @@ function preload(
})
}

function getPreloadCode(
environment: PartialEnvironment,
renderBuiltUrlBoolean: boolean,
isRelativeBase: boolean,
) {
const { modulePreload } = environment.config.build

const scriptRel =
modulePreload && modulePreload.polyfill
? `'modulepreload'`
: `/* @__PURE__ */ (${detectScriptRel.toString()})()`

// There are two different cases for the preload list format in __vitePreload
//
// __vitePreload(() => import(asyncChunk), [ ...deps... ])
//
// This is maintained to keep backwards compatibility as some users developed plugins
// using regex over this list to workaround the fact that module preload wasn't
// configurable.
const assetsURL =
renderBuiltUrlBoolean || isRelativeBase
? // If `experimental.renderBuiltUrl` is used, the dependencies might be relative to the current chunk.
// If relative base is used, the dependencies are relative to the current chunk.
// The importerUrl is passed as third parameter to __vitePreload in this case
`function(dep, importerUrl) { return new URL(dep, importerUrl).href }`
: // If the base isn't relative, then the deps are relative to the projects `outDir` and the base
// is appended inside __vitePreload too.
`function(dep) { return ${JSON.stringify(environment.config.base)}+dep }`
const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}`
return preloadCode
}

/**
* Build only. During serve this is performed as part of ./importAnalysis.
*/
export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
const getInsertPreload = (environment: Environment) =>
export function buildImportAnalysisPlugin(config: ResolvedConfig): [Plugin] {
const getInsertPreload = (environment: PartialEnvironment) =>
environment.config.consumer === 'client' &&
!config.isWorker &&
!config.build.lib

const enableNativePlugin = config.experimental.enableNativePlugin
const renderBuiltUrl = config.experimental.renderBuiltUrl
const isRelativeBase = config.base === './' || config.base === ''

return {
const jsPlugin = {
name: 'vite:build-import-analysis',
resolveId(id) {
if (id === preloadHelperId) {
@@ -188,35 +223,17 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {

load(id) {
if (id === preloadHelperId) {
const { modulePreload } = this.environment.config.build

const scriptRel =
modulePreload && modulePreload.polyfill
? `'modulepreload'`
: `/* @__PURE__ */ (${detectScriptRel.toString()})()`

// There are two different cases for the preload list format in __vitePreload
//
// __vitePreload(() => import(asyncChunk), [ ...deps... ])
//
// This is maintained to keep backwards compatibility as some users developed plugins
// using regex over this list to workaround the fact that module preload wasn't
// configurable.
const assetsURL =
renderBuiltUrl || isRelativeBase
? // If `experimental.renderBuiltUrl` is used, the dependencies might be relative to the current chunk.
// If relative base is used, the dependencies are relative to the current chunk.
// The importerUrl is passed as third parameter to __vitePreload in this case
`function(dep, importerUrl) { return new URL(dep, importerUrl).href }`
: // If the base isn't relative, then the deps are relative to the projects `outDir` and the base
// is appended inside __vitePreload too.
`function(dep) { return ${JSON.stringify(config.base)}+dep }`
const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}`
const preloadCode = getPreloadCode(
this.environment,
!!renderBuiltUrl,
isRelativeBase,
)
return { code: preloadCode, moduleSideEffects: false }
}
},

async transform(source, importer) {
async transform(source, importer, opts) {
if (opts?.moduleType === 'css') return
if (isInNodeModules(importer) && !dynamicImportPrefixRE.test(source)) {
return
}
@@ -544,7 +561,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
chunk.imports.forEach(addDeps)
// Ensure that the css imported by current chunk is loaded after the dependencies.
// So the style of current chunk won't be overwritten unexpectedly.
chunk.viteMetadata!.importedCss.forEach((file) => {
getChunkMetadata(chunk)!.importedCss.forEach((file) => {
deps.add(file)
})
}
@@ -553,8 +570,8 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
removedPureCssFilesCache.get(config)!
const chunk = removedPureCssFiles.get(filename)
if (chunk) {
if (chunk.viteMetadata!.importedCss.size) {
chunk.viteMetadata!.importedCss.forEach((file) => {
if (getChunkMetadata(chunk)!.importedCss.size) {
getChunkMetadata(chunk)!.importedCss.forEach((file) => {
deps.add(file)
})
hasRemovedPureCssChunk = true
@@ -696,7 +713,8 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
nextMap as RawSourceMap,
chunk.map as RawSourceMap,
]) as SourceMap
map.toUrl = () => genSourceMapUrl(map)
// TODO: rolldown's sourcemap type does not have toUrl function
// map.toUrl = () => genSourceMapUrl(map)
chunk.map = map

if (buildSourcemap === 'inline') {
@@ -716,5 +734,30 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
}
}
},
} as Plugin
if (enableNativePlugin) {
delete jsPlugin.transform
delete jsPlugin.resolveId
delete jsPlugin.load
}
return [
jsPlugin,
enableNativePlugin
? perEnvironmentPlugin('native:import-analysis-build', (environment) => {
const preloadCode = getPreloadCode(
environment,
!!renderBuiltUrl,
isRelativeBase,
)
return nativeBuildImportAnalysisPlugin({
preloadCode,
insertPreload: getInsertPreload(environment),
// this field looks redundant, put a dummy value for now
optimizeModulePreloadRelativePaths: false,
renderBuiltUrl: !!renderBuiltUrl,
isRelativeBase,
}) as unknown as Plugin
})
: null,
].filter(Boolean) as [Plugin]
}
3 changes: 2 additions & 1 deletion packages/vite/src/node/plugins/importMetaGlob.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,8 @@ import type {
SpreadElement,
TemplateLiteral,
} from 'estree'
import type { CustomPluginOptions, RollupAstNode, RollupError } from 'rollup'
import type { RollupAstNode } from 'rollup'
import type { CustomPluginOptions, RollupError } from 'rolldown'
import MagicString from 'magic-string'
import { stringifyQuery } from 'ufo'
import type { GeneralImportGlobOptions } from 'types/importGlob'
119 changes: 94 additions & 25 deletions packages/vite/src/node/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import aliasPlugin, { type ResolverFunction } from '@rollup/plugin-alias'
import type { ObjectHook } from 'rollup'
import type { ObjectHook } from 'rolldown'
import {
aliasPlugin as nativeAliasPlugin,
dynamicImportVarsPlugin as nativeDynamicImportVarsPlugin,
importGlobPlugin as nativeImportGlobPlugin,
jsonPlugin as nativeJsonPlugin,
modulePreloadPolyfillPlugin as nativeModulePreloadPolyfillPlugin,
transformPlugin as nativeTransformPlugin,
wasmFallbackPlugin as nativeWasmFallbackPlugin,
wasmHelperPlugin as nativeWasmHelperPlugin,
} from 'rolldown/experimental'
import type { PluginHookUtils, ResolvedConfig } from '../config'
import { isDepOptimizationDisabled } from '../optimizer'
import type { HookHandler, Plugin, PluginWithRequiredHook } from '../plugin'
import {
type HookHandler,
type Plugin,
type PluginWithRequiredHook,
perEnvironmentPlugin,
} from '../plugin'
import { watchPackageDataPlugin } from '../packages'
import { jsonPlugin } from './json'
import { resolvePlugin } from './resolve'
import { filteredResolvePlugin, resolvePlugin } from './resolve'
import { optimizedDepsPlugin } from './optimizedDeps'
import { esbuildPlugin } from './esbuild'
import { importAnalysisPlugin } from './importAnalysis'
import { cssAnalysisPlugin, cssPlugin, cssPostPlugin } from './css'
import { assetPlugin } from './asset'
@@ -23,6 +37,7 @@ import { assetImportMetaUrlPlugin } from './assetImportMetaUrl'
import { metadataPlugin } from './metadata'
import { dynamicImportVarsPlugin } from './dynamicImportVars'
import { importGlobPlugin } from './importMetaGlob'
import { oxcPlugin } from './oxc'

export async function resolvePlugins(
config: ResolvedConfig,
@@ -41,50 +56,104 @@ export async function resolvePlugins(
Object.values(config.environments).some(
(environment) => !isDepOptimizationDisabled(environment.optimizeDeps),
)
const enableNativePlugin = config.experimental.enableNativePlugin

return [
depOptimizationEnabled ? optimizedDepsPlugin() : null,
isBuild ? metadataPlugin() : null,
!isWorker ? watchPackageDataPlugin(config.packageCache) : null,
preAliasPlugin(config),
aliasPlugin({
entries: config.resolve.alias,
customResolver: viteAliasCustomResolver,
}),
!isBuild ? preAliasPlugin(config) : null,
enableNativePlugin
? nativeAliasPlugin({
entries: config.resolve.alias.map((item) => {
return {
find: item.find,
replacement: item.replacement,
}
}),
})
: aliasPlugin({
entries: config.resolve.alias,
customResolver: viteAliasCustomResolver,
}),

...prePlugins,

modulePreload !== false && modulePreload.polyfill
? modulePreloadPolyfillPlugin(config)
? enableNativePlugin
? perEnvironmentPlugin(
'native:modulepreload-polyfill',
(environment) => {
if (
config.command !== 'build' ||
environment.config.consumer !== 'client'
)
return false
return nativeModulePreloadPolyfillPlugin({
skip: false,
}) as unknown as Plugin
},
)
: modulePreloadPolyfillPlugin(config)
: null,
resolvePlugin({
root: config.root,
isProduction: config.isProduction,
isBuild,
packageCache: config.packageCache,
asSrc: true,
optimizeDeps: true,
externalize: true,
}),
enableNativePlugin
? filteredResolvePlugin({
root: config.root,
isProduction: config.isProduction,
isBuild,
packageCache: config.packageCache,
asSrc: true,
optimizeDeps: true,
externalize: true,
})
: resolvePlugin({
root: config.root,
isProduction: config.isProduction,
isBuild,
packageCache: config.packageCache,
asSrc: true,
optimizeDeps: true,
externalize: true,
}),
htmlInlineProxyPlugin(config),
cssPlugin(config),
config.esbuild !== false ? esbuildPlugin(config) : null,
jsonPlugin(config.json, isBuild),
wasmHelperPlugin(),
config.oxc !== false
? enableNativePlugin
? nativeTransformPlugin()
: oxcPlugin(config)
: null,
enableNativePlugin
? nativeJsonPlugin({
// TODO: support `json.stringify: 'auto'` and `json.namedExports`
stringify:
!config.json?.stringify || config.json.stringify === 'auto'
? false
: config.json?.stringify,
isBuild,
})
: jsonPlugin(config.json, isBuild),
enableNativePlugin ? nativeWasmHelperPlugin() : wasmHelperPlugin(),
webWorkerPlugin(config),
assetPlugin(config),

...normalPlugins,

wasmFallbackPlugin(),
enableNativePlugin ? nativeWasmFallbackPlugin() : wasmFallbackPlugin(),
definePlugin(config),
cssPostPlugin(config),
isBuild && buildHtmlPlugin(config),
workerImportMetaUrlPlugin(config),
assetImportMetaUrlPlugin(config),
...buildPlugins.pre,
dynamicImportVarsPlugin(config),
importGlobPlugin(config),
enableNativePlugin
? nativeDynamicImportVarsPlugin()
: dynamicImportVarsPlugin(config),
enableNativePlugin
? nativeImportGlobPlugin({
root: config.root,
restoreQueryExtension: config.experimental.importGlobRestoreExtension,
})
: importGlobPlugin(config),

...postPlugins,

3 changes: 3 additions & 0 deletions packages/vite/src/node/plugins/json.ts
Original file line number Diff line number Diff line change
@@ -71,6 +71,7 @@ export function jsonPlugin(
return {
code,
map: { mappings: '' },
moduleType: 'js', // TODO: remove later when not needed
}
}

@@ -89,6 +90,7 @@ export function jsonPlugin(
return {
code: `export default JSON.parse(${JSON.stringify(json)})`,
map: { mappings: '' },
moduleType: 'js', // TODO: remove later when not needed
}
}
}
@@ -99,6 +101,7 @@ export function jsonPlugin(
namedExports: options.namedExports,
}),
map: { mappings: '' },
moduleType: 'js', // TODO: remove later when not needed
}
} catch (e) {
const position = extractJsonErrorPosition(e.message, json.length)
33 changes: 20 additions & 13 deletions packages/vite/src/node/plugins/loadFallback.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import fsp from 'node:fs/promises'
import type { RolldownPlugin } from 'rolldown'
import { cleanUrl } from '../../shared/utils'
import type { Plugin } from '../plugin'

/**
* A plugin to provide build load fallback for arbitrary request with queries.
*/
export function buildLoadFallbackPlugin(): Plugin {
export function buildLoadFallbackPlugin(): RolldownPlugin {
return {
name: 'vite:load-fallback',
async load(id) {
try {
const cleanedId = cleanUrl(id)
const content = await fsp.readFile(cleanedId, 'utf-8')
this.addWatchFile(cleanedId)
return content
} catch {
const content = await fsp.readFile(id, 'utf-8')
this.addWatchFile(id)
return content
}
load: {
filter: {
id: {
exclude: [/^data:/],
},
},
async handler(id) {
try {
const cleanedId = cleanUrl(id)
const content = await fsp.readFile(cleanedId, 'utf-8')
this.addWatchFile(cleanedId)
return content
} catch {
const content = await fsp.readFile(id, 'utf-8')
this.addWatchFile(id)
return content
}
},
},
}
}
58 changes: 34 additions & 24 deletions packages/vite/src/node/plugins/manifest.ts
Original file line number Diff line number Diff line change
@@ -4,11 +4,12 @@ import type {
OutputAsset,
OutputChunk,
RenderedChunk,
} from 'rollup'
} from 'rolldown'
import type { Plugin } from '../plugin'
import { normalizePath, sortObjectKeys } from '../utils'
import { perEnvironmentState } from '../environment'
import { cssEntriesMap } from './asset'
import { getChunkMetadata } from './metadata'
import { generatedAssetsMap } from './asset'

const endsWithJSRE = /\.[cm]?js$/

@@ -107,11 +108,11 @@ export function manifestPlugin(): Plugin {
}
}

if (chunk.viteMetadata?.importedCss.size) {
manifestChunk.css = [...chunk.viteMetadata.importedCss]
if (getChunkMetadata(chunk)?.importedCss.size) {
manifestChunk.css = [...getChunkMetadata(chunk)!.importedCss]
}
if (chunk.viteMetadata?.importedAssets.size) {
manifestChunk.assets = [...chunk.viteMetadata.importedAssets]
if (getChunkMetadata(chunk)?.importedAssets.size) {
manifestChunk.assets = [...getChunkMetadata(chunk)!.importedAssets]
}

return manifestChunk
@@ -130,15 +131,18 @@ export function manifestPlugin(): Plugin {
return manifestChunk
}

const entryCssReferenceIds = cssEntriesMap.get(this.environment)!
const entryCssAssetFileNames = new Set(entryCssReferenceIds)
for (const id of entryCssReferenceIds) {
try {
const fileName = this.getFileName(id)
entryCssAssetFileNames.add(fileName)
} catch {
// The asset was generated as part of a different output option.
// It was already handled during the previous run of this plugin.
const assets = generatedAssetsMap.get(this.environment)!
const entryCssAssetFileNames = new Set()
for (const [id, asset] of assets.entries()) {
if (asset.isEntry) {
try {
const fileName = this.getFileName(id)
entryCssAssetFileNames.add(fileName)
} catch {
// The asset was generated as part of a different output option.
// It was already handled during the previous run of this plugin.
assets.delete(id)
}
}
}

@@ -148,24 +152,29 @@ export function manifestPlugin(): Plugin {
const chunk = bundle[file]
if (chunk.type === 'chunk') {
manifest[getChunkName(chunk)] = createChunk(chunk)
} else if (chunk.type === 'asset' && chunk.names.length > 0) {
} else if (chunk.type === 'asset' && typeof chunk.name === 'string') {
// Add every unique asset to the manifest, keyed by its original name
const src =
chunk.originalFileNames.length > 0
? chunk.originalFileNames[0]
: '_' + path.basename(chunk.fileName)
chunk.originalFileName ?? '_' + path.basename(chunk.fileName)
const isEntry = entryCssAssetFileNames.has(chunk.fileName)
const asset = createAsset(chunk, src, isEntry)

// If JS chunk and asset chunk are both generated from the same source file,
// prioritize JS chunk as it contains more information
const file = manifest[src]?.file
if (!(file && endsWithJSRE.test(file))) {
manifest[src] = asset
fileNameToAsset.set(chunk.fileName, asset)
}
if (file && endsWithJSRE.test(file)) continue

manifest[src] = asset
fileNameToAsset.set(chunk.fileName, asset)
}
}

for (const originalFileName of chunk.originalFileNames.slice(1)) {
// Add deduplicated assets to the manifest
for (const [referenceId, { originalFileName }] of assets.entries()) {
if (originalFileName && !manifest[originalFileName]) {
const fileName = this.getFileName(referenceId)
const asset = fileNameToAsset.get(fileName)
if (asset) {
manifest[originalFileName] = asset
}
}
@@ -195,6 +204,7 @@ export function getChunkOriginalFileName(
): string | undefined {
if (chunk.facadeModuleId) {
let name = normalizePath(path.relative(root, chunk.facadeModuleId))
// @ts-expect-error TODO: system format is not supported
if (format === 'system' && !chunk.name.includes('-legacy')) {
const ext = path.extname(name)
const endPos = ext.length !== 0 ? -ext.length : undefined
20 changes: 18 additions & 2 deletions packages/vite/src/node/plugins/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { ChunkMetadata } from 'types/metadata'
import type { OutputChunk, RenderedChunk } from 'rolldown'
import type { Plugin } from '../plugin'

// TODO: avoid memory leak
const chunkMetadataMap = new Map<string, ChunkMetadata>()

/**
* Prepares the rendered chunks to contain additional metadata during build.
*/
@@ -8,11 +13,22 @@ export function metadataPlugin(): Plugin {
name: 'vite:build-metadata',

async renderChunk(_code, chunk) {
chunk.viteMetadata = {
// Since the chunk come from rust side, mutate it directly will not sync back to rust side.
// The next usage will lost the metadata
chunkMetadataMap.set(chunk.fileName, {
importedAssets: new Set(),
importedCss: new Set(),
}
})
return null
},
}
}

// TODO: give users a way to access the metadata
export function getChunkMetadata(
chunk: RenderedChunk | OutputChunk,
): ChunkMetadata | undefined {
const preliminaryFileName =
'preliminaryFileName' in chunk ? chunk.preliminaryFileName : chunk.fileName
return chunkMetadataMap.get(preliminaryFileName)
}
322 changes: 322 additions & 0 deletions packages/vite/src/node/plugins/oxc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
import path from 'node:path'
import type {
TransformOptions as OxcTransformOptions,
TransformResult as OxcTransformResult,
} from 'rolldown/experimental'
import { transform } from 'rolldown/experimental'
import type { RawSourceMap } from '@ampproject/remapping'
import type { SourceMap } from 'rolldown'
import type { FSWatcher } from 'dep-types/chokidar'
import { TSConfckParseError } from 'tsconfck'
import { combineSourcemaps, createFilter, ensureWatchedFile } from '../utils'
import type { ResolvedConfig } from '../config'
import type { Plugin, PluginContext } from '../plugin'
import { cleanUrl } from '../../shared/utils'
import type { Logger } from '..'
import type { ViteDevServer } from '../server'
import type { ESBuildOptions } from './esbuild'
import { loadTsconfigJsonForFile } from './esbuild'

const jsxExtensionsRE = /\.(?:j|t)sx\b/
const validExtensionRE = /\.\w+$/

export interface OxcOptions extends OxcTransformOptions {
include?: string | RegExp | string[] | RegExp[]
exclude?: string | RegExp | string[] | RegExp[]
jsxInject?: string
jsxInclude?: string | RegExp | string[] | RegExp[]
jsxExclude?: string | RegExp | string[] | RegExp[]
}

export async function transformWithOxc(
ctx: PluginContext,
code: string,
filename: string,
options?: OxcTransformOptions,
inMap?: object,
config?: ResolvedConfig,
watcher?: FSWatcher,
): Promise<OxcTransformResult> {
let lang = options?.lang

if (!lang) {
// if the id ends with a valid ext, use it (e.g. vue blocks)
// otherwise, cleanup the query before checking the ext
const ext = path
.extname(validExtensionRE.test(filename) ? filename : cleanUrl(filename))
.slice(1)

if (ext === 'cjs' || ext === 'mjs') {
lang = 'js'
} else if (ext === 'cts' || ext === 'mts') {
lang = 'ts'
} else {
lang = ext as 'js' | 'jsx' | 'ts' | 'tsx'
}
}

const resolvedOptions = {
sourcemap: true,
...options,
lang,
}

if (lang === 'ts' || lang === 'tsx') {
try {
const { tsconfig: loadedTsconfig, tsconfigFile } =
await loadTsconfigJsonForFile(filename, config)
// tsconfig could be out of root, make sure it is watched on dev
if (watcher && tsconfigFile && config) {
ensureWatchedFile(watcher, tsconfigFile, config.root)
}
const loadedCompilerOptions = loadedTsconfig.compilerOptions ?? {}
// tsc compiler experimentalDecorators/target/useDefineForClassFields

resolvedOptions.jsx ??= {}
if (loadedCompilerOptions.jsxFactory) {
resolvedOptions.jsx.pragma = loadedCompilerOptions.jsxFactory
}
if (loadedCompilerOptions.jsxFragmentFactory) {
resolvedOptions.jsx.pragmaFrag =
loadedCompilerOptions.jsxFragmentFactory
}
if (loadedCompilerOptions.jsxImportSource) {
resolvedOptions.jsx.importSource = loadedCompilerOptions.jsxImportSource
}

switch (loadedCompilerOptions.jsx) {
case 'react-jsxdev':
resolvedOptions.jsx.runtime = 'automatic'
resolvedOptions.jsx.development = true
break
case 'react':
resolvedOptions.jsx.runtime = 'classic'
break
case 'react-jsx':
resolvedOptions.jsx.runtime = 'automatic'
break
case 'preserve':
ctx.warn('The tsconfig jsx preserve is not supported by oxc')
break
default:
break
}

/**
* | preserveValueImports | importsNotUsedAsValues | verbatimModuleSyntax | onlyRemoveTypeImports |
* | -------------------- | ---------------------- | -------------------- |---------------------- |
* | false | remove | false | false |
* | false | preserve, error | - | - |
* | true | remove | - | - |
* | true | preserve, error | true | true |
*/
if (loadedCompilerOptions.verbatimModuleSyntax !== undefined) {
resolvedOptions.typescript ??= {}
resolvedOptions.typescript.onlyRemoveTypeImports =
loadedCompilerOptions.verbatimModuleSyntax
} else if (
loadedCompilerOptions.preserveValueImports !== undefined ||
loadedCompilerOptions.importsNotUsedAsValues !== undefined
) {
const preserveValueImports =
loadedCompilerOptions.preserveValueImports ?? false
const importsNotUsedAsValues =
loadedCompilerOptions.importsNotUsedAsValues ?? 'remove'
if (
preserveValueImports === false &&
importsNotUsedAsValues === 'remove'
) {
resolvedOptions.typescript ??= {}
resolvedOptions.typescript.onlyRemoveTypeImports = true
} else if (
preserveValueImports === true &&
(importsNotUsedAsValues === 'preserve' ||
importsNotUsedAsValues === 'error')
) {
resolvedOptions.typescript ??= {}
resolvedOptions.typescript.onlyRemoveTypeImports = false
} else {
ctx.warn(
`preserveValueImports=${preserveValueImports} + importsNotUsedAsValues=${importsNotUsedAsValues} is not supported by oxc.` +
'Please migrate to the new verbatimModuleSyntax option.',
)
}
}
} catch (e) {
if (e instanceof TSConfckParseError) {
// tsconfig could be out of root, make sure it is watched on dev
if (watcher && e.tsconfigFile && config) {
ensureWatchedFile(watcher, e.tsconfigFile, config.root)
}
}
throw e
}
}

const result = transform(filename, code, resolvedOptions)

if (result.errors.length > 0) {
throw new Error(result.errors[0])
}

let map: SourceMap
if (inMap && result.map) {
const nextMap = result.map
nextMap.sourcesContent = []
map = combineSourcemaps(filename, [
nextMap as RawSourceMap,
inMap as RawSourceMap,
]) as SourceMap
} else {
map = result.map as SourceMap
}
return {
...result,
map,
}
}

export function oxcPlugin(config: ResolvedConfig): Plugin {
const options = config.oxc as OxcOptions
const {
jsxInject,
include,
exclude,
jsxInclude,
jsxExclude,
...oxcTransformOptions
} = options

const defaultInclude = Array.isArray(include)
? include
: [include || /\.(m?ts|[jt]sx)$/]
const filter = createFilter(
defaultInclude.concat(jsxInclude || []),
exclude || /\.js$/,
)
const jsxFilter = createFilter(
jsxInclude || /\.jsx$/,
jsxExclude || /\.(m?[jt]s|tsx)$/,
)

let server: ViteDevServer

return {
name: 'vite:oxc',
configureServer(_server) {
server = _server
},
async transform(code, id) {
if (filter(id) || filter(cleanUrl(id))) {
// disable refresh at ssr
if (
this.environment.config.consumer === 'server' &&
oxcTransformOptions.jsx?.refresh
) {
oxcTransformOptions.jsx.refresh = false
}
if (
(jsxFilter(id) || jsxFilter(cleanUrl(id))) &&
!oxcTransformOptions.lang
) {
oxcTransformOptions.lang = 'jsx'
}

const result = await transformWithOxc(
this,
code,
id,
oxcTransformOptions,
undefined,
config,
server?.watcher,
)
if (jsxInject && jsxExtensionsRE.test(id)) {
result.code = jsxInject + ';' + result.code
}
return {
code: result.code,
map: result.map,
}
}
},
}
}

export function convertEsbuildConfigToOxcConfig(
esbuildConfig: ESBuildOptions,
logger: Logger,
): OxcOptions {
const { jsxInject, include, exclude, ...esbuildTransformOptions } =
esbuildConfig

const oxcOptions: OxcOptions = {
jsxInject,
include,
exclude,
jsx: {},
}

switch (esbuildTransformOptions.jsx) {
case 'automatic':
oxcOptions.jsx!.runtime = 'automatic'
break

case 'transform':
oxcOptions.jsx!.runtime = 'classic'
break

case 'preserve':
logger.warn('The esbuild jsx preserve is not supported by oxc')
break

default:
break
}

if (esbuildTransformOptions.jsxDev) {
oxcOptions.jsx!.development = true
}
if (esbuildTransformOptions.jsxFactory) {
oxcOptions.jsx!.pragma = esbuildTransformOptions.jsxFactory
}
if (esbuildTransformOptions.jsxFragment) {
oxcOptions.jsx!.pragmaFrag = esbuildTransformOptions.jsxFragment
}
if (esbuildTransformOptions.jsxImportSource) {
oxcOptions.jsx!.importSource = esbuildTransformOptions.jsxImportSource
}
if (esbuildTransformOptions.loader) {
if (
['.js', '.jsx', '.ts', 'tsx'].includes(esbuildTransformOptions.loader)
) {
oxcOptions.lang = esbuildTransformOptions.loader as
| 'js'
| 'jsx'
| 'ts'
| 'tsx'
} else {
logger.warn(
`The esbuild loader ${esbuildTransformOptions.loader} is not supported by oxc`,
)
}
}
if (esbuildTransformOptions.define) {
oxcOptions.define = esbuildTransformOptions.define
}

switch (esbuildTransformOptions.sourcemap) {
case true:
case false:
oxcOptions.sourcemap = esbuildTransformOptions.sourcemap
break

default:
logger.warn(
`The esbuild sourcemap ${esbuildTransformOptions.sourcemap} is not supported by oxc`,
)
break
}

return oxcOptions
}
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/reporter.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import path from 'node:path'
import { gzip } from 'node:zlib'
import { promisify } from 'node:util'
import colors from 'picocolors'
import type { OutputBundle } from 'rollup'
import type { OutputBundle } from 'rolldown'
import type { Plugin } from '../plugin'
import type { ResolvedConfig } from '../config'
import type { Environment } from '../environment'
81 changes: 72 additions & 9 deletions packages/vite/src/node/plugins/resolve.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import colors from 'picocolors'
import type { PartialResolvedId } from 'rollup'
import type { PartialResolvedId, RolldownPlugin } from 'rolldown'
import { exports, imports } from 'resolve.exports'
import { hasESMSyntax } from 'mlly'
import type { Plugin } from '../plugin'
@@ -178,10 +178,50 @@ export interface ResolvePluginOptionsWithOverrides
extends ResolveOptions,
ResolvePluginOptions {}

export function filteredResolvePlugin(
resolveOptions: ResolvePluginOptionsWithOverrides,
): RolldownPlugin {
const originalPlugin = resolvePlugin(resolveOptions)

return {
name: 'vite:resolve',
options(option) {
option.resolve ??= {}
option.resolve.extensions = this.environment.config.resolve.extensions
option.resolve.extensionAlias = {
'.js': ['.ts', '.tsx', '.js'],
'.jsx': ['.ts', '.tsx', '.jsx'],
'.mjs': ['.mts', '.mjs'],
'.cjs': ['.cts', '.cjs'],
}
},
resolveId: {
filter: {
id: {
exclude: [
// relative paths without query
/^\.\.?[/\\][^?]+$/,
/^(?:\0|\/?virtual:)/,
],
},
},
// @ts-expect-error the options is incompatible
handler: originalPlugin.resolveId!,
},
load: originalPlugin.load,
}
}

export function resolvePlugin(
resolveOptions: ResolvePluginOptionsWithOverrides,
): Plugin {
const { root, isProduction, asSrc, preferRelative = false } = resolveOptions
const {
root,
isProduction,
isBuild,
asSrc,
preferRelative = false,
} = resolveOptions

// In unix systems, absolute paths inside root first needs to be checked as an
// absolute URL (/root/root/path-to-file) resulting in failed checks before falling
@@ -214,9 +254,7 @@ export function resolvePlugin(
return id
}

// this is passed by @rollup/plugin-commonjs
const isRequire: boolean =
resolveOpts?.custom?.['node-resolve']?.isRequire ?? false
const isRequire: boolean = resolveOpts.kind === 'require-call'

const currentEnvironmentOptions = this.environment.config

@@ -463,16 +501,41 @@ export function resolvePlugin(

load(id) {
if (id.startsWith(browserExternalId)) {
if (isProduction) {
return `export default {}`
if (isBuild) {
if (isProduction) {
// rolldown treats missing export as an error, and will break build.
// So use cjs to avoid it.
return `module.exports = {}`
} else {
id = id.slice(browserExternalId.length + 1)
// rolldown uses esbuild interop helper, so copy the proxy module from https://github.com/vitejs/vite/blob/main/packages/vite/src/node/optimizer/esbuildDepPlugin.ts#L259
return `\
module.exports = Object.create(new Proxy({}, {
get(_, key) {
if (
key !== '__esModule' &&
key !== '__proto__' &&
key !== 'constructor' &&
key !== 'splice'
) {
throw new Error(\`Module "${id}" has been externalized for browser compatibility. Cannot access "${id}.\${key}" in client code. See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\`)
}
}
}))`
}
} else {
id = id.slice(browserExternalId.length + 1)
return `\
// in dev, needs to return esm
if (isProduction) {
return `export default {}`
} else {
id = id.slice(browserExternalId.length + 1)
return `\
export default new Proxy({}, {
get(_, key) {
throw new Error(\`Module "${id}" has been externalized for browser compatibility. Cannot access "${id}.\${key}" in client code. See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\`)
}
})`
}
}
}
if (id.startsWith(optionalPeerDepId)) {
Loading