Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,7 @@ jobs:
run: yarn

- name: Test markdown links
run: |
find . -name \*.md -not -name CHANGELOG.md -not -path '*/node_modules/*' -print0 | xargs -0 -n1 yarn test-links -c $(pwd)/.github/markdown-link-check-config.json
run: yarn lage test-links --verbose --grouped

pr:
name: PR
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,13 @@ If you are creating a new component from scratch, you have the most leeway to de

To add a native Windows module:

1. Follow [these instructions](https://microsoft.github.io/react-native-windows/docs/native-modules-setup#creating-a-new-native-module-library-project) for creating a new C++/WinRT native windows module library. Complete all steps through the end of [Making your module ready for consumption in an app](https://microsoft.github.io/react-native-windows/docs/native-modules-setup#making-your-module-ready-for-consumption-in-an-app).
1. Follow [these instructions](https://microsoft.github.io/react-native-windows/docs/native-platform-getting-started#create-a-new-react-native-library-project) for creating a new C++/WinRT native windows module library. Complete all steps through the end of [Making your module ready for consumption in an app](https://microsoft.github.io/react-native-windows/docs/native-platform-getting-started#initialize-the-react-native-for-windows-native-code-and-projects).
- When creating Views and ViewManagers for your module, Windows components such as Expander and Windows modules outside of the FluentUI React Native repository such as the [`datetimepicker`](https://github.com/react-native-datetimepicker/datetimepicker/tree/master/windows/DateTimePickerWindows) are helpful resources
2. Follow the steps for [creating a new component](#creating-a-new-component) in FluentUI React Native.
- Other Windows components such as the Expander will be helpful with this step.
3. Copy the `windows` folder from the local native component library created in Step 1 into the root of the new componenet's directory.
4. Testing the component locally
1. Follow steps for Option 1 of [testing the module](https://microsoft.github.io/react-native-windows/docs/native-modules-setup#testing-the-module-before-it-gets-published)
1. Follow steps for Option 1 of [testing the module](https://microsoft.github.io/react-native-windows/docs/native-platform-getting-started#running-the-react-native-for-windows-example-app)
- Be sure to run the autolinking command from the component's root directory (`npx react-native autolink-windows`)
2. Check that the NuGet packages for the test application and component line up. i.e. If the component uses WinUI 2.6, the test application should as well.
- Right-click on the solution within VS. Select `Manage NuGet Packages for Solution…`. Look at differences under the consolidate tab.
Expand Down
10 changes: 10 additions & 0 deletions lage.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ const config = {
buildci: ['lint-repo', 'check-publishing', 'build-all', 'test', 'lint'],

// ── Worker tasks ───────────────────────────────────────────────────────
'test-links': {
// Runs markdown-link-check once per package directory; lage parallelizes
// across packages. The worker stops at nested package boundaries so the
// root target does not recurse into workspace directories.
type: 'worker',
options: {
worker: 'scripts/src/worker/test-links.mts',
},
cache: false,
},
pack: {
dependsOn: ['build-all', '^pack'],
type: 'worker',
Expand Down
106 changes: 106 additions & 0 deletions scripts/src/worker/test-links.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { WorkerRunnerFunction } from 'lage';

import { $, fs, glob } from 'zx';
import { dirname, join, relative, resolve } from 'node:path';

/**
* Lage worker that runs `markdown-link-check` for a single package directory.
*
* Lage invokes this worker once per workspace package, so each invocation only
* scans the markdown files that belong to that package. Because lage runs the
* targets concurrently, the repo's markdown is link-checked directory by
* directory in parallel instead of via a single serial `find | xargs` pass.
*
* Crucially, the walk stops descending at any nested workspace directory —
* that package gets its own worker invocation. This is what keeps the root
* package's invocation from recursing into `apps/*`, `packages/**`, and
* `scripts`; the root only checks its own top-level markdown plus non-workspace
* folders like `docs/`, `.github/`, and `.changeset/`.
*
* Note that the boundary is the set of *workspace* directories, not merely any
* directory containing a `package.json`. Some folders (e.g. `docs/`, the
* component template) carry a `package.json` but are not workspaces, so lage
* never spawns a target for them — their markdown must be covered by the
* nearest enclosing workspace (usually the root) rather than pruned away.
*/

const CONFIG_PATH = '.github/markdown-link-check-config.json';

// Directories that never contain checkable markdown and would only slow the walk.
const SKIP_DIRS = new Set(['node_modules', 'lib', 'lib-commonjs', 'dist', '.git']);

/**
* Resolve the set of workspace directories from the root package.json's
* `workspaces` globs, matching yarn's own resolution (a glob match that
* contains a `package.json`). Cached per worker process since it never changes
* across the targets a single worker handles.
*/
let workspaceDirsPromise: Promise<Set<string>> | undefined;
function getWorkspaceDirs(repoRoot: string): Promise<Set<string>> {
workspaceDirsPromise ??= (async () => {
const rootPkg = await fs.readJson(join(repoRoot, 'package.json'));
const patterns: string[] = rootPkg.workspaces ?? [];
const manifestGlobs = patterns.map((pattern) => (pattern === '.' ? 'package.json' : `${pattern}/package.json`));
const manifests = await glob(manifestGlobs, { cwd: repoRoot, ignore: ['**/node_modules/**'] });
return new Set(manifests.map((manifest) => resolve(repoRoot, dirname(manifest))));
})();
return workspaceDirsPromise;
}

/**
* Collect markdown files under `startDir`, stopping at nested workspace
* boundaries (each of which gets its own worker invocation). CHANGELOG.md is
* excluded to match the repo's link-check convention.
*/
async function findMarkdownFiles(startDir: string, workspaceDirs: Set<string>): Promise<string[]> {
const results: string[] = [];

async function walk(current: string): Promise<void> {
// Another workspace owns this subtree via its own worker invocation — don't
// recurse into it. (The starting directory is never pruned.)
if (current !== startDir && workspaceDirs.has(current)) {
return;
}

const entries = await fs.readdir(current, { withFileTypes: true });
for (const entry of entries) {
const full = join(current, entry.name);
if (entry.isDirectory()) {
if (SKIP_DIRS.has(entry.name)) {
continue;
}
await walk(full);
} else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'CHANGELOG.md') {
results.push(full);
}
}
}

await walk(startDir);
return results;
}

export const run: WorkerRunnerFunction = async ({ target }) => {
// Lage runs from the repo root, so the config and binary resolve from here.
const repoRoot = process.cwd();
const configPath = resolve(repoRoot, CONFIG_PATH);

const workspaceDirs = await getWorkspaceDirs(repoRoot);
const files = await findMarkdownFiles(resolve(target.cwd), workspaceDirs);
if (files.length === 0) {
return;
}

const failures: string[] = [];
for (const file of files) {
const relPath = relative(repoRoot, file);
const result = await $({ cwd: repoRoot, nothrow: true, verbose: true })`yarn markdown-link-check -c ${configPath} ${file}`;
if (result.exitCode !== 0) {
failures.push(relPath);
}
}

if (failures.length > 0) {
throw new Error(`Dead links found in:\n${failures.map((f) => ` - ${f}`).join('\n')}`);
}
};
Loading