mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
36aa14a5fc
Adds the Argent documentation site, published to [docs.swmansion.com/argent](https://docs.swmansion.com/argent): a branded Docusaurus project under `packages/docs/`, the full first set of pages, and the CI that checks and deploys it. <img width="1512" height="945" alt="Screenshot 2026-08-21 at 13 05 04" src="https://github.com/user-attachments/assets/4607e143-4359-49a8-93a2-aaa3af4c9534" /> ## Site A standalone Docusaurus 3.9 project, built on the shared Software Mansion docs theme ([`@swmansion/t-rex-ui`](https://www.npmjs.com/package/@swmansion/t-rex-ui)) and restyled for Argent: - **Palette** from [argent.swmansion.com](https://argent.swmansion.com): dark `#0D0F26`, blue `#99DAFF`, lavender `#E4E1FF`, light `#FEFEFE`, mist `#F1F1F1`. The `--swm-*` token names stay, since the shared theme resolves against them. - **Typography**: DM Sans and DM Mono, matching the landing page. - **Sidebar** with lucide icons on every page entry (`sidebar_custom_props.icon` in the front matter, registered in `src/theme/SidebarIcon`), the logo kept on the page background and the panel inset from the edge. - **Video component** for embedded screen recordings, with `scripts/encode-video.sh` producing a web-sized MP4 and a poster frame for each clip in `static/video/`. - **Copy page button** and a few theme fixes: inline code badges in tables, paginator hover. - **No landing page.** That stays at argent.swmansion.com. The root route redirects to Getting started. ## Content Three sections in `docs/`: - **Fundamentals**: getting started, installation, supported platforms. - **Features**: interacting with apps, flows, network, screen recording, lens, visual regression, profiling, debugging. Conceptual overviews with recordings, each linking to the tools reference. - **Reference**: tools, CLI, configuration, flow YAML, editors, telemetry. Prose follows Simplified Technical English. The conventions (style, front matter, icons, checks) are written down in `packages/docs/CLAUDE.md`, and a root `CLAUDE.md` adds a checklist so that code changes to tools, CLI, configuration or flows update the matching docs page in the same pull request. ## CI and deploy - `Docs build` runs `format:check`, `lint`, `typecheck` and `build` on every pull request that touches `packages/docs/`. The build has `onBrokenLinks: "throw"`, so it catches links left dangling by a moved page. - `Docs publish` deploys `packages/docs/build` through the GitHub Actions Pages source on every push to `main` that touches the docs. The repository Pages source needs to be set to **GitHub Actions** for the first run. ## Notes for review - The site is excluded from the root `packages/*` workspaces (`!packages/docs`) and keeps its own `package.json` and `package-lock.json`, so its dependency tree stays out of the toolkit's lockfile. Root Prettier, ESLint and knip ignore it; it formats and lints itself with the repo `.prettierrc` and its own `eslint.config.mjs`. `check-workspace-versions.mjs` skips it so its `0.0.0` version is not read as drift. - `webpack` is pinned to `5.105.4` and `@docusaurus/plugin-content-docs` / `theme-common` to `3.9.2` via `overrides`. Newer webpack fails Docusaurus 3.9's ProgressPlugin option validation, and a hoisted `plugin-content-docs@3.10.2` produced a duplicate React context that crashed SSR. - **Search is not wired up yet.** The shared theme always mounts a DocSearch bar, so an `algolia` block has to be present. It reads `ALGOLIA_APP_ID` / `ALGOLIA_API_KEY` / `ALGOLIA_INDEX_NAME` from the environment and the bar stays hidden until Argent has its own DocSearch application. Follow-up PR. ## Testing - `npm run format:check`, `npm run lint`, `npm run typecheck` and `npm run build` pass in `packages/docs/`. - Root `prettier --check`, `eslint` and `npm run knip` pass. - Walked the served production build in the browser in light and dark themes while iterating: root redirect, docs pages, sidebar icons, videos, TOC, footer. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
245 lines
11 KiB
JavaScript
245 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
// Fails if the workspace packages under packages/* don't all share the same
|
|
// version. Keeps the monorepo's lockstep versioning from silently drifting when
|
|
// a release bump misses a package.
|
|
//
|
|
// The root server.json (the MCP registry manifest) is held to the version and
|
|
// the npm coordinates in packages/argent/package.json, the package it points at.
|
|
// It is checked here because it is the one such file outside packages/*, so a
|
|
// bump that sweeps the workspace leaves it behind — and the registry then
|
|
// rejects the publish, either because `packages[].version` names a version npm
|
|
// has never seen or because `name` doesn't match the tarball's `mcpName`.
|
|
import { readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
import { dirname, join, relative } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { argv as processArgv } from "node:process";
|
|
|
|
const selfPath = realpathSync(fileURLToPath(import.meta.url));
|
|
const repoRoot = join(dirname(selfPath), "..");
|
|
const packagesDir = join(repoRoot, "packages");
|
|
const argentManifestPath = join(packagesDir, "argent", "package.json");
|
|
const serverJsonPath = join(repoRoot, "server.json");
|
|
|
|
// Directories under packages/ that are not workspace members and so are not
|
|
// part of the lockstep versioning. The docs site is excluded from the root
|
|
// `workspaces` glob (it keeps its own lockfile) and carries a standalone
|
|
// version that must not be read as drift.
|
|
const NON_WORKSPACE_DIRS = new Set(["docs"]);
|
|
|
|
/**
|
|
* Everything server.json has to keep in step with the npm package it points at,
|
|
* as one human-readable line per problem. Pure, separated from the fs reads so
|
|
* it can be unit-tested.
|
|
* @param {string | undefined} publishedVersion the version packages/argent/package.json carries
|
|
* @param {{ name?: string, mcpName?: string }} argentPkg packages/argent/package.json
|
|
* @param {{ name?: string, version?: string, packages?: { identifier?: string, version?: string }[] }} server server.json
|
|
* @returns {string[]} empty when in sync
|
|
*/
|
|
export function serverJsonMismatches(publishedVersion, argentPkg, server) {
|
|
const mismatches = [];
|
|
|
|
// Without a version on the manifest there is no target to compare against, and
|
|
// every comparison below would pass by matching undefined against undefined —
|
|
// so its absence is a mismatch in its own right, exactly like mcpName's.
|
|
if (!publishedVersion) {
|
|
mismatches.push(
|
|
"packages/argent/package.json has no version — nothing for server.json to be checked against"
|
|
);
|
|
} else if (server.version !== publishedVersion) {
|
|
mismatches.push(
|
|
`server.json version is ${server.version}, packages/argent/package.json is ${publishedVersion}`
|
|
);
|
|
}
|
|
|
|
// mcpName on the published npm tarball is what proves to the registry that the
|
|
// io.github.software-mansion namespace is ours, so its absence is as fatal as a
|
|
// mismatch — and an absent one equals an absent server.json name by ===.
|
|
if (!argentPkg.mcpName) {
|
|
mismatches.push(
|
|
"packages/argent/package.json has no mcpName — the registry reads it off the published tarball to prove the namespace"
|
|
);
|
|
} else if (server.name !== argentPkg.mcpName) {
|
|
mismatches.push(
|
|
`server.json name (${server.name}) != packages/argent/package.json mcpName (${argentPkg.mcpName})`
|
|
);
|
|
}
|
|
|
|
// Every packages[].identifier below is compared against this name, so without
|
|
// it those comparisons pass on undefined === undefined and server.json is free
|
|
// to name no installable package at all.
|
|
if (!argentPkg.name) {
|
|
mismatches.push(
|
|
"packages/argent/package.json has no name — server.json's identifier has nothing to point at"
|
|
);
|
|
}
|
|
|
|
// Absent, empty and non-array all leave the registry entry naming nothing to
|
|
// install, so they get one message rather than a TypeError out of the loop.
|
|
const packages = server.packages;
|
|
if (!Array.isArray(packages) || packages.length === 0) {
|
|
mismatches.push(
|
|
"server.json has no packages array — the registry entry would name nothing to install"
|
|
);
|
|
return mismatches;
|
|
}
|
|
|
|
for (const [i, pkg] of packages.entries()) {
|
|
// Same reason as the array check above: an entry that is not an object has no
|
|
// coordinates to read. `null` would throw out of the loop; a scalar or an
|
|
// array reads as undefined on every field and would otherwise be reported as
|
|
// a pile of missing values rather than the wrong shape it is.
|
|
if (pkg === null || typeof pkg !== "object" || Array.isArray(pkg)) {
|
|
const shape = pkg === null ? "null" : Array.isArray(pkg) ? "an array" : typeof pkg;
|
|
mismatches.push(
|
|
`server.json packages[${i}] is ${shape}, not an object naming a registry and identifier`
|
|
);
|
|
continue;
|
|
}
|
|
if (pkg.version !== publishedVersion) {
|
|
mismatches.push(
|
|
`server.json packages[${i}].version is ${pkg.version}, packages/argent/package.json is ${publishedVersion}`
|
|
);
|
|
}
|
|
if (pkg.identifier !== argentPkg.name) {
|
|
mismatches.push(
|
|
`server.json packages[${i}].identifier (${pkg.identifier}) != published package name (${argentPkg.name})`
|
|
);
|
|
}
|
|
}
|
|
|
|
return mismatches;
|
|
}
|
|
|
|
/**
|
|
* Reads a tracked JSON file, reporting a missing or malformed one as a sentence
|
|
* instead of a raw stack trace. Both only happen by hand-corrupting a file git
|
|
* tracks, and the fix is the same either way.
|
|
* @param {string} path
|
|
* @returns {any}
|
|
*/
|
|
function readTrackedJson(path) {
|
|
const shown = relative(repoRoot, path);
|
|
let raw;
|
|
try {
|
|
raw = readFileSync(path, "utf8");
|
|
} catch (error) {
|
|
console.error(`Cannot read ${shown}: ${error.message}`);
|
|
console.error(`\nIt is tracked in git — restore it with \`git checkout -- ${shown}\`.`);
|
|
process.exit(1);
|
|
}
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(raw);
|
|
} catch (error) {
|
|
console.error(`${shown} is not valid JSON: ${error.message}`);
|
|
console.error(`\nIt is tracked in git — restore it with \`git checkout -- ${shown}\`.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// JSON.parse returns whatever the file holds, so a scalar, null or an array
|
|
// parses cleanly and then either throws or compares equal to undefined on
|
|
// every field read below. Both callers want an object or nothing.
|
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
const shape = parsed === null ? "null" : Array.isArray(parsed) ? "an array" : typeof parsed;
|
|
console.error(`${shown} is ${shape}, not a JSON object`);
|
|
console.error(`\nIt is tracked in git — restore it with \`git checkout -- ${shown}\`.`);
|
|
process.exit(1);
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function main() {
|
|
const byVersion = new Map(); // version -> package names
|
|
// Sorted so the scan visits packages/* in the same order on every machine —
|
|
// readdir order is filesystem-dependent, and a scan that stops early would
|
|
// otherwise drop a different set of packages each run.
|
|
const entries = readdirSync(packagesDir, { withFileTypes: true }).sort((a, b) =>
|
|
a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
);
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory()) continue;
|
|
if (NON_WORKSPACE_DIRS.has(entry.name)) continue;
|
|
const manifestPath = join(packagesDir, entry.name, "package.json");
|
|
let manifest;
|
|
try {
|
|
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
} catch (error) {
|
|
// No package.json at all means the directory is not a workspace package —
|
|
// a submodule that was never checked out, a stray build directory. A
|
|
// manifest that is there but unusable is different: skipping it would drop
|
|
// a real package out of the comparison and pass a workspace that drifted.
|
|
if (error.code === "ENOENT") continue;
|
|
console.error(`Cannot read packages/${entry.name}/package.json: ${error.message}`);
|
|
console.error(
|
|
`\nIt is tracked in git — restore it with \`git checkout -- ${relative(repoRoot, manifestPath)}\`.`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
// `?.` would mask a manifest that parses to a non-object: JSON.parse
|
|
// succeeds on a file holding `null`, a bare scalar or an array, none of
|
|
// which reach the catch above — and each of those is a manifest that is
|
|
// *there but unusable*, the same class the readTrackedJson shape check
|
|
// rejects for server.json and packages/argent. Dropping one would take a
|
|
// real package out of the lockstep comparison and pass a workspace that
|
|
// drifted, so reject it on-message like every other unreadable manifest.
|
|
if (manifest === null || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
const shape =
|
|
manifest === null ? "null" : Array.isArray(manifest) ? "an array" : typeof manifest;
|
|
console.error(`packages/${entry.name}/package.json is ${shape}, not a JSON object`);
|
|
console.error(
|
|
`\nIt is tracked in git — restore it with \`git checkout -- ${relative(repoRoot, manifestPath)}\`.`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
if (!manifest.version) continue;
|
|
const names = byVersion.get(manifest.version) ?? [];
|
|
names.push(manifest.name ?? entry.name);
|
|
byVersion.set(manifest.version, names);
|
|
}
|
|
|
|
let failed = false;
|
|
|
|
// Reported before the reads below, so a corrupt server.json can't swallow it.
|
|
if (byVersion.size > 1) {
|
|
console.error("Workspace package versions are out of sync:");
|
|
for (const [version, names] of [...byVersion].sort()) {
|
|
console.error(` ${version}: ${names.sort().join(", ")}`);
|
|
}
|
|
console.error(
|
|
"\nEvery package under packages/* must share one version. Bump the outliers to match.\n"
|
|
);
|
|
failed = true;
|
|
}
|
|
|
|
// An empty packages/ never gets past this read. It only proves the file parses,
|
|
// though — a manifest missing its version is caught as a mismatch below.
|
|
const argentPkg = readTrackedJson(argentManifestPath);
|
|
const server = readTrackedJson(serverJsonPath);
|
|
|
|
// server.json names the npm coordinates of packages/argent, so that manifest
|
|
// is what it has to agree with. Reported even when packages/* disagree, so a
|
|
// half-finished bump surfaces every remaining edit in one run.
|
|
const mismatches = serverJsonMismatches(argentPkg.version, argentPkg, server);
|
|
if (mismatches.length > 0) {
|
|
console.error("server.json is out of sync with packages/argent/package.json:");
|
|
for (const line of mismatches) console.error(` ${line}`);
|
|
console.error(
|
|
"\nEvery line above starts with the file to edit. server.json is usually the one:\n" +
|
|
"it is not under packages/*, so a workspace bump sweeps past and leaves it behind."
|
|
);
|
|
failed = true;
|
|
}
|
|
|
|
if (failed) process.exit(1);
|
|
|
|
console.log(`All workspace packages and server.json are at ${argentPkg.version}.`);
|
|
}
|
|
|
|
// Run only when invoked directly, not when imported by the test. Both sides are
|
|
// realpath'd: node realpaths the main module before setting import.meta.url but
|
|
// leaves process.argv[1] as typed, so comparing them raw makes a path through a
|
|
// symlink skip main() and exit 0 with no output — a silent pass.
|
|
if (processArgv[1] && realpathSync(processArgv[1]) === selfPath) {
|
|
main();
|
|
}
|