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
1 change: 1 addition & 0 deletions docs/guide/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ vite [root]
| `--cors` | Enable CORS (`boolean`) |
| `--strictPort` | Exit if specified port is already in use (`boolean`) |
| `--force` | Force the optimizer to ignore the cache and re-bundle (`boolean`) |
| `--experimentalBundle` | Use experimental full bundle mode (this is highly experimental) (`boolean`) |
| `-c, --config <file>` | Use specified config file (`string`) |
| `--base <path>` | Public base path (default: `/`) (`string`) |
| `-l, --logLevel <level>` | info \| warn \| error \| silent (`string`) |
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/rollupLicensePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function licensePlugin(
): Plugin {
const originalPlugin = license({
thirdParty(dependencies) {
// https://github.com/rollup/rollup/blob/master/build-plugins/generate-license-file.js
// https://github.com/rollup/rollup/blob/1378cae13b33838de9c8ba9ef9152354f6eed27b/build-plugins/generate-license-file.js
// MIT Licensed https://github.com/rollup/rollup/blob/master/LICENSE-CORE.md
const coreLicense = fs.readFileSync(
new URL('../../LICENSE', import.meta.url),
Expand Down
188 changes: 149 additions & 39 deletions packages/vite/src/client/bundledDevHmrClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ export interface RolldownRuntimeLike {
loadExports(id: string): unknown
}

interface PropagationBoundary {
boundary: string
acceptedVia: string
isWithinCircularImport: boolean
}

type HmrUpdate =
| { type: 'noop' }
| { type: 'full-reload'; reason: string }
| {
type: 'boundaries'
/** `[boundary, acceptedVia]` pairs */
boundaries: [string, string][]
boundaries: PropagationBoundary[]
updateSet: string[]
}

Expand Down Expand Up @@ -69,7 +74,7 @@ export class BundledDevHMRClient extends HMRClient {
changedIds: string[],
opts?: { firstInvalidatedBy?: string },
): HmrUpdate {
const boundaries: [string, string][] = []
const boundaries: PropagationBoundary[] = []
const updateSet = new Set<string>()
const traversedModules = new Set<string>()
for (const changed of changedIds) {
Expand All @@ -95,7 +100,7 @@ export class BundledDevHMRClient extends HMRClient {
id: string,
stack: string[],
updateSet: Set<string>,
boundaries: [string, string][],
boundaries: PropagationBoundary[],
firstInvalidatedBy: string | undefined,
traversedModules: Set<string>,
): HmrUpdate | undefined {
Expand All @@ -109,7 +114,11 @@ export class BundledDevHMRClient extends HMRClient {
}
}
if (this.isSelfAccepted(id)) {
boundaries.push([id, id])
boundaries.push({
boundary: id,
acceptedVia: id,
isWithinCircularImport: this.isNodeWithinCircularImports(id, stack),
})
return
}
const parents = this.runtime
Expand All @@ -122,28 +131,107 @@ export class BundledDevHMRClient extends HMRClient {
}
}
for (const parent of parents) {
const subChain = [...stack, parent]
if (this.acceptsDep(parent, id)) {
boundaries.push([parent, id])
boundaries.push({
boundary: parent,
acceptedVia: id,
isWithinCircularImport: this.isNodeWithinCircularImports(
parent,
subChain,
),
})
continue
}
if (stack.includes(parent)) {
return {
type: 'full-reload',
reason: `circular import chain between \`${id}\` and \`${parent}\``,
}
if (!stack.includes(parent)) {
const fullReload = this.bubble(
parent,
subChain,
updateSet,
boundaries,
firstInvalidatedBy,
traversedModules,
)
if (fullReload) return fullReload
}
const fullReload = this.bubble(
parent,
[...stack, parent],
updateSet,
boundaries,
firstInvalidatedBy,
traversedModules,
)
if (fullReload) return fullReload
}
}

/**
* Check importers recursively if it's an import loop. An accepted module within
* an import loop cannot recover its execution order and should be reloaded.
*
* @param node The node that accepts HMR and is a boundary
* @param nodeChain The chain of nodes/imports that lead to the node.
* (The last node in the chain imports the `node` parameter)
* @param currentChain The current chain tracked from the `node` parameter
* @param traversedModules The set of modules that have traversed
*/
private isNodeWithinCircularImports(
node: string,
nodeChain: string[],
currentChain: string[] = [node],
traversedModules = new Set<string>(),
): boolean {
// To help visualize how each parameter works, imagine this import graph:
//
// A -> B -> C -> ACCEPTED -> D -> E -> NODE
// ^--------------------------|
//
// ACCEPTED: the node that accepts HMR. the `node` parameter.
// NODE : the initial node that triggered this HMR.
//
// This function will return true in the above graph, which:
// `node` : ACCEPTED
// `nodeChain` : [NODE, E, D, ACCEPTED]
// `currentChain` : [ACCEPTED, C, B]
//
// It works by checking if any `node` importers are within `nodeChain`, which
// means there's an import loop with a HMR-accepted module in it.

if (traversedModules.has(node)) {
return false
}
traversedModules.add(node)

for (const importer of this.runtime.getImporters(node)) {
// Node may import itself which is safe
if (importer === node) continue

// Check circular imports
const importerIndex = nodeChain.indexOf(importer)
if (importerIndex > -1) {
// Log extra debug information so users can fix and remove the circular imports
// Following explanation above:
// `importer` : E
// `currentChain` reversed : [B, C, ACCEPTED]
// `nodeChain` sliced & reversed : [D, E]
// Combined : [E, B, C, ACCEPTED, D, E]
const importChain = [
importer,
...[...currentChain].reverse(),
...nodeChain.slice(importerIndex, -1).reverse(),
]
this.logger.debug(
`circular imports detected: ${importChain.join(' -> ')}`,
)
return true
}

// Continue recursively
if (!currentChain.includes(importer)) {
const result = this.isNodeWithinCircularImports(
importer,
nodeChain,
currentChain.concat(importer),
traversedModules,
)
if (result) return result
}
}
return false
}

handlePush(payload: BundledDevUpdatePayload): void {
this.applyQueue = this.applyQueue
.then(() => this.applyPush(payload))
Expand Down Expand Up @@ -247,22 +335,41 @@ export class BundledDevHMRClient extends HMRClient {
}

// collect callbacks before the caches are removed
const applies = update.boundaries.map(([boundary, acceptedVia]) => ({
boundary,
acceptedVia,
callbacks:
this.hotModulesMap
.get(boundary)
?.callbacks.filter((c) => c.deps.includes(acceptedVia)) ?? [],
}))
const applies = update.boundaries.map(
({ boundary, acceptedVia, isWithinCircularImport }) => ({
boundary,
acceptedVia,
isWithinCircularImport,
callbacks:
this.hotModulesMap
.get(boundary)
?.callbacks.filter((c) => c.deps.includes(acceptedVia)) ?? [],
}),
)

for (const id of update.updateSet) {
this.runtime.removeModuleCache(id)
}

for (const { boundary, acceptedVia, callbacks } of applies) {
this.runtime.initModule(acceptedVia)
const fresh = this.runtime.loadExports(acceptedVia)
for (const {
boundary,
acceptedVia,
isWithinCircularImport,
callbacks,
} of applies) {
let fresh: unknown
try {
this.runtime.initModule(acceptedVia)
fresh = this.runtime.loadExports(acceptedVia)
} catch (err) {
if (isWithinCircularImport) {
this.requestFullReload(
`${acceptedVia} failed to apply HMR as it's within a circular import. Reloading page to reset the execution order.`,
)
return
}
throw err
}
try {
this.currentFirstInvalidatedBy = firstInvalidatedBy
for (const { deps, fn } of callbacks) {
Expand All @@ -284,16 +391,19 @@ export class BundledDevHMRClient extends HMRClient {
}

private toUpdatePayload(
boundaries: [string, string][],
boundaries: PropagationBoundary[],
firstInvalidatedBy: string | undefined,
): UpdatePayload {
const updates: Update[] = boundaries.map(([boundary, acceptedVia]) => ({
type: 'js-update',
path: boundary,
acceptedPath: acceptedVia,
timestamp: Date.now(),
firstInvalidatedBy,
}))
const updates: Update[] = boundaries.map(
({ boundary, acceptedVia, isWithinCircularImport }) => ({
type: 'js-update',
path: boundary,
acceptedPath: acceptedVia,
timestamp: Date.now(),
isWithinCircularImport,
firstInvalidatedBy,
}),
)
return { type: 'update', updates }
}

Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/optimizer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1469,7 +1469,7 @@ export async function cleanupDepsCacheStaleDirs(

// The ISC License
// Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors
// https://github.com/isaacs/node-graceful-fs/blob/main/LICENSE
// https://github.com/isaacs/node-graceful-fs/blob/234379906b7d2f4c9cfeb412d2516f42b0fb4953/LICENSE

// On Windows, A/V software can lock the directory, causing this
// to fail with an EACCES or EPERM if the directory contains newly
Expand Down
12 changes: 11 additions & 1 deletion packages/vite/src/node/server/middlewares/triggerLazyBundling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,17 @@ export function triggerLazyBundlingMiddleware(

const moduleId = params.get('id')
const clientId = params.get('clientId')
const result = await bundledDev.triggerLazyBundling(moduleId, clientId)
let result: { code: string; filename: string } | undefined
try {
result = await bundledDev.triggerLazyBundling(moduleId, clientId)
} catch (e) {
server.config.logger.error(
`Failed to trigger lazy bundling for ${moduleId} (clientId: ${clientId}):` +
e,
{ error: e },
)
return next(new Error(`Failed to trigger lazy bundling`))
}
if (result == null) {
return next()
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/types/alias.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
Types from https://github.com/rollup/plugins/blob/master/packages/alias/types/index.d.ts
Types from https://github.com/rollup/plugins/blob/4e85ed78cd2e941107fdf0e8e118e7bee550109d/packages/alias/types/index.d.ts
Inlined because the plugin is bundled.
https://github.com/rollup/plugins/blob/master/LICENSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,13 @@ if (isBuild) {
.toBe('worker-plain-updated')
})

test('lazy bundling errors return 500', async () => {
const response = await page.request.get(
new URL('/@vite/lazy?id=%2Ffoo%2Fbar&clientId=x', page.url()).href,
)
expect(response.status()).toBe(500)
})

// Blocked by https://github.com/rolldown/rolldown/issues/10340
test.skip('chained invalidate in an import cycle settles', async () => {
const original = readFile('cycle-a.js')
Expand Down
Loading
Loading