mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
### What? Fixes the `errors.json` git merge driver (`scripts/merge-errors-json/merge.mjs`) so that it allocates new error ids based on the largest existing numeric key rather than the entry count. When the current file has gaps in its numeric keys (e.g. retired error codes), the previous logic produced ids that collided with existing entries and silently overwrote them during a rebase/merge. ### Why? The driver computed the next id with: ```js let nextKey = Object.keys(result).length + 1 ``` This only works when ids are dense (`1..N` with no holes). `packages/next/errors.json` has accumulated gaps over time — for example missing ids around 1232–1234 and 1246 on `canary`. When a branch was rebased onto `canary` and the driver replayed a commit that added new entries, the computed `nextKey` (`length + 1`) pointed at an id that was already in use higher up in the map. The replayed entries then clobbered existing canary messages (e.g. "Route encountered %s during the initial render" and "in a Client Component" at 1244/1245), corrupting `errors.json` without raising a conflict. Because the driver writes the result directly and exits 0, this kind of corruption is invisible at rebase time and only surfaces later when the displaced messages start producing wrong/duplicate codes at runtime. ### How? - Introduce a `getMaxNumericKey(map)` helper that scans the keys of the merged map, parses each as an integer, and returns the largest value (or `0` if there are no numeric keys). - Use `getMaxNumericKey(result) + 1` as the starting `nextKey` in `mergeErrors`, so newly added messages are always appended above the highest currently-used id, regardless of gaps. Behavior for the dense / no-gap case is unchanged: `max + 1 === length + 1` when ids are `1..N` contiguous, so all three existing scenarios in `test/scripts/merge-errors-json/merge-errors-json.test.ts` (basic conflict, empty base, stacked PRs) continue to pass. No changes to `errors.json` itself are included here; this PR only fixes the driver going forward. <!-- NEXT_JS_LLM_PR --> Co-authored-by: v-work-app[bot] <262237222+v-work-app[bot]@users.noreply.github.com> Co-authored-by: Claude <[email protected]> Co-authored-by: Tobias Koppers <[email protected]>
139 lines
3.8 KiB
JavaScript
139 lines
3.8 KiB
JavaScript
// @ts-check
|
|
|
|
/**
|
|
* Git merge driver for packages/next/errors.json
|
|
*
|
|
* This script automatically resolves merge conflicts in the auto-generated
|
|
* errors.json file by reassigning error codes to avoid conflicts.
|
|
*
|
|
* Usage: node merge-errors-json.mjs <current> <base> <other> [<marker-size>]
|
|
*
|
|
* Arguments:
|
|
* - current: Path to the current version (our changes)
|
|
* - base: Path to the common ancestor version
|
|
* - other: Path to the other version (their changes)
|
|
* - marker-size: Size of conflict markers (optional, defaults to 7)
|
|
*
|
|
* Exit codes:
|
|
* - 0: Merge successful, result written to current file
|
|
* - 1: Merge failed, conflicts remain
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
|
|
function main() {
|
|
const args = process.argv.slice(2)
|
|
|
|
if (args.length < 3) {
|
|
console.error(
|
|
'Usage: node merge-errors-json.mjs <current> <base> <other> [<marker-size>]'
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
const [currentPath, basePath, otherPath] = args
|
|
|
|
try {
|
|
const base = readJsonSync(basePath)
|
|
const current = readJsonSync(currentPath)
|
|
const other = readJsonSync(otherPath)
|
|
|
|
const merged = mergeErrors(base, current, other)
|
|
|
|
// Git expects the result to be written to the "current" file
|
|
writeJsonSync(currentPath, merged)
|
|
|
|
const addedCount = Object.keys(merged).length - Object.keys(current).length
|
|
console.error(
|
|
`merge-errors-json: added ${addedCount === 1 ? '1 new message' : `${addedCount} new messages`} to errors.json`
|
|
)
|
|
process.exit(0)
|
|
} catch (error) {
|
|
console.error('merge-errors-json: merge failed:', error.message)
|
|
console.error()
|
|
console.error(
|
|
[
|
|
'if this error persists, you can disable the merge driver by running',
|
|
'',
|
|
' scripts/merge-errors-json/uninstall',
|
|
'',
|
|
'or by manually removing the `[merge "errors-json"]` section from your .git/config.',
|
|
].join('\n')
|
|
)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @typedef {Record<string, string>} ErrorsMap
|
|
*/
|
|
|
|
/**
|
|
* Merge three versions of errors.json, resolving conflicts by assigning new sequential IDs
|
|
* @param {ErrorsMap} base - Base version (common ancestor)
|
|
* @param {ErrorsMap} current - Current version (our changes, or the state of the branch we're rebasing onto)
|
|
* @param {ErrorsMap} other - Other version (their changes, or the state the branch we're rebasing, a.k.a. "incoming")
|
|
* @returns {ErrorsMap}
|
|
*/
|
|
function mergeErrors(base, current, other) {
|
|
/** @type {ErrorsMap} */
|
|
const result = { ...current }
|
|
|
|
/** @type {Set<string>} */
|
|
const existingMessages = new Set(Object.values(result))
|
|
|
|
let nextKey = getMaxNumericKey(result) + 1
|
|
|
|
for (const message of getNewMessages(base, other)) {
|
|
if (existingMessages.has(message)) {
|
|
continue
|
|
}
|
|
|
|
const key = nextKey++
|
|
result[key] = message
|
|
existingMessages.add(message)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Returns the largest numeric key in the map, or 0 if there are no numeric
|
|
* keys. Existing entries may have gaps (e.g. when error codes were retired),
|
|
* so we can't rely on `Object.keys(result).length` to derive the next id.
|
|
* @param {ErrorsMap} map
|
|
*/
|
|
function getMaxNumericKey(map) {
|
|
let max = 0
|
|
for (const key of Object.keys(map)) {
|
|
const n = Number(key)
|
|
if (Number.isInteger(n) && n > max) {
|
|
max = n
|
|
}
|
|
}
|
|
return max
|
|
}
|
|
|
|
function getNewMessages(
|
|
/** @type {ErrorsMap} */ prev,
|
|
/** @type {ErrorsMap} */ current
|
|
) {
|
|
const existing = new Set(Object.values(prev))
|
|
return Object.values(current).filter((msg) => !existing.has(msg))
|
|
}
|
|
|
|
function readJsonSync(/** @type {string} */ filePath) {
|
|
const content = readFileSync(filePath, 'utf8')
|
|
return JSON.parse(content)
|
|
}
|
|
|
|
function writeJsonSync(
|
|
/** @type {string} */ filePath,
|
|
/** @type {any} */ value
|
|
) {
|
|
const content = JSON.stringify(value, null, 2) + '\n'
|
|
writeFileSync(filePath, content, 'utf8')
|
|
}
|
|
|
|
main()
|