From a92124248519b0266821ac73760bf29ccb12386e Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 14 Nov 2024 20:48:11 +0100 Subject: [PATCH 01/54] chore(scripts): add flat-diff --- package-lock.json | 27 ++++--- package.json | 2 + scripts/diff-flat.ts | 184 +++++++++++++++++++++++++++++++++++++++++++ scripts/lib/git.ts | 13 ++- 4 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 scripts/diff-flat.ts diff --git a/package-lock.json b/package-lock.json index 39c4975a76ccc3..986546a797049e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "cli-progress": "^3.12.0", "compare-versions": "~6.1.0", "deep-diff": "~1.0.2", + "diff": "^7.0.0", "es-main": "~1.3.0", "eslint": "~8.57.0", "eslint-config-standard": "~17.1.0", @@ -3107,10 +3108,11 @@ } }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -6581,6 +6583,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/mocha/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/mocha/node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -8070,15 +8082,6 @@ "url": "https://opencollective.com/sinon" } }, - "node_modules/sinon/node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index d88a1e29ff84de..1150d7db123215 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "cli-progress": "^3.12.0", "compare-versions": "~6.1.0", "deep-diff": "~1.0.2", + "diff": "^7.0.0", "es-main": "~1.3.0", "eslint": "~8.57.0", "eslint-config-standard": "~17.1.0", @@ -99,6 +100,7 @@ "save-git-history": "node --loader=ts-node/esm --no-warnings=ExperimentalWarning scripts/save-git-history.ts", "prepare": "(husky || true) && npm run gentypes && npm run build", "diff": "node --loader=ts-node/esm --no-warnings=ExperimentalWarning scripts/diff.ts", + "diff:flat": "node --loader=ts-node/esm --no-warnings=ExperimentalWarning scripts/diff-flat.ts", "unittest": "NODE_ENV=test c8 mocha index.test.ts --recursive \"{,!(node_modules)/**}/*.test.ts\"", "coverage": "c8 report -r lcov && open-cli coverage/lcov-report/index.html", "format": "eslint . && prettier --check . && tsc --noEmit", diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts new file mode 100644 index 00000000000000..eb07e24e1f0bae --- /dev/null +++ b/scripts/diff-flat.ts @@ -0,0 +1,184 @@ +/* This file is a part of @mdn/browser-compat-data + * See LICENSE file for more information. */ + +import chalk from 'chalk-template'; +import { diffArrays, diffWords } from 'diff'; +import esMain from 'es-main'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +import { getMergeBase, getFileContent, getGitDiffStatuses } from './lib/git.js'; + +interface Contents { + base: string; + head: string; +} + +/** + * Get contents from base and head commits + * Note: This does not detect renamed files + * @param baseCommit Base commit + * @param basePath Base path + * @param headCommit Head commit + * @param headPath Head path + * @returns The contents of both commits + */ +const getBaseAndHeadContents = ( + baseCommit: string, + basePath: string, + headCommit: string, + headPath: string, +): Contents => { + const base = JSON.parse(getFileContent(baseCommit, basePath)); + const head = JSON.parse(getFileContent(headCommit, headPath)); + return { base, head }; +}; + +/** + * Flattens an object. + * @param obj the object to flatten. + * @param parentKey the parent key path. + * @param result the intermediate result. + * @returns the flattened object. + */ +const flattenObject = ( + obj: any, + parentKey = '', + result = {}, +): Record => { + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const fullKey = parentKey ? `${parentKey}.${key}` : key; + + if ( + typeof obj[key] === 'object' && + obj[key] !== null && + !Array.isArray(obj[key]) + ) { + // Recursively flatten nested objects + flattenObject(obj[key], fullKey, result); + } else { + // Assign value to the flattened key + result[fullKey] = obj[key]; + } + } + } + + return result; +}; + +/** + * Print diffs + * @param base Base ref + * @param head Head ref + * @param options Options + */ +const printDiffs = ( + base: string, + head = '', + options: { html: boolean }, +): void => { + for (const status of getGitDiffStatuses(base, head)) { + if (!status.headPath.endsWith('.json') || !status.headPath.includes('/')) { + continue; + } + + // Note that A means Added for git while it means Array for deep-diff + if (status.value === 'A') { + // TODO + } else if (status.value === 'D') { + // TODO + } else { + const contents = getBaseAndHeadContents( + base, + status.basePath, + head, + status.headPath, + ); + const baseData = flattenObject(contents.base); + const headData = flattenObject(contents.head); + + const keys = [ + ...new Set([ + ...Object.keys(baseData), + ...Object.keys(headData), + ]).values(), + ].sort(); + + if (!keys.length) { + continue; + } + + const prefix = diffArrays( + keys.at(0)?.split('.') ?? [], + keys.at(-1)?.split('.') ?? [], + )[0]?.value.join('.'); + console.log(options.html ? `

${prefix}

` : `${prefix}`); + + let lastKey = keys.at(0) ?? ''; + + for (const key of keys) { + const baseValue = JSON.stringify(baseData[key] ?? null); + const headValue = JSON.stringify(headData[key] ?? null); + if (baseValue === headValue) { + continue; + } + const keyDiff = diffArrays( + lastKey.slice(prefix.length).split('.'), + key.slice(prefix.length).split('.'), + ) + .filter((part) => !part.removed) + .map((part) => { + const key = part.value.join('.'); + + if (part.added) { + return options.html + ? `${key}` + : chalk`{bold ${key}}`; + } + + return key; + }) + .join('.'); + + console.log( + options.html + ? `${keyDiff} = ${baseValue}${headValue}
` + : chalk`${keyDiff} = {red ${baseValue}} → {green ${headValue}}`, + ); + lastKey = key; + } + + console.log(''); + } + } +}; + +if (esMain(import.meta)) { + const { argv } = yargs(hideBin(process.argv)).command( + '$0 [base] [head]', + 'Print a formatted diff for changes between base and head commits', + (yargs) => { + yargs + .positional('base', { + describe: + 'The base commit; may be commit hash or other git ref (e.g. "origin/main")', + type: 'string', + default: 'origin/main', + }) + .positional('head', { + describe: + 'The head commit that changes are applied to; may be commit hash or other git ref (e.g. "origin/main")', + type: 'string', + default: 'HEAD', + }) + .option('html', { + type: 'boolean', + default: false, + }); + }, + ); + + const { base, head, html } = argv as any; + printDiffs(getMergeBase(base, head), head, { html }); +} diff --git a/scripts/lib/git.ts b/scripts/lib/git.ts index e95aac25808bba..d9b818296ab74f 100644 --- a/scripts/lib/git.ts +++ b/scripts/lib/git.ts @@ -37,13 +37,20 @@ const parseFields = (fields: string[]): Fields => ({ * @param head The second git refs * @returns The diff statuses */ -const getGitDiffStatuses = (base: string, head: string): Fields[] => - child_process +const getGitDiffStatuses = (base: string, head: string): Fields[] => { + const stdout = child_process .execSync(`git diff --name-status ${base} ${head}`, { encoding: 'utf-8' }) - .trim() + .trim(); + + if (!stdout) { + return []; + } + + return stdout .split('\n') .map((line) => line.split('\t')) .map(parseFields); +}; /** * Get file contents from a specific commit and file path From b503accf7309dd23512322c81ca42aebaf78e234 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 15 Nov 2024 23:26:08 +0100 Subject: [PATCH 02/54] chore(flat-diff): flatten arrays --- scripts/diff-flat.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index eb07e24e1f0bae..7b4fb64971e481 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -50,11 +50,7 @@ const flattenObject = ( if (Object.prototype.hasOwnProperty.call(obj, key)) { const fullKey = parentKey ? `${parentKey}.${key}` : key; - if ( - typeof obj[key] === 'object' && - obj[key] !== null && - !Array.isArray(obj[key]) - ) { + if (typeof obj[key] === 'object' && obj[key] !== null) { // Recursively flatten nested objects flattenObject(obj[key], fullKey, result); } else { From 0f315ca448dc47a08a613fcea2a265e619a9231a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 15 Nov 2024 23:26:29 +0100 Subject: [PATCH 03/54] chore(flat-diff): convert notes to array --- scripts/diff-flat.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 7b4fb64971e481..50bcaae113041d 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -52,7 +52,11 @@ const flattenObject = ( if (typeof obj[key] === 'object' && obj[key] !== null) { // Recursively flatten nested objects - flattenObject(obj[key], fullKey, result); + flattenObject( + key === 'notes' && !Array.isArray(obj[key]) ? [obj[key]] : obj[key], + fullKey, + result, + ); } else { // Assign value to the flattened key result[fullKey] = obj[key]; From 3e7c0080870f8dcf4e787675980afc72a7407a5a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 15 Nov 2024 23:27:46 +0100 Subject: [PATCH 04/54] chore(flat-diff): improve HTML rendering --- scripts/diff-flat.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 50bcaae113041d..d9de6825da2d46 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -78,6 +78,10 @@ const printDiffs = ( head = '', options: { html: boolean }, ): void => { + if (options.html) { + console.log('
'); + } + for (const status of getGitDiffStatuses(base, head)) { if (!status.headPath.endsWith('.json') || !status.headPath.includes('/')) { continue; @@ -141,10 +145,23 @@ const printDiffs = ( }) .join('.'); + const value = [ + (baseData[key] ?? null) && + (options.html + ? `${baseValue}` + : chalk`{red ${baseValue}}`), + (headData[key] ?? null) && + (options.html + ? `${headValue}` + : chalk`{green ${headValue}}`), + ] + .filter(Boolean) + .join(' → '); + console.log( options.html - ? `${keyDiff} = ${baseValue}${headValue}
` - : chalk`${keyDiff} = {red ${baseValue}} → {green ${headValue}}`, + ? `${keyDiff} = ${value}
` + : chalk`${keyDiff} = ${value}`, ); lastKey = key; } @@ -152,6 +169,10 @@ const printDiffs = ( console.log(''); } } + + if (options.html) { + console.log('
'); + } }; if (esMain(import.meta)) { From d86cd734c2bf6a43f0650fe1766688562204605a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 15 Nov 2024 23:37:29 +0100 Subject: [PATCH 05/54] fix(diff-flat): show booleans --- scripts/diff-flat.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index d9de6825da2d46..038cfae0797503 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -145,12 +145,14 @@ const printDiffs = ( }) .join('.'); + const hasValue = (value: any) => typeof value === 'boolean' || !!value; + const value = [ - (baseData[key] ?? null) && + hasValue(baseData[key] ?? null) && (options.html ? `${baseValue}` : chalk`{red ${baseValue}}`), - (headData[key] ?? null) && + hasValue(headData[key] ?? null) && (options.html ? `${headValue}` : chalk`{green ${headValue}}`), From 871bcbc89dc2756614a5803aaccafc22d120006a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Fri, 15 Nov 2024 23:47:23 +0100 Subject: [PATCH 06/54] enhance(flat-diff): hide new mirror --- scripts/diff-flat.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 038cfae0797503..70d1c683cd218e 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -145,7 +145,8 @@ const printDiffs = ( }) .join('.'); - const hasValue = (value: any) => typeof value === 'boolean' || !!value; + const hasValue = (value: any) => + typeof value === 'boolean' || (!!value && value !== 'mirror'); const value = [ hasValue(baseData[key] ?? null) && @@ -160,6 +161,11 @@ const printDiffs = ( .filter(Boolean) .join(' → '); + if (!value.length) { + // e.g. null => "mirror" + continue; + } + console.log( options.html ? `${keyDiff} = ${value}
` From 777eacfe5dd9408544f359a350cf66bf6cb831de Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 18 Nov 2024 13:18:23 +0100 Subject: [PATCH 07/54] enhance(flat-diff): arrayify support statements --- scripts/diff-flat.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 70d1c683cd218e..7164c29d9c19c0 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -53,7 +53,19 @@ const flattenObject = ( if (typeof obj[key] === 'object' && obj[key] !== null) { // Recursively flatten nested objects flattenObject( - key === 'notes' && !Array.isArray(obj[key]) ? [obj[key]] : obj[key], + [ + 'chrome', + 'chrome_android', + 'edge', + 'firefox', + 'safari', + 'safari_ios', + 'webview_android', + ].includes(key) + ? toArray(obj[key]).reverse() + : key === 'notes' + ? toArray(obj[key]) + : obj[key], fullKey, result, ); @@ -67,6 +79,14 @@ const flattenObject = ( return result; }; +const toArray = (value: any): any[] => { + if (!Array.isArray(value)) { + value = [value]; + } + + return value; +}; + /** * Print diffs * @param base Base ref From 37cc7fbb77a5803b3b9bdbba0e19c2de4fa9a104 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 18 Nov 2024 13:36:32 +0100 Subject: [PATCH 08/54] enhance(flat-diff): stringify flags --- scripts/diff-flat.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 7164c29d9c19c0..7dd47ac97b9882 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -50,7 +50,11 @@ const flattenObject = ( if (Object.prototype.hasOwnProperty.call(obj, key)) { const fullKey = parentKey ? `${parentKey}.${key}` : key; - if (typeof obj[key] === 'object' && obj[key] !== null) { + if (key == 'flags') { + result[fullKey] = toArray(obj[key]).map((value) => + JSON.stringify(value), + ); + } else if (typeof obj[key] === 'object' && obj[key] !== null) { // Recursively flatten nested objects flattenObject( [ From f0fbc35084c79c8fe9def4aaa9783c48fc8729b9 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 18 Nov 2024 19:26:03 +0100 Subject: [PATCH 09/54] style: fix lints --- scripts/diff-flat.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 7dd47ac97b9882..981dd395a101c1 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -2,7 +2,7 @@ * See LICENSE file for more information. */ import chalk from 'chalk-template'; -import { diffArrays, diffWords } from 'diff'; +import { diffArrays } from 'diff'; import esMain from 'es-main'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; @@ -83,6 +83,11 @@ const flattenObject = ( return result; }; +/** + * Converts value to array unless it isn't. + * @param value array or any value. + * @returns the array, or an array with the value as a single item. + */ const toArray = (value: any): any[] => { if (!Array.isArray(value)) { value = [value]; @@ -96,6 +101,7 @@ const toArray = (value: any): any[] => { * @param base Base ref * @param head Head ref * @param options Options + * @param options.html */ const printDiffs = ( base: string, @@ -169,6 +175,11 @@ const printDiffs = ( }) .join('.'); + /** + * Checks whether the value is a relevant value. + * @param value the value. + * @returns TRUE if the value is relevant, FALSE otherwise. + */ const hasValue = (value: any) => typeof value === 'boolean' || (!!value && value !== 'mirror'); From cf432c7abd9705daabf4bf2cdce8292208806547 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 18 Nov 2024 20:46:03 +0100 Subject: [PATCH 10/54] enhance(diff-flat): add --group option Groups by change. --- scripts/diff-flat.ts | 156 ++++++++++++++++++++++++++++++++----------- 1 file changed, 118 insertions(+), 38 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 981dd395a101c1..3d34a14b48da5c 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -96,22 +96,51 @@ const toArray = (value: any): any[] => { return value; }; +/** + * Formats a key diff'ed with the previous key. + * @param key the current key + * @param lastKey the previous key + * @param options Options + * @param options.html Whether to return HTML, otherwise plaintext + * @returns diffed key + */ +const diffKeys = ( + lastKey: string, + key: string, + options: { html: boolean }, +): string => + diffArrays(lastKey.split('.'), key.split('.')) + .filter((part) => !part.removed) + .map((part) => { + const key = part.value.join('.'); + + if (part.added) { + return options.html ? `${key}` : chalk`{blue ${key}}`; + } + + return key; + }) + .join('.'); + /** * Print diffs * @param base Base ref * @param head Head ref * @param options Options - * @param options.html + * @param options.group Whether to group by value, rather than the common feature + * @param options.html Whether to output HTML, rather than plain-text */ const printDiffs = ( base: string, head = '', - options: { html: boolean }, + options: { group: boolean; html: boolean }, ): void => { if (options.html) { console.log('
'); } + const groups = new Map>(); + for (const status of getGitDiffStatuses(base, head)) { if (!status.headPath.endsWith('.json') || !status.headPath.includes('/')) { continue; @@ -119,9 +148,9 @@ const printDiffs = ( // Note that A means Added for git while it means Array for deep-diff if (status.value === 'A') { - // TODO + console.warn("diff:flat doesn't support file additions yet!"); } else if (status.value === 'D') { - // TODO + console.warn("diff:flat doesn't support file deletions yet!"); } else { const contents = getBaseAndHeadContents( base, @@ -147,7 +176,8 @@ const printDiffs = ( keys.at(0)?.split('.') ?? [], keys.at(-1)?.split('.') ?? [], )[0]?.value.join('.'); - console.log(options.html ? `

${prefix}

` : `${prefix}`); + + const commonName = options.html ? `

${prefix}

` : `${prefix}`; let lastKey = keys.at(0) ?? ''; @@ -157,23 +187,11 @@ const printDiffs = ( if (baseValue === headValue) { continue; } - const keyDiff = diffArrays( - lastKey.slice(prefix.length).split('.'), - key.slice(prefix.length).split('.'), - ) - .filter((part) => !part.removed) - .map((part) => { - const key = part.value.join('.'); - - if (part.added) { - return options.html - ? `${key}` - : chalk`{bold ${key}}`; - } - - return key; - }) - .join('.'); + const keyDiff = diffKeys( + lastKey.slice(prefix.length), + key.slice(prefix.length), + options, + ); /** * Checks whether the value is a relevant value. @@ -183,33 +201,91 @@ const printDiffs = ( const hasValue = (value: any) => typeof value === 'boolean' || (!!value && value !== 'mirror'); - const value = [ + const oldValue = hasValue(baseData[key] ?? null) && - (options.html - ? `${baseValue}` - : chalk`{red ${baseValue}}`), - hasValue(headData[key] ?? null) && + (options.html + ? `${baseValue}` + : chalk`{red ${baseValue}}`); + const newValue = + (hasValue(headData[key] ?? null) && (options.html ? `${headValue}` - : chalk`{green ${headValue}}`), - ] - .filter(Boolean) - .join(' → '); + : chalk`{green ${headValue}}`)) || + ''; + + const value = [oldValue, newValue].filter(Boolean).join(' → '); if (!value.length) { // e.g. null => "mirror" continue; } - console.log( - options.html + if (options.group) { + const group = groups.get(value) ?? new Set(); + group.add(key); + groups.set(value, group); + } else { + const change = options.html ? `${keyDiff} = ${value}
` - : chalk`${keyDiff} = ${value}`, - ); + : chalk`${keyDiff} = ${value}`; + const group = groups.get(commonName) ?? new Set(); + group.add(change); + groups.set(commonName, group); + } lastKey = key; } + } + } + + const entries: [string, string[]][] = [...groups.entries()].map( + ([key, set]) => [key, [...set.values()]], + ); - console.log(''); + if (options.group) { + entries.sort(([, a], [, b]) => b.length - a.length); + /** + * Reverses a key (e.g. "a.b.c" => "c.b.a"). + * @param key the key to reverse. + * @returns the reversed key. + */ + const reverseKey = (key: string): string => + key.split('.').reverse().join('.'); + entries.forEach((entry) => { + entry[1] = entry[1].map(reverseKey).sort().map(reverseKey); + }); + } + + let previousKey: string | null = null; + for (const entry of entries) { + if (options.group) { + const [value, keys] = entry; + if (keys.length == 1) { + const key = keys.at(0) as string; + const keyDiff = diffKeys(key, previousKey ?? key, options); + console.log(`${keyDiff}: ${value}`); + previousKey = key; + } else { + previousKey = keys.at(0) as string; + for (const key of keys) { + const keyDiff = diffKeys(key, previousKey, options); + console.log(keyDiff); + previousKey = key; + } + console.log(` ${value}`); + console.log(); + previousKey = null; + } + } else { + const [key, values] = entry; + if (values.length == 1) { + const keyDiff = diffKeys(key, previousKey ?? key, options); + console.log(`${keyDiff}: ${values.at(0)}`); + previousKey = key; + } else { + console.log(key); + values.forEach((value) => console.log(` ${value}`)); + console.log(); + } } } @@ -239,10 +315,14 @@ if (esMain(import.meta)) { .option('html', { type: 'boolean', default: false, + }) + .option('group', { + type: 'boolean', + default: false, }); }, ); - const { base, head, html } = argv as any; - printDiffs(getMergeBase(base, head), head, { html }); + const { base, head, html, group } = argv as any; + printDiffs(getMergeBase(base, head), head, { group, html }); } From 86bed0ba06e6d93619491c2d051361382adebcf7 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 18 Nov 2024 21:04:15 +0100 Subject: [PATCH 11/54] fix(flat-diff): group values by browser and field --- scripts/diff-flat.ts | 45 +++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 3d34a14b48da5c..1ac0d97f3d04d8 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -34,6 +34,17 @@ const getBaseAndHeadContents = ( return { base, head }; }; +const BROWSER_NAMES = [ + 'chrome', + 'chrome_android', + 'edge', + 'firefox', + 'firefox_android', + 'safari', + 'safari_ios', + 'webview_android', +]; + /** * Flattens an object. * @param obj the object to flatten. @@ -57,15 +68,7 @@ const flattenObject = ( } else if (typeof obj[key] === 'object' && obj[key] !== null) { // Recursively flatten nested objects flattenObject( - [ - 'chrome', - 'chrome_android', - 'edge', - 'firefox', - 'safari', - 'safari_ios', - 'webview_android', - ].includes(key) + BROWSER_NAMES.includes(key) ? toArray(obj[key]).reverse() : key === 'notes' ? toArray(obj[key]) @@ -105,8 +108,8 @@ const toArray = (value: any): any[] => { * @returns diffed key */ const diffKeys = ( - lastKey: string, key: string, + lastKey: string, options: { html: boolean }, ): string => diffArrays(lastKey.split('.'), key.split('.')) @@ -221,9 +224,25 @@ const printDiffs = ( } if (options.group) { - const group = groups.get(value) ?? new Set(); - group.add(key); - groups.set(value, group); + const reverseKeyParts = key.split('.').reverse(); + const browser = reverseKeyParts.find((part) => + BROWSER_NAMES.includes(part), + ); + const field = reverseKeyParts.find((part) => !/^\d+$/.test(part)); + const groupKey = `${browser ? `[${browser}] ` : ''}${field} = ${value}`; + const groupValue = key + .split('.') + .map((part) => + part !== browser && part !== field + ? part + : options.html + ? '{}' + : chalk`{grey \{\}}`, + ) + .join('.'); + const group = groups.get(groupKey) ?? new Set(); + group.add(groupValue); + groups.set(groupKey, group); } else { const change = options.html ? `${keyDiff} = ${value}
` From 8e6e18178a740ea3d395bfb1dd32628a5e358cd8 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 18 Nov 2024 22:12:23 +0100 Subject: [PATCH 12/54] enhance(diff-flat): align suffix --- scripts/diff-flat.ts | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 1ac0d97f3d04d8..2b859aa431ba91 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -104,26 +104,35 @@ const toArray = (value: any): any[] => { * @param key the current key * @param lastKey the previous key * @param options Options + * @param options.fill The number of characters to fill up to * @param options.html Whether to return HTML, otherwise plaintext * @returns diffed key */ const diffKeys = ( key: string, lastKey: string, - options: { html: boolean }, -): string => - diffArrays(lastKey.split('.'), key.split('.')) + options: { fill?: number; html: boolean }, +): string => { + const len = key.length; + let fill = options.fill ?? 0; + return diffArrays(lastKey.split('.'), key.split('.')) .filter((part) => !part.removed) .map((part) => { const key = part.value.join('.'); if (part.added) { - return options.html ? `${key}` : chalk`{blue ${key}}`; + const space = fill && len < fill ? ' '.repeat(fill - len) : ''; + fill = 0; + return ( + (options.html ? `${key}` : chalk`{blue ${key}}`) + + space + ); } return key; }) .join('.'); +}; /** * Print diffs @@ -229,7 +238,7 @@ const printDiffs = ( BROWSER_NAMES.includes(part), ); const field = reverseKeyParts.find((part) => !/^\d+$/.test(part)); - const groupKey = `${browser ? `[${browser}] ` : ''}${field} = ${value}`; + const groupKey = `${!browser ? '' : options.html ? `${browser} → ` : chalk`{cyan ${browser}} → `}${field} = ${value}`; const groupValue = key .split('.') .map((part) => @@ -237,7 +246,7 @@ const printDiffs = ( ? part : options.html ? '{}' - : chalk`{grey \{\}}`, + : chalk`{dim \{\}}`, ) .join('.'); const group = groups.get(groupKey) ?? new Set(); @@ -281,31 +290,34 @@ const printDiffs = ( if (keys.length == 1) { const key = keys.at(0) as string; const keyDiff = diffKeys(key, previousKey ?? key, options); - console.log(`${keyDiff}: ${value}`); + console.log(`${keyDiff}:\n ${value}`); previousKey = key; } else { - previousKey = keys.at(0) as string; + previousKey = null; + const maxKeyLength = Math.max(...keys.map((key) => key.length)); for (const key of keys) { - const keyDiff = diffKeys(key, previousKey, options); + const keyDiff = diffKeys(key, previousKey ?? (keys.at(1) as string), { + ...options, + fill: maxKeyLength, + }); console.log(keyDiff); previousKey = key; } console.log(` ${value}`); - console.log(); previousKey = null; } } else { const [key, values] = entry; if (values.length == 1) { const keyDiff = diffKeys(key, previousKey ?? key, options); - console.log(`${keyDiff}: ${values.at(0)}`); + console.log(`${keyDiff}:\n ${values.at(0)}`); previousKey = key; } else { console.log(key); values.forEach((value) => console.log(` ${value}`)); - console.log(); } } + console.log(); } if (options.html) { From 3642e183e13972d4d7e13b1762fe9443307e14be Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 18 Nov 2024 22:37:17 +0100 Subject: [PATCH 13/54] enhance(diff-flat): group groups Group keys by values, rather than keys by value. --- scripts/diff-flat.ts | 46 +++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 2b859aa431ba91..2bbe3339ae9041 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -265,12 +265,27 @@ const printDiffs = ( } } - const entries: [string, string[]][] = [...groups.entries()].map( + const originalEntries: [string, string[]][] = [...groups.entries()].map( ([key, set]) => [key, [...set.values()]], ); + const entryGroups = new Map(); + for (const [key, values] of originalEntries) { + const groupKey = JSON.stringify(values); + const keys = entryGroups.get(groupKey) ?? []; + keys.push(key); + entryGroups.set(groupKey, keys); + } + + const entries = [...entryGroups.entries()].map(([valuesJson, keys]) => [ + keys, + JSON.parse(valuesJson), + ]); + if (options.group) { - entries.sort(([, a], [, b]) => b.length - a.length); + entries.sort( + ([k1, v1], [k2, v2]) => k2.length * v2.length - k1.length * v1.length, + ); /** * Reverses a key (e.g. "a.b.c" => "c.b.a"). * @param key the key to reverse. @@ -283,14 +298,15 @@ const printDiffs = ( }); } - let previousKey: string | null = null; for (const entry of entries) { + let previousKey: string | null = null; if (options.group) { - const [value, keys] = entry; + const [values, keys] = entry; if (keys.length == 1) { const key = keys.at(0) as string; const keyDiff = diffKeys(key, previousKey ?? key, options); - console.log(`${keyDiff}:\n ${value}`); + console.log(`${keyDiff}:`); + values.forEach((value) => console.log(` ${value}`)); previousKey = key; } else { previousKey = null; @@ -303,19 +319,27 @@ const printDiffs = ( console.log(keyDiff); previousKey = key; } - console.log(` ${value}`); + values.forEach((value) => console.log(` ${value}`)); previousKey = null; } } else { - const [key, values] = entry; + const [keys, values] = entry; if (values.length == 1) { - const keyDiff = diffKeys(key, previousKey ?? key, options); - console.log(`${keyDiff}:\n ${values.at(0)}`); - previousKey = key; + for (const key of keys) { + const keyDiff = diffKeys(key, previousKey ?? key, options); + console.log(`${keyDiff}`); + previousKey = key; + } + values.forEach((value) => console.log(` ${value}`)); } else { - console.log(key); + for (const key of keys) { + const keyDiff = diffKeys(key, previousKey ?? key, options); + console.log(`${keyDiff}`); + previousKey = key; + } values.forEach((value) => console.log(` ${value}`)); } + previousKey = null; } console.log(); } From c10c106333644411cc77aee4f951f058a0f8ae97 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 18 Nov 2024 22:46:39 +0100 Subject: [PATCH 14/54] fix(diff-flat): print grouped values before keys --- scripts/diff-flat.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 2bbe3339ae9041..9f9385f8f7ca5b 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -305,21 +305,21 @@ const printDiffs = ( if (keys.length == 1) { const key = keys.at(0) as string; const keyDiff = diffKeys(key, previousKey ?? key, options); - console.log(`${keyDiff}:`); - values.forEach((value) => console.log(` ${value}`)); + values.forEach((value) => console.log(`${value}`)); + console.log(` ${keyDiff}`); previousKey = key; } else { previousKey = null; + values.forEach((value) => console.log(`${value}`)); const maxKeyLength = Math.max(...keys.map((key) => key.length)); for (const key of keys) { const keyDiff = diffKeys(key, previousKey ?? (keys.at(1) as string), { ...options, fill: maxKeyLength, }); - console.log(keyDiff); + console.log(` ${keyDiff}`); previousKey = key; } - values.forEach((value) => console.log(` ${value}`)); previousKey = null; } } else { From 700fe484375e1f6bd37e0a75408e7f8b51ec651a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 18 Nov 2024 23:05:36 +0100 Subject: [PATCH 15/54] enhance(diff-flat): annotate flags --- scripts/diff-flat.ts | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 9f9385f8f7ca5b..b2117b0c2bb754 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -7,6 +7,8 @@ import esMain from 'es-main'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; +import { FlagStatement } from '../types/types.js'; + import { getMergeBase, getFileContent, getGitDiffStatuses } from './lib/git.js'; interface Contents { @@ -45,6 +47,9 @@ const BROWSER_NAMES = [ 'webview_android', ]; +// FIXME This is bad. +const allFlags: string[] = []; + /** * Flattens an object. * @param obj the object to flatten. @@ -61,11 +66,19 @@ const flattenObject = ( if (Object.prototype.hasOwnProperty.call(obj, key)) { const fullKey = parentKey ? `${parentKey}.${key}` : key; - if (key == 'flags') { - result[fullKey] = toArray(obj[key]).map((value) => - JSON.stringify(value), - ); - } else if (typeof obj[key] === 'object' && obj[key] !== null) { + if (typeof obj[key] === 'object' && obj[key] !== null) { + // Merge values. + if ('version_added' in obj[key] && 'flags' in obj[key]) { + const { flags } = obj[key]; + delete obj[key].flags; + const flagsJson = JSON.stringify(flags); + if (!allFlags.includes(flagsJson)) { + allFlags.push(flagsJson); + } + const flagIndex = allFlags.indexOf(flagsJson); + obj[key].version_added += ` f${flagIndex}`; + } + // Recursively flatten nested objects flattenObject( BROWSER_NAMES.includes(key) @@ -344,6 +357,13 @@ const printDiffs = ( console.log(); } + if (allFlags.length > 0) { + console.log('Flags:'); + for (const [index, flagsJson] of allFlags.entries()) { + console.log(` f${index} = ${flagsJson}`); + } + } + if (options.html) { console.log('
'); } From 08db344679eb723e1be4a948c9937a572123bb43 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Mon, 18 Nov 2024 23:28:33 +0100 Subject: [PATCH 16/54] enhance(diff-flat): merge versions + prefix/flags/altname --- scripts/diff-flat.ts | 49 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index b2117b0c2bb754..f02843f4409717 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -7,7 +7,7 @@ import esMain from 'es-main'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { FlagStatement } from '../types/types.js'; +import { SimpleSupportStatement } from '../types/types.js'; import { getMergeBase, getFileContent, getGitDiffStatuses } from './lib/git.js'; @@ -68,15 +68,44 @@ const flattenObject = ( if (typeof obj[key] === 'object' && obj[key] !== null) { // Merge values. - if ('version_added' in obj[key] && 'flags' in obj[key]) { - const { flags } = obj[key]; - delete obj[key].flags; - const flagsJson = JSON.stringify(flags); - if (!allFlags.includes(flagsJson)) { - allFlags.push(flagsJson); + if ('version_added' in obj[key]) { + if ('flags' in obj[key]) { + // Deduplicate flag. + const flagsJson = JSON.stringify(obj[key].flags); + if (!allFlags.includes(flagsJson)) { + allFlags.push(flagsJson); + } + const flagIndex = allFlags.indexOf(flagsJson); + obj[key].flags = `†${flagIndex}`; } - const flagIndex = allFlags.indexOf(flagsJson); - obj[key].version_added += ` f${flagIndex}`; + + const { + version_added, + version_removed, + partial_implementation, + alternative_name, + prefix, + flags, + } = obj[key] as SimpleSupportStatement; + + const parts = [ + version_added && + version_removed && + `${version_added} - ${version_removed}`, + version_added && !version_removed && `since ${version_added}`, + partial_implementation && '(partial)', + flags, + prefix && `prefix=${prefix}`, + alternative_name && `altname=${alternative_name}`, + ].filter(Boolean); + + obj[key].version = parts.join(' '); + delete obj[key].version_added; + delete obj[key].version_removed; + delete obj[key].partial_implementation; + delete obj[key].alternative_name; + delete obj[key].prefix; + delete obj[key].flags; } // Recursively flatten nested objects @@ -360,7 +389,7 @@ const printDiffs = ( if (allFlags.length > 0) { console.log('Flags:'); for (const [index, flagsJson] of allFlags.entries()) { - console.log(` f${index} = ${flagsJson}`); + console.log(`†${index} = ${flagsJson}`); } } From 118405a40f46f5e1966b6c0cb3d519f542fd0cf2 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 00:12:56 +0100 Subject: [PATCH 17/54] enhance(diff-flat): improve group formatting --- scripts/diff-flat.ts | 124 +++++++++++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 27 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index f02843f4409717..95a8de2027d5b0 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -49,6 +49,21 @@ const BROWSER_NAMES = [ // FIXME This is bad. const allFlags: string[] = []; +const allNotes: string[] = []; + +/** + * Formats a flag reference. + * @param index the flag index. + * @returns formatted reference. + */ +const formatFlagIndex = (index: number): string => `[^f${index + 1}]`; + +/** + * Formats a flag reference. + * @param index the flag index. + * @returns formatted reference. + */ +const formatNoteIndex = (index: number): string => `[^n${index + 1}]`; /** * Flattens an object. @@ -76,7 +91,23 @@ const flattenObject = ( allFlags.push(flagsJson); } const flagIndex = allFlags.indexOf(flagsJson); - obj[key].flags = `†${flagIndex}`; + obj[key].flags = formatFlagIndex(flagIndex); + } + + if ('notes' in obj[key]) { + const notes = toArray(obj[key].notes); + obj[key].notes = notes + .map((note) => { + const notesJson = JSON.stringify(note); + if (!allNotes.includes(notesJson)) { + allNotes.push(notesJson); + } + const noteIndex = allNotes.indexOf(notesJson); + return noteIndex; + }) + .sort() + .map((index) => formatNoteIndex(index)) + .join(','); } const { @@ -86,17 +117,17 @@ const flattenObject = ( alternative_name, prefix, flags, + notes, } = obj[key] as SimpleSupportStatement; const parts = [ - version_added && - version_removed && - `${version_added} - ${version_removed}`, - version_added && !version_removed && `since ${version_added}`, + version_added && version_added && `added ${version_added}`, + version_removed && `removed ${version_removed}`, partial_implementation && '(partial)', flags, prefix && `prefix=${prefix}`, alternative_name && `altname=${alternative_name}`, + notes, ].filter(Boolean); obj[key].version = parts.join(' '); @@ -106,15 +137,12 @@ const flattenObject = ( delete obj[key].alternative_name; delete obj[key].prefix; delete obj[key].flags; + delete obj[key].notes; } // Recursively flatten nested objects flattenObject( - BROWSER_NAMES.includes(key) - ? toArray(obj[key]).reverse() - : key === 'notes' - ? toArray(obj[key]) - : obj[key], + BROWSER_NAMES.includes(key) ? toArray(obj[key]).reverse() : obj[key], fullKey, result, ); @@ -190,7 +218,7 @@ const printDiffs = ( options: { group: boolean; html: boolean }, ): void => { if (options.html) { - console.log('
'); + console.log('
');
   }
 
   const groups = new Map>();
@@ -267,7 +295,7 @@ const printDiffs = (
               : chalk`{green ${headValue}}`)) ||
           '';
 
-        const value = [oldValue, newValue].filter(Boolean).join(' → ');
+        const value = [newValue, oldValue].filter(Boolean).join(' ← ');
 
         if (!value.length) {
           // e.g. null => "mirror"
@@ -280,7 +308,7 @@ const printDiffs = (
             BROWSER_NAMES.includes(part),
           );
           const field = reverseKeyParts.find((part) => !/^\d+$/.test(part));
-          const groupKey = `${!browser ? '' : options.html ? `${browser} → ` : chalk`{cyan ${browser}} → `}${field} = ${value}`;
+          const groupKey = `${!browser ? '' : options.html ? `${browser}.` : chalk`{cyan ${browser}}.`}${field} = ${value}`;
           const groupValue = key
             .split('.')
             .map((part) =>
@@ -296,7 +324,7 @@ const printDiffs = (
           groups.set(groupKey, group);
         } else {
           const change = options.html
-            ? `${keyDiff} = ${value}
` + ? `${keyDiff} = ${value}` : chalk`${keyDiff} = ${value}`; const group = groups.get(commonName) ?? new Set(); group.add(change); @@ -324,6 +352,18 @@ const printDiffs = ( JSON.parse(valuesJson), ]); + const json = JSON.stringify(entries); + for (const flagIndex of allFlags.keys()) { + if (!json.includes(formatFlagIndex(flagIndex))) { + allFlags[flagIndex] = ''; + } + } + for (const noteIndex of allNotes.keys()) { + if (!json.includes(formatNoteIndex(noteIndex))) { + allNotes[noteIndex] = ''; + } + } + if (options.group) { entries.sort( ([k1, v1], [k2, v2]) => k2.length * v2.length - k1.length * v1.length, @@ -340,6 +380,13 @@ const printDiffs = ( }); } + /** + * Prints to stdout. + * @param line the line to print + * @returns nothing + */ + const print = (line = ''): void => console.log(line); + for (const entry of entries) { let previousKey: string | null = null; if (options.group) { @@ -347,21 +394,29 @@ const printDiffs = ( if (keys.length == 1) { const key = keys.at(0) as string; const keyDiff = diffKeys(key, previousKey ?? key, options); - values.forEach((value) => console.log(`${value}`)); - console.log(` ${keyDiff}`); + values.forEach((value) => print(`${value}`)); + print(` ${keyDiff}`); previousKey = key; } else { previousKey = null; - values.forEach((value) => console.log(`${value}`)); + console.log(values.join('\n')); const maxKeyLength = Math.max(...keys.map((key) => key.length)); + if (options.html && keys.length > 1) { + process.stdout.write( + `
${keys.length} paths`, + ); + } for (const key of keys) { const keyDiff = diffKeys(key, previousKey ?? (keys.at(1) as string), { ...options, fill: maxKeyLength, }); - console.log(` ${keyDiff}`); + print(` ${keyDiff}`); previousKey = key; } + if (options.html && keys.length > 1) { + process.stdout.write('
'); + } previousKey = null; } } else { @@ -369,32 +424,47 @@ const printDiffs = ( if (values.length == 1) { for (const key of keys) { const keyDiff = diffKeys(key, previousKey ?? key, options); - console.log(`${keyDiff}`); + print(`${keyDiff}`); previousKey = key; } - values.forEach((value) => console.log(` ${value}`)); + values.forEach((value) => print(` ${value}`)); } else { for (const key of keys) { const keyDiff = diffKeys(key, previousKey ?? key, options); - console.log(`${keyDiff}`); + print(`${keyDiff}`); previousKey = key; } - values.forEach((value) => console.log(` ${value}`)); + values.forEach((value) => print(` ${value}`)); } previousKey = null; } - console.log(); + print(''); } - if (allFlags.length > 0) { - console.log('Flags:'); + if (allFlags.some(Boolean)) { + print('Flags:'); for (const [index, flagsJson] of allFlags.entries()) { - console.log(`†${index} = ${flagsJson}`); + if (!flagsJson) { + continue; + } + print(`${formatFlagIndex(index)}: ${flagsJson}`); + } + print(); + } + + if (allNotes.some(Boolean)) { + print('Notes:'); + for (const [index, notesJson] of allNotes.entries()) { + if (!notesJson) { + continue; + } + print(`${formatNoteIndex(index)}: ${notesJson}`); } + print(); } if (options.html) { - console.log('
'); + console.log(''); } }; From 0eb3d608b7fd01e08386d623ab47a84d1f952d03 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 00:24:32 +0100 Subject: [PATCH 18/54] enhance(diff-flat): collapse groups + order by value --- scripts/diff-flat.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 95a8de2027d5b0..c4e4060a1233c5 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -121,8 +121,8 @@ const flattenObject = ( } = obj[key] as SimpleSupportStatement; const parts = [ - version_added && version_added && `added ${version_added}`, - version_removed && `removed ${version_removed}`, + version_added && version_added && `${version_added}+`, + version_removed && `−${version_removed}`, partial_implementation && '(partial)', flags, prefix && `prefix=${prefix}`, @@ -365,9 +365,7 @@ const printDiffs = ( } if (options.group) { - entries.sort( - ([k1, v1], [k2, v2]) => k2.length * v2.length - k1.length * v1.length, - ); + entries.sort(); /** * Reverses a key (e.g. "a.b.c" => "c.b.a"). * @param key the key to reverse. @@ -401,9 +399,9 @@ const printDiffs = ( previousKey = null; console.log(values.join('\n')); const maxKeyLength = Math.max(...keys.map((key) => key.length)); - if (options.html && keys.length > 1) { + if (options.html) { process.stdout.write( - `
${keys.length} paths`, + `
${keys.length} ${keys.length === 1 ? 'path' : 'paths'}`, ); } for (const key of keys) { @@ -414,7 +412,7 @@ const printDiffs = ( print(` ${keyDiff}`); previousKey = key; } - if (options.html && keys.length > 1) { + if (options.html) { process.stdout.write('
'); } previousKey = null; From 5611d2451837fcf9429a8c22f0a181e02d22701b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 00:34:44 +0100 Subject: [PATCH 19/54] enhance(diff-flat): omit "__compat"/"support" + trailing placeholder --- scripts/diff-flat.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index c4e4060a1233c5..43b7b67eda0be0 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -185,7 +185,16 @@ const diffKeys = ( ): string => { const len = key.length; let fill = options.fill ?? 0; - return diffArrays(lastKey.split('.'), key.split('.')) + /** + * Filters out irrelevant keys. + * @param part the key part. + * @returns true, if the part should be ignored, false otherwise + */ + const keyFilter = (part) => part !== '__compat' && part !== 'support'; + return diffArrays( + lastKey.split('.').filter(keyFilter), + key.split('.').filter(keyFilter), + ) .filter((part) => !part.removed) .map((part) => { const key = part.value.join('.'); @@ -311,9 +320,13 @@ const printDiffs = ( const groupKey = `${!browser ? '' : options.html ? `${browser}.` : chalk`{cyan ${browser}}.`}${field} = ${value}`; const groupValue = key .split('.') - .map((part) => - part !== browser && part !== field - ? part + .map((part) => (part !== browser && part !== field ? part : '{}')) + .reverse() + .filter((value, index) => index > 0 || value !== '{}') + .reverse() + .map((value) => + value !== '{}' + ? value : options.html ? '{}' : chalk`{dim \{\}}`, From 7f17aa7650876f54a047efee96020bf81f12d91a Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 00:41:55 +0100 Subject: [PATCH 20/54] enhance(flat-diff): join tags --- scripts/diff-flat.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 43b7b67eda0be0..5a6bf3bf8dd6c9 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -83,7 +83,9 @@ const flattenObject = ( if (typeof obj[key] === 'object' && obj[key] !== null) { // Merge values. - if ('version_added' in obj[key]) { + if ('tags' in obj[key]) { + obj[key].tags = obj[key].tags.join(','); + } else if ('version_added' in obj[key]) { if ('flags' in obj[key]) { // Deduplicate flag. const flagsJson = JSON.stringify(obj[key].flags); From 7fdbe6036117a070c80734f027897957d0e41cfc Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 01:00:58 +0100 Subject: [PATCH 21/54] enhance(diff-flat): diff value again --- scripts/diff-flat.ts | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 5a6bf3bf8dd6c9..8f37f89327f8e9 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -294,19 +294,28 @@ const printDiffs = ( const hasValue = (value: any) => typeof value === 'boolean' || (!!value && value !== 'mirror'); - const oldValue = - hasValue(baseData[key] ?? null) && - (options.html - ? `${baseValue}` - : chalk`{red ${baseValue}}`); - const newValue = - (hasValue(headData[key] ?? null) && - (options.html - ? `${headValue}` - : chalk`{green ${headValue}}`)) || - ''; - - const value = [newValue, oldValue].filter(Boolean).join(' ← '); + const valueDiff = diffArrays( + (hasValue(headData[key] ?? null) ? headValue : '').split(' '), + (hasValue(baseData[key] ?? null) ? baseValue : '').split(' '), + ) + .map((part) => { + // Note: removed/added is deliberately inversed here, to have additions first. + const value = part.value.join(' '); + if (part.removed) { + return options.html + ? `${value}` + : chalk`{green ${value}}`; + } else if (part.added) { + return options.html + ? `${value}` + : chalk`{red ${value}}`; + } + + return value; + }) + .join(''); + + const value = valueDiff; if (!value.length) { // e.g. null => "mirror" From 3712dfb642d078488efde6bef9dda3b256461713 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 01:09:24 +0100 Subject: [PATCH 22/54] fix(diff-flat): split at footnotes --- scripts/diff-flat.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 8f37f89327f8e9..e51d2f78a987d4 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -295,12 +295,16 @@ const printDiffs = ( typeof value === 'boolean' || (!!value && value !== 'mirror'); const valueDiff = diffArrays( - (hasValue(headData[key] ?? null) ? headValue : '').split(' '), - (hasValue(baseData[key] ?? null) ? baseValue : '').split(' '), + (hasValue(headData[key] ?? null) ? headValue : '').split( + /(?<=[\] ])/, + ), + (hasValue(baseData[key] ?? null) ? baseValue : '').split( + /(?<=[\] ])/, + ), ) .map((part) => { // Note: removed/added is deliberately inversed here, to have additions first. - const value = part.value.join(' '); + const value = part.value.join(''); if (part.removed) { return options.html ? `${value}` From a069adcffc19c30c569f437111970b9926b03551 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 01:10:34 +0100 Subject: [PATCH 23/54] refactor(diff-flat): inline print() --- scripts/diff-flat.ts | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index e51d2f78a987d4..84e8db6f381db8 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -406,13 +406,6 @@ const printDiffs = ( }); } - /** - * Prints to stdout. - * @param line the line to print - * @returns nothing - */ - const print = (line = ''): void => console.log(line); - for (const entry of entries) { let previousKey: string | null = null; if (options.group) { @@ -420,8 +413,8 @@ const printDiffs = ( if (keys.length == 1) { const key = keys.at(0) as string; const keyDiff = diffKeys(key, previousKey ?? key, options); - values.forEach((value) => print(`${value}`)); - print(` ${keyDiff}`); + values.forEach((value) => console.log(`${value}`)); + console.log(` ${keyDiff}`); previousKey = key; } else { previousKey = null; @@ -437,7 +430,7 @@ const printDiffs = ( ...options, fill: maxKeyLength, }); - print(` ${keyDiff}`); + console.log(` ${keyDiff}`); previousKey = key; } if (options.html) { @@ -450,43 +443,43 @@ const printDiffs = ( if (values.length == 1) { for (const key of keys) { const keyDiff = diffKeys(key, previousKey ?? key, options); - print(`${keyDiff}`); + console.log(`${keyDiff}`); previousKey = key; } - values.forEach((value) => print(` ${value}`)); + values.forEach((value) => console.log(` ${value}`)); } else { for (const key of keys) { const keyDiff = diffKeys(key, previousKey ?? key, options); - print(`${keyDiff}`); + console.log(`${keyDiff}`); previousKey = key; } - values.forEach((value) => print(` ${value}`)); + values.forEach((value) => console.log(` ${value}`)); } previousKey = null; } - print(''); + console.log(''); } if (allFlags.some(Boolean)) { - print('Flags:'); + console.log('Flags:'); for (const [index, flagsJson] of allFlags.entries()) { if (!flagsJson) { continue; } - print(`${formatFlagIndex(index)}: ${flagsJson}`); + console.log(`${formatFlagIndex(index)}: ${flagsJson}`); } - print(); + console.log(); } if (allNotes.some(Boolean)) { - print('Notes:'); + console.log('Notes:'); for (const [index, notesJson] of allNotes.entries()) { if (!notesJson) { continue; } - print(`${formatNoteIndex(index)}: ${notesJson}`); + console.log(`${formatNoteIndex(index)}: ${notesJson}`); } - print(); + console.log(); } if (options.html) { From 06bfed88e4126b053c68bb3bcd590e0a9f2e36ad Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 01:20:11 +0100 Subject: [PATCH 24/54] enhance(diff-flat): print refs in context --- scripts/diff-flat.ts | 52 +++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 84e8db6f381db8..071faa19a4adba 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -406,6 +406,34 @@ const printDiffs = ( }); } + /** + * Prints references found in the inputs. + * @param inputs the inputs to scan for references. + */ + const printRefs = (...inputs: string[]): void => { + const lines: string[] = []; + for (const [index, content] of allFlags.entries()) { + const ref = formatFlagIndex(index); + if (inputs.some((input) => input.includes(ref))) { + lines.push(`${ref}: ${content}`); + } + } + for (const [index, content] of allNotes.entries()) { + const ref = formatNoteIndex(index); + if (inputs.some((input) => input.includes(ref))) { + lines.push(`${ref}: ${content}`); + } + } + if (lines.length > 0) { + console.log(); + lines.forEach((line) => + console.log( + options.html ? `${line}` : chalk`{italic ${line}}`, + ), + ); + } + }; + for (const entry of entries) { let previousKey: string | null = null; if (options.group) { @@ -415,6 +443,7 @@ const printDiffs = ( const keyDiff = diffKeys(key, previousKey ?? key, options); values.forEach((value) => console.log(`${value}`)); console.log(` ${keyDiff}`); + printRefs(...values); previousKey = key; } else { previousKey = null; @@ -436,6 +465,7 @@ const printDiffs = ( if (options.html) { process.stdout.write('
'); } + printRefs(...values); previousKey = null; } } else { @@ -460,28 +490,6 @@ const printDiffs = ( console.log(''); } - if (allFlags.some(Boolean)) { - console.log('Flags:'); - for (const [index, flagsJson] of allFlags.entries()) { - if (!flagsJson) { - continue; - } - console.log(`${formatFlagIndex(index)}: ${flagsJson}`); - } - console.log(); - } - - if (allNotes.some(Boolean)) { - console.log('Notes:'); - for (const [index, notesJson] of allNotes.entries()) { - if (!notesJson) { - continue; - } - console.log(`${formatNoteIndex(index)}: ${notesJson}`); - } - console.log(); - } - if (options.html) { console.log(''); } From c845b50d90a175851a280190351c8b0a83327619 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 01:21:15 +0100 Subject: [PATCH 25/54] chore(deps-dev): add strip-ansi 7.1.0 # Conflicts: # package-lock.json --- package-lock.json | 255 ++++++++++++++++++++-------------------------- package.json | 1 + 2 files changed, 109 insertions(+), 147 deletions(-) diff --git a/package-lock.json b/package-lock.json index 986546a797049e..1af368b7f6ec07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "ora": "~8.1.0", "prettier": "~3.4.0", "sinon": "^19.0.1", + "strip-ansi": "^7.1.0", "tempy": "^3.1.0", "ts-node": "~10.9.1", "typescript": "~5.7.2", @@ -736,18 +737,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -771,21 +760,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -2649,18 +2623,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/cli-truncate/node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -2684,21 +2646,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2746,6 +2693,19 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -4160,6 +4120,19 @@ "node": "*" } }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -6019,18 +5992,6 @@ "node": ">=18.0.0" } }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/listr2/node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -6054,21 +6015,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/listr2/node_modules/wrap-ansi": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", @@ -6203,18 +6149,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/log-update/node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -6269,21 +6203,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/log-update/node_modules/wrap-ansi": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", @@ -6631,6 +6550,19 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -7018,18 +6950,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/ora/node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -7108,6 +7028,18 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/os-filter-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", + "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", + "dev": true, + "dependencies": { + "arch": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/p-cancelable": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", @@ -8270,6 +8202,19 @@ "node": ">=8" } }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -8279,6 +8224,19 @@ "node": ">=8" } }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -8329,15 +8287,19 @@ } }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-ansi-cjs": { @@ -8353,6 +8315,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -9110,16 +9085,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "engines": { + "node": ">=8" } }, "node_modules/wrap-ansi/node_modules/emoji-regex": { @@ -9145,21 +9121,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 1150d7db123215..1af261e35bd31e 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "ora": "~8.1.0", "prettier": "~3.4.0", "sinon": "^19.0.1", + "strip-ansi": "^7.1.0", "tempy": "^3.1.0", "ts-node": "~10.9.1", "typescript": "~5.7.2", From 879d074f5f7863a9a1a5e85ad1c3eac2ac2e9f22 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 01:33:12 +0100 Subject: [PATCH 26/54] fix(diff-flat): sort ignoring ANSI escape codes --- scripts/diff-flat.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 071faa19a4adba..4bbb73d3edf58a 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -4,6 +4,7 @@ import chalk from 'chalk-template'; import { diffArrays } from 'diff'; import esMain from 'es-main'; +import stripAnsi from 'strip-ansi'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; @@ -171,6 +172,15 @@ const toArray = (value: any): any[] => { return value; }; +/** + * Compares two strings ignoring ANSI escape codes. + * @param a one value + * @param b other value + * @returns comparison result. + */ +const stripAnsiCompare = (a: string, b: string): number => + stripAnsi(a).localeCompare(stripAnsi(b)); + /** * Formats a key diff'ed with the previous key. * @param key the current key @@ -375,7 +385,15 @@ const printDiffs = ( entryGroups.set(groupKey, keys); } - const entries = [...entryGroups.entries()].map(([valuesJson, keys]) => [ + const rawEntries = [...entryGroups.entries()]; + + if (options.group) { + rawEntries.sort(([, a], [, b]) => + stripAnsiCompare(a.at(0) as string, b.at(0) as string), + ); + } + + const entries = rawEntries.map(([valuesJson, keys]) => [ keys, JSON.parse(valuesJson), ]); @@ -393,7 +411,6 @@ const printDiffs = ( } if (options.group) { - entries.sort(); /** * Reverses a key (e.g. "a.b.c" => "c.b.a"). * @param key the key to reverse. @@ -402,7 +419,10 @@ const printDiffs = ( const reverseKey = (key: string): string => key.split('.').reverse().join('.'); entries.forEach((entry) => { - entry[1] = entry[1].map(reverseKey).sort().map(reverseKey); + entry[1] = entry[1] + .map(reverseKey) + .sort(stripAnsiCompare) + .map(reverseKey); }); } From 645d3b69937b9d03673c4df3f4dd3ab37da68afb Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 01:45:44 +0100 Subject: [PATCH 27/54] enhance(diff-flat): serialize status --- scripts/diff-flat.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 4bbb73d3edf58a..b072cce06af492 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -84,9 +84,22 @@ const flattenObject = ( if (typeof obj[key] === 'object' && obj[key] !== null) { // Merge values. + if ('status' in obj[key]) { + const { deprecated, standard_track, experimental } = obj[key].status; + const statusFlags = [ + deprecated && 'deprecated', + standard_track && 'standard_track', + experimental && 'experimental', + ].filter(Boolean); + + obj[key].status = statusFlags.join(','); + } + if ('tags' in obj[key]) { obj[key].tags = obj[key].tags.join(','); - } else if ('version_added' in obj[key]) { + } + + if ('version_added' in obj[key]) { if ('flags' in obj[key]) { // Deduplicate flag. const flagsJson = JSON.stringify(obj[key].flags); From e1173cd196aaf16b9e2fa5604490153d6b61227b Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 01:46:06 +0100 Subject: [PATCH 28/54] fix(diff-flat): improve word splitting --- scripts/diff-flat.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index b072cce06af492..b5f32d24c419be 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -317,13 +317,10 @@ const printDiffs = ( const hasValue = (value: any) => typeof value === 'boolean' || (!!value && value !== 'mirror'); + const splitRegexp = /(?<=^")|(?<=[\],/ ])|(?=[[,/ ])|(?="$)/; const valueDiff = diffArrays( - (hasValue(headData[key] ?? null) ? headValue : '').split( - /(?<=[\] ])/, - ), - (hasValue(baseData[key] ?? null) ? baseValue : '').split( - /(?<=[\] ])/, - ), + (hasValue(headData[key] ?? null) ? headValue : '').split(splitRegexp), + (hasValue(baseData[key] ?? null) ? baseValue : '').split(splitRegexp), ) .map((part) => { // Note: removed/added is deliberately inversed here, to have additions first. From 6bb510d8425f735141a92653d225e0e66a768186 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 01:49:13 +0100 Subject: [PATCH 29/54] fix(diff-flat): remove inverse key sort --- scripts/diff-flat.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index b5f32d24c419be..0f86b3bf49450d 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -420,22 +420,6 @@ const printDiffs = ( } } - if (options.group) { - /** - * Reverses a key (e.g. "a.b.c" => "c.b.a"). - * @param key the key to reverse. - * @returns the reversed key. - */ - const reverseKey = (key: string): string => - key.split('.').reverse().join('.'); - entries.forEach((entry) => { - entry[1] = entry[1] - .map(reverseKey) - .sort(stripAnsiCompare) - .map(reverseKey); - }); - } - /** * Prints references found in the inputs. * @param inputs the inputs to scan for references. From 8f45778bd84726ac8a1cd49a50cec28ef539dfb8 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Tue, 19 Nov 2024 02:04:27 +0100 Subject: [PATCH 30/54] fix(diff-flat): call diffKeys() with correct order --- scripts/diff-flat.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 0f86b3bf49450d..fd353f9a528e9c 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -295,7 +295,7 @@ const printDiffs = ( const commonName = options.html ? `

${prefix}

` : `${prefix}`; - let lastKey = keys.at(0) ?? ''; + let lastKey = ''; for (const key of keys) { const baseValue = JSON.stringify(baseData[key] ?? null); @@ -303,9 +303,12 @@ const printDiffs = ( if (baseValue === headValue) { continue; } + if (!lastKey) { + lastKey = key; + } const keyDiff = diffKeys( - lastKey.slice(prefix.length), key.slice(prefix.length), + lastKey.slice(prefix.length), options, ); From 612cfd4888deeaa252f11bb0e4c24bbc9e883de8 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 21 Nov 2024 19:25:53 +0100 Subject: [PATCH 31/54] enhance(diff-flat): add mirror option --- scripts/diff-flat.ts | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index fd353f9a528e9c..e5eead6fd2379f 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -8,13 +8,15 @@ import stripAnsi from 'strip-ansi'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { SimpleSupportStatement } from '../types/types.js'; +import { CompatData, SimpleSupportStatement } from '../types/types.js'; +import { walk } from '../utils/index.js'; +import { applyMirroring } from './build/index.js'; import { getMergeBase, getFileContent, getGitDiffStatuses } from './lib/git.js'; -interface Contents { - base: string; - head: string; +interface Contents { + base: T; + head: T; } /** @@ -26,12 +28,12 @@ interface Contents { * @param headPath Head path * @returns The contents of both commits */ -const getBaseAndHeadContents = ( +const getBaseAndHeadContents = ( baseCommit: string, basePath: string, headCommit: string, headPath: string, -): Contents => { +): Contents => { const base = JSON.parse(getFileContent(baseCommit, basePath)); const head = JSON.parse(getFileContent(headCommit, headPath)); return { base, head }; @@ -245,11 +247,12 @@ const diffKeys = ( * @param options Options * @param options.group Whether to group by value, rather than the common feature * @param options.html Whether to output HTML, rather than plain-text + * @param options.mirror Whether to apply mirroring, rather than ignore "mirror" values */ const printDiffs = ( base: string, head = '', - options: { group: boolean; html: boolean }, + options: { group: boolean; html: boolean; mirror: boolean }, ): void => { if (options.html) { console.log('
');
@@ -268,12 +271,21 @@ const printDiffs = (
     } else if (status.value === 'D') {
       console.warn("diff:flat doesn't support file deletions yet!");
     } else {
-      const contents = getBaseAndHeadContents(
+      const contents = getBaseAndHeadContents(
         base,
         status.basePath,
         head,
         status.headPath,
       );
+      if (options.mirror) {
+        for (const feature of walk(undefined, contents.base)) {
+          applyMirroring(feature);
+        }
+        for (const feature of walk(undefined, contents.head)) {
+          applyMirroring(feature);
+        }
+      }
+
       const baseData = flattenObject(contents.base);
       const headData = flattenObject(contents.head);
 
@@ -537,10 +549,14 @@ if (esMain(import.meta)) {
         .option('group', {
           type: 'boolean',
           default: false,
+        })
+        .option('mirror', {
+          type: 'boolean',
+          default: false,
         });
     },
   );
 
-  const { base, head, html, group } = argv as any;
-  printDiffs(getMergeBase(base, head), head, { group, html });
+  const { base, head, group, html, mirror } = argv as any;
+  printDiffs(getMergeBase(base, head), head, { group, html, mirror });
 }

From c90361d6eb6e7fe6ea6829bbc19b1cc71c2c8d0c Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 19:40:12 +0100
Subject: [PATCH 32/54] chore(diff-flat): add workaround for running from
 flat-diff branch

---
 scripts/diff-flat.ts | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index e5eead6fd2379f..58539adb01cc65 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -13,6 +13,7 @@ import { walk } from '../utils/index.js';
 
 import { applyMirroring } from './build/index.js';
 import { getMergeBase, getFileContent, getGitDiffStatuses } from './lib/git.js';
+import { exec } from './release/utils.js';
 
 interface Contents {
   base: T;
@@ -557,6 +558,17 @@ if (esMain(import.meta)) {
     },
   );
 
-  const { base, head, group, html, mirror } = argv as any;
+  const options = argv as any;
+
+  if (
+    options.head === 'HEAD' &&
+    exec('git branch --show-current') === 'flat-diff'
+  ) {
+    // Compare first positional parameter against origin/main.
+    [options.base, options.head] = [options.head, options.base];
+  }
+
+  const { base, head, group, html, mirror } = options;
+
   printDiffs(getMergeBase(base, head), head, { group, html, mirror });
 }

From 26a156d417006370fe46700c4d478c959caf745c Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 19:52:34 +0100
Subject: [PATCH 33/54] enhance(diff-flat): fetch if ref not available

---
 scripts/diff-flat.ts | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index 58539adb01cc65..86fa4751a75aa6 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -570,5 +570,10 @@ if (esMain(import.meta)) {
 
   const { base, head, group, html, mirror } = options;
 
+  exec(`
+    git rev-parse ${base} || git fetch origin ${base} 2>/dev/null || true &
+    git rev-parse ${head} || git fetch origin ${head} 2>/dev/null || true
+  `);
+
   printDiffs(getMergeBase(base, head), head, { group, html, mirror });
 }

From ae89f2dd28c86798bb12cb4e59a8d17865ab08b2 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 20:01:42 +0100
Subject: [PATCH 34/54] fixup! chore(diff-flat): add workaround for running
 from flat-diff branch

---
 scripts/diff-flat.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index 86fa4751a75aa6..d14056306cd1d7 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -564,8 +564,8 @@ if (esMain(import.meta)) {
     options.head === 'HEAD' &&
     exec('git branch --show-current') === 'flat-diff'
   ) {
-    // Compare first positional parameter against origin/main.
-    [options.base, options.head] = [options.head, options.base];
+    // Workaround: Compare first positional parameter against origin/main.
+    [options.base, options.head] = [options.head, 'origin/main'];
   }
 
   const { base, head, group, html, mirror } = options;

From d70d27992682c2db01d7423e9326ac1f1fdcb86a Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 20:04:00 +0100
Subject: [PATCH 35/54] enhance(diff-flat): support pr number

---
 scripts/diff-flat.ts | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index d14056306cd1d7..8363270ace1de3 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -560,6 +560,11 @@ if (esMain(import.meta)) {
 
   const options = argv as any;
 
+  if (/^\d+$/.test(options.base)) {
+    options.head = `pull/${options.base}/head`;
+    options.base = 'origin/main';
+  }
+
   if (
     options.head === 'HEAD' &&
     exec('git branch --show-current') === 'flat-diff'

From b6647b711f0b62dca4230f18bc2aae1756b62d98 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 20:04:39 +0100
Subject: [PATCH 36/54] fix(flat-diff): always try to fetch from remote

---
 scripts/diff-flat.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index 8363270ace1de3..883af00ac9b296 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -576,8 +576,8 @@ if (esMain(import.meta)) {
   const { base, head, group, html, mirror } = options;
 
   exec(`
-    git rev-parse ${base} || git fetch origin ${base} 2>/dev/null || true &
-    git rev-parse ${head} || git fetch origin ${head} 2>/dev/null || true
+    git fetch origin ${base} 2>/dev/null || true &
+    git fetch origin ${head} 2>/dev/null || true
   `);
 
   printDiffs(getMergeBase(base, head), head, { group, html, mirror });

From ff2b1734ac5892cc74282e8e9428eff8968bb1b4 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 20:18:20 +0100
Subject: [PATCH 37/54] refactor(utils): move {exec, execAsync,spawn}()

---
 scripts/diff-features.ts | 14 ++----------
 scripts/diff-flat.ts     |  3 +--
 scripts/release/index.ts |  4 ++--
 scripts/release/stats.ts |  4 ++--
 scripts/release/utils.ts | 47 ++--------------------------------------
 utils/exec-async.ts      | 13 +++++++++++
 utils/exec.ts            | 15 +++++++++++++
 utils/index.ts           |  5 ++++-
 utils/spawn.ts           | 31 ++++++++++++++++++++++++++
 9 files changed, 72 insertions(+), 64 deletions(-)
 create mode 100644 utils/exec-async.ts
 create mode 100644 utils/exec.ts
 create mode 100644 utils/spawn.ts

diff --git a/scripts/diff-features.ts b/scripts/diff-features.ts
index e6b3f1e446877f..d5bef6bc7594fc 100644
--- a/scripts/diff-features.ts
+++ b/scripts/diff-features.ts
@@ -1,9 +1,8 @@
 /* This file is a part of @mdn/browser-compat-data
  * See LICENSE file for more information. */
 
-import { exec, execSync } from 'node:child_process';
+import { execSync } from 'node:child_process';
 import fs from 'node:fs';
-import { promisify } from 'node:util';
 import path from 'node:path';
 
 import esMain from 'es-main';
@@ -11,16 +10,7 @@ import yargs from 'yargs';
 import { hideBin } from 'yargs/helpers';
 import { temporaryDirectoryTask } from 'tempy';
 
-/**
- * Executes a command asynchronously.
- * @param command The command to execute asynchronously.
- * @returns The output of the command.
- */
-const execAsync = async (command: string): Promise => {
-  const result = await promisify(exec)(command, { encoding: 'utf-8' });
-
-  return result.stdout.trim();
-};
+import { execAsync } from '../utils/index.js';
 
 /**
  * Compare two references and print diff as Markdown or JSON
diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index 883af00ac9b296..079626d9d942f3 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -9,11 +9,10 @@ import yargs from 'yargs';
 import { hideBin } from 'yargs/helpers';
 
 import { CompatData, SimpleSupportStatement } from '../types/types.js';
-import { walk } from '../utils/index.js';
+import { exec, walk } from '../utils/index.js';
 
 import { applyMirroring } from './build/index.js';
 import { getMergeBase, getFileContent, getGitDiffStatuses } from './lib/git.js';
-import { exec } from './release/utils.js';
 
 interface Contents {
   base: T;
diff --git a/scripts/release/index.ts b/scripts/release/index.ts
index 633bfc6db7a730..f2db3204a9cf07 100644
--- a/scripts/release/index.ts
+++ b/scripts/release/index.ts
@@ -7,18 +7,18 @@ import yargs from 'yargs';
 import { hideBin } from 'yargs/helpers';
 import { temporaryWriteTask } from 'tempy';
 
+import { exec, spawn } from '../../utils/index.js';
+
 import { getSemverBumpPulls } from './semver-pulls.js';
 import { getStats } from './stats.js';
 import { getChanges } from './changes.js';
 import { getNotes, addNotes } from './notes.js';
 import {
-  exec,
   requireGitHubCLI,
   requireWriteAccess,
   getLatestTag,
   getRefDate,
   keypress,
-  spawn,
   fetchMain,
 } from './utils.js';
 
diff --git a/scripts/release/stats.ts b/scripts/release/stats.ts
index fec529528bfd33..c366ccd4820c9c 100644
--- a/scripts/release/stats.ts
+++ b/scripts/release/stats.ts
@@ -22,10 +22,10 @@ type ChangeStats = Pick<
 
 import chalk from 'chalk-template';
 
-import { walk } from '../../utils/index.js';
+import { exec, walk } from '../../utils/index.js';
 import pluralize from '../lib/pluralize.js';
 
-import { exec, queryPRs, githubAPI } from './utils.js';
+import { queryPRs, githubAPI } from './utils.js';
 
 /**
  * Get stargazers for the repository
diff --git a/scripts/release/utils.ts b/scripts/release/utils.ts
index 9a337369538ed3..ee83c1104ab2b3 100644
--- a/scripts/release/utils.ts
+++ b/scripts/release/utils.ts
@@ -1,50 +1,7 @@
 /* This file is a part of @mdn/browser-compat-data
  * See LICENSE file for more information. */
 
-import {
-  execSync,
-  ExecSyncOptionsWithStringEncoding,
-  spawnSync,
-  SpawnSyncOptionsWithStringEncoding,
-} from 'node:child_process';
-
-/**
- * Execute a command
- * @param command The command to execute
- * @param opts The options to pass to execSync
- * @returns The output from the command
- */
-export const exec = (
-  command: string,
-  opts?: ExecSyncOptionsWithStringEncoding,
-): string => execSync(command, { encoding: 'utf8', ...opts }).trim();
-
-/**
- * Execute a command
- * @param command The command to execute
- * @param args The arguments to pass
- * @param opts The options to pass to spawnSync
- * @returns The output from the command
- */
-export const spawn = (
-  command: string,
-  args: readonly string[],
-  opts?: SpawnSyncOptionsWithStringEncoding,
-): string => {
-  const result = spawnSync(command, args, { encoding: 'utf8', ...opts });
-
-  if (result.error) {
-    throw result.error;
-  }
-
-  if (result.status !== 0) {
-    throw new Error(
-      `The command '${command}' returned non-zero exit status ${result.status}: ${result.stderr}`,
-    );
-  }
-
-  return result.stdout.trim();
-};
+import { exec } from '../../utils/index.js';
 
 /**
  * Check for GitHub CLI and exit the program if it's not existent
@@ -52,7 +9,7 @@ export const spawn = (
 export const requireGitHubCLI = (): void => {
   const command = 'gh auth status';
   try {
-    execSync(command, { encoding: 'utf8', stdio: 'ignore' });
+    exec(command, { stdio: 'ignore' });
   } catch (err) {
     console.trace(err);
     console.error(`Error: ${command} failed.`);
diff --git a/utils/exec-async.ts b/utils/exec-async.ts
new file mode 100644
index 00000000000000..ab47d217e5b774
--- /dev/null
+++ b/utils/exec-async.ts
@@ -0,0 +1,13 @@
+import { exec } from 'node:child_process';
+import { promisify } from 'node:util';
+
+/**
+ * Executes a command asynchronously.
+ * @param command The command to execute asynchronously.
+ * @returns The output of the command.
+ */
+export default async (command: string): Promise => {
+  const result = await promisify(exec)(command, { encoding: 'utf-8' });
+
+  return result.stdout.trim();
+};
diff --git a/utils/exec.ts b/utils/exec.ts
new file mode 100644
index 00000000000000..880067a907b6c3
--- /dev/null
+++ b/utils/exec.ts
@@ -0,0 +1,15 @@
+import {
+  execSync,
+  ExecSyncOptionsWithStringEncoding,
+} from 'node:child_process';
+
+/**
+ * Execute a command
+ * @param command The command to execute
+ * @param opts The options to pass to execSync
+ * @returns The output from the command
+ */
+export default (
+  command: string,
+  opts?: Omit,
+): string => execSync(command, { encoding: 'utf8', ...opts }).trim();
diff --git a/utils/index.ts b/utils/index.ts
index 4aa2ae4602cddd..5b88a505422dbb 100644
--- a/utils/index.ts
+++ b/utils/index.ts
@@ -1,9 +1,12 @@
 /* This file is a part of @mdn/browser-compat-data
  * See LICENSE file for more information. */
 
+import exec from './exec.js';
+import execAsync from './exec-async.js';
 import iterSupport from './iter-support.js';
 import query from './query.js';
+import spawn from './spawn.js';
 import walk from './walk.js';
 import normalizePath from './normalize-path.js';
 
-export { iterSupport, normalizePath, query, walk };
+export { exec, execAsync, iterSupport, normalizePath, query, spawn, walk };
diff --git a/utils/spawn.ts b/utils/spawn.ts
new file mode 100644
index 00000000000000..77a9e44d9a3938
--- /dev/null
+++ b/utils/spawn.ts
@@ -0,0 +1,31 @@
+import {
+  spawnSync,
+  SpawnSyncOptionsWithStringEncoding,
+} from 'node:child_process';
+
+/**
+ * Execute a command
+ * @param command The command to execute
+ * @param args The arguments to pass
+ * @param opts The options to pass to spawnSync
+ * @returns The output from the command
+ */
+export default (
+  command: string,
+  args: readonly string[],
+  opts?: SpawnSyncOptionsWithStringEncoding,
+): string => {
+  const result = spawnSync(command, args, { encoding: 'utf8', ...opts });
+
+  if (result.error) {
+    throw result.error;
+  }
+
+  if (result.status !== 0) {
+    throw new Error(
+      `The command '${command}' returned non-zero exit status ${result.status}: ${result.stderr}`,
+    );
+  }
+
+  return result.stdout.trim();
+};

From 12650ba514ccad62058545f9b110fee3481aafe5 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 20:32:38 +0100
Subject: [PATCH 38/54] enhance(diff-flat): fetch and resolve head/base

---
 scripts/diff-flat.ts | 23 +++++++++++++++++------
 1 file changed, 17 insertions(+), 6 deletions(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index 079626d9d942f3..0ca115cbf7cbe5 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -9,7 +9,7 @@ import yargs from 'yargs';
 import { hideBin } from 'yargs/helpers';
 
 import { CompatData, SimpleSupportStatement } from '../types/types.js';
-import { exec, walk } from '../utils/index.js';
+import { exec, execAsync, walk } from '../utils/index.js';
 
 import { applyMirroring } from './build/index.js';
 import { getMergeBase, getFileContent, getGitDiffStatuses } from './lib/git.js';
@@ -572,12 +572,23 @@ if (esMain(import.meta)) {
     [options.base, options.head] = [options.head, 'origin/main'];
   }
 
-  const { base, head, group, html, mirror } = options;
+  const fetchAndResolve = (ref: string) => {
+    if (ref.startsWith('origin/')) {
+      const remoteRef = ref.slice('origin/'.length);
+      exec(`git fetch origin ${remoteRef}`);
+      return exec(`git rev-parse ${ref}`);
+    } else if (ref.startsWith('pull/')) {
+      exec(`git fetch origin ${ref}`);
+      return exec('git rev-parse FETCH_HEAD');
+    }
+
+    return exec(`git rev-parse ${ref}`);
+  };
+
+  options.base = fetchAndResolve(options.base);
+  options.head = fetchAndResolve(options.head);
 
-  exec(`
-    git fetch origin ${base} 2>/dev/null || true &
-    git fetch origin ${head} 2>/dev/null || true
-  `);
+  const { base, head, group, html, mirror } = options;
 
   printDiffs(getMergeBase(base, head), head, { group, html, mirror });
 }

From a85e6732f449c5293bd10350223c805c26458f39 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 20:39:37 +0100
Subject: [PATCH 39/54] enhance(diff-flat): support fork:branch ref

---
 scripts/diff-flat.ts | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index 0ca115cbf7cbe5..e864a68506a47e 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -580,6 +580,10 @@ if (esMain(import.meta)) {
     } else if (ref.startsWith('pull/')) {
       exec(`git fetch origin ${ref}`);
       return exec('git rev-parse FETCH_HEAD');
+    } else if (ref.includes(':')) {
+      const remoteRef = `gh pr view ${ref} --json headRefOid -q '.headRefOid'`;
+      exec(`git fetch origin ${remoteRef}`);
+      return remoteRef;
     }
 
     return exec(`git rev-parse ${ref}`);

From 2082fcd4cf1d2961432ed8d52c632e3f40e0bd2b Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 20:47:21 +0100
Subject: [PATCH 40/54] refactor(diff-flat): extract gitFetch() + gitRevParse()

---
 scripts/diff-flat.ts | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 deletions(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index e864a68506a47e..f4d832750ef685 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -9,7 +9,7 @@ import yargs from 'yargs';
 import { hideBin } from 'yargs/helpers';
 
 import { CompatData, SimpleSupportStatement } from '../types/types.js';
-import { exec, execAsync, walk } from '../utils/index.js';
+import { exec, walk } from '../utils/index.js';
 
 import { applyMirroring } from './build/index.js';
 import { getMergeBase, getFileContent, getGitDiffStatuses } from './lib/git.js';
@@ -572,21 +572,24 @@ if (esMain(import.meta)) {
     [options.base, options.head] = [options.head, 'origin/main'];
   }
 
+  const gitFetch = (ref: string) => exec(`git fetch origin ${ref}`);
+  const gitRevParse = (ref: string) => exec(`git rev-parse ${ref}`);
+
   const fetchAndResolve = (ref: string) => {
     if (ref.startsWith('origin/')) {
       const remoteRef = ref.slice('origin/'.length);
-      exec(`git fetch origin ${remoteRef}`);
-      return exec(`git rev-parse ${ref}`);
+      gitFetch(remoteRef);
+      return gitRevParse(ref);
     } else if (ref.startsWith('pull/')) {
-      exec(`git fetch origin ${ref}`);
-      return exec('git rev-parse FETCH_HEAD');
+      gitFetch(ref);
+      return gitRevParse('FETCH_HEAD');
     } else if (ref.includes(':')) {
       const remoteRef = `gh pr view ${ref} --json headRefOid -q '.headRefOid'`;
-      exec(`git fetch origin ${remoteRef}`);
+      gitFetch(remoteRef);
       return remoteRef;
     }
 
-    return exec(`git rev-parse ${ref}`);
+    return gitRevParse(ref);
   };
 
   options.base = fetchAndResolve(options.base);

From 3780a92a9d250a8609f5eba2a69fd322ed416e2c Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 20:53:02 +0100
Subject: [PATCH 41/54] fix(diff-flat): support different remote name

---
 scripts/diff-flat.ts | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index f4d832750ef685..ff9a759c393ef6 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -572,7 +572,10 @@ if (esMain(import.meta)) {
     [options.base, options.head] = [options.head, 'origin/main'];
   }
 
-  const gitFetch = (ref: string) => exec(`git fetch origin ${ref}`);
+  const remote = exec(
+    'git remote -v | grep "mdn/browser-compat-data" | awk \'{print $1}\' | uniq',
+  );
+  const gitFetch = (ref: string) => exec(`git fetch ${remote} ${ref}`);
   const gitRevParse = (ref: string) => exec(`git rev-parse ${ref}`);
 
   const fetchAndResolve = (ref: string) => {
@@ -580,6 +583,10 @@ if (esMain(import.meta)) {
       const remoteRef = ref.slice('origin/'.length);
       gitFetch(remoteRef);
       return gitRevParse(ref);
+    } else if (ref.startsWith(`${remote}/`)) {
+      const remoteRef = ref.slice(`${remote}/`.length);
+      gitFetch(remoteRef);
+      return gitRevParse(ref);
     } else if (ref.startsWith('pull/')) {
       gitFetch(ref);
       return gitRevParse('FETCH_HEAD');

From d7d4ce514147160c9b7f6e29d9a2478b93b3d9f2 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 21:42:54 +0100
Subject: [PATCH 42/54] fix(diff-flat): suppress fetch output

---
 scripts/diff-flat.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index ff9a759c393ef6..6f9f0282c9f78d 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -575,7 +575,8 @@ if (esMain(import.meta)) {
   const remote = exec(
     'git remote -v | grep "mdn/browser-compat-data" | awk \'{print $1}\' | uniq',
   );
-  const gitFetch = (ref: string) => exec(`git fetch ${remote} ${ref}`);
+  const gitFetch = (ref: string) =>
+    exec(`git fetch ${remote} ${ref} 2>/dev/null`);
   const gitRevParse = (ref: string) => exec(`git rev-parse ${ref}`);
 
   const fetchAndResolve = (ref: string) => {

From 40bcfc3626281389ecef56a94f68a0798647fceb Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 21:58:46 +0100
Subject: [PATCH 43/54] enhance(diff-flat): align non-grouped values

---
 scripts/diff-flat.ts | 74 +++++++++++++++++++++++++++++++++++++-------
 1 file changed, 62 insertions(+), 12 deletions(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index 6f9f0282c9f78d..5379c022553759 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -208,10 +208,11 @@ const stripAnsiCompare = (a: string, b: string): number =>
 const diffKeys = (
   key: string,
   lastKey: string,
-  options: { fill?: number; html: boolean },
+  options: { fill?: number; html: boolean; raw?: boolean; separator?: string },
 ): string => {
-  const len = key.length;
+  const len = stripAnsi(key).length;
   let fill = options.fill ?? 0;
+  const separator = options.separator ?? '.';
   /**
    * Filters out irrelevant keys.
    * @param part the key part.
@@ -219,25 +220,28 @@ const diffKeys = (
    */
   const keyFilter = (part) => part !== '__compat' && part !== 'support';
   return diffArrays(
-    lastKey.split('.').filter(keyFilter),
-    key.split('.').filter(keyFilter),
+    lastKey.split(separator).filter(keyFilter),
+    key.split(separator).filter(keyFilter),
   )
     .filter((part) => !part.removed)
     .map((part) => {
-      const key = part.value.join('.');
+      const key = part.value.join(separator);
 
       if (part.added) {
         const space = fill && len < fill ? ' '.repeat(fill - len) : '';
         fill = 0;
         return (
-          (options.html ? `${key}` : chalk`{blue ${key}}`) +
-          space
+          (options.raw
+            ? key
+            : options.html
+              ? `${key}`
+              : chalk`{blue ${key}}`) + space
         );
       }
 
       return key;
     })
-    .join('.');
+    .join(separator);
 };
 
 /**
@@ -500,19 +504,65 @@ const printDiffs = (
     } else {
       const [keys, values] = entry;
       if (values.length == 1) {
+        const maxKeyLength = Math.max(...keys.map((key) => key.length));
         for (const key of keys) {
-          const keyDiff = diffKeys(key, previousKey ?? key, options);
+          const keyDiff = diffKeys(key, previousKey ?? key, {
+            ...options,
+            fill: maxKeyLength,
+          });
           console.log(`${keyDiff}`);
           previousKey = key;
         }
-        values.forEach((value) => console.log(`  ${value}`));
+
+        const maxValueLength = Math.max(
+          ...values.map((value) => stripAnsi(value).length),
+        );
+        let previousValue = null;
+        for (const value of values) {
+          const valueDiff = diffKeys(
+            value,
+            previousValue ?? values.at(values.indexOf(value) + 1) ?? value,
+            {
+              ...options,
+              html: false,
+              raw: true,
+              fill: maxValueLength,
+              separator: '=',
+            },
+          );
+          console.log(`  ${valueDiff}`);
+          previousValue = value;
+        }
       } else {
+        const maxKeyLength = Math.max(...keys.map((key) => key.length));
         for (const key of keys) {
-          const keyDiff = diffKeys(key, previousKey ?? key, options);
+          const keyDiff = diffKeys(key, previousKey ?? key, {
+            ...options,
+            fill: maxKeyLength,
+          });
           console.log(`${keyDiff}`);
           previousKey = key;
         }
-        values.forEach((value) => console.log(`  ${value}`));
+
+        const maxValueLength = Math.max(
+          ...values.map((value) => stripAnsi(value).length),
+        );
+        let previousValue = null;
+        for (const value of values) {
+          const valueDiff = diffKeys(
+            value,
+            previousValue ?? values.at(values.indexOf(value) + 1) ?? value,
+            {
+              ...options,
+              html: false,
+              raw: true,
+              fill: maxValueLength,
+              separator: '=',
+            },
+          );
+          console.log(`  ${valueDiff}`);
+          previousValue = value;
+        }
       }
       previousKey = null;
     }

From 1757ec11387eba5c9075749a720749f1363d8381 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 22:01:44 +0100
Subject: [PATCH 44/54] Revert "enhance(diff-flat): align non-grouped values"

This reverts commit 175cc7c4ca166224b306b48eb3c025defa41f479.
---
 scripts/diff-flat.ts | 74 +++++++-------------------------------------
 1 file changed, 12 insertions(+), 62 deletions(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index 5379c022553759..6f9f0282c9f78d 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -208,11 +208,10 @@ const stripAnsiCompare = (a: string, b: string): number =>
 const diffKeys = (
   key: string,
   lastKey: string,
-  options: { fill?: number; html: boolean; raw?: boolean; separator?: string },
+  options: { fill?: number; html: boolean },
 ): string => {
-  const len = stripAnsi(key).length;
+  const len = key.length;
   let fill = options.fill ?? 0;
-  const separator = options.separator ?? '.';
   /**
    * Filters out irrelevant keys.
    * @param part the key part.
@@ -220,28 +219,25 @@ const diffKeys = (
    */
   const keyFilter = (part) => part !== '__compat' && part !== 'support';
   return diffArrays(
-    lastKey.split(separator).filter(keyFilter),
-    key.split(separator).filter(keyFilter),
+    lastKey.split('.').filter(keyFilter),
+    key.split('.').filter(keyFilter),
   )
     .filter((part) => !part.removed)
     .map((part) => {
-      const key = part.value.join(separator);
+      const key = part.value.join('.');
 
       if (part.added) {
         const space = fill && len < fill ? ' '.repeat(fill - len) : '';
         fill = 0;
         return (
-          (options.raw
-            ? key
-            : options.html
-              ? `${key}`
-              : chalk`{blue ${key}}`) + space
+          (options.html ? `${key}` : chalk`{blue ${key}}`) +
+          space
         );
       }
 
       return key;
     })
-    .join(separator);
+    .join('.');
 };
 
 /**
@@ -504,65 +500,19 @@ const printDiffs = (
     } else {
       const [keys, values] = entry;
       if (values.length == 1) {
-        const maxKeyLength = Math.max(...keys.map((key) => key.length));
         for (const key of keys) {
-          const keyDiff = diffKeys(key, previousKey ?? key, {
-            ...options,
-            fill: maxKeyLength,
-          });
+          const keyDiff = diffKeys(key, previousKey ?? key, options);
           console.log(`${keyDiff}`);
           previousKey = key;
         }
-
-        const maxValueLength = Math.max(
-          ...values.map((value) => stripAnsi(value).length),
-        );
-        let previousValue = null;
-        for (const value of values) {
-          const valueDiff = diffKeys(
-            value,
-            previousValue ?? values.at(values.indexOf(value) + 1) ?? value,
-            {
-              ...options,
-              html: false,
-              raw: true,
-              fill: maxValueLength,
-              separator: '=',
-            },
-          );
-          console.log(`  ${valueDiff}`);
-          previousValue = value;
-        }
+        values.forEach((value) => console.log(`  ${value}`));
       } else {
-        const maxKeyLength = Math.max(...keys.map((key) => key.length));
         for (const key of keys) {
-          const keyDiff = diffKeys(key, previousKey ?? key, {
-            ...options,
-            fill: maxKeyLength,
-          });
+          const keyDiff = diffKeys(key, previousKey ?? key, options);
           console.log(`${keyDiff}`);
           previousKey = key;
         }
-
-        const maxValueLength = Math.max(
-          ...values.map((value) => stripAnsi(value).length),
-        );
-        let previousValue = null;
-        for (const value of values) {
-          const valueDiff = diffKeys(
-            value,
-            previousValue ?? values.at(values.indexOf(value) + 1) ?? value,
-            {
-              ...options,
-              html: false,
-              raw: true,
-              fill: maxValueLength,
-              separator: '=',
-            },
-          );
-          console.log(`  ${valueDiff}`);
-          previousValue = value;
-        }
+        values.forEach((value) => console.log(`  ${value}`));
       }
       previousKey = null;
     }

From 9c2d12ec19bdea9d76b33e7890b92bf98a16bfa3 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 22:16:57 +0100
Subject: [PATCH 45/54] fix(diff-flat): print refs after ungrouped values

---
 scripts/diff-flat.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index 6f9f0282c9f78d..b3726bb9781d1e 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -506,6 +506,7 @@ const printDiffs = (
           previousKey = key;
         }
         values.forEach((value) => console.log(`  ${value}`));
+        printRefs(...values);
       } else {
         for (const key of keys) {
           const keyDiff = diffKeys(key, previousKey ?? key, options);
@@ -513,6 +514,7 @@ const printDiffs = (
           previousKey = key;
         }
         values.forEach((value) => console.log(`  ${value}`));
+        printRefs(...values);
       }
       previousKey = null;
     }

From 90e442ad3680f170c57ad87a51ee382c0279336e Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 22:31:28 +0100
Subject: [PATCH 46/54] enhance(diff-flat): support added/deleted files

---
 scripts/diff-flat.ts | 230 +++++++++++++++++++++----------------------
 1 file changed, 114 insertions(+), 116 deletions(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index b3726bb9781d1e..94711894dc6f29 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -265,136 +265,134 @@ const printDiffs = (
       continue;
     }
 
-    // Note that A means Added for git while it means Array for deep-diff
-    if (status.value === 'A') {
-      console.warn("diff:flat doesn't support file additions yet!");
-    } else if (status.value === 'D') {
-      console.warn("diff:flat doesn't support file deletions yet!");
-    } else {
-      const contents = getBaseAndHeadContents(
-        base,
-        status.basePath,
-        head,
-        status.headPath,
-      );
-      if (options.mirror) {
-        for (const feature of walk(undefined, contents.base)) {
-          applyMirroring(feature);
-        }
-        for (const feature of walk(undefined, contents.head)) {
-          applyMirroring(feature);
-        }
+    const baseContents = (
+      status.value !== 'A'
+        ? JSON.parse(getFileContent(base, status.basePath))
+        : {}
+    ) as CompatData;
+    const headContents = (
+      status.value !== 'D'
+        ? JSON.parse(getFileContent(head, status.headPath))
+        : {}
+    ) as CompatData;
+
+    if (options.mirror) {
+      for (const feature of walk(undefined, baseContents)) {
+        applyMirroring(feature);
       }
+      for (const feature of walk(undefined, headContents)) {
+        applyMirroring(feature);
+      }
+    }
 
-      const baseData = flattenObject(contents.base);
-      const headData = flattenObject(contents.head);
+    const baseData = flattenObject(baseContents);
+    const headData = flattenObject(headContents);
 
-      const keys = [
-        ...new Set([
-          ...Object.keys(baseData),
-          ...Object.keys(headData),
-        ]).values(),
-      ].sort();
+    const keys = [
+      ...new Set([
+        ...Object.keys(baseData),
+        ...Object.keys(headData),
+      ]).values(),
+    ].sort();
 
-      if (!keys.length) {
-        continue;
-      }
+    if (!keys.length) {
+      continue;
+    }
 
-      const prefix = diffArrays(
-        keys.at(0)?.split('.') ?? [],
-        keys.at(-1)?.split('.') ?? [],
-      )[0]?.value.join('.');
+    const prefix = diffArrays(
+      keys.at(0)?.split('.') ?? [],
+      keys.at(-1)?.split('.') ?? [],
+    )[0]?.value.join('.');
 
-      const commonName = options.html ? `

${prefix}

` : `${prefix}`; + const commonName = options.html ? `

${prefix}

` : `${prefix}`; - let lastKey = ''; + let lastKey = ''; - for (const key of keys) { - const baseValue = JSON.stringify(baseData[key] ?? null); - const headValue = JSON.stringify(headData[key] ?? null); - if (baseValue === headValue) { - continue; - } - if (!lastKey) { - lastKey = key; - } - const keyDiff = diffKeys( - key.slice(prefix.length), - lastKey.slice(prefix.length), - options, - ); + for (const key of keys) { + const baseValue = JSON.stringify(baseData[key] ?? null); + const headValue = JSON.stringify(headData[key] ?? null); + if (baseValue === headValue) { + continue; + } + if (!lastKey) { + lastKey = key; + } + const keyDiff = diffKeys( + key.slice(prefix.length), + lastKey.slice(prefix.length), + options, + ); - /** - * Checks whether the value is a relevant value. - * @param value the value. - * @returns TRUE if the value is relevant, FALSE otherwise. - */ - const hasValue = (value: any) => - typeof value === 'boolean' || (!!value && value !== 'mirror'); - - const splitRegexp = /(?<=^")|(?<=[\],/ ])|(?=[[,/ ])|(?="$)/; - const valueDiff = diffArrays( - (hasValue(headData[key] ?? null) ? headValue : '').split(splitRegexp), - (hasValue(baseData[key] ?? null) ? baseValue : '').split(splitRegexp), - ) - .map((part) => { - // Note: removed/added is deliberately inversed here, to have additions first. - const value = part.value.join(''); - if (part.removed) { - return options.html - ? `${value}` - : chalk`{green ${value}}`; - } else if (part.added) { - return options.html - ? `${value}` - : chalk`{red ${value}}`; - } + /** + * Checks whether the value is a relevant value. + * @param value the value. + * @returns TRUE if the value is relevant, FALSE otherwise. + */ + const hasValue = (value: any) => + typeof value === 'boolean' || (!!value && value !== 'mirror'); + + const splitRegexp = /(?<=^")|(?<=[\],/ ])|(?=[[,/ ])|(?="$)/; + const valueDiff = diffArrays( + (hasValue(headData[key] ?? null) ? headValue : '').split(splitRegexp), + (hasValue(baseData[key] ?? null) ? baseValue : '').split(splitRegexp), + ) + .map((part) => { + // Note: removed/added is deliberately inversed here, to have additions first. + const value = part.value.join(''); + if (part.removed) { + return options.html + ? `${value}` + : chalk`{green ${value}}`; + } else if (part.added) { + return options.html + ? `${value}` + : chalk`{red ${value}}`; + } - return value; - }) - .join(''); + return value; + }) + .join(''); - const value = valueDiff; + const value = valueDiff; - if (!value.length) { - // e.g. null => "mirror" - continue; - } + if (!value.length) { + // e.g. null => "mirror" + continue; + } - if (options.group) { - const reverseKeyParts = key.split('.').reverse(); - const browser = reverseKeyParts.find((part) => - BROWSER_NAMES.includes(part), - ); - const field = reverseKeyParts.find((part) => !/^\d+$/.test(part)); - const groupKey = `${!browser ? '' : options.html ? `${browser}.` : chalk`{cyan ${browser}}.`}${field} = ${value}`; - const groupValue = key - .split('.') - .map((part) => (part !== browser && part !== field ? part : '{}')) - .reverse() - .filter((value, index) => index > 0 || value !== '{}') - .reverse() - .map((value) => - value !== '{}' - ? value - : options.html - ? '{}' - : chalk`{dim \{\}}`, - ) - .join('.'); - const group = groups.get(groupKey) ?? new Set(); - group.add(groupValue); - groups.set(groupKey, group); - } else { - const change = options.html - ? `${keyDiff} = ${value}` - : chalk`${keyDiff} = ${value}`; - const group = groups.get(commonName) ?? new Set(); - group.add(change); - groups.set(commonName, group); - } - lastKey = key; + if (options.group) { + const reverseKeyParts = key.split('.').reverse(); + const browser = reverseKeyParts.find((part) => + BROWSER_NAMES.includes(part), + ); + const field = reverseKeyParts.find((part) => !/^\d+$/.test(part)); + const groupKey = `${!browser ? '' : options.html ? `${browser}.` : chalk`{cyan ${browser}}.`}${field} = ${value}`; + const groupValue = key + .split('.') + .map((part) => (part !== browser && part !== field ? part : '{}')) + .reverse() + .filter((value, index) => index > 0 || value !== '{}') + .reverse() + .map((value) => + value !== '{}' + ? value + : options.html + ? '{}' + : chalk`{dim \{\}}`, + ) + .join('.'); + const group = groups.get(groupKey) ?? new Set(); + group.add(groupValue); + groups.set(groupKey, group); + } else { + const change = options.html + ? `${keyDiff} = ${value}` + : chalk`${keyDiff} = ${value}`; + const group = groups.get(commonName) ?? new Set(); + group.add(change); + groups.set(commonName, group); } + lastKey = key; } } From 1db936f83fa91afa79eabe479fa9f0d507153488 Mon Sep 17 00:00:00 2001 From: Claas Augner Date: Thu, 21 Nov 2024 22:46:18 +0100 Subject: [PATCH 47/54] enhane(diff-flat): add transform option --- scripts/diff-flat.ts | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts index 94711894dc6f29..ebe8e329443ce9 100644 --- a/scripts/diff-flat.ts +++ b/scripts/diff-flat.ts @@ -11,7 +11,7 @@ import { hideBin } from 'yargs/helpers'; import { CompatData, SimpleSupportStatement } from '../types/types.js'; import { exec, walk } from '../utils/index.js'; -import { applyMirroring } from './build/index.js'; +import { applyMirroring, transformMD } from './build/index.js'; import { getMergeBase, getFileContent, getGitDiffStatuses } from './lib/git.js'; interface Contents { @@ -248,11 +248,17 @@ const diffKeys = ( * @param options.group Whether to group by value, rather than the common feature * @param options.html Whether to output HTML, rather than plain-text * @param options.mirror Whether to apply mirroring, rather than ignore "mirror" values + * @param options.transform Whether to apply transforms */ const printDiffs = ( base: string, head = '', - options: { group: boolean; html: boolean; mirror: boolean }, + options: { + group: boolean; + html: boolean; + mirror: boolean; + transform: boolean; + }, ): void => { if (options.html) { console.log('
');
@@ -284,6 +290,14 @@ const printDiffs = (
         applyMirroring(feature);
       }
     }
+    if (options.transform) {
+      for (const feature of walk(undefined, baseContents)) {
+        transformMD(feature);
+      }
+      for (const feature of walk(undefined, headContents)) {
+        transformMD(feature);
+      }
+    }
 
     const baseData = flattenObject(baseContents);
     const headData = flattenObject(headContents);
@@ -553,6 +567,10 @@ if (esMain(import.meta)) {
         .option('mirror', {
           type: 'boolean',
           default: false,
+        })
+        .option('transform', {
+          type: 'boolean',
+          default: false,
         });
     },
   );
@@ -603,7 +621,12 @@ if (esMain(import.meta)) {
   options.base = fetchAndResolve(options.base);
   options.head = fetchAndResolve(options.head);
 
-  const { base, head, group, html, mirror } = options;
+  const { base, head, group, html, mirror, transform } = options;
 
-  printDiffs(getMergeBase(base, head), head, { group, html, mirror });
+  printDiffs(getMergeBase(base, head), head, {
+    group,
+    html,
+    mirror,
+    transform,
+  });
 }

From a96d4919491d6cc02a6dbbc3e39eb8025e33ffc7 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 23:06:55 +0100
Subject: [PATCH 48/54] fix(diff-flat): omit "+" after non-real support

---
 scripts/diff-flat.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index ebe8e329443ce9..c93bdf72a3e1c6 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -139,7 +139,8 @@ const flattenObject = (
           } = obj[key] as SimpleSupportStatement;
 
           const parts = [
-            version_added && version_added && `${version_added}+`,
+            typeof version_added === 'string' && `${version_added}+`,
+            typeof version_added === 'boolean' && `${version_added}`,
             version_removed && `−${version_removed}`,
             partial_implementation && '(partial)',
             flags,

From b704d2a11498d2fe06c7c5a413c47781eadcd602 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Thu, 21 Nov 2024 23:14:40 +0100
Subject: [PATCH 49/54] enhance(diff-flat): use natural sort

---
 scripts/diff-flat.ts | 19 +++++++++----------
 1 file changed, 9 insertions(+), 10 deletions(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index c93bdf72a3e1c6..51a5f808ea1072 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -188,15 +188,6 @@ const toArray = (value: any): any[] => {
   return value;
 };
 
-/**
- * Compares two strings ignoring ANSI escape codes.
- * @param a one value
- * @param b other value
- * @returns comparison result.
- */
-const stripAnsiCompare = (a: string, b: string): number =>
-  stripAnsi(a).localeCompare(stripAnsi(b));
-
 /**
  * Formats a key diff'ed with the previous key.
  * @param key the current key
@@ -426,8 +417,16 @@ const printDiffs = (
   const rawEntries = [...entryGroups.entries()];
 
   if (options.group) {
+    // Natural sort.
+    const collator = new Intl.Collator(undefined, {
+      numeric: true,
+      sensitivity: 'base',
+    });
     rawEntries.sort(([, a], [, b]) =>
-      stripAnsiCompare(a.at(0) as string, b.at(0) as string),
+      collator.compare(
+        stripAnsi(a.at(0) as string),
+        stripAnsi(b.at(0) as string),
+      ),
     );
   }
 

From d96c5c6b72f63ecffd2f0cbdf7f01bd4a56ef7f0 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Fri, 22 Nov 2024 00:00:21 +0100
Subject: [PATCH 50/54] fix(diff-flat): hide only initial false/mirror values

---
 scripts/diff-flat.ts | 35 +++++++++++++++++++++++++----------
 1 file changed, 25 insertions(+), 10 deletions(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index 51a5f808ea1072..99bd0ca89eddd7 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -84,6 +84,16 @@ const flattenObject = (
     if (Object.prototype.hasOwnProperty.call(obj, key)) {
       const fullKey = parentKey ? `${parentKey}.${key}` : key;
 
+      if (
+        key !== 'version' &&
+        typeof obj[key] === 'string' &&
+        obj[key] === 'mirror'
+      ) {
+        obj[key] = {
+          version: 'mirror',
+        };
+      }
+
       if (typeof obj[key] === 'object' && obj[key] !== null) {
         // Merge values.
         if ('status' in obj[key]) {
@@ -329,18 +339,23 @@ const printDiffs = (
         options,
       );
 
-      /**
-       * Checks whether the value is a relevant value.
-       * @param value the value.
-       * @returns TRUE if the value is relevant, FALSE otherwise.
-       */
-      const hasValue = (value: any) =>
-        typeof value === 'boolean' || (!!value && value !== 'mirror');
-
       const splitRegexp = /(?<=^")|(?<=[\],/ ])|(?=[[,/ ])|(?="$)/;
+      let headValueForDiff = headValue;
+      let baseValueForDiff = baseValue;
+
+      if (baseValue == 'null') {
+        baseValueForDiff = '';
+        if (headValue == '"mirror"' || headValue == '"false"') {
+          // Ignore initial "mirror"/"false" values.
+          headValueForDiff = '';
+        }
+      } else if (headValue == 'null') {
+        headValueForDiff = '';
+      }
+
       const valueDiff = diffArrays(
-        (hasValue(headData[key] ?? null) ? headValue : '').split(splitRegexp),
-        (hasValue(baseData[key] ?? null) ? baseValue : '').split(splitRegexp),
+        headValueForDiff.split(splitRegexp),
+        baseValueForDiff.split(splitRegexp),
       )
         .map((part) => {
           // Note: removed/added is deliberately inversed here, to have additions first.

From 04b18331d6e7701329bf6706dffa090a44256596 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Mon, 25 Nov 2024 15:45:11 +0100
Subject: [PATCH 51/54] fix(diff-flat): improve version range formatting

---
 scripts/diff-flat.ts | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index 99bd0ca89eddd7..74f93bb0f06718 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -149,9 +149,11 @@ const flattenObject = (
           } = obj[key] as SimpleSupportStatement;
 
           const parts = [
-            typeof version_added === 'string' && `${version_added}+`,
-            typeof version_added === 'boolean' && `${version_added}`,
-            version_removed && `−${version_removed}`,
+            typeof version_added === 'string'
+              ? typeof version_removed === 'string'
+                ? `${version_added}−${version_removed}`
+                : `${version_added}+`
+              : `${version_added}`,
             partial_implementation && '(partial)',
             flags,
             prefix && `prefix=${prefix}`,

From 0e5a01e5a6a8c64552f3c2fa4afb6a613802b53f Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Mon, 25 Nov 2024 16:10:09 +0100
Subject: [PATCH 52/54] enhance(diff-flat): use version_last

---
 scripts/diff-flat.ts | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index 74f93bb0f06718..d99d4df4775922 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -11,7 +11,7 @@ import { hideBin } from 'yargs/helpers';
 import { CompatData, SimpleSupportStatement } from '../types/types.js';
 import { exec, walk } from '../utils/index.js';
 
-import { applyMirroring, transformMD } from './build/index.js';
+import { addVersionLast, applyMirroring, transformMD } from './build/index.js';
 import { getMergeBase, getFileContent, getGitDiffStatuses } from './lib/git.js';
 
 interface Contents {
@@ -140,7 +140,7 @@ const flattenObject = (
 
           const {
             version_added,
-            version_removed,
+            version_last,
             partial_implementation,
             alternative_name,
             prefix,
@@ -150,8 +150,8 @@ const flattenObject = (
 
           const parts = [
             typeof version_added === 'string'
-              ? typeof version_removed === 'string'
-                ? `${version_added}−${version_removed}`
+              ? typeof version_last === 'string'
+                ? `${version_added}−${version_last}`
                 : `${version_added}+`
               : `${version_added}`,
             partial_implementation && '(partial)',
@@ -163,6 +163,7 @@ const flattenObject = (
 
           obj[key].version = parts.join(' ');
           delete obj[key].version_added;
+          delete obj[key].version_last;
           delete obj[key].version_removed;
           delete obj[key].partial_implementation;
           delete obj[key].alternative_name;
@@ -294,6 +295,12 @@ const printDiffs = (
         applyMirroring(feature);
       }
     }
+    for (const feature of walk(undefined, baseContents)) {
+      addVersionLast(feature);
+    }
+    for (const feature of walk(undefined, headContents)) {
+      addVersionLast(feature);
+    }
     if (options.transform) {
       for (const feature of walk(undefined, baseContents)) {
         transformMD(feature);

From 343095de7bc30d1fcf9d727cec9eed7fa3acb838 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Fri, 6 Dec 2024 11:01:42 +0100
Subject: [PATCH 53/54] chore(flat-diff): enable grouping by default

---
 scripts/diff-flat.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/diff-flat.ts b/scripts/diff-flat.ts
index d99d4df4775922..9564b7f47d8d10 100644
--- a/scripts/diff-flat.ts
+++ b/scripts/diff-flat.ts
@@ -586,7 +586,7 @@ if (esMain(import.meta)) {
         })
         .option('group', {
           type: 'boolean',
-          default: false,
+          default: true,
         })
         .option('mirror', {
           type: 'boolean',

From b7e53de5a9da3dcc22a39e54f26d3efe080530c4 Mon Sep 17 00:00:00 2001
From: Claas Augner 
Date: Mon, 9 Dec 2024 14:49:02 +0100
Subject: [PATCH 54/54] chore(deps): run `npm install`

---
 package-lock.json | 25 +++++++++++++------------
 1 file changed, 13 insertions(+), 12 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 1af368b7f6ec07..9672d2a96d8d01 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6950,6 +6950,19 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/ora/node_modules/ansi-regex": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+      "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
     "node_modules/ora/node_modules/emoji-regex": {
       "version": "10.4.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
@@ -7028,18 +7041,6 @@
         "url": "https://github.com/chalk/strip-ansi?sponsor=1"
       }
     },
-    "node_modules/os-filter-obj": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz",
-      "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==",
-      "dev": true,
-      "dependencies": {
-        "arch": "^2.1.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/p-cancelable": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",