From d030c0f937393449eb939ec9887880d417983312 Mon Sep 17 00:00:00 2001 From: Vladimir Morozov Date: Wed, 1 Apr 2026 10:29:19 -0700 Subject: [PATCH 1/2] Fork ubroken module (#15892) --- docs/react-native-windows-init.md | 2 +- package.json | 2 +- packages/@rnw-scripts/unbroken/.eslintrc.js | 4 + packages/@rnw-scripts/unbroken/.gitignore | 2 + .../@rnw-scripts/unbroken/.prettierignore | 2 + packages/@rnw-scripts/unbroken/LICENSE | 22 + packages/@rnw-scripts/unbroken/bin.js | 12 + packages/@rnw-scripts/unbroken/package.json | 44 ++ packages/@rnw-scripts/unbroken/src/checker.ts | 449 ++++++++++++++++++ .../@rnw-scripts/unbroken/src/unbroken.ts | 104 ++++ packages/@rnw-scripts/unbroken/tsconfig.json | 5 + yarn.lock | 115 +---- 12 files changed, 648 insertions(+), 115 deletions(-) create mode 100644 packages/@rnw-scripts/unbroken/.eslintrc.js create mode 100644 packages/@rnw-scripts/unbroken/.gitignore create mode 100644 packages/@rnw-scripts/unbroken/.prettierignore create mode 100644 packages/@rnw-scripts/unbroken/LICENSE create mode 100644 packages/@rnw-scripts/unbroken/bin.js create mode 100644 packages/@rnw-scripts/unbroken/package.json create mode 100644 packages/@rnw-scripts/unbroken/src/checker.ts create mode 100644 packages/@rnw-scripts/unbroken/src/unbroken.ts create mode 100644 packages/@rnw-scripts/unbroken/tsconfig.json diff --git a/docs/react-native-windows-init.md b/docs/react-native-windows-init.md index 9b7b44a8a48..5f432f49864 100644 --- a/docs/react-native-windows-init.md +++ b/docs/react-native-windows-init.md @@ -28,7 +28,7 @@ Alternatives considered: * T4: This would have required visual studio to be installed on the users machine and therefore only work on Windows. There were also some perf concerns and we would have to write and ship a standalone executable for the MSBuild tasks so we can call it from JavaScript. ### Quick mustache tutorial: -For proper docs see the [manual](http://mustache.github.io/mustache.5.html) of [mustache](http://mustache.github.io/) +For proper docs see the [manual](https://mustache.github.io/mustache.5.html) of [mustache](https://mustache.github.io/) But in short: You run `mustache` via `const text = mustache.render(inputText, obj);` where obj is a regular JavaScript object. For example: diff --git a/package.json b/package.json index 1f7052d2cb3..7a95dcb3ef8 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "husky": "^4.2.5", "prettier-plugin-hermes-parser": "0.21.1", "react-native-platform-override": "0.0.0-canary.1017", - "unbroken": "1.0.27", + "@rnw-scripts/unbroken": "*", "lage": "^2.7.1", "lodash": "^4.17.15" }, diff --git a/packages/@rnw-scripts/unbroken/.eslintrc.js b/packages/@rnw-scripts/unbroken/.eslintrc.js new file mode 100644 index 00000000000..35e0d115126 --- /dev/null +++ b/packages/@rnw-scripts/unbroken/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + extends: ['@rnw-scripts'], + parserOptions: {tsconfigRootDir : __dirname}, +}; diff --git a/packages/@rnw-scripts/unbroken/.gitignore b/packages/@rnw-scripts/unbroken/.gitignore new file mode 100644 index 00000000000..f42efbb9f7c --- /dev/null +++ b/packages/@rnw-scripts/unbroken/.gitignore @@ -0,0 +1,2 @@ +lib/ +lib-commonjs/ diff --git a/packages/@rnw-scripts/unbroken/.prettierignore b/packages/@rnw-scripts/unbroken/.prettierignore new file mode 100644 index 00000000000..2d65a1c61da --- /dev/null +++ b/packages/@rnw-scripts/unbroken/.prettierignore @@ -0,0 +1,2 @@ +lib-commonjs +bin.js diff --git a/packages/@rnw-scripts/unbroken/LICENSE b/packages/@rnw-scripts/unbroken/LICENSE new file mode 100644 index 00000000000..c13baa48cb1 --- /dev/null +++ b/packages/@rnw-scripts/unbroken/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020-2022 Alexander Sklar +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/@rnw-scripts/unbroken/bin.js b/packages/@rnw-scripts/unbroken/bin.js new file mode 100644 index 00000000000..e3d15d2a02f --- /dev/null +++ b/packages/@rnw-scripts/unbroken/bin.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node + +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * Forked from https://github.com/asklar/unbroken + * Original Copyright (c) 2020-2022 Alexander Sklar + */ + +process.setSourceMapsEnabled(true); +require('./lib-commonjs/unbroken'); diff --git a/packages/@rnw-scripts/unbroken/package.json b/packages/@rnw-scripts/unbroken/package.json new file mode 100644 index 00000000000..b2ead139d70 --- /dev/null +++ b/packages/@rnw-scripts/unbroken/package.json @@ -0,0 +1,44 @@ +{ + "name": "@rnw-scripts/unbroken", + "version": "0.0.1", + "private": true, + "description": "Detect broken links in markdown files. Forked from https://github.com/asklar/unbroken", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/react-native-windows", + "directory": "packages/@rnw-scripts/unbroken" + }, + "scripts": { + "build": "rnw-scripts build", + "clean": "rnw-scripts clean", + "lint": "rnw-scripts lint", + "lint:fix": "rnw-scripts lint:fix", + "watch": "rnw-scripts watch" + }, + "main": "lib-commonjs/unbroken.js", + "bin": { + "unbroken": "./bin.js" + }, + "dependencies": { + "@react-native-windows/fs": "^0.0.0-canary.70" + }, + "devDependencies": { + "@rnw-scripts/eslint-config": "1.2.38", + "@rnw-scripts/just-task": "2.3.58", + "@rnw-scripts/ts-config": "2.0.6", + "@types/node": "^22.14.0", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "eslint": "^8.19.0", + "prettier": "^3.0.0", + "typescript": "5.0.4" + }, + "files": [ + "bin.js", + "lib-commonjs" + ], + "engines": { + "node": ">= 22" + } +} diff --git a/packages/@rnw-scripts/unbroken/src/checker.ts b/packages/@rnw-scripts/unbroken/src/checker.ts new file mode 100644 index 00000000000..f5ebf392ce2 --- /dev/null +++ b/packages/@rnw-scripts/unbroken/src/checker.ts @@ -0,0 +1,449 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * Forked from https://github.com/asklar/unbroken + * Original Copyright (c) 2020-2022 Alexander Sklar + * + * @format + */ + +import * as path from 'path'; + +import fs from '@react-native-windows/fs'; + +function yellowBold(text: string): string { + return `\x1b[1m\x1b[33m${text}\x1b[0m`; +} + +function redBold(text: string): string { + return `\x1b[1m\x1b[31m${text}\x1b[0m`; +} + +async function msleep(n: number) { + return new Promise(resolve => setTimeout(resolve, n)); +} + +const DefaultUserAgent = 'Chrome/89.0.4346.0'; + +/** + * Set of HTTP status codes that indicate transient server errors worth + * retrying, since the server may recover on a subsequent attempt. + */ +const TRANSIENT_HTTP_ERRORS = new Set([502, 503, 504]); + +export interface Options { + quiet: boolean; + superquiet: boolean; + dir: string; + exclusions: string; + 'local-only': boolean; + 'allow-local-line-sections': boolean; + 'parse-ids': boolean; + 'user-agent': string; +} + +export class Checker { + constructor(public readonly options: Options) { + this.errors = []; + this.options = options; + this.ids = {}; + this.urlCache = {}; + if (this.options.superquiet) { + this.options.quiet = true; + } + + // Normalize options.dir for later + this.options.dir = path.resolve(this.options.dir || '.'); + + const exclusionsFileName = + this.options.exclusions || + path.join(this.options.dir, '.unbroken_exclusions'); + try { + const contents = + fs + .readFileSync(exclusionsFileName) + .toString() + .split(/\r?\n/) + .filter(x => x.trim() !== ''); + this.suppressions = contents + .filter(x => !x.startsWith('!')) + .map(x => Checker.normalizeSlashes(x)); + this.exclusions = contents + .filter(x => x.startsWith('!')) + .map(x => Checker.normalizeSlashes(path.normalize(x.slice(1)))); + } catch { + this.suppressions = []; + this.exclusions = []; + } + } + + errors: string[]; + ids: Record; + suppressions: string[]; + exclusions: string[]; + urlCache: Record; + + + private async recurseFindMarkdownFiles( + dirPath: string, + callback: {(path: string): Promise}, + ) { + const files = await fs.readdir(dirPath); + await asyncForEach( + files, + async (file: string) => { + const filePath = path.join(dirPath, file); + const relFilePath = Checker.normalizeSlashes( + this.getRelativeFilePath(filePath), + ); + const shouldSkip = this.exclusions.some(pattern => + path.matchesGlob(relFilePath, pattern), + ); + if (shouldSkip) { + this.log(`Skipping ${Checker.normalizeSlashes(relFilePath)}.`); + } else { + const stat = fs.statSync(filePath); + if (stat.isDirectory()) { + await this.recurseFindMarkdownFiles(filePath, callback); + } else if (filePath.toLowerCase().endsWith('.md')) { + await callback(filePath); + } + } + }, + true, + ); + } + + private async getAndStoreId(idPath: string) { + const lines = fs + .readFileSync(idPath) + .toString() + .split(/[\r\n]+/g); + if ( + lines.length > 2 && + lines[0].trim() === '---' && + lines[1].toLowerCase().startsWith('id:') + ) { + const id = lines[1].slice('id:'.length).trim(); + this.ids[id] = idPath; + } + } + + async run(dirPath?: string) { + if (!dirPath) { + dirPath = this.options.dir; + } + + if (this.options['parse-ids']) { + await this.recurseFindMarkdownFiles(dirPath, x => + this.getAndStoreId(x), + ); + } + await this.recurseFindMarkdownFiles(dirPath, x => + this.verifyMarkDownFile(x), + ); + let n = 0; + this.errors.forEach(err => { + if (this.suppressions.includes(err)) { + this.log(yellowBold('WARNING:'), err); + } else { + this.logError(err); + n++; + } + }); + return n; + } + + private log(...args: string[]) { + if (!this.options.quiet) { + console.log(args.join(' ')); + } + } + + private logError(error: string) { + if (!this.options.superquiet) { + console.log(redBold('ERROR:'), error); + } + } + + private getRelativeFilePath(filePath: string) { + return filePath.substr(this.options.dir.length + 1); + } + + private static normalizeSlashes(str: string) { + return str.replace(/\\/g, '/'); + } + + private validateFile(name: string, value: string, filePath: string) { + const dir = path.dirname(filePath); + const pathToCheck = path.join(dir, value); + if (!fs.existsSync(pathToCheck)) { + const pathToCheckReplaced = path.join(dir, value.replace(/_/g, '-')); + if (!fs.existsSync(pathToCheckReplaced)) { + if (!this.ids[value]) { + this.errors.push( + `File not found ${Checker.normalizeSlashes( + path.normalize(value), + )} while parsing ${Checker.normalizeSlashes( + this.getRelativeFilePath(filePath), + )}`, + ); + return undefined; + } else { + return this.ids[value]; + } + } else { + return pathToCheckReplaced; + } + } + return path.normalize(pathToCheck); + } + + private getAnchors(content: string): string[] { + const anchorRegex = /(^#+|\n+#+)\s*(?[^\s].*)/g; + const anchors: string[] = []; + const results = content.matchAll(anchorRegex); + for (const result of results) { + const title = result.groups!.anchorTitle; + const transformed = title.replace(/[^\w\d\s-]+/g, '').replace(/ /g, '-'); + let newItem = transformed; + const found = anchors.includes(transformed); + if (found) { + const regex = new RegExp(`${transformed}-\\d+`); + const lastIndex = anchors.find(x => regex.test(x)); + const newIndex = lastIndex ? lastIndex.length + 1 : 1; + newItem = transformed + '-' + newIndex; + } + anchors.push(newItem); + } + return anchors; + } + + private validateSection( + name: string, + value: string, + contents: string | null, + filePath: string, + ) { + const hash = value.indexOf('#'); + const sectionAnchor = value.substring(hash + 1); + const page = value.substring(0, hash); + let extra = ''; + if (page !== '') { + extra = ` while parsing ${Checker.normalizeSlashes( + this.getRelativeFilePath(filePath), + )}`; + const realFilePath = this.validateFile(name, page, filePath); + if (realFilePath) { + contents = fs.readFileSync(realFilePath).toString(); + } else { + // file doesn't exist - already logged that the file is missing + return; + } + } + if (!contents) { + this.errors.push("Couldn't read contents"); + return; + } + + const anchors = this.getAnchors(contents.toLowerCase()); + if (!anchors.includes(sectionAnchor.toLowerCase())) { + if ( + !anchors.includes(sectionAnchor.replace(/\./g, '').toLowerCase()) + ) { + if ( + !( + this.options['allow-local-line-sections'] && + !Checker.isWebLink(value) && + sectionAnchor.length > 1 && + sectionAnchor.startsWith('L') && + !isNaN(parseInt(sectionAnchor.substring(1), 10)) + ) + ) { + this.errors.push( + `Section ${sectionAnchor} not found in ${Checker.normalizeSlashes( + this.getRelativeFilePath(filePath), + )}${extra}. Available anchors: ${JSON.stringify(anchors)}`, + ); + } + } + } + } + + private async validateURL(name: string, value: string, filePath: string) { + const maxIterations = 5; + const ignoring429 = + this.suppressions.findIndex(x => x === 'HTTP/429') !== -1; + const relativeFilePath = Checker.normalizeSlashes( + this.getRelativeFilePath(filePath), + ); + + let result: number | undefined = + value in this.urlCache ? this.urlCache[value] : undefined; + + // spin while pending validates finish + while (result === -1) { + await msleep(100); + result = this.urlCache[value]; + } + + if (result === 200) { + // Previous success, bail early + return true; + } else if (result === undefined) { + // No previous result, actually check URL + this.urlCache[value] = -1; // block other validates until this is finished + this.log(`Verifying ${value} for ${relativeFilePath}`); + for (let i = 0; i < maxIterations; i++) { + if (i > 0) { + this.log(`Retrying ${value} for ${relativeFilePath}, attempt #${i}`); + } + + try { + const userAgent = this.options['user-agent'] || DefaultUserAgent; + const r = await fetch(value, { + headers: { + 'User-Agent': userAgent, + 'Accept-Encoding': 'gzip, deflate, br', + }, + redirect: 'follow', + }); + const status = r.status; + + if (status === 200) { + result = status; + break; + } + + let sleepSeconds = 0; + if (TRANSIENT_HTTP_ERRORS.has(status)) { + // Transient server error - retry with backoff + sleepSeconds = (i + 1) * 2; + this.log( + `HTTP/${status} for ${value}, transient error, sleeping for ${sleepSeconds}s before retry`, + ); + } else if (status === 429 && !ignoring429) { + // Being throttled with HTTP/429 + const retryAfterSeconds = r.headers.has('retry-after') + ? parseInt(r.headers.get('retry-after')!, 10) + : 0; + + sleepSeconds = ((i + 1) / maxIterations) * retryAfterSeconds; + this.log( + `HTTP/429 for ${value}, requested retry after ${retryAfterSeconds}s, sleeping for ${sleepSeconds}s`, + ); + } else { + // Non-transient error (or a 429 we're ignoring) - bail + result = status; + break; + } + await msleep(100 + sleepSeconds * 1000); + } catch { + // Network error (DNS failure, connection refused, etc.) - retry + await msleep(100); + } + } // for + } // else if + + // Normalize result to -1 sentinel if we hit the max retries + result = result === undefined ? -1 : result; + + // Save result + this.urlCache[value] = result; + + if (result === 200 || (result === 429 && ignoring429)) { + return true; + } else if (result === -1) { + this.errors.push( + `URL not found ${value} while parsing ${relativeFilePath} after ${maxIterations} retries`, + ); + return false; + } else { + this.errors.push( + `URL not found ${value} while parsing ${relativeFilePath} (HTTP ${result})`, + ); + return false; + } + } + + private async validateLink( + name: string, + value: string, + contents: string | null, + filePath: string, + ) { + if (value.startsWith('mailto:')) { + // Not implemented + } else if (Checker.isWebLink(value)) { + if (!this.options['local-only']) { + await this.validateURL(name, value, filePath); + } + } else if (value.includes('#')) { + this.validateSection(name, value, contents, filePath); + } else { + this.validateFile(name, value, filePath); + } + } + + private static isWebLink(url: string) { + return url.startsWith("https://") || url.startsWith('https://'); + } + + async verifyMarkDownFile(filePath: string) { + this.log( + `Verifying ${Checker.normalizeSlashes( + this.getRelativeFilePath(filePath), + )}`, + ); + const contents = fs.readFileSync(filePath).toString(); + + const balancedParensTwoLevels = '([^(\\s]*\\([^)\\s]*\\))?[^()\\s]*'; + const baseLink = `\\[(?=([^\`]*\`[^\`]*\`)*[^\`]*$)(?[^\\]]+)\\]\\s*\\((?${balancedParensTwoLevels}?)("(?[^"]*)")?\\)`; + const imgLink = `(?\\[\\!)${baseLink.replace( + /BASE/g, + 'img', + )}(?\\]\\((?[^)]+)\\))`; + const nonImgLink = baseLink.replace(/BASE/g, ''); + const mdLinkRegex = new RegExp(`(${imgLink})|(${nonImgLink})`, 'g'); + + const results = contents.matchAll(mdLinkRegex); + + let imgSrc: string | undefined; + for (const result of results) { + const groups = result.groups!; + let name = groups.name; + let value = groups.value; + + if (groups.imageLinkTag) { + name = groups.nameimg; + value = groups.linkTarget; + imgSrc = groups.valueimg; + } + + await this.validateLink(name, value, contents, filePath); + if (imgSrc) { + await this.validateLink(name, imgSrc, null, filePath); + } + } + } +} + +async function asyncForEach( + array: string[], + callback: {(file: string, index: number, arr: string[]): Promise}, + parallel: boolean, +) { + const calls = []; + for (let index = 0; index < array.length; index++) { + const call = callback(array[index], index, array); + if (parallel) { + calls.push(call); + } else { + await call; + } + } + if (parallel) { + await Promise.all(calls); + } +} diff --git a/packages/@rnw-scripts/unbroken/src/unbroken.ts b/packages/@rnw-scripts/unbroken/src/unbroken.ts new file mode 100644 index 00000000000..50dc20489cb --- /dev/null +++ b/packages/@rnw-scripts/unbroken/src/unbroken.ts @@ -0,0 +1,104 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * Forked from https://github.com/asklar/unbroken + * Original Copyright (c) 2020-2022 Alexander Sklar + * + * @format + */ + +import {parseArgs} from 'node:util'; +import * as path from 'path'; + +import fs from '@react-native-windows/fs'; + +import {Checker, Options} from './checker'; + +// --------------------------------------------------------------------------- +// CLI help +// --------------------------------------------------------------------------- + +function showHelp(): void { + const version = JSON.parse( + fs.readFileSync(path.join(__dirname, '../package.json')).toString(), + ).version; + + console.log(` +unbroken ${version} - no more broken links in markdown! + +Usage: + npx unbroken [directory] [options] + +Options: + -e, --exclusions The exclusions file (default: .unbroken_exclusions) + -l, --local-only Do not test http and https links + -a, --allow-local-line-sections Allow line sections like foo.cpp#L12 + -i, --init Create a default exclusions file if one doesn't exist + -q, --quiet Suppress output + -s, --superquiet Maximum silence + --parse-ids Allow anchors to point to Docusaurus id aliases + -u, --user-agent Custom User-Agent string + --help Show this help message + +Project home: https://github.com/asklar/unbroken +`); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function run() { + const {values, positionals} = parseArgs({ + options: { + exclusions: {type: 'string', short: 'e'}, + 'local-only': {type: 'boolean', short: 'l', default: false}, + init: {type: 'boolean', short: 'i', default: false}, + 'allow-local-line-sections': {type: 'boolean', short: 'a', default: false}, + quiet: {type: 'boolean', short: 'q', default: false}, + superquiet: {type: 'boolean', short: 's', default: false}, + 'parse-ids': {type: 'boolean', default: false}, + 'user-agent': {type: 'string', short: 'u'}, + help: {type: 'boolean', default: false}, + }, + allowPositionals: true, + }); + + if (values.help) { + showHelp(); + process.exit(0); + } + + if (values.init) { + if (fs.existsSync('.unbroken_exclusions')) { + console.error('.unbroken_exclusions already exists'); + process.exit(-1); + } else { + fs.writeFileSync('.unbroken_exclusions', '!node_modules'); + process.exit(0); + } + } + + const options: Options = { + dir: positionals[0] || '.', + exclusions: values.exclusions ?? '', + 'local-only': values['local-only']!, + 'allow-local-line-sections': values['allow-local-line-sections']!, + quiet: values.quiet!, + superquiet: values.superquiet!, + 'parse-ids': values['parse-ids']!, + 'user-agent': values['user-agent'] ?? '', + }; + + const c = new Checker(options); + const n = await c.run(); + if (c.errors.length) { + if (!options.superquiet) { + console.log(`${n} errors, ${c.errors.length - n} warnings.`); + } + } + process.exitCode = n; +} + +void run(); diff --git a/packages/@rnw-scripts/unbroken/tsconfig.json b/packages/@rnw-scripts/unbroken/tsconfig.json new file mode 100644 index 00000000000..c62faa78baf --- /dev/null +++ b/packages/@rnw-scripts/unbroken/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@rnw-scripts/ts-config", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/yarn.lock b/yarn.lock index f9192996852..082aac923ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4293,16 +4293,6 @@ aria-query@^5.3.2: resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== -array-back@^3.0.1, array-back@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" - integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== - -array-back@^4.0.1, array-back@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" - integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== - array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" @@ -4505,13 +4495,6 @@ axe-core@^4.10.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.11.0.tgz#16f74d6482e343ff263d4f4503829e9ee91a86b6" integrity sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ== -axios@^0.21.1: - version "0.21.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== - dependencies: - follow-redirects "^1.14.0" - axobject-query@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" @@ -4998,14 +4981,6 @@ chalk@^2.0.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -5306,26 +5281,6 @@ command-exists@^1.2.8: resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== -command-line-args@^5.1.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" - integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== - dependencies: - array-back "^3.1.0" - find-replace "^3.0.0" - lodash.camelcase "^4.3.0" - typical "^4.0.0" - -command-line-usage@^6.1.0: - version "6.1.3" - resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957" - integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw== - dependencies: - array-back "^4.0.2" - chalk "^2.4.2" - table-layout "^1.0.2" - typical "^5.2.0" - commander@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" @@ -5622,11 +5577,6 @@ deep-equal@1.1.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" -deep-extend@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -6702,13 +6652,6 @@ find-cache-dir@^2.0.0: make-dir "^2.0.0" pkg-dir "^3.0.0" -find-replace@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" - integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== - dependencies: - array-back "^3.0.1" - find-up@*: version "8.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-8.0.0.tgz#9e4663f9605eeb615edd7399f376a01b1de312fe" @@ -6811,11 +6754,6 @@ flow-parser@0.*: resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.292.0.tgz#e6b25674a25bd58531dd797c2189ebf2ad9c2e6c" integrity sha512-H7TRIkLYQucAszvp1DhsUMGrlu0ImgKV7eBLQ/wiOqQGDzsllBCWGgaPyw/n1CAw615VRCcRYf6TCYIpemenzw== -follow-redirects@^1.14.0: - version "1.15.11" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" - integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== - for-each@^0.3.3, for-each@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" @@ -8935,11 +8873,6 @@ locate-path@^8.0.0: dependencies: p-locate "^6.0.0" -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== - lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -9844,7 +9777,7 @@ micromark@4.0.1: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromatch@4.0.8, micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.8: +micromatch@4.0.8, micromatch@^4.0.0, micromatch@^4.0.4, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -10879,7 +10812,7 @@ prettier@3.6.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393" integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== -prettier@^3.4.2: +prettier@^3.0.0, prettier@^3.4.2: version "3.8.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== @@ -11285,11 +11218,6 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" -reduce-flatten@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" - integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== - reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" @@ -12324,16 +12252,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -table-layout@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" - integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== - dependencies: - array-back "^4.0.1" - deep-extend "~0.6.0" - typical "^5.2.0" - wordwrapjs "^4.0.0" - tar-fs@^2.0.0: version "2.1.4" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930" @@ -12691,16 +12609,6 @@ typescript@>=4.7.0: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== -typical@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" - integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== - -typical@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" - integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== - ua-parser-js@^0.7.21: version "0.7.41" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.41.tgz#9f6dee58c389e8afababa62a4a2dc22edb69a452" @@ -12731,17 +12639,6 @@ unbox-primitive@^1.1.0: has-symbols "^1.1.0" which-boxed-primitive "^1.1.1" -unbroken@1.0.27: - version "1.0.27" - resolved "https://registry.yarnpkg.com/unbroken/-/unbroken-1.0.27.tgz#230c25e4f7a272b30ff87139289fd974a8ddf2f9" - integrity sha512-KOZ9Ie+0KE2TxwzK8YXr5F3BnuvsBkSzVcc0P67aV3XzPyC2PR+FVPRej2GERm3YIoCNtrxt8VZ62LGMTVXolw== - dependencies: - axios "^0.21.1" - chalk "^3.0.0" - command-line-args "^5.1.1" - command-line-usage "^6.1.0" - micromatch "^4.0.2" - unbzip2-stream@^1.3.3: version "1.4.3" resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" @@ -13123,14 +13020,6 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -wordwrapjs@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" - integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== - dependencies: - reduce-flatten "^2.0.0" - typical "^5.2.0" - workspace-tools@^0.41.0: version "0.41.0" resolved "https://registry.yarnpkg.com/workspace-tools/-/workspace-tools-0.41.0.tgz#b9389f7af1ca79bf102ff613ce21bf2d02c4010a" From ceeed8a4d671fbf7299b061ccdf566917901ca3d Mon Sep 17 00:00:00 2001 From: Vladimir Morozov Date: Wed, 1 Apr 2026 14:34:20 -0700 Subject: [PATCH 2/2] Fix linting issues --- packages/@rnw-scripts/unbroken/src/checker.ts | 22 +++++++------------ .../@rnw-scripts/unbroken/src/unbroken.ts | 6 ++++- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/@rnw-scripts/unbroken/src/checker.ts b/packages/@rnw-scripts/unbroken/src/checker.ts index f5ebf392ce2..95a9b09d7dc 100644 --- a/packages/@rnw-scripts/unbroken/src/checker.ts +++ b/packages/@rnw-scripts/unbroken/src/checker.ts @@ -60,12 +60,11 @@ export class Checker { this.options.exclusions || path.join(this.options.dir, '.unbroken_exclusions'); try { - const contents = - fs - .readFileSync(exclusionsFileName) - .toString() - .split(/\r?\n/) - .filter(x => x.trim() !== ''); + const contents = fs + .readFileSync(exclusionsFileName) + .toString() + .split(/\r?\n/) + .filter(x => x.trim() !== ''); this.suppressions = contents .filter(x => !x.startsWith('!')) .map(x => Checker.normalizeSlashes(x)); @@ -84,7 +83,6 @@ export class Checker { exclusions: string[]; urlCache: Record; - private async recurseFindMarkdownFiles( dirPath: string, callback: {(path: string): Promise}, @@ -136,9 +134,7 @@ export class Checker { } if (this.options['parse-ids']) { - await this.recurseFindMarkdownFiles(dirPath, x => - this.getAndStoreId(x), - ); + await this.recurseFindMarkdownFiles(dirPath, x => this.getAndStoreId(x)); } await this.recurseFindMarkdownFiles(dirPath, x => this.verifyMarkDownFile(x), @@ -249,9 +245,7 @@ export class Checker { const anchors = this.getAnchors(contents.toLowerCase()); if (!anchors.includes(sectionAnchor.toLowerCase())) { - if ( - !anchors.includes(sectionAnchor.replace(/\./g, '').toLowerCase()) - ) { + if (!anchors.includes(sectionAnchor.replace(/\./g, '').toLowerCase())) { if ( !( this.options['allow-local-line-sections'] && @@ -387,7 +381,7 @@ export class Checker { } private static isWebLink(url: string) { - return url.startsWith("https://") || url.startsWith('https://'); + return url.startsWith('https://') || url.startsWith('https://'); } async verifyMarkDownFile(filePath: string) { diff --git a/packages/@rnw-scripts/unbroken/src/unbroken.ts b/packages/@rnw-scripts/unbroken/src/unbroken.ts index 50dc20489cb..7b1ca4e69ff 100644 --- a/packages/@rnw-scripts/unbroken/src/unbroken.ts +++ b/packages/@rnw-scripts/unbroken/src/unbroken.ts @@ -55,7 +55,11 @@ async function run() { exclusions: {type: 'string', short: 'e'}, 'local-only': {type: 'boolean', short: 'l', default: false}, init: {type: 'boolean', short: 'i', default: false}, - 'allow-local-line-sections': {type: 'boolean', short: 'a', default: false}, + 'allow-local-line-sections': { + type: 'boolean', + short: 'a', + default: false, + }, quiet: {type: 'boolean', short: 'q', default: false}, superquiet: {type: 'boolean', short: 's', default: false}, 'parse-ids': {type: 'boolean', default: false},