Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion docs/guide/static-deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ After your project has been imported and deployed, all subsequent pushes to bran
3. Vercel will detect that you are using Vite and will enable the correct settings for your deployment.
4. Your application is deployed! (e.g. [vite-vue-template.vercel.app](https://vite-vue-template.vercel.app/))

After your project has been imported and deployed, all subsequent pushes to branches will generate [Preview Deployments](https://vercel.com/docs/concepts/deployments/environments#preview), and all changes made to the Production Branch (commonly “main”) will result in a [Production Deployment](https://vercel.com/docs/concepts/deployments/environments#production).
After your project has been imported and deployed, all subsequent pushes to branches will generate [Preview Deployments](https://vercel.com/docs/deployments/environments#preview-environment-pre-production), and all changes made to the Production Branch (commonly “main”) will result in a [Production Deployment](https://vercel.com/docs/deployments/environments#production-environment).

Learn more about Vercel’s [Git Integration](https://vercel.com/docs/concepts/git).

Expand Down
12 changes: 10 additions & 2 deletions packages/plugin-legacy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,12 @@ const outputOptionsForLegacyChunks =
function resolveLegacyOutputMinify(
minify: BuildOptions['minify'],
supportsOxc: boolean | undefined,
target?: BuildOptions['target'],
): Rollup.OutputOptions['minify'] {
const usesOxc = supportsOxc && (minify === 'oxc' || minify === true)
return usesOxc ? true : false
if (!usesOxc) return false
if (target === undefined || target === false) return true
return { compress: { target } }
}

function resolveLegacyBuildMinify(
Expand Down Expand Up @@ -437,6 +440,8 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] {
options.externalSystemJS,
false,
supportsLegacyOxcMinification,
// Don't use newer syntax for legacy polyfill chunks
'es2015',
)
}
},
Expand Down Expand Up @@ -944,8 +949,10 @@ async function buildPolyfillChunk(
excludeSystemJS?: boolean,
prependModenChunkLegacyGuard?: boolean,
supportsLegacyOxcMinification?: boolean,
overrideMinifyCompressTarget?: BuildOptions['target'],
) {
const { assetsDir, sourcemap } = buildOptions
const { assetsDir, sourcemap, target } = buildOptions
const minifyCompressTarget = overrideMinifyCompressTarget ?? target
const minify = resolveLegacyBuildMinify(
buildOptions.minify,
supportsLegacyOxcMinification,
Expand Down Expand Up @@ -977,6 +984,7 @@ async function buildPolyfillChunk(
minify: resolveLegacyOutputMinify(
buildOptions.minify,
supportsLegacyOxcMinification,
minifyCompressTarget,
),
},
},
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/client/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ pre.frame {

.message {
line-height: 1.3;
font-weight: 600;
font-weight: 400;
white-space: pre-wrap;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/vite/src/module-runner/sourcemap/interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { OriginalMapping } from '@jridgewell/trace-mapping'
import type { ModuleRunner } from '../runner'
import { posixDirname, posixResolve } from '../utils'
import { decodeBase64, posixDirname, posixResolve } from '../utils'
import type { EvaluatedModules } from '../evaluatedModules'
import { slash } from '../../shared/utils'
import { DecodedMap, getOriginalPosition } from './decoder'
Expand Down Expand Up @@ -154,7 +154,7 @@ function retrieveSourceMap(source: string) {
if (reSourceMap.test(sourceMappingURL)) {
// Support source map URL as a data url
const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1)
sourceMapData = Buffer.from(rawData, 'base64').toString()
sourceMapData = decodeBase64(rawData)
sourceMappingURL = source
} else {
// Support source map URLs relative to the source URL
Expand Down
9 changes: 7 additions & 2 deletions packages/vite/src/module-runner/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { isWindows } from '../shared/utils'
const textDecoder = new TextDecoder()

export const decodeBase64: (base64: string) => string = (() => {
if (typeof Buffer === 'function' && typeof Buffer.from === 'function') {
return (base64: string) => Buffer.from(base64, 'base64').toString('utf-8')
// Capture the binding at module load: realms may legitimately delete
// `globalThis.Buffer` afterwards (e.g. browser-parity tests), and this
// decoder runs lazily while preparing stack traces
const capturedBuffer = typeof Buffer === 'function' ? Buffer : undefined
if (capturedBuffer && typeof capturedBuffer.from === 'function') {
return (base64: string) =>
capturedBuffer.from(base64, 'base64').toString('utf-8')
}

return (base64: string) =>
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export interface BuildEnvironmentOptions {
/**
* Override CSS minification specifically instead of defaulting to `build.minify`,
* so you can configure minification for JS and CSS separately.
* @default 'lightningcss'
* @default 'lightningcss', but false if build.minify is disabled for client build
*/
cssMinify?: boolean | 'lightningcss' | 'esbuild'
/**
Expand Down
19 changes: 16 additions & 3 deletions packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
isDefined,
isExternalUrl,
isFilePathESM,
isFilePathFormatExplicit,
isInNodeModules,
isJSRequest,
joinUrlSegments,
Expand Down Expand Up @@ -459,6 +460,17 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
_isNodeModeResult ??= isFilePathESM(importer, config.packageCache)
return _isNodeModeResult
}
let _isNodeModeForDynamicImportResult = config.legacy
?.inconsistentCjsInterop
? false
: undefined
const isNodeModeForDynamicImport = () => {
_isNodeModeForDynamicImportResult ??= isFilePathFormatExplicit(
importer,
config.packageCache,
)
return _isNodeModeForDynamicImportResult
}

await Promise.all(
imports.map(async (importSpecifier, index) => {
Expand Down Expand Up @@ -626,7 +638,9 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
url,
index,
importer,
isNodeMode(),
isDynamicImport
? isNodeModeForDynamicImport()
: isNodeMode(),
config,
)
rewriteDone = true
Expand Down Expand Up @@ -948,12 +962,11 @@ export function interopNamedImports(
} = importSpecifier
const exp = source.slice(expStart, expEnd)
if (dynamicIndex > -1) {
const inconsistentCjsInterop = !!config.legacy?.inconsistentCjsInterop
// rewrite `import('package')` to expose the default directly
str.overwrite(
expStart,
expEnd,
`import('${rewrittenUrl}').then(m => (${interopHelperStr})(m.default, ${inconsistentCjsInterop ? 0 : 1}))` +
`import('${rewrittenUrl}').then(m => (${interopHelperStr})(m.default, ${+isNodeMode}))` +
getLineBreaks(exp),
{ contentOnly: true },
)
Expand Down
9 changes: 9 additions & 0 deletions packages/vite/src/node/server/bundledDev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class MemoryFiles {
export class BundledDev {
private _devEngine!: DevEngine
private initialBuildCompleted = false
private _closed = false
private clients = new Clients()
private invalidateCalledModules = new Map<
NormalizedHotChannelClient,
Expand Down Expand Up @@ -98,6 +99,7 @@ export class BundledDev {
}

async listen(): Promise<void> {
this._closed = false
debug?.('INITIAL: setup bundle options')
const rolldownOptions = await this.getRolldownOptions()
// NOTE: only single outputOptions is supported here
Expand Down Expand Up @@ -206,18 +208,24 @@ export class BundledDev {
},
)
this.waitForInitialBuildFinish().then(() => {
if (this._closed) return
debug?.('INITIAL: build done')
this.environment.hot.send({ type: 'full-reload', path: '*' })
this.initialBuildCompleted = true
})
}

private async waitForInitialBuildFinish(): Promise<void> {
if (this._closed) return
await this.devEngine.ensureCurrentBuildFinish()
if (this._closed) return

let state = await this.devEngine.getBundleState()
while (this.memoryFiles.size === 0 && !state.lastBuildErrored) {
await setTimeout(10)
if (this._closed) return
await this.devEngine.ensureCurrentBuildFinish()
if (this._closed) return
state = await this.devEngine.getBundleState()
}
}
Expand Down Expand Up @@ -322,6 +330,7 @@ export class BundledDev {
}

async close(): Promise<void> {
this._closed = true
this.memoryFiles.clear()
await this._devEngine?.close()
this.initialBuildCompleted = false
Expand Down
3 changes: 1 addition & 2 deletions packages/vite/src/node/server/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,7 @@ export class DevEnvironment extends BaseEnvironment {

this._crawlEndFinder.cancel()
await Promise.allSettled([
this.pluginContainer.close(),
this.bundledDev?.close(),
this.bundledDev ? this.bundledDev.close() : this.pluginContainer.close(),
this.depsOptimizer?.close(),
// WebSocketServer is independent of HotChannel and should not be closed on environment close
isWebSocketServer in this.hot ? Promise.resolve() : this.hot.close(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { describe, expect } from 'vitest'
import type { ViteDevServer } from '../../..'
import type { ModuleRunnerContext } from '../../../../module-runner'
import { ESModulesEvaluator } from '../../../../module-runner'
import { SOURCEMAPPING_URL } from '../../../../shared/constants'
import {
createFixtureEditor,
createModuleRunnerTester,
Expand Down Expand Up @@ -156,6 +157,29 @@ describe('module runner initialization', async () => {
]
`)
})

it('should not crash when preparing a stack trace while globalThis.Buffer is absent', async ({
runner,
server,
}) => {
const mod = await runner.import('/fixtures/throws-error-method.ts')
const methodError = await getError(() => mod.throwError())

// Realms may legitimately remove `Buffer` after modules were loaded
// (e.g. browser-parity tests). Preparing a stack in that window must not
// depend on the global still existing.
const bufferBackup = globalThis.Buffer
Reflect.deleteProperty(globalThis, 'Buffer')
try {
// `.stack` access lazily invokes `prepareStackTrace`, which decodes the
// inline source map of the runner module
expect(serializeStack(server, methodError)).toBe(
' at Module.throwError (<root>/fixtures/throws-error-method.ts:6:9)',
)
} finally {
globalThis.Buffer = bufferBackup
}
})
})

describe('module runner with node:vm executor', async () => {
Expand Down Expand Up @@ -193,3 +217,68 @@ describe('module runner with node:vm executor', async () => {
).not.toThrow()
})
})

describe('module runner interceptor with inline data: source map', async () => {
const syntheticFile = '/virtual-bufferless/inline-map.js'
// Minimal map pointing every generated position at line 1, column 0 of
// "inline-map-original.ts"
const syntheticSourceMap = {
version: 3,
sources: ['inline-map-original.ts'],
names: [],
mappings: 'AAAA',
}
// The comment token is composed from SOURCEMAPPING_URL so that this spec
// file itself never contains it (see shared/constants.ts)
const syntheticFileContent =
`throw new Error('example')\n` +
`//# ${SOURCEMAPPING_URL}=data:application/json;base64,${btoa(
JSON.stringify(syntheticSourceMap),
)}\n`

class Evaluator extends ESModulesEvaluator {
async runInlinedModule(_: ModuleRunnerContext, __: string) {
const initModule = runInThisContext(
'() => { throw new Error("example") }',
{ filename: syntheticFile },
)

initModule()
}
}

const it = await createModuleRunnerTester(
{},
{
sourcemapInterceptor: {
// Serves a file that is not part of the runner module graph and
// carries an inline base64 source map, so mapping goes through
// `retrieveSourceMap` instead of the runner graph
retrieveFile(id) {
if (id === syntheticFile) {
return syntheticFileContent
}
},
},
evaluator: new Evaluator(),
},
)

it('should not crash when decoding an inline source map while globalThis.Buffer is absent', async ({
runner,
}) => {
const error: Error = await runner
.import('/fixtures/a.ts')
.catch((err) => err)

const bufferBackup = globalThis.Buffer
Reflect.deleteProperty(globalThis, 'Buffer')
try {
expect(error.stack).toContain(
'/virtual-bufferless/inline-map-original.ts:1:1',
)
} finally {
globalThis.Buffer = bufferBackup
}
})
})
20 changes: 20 additions & 0 deletions packages/vite/src/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,26 @@ export function isFilePathESM(
}
}

/**
* Whether the file's module format is explicitly determined as ESM or CJS by
* its extension or the nearest `package.json` `"type"` field, as opposed to
* being ambiguous.
*/
export function isFilePathFormatExplicit(
filePath: string,
packageCache?: PackageCache,
): boolean {
if (/\.[mc][jt]s$/.test(filePath)) {
return true
}
try {
const pkg = findNearestPackageData(path.dirname(filePath), packageCache)
return pkg?.data.type === 'module' || pkg?.data.type === 'commonjs'
} catch {
return false
}
}

export const splitRE: RegExp = /\r?\n/g

const range: number = 2
Expand Down
51 changes: 51 additions & 0 deletions playground/hmr-full-bundle-mode/__tests__/build-hooks.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import path from 'node:path'
import { type Plugin, type ViteDevServer, createServer } from 'vite'
import { afterEach, describe, expect, test } from 'vitest'
import { isServe } from '~utils'

let server: ViteDevServer | undefined

afterEach(async () => {
await server?.close()
server = undefined
})

// In full bundle mode the build is driven by Rolldown's dev engine, which is the
// one that invokes plugins' `buildStart`/`buildEnd` hooks. Regression test for
// `buildEnd` being called twice because both `bundledDev.close()` and
// `pluginContainer.close()` fired it on server close.
describe.runIf(isServe)('full bundle mode build hooks', () => {
test('buildStart and buildEnd are each called only once', async () => {
let buildStartCount = 0
let buildEndCount = 0
const countPlugin: Plugin = {
name: 'count-build-hooks',
buildStart() {
buildStartCount++
},
buildEnd() {
buildEndCount++
},
}

server = await createServer({
root: path.resolve(import.meta.dirname, '..'),
configFile: false,
logLevel: 'silent',
experimental: { bundledDev: true },
plugins: [countPlugin],
})
await server.listen()

// the initial full-bundle build runs on listen and fires buildStart + buildEnd once
await expect.poll(() => buildEndCount, { timeout: 10000 }).toBe(1)
expect(buildStartCount).toBe(1)

await server.close()
server = undefined

// closing must not fire buildStart/buildEnd again
expect(buildStartCount).toBe(1)
expect(buildEndCount).toBe(1)
})
})
Loading