-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix(docs-theme): preserve/ensure descending version order in sidebar … #4871
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import { extractKeysFromSource } from '../meta-loader.js' | ||
|
|
||
| describe('meta-loader', () => { | ||
| describe('extractKeysFromSource', () => { | ||
| it('should extract keys in source order for numeric string keys', () => { | ||
| const source = `export default { | ||
| '1140': '11.4.0', | ||
| '1130': '11.3.0', | ||
| '1120': '11.2.0', | ||
| '1110-1112': '11.1.0 ~ 11.1.2', | ||
| '1100': '11.0.0', | ||
| '9190': '9.19.0', | ||
| }` | ||
| const keys = extractKeysFromSource(source) | ||
| expect(keys).toEqual(['1140', '1130', '1120', '1110-1112', '1100', '9190']) | ||
| }) | ||
|
|
||
| it('should extract keys with double quotes', () => { | ||
| const source = `export default { | ||
| "foo": "Foo", | ||
| "bar": "Bar", | ||
| "baz": "Baz", | ||
| }` | ||
| const keys = extractKeysFromSource(source) | ||
| expect(keys).toEqual(['foo', 'bar', 'baz']) | ||
| }) | ||
|
|
||
| it('should extract unquoted keys', () => { | ||
| const source = `export default { | ||
| intro: 'Introduction', | ||
| getting_started: 'Getting Started', | ||
| advanced: 'Advanced', | ||
| }` | ||
| const keys = extractKeysFromSource(source) | ||
| expect(keys).toEqual(['intro', 'getting_started', 'advanced']) | ||
| }) | ||
|
|
||
| it('should handle mixed quote styles', () => { | ||
| const source = `export default { | ||
| 'single': 'Single Quotes', | ||
| "double": "Double Quotes", | ||
| unquoted: 'Unquoted Key', | ||
| }` | ||
| const keys = extractKeysFromSource(source) | ||
| expect(keys).toEqual(['single', 'double', 'unquoted']) | ||
| }) | ||
|
|
||
| it('should handle complex _meta with nested objects', () => { | ||
| const source = `export default { | ||
| '---': { | ||
| type: 'separator' | ||
| }, | ||
| qux: '', | ||
| nextra: { | ||
| title: 'Nextra', | ||
| href: 'https://nextra.site' | ||
| } | ||
| }` | ||
| const keys = extractKeysFromSource(source) | ||
| expect(keys).toEqual(['---', 'qux', 'nextra']) | ||
| }) | ||
|
|
||
| it('should return empty array for non-export-default source', () => { | ||
| const source = `const foo = { bar: 'baz' }` | ||
| const keys = extractKeysFromSource(source) | ||
| expect(keys).toEqual([]) | ||
| }) | ||
|
|
||
| it('should handle the querypie example from issue #4834', () => { | ||
| const source = `export default { | ||
| '1140': '11.4.0', | ||
| '1130': '11.3.0', | ||
| '1120': '11.2.0', | ||
| '1110-1112': '11.1.0 ~ 11.1.2', | ||
| '1100': '11.0.0', | ||
| '1030-1034': '10.3.0 ~ 10.3.4', | ||
| '1020-10212': '10.2.0 ~ 10.2.12', | ||
| '1010-10111': '10.1.0 ~ 10.1.11', | ||
| '1000-1002': '10.0.0 ~ 10.0.2', | ||
| '9200-9202': '9.20.0 ~ 9.20.2', | ||
| '9190': '9.19.0 ', | ||
| '9180-9183': '9.18.0 ~ 9.18.3', | ||
| }` | ||
| const keys = extractKeysFromSource(source) | ||
| // These should be in the EXACT order they appear in source, | ||
| // NOT reordered by JavaScript's numeric string key sorting | ||
| expect(keys).toEqual([ | ||
| '1140', | ||
| '1130', | ||
| '1120', | ||
| '1110-1112', | ||
| '1100', | ||
| '1030-1034', | ||
| '1020-10212', | ||
| '1010-10111', | ||
| '1000-1002', | ||
| '9200-9202', | ||
| '9190', | ||
| '9180-9183' | ||
| ]) | ||
| }) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,95 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { LoaderContext } from 'webpack' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * A webpack loader that transforms _meta files to preserve key order. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * JavaScript reorders numeric string keys in objects (e.g., '1140', '1130' become '1130', '1140'). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * This loader parses the source to extract the original key order and injects it as `__order__`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @see https://github.com/shuding/nextra/issues/4834 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Regex to match object property keys in the source | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Handles: 'key', "key", key:, 'key':, "key": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const KEY_REGEX = /(?:^|[,{]\s*)(?:'([^']+)'|"([^"]+)"|(\w[\w-]*))(?:\s*:)/gm | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const KEY_REGEX = /(?:^|[,{]\s*)(?:'([^']+)'|"([^"]+)"|(\w[\w-]*))(?:\s*:)/gm |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The value assigned to currentPos here is unused.
| let currentPos = 0 | |
| for (let i = 0; i < objectContent.length; i++) { | |
| const char = objectContent[i] | |
| if (char === '{') { | |
| depth++ | |
| if (depth === 1) { | |
| currentPos = i + 1 | |
| } | |
| for (let i = 0; i < objectContent.length; i++) { | |
| const char = objectContent[i] | |
| if (char === '{') { | |
| depth++ | |
| // No need to track currentPos | |
| // if (depth === 1) { | |
| // currentPos = i + 1 | |
| // } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded list of property names to skip (type, items, title, href, display, theme) is fragile and could miss nested properties with the same names. Consider using a depth counter or a more robust approach to distinguish between top-level keys and nested object properties. If a top-level key happens to be named "type" or "title", it would incorrectly be skipped.
| if (unquotedMatch && !remaining.startsWith('type:') && !remaining.startsWith('items:') && !remaining.startsWith('title:') && !remaining.startsWith('href:') && !remaining.startsWith('display:') && !remaining.startsWith('theme:')) { | |
| // Skip common nested property names | |
| if (unquotedMatch) { |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The extraction logic uses a simple character-by-character loop without handling string literals properly. If a key contains escaped quotes or the object value contains nested objects with colons, the depth tracking could become incorrect, potentially extracting wrong keys or missing legitimate top-level keys.
| for (let i = 0; i < objectContent.length; i++) { | |
| const char = objectContent[i] | |
| if (char === '{') { | |
| depth++ | |
| if (depth === 1) { | |
| currentPos = i + 1 | |
| } | |
| } else if (char === '}') { | |
| depth-- | |
| } else if (depth === 1) { | |
| // Only process at the first brace level | |
| // Check if we're at the start of a key | |
| const remaining = objectContent.slice(i) | |
| // Match quoted key: 'key' or "key" | |
| const quotedMatch = remaining.match(/^(['"])([^'"]+)\1\s*:/) | |
| if (quotedMatch) { | |
| keys.push(quotedMatch[2]!) | |
| i += quotedMatch[0].length - 1 | |
| continue | |
| } | |
| // Match unquoted key: key: | |
| const unquotedMatch = remaining.match(/^([\w][\w-]*)\s*:/) | |
| if (unquotedMatch && !remaining.startsWith('type:') && !remaining.startsWith('items:') && !remaining.startsWith('title:') && !remaining.startsWith('href:') && !remaining.startsWith('display:') && !remaining.startsWith('theme:')) { | |
| // Skip common nested property names | |
| keys.push(unquotedMatch[1]!) | |
| i += unquotedMatch[0].length - 1 | |
| continue | |
| let inString: false | "'" | '"' = false; | |
| let escape = false; | |
| for (let i = 0; i < objectContent.length; i++) { | |
| const char = objectContent[i]; | |
| if (inString) { | |
| if (escape) { | |
| escape = false; | |
| } else if (char === '\\') { | |
| escape = true; | |
| } else if (char === inString) { | |
| inString = false; | |
| } | |
| continue; | |
| } else { | |
| if (char === "'" || char === '"') { | |
| inString = char; | |
| continue; | |
| } | |
| } | |
| if (char === '{') { | |
| depth++; | |
| if (depth === 1) { | |
| currentPos = i + 1; | |
| } | |
| } else if (char === '}') { | |
| depth--; | |
| } else if (depth === 1) { | |
| // Only process at the first brace level | |
| // Check if we're at the start of a key | |
| const remaining = objectContent.slice(i); | |
| // Match quoted key: 'key' or "key" | |
| const quotedMatch = remaining.match(/^(['"])((?:\\.|[^\\'"])+)\1\s*:/); | |
| if (quotedMatch) { | |
| // Unescape the key | |
| let key = quotedMatch[2].replace(/\\(['"])/g, '$1'); | |
| keys.push(key); | |
| i += quotedMatch[0].length - 1; | |
| continue; | |
| } | |
| // Match unquoted key: key: | |
| const unquotedMatch = remaining.match(/^([\w][\w-]*)\s*:/); | |
| if (unquotedMatch && !remaining.startsWith('type:') && !remaining.startsWith('items:') && !remaining.startsWith('title:') && !remaining.startsWith('href:') && !remaining.startsWith('display:') && !remaining.startsWith('theme:')) { | |
| // Skip common nested property names | |
| keys.push(unquotedMatch[1]!); | |
| i += unquotedMatch[0].length - 1; | |
| continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex pattern doesn't handle template literals or computed property names (e.g.,
[key]: valueor`${version}`: 'title'). If _meta files use these ES6 features, the key extraction will fail silently, leading to incorrect ordering.