Skip to content

Commit 61a7095

Browse files
authored
fix: ensure importMap is reliably generated during HMR (fixes Next.js 16 issue) (#14474)
Fixes #14419 ## Problem Next.js 16 changes the order of `getPayload()` calls during HMR. In testing, the frontend now calls `getPayload()` before the admin panel does. Since the frontend typically has `skipImportMapGeneration: true` (due to no importMap passed to `getPayload`), this was skipping import map generation entirely, breaking the admin panel. ## Solution This PR ensures the import map is always regenerated during HMR by passing `false` as the second parameter to `reload()`, regardless of the `options.importMap` value passed to `getPayload()`. Additionally, a new `ignoreResolveError` option is added to handle cases where `getPayload()` is called exclusively from the frontend (where no import map file exists). This prevents errors from interrupting the HMR process in frontend-only scenarios.
1 parent c0de75e commit 61a7095

File tree

3 files changed

+36
-6
lines changed

3 files changed

+36
-6
lines changed

packages/payload/src/bin/generateImportMap/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,14 @@ export type AddToImportMap = (payloadComponent?: PayloadComponent | PayloadCompo
4242

4343
export async function generateImportMap(
4444
config: SanitizedConfig,
45-
options?: { force?: boolean; log: boolean },
45+
options?: {
46+
force?: boolean /**
47+
* If true, will not throw an error if the import map file path cannot be resolved
48+
Instead, it will return silently.
49+
*/
50+
ignoreResolveError?: boolean
51+
log: boolean
52+
},
4653
): Promise<void> {
4754
const shouldLog = options?.log ?? true
4855

@@ -64,6 +71,14 @@ export async function generateImportMap(
6471
rootDir,
6572
})
6673

74+
if (importMapFilePath instanceof Error) {
75+
if (options?.ignoreResolveError) {
76+
return
77+
} else {
78+
throw importMapFilePath
79+
}
80+
}
81+
6782
const importMapToBaseDirPath = getImportMapToBaseDirPath({
6883
baseDir,
6984
importMapPath: importMapFilePath,

packages/payload/src/bin/generateImportMap/utilities/resolveImportMapFilePath.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ export async function resolveImportMapFilePath({
2121
adminRoute?: string
2222
importMapFile?: string
2323
rootDir: string
24-
}) {
24+
}): Promise<Error | string> {
2525
let importMapFilePath: string | undefined = undefined
2626

2727
if (importMapFile?.length) {
2828
if (!(await pathOrFileExists(importMapFile))) {
2929
try {
3030
await fs.writeFile(importMapFile, '', { flag: 'wx' })
3131
} catch (err) {
32-
throw new Error(
32+
return new Error(
3333
`Could not find the import map file at ${importMapFile}${err instanceof Error && err?.message ? `: ${err.message}` : ''}`,
3434
)
3535
}
@@ -50,7 +50,7 @@ export async function resolveImportMapFilePath({
5050
await fs.writeFile(importMapFilePath, '', { flag: 'wx' })
5151
}
5252
} else {
53-
throw new Error(
53+
return new Error(
5454
`Could not find Payload import map folder. Looked in ${appLocation} and ${srcAppLocation}`,
5555
)
5656
}

packages/payload/src/index.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,9 +1000,13 @@ export const reload = async (
10001000
})
10011001
}
10021002

1003-
// Generate component map
1003+
// Generate import map
10041004
if (skipImportMapGeneration !== true && config.admin?.importMap?.autoGenerate !== false) {
1005+
// This may run outside of the admin panel, e.g. in the user's frontend, where we don't have an import map file.
1006+
// We don't want to throw an error in this case, as it would break the user's frontend.
1007+
// => just skip it => ignoreResolveError: true
10051008
await generateImportMap(config, {
1009+
ignoreResolveError: true,
10061010
log: true,
10071011
})
10081012
}
@@ -1098,7 +1102,18 @@ export const getPayload = async (
10981102
// will reach `if (cached.reload instanceof Promise) {` which then waits for the first reload to finish.
10991103
cached.reload = new Promise((res) => (resolve = res))
11001104
const config = await options.config
1101-
await reload(config, cached.payload, !options.importMap, options)
1105+
1106+
// Reload the payload instance after a config change (triggered by HMR in development).
1107+
// The second parameter (false) forces import map regeneration rather than deciding based on options.importMap.
1108+
//
1109+
// Why we always regenerate import map: getPayload() may be called from multiple sources (admin panel, frontend, etc.)
1110+
// that share the same cache but may pass different importMap values. Since call order is unpredictable,
1111+
// we cannot rely on options.importMap to determine if regeneration is needed.
1112+
//
1113+
// Example scenario: If the frontend calls getPayload() without importMap first, followed by the admin
1114+
// panel calling it with importMap, we'd incorrectly skip generation for the admin panel's needs.
1115+
// By always regenerating on reload, we ensure the import map stays in sync with the updated config.
1116+
await reload(config, cached.payload, false, options)
11021117

11031118
resolve()
11041119
}

0 commit comments

Comments
 (0)