diff --git a/.claude/skills/running-tests.md b/.claude/skills/running-tests.md deleted file mode 100644 index 1802b2aa..00000000 --- a/.claude/skills/running-tests.md +++ /dev/null @@ -1,36 +0,0 @@ -# Running Tests - -## Prerequisites - -Before running tests, you must build the release binaries: - -```bash -cargo build --release -``` - -## Running Integration Tests - -Use the following command to run integration tests: - -```bash -yarn test:integration -``` - -For example: -- Run all tests: `yarn test:integration` -- Run a specific test file: `yarn test:integration path/to/test.ts` -- Run tests matching a pattern: `yarn test:integration -t "pattern"` -- Run in watch mode: `yarn test:integration --watch` - -## Reading spawn logs - -You can access the logs of any Yarn command spawned within a test by adding the `JEST_LOG_SPAWNS=1` environment variable. - -## Creating temporary projects - -You can setup temporary projects by: - -1. Creating a new temporary folder -2. Adding an empty `package.json` file -3. Running `yarn switch link path/to/target/Release/yarn-bin` inside this temporary folder -4. Subsequent `yarn` commands should then use the local binary diff --git a/package.json b/package.json index 2d8a6baf..4e47934c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "workspaces": [ "documentation", + "website", "packages/*", "tests/acceptance-tests", "tests/acceptance-tests/pkg-tests-core", diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 00000000..ac5d2bca --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,2 @@ +dist/ +.astro/ diff --git a/website/astro.config.mjs b/website/astro.config.mjs new file mode 100644 index 00000000..55d6bbca --- /dev/null +++ b/website/astro.config.mjs @@ -0,0 +1,40 @@ +import react from '@astrojs/react'; +import sitemap from '@astrojs/sitemap'; +import tailwindcss from '@tailwindcss/vite'; +import {defineConfig} from 'astro/config'; +import remarkDirective from 'remark-directive'; + +import rehypeDocs from './plugins/rehype-docs.mjs'; +import rehypeFootnoteTooltips from './plugins/rehype-footnote-tooltips.mjs'; +import remarkAutolinkFields from './plugins/remark-autolink-fields.mjs'; +import remarkBluesky from './plugins/remark-bluesky.mjs'; +import remarkDocs from './plugins/remark-docs.mjs'; +import remarkMermaid from './plugins/remark-mermaid.mjs'; + +export default defineConfig({ + site: `https://v6.yarnpkg.com`, + integrations: [react(), sitemap({filter: page => !page.includes(`/presentation/`)})], + build: { + format: `file`, + }, + vite: { + plugins: [tailwindcss()], + optimizeDeps: { + include: [ + `prettier/standalone`, + `prettier/plugins/babel`, + `prettier/plugins/estree`, + `prettier/plugins/typescript`, + `prettier/plugins/postcss`, + `prettier/plugins/html`, + `prettier/plugins/markdown`, + `prettier/plugins/yaml`, + ], + }, + }, + markdown: { + syntaxHighlight: false, + remarkPlugins: [remarkDirective, remarkBluesky, remarkMermaid, remarkDocs, remarkAutolinkFields], + rehypePlugins: [rehypeDocs, rehypeFootnoteTooltips], + }, +}); diff --git a/website/config/navigation.json b/website/config/navigation.json new file mode 100644 index 00000000..72521984 --- /dev/null +++ b/website/config/navigation.json @@ -0,0 +1,49 @@ +{ + "topbar": [ + { "label": "Install", "key": "install", "href": "/getting-started/" }, + { "label": "Concepts", "key": "concepts", "href": "/concepts/switch/" }, + { "label": "Reference", "key": "reference", "href": "/configuration/manifest/" }, + { "label": "Extra", "key": "extra", "href": "/appendix/workspaces-and-peer-deps/" }, + { "label": "Benchmarks", "key": "benchmarks", "href": "/benchmarks/" }, + { "label": "Quiz", "key": "quiz", "href": "/quiz/" }, + { "label": "Blog", "key": "blog", "href": "/blog" } + ], + "categoryToSidebar": { + "getting-started": "getting-started", + "concepts": "concepts", + "appendix": "extra", + "appendixes": "extra", + "contributing": "extra", + "advanced": "advanced", + "protocols": "reference", + "reference": "reference" + }, + "sidebars": { + "getting-started": { "sections": ["getting-started"] }, + "concepts": { "sections": ["concepts"] }, + "advanced": { "sections": ["advanced"] }, + "appendix": { "sections": ["appendix"] }, + "contributing": { "sections": ["contributing"] }, + "extra": { "sections": ["appendix", "contributing"] }, + "reference": { + "links": [ + { "label": "Manifest — package.json", "href": "/configuration/manifest/", "page": "manifest" }, + { "label": "Settings — .yarnrc.yml", "href": "/configuration/yarnrc/", "page": "yarnrc" }, + { "label": "Yarn CLI", "href": "/cli/install/" }, + { "label": "Yarn Switch CLI", "href": "/switch/switch/" }, + { "label": "Protocols", "href": "/protocol/npm/", "pagePrefix": "protocol/" } + ], + "protocols": [ + { "label": "exec:", "href": "/protocol/exec/", "page": "protocol/exec" }, + { "label": "file:", "href": "/protocol/file/", "page": "protocol/file" }, + { "label": "git:", "href": "/protocol/git/", "page": "protocol/git" }, + { "label": "jsr:", "href": "/protocol/jsr/", "page": "protocol/jsr" }, + { "label": "link:", "href": "/protocol/link/", "page": "protocol/link" }, + { "label": "npm:", "href": "/protocol/npm/", "page": "protocol/npm" }, + { "label": "patch:", "href": "/protocol/patch/", "page": "protocol/patch" }, + { "label": "portal:", "href": "/protocol/portal/", "page": "protocol/portal" }, + { "label": "workspace:", "href": "/protocol/workspace/", "page": "protocol/workspace" } + ] + } + } +} diff --git a/website/config/quiz.json b/website/config/quiz.json new file mode 100644 index 00000000..3129be22 --- /dev/null +++ b/website/config/quiz.json @@ -0,0 +1,120 @@ +{ + "questions": [ + { + "slug": "exec-protocol", + "question": "Can Yarn run a Node.js script at install time to dynamically generate a package on the fly?", + "answer": true, + "wrongLine": "Actually — yes, via the exec: protocol.", + "rightLine": "Correct — it's called the exec: protocol.", + "explain": [ + "The exec: protocol lets you point a dependency at a generator script instead of a tarball. Yarn runs that script in a temporary directory at fetch time, and whatever files it produces become the package.", + "The script runs in a special context with an execEnv global that provides the target directory and other metadata. Useful for codegen, platform-specific builds, or any case where a static package isn't enough." + ] + }, + { + "slug": "builtin-node", + "question": "Can Yarn lock and manage the exact Node.js version used by your project?", + "answer": true, + "wrongLine": "It can — Node.js is treated as a regular locked dependency.", + "rightLine": "Right — and it's locked in yarn.lock like any other package.", + "explain": [ + "Yarn lets you declare Node.js as a project dependency via @builtin/node. The version is pinned in yarn.lock, and Yarn automatically downloads the correct binary for each developer's OS and architecture.", + "This means every contributor gets the exact same Node.js version without needing nvm, fnm, or any external version manager — it's part of the dependency graph." + ] + }, + { + "slug": "pre-post-scripts", + "question": "Does Yarn run prestart automatically before yarn start?", + "answer": false, + "wrongLine": "Unlike npm, Yarn intentionally dropped arbitrary pre/post hooks.", + "rightLine": "Right — Yarn chose clarity over convention.", + "explain": [ + "Yarn only supports a handful of well-defined lifecycle hooks (prepack, postpack, postinstall, etc.) and deliberately doesn't run arbitrary pre* and post* scripts.", + "The reasoning: implicit hooks made execution flow hard to follow and debug. If you need to run something before start, Yarn encourages you to make it explicit — for instance via task dependencies or a composite script." + ] + }, + { + "slug": "ghost-deps", + "question": "Can Yarn throw an error if a package tries to require() a dependency it never declared?", + "answer": true, + "wrongLine": "It does — these are called ghost dependencies, and Yarn catches them.", + "rightLine": "Exactly — Plug'n'Play enforces the dependency contract.", + "explain": [ + "With traditional node_modules, hoisting lets packages accidentally import dependencies they don't declare. It works until the hoisting layout changes, then it breaks unpredictably.", + "Under Plug'n'Play, Yarn resolves every require() against the package's declared dependencies. If a package tries to reach something it doesn't list, Yarn throws immediately — surfacing the bug before it hits production." + ] + }, + { + "slug": "workspace-git", + "question": "Can Yarn install a single workspace from a remote Git repository — without cloning the entire monorepo yourself?", + "answer": true, + "wrongLine": "It can — using the #workspace= syntax on a Git URL.", + "rightLine": "Correct — and not all package managers support this.", + "explain": [ + "Yarn's git: protocol supports a #workspace=name fragment. For example, yarn add my-lib@org/monorepo#workspace=my-lib clones the repo, builds the target workspace, and installs just that package.", + "Yarn even auto-detects the repo's package manager from its lock file (Yarn, npm, or pnpm) so it can pack the workspace correctly." + ] + }, + { + "slug": "constraints-fix", + "question": "Can Yarn automatically fix inconsistent dependency versions across workspaces in a monorepo?", + "answer": true, + "wrongLine": "It can — via yarn constraints --fix.", + "rightLine": "Right — constraints are both a linter and an auto-fixer.", + "explain": [ + "Yarn Constraints let you write declarative rules in JavaScript or TypeScript (via yarn.config.cjs) that describe the expected state of every package.json in the project.", + "Run yarn constraints to audit, or yarn constraints --fix to auto-correct. A typical rule: \"every workspace that depends on react must use the same version\" — and Yarn rewrites the manifests for you." + ] + }, + { + "slug": "no-node-modules", + "question": "Does Yarn support node_modules installs?", + "answer": true, + "wrongLine": "It does — even though Plug'n'Play is the default.", + "rightLine": "Right — even though Plug'n'Play is the default.", + "explain": [ + "Since Yarn 2, the default install strategy is Plug'n'Play. Dependencies live as compressed archives in a cache, and a generated .pnp.cjs file tells Node.js where to find each package — no node_modules tree, no phantom dependencies, no hoisting ambiguity.", + "If you prefer a classic layout, you can still opt in with nodeLinker: node-modules in .yarnrc.yml. There's also a pnpm linker mode as a middle ground." + ] + }, + { + "slug": "parallel-topo", + "question": "Can Yarn run a script in every workspace in parallel while still respecting the dependency graph?", + "answer": true, + "wrongLine": "It can — topological parallelism is built in.", + "rightLine": "Correct — one command, no extra tool needed.", + "explain": [ + "yarn workspaces foreach --all --parallel --topological run build runs build in every workspace, starting with leaves and working up the dependency tree. Independent workspaces run in parallel; dependent ones wait.", + "Add --jobs 4 to cap concurrency, or --interlaced to stream logs live instead of buffering per workspace." + ] + }, + { + "slug": "catalog", + "question": "Can Yarn let all monorepo workspaces share a single, centralized set of dependency version ranges?", + "answer": true, + "wrongLine": "It can — that's what the catalog: protocol is for.", + "rightLine": "Right — and it's transparent at publish time.", + "explain": [ + "Define shared version ranges in a catalog field, then reference them from any workspace with \"react\": \"catalog:\". Every workspace gets the same range without duplicating it in each package.json.", + "When you publish, Yarn transparently replaces catalog: references with the actual version range — so consumers never see the protocol and your packages work on any registry." + ] + }, + { + "slug": "npmrc", + "question": "Does Yarn read its configuration from .npmrc?", + "answer": false, + "wrongLine": "It doesn't — Yarn uses its own .yarnrc.yml format.", + "rightLine": "Right — Yarn has its own configuration system.", + "explain": [ + "Yarn 2+ uses .yarnrc.yml — a YAML-based configuration file with a completely different schema from npm's .npmrc. Settings like registry URLs, linker mode, and plugin configuration all live here.", + "This is a common migration pitfall: npm-specific settings in .npmrc (like registry or always-auth) won't be picked up by Yarn. You need to translate them into their .yarnrc.yml equivalents." + ] + } + ], + "levels": [ + { "min": 0, "title": "Curious", "tag": "Plenty to discover — Yarn has more tricks than most devs realize." }, + { "min": 4, "title": "Familiar", "tag": "You know the basics. A few surprises still lurking in the manual." }, + { "min": 7, "title": "Fluent", "tag": "Confidently above average. You've read past the install section." }, + { "min": 10, "title": "Expert", "tag": "You might be on the Yarn team. Or you should apply." } + ] +} diff --git a/website/config/rc.json b/website/config/rc.json new file mode 100644 index 00000000..8812812a --- /dev/null +++ b/website/config/rc.json @@ -0,0 +1,3 @@ +{ + "version": "v5.0.0-rc.2" +} diff --git a/website/config/stable.json b/website/config/stable.json new file mode 100644 index 00000000..7de8b2fd --- /dev/null +++ b/website/config/stable.json @@ -0,0 +1,3 @@ +{ + "version": "v4.8.1" +} diff --git a/website/config/tips.json b/website/config/tips.json new file mode 100644 index 00000000..5b199204 --- /dev/null +++ b/website/config/tips.json @@ -0,0 +1,22 @@ +[ + "Yarn's exec: protocol can run a Node.js script at install time to generate a package on the fly — useful for codegen or platform-specific builds.", + "You can declare Node.js itself as a project dependency via @builtin/node. The version is locked in yarn.lock, and every contributor gets the exact same binary.", + "Scripts with a colon in their name (like g:tsc) can be called from any workspace — $INIT_CWD tells you where the call came from.", + "Yarn Constraints use a declarative model: you describe the expected state of every package.json, and Yarn diffs reality against it — no if checks needed.", + "When you use zero-installs, storing archives uncompressed in Git actually saves space: Git's delta algorithm works better, shrinking one test repo from 2.1 GiB to 1.25 GiB.", + "The portal: protocol links to a local package and resolves its own dependencies, while link: treats the target as an opaque folder with no deps.", + "Yarn automatically applies a curated list of known ghost-dependency fixes from the ecosystem so you don't have to add common packageExtensions yourself.", + "Virtual packages solve the peer-dependency problem by creating separate instances of the same package for each unique set of peers — something flat node_modules can't do.", + "Virtual packages use a virtual filesystem layer, not symlinks — avoiding realpath issues and improving Windows compatibility.", + "Yarn's architecture supports multiple linkers simultaneously. A JavaScript package could theoretically depend on a Python package, each resolved by its own linker.", + "When using git: dependencies, Yarn detects whether the repo uses Yarn, npm, or pnpm (via its lock file) and packs it with the right tool automatically.", + "Yarn's telemetry is batched roughly every seven days, not per-command. The data goes through registry.yarnpkg.com, which is just a CNAME to npm's registry — no Yarn backend ever sees the HTTP logs.", + "The .pnp.cjs file is fully deterministic — you can commit it to version control without creating unnecessary diffs across machines.", + "Yarn maintains an unofficial patch for TypeScript that adds PnP support. It's applied to every downloaded TypeScript version, even if you're using node-modules linker.", + "Task dependencies support mixed parallelism: suffix a dep with & to run it in parallel, omit it for a sequential barrier — all in one definition.", + "The file: protocol copies the folder into the cache rather than linking it. To pick up local changes, run YARN_UPDATE_FILE_CACHE=1 yarn install.", + "Workspace profiles can extend other profiles, letting you compose complex dev-dependency sets from simple building blocks — but they deliberately only cover devDependencies.", + "Yarn can resolve yarn.lock merge conflicts automatically. When it detects Git conflict markers, it merges both sides and re-resolves.", + "The jsr: protocol locks the exact tarball URL in yarn.lock — because the JSR registry may regenerate tarballs, each revision gets a new immutable URL.", + "You can run a hybrid monorepo: PnP for most workspaces, node-modules for specific ones that need it — using separate lock files and pnpIgnorePatterns." +] diff --git a/website/package.json b/website/package.json new file mode 100644 index 00000000..6791c8c0 --- /dev/null +++ b/website/package.json @@ -0,0 +1,43 @@ +{ + "devDependencies": { + "puppeteer": "^24.42.0" + }, + "name": "@yarnpkg/website", + "type": "module", + "scripts": { + "dev": "astro dev", + "build": "astro build && node --experimental-strip-types scripts/generate-og.ts", + "preview": "astro preview", + "record": "node --experimental-strip-types scripts/record-terminal.ts" + }, + "dependencies": { + "@astrojs/react": "^5.0.4", + "@astrojs/sitemap": "^3.7.2", + "@clipanion/astro": "../scripts/@clipanion-astro.tgz", + "@clipanion/tools": "../scripts/@clipanion-tools.tgz", + "@iconify-json/octicon": "^1.2.23", + "@iconify-json/simple-icons": "^1.2.79", + "@monaco-editor/react": "^4.7.0", + "@tailwindcss/vite": "^4.2.4", + "@tanstack/react-router": "^1.169.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "algoliasearch": "^5.52.0", + "astro": "^5.9.3", + "mermaid": "^11.0.0", + "prettier": "^3.5.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-directive": "^4.0.0", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "shiki": "^4.0.2", + "tailwindcss": "^4.2.4", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", + "vite": "^8.0.9" + } +} diff --git a/website/plugins/rehype-docs.mjs b/website/plugins/rehype-docs.mjs new file mode 100644 index 00000000..dae56f82 --- /dev/null +++ b/website/plugins/rehype-docs.mjs @@ -0,0 +1,109 @@ +import {visit} from 'unist-util-visit'; + +const admonitionSvgs = { + note: ``, + tip: ``, + warning: ``, + danger: ``, +}; + +// eslint-disable-next-line arca/no-default-export +export default function rehypeDocs() { + return tree => { + visit(tree, `element`, node => { + const type = node.properties?.dataAdmonition; + if (!type || !admonitionSvgs[type]) return; + + const label = node.properties.dataLabel || type.toUpperCase(); + + const header = { + type: `element`, + tagName: `div`, + properties: {className: [`adm-header`]}, + children: [ + {type: `raw`, value: admonitionSvgs[type]}, + { + type: `element`, + tagName: `span`, + properties: {}, + children: [{type: `text`, value: label}], + }, + ], + }; + + const body = { + type: `element`, + tagName: `div`, + properties: {className: [`adm-body`]}, + children: node.children, + }; + + node.children = [header, body]; + delete node.properties.dataAdmonition; + delete node.properties.dataLabel; + }); + + // Heading anchors: wrap h2-h4 content and append # link + function textContent(node) { + if (node.type === `text`) return node.value; + if (node.children) return node.children.map(textContent).join(``); + return ``; + } + function slugifyId(s) { + return s.toLowerCase() + .replace(/[^\w\s-]/g, ``) + .replace(/\s+/g, `-`) + .replace(/-+/g, `-`) + .replace(/^-|-$/g, ``); + } + + visit(tree, `element`, node => { + if (![`h2`, `h3`, `h4`].includes(node.tagName)) return; + + if (!node.properties.id) { + node.properties.id = slugifyId(textContent(node)); + } + + const id = node.properties.id; + const text = {type: `element`, tagName: `span`, properties: {}, children: node.children}; + const anchor = { + type: `element`, + tagName: `a`, + properties: { + href: `#${id}`, + className: [`heading-anchor`], + ariaLabel: `Copy link to this section`, + }, + children: [{type: `text`, value: `#`}], + }; + const wrap = { + type: `element`, + tagName: `span`, + properties: {className: [`heading-wrap`]}, + children: [text, anchor], + }; + node.children = [wrap]; + }); + + // Lead paragraph: add .lead to the first

after

+ const children = tree.children || []; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (child.type === `element` && child.tagName === `h1`) { + for (let j = i + 1; j < children.length; j++) { + const next = children[j]; + if (next.type === `text` && !next.value.trim()) continue; + if (next.type === `element` && next.tagName === `p`) { + next.properties = next.properties || {}; + next.properties.className = [ + ...(next.properties.className || []), + `lead`, + ]; + } + break; + } + break; + } + } + }; +} diff --git a/website/plugins/rehype-footnote-tooltips.mjs b/website/plugins/rehype-footnote-tooltips.mjs new file mode 100644 index 00000000..782f70b4 --- /dev/null +++ b/website/plugins/rehype-footnote-tooltips.mjs @@ -0,0 +1,99 @@ +import {visit, SKIP} from 'unist-util-visit'; + +function escAttr(s) { + return s.replace(/&/g, `&`).replace(/"/g, `"`).replace(//g, `>`); +} + +function escText(s) { + return s.replace(/&/g, `&`).replace(//g, `>`); +} + +const VOID = new Set([`br`, `hr`, `img`, `input`]); + +function serializeNode(node) { + if (node.type === `text`) + return escText(node.value); + + if (node.type === `element`) { + const props = node.properties || {}; + + if (props.dataFootnoteBackref != null) + return ``; + if (props.className?.includes(`data-footnote-backref`)) + return ``; + + const tag = node.tagName; + + const attrs = []; + for (const [k, v] of Object.entries(props)) { + if (k === `className`) + attrs.push(`class="${escAttr(v.join(` `))}"`); + else if (typeof v === `string`) + attrs.push(`${k.replace(/([A-Z])/g, `-$1`).toLowerCase()}="${escAttr(v)}"`); + else if (v === true) + attrs.push(k.replace(/([A-Z])/g, `-$1`).toLowerCase()); + } + + const open = attrs.length ? `<${tag} ${attrs.join(` `)}>` : `<${tag}>`; + if (VOID.has(tag)) + return open; + + const inner = (node.children || []).map(serializeNode).join(``); + return `${open}${inner}`; + } + + if (node.type === `raw`) + return node.value; + + return ``; +} + +function serializeFootnote(children) { + return children + .filter(c => c.type === `element` && c.tagName === `p`) + .map(p => p.children.map(serializeNode).join(``).trim()) + .join(`
`) + .trim(); +} + +export default function rehypeFootnoteTooltips() { + return tree => { + const footnotes = new Map(); + + visit(tree, `element`, node => { + if (node.tagName !== `li`) return; + const id = node.properties?.id; + if (!id || !id.startsWith(`user-content-fn-`)) return; + + const key = id.replace(`user-content-fn-`, ``); + const html = serializeFootnote(node.children); + if (html) + footnotes.set(key, html); + }); + + if (!footnotes.size) return; + + visit(tree, `element`, node => { + if (node.tagName !== `sup`) return; + + const link = (node.children || []).find(c => + c.type === `element` && c.tagName === `a` && c.properties?.dataFootnoteRef != null, + ); + if (!link) return; + + const key = (link.properties.href || ``).replace(`#user-content-fn-`, ``); + const html = footnotes.get(key); + if (!html) return; + + node.properties = node.properties || {}; + node.properties.className = [...(node.properties.className || []), `fn-ref`]; + + node.children.push({ + type: `raw`, + value: `${html}`, + }); + + return SKIP; + }); + }; +} diff --git a/website/plugins/remark-autolink-fields.mjs b/website/plugins/remark-autolink-fields.mjs new file mode 100644 index 00000000..9430de18 --- /dev/null +++ b/website/plugins/remark-autolink-fields.mjs @@ -0,0 +1,65 @@ +import {visit} from 'unist-util-visit'; +import {schemaFieldNames} from '../src/utils/schema.ts'; +import {createRequire} from 'module'; + +const require = createRequire(import.meta.url); + +function slugify(s) { + return s.toLowerCase() + .replace(/[^\w\s-]/g, ``) + .replace(/\s+/g, `-`) + .replace(/-+/g, `-`) + .replace(/^-|-$/g, ``); +} + +function buildFieldMap() { + const manifest = require(`../../documentation/src/utils/configuration/manifest.json`); + const yarnrc = require(`../../documentation/src/utils/configuration/yarnrc.json`); + + const map = new Map(); + + for (const name of schemaFieldNames(manifest)) { + map.set(name, {url: `/configuration/manifest.html`, anchor: `field-${slugify(name)}`}); + } + + for (const name of schemaFieldNames(yarnrc)) { + if (!map.has(name)) + map.set(name, {url: `/configuration/yarnrc.html`, anchor: `field-${slugify(name)}`}); + } + + map.set(`package.json`, {url: `/configuration/manifest.html`, anchor: null}); + map.set(`.yarnrc.yml`, {url: `/configuration/yarnrc.html`, anchor: null}); + + return map; +} + +export default function remarkAutolinkFields() { + const fieldMap = buildFieldMap(); + + return tree => { + const replacements = []; + + visit(tree, `inlineCode`, (node, index, parent) => { + if (!parent || index === undefined) + return; + + if (parent.type === `heading` || parent.type === `link`) + return; + + const target = fieldMap.get(node.value); + if (!target) + return; + + replacements.push({parent, index, node, target}); + }); + + for (let i = replacements.length - 1; i >= 0; i--) { + const {parent, index, node, target} = replacements[i]; + parent.children.splice(index, 1, { + type: `link`, + url: target.anchor ? `${target.url}#${target.anchor}` : target.url, + children: [node], + }); + } + }; +} diff --git a/website/plugins/remark-bluesky.mjs b/website/plugins/remark-bluesky.mjs new file mode 100644 index 00000000..610e3d2b --- /dev/null +++ b/website/plugins/remark-bluesky.mjs @@ -0,0 +1,64 @@ +import {visit} from 'unist-util-visit'; + +function escapeHtml(str) { + return str + .replace(/&/g, `&`) + .replace(//g, `>`) + .replace(/"/g, `"`); +} + +function renderEmbed({handle, displayName, avatar, text, date, likes, postUrl, profileUrl}) { + const butterfly = `Bluesky`; + const heart = ``; + + return [ + `
`, + `
`, + `
`, + ` `, + `
`, + ` ${escapeHtml(displayName)}`, + ` @${escapeHtml(handle)}`, + `
`, + ` `, + `
`, + `
${escapeHtml(text)}
`, + ` `, + `
`, + `
`, + ].filter(Boolean).join(`\n`); +} + +export default function remarkBluesky() { + return tree => { + visit(tree, `containerDirective`, node => { + if (node.name !== `bsky`) return; + + const {handle, displayName, avatar, date, likes, post} = node.attributes; + const profileUrl = `https://bsky.app/profile/${handle}`; + const postUrl = `https://bsky.app/profile/${handle}/post/${post}`; + + const text = node.children + .filter(c => c.type === `paragraph`) + .map(p => p.children.map(c => c.value ?? ``).join(``)) + .join(`\n`); + + node.type = `html`; + node.value = renderEmbed({ + handle, + displayName, + avatar, + text, + date, + likes: parseInt(likes, 10) || 0, + postUrl, + profileUrl, + }); + node.children = []; + }); + }; +} diff --git a/website/plugins/remark-docs.mjs b/website/plugins/remark-docs.mjs new file mode 100644 index 00000000..dcc626fd --- /dev/null +++ b/website/plugins/remark-docs.mjs @@ -0,0 +1,259 @@ +import {visit} from 'unist-util-visit'; +import {createHighlighter, createCssVariablesTheme} from 'shiki'; + +const cssVarsTheme = createCssVariablesTheme({ + name: `css-variables`, + variablePrefix: `--shiki-`, + variableDefaults: {}, + fontStyle: true, +}); + +function escapeHtml(str) { + return str + .replace(/&/g, `&`) + .replace(//g, `>`) + .replace(/"/g, `"`); +} + +function toString(node) { + if (node.type === `text`) return node.value; + if (node.children) return node.children.map(toString).join(``); + return ``; +} + +function slugify(s) { + return s.toLowerCase() + .replace(/[^\w\s-]/g, ``) + .replace(/\s+/g, `-`) + .replace(/-+/g, `-`) + .replace(/^-|-$/g, ``); +} + +const LANG_ALIASES = {js: `javascript`, ts: `typescript`, sh: `shell`}; + +let _hlPromise; +function getHighlighter() { + if (!_hlPromise) { + _hlPromise = createHighlighter({ + themes: [cssVarsTheme], + langs: [`javascript`, `typescript`, `json`, `yaml`, `bash`, `html`, `css`, `jsx`, `tsx`, `diff`, `shell`], + }); + } + return _hlPromise; +} + +async function highlightCode(code, lang) { + if (!lang) return escapeHtml(code); + const resolved = LANG_ALIASES[lang] || lang; + try { + const hl = await getHighlighter(); + if (!hl.getLoadedLanguages().includes(resolved)) return escapeHtml(code); + const html = hl.codeToHtml(code, {lang: resolved, theme: `css-variables`}); + const match = html.match(/(.+?)<\/code>/s); + return match ? match[1] : escapeHtml(code); + } catch { + return escapeHtml(code); + } +} + +const PILL_NAMES = [`type`, `required`, `since`, `default`, `deprecated`]; + +const PILL = `inline-flex items-center font-mono text-[11px] leading-none px-[7px] py-1 rounded-[5px] border tracking-[0.01em] whitespace-nowrap`; +const PILL_V = { + type: `border-[var(--pill-type-border)] bg-[var(--pill-type-bg)] text-[var(--pill-type-fg)]`, + required: `border-[var(--pill-req-border)] bg-[var(--pill-req-bg)] text-[var(--pill-req-fg)]`, + since: `border-[var(--accent-line)] bg-[var(--accent-soft)] text-[var(--accent)]`, + default: `border-[var(--line)] bg-[color-mix(in_oklch,var(--fg)_5%,transparent)] text-[var(--fg-dim)]`, + deprecated: `border-[var(--line)] bg-[color-mix(in_oklch,var(--fg)_5%,transparent)] text-[var(--fg-mute)] line-through decoration-[var(--pill-dep-strike)] decoration-1`, +}; + +const CLS = { + fieldHead: `flex flex-wrap items-center gap-2.5 mb-2.5 scroll-mt-[88px]`, + fieldName: `font-mono text-[15.5px] font-medium text-[var(--fg)] tracking-[-0.005em]`, + fieldAnchor: `field-anchor text-[var(--fg-mute)] no-underline font-normal transition-color duration-150 cursor-pointer select-none font-mono text-[15px] border-0 -ml-1 hover:text-[var(--accent)]`, + fieldList: `border-t border-[var(--line-strong)] mt-0`, +}; + +function pillToHtml(name, content) { + const cls = `${PILL} ${PILL_V[name] || PILL_V.default}`; + switch (name) { + case `type`: return `${content}`; + case `required`: return `required`; + case `since`: return `${content}`; + case `default`: return `default:${content}`; + case `deprecated`: return `${content}`; + default: return ``; + } +} + +function buildTerminalHtml(content) { + const lines = content.split(`\n`); + const spans = lines.map(line => { + if (line.startsWith(`# `)) { + return `${escapeHtml(line.slice(2))}`; + } else if (line.startsWith(`> `)) { + return `${escapeHtml(line.slice(2))}`; + } else { + return `${escapeHtml(line)}`; + } + }).join(`\n`); + + return `
\n${spans}\n
`; +} + +async function buildCodeBlockHtml(content, lang, title) { + const highlighted = await highlightCode(content, lang); + let html = `
`; + if (title) html += `\n${escapeHtml(title)}`; + html += `\n
${highlighted}
\n
`; + return html; +} + +function isFieldHeading(node) { + if (node.type !== `heading`) return false; + const meaningful = node.children.filter(c => !(c.type === `text` && !c.value.trim())); + if (!meaningful.length) return false; + if (meaningful[0].type !== `inlineCode`) return false; + return meaningful.slice(1).every(c => c.type === `textDirective` && PILL_NAMES.includes(c.name)); +} + +function processFieldHeadings(tree) { + const children = tree.children; + const newChildren = []; + let i = 0; + + while (i < children.length) { + if (isFieldHeading(children[i])) { + const fieldDepth = children[i].depth; + const fields = []; + + while (i < children.length) { + if (!isFieldHeading(children[i])) break; + + const heading = children[i]; + const body = []; + i++; + + while (i < children.length) { + if (isFieldHeading(children[i])) break; + if (children[i].type === `heading` && children[i].depth <= fieldDepth) break; + body.push(children[i]); + i++; + } + + fields.push({heading, body}); + } + + for (const field of fields) { + const inlineCode = field.heading.children.find(c => c.type === `inlineCode`); + const name = inlineCode?.value || ``; + const directives = field.heading.children.filter(c => c.type === `textDirective`); + const pillsHtml = directives.map(d => pillToHtml(d.name, toString(d))).join(``); + const id = `field-${slugify(name)}`; + + const nameHtml = `${escapeHtml(name)}`; + const anchorHtml = `#`; + + newChildren.push( + {type: `html`, value: `
${anchorHtml}${nameHtml}${pillsHtml}
`}, + ...field.body, + ); + } + } else { + newChildren.push(children[i]); + i++; + } + } + + tree.children = newChildren; +} + +export default function remarkDocs() { + return async tree => { + const codeNodes = []; + visit(tree, `code`, (node, index, parent) => { + if (!parent) return; + + if (node.lang === `terminal`) { + parent.children[index] = { + type: `html`, + value: buildTerminalHtml(node.value), + }; + return index; + } + + codeNodes.push({node, index, parent}); + }); + + await Promise.all(codeNodes.map(async ({node, index, parent}) => { + const titleMatch = node.meta?.match(/title="([^"]+)"/); + parent.children[index] = { + type: `html`, + value: await buildCodeBlockHtml(node.value, node.lang || ``, titleMatch?.[1] || ``), + }; + })); + + visit(tree, `containerDirective`, (node, index, parent) => { + if (!parent) return; + const type = node.name; + + if ([`note`, `tip`, `warning`, `danger`].includes(type)) { + const labelChild = node.children.find(c => c.data?.directiveLabel); + const label = labelChild ? toString(labelChild) : type.toUpperCase(); + + node.children = node.children.filter(c => !c.data?.directiveLabel); + + const data = node.data || (node.data = {}); + data.hName = `div`; + data.hProperties = { + className: [`admonition`, type], + dataAdmonition: type, + dataLabel: label, + }; + } + + if (type === `steps`) { + const ol = node.children.find(c => c.type === `list` && c.ordered); + if (ol) { + const data = ol.data || (ol.data = {}); + data.hProperties = {...(data.hProperties || {}), className: [`steps`]}; + parent.children[index] = ol; + return index; + } + } + }); + + visit(tree, `list`, node => { + if (!node.ordered) return; + for (const item of node.children) { + if (item.type !== `listItem`) continue; + item.children = [ + {type: `html`, value: `
`}, + ...item.children, + {type: `html`, value: `
`}, + ]; + } + }); + + processFieldHeadings(tree); + + visit(tree, `inlineCode`, (node, index, parent) => { + if (!parent || parent.type === `link`) return; + const match = node.value.match(/^([a-z]+):$/); + if (!match) return; + parent.children[index] = { + type: `link`, + url: `/protocol/${match[1]}.html`, + children: [{type: `inlineCode`, value: node.value}], + }; + }); + + visit(tree, `textDirective`, (node, index, parent) => { + if (!parent || !PILL_NAMES.includes(node.name)) return; + const content = toString(node); + parent.children[index] = {type: `html`, value: pillToHtml(node.name, content)}; + return index; + }); + }; +} diff --git a/website/plugins/remark-mermaid.mjs b/website/plugins/remark-mermaid.mjs new file mode 100644 index 00000000..86c380d1 --- /dev/null +++ b/website/plugins/remark-mermaid.mjs @@ -0,0 +1,69 @@ +import {readFileSync} from 'fs'; +import {createRequire} from 'module'; +import {visit} from 'unist-util-visit'; + +const require = createRequire(import.meta.url); +const mermaidJs = readFileSync(require.resolve(`mermaid/dist/mermaid.min.js`), `utf-8`); + +let _browser; +async function getBrowser() { + if (!_browser) { + const puppeteer = await import(`puppeteer`); + _browser = await puppeteer.default.launch(); + } + return _browser; +} + +let _counter = 0; + +async function renderMermaid(code) { + const browser = await getBrowser(); + const page = await browser.newPage(); + + try { + await page.setContent(``); + await page.addScriptTag({content: mermaidJs}); + + const baseId = `m${++_counter}`; + + return await page.evaluate(async (code, baseId) => { + const svgs = {}; + for (const [key, theme] of [[`dark`, `dark`], [`light`, `default`]]) { + document.body.innerHTML = ``; + window.mermaid.initialize({startOnLoad: false, look: `handDrawn`, theme}); + const {svg} = await window.mermaid.render(baseId + key[0], code); + svgs[key] = svg; + } + return svgs; + }, code, baseId); + } finally { + await page.close(); + } +} + +export default function remarkMermaid() { + return async tree => { + const nodes = []; + + visit(tree, `code`, (node, index, parent) => { + if (!parent || node.lang !== `mermaid`) return; + nodes.push({node, index, parent}); + }); + + if (!nodes.length) return; + + await Promise.all(nodes.map(async ({node, index, parent}) => { + const {dark, light} = await renderMermaid(node.value); + + parent.children[index] = { + type: `html`, + value: [ + `
`, + `
${dark}
`, + `
${light}
`, + `
`, + ].join(`\n`), + }; + })); + }; +} diff --git a/website/plugins/test-rehype-footnote-tooltips.mjs b/website/plugins/test-rehype-footnote-tooltips.mjs new file mode 100644 index 00000000..95e1a5d5 --- /dev/null +++ b/website/plugins/test-rehype-footnote-tooltips.mjs @@ -0,0 +1,177 @@ +import {unified} from 'unified'; +import remarkParse from 'remark-parse'; +import remarkGfm from 'remark-gfm'; +import remarkRehype from 'remark-rehype'; +import rehypeRaw from 'rehype-raw'; +import rehypeStringify from 'rehype-stringify'; + +import rehypeFootnoteTooltips from './rehype-footnote-tooltips.mjs'; + +async function render(md) { + const file = await unified() + .use(remarkParse) + .use(remarkGfm) + .use(remarkRehype, {allowDangerousHtml: true}) + .use(rehypeRaw) + .use(rehypeFootnoteTooltips) + .use(rehypeStringify, {allowDangerousHtml: true}) + .process(md); + + return String(file); +} + +let passed = 0; +let failed = 0; + +function assert(condition, label) { + if (condition) { + passed++; + console.log(` PASS ${label}`); + } else { + failed++; + console.log(` FAIL ${label}`); + } +} + +function getTooltipContent(html) { + const m = html.match(/([\s\S]*?)<\/span>/); + return m ? m[1] : null; +} + +// ── Test 1: basic footnote ── + +const basic = await render(` +Hello world[^1]. + +[^1]: This is a footnote. +`); + +console.log(`\n─── Test 1: basic footnote ───`); +console.log(basic, `\n`); + +assert(basic.includes(`fn-ref`), `sup gets fn-ref class`); +assert(basic.includes(`fn-tooltip`), `tooltip element exists`); +assert(!basic.includes(`]*class="[^"]*fn-ref[^"]*"[^>]*>([\s\S]*?)<\/sup>/); +assert(fnRefMatch !== null, `fn-ref sup found`); +if (fnRefMatch) { + assert(fnRefMatch[1].includes(`fn-tooltip`), `tooltip is inside fn-ref`); + assert(fnRefMatch[1].includes(`This is a footnote`), `content inside fn-ref`); +} + +const content1 = getTooltipContent(basic); +assert(content1 !== null && !content1.includes(`

`), `no

in tooltip`); +assert(content1 !== null && content1.includes(`This is a footnote`), `text content present`); + +// ── Test 2: footnotes section stays visible ── + +console.log(`─── Test 2: footnotes section visible ───`); + +assert(!basic.includes(`class="footnotes sr-only"`), `no sr-only on footnotes section`); +assert(basic.includes(`data-footnotes`), `footnotes section present`); + +// ── Test 3: click-through links work ── + +console.log(`─── Test 3: click-through links ───`); + +const refLink = basic.match(/]*href="(#user-content-fn-[^"]*)"[^>]*data-footnote-ref/); +assert(refLink !== null, `footnote ref link exists`); +if (refLink) { + const targetId = refLink[1].replace(`#`, ``); + assert(basic.includes(`id="${targetId}"`), `link target exists in footnotes section`); +} + +const backrefLink = basic.match(/]*href="(#user-content-fnref-[^"]*)"[^>]*data-footnote-backref/); +assert(backrefLink !== null, `backref link exists in footnotes section`); +if (backrefLink) { + const backTargetId = backrefLink[1].replace(`#`, ``); + assert(basic.includes(`id="${backTargetId}"`), `backref target exists`); +} + +// ── Test 4: multiple footnotes ── + +const multi = await render(` +First[^a] and second[^b]. + +[^a]: Alpha note. +[^b]: Beta note. +`); + +console.log(`\n─── Test 4: multiple footnotes ───`); +console.log(multi, `\n`); + +assert(multi.includes(`Alpha note`), `first footnote content`); +assert(multi.includes(`Beta note`), `second footnote content`); + +const refMatches = multi.match(/]*class="[^"]*fn-ref[^"]*"[^>]*>/g); +assert(refMatches && refMatches.length === 2, `two fn-ref elements`); + +// ── Test 5: rich content ── + +const rich = await render(` +Text[^1]. + +[^1]: Contains **bold**, \`code\`, and [a link](https://example.com). +`); + +console.log(`─── Test 5: rich content ───`); +console.log(rich, `\n`); + +assert(rich.includes(`bold`), `bold in tooltip`); +assert(rich.includes(`code`), `code in tooltip`); +assert(rich.includes(`href="https://example.com"`), `link in tooltip`); + +const content5 = getTooltipContent(rich); +assert(content5 !== null && !content5.includes(`

`), `no

in rich tooltip`); + +// ── Test 6: no footnotes = no transformation ── + +const noFn = await render(` +Just a regular paragraph. +`); + +console.log(`─── Test 6: no footnotes ───`); + +assert(!noFn.includes(`fn-ref`), `no fn-ref`); +assert(!noFn.includes(`fn-tooltip`), `no tooltip`); + +// ── Test 7: backref stripped from tooltip ── + +console.log(`─── Test 7: backref stripped from tooltip ───`); + +const content7 = getTooltipContent(basic); +if (content7) { + assert(!content7.includes(`data-footnote-backref`), `backref removed from tooltip`); + assert(!content7.includes(`↩`), `backref arrow removed from tooltip`); +} else { + assert(false, `could not find tooltip content`); + assert(false, `(skipped backref arrow check)`); +} + +// ── Test 8: multi-paragraph footnote ── + +const multiPara = await render(` +Text[^1]. + +[^1]: First paragraph. + + Second paragraph. +`); + +console.log(`\n─── Test 8: multi-paragraph footnote ───`); +console.log(multiPara, `\n`); + +const content8 = getTooltipContent(multiPara); +assert(content8 !== null && content8.includes(`First paragraph`), `first paragraph present`); +assert(content8 !== null && content8.includes(`Second paragraph`), `second paragraph present`); +assert(content8 !== null && content8.includes(`
`), `paragraphs separated by
`); +assert(content8 !== null && !content8.includes(`

`), `no

in multi-paragraph`); + +// ── Summary ── + +console.log(`${'═'.repeat(40)}`); +console.log(` ${passed} passed, ${failed} failed`); +console.log(`${'═'.repeat(40)}\n`); + +process.exit(failed > 0 ? 1 : 0); diff --git a/website/public/_redirects b/website/public/_redirects new file mode 100644 index 00000000..289c113a --- /dev/null +++ b/website/public/_redirects @@ -0,0 +1 @@ +/package/* /package.html 200 diff --git a/website/public/blog/blog.js b/website/public/blog/blog.js new file mode 100644 index 00000000..e189dbd9 --- /dev/null +++ b/website/public/blog/blog.js @@ -0,0 +1,83 @@ +(function () { + function toast(msg) { + let t = document.querySelector('.blog-toast'); + if (!t) { + t = document.createElement('div'); + t.className = 'blog-toast'; + t.setAttribute('role', 'status'); + Object.assign(t.style, { + position: 'fixed', left: '50%', bottom: '28px', + transform: 'translateX(-50%) translateY(10px)', + padding: '10px 16px', borderRadius: '10px', + background: 'color-mix(in oklch, var(--bg-0) 80%, transparent)', + border: '1px solid var(--line-strong)', color: 'var(--fg)', + fontSize: '13px', fontFamily: 'inherit', zIndex: 100, + backdropFilter: 'blur(10px)', opacity: '0', + transition: 'opacity 0.2s, transform 0.2s', pointerEvents: 'none', + }); + document.body.appendChild(t); + } + t.textContent = msg; + requestAnimationFrame(() => { + t.style.opacity = '1'; + t.style.transform = 'translateX(-50%) translateY(0)'; + }); + clearTimeout(t._timer); + t._timer = setTimeout(() => { + t.style.opacity = '0'; + t.style.transform = 'translateX(-50%) translateY(10px)'; + }, 1600); + } + + var prose = document.querySelector('.article-prose'); + if (!prose) return; + + var headings = Array.from(prose.querySelectorAll('h2, h3')); + headings.forEach(function (h) { + var anchor = h.querySelector('.heading-anchor'); + if (anchor) { + anchor.addEventListener('click', function (e) { + e.preventDefault(); + var url = location.origin + location.pathname + '#' + h.id; + history.replaceState(null, '', '#' + h.id); + if (navigator.clipboard) { + navigator.clipboard.writeText(url).then( + function () { toast('Link copied'); }, + function () { toast('Press \u2318C to copy'); } + ); + } + }); + } + }); + + var toc = document.querySelector('.toc'); + if (toc) { + var h2s = headings.filter(function (h) { return h.tagName === 'H2'; }); + var links = Array.from(toc.querySelectorAll('a')); + function onScroll() { + var y = window.scrollY + 140; + var activeId = h2s[0].id; + for (var i = 0; i < h2s.length; i++) { + if (h2s[i].offsetTop <= y) activeId = h2s[i].id; + } + links.forEach(function (l) { + l.classList.toggle('active', l.getAttribute('href') === '#' + activeId); + }); + } + window.addEventListener('scroll', onScroll, { passive: true }); + onScroll(); + } + + document.querySelectorAll('[data-share="copy-url"]').forEach(function (btn) { + btn.addEventListener('click', function (e) { + e.preventDefault(); + var url = location.href; + if (navigator.clipboard) { + navigator.clipboard.writeText(url).then( + function () { toast('Link copied'); }, + function () { toast('Press \u2318C to copy'); } + ); + } + }); + }); +})(); diff --git a/website/public/cat.png b/website/public/cat.png new file mode 100644 index 00000000..2dc3b906 Binary files /dev/null and b/website/public/cat.png differ diff --git a/website/public/deck-stage.js b/website/public/deck-stage.js new file mode 100644 index 00000000..5bab9ccf --- /dev/null +++ b/website/public/deck-stage.js @@ -0,0 +1,511 @@ +(() => { + const DESIGN_W_DEFAULT = 1920; + const DESIGN_H_DEFAULT = 1080; + const OVERLAY_HIDE_MS = 1800; + + const pad2 = (n) => String(n).padStart(2, '0'); + + const stylesheet = ` + :host { + position: fixed; + inset: 0; + display: block; + background: transparent; + color: #fff; + font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; + overflow: hidden; + } + + .stage { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + } + + .canvas { + position: relative; + transform-origin: center center; + flex-shrink: 0; + background: transparent; + will-change: transform; + } + + ::slotted(*) { + position: absolute !important; + inset: 0 !important; + width: 100% !important; + height: 100% !important; + box-sizing: border-box !important; + overflow: hidden; + opacity: 0; + pointer-events: none; + visibility: hidden; + } + ::slotted([data-deck-active]) { + opacity: 1; + pointer-events: auto; + visibility: visible; + } + + .tapzones { + position: fixed; + inset: 0; + display: flex; + z-index: 2147482000; + pointer-events: none; + } + .tapzone { + flex: 1; + pointer-events: auto; + -webkit-tap-highlight-color: transparent; + } + @media (hover: hover) and (pointer: fine) { + .tapzones { display: none; } + } + + .overlay { + position: fixed; + left: 50%; + bottom: 22px; + transform: translate(-50%, 6px) scale(0.92); + filter: blur(6px); + display: flex; + align-items: center; + gap: 4px; + padding: 4px; + background: #000; + color: #fff; + border-radius: 999px; + font-size: 12px; + font-feature-settings: "tnum" 1; + letter-spacing: 0.01em; + opacity: 0; + pointer-events: none; + transition: opacity 260ms ease, transform 260ms cubic-bezier(.2,.8,.2,1), filter 260ms ease; + transform-origin: center bottom; + z-index: 2147483000; + user-select: none; + } + .overlay[data-visible] { + opacity: 1; + pointer-events: auto; + transform: translate(-50%, 0) scale(1); + filter: blur(0); + } + + .btn { + appearance: none; + -webkit-appearance: none; + background: transparent; + border: 0; + margin: 0; + padding: 0; + color: inherit; + font: inherit; + cursor: default; + display: inline-flex; + align-items: center; + justify-content: center; + height: 28px; + min-width: 28px; + border-radius: 999px; + color: rgba(255,255,255,0.72); + transition: background 140ms ease, color 140ms ease; + -webkit-tap-highlight-color: transparent; + } + .btn:hover { background: rgba(255,255,255,0.12); color: #fff; } + .btn:active { background: rgba(255,255,255,0.18); } + .btn:focus { outline: none; } + .btn:focus-visible { outline: none; } + .btn::-moz-focus-inner { border: 0; } + .btn svg { width: 14px; height: 14px; display: block; } + .btn.reset, + .btn.present { + font-size: 11px; + font-weight: 500; + letter-spacing: 0.02em; + padding: 0 10px 0 12px; + gap: 6px; + color: rgba(255,255,255,0.72); + } + .btn.reset .kbd, + .btn.present .kbd { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 16px; + height: 16px; + padding: 0 4px; + font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; + font-size: 10px; + line-height: 1; + color: rgba(255,255,255,0.88); + background: rgba(255,255,255,0.12); + border-radius: 4px; + } + + .count { + font-variant-numeric: tabular-nums; + color: #fff; + font-weight: 500; + padding: 0 8px; + min-width: 42px; + text-align: center; + font-size: 12px; + } + .count .sep { color: rgba(255,255,255,0.45); margin: 0 3px; font-weight: 400; } + .count .total { color: rgba(255,255,255,0.55); } + + .divider { + width: 1px; + height: 14px; + background: rgba(255,255,255,0.18); + margin: 0 2px; + } + + @media print { + :host { + position: static; + inset: auto; + background: none; + overflow: visible; + color: inherit; + } + .stage { position: static; display: block; } + .canvas { + transform: none !important; + width: auto !important; + height: auto !important; + background: none; + will-change: auto; + } + ::slotted(*) { + position: relative !important; + inset: auto !important; + width: var(--deck-design-w) !important; + height: var(--deck-design-h) !important; + box-sizing: border-box !important; + opacity: 1 !important; + visibility: visible !important; + pointer-events: auto; + break-after: page; + page-break-after: always; + break-inside: avoid; + overflow: hidden; + } + ::slotted(*:last-child) { + break-after: auto; + page-break-after: auto; + } + .overlay, .tapzones { display: none !important; } + } + `; + + class DeckStage extends HTMLElement { + static get observedAttributes() { return ['width', 'height', 'noscale']; } + + constructor() { + super(); + this._root = this.attachShadow({ mode: 'open' }); + this._index = 0; + this._slides = []; + this._hideTimer = null; + this._mouseIdleTimer = null; + this._isPresenting = false; + + this._onKey = this._onKey.bind(this); + this._onResize = this._onResize.bind(this); + this._onSlotChange = this._onSlotChange.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onTapBack = this._onTapBack.bind(this); + this._onTapForward = this._onTapForward.bind(this); + this._onFullscreenChange = this._onFullscreenChange.bind(this); + this._togglePresent = this._togglePresent.bind(this); + } + + get designWidth() { + return parseInt(this.getAttribute('width'), 10) || DESIGN_W_DEFAULT; + } + get designHeight() { + return parseInt(this.getAttribute('height'), 10) || DESIGN_H_DEFAULT; + } + + connectedCallback() { + this._render(); + window.addEventListener('keydown', this._onKey); + window.addEventListener('resize', this._onResize); + window.addEventListener('mousemove', this._onMouseMove, { passive: true }); + document.addEventListener('fullscreenchange', this._onFullscreenChange); + } + + disconnectedCallback() { + window.removeEventListener('keydown', this._onKey); + window.removeEventListener('resize', this._onResize); + window.removeEventListener('mousemove', this._onMouseMove); + document.removeEventListener('fullscreenchange', this._onFullscreenChange); + if (this._hideTimer) clearTimeout(this._hideTimer); + if (this._mouseIdleTimer) clearTimeout(this._mouseIdleTimer); + } + + attributeChangedCallback() { + if (this._canvas) { + this._canvas.style.width = this.designWidth + 'px'; + this._canvas.style.height = this.designHeight + 'px'; + this._canvas.style.setProperty('--deck-design-w', this.designWidth + 'px'); + this._canvas.style.setProperty('--deck-design-h', this.designHeight + 'px'); + this._fit(); + } + } + + _render() { + const style = document.createElement('style'); + style.textContent = stylesheet; + + const stage = document.createElement('div'); + stage.className = 'stage'; + + const canvas = document.createElement('div'); + canvas.className = 'canvas'; + canvas.style.width = this.designWidth + 'px'; + canvas.style.height = this.designHeight + 'px'; + canvas.style.setProperty('--deck-design-w', this.designWidth + 'px'); + canvas.style.setProperty('--deck-design-h', this.designHeight + 'px'); + + const slot = document.createElement('slot'); + slot.addEventListener('slotchange', this._onSlotChange); + canvas.appendChild(slot); + stage.appendChild(canvas); + + const tapzones = document.createElement('div'); + tapzones.className = 'tapzones'; + tapzones.setAttribute('aria-hidden', 'true'); + const tzBack = document.createElement('div'); + tzBack.className = 'tapzone'; + const tzMid = document.createElement('div'); + tzMid.className = 'tapzone'; + tzMid.style.pointerEvents = 'none'; + const tzFwd = document.createElement('div'); + tzFwd.className = 'tapzone'; + tzBack.addEventListener('click', this._onTapBack); + tzFwd.addEventListener('click', this._onTapForward); + tapzones.append(tzBack, tzMid, tzFwd); + + const overlay = document.createElement('div'); + overlay.className = 'overlay'; + overlay.setAttribute('role', 'toolbar'); + overlay.setAttribute('aria-label', 'Deck controls'); + overlay.innerHTML = ` + + 1/1 + + + + + `; + + overlay.querySelector('.prev').addEventListener('click', () => this._go(this._index - 1, 'click')); + overlay.querySelector('.next').addEventListener('click', () => this._go(this._index + 1, 'click')); + overlay.querySelector('.reset').addEventListener('click', () => this._go(0, 'click')); + overlay.querySelector('.present').addEventListener('click', this._togglePresent); + + this._root.append(style, stage, tapzones, overlay); + this._canvas = canvas; + this._slot = slot; + this._overlay = overlay; + this._countEl = overlay.querySelector('.current'); + this._totalEl = overlay.querySelector('.total'); + } + + _onSlotChange() { + this._collectSlides(); + this._restoreIndex(); + this._applyIndex({ showOverlay: false, broadcast: true, reason: 'init' }); + this._fit(); + } + + _collectSlides() { + const assigned = this._slot.assignedElements({ flatten: true }); + this._slides = assigned.filter((el) => { + const tag = el.tagName; + return tag !== 'TEMPLATE' && tag !== 'SCRIPT' && tag !== 'STYLE'; + }); + + this._slides.forEach((slide, i) => { + slide.setAttribute('data-deck-slide', String(i)); + }); + + const total = this._slides.length || 1; + const totalStr = pad2(total); + if (this._totalEl) this._totalEl.textContent = String(total); + this._slides.forEach((slide, i) => { + const slideStr = pad2(i + 1); + slide.querySelectorAll('[data-deck-slide-fill]').forEach((el) => { + el.textContent = slideStr; + }); + slide.querySelectorAll('[data-deck-total-fill]').forEach((el) => { + el.textContent = totalStr; + }); + }); + if (this._index >= this._slides.length) this._index = Math.max(0, this._slides.length - 1); + } + + _restoreIndex() { + const h = (location.hash || '').match(/^#(\d+)$/); + if (h) { + const n = parseInt(h[1], 10) - 1; + if (n >= 0 && n < this._slides.length) this._index = n; + } + } + + _applyIndex({ showOverlay = true, broadcast = true, reason = 'init' } = {}) { + if (!this._slides.length) return; + const prev = this._prevIndex == null ? -1 : this._prevIndex; + const curr = this._index; + try { history.replaceState(null, '', '#' + (curr + 1)); } catch (e) {} + this._slides.forEach((s, i) => { + if (i === curr) s.setAttribute('data-deck-active', ''); + else s.removeAttribute('data-deck-active'); + }); + if (this._countEl) this._countEl.textContent = String(curr + 1); + + if (broadcast) { + const detail = { + index: curr, + previousIndex: prev, + total: this._slides.length, + slide: this._slides[curr] || null, + previousSlide: prev >= 0 ? (this._slides[prev] || null) : null, + reason, + }; + this.dispatchEvent(new CustomEvent('slidechange', { + detail, + bubbles: true, + composed: true, + })); + } + + this._prevIndex = curr; + if (showOverlay) this._flashOverlay(); + } + + _flashOverlay() { + if (!this._overlay) return; + if (this._isPresenting) return; + this._overlay.setAttribute('data-visible', ''); + if (this._hideTimer) clearTimeout(this._hideTimer); + this._hideTimer = setTimeout(() => { + this._overlay.removeAttribute('data-visible'); + }, OVERLAY_HIDE_MS); + } + + _togglePresent() { + if (document.fullscreenElement) { + if (document.exitFullscreen) document.exitFullscreen().catch(() => {}); + } else { + const el = document.documentElement; + if (el && el.requestFullscreen) el.requestFullscreen().catch(() => {}); + } + } + + _onFullscreenChange() { + this._isPresenting = !!document.fullscreenElement; + if (!this._overlay) return; + if (this._isPresenting) { + this._overlay.removeAttribute('data-visible'); + if (this._hideTimer) { + clearTimeout(this._hideTimer); + this._hideTimer = null; + } + } + } + + _fit() { + if (!this._canvas) return; + if (this.hasAttribute('noscale')) { + this._canvas.style.transform = 'none'; + return; + } + const vw = window.innerWidth; + const vh = window.innerHeight; + const s = Math.min(vw / this.designWidth, vh / this.designHeight); + this._canvas.style.transform = `scale(${s})`; + } + + _onResize() { this._fit(); } + + _onMouseMove() { + this._flashOverlay(); + } + + _onTapBack(e) { + e.preventDefault(); + this._go(this._index - 1, 'tap'); + } + + _onTapForward(e) { + e.preventDefault(); + this._go(this._index + 1, 'tap'); + } + + _onKey(e) { + const t = e.target; + if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return; + if (e.metaKey || e.ctrlKey || e.altKey) return; + + const key = e.key; + let handled = true; + + if (key === 'ArrowRight' || key === 'PageDown' || key === ' ' || key === 'Spacebar') { + this._go(this._index + 1, 'keyboard'); + } else if (key === 'ArrowLeft' || key === 'PageUp') { + this._go(this._index - 1, 'keyboard'); + } else if (key === 'Home') { + this._go(0, 'keyboard'); + } else if (key === 'End') { + this._go(this._slides.length - 1, 'keyboard'); + } else if (key === 'r' || key === 'R') { + this._go(0, 'keyboard'); + } else if (key === 'p' || key === 'P') { + this._togglePresent(); + } else if (/^[0-9]$/.test(key)) { + const n = key === '0' ? 9 : parseInt(key, 10) - 1; + if (n < this._slides.length) this._go(n, 'keyboard'); + } else { + handled = false; + } + + if (handled) { + e.preventDefault(); + this._flashOverlay(); + } + } + + _go(i, reason = 'api') { + if (!this._slides.length) return; + const clamped = Math.max(0, Math.min(this._slides.length - 1, i)); + if (clamped === this._index) { + this._flashOverlay(); + return; + } + this._index = clamped; + this._applyIndex({ showOverlay: true, broadcast: true, reason }); + } + + get index() { return this._index; } + get length() { return this._slides.length; } + goTo(i) { this._go(i, 'api'); } + next() { this._go(this._index + 1, 'api'); } + prev() { this._go(this._index - 1, 'api'); } + reset() { this._go(0, 'api'); } + } + + if (!customElements.get('deck-stage')) { + customElements.define('deck-stage', DeckStage); + } +})(); diff --git a/website/public/docs/docs.js b/website/public/docs/docs.js new file mode 100644 index 00000000..29d0a237 --- /dev/null +++ b/website/public/docs/docs.js @@ -0,0 +1,158 @@ +/* ─────────────── Shared starfield + theme (minimal) ─────────────── */ +(function () { + // Theme + const saved = localStorage.getItem(`yarn-theme`) || `dark`; + document.documentElement.setAttribute(`data-theme`, saved); + window.__theme = saved; + + function setTheme(t) { + document.documentElement.setAttribute(`data-theme`, t); + localStorage.setItem(`yarn-theme`, t); + window.__theme = t; + window.dispatchEvent(new CustomEvent(`themechange`, {detail: t})); + } + window.__setTheme = setTheme; + + const btn = document.getElementById(`theme-toggle`); + if (btn) btn.addEventListener(`click`, () => setTheme(window.__theme === `dark` ? `light` : `dark`)); + + // Starfield canvas (lighter, non-interactive) + const canvas = document.getElementById(`stars`); + if (!canvas) return; + const ctx = canvas.getContext(`2d`); + let W = window.innerWidth, H = window.innerHeight, DPR = 1; + let stars = []; + function resize() { + DPR = Math.min(window.devicePixelRatio || 1, 2); + W = window.innerWidth; + H = window.innerHeight; + canvas.width = W * DPR; + canvas.height = H * DPR; + canvas.style.width = `${W}px`; + canvas.style.height = `${H}px`; + ctx.setTransform(DPR, 0, 0, DPR, 0, 0); + init(); + } + function init() { + // Fewer stars than the landing — reading comfort + const count = Math.round(180 * (W * H) / (1920 * 1080)); + stars = []; + for (let i = 0; i < count; i++) { + stars.push({ + x: Math.random() * W, + y: Math.random() * H, + r: Math.random() * 1.1 + 0.2, + a: Math.random() * 0.5 + 0.25, + tp: Math.random() * Math.PI * 2, + ts: Math.random() * 0.6 + 0.2, + }); + } + } + let t = 0; + function tick(ts) { + t = ts * 0.001; + ctx.clearRect(0, 0, W, H); + const isDark = window.__theme === `dark`; + const color = isDark ? `255,255,255` : `255,200,100`; + for (const s of stars) { + const tw = 0.55 + 0.45 * Math.sin(t * s.ts + s.tp); + const a = s.a * tw * 0.7; + ctx.beginPath(); + ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); + ctx.fillStyle = `rgba(${color},${a.toFixed(3)})`; + ctx.fill(); + } + requestAnimationFrame(tick); + } + window.addEventListener(`resize`, resize); + resize(); + requestAnimationFrame(tick); +})(); + +/* ─────────────── Docs-specific features ─────────────── */ +(function () { + /* Toast */ + const toast = document.createElement(`div`); + toast.className = `toast`; + toast.setAttribute(`role`, `status`); + toast.setAttribute(`aria-live`, `polite`); + document.body.appendChild(toast); + let toastTimer; + function showToast(msg) { + toast.textContent = msg; + toast.classList.add(`show`); + clearTimeout(toastTimer); + toastTimer = setTimeout(() => toast.classList.remove(`show`), 1800); + } + window.__showToast = showToast; + + /* Heading + field anchors: click = copy URL (anchors injected at build time) */ + document.addEventListener(`click`, e => { + const anchor = e.target.closest(`.heading-anchor, .field-anchor`); + if (!anchor) return; + e.preventDefault(); + const href = anchor.getAttribute(`href`); + history.replaceState(null, ``, href); + const url = location.origin + location.pathname + href; + navigator.clipboard?.writeText(url).then( + () => showToast(`Link copied`), + () => showToast(`Press ⌘C to copy`), + ); + }); + + /* Copy buttons on terminal + code blocks */ + document.querySelectorAll(`.terminal, .code-block`).forEach(el => { + if (el.querySelector(`.copy-btn`)) return; + const btn = document.createElement(`button`); + btn.className = `copy-btn`; + btn.setAttribute(`aria-label`, `Copy code`); + btn.innerHTML = ``; + btn.addEventListener(`click`, () => { + // Strip '$ ' prompt + '# ' from terminal, else raw text + const text = Array.from(el.querySelectorAll(`.term-line, pre code, pre`)) + .map(line => { + if (line.classList && line.classList.contains(`term-line`)) { + if (line.classList.contains(`no-prompt`) || line.classList.contains(`out`)) return line.textContent; + if (line.classList.contains(`comment`)) return `# ${line.textContent}`; + return `$ ${line.textContent}`; + } + return line.textContent; + }) + .join(`\n`) || el.textContent; + const toCopy = el.classList.contains(`terminal`) + ? Array.from(el.querySelectorAll(`.term-line`)) + .filter(l => !l.classList.contains(`out`) && !l.classList.contains(`comment`)) + .map(l => l.textContent) + .join(`\n`) + : (el.querySelector(`pre code`) || el.querySelector(`pre`)).textContent; + navigator.clipboard?.writeText(toCopy).then(() => { + btn.classList.add(`copied`); + btn.innerHTML = ``; + setTimeout(() => { + btn.classList.remove(`copied`); + btn.innerHTML = ``; + }, 1400); + }); + }); + const target = el.classList.contains(`code-block`) ? el.querySelector(`pre`) || el : el; + target.appendChild(btn); + }); + + /* Scrollspy for sidebar: mark active link based on scroll */ + const sbLinks = document.querySelectorAll(`.docs-sidebar a.sb-link[data-section]`); + if (sbLinks.length) { + const sections = Array.from(document.querySelectorAll(`.prose h2[id], .prose h3[id]`)); + function onScroll() { + const y = window.scrollY + 120; + let activeId = sections[0]?.id; + for (const s of sections) if (s.offsetTop <= y) activeId = s.id; + sbLinks.forEach(a => { + const want = a.getAttribute(`href`)?.replace(/^#/, ``); + a.classList.toggle(`active`, want === activeId); + }); + } + window.addEventListener(`scroll`, onScroll, {passive: true}); + onScroll(); + } + +})(); diff --git a/website/public/favicon.svg b/website/public/favicon.svg new file mode 100644 index 00000000..a7cae018 --- /dev/null +++ b/website/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/website/public/jsnation-2026-06-test-results.png b/website/public/jsnation-2026-06-test-results.png new file mode 100644 index 00000000..50a7341c Binary files /dev/null and b/website/public/jsnation-2026-06-test-results.png differ diff --git a/website/public/logo.svg b/website/public/logo.svg new file mode 100644 index 00000000..f299f403 --- /dev/null +++ b/website/public/logo.svg @@ -0,0 +1 @@ + diff --git a/website/public/quiz.js b/website/public/quiz.js new file mode 100644 index 00000000..6391ae57 --- /dev/null +++ b/website/public/quiz.js @@ -0,0 +1,318 @@ +/* ────────── "Do you know Yarn?" quiz logic ────────── */ +/* QUESTIONS and LEVELS are injected at build time from config/quiz.json */ + +/* ────────── State ────────── */ +var state = { + order: [], + cursor: 0, + answers: {}, + startedFromSlug: null, +}; + +/* ────────── Utilities ────────── */ +function shuffle(arr) { + var a = arr.slice(); + for (var i = a.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var tmp = a[i]; a[i] = a[j]; a[j] = tmp; + } + return a; +} + +function slugToIndex(slug) { + return QUESTIONS.findIndex(function(q) { return q.slug === slug; }); +} + +function buildOrder() { + var hash = (location.hash || '').replace(/^#/, '').trim(); + var allIdx = QUESTIONS.map(function(_, i) { return i; }); + if (hash) { + var startIdx = slugToIndex(hash); + if (startIdx >= 0) { + state.startedFromSlug = hash; + var rest = shuffle(allIdx.filter(function(i) { return i !== startIdx; })); + return [startIdx].concat(rest); + } + } + return allIdx; +} + +function showToast(msg) { + var el = document.getElementById('toast'); + if (!el) { + el = document.createElement('div'); + el.id = 'toast'; + el.className = 'toast'; + document.body.appendChild(el); + } + el.textContent = msg; + el.classList.add('show'); + clearTimeout(showToast._t); + showToast._t = setTimeout(function() { el.classList.remove('show'); }, 1800); +} + +function copyToClipboard(text) { + if (navigator.clipboard && navigator.clipboard.writeText) { + return navigator.clipboard.writeText(text); + } + var ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + return Promise.resolve(); +} + +/* ────────── Rendering ────────── */ +var stage, shell, progressFill, progressNum, scoreNum, progressTotal; + +function updateProgress() { + var total = state.order.length; + var answered = Object.keys(state.answers).length; + var correct = 0; + for (var k in state.answers) if (state.answers[k].correct) correct++; + progressNum.textContent = Math.min(state.cursor + 1, total); + progressTotal.textContent = total; + scoreNum.textContent = correct; + var pct = (answered / total) * 100; + progressFill.style.width = pct + '%'; + if (shell) { + var started = answered > 0 || state.cursor > 0; + shell.classList.toggle('compact', started); + } +} + +function renderQuestion() { + updateProgress(); + var total = state.order.length; + if (state.cursor >= total) { + renderEnd(); + return; + } + var q = QUESTIONS[state.order[state.cursor]]; + history.replaceState(null, '', '#' + q.slug); + + var already = state.answers[q.slug]; + + var content = document.createElement('div'); + content.className = 'quiz-stage'; + content.innerHTML = + '

' + + '
' + + '
Question ' + (state.cursor + 1) + ' of ' + total + '
' + + '

' + q.question + '

' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
'; + stage.replaceChildren(content); + + var buttons = content.querySelectorAll('.q-btn'); + buttons.forEach(function(btn) { + btn.addEventListener('click', function() { + btn.classList.add('pulse'); + handleAnswer(q, btn.dataset.answer === 'true'); + }); + }); + + if (already) { + applyAnswerUI(content, q, already.picked); + } +} + +function answerIcons() { + return '' + + ''; +} + +function applyAnswerUI(root, q, picked) { + var correct = picked === q.answer; + var buttons = root.querySelectorAll('.q-btn'); + buttons.forEach(function(b) { + b.disabled = true; + var btnAnswer = b.dataset.answer === 'true'; + var isPicked = btnAnswer === picked; + var isCorrectAnswer = btnAnswer === q.answer; + if (isPicked) { + b.classList.add('picked', correct ? 'correct' : 'wrong'); + } else if (isCorrectAnswer) { + b.classList.add('revealed-correct'); + } + }); + + var line = correct ? q.rightLine : q.wrongLine; + var verdictLabel = correct ? 'Correct' : 'Not quite'; + var verdictClass = correct ? 'right' : 'wrong'; + + var reveal = root.querySelector('#reveal'); + reveal.innerHTML = + '
' + + dotIcon(verdictClass) + + '' + verdictLabel + '' + + '
' + + '

' + line + '

' + + q.explain.map(function(p) { return '

' + p + '

'; }).join('') + + '
' + + '' + + '' + + '
'; + reveal.classList.add('open'); + + reveal.querySelector('#next-btn').addEventListener('click', advance); + var shareBtn = reveal.querySelector('#share-btn'); + shareBtn.addEventListener('click', function() { + var url = location.origin + location.pathname + '#' + q.slug; + copyToClipboard(url).then(function() { + shareBtn.classList.add('copied'); + reveal.querySelector('#share-label').textContent = 'Link copied'; + setTimeout(function() { + shareBtn.classList.remove('copied'); + var lbl = reveal.querySelector('#share-label'); + if (lbl) lbl.textContent = 'Share question'; + }, 1800); + }); + }); +} + +function dotIcon(kind) { + var color = kind === 'right' ? 'var(--accent)' : 'var(--fg-mute)'; + return ''; +} + +function handleAnswer(q, picked) { + if (state.answers[q.slug]) return; + var correct = picked === q.answer; + state.answers[q.slug] = { picked: picked, correct: correct }; + updateProgress(); + applyAnswerUI(stage.querySelector('.quiz-stage'), q, picked); +} + +function advance() { + state.cursor += 1; + renderQuestion(); + requestAnimationFrame(function() { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); +} + +/* ────────── End screen ────────── */ +function renderEnd() { + history.replaceState(null, '', '#results'); + var total = state.order.length; + var correct = 0; + for (var k in state.answers) if (state.answers[k].correct) correct++; + var level = LEVELS[0]; + for (var i = LEVELS.length - 1; i >= 0; i--) { + if (correct >= LEVELS[i].min) { level = LEVELS[i]; break; } + } + + var recap = state.order.map(function(idx) { + var q = QUESTIONS[idx]; + var a = state.answers[q.slug]; + return '' + + '' + + '' + q.question + '' + + '' + + ''; + }).join(''); + + var content = document.createElement('div'); + content.className = 'quiz-stage'; + content.innerHTML = + '
' + + '
Your Yarn level
' + + '

' + level.title + '

' + + '
' + correct + ' / ' + total + ' correct
' + + '

' + level.tag + '

' + + '
' + + '' + + '' + + '
' + + '
' + + '
Your answers \u2014 tap to revisit a question
' + + recap + + '
' + + '
'; + stage.replaceChildren(content); + + document.getElementById('restart-btn').addEventListener('click', restart); + var sb = document.getElementById('share-score-btn'); + sb.addEventListener('click', function() { + var url = location.origin + location.pathname; + copyToClipboard(url).then(function() { + sb.classList.add('copied'); + document.getElementById('share-score-label').textContent = 'Link copied'; + setTimeout(function() { + sb.classList.remove('copied'); + var lbl = document.getElementById('share-score-label'); + if (lbl) lbl.textContent = 'Copy shareable link'; + }, 1800); + }); + }); + + content.querySelectorAll('.recap-row').forEach(function(row) { + row.addEventListener('click', function(e) { + e.preventDefault(); + var slug = row.dataset.slug; + var pos = state.order.findIndex(function(i) { return QUESTIONS[i].slug === slug; }); + if (pos >= 0) { + state.cursor = pos; + renderQuestion(); + requestAnimationFrame(function() { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); + } + }); + }); +} + +function restart() { + state.answers = {}; + state.cursor = 0; + state.order = shuffle(QUESTIONS.map(function(_, i) { return i; })); + history.replaceState(null, '', location.pathname); + renderQuestion(); + requestAnimationFrame(function() { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); +} + +/* ────────── Init ────────── */ +function quizInit() { + stage = document.getElementById('stage'); + shell = document.querySelector('.quiz-shell'); + progressFill = document.getElementById('progress-fill'); + progressNum = document.getElementById('progress-num'); + scoreNum = document.getElementById('score-num'); + progressTotal = document.getElementById('progress-total'); + + if (!stage) return; + state.order = buildOrder(); + renderQuestion(); +} + +document.addEventListener('DOMContentLoaded', quizInit); diff --git a/website/public/robots.txt b/website/public/robots.txt new file mode 100644 index 00000000..944b6094 --- /dev/null +++ b/website/public/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Allow: / +Disallow: /presentation/ + +Sitemap: https://yarnpkg.com/sitemap-index.xml diff --git a/website/public/stars/island-01.png b/website/public/stars/island-01.png new file mode 100644 index 00000000..7d70ba00 Binary files /dev/null and b/website/public/stars/island-01.png differ diff --git a/website/public/stars/island-02.png b/website/public/stars/island-02.png new file mode 100644 index 00000000..1defb19b Binary files /dev/null and b/website/public/stars/island-02.png differ diff --git a/website/public/stars/island-03.png b/website/public/stars/island-03.png new file mode 100644 index 00000000..816907ef Binary files /dev/null and b/website/public/stars/island-03.png differ diff --git a/website/public/stars/island-04.png b/website/public/stars/island-04.png new file mode 100644 index 00000000..d5913d5d Binary files /dev/null and b/website/public/stars/island-04.png differ diff --git a/website/public/stars/island-05.png b/website/public/stars/island-05.png new file mode 100644 index 00000000..9610b025 Binary files /dev/null and b/website/public/stars/island-05.png differ diff --git a/website/public/stars/island-06.png b/website/public/stars/island-06.png new file mode 100644 index 00000000..1e9ae6e9 Binary files /dev/null and b/website/public/stars/island-06.png differ diff --git a/website/public/stars/island-07.png b/website/public/stars/island-07.png new file mode 100644 index 00000000..85dae8b7 Binary files /dev/null and b/website/public/stars/island-07.png differ diff --git a/website/public/stars/island-08.png b/website/public/stars/island-08.png new file mode 100644 index 00000000..bec66677 Binary files /dev/null and b/website/public/stars/island-08.png differ diff --git a/website/public/stars/island-09.png b/website/public/stars/island-09.png new file mode 100644 index 00000000..0a7f0c37 Binary files /dev/null and b/website/public/stars/island-09.png differ diff --git a/website/public/stars/island-10.png b/website/public/stars/island-10.png new file mode 100644 index 00000000..2ae19f84 Binary files /dev/null and b/website/public/stars/island-10.png differ diff --git a/website/public/stars/island-11.png b/website/public/stars/island-11.png new file mode 100644 index 00000000..62e29ecb Binary files /dev/null and b/website/public/stars/island-11.png differ diff --git a/website/public/stars/island-12.png b/website/public/stars/island-12.png new file mode 100644 index 00000000..abc2fb86 Binary files /dev/null and b/website/public/stars/island-12.png differ diff --git a/website/public/stars/island-13.png b/website/public/stars/island-13.png new file mode 100644 index 00000000..2faa7362 Binary files /dev/null and b/website/public/stars/island-13.png differ diff --git a/website/public/stars/island-14.png b/website/public/stars/island-14.png new file mode 100644 index 00000000..c0570e2e Binary files /dev/null and b/website/public/stars/island-14.png differ diff --git a/website/public/stars/island-15.png b/website/public/stars/island-15.png new file mode 100644 index 00000000..46008935 Binary files /dev/null and b/website/public/stars/island-15.png differ diff --git a/website/public/stars/island-16.png b/website/public/stars/island-16.png new file mode 100644 index 00000000..96f7e93e Binary files /dev/null and b/website/public/stars/island-16.png differ diff --git a/website/public/stars/island-17.png b/website/public/stars/island-17.png new file mode 100644 index 00000000..d26ac2ce Binary files /dev/null and b/website/public/stars/island-17.png differ diff --git a/website/public/stars/island-18.png b/website/public/stars/island-18.png new file mode 100644 index 00000000..1da1deb9 Binary files /dev/null and b/website/public/stars/island-18.png differ diff --git a/website/public/stars/island-19.png b/website/public/stars/island-19.png new file mode 100644 index 00000000..1a47fb15 Binary files /dev/null and b/website/public/stars/island-19.png differ diff --git a/website/public/stars/island-20.png b/website/public/stars/island-20.png new file mode 100644 index 00000000..feae8cbc Binary files /dev/null and b/website/public/stars/island-20.png differ diff --git a/website/public/stars/island-21.png b/website/public/stars/island-21.png new file mode 100644 index 00000000..f95c564f Binary files /dev/null and b/website/public/stars/island-21.png differ diff --git a/website/public/stars/island-22.png b/website/public/stars/island-22.png new file mode 100644 index 00000000..dd70ae3a Binary files /dev/null and b/website/public/stars/island-22.png differ diff --git a/website/public/stars/island-23.png b/website/public/stars/island-23.png new file mode 100644 index 00000000..be8cf7c7 Binary files /dev/null and b/website/public/stars/island-23.png differ diff --git a/website/public/stars/island-24.png b/website/public/stars/island-24.png new file mode 100644 index 00000000..b7fae66f Binary files /dev/null and b/website/public/stars/island-24.png differ diff --git a/website/public/stars/island-25.png b/website/public/stars/island-25.png new file mode 100644 index 00000000..6458a647 Binary files /dev/null and b/website/public/stars/island-25.png differ diff --git a/website/public/stars/island-26.png b/website/public/stars/island-26.png new file mode 100644 index 00000000..7ee5c9b1 Binary files /dev/null and b/website/public/stars/island-26.png differ diff --git a/website/public/stars/island-27.png b/website/public/stars/island-27.png new file mode 100644 index 00000000..37727f4a Binary files /dev/null and b/website/public/stars/island-27.png differ diff --git a/website/public/uploads/cat-shape.DVKSPW1i.png b/website/public/uploads/cat-shape.DVKSPW1i.png new file mode 100644 index 00000000..5f512965 Binary files /dev/null and b/website/public/uploads/cat-shape.DVKSPW1i.png differ diff --git a/website/scripts/generate-og.ts b/website/scripts/generate-og.ts new file mode 100644 index 00000000..a059fa07 --- /dev/null +++ b/website/scripts/generate-og.ts @@ -0,0 +1,128 @@ +import {createServer} from 'node:http'; +import {readdirSync, readFileSync, mkdirSync, existsSync} from 'node:fs'; +import {resolve, dirname, relative, extname, join} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import puppeteer from 'puppeteer'; + +const __dirname = fileURLToPath(new URL(`.`, import.meta.url)); +const distDir = resolve(__dirname, `..`, `dist`); +const ogDir = join(distDir, `og`); +const CONCURRENCY = 4; +const WIDTH = 1200; +const HEIGHT = 630; + +function collectHtmlFiles(dir: string, base: string = dir): string[] { + const entries = readdirSync(dir, {withFileTypes: true}); + const files: string[] = []; + + for (const entry of entries) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...collectHtmlFiles(full, base)); + } else if (entry.name.endsWith(`.html`)) { + const rel = relative(base, full); + if (!rel.startsWith(`presentation/`)) + files.push(rel); + } + } + + return files; +} + +function startStaticServer(root: string): Promise<{url: string; close: () => void}> { + return new Promise((resolve) => { + const mimeTypes: Record = { + '.html': `text/html`, + '.css': `text/css`, + '.js': `application/javascript`, + '.json': `application/json`, + '.png': `image/png`, + '.jpg': `image/jpeg`, + '.svg': `image/svg+xml`, + '.woff2': `font/woff2`, + '.woff': `font/woff`, + }; + + const server = createServer((req, res) => { + const url = new URL(req.url!, `http://localhost`); + let filePath = join(root, url.pathname); + + if (!existsSync(filePath) || !filePath.includes(`.`)) { + const withHtml = filePath.endsWith(`/`) + ? join(filePath, `index.html`) + : `${filePath}.html`; + if (existsSync(withHtml)) + filePath = withHtml; + } + + if (!existsSync(filePath)) { + res.writeHead(404); + res.end(); + return; + } + + const ext = extname(filePath); + res.writeHead(200, {'Content-Type': mimeTypes[ext] ?? `application/octet-stream`}); + res.end(readFileSync(filePath)); + }); + + server.listen(0, `127.0.0.1`, () => { + const addr = server.address() as {port: number}; + resolve({url: `http://127.0.0.1:${addr.port}`, close: () => server.close()}); + }); + }); +} + +async function run() { + const htmlFiles = collectHtmlFiles(distDir); + console.log(`Found ${htmlFiles.length} pages to screenshot`); + + const server = await startStaticServer(distDir); + const browser = await puppeteer.launch({headless: true}); + + let completed = 0; + + async function screenshot(htmlFile: string) { + const route = htmlFile + .replace(/\.html$/, ``) + .replace(/\/index$/, ``); + + const pagePath = route || `index`; + const outPath = join(ogDir, `${pagePath}.png`); + + mkdirSync(dirname(outPath), {recursive: true}); + + const page = await browser.newPage(); + await page.setViewport({width: WIDTH, height: HEIGHT}); + + const url = `${server.url}/${htmlFile}`; + await page.goto(url, {waitUntil: `networkidle2`, timeout: 30_000}); + + await page.screenshot({path: outPath, type: `png`}); + await page.close(); + + completed++; + if (completed % 10 === 0 || completed === htmlFiles.length) + console.log(` ${completed}/${htmlFiles.length}`); + } + + const queue = [...htmlFiles]; + async function worker() { + while (queue.length > 0) { + const file = queue.shift()!; + await screenshot(file); + } + } + + await Promise.all(Array.from({length: CONCURRENCY}, () => worker())); + + await browser.close(); + server.close(); + + console.log(`Generated ${completed} OG images in dist/og/`); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/website/scripts/record-terminal.ts b/website/scripts/record-terminal.ts new file mode 100644 index 00000000..6c996b83 --- /dev/null +++ b/website/scripts/record-terminal.ts @@ -0,0 +1,186 @@ +import {spawn} from 'node:child_process'; +import {writeFileSync, mkdirSync} from 'node:fs'; +import {resolve, dirname} from 'node:path'; +import {platform} from 'node:os'; + +const SGR_MAP: Record = { + 0: null, + 2: `dim`, + 31: `err`, 91: `err`, + 32: `ok`, 92: `ok`, + 33: `warn`, 93: `warn`, + 34: `accent`, 94: `accent`, + 35: `accent`, 95: `accent`, + 36: `accent`, 96: `accent`, +}; + +function escapeHtml(s: string): string { + return s.replace(/&/g, `&`).replace(//g, `>`); +} + +function ansiToHtml(line: string): string { + const parts: Array = []; + let cls: string | null = null; + let last = 0; + + const re = /\x1b\[([\d;]*)m/g; + let m: RegExpExecArray | null; + + while ((m = re.exec(line)) !== null) { + const text = line.slice(last, m.index); + if (text) { + parts.push(cls ? `${escapeHtml(text)}` : escapeHtml(text)); + } + last = m.index + m[0].length; + + const codes = m[1] ? m[1].split(`;`).map(Number) : [0]; + for (const code of codes) { + if (code in SGR_MAP) + cls = SGR_MAP[code]; + } + } + + const tail = line.slice(last); + if (tail) { + parts.push(cls ? `${escapeHtml(tail)}` : escapeHtml(tail)); + } + + return parts.join(``); +} + +function stripControl(s: string): string { + s = s + .replace(/\x1b\[[\d;]*[A-HJKSTfhlnr]/g, ``) + .replace(/\x1b\[\?[\d;]*[a-zA-Z]/g, ``) + .replace(/\x1b[()][AB012]/g, ``) + .replace(/\x1b[>=]/g, ``); + + while (s.includes(`\x08`)) { + s = s.replace(/[^\x08]\x08/, ``); + s = s.replace(/^\x08+/, ``); + } + + return s.replace(/[\x00-\x07\x0e-\x1a\x1c-\x1f\x7f]/g, ``); +} + +const ddIdx = process.argv.indexOf(`--`); +if (ddIdx < 0 || ddIdx + 1 >= process.argv.length) { + console.error(`Usage: node record-terminal.ts [] -- [args...]`); + process.exit(1); +} + +const preArgs = process.argv.slice(2, ddIdx); +const terminalId = preArgs[0] ?? null; + +const args = process.argv.slice(ddIdx + 1); +const cmd = args[0]; +const cmdArgs = args.slice(1); + +type Entry = {html: string; delay: number; clear?: number}; +const entries: Array = []; + +entries.push({ + html: `$ ${escapeHtml(args.join(` `))}`, + delay: 0, +}); + +const env = {...process.env, FORCE_COLOR: `3`, CLICOLOR_FORCE: `1`}; +delete env.NO_COLOR; + +let spawnCmd: string; +let spawnArgs: Array; + +if (platform() === `darwin`) { + spawnCmd = `script`; + spawnArgs = [`-q`, `/dev/null`, cmd, ...cmdArgs]; +} else if (platform() === `linux`) { + const quoted = [cmd, ...cmdArgs].map(a => `'${a.replace(/'/g, `'\\''`)}'`).join(` `); + spawnCmd = `script`; + spawnArgs = [`-qc`, quoted, `/dev/null`]; +} else { + spawnCmd = cmd; + spawnArgs = cmdArgs; +} + +const child = spawn(spawnCmd, spawnArgs, { + stdio: [`inherit`, `pipe`, `pipe`], + env, +}); + +let buf = ``; +let lastTime = performance.now(); +let afterCR = false; +const BASE_DELAY = 300; + +function emitLine(raw: string) { + const cleaned = stripControl(raw); + const html = ansiToHtml(cleaned); + const delay = entries.length === 1 ? BASE_DELAY : Math.round(performance.now() - lastTime); + + const entry: Entry = {html, delay}; + if (afterCR) entry.clear = 1; + + entries.push(entry); + lastTime = performance.now(); + afterCR = false; +} + +function flush(chunk: string) { + buf += chunk; + + buf = buf.replace(/\x1b\[2K/g, `\r`); + + let pos = 0; + while (pos < buf.length) { + const rIdx = buf.indexOf(`\r`, pos); + const nIdx = buf.indexOf(`\n`, pos); + + if (rIdx === -1 && nIdx === -1) break; + + if (rIdx !== -1 && (nIdx === -1 || rIdx < nIdx)) { + if (rIdx + 1 >= buf.length) break; + + const text = buf.slice(pos, rIdx); + + if (buf[rIdx + 1] === `\n`) { + if (text) emitLine(text); + pos = rIdx + 2; + } else { + if (text) emitLine(text); + afterCR = true; + pos = rIdx + 1; + } + } else { + const text = buf.slice(pos, nIdx); + if (text) emitLine(text); + pos = nIdx + 1; + } + } + + buf = buf.slice(pos); +} + +child.stdout.on(`data`, (d: Buffer) => flush(d.toString())); +child.stderr.on(`data`, (d: Buffer) => flush(d.toString())); + +child.on(`close`, () => { + if (buf.length > 0) { + const cleaned = stripControl(buf); + const html = ansiToHtml(cleaned); + const delay = entries.length === 1 ? BASE_DELAY : Math.round(performance.now() - lastTime); + const entry: Entry = {html, delay}; + if (afterCR) entry.clear = 1; + entries.push(entry); + } + + const json = JSON.stringify(entries, null, 2) + `\n`; + + if (terminalId) { + const outPath = resolve(import.meta.dirname!, `../src/data/terminals/${terminalId}.json`); + mkdirSync(dirname(outPath), {recursive: true}); + writeFileSync(outPath, json); + console.error(`Wrote ${entries.length} entries to ${outPath}`); + } else { + process.stdout.write(json); + } +}); diff --git a/website/src/blog/2026-01-28-yarn-6-preview.md b/website/src/blog/2026-01-28-yarn-6-preview.md new file mode 100644 index 00000000..e7705200 --- /dev/null +++ b/website/src/blog/2026-01-28-yarn-6-preview.md @@ -0,0 +1,125 @@ +--- +slug: 2026-01-28-yarn-6-preview +title: Yarn 6 Preview +date: 2026-01-28 +category: Release +author: + name: "Maël Nison" + title: Lead Yarn maintainer + url: https://bsky.app/profile/mael.dev + image_url: https://github.com/arcanis.png +--- + +I'm excited to announce the next evolution for Yarn, redefining what "state of the art" means when it comes to JavaScript package managers. + +Yarn has always prioritized three pillars: correctness, developer experience, and performance. Excelling in all three simultaneously is a challenge, but we believe you shouldn't expect less from the very foundation of your projects. While we are proud of our correctness, stability, and DX, it became clear in recent years that Yarn was hitting a ceiling on performance - especially within massive monorepos hosting thousands of workspaces. + +Almost ten years after the first public release of Yarn, now is the perfect time to reveal our plans to port Yarn to Rust. This project, started over a year ago, is now ready to share more broadly. While still a preview, we expect to complete this transition in the next 6-8 months. This evolution will lead to drastically higher responsiveness and lower memory footprints, enabling features we simply couldn't efficiently pull off until now. + +## How much faster? + +While performance was a significant driver for this rewrite, our initial focus has been strict compatibility. Even with that constraint, the results are compelling. Many low-hanging fruits remain, and we're excited to now be able to publicly work with the JavaScript + Rust tooling community to widen the gap even further. + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TestBeforeAfterPnpm
Next.js - few but heavy dependencies
Cold cache4.1s2.5s3.0s
Warm cache577ms**184ms**686ms
Gatsby - lots of small dependencies
Cold cache19.8s11.7s13.1s
Warm cache1.7s**0.3s**1.9s
+
+ +*(more benchmarks are available [here](https://p.datadoghq.eu/sb/d2wdprp9uki7gfks-badb2c0f08402af744326888f9535a82); we're also setting up a new benchmark on massive megarepos, but those don't run on other package managers yet)* + +Beyond raw speed, these numbers unlock new opportunities. While some concepts are still in the ideation phase, others have already been implemented - such as the Lazy Install feature I'm just about to present. + +## New features + +Our primary focus is completing the rewrite, but some features were foundational enough that we felt the need to implement them as part of the MVP, knowing they would instruct the design. + +### Yarn Switch + +If you're familiar with the Node.js ecosystem you may have heard of Corepack. Corepack is an experimental version manager of packages managers built in tandem with Node.js core contributors and has been the recommended way to use Yarn since it was first distributed via Node.js official releases. While package managers all lock dependencies nowadays, they don't always support locking their own version. Corepack was the response to this need, but with the recent decision from Node.js to stop distributing Corepack, we had to look elsewhere. + +Consequently, we developed our own replacement: **Yarn Switch**. + +Yarn Switch, also written in Rust, is the binary that gets installed on your machine when following our new [installation guide](/getting-started). When executed it will read the `packageManager` field from your project and transparently download, cache, and forward the command to the appropriate Yarn version. Think of it as a `rustup` or `nvm` equivalent, but intended specifically for Yarn. + +We intend to keep supporting using Yarn 6.x from Corepack on mainstream platforms (i.e. Linux, macOS, and Windows, on x86-64 and ARM) as long as Corepack is still shipped with Node.js 24.x, however we're confident that Yarn Switch will provide a much superior DX. + +For more details about Yarn Switch, check out the [Yarn Switch documentation](/concepts/switch). + +### Lazy Installs + +Yarn has long supported "Zero Installs" - the ability to check-in install artifacts to your repository so you can omit running installs when switching branches. This worked well but had drawbacks regarding repository size, particularly in massive monorepos. + +Starting with Yarn 6.x, we are introducing a new default mode: **Lazy Installs**. + +Under this model, running most Yarn commands - including `yarn run` - will prompt Yarn to silently perform an install if it detects the artifacts are out of sync with package.json. Thanks to the native Rust implementation, this check has negligible overhead in the happy path. We believe this offers most of the benefits of Zero Installs without the repository bloat. + +## Versioning roadmap + +Our current release is Yarn 4.12. We plan for the JS codebase to continue into the Yarn 5.x series. This will be released in a couple of months as a stepping stone, including some of the deprecations introduced in the Rust-based Yarn 6.x series. + +The first Yarn 6.x stable release will be published once the Rust implementation has reached sufficient parity with current Yarn releases. We do not expect this until at least **Q3 2026**. + +Once 6.x is stable, Yarn 5.x will enter LTS status: the codebase will receive critical bugfixes for approximately 30 months, while active development shifts to the Rust codebase. + +Backward compatibility is a primary concern. We are using the exact same test suite to validate Yarn 6.x against its predecessors. Experimental releases of Yarn 6.x have already been successfully deployed in production at **Datadog** with minimal breaking changes. + +## Next steps + +This preview is an important steps, but Yarn 6.x is still months away. Important features remain to tackle: + +- Windows support +- Interactive commands +- Collaboration with third-party tools that manually parse the lockfile +- Wrapping up the last remaining failing tests and missing commands + +This work is ongoing, and we are eager to build this next chapter alongside the community. + +If you are looking to expand your knowledge of Rust or deep-dive into JavaScript tooling, there has never been a better time to get involved. We are actively looking for contributors to help us tackle the remaining challenges - check out our [issues](https://github.com/yarnpkg/zpm/issues) or say hello on our [Discord server](https://discord.com/invite/yarnpkg) to get started. + +Finally, if your company relies on Yarn and wants to ensure its sustainable development, please consider backing us via [GitHub Sponsors](http://github.com/sponsors/yarnpkg) or [OpenCollective](https://opencollective.com/yarnpkg). diff --git a/website/src/blog/2026-05-08-package-managers-import-maps.md b/website/src/blog/2026-05-08-package-managers-import-maps.md new file mode 100644 index 00000000..63c8efc0 --- /dev/null +++ b/website/src/blog/2026-05-08-package-managers-import-maps.md @@ -0,0 +1,267 @@ +--- +slug: 2026-05-08-package-maps-import-maps +title: Package managers need more than import maps +date: 2026-05-08 +category: Technology +author: + name: "Maël Nison" + title: Lead Yarn maintainer + url: https://bsky.app/profile/mael.dev + image_url: https://github.com/arcanis.png +--- + +Leveraging everything we learned these past six years working on Yarn and Node.js our team collaborated with other package managers to put forward a proposal for a new feature called Package Maps. + +The [pull request](https://github.com/nodejs/node/pull/62239) already makes a good case for them, so in this post I want to focus on an interesting question that came up during the review: "Why don't we just use import maps?". So let's get down to it! + +:::bsky{handle="justinfagnani.com" displayName="Justin Fagnani" avatar="https://cdn.bsky.app/img/avatar/plain/did:plc:ec64xv7n5dszeizw56yr6b5h/bafkreif2i73jwfh77gngf3yitfvlhceke4xruijblwxcg7e2m4wxin37q4" post="3mj6n2pmldk2p" date="Apr 11, 2026" likes="4"} +One of the original import map creators here 👋 This is awesome! I hope it goes it! +::: + +## What we're actually trying to solve + +Before talking about formats, let's be specific about the problems. The `node_modules` resolution algorithm has worked for a long time, but it has some well-known sharp edges: + +### Hoisting is declaratively lossy. + +Once trees are hoisted the runtime can no longer distinguish between a direct dependency and a transitive one. Since Node.js and other runtimes don't leverage the `dependencies` field in any way during resolution, the "permission" to import a package is discarded during the install step. + +This leaves the runtime with no way to enforce boundaries, causing problems in moderately large dependency trees where some packages will accidentally rely on transitive or even sibling dependencies - a pattern often called **"phantom dependencies"** that package managers like Yarn or pnpm have tried to curb. + +### Peer dependency resolution in monorepos. + +If app-v1 uses React 18, app-v2 uses React 19, and both depend on a shared workspace library that lists React as a peer dependency, no possible flat `node_modules` layout can resolve the shared library correctly in both apps: whichever React got hoisted will win. A situation described in more details in our [Workspaces & peer deps](/appendix/workspaces-and-peer-deps/) appendix. + +### Resolution requires I/O. + +Despite package managers knowing where packages are installed on disk, Node.js still relies on a try-catch-repeat pattern: first we check whether the package is located in a folder, then if it fails we try in the parent directory, then if it fails we go one step further, etc until we finally reach a file that exists. Outside of the obvious I/O waste, we also end up with some silly results. + +## What import maps were built for + +Import maps originated around the 2018 browser standardization work as a mean for web developers to import bare identifiers using a similar syntax to the one used by Node.js. However, due to specificities around the web stack, designers decided against ensuring compatibility with the Node.js resolution algorithm. To quote the [explainer](https://github.com/WICG/import-maps/blob/abc4c6b24e0cc9a764091be916c5057e83c30c23/README.md#the-nodejs-module-resolution-algorithm): + +> Unlike in Node.js, in the browser we don't have the luxury of a reasonably-fast file system that we can crawl looking for modules. Thus, we cannot implement the Node module resolution algorithm directly; it would require performing multiple server round-trips for every import statement, wasting bandwidth and time as we continue to get 404s. We need to ensure that every import statement causes only one HTTP request; this necessitates some measure of precomputation. + +This makes sense - the try-catch-repeat model used in Node.js, reasonable on a local file system, would create unacceptable waterfalls within browsers. So designers decided to pass on the Node.js module-resolution semantics: the `node_modules` walk, but also conditional exports[^1], imports field, extension checks, and more. The resolution becomes purely static: take _this bare specifier_ imported by _that location_, rewrite it to _that other location_. + +That's a clean design for the web platform, but when you try to retrofit it onto Node.js, problems start. + +## Where import maps fall short + +Before we get started, don't get me wrong - import maps are great as build artifacts. They allow the resolution to be fully static and deterministic. For local development though, they face two very significant hurdles: + +### Last-mile resolution + +The first one was already discussed: import maps take ownership of resolution. Under the current spec, when an import map matches a specifier, the rewrite is the answer. Node's accumulated semantics - conditional exports, subpath imports, the node-addons condition - don't naturally compose with that. To handle conditions, you would have to resolve them at install time. But conditions like production vs development, or commonjs vs ESM, are runtime data, not install-time data. An import map has one entry per specifier; asking a package manager to predict every possible runtime environment at install time would be impossible without extra fields[^2]. + +An answer to that could be for package managers to only encode folders within import maps, and pretend that when an import map URL ends with a slash then the underlying runtime's resolution kicks in: + +```json +{ + "imports": { + "react": "./node_modules/react/" + } +} +``` + +This could actually work, although at that point we already deviated enough from the spec that the generated import maps would be unable to work within a browser. At that point, aren't we just cosplaying import maps? + +### No support for abstract packages + +This one is a little technical if you're not a package manager author so bear with me. We'll discuss a less-known aspect of **peer dependencies**. + +When a package only has regular dependencies, representing it on a graph is simple: just treat it as a node. + +```mermaid +graph LR + root --> vite + vite --> lightningcss + vite --> picomatch +``` + +Even when you're in a monorepo setup where multiple monorepos have the same dependency, package managers can easily reuse the same node: + +```mermaid +graph LR + root --> a[app A] + root --> b[app B] + a --> vite + b --> vite + vite --> lightningcss + vite --> picomatch +``` + +But peer dependencies are tricky. Consider that situation, where our monorepo hosts two applications that both use the same version of `vitest` (which has a peer dependency on `vite`), but each using a different versions of `vite`: + +```mermaid +graph LR + root --> a[app A] + root --> b[app B] + a --> vite_6["vite@6"] + b --> vitest + b --> vite_8["vite@8"] + a --> vitest + vitest -.-> vite +``` + +At this stage of the graph peer dependencies don't yet represent concrete versions, so they can't be turned into a `node_modules` layout - package managers must first run a post-process pass to fulfill them based on what their immediate parent in the dependency graph provided. + +Once that's done no peer dependencies remain in the graph as they all got turned into regular dependencies. Another effect is that packages listing peer dependencies also got their nodes duplicated in the graph, each variant connected to a different set of dependencies: + +```mermaid +graph LR + root --> a[app A] + root --> b[app B] + a --> x[vitest #1] + a --> vite_6["vite@6"] + b --> y[vitest #2] + b --> vite_8["vite@8"] + x --> vite_6 + y --> vite_8 +``` + +Making that work with both import maps and standard Node.js resolution is a little difficult, because they both assume that a single location on disk will only ever be connected to a single dependency list. + +A reasonable first approach is to simply duplicate the packages in the hydrated `node_modules` tree. So we'd end up here with something akin to: + +```json +{ + "imports": {}, + "scopes": { + "./app-a/": { + "vite": "./app-a/node_modules/vite/", + "vitest": "./app-a/node_modules/vitest/" + }, + "./app-b/": { + "vite": "./app-b/node_modules/vite/", + "vitest": "./app-b/node_modules/vitest/" + } + } +} +``` + +Here the package manager prevented `vitest` from being hoisted to the top, thus making sure the Node.js resolution will let both variants retrieve the appropriate `vite` version. The `vitest` package ends up duplicated, but the user is none the wiser and things work out of the box. + +But that's the easy case. What if we instead have a graph like this, where both `app-a` and `app-b` depend on a shared `lib` workspace with a peer dependency on `react`? + +```mermaid +graph LR + root --> a[app A] + root --> b[app B] + a --> react_15["react@15"] + a --> component-lib + root --> component-lib + b --> react_18["react@18"] + b --> component-lib + component-lib -.-> react +``` + +As we saw earlier, the package manager will need to duplicate the `component-lib` graph so each of its variants end up with an appropriate dependency set: + +```mermaid +graph LR + root --> a[app A] + root --> b[app B] + a --> react_15["react@15"] + a --> component-lib_15["component-lib #1"] + b --> react_18["react@18"] + b --> component-lib_18["component-lib #2"] + component-lib_15 --> react_15 + component-lib_18 --> react_18 +``` + +But there's an important difference here: **component-lib is a workspace**. It's a directory that's directly part of the user's project. We can't just be duplicating it on disk like we did for `vitest`! This would be ok if the import map was the final artifact we produced right before publishing the code to our production buckets, but for local development it'd be a **nightmare**: any change you make to your shared workspace would require an install to sync it to its copies.[^3] + +## What package maps add + +Package maps offer an alternative to import maps in two ways: + +- **It only informs package location**; anything after that is left entirely up to the runtime. Package maps don't specify what that last-mile resolution looks like, and it may be different depending on the runtime and the features they support. + +- **It treats the dependency graph as an actual graph**: the file contains a list of nodes with arbitrary IDs so that multiple of these nodes can share the same underlying location. It's then up to the runtime to key the modules loaded from there based on their node IDs rather than their mere path on disk. + +Those changes lead to a slightly different data shape: + +```json +{ + "packages": { + "my-app": { + "path": "./src", + "dependencies": { + "lodash": "lodash", + "react": "react" + } + }, + "lodash": { + "path": "./node_modules/lodash" + }, + "react": { + "path": "./node_modules/react" + } + } +} +``` + +Unlike import maps which define the `scopes` field keyed by URLs / relative file paths, we have a `packages` field keyed by arbitrary IDs. Each entry in the package map then defines the path where its files may be found, and its dependency set. Through that shape the full dependency graph is preserved, and we can safely represent the case we described above: + +```json +{ + "packages": { + "root": { + "path": ".", + "dependencies": { + "app-a": "app-a", + "app-b": "app-b" + } + }, + "app-a": { + "path": "./apps/app-a", + "dependencies": { + "react": "react@15", + "component-lib": "component-lib+react@15" + } + }, + "app-b": { + "path": "./apps/app-b", + "dependencies": { + "react": "react@18", + "component-lib": "component-lib+react@18" + } + }, + "component-lib+react@15": { + "path": "./packages/component-lib", + "dependencies": { + "react": "react@15" + } + }, + "component-lib+react@18": { + "path": "./packages/component-lib", + "dependencies": { + "react": "react@18" + } + }, + "react@15": { + "path": "./node_modules/.store/react@15/node_modules/react" + }, + "react@18": { + "path": "./node_modules/.store/react@18/node_modules/react" + } + } +} +``` + +## On coexistence + +There's an understandable instinct to align Node.js with the web wherever possible. Running the same code in both environments is genuinely valuable, and a lot of what makes Node pleasant to work with today comes from that effort. + +But web compatibility is one goal among several. Server runtimes also have to support large monorepos, peer dependencies, conditional exports, and tight developer iteration loops that don't exist in the browser. When those goals conflict with strict web conformance, it's reasonable to pick the tool that fits the job - even when it means maintaining two formats instead of one. + +Package maps for the server, import maps for the browser, and a clear path between them. + +--- + +[^1]: Which admittedly didn't exist at the time Import Maps were designed. Didn't stop us from extending the Node.js resolution in new ways though, for the embetterment of the ecosystem. + +[^2]: This flaw was well known even then. It was however [left for runtime implementors to figure out](https://github.com/WICG/import-maps/issues/15#issuecomment-508188614). + +[^3]: That's what pnpm does with [`injectWorkspacePackages`](https://pnpm.io/workspaces#injectworkspacepackages), although it's disabled by default. They also try to offset it by using hardlinks to share file updates, but adding and removing files still require new installs. diff --git a/website/src/components/Breadcrumb.astro b/website/src/components/Breadcrumb.astro new file mode 100644 index 00000000..d81cddae --- /dev/null +++ b/website/src/components/Breadcrumb.astro @@ -0,0 +1,19 @@ +--- +interface Props { + segments: string[]; +} + +const { segments } = Astro.props; +--- + +
+ {segments.map((seg, i) => ( + + {i > 0 && /} + {i === segments.length - 1 + ? {seg} + : {seg} + } + + ))} +
diff --git a/website/src/components/ContentSidebar.astro b/website/src/components/ContentSidebar.astro new file mode 100644 index 00000000..6d2277e1 --- /dev/null +++ b/website/src/components/ContentSidebar.astro @@ -0,0 +1,17 @@ +--- +import { getCollection } from 'astro:content'; +import DocsSidebar from './DocsSidebar.astro'; +import { buildSidebarGroups } from './sidebar'; + +interface Props { + sections: string[]; + activePage: string; +} + +const { sections, activePage } = Astro.props; + +const allDocs = await getCollection(`docs`); +const groups = sections.flatMap(section => buildSidebarGroups(allDocs, section, activePage)); +--- + + diff --git a/website/src/components/DocsSidebar.astro b/website/src/components/DocsSidebar.astro new file mode 100644 index 00000000..555ee283 --- /dev/null +++ b/website/src/components/DocsSidebar.astro @@ -0,0 +1,27 @@ +--- +import type { SidebarGroup } from './sidebar'; + +interface Props { + groups: SidebarGroup[]; +} + +const { groups } = Astro.props; +--- + +{groups.map(group => ( +
+

+ {group.title} +

+ + {group.items.map(item => + `subtitle` in item + ?
+ {item.subtitle} +
+ : + {item.label} + + )} +
+))} diff --git a/website/src/components/Nav.astro b/website/src/components/Nav.astro new file mode 100644 index 00000000..5b912141 --- /dev/null +++ b/website/src/components/Nav.astro @@ -0,0 +1,200 @@ +--- +import Logo from '../../public/logo.svg'; +import SimpleIcon from './SimpleIcon.astro'; +import SearchModal from './SearchModal.tsx'; + +interface Props { + variant?: 'index' | 'docs'; + activePage?: string; +} + +import navigation from '../../config/navigation.json'; + +const { variant = `index`, activePage } = Astro.props; + +const isDocs = variant === `docs`; +const navBgOpacity = isDocs ? `55%` : `25%`; + +const links = navigation.topbar; + +const activeClass = `no-underline text-[var(--fg)] font-medium`; +const inactiveClass = `hover:text-[var(--fg)] transition-colors no-underline text-inherit`; + +const socialLinkClass = `text-[var(--fg-dim)] inline-flex items-center justify-center w-8 h-8 rounded-lg transition-colors no-underline hover:text-[var(--fg)] hover:bg-[color-mix(in_oklch,var(--fg)_8%,transparent)]`; +--- + + + + + +
+
+
+
+ + + + + yarn + + + + +
+ +
+ {links.map(link => ( + + {link.label} + + ))} +
+ + +
+
+ + + + diff --git a/website/src/components/PageHeader.astro b/website/src/components/PageHeader.astro new file mode 100644 index 00000000..fb7f013d --- /dev/null +++ b/website/src/components/PageHeader.astro @@ -0,0 +1,29 @@ +--- +import Pill from './Pill.astro'; + +interface Props { + title: string; + lead: string; + mono?: boolean; + pill?: string; +} + +const { title, lead, mono, pill } = Astro.props; +--- + +{(mono || pill) ? ( +
+

+ {title} +

+ {pill && {pill}} +
+) : ( +

+ {title} +

+)} + +

+ {lead} +

diff --git a/website/src/components/Pill.astro b/website/src/components/Pill.astro new file mode 100644 index 00000000..cdaf5407 --- /dev/null +++ b/website/src/components/Pill.astro @@ -0,0 +1,18 @@ +--- +interface Props { + variant: 'type' | 'required' | 'since' | 'default' | 'deprecated'; +} + +const { variant } = Astro.props; + +const base = `inline-flex items-center font-mono text-[11px] leading-none px-[7px] py-1 rounded-[5px] border tracking-[0.01em] whitespace-nowrap`; +const variants: Record = { + type: `border-[var(--pill-type-border)] bg-[var(--pill-type-bg)] text-[var(--pill-type-fg)]`, + required: `border-[var(--pill-req-border)] bg-[var(--pill-req-bg)] text-[var(--pill-req-fg)]`, + since: `border-[var(--accent-line)] bg-[var(--accent-soft)] text-[var(--accent)]`, + default: `border-[var(--line)] bg-[color-mix(in_oklch,var(--fg)_5%,transparent)] text-[var(--fg-dim)]`, + deprecated: `border-[var(--line)] bg-[color-mix(in_oklch,var(--fg)_5%,transparent)] text-[var(--fg-mute)] line-through decoration-[var(--pill-dep-strike)] decoration-1`, +}; +--- + + diff --git a/website/src/components/PostMeta.astro b/website/src/components/PostMeta.astro new file mode 100644 index 00000000..649f73b4 --- /dev/null +++ b/website/src/components/PostMeta.astro @@ -0,0 +1,47 @@ +--- +interface Props { + author: { name: string; url?: string; image_url?: string }; + date: string; + readingTime: number; + variant?: 'card' | 'article'; +} + +const { author, date, readingTime, variant = `card` } = Astro.props; +const initials = author.name.split(` `).map((w: string) => w[0]).join(``).slice(0, 2); + +const wrapperClass = variant === `article` + ? `flex items-center gap-3.5 text-[13.5px] text-[var(--fg-dim)] flex-wrap` + : `flex items-center gap-3.5 text-[12.5px] text-[var(--fg-mute)]`; +--- + + + + + + + {date} + + + + {readingTime} min read + + diff --git a/website/src/components/PrevNextNav.astro b/website/src/components/PrevNextNav.astro new file mode 100644 index 00000000..91d033ae --- /dev/null +++ b/website/src/components/PrevNextNav.astro @@ -0,0 +1,38 @@ +--- +interface Props { + prev?: { href: string; label: string }; + next?: { href: string; label: string }; + prevEyebrow?: string; + nextEyebrow?: string; +} + +const { prev, next, prevEyebrow = `← Previous`, nextEyebrow = `Next →` } = Astro.props; +--- + +{(prev || next) && ( + +)} diff --git a/website/src/components/ReferenceSidebar.astro b/website/src/components/ReferenceSidebar.astro new file mode 100644 index 00000000..2cba7bb0 --- /dev/null +++ b/website/src/components/ReferenceSidebar.astro @@ -0,0 +1,90 @@ +--- +import DocsSidebar from './DocsSidebar.astro'; +import type { SidebarGroup, SidebarItem } from './sidebar'; +import navigation from '../../config/navigation.json'; + +type CliEntry = {id: string; data: {binaryName: string; commandSpec: {primaryPath: string[]; category: string | null}}}; + +interface Props { + activePage: string; + fields?: string[]; + cliEntries?: CliEntry[]; + cliPrefix?: string; +} + +const { activePage, fields, cliEntries, cliPrefix = `cli` } = Astro.props; + +function slugify(s: string): string { + return s.toLowerCase() + .replace(/[^\w\s-]/g, ``) + .replace(/\s+/g, `-`) + .replace(/-+/g, `-`) + .replace(/^-|-$/g, ``); +} + +const refConfig = navigation.sidebars.reference; +const isProtocolPage = activePage.startsWith(`protocol/`); + +const configGroup: SidebarGroup = { + title: `Reference`, + items: refConfig.links.map(link => ({ + label: link.label, + href: link.href, + active: `page` in link ? activePage === link.page : `pagePrefix` in link ? activePage.startsWith(link.pagePrefix!) : false, + })), +}; + +const groups: SidebarGroup[] = [configGroup]; + +if (isProtocolPage) { + groups.push({ + title: `Protocols`, + items: refConfig.protocols.map(p => ({ + label: p.label, + href: p.href, + active: activePage === p.page, + mono: true, + })), + }); +} + +if (fields) { + groups.push({ + title: `Fields`, + items: fields.map(name => ({ + label: name, + href: `#field-${slugify(name)}`, + mono: true, + })), + }); +} + +if (cliEntries) { + const byCategory = new Map(); + for (const entry of cliEntries) { + const cat = entry.data.commandSpec.category; + if (!cat) continue; + if (!byCategory.has(cat)) byCategory.set(cat, []); + byCategory.get(cat)!.push(entry); + } + + for (const items of byCategory.values()) + items.sort((a, b) => a.data.commandSpec.primaryPath.join(` `).localeCompare(b.data.commandSpec.primaryPath.join(` `))); + + for (const [category, entries] of byCategory) { + const items: SidebarItem[] = entries.map(entry => { + const slug = entry.id.replace(new RegExp(`^${cliPrefix}/`), ``); + return { + label: `${entry.data.binaryName} ${entry.data.commandSpec.primaryPath.join(` `)}`, + href: `/${cliPrefix}/${slug}/`, + active: entry.id === activePage, + mono: true, + }; + }); + + groups.push({title: category, items}); + } +} +--- + + diff --git a/website/src/components/SearchModal.tsx b/website/src/components/SearchModal.tsx new file mode 100644 index 00000000..4fedd15e --- /dev/null +++ b/website/src/components/SearchModal.tsx @@ -0,0 +1,709 @@ +import {liteClient as algoliasearch} from 'algoliasearch/lite'; +import octIconData from '@iconify-json/octicon/icons.json'; +import {useState, useEffect, useRef, useCallback, type JSX} from 'react'; + +const docsClient = algoliasearch(`STXW7VT1S5`, `ecdfaea128fd901572b14543a2116eee`); +const pkgClient = algoliasearch(`OFCNCOG2CU`, `f54e21fa3a2a0160595bb058179bfb1e`); + +function octicon(name: string, size: number, className?: string) { + const icon = (octIconData as any).icons[name]; + if (!icon) return null; + const w = icon.width ?? 16; + const h = icon.height ?? 16; + return ( + + ); +} + +type Scope = `all` | `docs` | `pkg` | `cli`; +type ResultKind = `docs` | `pkg` | `cli`; + +interface SearchItem { + kind: ResultKind; + title: string; + titleHtml: string; + crumbs?: Array; + snippet?: string; + snippetHtml?: string; + href: string; + version?: string; + downloads?: string; + downloadsRaw?: number; + author?: string; + license?: string; +} + +// ── Icons ── + +function SearchIcon({size = 16, className}: {size?: number, className?: string}) { + return octicon(`search-16`, size, className); +} + +function CloseIcon() { + return octicon(`x-16`, 12); +} + +function ClockIcon() { + return octicon(`clock-16`, 13); +} + +function FlameIcon({color}: {color: string}) { + return ( + + {octicon(`flame-16`, 10)} + + ); +} + +function NoResultsIcon() { + return octicon(`search-16`, 18); +} + +const SCOPE_ICONS: Record = { + all: null, + docs: octicon(`file-16`, 11)!, + pkg: octicon(`package-16`, 11)!, + cli: octicon(`terminal-16`, 11)!, +}; + +const KIND_GLYPHS: Record = { + docs: octicon(`file-16`, 14)!, + pkg: octicon(`package-16`, 14)!, + cli: octicon(`terminal-16`, 14)!, +}; + +const SCOPES: Array<{key: Scope, label: string}> = [ + {key: `all`, label: `All`}, + {key: `docs`, label: `Docs`}, + {key: `pkg`, label: `Packages`}, + {key: `cli`, label: `CLI`}, +]; + +const KIND_LABELS: Record = { + docs: `Documentation`, + pkg: `Packages`, + cli: `CLI`, +}; + +const SUGGESTED = [ + `yarn install`, `workspace protocol`, `zero-installs`, + `constraints`, `migration from v1`, `lodash`, +]; + +// ── Recent searches (localStorage) ── + +const RECENTS_KEY = `yarn-search-recents`; + +function getRecents(): Array<{term: string, kind: string}> { + try { + const raw = localStorage.getItem(RECENTS_KEY); + if (raw) return JSON.parse(raw); + } catch {} + return []; +} + +function addRecent(term: string, kind: string) { + const recents = getRecents().filter(r => r.term !== term); + recents.unshift({term, kind}); + if (recents.length > 5) recents.length = 5; + try { + localStorage.setItem(RECENTS_KEY, JSON.stringify(recents)); + } catch {} +} + +// ── Helpers ── + +function highlightValue(hit: any, attr: string): string { + return hit?._highlightResult?.[attr]?.value ?? hit?.[attr] ?? ``; +} + +function stripTags(html: string): string { + return html.replace(/<[^>]*>/g, ``); +} + +function getFlameColor(downloadsRaw?: number): string | null { + if (downloadsRaw == null) return null; + if (downloadsRaw >= 10_000_000) return `#ef4444`; + if (downloadsRaw >= 1_000_000) return `#f59e0b`; + return null; +} + +function looksLikePackage(q: string): boolean { + const t = q.trim(); + if (!t) return false; + return /^@?[a-z0-9][\w.\-]*(?:\/[a-z0-9][\w.\-]*)?$/i.test(t); +} + +interface ResultGroup { + kind: ResultKind; + label: string; + items: Array; +} + +function groupResults(results: Array, scope: Scope, query: string): Array { + const filtered = scope === `all` ? results : results.filter(r => r.kind === scope); + const groups: Array = []; + + if (scope === `all`) { + const docs = filtered.filter(r => r.kind === `docs`); + const pkgs = filtered.filter(r => r.kind === `pkg`); + const cli = filtered.filter(r => r.kind === `cli`); + + if (looksLikePackage(query)) { + const hotPkgs = pkgs.filter(r => getFlameColor(r.downloadsRaw) != null).slice(0, 2); + const restPkgs = pkgs.filter(r => !hotPkgs.includes(r)); + if (hotPkgs.length) groups.push({kind: `pkg`, label: `Popular packages`, items: hotPkgs}); + if (docs.length) groups.push({kind: `docs`, label: KIND_LABELS.docs, items: docs}); + if (cli.length) groups.push({kind: `cli`, label: KIND_LABELS.cli, items: cli}); + if (restPkgs.length) groups.push({kind: `pkg`, label: KIND_LABELS.pkg, items: restPkgs}); + } else { + if (docs.length) groups.push({kind: `docs`, label: KIND_LABELS.docs, items: docs}); + if (pkgs.length) groups.push({kind: `pkg`, label: KIND_LABELS.pkg, items: pkgs}); + if (cli.length) groups.push({kind: `cli`, label: KIND_LABELS.cli, items: cli}); + } + } else if (filtered.length) { + groups.push({kind: scope as ResultKind, label: KIND_LABELS[scope as ResultKind], items: filtered}); + } + + return groups; +} + +function flattenGroups(groups: Array): Array { + return groups.flatMap(g => g.items); +} + +// ── Subcomponents ── + +function Kbd({children}: {children: React.ReactNode}) { + return ( + + {children} + + ); +} + +function ScopeChip({scope, active, count, onClick}: {scope: Scope, active: boolean, count: number, onClick: () => void}) { + return ( + + ); +} + +function ResultGlyph({kind}: {kind: ResultKind}) { + return ( + + {KIND_GLYPHS[kind]} + + ); +} + +function Crumbs({crumbs, separator = `›`}: {crumbs: Array, separator?: string}) { + return ( + + {crumbs.map((c, i) => ( + {i > 0 && {separator}}{c} + ))} + + ); +} + +function GroupHeader({label, count}: {label: string, count: number}) { + return ( +
+ {label} + + {count} +
+ ); +} + +function DocResultRow({item, isActive, onMouseEnter, onClick}: {item: SearchItem, isActive: boolean, onMouseEnter: () => void, onClick: () => void}) { + return ( + { e.preventDefault(); onClick(); }} + role="option" + aria-selected={isActive} + > + + + + + + {item.snippetHtml && ( + + )} + + + + {item.crumbs && item.crumbs.length > 0 && } + + open + + + + ); +} + +function PkgResultRow({item, isActive, onMouseEnter, onClick}: {item: SearchItem, isActive: boolean, onMouseEnter: () => void, onClick: () => void}) { + const flameColor = getFlameColor(item.downloadsRaw); + + return ( + { e.preventDefault(); onClick(); }} + role="option" + aria-selected={isActive} + > + + + +
+ + + {item.downloads && flameColor && ( + + + {item.downloads} + + )} +
+ + + + + {item.author} + · + {item.license} + + + + + {item.version && ( + + {item.version} + + )} + +
+ ); +} + +function ResultGroups({groups, activeIdx, onHover, onSelect}: { + groups: Array; + activeIdx: number; + onHover: (idx: number) => void; + onSelect: (item: SearchItem) => void; +}) { + let idx = 0; + return ( + <> + {groups.map((group, gi) => ( +
+ + {group.items.map(item => { + const myIdx = idx++; + const Row = item.kind === `pkg` ? PkgResultRow : DocResultRow; + return ( + onHover(myIdx)} + onClick={() => onSelect(item)} + /> + ); + })} +
+ ))} + + ); +} + +function EmptyState({onSelect}: {onSelect: (term: string) => void}) { + const recents = getRecents(); + + return ( +
+ {recents.length > 0 && ( +
+
Recent
+ {recents.map((r, i) => ( +
onSelect(r.term)} + className="flex items-center gap-2.5 py-2 px-1 rounded-lg cursor-pointer text-[var(--fg-dim)] text-[13.5px] transition-colors hover:text-[var(--fg)] hover:bg-[color-mix(in_oklch,var(--fg)_4%,transparent)] hover:pl-2" + > + + {r.term} + {r.kind} +
+ ))} +
+ )} + +
+
Suggested
+ +
+ {SUGGESTED.map((term, i) => ( +
onSelect(term)} + className="border border-[var(--line)] rounded-[10px] px-3 py-2.5 bg-[color-mix(in_oklch,var(--fg)_2%,transparent)] cursor-pointer flex items-center gap-2.5 transition-colors hover:border-[var(--accent-line)] hover:bg-[var(--accent-soft)] group" + > + {String(i + 1).padStart(2, `0`)} + {term} +
+ ))} +
+
+
+ ); +} + +function NoResults({query}: {query: string}) { + return ( +
+
+ +
+ +
No matches
+ +
+ Nothing for "{query}" in this scope. +
+
+ ); +} + +function Footer() { + return ( +
+
+ open + navigate + tab filter + esc close +
+ + + + search by Algolia + +
+ ); +} + +// ── Main component ── + +export default function SearchModal() { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(``); + const [scope, setScope] = useState(`all`); + const [results, setResults] = useState>([]); + const [loading, setLoading] = useState(false); + const [activeIdx, setActiveIdx] = useState(0); + + const inputRef = useRef(null); + const resultsRef = useRef(null); + const debounceRef = useRef | null>(null); + + const openModal = useCallback(() => { + setOpen(true); + setQuery(``); + setResults([]); + setActiveIdx(0); + }, []); + + const closeModal = useCallback(() => { + setOpen(false); + setQuery(``); + setResults([]); + }, []); + + // Cmd+K global shortcut + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === `k`) { + e.preventDefault(); + if (open) closeModal(); + else openModal(); + } + }; + document.addEventListener(`keydown`, handleKeyDown); + return () => document.removeEventListener(`keydown`, handleKeyDown); + }, [open, openModal, closeModal]); + + // Wire nav search trigger + useEffect(() => { + const trigger = document.querySelector(`nav [role="search"]`); + if (!trigger) return; + + const handleClick = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + openModal(); + }; + + trigger.addEventListener(`click`, handleClick); + return () => trigger.removeEventListener(`click`, handleClick); + }, [openModal]); + + // Body scroll lock + autofocus + useEffect(() => { + if (open) { + document.body.style.overflow = `hidden`; + setTimeout(() => inputRef.current?.focus(), 50); + } else { + document.body.style.overflow = ``; + } + }, [open]); + + // Algolia search + const search = useCallback(async (q: string) => { + if (!q.trim()) { + setResults([]); + setLoading(false); + return; + } + + setLoading(true); + + try { + const [docsResponse, pkgResponse] = await Promise.all([ + docsClient.search([{ + indexName: `yarnpkg_next`, + query: q, + params: { + hitsPerPage: 15, + attributesToHighlight: [`hierarchy.lvl0`, `hierarchy.lvl1`, `hierarchy.lvl2`, `hierarchy.lvl3`, `hierarchy.lvl4`, `hierarchy.lvl5`, `hierarchy.lvl6`, `content`], + attributesToSnippet: [`content:30`], + }, + }]), + pkgClient.search([{ + indexName: `npm-search`, + query: q, + params: { + hitsPerPage: 10, + attributesToRetrieve: [`name`, `version`, `description`, `owner`, `humanDownloadsLast30Days`, `downloadsLast30Days`, `license`], + attributesToHighlight: [`name`, `description`], + }, + }]), + ]); + + const docsHits: Array = (docsResponse.results[0] as any)?.hits?.map((hit: any) => { + const hierarchy = hit.hierarchy || {}; + const levels = [hierarchy.lvl0, hierarchy.lvl1, hierarchy.lvl2, hierarchy.lvl3, hierarchy.lvl4, hierarchy.lvl5, hierarchy.lvl6].filter(Boolean); + const title = levels[levels.length - 1] || `Untitled`; + const crumbs = levels.slice(0, -1); + const url: string = hit.url || ``; + const isCli = url.includes(`/cli/`) || (hierarchy.lvl0 || ``).toLowerCase().includes(`cli`); + + const snippetHtml = hit._snippetResult?.content?.value || ``; + const titleHtml = highlightValue(hit, `hierarchy.lvl${levels.length - 1}`); + + return { + kind: isCli ? `cli` : `docs`, + title: stripTags(title), + titleHtml: titleHtml || title, + crumbs, + snippet: stripTags(snippetHtml), + snippetHtml, + href: url, + } satisfies SearchItem; + }) ?? []; + + const pkgHits: Array = (pkgResponse.results[0] as any)?.hits?.map((hit: any) => ({ + kind: `pkg` as const, + title: hit.name || ``, + titleHtml: highlightValue(hit, `name`), + snippet: stripTags(hit.description || ``), + snippetHtml: highlightValue(hit, `description`), + href: `/package/${hit.name}`, + version: hit.version, + downloads: hit.humanDownloadsLast30Days, + downloadsRaw: hit.downloadsLast30Days, + author: hit.owner?.name, + license: hit.license, + })) ?? []; + + setResults([...docsHits, ...pkgHits]); + setActiveIdx(0); + } catch { + setResults([]); + } finally { + setLoading(false); + } + }, []); + + // Debounced search + useEffect(() => { + if (debounceRef.current) clearTimeout(debounceRef.current); + if (!query.trim()) { + setResults([]); + return; + } + debounceRef.current = setTimeout(() => search(query), 200); + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current); + }; + }, [query, search]); + + const counts: Record = {all: results.length, docs: 0, pkg: 0, cli: 0}; + results.forEach(r => counts[r.kind]++); + + const groups = groupResults(results, scope, query); + const flatItems = flattenGroups(groups); + + const navigateToResult = useCallback((item: SearchItem) => { + addRecent(item.title, item.kind); + window.location.href = item.href; + }, []); + + const handleSelect = useCallback((term: string) => { + setQuery(term); + search(term); + }, [search]); + + // Keyboard navigation + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === `Escape`) { + e.preventDefault(); + closeModal(); + } else if (e.key === `ArrowDown`) { + e.preventDefault(); + setActiveIdx(i => Math.min(i + 1, flatItems.length - 1)); + } else if (e.key === `ArrowUp`) { + e.preventDefault(); + setActiveIdx(i => Math.max(i - 1, 0)); + } else if (e.key === `Enter` && flatItems[activeIdx]) { + e.preventDefault(); + navigateToResult(flatItems[activeIdx]); + } else if (e.key === `Tab`) { + e.preventDefault(); + const scopeKeys = SCOPES.map(s => s.key); + const curIdx = scopeKeys.indexOf(scope); + const nextIdx = e.shiftKey + ? (curIdx - 1 + scopeKeys.length) % scopeKeys.length + : (curIdx + 1) % scopeKeys.length; + setScope(scopeKeys[nextIdx]); + setActiveIdx(0); + } + }, [activeIdx, flatItems, scope, closeModal, navigateToResult]); + + // Scroll active into view + useEffect(() => { + const el = resultsRef.current?.querySelector(`.active`); + el?.scrollIntoView({block: `nearest`}); + }, [activeIdx]); + + if (!open) return null; + + return ( +
{ if (e.target === e.currentTarget) closeModal(); }} + > +
+ {/* Header */} +
+ + + setQuery(e.target.value)} + className="flex-1 bg-transparent border-0 outline-0 text-[var(--fg)] font-sans text-[17px] tracking-[-0.005em] py-1 min-w-0 placeholder:text-[var(--fg-mute)]" + /> + {query && ( + + )} + + +
+ + {/* Scope chips */} +
+ scope + + {SCOPES.map(s => ( + { setScope(s.key); setActiveIdx(0); }} + /> + ))} +
+ + {/* Results */} +
+ {query.trim() === `` ? ( + + ) : loading && results.length === 0 ? ( +
+ Searching… +
+ ) : flatItems.length === 0 ? ( + + ) : ( + + )} +
+ +
+
+
+ ); +} diff --git a/website/src/components/SimpleIcon.astro b/website/src/components/SimpleIcon.astro new file mode 100644 index 00000000..409e2965 --- /dev/null +++ b/website/src/components/SimpleIcon.astro @@ -0,0 +1,25 @@ +--- +import data from '@iconify-json/simple-icons/icons.json'; + +interface Props { + name: string; + size?: number; + class?: string; +} + +const { name, size = 16, class: className } = Astro.props; +const icon = (data as any).icons[name]; +if (!icon) throw new Error(`Unknown simple-icon: ${name}`); +const viewBox = `0 0 ${icon.width ?? data.width ?? 24} ${icon.height ?? data.height ?? 24}`; +--- + + diff --git a/website/src/components/SkeetCard.astro b/website/src/components/SkeetCard.astro new file mode 100644 index 00000000..e688d826 --- /dev/null +++ b/website/src/components/SkeetCard.astro @@ -0,0 +1,46 @@ +--- +interface Props { + avatarUrl: string; + name: string; + handle: string; + date: string; + text: string; + postUrl: string; + likeCount: number; +} + +const { avatarUrl, name, handle, date, text, postUrl, likeCount } = Astro.props; + +const heartSvg = ``; +const bskySvg = ``; +--- + +
+
+
+ + + + +
+ + {name} + + + + {handle} + + + {date} + +
+
+ + +
diff --git a/website/src/components/benchmarks/BenchmarkChart.tsx b/website/src/components/benchmarks/BenchmarkChart.tsx new file mode 100644 index 00000000..73e7e91c --- /dev/null +++ b/website/src/components/benchmarks/BenchmarkChart.tsx @@ -0,0 +1,486 @@ +import {useState, useEffect, useMemo, useRef, useCallback, type JSX} from 'react'; + +import type {HoverInfo} from './BenchmarkTooltip'; +import {SERIES_COLORS, median, getSeriesValues, type SeriesMeta, type Scenario, type Project, type Incident, type BenchPoint} from './BenchmarksDashboard'; +import type {VersionEntry} from './useVersions'; + +const ML = 30, MR = 6, MT = 8, MB = 18; + +const GITHUB_REPOS: Record = { + npm: {repo: `npm/cli`, tagPrefix: `v`}, + pnpm: {repo: `pnpm/pnpm`, tagPrefix: `v`}, + classic: {repo: `yarnpkg/yarn`, tagPrefix: `v`}, + yarn: {repo: `yarnpkg/berry`, tagPrefix: `@yarnpkg/cli/`}, +}; + +interface Props { + scenario: Scenario; + project: Project; + data: Record>; + seriesOrder: ReadonlyArray; + seriesMeta: Record; + mutedSeries: Record; + incidents: Array; + versions: Record> | null; + showVersions: boolean; + hoveredIndex: number | null; + onHover: (info: HoverInfo | null | ((prev: HoverInfo | null) => HoverInfo | null)) => void; +} + +export function BenchmarkChart({scenario, project, data, seriesOrder, seriesMeta, mutedSeries, incidents, versions, showVersions, hoveredIndex, onHover}: Props): JSX.Element { + const svgRef = useRef(null); + const containerRef = useRef(null); + const [size, setSize] = useState<{w: number, h: number} | null>(null); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + const ro = new ResizeObserver(entries => { + const {width, height} = entries[0].contentRect; + setSize({w: width, h: height}); + }); + ro.observe(el); + return () => ro.disconnect(); + }, []); + + const chartData = useMemo(() => { + if (!size) return null; + + const {w, h} = size; + const pw = w - ML - MR; + const ph = h - MT - MB; + + if (pw <= 0 || ph <= 0) + return null; + + const visible = seriesOrder.filter(s => !mutedSeries[s]); + const allVals: Array = []; + for (const sid of visible) { + const vals = getSeriesValues(data, sid); + for (const v of vals) + if (v !== null) allVals.push(v); + } + + if (!allVals.length) return null; + + const yMin = 0; + let yMax = Math.max(...allVals); + const pad = yMax * 0.12 || 0.1; + yMax = yMax + pad; + + const points = data[seriesOrder[0]]; + const N = points?.length ?? 0; + if (!N) return null; + + const xScale = (i: number) => ML + (i / (N - 1)) * pw; + const yScale = (v: number) => MT + ph - ((v - yMin) / (yMax - yMin)) * ph; + + const incidentSet: Record = {}; + const incidentRanges: Array<{start: number, end: number, label: string}> = []; + + for (const inc of incidents) { + let iStart = -1, iEnd = -1; + for (let ip = 0; ip < N; ip++) { + const ts = points[ip].timestamp; + if (ts >= inc.start && iStart === -1) iStart = ip; + if (ts <= inc.end) iEnd = ip; + } + if (iStart === -1 || iEnd === -1 || iEnd < iStart) continue; + incidentRanges.push({start: iStart, end: iEnd, label: inc.label}); + for (let ik = iStart; ik <= iEnd; ik++) incidentSet[ik] = true; + } + + const drawOrder = seriesOrder.filter(s => s !== `zpm`).concat(`zpm`); + + const paths: Array<{id: string, d: string, cls: string, color: string}> = []; + for (const sid of drawOrder) { + if (mutedSeries[sid]) continue; + const seriesPoints = data[sid]; + if (!seriesPoints) continue; + + let pathD = ``; + let prevWasNull = true; + for (let pi = 0; pi < seriesPoints.length; pi++) { + const sv = seriesPoints[pi].value; + if (sv === null || incidentSet[pi]) { + prevWasNull = true; continue; + } + const px = xScale(pi), py = yScale(sv); + pathD += `${(prevWasNull ? `M` : `L`) + px.toFixed(2)},${py.toFixed(2)}`; + prevWasNull = false; + } + if (!pathD) continue; + + const meta = seriesMeta[sid]; + const cls = `series-line${meta.dashed ? ` dashed` : ``}${meta.accent ? ` highlight` : ``}`; + paths.push({id: sid, d: pathD, cls, color: SERIES_COLORS[sid]}); + } + + let band: {x: number, y: number, w: number, h: number} | null = null; + if (!mutedSeries.zpm) { + const zpmVals = getSeriesValues(data, `zpm`); + const zpmMed = median(zpmVals); + const top = yScale(zpmMed * 1.08); + const bot = yScale(zpmMed * 0.92); + if (bot > top) band = {x: ML, y: top, w: pw, h: bot - top}; + } + + const versionDots: Array<{cx: number, cy: number, r: number, color: string, cls: string, url: string | null}> = []; + if (showVersions && versions) { + for (const sid of drawOrder) { + if (mutedSeries[sid]) continue; + const vers = versions[sid] ?? []; + const seriesP = data[sid]; + if (!seriesP || !vers.length) continue; + + for (let vi = 0; vi < vers.length; vi++) { + const ver = vers[vi]; + let bestIdx = 0, bestDist = Infinity; + for (let vp = 0; vp < N; vp++) { + const dist = Math.abs(points[vp].timestamp - ver.t); + if (dist < bestDist) { + bestDist = dist; bestIdx = vp; + } + } + if (incidentSet[bestIdx]) continue; + const sv = seriesP[bestIdx]?.value; + if (sv === null || sv === undefined) continue; + + const gh = GITHUB_REPOS[sid]; + let url: string | null = null; + if (gh) { + if (vi > 0) { + url = `https://github.com/${gh.repo}/compare/${gh.tagPrefix}${vers[vi - 1].v}...${gh.tagPrefix}${ver.v}`; + } else { + url = `https://github.com/${gh.repo}/releases/tag/${gh.tagPrefix}${ver.v}`; + } + } + + versionDots.push({ + cx: xScale(bestIdx), + cy: yScale(sv), + r: seriesMeta[sid].accent ? 4 : 3, + color: SERIES_COLORS[sid], + cls: `version-dot`, + url, + }); + } + } + } + + const yTicks = [yMin, (yMin + yMax) / 2, yMax].map(v => ({ + value: v, + label: `${v < 1 ? v.toFixed(2) : v < 10 ? v.toFixed(1) : Math.round(v).toString()}s`, + pct: (yScale(v) / h * 100), + })); + + const dateIndices = [0, Math.floor(N / 4), Math.floor(N / 2), Math.floor(3 * N / 4), N - 1]; + const xLabels = dateIndices.map(idx => { + const ts = points[idx].timestamp; + const d = new Date(ts * 1000); + return { + label: `${d.getMonth() + 1}/${d.getDate()}`, + pct: (xScale(idx) / w * 100), + }; + }); + + return { + w, h, pw, ph, + yMin, yMax, N, points, xScale, yScale, + incidentSet, incidentRanges, + paths, band, versionDots, + yTicks, xLabels, + drawOrder, + }; + }, [data, seriesOrder, seriesMeta, mutedSeries, incidents, versions, showVersions, size]); + + const zpmValues = useMemo(() => getSeriesValues(data, `zpm`), [data]); + const zpmMedian = useMemo(() => median(zpmValues), [zpmValues]); + + const pill = useMemo(() => { + if (zpmMedian <= 0) + return null; + + const medians: Array<{id: string, m: number}> = []; + for (const sid of seriesOrder) { + if (mutedSeries[sid]) continue; + const m = median(getSeriesValues(data, sid)); + if (m > 0) medians.push({id: sid, m}); + } + + const others = medians.filter(x => x.id !== `zpm`); + if (!others.length) + return {cls: `fastest`, text: `no comparison data`}; + + const fastest = others.reduce((min, x) => x.m < min.m ? x : min, others[0]); + const name = seriesMeta[fastest.id]?.name ?? fastest.id; + + if (zpmMedian <= fastest.m) { + const diff = +(fastest.m - zpmMedian).toFixed(1); + if (diff === 0) return {cls: `contested`, text: `tied with ${name}`}; + return {cls: `fastest`, text: `${diff}s faster than ${name}`}; + } + + const diff = +(zpmMedian - fastest.m).toFixed(1); + + const cls = diff / zpmMedian <= 0.1 ? `contested` : `slower`; + return {cls, text: `${diff}s slower than ${name}`}; + }, [data, seriesOrder, seriesMeta, mutedSeries, zpmMedian]); + + const prevIdxRef = useRef(null); + + const handleMouseMove = useCallback((e: React.MouseEvent) => { + if (!chartData || !svgRef.current) return; + const rect = svgRef.current.getBoundingClientRect(); + const sx = e.clientX - rect.left; + + if (sx < ML || sx > chartData.w - MR) { + prevIdxRef.current = null; + onHover(null); + return; + } + + const tFrac = (sx - ML) / chartData.pw; + const idx = Math.max(0, Math.min(chartData.N - 1, Math.round(tFrac * (chartData.N - 1)))); + + if (idx === prevIdxRef.current) { + onHover(prev => prev ? {...prev, mouseX: e.clientX, mouseY: e.clientY, index: idx} : prev); + return; + } + prevIdxRef.current = idx; + + const ts = chartData.points[idx].timestamp; + const d = new Date(ts * 1000); + const dateStr = d.toISOString().slice(0, 10); + const inIncident = !!chartData.incidentSet[idx]; + + if (inIncident) { + let incLabel = ``; + for (const ir of chartData.incidentRanges) + if (idx >= ir.start && idx <= ir.end) { + incLabel = ir.label; break; + } + + onHover({ + mouseX: e.clientX, mouseY: e.clientY, index: idx, + dateStr, scenarioTitle: scenario.title, projectName: project.name, + isIncident: true, incidentLabel: incLabel, + rows: [], versionMap: null, showVersions, seriesMeta, + }); + return; + } + + const rows: Array<{id: string, value: number}> = []; + for (const sid of seriesOrder) { + if (mutedSeries[sid]) continue; + const sp = data[sid]; + if (!sp?.[idx] || sp[idx].value === null) continue; + rows.push({id: sid, value: sp[idx].value!}); + } + rows.sort((a, b) => a.value - b.value); + + let versionMap: Record | null = null; + if (showVersions && versions) { + versionMap = {}; + for (const sid of seriesOrder) { + const vers = versions[sid]; + if (!vers?.length) continue; + for (let i = vers.length - 1; i >= 0; i--) { + if (vers[i].t <= ts) { + versionMap[sid] = vers[i].v; break; + } + } + } + } + + onHover({ + mouseX: e.clientX, mouseY: e.clientY, index: idx, + dateStr, scenarioTitle: scenario.title, projectName: project.name, + isIncident: false, + rows, versionMap, showVersions, seriesMeta, + }); + }, [chartData, data, seriesOrder, seriesMeta, mutedSeries, scenario, project, versions, showVersions, onHover]); + + const handleMouseLeave = useCallback(() => { + prevIdxRef.current = null; + onHover(null); + }, [onHover]); + + const cellRef = useRef(null); + const touchDepsRef = useRef({chartData, data, seriesOrder, seriesMeta, mutedSeries, scenario, project, versions, showVersions, onHover}); + touchDepsRef.current = {chartData, data, seriesOrder, seriesMeta, mutedSeries, scenario, project, versions, showVersions, onHover}; + + useEffect(() => { + const el = cellRef.current; + if (!el) return; + + const onTouch = (e: TouchEvent) => { + const {chartData: cd, data: d, seriesOrder: so, seriesMeta: sm, mutedSeries: ms, scenario: sc, project: pr, versions: ver, showVersions: sv, onHover: oh} = touchDepsRef.current; + if (!cd || !svgRef.current) return; + e.preventDefault(); + const touch = e.touches[0]; + if (!touch) { + prevIdxRef.current = null; oh(null); return; + } + const rect = svgRef.current.getBoundingClientRect(); + const sx = touch.clientX - rect.left; + + if (sx < ML || sx > cd.w - MR) { + prevIdxRef.current = null; + oh(null); + return; + } + + const tFrac = (sx - ML) / cd.pw; + const idx = Math.max(0, Math.min(cd.N - 1, Math.round(tFrac * (cd.N - 1)))); + + if (idx === prevIdxRef.current) { + oh((prev: any) => prev ? {...prev, mouseX: touch.clientX, mouseY: touch.clientY, index: idx} : prev); + return; + } + prevIdxRef.current = idx; + + const ts = cd.points[idx].timestamp; + const dt = new Date(ts * 1000); + const dateStr = dt.toISOString().slice(0, 10); + const inIncident = !!cd.incidentSet[idx]; + + if (inIncident) { + let incLabel = ``; + for (const ir of cd.incidentRanges) + if (idx >= ir.start && idx <= ir.end) { + incLabel = ir.label; break; + } + + oh({mouseX: touch.clientX, mouseY: touch.clientY, index: idx, dateStr, scenarioTitle: sc.title, projectName: pr.name, isIncident: true, incidentLabel: incLabel, rows: [], versionMap: null, showVersions: sv, seriesMeta: sm}); + return; + } + + const rows: Array<{id: string, value: number}> = []; + for (const sid of so) { + if (ms[sid]) continue; + const sp = d[sid]; + if (!sp?.[idx] || sp[idx].value === null) continue; + rows.push({id: sid, value: sp[idx].value!}); + } + rows.sort((a, b) => a.value - b.value); + + let versionMap: Record | null = null; + if (sv && ver) { + versionMap = {}; + for (const sid of so) { + const vs = ver[sid]; + if (!vs?.length) continue; + for (let i = vs.length - 1; i >= 0; i--) { + if (vs[i].t <= ts) { + versionMap[sid] = vs[i].v; break; + } + } + } + } + + oh({mouseX: touch.clientX, mouseY: touch.clientY, index: idx, dateStr, scenarioTitle: sc.title, projectName: pr.name, isIncident: false, rows, versionMap, showVersions: sv, seriesMeta: sm}); + }; + + const onEnd = () => { + prevIdxRef.current = null; + touchDepsRef.current.onHover(null); + }; + + el.addEventListener(`touchstart`, onTouch, {passive: false}); + el.addEventListener(`touchmove`, onTouch, {passive: false}); + el.addEventListener(`touchend`, onEnd); + return () => { + el.removeEventListener(`touchstart`, onTouch); + el.removeEventListener(`touchmove`, onTouch); + el.removeEventListener(`touchend`, onEnd); + }; + }, []); + + if (!chartData) { + return ( +
+
{project.name}
+
+ {size ? `No data` : ``} +
+
+
+ ); + } + + const gridY = [0.25, 0.5, 0.75]; + + return ( +
+
{project.name}
+
+ yarn median {zpmMedian.toFixed(2)}s + {pill && {pill.text}} +
+
+ + {gridY.map(f => { + const gy = MT + f * chartData.ph; + return ; + })} + + + + + {chartData.incidentRanges.map((ir, i) => { + const ix1 = chartData.xScale(ir.start); + const ix2 = chartData.xScale(ir.end); + const iw = Math.max(ix2 - ix1, 2); + return ( + + + + + ); + })} + + {chartData.band && ( + + )} + + {chartData.paths.map(p => ( + + ))} + + {chartData.versionDots.map((dot, i) => + dot.url ? ( + + + + ) : ( + + ), + )} + + {hoveredIndex !== null && hoveredIndex < chartData.N && ( + + )} + + + {chartData.yTicks.map((tick, i) => ( + + {tick.label} + + ))} + + {chartData.xLabels.map((lbl, i) => ( + + {lbl.label} + + ))} +
+
+ ); +} diff --git a/website/src/components/benchmarks/BenchmarkSummary.tsx b/website/src/components/benchmarks/BenchmarkSummary.tsx new file mode 100644 index 00000000..66cdd713 --- /dev/null +++ b/website/src/components/benchmarks/BenchmarkSummary.tsx @@ -0,0 +1,120 @@ +import {useMemo, type JSX} from 'react'; +import {SERIES_COLORS, median, getSeriesValues, type SeriesMeta, type Project, type BenchPoint} from './BenchmarksDashboard'; + +interface Props { + data: Record>>>; + seriesOrder: readonly string[]; + seriesMeta: Record; + projects: Array; + mutedSeries: Record; +} + +function aggregate( + scenarioId: string, + data: Props[`data`], + seriesOrder: readonly string[], + seriesMeta: Record, + projects: Array, + mutedSeries: Record, +) { + const visibleSeries = seriesOrder.filter(sid => !mutedSeries[sid]); + + // Keep only projects where every visible series has a usable median. + // Comparing geomeans across series is only meaningful when they're computed + // over the same project set. + const rows: Array> = []; + for (const p of projects) { + const projectData = data[scenarioId]?.[p.id]; + if (!projectData) continue; + const medians: Record = {}; + let complete = true; + for (const sid of visibleSeries) { + const m = median(getSeriesValues(projectData, sid)); + if (m > 0) { + medians[sid] = m; + } else { + complete = false; + break; + } + } + if (complete) rows.push(medians); + } + + if (visibleSeries.length === 0 || rows.length === 0) return []; + + // Geomean of absolute medians per series. Computed in log space for + // numerical stability when projects span very different magnitudes. + const geomean: Record = {}; + for (const sid of visibleSeries) { + let logSum = 0; + for (const r of rows) logSum += Math.log(r[sid]); + geomean[sid] = Math.exp(logSum / rows.length); + } + + // Single normalization pass against the slowest aggregate, so the slowest + // series lands on exactly 1.00× and the rest read as "X× the slowest." + const slowest = Math.max(...visibleSeries.map(sid => geomean[sid])); + + const out = visibleSeries.map(sid => ({ + id: sid, + name: seriesMeta[sid].name, + normalized: geomean[sid] / slowest, + color: SERIES_COLORS[sid], + accent: seriesMeta[sid].accent, + })); + + out.sort((a, b) => a.normalized - b.normalized); + return out; +} + +function SummaryCard({title, agg}: {title: string; agg: ReturnType}): JSX.Element { + return ( +
+

{title}

+
+ {agg.map(a => ( +
+
{a.name}
+
+
+
+
{a.normalized.toFixed(2)}×
+
+ ))} +
+
+ ); +} + +export function BenchmarkSummary({data, seriesOrder, seriesMeta, projects, mutedSeries}: Props): JSX.Element { + const coldAgg = useMemo( + () => aggregate(`install-full-cold`, data, seriesOrder, seriesMeta, projects, mutedSeries), + [data, seriesOrder, seriesMeta, projects, mutedSeries], + ); + const warmAgg = useMemo( + () => aggregate(`install-cache-and-lock`, data, seriesOrder, seriesMeta, projects, mutedSeries), + [data, seriesOrder, seriesMeta, projects, mutedSeries], + ); + + return ( + <> +
+
§ Aggregate
+

Median across all scenarios.

+

+ Geometric mean of per-project medians for each series, then normalized so the slowest series reads as 1.00×. Every other series is the ratio of its average run time to the slowest. Lower is faster. +

+
+
+ + +
+ + ); +} diff --git a/website/src/components/benchmarks/BenchmarkTooltip.tsx b/website/src/components/benchmarks/BenchmarkTooltip.tsx new file mode 100644 index 00000000..e5d05d0f --- /dev/null +++ b/website/src/components/benchmarks/BenchmarkTooltip.tsx @@ -0,0 +1,83 @@ +import {useRef, useLayoutEffect, type JSX} from 'react'; +import {SERIES_COLORS, type SeriesMeta} from './BenchmarksDashboard'; + +export interface HoverInfo { + mouseX: number; + mouseY: number; + index: number; + dateStr: string; + scenarioTitle: string; + projectName: string; + isIncident: boolean; + incidentLabel?: string; + rows: Array<{id: string; value: number}>; + versionMap: Record | null; + showVersions: boolean; + seriesMeta: Record; +} + +export function BenchmarkTooltip({info}: {info: HoverInfo | null}): JSX.Element | null { + const ref = useRef(null); + + useLayoutEffect(() => { + if (!info || !ref.current) return; + const el = ref.current; + const tw = el.offsetWidth; + const th = el.offsetHeight; + const vw = window.innerWidth; + const vh = window.innerHeight; + const margin = 12; + const offset = 14; + + let tx = info.mouseX + offset; + let ty = info.mouseY + offset; + + if (tx + tw > vw - margin) tx = info.mouseX - tw - offset; + if (tx < margin) tx = margin; + + if (ty + th > vh - margin) ty = info.mouseY - th - offset; + if (ty < margin) ty = margin; + + el.style.left = `${tx}px`; + el.style.top = `${ty}px`; + }); + + if (!info) return null; + + return ( +
+ {info.isIncident ? ( + <> +
{info.dateStr} · {info.projectName}
+
+ {info.incidentLabel} +
+ + ) : ( + <> +
+ {info.dateStr} · {info.scenarioTitle} · {info.projectName} +
+ {info.rows.map(r => { + let nameStr = info.seriesMeta[r.id].name; + let verEl: JSX.Element | null = null; + if (info.showVersions) { + if (r.id === `zpm`) { + verEl = main; + } else if (info.versionMap?.[r.id]) { + verEl = v{info.versionMap[r.id]}; + } + } + return ( +
+ + {nameStr}{verEl} + {r.value.toFixed(2)}s +
+ ); + })} + + )} +
+ ); +} diff --git a/website/src/components/benchmarks/BenchmarksDashboard.tsx b/website/src/components/benchmarks/BenchmarksDashboard.tsx new file mode 100644 index 00000000..86fdbb07 --- /dev/null +++ b/website/src/components/benchmarks/BenchmarksDashboard.tsx @@ -0,0 +1,225 @@ +import {useState, useEffect, useCallback, type JSX} from 'react'; + +import {BenchmarkChart} from './BenchmarkChart'; +import {BenchmarkSummary} from './BenchmarkSummary'; +import {BenchmarkTooltip, type HoverInfo} from './BenchmarkTooltip'; +import {useVersions} from './useVersions'; + +export interface BenchPoint { + timestamp: number; + value: number | null; +} + +export interface SeriesMeta { + name: string; + dashed: boolean; + accent: boolean; +} + +export interface Project { + id: string; + name: string; +} + +export interface Scenario { + id: string; + num: string; + title: string; + desc: string; +} + +export interface Incident { + start: number; + end: number; + label: string; +} + +export const SERIES_COLORS: Record = { + zpm: `oklch(0.78 0.16 var(--accent-h))`, + yarn: `oklch(0.65 0.10 var(--accent-h))`, + npm: `oklch(0.70 0.15 25)`, + pnpm: `oklch(0.75 0.13 220)`, + classic: `oklch(0.55 0.08 var(--accent-h))`, +}; + +export function median(arr: Array): number { + const nums = arr.filter((v): v is number => v !== null && v !== undefined); + if (!nums.length) return 0; + nums.sort((a, b) => a - b); + return nums[Math.floor(nums.length / 2)]; +} + +export function getSeriesValues(projectData: Record>, pm: string): Array { + if (!projectData?.[pm]) return []; + return projectData[pm].map(p => p.value); +} + +interface Props { + data: Record>>>; + seriesOrder: ReadonlyArray; + seriesMeta: Record; + projects: Array; + scenarios: Array; + incidents: Array; + benchMinTs: number; + benchMaxTs: number; +} + +const SWATCH_STYLES: Record = { + zpm: {dashed: false}, + yarn: {dashed: false}, + npm: {dashed: false}, + pnpm: {dashed: false}, + classic: {dashed: true}, +}; + +export function BenchmarksDashboard({data, seriesOrder, seriesMeta, projects, scenarios, incidents, benchMinTs, benchMaxTs}: Props): JSX.Element { + const [mutedSeries, setMutedSeries] = useState>({}); + const [selectedProject, setSelectedProject] = useState(`all`); + const [showVersions, setShowVersions] = useState(false); + const [hoverInfo, setHoverInfo] = useState(null); + const [controlsOpen, setControlsOpen] = useState(false); + + const {versions, loading: versionsLoading} = useVersions(benchMinTs, benchMaxTs, showVersions); + + const visibleProjects = selectedProject === `all` + ? projects + : projects.filter(p => p.id === selectedProject); + + const toggleMute = useCallback((pm: string) => { + setMutedSeries(prev => { + if (prev[pm]) { + const next = {...prev}; + delete next[pm]; + return next; + } + if (Object.keys(prev).length >= seriesOrder.length - 1) return prev; + return {...prev, [pm]: true}; + }); + }, [seriesOrder.length]); + + const handleHover = useCallback((infoOrUpdater: HoverInfo | null | ((prev: HoverInfo | null) => HoverInfo | null)) => { + if (typeof infoOrUpdater === `function`) { + setHoverInfo(infoOrUpdater); + } else { + setHoverInfo(infoOrUpdater); + } + }, []); + + useEffect(() => { + const dismiss = () => setHoverInfo(null); + window.addEventListener(`scroll`, dismiss, {passive: true}); + return () => window.removeEventListener(`scroll`, dismiss); + }, []); + + return ( + <> + {/* Sticky controls */} +
+
+ +
+ {/* Project filter */} +
+ Project + {[{id: `all`, name: `All`}, ...projects].map(p => ( + + ))} +
+ + {/* Series legend */} +
+ Series + {seriesOrder.map(sid => ( + + {(sid === `npm` || sid === `classic`) && } + + + ))} + + Click to mute · y-axis = seconds + + +
+
+
+
+ + {/* Benchmark grid */} +
+
scenario \ project
+ {visibleProjects.map(p => ( +
+ {p.name} +
+ ))} + + {scenarios.map(sc => ( + +
+
§ {sc.num} · scenario
+

{sc.title}

+

{sc.desc}

+
+ {visibleProjects.map(p => ( + + ))} +
+ ))} +
+ + {/* Aggregate summary */} + + + {/* Tooltip */} + + + ); +} diff --git a/website/src/components/benchmarks/useVersions.ts b/website/src/components/benchmarks/useVersions.ts new file mode 100644 index 00000000..7413e4ab --- /dev/null +++ b/website/src/components/benchmarks/useVersions.ts @@ -0,0 +1,66 @@ +import {useState, useEffect, useRef} from 'react'; + +export interface VersionEntry { + v: string; + t: number; +} + +const VERSION_PACKAGES: [string, string, string | null][] = [ + [`npm`, `npm`, null], + [`pnpm`, `pnpm`, null], + [`classic`, `yarn`, `1.`], + [`yarn`, `@yarnpkg/cli`, null], +]; + +export function useVersions(benchMinTs: number, benchMaxTs: number, enabled: boolean) { + const [versions, setVersions] = useState> | null>(null); + const [loading, setLoading] = useState(false); + const fetchedRef = useRef(false); + + useEffect(() => { + if (!enabled || fetchedRef.current) + return; + + fetchedRef.current = true; + setLoading(true); + + Promise.all( + VERSION_PACKAGES.map(([pm, pkg, prefix]) => + fetch(`https://registry.npmjs.org/${pkg}`) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (!data?.time) return [pm, []] as const; + const entries: Array = []; + let latestBefore: VersionEntry | null = null; + + for (const v in data.time) { + if (v === `created` || v === `modified`) continue; + if (v.includes(`-`)) continue; + if (prefix && !v.startsWith(prefix)) continue; + + const t = Math.floor(new Date(data.time[v]).getTime() / 1000); + if (t >= benchMinTs && t <= benchMaxTs) { + entries.push({v, t}); + } else if (t < benchMinTs) { + if (!latestBefore || t > latestBefore.t) latestBefore = {v, t}; + } + } + + if (latestBefore) entries.push(latestBefore); + entries.sort((a, b) => a.t - b.t); + return [pm, entries] as const; + }) + .catch(() => [pm, []] as const), + ), + ).then(results => { + const map: Record> = {}; + for (const [pm, entries] of results) + map[pm] = entries; + + setVersions(map); + setLoading(false); + }); + }, [enabled, benchMinTs, benchMaxTs]); + + return {versions, loading}; +} diff --git a/website/src/components/deck/ClosingSlide.astro b/website/src/components/deck/ClosingSlide.astro new file mode 100644 index 00000000..63fafe73 --- /dev/null +++ b/website/src/components/deck/ClosingSlide.astro @@ -0,0 +1,19 @@ +--- +import SlideChrome from './SlideChrome.astro'; + +interface Props { + label?: string; + bottomLeft?: string; + bottomRight?: string; +} + +const { label = `Closing`, bottomLeft, bottomRight } = Astro.props; +--- + +
+ +
+ +
+
+
diff --git a/website/src/components/deck/CodeBlock.astro b/website/src/components/deck/CodeBlock.astro new file mode 100644 index 00000000..f23499b4 --- /dev/null +++ b/website/src/components/deck/CodeBlock.astro @@ -0,0 +1,39 @@ +--- +import { highlight } from '../../utils/highlight'; + +interface Props { + filename?: string; + lang?: string; + code?: string; + compact?: boolean; +} + +const { filename, lang, code, compact = false } = Astro.props; + +const highlighted = code && lang ? await highlight(code, lang) : null; +--- + +
+
+ + + + + + {filename && ( + + {filename} + + )} + + {lang && ( + + {lang} + + )} +
+ + {highlighted + ?
+ :
} +
diff --git a/website/src/components/deck/ContentSlide.astro b/website/src/components/deck/ContentSlide.astro new file mode 100644 index 00000000..e9ec4c80 --- /dev/null +++ b/website/src/components/deck/ContentSlide.astro @@ -0,0 +1,23 @@ +--- +import SlideChrome from './SlideChrome.astro'; + +interface Props { + sectionTag: string; + bottomLeft?: string; + bottomRight?: string; + autoCounter?: boolean; + label?: string; + colRule?: boolean; +} + +const { sectionTag, bottomLeft, bottomRight, autoCounter = false, label, colRule = false } = Astro.props; +--- + +
+ + + +
diff --git a/website/src/components/deck/ImageFrame.astro b/website/src/components/deck/ImageFrame.astro new file mode 100644 index 00000000..eaf06f64 --- /dev/null +++ b/website/src/components/deck/ImageFrame.astro @@ -0,0 +1,32 @@ +--- +interface Props { + href?: string; + alt?: string; + placeholder?: string; + caption?: string; + aspectRatio?: string; + style?: string; +} + +const { href, alt = ``, placeholder, caption, aspectRatio, style } = Astro.props; +--- + +
+ {href ? ( + {alt} + ) : placeholder ? ( +
+ {placeholder} +
+ ) : ( + + )} + {caption && ( +
+ {caption} +
+ )} +
diff --git a/website/src/components/deck/SectionSlide.astro b/website/src/components/deck/SectionSlide.astro new file mode 100644 index 00000000..2c60ae1b --- /dev/null +++ b/website/src/components/deck/SectionSlide.astro @@ -0,0 +1,36 @@ +--- +import SlideChrome from './SlideChrome.astro'; + +interface Props { + sectionTag: string; + partLabel: string; + title: string; + subtitle?: string; + lede?: string; + bottomLeft?: string; + bottomRight?: string; + label?: string; +} + +const { sectionTag, partLabel, title, subtitle, lede, bottomLeft, bottomRight, label } = Astro.props; +--- + +
+ +
+

+ {partLabel} +

+ +
+ +

${subtitle}` : ``)} /> + + {lede && ( +

+ {lede} +

+ )} +

+
+
diff --git a/website/src/components/deck/SlideChrome.astro b/website/src/components/deck/SlideChrome.astro new file mode 100644 index 00000000..68908a2f --- /dev/null +++ b/website/src/components/deck/SlideChrome.astro @@ -0,0 +1,44 @@ +--- +import YarnBrand from './YarnBrand.astro'; + +interface Props { + tag: string; + bottomLeft?: string; + bottomRight?: string; + autoCounter?: boolean; + revealBrand?: boolean; +} + +const { tag, bottomLeft, bottomRight, autoCounter = false, revealBrand = false } = Astro.props; + +const hasAutoBottomRight = !bottomRight && autoCounter; +const hasBottom = !!(bottomLeft || bottomRight || hasAutoBottomRight); +--- + +
+ +
+ + + + + {tag} + +
+ +
+ +
+ +{hasBottom && ( +
+ {bottomLeft} + + {hasAutoBottomRight ? ( + <>? / ? + ) : ( + bottomRight + )} + +
+)} diff --git a/website/src/components/deck/TagPill.astro b/website/src/components/deck/TagPill.astro new file mode 100644 index 00000000..d7d2f63f --- /dev/null +++ b/website/src/components/deck/TagPill.astro @@ -0,0 +1,18 @@ +--- +interface Props { + badge: string; + version: string; + badgeStyle?: string; +} + +const { badge, version, badgeStyle } = Astro.props; +--- + + + + {badge} + + + {version} + + diff --git a/website/src/components/deck/Terminal.astro b/website/src/components/deck/Terminal.astro new file mode 100644 index 00000000..de35c49b --- /dev/null +++ b/website/src/components/deck/Terminal.astro @@ -0,0 +1,33 @@ +--- +interface Props { + title?: string; + id?: string; + animated?: boolean; +} + +const { title, id, animated = false } = Astro.props; + +const terminalFiles = import.meta.glob<{default: unknown[]}>('/src/data/terminals/*.json', { eager: true }); +const recipe = id ? terminalFiles[`/src/data/terminals/${id}.json`]?.default : undefined; +--- + +
+
+ + + + + + + {title && ( + + {title} + + )} +
+ +
+ {recipe && diff --git a/website/src/components/package/AuditPanel.tsx b/website/src/components/package/AuditPanel.tsx new file mode 100644 index 00000000..40940ba9 --- /dev/null +++ b/website/src/components/package/AuditPanel.tsx @@ -0,0 +1,46 @@ +import {useIcons} from './contexts'; +import {OctIcon} from './icons'; + +export function AuditPanel() { + const {oct} = useIcons(); + + const bars = [ + {label: `Maintenance`, value: 0.84, display: `84`}, + {label: `Popularity`, value: 0.76, display: `76`}, + {label: `Quality`, value: 0.91, display: `91`}, + ]; + + return ( +
+
+ + + + Package scores +
+
+
+ Scores are computed from npm registry metadata. Detailed audit data requires a full dependency analysis. +
+ + {bars.map(bar => ( +
+
{bar.label}
+
+
+
+
{bar.display}
+
+ ))} +
+
+ ); +} diff --git a/website/src/components/package/DependenciesCard.tsx b/website/src/components/package/DependenciesCard.tsx new file mode 100644 index 00000000..20d281a6 --- /dev/null +++ b/website/src/components/package/DependenciesCard.tsx @@ -0,0 +1,24 @@ +export function DependenciesCard({deps, title}: {deps: Record, title: string}) { + const entries = Object.entries(deps); + if (entries.length === 0) return null; + + return ( +
+
+ {title} + {entries.length} +
+ {entries.map(([name, range]) => ( +
+ + {name} + + {range} +
+ ))} +
+ ); +} diff --git a/website/src/components/package/DownloadsCard.tsx b/website/src/components/package/DownloadsCard.tsx new file mode 100644 index 00000000..e17d8c51 --- /dev/null +++ b/website/src/components/package/DownloadsCard.tsx @@ -0,0 +1,62 @@ +import type {DownloadDay} from './types'; +import {formatNumberFull, sparklinePath} from './utils'; + +export function DownloadsCard({downloads}: {downloads: Array | null}) { + if (!downloads || downloads.length === 0) return null; + + const lastWeek = downloads.slice(-7).reduce((s, d) => s + d.downloads, 0); + const prevWeek = downloads.slice(-14, -7).reduce((s, d) => s + d.downloads, 0); + + const pctChange = prevWeek > 0 ? ((lastWeek - prevWeek) / prevWeek * 100) : 0; + + const dailyData = downloads.map(d => d.downloads); + const startDate = downloads[0]?.day; + const endDate = downloads[downloads.length - 1]?.day; + + const W = 280; + const H = 56; + + const line = sparklinePath(dailyData, W, H); + const area = line ? `${line} L${W},${H} L0,${H} Z` : ``; + + return ( +
+
+ Weekly downloads + all versions +
+ +
+ {formatNumberFull(lastWeek)} + {pctChange !== 0 && ( + 0 ? `text-[oklch(0.78_0.16_145)]` : `text-[oklch(0.78_0.16_25)]`}`}> + {pctChange > 0 ? `+` : ``}{pctChange.toFixed(1)}% + + )} +
+ +
+ {startDate} → {endDate} +
+ + {line && ( + <> + + + + + + + + + + +
+ {startDate ? new Date(startDate).toLocaleDateString(`en-US`, {month: `short`, day: `numeric`}) : ``} + {endDate ? new Date(endDate).toLocaleDateString(`en-US`, {month: `short`, day: `numeric`}) : ``} +
+ + )} +
+ ); +} diff --git a/website/src/components/package/FilesExplorer.tsx b/website/src/components/package/FilesExplorer.tsx new file mode 100644 index 00000000..cd30ef7f --- /dev/null +++ b/website/src/components/package/FilesExplorer.tsx @@ -0,0 +1,530 @@ +import {useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense} from 'react'; + +import type {FileEntry, Tab, TreeNode} from './types'; +import {useIcons} from './contexts'; +import {OctIcon} from './icons'; +import {formatBytes, timeAgo, langFromPath, setupMonacoTheme, buildFileTree, formatWithPrettier, canPrettify, compareSemverDesc, isNoisyPrerelease} from './utils'; + +const MonacoEditor = lazy(() => import(`@monaco-editor/react`).then(m => ({default: m.default}))); +const MonacoDiffEditor = lazy(() => import(`@monaco-editor/react`).then(m => ({default: m.DiffEditor}))); + +// ── Internal components ── + +function SmallChevron({open}: {open: boolean}) { + const {oct} = useIcons(); + return ; +} + +function TreeFileIcon() { + const {oct} = useIcons(); + return ; +} + +function TreeFolderIcon({open}: {open: boolean}) { + const {oct} = useIcons(); + return ; +} + +function CompareIcon() { + const {oct} = useIcons(); + return ; +} + +const CHANGE_COLORS: Record = { + added: `oklch(0.75 0.15 145)`, + removed: `oklch(0.75 0.15 25)`, + modified: `oklch(0.80 0.14 80)`, +}; + +function ExplorerTreeNode({node, depth, selectedFile, onSelectFile, changeMap}: { + node: TreeNode; depth: number; selectedFile: string | null; onSelectFile: (path: string) => void; + changeMap?: Map; +}) { + const [expanded, setExpanded] = useState(depth < 1); + + const isDir = !!node.children; + const indent = depth * 16; + + const changeType = changeMap?.get(node.path); + const nameColor = changeType ? CHANGE_COLORS[changeType] : undefined; + + if (isDir) { + return ( + <> +
setExpanded(!expanded)} + > + + + {node.name} +
+ {expanded && node.children?.map(child => ( + + ))} + + ); + } + + return ( +
onSelectFile(node.path)} + > + + + {node.name} + {node.size != null && {formatBytes(node.size)}} +
+ ); +} + +// ── Main component ── + +export function FilesExplorer({ + files, name, version, versions, distTags, time, onVersionChange, + onTabChange, onFileChange, onCompareChange, selectedFile, compareVersion, +}: { + files: Array | null; + name: string; version: string; + versions: Array; + distTags: Record; + time: Record; + onVersionChange: (v: string) => void; + onTabChange: (t: Tab) => void; + onFileChange: (path: string | null) => void; + onCompareChange: (compareVersion: string | null) => void; + selectedFile: string | null; + compareVersion: string | null; +}) { + const {oct} = useIcons(); + + const [fileContent, setFileContent] = useState(null); + const [fileLoading, setFileLoading] = useState(false); + const [fileError, setFileError] = useState(null); + const [monacoReady, setMonacoReady] = useState(false); + const [explorerOpen, setExplorerOpen] = useState(true); + const [versionsOpen, setVersionsOpen] = useState(true); + const [formatOpen, setFormatOpen] = useState(true); + const monacoRef = useRef(null); + + const [compareFiles, setCompareFiles] = useState | null>(null); + const [origContent, setOrigContent] = useState(null); + const [origLoading, setOrigLoading] = useState(false); + + const [prettify, setPrettify] = useState(false); + const [formattedContent, setFormattedContent] = useState(null); + const [formattedOrig, setFormattedOrig] = useState(null); + + const sorted = useMemo(() => + versions.filter(v => !isNoisyPrerelease(v)).sort(compareSemverDesc) + , [versions]); + + const tagForVersion = (v: string) => Object.entries(distTags).find(([, ver]) => ver === v)?.[0]; + + const exitCompare = useCallback(() => { + onCompareChange(null); + }, [onCompareChange]); + + useEffect(() => { + if (!compareVersion || !name) { + setCompareFiles(null); + return; + } + const abortCtrl = new AbortController(); + fetch(`https://data.jsdelivr.com/v1/package/npm/${name}@${compareVersion}/flat`, {signal: abortCtrl.signal}) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (data?.files) setCompareFiles(data.files); + }) + .catch(err => { + if (err.name !== `AbortError`) setCompareFiles(null); + }); + return () => abortCtrl.abort(); + }, [compareVersion, name]); + + const changeMap = useMemo(() => { + if (!compareVersion || !files || !compareFiles) return null; + const map = new Map(); + const oldByPath = new Map(compareFiles.map(f => [f.name.replace(/^\//, ``), f.hash])); + const newByPath = new Map(files.map(f => [f.name.replace(/^\//, ``), f.hash])); + + for (const [path, hash] of newByPath) { + const oldHash = oldByPath.get(path); + if (!oldHash) map.set(path, `added`); + else if (oldHash !== hash) map.set(path, `modified`); + } + for (const [path] of oldByPath) + if (!newByPath.has(path)) map.set(path, `removed`); + + return map; + }, [compareVersion, files, compareFiles]); + + const displayFiles = useMemo(() => { + if (!files) return null; + if (!changeMap) return files; + const removedFiles: Array = compareFiles + ? compareFiles.filter(f => changeMap.get(f.name.replace(/^\//, ``)) === `removed`) + : []; + const currentChanged = files.filter(f => changeMap.has(f.name.replace(/^\//, ``))); + return [...currentChanged, ...removedFiles]; + }, [files, compareFiles, changeMap]); + + const tree = displayFiles ? buildFileTree(displayFiles, name) : null; + + useEffect(() => { + if (!selectedFile || !name || !version) { + setFileContent(null); + setFileError(null); + return; + } + const changeType = changeMap?.get(selectedFile); + if (changeType === `removed`) { + setFileContent(``); + setFileLoading(false); + setFileError(null); + return; + } + const abortCtrl = new AbortController(); + setFileLoading(true); + setFileContent(null); + setFileError(null); + fetch(`https://cdn.jsdelivr.net/npm/${name}@${version}/${selectedFile}`, {signal: abortCtrl.signal}) + .then(r => { + if (!r.ok) throw new Error(`Failed to load file`); + return r.text(); + }) + .then(text => setFileContent(text)) + .catch(err => { + if (err.name !== `AbortError`) { + setFileContent(null); + setFileError(err.message); + } + }) + .finally(() => setFileLoading(false)); + return () => abortCtrl.abort(); + }, [selectedFile, name, version, changeMap]); + + useEffect(() => { + if (!compareVersion || !selectedFile || !name) { + setOrigContent(null); + return; + } + const changeType = changeMap?.get(selectedFile); + if (changeType === `added`) { + setOrigContent(``); + return; + } + const abortCtrl = new AbortController(); + setOrigLoading(true); + setOrigContent(null); + fetch(`https://cdn.jsdelivr.net/npm/${name}@${compareVersion}/${selectedFile}`, {signal: abortCtrl.signal}) + .then(r => { + if (!r.ok) throw new Error(`fetch failed`); + return r.text(); + }) + .then(text => setOrigContent(text)) + .catch(err => { + if (err.name !== `AbortError`) setOrigContent(``); + }) + .finally(() => setOrigLoading(false)); + return () => abortCtrl.abort(); + }, [compareVersion, selectedFile, name, changeMap]); + + useEffect(() => { + if (!prettify || fileContent == null || !selectedFile) { + setFormattedContent(null); + return; + } + let cancelled = false; + formatWithPrettier(fileContent, selectedFile) + .then(result => { if (!cancelled) setFormattedContent(result); }) + .catch(() => { if (!cancelled) setFormattedContent(null); }); + return () => { cancelled = true; }; + }, [prettify, fileContent, selectedFile]); + + useEffect(() => { + if (!prettify || origContent == null || !selectedFile) { + setFormattedOrig(null); + return; + } + let cancelled = false; + formatWithPrettier(origContent, selectedFile) + .then(result => { if (!cancelled) setFormattedOrig(result); }) + .catch(() => { if (!cancelled) setFormattedOrig(null); }); + return () => { cancelled = true; }; + }, [prettify, origContent, selectedFile]); + + const prevVersionRef = useRef(version); + useEffect(() => { + if (prevVersionRef.current && prevVersionRef.current !== version) { + setFileContent(null); + setCompareFiles(null); + setOrigContent(null); + setFileLoading(false); + setOrigLoading(false); + setFileError(null); + } + prevVersionRef.current = version; + }, [version]); + + const handleMonacoMount = useCallback((_editor: any, monaco: any) => { + monacoRef.current = monaco; + setupMonacoTheme(monaco); + setMonacoReady(true); + }, []); + + const [isDark, setIsDark] = useState(() => typeof document !== `undefined` && document.documentElement.getAttribute(`data-theme`) !== `light`); + useEffect(() => { + const handler = (e: Event) => setIsDark((e as CustomEvent).detail !== `light`); + window.addEventListener(`themechange`, handler); + return () => window.removeEventListener(`themechange`, handler); + }, []); + const editorTheme = monacoReady ? (isDark ? `pkg-dark` : `pkg-light`) : (isDark ? `vs-dark` : `vs`); + + const inCompare = !!compareVersion; + + const awaitingFormat = prettify && selectedFile != null && canPrettify(selectedFile); + const contentFormatted = !awaitingFormat || formattedContent != null; + const origFormatted = !awaitingFormat || !inCompare || formattedOrig != null; + + const isLoading = fileLoading || (inCompare && origLoading) || (awaitingFormat && (!contentFormatted || !origFormatted)); + + const displayContent = awaitingFormat && formattedContent != null ? formattedContent : fileContent; + const displayOrig = awaitingFormat && formattedOrig != null ? formattedOrig : origContent; + + const topBar = ( +
+ + +
+ {name} + {inCompare ? ( + + {version} + + {compareVersion} + + + ) : ( + {version} + )} +
+
+ ); + + const changedCount = changeMap?.size ?? 0; + const filesLoading = !files; + + return ( +
+ {topBar} + +
+ {/* Sidebar */} +
+
setExplorerOpen(!explorerOpen)}> + + {inCompare ? `Changed files` : `Explorer`} + + {filesLoading ? `…` : inCompare ? (changeMap ? changedCount : `…`) : `${files.length} files`} + +
+ {explorerOpen && ( +
+ {filesLoading ? ( +
+
+
+ ) : inCompare && !compareFiles ? ( +
+
+
+ ) : tree && tree.children?.map(child => ( + + ))} +
+ )} + +
setVersionsOpen(!versionsOpen)}> + + Versions + {versions.length} +
+ {versionsOpen && ( +
+ {sorted.map(v => { + const tag = tagForVersion(v); + const isCompareTarget = v === compareVersion; + return ( +
onVersionChange(v)} + > + {v} + {tag && ( + {tag} + )} + + {v !== version && ( + + )} + {time[v] ? timeAgo(time[v]) : ``} +
+ ); + })} +
+ )} + +
setFormatOpen(!formatOpen)}> + + Format +
+ {formatOpen && ( +
+
setPrettify(!prettify)} + > + + {prettify && `\u2713`} + + Prettify + {prettify && selectedFile && !canPrettify(selectedFile) && ( + unsupported + )} +
+
+ )} +
+ + {/* Editor */} +
+ {selectedFile ? ( + <> +
+ + {selectedFile} + {changeMap?.get(selectedFile) && ( + {changeMap.get(selectedFile)} + )} + {!inCompare && files && (() => { + const f = files.find(f => f.name === selectedFile || f.name === `/${selectedFile}`); + return f ? {formatBytes(f.size)} : null; + })()} +
+
+ {isLoading ? ( +
+
+ Loading… +
+ ) : fileError ? ( +
+ Could not load file + {fileError} +
+ ) : inCompare && displayOrig != null && displayContent != null ? ( + Loading diff editor…
}> + + + ) : displayContent != null ? ( + Loading editor…
}> + + + ) : ( +
+ Could not load file +
+ )} +
+ + ) : ( +
+ {inCompare ? ( + <> + + Comparing {version}{compareVersion} + {changeMap ? `${changedCount} file${changedCount !== 1 ? `s` : ``} changed` : `Loading…`} + + ) : ( + <> + + Select a file to view its contents + {name}@{version} + + )} +
+ )} +
+
+
+ ); +} diff --git a/website/src/components/package/InstallCard.tsx b/website/src/components/package/InstallCard.tsx new file mode 100644 index 00000000..6fc2c243 --- /dev/null +++ b/website/src/components/package/InstallCard.tsx @@ -0,0 +1,66 @@ +import {useState, useCallback} from 'react'; + +import type {PmTab} from './types'; +import {useIcons} from './contexts'; +import {OctIcon, BrandIcon} from './icons'; +import {PM_COMMANDS} from './utils'; + +export function InstallCard({name, pmTab, onPmTabChange}: { + name: string; pmTab: PmTab; onPmTabChange: (t: PmTab) => void; +}) { + const {oct, brand} = useIcons(); + + const [copied, setCopied] = useState(false); + + const cmd = PM_COMMANDS[pmTab]; + const fullCmd = `${cmd.verb} ${cmd.rest} ${name}`; + + const handleCopy = useCallback(() => { + navigator.clipboard?.writeText(fullCmd).catch(() => {}); + setCopied(true); + setTimeout(() => setCopied(false), 1400); + }, [fullCmd]); + + return ( +
+
+ // install + +
+ {([`yarn`, `npm`, `pnpm`, `bun`] as Array).map(pm => ( + + ))} +
+
+ +
+ $ + + {cmd.verb}{` `}{cmd.rest} {name} + + +
+
+ ); +} diff --git a/website/src/components/package/KeywordsCard.tsx b/website/src/components/package/KeywordsCard.tsx new file mode 100644 index 00000000..c9ca72f0 --- /dev/null +++ b/website/src/components/package/KeywordsCard.tsx @@ -0,0 +1,22 @@ +export function KeywordsCard({keywords}: {keywords: Array}) { + if (keywords.length === 0) return null; + + return ( +
+
+ Keywords + {keywords.length} +
+
+ {keywords.map(kw => ( + + {kw} + + ))} +
+
+ ); +} diff --git a/website/src/components/package/LeftRail.tsx b/website/src/components/package/LeftRail.tsx new file mode 100644 index 00000000..1e3f9ee8 --- /dev/null +++ b/website/src/components/package/LeftRail.tsx @@ -0,0 +1,50 @@ +import {useIcons} from './contexts'; +import {OctIcon, BrandIcon} from './icons'; +import {NavLink, NavLinkExternal} from './NavLink'; + +export function LeftRail({ + name, version, distTags, homepage, repoUrl, + activeNav, onNavClick, versionCount, fileCount, +}: { + name: string; version: string; + distTags: Record; + homepage: string | undefined; + repoUrl: string | null; + activeNav: string; + onNavClick: (id: string) => void; + versionCount: number; + fileCount: number; +}) { + const {oct, brand} = useIcons(); + return ( + + ); +} diff --git a/website/src/components/package/MaintainersCard.tsx b/website/src/components/package/MaintainersCard.tsx new file mode 100644 index 00000000..78894739 --- /dev/null +++ b/website/src/components/package/MaintainersCard.tsx @@ -0,0 +1,37 @@ +export function MaintainersCard({maintainers}: {maintainers: Array<{name: string, email: string}>}) { + if (maintainers.length === 0) return null; + + const hueForName = (name: string) => { + let hash = 0; + + for (let i = 0; i < name.length; i++) + hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0; + + return Math.abs(hash) % 360; + }; + + return ( +
+
+ Maintainers + {maintainers.length} +
+ {maintainers.map((m, i) => { + const h = hueForName(m.name); + const initials = m.name.slice(0, 2).toLowerCase(); + return ( +
+
+ {initials} +
+ {m.name} + {i === 0 && owner} +
+ ); + })} +
+ ); +} diff --git a/website/src/components/package/NavLink.tsx b/website/src/components/package/NavLink.tsx new file mode 100644 index 00000000..699b29a3 --- /dev/null +++ b/website/src/components/package/NavLink.tsx @@ -0,0 +1,34 @@ +export function NavLink({icon, label, id, active, onClick, badge}: { + icon: React.ReactNode; label: string; id: string; active: boolean; + onClick: (id: string) => void; badge?: string; +}) { + return ( + + ); +} + +export function NavLinkExternal({icon, label, href}: {icon: React.ReactNode, label: string, href: string}) { + return ( + + {icon} + {label} + + + ); +} diff --git a/website/src/components/package/PackagePage.tsx b/website/src/components/package/PackagePage.tsx new file mode 100644 index 00000000..59d6e484 --- /dev/null +++ b/website/src/components/package/PackagePage.tsx @@ -0,0 +1,20 @@ +import {RouterProvider} from '@tanstack/react-router'; +import {useMemo} from 'react'; + +import {PackageCtx} from './contexts'; +import {PackagePageInner} from './PackagePageInner'; +import {splatRoute, getRouter} from './router'; + +import type {BrandIcons, Octicons} from './types'; + +splatRoute.update({component: PackagePageInner}); + +export default function PackagePage({brandIcons, octicons}: {brandIcons: BrandIcons, octicons: Octicons}) { + const ctx = useMemo(() => ({brandIcons, octicons}), [brandIcons, octicons]); + const router = useMemo(() => getRouter(), []); + return ( + + + + ); +} diff --git a/website/src/components/package/PackagePageInner.tsx b/website/src/components/package/PackagePageInner.tsx new file mode 100644 index 00000000..f84b0c03 --- /dev/null +++ b/website/src/components/package/PackagePageInner.tsx @@ -0,0 +1,344 @@ +import {useState, useEffect, useCallback, useMemo, useContext} from 'react'; + +import {PackageCtx, IconCtx} from './contexts'; +import {LeftRail} from './LeftRail'; +import {VersionSelector} from './VersionSelector'; +import {TabBar} from './TabBar'; +import {StatGrid} from './StatGrid'; +import {InstallCard} from './InstallCard'; +import {ReadmePanel} from './ReadmePanel'; +import {VersionsTimeline} from './VersionsTimeline'; +import {FilesExplorer} from './FilesExplorer'; +import {AuditPanel} from './AuditPanel'; +import {DownloadsCard} from './DownloadsCard'; +import {VersionsCard} from './VersionsCard'; +import {DependenciesCard} from './DependenciesCard'; +import {MaintainersCard} from './MaintainersCard'; +import {KeywordsCard} from './KeywordsCard'; +import {OctIcon} from './icons'; +import {parseSplat, packagePath, getLicense, getRepoUrl} from './utils'; +import {usePackageNavigate, splatRoute} from './router'; + +import type {RegistryData, FileEntry, DownloadDay, Tab, PmTab} from './types'; + +function LoadingSpinner() { + return ( +
+
+
Loading package...
+
+ ); +} + +function ErrorDisplay({message}: {message: string}) { + return ( +
+
!
+
Package not found
+
{message}
+
+ ); +} + +export function PackagePageInner() { + const {brandIcons, octicons} = useContext(PackageCtx); + const nav = usePackageNavigate(); + const {_splat: splat} = splatRoute.useParams(); + const parsed = useMemo(() => parseSplat(splat ?? ``), [splat]); + + const name = parsed.name; + const urlVersion = parsed.version; + const activeTab: Tab = parsed.tab ?? (parsed.compareVersion ? `files` : `readme`); + const urlFile = parsed.filePath ?? null; + const urlCompare = parsed.compareVersion ?? null; + const activeNav = activeTab === `versions` ? `versions` : activeTab === `files` ? `files` : `info`; + + const [registry, setRegistry] = useState(null); + const [files, setFiles] = useState | null>(null); + const [readme, setReadme] = useState(``); + const [downloads, setDownloads] = useState | null>(null); + const [pmTab, setPmTab] = useState(`yarn`); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const selectedVersion = urlVersion + || (registry ? registry[`dist-tags`]?.latest || Object.keys(registry.versions).pop() || `` : ``); + + useEffect(() => { + if (!name) { + setLoading(false); return; + } + + const abortCtrl = new AbortController(); + setLoading(true); + setError(null); + + fetch(`https://registry.yarnpkg.com/${name}`, {signal: abortCtrl.signal}) + .then(r => { + if (!r.ok) throw new Error(r.status === 404 ? `Package "${name}" not found` : `Failed to fetch package`); + return r.json(); + }) + .then((data: RegistryData) => { + setRegistry(data); + if (data.readme) setReadme(data.readme); + document.title = `${data.name} — Yarn`; + }) + .catch(err => { + if (err.name !== `AbortError`) setError(err.message); + }) + .finally(() => setLoading(false)); + return () => abortCtrl.abort(); + }, [name]); + + useEffect(() => { + if (!selectedVersion || !name) return; + + const abortCtrl = new AbortController(); + setFiles(null); + + fetch(`https://data.jsdelivr.com/v1/package/npm/${name}@${selectedVersion}/flat`, {signal: abortCtrl.signal}) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (data?.files) setFiles(data.files); + }) + .catch(err => { + if (err.name !== `AbortError`) setFiles(null); + }); + + fetch(`https://cdn.jsdelivr.net/npm/${name}@${selectedVersion}/README.md`, {signal: abortCtrl.signal}) + .then(r => { + if (!r.ok) throw new Error(`no readme`); + return r.text(); + }) + .then(text => setReadme(text)) + .catch(err => { + if (err.name === `AbortError`) return; + fetch(`https://cdn.jsdelivr.net/npm/${name}@${selectedVersion}/readme.md`, {signal: abortCtrl.signal}) + .then(r => r.ok ? r.text() : ``) + .then(text => { + if (text) setReadme(text); + }) + .catch(() => {}); + }); + + return () => abortCtrl.abort(); + }, [name, selectedVersion]); + + useEffect(() => { + if (!name) return; + const abortCtrl = new AbortController(); + fetch(`https://api.npmjs.org/downloads/range/last-month/${name}`, {signal: abortCtrl.signal}) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (data?.downloads) setDownloads(data.downloads); + }) + .catch(err => { + if (err.name !== `AbortError`) setDownloads(null); + }); + return () => abortCtrl.abort(); + }, [name]); + + const handleVersionChange = useCallback((v: string) => { + const filePath = activeTab === `files` ? urlFile : undefined; + nav(packagePath(name, v, activeTab, filePath ?? undefined)); + }, [name, activeTab, nav, urlFile]); + + const handleNavClick = useCallback((id: string) => { + const tab: Tab = id === `versions` ? `versions` : id === `files` ? `files` : `readme`; + const cmp = tab === `files` ? urlCompare ?? undefined : undefined; + nav(packagePath(name, selectedVersion, tab, undefined, cmp)); + }, [name, selectedVersion, nav, urlCompare]); + + const handleTabChange = useCallback((t: Tab) => { + const cmp = t === `files` ? urlCompare ?? undefined : undefined; + nav(packagePath(name, selectedVersion, t, undefined, cmp)); + }, [name, selectedVersion, nav, urlCompare]); + + const handleFileChange = useCallback((filePath: string | null) => { + nav(packagePath(name, selectedVersion, `files`, filePath ?? undefined, urlCompare ?? undefined)); + }, [name, selectedVersion, nav, urlCompare]); + + const handleCompareChange = useCallback((cmpVersion: string | null) => { + nav(packagePath(name, selectedVersion, `files`, urlFile ?? undefined, cmpVersion ?? undefined)); + }, [name, selectedVersion, nav, urlFile]); + + useEffect(() => { + document.body.style.overflow = activeTab === `files` ? `hidden` : ``; + return () => { + document.body.style.overflow = ``; + }; + }, [activeTab]); + + const iconCtxValue = useMemo(() => ({brand: brandIcons, oct: octicons}), [brandIcons, octicons]); + + if (!name) + return ; + + if (loading) return ; + if (error || !registry) return ; + + const allVersions = Object.keys(registry.versions); + const distTags = registry[`dist-tags`] || {}; + const versionData = registry.versions[selectedVersion]; + const repoUrl = getRepoUrl(registry.repository); + const license = getLicense(registry.license); + const deps = versionData?.dependencies || {}; + const peerDeps = versionData?.peerDependencies || {}; + const keywords = registry.keywords || []; + const maintainers = registry.maintainers || []; + + if (activeTab === `files`) { + return ( + +
+ +
+
+ ); + } + + return ( + +
+ {/* Breadcrumb */} +
+ packages + / + {name} +
+ +
+ {/* Left Rail */} + + + {/* Main Column */} +
+ {/* Header */} +
+
+

+ {name} +

+ + {registry.description && ( +

+ {registry.description} +

+ )} + +
+ + + {license} + + + {repoUrl && ( + + + {repoUrl.replace(/^https?:\/\/(www\.)?github\.com\//, ``)} + + )} + + {registry.homepage && ( + + + homepage + + )} +
+
+ + +
+ + + + + + {activeTab === `readme` && ( + <> + + + + + )} + + {activeTab === `versions` && ( + + )} + + {activeTab === `audit` && ( + + )} +
+ + {/* Right Rail */} + +
+
+
+ ); +} diff --git a/website/src/components/package/ReadmePanel.tsx b/website/src/components/package/ReadmePanel.tsx new file mode 100644 index 00000000..4b4c0dd3 --- /dev/null +++ b/website/src/components/package/ReadmePanel.tsx @@ -0,0 +1,18 @@ +import {useIcons} from './contexts'; +import {OctIcon} from './icons'; +import {renderMarkdown} from './utils'; + +export function ReadmePanel({readme, name}: {readme: string, name: string}) { + const {oct} = useIcons(); + return ( +
+
+ + + + README.md +
+
+
+ ); +} diff --git a/website/src/components/package/StatGrid.tsx b/website/src/components/package/StatGrid.tsx new file mode 100644 index 00000000..8a2eb329 --- /dev/null +++ b/website/src/components/package/StatGrid.tsx @@ -0,0 +1,42 @@ +import type {FileEntry, VersionManifest} from './types'; +import {formatBytes, formatDate} from './utils'; + +function StatCell({label, value, unit}: {label: string, value: string, unit?: string}) { + const parts = value.match(/^([\d.,]+)\s*(.*)$/); + + const num = parts ? parts[1] : value; + const suffix = parts ? parts[2] : unit; + + return ( +
+
{label}
+ +
+ {num} + {suffix && {suffix}} +
+
+ ); +} + +export function StatGrid({versionData, time, version, files}: { + versionData: VersionManifest | undefined; + time: Record; + version: string; + files: Array | null; +}) { + const unpackedSize = versionData?.dist?.unpackedSize; + const totalSize = files ? files.reduce((s, f) => s + f.size, 0) : unpackedSize; + const fileCount = files?.length ?? versionData?.dist?.fileCount; + const depCount = versionData?.dependencies ? Object.keys(versionData.dependencies).length : 0; + const pubDate = time[version]; + + return ( +
+ + + + +
+ ); +} diff --git a/website/src/components/package/TabBar.tsx b/website/src/components/package/TabBar.tsx new file mode 100644 index 00000000..a883f678 --- /dev/null +++ b/website/src/components/package/TabBar.tsx @@ -0,0 +1,36 @@ +import type {Tab} from './types'; + +export function TabBar({active, onTabChange, readmeLabel, versionCount, fileCount}: { + active: Tab; onTabChange: (t: Tab) => void; + readmeLabel: string; versionCount: number; fileCount: number; +}) { + const tabs: Array<{key: Tab, label: string, num: string}> = [ + {key: `readme`, label: `README`, num: readmeLabel}, + {key: `versions`, label: `Versions`, num: String(versionCount)}, + {key: `files`, label: `Files`, num: String(fileCount)}, + {key: `audit`, label: `Audit`, num: `—`}, + ]; + + return ( +
+ {tabs.map(t => ( + + ))} +
+ ); +} diff --git a/website/src/components/package/VersionSelector.tsx b/website/src/components/package/VersionSelector.tsx new file mode 100644 index 00000000..1dacf31b --- /dev/null +++ b/website/src/components/package/VersionSelector.tsx @@ -0,0 +1,83 @@ +import {useState, useEffect, useRef} from 'react'; + +import {useIcons} from './contexts'; +import {OctIcon} from './icons'; +import {formatDateShort, timeAgo, compareSemverDesc, isNoisyPrerelease} from './utils'; + +export function VersionSelector({ + version, distTags, versions, time, onVersionChange, +}: { + version: string; + distTags: Record; + versions: Array; + time: Record; + onVersionChange: (v: string) => void; +}) { + const {oct} = useIcons(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!open) return; + const handler = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); + }; + document.addEventListener(`mousedown`, handler); + return () => document.removeEventListener(`mousedown`, handler); + }, [open]); + + const isLatest = distTags.latest === version; + const age = time[version] ? timeAgo(time[version]) : ``; + + const tagForVersion = (v: string) => Object.entries(distTags).find(([, ver]) => ver === v)?.[0]; + + return ( +
+ version + + + + {open && ( +
+
+ {versions.filter(v => !isNoisyPrerelease(v)).sort(compareSemverDesc).slice(0, 50).map(v => { + const tag = tagForVersion(v); + return ( + + ); + })} +
+
+ )} +
+ ); +} diff --git a/website/src/components/package/VersionsCard.tsx b/website/src/components/package/VersionsCard.tsx new file mode 100644 index 00000000..117f7bc4 --- /dev/null +++ b/website/src/components/package/VersionsCard.tsx @@ -0,0 +1,54 @@ +import {useState} from 'react'; + +import {formatDateShort, compareSemverDesc, isNoisyPrerelease} from './utils'; + +export function VersionsCard({versions, distTags, time, onVersionChange}: { + versions: Array; distTags: Record; + time: Record; onVersionChange: (v: string) => void; +}) { + const [showAll, setShowAll] = useState(false); + + const sorted = versions.filter(v => !isNoisyPrerelease(v)).sort(compareSemverDesc); + const displayed = showAll ? sorted : sorted.slice(0, 6); + + const tagForVersion = (v: string) => Object.entries(distTags).find(([, ver]) => ver === v)?.[0]; + + return ( +
+
+ Versions + {versions.length} total +
+ {displayed.map(v => { + const tag = tagForVersion(v); + return ( +
onVersionChange(v)} + className={`grid grid-cols-[1fr_auto] items-center py-2 mono text-[12.5px] text-[var(--fg-dim)] border-b border-dashed border-[var(--line)] last:border-b-0 cursor-pointer transition-colors hover:text-[var(--fg)]`} + > +
+ {v} + {tag && ( + {tag} + )} +
+ {time[v] ? formatDateShort(time[v]) : ``} +
+ ); + })} + {!showAll && sorted.length > 6 && ( + + )} +
+ ); +} diff --git a/website/src/components/package/VersionsTimeline.tsx b/website/src/components/package/VersionsTimeline.tsx new file mode 100644 index 00000000..d281b952 --- /dev/null +++ b/website/src/components/package/VersionsTimeline.tsx @@ -0,0 +1,86 @@ +import {useState} from 'react'; + +import {useIcons} from './contexts'; +import {OctIcon} from './icons'; +import {formatDate, versionLabel, compareSemverDesc, isNoisyPrerelease} from './utils'; + +export function VersionsTimeline({versions, distTags, time}: { + versions: Array; distTags: Record; time: Record; +}) { + const {oct} = useIcons(); + + const [showAll, setShowAll] = useState(false); + const [includeNoisy, setIncludeNoisy] = useState(false); + + const sorted = (includeNoisy ? versions.slice() : versions.filter(v => !isNoisyPrerelease(v))).sort(compareSemverDesc); + const displayed = showAll ? sorted : sorted.slice(0, 15); + + return ( +
+
+ + + + Release timeline +
setIncludeNoisy(!includeNoisy)} + > + + {includeNoisy && `\u2713`} + + Show all versions +
+
+
+ {displayed.map((v, i) => { + const prev = i < displayed.length - 1 ? displayed[i + 1] : null; + const label = versionLabel(v, prev); + const isMajor = label === `major` || label === `initial`; + const tag = Object.entries(distTags).find(([, ver]) => ver === v)?.[0]; + + return ( +
+
+ {time[v] ? formatDate(time[v]) : ``} +
+
+
+ {i < displayed.length - 1 &&
} +
+
+
+ {v} + {tag && ( + {tag} + )} +
+
+ {label} +
+
+
+ ); + })} + {!showAll && sorted.length > 15 && ( + + )} +
+
+ ); +} diff --git a/website/src/components/package/contexts.ts b/website/src/components/package/contexts.ts new file mode 100644 index 00000000..bc94caef --- /dev/null +++ b/website/src/components/package/contexts.ts @@ -0,0 +1,11 @@ +import {createContext, useContext} from 'react'; + +import type {BrandIcons, Octicons} from './types'; + +export const IconCtx = createContext<{brand: BrandIcons, oct: Octicons}>(null!); + +export function useIcons() { + return useContext(IconCtx); +} + +export const PackageCtx = createContext<{brandIcons: BrandIcons, octicons: Octicons}>(null!); diff --git a/website/src/components/package/icons.tsx b/website/src/components/package/icons.tsx new file mode 100644 index 00000000..01a5b7d0 --- /dev/null +++ b/website/src/components/package/icons.tsx @@ -0,0 +1,29 @@ +import type {IconData} from './types'; + +export function OctIcon({icon, size = 16, className, style}: {icon: IconData, size?: number, className?: string, style?: React.CSSProperties}) { + return ( + + ); +} + +export function BrandIcon({icon, size = 14}: {icon: IconData, size?: number}) { + return ( + + ); +} diff --git a/website/src/components/package/router.ts b/website/src/components/package/router.ts new file mode 100644 index 00000000..8d37445d --- /dev/null +++ b/website/src/components/package/router.ts @@ -0,0 +1,34 @@ +import {createRouter, createRoute, createRootRoute, useRouter} from '@tanstack/react-router'; +import {useCallback} from 'react'; + +const rootRoute = createRootRoute(); + +export const splatRoute = createRoute({ + getParentRoute: () => rootRoute, + path: `/package/$`, +}); + +export const routeTree = rootRoute.addChildren([splatRoute]); + +let routerInstance: ReturnType | null = null; + +export function getRouter() { + if (!routerInstance) + routerInstance = createRouter({routeTree}); + + return routerInstance; +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType>; + } +} + +export function usePackageNavigate() { + const router = useRouter(); + return useCallback((path: string) => { + router.history.push(path); + router.load(); + }, [router]); +} diff --git a/website/src/components/package/types.ts b/website/src/components/package/types.ts new file mode 100644 index 00000000..cc89c2dd --- /dev/null +++ b/website/src/components/package/types.ts @@ -0,0 +1,76 @@ +export interface VersionManifest { + name: string; + version: string; + description?: string; + dependencies?: Record; + peerDependencies?: Record; + devDependencies?: Record; + dist: { + tarball: string; + shasum: string; + unpackedSize?: number; + fileCount?: number; + integrity?: string; + }; +} + +export interface RegistryData { + name: string; + description: string; + 'dist-tags': Record; + versions: Record; + time: Record; + maintainers: Array<{name: string, email: string}>; + keywords?: Array; + repository?: {type: string, url: string} | string; + homepage?: string; + license?: string | {type: string, url?: string}; + readme?: string; + bugs?: {url: string} | string; +} + +export interface FileEntry { + name: string; + hash: string; + size: number; +} + +export interface TreeNode { + name: string; + path: string; + size?: number; + children?: Array; +} + +export interface DownloadDay { + downloads: number; + day: string; +} + +export type Tab = `readme` | `versions` | `files` | `audit`; +export type PmTab = `yarn` | `npm` | `pnpm` | `bun`; + +export interface IconData { + body: string; + width: number; + height: number; +} + +export interface BrandIcons { + github: IconData; + npm: IconData; + yarn: IconData; + pnpm: IconData; + bun: IconData; +} + +export type OcticonName = `package` | `info` | `globe` | `file-directory` | `file-directory-fill` | `file-directory-open-fill` | `versions` | `file` | `file-code` | `copy` | `chevron-down` | `chevron-right` | `shield` | `law` | `repo` | `home` | `diff` | `link-external` | `x`; +export type Octicons = Record; + +export interface ParsedUrl { + name: string; + version?: string; + compareVersion?: string; + tab?: Tab; + filePath?: string; +} diff --git a/website/src/components/package/utils.ts b/website/src/components/package/utils.ts new file mode 100644 index 00000000..c983ea53 --- /dev/null +++ b/website/src/components/package/utils.ts @@ -0,0 +1,492 @@ +import type {FileEntry, PmTab, ParsedUrl, RegistryData, Tab, TreeNode} from './types'; + +// ── URL Parsing ── + +const TAB_NAMES = new Set([`versions`, `files`, `file`, `audit`]); + +export function parseSplat(splat: string): ParsedUrl { + const parts = splat.split(`/`).map(decodeURIComponent).filter(Boolean); + if (!parts.length) + return {name: ``}; + + let idx = 0; + + let name: string; + if (parts[0].startsWith(`@`) && parts.length >= 2) { + name = `${parts[0]}/${parts[1]}`; + idx = 2; + } else { + name = parts[0]; + idx = 1; + } + + let version: string | undefined; + let compareVersion: string | undefined; + let tab: Tab | undefined; + let filePath: string | undefined; + + if (idx < parts.length && !TAB_NAMES.has(parts[idx])) { + const segment = parts[idx]; + const dotdot = segment.indexOf(`..`); + if (dotdot !== -1) { + version = segment.slice(0, dotdot); + compareVersion = segment.slice(dotdot + 2); + } else { + version = segment; + } + idx++; + } + + if (idx < parts.length && TAB_NAMES.has(parts[idx])) { + const segment = parts[idx]; + idx++; + if (segment === `file`) { + tab = `files`; + filePath = parts.slice(idx).join(`/`) || undefined; + } else { + tab = segment as Tab; + } + } + + return {name, version, compareVersion, tab, filePath}; +} + +export function packagePath(name: string, version?: string, tab?: Tab, filePath?: string, compareVersion?: string): string { + let p = `/package/${name}`; + if (version) { + p += `/${version}`; + if (compareVersion) p += `..${compareVersion}`; + } + if (tab && tab !== `readme`) { + if (tab === `files` && filePath) { + p += `/file/${filePath}`; + } else { + p += `/${tab}`; + } + } + return p; +} + +// ── Formatting Helpers ── + +export function formatNumber(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n >= 1_000) return `${(n / 1_000).toFixed(n >= 10_000 ? 0 : 1)}k`; + return n.toLocaleString(); +} + +export function formatNumberFull(n: number): string { + return n.toLocaleString(); +} + +export function formatBytes(bytes: number): string { + if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`; + if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)} kB`; + return `${bytes} B`; +} + +export function formatDate(dateStr: string): string { + const d = new Date(dateStr); + return d.toLocaleDateString(`en-US`, {month: `short`, day: `numeric`, year: `numeric`}); +} + +export function formatDateShort(dateStr: string): string { + const d = new Date(dateStr); + const month = d.toLocaleDateString(`en-US`, {month: `short`}); + const day = d.getDate(); + const year = String(d.getFullYear()).slice(2); + return `${month} ${day} '${year}`; +} + +export function timeAgo(dateStr: string): string { + const diff = Date.now() - new Date(dateStr).getTime(); + + const days = Math.floor(diff / 86400000); + if (days < 1) return `today`; + if (days === 1) return `1d`; + if (days < 30) return `${days}d`; + + const months = Math.floor(days / 30); + if (months < 12) return `${months}mo`; + + const years = Math.floor(months / 12); + return `${years}y`; +} + +export function getLicense(license: RegistryData[`license`]): string { + if (!license) return `Unknown`; + if (typeof license === `string`) return license; + return license.type || `Unknown`; +} + +export function getRepoUrl(repo: RegistryData[`repository`]): string | null { + if (!repo) + return null; + + let url = typeof repo === `string` ? repo : repo.url; + if (!url) + return null; + + url = url + .replace(/^git\+/, ``) + .replace(/\.git$/, ``) + .replace(/^git:\/\//, `https://`) + .replace(/^ssh:\/\/git@/, `https://`); + + return url; +} + +export function getBugsUrl(bugs: RegistryData[`bugs`]): string | null { + if (!bugs) return null; + return typeof bugs === `string` ? bugs : bugs.url || null; +} + +// ── Markdown Renderer ── + +function escapeHtml(str: string): string { + return str + .replace(/&/g, `&`) + .replace(//g, `>`) + .replace(/"/g, `"`); +} + +function isSafeUrl(url: string): boolean { + const decoded = url.replace(/&/g, `&`); + if (decoded.startsWith(`/`) || decoded.startsWith(`#`)) return true; + try { + const parsed = new URL(decoded); + return [`https:`, `http:`, `mailto:`].includes(parsed.protocol); + } catch { + return false; + } +} + +export function renderMarkdown(md: string): string { + if (!md) + return ``; + + const codeBlocks: Array = []; + + let html = md.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => { + codeBlocks.push(`
${escapeHtml(code.trimEnd())}
`); + return `\x00CB${codeBlocks.length - 1}\x00`; + }); + + html = escapeHtml(html); + + html = html + .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => { + const safeSrc = isSafeUrl(src) ? src : ``; + return safeSrc ? `${alt}` : alt; + }); + + html = html + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => { + const safe = isSafeUrl(url) ? url : `#`; + return `${text}`; + }); + + html = html + .replace(/^######\s+(.+)$/gm, `
$1
`) + .replace(/^#####\s+(.+)$/gm, `
$1
`) + .replace(/^####\s+(.+)$/gm, `

$1

`) + .replace(/^###\s+(.+)$/gm, `

$1

`) + .replace(/^##\s+(.+)$/gm, `

$1

`) + .replace(/^#\s+(.+)$/gm, `

$1

`); + + html = html + .replace(/\*\*\*(.+?)\*\*\*/g, `$1`) + .replace(/\*\*(.+?)\*\*/g, `$1`) + .replace(/(?$1`); + + html = html + .replace(/`([^`]+)`/g, `$1`); + + html = html + .replace(/^>\s+(.+)$/gm, `

$1

`); + + html = html + .replace(/^---+$/gm, `
`); + + html = html + .replace(/^[*-]\s+(.+)$/gm, `
  • $1
  • `) + .replace(/((?:
  • [\s\S]*?<\/li>\s*)+)/g, `
      $1
    `); + + html = html + .replace(/^\d+\.\s+(.+)$/gm, `
  • $1
  • `); + + const lines = html.split(`\n`); + const result: Array = []; + let inBlock = false; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) { + if (inBlock) { + result.push(``); inBlock = false; + } + continue; + } + if (trimmed.startsWith(`<`) || trimmed.startsWith(`\x00`)) { + result.push(trimmed); + inBlock = false; + } else { + result.push(`

    ${trimmed}

    `); + inBlock = true; + } + } + html = result.join(`\n`); + + html = html.replace(/\x00CB(\d+)\x00/g, (_, idx) => codeBlocks[parseInt(idx)]); + + return html; +} + +// ── File Tree Builder ── + +export function buildFileTree(files: Array, packageName: string): TreeNode { + const root: TreeNode = {name: packageName, path: ``, children: []}; + + for (const file of files) { + const parts = file.name.split(`/`).filter(Boolean); + let current = root; + + for (let i = 0; i < parts.length; i++) { + const isFile = i === parts.length - 1; + + if (!current.children) + current.children = []; + + let child = current.children.find(c => c.name === parts[i]); + if (!child) { + child = { + name: parts[i], + path: parts.slice(0, i + 1).join(`/`), + ...(isFile ? {size: file.size} : {children: []}), + }; + current.children.push(child); + } + + current = child; + } + } + + sortTree(root); + + return root; +} + +function sortTree(node: TreeNode): void { + if (!node.children) return; + node.children.sort((a, b) => { + const aDir = !!a.children; + const bDir = !!b.children; + if (aDir !== bDir) return aDir ? -1 : 1; + return a.name.localeCompare(b.name); + }); + node.children.forEach(sortTree); +} + +// ── Sparkline ── + +export function sparklinePath(data: Array, w: number, h: number): string { + if (data.length < 2) + return ``; + + const max = Math.max(...data); + const min = Math.min(...data); + const range = max - min || 1; + const step = w / (data.length - 1); + + return data.map((v, i) => { + const x = i * step; + const y = h - ((v - min) / range) * (h * 0.85) - h * 0.05; + return `${i === 0 ? `M` : `L`}${x.toFixed(1)},${y.toFixed(1)}`; + }).join(` `); +} + +// ── Semver helpers ── + +function parseVersion(v: string): {major: number, minor: number, patch: number, pre: string} { + const match = v.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/); + if (!match) return {major: 0, minor: 0, patch: 0, pre: v}; + return {major: +match[1], minor: +match[2], patch: +match[3], pre: match[4]}; +} + +export function isNoisyPrerelease(v: string): boolean { + const pre = v.replace(/^\d+\.\d+\.\d+[-.]?/, ``); + if (!pre) return false; + for (const seg of pre.split(/[.-]/)) { + if (/[a-f0-9]{6,}/i.test(seg) && /[a-f]/i.test(seg) && /\d/.test(seg)) return true; + if (/^\d{8}$/.test(seg)) return true; + } + return false; +} + +export function compareSemverDesc(a: string, b: string): number { + const pa = parseVersion(a); + const pb = parseVersion(b); + if (pa.major !== pb.major) return pb.major - pa.major; + if (pa.minor !== pb.minor) return pb.minor - pa.minor; + if (pa.patch !== pb.patch) return pb.patch - pa.patch; + if (!pa.pre && pb.pre) return -1; + if (pa.pre && !pb.pre) return 1; + return pa.pre < pb.pre ? 1 : pa.pre > pb.pre ? -1 : 0; +} + +export function versionLabel(v: string, prev: string | null): string { + if (!prev) return `initial`; + const cur = parseVersion(v); + const prv = parseVersion(prev); + if (cur.major !== prv.major) return `major`; + if (cur.minor !== prv.minor) return `minor`; + return `patch`; +} + +// ── PM install commands ── + +export const PM_COMMANDS: Record = { + yarn: {verb: `yarn`, rest: `add`}, + npm: {verb: `npm`, rest: `install`}, + pnpm: {verb: `pnpm`, rest: `add`}, + bun: {verb: `bun`, rest: `add`}, +}; + +// ── Language detection ── + +const EXT_LANG: Record = { + js: `javascript`, mjs: `javascript`, cjs: `javascript`, + ts: `typescript`, mts: `typescript`, cts: `typescript`, + jsx: `javascript`, tsx: `typescript`, + json: `json`, json5: `json`, + md: `markdown`, mdx: `markdown`, + css: `css`, scss: `scss`, less: `less`, + html: `html`, htm: `html`, + yaml: `yaml`, yml: `yaml`, + xml: `xml`, svg: `xml`, + sh: `shell`, bash: `shell`, zsh: `shell`, + py: `python`, rb: `ruby`, rs: `rust`, go: `go`, + java: `java`, kt: `kotlin`, swift: `swift`, + c: `c`, cpp: `cpp`, h: `c`, hpp: `cpp`, + graphql: `graphql`, gql: `graphql`, + sql: `sql`, toml: `ini`, ini: `ini`, + txt: `plaintext`, log: `plaintext`, +}; + +export function langFromPath(filepath: string): string { + const ext = filepath.split(`.`).pop()?.toLowerCase() ?? ``; + return EXT_LANG[ext] ?? `plaintext`; +} + +export function setupMonacoTheme(monaco: any) { + monaco.editor.defineTheme(`pkg-dark`, { + base: `vs-dark`, + inherit: true, + rules: [ + {token: `comment`, foreground: `6872a0`}, + {token: `keyword`, foreground: `c4a0f5`}, + {token: `string`, foreground: `a0dbb0`}, + {token: `number`, foreground: `d4c080`}, + {token: `type`, foreground: `90c8e8`}, + {token: `function`, foreground: `90c8e8`}, + ], + colors: { + 'editor.background': `#0a0e28`, + 'editor.foreground': `#d6daf5`, + 'editor.lineHighlightBackground': `#ffffff08`, + 'editorLineNumber.foreground': `#6872a0`, + 'editorLineNumber.activeForeground': `#a8b0d4`, + 'editor.selectionBackground': `#ffffff18`, + 'editor.inactiveSelectionBackground': `#ffffff0d`, + 'editorIndentGuide.background': `#ffffff0a`, + 'editorIndentGuide.activeBackground': `#ffffff18`, + 'editorWidget.background': `#0a0e28`, + 'editorWidget.border': `#a8b0d424`, + 'scrollbarSlider.background': `#a8b0d428`, + 'scrollbarSlider.hoverBackground': `#a8b0d440`, + }, + }); + monaco.editor.defineTheme(`pkg-light`, { + base: `vs`, + inherit: true, + rules: [ + {token: `comment`, foreground: `7a84a8`}, + {token: `keyword`, foreground: `7030b0`}, + {token: `string`, foreground: `286840`}, + {token: `number`, foreground: `885510`}, + {token: `type`, foreground: `205878`}, + {token: `function`, foreground: `205878`}, + ], + colors: { + 'editor.background': `#f2f5fc`, + 'editor.foreground': `#0c1030`, + 'editor.lineHighlightBackground': `#00000005`, + 'editorLineNumber.foreground': `#515a7a`, + 'editorLineNumber.activeForeground': `#252d50`, + 'editor.selectionBackground': `#0c103018`, + 'editor.inactiveSelectionBackground': `#0c10300d`, + 'editorIndentGuide.background': `#0c103010`, + 'editorIndentGuide.activeBackground': `#0c103020`, + 'editorWidget.background': `#f2f5fc`, + 'editorWidget.border': `#0c10301c`, + 'scrollbarSlider.background': `#0c103018`, + 'scrollbarSlider.hoverBackground': `#0c103028`, + }, + }); +} + +// ── Prettier formatting ── + +type PrettierCfg = {parser: string, load: () => Promise>}; + +const jsPlugins = () => Promise.all([import(`prettier/plugins/babel`), import(`prettier/plugins/estree`)]); +const tsPlugins = () => Promise.all([import(`prettier/plugins/typescript`), import(`prettier/plugins/estree`)]); +const cssPlugins = () => import(`prettier/plugins/postcss`).then(m => [m]); +const htmlPlugins = () => import(`prettier/plugins/html`).then(m => [m]); +const mdPlugins = () => import(`prettier/plugins/markdown`).then(m => [m]); +const yamlPlugins = () => import(`prettier/plugins/yaml`).then(m => [m]); + +const PRETTIER: Record = { + js: {parser: `babel`, load: jsPlugins}, + jsx: {parser: `babel`, load: jsPlugins}, + mjs: {parser: `babel`, load: jsPlugins}, + cjs: {parser: `babel`, load: jsPlugins}, + ts: {parser: `typescript`, load: tsPlugins}, + tsx: {parser: `typescript`, load: tsPlugins}, + mts: {parser: `typescript`, load: tsPlugins}, + cts: {parser: `typescript`, load: tsPlugins}, + json: {parser: `json`, load: jsPlugins}, + css: {parser: `css`, load: cssPlugins}, + scss: {parser: `scss`, load: cssPlugins}, + less: {parser: `less`, load: cssPlugins}, + html: {parser: `html`, load: htmlPlugins}, + htm: {parser: `html`, load: htmlPlugins}, + md: {parser: `markdown`, load: mdPlugins}, + markdown: {parser: `markdown`, load: mdPlugins}, + yaml: {parser: `yaml`, load: yamlPlugins}, + yml: {parser: `yaml`, load: yamlPlugins}, +}; + +export async function formatWithPrettier(code: string, filepath: string): Promise { + const ext = filepath.split(`.`).pop()?.toLowerCase() ?? ``; + + const cfg = PRETTIER[ext]; + if (!cfg) + return code; + + try { + const [prettier, plugins] = await Promise.all([ + import(`prettier/standalone`), + cfg.load(), + ]); + return await prettier.format(code, {parser: cfg.parser, plugins}); + } catch (err) { + console.error(`[prettier] failed to format ${filepath}:`, err); + return code; + } +} + +export function canPrettify(filepath: string): boolean { + const ext = filepath.split(`.`).pop()?.toLowerCase() ?? ``; + return ext in PRETTIER; +} diff --git a/website/src/components/sidebar.ts b/website/src/components/sidebar.ts new file mode 100644 index 00000000..eedd4b21 --- /dev/null +++ b/website/src/components/sidebar.ts @@ -0,0 +1,114 @@ +export type SidebarLink = { + label: string; + href: string; + active?: boolean; + sub?: boolean; + mono?: boolean; + section?: boolean; +}; + +export type SidebarSubtitle = { + subtitle: string; +}; + +export type SidebarItem = SidebarLink | SidebarSubtitle; + +export interface SidebarGroup { + title: string; + items: SidebarItem[]; +} + +const metaGlob = import.meta.glob(`../docs/**/_meta.{yml,yaml}`, {eager: true, query: `?raw`, import: `default`}); +const docGlob = import.meta.glob(`../docs/**/*.md`, {eager: true, query: `?raw`, import: `default`}); + +const metaLookup = new Map(); + +for (const [filePath, content] of Object.entries(metaGlob)) { + const relDir = filePath + .replace(/^\.\.\/docs\//, ``) + .replace(/\/_meta\.(yml|yaml)$/, ``); + const label = content.match(/^label:\s*(.+)$/m)?.[1]?.trim(); + const order = parseInt(content.match(/^order:\s*(\d+)$/m)?.[1] ?? `99`, 10); + metaLookup.set(relDir, { label: label ?? relDir, order }); +} + +const slugToDir = new Map(); + +for (const [filePath, content] of Object.entries(docGlob)) { + const slug = content.match(/^slug:\s*(.+)$/m)?.[1]?.trim(); + if (slug) { + const relPath = filePath.replace(/^\.\.\/docs\//, ``); + const lastSlash = relPath.lastIndexOf(`/`); + slugToDir.set(slug, lastSlash >= 0 ? relPath.substring(0, lastSlash) : `.`); + } +} + +export function formatLabel(dirName: string): string { + return dirName + .split(`-`) + .map(w => w[0].toUpperCase() + w.slice(1)) + .join(` `); +} + +export function getDirForSlug(slug: string): string | undefined { + return slugToDir.get(slug); +} + +export function getMetaForDir(dir: string): { label: string; order: number } | undefined { + return metaLookup.get(dir); +} + +export function getGroupLabelForSlug(slug: string): string | undefined { + const dir = slugToDir.get(slug); + if (!dir) return undefined; + const meta = metaLookup.get(dir); + return meta?.label ?? formatLabel(dir.split(`/`).pop()!); +} + +export function buildSidebarGroups( + allDocs: Array<{data: {slug: string; title: string; sidebar?: {order?: number; hidden?: boolean}; sidebar_position?: number}}>, + section: string, + activePage: string, +): SidebarGroup[] { + const docs = allDocs.filter(doc => { + const dir = getDirForSlug(doc.data.slug); + if (!dir?.startsWith(section)) return false; + if (doc.data.sidebar?.hidden) return false; + + return true; + }); + + const groupMap = new Map(); + + for (const doc of docs) { + const fsDir = getDirForSlug(doc.data.slug) ?? `.`; + + if (!groupMap.has(fsDir)) { + const meta = getMetaForDir(fsDir); + groupMap.set(fsDir, { + label: meta?.label ?? formatLabel(fsDir.split(`/`).pop()!), + sortKey: meta?.order ?? 99, + docs: [], + }); + } + + groupMap.get(fsDir)!.docs.push(doc); + } + + return [...groupMap.values()] + .sort((a, b) => a.sortKey - b.sortKey) + .map(({ label, docs: groupDocs }) => ({ + title: label, + items: groupDocs + .sort((a, b) => { + const orderA = a.data.sidebar?.order ?? a.data.sidebar_position ?? 99; + const orderB = b.data.sidebar?.order ?? b.data.sidebar_position ?? 99; + return orderA - orderB; + }) + .map(doc => ({ + label: doc.data.title, + href: `/${doc.data.slug}/`, + active: doc.data.slug === activePage, + })), + })); +} diff --git a/website/src/content.config.ts b/website/src/content.config.ts new file mode 100644 index 00000000..ad15233a --- /dev/null +++ b/website/src/content.config.ts @@ -0,0 +1,95 @@ +import {fileURLToPath} from 'url'; +import path from 'path'; +import {glob} from 'astro/loaders'; +import {clipanionLoaders} from '@clipanion/astro'; +import {defineCollection, z} from 'astro:content'; +import {cliBody} from './utils/cli'; + +const docs = defineCollection({ + loader: glob({pattern: `**/*.{md,mdx}`, base: `./src/docs`}), + schema: z.object({ + slug: z.string(), + title: z.string(), + description: z.string().optional(), + category: z.string().optional(), + sidebar_position: z.number().optional(), + sidebar: z.object({ + order: z.number().optional(), + hidden: z.coerce.boolean().optional(), + }).optional(), + }), +}); + +const blog = defineCollection({ + loader: glob({pattern: `**/*.{md,mdx}`, base: `./src/blog`}), + schema: z.object({ + slug: z.string(), + title: z.string(), + date: z.coerce.date(), + category: z.string().optional(), + author: z.object({ + name: z.string(), + title: z.string(), + url: z.string().optional(), + image_url: z.string().optional(), + }), + }), +}); + +const __dirname = fileURLToPath(new URL(`.`, import.meta.url)); + +const cliSchema = z.object({ + binaryName: z.string(), + commandSpec: z.any(), + title: z.string(), + category: z.string(), +}); + +const cliLoaders = clipanionLoaders({ + id: `cli`, + name: `yarn`, + path: path.resolve(__dirname, `../../target/release/yarn-bin`), + + filter: entry => entry.data.commandSpec.category !== null, + + entry: entry => ({ + ...entry, + data: { + ...entry.data, + title: `yarn ${entry.data.commandSpec.primaryPath.join(` `)}`, + category: entry.data.commandSpec.category!, + }, + }), + + body: cliBody, +}); + +const switchLoaders = clipanionLoaders({ + id: `switch`, + name: `yarn`, + path: path.resolve(__dirname, `../../target/release/yarn`), + + specCommand: [`switch`, `--clipanion-commands`], + + filter: entry => + entry.data.commandSpec.category !== null + && entry.data.commandSpec.primaryPath[0] === `switch`, + + entry: entry => ({ + ...entry, + data: { + ...entry.data, + title: `yarn ${entry.data.commandSpec.primaryPath.join(` `)}`, + category: entry.data.commandSpec.category!, + }, + }), + + body: cliBody, +}); + +export const collections = { + blog, + docs, + cli: defineCollection({loader: cliLoaders.commands, schema: cliSchema}), + switch: defineCollection({loader: switchLoaders.commands, schema: cliSchema}), +}; diff --git a/website/src/data/constellations.ts b/website/src/data/constellations.ts new file mode 100644 index 00000000..a1302a93 --- /dev/null +++ b/website/src/data/constellations.ts @@ -0,0 +1,129 @@ +type Point = [number, number]; + +export interface Constellation { + name: string; + stars: Point[]; + edges: [number, number][]; +} + +function compile(name: string, strokes: Point[][]): Constellation { + const stars: Point[] = []; + const edges: [number, number][] = []; + const EPS = 0.02; + + const findOrAdd = ([x, y]: Point): number => { + for (let i = 0; i < stars.length; i++) { + const [sx, sy] = stars[i]; + if (Math.abs(sx - x) < EPS && Math.abs(sy - y) < EPS) return i; + } + stars.push([x, y]); + return stars.length - 1; + }; + + for (const stroke of strokes) { + const idxs = stroke.map(findOrAdd); + for (let i = 1; i < idxs.length; i++) { + if (idxs[i - 1] !== idxs[i]) edges.push([idxs[i - 1], idxs[i]]); + } + } + + return { name, stars, edges }; +} + +const L = 0.15, R = 0.85, T = 0.10, B = 0.90, M = 0.50, MY = 0.50; + +const runes: Constellation[] = [ + compile(`fehu`, [[[L,T],[L,B]], [[L,0.25],[R,0.10]], [[L,0.45],[R,0.30]]]), + compile(`uruz`, [[[L,B],[L,T],[R,0.30],[R,B]]]), + compile(`thurisaz`, [[[L,T],[L,B]], [[L,0.30],[R,MY],[L,0.70]]]), + compile(`ansuz`, [[[L,T],[L,B]], [[L,0.22],[R,0.08]], [[L,0.40],[0.60,0.30]]]), + compile(`raidho`, [[[L,T],[L,B]], [[L,T],[R,0.30],[L,MY]], [[L,MY],[R,B]]]), + compile(`kenaz`, [[[L,T],[R,MY],[L,B]]]), + compile(`gebo`, [[[L,T],[R,B]], [[R,T],[L,B]]]), + compile(`wunjo`, [[[L,T],[L,B]], [[L,T],[R,0.25],[L,MY]]]), + compile(`hagalaz`, [[[L,T],[L,B]], [[R,T],[R,B]], [[L,MY],[R,MY]]]), + compile(`nauthiz`, [[[L,T],[L,B]], [[L,0.30],[R,0.70]]]), + compile(`isa`, [[[M,T],[M,B]]]), + compile(`jera`, [[[L,0.30],[0.40,T],[M,0.30]], [[M,0.70],[0.60,B],[R,0.70]]]), + compile(`eihwaz`, [[[L,0.25],[M,T]], [[M,T],[M,B]], [[M,B],[R,0.75]]]), + compile(`perthro`, [[[R,T],[L,T],[L,B],[R,B]]]), + compile(`algiz`, [[[M,B],[M,MY]], [[M,MY],[L,T]], [[M,MY],[R,T]]]), + compile(`sowilo`, [[[R,T],[0.40,0.30],[0.60,0.70],[L,B]]]), + compile(`tiwaz`, [[[M,T],[M,B]], [[L,0.30],[M,T],[R,0.30]]]), + compile(`berkano`, [[[L,T],[L,B]], [[L,T],[R,0.28],[L,MY]], [[L,MY],[R,0.72],[L,B]]]), + compile(`ehwaz`, [[[L,B],[L,T]], [[L,T],[R,B]], [[R,B],[R,T]], [[L,0.30],[R,0.30]]]), + compile(`mannaz`, [[[L,B],[L,T]], [[R,B],[R,T]], [[L,T],[R,B]], [[R,T],[L,B]]]), + compile(`laguz`, [[[L,T],[L,B]], [[L,T],[0.55,0.25]]]), + compile(`ingwaz`, [[[M,T],[R,MY],[M,B],[L,MY],[M,T]]]), + compile(`dagaz`, [[[L,T],[L,B]], [[R,T],[R,B]], [[L,T],[R,B]], [[L,B],[R,T]]]), + compile(`othala`, [[[M,T],[R,0.35],[0.65,0.65],[M,MY],[0.35,0.65],[L,0.35],[M,T]], [[0.35,0.65],[0.25,B]], [[0.65,0.65],[0.75,B]]]), +]; + +const extensions: Constellation[] = [ + compile(`ear`, [[[M,T],[M,B]], [[L,0.25],[M,T],[R,0.25]], [[L,0.50],[M,0.30]]]), + compile(`cweorth`, [[[M,B],[L,T]], [[M,B],[M,T]], [[M,B],[R,T]]]), + compile(`calc`, [[[M,T],[M,MY]], [[M,MY],[L,B]], [[M,MY],[R,B]]]), + compile(`stan`, [[[L,B],[L,0.30],[M,T],[R,0.30],[R,B]], [[L,0.30],[R,0.30]]]), + compile(`gar`, [[[M,T],[M,B]], [[L,MY],[R,MY]], [[L,T],[R,B]], [[R,T],[L,B]]]), + compile(`yr`, [[[M,T],[M,B]], [[L,B],[M,MY],[R,B]]]), + compile(`hagall`, [[[M,T],[M,B]], [[L,0.25],[R,0.75]], [[R,0.25],[L,0.75]]]), + compile(`bind-gebo-isa`, [[[L,T],[R,B]], [[R,T],[L,B]], [[M,T],[M,B]]]), + compile(`bind-tiwaz-stack`, [[[M,T],[M,B]], [[0.30,0.20],[M,0.10],[0.70,0.20]], [[0.30,0.60],[M,MY],[0.70,0.60]]]), + compile(`bind-algiz-isa`, [[[M,T],[M,B]], [[L,T],[M,0.35],[R,T]]]), + compile(`bind-fehu-2`, [[[L,T],[L,B]], [[L,0.22],[M,0.10]], [[L,0.40],[0.55,0.30]], [[R,T],[R,B]], [[R,0.22],[0.70,0.10]]]), + compile(`bind-othala`, [[[M,T],[R,MY],[M,B],[L,MY],[M,T]], [[L,MY],[0.05,0.80]], [[R,MY],[0.95,0.80]]]), + compile(`bind-ehwaz-rot`, [[[L,T],[L,B]], [[R,T],[R,B]], [[L,T],[R,B]], [[L,0.30],[R,0.30]], [[L,0.70],[R,0.70]]]), + compile(`aegishjalmr`, [[[M,T],[M,B]], [[L,MY],[R,MY]], [[0.25,0.25],[0.75,0.75]], [[0.75,0.25],[0.25,0.75]]]), + compile(`valknut`, [[[M,T],[L,B],[R,B],[M,T]], [[0.35,0.40],[M,B]]]), + compile(`vegvisir`, [[[M,T],[M,B]], [[L,MY],[R,MY]], [[M,0.20],[0.40,0.10]], [[M,0.20],[0.60,0.10]], [[M,0.80],[0.40,0.90]], [[M,0.80],[0.60,0.90]]]), +]; + +let seed = 0xbeefcafe; +const rnd = (): number => { seed = (seed * 1664525 + 1013904223) | 0; return ((seed >>> 0) % 1e6) / 1e6; }; +const pick = (arr: T[]): T => arr[Math.floor(rnd() * arr.length)]; + +const makeStroke = (kind: string): Point[] => { + switch (kind) { + case `stave`: return [[M, T], [M, B]]; + case `hstave`: return [[L, MY], [R, MY]]; + case `slash-dr`: return [[L, T], [R, B]]; + case `slash-ur`: return [[L, B], [R, T]]; + case `chevron-r`: return [[L, T], [R, MY], [L, B]]; + case `chevron-l`: return [[R, T], [L, MY], [R, B]]; + case `chevron-u`: return [[L, B], [M, T], [R, B]]; + case `chevron-d`: return [[L, T], [M, B], [R, T]]; + case `top-arm-r`: return [[M, T], [0.70 + rnd() * 0.15, 0.25 + rnd() * 0.1]]; + case `top-arm-l`: return [[M, T], [0.30 - rnd() * 0.15, 0.25 + rnd() * 0.1]]; + case `bot-arm-r`: return [[M, B], [0.70 + rnd() * 0.15, 0.75 - rnd() * 0.1]]; + case `bot-arm-l`: return [[M, B], [0.30 - rnd() * 0.15, 0.75 - rnd() * 0.1]]; + case `side-arm`: { const y = 0.30 + rnd() * 0.4; return [[L, y], [M, 0.25 + rnd() * 0.5]]; } + case `diamond-s`: return [[M, 0.30], [0.62, MY], [M, 0.70], [0.38, MY], [M, 0.30]]; + case `triangle-r`: return [[L, 0.30], [R, MY], [L, 0.70]]; + case `triangle-l`: return [[R, 0.30], [L, MY], [R, 0.70]]; + case `half-x`: return [[L, 0.30], [R, 0.70]]; + case `half-x2`: return [[L, 0.70], [R, 0.30]]; + } + return [[M, T], [M, B]]; +}; + +const strokeKinds = [`stave`,`hstave`,`slash-dr`,`slash-ur`,`chevron-r`,`chevron-l`,`chevron-u`,`chevron-d`,`top-arm-r`,`top-arm-l`,`bot-arm-r`,`bot-arm-l`,`side-arm`,`diamond-s`,`triangle-r`,`triangle-l`,`half-x`,`half-x2`]; + +const combined = [...runes, ...extensions]; +const needed = 80 - combined.length; + +for (let i = 0; i < needed; i++) { + const count = 2 + Math.floor(rnd() * 2); + const strokes: Point[][] = []; + const used = new Set(); + for (let j = 0; j < count; j++) { + let k = pick(strokeKinds); + let guard = 0; + while (used.has(k) && guard++ < 4) k = pick(strokeKinds); + used.add(k); + strokes.push(makeStroke(k)); + } + if (rnd() < 0.55 && !used.has(`stave`)) strokes.unshift(makeStroke(`stave`)); + combined.push(compile(`bindrune-` + i, strokes)); +} + +export const CONSTELLATION_LIBRARY: Constellation[] = combined.slice(0, 80); diff --git a/website/src/docs/advanced/general-reference/_meta.yml b/website/src/docs/advanced/general-reference/_meta.yml new file mode 100644 index 00000000..3ee67ac2 --- /dev/null +++ b/website/src/docs/advanced/general-reference/_meta.yml @@ -0,0 +1,2 @@ +label: General reference +order: 1 diff --git a/website/src/docs/advanced/general-reference/error-codes.md b/website/src/docs/advanced/general-reference/error-codes.md new file mode 100644 index 00000000..99ac3a10 --- /dev/null +++ b/website/src/docs/advanced/general-reference/error-codes.md @@ -0,0 +1,489 @@ +--- +category: advanced +slug: advanced/error-codes +title: Error Codes +description: A list of Yarn's error codes with detailed explanations. +sidebar: + order: 1 +--- + +{/* */} + +:::note +Are you a plugin author and want to declare your own error codes that don't match the semantic of the ones provided here? Please relinquish one character and use the `YNX` prefix (ex `YNX001`) instead of `YN0`! + +Keeping this convention will help our users to figure out which error codes can be found on this documentation and which ones should instead be checked against the individual documentation of the plugins they use. +::: + +## YN0000 - `UNNAMED` + +This code is used to log regular messages, mostly to align all the lines in the Yarn output. No worry! + +## YN0001 - `EXCEPTION` + +An exception had be thrown by the program. + +This error typically should never happen (it should instead point to a different error message from this page so that it can be properly documented), so it should be considered a bug in Yarn. Feel free to open an issue or, even better, a pull request aiming to fix it. + +## YN0002 - `MISSING_PEER_DEPENDENCY` + +A package requests a peer dependency, but one or more of its parents in the dependency tree doesn't provide it. + +Note that Yarn enforces peer dependencies at every level of the dependency tree. That is, if `─D>` is a dependency and `─P>` is a peer dependency, + +```sh +# bad +project +├─D> packagePeer +└─D> packageA + └─P> packageB + └─P> packagePeer + +# good +project +├─D> packagePeer +└─D> packageA + ├─P> packagePeer + └─D> packageB + └─P> packagePeer +``` + +Depending on your situation, multiple options are possible: + +- The author of `packageA` can fix this problem by adding a peer dependency on `packagePeer`. If relevant, they can use [optional peer dependencies](https://yarnpkg.com/configuration/manifest#peerDependenciesMeta.optional) to this effect. + +- The author of `packageB` can fix this problem by marking the `packagePeer` peer dependency as optional - but only if the peer dependency is actually optional, of course! + +- The author of `project` can fix this problem by manually overriding the `packageA` and/or `packageB` definitions via the [`packageExtensions` config option](/configuration/yarnrc#packageExtensions). + +To understand more about this issue, check out [this blog post](https://dev.to/arcanis/implicit-transitive-peer-dependencies-ed0). + +## YN0003 - `CYCLIC_DEPENDENCIES` + +Two packages with build scripts have cyclic dependencies. + +Cyclic dependencies are a can of worms. They happen when a package `A` depends on a package `B` and vice-versa Sometime can arise through a chain of multiple packages - for example when `A` depends on `B`, which depends on `C`, which depends on `A`. + +While cyclic dependencies may work fine in the general Javascript case (and in fact Yarn won't warn you about it in most cases), they can cause issues as soon as build scripts are involved. Indeed, in order to build a package, we first must make sure that its own dependencies have been properly built. How can we do that when two packages reference each other? Since the first one to build cannot be deduced, such patterns will cause the build scripts of every affected package to simply be ignored (and a warning emitted). + +There's already good documentation online explaining how to get rid of cyclic dependencies, the simplest one being to extract the shared part of your program into a third package without dependencies. So the first case we described would become `A` depends on `C`, `B` depends on `C`, `C` doesn't depend on anything. + +## YN0004 - `DISABLED_BUILD_SCRIPTS` + +A package has build scripts, but they've been disabled across the project. + +Build scripts can be disabled on a global basis through the use of the `enableScripts` settings. When it happens, a warning is still emitted to let you know that the installation might not be complete. + +The safest way to downgrade the warning into a notification is to explicitly disable build scripts for the affected packages through the use of the `dependenciesMeta[].built` key. + +## YN0005 - `BUILD_DISABLED` + +A package has build scripts, but they've been disabled through its configuration. + +Build scripts can be disabled on a per-project basis through the use of the `dependenciesMeta` settings from the `package.json` file. When it happens, a notification is still emitted to let you know that the installation might not be complete. + +## YN0006 - `SOFT_LINK_BUILD` + +A package has build scripts, but is linked through a soft link. + +For Yarn, a hard link is when a package is owned by the package manager. In these instances Yarn will typically copy packages having build scripts into a project-local cache so that multiple projects with multiple dependency trees don't use the same build artifacts. So what's the problem with so-called "soft links"? + +Soft links are when the package manager doesn't own the package source. An example is a workspace, or a dependency referenced through the `portal:` specifier. In these instances Yarn cannot safely assume that executing build scripts there is the intended behavior, because it would likely involve mutating your project or, even worse, an external location on your disk that might be shared by multiple projects. Since Yarn avoids doing anything unsafe, it cannot run build scripts on soft links. + +There are a few workarounds: + +- Using `file:` instead of `portal:` will cause a hard link to be used instead of a soft link. The other side of the coin will be that the packages will be copied into the cache as well, meaning that changing the package source will require you to run `YARN_UPDATE_FILE_CACHE=1 yarn install` for your changes to be taken into account. + +- You can manually run `yarn run postinstall` (or whatever is named your build script) from the directory of the affected packages. This requires you to know in which order they'll have to be called, but is generally the safest option. + +- You can simply abstain from using build scripts with soft links. While this suggestion might seem like a bad case of "fix a problem by not encountering the problem", consider that build scripts in development might not be of the best effect from a developer experience perspective - they usually mean that you'll need to run a script before being able to see your changes, which is often not what you seek. + +## YN0007 - `MUST_BUILD` + +A package must be built. + +This informational message occurs when Yarn wishes to let you know that a package will need to be built for the installation to complete. This usually occurs in only two cases: either the package never has been built before, or its previous build failed (returned a non-zero exit code). + +## YN0008 - `MUST_REBUILD` + +A package must be rebuilt. + +This information message occurs when Yarn wishes to let you know that a package will need to be rebuilt in order for the installation to complete. This usually occurs in a single case: when the package's dependency tree has changed. Note that this also includes its transitive dependencies, which sometimes may cause surprising rebuilds (for example, if `A` depends on `B` that depends on `C@1`, and if Yarn decides for some reason that `C` should be bumped to `C@2`, then `A` will need to be rebuilt). + +## YN0009 - `BUILD_FAILED` + +A package build failed. + +This problem typically doesn't come from Yarn itself, and simply means that a package described as having build directives couldn't get built successfully. + +To see the actual error message, read the file linked in the report. It will contain the full output of the failing script. + +## YN0010 - `RESOLVER_NOT_FOUND` + +A resolver cannot be found for the given package. + +Resolvers are the components tasked from converting ranges (`^1.0.0`) into references (`1.2.3`). They each contain their own logic to do so - the semver resolver is the most famous one but far from being the only one. The GitHub resolver transforms GitHub repositories into tarball urls, the Git resolver normalizes the paths sent to git, ... each resolver takes care of a different resolution strategy. A missing resolver means that one of those strategies is missing. + +This error is usually caused by a Yarn plugin being missing. + +## YN0011 - `FETCHER_NOT_FOUND` + +A fetcher cannot be found for the given package. + +Fetchers are the components that take references and fetch the source code from the remote location. A semver fetcher would likely fetch the packages from some registry, while a workspace fetcher would simply redirect to the location on the disk where the sources can be found. + +This error is usually caused by a Yarn plugin being missing. + +## YN0012 - `LINKER_NOT_FOUND` + +A linker cannot be found for the given package. + +Linkers are the components tasked from extracting the sources from the artifacts returned by the fetchers and putting them on the disk in a manner that can be understood by the target environment. The Node linker would use the Plug'n'Play strategy, while a PHP linker would use an autoload strategy instead. + +This error is usually caused by a Yarn plugin being missing. + +## YN0013 - `FETCH_NOT_CACHED` + +A package cannot be found in the cache for the given package and will be fetched from its remote location. + +When a package is downloaded from whatever its remote location is, Yarn stores it in a specific folder called then cache. Then, the next time this package was to be downloaded, Yarn simply check this directory and use the stored package if available. This message simply means that the package couldn't be found there. It's not a huge issue, but you probably should try to limit it as much as possible - for example by using [Zero-Installs](/features/caching#zero-installs). + +## YN0014 - `YARN_IMPORT_FAILED` + +A lockfile couldn't be properly imported from a v1 lockfile. + +The v2 release contains major changes in the way Yarn is designed, and the lockfile format is one of them. In some rare cases, the data contained in the v1 lockfile aren't compatible with the ones we stored within the v2 files. When it happens, Yarn will emit this warning and resolve the package descriptor again. Only this package will be affected; all others will continue to be imported as expected. + +## YN0015 - `REMOTE_INVALID` + +The remote source returned invalid data. + +This error is thrown by the resolvers and fetchers when the remote sources they communicate with return values that aren't consistent with what we would expect (for example because they are missing fields). + +## YN0016 - `REMOTE_NOT_FOUND` + +The remote source returned valid data, but told us the package couldn't be found. + +This error is thrown by the resolvers and fetchers when the remote sources they communicate with inform them that the package against which have been made the request doesn't exist. This might happen if the package has been unpublished, and there's usually nothing Yarn can do. + +## YN0017 - `RESOLUTION_PACK` + +This error code isn't used at the moment (it used to print the number of packages that took part in each pass of the resolution algorithm, but was deemed too verbose compared to its usefulness). + +## YN0018 - `CACHE_CHECKSUM_MISMATCH` + +The checksum of a package from the cache doesn't match what the lockfile expects. + +This situation usually happens after you've modified the zip archives from your cache by editing the files it contains for debug purposes. Use one of the three following commands in order to bypass it: + +- `YARN_CHECKSUM_BEHAVIOR=reset` will remove the files from the cache and download them again +- `YARN_CHECKSUM_BEHAVIOR=update` will update the lockfile to contain the new checksum +- `YARN_CHECKSUM_BEHAVIOR=ignore` will use the existing files but won't update the lockfile + +## YN0019 - `UNUSED_CACHE_ENTRY` + +A file from the cache has been detected unused while installing dependencies. + +Running `yarn cache clean` will cause Yarn to remove everything inside `.yarn/cache`. + +## YN0020 - `MISSING_LOCKFILE_ENTRY` + +A package descriptor cannot be found in the lockfile. + +A lot of commands (for example `yarn run`) require the lockfile to be in a state consistent with the current project in order to behave properly. This error will be generated when Yarn detects that your project references a package that isn't listed within the lockfile (usually because you modified a `dependencies` field without running `yarn install`, or because you added a new workspace). Running `yarn install` will almost certainly fix this particular error. + +## YN0021 - `WORKSPACE_NOT_FOUND` + +A dependency uses a `workspace:` range that cannot be resolved to an existing workspace. + +The `workspace:` protocol is a new feature that appeared in Yarn v2 that allows to target a specific workspace of the current project without risking to ever pull data from other sources in case the workspace doesn't exist. This error precisely means that the workspace doesn't exist for the reason described in the error message. + +## YN0022 - `TOO_MANY_MATCHING_WORKSPACES` + +This error should be considered obsolete and not exist; open an issue if you have it. + +## YN0023 - `CONSTRAINTS_MISSING_DEPENDENCY` + +One of your workspaces should depend on a dependency but doesn't. + +A [constraint](/features/constraints) has been put into effect that declares that the specified workspace must depend on the specified range of the specified dependency. Since it currently doesn't, Yarn emits this error when running `yarn constraints`. In order to fix it simply run `yarn constraints --fix` which will autofix all such errors. + +## YN0024 - `CONSTRAINTS_INCOMPATIBLE_DEPENDENCY` + +One of your workspaces should depend on a specific version of a dependency but doesn't. + +A [constraint](/features/constraints) has been put into effect that declares that the specified workspace must depend on the specified range of the specified dependency. Since it currently doesn't, Yarn emits this error when running `yarn constraints`. In order to fix it simply run `yarn constraints --fix` which will autofix all such errors. + +## YN0025 - `CONSTRAINTS_EXTRANEOUS_DEPENDENCY` + +One of your workspaces shouldn't depend on one of the dependencies it lists. + +A [constraint](/features/constraints) has been put into effect that declares that the specified workspace must depend on the specified range of the specified dependency. Since it currently doesn't, Yarn emits this error when running `yarn constraints`. In order to fix it simply run `yarn constraints --fix` which will autofix all such errors. + +## YN0026 - `CONSTRAINTS_INVALID_DEPENDENCY` + +One of your workspaces lists an invalid dependency. + +A [constraint](/features/constraints) has been put into effect that declares that the specified workspace probably shouldn't depend on the specified dependency in its current state. Since it currently does, Yarn emits this error when running `yarn constraints`. Fixing this error require manual intervention as the fix is ambiguous from Yarn's point of view. + +## YN0027 - `CANT_SUGGEST_RESOLUTIONS` + +Yarn cannot figure out proper range suggestions for the packages you're adding to your project. + +When running `yarn add` without adding explicit ranges to the packages to add, Yarn will try to find versions that match your intent. Generally it means that it will prefer project workspaces and, if it cannot find any, will instead try to query the npm registry for the list of published releases and use whatever is the highest one. This error means that this process failed and Yarn cannot successfully figure out which version of the package should be added to your project. + +## YN0028 - `FROZEN_LOCKFILE_EXCEPTION` + +Your lockfile would be modified if Yarn was to finish the install. + +When passing the `--immutable` option to `yarn install`, Yarn will ensure that the lockfile isn't modified in the process and will instead throw an exception if this situation was to happen (for example if a newly added package was missing from the lockfile, or if the current Yarn release required some kind of migration before being able to work with the lockfile). + +This option is typically meant to be used on your CI and production servers, and fixing this error should simply be a matter of running `yarn install` on your local development environment and submitting a PR containing the updated lockfile. + +## YN0029 - `CROSS_DRIVE_VIRTUAL_LOCAL` + +> **Removed:** Virtuals aren't implemented using symlinks anymore. + +## YN0030 - `FETCH_FAILED` + +This error code isn't used at the moment; we ideally want to explain **why** did the fetch fail rather than . + +## YN0031 - `DANGEROUS_NODE_MODULES` + +Yarn is installing packages using [Plug'n'Play](/features/pnp), but a `node_modules` folder has been found. + +This warning is emitted when your project is detected as containing `node_modules` folders that actually seem to contain packages. This is not advised as they're likely relicts of whatever package manager you used before, and might confuse your tools and lead you into "works on my machine" situations. + +## YN0032 - `NODE_GYP_INJECTED` + +In some situation Yarn might detect that `node-gyp` is required by a package without this package explicitly listing the dependency. This behavior is there for legacy reason and should not be relied upon for the following reasons: + +- The main way to detect whether `node-gyp` is implicitly required is to check whether the package contains a `bindings.gyp` file. However, doing this check implies that the package listing is known at the time Yarn resolves the dependency tree. This would require to fetch all npm archives as part of the resolution step (rather than wait until the dedicated fetch step), and all that just for the sake of this problematic feature. + +- Implicit dependencies on `node-gyp` don't provide any hint to the package manager as to which versions of `node-gyp` are compatible with the package being built. Yarn does its best by adding an implicit dependency on `npm:*`, but it might be wrong and we'll have no way to know it - your installs will just crash unexpectedly when compiled with incompatible versions. + +Packages omitting `node-gyp` usually do so in order to decrease the amount of packages in the final dependency tree when building the package isn't required (prebuilt binaries). While a reasonable wish, doing this goes against the package manager rules and we would prefer to solve this through a dedicated feature rather than through such hacks. In the meantime we strongly recommend to consider prebuilding native dependencies via WebAssembly if possible - then the `node-gyp` problem completely disappears. + +## YN0046 - `AUTOMERGE_FAILED_TO_PARSE` + +This error is triggered when Git conflict tokens are found within the `yarn.lock` file and one or both of the individual candidate lockfiles cannot be parsed. This typically happens because of one of those two situations: + +- If you're working on a branch with Yarn v2 and are trying to merge a branch using Yarn v1, this error will be triggered (the v1 lockfiles aren't Yaml, which prevents them from being parsed. Even if we could, they don't contain enough information compared to the v2 lockfiles). + + - The easiest way to fix it is to use `git checkout --theirs yarn.lock`, and follow up with `yarn install` again (which can be followup by `yarn cache clean` to remove any file that wouldn't be needed anymore). This will cause the v1 lockfile to be re-imported. The v2 resolutions will be lost, but Yarn will detect it and resolve them all over again. + +- If you have multiple levels of conflicts. Yarn doesn't support such conflicts, and you'll have to figure out a way to only have two levels. This is typically done by first resolving the conflicts between two branches, and then resolving them again on the merge result of the previous step and the third branch. + +## YN0047 - `AUTOMERGE_IMMUTABLE` + +This error is triggered when Git conflict tokens are found within the `yarn.lock` file while Yarn is executing under the immutable mode (`yarn install --immutable`). + +When under this mode, Yarn isn't allowed to edit any file, not even for automatically resolving conflicts. This mode is typically used on CI to ensure that your projects are always in a correct state before being merged into the trunk. + +In order to solve this problem, try running `yarn install` again on your computer without the `--immutable` flag, then commit the changes if the command succeeded. + +## YN0048 - `AUTOMERGE_SUCCESS` + +This informational message is emitted when Git conflict tokens were found within the `yarn.lock` file but were automatically fixed by Yarn. There's nothing else to do, everything should work out of the box! + +## YN0049 - `AUTOMERGE_REQUIRED` + +This informational message is emitted when Git conflict tokens are found within the `yarn.lock` file. Yarn will then try to automatically resolve the conflict by following its internal heuristic. + +The automerge logic is pretty simple: it will take the lockfile from the pulled branch, modify it by adding the information from the local branch, and run `yarn install` again to fix anything that might have been lost in the process. + +## YN0050 - `DEPRECATED_CLI_SETTINGS` + +This error is triggered when passing options to a CLI command through its arguments (for example `--cache-folder`). + +Starting from the v2, this isn't supported anymore. The reason for this is that we've consolidated all of our configuration inside a single store that can be defined from a yarnrc file. This guarantees that all your commands run inside the same environments (which previously wasn't the case depending on whether you were using `--cache-folder` on all your commands or just the install). CLI options will now only be used to control the _one-time-behaviors_ of a particular command (like `--verbose`). + +**Special note for Netlify users:** Netlify currently [automatically passes](https://github.com/netlify/build-image/blob/f9c7f9a87c10314e4d65b121d45d68dc976817a2/run-build-functions.sh#L109) the `--cache-folder` option to Yarn, and you cannot disable it. For this reason we decided to make it a warning rather than an error when we detect that Yarn is running on Netlify (we still ignore the flag). We suggest upvoting [the relevant issue](https://github.com/netlify/build-image/issues/319) on their repository, as we're likely to remove this special case in a future major release. + +## YN0059 - `INVALID_RANGE_PEER_DEPENDENCY` + +A package requests a peer dependency, but the range provided is not a valid semver range. It is not possible to ensure the provided package meets the peer dependency request. The range must be fixed in order for the warning to go away. This will not prevent resolution, but may leave the system in an incorrect state. + +## YN0060 - `INCOMPATIBLE_PEER_DEPENDENCY` + +A package requests a peer dependency, but its parent in the dependency tree provides a version that does not satisfy the peer dependency's range. The parent should be altered to provide a valid version or the peer dependency range updated. This will not prevent resolution, but may leave the system in an incorrect state. + +## YN0061 - `DEPRECATED_PACKAGE` + +A package is marked as deprecated by the publisher. Avoid using it, use the alternative provided in the deprecation message instead. + +## YN0062 - `INCOMPATIBLE_OS` + +> **Removed:** Replaced by [`INCOMPATIBLE_ARCHITECTURE`](#yn0076---incompatible_architecture). + +## YN0063 - `INCOMPATIBLE_CPU` + +> **Removed:** Replaced by [`INCOMPATIBLE_ARCHITECTURE`](#yn0076---incompatible_architecture). + +## YN0068 - `UNUSED_PACKAGE_EXTENSION` + +A packageExtension is detected by Yarn as being unused, which means that the selector doesn't match any of the installed packages. + +## YN0069 - `REDUNDANT_PACKAGE_EXTENSION` + +A packageExtension is detected by Yarn as being unneeded, which means that the selected packages have the same behavior with and without the extension. + +## YN0071 - `NM_CANT_INSTALL_EXTERNAL_SOFT_LINK` + +An external soft link (portal) cannot be installed, because incompatible version of a dependency exists in the parent package. This prevents portal representation for node_modules installs without a need to write files into portal's target directory, which is forbidden for security reasons. + +**Workarounds** If the ranges for conflicting dependencies overlap between portal target and portal parent, the workaround is to use `yarn dedupe foo` (where `foo` is the conflicting dependency name) to upgrade the conflicting dependencies to the highest available versions, if `yarn dedupe` is used without arguments, all the dependencies across the project will be upgraded to the highest versions within their ranges in `package.json`. Another alternative is to use `link:` protocol instead of `portal:` and install dependencies inside the target directory explicitly. + +## YN0072 - `NM_PRESERVE_SYMLINKS_REQUIRED` + +A portal dependency with subdependencies is used in the project. `--preserve-symlinks` Node option must be used +to start the application in order for portal dependency to find its subdependencies and peer dependencies. + +## YN0074 - `NM_HARDLINKS_MODE_DOWNGRADED` + +`nmMode` has been downgraded to `hardlinks-local` due to global cache and install folder being on different devices. Consider changing `globalFolder` setting and place the global cache on the same device as your project, if you want `hardlinks-global` to take effect. + +## YN0075 - `PROLOG_INSTANTIATION_ERROR` + +This error appears when a Prolog predicate is called with an invalid signature. Specifically, it means that some of the predicate parameters are non-instantiated (ie have no defined value), when the predicate would expect some. This doesn't mean that you need to hardcode a value, just that you need to assign one before calling the predicate. In the case of the `WorkspaceCwd` parameter from most of the Yarn predicates, it means that instead of calling: + +``` +workspace_field(WorkspaceCwd, 'name', _). +``` + +You would also use the `workspace/1` predicate to let Prolog "fill" the `WorkspaceCwd` parameter prior to using it in `workspace_field/3`: + +``` +workspace(WorkspaceCwd), workspace_field(WorkspaceCwd, 'name', _). +``` + +For more information about the parameters that must be instantiated when calling the predicate reported by the error message, consult the [dedicated page](/features/constraints#query-predicate) from our documentation. + +## YN0076 - `INCOMPATIBLE_ARCHITECTURE` + +A package is specified in its manifest (through the [`os`](/configuration/manifest#os) / [`cpu`](/configuration/manifest#cpu) / [`libc`](/configuration/manifest#libc) fields) as being incompatible with the system architecture. It will not be fetched, linked, and its postinstall scripts will not run on this system. + +## YN0077 - `GHOST_ARCHITECTURE` + +Some native packages may be excluded from the install if they signal they don't support the systems the project is intended for. This detection is typically based on your current system parameters, but it can be configured using the [`supportedArchitectures` config option](/configuration/yarnrc#supportedArchitectures). If your os or cpu are missing from this list, Yarn will skip the packages and raise a warning. + +Note that all fields from `supportedArchitectures` default to `current`, which is a dynamic value depending on your local parameters. For instance, if you wish to support "my current os, whatever it is, plus linux", you can set `supportedArchitectures.os` to `["current", "linux"]`. + +## YN0078 - `RESOLUTION_MISMATCH` + +Starting from Yarn 4, Yarn will automatically enable the `--check-resolutions` flag on CI when it detects the current environment is a pull request. Under this mode, Yarn will check that the lockfile resolutions are consistent with what the initial range is. For example, given an initial dependency of `foo@npm:^1.0.0`: + +- `foo@npm:1.2.0` is a valid resolution +- `foo@npm:2.0.0` isn't a valid resolution, because it doesn't match the expected semver range +- `bar@npm:1.2.0` isn't a valid resolution either, because the name doesn't match + +This error should never trigger under normal circumstances, as Yarn should always generate satisfying resolutions given a dependency. If you hit it nonetheless, it may be either of two things: + +- Yarn has a bug. It may happen! Review the mismatch to be sure and, in case you have a doubt, ping us on Discord and we'll tell you whether it's something to worry about (before doing that, take a quick look at our [repository issues](https://github.com/yarnpkg/berry/issues?q=is%3Aissue+is%3Aopen+YN0078) in case someone reported the same behaviour). + +- Or you might have someone doing strange things on your lockfile. It might be a mistake (for example someone manually modifying a lockfile for debug but forgetting to revert the changes), or a problem (for example a malicious users trying to perform some sort of [supply chain attack](https://en.wikipedia.org/wiki/Supply_chain_attack)). + +If the use case appears legit (for example if the bug comes from Yarn), you can bypass the check on PRs by adding a `--no-check-resolutions` flag to your `yarn install` command. But be careful: this is a security feature; disabling it may have consequences. + +## YN0080 - `NETWORK_DISABLED` + +The `enableNetwork` flag is set to `false`, preventing any request to be made. + +Note that the Yarn configuration allows [`enableNetwork`](/configuration/yarnrc#enableNetwork) to be set on a per-registry basis via `npmRegistries`. + +## YN0081 - `NETWORK_UNSAFE_HTTP` + +Yarn will by default refuse to perform http (non-https) queries to protect you against accidental man-in-the-middle attacks. + +To bypass this protection, add the specified hostname to [`unsafeHttpWhitelist`](/configuration/yarnrc#unsafeHttpWhitelist). + +## YN0082 - `RESOLUTION_FAILED` + +Yarn failed to locate a package version that could satisfy the requested range. This usually happens with semver ranges that target versions not published yet (for example `^1.0.0` when the latest version is `0.9.0`), but can be also caused by a couple of other reasons: + +- The registry may not have been set properly (so Yarn is querying the public npm registry instead of your internal one) + +- The version may have been unpublished (although this shouldn't be possible for the public registry) + +## YN0083 - `AUTOMERGE_GIT_ERROR` + +When autofixing merge conflicts, Yarn needs to know what are the two lockfile versions it must merge together. To do that, it'll run `git rev-parse MERGE_HEAD HEAD` and/or `git rev-parse REBASE_HEAD HEAD`, depending on the situation. If both of those commands fail, the merge cannot succeed. + +This may happen if someone accidentally committed the lockfile without first resolving the merge conflicts - should that happen, you'll need to revert the lockfile to an earlier working version and run `yarn install`. + +## YN0085 - `UPDATED_RESOLUTION_RECORD` + +This message is printed when a lockfile entry is added or removed from a project. + +## YN0086 - `EXPLAIN_PEER_DEPENDENCIES_CTA` + +Peer dependencies are a little complex, and debugging them may require a lot of information. Since Yarn tries its best to keep messages on a single line, we provide a `yarn explain peer-requirements` command that goes into much more details than what we show on regular installs. + +To use it, simply pass it the `p`-prefixed code provided in the original peer resolution error message: + +``` +yarn explain peer-requirements pf649c +``` + +## YN0087 - `MIGRATION_SUCCESS` + +When migrating from a major version to the next, some default values may change. When that's the case, Yarn will attempt to temporarily keep the old default by pinning their values in your configuration settings. + +To see the exact changes applied when this message appears, check the content of the `.yarnrc.yml` file and any other file that may appear modified in your repository checkout. + +## YN0088 - `VERSION_NOTICE` + +On local machines, Yarn will periodically check whether new versions are available. Should one be, an informational message will be printed once, then silenced until the next day. + +You don't have to upgrade if you don't wish to - but keeping Yarn up-to-date is generally a good idea, as they tend to often come with a significant amount of performance improvements, bugfixes, and new features. + +## YN0089 - `TIPS_NOTICE` + +Our research showed that even our power users aren't always aware of some of the less obvious features in Yarn. To improve discoverability, on local machines, Yarn will display every day a tip about some of the nuggets it contains. Perhaps one of them will help you improve your infrastructure someday? + +## YN0090 - `OFFLINE_MODE_ENABLED` + +When enabled, the `enableOfflineMode` flag tells Yarn to ignore remote registries and only pull data from its internal caches. This is a handy mode when working from within network-constrained environments such as planes or trains. + +To leave the offline work mode, check how it got enabled by running `yarn config --why`. If ``, run `unset YARN_ENABLE_OFFLINE_MODE` in your terminal. Otherwise, remove the `enableOfflineMode` flag from the relevant `.yarnrc.yml` files. + +## YN0091 - `INVALID_PROVENANCE_ENVIRONMENT` + +This error is triggered when the [provenance statement](https://docs.npmjs.com/generating-provenance-statements) cannot be generated in the current environment. GitHub Actions and GitLab CI are the only supported environments at the moment, and this error is triggered when either running in another environment or when credentials are missing. + +On GitHub Actions, you need to grant the `write-id` permission to your workflow. Here is an example of how to do that: + +```yaml +name: Publish Package to npmjs +on: + push: + branches: [main] +jobs: + publish: + runs-on: ubuntu-latest # Must run on GitHub-hosted runners + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - run: npm install -g corepack && corepack enable + - run: yarn && yarn build + - run: yarn config set npmAuthToken '${{ secrets.NPM_TOKEN }}' + - run: yarn publish --provenance --tolerate-republish +``` + +On GitLab CI, you need to produce a `SIGSTORE_ID_TOKEN` for your workflow. Here is an example of how to do that: + +```yaml +publish: + image: "node:22" + rules: + - if: '$CI_COMMIT_BRANCH == "main"' + id_tokens: + SIGSTORE_ID_TOKEN: + aud: sigstore + script: + - npm install -g corepack && corepack enable + - yarn && yarn build + - yarn config set npmAuthToken $NPM_TOKEN + - yarn publish --provenance --tolerate-republish +``` diff --git a/website/src/docs/advanced/general-reference/lexicon.md b/website/src/docs/advanced/general-reference/lexicon.md new file mode 100644 index 00000000..5474c67d --- /dev/null +++ b/website/src/docs/advanced/general-reference/lexicon.md @@ -0,0 +1,212 @@ +--- +category: advanced +slug: advanced/lexicon +title: Lexicon +description: Definitions of common terms used throughout the documentation. +sidebar: + order: 2 +--- + +### Build Scripts + +Refers to tasks executed right after the packages got installed; typically the `postinstall` scripts configured in the `scripts` field from the manifest. + +Build scripts should be left to native dependencies, there is virtually no reason for pure JavaScript packages to use them. They have [significant side effects](/advanced/lifecycle-scripts#a-note-about-postinstall) on your user's projects, so weight carefully whether you really need them. + +See also: [Lifecycle Scripts](/advanced/lifecycle-scripts) + +### Dependency + +A dependency (listed in the `dependencies` field of the manifest) describes a relationship between two packages. + +When a package A has a dependency B, Yarn guarantees that A will be able to access B if the install is successful. Note that this is the only promise we make regarding regular dependencies: in particular, there is no guarantee that package B will be the same version than the one used in other parts of the application. + +See also: [Development Dependency](#development-dependency), [Peer Dependency](#peer-dependency) + +### Descriptor + +A descriptor is a combination of a package name (for example `lodash`) and a package range (for example `^1.0.0`). Descriptors are used to identify a set of packages rather than one unique package. + +### Development Dependency + +A dependency (listed in the `devDependencies` field of the manifest) describes a relationship between two packages. + +Development dependencies are very much like regular dependencies except that they only matter for local packages. Packages fetched from remote registries such as npm will not be able to access their development dependencies, but packages installed from local sources (such as [workspaces](#workspaces) or the [`portal:` protocol](#portals)) will. + +See also: [Dependency](#dependency), [Peer Dependency](#peer-dependency) + +### Fetcher + +Fetchers are the components tasked with extracting the full package data from a reference. For example, the npm fetcher would download the package tarballs from the npm registry. + +See also: [Architecture](/advanced/architecture), [Fetcher interface](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Fetcher.ts#L34) + +### Hoisting + +Hoisting is the act of transforming the dependency tree to optimize it by removing as many nodes as possible. There isn't a single way to decide how to transform the tree, and different package managers make different tradeoffs (some optimize for package popularity, package size, highest versions, ...). For this reason, no guarantee can be made regarding the final hoisting layout - except that packages will always be able to access the dependencies they listed in their [manifests](#Manifest). + +Because the hoisting is heavily connected to the filesystem and the Node resolution, its very design makes it easy to make an error and accidentally access packages without them being properly defined as dependencies - and thus without being accounted for during the hoisting process, making their very existence unpredictable. For this reason and others, hoisting got sidelined starting from Yarn 2 in favour of the [Plug'n'Play resolution](#plugnplay). + +### Linker + +Linkers are the components that consume both a dependency tree and a store of package data, and generate in return disk artifacts specific to the environment they target. For example, the Plug'n'Play linker generates a single `.pnp.cjs` file. + +See also: [Architecture](/advanced/architecture), [Installer interface](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Installer.ts#L18), [Linker interface](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Linker.ts#L28) + +### Local Cache + +The local cache, or offline mirror, is a way to protect your project against the package registry going down. + +When the local cache is enabled, Yarn generates a copy of all packages you install in the `.yarn/cache` folder that you can then add to your repository. Subsequent installs will then reuse packages from this folder rather than downloading them anew. + +While not always practical (it causes the repository size to grow, although we have ways to mitigate it significantly), it presents various interesting properties: + +- It doesn't require additional infrastructure, such as a [Verdaccio proxy](https://verdaccio.org/) +- It doesn't require additional configuration, such as registry authentication +- The install fetch step is as fast as it can be, with no data transfer at all +- It lets you reach [zero-installs](/features/caching#zero-installs) if you also use the PnP linker + +To enable the local cache, set `enableGlobalCache` to `false`, run an install, and add the new artifacts to your repository (you might want to [update your gitignore](/getting-started/qa#which-files-should-be-gitignored) accordingly). + +### Locator + +A locator is a combination of a package name (for example `lodash`) and a package reference (for example `1.2.3`). Locators are used to identify a single unique package (interestingly, all valid locators also are valid descriptors). + +### Manifest + +The manifest is the file defining the metadata associated to a package (its name, version, dependencies...). In the JavaScript ecosystem, it's the `package.json` file. + +### Monorepo + +A monorepo is a repository that contains multiple packages. [Babel](https://github.com/babel/babel/tree/master/packages), [Jest](https://github.com/facebook/jest/tree/master/packages), and even [Yarn itself](https://github.com/yarnpkg/yarn/tree/master/packages) are examples of such repositories - they each contain dozen of small packages that depend on one another. + +Yarn provides native support for monorepos via "workspaces". It makes it easy to install the dependencies of multiple local packages by running a single install, and to connect them all together so that they don't have to be published before their changes can be reused by other parts of your project. + +See also: [Workspaces (feature)](/features/workspaces), [Workspace (lexicon)](#workspace). + +### Package + +Packages are nodes of the dependency tree. Simply put, a package is a bundle of source code usually characterized by a `package.json` at its root. Packages can define dependencies, which are other packages that need to be made available for it to work properly. + +### Peer Dependency + +A dependency (listed in the `peerDependencies` field of the manifest) describes a relationship between two packages. + +Contrary to regular dependencies, a package A with a peer dependency on B doesn't guarantee that A will be able to access B - it's up to the package that depends on A to manually provide a version of B compatible with request from A. This drawback has a good side too: the package instance of B that A will access is guaranteed to be the exact same one as the one used by the ancestor of A. This matters a lot when B uses `instanceof` checks or singletons. + +See also: [Development Dependency](#development-dependency), [Singleton Package](#singleton-package) + +### Peer-Dependent Package + +A peer-dependent package is a package that lists peer dependencies. + +See also: [Virtual Packages](#virtual-package) + +### Plugin + +Plugins are a new concept introduced in Yarn 2+. Through the use of plugins, Yarn can be extended and made even more powerful - whether it's through the addition of new resolvers, fetchers, or linkers. + +See also: [Plugins](/features/extensibility), [Plugin interface](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Plugin.ts#L67) + +### Plug'n'Play + +Plug'n'Play is an alternative installation strategy that, instead of generating the typical `node_modules` directories, generate one single file that is then injected into Node to let it know where to find the installed packages. Starting from the v2, Plug'n'Play becomes the default installation strategy for Javascript projects. + +See also: [Plug'n'Play](/features/pnp) + +### PnP + +See [Plug'n'Play](#plugnplay) + +### Portal + +A portal is a dependency that uses the `portal:` protocol, pointing to a package located on the disk. + +Contrary to the `link:` protocol (which can point to any location but cannot have dependencies), Yarn will setup its dependency map in such a way that not only will the dependent package be able to access the file referenced through the portal, but the portal itself will also be able to access its own dependencies. Even peer dependencies! + +### Project + +The term project is used to encompass all the worktrees that belong to the same dependency tree. + +See also: [Workspaces](/features/workspaces) + +### Range + +A range is a string that, when combined with a package name, can be used to select multiple versions of a single package. Ranges typically follow semver, but can use any of the supported Yarn protocols. + +See also: [Protocols](/protocols) + +### Reference + +A reference is a string that, when combined with a package name, can be used to select one single version of a single package. References typically follow semver, but can use any of the supported Yarn protocols. + +See also: [Protocols](/protocols) + +### Resolver + +Resolvers are the components tasked from converting descriptors into locators, and extracting the package manifests from the package locators. For example, the npm resolver would check what versions are available on the npm registry and return all the candidates that satisfy the semver requirements, then would query the npm registry to fetch the full metadata associated with the selected resolution. + +See also: [Architecture](/advanced/architecture), [Resolver interface](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Resolver.ts#L45) + +### Scope + +Scopes are a term linked inherited from the npm registry; they are used to describe a set of packages that all belong to the same entity. For example, all the Yarn packages related to the v2 belong to the `berry` scope on the npm registry. Scopes are traditionally prefixed with the `@` symbol. + +### Singleton Package + +A singleton package is a package which is instantiated a single time across the dependency tree. + +While singleton packages aren't a first-class citizen, they can be easily created using [peer dependencies](#peer-dependency) by using one of their properties: since packages depended upon by peer dependencies are guaranteed to be the exact same instance as the one used by their direct ancestor, using peer dependencies across the entire dependency branch all the way up to the nearest workspace will ensure that a single instance of the package is ever created - making it a de-facto singleton package. + +See also: [Peer Dependency](#peer-dependency) + +### Transitive Dependency + +A transitive dependency is a dependency of a package you depend on. + +Imagine the case of `react`. Your application depends on it (you listed it yourself in your manifest), so it's a direct dependency. But `react` also depends on `prop-types`! That makes `prop-types` a transitive dependency, in that you don't directly declare it. + +### Unplugged Package + +With Yarn PnP, most packages are kept within their zip archives rather than being unpacked on the disk. The archives are then mounted on the filesystem at runtime, and transparently accessed. The mounts are read-only so that the archives don't get corrupted if something tries to write into them. + +In some cases, however, keeping the package read-only may be difficult (such as when a package lists postinstall scripts - the build steps will often need to generate build artifacts, making read-only folders impractical). For those situations, Yarn can unpack specific packages and keep them into their own individual folders. Such packages are referred to as "unplugged". + +Packages are unplugged in a few scenarios: + +- explicitly by setting the `dependenciesMeta[].unplugged` field to `true` +- explicitly when the package set its `preferUnplugged` field to `true` +- implicitly when the package lists postinstall scripts +- implicitly when the package contains native files + +### Virtual Package + +Because [peer-dependent packages](#peer-dependent-package) effectively define an _horizon_ of possible dependency sets rather than a single static set of dependencies, a peer-dependent package may have multiple dependency sets. When this happens, the package will need to be instantiated at least once for each such set. + +Since in Node-land the JS modules are instantiated based on their path (a file is never instantiated twice for any given path), and since PnP makes it so that packages are installed only once in any given project, the only way to instantiate those packages multiple times is to give them multiple paths while still referencing to the same on-disk location. That's where virtual packages come handy. + +Virtual packages are specialized instances of the peer-dependent packages that encode the set of dependencies that this particular instance should use. Each virtual package is given a unique filesystem path that ensures that the scripts it references will be instantiated with their proper dependency set. + +In the past virtual packages were implemented using symlinks, but this recently changed and they are now implemented through a virtual filesystem layer. This circumvents the need to create hundreds of confusing symlinks, improving compatibility with Windows and preventing issues that would arise with third-party tools calling `realpath`. + +### Workspace + +Generally speaking workspaces are a Yarn features used to work on multiple projects stored within the same repository. + +In the context of Yarn's vocabulary, workspaces are local packages that directly belong to a project. + +See also: [Workspaces](/features/workspaces) + +### Worktree + +A worktree is a private workspace that adds new child workspaces to the current project. + +See also: [Workspaces](/features/workspaces) + +### Yarn + +Yarn is a command line tool used to manage programming environments. Written in Javascript, it is mostly used along with other Javascript projects but has capabilities that make it suitable to be used in various situations. + +### Zero-Install + +See also: [Zero-Install](/features/caching#zero-installs) diff --git a/website/src/docs/advanced/general-reference/protocols.md b/website/src/docs/advanced/general-reference/protocols.md new file mode 100644 index 00000000..324dfc35 --- /dev/null +++ b/website/src/docs/advanced/general-reference/protocols.md @@ -0,0 +1,16 @@ +--- +title: "Protocols" +slug: protocols +sidebar: + order: 3 +--- + +- **[Exec Protocol](/protocol/exec)** +- **[File Protocol](/protocol/file)** +- **[Git Protocol](/protocol/git)** +- **[Link Protocol](/protocol/link)** +- **[NPM Protocol](/protocol/npm)** +- **[JSR Protocol](/protocol/jsr)** +- **[Patch Protocol](/protocol/patch)** +- **[Portal Protocol](/protocol/portal)** +- **[Workspace Protocol](/protocol/workspace)** diff --git a/website/src/docs/advanced/general-reference/protocols/_meta.yml b/website/src/docs/advanced/general-reference/protocols/_meta.yml new file mode 100644 index 00000000..84b62017 --- /dev/null +++ b/website/src/docs/advanced/general-reference/protocols/_meta.yml @@ -0,0 +1 @@ +label: Protocols diff --git a/website/src/docs/advanced/general-reference/protocols/exec.md b/website/src/docs/advanced/general-reference/protocols/exec.md new file mode 100644 index 00000000..4bf058e4 --- /dev/null +++ b/website/src/docs/advanced/general-reference/protocols/exec.md @@ -0,0 +1,91 @@ +--- +category: protocols +slug: protocol/exec +title: Exec Protocol +description: How exec dependencies work in Yarn. +--- + +The `exec:` protocol executes a Node.js script inside a temporary directory at fetch-time with a preconfigured runtime environment. This script is then expected to populate a special directory defined in the environment, and exit once the generation has finished. + +``` +yarn add my-pkg@exec:./package-builder.js +``` + +## Why would you want that + +Typical Yarn fetchers download packages from the internet - this works fine if the project you want to use got packaged beforehand, but fails short as soon as you need to bundle it yourself. Yarn's builtin mechanism allows you to run the `prepare` script on compatible git repositories and use the result as final package, but even that isn't always enough - you may need to clone a specific branch, go into a specific directory, run a specific build script ... all things that makes it hard for us to support every single use case. + +The `exec:` protocol represents a way to define yourself how the specified package should be fetched. In a sense, it can be seen as a more high-level version of the [Fetcher API](/advanced/lexicon#fetcher) that Yarn provides. + +## Generator scripts & `require` + +Because the generator will be called in a very special context (before any package has been installed on the disk), it won't be able to call the `require` function (not even with relative paths). Should you need very complex generators, just bundle them up beforehand in a single script using tools such as Webpack or Rollup. + +Because of this restriction, and because generators will pretty much always need to use the Node builtin modules, those are made available in the global scope - in a very similar way to what the Node REPL already does. As a result, no need to manually require the `fs` module: it's available through the global `fs` variable! + +## Runtime environment + +In order to let the script knows about the various predefined folders involved in the generation process, Yarn will inject a special `execEnv` global variable available to the script. This object's [interface](/api/plugin-exec/interface/ExecEnv) is defined as such: + +| Property | Type | Description | +| ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `tempDir` | `string` | Absolute path of an empty temporary directory that the script is free to use. Automatically created before the script is invoked. | +| `buildDir` | `string` | Absolute path of an empty directory where the script is expected to generate the package files. Automatically created before the script is invoked. | +| `locator` | `string` | Stringified [locator](/advanced/lexicon#locator) identifying the generator package. | + +You're free to do whatever you want inside `execEnv.tempDir` but, at the end of the execution, Yarn will expect `execEnv.buildDir` to contain the files that can be compressed into an archive and stored within the cache. + +## Examples + +Generate an hello world package: + +```ts +fs.writeFileSync( + path.join(execEnv.buildDir, "package.json"), + JSON.stringify({ + name: "hello-world", + version: "1.0.0", + }) +); + +fs.writeFileSync( + path.join(execEnv.buildDir, "index.js"), + ` + module.exports = 'hello world!'; +` +); +``` + +Clone a monorepo and build a specific package: + +```ts +const pathToRepo = path.join(execEnv.tempDir, "repo"); +const pathToArchive = path.join(execEnv.tempDir, "archive.tgz"); +const pathToSubpackage = path.join(pathToRepo, "packages/foobar"); + +// Clone the repository +child_process.execFileSync(`git`, [ + `clone`, + `git@github.com:foo/bar`, + pathToRepo, +]); + +// Install the dependencies +child_process.execFileSync(`yarn`, [`install`], { cwd: pathToRepo }); + +// Pack a specific workspace +child_process.execFileSync(`yarn`, [`pack`, `--out`, pathToArchive], { + cwd: pathToSubpackage, +}); + +// Send the package content into the build directory +child_process.execFileSync(`tar`, [ + `-x`, + `-z`, + `--strip-components=1`, + `-f`, + pathToArchive, + `-C`, + execEnv.buildDir, +]); +``` diff --git a/website/src/docs/advanced/general-reference/protocols/file.md b/website/src/docs/advanced/general-reference/protocols/file.md new file mode 100644 index 00000000..d9241155 --- /dev/null +++ b/website/src/docs/advanced/general-reference/protocols/file.md @@ -0,0 +1,30 @@ +--- +category: protocols +slug: protocol/file +title: File Protocol +description: How file dependencies work in Yarn. +--- + +The `file:` protocol fetches a package from a local path. This can be useful for testing packages locally before publishing them to the npm registry, or distributing a particular dependency without relying on the version from the npm registry. + +``` +yarn add my-pkg@file:./relative/path/to/dependency/folder +``` + +## Packing + +:::caution +Unlike [`git:` dependencies](/protocol/git), the folder pointed to by `file:` is not packed before being imported in the project. This is something we'll likely fix in a future major version. +::: + +## Folder-based links + +When `file:` points to a folder, Yarn will copy it rather than directly reference its sources. For the `node_modules` linker, it means that the content of the generated `node_modules` will be unique files, and that changes performed there won't affect the original source folder. + +``` +file:./relative/path/to/package.tgz +``` + +## Tarball-based links + +When `file:` points to a `.tgz` file, Yarn will transparently let you require files from within the archive. For the `node_modules` linker, it means that the archive will be unpacked into the generated `node_modules` folder. diff --git a/website/src/docs/advanced/general-reference/protocols/git.md b/website/src/docs/advanced/general-reference/protocols/git.md new file mode 100644 index 00000000..ae5897e0 --- /dev/null +++ b/website/src/docs/advanced/general-reference/protocols/git.md @@ -0,0 +1,44 @@ +--- +category: protocols +slug: protocol/git +title: Git Protocol +description: How git dependencies work in Yarn. +--- + +The `git:` protocol fetches packages directly from a git repository. This is useful when you need to use a version of a package that has not been published to the npm registry. + +``` +yarn add typanion@git@github.com/arcanis/typanion.git +``` + +## Packing + +The target repository won't be used as-is - it will first be packed using [`pack`](/cli/pack). + +:::note +To be sure the output is identical to what the linked repository would look like after being published, the packing will look at its configuration to decide which package manager to use. + +In other words, the project will be packed using Yarn if there's a `yarn.lock`, npm if there's a `package-lock.json`, or pnpm if there's a `pnpm-lock.yaml`. +::: + +## Commit pinning + +You can explicitly request a tag, commit, branch, or semver tag, by using one of those keywords (if you're missing the keyword, Yarn will look for the first thing that seems to match, as in prior versions): + +``` +git@github.com:yarnpkg/berry.git#tag=@yarnpkg/cli/2.2.0 +git@github.com:yarnpkg/berry.git#commit=a806c88 +git@github.com:yarnpkg/berry.git#head=master +``` + +## Workspaces support + +Workspaces can be cloned as long as the remote repository uses Yarn (or npm, in which case npm@>=7.x has to be installed on the system): + +``` +git@github.com:yarnpkg/berry.git#workspace=@yarnpkg/shell&tag=@yarnpkg/shell/2.1.0 +``` + +:::caution +Not all package managers support installing workspaces from git repositories; you shouldn't rely on this feature in your `dependencies` field if your package is meant to be published. +::: diff --git a/website/src/docs/advanced/general-reference/protocols/jsr.md b/website/src/docs/advanced/general-reference/protocols/jsr.md new file mode 100644 index 00000000..d2fda683 --- /dev/null +++ b/website/src/docs/advanced/general-reference/protocols/jsr.md @@ -0,0 +1,22 @@ +--- +category: protocols +slug: protocol/jsr +title: JSR Protocol +description: How JSR dependencies work in Yarn. +--- + +The `jsr:` protocol fetches packages from the [JSR registry](https://jsr.io/). + +``` +yarn add @luca/flag@jsr:2.0.0 +``` + +Note that because the JSR registry is responsible for compiling packages from TypeScript to JavaScript they sometimes re-pack packages. As a result, the Yarn lockfile contains the full tarball URLs. + +Quoting the [JSR documentation](https://jsr.io/docs/npm-compatibility): + +> The specific tarballs advertised for a given version of a package may change over time, even if the version itself is not changed. This is because the JSR registry may re-generate npm compatible tarballs for a package version to fix compatibility issues with npm or improve the transpile output in the generated tarball. We refer to this as the “revision” of a tarball. The revision of a tarball is not advertised in the npm registry endpoint, but it is included in the URL of the tarball itself and is included in the `package.json` file in the tarball at the `_jsr_revision` field. The revision of a tarball is not considered part of the package version, and does not affect semver resolution. +> +> However, tarball URLs are immutable. Tools that have a reference to a specific tarball URL will always be able to download that exact tarball. When a new revision of a tarball is generated, the old tarball is not deleted and will continue to be available at the same URL. The new tarball will be available at a new URL that includes the new revision. +> +> Because the tarball URL is included in package manager lock files, running `npm i` / `yarn` / `pnpm i` will never accidentally download a new revision of the tarball. diff --git a/website/src/docs/advanced/general-reference/protocols/link.md b/website/src/docs/advanced/general-reference/protocols/link.md new file mode 100644 index 00000000..98312a1c --- /dev/null +++ b/website/src/docs/advanced/general-reference/protocols/link.md @@ -0,0 +1,18 @@ +--- +category: protocols +slug: protocol/link +title: Link Protocol +description: How link dependencies work in Yarn. +--- + +The `link:` protocol lets you connect your project to an external directory. + +``` +yarn add imgs@link:./static/imgs +``` + +## Links vs portals + +Since the target referenced by `link:` may not exist until postinstall scripts have run, and since Yarn guarantees a predictable behavior regardless of the execution order, we need to disambiguate _packages_ (which contain `package.json` files, and may list dependencies of their own) from _arbitrary folders_ (which may not). + +In practice, this means that `link:` dependencies can only point to arbitrary folders - in other words, whatever it references cannot have a `package.json` file, and thus can't have dependencies. If you need to symlink to a _package_, use [portals](/protocol/portal) instead. diff --git a/website/src/docs/advanced/general-reference/protocols/npm.md b/website/src/docs/advanced/general-reference/protocols/npm.md new file mode 100644 index 00000000..b9d3673e --- /dev/null +++ b/website/src/docs/advanced/general-reference/protocols/npm.md @@ -0,0 +1,12 @@ +--- +category: protocols +slug: protocol/npm +title: Npm Protocol +description: How npm dependencies work in Yarn. +--- + +The default `npm:` protocol fetches packages from the npm registry. This is the protocol used by default if no protocol is specified in the package version. + +``` +yarn add react@17.0.2 +``` diff --git a/website/src/docs/advanced/general-reference/protocols/patch.md b/website/src/docs/advanced/general-reference/protocols/patch.md new file mode 100644 index 00000000..14286d51 --- /dev/null +++ b/website/src/docs/advanced/general-reference/protocols/patch.md @@ -0,0 +1,18 @@ +--- +category: protocols +slug: protocol/patch +title: Patch Protocol +description: How patch dependencies work in Yarn. +--- + +The `patch:` protocol applies a patch to the package source. This is useful for applying bug fixes that have not yet been merged into the main package source. + +``` +yarn add @types/react@patch:@types/react@18.0.0#./my-patches/react-types.patch +``` + +## Generating a patch + +Patches are meant to be generated by the [`yarn patch`](/cli/patch) workflow rather than created manually. + +Check the [patching documentation](/features/patching) for all details. diff --git a/website/src/docs/advanced/general-reference/protocols/portal.md b/website/src/docs/advanced/general-reference/protocols/portal.md new file mode 100644 index 00000000..19d54f23 --- /dev/null +++ b/website/src/docs/advanced/general-reference/protocols/portal.md @@ -0,0 +1,18 @@ +--- +category: protocols +slug: protocol/portal +title: Portal Protocol +description: How portal dependencies work in Yarn. +--- + +The `portal:` protocol is similar to the [`link:` protocol](/protocol/link) (it must be a relative path to a folder which will be made available without copies), but the target is assumed to be a [package](/advanced/lexicon#package) instead. + +``` +yarn add react-dom@portal:./my-react-dom +``` + +## Portals vs links + +Links have to operate under the assumption that their target folder may not exist until the install is finished; this prevents them from reading the content of the folder, including any `package.json` files, and in turn preventing them from listing dependencies. + +Portals, on the other hand, must exist at resolution time or an error is thrown. This lets them read the content of the `package.json` file and be treated like any other package in the dependency tree - except that its content will be made directly available to the user, rather than copied [like `file:` would do](/protocol/file). diff --git a/website/src/docs/advanced/general-reference/protocols/workspace.md b/website/src/docs/advanced/general-reference/protocols/workspace.md new file mode 100644 index 00000000..8a164135 --- /dev/null +++ b/website/src/docs/advanced/general-reference/protocols/workspace.md @@ -0,0 +1,12 @@ +--- +category: protocols +slug: protocol/workspace +title: Workspace Protocol +description: How workspace dependencies work in Yarn. +--- + +The `workspace:` protocol can be used to reference other workspaces in a monorepo setup. For more information, consult the dedicated page about [workspaces](/features/workspaces). + +``` +yarn add lib@workspace:^ +``` diff --git a/website/src/docs/advanced/package-management/_meta.yml b/website/src/docs/advanced/package-management/_meta.yml new file mode 100644 index 00000000..062be500 --- /dev/null +++ b/website/src/docs/advanced/package-management/_meta.yml @@ -0,0 +1,2 @@ +label: Package management +order: 2 diff --git a/website/src/docs/advanced/package-management/lifecycle-scripts.md b/website/src/docs/advanced/package-management/lifecycle-scripts.md new file mode 100644 index 00000000..6deaf6af --- /dev/null +++ b/website/src/docs/advanced/package-management/lifecycle-scripts.md @@ -0,0 +1,58 @@ +--- +category: advanced +slug: advanced/lifecycle-scripts +title: Lifecycle Scripts +description: An overview of Yarn's supported lifecycle scripts. +--- + +Packages can define in the `scripts` field of their manifest various actions that should be executed when the package manager executes a particular workflow. + +:::note +Note that we don't support every single lifecycle script originally present in npm. This is a deliberate decision based on the observation that too many lifecycle scripts make it difficult to know which one to use in which circumstances, leading to confusion and mistakes. We are open to add the missing ones on a case-by-case basis if compelling use cases are provided. + +In particular, we intentionally don't support arbitrary `pre` and `post` hooks for user-defined scripts (such as `prestart`). This behavior caused scripts to be implicit rather than explicit, obfuscating the execution flow. It also sometimes led to surprising behaviors, like `yarn serve` also running `yarn preserve`. +::: + +## `prepack` and `postpack` + +Those script are called right at the beginning and the end of each call to `yarn pack`. They are respectively meant to turn your package from development into production, and cleanup any lingering artifact. For instance, a typical `prepack` script would call Babel or TypeScript on the source directory to turn `.ts` files into `.js` files. + +:::note +Although rarely called directly, `yarn pack` is a crucial part of Yarn. Each time Yarn has to fetch a dependency from a "raw" source (such as a Git repository), it will automatically run `yarn install` and `yarn pack` to generate the package to use. +::: + +## `prepublish` + +This script is called before `yarn npm publish` before the package has even been packed. This is the place where you'll want to check that the project is in an ok state. + +:::caution +Because it's only called on prepublish, **the prepublish hook shouldn't have side effects.** In particular don't transpile the package sources in `prepublish`, as people consuming directly your repository (such as through the [`git:` protocol](/protocol/git)) wouldn't be able to use your project. Instead, use `prepack`. +::: + +## `postinstall` + +This script is called after the package dependency tree changed in any way -- usually after a dependency (or transitive dependency) got added, removed, or updated, but also sometimes when the project configuration or environment changed (for example when changing the Node.js version). + +It is guaranteed to be called in topological order (in other words, your dependencies' `postinstall` scripts will always run before yours). + +For backwards compatibility, the `preinstall` and `install` scripts, if presents, are called right before running the `postinstall` script from the same package. In general, prefer using `postinstall` over those two. + +:::caution +Postinstall scripts should be avoided at all cost, as they make installs slower and riskier. Many users will refuse to install dependencies that have `postinstall` scripts. Additionally, since the output isn't shown out of the box, using them to print a message to the user will not work as you expect. +::: + +## Environment variables + +When running scripts and binaries, some environment variables are usually made available: + +| Variable | Description | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `$INIT_CWD` | Directory from which the script has been invoked. This isn't the same as the cwd, which for scripts is always equal to the closest package root. | +| `$PROJECT_CWD` | Root of the project on the filesystem. | +| `$npm_package_name` | Name of the running package. | +| `$npm_package_version` | Version of the running package. | +| `$npm_package_json` | Absolute path to the `package.json` of the running package. | +| `$npm_execpath` | Absolute path to the Yarn binary. | +| `$npm_node_execpath` | Absolute path to the Node binary. | +| `$npm_config_user_agent` | String defining the Yarn version currently in use. | +| `$npm_lifecycle_event` | Name of the script or lifecycle event, if relevant. | diff --git a/website/src/docs/advanced/package-management/rulebook.md b/website/src/docs/advanced/package-management/rulebook.md new file mode 100644 index 00000000..0b620d40 --- /dev/null +++ b/website/src/docs/advanced/package-management/rulebook.md @@ -0,0 +1,117 @@ +--- +category: advanced +slug: advanced/rulebook +title: Rulebook +description: An in-depth rulebook of best-practices and recommendations regarding dependencies. +--- + +Writing portable packages is incredibly important, as it ensures that your users will benefit from an optimal experience regardless of their package manager. + +To help with that, this page details the up-to-date collection of good practices you should follow in order to make your package work seamlessly on all three main package managers (Yarn, pnpm, and npm), and explanations if you want to learn more. + +## Packages should only ever require what they formally list in their dependencies + +**Why?** Because otherwise your package will be susceptible to unpredictable [hoisting](/advanced/lexicon#hoisting) that will lead some of your consumers to experience pseudo-random crashes, depending on the other packages they will happen to use. + +Imagine that Alice uses Babel. Babel depends on an utility package which itself depends on an old version of Lodash. Since the utility package already depends on Lodash, Bob, the Babel maintainer, decided to use Lodash without formally declaring it in Babel itself. + +![](/2020-08-28-23-21-52.png) + +Because of the hoisting, Lodash will be put at the top, the tree becoming something like this: + +![](/2020-08-29-16-38-30.png) + +So far, everything is nice: the utility package can still require Lodash, but we no longer need to create sub-directories within Babel. Now, imagine that Alice also adds Gatsby to the mix, which we'll pretend also depends on Lodash, but this time on a modern release; the tree will look like this: + +![](/2020-08-29-16-34-13.png) + +The hoisting becomes more interesting - since Babel doesn't formally declare the dependency, two different hoisting layouts can happen. The first one is pretty much identical to what we already had before, with the exception that we now have two copies of Lodash, with only a single one hoisted to the stop so we don't cause a conflict: + +![](/2020-08-29-16-43-25.png) + +But a second layout is just as likely! And that's when things become trickier: + +![](/2020-08-29-16-46-00.png) + +First, let's check that this layout is valid: Gatsby still gets its Lodash 4 dependency, the Babel utility package still gets Lodash 1, and Babel itself still gets the utility package, just like before. But something else changed! Babel will no longer access Lodash 1! It'll instead retrieve the Lodash 4 copy that Gatsby provided, likely incompatible with whatever Babel originally expected. In the best case the application will crash, in the worst case it'll silently pass and generate incorrect results. + +If Babel had instead defined Lodash 1 as its own dependency, the package manager would have been able to encode this constraint and ensure that the requirement would have been met regardless of the hoisting. + +**Solution:** In most cases (when the missing dependency is a utility package), the fix is really just to add the missing entry to the [`dependencies` field](/configuration/manifest#dependencies). While often enough, a few more complex cases sometimes arise: + +- If your package is a plugin (for example `babel-plugin-transform-commonjs`) and the missing dependency is the core (for example `babel-core`), you would need to instead register the dependency inside the [`peerDependencies` field](/configuration/manifest#peerDependencies). + +- If your package is something that automatically loads plugins (for example `eslint`), peer dependencies obviously aren't an option as you can't reasonably list all plugins. Instead, you should use the [`createRequire` function](https://nodejs.org/api/module.html#module_module_createrequire_filename) (or its [polyfill](https://github.com/nuxt-contrib/create-require)) to load plugins _on behalf of_ the configuration file that lists the plugins to load - be it the package.json or a custom one like the `.eslintrc.js` file. + +- If your package only requires the dependency in specific cases that the user control (for example `mikro-orm` which only depends on `sqlite3` if the consumer actually uses a SQLite3 database), use the [`peerDependenciesMeta` field](/configuration/manifest#peerDependenciesMeta.optional) to declare the peer dependency as optional and silence any warning when unmet. + +- If your package is a meta-package of utilities (for example Next.js, which itself depends on Webpack so that its consumers don't have to do it), the situation is a bit complicated and you have two different options: + + - The preferred one is to list the dependency (in Next.js's case, `webpack`) as _both a regular dependency and a peer dependency_. Yarn will interpret this pattern as "peer dependency with a default", meaning that your users will be able to take ownership of the Webpack package if they need to, while still giving the package manager the ability to emit a warning if the provided version is incompatible with the one your package expects. + + - An alternative is to instead re-export the dependency as part of your public API. For example, Next could expose a `next/webpack` file that would only contain `module.exports = require('webpack')`, and consumers would require that instead of the typical `webpack` module. This isn't the recommended approach, however, because it wouldn't play well with plugins that expect Webpack to be a peer dependency (they wouldn't know that they need to use this `next/webpack` module instead). + +## Modules shouldn't hardcode `node_modules` paths to access other modules + +**Why?** The hoisting makes it impossible to be sure that the layout of the `node_modules` folder will always be the same. In fact, depending on the exact install strategy, the `node_modules` folders may not even exist. + +**Solution:** If you need to access one of your dependencies' files through the `fs` API (for example to read a dependency's `package.json`), just use `require.resolve` to obtain the path without having to make assumptions about the dependency location: + +```ts +const fs = require(`fs`); +const data = fs.readFileSync(require.resolve(`my-dep/package.json`)); +``` + +If you need to access one of your dependencies' dependency (we really don't recommend that, but in some fringe cases it may happen), instead of hardcoding the `node_modules` path, use the [`createRequire`](https://nodejs.org/api/module.html#module_module_createrequire_filename) function: + +```ts +const { createRequire } = require(`module`); +const firstDepReq = createRequire(require.resolve(`my-dep/package.json`)); +const secondDep = firstDepReq(`transitive-dep`); +``` + +Note that while `createRequire` is Node 12+, a polyfill exists under the name [`create-require`](https://github.com/nuxt-contrib/create-require). + +## User scripts shouldn't hardcode the `node_modules/.bin` folder + +**Why?** The `.bin` folder is an implementation detail, and may not exist at all depending on the install strategy. + +**Solution:** If you're writing a [script](/configuration/manifest#scripts), you can just refer to the binary by its name! So instead of `node_modules/.bin/jest -w`, prefer just writing `jest -w` which will work just fine. If for some reason `jest` isn't available, check that the current package properly [defines it as a dependency](#a-package-should-only-require-what-it-lists-in-its-dependencies). + +Sometimes you may find yourself having slightly more complex needs, for example if you wish to spawn a script with specific Node flags. Depending on the context we recommend passing options via the [`NODE_OPTIONS` environment variable](https://nodejs.org/api/cli.html#cli_node_options_options) rather than the CLI, but if that's not an option you can use `yarn bin name` to get the specified binary path: + +``` +yarn node --inspect $(yarn bin jest) +``` + +Note that, in this particular case, `yarn run` also supports the `--inspect` flag so you could just write: + +``` +yarn run --inspect jest +``` + +## Published packages should avoid using `npm run` in their scripts + +**Why?** This is a tricky one ... basically, it boils down to: package managers are not interchangeable. Using one package manager on a project installed by another is a recipe for troubles, as they follow different configuration settings and rules. For example, Yarn offers a hook system that allows its users to track which scripts are executed and how much time they take. Because `npm run` wouldn't know how to call these hooks, they would get ignore, leading to frustrating experiences for your consumers. + +**Solution:** While not the most esthetically pleasing option, the most portable one at the moment is to simply replace `npm run name` (or `yarn run name`) in your postinstall scripts and derived by the following: + +``` +$npm_execpath run +``` + +The `$npm_execpath` environment variable will get replaced by the right binary depending on the package manager your consumers will use. Yarn also supports just calling `run ` without any mention of the package manager, but to this date no other package manager does. + +## Packages should never write inside their own folder outside of postinstall + +**Why?** Depending on the install strategy, packages may be kept in read-only data stores where write accesses will be rejected. This is particularly true when using "system-global" stores, where modifying the sources for one package would risk corrupting all the projects depending on it from the same machine. + +**Solution:** Just write in another directory rather than your own package. Anything would work, but a very common idiom is to use the `node_modules/.cache` folder in order to store cache data - that's for example what Babel, Webpack, and more do. + +If you absolutely need to write into your package's source folder (but really, we never came across this use case before), you still have the option to use [`preferUnplugged`](/configuration/manifest#preferUnplugged) to instruct Yarn to disable optimizations on your package and store it inside its own project-local copy, where you'll be able to mutate it at will. + +## Packages should use the `prepack` script to generate dist files before publishing + +**Why?** The original npm supported [many different scripts](https://docs.npmjs.com/misc/scripts). So much, in fact, that it became very difficult to know which script one would want to use in which context. In particular, the very subtle differences between the `prepack`, `prepare`, `prepublish`, and `prepublish-only` scripts led many to use the wrong script in the wrong context. For this reason, Yarn 2 deprecated most of the scripts and consolidated them around a restricted set of portable scripts. + +**Solution:** Always use the `prepack` script if you wish to generate dist artifacts before publishing your package. It will get called before calling `yarn pack` (which itself is called before calling `yarn npm publish`), when cloning your git repository as a git dependency, and any time you will run `yarn prepack`. As for `prepublish`, never use it with side effects - its only use should be to run tests before the publish step. diff --git a/website/src/docs/advanced/pnp/_meta.yml b/website/src/docs/advanced/pnp/_meta.yml new file mode 100644 index 00000000..b628c806 --- /dev/null +++ b/website/src/docs/advanced/pnp/_meta.yml @@ -0,0 +1,2 @@ +label: Working with Yarn PnP +order: 3 diff --git a/website/src/docs/advanced/pnp/pnp-api.md b/website/src/docs/advanced/pnp/pnp-api.md new file mode 100644 index 00000000..26dc19b8 --- /dev/null +++ b/website/src/docs/advanced/pnp/pnp-api.md @@ -0,0 +1,382 @@ +--- +category: advanced +slug: advanced/pnpapi +title: PnP API +description: In-depth documentation of the PnP API. +sidebar: + order: 1 +--- + +## Overview + +Every script running within a Plug'n'Play runtime environment has access to a special builtin module (`pnpapi`) that allows you to introspect the dependency tree at runtime. + +## Data Structures + +### `PackageLocator` + +```ts +export type PackageLocator = { + name: string; + reference: string; +}; +``` + +A package locator is an object describing one unique instance of a package in the dependency tree. The `name` field is guaranteed to be the name of the package itself, but the `reference` field should be considered an opaque string whose value may be whatever the PnP implementation decides to put there. + +Note that one package locator is different from the others: the top-level locator (available through `pnp.topLevel`, cf below) sets _both_ `name` and `reference` to `null`. This special locator will always mirror the top-level package (which is generally the root of the repository, even when working with workspaces). + +### `PackageInformation` + +```ts +export type PackageInformation = { + packageLocation: string; + packageDependencies: Map; + packagePeers: Set; + linkType: "HARD" | "SOFT"; +}; +``` + +The package information set describes the location where the package can be found on the disk, and the exact set of dependencies it is allowed to require. The `packageDependencies` values are meant to be interpreted as such: + +- If a string, the value is meant to be used as a reference in a locator whose name is the dependency name. + +- If a `[string, string]` tuple, the value is meant to be used as a locator whose name is the first element of the tuple and reference is the second one. This typically occurs with package aliases (such as `"foo": "npm:bar@1.2.3"`). + +- If `null`, the specified dependency isn't available at all. This typically occurs when a package's peer dependency didn't get provided by its direct parent in the dependency tree. + +The `packagePeers` field, if present, indicates which dependencies have an enforced contract on using the exact same instance as the package that depends on them. This field is rarely useful in pure PnP context (because our instantiation guarantees are stricter and more predictable than this), but is required to properly generate a `node_modules` directory from a PnP map. + +The `linkType` field is only useful in specific cases - it describes whether the producer of the PnP API was asked to make the package available through a hard linkage (in which case all the `packageLocation` field is reputed being owned by the linker) or a soft linkage (in which case the `packageLocation` field represents a location outside of the sphere of influence of the linker). + +## Runtime Constants + +### `process.versions.pnp` + +When operating under PnP environments, this value will be set to a number indicating the version of the PnP standard in use (which is strictly identical to `require('pnpapi').VERSIONS.std`). + +This value is a convenient way to check whether you're operating under a Plug'n'Play environment (where you can `require('pnpapi')`) or not: + +```js +if (process.versions.pnp) { + // do something with the PnP API ... +} else { + // fallback +} +``` + +### `require('module')` + +The `module` builtin module is extended when operating within the PnP API with one extra function: + +```ts +export function findPnpApi(lookupSource: URL | string): PnpApi | null; +``` + +When called, this function will traverse the filesystem hierarchy starting from the given `lookupSource` in order to locate the closest `.pnp.cjs` file. It'll then load this file, register it inside the PnP loader internal store, and return the resulting API to you. + +Note that while you'll be able to resolve the dependencies by using the API returned to you, you'll need to make sure they are properly _loaded_ on behalf of the project too, by using `createRequire`: + +```ts +const { createRequire, findPnpApi } = require(`module`); + +// We'll be able to inspect the dependencies of the module passed as first argument +const targetModule = process.argv[2]; + +const targetPnp = findPnpApi(targetModule); +const targetRequire = createRequire(targetModule); + +const resolved = targetPnp.resolveRequest(`eslint`, targetModule); +const instance = targetRequire(resolved); // <-- important! don't use `require`! +``` + +Finally, it can be noted that `findPnpApi` isn't actually needed in most cases and we can do the same with just `createRequire` thanks to its `resolve` function: + +```ts +const { createRequire } = require(`module`); + +// We'll be able to inspect the dependencies of the module passed as first argument +const targetModule = process.argv[2]; + +const targetRequire = createRequire(targetModule); + +const resolved = targetRequire.resolve(`eslint`); +const instance = targetRequire(resolved); // <-- still important +``` + +### `require('pnpapi')` + +When operating under a Plug'n'Play environment, a new builtin module will appear in your tree and will be made available to all your packages (regardless of whether they define it in their dependencies or not): `pnpapi`. It exposes the constants a function described in the rest of this document. + +Note that we've reserved the `pnpapi` package name on the npm registry, so there's no risk that anyone will be able to snatch the name for nefarious purposes. We might use it later to provide a polyfill for non-PnP environments (so that you'd be able to use the PnP API regardless of whether the project got installed via PnP or not), but as of now it's still an empty package. + +Note that the `pnpapi` builtin is _contextual_: while two packages from the same dependency tree are guaranteed to read the same one, two packages from different dependency trees will get different instances - each reflecting the dependency tree they belong to. This distinction doesn't often matter except sometimes for project generator (which typically run within their own dependency tree while also manipulating the project they're generating). + +## API Interface + +### `VERSIONS` + +```ts +export const VERSIONS: { std: number; [key: string]: number }; +``` + +The `VERSIONS` object contains a set of numbers that detail which version of the API is currently exposed. The only version that is guaranteed to be there is `std`, which will refer to the version of this document. Other keys are meant to be used to describe extensions provided by third-party implementors. Versions will only be bumped when the signatures of the public API change. + +:::note +The current version is 3. We bump it responsibly and strive to make each version backward-compatible with the previous ones, but as you can probably guess some features are only available with the latest versions. +::: + +### `topLevel` + +```ts +export const topLevel: { name: null; reference: null }; +``` + +The `topLevel` object is a simple package locator pointing to the top-level package of the dependency tree. Note that even when using workspaces you'll still only have one single top-level for the entire project. + +This object is provided for convenience and doesn't necessarily needs to be used; you may create your own top-level locator by using your own locator literal with both fields set to `null`. + +:::note +These special top-level locators are merely aliases to physical locators, which can be accessed by calling `findPackageLocator`. +::: + +### `getLocator(...)` + +```ts +export function getLocator( + name: string, + referencish: string | [string, string] +): PackageLocator; +``` + +This function is a small helper that makes it easier to work with "referencish" ranges. As you may have seen in the `PackageInformation` interface, the `packageDependencies` map values may be either a string or a tuple - and the way to compute the resolved locator changes depending on that. To avoid having to manually make a `Array.isArray` check, we provide the `getLocator` function that does it for you. + +Just like for `topLevel`, you're under no obligation to actually use it - you're free to roll your own version if for some reason our implementation wasn't what you're looking for. + +### `getDependencyTreeRoots(...)` + +```ts +export function getDependencyTreeRoots(): PackageLocator[]; +``` + +The `getDependencyTreeRoots` function will return the set of locators that constitute the roots of individual dependency trees. In Yarn, there is exactly one such locator for each workspace in the project. + +:::note +This function will always return the physical locators, so it'll never return the special top-level locator described in the `topLevel` section. +::: + +### `getAllLocators(...)` + +```ts +export function getAllLocators(): PackageLocator[]; +``` + +:::danger +This function is not part of the Plug'n'Play specification and only available as a Yarn extension. In order to use it, you first must check that the [`VERSIONS`](/advanced/pnpapi#versions) dictionary contains a valid `getAllLocators` property. +::: + +The `getAllLocators` function will return all locators from the dependency tree, in no particular order (although it'll always be a consistent order between calls for the same API). It can be used when you wish to know more about the packages themselves, but not about the exact tree layout. + +### `getPackageInformation(...)` + +```ts +export function getPackageInformation( + locator: PackageLocator +): PackageInformation; +``` + +The `getPackageInformation` function returns all the information stored inside the PnP API for a given package. + +### `findPackageLocator(...)` + +```ts +export function findPackageLocator(location: string): PackageLocator | null; +``` + +Given a location on the disk, the `findPackageLocator` function will return the package locator for the package that "owns" the path. For example, running this function on something conceptually similar to `/path/to/node_modules/foo/index.js` would return a package locator pointing to the `foo` package (and its exact version). + +:::note +This function will always return the physical locators, so it'll never return the special top-level locator described in the `topLevel` section. You can leverage this property to extract the physical locator for the top-level package: +::: + +```ts +const virtualLocator = pnpApi.topLevel; +const physicalLocator = pnpApi.findPackageLocator( + pnpApi.getPackageInformation(virtualLocator).packageLocation +); +``` + +### `resolveToUnqualified(...)` + +```ts +export function resolveToUnqualified( + request: string, + issuer: string | null, + opts?: { considerBuiltins?: boolean } +): string | null; +``` + +The `resolveToUnqualified` function is maybe the most important function exposed by the PnP API. Given a request (which may be a bare specifier like `lodash`, or an relative/absolute path like `./foo.js`) and the path of the file that issued the request, the PnP API will return an unqualified resolution. + +For example, the following: + +``` +lodash/uniq +``` + +Might very well be resolved into: + +``` +/my/cache/lodash/1.0.0/node_modules/lodash/uniq +``` + +As you can see, the `.js` extension didn't get added. This is due to the difference between [qualified and unqualified resolutions](#qualified-vs-unqualified-resolutions). In case you must obtain a path ready to be used with the filesystem API, prefer using `resolveRequest` instead. + +Note that in some cases you may just have a folder to work with as `issuer` parameter. When this happens, just suffix the issuer with an extra slash (`/`) to indicate to the PnP API that the issuer is a folder. + +This function will return `null` if the request is a builtin module, unless `considerBuiltins` is set to `false`. + +### `resolveUnqualified(...)` + +```ts +export function resolveUnqualified( + unqualified: string, + opts?: { extensions?: string[] } +): string; +``` + +The `resolveUnqualified` function is mostly provided as an helper; it reimplements the Node resolution for file extensions and folder indexes, but not the regular `node_modules` traversal. It makes it slightly easier to integrate PnP into some projects, although it isn't required in any way if you already have something that fits the bill. + +To give you an example `resolveUnqualified` isn't needed with `enhanced-resolved`, used by Webpack, because it already implements its own way the logic contained in `resolveUnqualified` (and more). Instead, we only have to leverage the lower-level `resolveToUnqualified` function and feed it to the regular resolver. + +For example, the following: + +``` +/my/cache/lodash/1.0.0/node_modules/lodash/uniq +``` + +Might very well be resolved into: + +``` +/my/cache/lodash/1.0.0/node_modules/lodash/uniq/index.js +``` + +### `resolveRequest(...)` + +```ts +export function resolveRequest(request: string, issuer: string | null, opts?: {considerBuiltins?: boolean, extensions?: string[]]}): string | null; +``` + +The `resolveRequest` function is a wrapper around both `resolveToUnqualified` and `resolveUnqualified`. In essence, it's a bit like calling `resolveUnqualified(resolveToUnqualified(...))`, but shorter. + +Just like `resolveUnqualified`, `resolveRequest` is entirely optional and you might want to skip it to directly use the lower-level `resolveToUnqualified` if you already have a resolution pipeline that just needs to add support for Plug'n'Play. + +For example, the following: + +``` +lodash +``` + +Might very well be resolved into: + +``` +/my/cache/lodash/1.0.0/node_modules/lodash/uniq/index.js +``` + +This function will return `null` if the request is a builtin module, unless `considerBuiltins` is set to `false`. + +### `resolveVirtual(...)` + +```ts +export function resolveVirtual(path: string): string | null; +``` + +:::danger +This function is not part of the Plug'n'Play specification and only available as a Yarn extension. In order to use it, you first must check that the [`VERSIONS`](/advanced/pnpapi#versions) dictionary contains a valid `resolveVirtual` property. +::: + +The `resolveVirtual` function will accept any path as parameter and return the same path minus any [virtual component](/advanced/lexicon#virtual-package). This makes it easier to store the location to the files in a portable way as long as you don't care about losing the dependency tree information in the process (requiring files through those paths will prevent them from accessing their peer dependencies). + +## Qualified vs Unqualified Resolutions + +This document detailed two types of resolutions: qualified and unqualified. Although similar, they present different characteristics that make them suitable in different settings. + +The difference between qualified and unqualified resolutions lies in the quirks of the Node resolution itself. Unqualified resolutions can be statically computed without ever accessing the filesystem, but only can only resolve relative paths and bare specifiers (like `lodash`); they won't ever resolve the file extensions or folder indexes. By contrast, qualified resolutions are ready to be used to access the filesystem. + +Unqualified resolutions are the core of the Plug'n'Play API; they represent data that cannot be obtained any other way. If you're looking to integrate Plug'n'Play inside your resolver, they're likely what you're looking for. On the other hand, fully qualified resolutions are handy if you're working with the PnP API as a one-off and just want to obtain some information on a given file or package. + +Two great options for two different use cases 🙂 + +## Accessing the files + +The paths returned in the `PackageInformation` structures are in the native format (so Posix on Linux/OSX and Win32 on Windows), but they may reference files outside of the typical filesystem. This is particularly true for Yarn, which references packages directly from within their zip archives. + +To access such files, you can use the `@yarnpkg/fslib` project which abstracts the filesystem under a multi-layer architecture. For example, the following code would make it possible to access any path, regardless of whether they're stored within a zip archive or not: + +```ts +const { PosixFS, ZipOpenFS } = require(`@yarnpkg/fslib`); +const libzip = require(`@yarnpkg/libzip`).getLibzipSync(); + +// This will transparently open zip archives +const zipOpenFs = new ZipOpenFS({ libzip }); + +// This will convert all paths into a Posix variant, required for cross-platform compatibility +const crossFs = new PosixFS(zipOpenFs); + +console.log(crossFs.readFileSync(`C:\\path\\to\\archive.zip\\package.json`)); +``` + +## Traversing the dependency tree + +The following function implements a tree traversal in order to print the list of locators from the tree. + +:::danger +This implementation iterates over **all** the nodes in the tree, even if they are found multiple times (which is very often the case). As a result the execution time is way higher than it could be. Optimize as needed 🙂 +::: + +```ts +const pnp = require(`pnpapi`); +const seen = new Set(); + +const getKey = (locator) => JSON.stringify(locator); + +const isPeerDependency = (pkg, parentPkg, name) => + getKey(pkg.packageDependencies.get(name)) === + getKey(parentPkg.packageDependencies.get(name)); + +const traverseDependencyTree = (locator, parentPkg = null) => { + // Prevent infinite recursion when A depends on B which depends on A + const key = getKey(locator); + if (seen.has(key)) return; + + const pkg = pnp.getPackageInformation(locator); + console.assert(pkg, `The package information should be available`); + + seen.add(key); + + console.group(locator.name); + + for (const [name, referencish] of pkg.packageDependencies) { + // Unmet peer dependencies + if (referencish === null) continue; + + // Avoid iterating on peer dependencies - very expensive + if (parentPkg !== null && isPeerDependency(pkg, parentPkg, name)) continue; + + const childLocator = pnp.getLocator(name, referencish); + traverseDependencyTree(childLocator, pkg); + } + + console.groupEnd(locator.name); + + // Important: This `delete` here causes the traversal to go over nodes even + // if they have already been traversed in another branch. If you don't need + // that, remove this line for a hefty speed increase. + seen.delete(key); +}; + +// Iterate on each workspace +for (const locator of pnp.getDependencyTreeRoots()) { + traverseDependencyTree(locator); +} +``` diff --git a/website/src/docs/advanced/pnp/pnp-spec.md b/website/src/docs/advanced/pnp/pnp-spec.md new file mode 100644 index 00000000..38cd0593 --- /dev/null +++ b/website/src/docs/advanced/pnp/pnp-spec.md @@ -0,0 +1,350 @@ +--- +category: advanced +slug: advanced/pnp-spec +title: PnP Specification +description: In-depth documentation of the PnP spec. +sidebar: + order: 2 +--- + +## About this document + +To make interoperability easier for third-party projects, this document describes the specification we follow when installing files on disk under the [Plug'n'Play install strategy](/features/pnp). It also means: + +- any change we make to this document will follow semver rules +- we'll do our best to preserve backward compatibility +- new features will be intended to gracefully degrade + +## High-level idea + +Plug'n'Play works by keeping in memory a table of all packages part of the dependency tree, in such a way that we can easily answer two different questions: + +- Given a path, what package does it belong to? +- Given a package, where are the dependencies it can access? + +Resolving a package import thus becomes a matter of interlacing those two operations: + +- First, locate which package is requesting the resolution +- Then retrieve its dependencies, check if the requested package is amongst them +- If it is, then retrieve the dependency information, and return its location + +Extra features can then be designed, but are optional. For example, Yarn leverages the information it knows about the project to throw semantic errors when a dependency cannot be resolved: since we know the state of the whole dependency tree, we also know why a package may be missing. + +## Basic concepts + +All packages are uniquely referenced by **locators**. A locator is a combination of a **package ident**, which includes its scope if relevant, and a **package reference**, which can be seen as a unique ID used to distinguish different instances (or versions) of a same package. The package references should be treated as an opaque value: it doesn't matter from a resolution algorithm perspective that they start with `workspace:`, `virtual:`, `npm:`, or any other protocol. + +### Portability + +For portability reasons, all paths inside of the manifests: + +- must use the unix path format (`/` as separators). +- must be relative to the manifest folder (so they are the same regardless of the location of the project on disk). + +:::caution +All algorithms in this specification assume that paths have been normalized according to these two rules. +::: + +## Fallback + +For improved compatibility with legacy codebases, Plug'n'Play supports a feature we call "fallback". The fallback triggers when a package makes a resolution request to a dependency it doesn't list in its dependencies. In normal circumstances the resolver would throw, but when the fallback is enabled the resolver should first try to find the dependency packages amongst the dependencies of a set of special packages. If it finds it, it then returns it transparently. + +In a sense, the fallback can be seen as a limited and safer form of hoisting. While hoisting allows unconstrainted access through multiple levels of dependencies, the fallback requires to explicitly define a fallback package - usually the top-level one. + +## Package locations + +While the Plug'n'Play specification doesn't by itself require runtimes to support anything else than the regular filesystem when accessing package files, producers may rely on more complex data storage mechanisms. For instance, Yarn itself requires the two following extensions which we strongly recommend to support: + +### Zip access + +Files named `*.zip` must be treated as folders for the purpose of file access. For instance, `/foo/bar.zip/package.json` requires to access the `package.json` file located within the `/foo/bar.zip` zip archive. + +If writing a JS tool, the [`@yarnpkg/fslib`](https://yarnpkg.com/package/@yarnpkg/fslib) package may be of assistance, providing a zip-aware filesystem layer called `ZipOpenFS`. + +### Virtual folders + +In order to properly represent packages listing peer dependencies, Yarn relies on a concept called [Virtual Packages](/advanced/lexicon#virtual-package). Their most notable property is that they all have different paths (so that Node.js instantiates them as many times as needed), while still being baked by the same concrete folder on disk. + +This is done by adding path support for the following scheme: + +``` +/path/to/some/folder/__virtual__///subpath/to/file.dat +``` + +When this pattern is found, the `__virtual__//` part must be removed, the `hash` ignored, and the `dirname` operation applied `n` times to the `/path/to/some/folder` part. Some examples: + +``` +/path/to/some/folder/__virtual__/a0b1c2d3/0/subpath/to/file.dat +/path/to/some/folder/subpath/to/file.dat + +/path/to/some/folder/__virtual__/e4f5a0b1/0/subpath/to/file.dat +/path/to/some/folder/subpath/to/file.dat (different hash, same result) + +/path/to/some/folder/__virtual__/a0b1c2d3/1/subpath/to/file.dat +/path/to/some/subpath/to/file.dat + +/path/to/some/folder/__virtual__/a0b1c2d3/3/subpath/to/file.dat +/path/subpath/to/file.dat +``` + +If writing a JS tool, the [`@yarnpkg/fslib`](https://yarnpkg.com/package/@yarnpkg/fslib) package may be of assistance, providing a virtual-aware filesystem layer called `VirtualFS`. + +:::note +The `__virtual__` folder name appeared with Yarn 3.0. Earlier releases used `$$virtual`, but we changed it after discovering that this pattern triggered bugs in software where paths were used as either regexps or replacement. For example, `$$` found in the second parameter from [`String.prototype.replace`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) silently turned into `$`. +::: + +## Manifest reference + +When [`pnpEnableInlining`](/configuration/yarnrc#pnpEnableInlining) is explicitly set to `false`, Yarn will generate an additional `.pnp.data.json` file containing the following fields. + +This document only covers the data file itself - you should define your own in-memory data structures, populated at runtime with the information from the manifest. For example, Yarn turns the `packageRegistryData` table into two separate memory tables: one that maps a path to a package, and another that maps a package to a path. + +:::note +You may notice that various places use arrays of tuples in place of maps. This is mostly intended to make it easier to hydrate ES6 maps, but also sometimes to have non-string keys (for instance `packageRegistryData` will have a `null` key in one particular case). +::: + +## Resolution algorithm + +:::note +For simplicity, this algorithm doesn't mention all the Node.js features that allow mapping a module to another, such as [`imports`](https://nodejs.org/api/packages.html#imports), [`exports`](https://nodejs.org/api/packages.html#exports), or other vendor-specific features. +::: + +### NM_RESOLVE + +``` +NM_RESOLVE(specifier, parentURL) +``` + +1. This function is specified in the [Node.js documentation](https://nodejs.org/api/esm.html#resolver-algorithm-specification) + +### PNP_RESOLVE + +``` +PNP_RESOLVE(specifier, parentURL) +``` + +1. Let `resolved` be **undefined** + +2. If `specifier` is a Node.js builtin, then + + 1. Set `resolved` to `specifier` itself and return it + +3. Otherwise, if `specifier` is either an absolute path or a path prefixed with "./" or "../", then + + 1. Set `resolved` to [`NM_RESOLVE`](#nm_resolve)`(specifier, parentURL)` and return it + +4. Otherwise, + + 1. Note: `specifier` is now a bare identifier + + 2. Let `unqualified` be [`RESOLVE_TO_UNQUALIFIED`](#resolve_to_unqualified)`(specifier, parentURL)` + + 3. Set `resolved` to [`NM_RESOLVE`](#nm_resolve)`(unqualified, parentURL)` + +### RESOLVE_TO_UNQUALIFIED + +``` +RESOLVE_TO_UNQUALIFIED(specifier, parentURL) +``` + +1. Let `resolved` be **undefined** + +2. Let `ident` and `modulePath` be the result of [`PARSE_BARE_IDENTIFIER`](#parse_bare_identifier)`(specifier)` + +3. Let `manifest` be [`FIND_PNP_MANIFEST`](#find_pnp_manifest)`(parentURL)` + +4. If `manifest` is null, then + + 1. Set `resolved` to [`NM_RESOLVE`](#nm_resolve)`(specifier, parentURL)` and return it + +5. Let `parentLocator` be [`FIND_LOCATOR`](#find_locator)`(manifest, parentURL)` + +6. If `parentLocator` is null, then + + 1. Set `resolved` to [`NM_RESOLVE`](#nm_resolve)`(specifier, parentURL)` and return it + +7. Let `parentPkg` be [`GET_PACKAGE`](#get_package)`(manifest, parentLocator)` + +8. Let `referenceOrAlias` be the entry from `parentPkg.packageDependencies` referenced by `ident` + +9. If `referenceOrAlias` is **null** or **undefined**, then + + 1. If `manifest.enableTopLevelFallback` is **true**, then + + 1. If `parentLocator` **isn't** in `manifest.fallbackExclusionList`, then + + 1. Let `fallback` be [`RESOLVE_VIA_FALLBACK`](#resolve_via_fallback)`(manifest, ident)` + + 2. If `fallback` is neither **null** nor **undefined** + + 1. Set `referenceOrAlias` to `fallback` + +10. If `referenceOrAlias` is still **undefined**, then + + 1. Throw a resolution error + +11. If `referenceOrAlias` is still **null**, then + + 1. Note: It means that `parentPkg` has an unfulfilled peer dependency on `ident` + + 2. Throw a resolution error + +12. Otherwise, if `referenceOrAlias` is an array, then + + 1. Let `alias` be `referenceOrAlias` + + 2. Let `dependencyPkg` be [`GET_PACKAGE`](#get_package)`(manifest, alias)` + + 3. Return `path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)` + +13. Otherwise, + + 1. Let `reference` be `referenceOrAlias` + + 2. Let `dependencyPkg` be [`GET_PACKAGE`](#get_package)`(manifest, {ident, reference})` + + 3. Return `path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)` + +### GET_PACKAGE + +``` +GET_PACKAGE(manifest, locator) +``` + +1. Let `referenceMap` be the entry from `parentPkg.packageRegistryData` referenced by `locator.ident` + +2. Let `pkg` be the entry from `referenceMap` referenced by `locator.reference` + +3. Return `pkg` + + 1. Note: `pkg` cannot be **undefined** here; all packages referenced in any of the Plug'n'Play data tables [**MUST**](#must) have a corresponding entry inside `packageRegistryData`. + +### FIND_LOCATOR + +``` +FIND_LOCATOR(manifest, moduleUrl) +``` + +:::note +The algorithm described here is quite inefficient. You should make sure to prepare data structure more suited for this task when you read the manifest. +::: + +1. Let `bestLength` be **0** + +2. Let `bestLocator` be **null** + +3. Let `relativeUrl` be the relative path between `manifest` and `moduleUrl` + + 1. Note: The relative path must not start with `./`; trim it if needed + +4. If `relativeUrl` matches `manifest.ignorePatternData`, then + + 1. Return **null** + +5. Let `relativeUrlWithDot` be `relativeUrl` prefixed with `./` or `../` as necessary + +6. For each `referenceMap` value in `manifest.packageRegistryData` + + 1. For each `registryPkg` value in `referenceMap` + + 1. If `registryPkg.discardFromLookup` **isn't true**, then + + 1. If `registryPkg.packageLocation.length` is greater than `bestLength`, then + + 1. If `relativeUrl` starts with `registryPkg.packageLocation`, then + + 1. Set `bestLength` to `registryPkg.packageLocation.length` + + 2. Set `bestLocator` to the current `registryPkg` locator + +7. Return `bestLocator` + +### RESOLVE_VIA_FALLBACK + +``` +RESOLVE_VIA_FALLBACK(manifest, ident) +``` + +1. Let `topLevelPkg` be [`GET_PACKAGE`](#get_package)`(manifest, {null, null})` + +2. Let `referenceOrAlias` be the entry from `topLevelPkg.packageDependencies` referenced by `ident` + +3. If `referenceOrAlias` is defined, then + + 1. Return it immediately + +4. Otherwise, + + 1. Let `referenceOrAlias` be the entry from `manifest.fallbackPool` referenced by `ident` + + 2. Return it immediately, whether it's defined or not + +### FIND_PNP_MANIFEST + +``` +FIND_PNP_MANIFEST(url) +``` + +Finding the right PnP manifest to use for a resolution isn't always trivial. There are two main options: + +- Assume that there is a single PnP manifest covering the whole project. This is the most common case, as even when referencing third-party projects (for example via the [`portal:` protocol](/protocol/portal#portals-vs-links)) their dependency trees are stored in the same manifest as the main project. + + To do that, call [`FIND_CLOSEST_PNP_MANIFEST`](#find_closest_pnp_manifest)`(require.main.filename)` once at the start of the process, cache its result, and return it for each call to [`FIND_PNP_MANIFEST`](#find_pnp_manifest) (if you're running in Node.js, you can even use `require.resolve('pnpapi')` which will do this work for you). + +- Try to operate within a multi-project world. **This is rarely required**. We support it inside the Node.js PnP loader, but only because of "project generator" tools like `create-react-app` which are run via `yarn create react-app` and require two different projects (the generator one `and` the generated one) to cooperate within the same Node.js process. + + Supporting this use case is difficult, as it requires a bookkeeping mechanism to track the manifests used to access modules, reusing them as much as possible and only looking for a new one when the chain breaks. + +### FIND_CLOSEST_PNP_MANIFEST + +``` +FIND_CLOSEST_PNP_MANIFEST(url) +``` + +1. Let `manifest` be **null** + +2. Let `directoryPath` be the directory for `url` + +3. Let `pnpPath` be `directoryPath` concatenated with `/.pnp.cjs` + +4. If `pnpPath` exists on the filesystem, then + + 1. Let `pnpDataPath` be `directoryPath` concatenated with `/.pnp.data.json` + + 2. Set `manifest` to `JSON.parse(readFile(pnpDataPath))` + + 3. Set `manifest.dirPath` to `directoryPath` + + 4. Return `manifest` + +5. Otherwise, if `directoryPath` is `/`, then + + 1. Return **null** + +6. Otherwise, + + 1. Return [`FIND_PNP_MANIFEST`](#find_pnp_manifest)`(directoryPath)` + +### PARSE_BARE_IDENTIFIER + +``` +PARSE_BARE_IDENTIFIER(specifier) +``` + +1. If `specifier` starts with "@", then + + 1. If `specifier` doesn't contain a "/" separator, then + + 1. Throw an error + + 2. Otherwise, + + 1. Set `ident` to the substring of `specifier` until the second "/" separator or the end of string, whatever happens first + +2. Otherwise, + + 1. Set `ident` to the substring of `specifier` until the first "/" separator or the end of string, whatever happens first + +3. Set `modulePath` to the substring of `specifier` starting from `ident.length` + +4. Return `{ident, modulePath}` diff --git a/website/src/docs/advanced/pnp/pnpify.md b/website/src/docs/advanced/pnp/pnpify.md new file mode 100644 index 00000000..86c32126 --- /dev/null +++ b/website/src/docs/advanced/pnp/pnpify.md @@ -0,0 +1,48 @@ +--- +category: advanced +slug: advanced/pnpify +title: PnPify +description: An overview of PnPify, one of the PnP compatibility layers which emulates virtual node_modules directories and provides IDE support. +sidebar: + order: 3 +--- + +:::danger +PnPify is mostly deprecated since Yarn supports [`node_modules`](/configuration/yarnrc#nodeLinker) installs out of the box. +::: + +## Motivation + +Plug'n'Play is, by design, compatible with all projects that only make use of the `require` API - whether it's `require`, `require.resolve`, or `createRequire`. However, some rare projects prefer to reimplement the Node resolution themselves and as such aren't compatible by default with our environment (unless they integrate their resolvers with the [PnP API](/advanced/pnpapi)). + +## PnPify + +PnPify is a tool designed to work around these compatibility issues. It's not perfect - it brings its own set of caveats and doesn't allow you to leverage all the features that PnP has to offer - but it's often good enough to unblock you until better solutions are implemented. + +How it works is simple: when a non-PnP-compliant project tries to access the `node_modules` directories (for example through `readdir` or `readFile`), PnPify intercepts those calls and converts them into calls to the PnP API. Then, based on the result, it simulates the existence of a virtual `node_modules` folder that the underlying tool will then consume - still unaware that the files are extracted from a virtual filesystem. + +## Usage + +1. Add PnPify to your dependencies: + +```bash +yarn add @yarnpkg/pnpify +``` + +2. Use pnpify to run the incompatible tool: + +```bash +yarn pnpify tsc +``` + +More details about the run command can be found on its [dedicated page](https://yarnpkg.com/cli/pnpify/run). + +## Caveat + +- Due to how PnPify emulates the `node_modules` directory, some problems are to be expected, especially with tools that watch directories inside `node_modules`. + +- PnPify isn't designed to be a long-term solution; its purpose is purely to help projects during their transition to the stricter Plug'n'Play module resolution scheme. Relying on PnPify doesn't allow you to take full advantage of everything Plug'n'Play has to offer, in particular perfect flattening and boundary checks. + +## IDE Support + +When using Plug'n'Play installs with your favorite text editors you will probably want to keep using your extensions, like ESLint or Prettier. To do so, you may need to use `yarn sdks`. For more information, consult the detailed documentation in the [Editor SDKs](/getting-started/editor-sdks) section. diff --git a/website/src/docs/advanced/technical/_meta.yml b/website/src/docs/advanced/technical/_meta.yml new file mode 100644 index 00000000..faad93f7 --- /dev/null +++ b/website/src/docs/advanced/technical/_meta.yml @@ -0,0 +1,2 @@ +label: Working with Yarn +order: 4 diff --git a/website/src/docs/advanced/technical/architecture.md b/website/src/docs/advanced/technical/architecture.md new file mode 100644 index 00000000..a7901b97 --- /dev/null +++ b/website/src/docs/advanced/technical/architecture.md @@ -0,0 +1,42 @@ +--- +category: advanced +slug: advanced/architecture +title: Architecture +description: An overview of Yarn's architecture. +--- + +## General architecture + +Yarn works through a core package (published as `@yarnpkg/core`) that exposes the various base components that make up a project. Some of the components are classes that you might recognize from the API: `Configuration`, `Project`, `Workspace`, `Cache`, `Manifest`, and others. All those are provided by the core package. + +The core itself doesn't do much - it merely contains the logic required to manage a project. In order to use this logic from the command-line Yarn provides an indirection called `@yarnpkg/cli` which, interestingly, doesn't do much either. It however has two very important responsibilities: it hydrates a project instance based on the current directory (`cwd`), and inject the prebuilt Yarn plugins into the environment. + +See, Yarn is built in modular way that allow most of the business logic related to third-party interactions to be externalized inside their own package - for example the [npm resolver](https://github.com/yarnpkg/berry/tree/master/packages/plugin-npm) is but one plugin amongst many others. This design gives us a much simpler codebase to work with (hence an increased development speed and stabler product), and offers plugin authors the ability to write their own external logic without having to modify the Yarn codebase itself. + +## Install architecture + +What happens when running `yarn install` can be summarized in a few different steps: + +1. First we enter the "resolution step": + + - First we load the entries stored within the lockfile, then based on those data and the current state of the project (that it figures out by reading the manifest files, aka `package.json`) the core runs an internal algorithm to find out which entries are missing. + + - For each of those missing entries, it queries the plugins using the [`Resolver`](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Resolver.ts) interface, and asks them whether they would know about a package that would match the given descriptor ([`supportsDescriptor`](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Resolver.ts#L54)) and its exact identity ([`getCandidates`](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Resolver.ts#L114)) and transitive dependency list ([`resolve`](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Resolver.ts#L123)). + + - Once it has obtained a new list of package metadata, the core starts a new resolution pass on the transitive dependencies of the newly added packages. This will be repeated until it figures out that all packages from the dependency tree now have their metadata stored within the lockfile. + + - Finally, once every package range from the dependency tree has been resolved into metadata, the core builds the tree in memory one last time in order to generate what we call "virtual packages". In short, those virtual packages are split instances of the same base package - we use them to disambiguate all packages that list peer dependencies, whose dependency set would change depending on their location in the dependency tree (consult [this lexicon entry](/advanced/lexicon#virtualpackages) for more information). + +2. Once the resolution is done, we enter the "fetch step": + + - Now that we have the exact set of packages that make up our dependency tree, we iterate over it and for each of them we start a new request to the cache to know whether the package is anywhere to be found. If it isn't we do just like we did in the previous step and we ask our plugins (through the [`Fetcher`](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Fetcher.ts) interface) whether they know about the package ([`supports`](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Fetcher.ts#L43)) and if so to retrieve it from whatever its remote location is ([`fetch`](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Fetcher.ts#L67)). + + - Interesting tidbit regarding the fetchers: they communicate with the core through an abstraction layer over `fs`. We do this so that our packages can come from many different sources - it can be from a zip archive for packages downloaded from a registry, or from an actual directory on the disk for [`portal:`](/protocol/portal) dependencies. + +3. And finally, once all the packages are ready for consumption, comes the "link step": + + - In order to work properly, the packages you use must be installed on the disk in some way. For example, in the case of a native Node application, your packages would have to be installed into a set of `node_modules` directories so that they could be located by the interpreter. That's what the linker is about. Through the [`Linker`](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Linker.ts) and [`Installer`](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Installer.ts) interfaces the Yarn core will communicate with the registered plugins to let them know about the packages listed in the dependency tree, and describe their relationships (for example it would tell them that `tapable` is a dependency of `webpack`). The plugins can then decide what to do of this information in whatever way they see fit. + + - Doing this means that new linkers can be created for other programming languages pretty easily - you just need to write your own logic regarding what should happen from the packages provided by Yarn. Want to generate an `__autoload.php`? Do it! Want to setup a Python virtual env? No problem! + + - Something else that's pretty cool is that the packages from within the dependency tree don't have to all be of the same type. Our plugin design allows instantiating multiple linkers simultaneously. Even better - the packages can depend on one another across linkers! You could have a JavaScript package depending on a Python package (which is technically the case of `node-gyp`, for example). diff --git a/website/src/docs/advanced/technical/changelog.md b/website/src/docs/advanced/technical/changelog.md new file mode 100644 index 00000000..86042b68 --- /dev/null +++ b/website/src/docs/advanced/technical/changelog.md @@ -0,0 +1,8 @@ +--- +category: advanced +slug: advanced/changelog +title: Changelog +description: All changes, version by version. +--- + +See the [CHANGELOG on GitHub](https://github.com/yarnpkg/zpm/blob/main/CHANGELOG.md) for the full list of changes. diff --git a/website/src/docs/advanced/technical/contributing.md b/website/src/docs/advanced/technical/contributing.md new file mode 100644 index 00000000..f561115d --- /dev/null +++ b/website/src/docs/advanced/technical/contributing.md @@ -0,0 +1,156 @@ +--- +category: advanced +slug: advanced/contributing +title: Contributing +description: Yarn's contributing guide. +--- + +Thanks for being here! Yarn gives a lot of importance to being a community project, and we rely on your help as much as you rely on ours. In order to help you help us, we've invested in an infra and documentation that should make contributing to Yarn very easy. If you have any feedback on what we could improve, please open an issue to discuss it! + +## Opening an issue + +Issues have to follow the issue template. Make sure to follow everything, with a special attention for the reproduction. If we can't reproduce your problem, we won't solve it. + +## How can you help? + +- Review our documentation! We often aren't native english speakers, and our grammar might be a bit off. Any help we can get that makes our documentation more digestible is appreciated! + +- Talk about Yarn in your local meetups! Even our users aren't always aware of some of our features. Learn, then share your knowledge with your own circles! + +- Help with our infra! There are always small improvements to do: run tests faster, uniformize the test names, improve the way our version numbers are setup, ... + +- Write code! We have so many features we want to implement, and so little time to actually do it... Any help you can afford will be appreciated, and you will have the satisfaction to know that your work helped literally millions of developers! + +## Finding work to do + +It might be difficult to know where to start on a fresh codebase. To help a bit with this, we try to mark various issues with tags meant to highlight issues that we think don't require as much context as others: + +- [Good First Issue](https://github.com/yarnpkg/berry/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) are typically self-contained features of a limited scope that are a good way to get some insight as to how Yarn works under the hood. + +- [Help Wanted](https://github.com/yarnpkg/berry/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) are issues that don't require a lot of context but also have less impact than the ones who do, so no core maintainer has the bandwidth to work on them. + +Finally, feel free to pop on our [Discord channel](https://discordapp.com/invite/yarnpkg) to ask for help and guidance. We're always happy to see new blood, and will help you our best to make your first open-source contribution a success! + +## Writing your feature + +Our repository is set up in such a way that calling `yarn` inside it will always use the TypeScript sources themselves - you don't have to rebuild anything for your changes to be applied there (we use `esbuild` to automatically transpile the files as we require them). The downside is that it's slower than the regular Yarn, but the improved developer experience is well worth it. + +```bash +yarn install # Will automatically pick up any changes you made to sources +``` + +## Testing your code + +We currently have two testsuites, built for different purposes. The first one is unit tests and can be triggered by running the following command from anywhere within the repository: + +```bash +yarn test:unit +``` + +While various subcomponents that have a strict JS interface contract are tested via unit tests (for example the portable shell library, or the various util libraries we ship), Yarn as a whole relies on integration tests. Being much closer to what our users experience, they give us a higher confidence when refactoring the application that everything will work according to plan. Those tests can be triggered by running the following command (again, from anywhere within the repository): + +```bash +yarn build:cli +yarn test:integration +``` + +Note that because we want to avoid adding the `esbuild` overhead to each Yarn call the CLI will need to be prebuilt for the integration tests to run - that's what the `yarn build:cli` command is for. This unfortunately means that you will need to rebuild the CLI after each modification if you want the integration tests to pick up your changes. + +Both unit tests and integration tests use Jest, which means that you can filter the tests you want to run by using the `-t` flag (or simply the file path): + +```bash +yarn test:unit yarnpkg-shell +yarn test:integration -t 'it should correctly install a single dependency that contains no sub-dependencies' +``` + +Should you need to write a test (and you certainly will if you add a feature or fix a bug 😉), they are located in the following directories: + +- **Unit tests:** [`packages/*/tests`](https://github.com/search?utf8=%E2%9C%93&q=repo%3Ayarnpkg%2Fberry+filename%3Atest.ts+language%3ATypeScript+language%3ATypeScript&type=Code&ref=advsearch&l=TypeScript&l=TypeScript) +- **Integration tests:** [`packages/acceptance-tests/pkg-test-specs/sources`](https://github.com/yarnpkg/berry/tree/master/packages/acceptance-tests/pkg-tests-specs/sources) + +The `makeTemporaryEnv` utility generates a very basic temporary environment just for the context of your test. The first parameter will be used to generate a `package.json` file, the second to generate a `.yarnrc.yml` file, and the third is the callback that will be run once the temporary environment has been created. + +## Formatting your code + +Before submitting your code for review, please make sure your code is properly formatted by using the following command from anywhere within the repository: + +```bash +yarn test:lint +``` + +We use ESLint to check this, so using the `--fix` flag will cause ESLint to attempt to automatically correct most errors that might be left in your code: + +```bash +yarn test:lint --fix +``` + +## Checking Constraints + +We use [constraints](/features/constraints) to enforce various rules across the repository. They are declared inside the [`constraints.pro` file](https://github.com/yarnpkg/berry/blob/master/constraints.pro) and their purposes are documented with comments. + +Constraints can be checked with `yarn constraints`, and fixed with `yarn constraints --fix`. Generally speaking: + +- Workspaces must not depend on conflicting ranges of dependencies. Use the `-i,--interactive` flag and select "Reuse" when installing dependencies and you shouldn't ever have to deal with this rule. + +- Workspaces must not depend on non-workspace ranges of available workspaces. Use the `-i,--interactive` flag and select "Reuse" or "Attach" when installing dependencies and you shouldn't ever have to deal with this rule. + +- Workspaces that are part of the standard bundle or plugins must have specific build scripts. The ones that aren't, must be declared inside the `constraints.pro` file with `inline_compile`. + +- Workspaces must point our repository through the `repository` field. + +## Preparing your PR to be released + +In order to track which packages need to be released, we use the workflow described in the [following document](/features/release-workflow). To summarize, you must run `yarn version check --interactive` on each PR you make, and select which packages should be released again for your changes to be effective (and to which version), if any. + +You can check if you've set everything correctly with `yarn version check`. + +If you expect a package to have to be released again but Yarn doesn't offer you this choice, first check whether the name of your local branch is `master`. If that's the case, Yarn might not be able to detect your changes (since it will do it against `master`, which is yourself). Run the following commands: + +```bash +git checkout -b my-feature +git checkout - +git reset --hard upstream/master +git checkout - +yarn version check --interactive +``` + +If it fails and you have no idea why, feel free to ping a maintainer and we'll do our best to help you. + +:::note +If you modify one of the [default plugins](https://github.com/yarnpkg/berry#default-plugins), you will also need to bump `@yarnpkg/cli`. +::: + +## Reviewing other PRs + +You're welcome to leave comments if you spot glaring bugs, but do not approve PRs if you're not a member. +It's generally seen as [bad form](https://twitter.com/brian_d_vaughn/status/1224051534536667137) in the open source community. + +## Writing documentation + +We use [Docusaurus](https://docusaurus.io/docs) to generate HTML pages from [mdx](https://mdxjs.com/docs/) sources files. + +Our website is stored within the [`packages/docusaurus`](https://github.com/yarnpkg/berry/tree/master/packages/docusaurus) directory. You can change a page by modifying the corresponding `.mdx` file in the `docs` folder. For example, you'd edit this very page [here](https://github.com/yarnpkg/berry/blob/master/packages/docusaurus/docs/advanced/04-technical/contributing.mdx). + +Then run the following command to spawn a local server and see your changes: + +```bash +yarn start +``` + +Once you're happy with what the documentation looks like, just commit your local changes and open a PR. Netlify will pick up your changes and create a fresh preview for everyone to see: + +![](https://user-images.githubusercontent.com/1037931/61949789-3cc09300-afac-11e9-9817-89e97771a4e1.png) + +## Profiling + +Run the following command to generate an unminified bundle: + +```bash +yarn build:cli --no-minify +``` + +Use a profiler on the generated bundle at `packages/yarnpkg-cli/bundles/yarn.js`. Here is an example which uses the Node.js built-in profiler: + +```bash +YARN_IGNORE_PATH=1 node --prof packages/yarnpkg-cli/bundles/yarn.js +``` diff --git a/website/src/docs/advanced/technical/plugin-tutorial.md b/website/src/docs/advanced/technical/plugin-tutorial.md new file mode 100644 index 00000000..550e55d4 --- /dev/null +++ b/website/src/docs/advanced/technical/plugin-tutorial.md @@ -0,0 +1,192 @@ +--- +category: advanced +slug: advanced/plugin-tutorial +title: Plugin Tutorial +description: A basic plugin tutorial which shows how to work with Yarn's plugin API. +--- + +Starting from the Yarn 2, Yarn now supports plugins. For more information about what they are and in which case you'd want to use them, consult the [dedicated page](/features/extensibility). We'll talk here about the exact steps needed to write one. It's quite simple, really! + +## What does a plugin look like? + +Plugins are scripts that get loaded at runtime by Yarn, and that can inject new behaviors into it. They also can require some packages provided by Yarn itself, such as `@yarnpkg/core`. This allows you to use the exact same core API as the Yarn binary currently in use, kinda like if it was a peer dependency! + +:::note +Since plugins are loaded before Yarn starts (and thus before you make your first install), it's strongly advised to write your plugins in such a way that they work without dependencies. If that becomes difficult, know that we provide a powerful tool ([`@yarnpkg/builder`](#all-in-one-plugin-builder) that can bundle your plugins into a single Javascript file, ready to be published. +::: + +## Writing our first plugin + +Open in a text editor a new file called `plugin-hello-world.js`, and type the following code: + +```js +module.exports = { + name: `plugin-hello-world`, + factory: (require) => ({ + // What is this `require` function, you ask? It's a `require` + // implementation provided by Yarn core that allows you to + // access various packages (such as @yarnpkg/core) without + // having to list them in your own dependencies - hence + // lowering your plugin bundle size, and making sure that + // you'll use the exact same core modules as the rest of the + // application. + // + // Of course, the regular `require` implementation remains + // available, so feel free to use the `require` you need for + // your use case! + }), +}; +``` + +We have our plugin, but now we need to register it so that Yarn knows where to find it. To do this, we'll just add an entry within the `.yarnrc.yml` file at the root of the repository: + +```yaml +plugins: + - ./plugin-hello-world.js +``` + +That's it! You have your first plugin, congratulations! Of course it doesn't do much (or anything at all, really), but we'll see how to extend it to make it more powerful. + +## All-in-one plugin builder + +As we saw, plugins are meant to be standalone JavaScript source files. It's very possible to author them by hand, especially if you only need a small one, but once you start adding multiple commands it can become a bit more complicated. To make this process easier, we maintain a package called `@yarnpkg/builder`. This builder is to Yarn what Next.js is to web development - it's a tool designed to help creating, building, and managing complex plugins written in TypeScript. + +Its documentation can be found on the [dedicated page](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-builder/README.md), but remember that you're not required to use it. Sometimes good old scripts are just fine! + +## Adding commands + +Plugins can also register their own commands. To do this, we just have to write them using the [`clipanion`](https://github.com/arcanis/clipanion) library - and we don't even have to add it to our dependencies! Let's see an example: + +```js +module.exports = { + name: `plugin-hello-world`, + factory: (require) => { + const { BaseCommand } = require(`@yarnpkg/cli`); + + class HelloWorldCommand extends BaseCommand { + static paths = [[`hello`]]; + + async execute() { + this.context.stdout.write(`This is my very own plugin 😎\n`); + } + } + + return { + commands: [HelloWorldCommand], + }; + }, +}; +``` + +Now, try to run `yarn hello`. You'll see your message appear! Note that you can use the full set of features provided by clipanion, including short options, long options, variadic argument lists, ... You can even validate your options using the [`typanion`](https://github.com/arcanis/typanion) library, which we provide. Here's an example where we only accept numbers as parameter: + +```js +module.exports = { + name: `plugin-addition`, + factory: (require) => { + const { BaseCommand } = require(`@yarnpkg/cli`); + const { Command, Option } = require(`clipanion`); + const t = require(`typanion`); + + class AdditionCommand extends BaseCommand { + static paths = [[`addition`]]; + + // Show descriptive usage for a --help argument passed to this command + static usage = Command.Usage({ + description: `hello world!`, + details: ` + This command will print a nice message. + `, + examples: [[`Add two numbers together`, `yarn addition 42 10`]], + }); + + a = Option.String({ validator: t.isNumber() }); + b = Option.String({ validator: t.isNumber() }); + + async execute() { + this.context.stdout.write(`${this.a}+${this.b}=${this.a + this.b}\n`); + } + } + + return { + commands: [AdditionCommand], + }; + }, +}; +``` + +## Using hooks + +Plugins can register to various events in the Yarn lifetime, and provide them additional information to alter their behavior. To do this, you just need to declare a new `hooks` property in your plugin and add members for each hook you want to listen to: + +```js +module.exports = { + name: `plugin-hello-world`, + factory: (require) => ({ + hooks: { + setupScriptEnvironment(project, scriptEnv) { + scriptEnv.HELLO_WORLD = `my first plugin!`; + }, + }, + }), +}; +``` + +In this example, we registered to the `setupScriptEnvironment` hook and used it to inject an argument into the environment. Now, each time you'll run a script, you'll see that your env will contain a new value called `HELLO_WORLD`! + +Hooks are numerous, and we're still working on them. Some might be added, removed, or changed, based on your feedback. So if you'd like to do something hooks don't allow you to do yet, come tell us! + +## Using the Yarn API + +Most of Yarn's hooks are called with various arguments that tell you more about the context under which the hook is being called. The exact argument list is different for each hook, but in general they are of the types defined in the `@yarnpkg/core` library. + +In this example, we will integrate with the `afterAllInstalled` hook in order to print some basic information about the dependency tree after each install. This hook gets invoked with an additional parameter that is the public `Project` instance where lie most of the information Yarn has collected about the project: dependencies, package manifests, workspace information, and so on. + +```js +const fs = require(`fs`); +const util = require(`util`); + +module.exports = { + name: `plugin-project-info`, + factory: (require) => { + const { structUtils } = require(`@yarnpkg/core`); + + return { + default: { + hooks: { + afterAllInstalled(project) { + let descriptorCount = 0; + for (const descriptor of project.storedDescriptors.values()) + if (!structUtils.isVirtualDescriptor(descriptor)) + descriptorCount += 1; + + let packageCount = 0; + for (const pkg of project.storedPackages.values()) + if (!structUtils.isVirtualLocator(pkg)) packageCount += 1; + + console.log( + `This project contains ${descriptorCount} different descriptors that resolve to ${packageCount} packages` + ); + }, + }, + }, + }; + }, +}; +``` + +This is getting interesting. As you can see, we accessed the `storedDescriptors` and `storedPackages` fields from our project instance, and iterated over them to obtain the number of non-virtual items (virtual packages are described in more details [here](/advanced/lexicon#virtual-package)). This is a very simple use case, but we could have done many more things: the project root is located in the `cwd` property, the workspaces are exposed as `workspaces`, the link between descriptors and packages can be made via `storedResolutions`, ... etc. + +Note that we've only scratched the surface of the `Project` class instance! The Yarn core provides many other classes (and hooks) that allow you to work with the cache, download packages, trigger http requests, ... and much more. Next time you want to write a plugin, give it a look, there's almost certainly an utility there that will allow you to avoid having to reimplement the wheel. + +## Dynamically loading plugins using the `YARN_PLUGINS` environment variable + +While plugins are usually declared inside `.yarnrc.yml` configuration files, those represent the user-facing configuration that third-party tools shouldn't modify without the user's permission. + +The `YARN_PLUGINS` environment variable is a semicolon-separated list of plugin paths that Yarn will dynamically load when called. Paths are resolved relative to the `startingCwd` Yarn is called in. + +Packages can use this mechanism to dynamically register plugins and query the Yarn API using commands without having to explicitly depend on the Yarn packages and deal with potential version mismatches. + +## Official hooks + +Our new website doesn't support generating the hook list yet; sorry :( diff --git a/website/src/docs/advanced/technical/telemetry.md b/website/src/docs/advanced/technical/telemetry.md new file mode 100644 index 00000000..f1d2b964 --- /dev/null +++ b/website/src/docs/advanced/technical/telemetry.md @@ -0,0 +1,43 @@ +--- +category: advanced +slug: advanced/telemetry +title: Telemetry +description: An overview of Yarn's telemetry collection. +--- + +## Why does Yarn need some telemetry? + +As maintainers, it's sometimes difficult to know what we should prioritize. Are large monorepos the most common situation our users encounter? What packageExtensions are the most common? How many people opted-out to the nm linker? Etc. + +Additionally, because of the lack of telemetry, some projects also had trouble taking us seriously. Various threads in the Node docker image repositories suggested to remove Yarn from the Docker image, citing Yarn as a fringe tool. Our team doesn't have time to spend collecting the various polls from the surface of the earth, nor should we have to. + +## Will my information go to Facebook? + +No. [Yarn is not a Facebook project](/getting-started/qa#is-yarn-operated-by-facebook), and Facebook won't receive **any** amount of data collected this way, and neither will Google, or Microsoft. + +The data we collect are stored on [Datadog](https://www.datadoghq.com/), a trusted large-scale monitoring company with a heavy focus on security. + +## Which information are we talking about? + +As of today, we collect the following: + +- The Yarn version +- Which command name is used (but not its arguments) +- The active plugin names (only for our own plugins; yours are hidden) +- The number of installs run during the week +- The number of different projects having been installed +- How many installs for the nm linker +- The number of workspaces +- The number of dependencies +- The packageExtensions field (name of extended + name of the extra dependency) +- The IP address (most providers unfortunately don't let us remove that) + +Data are sent via batches, roughly every seven days. This prevents us from tracking your usage with a too high granularity, leaving us only the most useful information to do our job efficiently. + +## How can I disable it? + +Note that, regardless of the configuration, the telemetry won't ever run on CI. + +- To disable it on a project (including for anyone who would clone it), run `yarn config set enableTelemetry 0`. This will make our life ever so slightly more difficult, so please consider whether it's really what you want to do. + +- To disable it on your whole machine (but not for anyone else), run `yarn config set --home enableTelemetry 0`. diff --git a/website/src/docs/appendix/pnp-api.md b/website/src/docs/appendix/pnp-api.md new file mode 100644 index 00000000..6a615b8d --- /dev/null +++ b/website/src/docs/appendix/pnp-api.md @@ -0,0 +1,382 @@ +--- +category: appendix +slug: appendix/pnpapi +title: Plug'n'Play API +description: In-depth documentation of the PnP API. +sidebar: + order: 1 +--- + +## Overview + +Every script running within a Plug'n'Play runtime environment has access to a special builtin module (`pnpapi`) that allows you to introspect the dependency tree at runtime. + +## Data Structures + +### `PackageLocator` + +```ts +export type PackageLocator = { + name: string; + reference: string; +}; +``` + +A package locator is an object describing one unique instance of a package in the dependency tree. The `name` field is guaranteed to be the name of the package itself, but the `reference` field should be considered an opaque string whose value may be whatever the PnP implementation decides to put there. + +Note that one package locator is different from the others: the top-level locator (available through `pnp.topLevel`, cf below) sets _both_ `name` and `reference` to `null`. This special locator will always mirror the top-level package (which is generally the root of the repository, even when working with workspaces). + +### `PackageInformation` + +```ts +export type PackageInformation = { + packageLocation: string; + packageDependencies: Map; + packagePeers: Set; + linkType: "HARD" | "SOFT"; +}; +``` + +The package information set describes the location where the package can be found on the disk, and the exact set of dependencies it is allowed to require. The `packageDependencies` values are meant to be interpreted as such: + +- If a string, the value is meant to be used as a reference in a locator whose name is the dependency name. + +- If a `[string, string]` tuple, the value is meant to be used as a locator whose name is the first element of the tuple and reference is the second one. This typically occurs with package aliases (such as `"foo": "npm:bar@1.2.3"`). + +- If `null`, the specified dependency isn't available at all. This typically occurs when a package's peer dependency didn't get provided by its direct parent in the dependency tree. + +The `packagePeers` field, if present, indicates which dependencies have an enforced contract on using the exact same instance as the package that depends on them. This field is rarely useful in pure PnP context (because our instantiation guarantees are stricter and more predictable than this), but is required to properly generate a `node_modules` directory from a PnP map. + +The `linkType` field is only useful in specific cases - it describes whether the producer of the PnP API was asked to make the package available through a hard linkage (in which case all the `packageLocation` field is reputed being owned by the linker) or a soft linkage (in which case the `packageLocation` field represents a location outside of the sphere of influence of the linker). + +## Runtime Constants + +### `process.versions.pnp` + +When operating under PnP environments, this value will be set to a number indicating the version of the PnP standard in use (which is strictly identical to `require('pnpapi').VERSIONS.std`). + +This value is a convenient way to check whether you're operating under a Plug'n'Play environment (where you can `require('pnpapi')`) or not: + +```js +if (process.versions.pnp) { + // do something with the PnP API ... +} else { + // fallback +} +``` + +### `require('module')` + +The `module` builtin module is extended when operating within the PnP API with one extra function: + +```ts +export function findPnpApi(lookupSource: URL | string): PnpApi | null; +``` + +When called, this function will traverse the filesystem hierarchy starting from the given `lookupSource` in order to locate the closest `.pnp.cjs` file. It'll then load this file, register it inside the PnP loader internal store, and return the resulting API to you. + +Note that while you'll be able to resolve the dependencies by using the API returned to you, you'll need to make sure they are properly _loaded_ on behalf of the project too, by using `createRequire`: + +```ts +const { createRequire, findPnpApi } = require(`module`); + +// We'll be able to inspect the dependencies of the module passed as first argument +const targetModule = process.argv[2]; + +const targetPnp = findPnpApi(targetModule); +const targetRequire = createRequire(targetModule); + +const resolved = targetPnp.resolveRequest(`eslint`, targetModule); +const instance = targetRequire(resolved); // <-- important! don't use `require`! +``` + +Finally, it can be noted that `findPnpApi` isn't actually needed in most cases and we can do the same with just `createRequire` thanks to its `resolve` function: + +```ts +const { createRequire } = require(`module`); + +// We'll be able to inspect the dependencies of the module passed as first argument +const targetModule = process.argv[2]; + +const targetRequire = createRequire(targetModule); + +const resolved = targetRequire.resolve(`eslint`); +const instance = targetRequire(resolved); // <-- still important +``` + +### `require('pnpapi')` + +When operating under a Plug'n'Play environment, a new builtin module will appear in your tree and will be made available to all your packages (regardless of whether they define it in their dependencies or not): `pnpapi`. It exposes the constants a function described in the rest of this document. + +Note that we've reserved the `pnpapi` package name on the npm registry, so there's no risk that anyone will be able to snatch the name for nefarious purposes. We might use it later to provide a polyfill for non-PnP environments (so that you'd be able to use the PnP API regardless of whether the project got installed via PnP or not), but as of now it's still an empty package. + +Note that the `pnpapi` builtin is _contextual_: while two packages from the same dependency tree are guaranteed to read the same one, two packages from different dependency trees will get different instances - each reflecting the dependency tree they belong to. This distinction doesn't often matter except sometimes for project generator (which typically run within their own dependency tree while also manipulating the project they're generating). + +## API Interface + +### `VERSIONS` + +```ts +export const VERSIONS: { std: number; [key: string]: number }; +``` + +The `VERSIONS` object contains a set of numbers that detail which version of the API is currently exposed. The only version that is guaranteed to be there is `std`, which will refer to the version of this document. Other keys are meant to be used to describe extensions provided by third-party implementors. Versions will only be bumped when the signatures of the public API change. + +:::note +The current version is 3. We bump it responsibly and strive to make each version backward-compatible with the previous ones, but as you can probably guess some features are only available with the latest versions. +::: + +### `topLevel` + +```ts +export const topLevel: { name: null; reference: null }; +``` + +The `topLevel` object is a simple package locator pointing to the top-level package of the dependency tree. Note that even when using workspaces you'll still only have one single top-level for the entire project. + +This object is provided for convenience and doesn't necessarily needs to be used; you may create your own top-level locator by using your own locator literal with both fields set to `null`. + +:::note +These special top-level locators are merely aliases to physical locators, which can be accessed by calling `findPackageLocator`. +::: + +### `getLocator(...)` + +```ts +export function getLocator( + name: string, + referencish: string | [string, string] +): PackageLocator; +``` + +This function is a small helper that makes it easier to work with "referencish" ranges. As you may have seen in the `PackageInformation` interface, the `packageDependencies` map values may be either a string or a tuple - and the way to compute the resolved locator changes depending on that. To avoid having to manually make a `Array.isArray` check, we provide the `getLocator` function that does it for you. + +Just like for `topLevel`, you're under no obligation to actually use it - you're free to roll your own version if for some reason our implementation wasn't what you're looking for. + +### `getDependencyTreeRoots(...)` + +```ts +export function getDependencyTreeRoots(): PackageLocator[]; +``` + +The `getDependencyTreeRoots` function will return the set of locators that constitute the roots of individual dependency trees. In Yarn, there is exactly one such locator for each workspace in the project. + +:::note +This function will always return the physical locators, so it'll never return the special top-level locator described in the `topLevel` section. +::: + +### `getAllLocators(...)` + +```ts +export function getAllLocators(): PackageLocator[]; +``` + +:::danger +This function is not part of the Plug'n'Play specification and only available as a Yarn extension. In order to use it, you first must check that the [`VERSIONS`](/advanced/pnpapi#versions) dictionary contains a valid `getAllLocators` property. +::: + +The `getAllLocators` function will return all locators from the dependency tree, in no particular order (although it'll always be a consistent order between calls for the same API). It can be used when you wish to know more about the packages themselves, but not about the exact tree layout. + +### `getPackageInformation(...)` + +```ts +export function getPackageInformation( + locator: PackageLocator +): PackageInformation; +``` + +The `getPackageInformation` function returns all the information stored inside the PnP API for a given package. + +### `findPackageLocator(...)` + +```ts +export function findPackageLocator(location: string): PackageLocator | null; +``` + +Given a location on the disk, the `findPackageLocator` function will return the package locator for the package that "owns" the path. For example, running this function on something conceptually similar to `/path/to/node_modules/foo/index.js` would return a package locator pointing to the `foo` package (and its exact version). + +:::note +This function will always return the physical locators, so it'll never return the special top-level locator described in the `topLevel` section. You can leverage this property to extract the physical locator for the top-level package: +::: + +```ts +const virtualLocator = pnpApi.topLevel; +const physicalLocator = pnpApi.findPackageLocator( + pnpApi.getPackageInformation(virtualLocator).packageLocation +); +``` + +### `resolveToUnqualified(...)` + +```ts +export function resolveToUnqualified( + request: string, + issuer: string | null, + opts?: { considerBuiltins?: boolean } +): string | null; +``` + +The `resolveToUnqualified` function is maybe the most important function exposed by the PnP API. Given a request (which may be a bare specifier like `lodash`, or an relative/absolute path like `./foo.js`) and the path of the file that issued the request, the PnP API will return an unqualified resolution. + +For example, the following: + +``` +lodash/uniq +``` + +Might very well be resolved into: + +``` +/my/cache/lodash/1.0.0/node_modules/lodash/uniq +``` + +As you can see, the `.js` extension didn't get added. This is due to the difference between [qualified and unqualified resolutions](#qualified-vs-unqualified-resolutions). In case you must obtain a path ready to be used with the filesystem API, prefer using `resolveRequest` instead. + +Note that in some cases you may just have a folder to work with as `issuer` parameter. When this happens, just suffix the issuer with an extra slash (`/`) to indicate to the PnP API that the issuer is a folder. + +This function will return `null` if the request is a builtin module, unless `considerBuiltins` is set to `false`. + +### `resolveUnqualified(...)` + +```ts +export function resolveUnqualified( + unqualified: string, + opts?: { extensions?: string[] } +): string; +``` + +The `resolveUnqualified` function is mostly provided as an helper; it reimplements the Node resolution for file extensions and folder indexes, but not the regular `node_modules` traversal. It makes it slightly easier to integrate PnP into some projects, although it isn't required in any way if you already have something that fits the bill. + +To give you an example `resolveUnqualified` isn't needed with `enhanced-resolved`, used by Webpack, because it already implements its own way the logic contained in `resolveUnqualified` (and more). Instead, we only have to leverage the lower-level `resolveToUnqualified` function and feed it to the regular resolver. + +For example, the following: + +``` +/my/cache/lodash/1.0.0/node_modules/lodash/uniq +``` + +Might very well be resolved into: + +``` +/my/cache/lodash/1.0.0/node_modules/lodash/uniq/index.js +``` + +### `resolveRequest(...)` + +```ts +export function resolveRequest(request: string, issuer: string | null, opts?: {considerBuiltins?: boolean, extensions?: string[]]}): string | null; +``` + +The `resolveRequest` function is a wrapper around both `resolveToUnqualified` and `resolveUnqualified`. In essence, it's a bit like calling `resolveUnqualified(resolveToUnqualified(...))`, but shorter. + +Just like `resolveUnqualified`, `resolveRequest` is entirely optional and you might want to skip it to directly use the lower-level `resolveToUnqualified` if you already have a resolution pipeline that just needs to add support for Plug'n'Play. + +For example, the following: + +``` +lodash +``` + +Might very well be resolved into: + +``` +/my/cache/lodash/1.0.0/node_modules/lodash/uniq/index.js +``` + +This function will return `null` if the request is a builtin module, unless `considerBuiltins` is set to `false`. + +### `resolveVirtual(...)` + +```ts +export function resolveVirtual(path: string): string | null; +``` + +:::danger +This function is not part of the Plug'n'Play specification and only available as a Yarn extension. In order to use it, you first must check that the [`VERSIONS`](/advanced/pnpapi#versions) dictionary contains a valid `resolveVirtual` property. +::: + +The `resolveVirtual` function will accept any path as parameter and return the same path minus any [virtual component](/advanced/lexicon#virtual-package). This makes it easier to store the location to the files in a portable way as long as you don't care about losing the dependency tree information in the process (requiring files through those paths will prevent them from accessing their peer dependencies). + +## Qualified vs Unqualified Resolutions + +This document detailed two types of resolutions: qualified and unqualified. Although similar, they present different characteristics that make them suitable in different settings. + +The difference between qualified and unqualified resolutions lies in the quirks of the Node resolution itself. Unqualified resolutions can be statically computed without ever accessing the filesystem, but only can only resolve relative paths and bare specifiers (like `lodash`); they won't ever resolve the file extensions or folder indexes. By contrast, qualified resolutions are ready to be used to access the filesystem. + +Unqualified resolutions are the core of the Plug'n'Play API; they represent data that cannot be obtained any other way. If you're looking to integrate Plug'n'Play inside your resolver, they're likely what you're looking for. On the other hand, fully qualified resolutions are handy if you're working with the PnP API as a one-off and just want to obtain some information on a given file or package. + +Two great options for two different use cases 🙂 + +## Accessing the files + +The paths returned in the `PackageInformation` structures are in the native format (so Posix on Linux/OSX and Win32 on Windows), but they may reference files outside of the typical filesystem. This is particularly true for Yarn, which references packages directly from within their zip archives. + +To access such files, you can use the `@yarnpkg/fslib` project which abstracts the filesystem under a multi-layer architecture. For example, the following code would make it possible to access any path, regardless of whether they're stored within a zip archive or not: + +```ts +const { PosixFS, ZipOpenFS } = require(`@yarnpkg/fslib`); +const libzip = require(`@yarnpkg/libzip`).getLibzipSync(); + +// This will transparently open zip archives +const zipOpenFs = new ZipOpenFS({ libzip }); + +// This will convert all paths into a Posix variant, required for cross-platform compatibility +const crossFs = new PosixFS(zipOpenFs); + +console.log(crossFs.readFileSync(`C:\\path\\to\\archive.zip\\package.json`)); +``` + +## Traversing the dependency tree + +The following function implements a tree traversal in order to print the list of locators from the tree. + +:::danger +This implementation iterates over **all** the nodes in the tree, even if they are found multiple times (which is very often the case). As a result the execution time is way higher than it could be. Optimize as needed 🙂 +::: + +```ts +const pnp = require(`pnpapi`); +const seen = new Set(); + +const getKey = (locator) => JSON.stringify(locator); + +const isPeerDependency = (pkg, parentPkg, name) => + getKey(pkg.packageDependencies.get(name)) === + getKey(parentPkg.packageDependencies.get(name)); + +const traverseDependencyTree = (locator, parentPkg = null) => { + // Prevent infinite recursion when A depends on B which depends on A + const key = getKey(locator); + if (seen.has(key)) return; + + const pkg = pnp.getPackageInformation(locator); + console.assert(pkg, `The package information should be available`); + + seen.add(key); + + console.group(locator.name); + + for (const [name, referencish] of pkg.packageDependencies) { + // Unmet peer dependencies + if (referencish === null) continue; + + // Avoid iterating on peer dependencies - very expensive + if (parentPkg !== null && isPeerDependency(pkg, parentPkg, name)) continue; + + const childLocator = pnp.getLocator(name, referencish); + traverseDependencyTree(childLocator, pkg); + } + + console.groupEnd(locator.name); + + // Important: This `delete` here causes the traversal to go over nodes even + // if they have already been traversed in another branch. If you don't need + // that, remove this line for a hefty speed increase. + seen.delete(key); +}; + +// Iterate on each workspace +for (const locator of pnp.getDependencyTreeRoots()) { + traverseDependencyTree(locator); +} +``` diff --git a/website/src/docs/appendix/pnp-spec.md b/website/src/docs/appendix/pnp-spec.md new file mode 100644 index 00000000..bdb0d212 --- /dev/null +++ b/website/src/docs/appendix/pnp-spec.md @@ -0,0 +1,353 @@ +--- +category: appendixes +slug: appendix/pnp-spec +title: Plug'n'Play specification +description: Official specification for Yarn Plug'n'Play. +--- + +## About this document + +To make interoperability easier for third-party projects, this document describes the specification we follow when installing files on disk under the [Plug'n'Play install strategy](/features/pnp). It also means: + +- any change we make to this document will follow semver rules +- we'll do our best to preserve backward compatibility +- new features will be intended to gracefully degrade + +## High-level idea + +Plug'n'Play works by keeping in memory a table of all packages part of the dependency tree, in such a way that we can easily answer two different questions: + +- Given a path, what package does it belong to? +- Given a package, where are the dependencies it can access? + +Resolving a package import thus becomes a matter of interlacing those two operations: + +- First, locate which package is requesting the resolution +- Then retrieve its dependencies, check if the requested package is amongst them +- If it is, then retrieve the dependency information, and return its location + +Extra features can then be designed, but are optional. For example, Yarn leverages the information it knows about the project to throw semantic errors when a dependency cannot be resolved: since we know the state of the whole dependency tree, we also know why a package may be missing. + +## Basic concepts + +All packages are uniquely referenced by **locators**. A locator is a combination of a **package ident**, which includes its scope if relevant, and a **package reference**, which can be seen as a unique ID used to distinguish different instances (or versions) of a same package. The package references should be treated as an opaque value: it doesn't matter from a resolution algorithm perspective that they start with `workspace:`, `virtual:`, `npm:`, or any other protocol. + +### Portability + +For portability reasons, all paths inside of the manifests: + +- must use the unix path format (`/` as separators). +- must be relative to the manifest folder (so they are the same regardless of the location of the project on disk). + +:::caution +All algorithms in this specification assume that paths have been normalized according to these two rules. +::: + +## Fallback + +For improved compatibility with legacy codebases, Plug'n'Play supports a feature we call "fallback". The fallback triggers when a package makes a resolution request to a dependency it doesn't list in its dependencies. In normal circumstances the resolver would throw, but when the fallback is enabled the resolver should first try to find the dependency packages amongst the dependencies of a set of special packages. If it finds it, it then returns it transparently. + +In a sense, the fallback can be seen as a limited and safer form of hoisting. While hoisting allows unconstrainted access through multiple levels of dependencies, the fallback requires to explicitly define a fallback package - usually the top-level one. + +## Package locations + +While the Plug'n'Play specification doesn't by itself require runtimes to support anything else than the regular filesystem when accessing package files, producers may rely on more complex data storage mechanisms. For instance, Yarn itself requires the two following extensions which we strongly recommend to support: + +### Zip access + +Files named `*.zip` must be treated as folders for the purpose of file access. For instance, `/foo/bar.zip/package.json` requires to access the `package.json` file located within the `/foo/bar.zip` zip archive. + +If writing a JS tool, the [`@yarnpkg/fslib`](https://yarnpkg.com/package/@yarnpkg/fslib) package may be of assistance, providing a zip-aware filesystem layer called `ZipOpenFS`. + +### Virtual folders + +In order to properly represent packages listing peer dependencies, Yarn relies on a concept called [Virtual Packages](/advanced/lexicon#virtual-package). Their most notable property is that they all have different paths (so that Node.js instantiates them as many times as needed), while still being baked by the same concrete folder on disk. + +This is done by adding path support for the following scheme: + +``` +/path/to/some/folder/__virtual__///subpath/to/file.dat +``` + +When this pattern is found, the `__virtual__//` part must be removed, the `hash` ignored, and the `dirname` operation applied `n` times to the `/path/to/some/folder` part. Some examples: + +``` +/path/to/some/folder/__virtual__/a0b1c2d3/0/subpath/to/file.dat +/path/to/some/folder/subpath/to/file.dat + +/path/to/some/folder/__virtual__/e4f5a0b1/0/subpath/to/file.dat +/path/to/some/folder/subpath/to/file.dat (different hash, same result) + +/path/to/some/folder/__virtual__/a0b1c2d3/1/subpath/to/file.dat +/path/to/some/subpath/to/file.dat + +/path/to/some/folder/__virtual__/a0b1c2d3/3/subpath/to/file.dat +/path/subpath/to/file.dat +``` + +If writing a JS tool, the [`@yarnpkg/fslib`](https://yarnpkg.com/package/@yarnpkg/fslib) package may be of assistance, providing a virtual-aware filesystem layer called `VirtualFS`. + +:::note +The `__virtual__` folder name appeared with Yarn 3.0. Earlier releases used `$$virtual`, but we changed it after discovering that this pattern triggered bugs in software where paths were used as either regexps or replacement. For example, `$$` found in the second parameter from [`String.prototype.replace`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace) silently turned into `$`. +::: + +## Manifest reference + +When [`pnpEnableInlining`](/configuration/yarnrc#pnpEnableInlining) is explicitly set to `false`, Yarn will generate an additional `.pnp.data.json` file containing the following fields. + +This document only covers the data file itself - you should define your own in-memory data structures, populated at runtime with the information from the manifest. For example, Yarn turns the `packageRegistryData` table into two separate memory tables: one that maps a path to a package, and another that maps a package to a path. + +:::note +You may notice that various places use arrays of tuples in place of maps. This is mostly intended to make it easier to hydrate ES6 maps, but also sometimes to have non-string keys (for instance `packageRegistryData` will have a `null` key in one particular case). +::: + +import pnpSchema from "@/utils/configuration/pnp.json"; +import JsonToDoc from "@/components/JsonToDoc"; + + + +## Resolution algorithm + +:::note +For simplicity, this algorithm doesn't mention all the Node.js features that allow mapping a module to another, such as [`imports`](https://nodejs.org/api/packages.html#imports), [`exports`](https://nodejs.org/api/packages.html#exports), or other vendor-specific features. +::: + +### NM_RESOLVE + +``` +NM_RESOLVE(specifier, parentURL) +``` + +1. This function is specified in the [Node.js documentation](https://nodejs.org/api/esm.html#resolver-algorithm-specification) + +### PNP_RESOLVE + +``` +PNP_RESOLVE(specifier, parentURL) +``` + +1. Let `resolved` be **undefined** + +2. If `specifier` is a Node.js builtin, then + + 1. Set `resolved` to `specifier` itself and return it + +3. Otherwise, if `specifier` is either an absolute path or a path prefixed with "./" or "../", then + + 1. Set `resolved` to [`NM_RESOLVE`](#nm_resolve)`(specifier, parentURL)` and return it + +4. Otherwise, + + 1. Note: `specifier` is now a bare identifier + + 2. Let `unqualified` be [`RESOLVE_TO_UNQUALIFIED`](#resolve_to_unqualified)`(specifier, parentURL)` + + 3. Set `resolved` to [`NM_RESOLVE`](#nm_resolve)`(unqualified, parentURL)` + +### RESOLVE_TO_UNQUALIFIED + +``` +RESOLVE_TO_UNQUALIFIED(specifier, parentURL) +``` + +1. Let `resolved` be **undefined** + +2. Let `ident` and `modulePath` be the result of [`PARSE_BARE_IDENTIFIER`](#parse_bare_identifier)`(specifier)` + +3. Let `manifest` be [`FIND_PNP_MANIFEST`](#find_pnp_manifest)`(parentURL)` + +4. If `manifest` is null, then + + 1. Set `resolved` to [`NM_RESOLVE`](#nm_resolve)`(specifier, parentURL)` and return it + +5. Let `parentLocator` be [`FIND_LOCATOR`](#find_locator)`(manifest, parentURL)` + +6. If `parentLocator` is null, then + + 1. Set `resolved` to [`NM_RESOLVE`](#nm_resolve)`(specifier, parentURL)` and return it + +7. Let `parentPkg` be [`GET_PACKAGE`](#get_package)`(manifest, parentLocator)` + +8. Let `referenceOrAlias` be the entry from `parentPkg.packageDependencies` referenced by `ident` + +9. If `referenceOrAlias` is **null** or **undefined**, then + + 1. If `manifest.enableTopLevelFallback` is **true**, then + + 1. If `parentLocator` **isn't** in `manifest.fallbackExclusionList`, then + + 1. Let `fallback` be [`RESOLVE_VIA_FALLBACK`](#resolve_via_fallback)`(manifest, ident)` + + 2. If `fallback` is neither **null** nor **undefined** + + 1. Set `referenceOrAlias` to `fallback` + +10. If `referenceOrAlias` is still **undefined**, then + + 1. Throw a resolution error + +11. If `referenceOrAlias` is still **null**, then + + 1. Note: It means that `parentPkg` has an unfulfilled peer dependency on `ident` + + 2. Throw a resolution error + +12. Otherwise, if `referenceOrAlias` is an array, then + + 1. Let `alias` be `referenceOrAlias` + + 2. Let `dependencyPkg` be [`GET_PACKAGE`](#get_package)`(manifest, alias)` + + 3. Return `path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)` + +13. Otherwise, + + 1. Let `reference` be `referenceOrAlias` + + 2. Let `dependencyPkg` be [`GET_PACKAGE`](#get_package)`(manifest, {ident, reference})` + + 3. Return `path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)` + +### GET_PACKAGE + +``` +GET_PACKAGE(manifest, locator) +``` + +1. Let `referenceMap` be the entry from `parentPkg.packageRegistryData` referenced by `locator.ident` + +2. Let `pkg` be the entry from `referenceMap` referenced by `locator.reference` + +3. Return `pkg` + + 1. Note: `pkg` cannot be **undefined** here; all packages referenced in any of the Plug'n'Play data tables [**MUST**](#must) have a corresponding entry inside `packageRegistryData`. + +### FIND_LOCATOR + +``` +FIND_LOCATOR(manifest, moduleUrl) +``` + +:::note +The algorithm described here is quite inefficient. You should make sure to prepare data structure more suited for this task when you read the manifest. +::: + +1. Let `bestLength` be **0** + +2. Let `bestLocator` be **null** + +3. Let `relativeUrl` be the relative path between `manifest` and `moduleUrl` + + 1. Note: The relative path must not start with `./`; trim it if needed + +4. If `relativeUrl` matches `manifest.ignorePatternData`, then + + 1. Return **null** + +5. Let `relativeUrlWithDot` be `relativeUrl` prefixed with `./` or `../` as necessary + +6. For each `referenceMap` value in `manifest.packageRegistryData` + + 1. For each `registryPkg` value in `referenceMap` + + 1. If `registryPkg.discardFromLookup` **isn't true**, then + + 1. If `registryPkg.packageLocation.length` is greater than `bestLength`, then + + 1. If `relativeUrl` starts with `registryPkg.packageLocation`, then + + 1. Set `bestLength` to `registryPkg.packageLocation.length` + + 2. Set `bestLocator` to the current `registryPkg` locator + +7. Return `bestLocator` + +### RESOLVE_VIA_FALLBACK + +``` +RESOLVE_VIA_FALLBACK(manifest, ident) +``` + +1. Let `topLevelPkg` be [`GET_PACKAGE`](#get_package)`(manifest, {null, null})` + +2. Let `referenceOrAlias` be the entry from `topLevelPkg.packageDependencies` referenced by `ident` + +3. If `referenceOrAlias` is defined, then + + 1. Return it immediately + +4. Otherwise, + + 1. Let `referenceOrAlias` be the entry from `manifest.fallbackPool` referenced by `ident` + + 2. Return it immediately, whether it's defined or not + +### FIND_PNP_MANIFEST + +``` +FIND_PNP_MANIFEST(url) +``` + +Finding the right PnP manifest to use for a resolution isn't always trivial. There are two main options: + +- Assume that there is a single PnP manifest covering the whole project. This is the most common case, as even when referencing third-party projects (for example via the [`portal:` protocol](/protocol/portal#portals-vs-links)) their dependency trees are stored in the same manifest as the main project. + + To do that, call [`FIND_CLOSEST_PNP_MANIFEST`](#find_closest_pnp_manifest)`(require.main.filename)` once at the start of the process, cache its result, and return it for each call to [`FIND_PNP_MANIFEST`](#find_pnp_manifest) (if you're running in Node.js, you can even use `require.resolve('pnpapi')` which will do this work for you). + +- Try to operate within a multi-project world. **This is rarely required**. We support it inside the Node.js PnP loader, but only because of "project generator" tools like `create-react-app` which are run via `yarn create react-app` and require two different projects (the generator one `and` the generated one) to cooperate within the same Node.js process. + + Supporting this use case is difficult, as it requires a bookkeeping mechanism to track the manifests used to access modules, reusing them as much as possible and only looking for a new one when the chain breaks. + +### FIND_CLOSEST_PNP_MANIFEST + +``` +FIND_CLOSEST_PNP_MANIFEST(url) +``` + +1. Let `manifest` be **null** + +2. Let `directoryPath` be the directory for `url` + +3. Let `pnpPath` be `directoryPath` concatenated with `/.pnp.cjs` + +4. If `pnpPath` exists on the filesystem, then + + 1. Let `pnpDataPath` be `directoryPath` concatenated with `/.pnp.data.json` + + 2. Set `manifest` to `JSON.parse(readFile(pnpDataPath))` + + 3. Set `manifest.dirPath` to `directoryPath` + + 4. Return `manifest` + +5. Otherwise, if `directoryPath` is `/`, then + + 1. Return **null** + +6. Otherwise, + + 1. Return [`FIND_PNP_MANIFEST`](#find_pnp_manifest)`(directoryPath)` + +### PARSE_BARE_IDENTIFIER + +``` +PARSE_BARE_IDENTIFIER(specifier) +``` + +1. If `specifier` starts with "@", then + + 1. If `specifier` doesn't contain a "/" separator, then + + 1. Throw an error + + 2. Otherwise, + + 1. Set `ident` to the substring of `specifier` until the second "/" separator or the end of string, whatever happens first + +2. Otherwise, + + 1. Set `ident` to the substring of `specifier` until the first "/" separator or the end of string, whatever happens first + +3. Set `modulePath` to the substring of `specifier` starting from `ident.length` + +4. Return `{ident, modulePath}` diff --git a/website/src/docs/appendix/workspaces-and-peer-deps.md b/website/src/docs/appendix/workspaces-and-peer-deps.md new file mode 100644 index 00000000..e6b71563 --- /dev/null +++ b/website/src/docs/appendix/workspaces-and-peer-deps.md @@ -0,0 +1,53 @@ +--- +category: appendixes +slug: appendix/workspaces-and-peer-deps +title: Workspaces & peer deps +description: An overview of incompatibilities between workspaces and peer dependencies when using the node_modules or pnpm installation strategies. +--- + +Consider the workspaces below. The resulting hoisting will be something like this: + +- `node_modules/react` will have version 18, hoisted from `packages/web` +- `packages/mobile/node_modules/react` can't be hoisted and will have version 19 +- `packages/mobile/node_modules/component-lib` will be a symlink to `packages/component-lib` + +But this tree is invalid: because the `node_modules` resolution algorithm [always resolves symlinks](https://github.com/nodejs/node/issues/3402) before importing modules, the `component-lib` workspace will always require `react` in its version 18, even when accessed through `mobile`. This is despite `mobile` correctly providing the `react` dependency in version 19 through the peer dependency. + +There is unfortunately no `node_modules` layout that can address that correctly. + +:::note +Pnpm provides [`injectWorkspacePackages`](https://pnpm.io/settings#injectworkspacepackages) to workaround this issue by making hardlinked copies of `component-lib` into both `mobile` and `web`, but this requires more filesystem operations, and the copies need to be periodically synced to add new files and remove old ones. +::: + +### packages/component-lib + +```json +{ + "name": "component-lib", + "peerDependencies": { + "react": "*" + } +} +``` + +### packages/web + +```json +{ + "dependencies": { + "component-lib": "workspace:*", + "react": "^18" + } +} +``` + +### packages/mobile + +```json +{ + "dependencies": { + "component-lib": "workspace:*", + "react": "^19" + } +} +``` diff --git a/website/src/docs/concepts/advanced/_meta.yml b/website/src/docs/concepts/advanced/_meta.yml new file mode 100644 index 00000000..a386b29e --- /dev/null +++ b/website/src/docs/concepts/advanced/_meta.yml @@ -0,0 +1,2 @@ +label: Advanced concepts +order: 3 diff --git a/website/src/docs/concepts/advanced/performances.md b/website/src/docs/concepts/advanced/performances.md new file mode 100644 index 00000000..03024c95 --- /dev/null +++ b/website/src/docs/concepts/advanced/performances.md @@ -0,0 +1,26 @@ +--- +category: concepts +slug: concepts/performances +title: Performances +description: How we track and optimize the performance of Yarn. +sidebar: + order: 6 +--- + +Performances are an important aspect of Yarn's design and development, especially as of Yarn 6. + +To track our progress and make sure we don't accidentally regress, we have our CI run automated performance tests multiple times a day every day, across all major package managers. + +What follows is only a small subset of the various scenarios we benchmark. To see all variants of these tests, consult our dedicated [Datadog dashboard](https://yarnpkg.com/benchmarks). + + + + diff --git a/website/src/docs/concepts/advanced/virtual-packages.md b/website/src/docs/concepts/advanced/virtual-packages.md new file mode 100644 index 00000000..697e3360 --- /dev/null +++ b/website/src/docs/concepts/advanced/virtual-packages.md @@ -0,0 +1,47 @@ +--- +category: concepts +slug: concepts/virtual-packages +title: Virtual packages +description: An explanation of virtual packages, why they are necessary, and how to keep them in check. +sidebar: + order: 7 +--- + +:::caution +What follows is an advanced topic that isn't strictly necessary to understand how peer dependencies work. It however provides a deeper understanding of how Yarn manages the dependency graph. +::: + +## Prior context + +First, let's clarify a point of detail about how Yarn works. Before peer dependencies are processed, Yarn generates a graph in which each node represents a package, and each dependency represents an edge. Both as an optimization and to make the lockfile more readable, Yarn ensures that identical dependencies always point to the same node in the graph. So if you have a package listing a dependency `"foo": "^1.0.0"` and another package with the *exact* same dependency, Yarn will ensure that both `foo` dependencies will point to the same node (same version). + +It works perfectly for regular dependencies, but peer dependencies shatter that model. The problem we face is that a single package (let's say `my-react-component`) listing a peer dependency (on `react`) is no longer unique. Depending on which package is its ancestor (let's say either `web`, which provides `react@19`, or `mobile`, which provides `react@18`), `my-react-component` may end up connected to different versions of `react`. We can't just connect `web` and `mobile` to the same `react` node, because we'd have no way to decide whether `my-react-component` should be connected to `react@19` or `react@18`. + +## Virtual packages + +To solve this problem Yarn introduces the concept of *virtual packages*, which are unique copies of the `my-react-component` node created by the graph resolver for each time `my-react-component` was found while traversing the dependency graph. Each copy will have access to a different set of peer dependencies. + +Taking the example above: + +- The `web` node will be connected to `my-react-component#1`, itself connected to `react@19` +- The `mobile` node will be connected to `my-react-component#2`, itself connected to `react@18` + +Those virtual packages have different representations on disk depending on your [linker](/concepts/linkers): + +- The node-modules and pnpm linkers will duplicate those packages on disk. + +- The Yarn Plug'n'Play linker will keep those packages virtual; each of them will be assigned unique "virtual paths" (ie `/my/project/.yarn/__virtual__/...`), but they will all turn into the same path before Node.js performs the actuall syscalls. This is similar in idea to symlinks, but without actually being symlinks to prevent Node.js resolving them when passing file names to `realpath` before `import` calls. + +:::note +You may wonder *"why is this so complicated in Yarn? Why doesn't it work just like npm and pnpm, which don't need all that complexity?"* - that's because their only partially support peer deps! + +Due to the reliance on the filesystem, both node-modules and pnpm installs are unable to enforce the peer dependency contract with workspaces. This limitation is referred to as the nm / peer deps issue, which is documented [here](/appendix/nm-peer-deps). +::: + +## Package duplication + +We saw that Yarn will create a virtual package for each peer dependency set so that each package gets exactly what it should per the dependency graph. This is all fine when everything is working as expected, but it comes with challenges. + +For one, it goes both ways: while you can be sure that all virtual packages will get exactly what you provide, it also means you have to be careful about the peer dependencies you provide. If you're not careful you may cause a package to be duplicated one or more times in separate virtual package instances. This can lead to worse runtime performances, broken `instanceof` checks, and broken features relying on shared data structures (such as React contexts). + +Another issue are dependency cycles. Yarn will optimize the dependency graph to avoid keeping multiple copies of the same virtual package with the same peer dependency sets, but that strategy has limits. We haven't found a satisfying way to deduplicate that depend on each other. diff --git a/website/src/docs/concepts/advanced/zero-installs.md b/website/src/docs/concepts/advanced/zero-installs.md new file mode 100644 index 00000000..5dd1a47f --- /dev/null +++ b/website/src/docs/concepts/advanced/zero-installs.md @@ -0,0 +1,52 @@ +--- +category: concepts +slug: concepts/zero-installs +title: Zero Installs +description: A way to make a project install-free. +sidebar: + order: 11 +--- + +Working on a high-velocity project comes with its own set of challenges. One of those is the constant need to reinstall dependencies whenever you change branches. Yarn came up with an opt-in pattern addressing this problem, which we call Zero Installs. + +:::caution +Zero-Installs come with their drawbacks which are documented below. Modern versions of Yarn implement by default an alternate approach called Lazy Installs that we believe scale better on very large projects. Nonetheless Zero Installs can be a useful option for smaller internal projects. +::: + +## How does it work? + +The idea of Zero Installs is extremely simple: what if all install artifacts were checked-in to version control? This way, when you clone a repository, you already have all the necessary files to run the project without needing to install anything. Changing branches would also be seamless, as your VCS will automatically update the install artifacts as it performs a checkout. + +While simple in appearance, I'm sure you'll quickly raise questions about its feasibility: checking in all install artifacts in a typical Node.js project means checking-in your `node_modules` directory. This can be a significant overhead, especially for large projects with many dependencies. This issue is exacerbated by the way `node_modules` hoisting works, which can lead to files being arbitrarily moved around as you add & remove dependencies, creating massive diffs in your PRs. + +That's true, and that's why we don't recommend checking-in your `node_modules` directory. That's where [Yarn Plug'n'Play](https://yarnpkg.com/concepts/pnp) comes to the rescue! Under this mode, Yarn doesn't generate a `node_modules` directory. Instead, a single file is generated called the `.pnp.cjs` file (plus another called `.pnp.loader.mjs` for ESM support). + +These files are [Node.js loaders](https://nodejs.org/api/module.html#customization-hooks) that contains a mapping of all dependencies to their respective locations on disk, allowing Yarn to resolve dependencies without needing to generate a `node_modules` directory. Their content is deterministic, so they can safely be checked-in to version control. + +Those loaders are one key to Zero Installs, but not the only one. The `.pnp.cjs` file will contain by default references to dependencies from your global filesystem cache. This cache is unique to your machine, so if someone else uses it they will probably be missing some packages. But Yarn has a way to address that, thanks to the `enableLocalCache` option. + +With this setting set, Yarn will keep your project's cache into your project, in the `.yarn/cache` directory. Thanks to that, any package you add to your project will be stored as a separate unique zip file in that directory. And while you might think keeping binary files into your repository is an unfathomable idea, it turns out Git providers are perfectly fine with this pattern. + +It also solves the issues we discussed with checking-in `node_modules` folders: + +- The Yarn Plug'n'Play dependency tree is guaranteed to be perfectly flat, so no package will ever be updated just because you add or remove unrelated dependencies. + +- Zip archives can be stored uncompressed, allowing Git to compute deltas between versions. + +- Each third-party package is tracked as a single file, making it easy for Git to track changes. + +## Limitations + +What I write here is based on my experience with an internal repository I worked on. It used the strategy described in this paper for more than five years until we switched to Lazy Installs. + +Keep in mind that this repository had an impressive scale (we're talking thousands of workspaces), and was very active (30+ `yarn.lock` updates per day). That Zero Install worked at all for so long demonstrates that the pattern is viable, especially when starting right away with the mitigations we discovered along the way. + +So with that context in mind, here are the challenges we faced: + +- Some Yarn updates required regenerating all of the cache files, which Git didn't like. This is less of an issue on modern releases, as we control much better the byte representation of the cache files, and can avoid unnecessary updates. + +- We discovered that storing files uncompressed in Git was better than storing them compressed. As an example this [uncompressed repo](https://github.com/yarnpkg/example-repo-zip0) weights [1.25GiB](https://api.github.com/repos/yarnpkg/example-repo-zip0), whereas [this identical compressed one](https://github.com/yarnpkg/example-repo-zipn) weights [2.1GiB](https://api.github.com/repos/yarnpkg/example-repo-zipn). + +- We also found out that the zip cache is only part of the story, and not actually the heaviest contributor to the repository size. Surprisingly, the actual heaviest contributor was the `.pnp.cjs` file. This was due in part to the very large amount of workspaces and inter-dependencies (leading to the `.pnp.cjs` file being almost 25MiB), but also to Git's delta algorithm being inefficient at dealing with that update pattern. + +- Generally speaking, the issues were mostly that once a problem was spotted, it was difficult to fully address it, as it would have required rewriting the full commit history. diff --git a/website/src/docs/concepts/core/_meta.yml b/website/src/docs/concepts/core/_meta.yml new file mode 100644 index 00000000..3705c1e5 --- /dev/null +++ b/website/src/docs/concepts/core/_meta.yml @@ -0,0 +1,2 @@ +label: Core concepts +order: 1 diff --git a/website/src/docs/concepts/core/dependency-protocols.md b/website/src/docs/concepts/core/dependency-protocols.md new file mode 100644 index 00000000..b76a4a77 --- /dev/null +++ b/website/src/docs/concepts/core/dependency-protocols.md @@ -0,0 +1,33 @@ +--- +category: concepts +slug: concepts/protocols +title: Dependency protocols +description: The various options you have to define dependencies in your application. +sidebar: + order: 3 +--- + +Yarn supports various protocols for defining dependencies in your application. While you're certainly familiar with the semver protocol which downloads packages from the npm registry, Yarn is also able to retrieve packages from git, the filesystem, or even generate them on the fly. + +## Available protocols + +:::note +Some of these protocols have alternative syntaxes, such as the git protocol which also supports strings like `/`. Check their respective pages for more information. +::: + +
    + +| Protocol | Description | +| --- | --- | +| `catalog:` | Delegate the dependency to the project configuration. | +| `exec:` | Generate a package on the fly via a script. | +| `file:` | Compile a local folder or extract a tgz archive. | +| `git:` | Retrieve a package from a git repository. | +| `jsr:` | Download a package from the JSR registry. | +| `link:` | Pretend the specified folder is a package. | +| `npm:` | Download a package from the npm registry. | +| `patch:` | Apply a patch to an existing package. | +| `portal:` | Connect the project to a package in another folder. | +| `workspace:` | Connect a workspace to another workspace. | + +
    diff --git a/website/src/docs/concepts/core/linkers.md b/website/src/docs/concepts/core/linkers.md new file mode 100644 index 00000000..33547923 --- /dev/null +++ b/website/src/docs/concepts/core/linkers.md @@ -0,0 +1,73 @@ +--- +category: concepts +slug: concepts/node-linkers +title: Node.js linkers +description: The different ways to install your project. +sidebar: + order: 4 +--- + +Yarn supports three different ways to install your projects on disk. This document gives a quick overview of each of them, along with the pros and cons of each. + +:::note +All install modes are **stable** and **production-ready**. Yarn uses PnP installs by default, but the `pnpm` and `node-modules` linkers are first-class citizens as well, supported by a wide range of tests. +::: + +## `nodeLinker: pnp` + +*For more details about Plug'n'Play installs, check the [dedicated section](/concepts/pnp).* + +Under this mode Yarn will generate a single Node.js loader file directory referencing your packages from their cache location. No need for file copies, or even symlinks / hardlinks. + +
    + +| Pros | Cons | +| --- | --- | +| Extremely fast | Less idiomatic | +| Content-addressable store | IDE integrations often require [SDKs](/getting-started/editor-sdks) | +| Protects against ghost dependencies | Sometimes requires `packageExtensions` | +| Semantic dependency errors | | +| Perfect hoisting optimizations | | +| Provides a [dependency tree API](/advanced/pnpapi) | | +| Can be upgraded into [zero-installs](/features/caching#zero-installs) | | + +
    + +:::note +Yarn Plug'n'Play has been the default installation strategy in Yarn since 2019, and the compatibility story significantly improved along the years as we worked with tooling authors to smoothen the edges. +::: + +## `nodeLinker: pnpm` + +Under this mode, a flat folder is generated in `node_modules/.pnpm` containing one folder for each dependency in the project. Each dependency folder is populated with hardlinks obtained from a central store common to all projects on the system (by default `$HOME/.yarn/berry/index`). Finally, symlinks to the relevant folders from the flat store are placed into the `node_modules` folders. + +
    + +| Pros | Cons | +| --- | --- | +| Slower than PnP, but still very fast | Symlinks aren't always supported by tools | +| Content-addressable store | Hard links can lead to strange behaviors | +| Protects against _some_ ghost dependencies | Generic dependency errors | +| No need for IDE SDKs | Sometimes requires `packageExtensions` | + +
    + +:::note +The pnpm mode is an interesting middle ground between traditional `node_modules` installs and the more modern Yarn PnP installs; it doesn't decrease the performances much and provides a slightly better compatibility story, at the cost of losing a couple of interesting features. +::: + +## `nodeLinker: node-modules` + +This mode is the old tried and true way to install Node.js projects, supported natively by Node.js and virtually the entirety of the JavaScript ecosystem. + +While we tend to recommend trying one of the two other modes first, it remains a solid option in case you face problems with your dependencies that you don't have the time to address right now. Sure, your project may be a little more unstable as you won't notice if ghost dependencies creep in, but it may be a reasonable trade-off depending on the circumstances. + +
    + +| Pros | Cons | +| --- | --- | +| Perfect compatibility with the whole ecosystem | Average speed | +| Optional support for hardlinks (`nmMode`) | No protection against ghost dependencies | +| No need for IDE SDKs | Imperfect hoisting due to the filesystem reliance | + +
    diff --git a/website/src/docs/concepts/core/workspaces.md b/website/src/docs/concepts/core/workspaces.md new file mode 100644 index 00000000..d212e8e9 --- /dev/null +++ b/website/src/docs/concepts/core/workspaces.md @@ -0,0 +1,60 @@ +--- +category: concepts +slug: concepts/workspaces +title: Workspaces +description: A tour of what Yarn has to offer to monorepo projects. +sidebar: + order: 8 +--- + +Workspaces are a core part of Yarn's design, making it easy to manage multiple packages within the same repository - a pattern you may already be familiar with under the name of "monorepo". + +In Yarn, each workspace is an isolated unit of that can reference other workspaces in its dependencies. As all workspaces have their own `package.json` file, they can choose whether to share scripts and dependencies with their siblings or not. + +All projects use workspaces - even those that don't have multiple packages! The root workspace is always present, and it serves as the default workspace for the project. + +## Why should I use a monorepo? + +Monorepos offer several key benefits. + +- Applying changes to multiple packages can be done in a single PR. This impacts refactorings, incentivizes modular code, encourages collaboration, and can simplify release processes that involve different environments like backend and frontend. + +- Sharing information between workspaces is much easier than between multiple repositories. Ensuring that your frontend types are up-to-date with your backend types is usually trivial. + +- Because you have a single source of truth, you can more easily understand what was the state of the world at any given time to trace changes and dependencies. + +They also have a couple of drawbacks: + +- It's very difficult to have different privacy settings between different parts of the repository. Unless your project is fully public, you'll likely need to extract your public dependencies into separate repositories. + +- Good CI hygiene is essential to keep operations smooth. If some of the teams working on the monorepo aren't careful and let failing status checks reach production, they will be repercussed in every PR and may hinder your ability to merge changes. + +From the author's experience, those drawbacks can be mitigated well enough that monorepos are a great choice for most projects, in both the open-source and corporate worlds. + +## How to declare workspaces in Yarn? + +Workspaces are declared in Yarn by adding a `workspaces` field to your `package.json` files, listing the workspaces' directories. This field accepts glob patterns, so you would usually put something like this: + +```json +{ + "workspaces": [ + "packages/*" + ] +} +``` + +## How to manage workspaces in Yarn? + +Yarn puts various tools at your disposal to help you manage your project. These includes: + +- The special [`catalog:`](/protocols/catalog) protocol lets you share dependency ranges between workspaces. Because this protocol is transparently replaced at publish-time, it's suitable for both public and internal packages. + +- Another special protocol, [`workspace:`](/protocols/workspace), lets a workspace declare dependencies on other workspaces. Just like `catalog:` it's transparently replaced at publish-time. + +- [Constraints](/concepts/constraints) let you ensure that your workspaces follow consistent rules. In a sense they can be seen as a linter / autofixer for monorepos. + +- Our CLI accepts a path as optional first argument, provided it contains a `/`. As an example, running `yarn ./documentation vite` would run the `vite` binary in the `documentation` workspace. + +- [Workspace profiles](/concepts/profiles), which let you declare in your configuration dependencies that should be automatically added to your workspaces' dependencies. + +Plus various workspace-related commands which you can find in our [CLI reference](http://localhost:4321/cli). diff --git a/website/src/docs/concepts/core/yarn-switch.md b/website/src/docs/concepts/core/yarn-switch.md new file mode 100644 index 00000000..bcd692aa --- /dev/null +++ b/website/src/docs/concepts/core/yarn-switch.md @@ -0,0 +1,70 @@ +--- +category: concepts +slug: concepts/switch +title: Yarn Switch +description: A description of Yarn Switch, the official way to manage Yarn binaries across projects. +sidebar: + order: 10 +--- + +Yarn manages your dependency versions, but what about the Yarn version? That's the role of Yarn Switch. + +Distributed as a separate binary with each Yarn release, Yarn Switch is a light binary that substitutes to the Yarn binaries and ensures that your team always uses the correct version of Yarn for the active project. + +Here's what happens under the hood when you run a Yarn command: + +1. Yarn Switch (`~/.yarn/switch/bin/yarn`) gets called. +2. It finds the nearest `package.json` file containing a `packageManager` field. +3. It checks whether that field references Yarn, and returns an error if not. +4. It then checks whether the requested version is available locally. If not, it downloads it. +5. It executes the cached binary, passing along any CLI arguments you provided. + +:::note +Yarn Switch is very similar in idea to [Corepack](https://www.google.com/search?q=corepack&oq=corepack&gs_lcrp=EgZjaHJvbWUyCQgAEEUYORiABDIHCAEQABiABDIHCAIQABiABDIHCAMQABiABDIGCAQQRRhBMgYIBRBFGD0yBggGEEUYPDIGCAcQRRhB0gEIMTAzM2oxajSoAgCwAgE&sourceid=chrome&ie=UTF-8), except it's officially maintained by the Yarn team and is designed specifically for Yarn. + +Given that the Node.js TSC [decided to phase out Corepack into Node.js](https://github.com/nodejs/TSC/pull/1697#issuecomment-2737093616), we decided to refocus our efforts, and now recommend using Yarn Switch over Corepack when possible. +::: + +## Where are the binaries downloaded from? + +Yarn Switch downloads the binaries from the official website. Your network administrators may need to allowlist the `repo.yarnpkg.com` domain for the endpoints to be available. + +We don't offer proxy settings at the moment, but contributions to this effect are welcome. + +## Configuring Yarn Switch for Docker images + +As you won't want to rely on our endpoints for your runtime images, you should make sure to populate your images at build time with the Yarn version your project will need to run. You usually will face one of those two scenarios: + +### You run your container as root + +```docker +RUN curl -s https://repo.yarnpkg.com/install | bash +ENV PATH="/root/.yarn/switch/bin:$PATH" + +# The --install flag makes sure your image contains +# the Yarn version used in your local project +RUN yarn switch cache --install +``` + +### Your runtime user is different from the build user + +```docker +RUN curl -s https://repo.yarnpkg.com/install | bash +RUN mv ~/.yarn/switch/bin/yarn /usr/local/bin/yarn + +USER node + +# The --install flag makes sure your image contains +# the Yarn version used in your local project +RUN yarn switch cache --install +``` + +## Frequent questions + +### How to upgrade to a new Yarn version? + +Running `yarn set version latest` will make Yarn bump the `packageManager` field in your `package.json` file to the new release. + +### Are the binaries signed? + +The binaries aren't signed at the moment, but we're working on it and hope to have that set up before Yarn 6 reaches a stable release. diff --git a/website/src/docs/concepts/intermediary/_meta.yml b/website/src/docs/concepts/intermediary/_meta.yml new file mode 100644 index 00000000..b40f4ab2 --- /dev/null +++ b/website/src/docs/concepts/intermediary/_meta.yml @@ -0,0 +1,2 @@ +label: Intermediary concepts +order: 2 diff --git a/website/src/docs/concepts/intermediary/constraints.md b/website/src/docs/concepts/intermediary/constraints.md new file mode 100644 index 00000000..68138765 --- /dev/null +++ b/website/src/docs/concepts/intermediary/constraints.md @@ -0,0 +1,79 @@ +--- +category: concepts +slug: concepts/constraints +title: Constraints +description: Yarn constraints, a way to enforce common rules across a project. +sidebar: + order: 1 +--- + +Constraints are a powerful feature in Yarn that allow you to define and enforce rules across your project. They can be used to validate the structure of your `package.json` files, raise errors when they are not met, and even declare fixes to automatically apply. + +Unlike Eslint-based linting, constraints have access to the project's entire dependency tree, allowing them to enforce rules that would be difficult to implement with static analysis alone - think circular dependencies or version consistency checks. + +## Definining constraints + +Constraints are created by adding a `yarn.config.cjs` file at the root of your project. This file should export an object with a `constraints` method. This method will be called by the constraints engine, and must define the rules to enforce on the project, using the provided API. For example: + +### Enforcing dependency versions + +```ts +module.exports = { + async constraints({ Yarn }) { + for (const dep of Yarn.dependencies({ ident: "react" })) { + dep.update(`18.0.0`); + } + }, +}; +``` + +### Enforcing package.json fields + +```ts +module.exports = { + async constraints({ Yarn }) { + for (const workspace of Yarn.workspaces()) { + workspace.set("engines.node", `20.0.0`); + } + }, +}; +``` + +## Declarative model + +Constraints are defined using a declarative model: you declare what the expected state should be, and Yarn checks whether it matches the reality or not. If it doesn't, Yarn will either throw an error (when calling `yarn constraints` without arguments) or attempt to fix the issue (when calling `yarn constraints --fix`). + +Because of this declarative model, you shouldn't check the actual values yourself. For instance, the check here is extraneous and should be removed: + +```ts +module.exports = { + async constraints({ Yarn }) { + for (const dep of Yarn.dependencies({ ident: "ts-node" })) { + // No need to check for the actual value! Just always call `update`. + if (dep.range !== `18.0.0`) { + dep.update(`18.0.0`); + } + } + }, +}; +``` + +## TypeScript support + +Yarn provides a type package to make it easier to write constraints. To use them, first add the package to your top-level dependencies: + +``` +yarn add @yarnpkg/types +``` + +Then rename your configuration file into `yarn.config.ts` and use the `defineConfig` function: + +```ts +import {defineConfig} from '@yarnpkg/types'; + +export default defineConfig({ + async constraints({ Yarn }) { + // `Yarn` is now well-typed ✨ + }, +}); +``` diff --git a/website/src/docs/concepts/intermediary/dependency-patches.md b/website/src/docs/concepts/intermediary/dependency-patches.md new file mode 100644 index 00000000..4dd14721 --- /dev/null +++ b/website/src/docs/concepts/intermediary/dependency-patches.md @@ -0,0 +1,32 @@ +--- +category: concepts +slug: concepts/patches +title: Dependency patches +description: How to fix your dependencies without having to fork them entirely while waiting for an update. +sidebar: + order: 2 +--- + +It sometimes happen that you need to make small changes to a dependency, just to workaround some small issue. The recommended action is to make a PR upstream, but it may take time until your changes get through review and end up in a release; what to do in the meantime? You have two options: + +- You can use the `git:` protocol, which will let you install a project straight from its development repository, provided it was correctly setup. + +- Or you can use the `patch:` protocol to make small changes to the dependencies straight from your project, while keeping them separated from the original code. + +No more waiting around for pull requests to be merged and published, no more forking repos just to fix that one tiny thing preventing your app from working: the builtin patch mechanism will always let you unblock yourself. + +## Making patches + +To create a patch, run the `yarn patch` command and pass it a package name. Yarn will extract the requested package to a temporary folder which you're then free to edit as you wish. + +Once you're done with your changes, all that remains is to run `yarn patch-commit -s` and pass it the path to the temporary folder Yarn generated: a patch file will be generated in `.yarn/patches` and applied to your project. Commit it, and you're set to go. + +## Maintaining patches + +By default, `yarn patch` will always reset the patch. If you wish to add new changes, use the `yarn patch ! --update` flag and follow the same procedure as before - your patch will be regenerated. + +## Limitations + +- Patches are computed at fetch time rather than resolution time, so package dependencies have already been extracted by the time Yarn reads your patched files. Prefer the `packageExtensions` mechanism to add new dependencies to a package. + +- Patches are ill-suited for modifying binary files. Minified files are problematic as well, although we would welcome a PR improving the feature to automatically process such files through a file formatter. diff --git a/website/src/docs/concepts/intermediary/nodejs-management.md b/website/src/docs/concepts/intermediary/nodejs-management.md new file mode 100644 index 00000000..66bd0c62 --- /dev/null +++ b/website/src/docs/concepts/intermediary/nodejs-management.md @@ -0,0 +1,87 @@ +--- +category: concepts +slug: concepts/nvm +title: Node.js versioning +description: Pin the version of Node.js used in your application +sidebar: + order: 3 +--- + +One of the subtle causes of "works on my machine" bugs comes from differences in Node.js versions between developers. While tools like nvm, fnm, or Volta help manage local Node.js installations, they require each team member to manually configure their environment. Yarn takes a different approach by letting you declare Node.js as a project dependency, ensuring everyone uses exactly the same version without any extra setup. + +## The `@builtin/node` dependency + +Yarn provides a special `@builtin/node` package that you can add to your dependencies just like any other package. When installed, Yarn will download the appropriate Node.js binary for your platform directly from [nodejs.org](https://nodejs.org/) and make it available to your project. + +```json +{ + "name": "my-app", + "dependencies": { + "@builtin/node": "^22.0.0" + } +} +``` + +The range here works similarly to semver ranges - Yarn will resolve it to the highest available Node.js version that satisfies your constraint. Once resolved, this version gets locked in your `yarn.lock` file, guaranteeing that every developer and CI environment uses the exact same Node.js release. + +## Why manage Node.js through Yarn? + +There are several advantages to managing Node.js as a dependency: + +- **Zero configuration** - CI and team members don't need to install nvm or any other version manager. Just `yarn install` and they're ready to go. + +- **Version locking** - The exact Node.js version is recorded and cached along with all other dependencies in your project. + +- **Per-project versions** - Different projects can use different Node.js versions without any manual switching. Yarn handles it automatically. + +- **Per-workspace versions** - You can easily override the Node.js version to use for a single workspace or a set of workspaces through [profiles](/concepts/profiles). + +## Using the managed Node.js + +Once installed, the managed Node.js binary is available through the `node` binary that Yarn injects into your environment. You can use it in several ways: + +### Through Yarn scripts + +Package scripts automatically use the project's Node.js version: + +```json +{ + "scripts": { + "start": "node server.js" + } +} +``` + +### Through `yarn node` / `yarn exec` + +Both commands will run Node.js with the correct environment setup: + +```bash +yarn node --version +yarn node script.js +yarn exec node --version +``` + +## Monorepo support + +In a monorepo, you typically want all workspaces to use the same Node.js version. Rather than adding `@builtin/node` to each workspace's `package.json`, you can use [workspace profiles](/concepts/profiles) to declare it once: + +```yaml +workspaceProfiles: + default: + devDependencies: + "@builtin/node": "builtin:^22.0.0" +``` + +Since the `default` profile is automatically applied to all workspaces, every package in your monorepo will use the same Node.js version without any additional configuration. This keeps your Node.js version centralized and easy to update. + +## Platform support + +The `@builtin/node` package automatically downloads the correct binary for your operating system and architecture. Currently supported platforms include: + +- Linux (x64, arm64) +- macOS (x64, arm64) + +When working in a team with mixed platforms, Yarn will store metadata about all required platform variants in the lockfile, but each developer will only downloads the binary they need for their platform (configurable through `supportedArchitectures`). + +This ensures the lockfile remains consistent across the entire team while keeping installs as lightweight as possible. diff --git a/website/src/docs/concepts/intermediary/peer-dependencies.md b/website/src/docs/concepts/intermediary/peer-dependencies.md new file mode 100644 index 00000000..3bf4144b --- /dev/null +++ b/website/src/docs/concepts/intermediary/peer-dependencies.md @@ -0,0 +1,36 @@ +--- +category: concepts +slug: concepts/peer-dependencies +title: Peer dependencies +description: A primer on peer dependencies, when to use them, and how to manage them. +sidebar: + order: 3 +--- + +Peer dependencies are often a feared tool due to subtle differences in how different package managers treat them. But they're also a powerful tool that addresses a very common problem: singleton packages. How to use them effectively is what we'll cover in this article. + +## The dependency contract + +Package managers turn the packages contained in your project into a dependency graph that's then turned into disk artifacts. The way the graph is constructed depends on the dependencies you declare. To make this process deterministic, package managers define a set of rules that govern how dependencies are connected to one another. We call these rules the dependency contract. + +The most simple contract is the one enforced by dependencies listed in the `dependencies` field. Through this field the package informs the package manager that for the install to be valid, the package must be able to require the provided package and obtain in response a version that satisfies the semver range they requested (or the exact package they reference, in the case of [special protocols](/concepts/protocols)). That's it. Package managers are free to choose any way they want to satisfy it, as long as this cardinal rule is respected. + +In particular, you'll note this requirement doesn't mention anything about other projects in the dependency graph. Since it's not part of the contract, package managers are under no obligation to ensure that the versions provided to the same dependencies from different packages are the same. This freedom isn't accidental - it's part of what allows package managers to compute efficient hoisting layouts. + +## Peer dependencies + +The problem however is that while regular dependencies work well enough when you don't care whether you use or not the same dependency as any other package, it sometimes happen that you do care. Take those cases: + +- You write a development server and want to provide an integration for various optional packages. You don't want to install them all by default, but you want to be able to import them when present. + +- You write a library (`my-react-component`) that uses primitives from another core library (`react`), and you need to ensure that you use exactly the same instance of that library as every other package that uses it to make sure global data structures (such as context) are shared. + +- Your public interface relies on types provided from another library, and those types have high chance to be wildly different between different versions (`@types/node` or `@types/react`). + +In those cases `dependencies` won't make the cut, because the package will just ensure that you get "a" version that matches what you asked for, but not necessarily the same version as other packages. That's where `peerDependencies` come in! + +Peer dependencies may look similar to regular dependencies, but are quite different. For one, they only work with semver ranges (except for the `workspace:` and `catalog:` special protocols). That's because the package manager never "installs" them - it just adds an edge in the dependency graph that goes from your package to the relevant dependency node from your *parent package*. + +Said another way, the peer dependency contract is written as such: "If the install is valid, a package with a peer dependency is guaranteed to obtain the exact same instance of this dependency when they import it as if the dependency was imported by the parent package in the dependency graph". + +It's still a mouthful, but it's very simple at its core: instead of declaring the dependency yourself, you leave it up to the package that uses you to declare it. They are in control of the version you'll get (although you can still validate it matches your expectations by providing a semver range), and you can be sure that the version you get is the same as the version that your sibling packages will also get, provided they also declare the same peer dependency! diff --git a/website/src/docs/concepts/intermediary/profiles.md b/website/src/docs/concepts/intermediary/profiles.md new file mode 100644 index 00000000..66eccce0 --- /dev/null +++ b/website/src/docs/concepts/intermediary/profiles.md @@ -0,0 +1,57 @@ +--- +category: concepts +slug: concepts/profiles +title: Workspace profiles +description: Reuse configuration between your workspaces +sidebar: + order: 4 +--- + +When working with monorepos, you often find yourself repeating the same dev dependencies across multiple workspaces. Maybe all your TypeScript packages need `@types/node`, or all your React packages need `@testing-library/react`. Instead of duplicating these dependencies in every `package.json`, workspace profiles let you define reusable sets of dev dependencies that can be shared across workspaces. + +## Defining profiles + +Profiles are defined in your `.yarnrc.yml` file under the `workspaceProfiles` key. Each profile can contain dev dependencies and can extend other profiles: + +```yaml +workspaceProfiles: + typescript: + devDependencies: + "@types/node": "^20.0.0" + "typescript": "^5.0.0" + + react: + devDependencies: + "@testing-library/react": "^14.0.0" + "react": "^18.0.0" + + fullstack: + extends: + - typescript + - react + devDependencies: + "eslint": "^8.0.0" +``` + +## Using profiles + +To apply a profile to a workspace, add an `extends` field to its `package.json`: + +```json +{ + "name": "my-package", + "extends": ["typescript"] +} +``` + +You can extend multiple profiles, and they will all be merged together. The `default` profile is always automatically included, even if you don't specify it explicitly. + +## Profile inheritance + +Profiles can extend other profiles, allowing you to build up complex configurations from simpler building blocks. When a profile extends another, all of its dev dependencies are inherited. If multiple profiles define the same dependency, the workspace's own `devDependencies` take precedence, followed by profiles applied later in the resolution order. + +## Limitations + +- Profiles only apply to dev dependencies. Regular dependencies must still be declared in each workspace's `package.json`. + +- If a workspace already declares a dev dependency in its `package.json`, profiles won't override it. This ensures that workspace-specific requirements always take precedence over shared profiles. diff --git a/website/src/docs/concepts/intermediary/tasks.md b/website/src/docs/concepts/intermediary/tasks.md new file mode 100644 index 00000000..ea97dc62 --- /dev/null +++ b/website/src/docs/concepts/intermediary/tasks.md @@ -0,0 +1,282 @@ +--- +category: concepts +slug: concepts/tasks +title: Task dependencies +description: Define and manage task execution order with sequential and parallel dependencies, cross-workspace dependencies, and task includes. +sidebar: + order: 6 +--- + +Task dependencies allow you to define the execution order of tasks in your project. When running a task, Yarn automatically resolves and executes all required dependencies first, ensuring that prerequisites are met before each task starts. + +## Defining tasks + +Tasks are defined in a `taskfile` at the root of each workspace. Each task has a name, optional dependencies, and a script to execute: + +``` +build: + echo "Building the project" + +test: build + echo "Running tests" +``` + +In this example, running `yarn test` will first execute `build`, then `test`. + +## Sequential dependencies + +By default, dependencies are sequential. Each dependency must complete before the next one starts: + +``` +lint: + echo "Linting" + +typecheck: + echo "Type checking" + +build: lint typecheck + echo "Building" +``` + +When you run `yarn build`, the execution order is: +1. `lint` runs first +2. `typecheck` runs after `lint` completes +3. `build` runs after `typecheck` completes + +This creates a strict ordering where each task waits for the previous one to finish. + +## Parallel dependencies + +To run dependencies in parallel, add the `&` suffix to the dependency name: + +``` +lint: + echo "Linting" + +typecheck: + echo "Type checking" + +build: lint& typecheck& + echo "Building" +``` + +Now when you run `yarn build`: +1. `lint` and `typecheck` run simultaneously +2. `build` runs after both complete + +Parallel execution can significantly speed up your build pipeline when tasks are independent of each other. + +## Mixing sequential and parallel dependencies + +You can combine sequential and parallel dependencies in a single task definition. Tasks with `&` form parallel groups, while tasks without `&` create sequential barriers: + +``` +a: + echo "Task A" + +b: + echo "Task B" + +c: + echo "Task C" + +d: + echo "Task D" + +e: a b& c& d + echo "Task E" +``` + +The execution order for `yarn e` is: +1. `a` runs first (sequential barrier) +2. `b` and `c` run in parallel (both have `&`) +3. `d` runs after `b` and `c` complete (sequential barrier) +4. `e` runs after `d` completes + +This pattern is useful when you have a setup task that must run first, followed by independent tasks that can run in parallel, and finally tasks that need all previous work to be done. + +## Cross-workspace dependencies + +Tasks can depend on tasks from other workspaces that are listed as dependencies in your `package.json`. Use the `workspace:task` syntax: + +``` +# In packages/app/taskfile +build: pkg-utils:build pkg-core:build + echo "Building app" +``` + +This ensures that both `pkg-utils` and `pkg-core` are built before `app`. Note that `pkg-utils` and `pkg-core` must be declared as dependencies (or devDependencies) of `app` in its `package.json`. + +You can also use glob patterns to match multiple dependency workspaces: + +``` +# Depend on all dependency packages matching the pattern +build: @my-scope/*:build + echo "Building after all @my-scope packages" +``` + +The glob pattern only matches workspaces that are both: +1. Listed as dependencies of the current workspace +2. Match the glob pattern + +This ensures that task dependencies follow the same dependency graph as your packages, preventing accidental coupling between unrelated workspaces. + +## Parallel cross-workspace dependencies + +Cross-workspace dependencies also support the parallel `&` modifier: + +``` +# In packages/app/taskfile +build: pkg-utils:build& pkg-core:build& + echo "Building app" +``` + +Now `pkg-utils:build` and `pkg-core:build` run in parallel before `app:build`. + +You can mix local and cross-workspace dependencies with any combination of sequential and parallel: + +``` +build: setup pkg-utils:build& pkg-core:build& finalize + echo "Building app" +``` + +Execution order: +1. `setup` runs first +2. `pkg-utils:build` and `pkg-core:build` run in parallel +3. `finalize` runs after both complete +4. `build` runs last + +## Transitive dependencies + +Yarn automatically resolves transitive dependencies. If task A depends on B, and B depends on C, running A will execute C, then B, then A: + +``` +# packages/pkg-a/taskfile +build: + echo "Building pkg-a" + +# packages/pkg-b/taskfile +build: pkg-a:build + echo "Building pkg-b" + +# packages/pkg-c/taskfile +build: pkg-b:build + echo "Building pkg-c" +``` + +Running `yarn build` in `pkg-c` will: +1. Execute `pkg-a:build` first (no dependencies) +2. Execute `pkg-b:build` after `pkg-a` completes +3. Execute `pkg-c:build` after `pkg-b` completes + +The dependency resolution computes the full transitive closure, so `pkg-c:build` knows it must wait for both `pkg-a:build` and `pkg-b:build`. + +## Including tasks from other workspaces + +You can include task definitions from dependency workspaces using the `include` directive. This allows you to reuse common task definitions across multiple workspaces without duplicating them. + +### Basic include + +To include all tasks from a dependency workspace's taskfile: + +``` +include pkg-utils + +build: lint + echo "Building" +``` + +This imports all tasks defined in `pkg-utils`'s `taskfile` into the current workspace. If `pkg-utils` has a `lint` task, it becomes available as if it were defined locally. + +### Include with custom path + +By default, `include` loads the `taskfile` at the root of the target workspace. You can specify a custom path: + +``` +include pkg-utils/tasks/common.tasks + +build: lint typecheck + echo "Building" +``` + +This loads tasks from `tasks/common.tasks` within the `pkg-utils` workspace instead of the default `taskfile`. + +### Scoped package includes + +Scoped packages are fully supported: + +``` +include @my-scope/my-lib + +build: lint + echo "Building" +``` + +You can also specify a custom path for scoped packages: + +``` +include @my-scope/my-lib/tasks/shared.tasks +``` + +### Include requirements + +The include target must be declared as a dependency (or devDependency) in your `package.json`. This ensures that task includes follow the same dependency graph as your packages: + +```json +{ + "name": "my-app", + "dependencies": { + "pkg-utils": "workspace:*" + } +} +``` + +If you try to include a workspace that isn't a dependency, you'll get an error: + +``` +Error: Cannot include 'pkg-other' from 'my-app': not listed as a dependency +``` + +### Task precedence + +When including tasks, local task definitions take precedence over included ones. If both your taskfile and an included taskfile define the same task name, your local definition is used: + +``` +include pkg-utils + +# This overrides any 'build' task from pkg-utils +build: + echo "Custom build" +``` + +### Multiple includes + +You can include from multiple workspaces: + +``` +include pkg-utils +include pkg-core + +build: lint typecheck test + echo "Building" +``` + +Tasks from earlier includes take precedence over later ones if there are naming conflicts. + +## Cycle detection + +Yarn detects circular dependencies and reports an error: + +``` +# This will fail! +a: b + echo "A" + +b: c + echo "B" + +c: a + echo "C" +``` + +Running any of these tasks will result in an error indicating the cycle: `a -> b -> c -> a`. diff --git a/website/src/docs/concepts/intermediary/yarn-plugnplay.md b/website/src/docs/concepts/intermediary/yarn-plugnplay.md new file mode 100644 index 00000000..6e51fe59 --- /dev/null +++ b/website/src/docs/concepts/intermediary/yarn-plugnplay.md @@ -0,0 +1,67 @@ +--- +category: concepts +slug: concepts/pnp +title: Yarn Plug'n'Play +description: An overview of Yarn Plug'n'Play, a powerful and innovative installation strategy for Node.js. +sidebar: + order: 5 +--- + +Yarn Plug'n'Play, also known as Yarn PnP, is the default installation strategy in modern releases of Yarn. While it can be swapped out for more traditional strategies such as `node_modules` or pnpm-style symlink-based installs, we recommend it when creating new projects. + +## First some context + +The only builtin resolution strategy in Node.js at this point in time is the `node_modules` one. When performing a resolution, Node.js will look for the package in the current directory's `node_modules` folder, then in the parent directory's `node_modules` folder, and so on until it reaches the root directory. The first directory it finds that contains the file will be used. + +This approach is simple, but comes with some limitations. For one, a naive package layout where each package simply contains its own dependencies would lead to a massive `node_modules` footprint, and would often [break path length limits](https://github.com/npm/npm/issues/3697). + +The main optimization is called hoisting. An hoisted `node_modules` tree doesn't just contain its own dependencies - it also contains the dependencies of its dependencies, and so on. This neat trick removes a lot of package duplication, but can't fully address the problem - multiple versions of the same package can't coexist in the same directory, so package managers have to duplicate them based on heuristics. + +Another major issue is that hoisting allows each package to import not only its own dependencies, but also any other package that happens to have been hoisted in its `node_modules` directory. This issue, where a package accidentally imports a dependency that isn't listed in its `package.json`, is often referred to as "ghost dependencies". + +Ghost dependencies lead to unexpected behaviors and bugs as the addition or removal of even a single unrelated package can impact our hoisting heuristics and drastically reorganize the `node_modules` layout. + +To address these issues, other package managers such as pnpm came up with improvements. Thanks to a smart use of symlinks those package managers can avoid some ghost dependencies by creating entirely separate `node_modules` branches for each package. + +While being a significant improvement over the naive `node_modules` approach, this strategy doesn't solve everything. For one it still involves a large amount of filesystem operations to generate symlinks and copy files. It also isn't able to represent all [dependency trees](/appendix/nm-peer-deps). + +## How does Plug'n'Play work? + +Yarn Plug'n'Play works by creating a [Node.js loader](https://nodejs.org/api/module.html#customization-hooks) instead of `node_modules` folder. This loader contains a map of all packages and their dependencies, along with their locations on disk. This map is used to resolve imports at runtime, avoiding the need to query the file system. + +Because our map contains the whole dependency tree, we can easily check that a package only accesses dependencies it declares in its `package.json`. This ensures that no ghost dependencies are introduced, and that the package's behavior is predictable and consistent. + +## Ecosystem compatibility + +While our loader integrates perfectly with the standard Node.js resolution APIs such as [`require.resolve`](https://nodejs.org/api/modules.html#requireresolverequest-options), [`createRequire`](https://nodejs.org/api/module.html#modulecreaterequirefilename) or [`import.meta.resolve`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve), we don't have `node_modules` folders. As a result, third-party packages or tools that make assumptions about their presence may have issues. Two examples: + +- Packages that accidentally read into the `node_modules` folder, for example to load packages starting with a given prefix as plugins. Those packages usually degrade gracefully, as they often also offer their users to be explicit about the plugins they want to use. + +- Tools that implement their own dependency resolution logic rather than using the standard Node.js APIs. This is often the case with bundlers and linters as they need to support various features that Node.js wouldn't otherwise support (`browser` or `types` fields, etc). + +In that last case we worked with the relevant teams to implement native support Yarn Plug'n'Play in their pipeline. This work was made easier thanks to the [Plug'n'Play specification](https://yarnpkg.com/features/pnp) and the [`pnp-rs` crate](https://github.com/yarnpkg/pnp-rs), which explain how to implement Plug'n'Play support outside of Node.js environments. + +Today, Yarn Plug'n'Play is supported natively by Vite, Webpack, Esbuild, Rspack, Eslint, and many more. + +## Frequently asked questions + +### How can I fix dependencies? + +Unlike the `node-modules` and `pnpm` linkers, accessing ghost dependencies under the Plug'n'Play strategy will throw an exception letting you know of the issue, leaving it up to you to decide how to proceed. + +The easiest way to fix such dependencies is by using the `packageExtensions` setting; it allows you to inject new dependencies into any package from your dependency tree. For example, should you face an error such as `@babel/core tried to access @babel/types, but it isn't declared in its dependencies`, you could easily fix it by adding the following to your `.yarnrc.yml` file: + +```yaml +packageExtensions: + "@babel/core@*": + dependencies: + "@babel/types": "*" +``` + +:::note +It may sometimes make sense to extend the `peerDependencies` field rather the `dependencies` field, this is to be addressed case-by-case. +::: + +:::tip +To avoid you having to maintain a large set of `packageExtensions` entries, the Yarn team maintains a list of [known ghost dependencies in the ecosystem](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-extensions/sources/index.ts) that Yarn automatically applies. This list is shared between Yarn and pnpm, and we're more than happy to merge contributions. +::: diff --git a/website/src/docs/contributing/building-and-testing.md b/website/src/docs/contributing/building-and-testing.md new file mode 100644 index 00000000..fdf69ddb --- /dev/null +++ b/website/src/docs/contributing/building-and-testing.md @@ -0,0 +1,66 @@ +--- +category: contributing +slug: contributing/building +title: Building & testing +description: How to build Yarn from sources and run its tests. +sidebar: + order: 1 +--- + +## Installing dependencies + +| Software | How to install | +| --- | --- | +| Yarn | [https://yarnpkg.com/getting-started/install](/getting-started/install) | +| Rust / Rustup | https://rust-lang.org/tools/install | + +## Building Yarn + +Clone the ZPM repository and cd into it: + +```bash +git clone https://github.com/yarnpkg/zpm.git +cd zpm +``` + +You should now be able to build the project: + +```bash +cargo build --release -p zpm-switch -p zpm +``` + +We tend to build Yarn in release mode, even in development, because Rust is known to be significantly slower in debug mode. Regardless of whether you want to use the release or debug version, create a symbolic link named `local` pointing to the binary you just created: + +```bash +ln -s target/release local +``` + +Also configure your system's Yarn Switch to use this local version when working on the project: + +```bash +yarn switch link target/release/yarn-bin +``` + +## Testing Yarn + +One of the reasons why the migration from the Classic codebase to the Berry one was so painful was that we lost all our testing framework. All Classic tests were written using internal primitives, so they couldn't be reused after the redesign. + +We learned from that mistake, and the Berry tests were written using the regular CLI as interface. This means it's easy to swap the binary from Berry to ZPM and run the full Yarn testsuite! + +Start by cloning the Berry repository: + +```bash +git clone https://github.com/yarnpkg/berry.git ~/berry +``` + +Export a `BERRY_DIR` environment variable pointing to the Berry repository: + +```bash +export BERRY_DIR=~/berry +``` + +Then run the `yarn berry` command from the ZPM repository: + +```bash +yarn berry test:integration commands/add.test +``` diff --git a/website/src/docs/contributing/data-structures.md b/website/src/docs/contributing/data-structures.md new file mode 100644 index 00000000..091d36fe --- /dev/null +++ b/website/src/docs/contributing/data-structures.md @@ -0,0 +1,30 @@ +--- +category: contributing +slug: contributing/data-structures +title: Data structures +description: Overview of the data structures used in Yarn. +sidebar: + order: 2 +--- + +Various data structures should be used when working with the Yarn codebase. Most of these will be familiar to anyone who has worked with the Berry codebase, but some are new. + +## Core primitives + +- [**Ranges**](https://github.com/yarnpkg/zpm/blob/main/packages/zpm-primitives/src/range.rs) are enumerations used to represent a set of potential packages. Yarn supports a variety of ranges, the most common being semver ranges but also git ranges, http ranges, file ranges, etc. + +- [**References**](https://github.com/yarnpkg/zpm/blob/main/packages/zpm-primitives/src/reference.rs) are enumerations as well. They are very much like ranges, but they only ever represent a single package. For this reason some ranges don't have a direct mapping to references (instead we rely on resolvers to convert them), but references can always be converted back to ranges. + +- [**Idents**](https://github.com/yarnpkg/zpm/blob/main/packages/zpm-primitives/src/ident.rs) are structs that represent package names. They are a combination of a package scope and a package name. + +- [**Descriptors**](https://github.com/yarnpkg/zpm/blob/main/packages/zpm-primitives/src/descriptor.rs) are structs that represent the combination of an ident and a range. The `dependencies` field of a `package.json` file is a collection of descriptors. + +- [**Locators**](https://github.com/yarnpkg/zpm/blob/main/packages/zpm-primitives/src/locator.rs) are similar to descriptors in that they represent the combination of an ident and a reference. They are used to uniquely identify a package within a project. + +## Miscellaneous primitives + +- [**LooseDescriptor**](https://github.com/yarnpkg/zpm/blob/main/packages/zpm/src/descriptor_loose.rs) represent a potentially incomplete descriptor that Yarn first needs to resolve. This is for example the type that `yarn add ` accepts: `` can be a package name, a package name and its version, a git URL, etc. + +- [**Resolutions**](https://github.com/yarnpkg/zpm/blob/main/packages/zpm/src/resolvers/mod.rs) contain both a locator and additional metadata about the package (such as its version, its dependencies, etc). The lockfile is a serialized list of resolution entries. + + - Resolutions can only store metadata that we could retrieve from the npm registry metadata endpoints, as they are pulled before the package is actually fetched. diff --git a/website/src/docs/contributing/general-design.md b/website/src/docs/contributing/general-design.md new file mode 100644 index 00000000..847fa629 --- /dev/null +++ b/website/src/docs/contributing/general-design.md @@ -0,0 +1,28 @@ +--- +category: contributing +slug: contributing/design +title: General design +description: Overview of the design decisions made in Yarn. +sidebar: + order: 3 +--- + +The ZPM codebase shares a similar high-level design with the Berry codebase. Installs work as follows: + +1. We start with a queue containing a set of root descriptors. Typically that will be the descriptors for the workspaces in the project. + +2. For each descriptor, we pass it down to the `resolve_descriptor` function, which will select the proper resolver function for the given range. This resolver function will then return us a resolution object. + +3. Each resolution we receive triggers two events: + + - First, we enqueue new descriptors into our queue for each dependency listed in the resolution. + + - Second, we forward the resolution's locator to the [`fetch_locator`](https://github.com/yarnpkg/zpm/blob/main/packages/zpm/src/fetchers/mod.rs) function. It will select the proper fetch function for the given reference, which will then pull the package data (either as a cache reference, or local path reference). + +4. Once we have finished resolving all descriptors and fetching all locators, we enter the link phase by calling [`link_project`](https://github.com/yarnpkg/zpm/blob/main/packages/zpm/src/linker/mod.rs). What happens there changes depending on the configured [linker](/concepts/node-linkers), but in the end they yield two new pieces of information: + + - A list of locations on disk for each package in the dependency graph. For the Yarn PnP and pnpm linkers we'll have exactly one location per locator, whereas the `node_modules` linker may return multiple locations per locator to satisfy the hoisting. + + - A list of "build requests", ie instructions on how to build the packages that have been laid out on disk. These requests can depend on other requests. + +5. The core will then take all the build requests and process them, parallelizing the builds as much as possible while still respecting the dependencies between them. diff --git a/website/src/docs/contributing/primer-on-rust.md b/website/src/docs/contributing/primer-on-rust.md new file mode 100644 index 00000000..f9aabfe3 --- /dev/null +++ b/website/src/docs/contributing/primer-on-rust.md @@ -0,0 +1,120 @@ +--- +category: contributing +slug: contributing/rust +title: Primer on Rust +description: Learn the basics of Rust programming language in the context of Yarn development. +sidebar: + order: 4 +--- + +Rust is a modern programming language designed for performance and safety. We'll cover here the basics when coming from a TypeScript background. Some things are very similar, some others are worth longer explanations. + +## General differences + +- Variables can be freely redefined. You can have multiple statements in a row doing `let foo = ...;`: + +```rs +let foo = 1; +let foo = String::from("bar"); +``` + +- Their _content_, however, is immutable by default. The declaration must be annotated with `mut` to allow mutating the object stored in the variable: + +```rs +let mut items = Vec::new(); +items.push("foo"); +``` + +- Rust uses iterators _a lot_. It's a powerful tool for processing collections of data efficiently, and thanks to the trait system, you can add new methods to existing iterators: + +```rs +items.iter() + .filter(|item| !item.starts_with("_")) + .map(|item| item.to_uppercase()) + .collect::>(); +``` + +## Error handling + +Rather than throwing exceptions, Rust uses a `Result` type to handle errors. This generic type accepts two parameters: `T` represents the type of the successful result, and `E` represents the type of the error. + +The trick is a special operator, `?`. This operator, which can only be used in functions that return a `Result`, allows you to propagate errors up the call stack. + +In the example below, if `input.parse()` returns an `Err(...)`, `parse_number` will immediately stop its execution and return that `err`: + +```rs +fn parse_number(input: &str) -> Result { + let number = input.parse()?; + Ok(number) +} +``` + +## Async / await + +Rust uses a similar syntax to JavaScript's `async/await`: + +```rs +async fn my_task() { + do_something().await; +} +``` + +Instead of working with promises, Rust uses `Future`s. Both are very similar, but futures **only get executed when awaited**. Another difference is that futures' typing encode not only the result of the computation, but also whoever generated it. + +In the following example, even if both futures return the same type, because they are generated by different functions, they are not the same type: + +```rs +async fn do_something_1() -> () {} +async fn do_something_2() -> () {} + +fn main() { + let mut items = vec![]; + // error[E0308]: mismatched types + items.push(do_something_1()); + items.push(do_something_2()); +} +``` + +They must be "boxed" by using a special type, `BoxFuture`: + +```rs +use futures::future::BoxFuture; + +async fn do_something_1() -> () {} +async fn do_something_2() -> () {} + +fn main() { + let mut items = vec![]; + items.push(BoxFuture::new(do_something_1())); + items.push(BoxFuture::new(do_something_2())); +} +``` + +However the borrow checker is very careful with lifetimes; passing references to async functions can be tricky when the compiler cannot prove that the references will be valid for the duration of the async function execution: + +```rs +async fn do_something(val: &i32) { + // ... +} + +async fn multiple_things_in_parallel() { + let mut futures = Vec::new(); + for i in 0..10 { + // error[E0597]: `i` does not live long enough + futures.push(do_something(&i)); + } +} +``` + +In the example above we pass a reference to the memory where `i` is stored; the compiler is smart enough to understand that this memory space will be rewritten after each `for` iteration, so it refuses to pass it to `do_something` (note that we're not calling `await`, so unlike JavaScript's promises `do_something` doesn't immediately start inside the loop). + +To wait for all futures to complete, you can use the [`join_all`](https://docs.rs/futures/latest/futures/future/fn.join_all.html) function, similar to [`Promise.allSettled`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) in JavaScript: + +```rs +async fn multiple_things_in_parallel() { + // ... + futures::future::join_all(futures).await; +} +``` + +This works well enough when you know in advance how many futures you will be waiting for. That's not always the case, especially in a package manager where tasks may cascade into other tasks. A solution to this problem is to use a [`FuturesUnordered`](https://docs.rs/futures/latest/futures/stream/struct.FuturesUnordered.html) container, which allows pushing new futures. diff --git a/website/src/docs/contributing/welcome.md b/website/src/docs/contributing/welcome.md new file mode 100644 index 00000000..848f194f --- /dev/null +++ b/website/src/docs/contributing/welcome.md @@ -0,0 +1,24 @@ +--- +category: contributing +slug: contributing/welcome +title: Welcome +description: Are you interested in contributing to Yarn? This guide will help you get started. +sidebar: + hidden: true +--- + +Are you interested in contributing to Yarn? This guide will help you get started! + +While the older codebase, codenamed Berry, was written in TypeScript, the newer codebase, codenamed ZPM, is written in Rust. Using a native language comes with both challenges and advantages. + +**Challenges:** + +- Every platform needs to be distributed. +- Higher contribution bar. + +**Opportunities:** + +- No runtime dependencies outside of the system itself. +- Control over performances. + +While we believe that the benefits will outweight the negative aspects, we also want to ensure that the community is well-supported and that everyone can contribute to the project. For this reason we have written various articles to explain the architecture of ZPM, and how to contribute to it. diff --git a/website/src/docs/contributing/writing-new-commands.md b/website/src/docs/contributing/writing-new-commands.md new file mode 100644 index 00000000..218eac2f --- /dev/null +++ b/website/src/docs/contributing/writing-new-commands.md @@ -0,0 +1,75 @@ +--- +category: contributing +slug: contributing/commands +title: Writing new commands +description: Learn how to write new commands for Yarn. +sidebar: + order: 5 +--- + +Yarn has used a library called Clipanion to power its CLI ever since the Berry codebase. Where other frameworks tend to either use functional dedicated APIs to declare their commands, Clipanion attempts to provide a more intuitive and user-friendly experience, while remaining highly integrated with TypeScript. + +The same is true in the ZPM codebase, as we ported Clipanion over to Rust (repository [here](https://github.com/arcanis/clipanion-rs)). The syntax is similar to the TypeScript implementation of Clipanion, with some twists and new capabilities. + +## Example + +```rs +use clipanion::cli; + +#[cli::command] +#[cli::path("commit")] +#[cli::category("Miscellaneous commands")] +struct MyCommand { + #[cli::option("-v,--verbose")] + verbose: bool, + + #[cli::option("-m,--message")] + message: Option, + + all: Vec, +} + +impl MyCommand { + fn run(&self) -> Result<(), String> { + // ... + Ok(()) + } +} +``` + +## Typed parameters + +Clipanion supports typed parameters out of the box. Any type that implements the `FromStr` trait can be used as a parameter. This is for example the case of the `Ident` / `Descriptor` / `Locator` types: + +```rs +#[cli::command] +#[cli::path("add")] +struct AddCommand { + packages: Vec, +} +``` + +## Documentation + +Clipanion will leverage Rust [doc comments](https://doc.rust-lang.org/rust-by-example/meta/doc.html#doc-comments) to generate documentation for each command. The documentation will be displayed in the help output of the CLI: + +```rs +/// Commit changes to the repository. +/// +/// This command will commit all changes to the repository. +#[cli::command] +#[cli::path("commit")] +#[cli::category("Miscellaneous commands")] +struct CommitCommand { + /// Verbose mode. + #[cli::option("-v,--verbose")] + verbose: bool, + + /// Commit message. + #[cli::option("-m,--message")] + message: Option, + + /// Files to commit. + all: Vec, +} +``` diff --git a/website/src/docs/getting-started/basics/_meta.yml b/website/src/docs/getting-started/basics/_meta.yml new file mode 100644 index 00000000..e8245ca0 --- /dev/null +++ b/website/src/docs/getting-started/basics/_meta.yml @@ -0,0 +1,2 @@ +label: Starting with Yarn +order: 2 diff --git a/website/src/docs/getting-started/basics/comparisons.md b/website/src/docs/getting-started/basics/comparisons.md new file mode 100644 index 00000000..ca3927b1 --- /dev/null +++ b/website/src/docs/getting-started/basics/comparisons.md @@ -0,0 +1,74 @@ +--- +category: getting-started +slug: getting-started/comparisons +title: Comparisons +description: How Yarn compares to npm, pnpm, and bun. +sidebar: + hidden: true +--- + +Every package manager makes trade-offs. This page aims to be factual about where Yarn differs, and honest about where the others do well. + +:::note +This page reflects the state of each tool as of early 2026. Package managers evolve quickly; if something here is outdated, please open an issue. +::: + +## npm + +### Where Yarn differs + +- **Ghost dependency protection.** npm uses a flat, hoisted `node_modules` layout that lets your code import packages you never declared as dependencies. Yarn's default install strategy, [PnP](/concepts/pnp), enforces your declared dependency tree through a Node.js loader, eliminating this class of bugs entirely. + +- **Workspace tooling.** Both support workspaces, but Yarn builds significantly more on top of them: [constraints](/concepts/constraints) for linting your entire dependency tree, [workspace profiles](/concepts/profiles) for shared dev dependency sets, the [`catalog:` protocol](/concepts/protocols) for unified dependency ranges, and a [task runner](/concepts/tasks) with cross-workspace dependencies. + +- **Built-in features.** Yarn includes [dependency patching](/concepts/patches), a [task runner](/concepts/tasks), and [Node.js version management](/concepts/nvm) out of the box. With npm, these require separate tools. + +- **Version management.** Yarn uses [Yarn Switch](/concepts/switch) to pin the exact package manager binary per project via the `packageManager` field. npm is bundled with Node.js and generally uses a single global version. + +- **Install speed.** Yarn's PnP mode avoids filesystem writes entirely, bringing typical installs down to a few seconds. Yarn tracks comparative benchmarks continuously on a [public dashboard](/concepts/performances). + +### Where npm does well + +- **Universality.** npm ships with Node.js, so it requires zero setup. Every Node.js developer has it available from the start. + +- **Ecosystem assumptions.** Some tools assume an npm-style `node_modules` layout, which means npm has fewer compatibility considerations to work around. + +## pnpm + +### Where Yarn differs + +- **Resolution approach.** pnpm uses symlinks and a content-addressable store to build a nested `node_modules` tree. Yarn's [PnP](/concepts/pnp) avoids `node_modules` entirely and resolves dependencies via a Node.js loader, which is faster and enables features like [zero-installs](/concepts/zero-installs). + +- **Ghost dependency coverage.** pnpm's symlink-based isolation prevents many ghost dependencies, but its `node_modules` layout cannot correctly represent all dependency trees — particularly those involving [peer dependencies](/appendix/nm-peer-deps). PnP handles these cases because it doesn't rely on a filesystem tree to model the dependency graph. + +- **Monorepo tooling.** Both have strong workspace support, but Yarn adds [constraints](/concepts/constraints) for dependency tree linting, [workspace profiles](/concepts/profiles), the [`catalog:` protocol](/concepts/protocols), and a built-in [task runner](/concepts/tasks) with cross-workspace dependencies. + +- **Node.js management.** Yarn can manage Node.js as a [regular project dependency](/concepts/nvm) via `@builtin/node`, locked in `yarn.lock`. pnpm provides `pnpm env` commands for managing Node.js versions, but not as a project dependency. + +- **Linker flexibility.** Yarn supports [three linker modes](/concepts/node-linkers) — PnP, pnpm-style symlinks, and traditional `node_modules` — all as first-class options from the same tool. + +### Where pnpm does well + +- **Disk efficiency.** pnpm's content-addressable store with hard links is very space-efficient for projects that need a `node_modules` directory. + +- **Strictness without a loader.** pnpm's symlink approach provides meaningful ghost dependency protection without requiring a custom Node.js loader, which some teams prefer. + +## bun + +### Where Yarn differs + +- **Maturity.** Yarn has been in production use since 2016 and has an extensive test suite. bun's package manager is newer and still evolving. + +- **Correctness.** bun prioritizes speed, sometimes at the expense of strict correctness. Yarn prioritizes both, with [PnP](/concepts/pnp) enforcing dependency boundaries that bun's flat `node_modules` layout does not. + +- **Monorepo features.** Yarn provides a comprehensive monorepo toolkit: [constraints](/concepts/constraints), [workspace profiles](/concepts/profiles), [task dependencies](/concepts/tasks), and the [`catalog:` protocol](/concepts/protocols). bun's workspace support is more limited. + +- **Independence.** Yarn is community-maintained and not tied to any runtime or company. bun's package manager is part of a VC-funded runtime, which means its roadmap is tied to bun's commercial direction. + +- **Linker options.** bun only produces `node_modules`. Yarn gives you [three strategies](/concepts/node-linkers) to choose from. + +### Where bun does well + +- **Raw speed.** bun's install speed is competitive, especially for cold installs on projects using `node_modules`. + +- **Unified runtime.** If you already use bun as your runtime, its built-in package manager avoids needing a separate tool. diff --git a/website/src/docs/getting-started/basics/install.md b/website/src/docs/getting-started/basics/install.md new file mode 100644 index 00000000..f0d0841c --- /dev/null +++ b/website/src/docs/getting-started/basics/install.md @@ -0,0 +1,67 @@ +--- +category: getting-started +slug: getting-started +title: Installation +description: Yarn's in-depth installation guide. +--- + +Yarn is intended to be used with [Yarn Switch](/concepts/switch), a tool that lets you use different Yarn versions depending on the project you're in. + +```bash +curl -sS https://repo.yarnpkg.com/install | bash +``` + +:::caution +You will likely have to restart your terminal sessions, **IDEs included**, for the changes to take effect. +::: + +Once Yarn Switch is ready, if you wish to start a new project, running `yarn init` is all that's needed. If you instead prefer to migrate an existing project, run the following command: + +``` +yarn switch zpm set version zpm +``` + +It instructs Yarn Switch to download the latest version of Yarn on its `zpm` release branch and run the `yarn set version zpm` command with it. This command will in turn update `packageManager` with the appropriate version. + +## Updating Yarn + +Any time you'll want to update Yarn to the latest version, just run: + +```bash +yarn set version latest +``` + +Yarn will then configure your project to use the most recent stable binary. + +## Alternative installs + +### Corepack + +While Corepack doesn't support installing different binaries depending on the platform, we provide a compatibility setting under the form of `YARNSW_COREPACK_COMPAT` which, when set, will instruct Corepack to install Yarn Switch and transparently forward Yarn CLI commands to it. + +While not recommended for a local setup, this compatibility layer can unlock you if you work in environments where dependencies are installed with Corepack before you even have a chance to run your own code, such as Netlify runners. + +### GitHub Actions + +We provide a `yarnpkg/setup-action` repository that runs the standard installation flow on GitHub Actions: + +```yaml +jobs: + my-job: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Yarn + uses: yarnpkg/setup-action@main +``` + +### Self-managed binaries + +Our [release tarballs](https://github.com/yarnpkg/zpm/releases) all provide two binaries: + +- `yarn` is the [Yarn Switch](/concepts/switch) binary, which adds support for reading the `packageManager` field. + +- `yarn-bin` is the "true" binary, containing all commands such as `yarn install`, `yarn add`, etc. + +You can bypass Yarn Switch by extracting the `yarn-bin` binary somewhere into your `$PATH` and renaming it into `yarn`. diff --git a/website/src/docs/getting-started/basics/usage.md b/website/src/docs/getting-started/basics/usage.md new file mode 100644 index 00000000..ce6fca6a --- /dev/null +++ b/website/src/docs/getting-started/basics/usage.md @@ -0,0 +1,16 @@ +--- +category: getting-started +slug: getting-started/usage +title: Usage +description: A short overview of Yarn's most used commands. +--- + +If you're coming from npm, the main changes are: + +- Running `yarn` is enough to run an install! It's an alias to `yarn install`. +- Adding or updating a dependency to a single package is done with `yarn add`. +- Upgrading a dependency across the whole project is done with `yarn up`. +- Your scripts are aliased. Calling `yarn build` is the same as `yarn run build`! +- Most registry-related commands are moved behind `yarn npm` (ex: `yarn npm audit`). + +To see the full list of commands, check the [CLI reference](/cli). diff --git a/website/src/docs/getting-started/basics/why-yarn.md b/website/src/docs/getting-started/basics/why-yarn.md new file mode 100644 index 00000000..d4311c30 --- /dev/null +++ b/website/src/docs/getting-started/basics/why-yarn.md @@ -0,0 +1,58 @@ +--- +category: getting-started +slug: welcome +title: Why Yarn? +description: What makes Yarn different, and why it might be the right package manager for your project. +sidebar: + order: 1 +--- + +Yarn is an open-source package manager for JavaScript and TypeScript projects. It focuses on correctness, performance, and developer experience — and ships with the tools you'd otherwise need to assemble yourself. + +## Correctness by default + +Most package managers use a flat `node_modules` layout that lets your code import packages you never declared as dependencies. These *ghost dependencies* cause builds that work today and break tomorrow, when the hoisting layout changes. + +Yarn's default install strategy, [Plug'n'Play](/concepts/pnp), eliminates this class of bugs entirely. It replaces `node_modules` with a Node.js loader that enforces your declared dependency tree, so every import is validated at runtime. PnP is supported natively by Vite, Webpack, Esbuild, Rspack, ESLint, and many more. + +## Fast and efficient + +PnP installs avoid copying files into `node_modules` altogether. Dependencies are stored in a content-addressable cache and resolved directly, bringing typical install times down to a few seconds — even on large projects. + +If a few seconds is still too many, [zero-installs](/concepts/zero-installs) lets you check your install artifacts into version control so that `git clone` is all you need. No install step, no waiting. + +Yarn tracks comparative benchmarks continuously on every commit, and publishes them on a [public dashboard](/concepts/performances). + +## Batteries included + +Yarn ships features that other package managers leave to third-party tools: + +- [**Workspaces**](/concepts/workspaces) are a core part of Yarn's design. Every project is a workspace, and monorepos get first-class support for cross-package references, shared dependency ranges via the `catalog:` protocol, and more. + +- [**Constraints**](/concepts/constraints) let you lint and auto-fix your entire dependency tree — enforce version consistency, detect circular dependencies, or validate `package.json` structure across all your workspaces. + +- [**Task dependencies**](/concepts/tasks) give you a built-in task runner with sequential and parallel execution, cross-workspace dependencies, and glob patterns — no need for a separate orchestration tool. + +- [**Dependency patching**](/concepts/patches) lets you fix a bug in a dependency without forking the repository. + +- [**Node.js management**](/concepts/nvm) treats Node.js as a project dependency via `@builtin/node`. The version is locked in `yarn.lock`, so every team member and CI runner uses exactly the same one. + +- [**Workspace profiles**](/concepts/profiles) let you declare shared dev dependencies once and apply them to any workspace, keeping your monorepo configuration DRY. + +## Flexible installation strategies + +Not every project can adopt PnP on day one. Yarn supports [three linker modes](/concepts/node-linkers), all stable and production-ready: + +- **PnP** (default) — the fastest and strictest option, with content-addressable storage and ghost dependency protection. +- **pnpm mode** — a symlink-based layout that offers a good middle ground between speed and compatibility. +- **node-modules mode** — a traditional `node_modules` tree for maximum ecosystem compatibility. + +You can start with `node-modules` and migrate to PnP when you're ready. The switch is a single configuration change. + +## Independent and open-source + +Yarn isn't proprietary, and it isn't backed by venture capital. It's maintained by an independent team, and its roadmap is driven by community needs rather than commercial strategy. + +## How is Yarn different from X? + +Every package manager makes different trade-offs. For a detailed look at how Yarn compares to npm, pnpm, and bun, see our [comparisons page](/getting-started/comparisons). diff --git a/website/src/docs/getting-started/basics/yarn-6.md b/website/src/docs/getting-started/basics/yarn-6.md new file mode 100644 index 00000000..b8c5c877 --- /dev/null +++ b/website/src/docs/getting-started/basics/yarn-6.md @@ -0,0 +1,57 @@ +--- +category: getting-started +slug: concepts/yarn-6 +title: Yarn 6.x +description: What's new in Yarn 6, the Rust-based rewrite of Yarn. +sidebar: + order: 2 +--- + +Yarn 6.x is a ground-up rewrite of Yarn in Rust, designed to push past the performance ceiling that JavaScript imposed on large-scale monorepos. While the core principles remain the same - correctness, developer experience, and performance - the native implementation unlocks dramatically faster operations and lower memory footprints. + +:::note +Yarn 6.x is currently in preview. The first stable release is expected in **Q3 2026**. Preview releases are already deployed in production at Datadog with minimal breaking changes. +::: + +## Performance + +The Rust rewrite delivers significant speed improvements across the board, particularly on warm cache scenarios where Yarn 6.x is up to 5x faster than its JavaScript predecessor: + +| Project | Cache | Yarn 4.x | Yarn 6.x | +| --- | --- | --- | --- | +| Next.js | Cold | 4.1s | 2.5s | +| Next.js | Warm | 577ms | 184ms | +| Gatsby | Cold | 19.8s | 11.7s | +| Gatsby | Warm | 1.7s | 0.3s | + +These gains are especially impactful in massive monorepos, where install times were previously a bottleneck. They also enable features that would have been too expensive to run in JavaScript, such as Lazy Installs. + +## Lazy Installs + +Previous Yarn versions offered two modes: regular installs (run `yarn install` after every pull) and [Zero Installs](/features/caching#zero-installs) (check install artifacts into the repository). Zero Installs removed the need for explicit installs but came at the cost of repository size, which became prohibitive in large monorepos. + +Yarn 6.x introduces a third option as the new default: **Lazy Installs**. + +Under this model, Yarn silently performs an install whenever it detects that the on-disk artifacts are out of sync with `package.json`. This check happens automatically before most commands - including `yarn run` - and has negligible overhead in the happy path thanks to the native Rust implementation. + +In practice, this means you no longer need to remember to run `yarn install` after switching branches or pulling changes. Yarn handles it for you, without bloating your repository with cached packages. + +## Yarn Switch + +With Node.js [phasing out Corepack](https://github.com/nodejs/TSC/pull/1697#issuecomment-2737093616), Yarn 6.x ships with its own version manager: [Yarn Switch](/concepts/switch). Written in Rust, it reads the `packageManager` field from your project and transparently downloads, caches, and forwards commands to the correct Yarn version. + +For more details, see the [Yarn Switch documentation](/concepts/switch). + +## Versioning roadmap + +The transition from Yarn 4.x to 6.x follows a deliberate path: + +1. **Yarn 5.x** is a stepping stone release based on the JavaScript codebase. It introduces some of the deprecations coming in 6.x, giving you time to adapt. +2. **Yarn 6.x** is the Rust-based release. Once stable, all active development will shift to this codebase. +3. **Yarn 5.x LTS** will receive critical bugfixes for approximately 30 months after 6.x reaches stable. + +## Backward compatibility + +Backward compatibility is a primary concern. Yarn 6.x is validated against the exact same test suite as its predecessors, ensuring that existing projects can upgrade with minimal friction. + +Projects using the `pnp`, `pnpm`, or `node-modules` linkers will continue to work as before. The same applies to workspaces, constraints, and other Yarn features. If your project works on Yarn 4.x, it should work on Yarn 6.x. diff --git a/website/src/docs/getting-started/extra/_meta.yaml b/website/src/docs/getting-started/extra/_meta.yaml new file mode 100644 index 00000000..de4c7107 --- /dev/null +++ b/website/src/docs/getting-started/extra/_meta.yaml @@ -0,0 +1,2 @@ +label: Good to Know +order: 3 diff --git a/website/src/docs/getting-started/extra/editor-sdks.md b/website/src/docs/getting-started/extra/editor-sdks.md new file mode 100644 index 00000000..1b4594f8 --- /dev/null +++ b/website/src/docs/getting-started/extra/editor-sdks.md @@ -0,0 +1,122 @@ +--- +category: getting-started +slug: getting-started/editor-sdks +title: Editor SDKs +description: An overview of the editor SDKs used to bring PnP compatibility to editors. +--- + +Smart IDEs (such as VSCode or IntelliJ) require special configuration for TypeScript to work when using [Yarn PnP](/concepts/pnp) installs. This page is a collection of settings for each editor we've looked into. + +The editor SDKs and settings can be generated using `yarn dlx @yarnpkg/sdks`. + +:::note +**Why are SDKs needed with Yarn PnP?** + +Yarn PnP works by generating a [Node.js loader](https://nodejs.org/api/esm.html#loaders), which has to be injected within the Node.js runtime. Many IDE extensions execute the packages they wrap (Prettier, TypeScript, ...) without consideration for loaders. + +The SDKs workaround that by generating indirection packages. When required, these indirection automatically setup the loader before forwarding the `require` calls to the real packages. +::: + +## Usage + +Generate both the base SDK and the editor settings: + +```bash +yarn dlx @yarnpkg/sdks vscode vim ... +``` + +Generate the base SDK, but no editor settings: + +```bash +yarn dlx @yarnpkg/sdks base +``` + +Update all installed SDKs & editor settings: + +```bash +yarn dlx @yarnpkg/sdks +``` + +## Tools currently supported + +:::caution +The `yarn dlx @yarnpkg/sdks` command will look at the content of your _root_ `package.json` to figure out the SDKs you need - it won't look at the dependencies from any other workspaces. +::: + +
    + +| Supported extension | Enabled if ... is found in your `package.json` dependencies | +| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| Builtin VSCode TypeScript Server | [typescript](https://yarnpkg.com/package/typescript) | +| [astro-vscode](https://marketplace.visualstudio.com/items?itemName=astro-build.astro-vscode) | [astro](https://astro.build/) | +| [vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) | [eslint](https://yarnpkg.com/package/eslint) | +| [prettier-vscode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) | [prettier](https://yarnpkg.com/package/prettier) | +| [relay](https://marketplace.visualstudio.com/items?itemName=meta.relay) | [relay](https://relay.dev/) | + +If you'd like to contribute more, [take a look here!](https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-sdks/sources/generateSdk.ts) + +
    + +## Editor setup + +### CoC nvim + +1. Install [vim-rzip](https://github.com/lbrayner/vim-rzip) + +2. Run the following command, which will generate a `.vim/coc-settings.json` file: + +```bash +yarn dlx @yarnpkg/sdks vim +``` + +### Emacs + +The SDK comes with a typescript-language-server wrapper which enables you to use the ts-ls LSP client. + +1. Run the following command, which will generate a new directory called `.yarn/sdks`: + +```bash +yarn dlx @yarnpkg/sdks base +``` + +2. Create a `.dir-locals.el` with the following content to enable Flycheck and LSP support, and make sure LSP is loaded after local variables are applied to trigger the `eval-after-load`: + +```lisp +((typescript-mode + . ((eval . (let ((project-directory (car (dir-locals-find-file default-directory)))) + (setq lsp-clients-typescript-server-args `("--tsserver-path" ,(concat project-directory ".yarn/sdks/typescript/bin/tsserver") "--stdio"))))))) +``` + +### Neovim Native LSP + +1. Install [vim-rzip](https://github.com/lbrayner/vim-rzip) + +2. Run the following command, which will generate a new directory called `.yarn/sdks`: + +```bash +yarn dlx @yarnpkg/sdks base +``` + +TypeScript support should then work out of the box with [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) and [theia-ide/typescript-language-server](https://github.com/theia-ide/typescript-language-server). + +### VSCode + +1. Install the [ZipFS](https://marketplace.visualstudio.com/items?itemName=arcanis.vscode-zipfs) extension, which is maintained by the Yarn team. + +2. Run the following command, which will generate a `.vscode/settings.json` file: + +```bash +yarn dlx @yarnpkg/sdks vscode +``` + +3. For safety reason VSCode requires you to explicitly activate the custom TS settings: + + 1. Press ctrl+shift+p in a TypeScript file + 2. Choose "Select TypeScript Version" + 3. Pick "Use Workspace Version" + +Your VSCode project is now configured to use the exact same version of TypeScript as the one you usually use, except that it will be able to properly resolve the type definitions. + +### Zed + +Zed has [native support](https://zed.dev/docs/languages/yarn) for Yarn Plug'n'Play after the base SDK is installed. diff --git a/website/src/docs/getting-started/extra/github-actions.md b/website/src/docs/getting-started/extra/github-actions.md new file mode 100644 index 00000000..34c8702e --- /dev/null +++ b/website/src/docs/getting-started/extra/github-actions.md @@ -0,0 +1,29 @@ +--- +category: getting-started +slug: getting-started/github-actions +title: GitHub Actions +description: Useful tools and settings when operating within GitHub Action environments. +--- + +Yarn provides a [`yarnpkg/setup-action`](https://github.com/yarnpkg/setup-action) action you can use in your CI workflows: + +```yaml +name: JavaScript + +on: + push: + +jobs: + typecheck: + name: Typecheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 + + - name: Setup Yarn + uses: yarnpkg/setup-action@c9778d0717d1b46cf29f06ae5ae5ad41c1bb1c1a + + - name: Running TypeScript + run: | + yarn tsc +``` diff --git a/website/src/docs/getting-started/extra/questions-and-answers.md b/website/src/docs/getting-started/extra/questions-and-answers.md new file mode 100644 index 00000000..e683d2a2 --- /dev/null +++ b/website/src/docs/getting-started/extra/questions-and-answers.md @@ -0,0 +1,221 @@ +--- +category: getting-started +slug: getting-started/qa +title: Questions & Answers +description: A list of answers to commonly asked questions. +--- + +## Why is the `yarn` package on npm still on 1.x? + +Modern releases of Yarn haven't been distributed on npm since 2019. + +The reason is simple: because Yarn wasn't distributed alongside Node.js, many people relied on something like `npm install -g yarn` as part of their image building. It meant that any breaking change would make their way on everyone using this pattern, and break their deployments. + +As a result, we decided to retire the `yarn` npm package and only use it for the few 1.x maintenance releases needed. Yarn is now installed directly from our website, via either [Corepack](https://nodejs.org/api/corepack.html) or `yarn set version`. + +## Why should you upgrade to Yarn Modern? + +While the Yarn Classic line (1.x) remains a pillar of the JavaScript ecosystem, we recommend upgrading if possible. Why's that? + +1. New features: On top of the classic features you're already used to, on top of the new ones you'll discover ([`yarn dlx`](/cli/dlx), [builtin `patch:` protocol](https://github.com/yarnpkg/berry/tree/master/packages/plugin-patch), ...), Modern offers plugins extending Yarn's featureset with changesets, [constraints](/concepts/constraints), [workspaces](/cli/workspaces/foreach), ... + +2. Efficiency: Modern features new install strategies, leading projects to only be a fraction of their past self; as an example, under the default configuration the stock CRA artifacts now only take 45MB instead of 237MB. [Performances](https://p.datadoghq.eu/sb/d2wdprp9uki7gfks-c562c42f4dfd0ade4885690fa719c818) were improved as well, with most installs now only taking a few seconds even on extremely large projects. We even made it possible to reach [zero seconds](/concepts/zero-installs)! + +3. Extensibility: Modern's architecture allows you to build your own features as you need it. No more of you being blocked waiting for us to implement this feature you dream of - you can now do it yourself, according to your own specs! Focused workspaces, custom installs, project validation, ... + +4. Stability: Modern comes after years of experience with maintaining Classic; it allowed us to finally fix longstanding design issues with how some features were implemented. Workspaces are now core components, the resolution pipeline has been streamlined, data structures are more efficient... as a result, Modern is much less likely to suffer from incorrect assumptions and other design flaws. + +5. Future proof: A big reason why we invested in Modern was that we noticed how building new features on Classic was becoming difficult - each change being too likely to have unforeseen consequences. The Modern architecture learned from our mistakes, and was designed to allow us to build features at a much higher pace - as evidenced by our new gained velocity. + +## How easy should you expect the migration from Classic to Modern to be? + +Generally, a few main things will need to be taken care of: + +1. The settings format changed. We don't read the `.npmrc` or `.yarnrc` files anymore, instead of consuming the settings from the [`.yarnrc.yml` file](/configuration/yarnrc). + +2. Some third-party packages don't list their dependencies properly and will need to be helped through the [`packageExtensions`](/configuration/yarnrc#packageExtensions) settings. + +3. Support for text editors is pretty good, but you'll need to run the one-time-setup listed in our [SDK documentation](/getting-started/editor-sdks). + +4. Some tools (mostly React Native and Flow) will require downgrading to the `node_modules` install strategy by setting the [`nodeLinker`](/configuration/yarnrc#nodeLinker) setting to `node-modules`. TypeScript doesn't have this problem. + +Most projects will only face those four problems, which can all be fixed in a good afternoon of work. For more detailed instructions, please see the detailed [migration guide](/getting-started/breaking-changes). + +## Which files should be gitignored? + +If you're using Zero-Installs: + +```gitignore +.yarn/* +!.yarn/cache +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +``` + +If you're not using Zero-Installs: + +```gitignore +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions +``` + +If you're interested to know more about each of these files: + +- `.yarn/cache` and `.pnp.*` may be safely ignored, but you'll need to run `yarn install` to regenerate them between each branch switch - which would be optional otherwise, cf [Zero-Installs](/concepts/zero-installs). + +- `.yarn/install-state.gz` is an optimization file that you shouldn't ever have to commit. It simply stores the exact state of your project so that the next commands can boot without having to resolve your workspaces all over again. + +- `.yarn/patches` contain the patchfiles you've been generating with the [`yarn patch-commit`](/cli/patch-commit) command. You always want them in your repository, since they are necessary to install your dependencies. + +- `.yarn/plugins` and `.yarn/releases` contain the Yarn releases used in the current repository (as defined by [`yarn set version`](/cli/set/version)). You will want to keep them versioned (this prevents potential issues if, say, two engineers use different Yarn versions with different features). + +- `.yarn/sdks` contains the editor SDKs generated by `@yarnpkg/sdks`. Whether to keep it in your repository or not is up to you; if you don't, you'll need to follow the editor procedure again on new clones. See [Editor SDKs](/getting-started/editor-sdks) for more details. + +- `.yarn/unplugged` should likely always be ignored since they typically hold machine-specific build artifacts. Ignoring it might however prevent [Zero-Installs](/concepts/zero-installs) from working (to prevent this, set [`enableScripts`](/configuration/yarnrc#enableScripts) to `false`). + +- `.yarn/versions` is used by the version plugin to store the package release definitions. You will want to keep it within your repository. + +- `yarn.lock` should always be stored within your repository ([even if you develop a library](#should-lockfiles-be-committed-to-the-repository)). + +- `.yarnrc.yml` (and its older counterpart, `.yarnrc`) are configuration files. They should always be stored in your project. + +:::tip +You can also add a `.gitattributes` file to identify the release and plugin bundles as binary content. This way Git won't bother showing massive diffs when each time you subsequently add or update them: + +```gitattributes +/.yarn/releases/** binary +/.yarn/plugins/** binary +``` + +::: + +## Does Yarn support ESM? + +**Yes.** + +First, remember that Yarn supports the [`node-modules` install strategy](/configuration/yarnrc#nodeLinker), which installs package exactly the same as, say, npm would. So if Yarn didn't support ESM, nothing would. If you hear someone say it doesn't, they actually mean "[Yarn PnP](/concepts/pnp) doesn't support ESM" - **except it does**, ever since the [3.1](https://dev.to/arcanis/yarn-31-corepack-esm-pnpm-optional-packages--3hak#esm-support). + +So this alone should answer your question. But if you want more details about the PnP and ESM story, then let's talk about ESM itself first. ESM is two things: at its core, it's a spec that got drafted in ES2015. However, no engine implemented it straight away: at this time the spec was pretty much just a syntactic placeholder, with nothing concrete underneath. It's only starting from late 2019 that Node finally received support for native ESM, without requiring an experimental flag. But this support had one major caveat: **the ESM loaders weren't there**. Loaders are the things that allow projects to tell Node how to locate packages and modules on the disk. You probably know some of them: [`@babel/register`](https://babeljs.io/docs/en/babel-register#compiling-plugins-and-presets-on-the-fly), [`ts-node`](https://github.com/TypeStrong/ts-node/discussions/1321), [Jest's mocks](https://github.com/facebook/jest/issues/9430), [Electron](https://github.com/electron/electron/issues/21457), and many more. + +Unlike CommonJS, the ESM module resolution pipeline is intended to be completely walled from the outside, for example so that multiple threads can share the same resolver instance. Amongst other things it meant that, without official loader support, **it was impossible to support alternate resolution strategies** - monkey-patching the resolution primitives wasn't viable anymore, so all those projects literally couldn't support ESM at all. It could only mean one thing: **ESM wasn't ready**. Yes, it was supported natively, but given it broke a sizeable part of the ecosystem with no alternative whatsoever, it couldn't be a reasonable standard - yet. + +Fortunately, Node saw the issue, started to work on loaders, and shipped a first iteration. Fast forward to today, Node Loaders are still in [heavy work](https://nodejs.org/api/esm.html#loaders) (and changed shape more than once, as highlighted by this "experimental" annotation), but have allowed us to draft a first implementation of a ESM-compatible PnP loader, which we shipped in 3.1. Strong of those learnings, we started to contribute to the Node Loaders working group, not only to help Yarn's own use cases but also those from other projects susceptible to follow our lead. + +Loaders aren't perfect yet, and until they are **ESM-only packages cannot be recommended**, but there's a way forward and as we work together we'll get there. We just have to be careful not to push people aside as we run towards our goal. + +## Should lockfiles be committed to the repository? + +**Yes.** + +Lockfiles are meant to always be stored along with your project sources - and this regardless of whether you're writing a standalone application or a distributed library. + +One persisting argument against checking-in the lockfile in the repository is about being made aware of potential problems against the latest versions of the library. People saying this argue that the lockfile being present prevents contributors from seeing such issues, as all dependencies are locked and appear fine until a consumer installs the library and uses more recent (and incompatible) dependencies. + +Although tempting, this reasoning has a fatal flaw: removing the lockfile from the repository doesn't prevent this problem from happening. In particular: + +- Active contributors won't get new versions unless they explicitly remove their install artifacts (`node_modules`), which may not happen often. Problematic dependency upgrades will thus be mainly discovered by new contributors, which doesn't make for a good first experience and may deter contributions. + +- Even assuming you run fresh installs every week, your upgrades won't be easily reversible - once you test the most recent packages, you won't test against the less recent ones. The compatibility issues will still exist, they just will be against packages that used to work but that you don't test anymore. in other words, by always testing the most recent semver release, you won't see if you accidentally start relying on a feature that wasn't available before. + +Of course these points are only part of the problem - the lack of lockfile also means that key state information are missing from the repository. When months later you or your contributors want to make a fix on one of your old projects you might not even be able to _build_ it anymore, let alone improve it. + +Lockfiles should **always** be kept within the repository. Continuous integration testing **is a good idea**, but should be left to continuous integration systems. For example, Yarn itself runs [daily tests](https://github.com/yarnpkg/berry#current-status) against the latest versions of major open-source frameworks and tools, which allows us to quickly spot any compatibility issue with the newest release, while still being guarateed that every contributor will have a consistent experience working with the project. [Dependabot](https://dependabot.com/#how-it-works) and [Renovate](https://www.whitesourcesoftware.com/free-developer-tools/renovate) are also good tools that track your dependencies updates for you. + +## How to share scripts between workspaces? + +Little-known Yarn feature: any script with a colon in its name (`build:foo`) can be called from any workspace. Another little-known feature: `$INIT_CWD` will always point to the directory running the script. Put together, you can write scripts that can be reused this way: + +```json +{ + "dependencies": { + "typescript": "^3.8.0" + }, + "scripts": { + "g:tsc": "cd $INIT_CWD && tsc" + } +} +``` + +Then, from any workspace that contains its own `tsconfig.json`, you'll be able to call TypeScript: + +```json +{ + "scripts": { + "build": "yarn g:tsc" + } +} +``` + +or if you only want to use `tsc` from the root workspace: + +```json +{ + "scripts": { + "build": "run -T tsc" + } +} +``` + +Should you want to run a script in the base of your project: + +```json +{ + "scripts": { + "build": "node ${PROJECT_CWD}/scripts/update-contributors.js" + } +} +``` + +## Is Yarn operated by Facebook? + +**No.** + +Despite the first version of Yarn having been implemented by [Sebastian McKenzie](https://twitter.com/sebmck) while working at Facebook, the initial design received feedbacks from various other companies (such as [Tilde](https://www.tilde.io) via [Yehuda Katz](https://yehudakatz.com/2016/10/11/im-excited-to-work-on-yarn-the-new-js-package-manager-2/)) and the project was put into its own [GitHub organization](https://github.com/yarnpkg). Facebook kept investing in it during the following years (mostly because it proved to be a critical part of the RN ecosystem) but major contributions came from the open-source too. + +Nowadays, the active development team is composed exclusively of people employed by non-founders companies. Facebook employees are of course still welcome to offer contributions to the project, but they would go through the same review process as everyone else. + +## Why `registry.yarnpkg.com`? Does Facebook track us? + +**No.** + +When Yarn got created, the npm registry used to be served through Fastly. This was apparently affecting the install performances, so the initial team decided to partner with Cloudflare and setup a [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy) that would simply better cache the requests before returning them. This setup didn't even have a backend on our side. + +At some point npm switched to Cloudflare as well, and we turned off the proxy to replace it by a [CNAME](https://en.wikipedia.org/wiki/CNAME_record) ([proof](https://toolbox.googleapps.com/apps/dig/#CNAME/registry.yarnpkg.com)). We still keep the hostname for reliability reasons - while it stands to reason that the Yarn domain name will keep being maintained for as long as Yarn is being used, the same isn't necessarily true of the npm domain name. That gives us the ability to redirect to a read-only copy of the registry should the primary source become unavailable. + +While we do gather some basic [client-side telemetry](/advanced/telemetry), no http logs can ever even reach the Yarn project infrastructure - and even less Facebook, which has no control over the project (see also, [Is Yarn operated by Facebook?](/getting-started/qa#is-yarn-operated-by-facebook)). + +## Queries to `registry.yarnpkg.com` return a 404/500/...; is it down? + +**No.** + +As mentioned in the [previous section](#why-registryyarnpkgcom-does-facebook-track-us), the Yarn registry is just a CNAME to the npm registry. Since we don't even have a backend, any server error can only come from the npm registry and thus should be reported to them and monitored on their [status page](https://status.npmjs.org/). + +## Is Yarn faster than other package managers? + +**Shrug 🤷‍♀️** + +At the time Yarn got released Yarn was effectively much faster than some of its competitors. Unfortunately, we failed to highlight that performance wasn't the main reason why we kept working on Yarn. Performances come and go, so while we were super fast it wasn't so much because we were doing something incredibly well, but rather that the competing implementations had a serious bug. When that bug got fixed, our miscommunication became more apparent as some people thought that Yarn was all about performances. + +Put simply, our differences lie in our priorities. Different projects make different tradeoffs, and it's exactly what happens here. We prioritized workspaces because we felt like monorepos were providing significant value. We've spent significant resources pushing for Plug'n'Play (including through [dozens of contributions to third-party projects](https://github.com/pulls?utf8=%E2%9C%93&q=is%3Apr+author%3Aarcanis+archived%3Afalse+is%3Aclosed+pnp+-user%3Ayarnpkg+)) because we felt like this was important for the ecosystem. This is the main difference: we make our own informed decisions regarding the project roadmap. + +Speed is relative and a temporary state. Processes, roadmaps and core values are what stick. + +## Why is TypeScript patched even if I don't use Plug'n'Play? + +Given that PnP is a resolver standard different from Node, tools that reimplement the `require.resolve` API need to add some logic to account for the PnP resolution. While various projects did so (for example Webpack 5 now supports PnP out of the box), a few are still on the fence about it. In the case of TypeScript we started and keep maintaining a [pull request](https://github.com/microsoft/TypeScript/pull/35206), but the TypeScript team still has to accept it. In order to unblock our users, we made the decision to automatically apply this exact pull request to the downloaded TypeScript versions, using our new [`patch:` protocol](/protocol/patch). + +Which now begs the question: why do we still apply this patch even when Plug'n'Play is disabled? The main reason is that Yarn intends to provide consistent behaviour. Some setups involve using the `node_modules` linker during development (to avoid having to setup editor [SDKS](/getting-started/editor-sdks)) and PnP in production (for install speed). If we were to only apply the patches when PnP is enabled, then the package cache would turn different, which would for example break immutable installs. + +We _could_ potentially make it configurable through a switch, but in the end we decided it wasn't worth the extra configuration: + +- The TypeScript patch is a noop if PnP isn't enabled, so this shouldn't affect your work (if it does, please open an issue) +- We hope to eventually land this PR in TypeScript one day, so the more eyes we can get on it the higher our confidence will be +- Since Yarn 3+, failing builtin patches are simply ignored and fallback to the original sources diff --git a/website/src/docs/getting-started/extra/recipes.md b/website/src/docs/getting-started/extra/recipes.md new file mode 100644 index 00000000..2e3942ec --- /dev/null +++ b/website/src/docs/getting-started/extra/recipes.md @@ -0,0 +1,62 @@ +--- +category: getting-started +slug: getting-started/recipes +title: Recipes +description: Various cool things you can do with Yarn 2 +--- + +## TypeScript + PnP quick start: + +- Initialize the repo using Yarn 2: + +``` +yarn init -2 +``` + +- Add typescript and enable [VSCode integration](/getting-started/editor-sdks): + +``` +yarn add --dev typescript +yarn dlx @yarnpkg/sdks vscode +``` + +## Running a Yarn CLI command in the specified directory: + +- Starting a new library inside a monorepo directly, without manually creating directories for it. + +``` +yarn packages/my-new-lib init +``` + +- Running an arbitrary command inside a specific workspace: + +``` +yarn packages/app tsc --noEmit +``` + +## Hybrid PnP + node_modules mono-repo: + +You may sometimes need to use `node_modules` on just part of your workspace (for example, if you use React-Native). + +- Create a separate directory for the `node_modules` project. + +``` +mkdir nm-packages/myproj +touch nm-packages/myproj/yarn.lock +``` + +- Enable the `node-modules` linker : + +``` +yarn --cwd packages/myproj config set nodeLinker node-modules +``` + +- Add a PnP ignore pattern for this path in your main `.yarnrc.yml` at the root of your monorepo: + +```yml +pnpIgnorePatterns: + - ./nm-packages/** +``` + +- Run `yarn install` to apply `pnpIgnorePatterns` in the repo root. +- Run `cd nm-packages/myproj && yarn` to install the now isolated project. diff --git a/website/src/docs/getting-started/migrating/_meta.yaml b/website/src/docs/getting-started/migrating/_meta.yaml new file mode 100644 index 00000000..8d4b6534 --- /dev/null +++ b/website/src/docs/getting-started/migrating/_meta.yaml @@ -0,0 +1,2 @@ +label: Migrations +order: 4 diff --git a/website/src/docs/getting-started/migrating/breaking-changes.md b/website/src/docs/getting-started/migrating/breaking-changes.md new file mode 100644 index 00000000..d519ddeb --- /dev/null +++ b/website/src/docs/getting-started/migrating/breaking-changes.md @@ -0,0 +1,74 @@ +--- +category: getting-started +slug: getting-started/breaking-changes +title: Breaking Changes +description: A detailed explanation of the breaking changes between two versions. +--- + +## Yarn 4 → Yarn 6 + +:::caution +This document lists the **intended breaking changes**. Yarn 6 being still in development, some features are still missing and will be implemented before we publish the first stable release. +::: + +### Not implemented + +Reimplementing a codebase comes with challenges, and the two following features haven't been implemented **yet**. We plan to address them before the first stable release: + +- Plugins; various other projects (Biome, Oxc, etc) are experimenting on that topic, and we prefer to let them clear the way before building our own solutions. + +- Windows support; we already have a path abstraction to prepare for this task, but no tests have been run on Windows yet and various things are likely broken. We recommend WSL as a workaround. + +### Important features + +Some new features have been implemented. They are not "breaking changes" per se, but may make some of your existing tooling obsolete, so be sure to take a look at them: + +- [Native Node.js version management](/concepts/nvm), which allows Yarn to treat Node.js as any other dependency, removing the need for third-party tools like nvm / fnm / volta / ... + +- [Workspace profiles](/concepts/profiles), which let you define a set of dependencies to reuse in your workspaces + +### Lockfile + +- The lockfile (`yarn.lock`) is now formatted in JSON to benefit from heavily optimized JSON parsers. Some of its layout has slightly changed: + + - All records are wrapped in an `entries` field. + - Record definition have most of their fields wrapped in a `resolution` field. + - We generally recommend using `yarn info --json` rather than manually parsing the lockfile. + +- Workspaces aren't stored in the lockfile anymore as they would waste gigabytes of storage on very large monorepos despite Yarn never using those entries. + +### Features + +- Support for the legacy Prolog constraints engine has been dropped. Constraints must be migrated to the [JavaScript engine](/concepts/constraints) introduced in Yarn 4. + +- Support for the `yarnPath` field has been dropped. Use [Yarn Switch](/concepts/switch) to manage Yarn versions in your repository. Use `yarn switch link` should you need to use a local binary. + +- Support for the `--cwd` flag has been dropped. Instead, pass the cwd path as first argument on the CLI (for example `yarn ./packages/foo add lodash`, or `yarn /path/to/project install`). As long as it contains a slash, it'll be interpreted as a path (this syntax works with both Yarn Berry and Yarn ZPM). + +### Internal design + +- Yarn doesn't support anymore having multiple workspaces in the same project sharing the same name but with different version. If set, workspace names must be unique across the project. + +- Yarn will now prioritize referencing workspaces by their name rather than their path when serializing their locators (ie you'll see `foo@workspace:foo` rather than `foo@workspace:packages/foo`). + +- Yarn won't overwrite your `package.json` formatting anymore. This currently includes the sorting of the keys in the `dependencies` / `devDependencies` / `peerDependencies` fields. + +- Yarn will automatically run transparent installs when it detects your project changed since the last time an install was run. + +- The `yarn config` command, when called with no arguments, has a different output. + +### Deprecations + +- The `.pnp.cjs` file isn't generated with the `+x` flag anymore. + +- The `yarn version apply` command has a couple of changes: + + - It will only update package versions and won't update the cross-dependency ranges anymore. Use the magic workspace ranges instead (`workspace:^`, `workspace:~`, `workspace:=`). + + - The `--recursive` flag isn't supported at the moment. Please reach out if you find it useful, as its behaviour doesn't feel intuitive and I feel like it might be in need of a rework. Consider using `--all` instead. + +- Behavior inherited from npm, packages are currently allowed to omit listing dependencies on `node-gyp` if the package happens to contain a `binding.gyp` file. + + This behavior is unsafe as the only reasonable thing the package manager can do is to imply a dependency on `*`, meaning there are no guarantees as to the version of `node-gyp` projects would end up using. + + This undocumented behavior is now **deprecated** and will be removed in a future release. Popular packages that already rely on it will get an hardcoded package extension so they keep working, but the implicit `node-gyp` dependency won't be applied to any other package going forward. diff --git a/website/src/docs/getting-started/migrating/migration-mode.md b/website/src/docs/getting-started/migrating/migration-mode.md new file mode 100644 index 00000000..bc52f477 --- /dev/null +++ b/website/src/docs/getting-started/migrating/migration-mode.md @@ -0,0 +1,57 @@ +--- +category: getting-started +slug: getting-started/migration-mode +title: Migration Mode +description: An advanced mode for gradually migrating large-scale monorepos to Yarn. +--- + +Migrating to new major releases of Yarn on small repositories is easy thanks to the `packageManager` field. As soon as you update it, either manually or through `yarn set version`, [Yarn Switch](/concepts/switch) will start using this new version. Committing the update in the repository will make sure everyone pulling your repository will use the exact version of Yarn you intended. + +This workflow works well in the majority of cases, but what of large high-velocity monorepos? Imagine a monorepo receiving hundreds of PRs a day from dozens of contributors. Performing upgrades between major releases there can be scary for developer experience teams - could the new version include an unforeseen regression that would impact your users? + +The migration mode is a tool that lets you configure the repository so that only some people use the new release, allowing you to efficiently perform gradual rollouts. + +:::warning[WARNING] +Keep in mind the procedure described here is intended for **high-velocity repositories** with **dozens of contributors**. Those repositories tend to have dedicated developer experience teams that can afford spending more time to validate software migrations. + +For most other situations the `yarn set version` flow is recommended. +::: + +## What is the migration mode? + +Under the migration mode, [Yarn Switch](/concepts/switch) will use the `packageManagerMigration` field from your `package.json` rather than `packageManager`. + +Some default settings will also change: + +- The lockfile will be written in the `.yarn/ignore/migration` folder rather than the project root. +- The local cache will be disabled; new downloaded packages will be stored in the global system cache. + +Those changes are all in the service of one goal: **the Yarn version you're migrating to isn't allowed to have lasting effect on the repository**. This ensures that only contributors who opted-in to the migration can be impacted by potential regressions. + +## How to enable the migration mode? + +1. Add a new `packageManagerMigration` field next to the existing `packageManager` field. + +2. Anyone who wish to opt-in to the migration should run `yarn switch link --migration`. + +3. Opting-out from the migration is as simple as running `yarn switch unlink`. + +Yarn Switch will automatically unlink migrations once the `packageManagerMigration` field is removed from the repository. + +## Special considerations + +### Manipulating dependencies during a migration + +Should you change your project dependencies while under the effects of a migration, Yarn will upgrade the _migrated_ lockfile (ie. the one in `.yarn/ignore/migration`) but not the mainstream one. + +Since your CI workflows will likely report errors due to the automatic enablement of the `--immutable` flag, users who opt-in to the migration mode will need to run the following command locally and check-in the produced changes: + +``` +YARN_ENABLE_MIGRATION_MODE=0 yarn install +``` + +### Zero-installs + +You'll notice an issue when running installs on [zero-install](/concepts/zero-installs)-enabled repositories: Yarn will update the `.pnp.cjs` and associated files, but **you shouldn't commit those changes**. + +We didn't find a way to write these files in temporary directories, as various third-party tools (Esbuild, TypeScript, ...) read information directly from them. diff --git a/website/src/docs/reference/configuration/_meta.yml b/website/src/docs/reference/configuration/_meta.yml new file mode 100644 index 00000000..75840e15 --- /dev/null +++ b/website/src/docs/reference/configuration/_meta.yml @@ -0,0 +1,2 @@ +label: Configuration files +order: 1 diff --git a/website/src/layouts/BaseLayout.astro b/website/src/layouts/BaseLayout.astro new file mode 100644 index 00000000..9c2d7e5f --- /dev/null +++ b/website/src/layouts/BaseLayout.astro @@ -0,0 +1,66 @@ +--- +import Nav from '../components/Nav.astro'; +import '../styles/tailwind.css'; +import '../styles/search.css'; + +interface Props { + title: string; + variant?: 'index' | 'docs'; + activePage?: string; + description?: string; + canonicalPath?: string; + ogType?: string; + ogImage?: string; +} + +const { title, variant = `index`, activePage, description, canonicalPath, ogType = `website`, ogImage } = Astro.props; +const isDocs = variant === `docs`; +const canonicalUrl = new URL(canonicalPath ?? Astro.url.pathname, Astro.site).href; +const pathname = Astro.url.pathname.replace(/\.html$/, ``).replace(/\/$/, ``) || `/index`; +const ogImageUrl = ogImage ?? new URL(`/og${pathname}.png`, Astro.site).href; +--- + + + + + + +{title} +{description && } + + +{description && } + + + + + + +{description && } + + + + + + + + + + +
    + + + +{isDocs && ( +
    +)} + +
    +
    + + + diff --git a/website/src/layouts/BlogLayout.astro b/website/src/layouts/BlogLayout.astro new file mode 100644 index 00000000..5915ea03 --- /dev/null +++ b/website/src/layouts/BlogLayout.astro @@ -0,0 +1,20 @@ +--- +import BaseLayout from './BaseLayout.astro'; +import '../styles/theme.css'; +import '../styles/docs.css'; +import '../styles/blog.css'; + +interface Props { + title: string; + description?: string; +} + +const { title, description } = Astro.props; +--- + + + + + + + diff --git a/website/src/layouts/DeckLayout.astro b/website/src/layouts/DeckLayout.astro new file mode 100644 index 00000000..0b801f2d --- /dev/null +++ b/website/src/layouts/DeckLayout.astro @@ -0,0 +1,58 @@ +--- +import '../styles/tailwind.css'; +import '../styles/theme.css'; +import '../styles/deck.css'; + +interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + + + + + +{title} + + + + + + + + + +
    + +
    + + + + + +
    + + + + + + + + diff --git a/website/src/layouts/DocsLayout.astro b/website/src/layouts/DocsLayout.astro new file mode 100644 index 00000000..99a32a88 --- /dev/null +++ b/website/src/layouts/DocsLayout.astro @@ -0,0 +1,34 @@ +--- +import BaseLayout from './BaseLayout.astro'; +import Breadcrumb from '../components/Breadcrumb.astro'; +import PageHeader from '../components/PageHeader.astro'; +import '../styles/theme.css'; +import '../styles/docs.css'; + +interface Props { + title: string; + description?: string; + breadcrumb?: string[]; + activePage?: string; + mainMaxWidth?: string; +} + +const { title, description, breadcrumb, activePage, mainMaxWidth = `780px` } = Astro.props; +--- + + +
    + + + +
    + {breadcrumb && } + {description && } + +
    +
    + + +
    diff --git a/website/src/layouts/MarkdownDocsLayout.astro b/website/src/layouts/MarkdownDocsLayout.astro new file mode 100644 index 00000000..4a1d9c2f --- /dev/null +++ b/website/src/layouts/MarkdownDocsLayout.astro @@ -0,0 +1,30 @@ +--- +import DocsLayout from './DocsLayout.astro'; +import ContentSidebar from '../components/ContentSidebar.astro'; +import ReferenceSidebar from '../components/ReferenceSidebar.astro'; +import PrevNextNav from '../components/PrevNextNav.astro'; +import navigation from '../../config/navigation.json'; + +const { frontmatter } = Astro.props; +const breadcrumb = Array.isArray(frontmatter.breadcrumb) + ? frontmatter.breadcrumb + : [`Docs`, frontmatter.breadcrumb]; + +const sidebarActivePage = frontmatter.sidebarActivePage || frontmatter.activePage; +const sidebarKey = frontmatter.sidebar || `getting-started`; +const sidebarDef = (navigation.sidebars as Record)[sidebarKey]; +const sections = sidebarDef?.sections; +--- + + + + {sections + ? + : + } + + + + + + diff --git a/website/src/pages/[...slug].astro b/website/src/pages/[...slug].astro new file mode 100644 index 00000000..05b646f7 --- /dev/null +++ b/website/src/pages/[...slug].astro @@ -0,0 +1,36 @@ +--- +import { getCollection, render } from 'astro:content'; +import MarkdownDocsLayout from '../layouts/MarkdownDocsLayout.astro'; +import { getDirForSlug, getGroupLabelForSlug } from '../components/sidebar'; +import navigation from '../../config/navigation.json'; + +const categoryToSidebar = navigation.categoryToSidebar as Record; + +export async function getStaticPaths() { + const docs = await getCollection(`docs`); + return docs.map(doc => ({ + params: {slug: doc.data.slug}, + props: {doc}, + })); +} + +const { doc } = Astro.props; +const { Content } = await render(doc); + +const groupLabel = getGroupLabelForSlug(doc.data.slug); +const sidebar = categoryToSidebar[doc.data.category ?? ``] + ?? getDirForSlug(doc.data.slug)?.split(`/`)[0] + ?? `getting-started`; + +const frontmatter = { + title: doc.data.title, + description: doc.data.description, + activePage: doc.data.slug, + sidebar, + breadcrumb: groupLabel ? ['Docs', groupLabel, doc.data.title] : ['Docs', doc.data.title], +}; +--- + + + + diff --git a/website/src/pages/benchmarks.astro b/website/src/pages/benchmarks.astro new file mode 100644 index 00000000..8c21b808 --- /dev/null +++ b/website/src/pages/benchmarks.astro @@ -0,0 +1,765 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +import {BenchmarksDashboard} from '../components/benchmarks/BenchmarksDashboard'; +import '../styles/theme.css'; +import '../styles/docs.css'; + +const benchmarkData = await fetch('https://repo.yarnpkg.com/benchmarks').then(r => r.json()); + +interface BenchPoint { timestamp: number; value: number | null; } +interface BenchEntry { subtest: string; test: string; pm: string; points: BenchPoint[]; } + +const SERIES_ORDER = ['zpm', 'yarn', 'npm', 'pnpm', 'classic'] as const; +const SERIES_META: Record = { + zpm: { name: 'yarn 6.x', dashed: false, accent: true }, + yarn: { name: 'yarn 4.x', dashed: false, accent: false }, + npm: { name: 'npm', dashed: false, accent: false }, + pnpm: { name: 'pnpm', dashed: false, accent: false }, + classic: { name: 'yarn classic', dashed: true, accent: false }, +}; + +const PROJECTS = [ + { id: 'next', name: 'Next.js' }, + { id: 'gatsby', name: 'Gatsby' }, + { id: 'monorepo', name: 'Monorepo' }, +]; + +const SCENARIOS = [ + { id: 'install-full-cold', num: '01', title: 'Full cold install', desc: 'Simulates the very first install on a machine that has never seen the project — no cache, no lockfile, no artifacts. Everything is fetched from the registry and written from scratch.' }, + { id: 'install-cache-only', num: '02', title: 'Cache only', desc: 'Like checking out a repo for the first time on a CI runner that has already built other projects. The lockfile is missing so resolution runs from scratch, but tarballs are reused from the global cache.' }, + { id: 'install-cache-and-lock', num: '03', title: 'Cache + lockfile', desc: 'The most common CI and day-to-day path. The lockfile and cache are both present — the manager just needs to materialize dependencies on disk.' }, + { id: 'install-ready', num: '04', title: 'Recurrent call', desc: 'Everything is already in place. Measures how quickly the manager can verify the tree and exit — matters every time you run install as part of a script or editor hook.' }, +]; + +const DATA: Record>> = {}; +for (const entry of benchmarkData as BenchEntry[]) { + if (!SERIES_ORDER.includes(entry.pm as any)) continue; + if (!PROJECTS.some(p => p.id === entry.test)) continue; + if (!DATA[entry.subtest]) DATA[entry.subtest] = {}; + if (!DATA[entry.subtest][entry.test]) DATA[entry.subtest][entry.test] = {}; + DATA[entry.subtest][entry.test][entry.pm] = entry.points; +} + +const lastPoint = (benchmarkData as BenchEntry[]).find(e => e.pm === 'zpm')?.points.filter(p => p.value !== null).pop(); +const lastRunDate = lastPoint ? new Date(lastPoint.timestamp * 1000).toISOString().slice(0, 10) : 'N/A'; + +const allTimestamps = (benchmarkData as BenchEntry[]).flatMap(e => e.points.map(p => p.timestamp)); +const benchMinTs = Math.min(...allTimestamps); +const benchMaxTs = Math.max(...allTimestamps); + +const INCIDENTS = [ + { + start: Date.UTC(2026, 1, 24, 18, 20, 0) / 1000, + end: Date.UTC(2026, 2, 11, 18, 18, 0) / 1000, + label: 'Accidental regression in Node.js 25.7 broke our benchmarks', + }, +]; +--- + + + + +
    + + +
    + Docs + / + Benchmarks +
    + + +
    +
    +

    How fast are we?

    +

    + Continuous benchmarks of{' '} + Yarn 6.x, + Yarn 4.x, + npm, + pnpm, and + Yarn Classic across four install scenarios on three reference projects. Lower is better. +

    +
    +
    +
    +
    Last run
    +
    {lastRunDate}
    +
    +
    +
    Runner
    +
    ubuntu-22.04 4 vCPU · 16 GB
    +
    +
    +
    Data points
    +
    90 days of history
    +
    +
    +
    + + + + + +
    +

    Methodology

    +

    Every four hours — and on every push to main — a GitHub Actions workflow runs the full benchmark matrix. Each combination of scenario, project, and package manager is timed with Hyperfine, measuring wall-clock duration. The results are published here automatically.

    + +

    How benchmarks run

    +

    Hyperfine executes each install command a minimum of 10 times after 1 warmup run. Before every timed run, a prepare step resets the environment to match the scenario — for example, deleting node_modules, the lockfile, or the global cache — so each measurement starts from the same state. The final result is the median of all timed runs.

    +

    The runner is ubuntu-latest on GitHub Actions. zpm is compiled from source with the release-lto profile; all other managers use their latest stable release at the time of each run.

    + +
    +
    + + NOTE +
    +
    + Series rendered with a dashed stroke (yarn classic) are kept for historical reference. Yarn classic is in maintenance and we do not recommend it for new projects — the modern release line is what you want for production work. +
    +
    + +

    Reference projects

    +

    We chose real-world TypeScript projects with different dependency profiles to test a wide variety of use cases.

    + +
    + + + + + + + + + + + + + + + + + + + + + + +
    Reference projects
    ProjectDescription
    nextNext.js vendors most of its dependencies except native ones. Small dependency graphs with very heavy packages.
    gatsbyGatsby lists its dependencies raw, without vendoring. Very large dependency graph made up of small packages.
    monorepoSynthetic monorepo of around ~1.5k workspaces, each with their own arbitrary set of dependencies.
    +
    +
    + +
    + + + +
    diff --git a/website/src/pages/blog/[...slug].astro b/website/src/pages/blog/[...slug].astro new file mode 100644 index 00000000..b48d5acb --- /dev/null +++ b/website/src/pages/blog/[...slug].astro @@ -0,0 +1,108 @@ +--- +import { getCollection, render } from 'astro:content'; +import BlogLayout from '../../layouts/BlogLayout.astro'; +import PostMeta from '../../components/PostMeta.astro'; +import PrevNextNav from '../../components/PrevNextNav.astro'; + +export async function getStaticPaths() { + const posts = await getCollection(`blog`); + posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime()); + + return posts.map((post, idx) => ({ + params: { slug: post.data.slug }, + props: { + post, + older: posts[idx + 1] ?? null, + newer: posts[idx - 1] ?? null, + }, + })); +} + +const { post, older, newer } = Astro.props; +const { Content, headings, remarkPluginFrontmatter } = await render(post); + +const tocHeadings = headings + .filter(h => h.depth === 2 && h.slug !== `footnote-label`) + .map(h => ({ ...h, text: h.text.replace(/#$/, ``).trim() })); +const showToc = tocHeadings.length >= 4; + +const wordCount = (remarkPluginFrontmatter?.wordCount as number) || post.body?.split(/\s+/).length || 0; +const readingTime = Math.max(1, Math.ceil(wordCount / 250)); + +const formattedDate = post.data.date.toLocaleDateString(`en-US`, { + year: `numeric`, + month: `long`, + day: `numeric`, +}); + +const postDescription = post.body ? post.body.replace(/[#*_\[\]`>]/g, ``).slice(0, 155).trim() + `\u2026` : post.data.title; + +const bskyShareUrl = `https://bsky.app/intent/compose?text=${encodeURIComponent(post.data.title)}`; +--- + + + + diff --git a/website/src/pages/blog/index.astro b/website/src/pages/blog/index.astro new file mode 100644 index 00000000..93e248e0 --- /dev/null +++ b/website/src/pages/blog/index.astro @@ -0,0 +1,100 @@ +--- +import { getCollection, render } from 'astro:content'; +import BlogLayout from '../../layouts/BlogLayout.astro'; +import PostMeta from '../../components/PostMeta.astro'; +import SkeetCard from '../../components/SkeetCard.astro'; +import { fetchSkeets, type Skeet } from '../../utils/bluesky'; + +const posts = await getCollection(`blog`); + +const enrichedPosts = await Promise.all(posts.map(async (post) => { + const { remarkPluginFrontmatter } = await render(post); + const wordCount = (remarkPluginFrontmatter?.wordCount as number) || post.body?.split(/\s+/).length || 0; + const readingTime = Math.max(1, Math.ceil(wordCount / 250)); + return { ...post, readingTime }; +})); + +const skeets = await fetchSkeets(`yarnpkg.dev`, 5); + +type TimelinePost = { type: 'post'; sortDate: Date; post: typeof enrichedPosts[number] }; +type TimelineSkeet = { type: 'skeet'; sortDate: Date; skeet: Skeet }; +type TimelineEntry = TimelinePost | TimelineSkeet; + +const entries: TimelineEntry[] = [ + ...enrichedPosts.map(post => ({ type: `post` as const, sortDate: post.data.date, post })), + ...skeets.map(skeet => ({ type: `skeet` as const, sortDate: skeet.sortDate, skeet })), +]; + +entries.sort((a, b) => b.sortDate.getTime() - a.sortDate.getTime()); + +const monthGroups = new Map(); +for (const entry of entries) { + const key = entry.sortDate.toLocaleDateString(`en-US`, { year: `numeric`, month: `long` }); + if (!monthGroups.has(key)) monthGroups.set(key, []); + monthGroups.get(key)!.push(entry); +} +--- + + +
    +
    + The Yarn Blog +
    + +

    + Let's keep in touch. +

    + +

    + Releases posts, design notes, and the occasional thinking-out-loud skeets from the @yarnpkg.dev team. +

    + +
    + {Array.from(monthGroups.entries()).map(([month, groupEntries]) => ( + <> +
    + + {month} + +
    + + {groupEntries.map(entry => { + if (entry.type === 'post') { + const { post } = entry; + const formattedDate = post.data.date.toLocaleDateString(`en-US`, { month: `short`, day: `numeric`, year: `numeric` }); + const excerpt = post.body?.split(`\n`).find((l: string) => l.trim() && !l.startsWith(`#`) && !l.startsWith(`---`) && !l.startsWith(`{`)) || ``; + + return ( +
    + + {post.data.category && ( +
    + {post.data.category} +
    + )} +

    + {post.data.title} +

    +

    + {excerpt.slice(0, 200)}{excerpt.length > 200 ? `\u2026` : ``} +

    + +
    +
    + ); + } + + return ; + })} + + ))} + +
    — end of thread
    +
    + +
    +
    diff --git a/website/src/pages/cli/[...slug].astro b/website/src/pages/cli/[...slug].astro new file mode 100644 index 00000000..7c16aefe --- /dev/null +++ b/website/src/pages/cli/[...slug].astro @@ -0,0 +1,40 @@ +--- +import {getCollection, render} from 'astro:content'; +import DocsLayout from '../../layouts/DocsLayout.astro'; +import ReferenceSidebar from '../../components/ReferenceSidebar.astro'; +import PageHeader from '../../components/PageHeader.astro'; +import Breadcrumb from '../../components/Breadcrumb.astro'; + +const cliEntries = await getCollection(`cli`); + +export async function getStaticPaths() { + const entries = await getCollection(`cli`); + return entries.map(entry => ({ + params: {slug: entry.id.replace(/^cli\//, ``)}, + props: {entry}, + })); +} + +const {entry} = Astro.props; +const {Content} = await render(entry); +const description = entry.data.commandSpec.documentation?.description ?? ``; +const pathSegments = entry.data.commandSpec.primaryPath; +--- + + + + + + + + + + +
    + +
    +
    diff --git a/website/src/pages/configuration/manifest.astro b/website/src/pages/configuration/manifest.astro new file mode 100644 index 00000000..70cabd2f --- /dev/null +++ b/website/src/pages/configuration/manifest.astro @@ -0,0 +1,25 @@ +--- +import DocsLayout from '../../layouts/DocsLayout.astro'; +import ReferenceSidebar from '../../components/ReferenceSidebar.astro'; +import PageHeader from '../../components/PageHeader.astro'; +import Breadcrumb from '../../components/Breadcrumb.astro'; +import { schemaToMarkdown, schemaFieldNames } from '../../utils/schema'; +import { renderDocsMarkdown } from '../../utils/render-markdown'; + +import schema from '../../../../documentation/src/utils/configuration/manifest.json'; + +const html = await renderDocsMarkdown(schemaToMarkdown(schema)); +const fields = schemaFieldNames(schema); +--- + + + + + + + + + + +
    + diff --git a/website/src/pages/configuration/yarnrc.astro b/website/src/pages/configuration/yarnrc.astro new file mode 100644 index 00000000..c7b19759 --- /dev/null +++ b/website/src/pages/configuration/yarnrc.astro @@ -0,0 +1,25 @@ +--- +import DocsLayout from '../../layouts/DocsLayout.astro'; +import ReferenceSidebar from '../../components/ReferenceSidebar.astro'; +import PageHeader from '../../components/PageHeader.astro'; +import Breadcrumb from '../../components/Breadcrumb.astro'; +import { schemaToMarkdown, schemaFieldNames } from '../../utils/schema'; +import { renderDocsMarkdown } from '../../utils/render-markdown'; + +import schema from '../../../../documentation/src/utils/configuration/yarnrc.json'; + +const html = await renderDocsMarkdown(schemaToMarkdown(schema)); +const fields = schemaFieldNames(schema); +--- + + + + + + + + + + +
    + diff --git a/website/src/pages/deck/jsnation-2026-06.astro b/website/src/pages/deck/jsnation-2026-06.astro new file mode 100644 index 00000000..9de87ab8 --- /dev/null +++ b/website/src/pages/deck/jsnation-2026-06.astro @@ -0,0 +1,411 @@ +--- +import DeckLayout from '../../layouts/DeckLayout.astro'; +import TitleLayout from '../../components/deck/layouts/TitleLayout.astro'; +import OneColumnLayout from '../../components/deck/layouts/OneColumnLayout.astro'; +import QuoteLayout from '../../components/deck/layouts/QuoteLayout.astro'; +import TwoColumnLayout from '../../components/deck/layouts/TwoColumnLayout.astro'; +import ImageLayout from '../../components/deck/layouts/ImageLayout.astro'; +import ClosingLayout from '../../components/deck/layouts/ClosingLayout.astro'; +import Terminal from '../../components/deck/Terminal.astro'; +import FullCodeLayout from '../../components/deck/layouts/FullCodeLayout.astro'; +import SimpleIcon from '../../components/SimpleIcon.astro'; +--- + + + + + + +
    +
    +
    2017
    +
    Yarn 1.x
    +
    first stable release
    +
    +
    +
    2019
    +
    Yarn 2.x
    +
    rewrite in TypeScript
    +
    +
    +
    2026
    +
    Yarn 6.x
    +
    rewrite in Rust
    +
    +
    +
    + + + What are we doing? + + + {/* ── SLIDE 4 — YARN, AS IT STANDS ── */} + +
    +
    + We rarely had install bugs. +
    +
    + Installs were reasonably fast. +
    +
    + Building new features was easy. +
    +
    +
    + + +
    +
    We supported monorepos with hundreds of packages. Supporting thousands was more difficult.
    +
    On such repos, yarn run could have more than 1s of latency.
    +
    +
    + + +
    + Such as Lazy Installs, which let Yarn check whether your projects are up-to-date with their dependencies, installing them if not. +
    +
    + + +
    +
    Ever since the TS rewrite I always wanted Yarn to be more than a tool for JavaScript. What about Python? Rust?
    +
    And Node.js didn't make it easy to do so, primarily because of distribution.
    +
    +
    + + + It was time to reinvent ourselves. + + + +
    +
    The TS rewrite was a teaching moment.
    +
    Too many breaking changes.
    +
    Too much friction.
    +
    +
    + + +
    +
    Back in 2019, all our tests had to be rewritten...
    +
    But now in 2026, we can reuse the tests from the TS rewrite!
    +
    +
    + + { + // The magic was in this run function - it can point to any binary! + await run(\`install\`); + + await expect(source(\`require('one-fixed-dep')\`)).resolves.toMatchObject({ + name: \`one-fixed-dep\`, + version: \`1.0.0\`, + dependencies: { + [\`no-deps\`]: { + name: \`no-deps\`, + version: \`1.0.0\`, + }, + }, + }); + }), +);`} + /> + + + + + Out of 1590 tests, 1468 are now passing. + + + +
    +
    This project started early during the AI boom. But "tell me how you'd do <JS code> but in Rust" was already useful.
    +
    Also very motivating to see my Rust proficiency grow over the course of the project!
    +
    +
    + + +
    +
    Rust has good tooling, Cargo is nice.
    +
    A lot of experimental features stuck in limbo.
    +
    Rust prevented a LOT of concurrency / shared mem pitfalls.
    +
    It however required mental model shifts.
    +
    +
    + + +
    +
    Unlike Bun, we used AI sparingly.
    +
    Considering Rust's complexity, I feel more comfortable with a human-driven rewrite.
    +
    It was still used for code review, heisenbug investigations, experimental features, and porting easy tests.
    +
    +
    + + + We're reaching the conclusion. + + + +
    +
    Any rewrite is a brutal endeavor.
    +
    If I spend my time rewriting the thing, when do I integrate it?
    +
    Can you afford freezing your main branch for N months?
    +
    First few months feel like walking in the desert.
    +
    +
    + + +
    +
    I'd be cautious about that. Rebuilding a project comes with a long tail of problems. Rust isn't a silver bullet.
    +
    AI is changing the trade-offs, but comes with hidden long-term costs - can we afford to not be intimately familiar with our codebase?
    +
    +
    + + + Curious about performances? + + + +
    +
    +

    Cold install · seconds

    + {[ + { name: `yarn 6.x`, time: `3.98`, w: `0.165`, self: true }, + { name: `pnpm`, time: `6.67`, w: `0.277`, self: false }, + { name: `yarn 4.x`, time: `8.55`, w: `0.355`, self: false }, + { name: `yarn classic`, time: `22.74`, w: `0.945`, self: false }, + { name: `npm`, time: `24.06`, w: `1.000`, self: false }, + ].map(b => ( +
    +
    {b.name}
    +
    +
    {b.time}s
    +
    + ))} +
    + +
    +

    Warm install · seconds

    + {[ + { name: `yarn 6.x`, time: `0.84`, w: `0.095`, self: true }, + { name: `pnpm`, time: `1.36`, w: `0.154`, self: false }, + { name: `yarn 4.x`, time: `1.56`, w: `0.177`, self: false }, + { name: `yarn classic`, time: `4.42`, w: `0.501`, self: false }, + { name: `npm`, time: `8.83`, w: `1.000`, self: false }, + ].map(b => ( +
    +
    {b.name}
    +
    +
    {b.time}s
    +
    + ))} +
    +
    + +

    + Geometric mean of per-project medians (Next.js, Gatsby) for each series. Bar widths normalize against the slowest series. Source: repo.yarnpkg.com/benchmarks. +

    +
    + + +
    +
    +

    Small installs · seconds

    + {[ + { name: `yarn 6.x`, time: `0.17`, w: `0.065`, self: true }, + { name: `yarn 4.x`, time: `1.01`, w: `0.393`, self: false }, + { name: `pnpm`, time: `1.28`, w: `0.495`, self: false }, + { name: `yarn classic`, time: `1.61`, w: `0.624`, self: false }, + { name: `npm`, time: `2.58`, w: `1.000`, self: false }, + ].map(b => ( +
    +
    {b.name}
    +
    +
    {b.time}s
    +
    + ))} +
    +
    +
    + + +
    +
    We built the package manager we need for the next decade.
    +
    This doesn't scale; motivation and resilience were key.
    +
    And yes, the multi-ecosystem story is coming.
    +
    More devlogs to come, stay tuned!
    +
    + + + @yarnpkg.dev + + + + @yarnpkg + + + + discord.gg/yarnpkg + +
    +
    +
    + + {/* ── Slide entrance animation ── */} + + +
    + + diff --git a/website/src/pages/docs/concepts.md b/website/src/pages/docs/concepts.md new file mode 100644 index 00000000..a4d3308c --- /dev/null +++ b/website/src/pages/docs/concepts.md @@ -0,0 +1,325 @@ +--- +layout: ../../layouts/MarkdownDocsLayout.astro +title: "Concepts — Yarn docs" +activePage: concepts +sidebar: concepts +breadcrumb: Concepts +prev: { href: "get-started/", label: "Get Started" } +next: { href: "#", label: "Reference" } +--- + +# Concepts + +Yarn makes a handful of strong opinions about what a package manager should do. This section explains *why* those opinions exist — and how to leverage them as your projects grow from a single package to a hundred. + +## Core concepts + +Every Yarn project — from the smallest CLI tool to the largest monorepo — is built on four primitives. Understanding them is enough to use Yarn productively. The later sections build on these. + +### Dependency protocols + +Yarn extends `package.json` with a rich set of protocols that declare **where** a dependency comes from, not just its version. A protocol prefix in a version range reroutes resolution to a non-registry source. + +```json title="package.json" +{ + "dependencies": { + "lodash": "npm:^4.17.21", + "my-fork": "npm:lodash@npm:4.17.21", + "internal-ui": "workspace:^", + "design-system": "portal:../design-system", + "tap-parser": "patch:tap-parser@npm:11.0.2#./patches/tap.patch", + "prettier": "github:prettier/prettier#main", + "legacy-lib": "file:./vendor/legacy.tgz" + } +} +``` + +Each protocol resolves to a cache entry, so installs remain deterministic. The most common protocols are: + +- **npm:** — the default. Points at a semver range on the npm registry (or your configured mirror). +- **workspace:** — always resolve to another workspace in the same monorepo. Enforces co-versioning at install time. +- **portal:** — symlink to a local path. Unlike `file:`, dependencies of the portal are resolved by *your* project, not theirs. +- **patch:** — apply a unified diff to a package before it's written to the cache. Survives re-install; commits to git. +- **github: / git:** — pin to a git commit, branch, or tag. Resolution records the exact SHA in the lockfile. + +:::tip[TIP] +Use `workspace:^` in monorepos instead of pinning versions. At publish time, Yarn rewrites it to the concrete version your workspace currently resolves to — so consumers of your package see a normal semver range. +::: + +### Node.js linkers + +A **linker** decides how resolved packages end up on disk. Yarn ships three first-class linkers; all produce the same module graph, only the strategy differs. + +:::steps + +1. Plug'n'Play (`pnp`) — the default + + Yarn generates a single `.pnp.cjs` file that maps every import to a zipped tarball in the global cache. No `node_modules` is created at all. Imports are resolved in O(1) at runtime. + +2. PnPM-style (`pnpm`) + + Packages are hoisted into a content-addressable store, and `node_modules` contains symlinks into that store. Compatible with tools that walk the filesystem. + +3. Classic (`node-modules`) + + A traditional `node_modules` tree, hoisted the same way npm would. Slowest but maximally compatible — useful for React Native, some legacy bundlers, and CI shells that cannot register a Node loader. + +::: + +Configure in `.yarnrc.yml`: + +```yaml title=".yarnrc.yml" +nodeLinker: pnp # or 'pnpm' or 'node-modules' +pnpMode: strict # 'strict' forbids unlisted imports +enableGlobalCache: true +``` + +:::note[NOTE] +Switching linkers is a one-command operation. Yarn re-materializes the project to match the new strategy; no changes to source code are required unless you relied on specific `node_modules` paths (e.g. `require.resolve` with a hard-coded traversal). +::: + +### Workspaces + +A workspace is a package that belongs to a larger repository — a monorepo. Declaring workspaces unlocks topologically-ordered scripts, shared dependency ranges, and constraint enforcement across packages. + +```json title="package.json (root)" +{ + "private": true, + "name": "acme", + "workspaces": [ + "packages/*", + "apps/*", + "tools/*" + ] +} +``` + +Common workspace commands: + +```terminal +yarn workspaces list --json +# Run a script in every workspace, in topological order +yarn workspaces foreach -At run build +# Add a dep to a single workspace +yarn workspace @acme/api add fastify +``` + +### Yarn Switch + +**Yarn Switch** lets a single repository declare the exact Yarn version it expects — down to the patch — and transparently fetches that binary the first time someone runs `yarn`. Everyone on the team runs the same Yarn, without globally installing anything. + +```terminal +# Pin the current repo to Yarn 4.8.1 +yarn set version 4.8.1 +> ✔ Yarn binary saved to .yarn/releases/yarn-4.8.1.cjs +> ✔ .yarnrc.yml updated +``` + +The binary is committed to the repo under `.yarn/releases/`. A tiny shim in `.yarnrc.yml` (`yarnPath:`) reroutes every subsequent `yarn` invocation to the pinned file, bypassing whatever version is globally installed. + +## Intermediary concepts + +Once the core is comfortable, these tools cover the gap between "a project that works on my laptop" and "a project that scales across a team and CI." + +### Constraints + +Constraints are declarative rules about the *contents* of `package.json` files across a monorepo. Written in Prolog (or, since 4.2, a JavaScript DSL), they answer questions like *"does every workspace declare the same version of React?"* in a single CI step. + +```js title="yarn.config.cjs" +module.exports = { + async constraints({ Yarn }) { + // Every workspace must use the same React version + const pinned = '18.3.1'; + for (const dep of Yarn.dependencies({ ident: 'react' })) { + dep.update(pinned); + } + // No workspace may depend on lodash + for (const dep of Yarn.dependencies({ ident: 'lodash' })) { + dep.error('lodash is banned; use es-toolkit instead'); + } + }, +}; +``` + +:::warning[WARNING] +Constraints run in CI by default. A failing rule will block your build — which is the point. Start strict and relax rules as the team negotiates exceptions; the opposite is much harder. +::: + +### Dependency patches + +Yarn's `patch:` protocol lets you fork a package without a real fork. Changes are captured as a unified diff, stored alongside your code, and re-applied every install. + +:::steps + +1. Open an editable copy of the package + + Yarn extracts the package to a temporary folder and opens your editor. + + ```terminal + yarn patch react-dom + > To apply changes, run: yarn patch-commit /tmp/xfs-abc/user + ``` + +2. Make your edits in the extracted folder. + +3. Commit the patch back into your project + + ```terminal + yarn patch-commit -s /tmp/xfs-abc/user + > ✔ Wrote patch to .yarn/patches/react-dom-npm-18.3.1-a91.patch + > ✔ Rewrote package.json + ``` + +4. The resulting `package.json` entry now reads `patch:react-dom@npm:18.3.1#./.yarn/patches/…`. Commit both the patch file and the manifest. + +::: + +### Node.js versioning + +Projects may pin a specific Node.js version via the `engines.node` field; Yarn (when `enableEngineChecks` is on) refuses to install if your runtime is out of range. For heavier projects, use **Corepack** or **Volta** to install Node itself per-project. + +```json title="package.json" +{ + "engines": { + "node": "^20.10.0 || ^22.0.0" + }, + "packageManager": "yarn@4.8.1" +} +``` + +### Peer dependencies + +A peer dependency is a package your library expects the **host application** to provide. React plugins, for example, declare `react` as a peer so that every plugin shares the same React instance. + +Yarn enforces peer ranges strictly: + +- Missing peers cause installs to fail unless marked `peerDependenciesMeta.optional`. +- Incompatible peer ranges print a single, grouped warning — never a wall of duplicates. +- Peers participate in Plug'n'Play's "virtual package" system (see [Virtual packages](#virtual-packages)), ensuring every consumer sees the peer its author intended. + +### Workspace profiles + +Profiles let you describe partial installs: "CI only needs runtime dependencies," "Docker image only needs `@acme/api`," "IDE needs everything including `devDependencies`." Each is a named tuple of `--workspace`, `--focus`, and `--production` flags captured in config. + +```yaml title=".yarnrc.yml" +workspaceProfiles: + ci: + focus: ["@acme/api", "@acme/worker"] + production: true + docker-api: + focus: "@acme/api" + production: true + includeRoot: false + ide: + production: false +``` + +```terminal +yarn install --profile docker-api +``` + +### Yarn Plug'n'Play + +Plug'n'Play (PnP) is Yarn's strict, hoist-free module resolver. Instead of a `node_modules` tree, Yarn generates a single lookup file — `.pnp.cjs` — that maps *(package, version, dependency)* triples to on-disk locations. + +The result: + +- **Installation is I/O-light.** No files are copied; packages stay as gzipped tarballs in the global cache. +- **Resolution is deterministic.** A module cannot accidentally require a package it didn't declare — there's no hoisted `node_modules` to fall through to. +- **Cold cache is fast.** `yarn install` on a 400-package repo drops from ~48s (npm) to ~14s (Yarn PnP) on a 4-core runner. + +:::note[NOTE] +PnP requires Node to load an extra resolver. Yarn ships a loader that Corepack sets up for you; most tools (Jest, TypeScript, ESLint, webpack, esbuild, Vite, Next.js) detect PnP automatically via their own resolver plugins. +::: + +### Task dependencies + +Scripts in a monorepo often depend on other scripts in *other* workspaces: `@acme/web`'s `build` needs `@acme/ui`'s `build` to have run first. Yarn's `workspaces foreach` understands topological order natively: + +```terminal +yarn workspaces foreach -pt run build +``` + +The flags: + +- `-p` — parallel (bounded by `--jobs`, default = CPU count) +- `-t` — topological; never run a workspace before its dependencies +- `-A` — include the root workspace +- `--from ` — only run in workspaces downstream of `` + +## Advanced concepts + +These sections cover the sharp edges. You don't need them to ship — but understanding them turns a big monorepo from a chore into a force multiplier. + +### Performances + +Yarn's performance model is built on three invariants: + +1. Every network fetch is content-addressable and cached globally. +2. Every on-disk artifact is idempotent — running `yarn install` a second time is a no-op. +3. Every stage (resolve, fetch, link, build) runs in its own bounded worker pool. + +In practice that means: + +```bash title="hyperfine output — 10k-dep monorepo, warm network, empty cache" +# cold install, median of 50 runs, 4-core CI runner +Benchmark: yarn install + Time (mean ± σ): 14.213 s ± 0.214 s + Range (min … max): 13.889 s … 14.902 s + +Benchmark: npm ci + Time (mean ± σ): 48.441 s ± 1.108 s + Range (min … max): 47.019 s … 50.722 s +``` + +With a warm cache the numbers drop another order of magnitude — a zero-install repo reaches runtime in under 400ms. + +### Virtual packages + +When two workspaces share a dependency but have *different* peer dependencies, Yarn creates a **virtual package**: an alias for the shared dep, parameterized by the caller's peers. This is what makes strict PnP workable — each consumer sees its own view of the dependency graph without duplication on disk. + +Conceptually: + +```js title="Resolved identities" +// @acme/web depends on styled-components@6, which peers react@18 +// @acme/docs depends on styled-components@6, which peers react@17 +// +// Yarn produces two *virtual* identities that point at the same tarball: +// +// styled-components@virtual:abc123#npm:6.0.0 (react@18 context) +// styled-components@virtual:def456#npm:6.0.0 (react@17 context) +``` + +:::danger[DANGER] +Never hard-code a virtual identity in your code. The `virtual:` hash is derived from the peer context and *will* change when peers move. Use the package name, and let PnP resolve it at runtime. +::: + +### Zero Installs + +A **Zero Install** repository commits its entire offline cache alongside its source. Cloning the repo is enough to run it — there is no install step, no network I/O, no CI warm-up. + +The trade-off is repository size. A 400-package project typically adds 40–80MB of compressed tarballs to the repo — paid once, amortized over every future `git clone`. + +:::steps + +1. Enable in `.yarnrc.yml` + + ```yaml title=".yarnrc.yml" + enableGlobalCache: false + nmMode: classic + nodeLinker: pnp + ``` + +2. Commit `.yarn/cache/` and `.pnp.cjs` to git. + +3. Add a check that the working tree is clean after `yarn install`: + + ```terminal + yarn install --immutable --immutable-cache --check-cache + ``` + +::: + +:::tip[TIP] +Zero Installs pair extremely well with git's `sparse-checkout`: CI jobs that only need `apps/api` can clone just that subtree plus `.yarn/cache/` and skip the rest. `yarn install` stays a no-op. +::: diff --git a/website/src/pages/docs/get-started.md b/website/src/pages/docs/get-started.md new file mode 100644 index 00000000..b285e957 --- /dev/null +++ b/website/src/pages/docs/get-started.md @@ -0,0 +1,199 @@ +--- +layout: ../../layouts/MarkdownDocsLayout.astro +title: "Get Started — Yarn docs" +activePage: get-started +sidebar: getting-started +breadcrumb: Get Started +prev: { href: "/", label: "Home" } +next: { href: "concepts/", label: "Concepts" } +--- + +# Get started with Yarn + +Install Yarn, set up your first project, and learn the commands you'll reach for ten times a day. This page is short on purpose — once you're comfortable, read [Concepts](concepts/) next. + +:::note[PREREQUISITES] +You'll need **Node.js 18.12 or newer**. Verify with `node --version`. If you don't have Node, install it via [nodejs.org](#), `nvm`, or `fnm` — Yarn doesn't care which. +::: + +## Installation + +There are three recommended ways to install Yarn. Corepack is the official route — it ships with Node itself and guarantees each project uses the exact Yarn version it declared. + +### via Corepack (recommended) + +Corepack is bundled with Node 16+. One command enables it globally: + +```terminal +corepack enable +# Opt-in to the latest stable Yarn release +corepack install --global yarn@stable +yarn --version +> 4.8.1 +``` + +### via Homebrew (macOS / Linux) + +```terminal +brew install yarn +``` + +### via winget (Windows) + +```terminal +winget install Yarn.Yarn +``` + +:::warning[WARNING] +Do **not** install Yarn 1.x (Classic) via `npm install -g yarn`. That release is in maintenance-only mode and lacks most of the features described in these docs. If you see `1.22.x`, you're on the wrong major. +::: + +## Your first project + +Starting a new Yarn project takes under a minute. + +:::steps + +1. Create a directory and pin the Yarn version + + The `set version` command writes a Yarn binary into `.yarn/releases/` and a `packageManager` field into `package.json` — so everyone on your team runs the same Yarn. + + ```terminal + mkdir hello-yarn && cd hello-yarn + yarn init -2 + > ✔ Project initialized. Yarn 4.8.1 pinned. + ``` + +2. Add a dependency + + ```terminal + yarn add lodash + > ➤ YN0000: · Yarn 4.8.1 + > ➤ YN0000: ┌ Resolution step + > ➤ YN0000: └ Completed in 0s 214ms + > ➤ YN0000: ┌ Fetch step + > ➤ YN0013: │ lodash@npm:4.17.21 can't be found in the cache and will be fetched from the remote registry + > ➤ YN0000: └ Completed in 0s 488ms + > ➤ YN0000: ┌ Link step + > ➤ YN0000: └ Completed + > ➤ YN0000: · Done in 0s 782ms + ``` + +3. Create an entry point and run it + + ```js title="index.js" + import { chunk } from 'lodash'; + + const rows = chunk(['a', 'b', 'c', 'd', 'e'], 2); + console.log(rows); + ``` + + ```terminal + yarn node index.js + > [ [ 'a', 'b' ], [ 'c', 'd' ], [ 'e' ] ] + ``` + +4. Commit the lockfile and `.yarnrc.yml` + + At minimum, commit `package.json`, `yarn.lock`, `.yarnrc.yml`, and `.yarn/releases/`. See [Migrating from npm](#migrating-from-npm) for the full `.gitignore`. + +::: + +## Common commands + +A quick reference for the handful of commands that cover 90% of daily use: + +- **`yarn`** — install everything declared in `package.json`. Equivalent to `yarn install`. +- **`yarn add `** — add a runtime dependency. Use `-D` for dev, `-P` for peer, `-O` for optional. +- **`yarn remove `** — remove a dependency from every section it appears in. +- **`yarn up `** — bump to the latest version matching your range. Add `-R` to hit the entire monorepo. +- **`yarn run + + + +
    +
    +
    +
    +
    + + {stableVersion !== canaryVersion && ( + +

    + Ship with
    + + confidence. + +

    + +

    + Deterministic, offline-first package management for JavaScript. + Built for monorepos. +

    + + + +
    + $ + curl repo.yarnpkg.com/install | bash + +
    +
    + +
    +
    + + + + + + + +
    +
    + + // did you know + + + 01 / 06 + +
    + +

    + +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    + + Principled + + +
    + + + A single yarn.lock produces byte-identical node_modules across machines, OSes, and CI providers. If it installs for you, it installs for them. + + + + Skip node_modules entirely. PnP maps imports directly to the content-addressable cache — one install instead of thousands of file copies. + + + + Every package you've ever resolved lives in the global cache. Subsequent installs — even in fresh clones — never hit the registry. + + + + Monorepo primitives are native, not bolted on: topological builds, shared ranges, and constraint-based version policies across hundreds of packages. + + + + Fetch, link, and build stages run concurrently with bounded worker pools. yarn install saturates your CPU, not your patience. + + + + Enforce version policy declaratively across a monorepo. Prolog-style rules catch drift before it lands in main. + +
    +
    +
    + + +
    +
    + + Performant + + +
    +
    +
    + + + Yarn 6.x + + + + others + +
    +

    Full cold install (install-full-cold) of the Gatsby fixture — no cache, no lockfile, no node_modules. Median of multiple runs.

    +

    + // source: repo.yarnpkg.com/benchmarks/gatsby/summary +

    +
    + +
    +
    +
    +
    + + +
    +
    + + Trusted + +
    + {['fintech co.', 'social network', 'video platform', 'e-commerce', 'dev tools', 'cloud provider', 'ride-hailing', 'streaming'].map(name => ( +
    + + {name} +
    + ))} +
    +
    +
    + + +
    +
    +
    +
    + + + + + + + + + yarn + +
    + The modern JavaScript package manager. BSD-2-Clause. Built by a distributed team of maintainers and contributors. +
    + + + + +
    +
    + // Yarn @ 2026 — GPL 3.0 +
    +
    +
    + + + + diff --git a/website/src/pages/package/[...slug].astro b/website/src/pages/package/[...slug].astro new file mode 100644 index 00000000..e5129726 --- /dev/null +++ b/website/src/pages/package/[...slug].astro @@ -0,0 +1,78 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import PackagePage from '../../components/package/PackagePage'; +import '../../styles/theme.css'; +import '../../styles/package.css'; +import simpleIconData from '@iconify-json/simple-icons/icons.json'; +import octIconData from '@iconify-json/octicon/icons.json'; + +export function getStaticPaths() { + return [{params: {slug: undefined}}]; +} + +function extractSimple(name: string) { + const icon = (simpleIconData as any).icons[name]; + if (!icon) throw new Error(`Unknown simple-icon: ${name}`); + const dw = (simpleIconData as any).width ?? 24; + return { body: icon.body, width: icon.width ?? dw, height: icon.height ?? (simpleIconData as any).height ?? dw }; +} + +function extractOct(name: string) { + const icon = (octIconData as any).icons[name]; + if (!icon) throw new Error(`Unknown octicon: ${name}`); + const suffix = name.match(/-(\d+)$/)?.[1]; + const base = suffix ? Number(suffix) : 16; + return { body: icon.body, width: icon.width ?? base, height: icon.height ?? base }; +} + +const brandIcons = { + github: extractSimple(`github`), + npm: extractSimple(`npm`), + yarn: extractSimple(`yarn`), + pnpm: extractSimple(`pnpm`), + bun: extractSimple(`bun`), +}; + +const octicons = { + 'package': extractOct(`package-16`), + 'info': extractOct(`info-16`), + 'globe': extractOct(`globe-16`), + 'file-directory': extractOct(`file-directory-16`), + 'file-directory-fill': extractOct(`file-directory-fill-16`), + 'file-directory-open-fill': extractOct(`file-directory-open-fill-16`), + 'versions': extractOct(`versions-16`), + 'file': extractOct(`file-16`), + 'file-code': extractOct(`file-code-16`), + 'copy': extractOct(`copy-16`), + 'chevron-down': extractOct(`chevron-down-16`), + 'chevron-right': extractOct(`chevron-right-12`), + 'shield': extractOct(`shield-16`), + 'law': extractOct(`law-16`), + 'repo': extractOct(`repo-16`), + 'home': extractOct(`home-16`), + 'diff': extractOct(`diff-16`), + 'link-external': extractOct(`link-external-16`), + 'x': extractOct(`x-16`), +}; +--- + + + + + + diff --git a/website/src/pages/presentation/demo.astro b/website/src/pages/presentation/demo.astro new file mode 100644 index 00000000..8603f474 --- /dev/null +++ b/website/src/pages/presentation/demo.astro @@ -0,0 +1,448 @@ +--- +import DeckLayout from '../../layouts/DeckLayout.astro'; +import TitleLayout from '../../components/deck/layouts/TitleLayout.astro'; +import SectionLayout from '../../components/deck/layouts/SectionLayout.astro'; +import OneColumnLayout from '../../components/deck/layouts/OneColumnLayout.astro'; +import TwoColumnLayout from '../../components/deck/layouts/TwoColumnLayout.astro'; +import TextImageLayout from '../../components/deck/layouts/TextImageLayout.astro'; +import CodeSideLayout from '../../components/deck/layouts/CodeSideLayout.astro'; +import FullCodeLayout from '../../components/deck/layouts/FullCodeLayout.astro'; +import TerminalLayout from '../../components/deck/layouts/TerminalLayout.astro'; +import ImageLayout from '../../components/deck/layouts/ImageLayout.astro'; +import TextTerminalLayout from '../../components/deck/layouts/TextTerminalLayout.astro'; +import ClosingLayout from '../../components/deck/layouts/ClosingLayout.astro'; +import Terminal from '../../components/deck/Terminal.astro'; +--- + + + + {/* ── SLIDE 1 — TITLE ── */} + + + {/* ── SLIDE 2 — SECTION DIVIDER ── */} + + + {/* ── SLIDE 3 — ONE COLUMN ── */} + +

    + A single yarn.lock produces byte-identical node_modules across every machine, OS, and CI provider that touches the repo. No more "works on my laptop." No more 3am cache-busting in CI. +

    +

    + Determinism is the floor, not a feature. Everything else — the cache, Plug'n'Play, workspaces — only works because resolution is boring and predictable. Read the design doc if you want the gory parts. +

    +
    + + {/* ── SLIDE 4 — TWO COLUMNS ── */} + + +

    The old way

    +

    + Resolve, then copy thousands of files into node_modules. Hope the layout matches what your bundler expects. Pray nothing duplicates. +

    +
      +
    • ~40,000 file copies, typical app
    • +
    • Phantom dependencies hide bugs
    • +
    • Cold installs are I/O-bound
    • +
    +
    + +

    The PnP way

    +

    + Resolve once, write a single .pnp.cjs. Imports map directly to the content-addressable cache. No copies, no duplicates, no surprises. +

    +
      +
    • One file, ~3MB, fully indexed
    • +
    • Strict — phantom deps are errors
    • +
    • Cold installs are network-bound
    • +
    +
    +
    + + {/* ── SLIDE 5 — TEXT + IMAGE ── */} + +

    + Every package you've ever resolved lives in a global, content-addressable cache. Subsequent installs — even in fresh clones, even on a new laptop — hit the registry zero times. +

    +
      +
    • Zero-install repos commit the offline mirror
    • +
    • Tarballs are checksummed; tampering is a hard error
    • +
    • CI cache restores in seconds, not minutes
    • +
    +
    + + {/* ── SLIDE 6 — LARGE CODE BLOCK ── */} + +

    + Three things to notice: +

    +
      +
    1. packageManager pins the Yarn version per-project. Corepack handles the install.
    2. +
    3. workspaces is a glob — that's all the monorepo configuration most teams need.
    4. +
    5. resolutions override transitive deps without forking. Use sparingly.
    6. +
    +
    + + {/* ── SLIDE 7 — CODE WITH HIGHLIGHT ── */} + highlighted block is the whole policy — every react dep, anywhere in the monorepo, gets pinned to ^19.1.0. Run yarn constraints --fix to apply.`} + code={`// Enforce a single React version across every workspace. +// Drift is a CI failure, not a code review nit. + +module.exports = { + async constraints({ Yarn }) { + for (const dep of Yarn.dependencies({ ident: 'react' })) { + dep.update('^19.1.0'); + } + + for (const ws of Yarn.workspaces()) { + ws.set('engines.node', '>=20.11'); + ws.set('license', 'BSD-2-Clause'); + } + }, +};`} + /> + + {/* ── SLIDE 8 — TERMINAL ANIMATION ── */} + + + {/* ── SLIDE 9 — IMAGE / SCREENSHOT ── */} + + + {/* ── SLIDE 10 — INLINE STYLES KITCHEN SINK ── */} + + +

    Text formatting

    +

    + Bold for emphasis you'd actually say out loud. Italic for register shifts and titles like The Mythical Man-Month. Underlined when something is a defined term. +

    +

    + Use inline code for filenames like .yarnrc.yml, commands like yarn add, and identifiers like packageManager. +

    +

    + Links go to yarnpkg.com or to the internal RFC tracker. Keyboard shortcuts use K. +

    +
    + + +

    Lists

    + +

    // unordered

    +
      +
    • Determinism is the floor, not a feature
    • +
    • The cache makes everything else possible
    • +
    • Workspaces are first-class
    • +
    + +

    // ordered

    +
      +
    1. Run corepack enable
    2. +
    3. Pin with yarn set version stable
    4. +
    5. Commit the .yarn/releases binary
    6. +
    +
    +
    + + {/* ── SLIDE 11 — TERMINAL + TEXT ── */} + +

    + yarn workspaces foreach runs scripts across every package in topological order. The -pt flag enables parallel execution with topological awareness — dependents wait for dependencies, everything else runs at once. +

    +

    + No Lerna. No Nx config. No glue. +

    +
    + + {/* ── SLIDE 12 — SECTION DIVIDER 2 ── */} + + + {/* ── SLIDE 13 — STEPS + CODE ── */} + + +
      +
    1. + Enable corepack.
      + Ships with Node ≥16.10. One-time setup, system-wide. +
    2. +
    3. + Pin the version.
      + Writes packageManager into package.json. +
    4. +
    5. + Convert the lockfile.
      + yarn import reads package-lock.json and produces yarn.lock. +
    6. +
    7. + Verify in CI.
      + --immutable fails the build if the lockfile would change. +
    8. +
    +
    + + + + # 1. enable corepack + $ corepack enable + + # 2. pin yarn 4.8.1 + $ yarn set version stable + Yarn 4.8.1 written to .yarn/releases + + # 3. convert lockfile + $ yarn import + Imported 412 entries from package-lock.json + + # 4. lock down ci + $ yarn install --immutable + Done in 14.2s. + + +
    + + {/* ── SLIDE 14 — CLOSING / Q&A ── */} + + + {/* ── Terminal animation script ── */} + + +
    diff --git a/website/src/pages/quiz.astro b/website/src/pages/quiz.astro new file mode 100644 index 00000000..38e67a5e --- /dev/null +++ b/website/src/pages/quiz.astro @@ -0,0 +1,68 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +import '../styles/theme.css'; +import '../styles/quiz.css'; +import quiz from '../../config/quiz.json'; +const { questions, levels } = quiz; +--- + + + +
    +
    +
    + A short pop quiz +
    + +

    + Do you know Yarn? +

    + +

    + Ten yes-or-no questions about what Yarn can actually do. Most developers are surprised by a few! +

    +
    + +
    +
    +
    +
    + +
    + 1 + / + + 10 + + · + 0 + + correct + +
    +
    + +
    +
    +
    + + +
    + + + + +
    diff --git a/website/src/pages/switch/[...slug].astro b/website/src/pages/switch/[...slug].astro new file mode 100644 index 00000000..ad2f18a1 --- /dev/null +++ b/website/src/pages/switch/[...slug].astro @@ -0,0 +1,40 @@ +--- +import {getCollection, render} from 'astro:content'; +import DocsLayout from '../../layouts/DocsLayout.astro'; +import ReferenceSidebar from '../../components/ReferenceSidebar.astro'; +import PageHeader from '../../components/PageHeader.astro'; +import Breadcrumb from '../../components/Breadcrumb.astro'; + +const switchEntries = await getCollection(`switch`); + +export async function getStaticPaths() { + const entries = await getCollection(`switch`); + return entries.map(entry => ({ + params: {slug: entry.id.replace(/^switch\//, ``)}, + props: {entry}, + })); +} + +const {entry} = Astro.props; +const {Content} = await render(entry); +const description = entry.data.commandSpec.documentation?.description ?? ``; +const pathSegments = entry.data.commandSpec.primaryPath; +--- + + + + + + + + + + +
    + +
    +
    diff --git a/website/src/scripts/starfield.ts b/website/src/scripts/starfield.ts new file mode 100644 index 00000000..439c73c5 --- /dev/null +++ b/website/src/scripts/starfield.ts @@ -0,0 +1,266 @@ +import {CONSTELLATION_LIBRARY} from '../data/constellations'; + +export interface StarfieldState { + theme: string; + starDensity: number; + starSpeed: number; + starOpacity: number; + constellations: boolean; +} + +interface Star { + theta: number; + phi: number; + r: number; + baseAlpha: number; + twinklePhase: number; + twinkleSpeed: number; + isConstellation?: boolean; + _visible?: boolean; + _sx?: number; + _sy?: number; + _depth?: number; +} + +interface ShootingStar { + x: number; + y: number; + vx: number; + vy: number; + life: number; + maxLife: number; + trail: { x: number; y: number }[]; +} + +export function createStarfield( + canvas: HTMLCanvasElement, + state: StarfieldState, +): { initStars: () => void } { + const ctx = canvas.getContext(`2d`)!; + let stars: Star[] = []; + let constellationStars: Star[] = []; + let constellations: [Star, Star][] = []; + let shootingStars: ShootingStar[] = []; + let W = window.innerWidth, H = window.innerHeight, DPR = 1; + + let catImg: HTMLImageElement | null = null; + let catImgReady = false; + let catRect: { x: number; y: number; w: number; h: number } | null = null; + + (function loadCat() { + const im = new Image(); + im.crossOrigin = `anonymous`; + im.onload = () => { catImg = im; catImgReady = true; computeCatPath(); }; + im.src = `cat.png`; + })(); + + function computeCatPath() { + const el = document.getElementById(`cat-img`); + if (!el) { catRect = null; return; } + const rect = el.getBoundingClientRect(); + if (catImg) { + const iw = catImg.naturalWidth, ih = catImg.naturalHeight; + const scale = Math.min(rect.width / iw, rect.height / ih); + const dw = iw * scale, dh = ih * scale; + const dx = rect.left + (rect.width - dw) / 2; + const dy = rect.top + (rect.height - dh) / 2; + catRect = { x: dx, y: dy, w: dw, h: dh }; + } else { + catRect = { x: rect.left, y: rect.top, w: rect.width, h: rect.height }; + } + } + + function resize() { + DPR = Math.min(window.devicePixelRatio || 1, 2); + W = window.innerWidth; + H = window.innerHeight; + canvas.width = W * DPR; + canvas.height = H * DPR; + canvas.style.width = W + `px`; + canvas.style.height = H + `px`; + ctx.setTransform(DPR, 0, 0, DPR, 0, 0); + computeCatPath(); + initStars(); + } + + function initStars() { + const density = state.starDensity; + const targetOnScreen = (density / 100) * 2700; + const base = targetOnScreen * 3.0; + const count = Math.max(0, Math.round(base * (W * H) / (1920 * 1080))); + + stars = []; + for (let i = 0; i < count; i++) { + const u = Math.random(), v = Math.random(); + const theta = 2 * Math.PI * u; + const phi = Math.acos(2 * v - 1); + stars.push({ theta, phi, r: Math.random()*1.2+0.25, baseAlpha: Math.random()*0.6+0.3, twinklePhase: Math.random()*Math.PI*2, twinkleSpeed: Math.random()*0.8+0.3 }); + } + + constellations = []; + constellationStars = []; + const MIN_ANGLE = 0.42; + const MIN_COS = Math.cos(MIN_ANGLE); + const anchors: { theta: number; phi: number }[] = []; + const MAX_ATTEMPTS = 4000; + let attempts = 0; + while (anchors.length < 40 && attempts++ < MAX_ATTEMPTS) { + const u = Math.random(), v = Math.random(); + const theta0 = 2 * Math.PI * u; + const phi0 = Math.acos(2 * v - 1); + const ok = anchors.every(a => { + const dot = Math.sin(phi0)*Math.sin(a.phi)*Math.cos(theta0-a.theta) + Math.cos(phi0)*Math.cos(a.phi); + return dot < MIN_COS; + }); + if (ok) anchors.push({ theta: theta0, phi: phi0 }); + } + + const shuffled = CONSTELLATION_LIBRARY.slice().sort(() => Math.random() - 0.5); + + for (let idx = 0; idx < anchors.length && idx < shuffled.length; idx++) { + const anchor = anchors[idx]; + const pattern = shuffled[idx]; + const sP = Math.sin(anchor.phi), cP = Math.cos(anchor.phi); + const sT = Math.sin(anchor.theta), cT = Math.cos(anchor.theta); + const ex = { x: -sT, y: 0, z: cT }; + const ey = { x: cP*cT, y: -sP, z: cP*sT }; + const n = { x: sP*cT, y: cP, z: sP*sT }; + const sizeRad = 0.14 + Math.random() * 0.08; + const rot = Math.random() * Math.PI * 2; + const nodes = pattern.stars.map(([nx, ny]) => { + const lx = (nx - 0.5) * 2 * sizeRad + (Math.random() - 0.5) * 0.008; + const ly = (ny - 0.5) * 2 * sizeRad + (Math.random() - 0.5) * 0.008; + const rx = lx*Math.cos(rot) - ly*Math.sin(rot); + const ry = lx*Math.sin(rot) + ly*Math.cos(rot); + const px = n.x + rx*ex.x + ry*ey.x; + const py = n.y + rx*ex.y + ry*ey.y; + const pz = n.z + rx*ex.z + ry*ey.z; + const mag = Math.hypot(px, py, pz); + const ux = px/mag, uy = py/mag, uz = pz/mag; + const phi = Math.acos(Math.max(-1, Math.min(1, uy))); + const theta = Math.atan2(uz, ux); + const star: Star = { theta, phi, r: 1.4+Math.random()*0.6, baseAlpha: 0.9+Math.random()*0.1, twinklePhase: Math.random()*Math.PI*2, twinkleSpeed: 0.6+Math.random()*0.5, isConstellation: true }; + stars.push(star); + constellationStars.push(star); + return star; + }); + for (const [a, b] of pattern.edges) constellations.push([nodes[a], nodes[b]]); + } + } + + function spawnShootingStar() { + const startX = Math.random() * W * 0.6; + const startY = Math.random() * 300 + 40; + const angle = Math.PI / 4 + (Math.random() - 0.5) * 0.3; + const speed = 8 + Math.random() * 4; + shootingStars.push({ x: startX, y: startY, vx: Math.cos(angle)*speed, vy: Math.sin(angle)*speed, life: 0, maxLife: 60+Math.random()*30, trail: [] }); + } + + let t = 0; + let lastShoot = 0; + let lastFrameTs = 0; + let rotationAngle = 0; + const ROT_AXIS = (() => { const v = { x: 0.25, y: 0.92, z: 0.30 }; const m = Math.hypot(v.x, v.y, v.z); return { x: v.x/m, y: v.y/m, z: v.z/m }; })(); + + function rotateAroundAxis(x: number, y: number, z: number, axis: { x: number; y: number; z: number }, ang: number) { + const c = Math.cos(ang), s = Math.sin(ang); + const { x: ux, y: uy, z: uz } = axis; + const dot = ux*x + uy*y + uz*z; + return { x: x*c+(uy*z-uz*y)*s+ux*dot*(1-c), y: y*c+(uz*x-ux*z)*s+uy*dot*(1-c), z: z*c+(ux*y-uy*x)*s+uz*dot*(1-c) }; + } + + function tick(ts: number) { + const dt = lastFrameTs ? Math.min(0.05, (ts - lastFrameTs) / 1000) : 0; + lastFrameTs = ts; + t = ts * 0.001; + rotationAngle += (state.starSpeed || 0) * dt; + ctx.clearRect(0, 0, W, H); + const isDark = state.theme === `dark`; + const starColor = isDark ? `255, 255, 255` : `255, 200, 100`; + const conColor = isDark ? `200, 210, 255` : `12, 16, 48`; + ctx.save(); + const opacityMul = Math.max(0, Math.min(1, (state.starOpacity ?? 100) / 100)); + const cx = W/2, cy = H/2; + const projScale = Math.hypot(W, H) * 0.55; + for (const s of stars) { + const sinPhi = Math.sin(s.phi); + const bx = sinPhi * Math.cos(s.theta); + const by = Math.cos(s.phi); + const bz = sinPhi * Math.sin(s.theta); + const p = rotateAroundAxis(bx, by, bz, ROT_AXIS, rotationAngle); + s._visible = (p.z >= -0.1); + s._sx = cx + p.x * projScale; + s._sy = cy + p.y * projScale; + s._depth = p.z; + if (!s._visible) continue; + if (s._sx < -40 || s._sx > W+40 || s._sy < -40 || s._sy > H+40) continue; + const depthFade = Math.min(1, Math.max(0, (p.z + 0.1) / 0.4)); + const tw = 0.6 + 0.4 * Math.sin(t * s.twinkleSpeed + s.twinklePhase); + const a = s.baseAlpha * tw * depthFade * opacityMul; + if (a <= 0.01) continue; + ctx.beginPath(); + ctx.arc(s._sx, s._sy, s.r, 0, Math.PI * 2); + ctx.fillStyle = `rgba(${starColor}, ${a.toFixed(3)})`; + ctx.fill(); + if (s.r > 1.0) { + ctx.beginPath(); + ctx.arc(s._sx, s._sy, s.r * 3, 0, Math.PI * 2); + ctx.fillStyle = `rgba(${starColor}, ${(a * 0.12).toFixed(3)})`; + ctx.fill(); + } + } + if (state.constellations) { + ctx.strokeStyle = `rgba(${conColor}, ${isDark ? 0.18 : 0.12})`; + ctx.lineWidth = 0.6; + for (const [a, b] of constellations) { + if (!a._visible || !b._visible) continue; + const minDepth = Math.min(a._depth!, b._depth!); + const fade = Math.min(1, Math.max(0, (minDepth + 0.1) / 0.4)); + if (fade <= 0.02) continue; + ctx.globalAlpha = fade * opacityMul; + ctx.beginPath(); + ctx.moveTo(a._sx!, a._sy!); + ctx.lineTo(b._sx!, b._sy!); + ctx.stroke(); + } + ctx.globalAlpha = 1; + } + if (ts - lastShoot > 5000 + Math.random() * 6000 && isDark) { spawnShootingStar(); lastShoot = ts; } + shootingStars = shootingStars.filter(ss => { + ss.x += ss.vx; ss.y += ss.vy; ss.life++; + ss.trail.push({ x: ss.x, y: ss.y }); + if (ss.trail.length > 18) ss.trail.shift(); + const alpha = 1 - (ss.life / ss.maxLife); + for (let i = 0; i < ss.trail.length; i++) { + const p = ss.trail[i]; + const ta = (i / ss.trail.length) * alpha; + ctx.beginPath(); + ctx.arc(p.x, p.y, 1.2 * (i / ss.trail.length), 0, Math.PI * 2); + ctx.fillStyle = `rgba(255, 240, 200, ${ta.toFixed(3)})`; + ctx.fill(); + } + ctx.beginPath(); + ctx.arc(ss.x, ss.y, 1.6, 0, Math.PI * 2); + ctx.fillStyle = `rgba(255, 250, 230, ${alpha.toFixed(3)})`; + ctx.fill(); + return ss.life < ss.maxLife; + }); + if (catImgReady && catRect) { + ctx.globalCompositeOperation = `destination-out`; + ctx.drawImage(catImg!, catRect.x, catRect.y, catRect.w, catRect.h); + ctx.globalCompositeOperation = `source-over`; + } + ctx.restore(); + requestAnimationFrame(tick); + } + + window.addEventListener(`resize`, resize); + window.addEventListener(`scroll`, () => computeCatPath(), { passive: true }); + window.addEventListener(`load`, () => setTimeout(computeCatPath, 200)); + document.fonts?.ready?.then(() => computeCatPath()); + + resize(); + requestAnimationFrame(tick); + + return { initStars }; +} diff --git a/website/src/styles/blog.css b/website/src/styles/blog.css new file mode 100644 index 00000000..43e9cb7e --- /dev/null +++ b/website/src/styles/blog.css @@ -0,0 +1,787 @@ +/* ────────── Blog styles ────────── */ + +/* Layout shell */ +.blog-shell { + max-width: 820px; + margin: 0 auto; + padding: 64px 32px 120px; +} + +/* Page header */ +.blog-eyebrow { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--fg-mute); + margin-bottom: 14px; +} +.blog-title { + font-size: clamp(38px, 5vw, 56px); + font-weight: 500; + line-height: 1.04; + letter-spacing: -0.02em; + color: var(--fg); + margin: 0 0 18px; +} +.blog-lede { + font-size: 17px; + line-height: 1.6; + color: var(--fg-dim); + max-width: 620px; + margin: 0 0 48px; +} + +/* Filter / subscribe row */ +.blog-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: 1px solid var(--line-strong); + border-radius: 999px; + font-size: 12.5px; + color: var(--fg-dim); + background: color-mix(in oklch, var(--fg) 4%, transparent); + cursor: pointer; + transition: all 0.15s; + text-decoration: none; + font-family: inherit; +} +.blog-chip:hover { color: var(--fg); border-color: var(--fg-mute); } +.blog-chip.active { + color: var(--accent); + background: var(--accent-soft); + border-color: var(--accent-line); +} +.blog-chip-count { + font-family: 'JetBrains Mono', monospace; + font-size: 10.5px; + color: var(--fg-mute); +} + +.blog-subscribe { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 14px; + border: 1px solid var(--line-strong); + border-radius: 10px; + font-size: 13px; + color: var(--fg-dim); + background: color-mix(in oklch, var(--fg) 4%, transparent); + text-decoration: none; + transition: all 0.15s; + font-family: inherit; +} +.blog-subscribe:hover { color: var(--fg); border-color: var(--fg-mute); } +.blog-subscribe svg { color: oklch(0.75 0.15 40); } + +/* ───────── Timeline ───────── */ +.timeline { + position: relative; + padding-left: 48px; +} +.timeline::before { + content: ""; + position: absolute; + left: 11px; + top: 6px; + bottom: 6px; + width: 0; + border-left: 2px dotted var(--line-strong); +} + +/* Month label */ +.tl-month { + position: relative; + margin: 56px 0 28px; + padding-left: 0; +} +.tl-month:first-child { margin-top: 0; } +.tl-month-inner { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 5px 12px 5px 10px; + border: 1px solid var(--line-strong); + border-radius: 999px; + background: color-mix(in oklch, var(--bg-0) 80%, transparent); + backdrop-filter: blur(8px); + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--fg-dim); +} +.tl-month-inner::before { + content: ""; + display: inline-block; + width: 6px; height: 6px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} +.tl-month::before { + content: ""; + position: absolute; + left: -44px; + top: 11px; + width: 14px; height: 14px; + border-radius: 50%; + background: var(--bg-0); + border: 2px solid var(--accent); +} + +/* Base timeline item */ +.tl-item { + position: relative; + margin: 0 0 28px; +} +.tl-item::before { + content: ""; + position: absolute; + left: -42px; + top: 22px; + width: 10px; height: 10px; + border-radius: 50%; + background: var(--bg-0); + border: 2px solid var(--fg-mute); + z-index: 1; +} +.tl-item:last-child { margin-bottom: 0; } + +/* ───── Post card (prominent) ───── */ +.tl-item.is-post { + margin: 44px 0 44px; +} +.tl-item.is-post + .tl-item { margin-top: 44px; } + +.tl-post { + position: relative; + display: block; + padding: 30px 32px 28px; + border: 1px solid var(--line-strong); + border-radius: 16px; + background: color-mix(in oklch, var(--card) 115%, var(--accent-soft) 6%); + backdrop-filter: blur(14px) saturate(130%); + -webkit-backdrop-filter: blur(14px) saturate(130%); + text-decoration: none; + color: inherit; + transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s, background 0.2s; + box-shadow: + 0 1px 0 color-mix(in oklch, var(--fg) 8%, transparent) inset, + 0 20px 50px -30px color-mix(in oklch, var(--accent) 50%, transparent), + 0 1px 2px rgba(0,0,0,0.2); + overflow: hidden; +} +.tl-post::after { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 2px; + background: linear-gradient(to right, var(--accent) 0%, var(--accent-line) 40%, transparent 100%); + opacity: 0.9; +} +.tl-post:hover { + border-color: var(--accent-line); + transform: translateY(-1px); + background: color-mix(in oklch, var(--card) 105%, var(--accent-soft) 14%); + box-shadow: + 0 1px 0 color-mix(in oklch, var(--fg) 12%, transparent) inset, + 0 28px 60px -28px color-mix(in oklch, var(--accent) 70%, transparent), + 0 2px 4px rgba(0,0,0,0.25); +} +.tl-item.is-post::before { + width: 14px; height: 14px; + left: -44px; + top: 30px; + border-color: var(--accent); + background: var(--accent); + box-shadow: + 0 0 0 4px color-mix(in oklch, var(--bg-0) 60%, transparent), + 0 0 0 6px color-mix(in oklch, var(--accent) 45%, transparent), + 0 0 14px 2px color-mix(in oklch, var(--accent) 40%, transparent); +} + +.post-kind { + font-family: 'JetBrains Mono', monospace; + font-size: 10.5px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 14px; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 4px 10px 4px 8px; + border: 1px solid var(--accent-line); + border-radius: 999px; + background: var(--accent-soft); +} +.post-kind::before { + content: ""; + width: 5px; height: 5px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 6px var(--accent); +} +.post-title { + font-size: 26px; + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.02em; + color: var(--fg); + margin: 0 0 12px; + text-wrap: balance; +} +.tl-post:hover .post-title { color: var(--accent); } +.post-excerpt { + font-size: 15px; + line-height: 1.6; + color: var(--fg-dim); + margin: 0 0 22px; +} +.post-meta { + display: flex; + align-items: center; + gap: 14px; + font-size: 12.5px; + color: var(--fg-mute); +} +.post-author { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--fg-dim); +} +.post-avatar { + width: 22px; + height: 22px; + border-radius: 50%; + background: linear-gradient(135deg, oklch(0.7 0.15 var(--avatar-h, 280)), oklch(0.5 0.18 calc(var(--avatar-h, 280) + 60))); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 9.5px; + font-family: 'JetBrains Mono', monospace; + font-weight: 600; + color: #fff; + letter-spacing: 0; + flex-shrink: 0; +} +.post-avatar.real-img { + background: none; +} +.post-avatar.real-img img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} +.post-dot { + width: 3px; height: 3px; + border-radius: 50%; + background: var(--fg-mute); + opacity: 0.6; +} + +/* ───── Skeet card ───── */ +.tl-skeet { + position: relative; + padding: 16px 18px 14px 18px; + border: 1px solid var(--line); + border-radius: 12px; + background: color-mix(in oklch, var(--card) 60%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + transition: border-color 0.15s; +} +.tl-skeet:hover { border-color: var(--line-strong); } +.tl-item.is-skeet::before { + border-color: oklch(0.70 0.15 230); + background: var(--bg-0); +} + +.skeet-head { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} +.skeet-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + flex-shrink: 0; + overflow: hidden; +} +.skeet-avatar img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} +.skeet-author { + display: flex; + align-items: baseline; + gap: 7px; + flex: 1; + min-width: 0; + flex-wrap: wrap; +} +.skeet-name { + font-size: 13.5px; + font-weight: 500; + color: var(--fg); +} +.skeet-handle { + font-size: 12px; + color: var(--fg-mute); + font-family: 'JetBrains Mono', monospace; +} +.skeet-date { + font-size: 11.5px; + color: var(--fg-mute); + margin-left: auto; + font-family: 'JetBrains Mono', monospace; + flex-shrink: 0; +} +.skeet-icon { + width: 14px; + height: 14px; + color: oklch(0.70 0.15 230); + flex-shrink: 0; + margin-left: -2px; +} +.skeet-text { + font-size: 14.5px; + line-height: 1.55; + color: var(--fg); + margin: 0 0 12px; + white-space: pre-wrap; + word-wrap: break-word; +} +.skeet-text a { + color: var(--accent); + text-decoration: none; +} +.skeet-text a:hover { text-decoration: underline; } +.skeet-text code { + font-family: 'JetBrains Mono', monospace; + font-size: 0.9em; + padding: 1px 5px; + border-radius: 4px; + background: color-mix(in oklch, var(--fg) 7%, transparent); + border: 1px solid var(--line); +} +.skeet-like { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 10px; + border: 1px solid var(--line); + border-radius: 999px; + font-size: 12px; + color: var(--fg-dim); + text-decoration: none; + background: transparent; + transition: all 0.15s; + font-family: inherit; +} +.skeet-like:hover { + color: oklch(0.75 0.18 20); + border-color: oklch(0.60 0.18 20 / 0.4); + background: oklch(0.55 0.18 20 / 0.08); +} +.skeet-like svg { width: 12px; height: 12px; } + +/* ───── BlueSky static embed ───── */ +.bsky-embed { + margin: 28px auto; + padding: 20px 22px 16px; + border: 1px solid var(--line-strong); + border-radius: 12px; + background: #fff; + color: #000; +} +.bsky-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 14px; +} +.bsky-avatar { + width: 42px; + height: 42px; + border-radius: 50%; + flex-shrink: 0; + object-fit: cover; +} +.bsky-author-info { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} +.bsky-name { + font-size: 15px; + font-weight: 600; + color: #000; + line-height: 1.3; + text-decoration: none; +} +.bsky-name:hover { text-decoration: underline; } +.bsky-handle { + font-size: 12.5px; + color: #888; + font-family: 'JetBrains Mono', monospace; +} +.bsky-logo { + flex-shrink: 0; + color: #1185FE !important; + transition: opacity 0.15s; +} +.bsky-logo:hover { opacity: 0.7; } +.bsky-logo-icon { + width: 22px; + height: 22px; + display: block; +} +.bsky-text { + font-size: 16px; + line-height: 1.6; + color: #000; + margin: 0 0 14px; + white-space: pre-wrap; + word-wrap: break-word; + max-width: 500px; +} +.bsky-text a { + color: #0085ff; + text-decoration: none; +} +.bsky-text a:hover { text-decoration: underline; } +.bsky-footer { + display: flex; + align-items: center; + gap: 14px; + font-size: 12.5px; + color: #888; + font-family: 'JetBrains Mono', monospace; +} +.bsky-date { + color: #888; + text-decoration: none; + transition: color 0.15s; +} +.bsky-date:hover { color: #555; } +.bsky-likes { + display: inline-flex; + align-items: center; + gap: 5px; + color: #e0245e; +} +.bsky-heart-icon { width: 13px; height: 13px; } + +/* End of timeline */ +.tl-end { + position: relative; + margin-top: 48px; + padding-left: 0; + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--fg-mute); +} +.tl-end::before { + content: ""; + position: absolute; + left: -45px; + top: 5px; + width: 10px; height: 10px; + border-radius: 50%; + background: var(--bg-0); + border: 2px solid var(--fg-mute); +} + +/* ───────── Article page ───────── */ +.article-shell { + max-width: 720px; + margin: 0 auto; + padding: 64px 32px 120px; + position: relative; +} + +.article-header { + margin-bottom: 56px; + padding-bottom: 32px; + border-bottom: 1px solid var(--line); +} +.article-eyebrow { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 18px; + display: flex; + align-items: center; + gap: 10px; +} +.article-eyebrow::before { + content: ""; + width: 18px; height: 1px; + background: var(--accent); + display: inline-block; +} +.article-title { + font-size: clamp(32px, 4.5vw, 48px); + font-weight: 500; + line-height: 1.1; + letter-spacing: -0.02em; + margin: 0 0 24px; + color: var(--fg); +} +.article-header .post-avatar { width: 28px; height: 28px; font-size: 10.5px; } + +/* Prose — blog-specific overrides on top of .prose base */ +.article-prose { + counter-reset: h2-counter; +} + +.article-prose h2, +.article-prose h3 { + scroll-margin-top: 88px; +} +.article-prose h2 { + display: flex; + align-items: baseline; + gap: 14px; +} +.article-prose h2::before { + content: counter(h2-counter, decimal-leading-zero); + counter-increment: h2-counter; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + color: var(--fg-mute); + letter-spacing: 0.1em; + flex-shrink: 0; +} + +.article-prose p { + font-size: 17px; + line-height: 1.7; +} + +.article-prose a:not(.heading-anchor):not(.no-prose-link) { + text-decoration: underline; + text-decoration-color: var(--accent-line); + text-underline-offset: 3px; + border-bottom: none; +} +.article-prose a:not(.heading-anchor):not(.no-prose-link):hover { + text-decoration-color: var(--accent); +} + +.article-prose .footnotes ol { + list-style: decimal; + padding-left: 1.4em; + counter-reset: none; +} +.article-prose .footnotes ol > li { + display: list-item; + min-height: auto; + scroll-margin-top: 160px; +} +.article-prose .footnotes ol > li + li { + margin-top: 12px; +} +.article-prose .footnotes ol > li::before, +.article-prose .footnotes ol > li::after { + content: none; +} +.article-prose .footnotes ol > li::marker { + color: var(--fg-mute); +} + +/* ───── Footnote tooltips ───── */ +.fn-ref, +.fn-ref > a { + scroll-margin-top: 160px; +} +.fn-ref { + cursor: help; + position: relative; +} +.fn-tooltip { + display: none; + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + width: max-content; + max-width: 380px; + padding: 12px 16px; + border-radius: 10px; + background: var(--card); + border: 1px solid var(--line-strong); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25); + font-size: 13.5px; + line-height: 1.55; + color: var(--fg-dim); + z-index: 100; + text-align: left; + font-weight: 400; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + pointer-events: none; +} +.fn-tooltip::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 6px solid transparent; + border-top-color: var(--line-strong); +} +.fn-tooltip a { + color: var(--accent); + text-decoration: underline; + text-decoration-color: var(--accent-line); + text-underline-offset: 2px; +} +.fn-tooltip code { + font-family: 'JetBrains Mono', monospace; + font-size: 0.9em; + padding: 1px 5px; + border-radius: 4px; + background: color-mix(in oklch, var(--fg) 7%, transparent); + border: 1px solid var(--line); +} +.fn-ref:hover .fn-tooltip { + display: block; +} + +.article-prose hr { + border: 0; + border-top: 1px solid var(--line); + margin: 48px 0; +} + +/* Share buttons */ +.share-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: 1px solid var(--line-strong); + border-radius: 8px; + font-size: 12.5px; + color: var(--fg-dim); + background: transparent; + text-decoration: none; + transition: all 0.15s; + font-family: inherit; + cursor: pointer; +} +.share-btn:hover { + color: var(--fg); + border-color: var(--fg-mute); + background: color-mix(in oklch, var(--fg) 4%, transparent); +} + +/* Back-to-blog link */ +.back-link { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--fg-dim); + text-decoration: none; + margin-bottom: 28px; + font-family: 'JetBrains Mono', monospace; + transition: color 0.15s; +} +.back-link:hover { color: var(--fg); } + +/* Floating TOC */ +.toc { + position: fixed; + top: 120px; + right: max(24px, calc((100vw - 720px) / 2 - 240px)); + width: 220px; + font-size: 12.5px; + line-height: 1.45; + max-height: calc(100vh - 160px); + overflow-y: auto; + padding: 4px 0 4px 16px; + border-left: 1px solid var(--line); + z-index: 5; +} +.toc-label { + font-family: 'JetBrains Mono', monospace; + font-size: 10.5px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--fg-mute); + margin-bottom: 12px; +} +.toc ol { + list-style: none; + margin: 0; + padding: 0; + counter-reset: toc-c; +} +.toc li { margin: 0; } +.toc a { + display: flex; + align-items: baseline; + gap: 8px; + padding: 5px 0; + padding-left: 18px; + text-decoration: none; + color: var(--fg-mute); + transition: color 0.15s; + border-left: 2px solid transparent; + margin-left: -18px; +} +.toc a::before { + counter-increment: toc-c; + content: counter(toc-c, decimal-leading-zero); + flex-shrink: 0; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + letter-spacing: 0.05em; + color: var(--fg-mute); + opacity: 0.5; +} +.toc a span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.toc a:hover { color: var(--fg-dim); } +.toc a.active { + color: var(--accent); + border-left-color: var(--accent); +} +.toc a.active::before { color: var(--accent); opacity: 1; } + +@media (max-width: 1280px) { + .toc { display: none; } +} + + +/* Responsive */ +@media (max-width: 720px) { + .blog-shell, .article-shell { padding: 40px 20px 80px; } + .timeline { padding-left: 36px; } + .timeline::before { left: 9px; } + .tl-item::before { left: -32px; } + .tl-month::before { left: -34px; } + .tl-end::before { left: -35px; } + .blog-meta-row { flex-direction: column; align-items: flex-start; } +} diff --git a/website/src/styles/deck.css b/website/src/styles/deck.css new file mode 100644 index 00000000..edaf7e9f --- /dev/null +++ b/website/src/styles/deck.css @@ -0,0 +1,189 @@ +/* Deck presentation styles — CSS custom properties */ +.deck-root { + --accent-h: 38; + --accent: oklch(0.78 0.16 var(--accent-h)); + --accent-soft: oklch(0.78 0.16 var(--accent-h) / 0.14); + --accent-line: oklch(0.78 0.16 var(--accent-h) / 0.35); + + --code-bg: rgba(8, 10, 30, 0.78); + --code-border: rgba(120, 130, 180, 0.20); + --term-bg: rgba(4, 6, 18, 0.85); + + --sx-keyword: oklch(0.78 0.16 38); + --sx-string: oklch(0.78 0.10 150); + --sx-number: oklch(0.78 0.13 280); + --sx-comment: oklch(0.55 0.04 260); + --sx-fn: oklch(0.82 0.10 220); + --sx-prop: oklch(0.85 0.06 60); + --sx-punct: oklch(0.65 0.03 260); + --sx-tag: oklch(0.78 0.13 340); + + --term-prompt: oklch(0.78 0.16 38); + --term-out: oklch(0.85 0.04 260); + --term-dim: oklch(0.62 0.04 260); + --term-ok: oklch(0.80 0.13 145); + --term-warn: oklch(0.80 0.14 80); + --term-err: oklch(0.72 0.18 25); +} + +html[data-theme="light"] .deck-root { + --code-bg: rgba(255, 255, 255, 0.78); + --code-border: rgba(12, 16, 48, 0.10); + --term-bg: rgba(255, 255, 255, 0.86); + + --sx-keyword: oklch(0.55 0.18 38); + --sx-string: oklch(0.50 0.13 150); + --sx-number: oklch(0.50 0.18 280); + --sx-comment: oklch(0.55 0.04 260); + --sx-fn: oklch(0.50 0.14 230); + --sx-prop: oklch(0.45 0.08 60); + --sx-punct: oklch(0.45 0.03 260); + --sx-tag: oklch(0.55 0.18 340); + + --term-prompt: oklch(0.55 0.18 38); + --term-out: oklch(0.30 0.04 260); + --term-dim: oklch(0.55 0.04 260); + --term-ok: oklch(0.55 0.15 145); + --term-warn: oklch(0.60 0.15 80); + --term-err: oklch(0.55 0.20 25); +} + +/* deck-stage transparent passthrough */ +deck-stage { display: block; width: 100%; height: 100vh; background: transparent !important; } + +/* ── Stars background ── */ +.slide-stars { + background-image: + radial-gradient(1px 1px at 12% 18%, rgba(255,255,255,0.7), transparent 60%), + radial-gradient(1px 1px at 28% 64%, rgba(255,255,255,0.5), transparent 60%), + radial-gradient(1.4px 1.4px at 47% 22%, rgba(255,255,255,0.85), transparent 60%), + radial-gradient(1px 1px at 62% 80%, rgba(255,255,255,0.55), transparent 60%), + radial-gradient(1px 1px at 78% 12%, rgba(255,255,255,0.7), transparent 60%), + radial-gradient(1.2px 1.2px at 84% 52%, rgba(255,255,255,0.65), transparent 60%), + radial-gradient(1px 1px at 36% 38%, rgba(255,255,255,0.45), transparent 60%), + radial-gradient(1px 1px at 70% 32%, rgba(255,255,255,0.5), transparent 60%), + radial-gradient(1px 1px at 92% 86%, rgba(255,255,255,0.55), transparent 60%), + radial-gradient(1px 1px at 6% 78%, rgba(255,255,255,0.5), transparent 60%), + radial-gradient(1px 1px at 50% 8%, rgba(255,255,255,0.6), transparent 60%), + radial-gradient(1px 1px at 18% 92%, rgba(255,255,255,0.5), transparent 60%); +} +html[data-theme="light"] .slide-stars { opacity: 0; } + +/* ── Title em styling ── */ +h1.slide-hero-title em { font-style: normal; color: var(--fg-dim); font-weight: 300; } +h2.slide-title em { font-style: normal; color: var(--fg-dim); font-weight: 300; } + +/* ── Inline formatting cascades ── */ +.slide-body-text strong, .slide-lede strong, .slide-bullets li strong, .slide-numbered li strong { color: var(--fg); font-weight: 500; } +.slide-body-text em, .slide-lede em, .slide-bullets li em, .slide-numbered li em { font-style: italic; color: var(--fg); } +.slide-body-text u, .slide-bullets li u, .slide-numbered li u { text-decoration-color: var(--accent); text-underline-offset: 4px; text-decoration-thickness: 2px; } +.slide-body-text a, .slide-lede a, .slide-bullets li a, .slide-numbered li a { + color: var(--accent); + text-decoration: none; + border-bottom: 1px solid var(--accent-line); + padding-bottom: 1px; + transition: border-color 0.2s; +} +.slide-body-text a:hover { border-bottom-color: var(--accent); } +.slide-body-text + .slide-body-text { margin-top: 22px; } + +/* ── List pseudo-elements ── */ +.slide-bullets li::before { + content: ''; + position: absolute; + left: 4px; + top: 28px; + width: 18px; + height: 1px; + background: var(--accent); +} +.slide-bullets li:last-child, .slide-numbered li:last-child { border-bottom: none; } +.slide-numbered { counter-reset: oli; } +.slide-numbered li { counter-increment: oli; } +.slide-numbered li::before { + content: counter(oli, decimal-leading-zero); + position: absolute; + left: 0; + top: 14px; + font-family: 'JetBrains Mono', monospace; + font-size: 16px; + color: var(--accent); + letter-spacing: 0.06em; + font-weight: 500; +} + +/* ── Column rule ── */ +.slide-col-rule .slide-cols-2 > div:nth-child(2) { border-left: 1px solid var(--line); padding-left: 80px; } + +/* ── Code block dot colors ── */ +.slide-codeblock-dot:nth-child(1) { background: oklch(0.70 0.16 25 / 0.6); } +.slide-codeblock-dot:nth-child(2) { background: oklch(0.80 0.14 80 / 0.6); } +.slide-codeblock-dot:nth-child(3) { background: oklch(0.78 0.13 145 / 0.6); } + +/* ── Shiki overrides ── */ +.slide-codeblock .shiki { background: transparent !important; } +.slide-codeblock .shiki code { font-family: inherit; } + +/* ── Terminal dot colors ── */ +.slide-terminal-dot:nth-child(1) { background: oklch(0.70 0.16 25 / 0.6); } +.slide-terminal-dot:nth-child(2) { background: oklch(0.80 0.14 80 / 0.6); } +.slide-terminal-dot:nth-child(3) { background: oklch(0.78 0.13 145 / 0.6); } + +/* ── Terminal JS token classes ── */ +.slide-terminal .line { display: block; min-height: 1.55em; white-space: pre-wrap; } +.slide-terminal .prompt { color: var(--term-prompt); } +.slide-terminal .cmd { color: var(--fg); } +.slide-terminal .out { color: var(--term-out); } +.slide-terminal .dim { color: var(--term-dim); } +.slide-terminal .ok { color: var(--term-ok); } +.slide-terminal .warn { color: var(--term-warn); } +.slide-terminal .err { color: var(--term-err); } +.slide-terminal .accent { color: var(--accent); } +.slide-terminal .cursor { + display: inline-block; + width: 0.55em; + height: 1.05em; + background: var(--fg); + vertical-align: text-bottom; + margin-left: 2px; + animation: deck-blink 1.05s steps(1) infinite; +} +@keyframes deck-blink { 50% { opacity: 0; } } + +/* ── Image frame backgrounds ── */ +.slide-image-frame .placeholder { + background-image: repeating-linear-gradient( + 45deg, + transparent 0, + transparent 14px, + var(--line) 14px, + var(--line) 15px + ); +} +.slide-image-frame .caption { + background: linear-gradient(0deg, var(--card) 0%, transparent 100%); +} + +/* ── Entrance animations ── */ +section.slide[data-deck-active].is-entering .reveal { + animation: deck-reveal 700ms cubic-bezier(0.22, 1, 0.36, 1) both; +} +section.slide[data-deck-active].is-entering .reveal-1 { animation-delay: 60ms; } +section.slide[data-deck-active].is-entering .reveal-2 { animation-delay: 180ms; } +section.slide[data-deck-active].is-entering .reveal-3 { animation-delay: 300ms; } +section.slide[data-deck-active].is-entering .reveal-4 { animation-delay: 420ms; } +section.slide[data-deck-active].is-entering .reveal-5 { animation-delay: 540ms; } +@keyframes deck-reveal { + from { opacity: 0; transform: translateY(14px); filter: blur(2px); } + to { opacity: 1; transform: translateY(0); filter: blur(0); } +} +section.slide[data-deck-active].is-entering { + animation: deck-slidein 520ms cubic-bezier(0.22, 1, 0.36, 1) both; +} +@keyframes deck-slidein { + from { transform: translateY(18px); } + to { transform: translateY(0); } +} + +/* ── Theme toggle hover ── */ +.deck-theme-toggle:hover { color: var(--fg); border-color: var(--fg-dim); transform: scale(1.05); } diff --git a/website/src/styles/docs.css b/website/src/styles/docs.css new file mode 100644 index 00000000..9d4c7d60 --- /dev/null +++ b/website/src/styles/docs.css @@ -0,0 +1,493 @@ +/* ────────── Documentation page styles ────────── */ + +/* ────────── Flow spacing system ────────── + A single pair-based rhythm: every pair of siblings gets exactly + one rule. Priority cascade (later wins): + 1. reset — every .prose child has margin: 0 + 2. default sibling gap + 3. block-adjacent (symmetric padding around heavy blocks) + 4. section starters (space BEFORE a heading) + 5. heading → first child (tight, intentional) +*/ +.prose { + color: var(--fg); + --flow-xs: calc(var(--spacing) * 4); + --flow-sm: calc(var(--spacing) * 6); + --flow-md: calc(var(--spacing) * 8); + --flow-lg: calc(var(--spacing) * 10); + --flow-xl: calc(var(--spacing) * 14); +} + +/* 1. Reset — one source of truth */ +:where(.prose > *) { margin: 0; } + +/* 2. Default gap between any two siblings */ +:where(.prose > * + *) { margin-top: var(--flow-md); } + +/* 3. Heading → first child (tight when child is plain text) */ +:where(.prose > h1 + *) { margin-top: var(--flow-sm); } +:where(.prose > h2 + *) { margin-top: var(--flow-sm); } +:where(.prose > h3 + *) { margin-top: var(--flow-xs); } +:where(.prose > h4 + *) { margin-top: var(--flow-xs); } + +/* 5. Space BEFORE a heading — major section breaks (highest priority) */ +:where(.prose > * + h2) { margin-top: var(--flow-lg); } +:where(.prose > * + h3) { margin-top: var(--flow-lg); } +:where(.prose > * + h4) { margin-top: var(--flow-md); } + +.prose h1 { + font-size: clamp(32px, 4vw, 44px); + font-weight: 500; + letter-spacing: -0.025em; + line-height: 1.1; + text-wrap: balance; +} +.prose h2 { + font-size: 26px; + font-weight: 500; + letter-spacing: -0.015em; + line-height: 1.2; + scroll-margin-top: 88px; +} +.prose h3 { + font-size: 19px; + font-weight: 500; + letter-spacing: -0.01em; + line-height: 1.3; + scroll-margin-top: 88px; +} +.prose h4 { + font-size: 15px; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--fg-dim); + scroll-margin-top: 88px; +} +.prose .lead { + font-size: 18px; + line-height: 1.55; + color: var(--fg-dim); + text-wrap: pretty; +} +.prose li { + text-box: trim-both cap alphabetic; +} +.prose p { + text-box: trim-both cap alphabetic; + font-size: 15.5px; + line-height: 1.68; + color: var(--fg-dim); + text-wrap: pretty; +} +.prose p strong { color: var(--fg); font-weight: 600; } +.prose a:not(.heading-anchor):not(.no-prose-link) { + color: var(--accent); + text-decoration: none; + border-bottom: 1px solid color-mix(in oklch, var(--accent) 35%, transparent); + transition: border-color 0.15s; +} +.prose a:has(code:first-child:last-child) { + border-bottom: none; +} +.prose a:has(code:first-child:last-child) code { + text-decoration: underline; +} +.prose a:not(.heading-anchor):not(.no-prose-link):hover { + border-bottom-color: var(--accent); +} +.prose :not(pre) > code { + font-size: 0.88em; + padding: 0.15em 0.42em; + border-radius: 5px; + background: color-mix(in oklch, var(--fg) 8%, transparent); + border: 1px solid var(--line); + color: var(--fg); + font-weight: 500; +} + +/* Toast */ +.toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%) translateY(8px); + background: var(--card); + backdrop-filter: blur(16px) saturate(160%); + -webkit-backdrop-filter: blur(16px) saturate(160%); + border: 1px solid var(--line-strong); + color: var(--fg); + padding: 10px 16px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease, transform 0.25s cubic-bezier(0.22, 1, 0.36, 1); + z-index: 200; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25); +} +.toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +/* ─── Ordered list (step-by-step) ─── */ +.prose ol { + list-style: none; + padding: 0; + counter-reset: step; +} +.prose ol > li { + position: relative; + display: flex; + align-items: first baseline; + min-height: 32px; + padding-bottom: 20px; + counter-increment: step; +} +.prose ol > li:last-child { + padding-bottom: 0; +} +.prose ol > li::before { + content: counter(step, decimal-leading-zero); + margin-right: var(--flow-md); + flex: none; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + font-family: 'JetBrains Mono', monospace; + font-size: 11.5px; + font-weight: 500; + color: var(--accent); + background: var(--accent-soft); + border: 1px solid var(--accent-line); + border-radius: 8px; + letter-spacing: 0.02em; +} +.prose ol > li:not(:last-child)::after { + content: ''; + position: absolute; + left: 15.5px; + top: 36px; + bottom: 6px; + width: 1px; + background: var(--line); +} +.prose ol > li > .item > p:first-child { + color: var(--fg); + font-weight: 500; + font-size: 15.5px; + line-height: 1.45; + margin-bottom: 6px; +} +.prose ol > li > .item > * + * { + margin-top: var(--flow-sm); +} + +/* ─── Unordered lists ─── */ +.prose ul { + list-style: none; + padding: 0; +} +.prose ul > li { + position: relative; + padding-left: 22px; + font-size: 15.5px; + line-height: 1.68; + color: var(--fg-dim); + margin-top: var(--flow-sm); +} +.prose ul > li::before { + content: ''; + position: absolute; + left: 6px; + top: 0.22em; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--accent); + opacity: 0.7; +} +.prose ul > li > * + * { + margin-top: var(--flow-sm); +} + +/* ─── Table ─── */ +.prose table { + width: 100%; + border-collapse: collapse; + font-size: 14.5px; + line-height: 1.6; +} +.prose thead th { + text-align: left; + font-weight: 600; + font-size: 12px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--fg-mute); + padding: 8px 14px; + border-bottom: 1px solid var(--line-strong); +} +.prose tbody td { + padding: 10px 14px; + color: var(--fg-dim); + border-bottom: 1px solid var(--line); + vertical-align: top; +} +.prose tbody tr:last-child td { + border-bottom: none; +} +.prose tbody td code { + font-size: 0.88em; + padding: 0.12em 0.4em; + border-radius: 5px; + background: color-mix(in oklch, var(--fg) 8%, transparent); + border: 1px solid var(--line); +} + +/* ─── Blockquote ─── */ +.prose blockquote { + border-left: 3px solid var(--line-strong); + padding: var(--flow-xs) var(--flow-sm); + color: var(--fg-mute); + font-style: italic; +} +.prose blockquote > * + * { margin-top: var(--flow-sm); } +.prose blockquote p { color: inherit; } + +/* ─── Admonition ─── */ +.admonition { + border: 1px solid var(--line); + border-left-width: 3px; + border-radius: 10px; + padding: var(--flow-xs) var(--flow-sm) var(--flow-sm); + background: color-mix(in oklch, var(--card) 50%, transparent); + backdrop-filter: blur(8px); +} +.admonition .adm-header { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + margin-bottom: var(--flow-xs); +} +.admonition .adm-header svg { width: 14px; height: 14px; flex-shrink: 0; } +.admonition .adm-body { color: var(--fg-dim); font-size: 14.5px; line-height: 1.6; } +.admonition .adm-body > * + * { margin-top: var(--flow-md); } +.admonition .adm-body code { + background: color-mix(in oklch, var(--fg) 10%, transparent); + border: 1px solid var(--line); + padding: 0.1em 0.35em; + border-radius: 4px; + font-size: 0.9em; +} + +.admonition.note { background: var(--adm-note-bg); border-left-color: var(--adm-note-border); } +.admonition.note .adm-header { color: var(--adm-note-fg); } +.admonition.tip { background: var(--adm-tip-bg); border-left-color: var(--adm-tip-border); } +.admonition.tip .adm-header { color: var(--adm-tip-fg); } +.admonition.warning { background: var(--adm-warn-bg); border-left-color: var(--adm-warn-border); } +.admonition.warning .adm-header { color: var(--adm-warn-fg); } +.admonition.danger { background: var(--adm-danger-bg); border-left-color: var(--adm-danger-border); } +.admonition.danger .adm-header { color: var(--adm-danger-fg); } + +/* ─── Terminal block ─── */ +.terminal { + position: relative; + background: var(--term-bg); + border: 1px solid var(--term-border); + border-radius: 10px; + padding: 14px 54px 14px 18px; + font-family: 'JetBrains Mono', monospace; + font-size: 13.5px; + line-height: 1.65; + color: var(--term-fg); + overflow-x: auto; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} +.terminal .term-line { display: block; white-space: pre; } +.terminal .term-line::before { + content: '$ '; + color: var(--term-prompt); + user-select: none; +} +.terminal .term-line.no-prompt::before { content: ''; } +.terminal .term-line.out { color: var(--term-comment); } +.terminal .term-line.out::before { content: ''; } +.terminal .term-line.comment { color: var(--term-comment); } +.terminal .term-line.comment::before { content: '# '; color: var(--term-comment); } +.terminal .term-cmd { color: var(--term-fg); } +.terminal .term-flag { color: oklch(0.78 0.12 70); } +.terminal .term-arg { color: oklch(0.80 0.10 200); } +.terminal .copy-btn, +.code-block .copy-btn { + position: absolute; + top: 8px; + right: 8px; + width: 30px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + background: color-mix(in oklch, var(--fg) 6%, transparent); + border: 1px solid var(--line); + border-radius: 7px; + color: var(--fg-dim); + cursor: pointer; + padding: 0; + transition: color 0.15s, background 0.15s, border-color 0.15s; +} +.terminal .copy-btn:hover, +.code-block .copy-btn:hover { + color: var(--fg); + background: color-mix(in oklch, var(--fg) 10%, transparent); + border-color: var(--line-strong); +} +.terminal .copy-btn.copied, +.code-block .copy-btn.copied { + color: var(--accent); + border-color: var(--accent-line); +} + +/* ─── Code block (non-terminal) ─── */ +.code-block { + position: relative; + background: var(--syn-bg); + border: 1px solid var(--syn-border); + border-radius: 10px; + overflow: hidden; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} +.code-block .code-lang { + display: block; + padding: 8px 18px 4px; + font-family: 'JetBrains Mono', monospace; + font-size: 10.5px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--fg-mute); + border-bottom: 1px solid var(--line); +} +.code-block pre { + position: relative; + margin: 0; + padding: 14px 54px 16px 18px; + font-size: 13.5px; + line-height: 1.65; + color: var(--syn-fg); + overflow-x: auto; +} +.code-block code { font-family: 'JetBrains Mono', monospace; } + +/* Shiki overrides for docs code blocks */ +.code-block .shiki { background: transparent !important; } +.code-block .shiki code { font-family: inherit; } + +/* ─── Sidebar ─── */ +.docs-sidebar { + position: sticky; + top: 67px; + height: calc(100vh - 67px); + overflow-y: auto; + padding: 28px 24px 48px 8px; + scrollbar-width: thin; + scrollbar-color: var(--line-strong) transparent; +} +.docs-sidebar::-webkit-scrollbar { width: 6px; } +.docs-sidebar::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 3px; } +.docs-sidebar .sb-group + .sb-group { margin-top: 26px; } +.docs-sidebar .sb-title { + font-family: 'JetBrains Mono', monospace; + font-size: 10.5px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--fg-mute); + margin: 0 0 8px; + padding: 0 12px; + font-weight: 500; +} +.docs-sidebar a.sb-link { + display: block; + padding: 6px 12px; + font-size: 14px; + color: var(--fg-dim); + text-decoration: none; + border-radius: 6px; + line-height: 1.4; + transition: color 0.12s, background 0.12s; + border-left: 1px solid transparent; + margin-left: 4px; + padding-left: 11px; +} +.docs-sidebar a.sb-link:hover { + color: var(--fg); + background: color-mix(in oklch, var(--fg) 4%, transparent); +} +.docs-sidebar a.sb-link.active { + color: var(--accent); + border-left-color: var(--accent); + font-weight: 500; +} +.docs-sidebar a.sb-link.sub { + font-size: 13.5px; + padding-left: 24px; + color: var(--fg-mute); +} +.docs-sidebar a.sb-link.sub.active { + color: var(--accent); +} +.docs-sidebar a.sb-link.mono { + font-family: var(--font-mono); + font-size: 13px; +} + +@media (max-width: 960px) { + .docs-sidebar { display: none; } + .docs-main { grid-column: 1 / -1 !important; } +} + +/* State classes for theme toggle etc */ +#copy-btn.copied { color: var(--accent); border-color: var(--accent-line); } + +/* CLI synopsis — like terminal but no prompt, subtly different border */ +.synopsis { + background: var(--term-bg); + border: 1px solid var(--term-border); + border-radius: 10px; + padding: 14px 18px; + font-family: 'JetBrains Mono', monospace; + font-size: 14px; + line-height: 1.6; + color: var(--term-fg); + overflow-x: auto; + backdrop-filter: blur(12px); +} +.synopsis .syn-cmd { color: var(--term-fg); font-weight: 500; } +.synopsis .syn-opt { color: var(--term-comment); } +.synopsis .syn-arg { color: oklch(0.80 0.10 200); } +html[data-theme="light"] .synopsis .syn-arg { color: oklch(0.40 0.13 200); } + +/* Sidebar nested group (for CLI command groups) */ +.docs-sidebar .sb-group-sub { + margin-top: 2px; +} +.docs-sidebar .sb-subtitle { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-mute); + margin: 14px 0 4px; + padding: 0 12px; + font-weight: 500; + opacity: 0.75; +} diff --git a/website/src/styles/package.css b/website/src/styles/package.css new file mode 100644 index 00000000..566e069e --- /dev/null +++ b/website/src/styles/package.css @@ -0,0 +1,323 @@ +/* Package page — animations and styles Tailwind can't express */ + +@keyframes pkg-grow { + to { transform: scaleX(var(--w, 1)); } +} + +@keyframes pkg-dropdown-in { + from { opacity: 0; transform: translateY(-4px) scale(0.98); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +@keyframes pkg-spin { + to { transform: rotate(360deg); } +} + +@keyframes pkg-fade-in { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +/* README content — aligned with article-prose (blog.css) */ +.pkg-readme { + padding: 28px 36px 36px; + color: var(--fg); + line-height: 1.7; + font-size: 15.5px; +} +.pkg-readme > * { margin: 0; } +.pkg-readme > * + * { margin-top: 20px; } +.pkg-readme > * + h2 { margin-top: 48px; } +.pkg-readme > * + h3 { margin-top: 36px; } +.pkg-readme > h2 + *, +.pkg-readme > h3 + * { margin-top: 18px; } +.pkg-readme h1 { + font-size: 26px; + color: var(--fg); + letter-spacing: -0.01em; + font-weight: 500; + line-height: 1.2; +} +.pkg-readme h2 { + font-size: 22px; + color: var(--fg); + font-weight: 500; + letter-spacing: -0.015em; + padding-bottom: 8px; + border-bottom: 1px solid var(--line); +} +.pkg-readme h3 { + font-size: 18px; + color: var(--fg); + font-weight: 500; + letter-spacing: -0.01em; +} +.pkg-readme p { + color: var(--fg-dim); + text-wrap: pretty; +} +.pkg-readme strong { color: var(--fg); font-weight: 600; } +.pkg-readme code { + font-family: 'JetBrains Mono', monospace; + font-size: 0.87em; + padding: 1px 6px; + border-radius: 5px; + background: color-mix(in oklch, var(--fg) 7%, transparent); + border: 1px solid var(--line); + color: var(--fg); +} +.pkg-readme pre { + padding: 20px 22px; + border: 1px solid var(--syn-border); + border-radius: 12px; + background: var(--syn-bg); + backdrop-filter: blur(8px); + overflow-x: auto; + font-size: 13.5px; + line-height: 1.65; + color: var(--syn-fg); +} +.pkg-readme pre code { + background: none; + border: 0; + padding: 0; + font-size: 1em; + color: inherit; +} +.pkg-readme ul, .pkg-readme ol { + padding-left: 1.4em; + color: var(--fg-dim); +} +.pkg-readme li { margin: 8px 0; } +.pkg-readme li::marker { color: var(--fg-mute); } +.pkg-readme a { + color: var(--accent); + text-decoration: none; + border-bottom: 1px solid color-mix(in oklch, var(--accent) 35%, transparent); + transition: border-color 0.15s; +} +.pkg-readme a:hover { border-bottom-color: var(--accent); } +.pkg-readme blockquote { + border-left: 2px solid var(--accent-line); + padding: 4px 0 4px 22px; + margin: 0; + color: var(--fg-dim); + font-style: italic; +} +.pkg-readme blockquote p { color: var(--fg-dim); } +.pkg-readme hr { + border: 0; + border-top: 1px solid var(--line); + margin: 48px 0; +} +.pkg-readme img { + max-width: 100%; + height: auto; + border-radius: 8px; +} +.pkg-readme table { + width: 100%; + border-collapse: collapse; + font-size: 14.5px; + line-height: 1.6; +} +.pkg-readme thead th { + text-align: left; + font-weight: 600; + font-size: 12px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--fg-mute); + padding: 8px 14px; + border-bottom: 1px solid var(--line-strong); +} +.pkg-readme tbody td { + padding: 10px 14px; + color: var(--fg-dim); + border-bottom: 1px solid var(--line); + vertical-align: top; +} +.pkg-readme tbody tr:last-child td { border-bottom: none; } + +/* ─── VSCode-like Files Explorer ─── */ +.files-explorer { + display: flex; + overflow: hidden; + background: var(--card); + backdrop-filter: blur(16px); + flex: 1; + min-height: 0; +} +.files-sidebar { + width: 260px; + min-width: 260px; + border-right: 1px solid var(--line); + display: flex; + flex-direction: column; + overflow: hidden; +} +.files-sidebar-section { + display: flex; + align-items: center; + gap: 6px; + height: 40px; + padding: 0 14px; + font-family: 'JetBrains Mono', monospace; + font-size: 10.5px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--fg-mute); + font-weight: 500; + border-bottom: 1px solid var(--line); + user-select: none; + cursor: pointer; + transition: color 0.12s; +} +.files-sidebar-section:hover { color: var(--fg-dim); } +.files-sidebar-section svg { transition: transform 0.15s; } +.files-tree-wrap { + flex: 1; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--line-strong) transparent; +} +.files-tree-wrap::-webkit-scrollbar { width: 6px; } +.files-tree-wrap::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 3px; } +.files-versions-wrap { + min-height: 120px; + max-height: 280px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--line-strong) transparent; +} +.files-versions-wrap::-webkit-scrollbar { width: 6px; } +.files-versions-wrap::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 3px; } + +/* Tree items */ +.ftree-row { + display: flex; + align-items: center; + gap: 4px; + padding: 2px 10px 2px 0; + cursor: pointer; + font-size: 13px; + color: var(--fg-dim); + transition: background 0.08s, color 0.08s; + white-space: nowrap; +} +.ftree-row:hover { background: color-mix(in oklch, var(--fg) 5%, transparent); color: var(--fg); } +.ftree-row.active { background: var(--accent-soft); color: var(--accent); } +.ftree-chevron { + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--fg-mute); +} +.ftree-icon { + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--fg-mute); +} +.ftree-row.active .ftree-icon { color: var(--accent); } +.ftree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; } +.ftree-size { + font-size: 10.5px; + color: var(--fg-mute); + flex-shrink: 0; + margin-left: auto; + padding-left: 8px; +} + +/* Version list items */ +.fver-row { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 14px; + cursor: pointer; + font-size: 12.5px; + color: var(--fg-dim); + transition: background 0.08s, color 0.08s; +} +.fver-row:hover { background: color-mix(in oklch, var(--fg) 5%, transparent); color: var(--fg); } +.fver-row.active { color: var(--accent); } +.fver-row.comparing { background: var(--accent-soft); } + +/* Compare pill on version rows */ +.fver-compare-btn { + display: inline-flex; + align-items: center; + border-radius: 9999px; + border: none; + background: color-mix(in oklch, var(--fg) 8%, transparent); + color: var(--fg-mute); + cursor: pointer; + opacity: 0; + transition: opacity 0.1s, color 0.1s, background 0.1s; + padding: 1px 8px; + font-size: 10px; + font-family: inherit; + letter-spacing: 0.02em; + flex-shrink: 0; + white-space: nowrap; +} +.fver-row:hover .fver-compare-btn, +.fver-compare-btn.active { opacity: 1; } +.fver-compare-btn:hover { color: var(--fg); background: color-mix(in oklch, var(--fg) 12%, transparent); } +.fver-compare-btn.active { background: var(--accent-soft); color: var(--accent); opacity: 1; } + +/* Editor area */ +.files-editor { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + overflow: hidden; +} +.files-editor-tab { + display: flex; + align-items: center; + gap: 8px; + height: 40px; + padding: 0 14px; + border-bottom: 1px solid var(--line); + font-family: 'JetBrains Mono', monospace; + font-size: 12.5px; + color: var(--fg-dim); +} +.files-editor-body { + flex: 1; + overflow: hidden; +} +.files-editor-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + color: var(--fg-mute); + font-size: 13px; +} + +@media (max-width: 768px) { + .files-explorer { flex-direction: column; } + .files-sidebar { width: 100%; min-width: unset; border-right: none; border-bottom: 1px solid var(--line); } + .files-tree-wrap { max-height: 280px; } + .files-editor-body { min-height: 400px; } +} + +/* Scrollbar */ +.pkg-scroll { + scrollbar-width: thin; + scrollbar-color: var(--line-strong) transparent; +} +.pkg-scroll::-webkit-scrollbar { width: 8px; } +.pkg-scroll::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 4px; } diff --git a/website/src/styles/quiz.css b/website/src/styles/quiz.css new file mode 100644 index 00000000..d9fa99be --- /dev/null +++ b/website/src/styles/quiz.css @@ -0,0 +1,497 @@ +/* ────────── "Do you know Yarn?" quiz styles ────────── */ + +/* Layout */ +.quiz-shell { + max-width: 1040px; + margin: 0 auto; + padding: 40px 24px 64px; + display: flex; + flex-direction: column; +} +@media (min-width: 880px) { + .quiz-shell { min-height: calc(100vh - 67px); } +} + +.quiz-header { + margin-bottom: 32px; + max-width: 680px; +} + +.quiz-eyebrow { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--fg-mute); + margin-bottom: 12px; +} + +.quiz-title { + font-size: clamp(28px, 4.4vw, 40px); + font-weight: 500; + line-height: 1.08; + letter-spacing: -0.025em; + margin: 0 0 12px; + text-wrap: balance; +} + +.quiz-subtitle { + font-size: 15px; + line-height: 1.55; + color: var(--fg-dim); + margin: 0; + text-wrap: pretty; + max-width: 52ch; +} + +.quiz-shell.compact .quiz-header { + margin-bottom: 20px; +} +.quiz-shell.compact .quiz-title, +.quiz-shell.compact .quiz-subtitle { + display: none; +} +.quiz-shell.compact .quiz-eyebrow { + margin-bottom: 0; + color: var(--fg-dim); + font-size: 12px; +} + +/* ── Progress bar ── */ +.quiz-progress { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 32px; + font-family: 'JetBrains Mono', monospace; + font-size: 11.5px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-mute); +} +.quiz-progress .bar { + flex: 1; + height: 4px; + background: color-mix(in oklch, var(--fg) 8%, transparent); + border-radius: 999px; + overflow: hidden; + position: relative; +} +.quiz-progress .bar-fill { + position: absolute; + inset: 0 auto 0 0; + background: var(--accent); + border-radius: 999px; + transition: width 0.45s cubic-bezier(0.22, 1, 0.36, 1); + width: 0; +} +.quiz-progress .score { + white-space: nowrap; +} +.quiz-progress .score .num { + color: var(--fg); + font-weight: 500; +} + +/* ── Card / question body ── */ +.quiz-card { + position: relative; + max-width: 720px; +} +@media (min-width: 880px) { + .quiz-card { flex: 1; max-width: none; } +} + +.quiz-stage { +} + +.q-head { + display: block; +} +@media (min-width: 880px) { + .q-head { + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; + gap: 48px; + align-items: center; + margin-bottom: 32px; + } + .q-head .q-answers { margin-bottom: 0; } +} + +.q-number { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--fg-mute); + margin-bottom: 16px; +} + +.q-prompt { + font-size: clamp(22px, 3.4vw, 30px); + font-weight: 500; + line-height: 1.25; + letter-spacing: -0.015em; + margin: 0 0 32px; + color: var(--fg); + text-wrap: balance; +} +@media (min-width: 880px) { + .q-head .q-prompt { margin-bottom: 0; } +} + +/* ── Answer buttons ── */ +.q-answers { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-bottom: 32px; +} +@media (max-width: 420px) { + .q-answers { grid-template-columns: 1fr; } +} +@media (min-width: 880px) { + .q-answers { + grid-template-columns: 1fr; + gap: 10px; + } +} + +.q-btn { + appearance: none; + font-family: inherit; + font-size: 16px; + font-weight: 500; + padding: 18px 20px; + background: color-mix(in oklch, var(--fg) 4%, transparent); + border: 1px solid var(--line-strong); + border-radius: 12px; + color: var(--fg); + cursor: pointer; + transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.15s; + letter-spacing: -0.005em; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; +} +.q-btn:hover:not([disabled]) { + background: color-mix(in oklch, var(--fg) 8%, transparent); + border-color: var(--fg-dim); + transform: translateY(-1px); +} +.q-btn[disabled] { + cursor: default; + opacity: 0.6; +} +.q-btn.picked.correct { + background: var(--accent-soft); + border-color: var(--accent-line); + color: var(--accent); + opacity: 1; +} +.q-btn.picked.wrong { + background: color-mix(in oklch, var(--fg-mute) 12%, transparent); + border-color: color-mix(in oklch, var(--fg-mute) 40%, transparent); + color: var(--fg); + opacity: 1; +} +.q-btn.revealed-correct { + background: var(--accent-soft); + border-color: var(--accent-line); + color: var(--accent); + opacity: 1; +} + +/* Icons: both hidden by default; the right one fades in after answer */ +.q-btn .q-icon { + width: 18px; + height: 18px; + opacity: 0; + transition: opacity 0.2s; + flex-shrink: 0; +} +.q-btn .q-icon-cross { display: none; } + +.q-btn.picked.correct .q-icon-check { opacity: 1; } +.q-btn.picked.correct .q-icon-cross { display: none; } + +.q-btn.picked.wrong .q-icon-check { display: none; } +.q-btn.picked.wrong .q-icon-cross { display: inline; opacity: 1; } + +.q-btn.revealed-correct .q-icon-check { opacity: 0.7; } +.q-btn.revealed-correct .q-icon-cross { display: none; } + +/* ── Answer button pulse ── */ +@keyframes q-btn-pulse { + 0% { transform: scale(1); } + 40% { transform: scale(0.95); } + 100% { transform: scale(1); } +} +.q-btn.pulse { + animation: q-btn-pulse 0.3s cubic-bezier(0.22, 1, 0.36, 1); +} + +/* ── Reveal / feedback ── */ +.q-reveal { + max-height: 0; + overflow: hidden; + opacity: 0; + transform: translateX(12px); +} +.q-reveal.open { + max-height: none; + opacity: 1; + max-width: 680px; + transform: translateX(0); + transition: opacity 0.35s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); +} + +.q-verdict { + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + letter-spacing: 0.14em; + text-transform: uppercase; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; +} +.q-verdict.right { color: var(--accent); } +.q-verdict.wrong { color: var(--fg-mute); } + +.q-verdict-line { + font-size: 18px; + line-height: 1.45; + font-weight: 500; + margin: 0 0 14px; + color: var(--fg); + letter-spacing: -0.01em; + text-wrap: pretty; +} + +.q-explain { + font-size: 15.5px; + line-height: 1.65; + color: var(--fg-dim); + margin: 0; + text-wrap: pretty; +} +.q-explain + .q-explain { margin-top: 12px; } +.q-explain code { + font-family: 'JetBrains Mono', monospace; + font-size: 0.88em; + padding: 0.15em 0.42em; + border-radius: 5px; + background: color-mix(in oklch, var(--fg) 8%, transparent); + border: 1px solid var(--line); + color: var(--fg); + font-weight: 500; +} +.q-explain a { + color: var(--accent); + text-decoration: none; + border-bottom: 1px solid color-mix(in oklch, var(--accent) 35%, transparent); +} +.q-explain a:hover { + border-bottom-color: var(--accent); +} + +.q-actions { + margin-top: 28px; + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.q-next { + appearance: none; + font-family: inherit; + font-size: 14.5px; + font-weight: 500; + padding: 11px 18px 11px 20px; + background: var(--fg); + color: var(--bg-0); + border: 1px solid var(--fg); + border-radius: 10px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; + transition: transform 0.15s; +} +.q-next:hover { transform: translateY(-1px); } +.q-next svg { width: 14px; height: 14px; } + +.q-share { + appearance: none; + font-family: inherit; + font-size: 13px; + padding: 10px 14px; + background: transparent; + color: var(--fg-dim); + border: 1px solid var(--line-strong); + border-radius: 10px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 7px; + transition: color 0.15s, border-color 0.15s; +} +.q-share:hover { color: var(--fg); border-color: var(--fg-dim); } +.q-share.copied { color: var(--accent); border-color: var(--accent-line); } +.q-share svg { width: 13px; height: 13px; } + +/* ── End screen ── */ +.end-screen { + text-align: center; + padding: 24px 0 12px; +} +.end-level-label { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--fg-mute); + margin-bottom: 12px; +} +.end-level { + font-size: clamp(44px, 7vw, 64px); + font-weight: 500; + letter-spacing: -0.03em; + line-height: 1; + margin: 0 0 16px; + color: var(--accent); +} +.end-score { + font-family: 'JetBrains Mono', monospace; + font-size: 22px; + color: var(--fg); + margin-bottom: 10px; +} +.end-score .total { color: var(--fg-mute); } +.end-tagline { + font-size: 16px; + line-height: 1.55; + color: var(--fg-dim); + margin: 0 auto 32px; + max-width: 42ch; + text-wrap: pretty; +} +.end-actions { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; +} +.end-recap { + margin-top: 48px; + text-align: left; + border-top: 1px solid var(--line); + padding-top: 28px; +} +.end-recap-title { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--fg-mute); + margin-bottom: 16px; +} +.recap-row { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid var(--line); + text-decoration: none; + color: inherit; + transition: background 0.15s; + border-radius: 4px; +} +.recap-row:hover { + background: color-mix(in oklch, var(--fg) 4%, transparent); +} +.recap-row:last-child { border-bottom: 0; } +.recap-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + margin-top: 8px; + background: var(--fg-mute); +} +.recap-dot.right { background: var(--accent); } +.recap-text { + flex: 1; + font-size: 14.5px; + line-height: 1.45; + color: var(--fg-dim); +} +.recap-arrow { + color: var(--fg-mute); + flex-shrink: 0; + opacity: 0; + transition: opacity 0.15s, transform 0.15s; +} +.recap-row:hover .recap-arrow { + opacity: 1; + transform: translateX(2px); +} + +/* ── Toast ── */ +.toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%) translateY(8px); + background: var(--card); + backdrop-filter: blur(16px) saturate(160%); + -webkit-backdrop-filter: blur(16px) saturate(160%); + border: 1px solid var(--line-strong); + color: var(--fg); + padding: 10px 16px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease, transform 0.25s cubic-bezier(0.22, 1, 0.36, 1); + z-index: 200; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25); +} +.toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + .quiz-stage, + .q-reveal, + .quiz-progress .bar-fill, + .toast, + .q-btn { + transition: none !important; + animation: none !important; + } +} + +/* Mobile tuning */ +@media (max-width: 640px) { + .quiz-shell { + padding: 28px 18px 48px; + } + .quiz-header { + margin-bottom: 32px; + } + .q-prompt { + margin-bottom: 24px; + } + .quiz-footer { + font-size: 12px !important; + } +} diff --git a/website/src/styles/search.css b/website/src/styles/search.css new file mode 100644 index 00000000..82976e67 --- /dev/null +++ b/website/src/styles/search.css @@ -0,0 +1,30 @@ +/* Only rules that Tailwind can't express */ + +@keyframes searchIn { + from { opacity: 0; transform: translateY(-8px) scale(0.985); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +/* Scrollbar */ +.search-results-scroll { + scrollbar-width: thin; + scrollbar-color: var(--line-strong) transparent; +} +.search-results-scroll::-webkit-scrollbar { width: 8px; } +.search-results-scroll::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 4px; } + +/* Algolia highlight marks */ +.search-result-row mark { + background: var(--accent-soft); + color: var(--accent); + padding: 0 1px; + border-radius: 2px; +} + +/* Line-clamp snippet (needs -webkit prefixes) */ +.search-snippet-clamp { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} diff --git a/website/src/styles/tailwind.css b/website/src/styles/tailwind.css new file mode 100644 index 00000000..ccd5923b --- /dev/null +++ b/website/src/styles/tailwind.css @@ -0,0 +1,7 @@ +@import "tailwindcss"; +@source "../../plugins"; + +@theme { + --font-sans: 'Space Grotesk', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, monospace; +} diff --git a/website/src/styles/theme.css b/website/src/styles/theme.css new file mode 100644 index 00000000..88d692ca --- /dev/null +++ b/website/src/styles/theme.css @@ -0,0 +1,235 @@ +/* Shared theme tokens — mirrors Yarn.html */ +html { scroll-behavior: smooth; } +:root { + --accent-h: 280; + --accent: oklch(0.78 0.16 var(--accent-h)); + --accent-soft: oklch(0.78 0.16 var(--accent-h) / 0.14); + --accent-line: oklch(0.78 0.16 var(--accent-h) / 0.35); + + --bg-0: #05060f; + --bg-1: #0a0e28; + --bg-2: #141552; + --fg: #e8ecff; + --fg-dim: #a8b0d4; + --fg-mute: #6872a0; + --line: rgba(168, 176, 212, 0.14); + --line-strong: rgba(168, 176, 212, 0.28); + --card: rgba(12, 16, 44, 0.55); + --card-border: rgba(120, 130, 180, 0.18); + + /* Admonition colors (dark) */ + --adm-note-bg: oklch(0.30 0.09 240 / 0.28); + --adm-note-border: oklch(0.65 0.12 240 / 0.45); + --adm-note-fg: oklch(0.82 0.10 230); + --adm-tip-bg: oklch(0.30 0.10 150 / 0.25); + --adm-tip-border: oklch(0.65 0.12 150 / 0.45); + --adm-tip-fg: oklch(0.82 0.12 150); + --adm-warn-bg: oklch(0.35 0.12 70 / 0.28); + --adm-warn-border: oklch(0.70 0.14 70 / 0.5); + --adm-warn-fg: oklch(0.85 0.13 75); + --adm-danger-bg: oklch(0.30 0.14 22 / 0.28); + --adm-danger-border: oklch(0.65 0.18 22 / 0.5); + --adm-danger-fg: oklch(0.78 0.15 25); + + /* Syntax highlighting (dark) */ + --syn-bg: rgba(8, 10, 28, 0.72); + --syn-border: rgba(120, 130, 180, 0.18); + --syn-fg: #d6daf5; + --syn-comment: #6872a0; + --syn-keyword: oklch(0.78 0.14 300); + --syn-string: oklch(0.80 0.14 140); + --syn-number: oklch(0.80 0.14 70); + --syn-func: oklch(0.82 0.12 210); + --syn-prop: oklch(0.82 0.10 180); + --syn-punct: #8890b8; + --syn-tag: oklch(0.78 0.14 22); + --syn-attr: oklch(0.82 0.12 70); + + /* Shiki css-variables theme → docs tokens */ + --shiki-token-keyword: var(--syn-keyword); + --shiki-token-string: var(--syn-string); + --shiki-token-string-expression: var(--syn-string); + --shiki-token-constant: var(--syn-number); + --shiki-token-function: var(--syn-func); + --shiki-token-parameter: var(--syn-prop); + --shiki-token-punctuation: var(--syn-punct); + --shiki-token-comment: var(--syn-comment); + --shiki-token-link: var(--syn-string); + --shiki-token-inserted: oklch(0.80 0.14 140); + --shiki-token-deleted: oklch(0.78 0.14 22); + --shiki-foreground: var(--syn-fg); + --shiki-background: transparent; + + /* Terminal */ + --term-bg: rgba(6, 8, 22, 0.75); + --term-border: rgba(120, 130, 180, 0.22); + --term-prompt: oklch(0.78 0.14 150); + --term-fg: #e8ecff; + --term-comment: #8890b8; + + /* Pill variant colors */ + --pill-type-fg: oklch(0.82 0.10 200); + --pill-type-border: oklch(0.50 0.08 200 / 0.35); + --pill-type-bg: oklch(0.45 0.09 200 / 0.15); + --pill-req-fg: oklch(0.80 0.14 25); + --pill-req-border: oklch(0.55 0.15 25 / 0.4); + --pill-req-bg: oklch(0.45 0.14 25 / 0.15); + --pill-dep-strike: oklch(0.70 0.12 25 / 0.6); +} + +html[data-theme="light"] { + --accent: oklch(0.45 0.18 var(--accent-h)); + --accent-soft: oklch(0.45 0.18 var(--accent-h) / 0.10); + --accent-line: oklch(0.45 0.18 var(--accent-h) / 0.30); + --bg-0: #eef3ff; + --bg-1: #dce7fb; + --bg-2: #c5d5f2; + --fg: #0c1030; + --fg-dim: #252d50; + --fg-mute: #515a7a; + --line: rgba(12, 16, 48, 0.12); + --line-strong: rgba(12, 16, 48, 0.28); + --card: rgba(255, 255, 255, 0.72); + --card-border: rgba(12, 16, 48, 0.14); + + --adm-note-bg: oklch(0.96 0.04 240 / 0.7); + --adm-note-border: oklch(0.60 0.14 240 / 0.6); + --adm-note-fg: oklch(0.40 0.14 240); + --adm-tip-bg: oklch(0.94 0.06 150 / 0.7); + --adm-tip-border: oklch(0.55 0.15 150 / 0.55); + --adm-tip-fg: oklch(0.35 0.13 150); + --adm-warn-bg: oklch(0.96 0.07 80 / 0.85); + --adm-warn-border: oklch(0.65 0.15 70 / 0.6); + --adm-warn-fg: oklch(0.42 0.14 60); + --adm-danger-bg: oklch(0.95 0.05 25 / 0.8); + --adm-danger-border: oklch(0.60 0.18 25 / 0.6); + --adm-danger-fg: oklch(0.45 0.18 25); + + --syn-bg: rgba(255, 255, 255, 0.72); + --syn-border: rgba(12, 16, 48, 0.10); + --syn-fg: #0c1030; + --syn-comment: #7a84a8; + --syn-keyword: oklch(0.45 0.18 300); + --syn-string: oklch(0.45 0.16 150); + --syn-number: oklch(0.50 0.15 50); + --syn-func: oklch(0.45 0.15 220); + --syn-prop: oklch(0.48 0.13 195); + --syn-punct: #505878; + --syn-tag: oklch(0.50 0.17 20); + --syn-attr: oklch(0.48 0.14 60); + + --shiki-token-keyword: var(--syn-keyword); + --shiki-token-string: var(--syn-string); + --shiki-token-string-expression: var(--syn-string); + --shiki-token-constant: var(--syn-number); + --shiki-token-function: var(--syn-func); + --shiki-token-parameter: var(--syn-prop); + --shiki-token-punctuation: var(--syn-punct); + --shiki-token-comment: var(--syn-comment); + --shiki-token-link: var(--syn-string); + --shiki-token-inserted: oklch(0.45 0.16 150); + --shiki-token-deleted: oklch(0.50 0.17 20); + --shiki-foreground: var(--syn-fg); + --shiki-background: transparent; + + --term-bg: rgba(252, 252, 255, 0.75); + --term-border: rgba(12, 16, 48, 0.12); + --term-prompt: oklch(0.45 0.15 150); + --term-fg: #0c1030; + --term-comment: #505878; + + --pill-type-fg: oklch(0.40 0.13 200); + --pill-type-border: oklch(0.60 0.13 200 / 0.35); + --pill-type-bg: oklch(0.85 0.06 200 / 0.4); + --pill-req-fg: oklch(0.50 0.18 25); + --pill-req-border: oklch(0.60 0.15 25 / 0.4); + --pill-req-bg: oklch(0.92 0.06 25 / 0.5); +} + +/* ─── Mermaid diagram ─── */ +.mermaid-diagram { + background: var(--syn-bg); + border: 1px solid var(--syn-border); + border-radius: 10px; + padding: 24px; + overflow-x: auto; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + display: flex; + justify-content: center; +} +.mermaid-diagram svg { + max-width: 100%; + height: auto; +} +.mermaid-light { display: none; } +.mermaid-light, .mermaid-dark { + flex: auto; +} +html[data-theme="light"] .mermaid-dark { display: none; } +html[data-theme="light"] .mermaid-light { display: block; } + +/* Heading anchor (hover reveal) — shared across docs & blog */ +.heading-wrap { + position: relative; + display: flex; + align-items: baseline; + gap: 0.5rem; +} +.heading-anchor { + opacity: 0; + color: var(--fg-mute); + text-decoration: none; + font-weight: 400; + transition: opacity 0.15s, color 0.15s; + cursor: pointer; + user-select: none; + line-height: 1; + font-size: 0.85em; + border: 0; +} +.heading-wrap:hover .heading-anchor, +.heading-anchor:focus { + opacity: 1; +} +.heading-anchor:hover { color: var(--accent); } + +body { + font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif; + background: var(--bg-0); + color: var(--fg); + transition: background 0.6s ease, color 0.6s ease; +} + +.mono, code, pre, kbd { + font-family: 'JetBrains Mono', ui-monospace, monospace; +} + +/* Sky — dimmed for docs reading */ +.sky { + background: + radial-gradient(ellipse 60% 35% at 78% 18%, oklch(0.42 0.18 290 / 0.45) 0%, transparent 60%), + radial-gradient(ellipse 80% 40% at 20% 92%, oklch(0.38 0.14 220 / 0.35) 0%, transparent 55%), + radial-gradient(ellipse 55% 45% at 8% 55%, oklch(0.40 0.14 235 / 0.30) 0%, transparent 60%), + radial-gradient(ellipse 100% 70% at 50% 30%, oklch(0.28 0.12 270) 0%, transparent 65%), + radial-gradient(ellipse 120% 100% at 50% 50%, transparent 40%, oklch(0.10 0.04 265 / 0.55) 100%), + linear-gradient(180deg in oklch, + oklch(0.12 0.05 265) 0%, + oklch(0.18 0.09 270) 22%, + oklch(0.22 0.11 275) 48%, + oklch(0.16 0.08 272) 78%, + oklch(0.09 0.04 265) 100%); + transition: background 0.6s ease; +} +html[data-theme="light"] .sky { + background: + radial-gradient(ellipse 50% 35% at 82% 12%, oklch(0.97 0.06 85 / 0.85) 0%, transparent 55%), + radial-gradient(ellipse 110% 35% at 50% 98%, oklch(0.92 0.08 65) 0%, transparent 60%), + radial-gradient(ellipse 80% 50% at 20% 40%, oklch(0.95 0.04 240 / 0.6) 0%, transparent 60%), + radial-gradient(ellipse 140% 100% at 50% 50%, transparent 55%, oklch(0.75 0.06 240 / 0.25) 100%), + linear-gradient(180deg in oklch, + oklch(0.96 0.03 240) 0%, + oklch(0.92 0.05 240) 35%, + oklch(0.88 0.06 230) 65%, + oklch(0.92 0.06 70) 100%); +} diff --git a/website/src/utils/bluesky.ts b/website/src/utils/bluesky.ts new file mode 100644 index 00000000..521d30e0 --- /dev/null +++ b/website/src/utils/bluesky.ts @@ -0,0 +1,140 @@ +const API_BASE = `https://public.api.bsky.app/xrpc`; + +interface BskyFacetFeature { + $type: string; + uri?: string; + did?: string; + tag?: string; +} + +interface BskyFacet { + index: { byteStart: number; byteEnd: number }; + features: BskyFacetFeature[]; +} + +interface BskyPostRecord { + text: string; + createdAt: string; + facets?: BskyFacet[]; + reply?: unknown; +} + +interface BskyPost { + uri: string; + author: { + handle: string; + displayName: string; + avatar: string; + }; + record: BskyPostRecord; + likeCount: number; +} + +interface BskyFeedItem { + post: BskyPost; + reason?: unknown; +} + +export interface Skeet { + avatarUrl: string; + name: string; + handle: string; + sortDate: Date; + date: string; + text: string; + postUrl: string; + likeCount: number; +} + +function renderFacets(text: string, facets?: BskyFacet[]): string { + if (!facets || facets.length === 0) + return escapeHtml(text); + + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const bytes = encoder.encode(text); + + const sorted = [...facets].sort((a, b) => a.index.byteStart - b.index.byteStart); + + let html = ``; + let cursor = 0; + + for (const facet of sorted) { + const {byteStart, byteEnd} = facet.index; + if (byteStart < cursor) continue; + + html += escapeHtml(decoder.decode(bytes.slice(cursor, byteStart))); + + const segment = escapeHtml(decoder.decode(bytes.slice(byteStart, byteEnd))); + + const link = facet.features.find(f => f.$type === `app.bsky.richtext.facet#link`); + const mention = facet.features.find(f => f.$type === `app.bsky.richtext.facet#mention`); + const tag = facet.features.find(f => f.$type === `app.bsky.richtext.facet#tag`); + + if (link?.uri) { + html += `${segment}`; + } else if (mention?.did) { + html += `${segment}`; + } else if (tag?.tag) { + html += `${segment}`; + } else { + html += segment; + } + + cursor = byteEnd; + } + + html += escapeHtml(decoder.decode(bytes.slice(cursor))); + return html; +} + +function escapeHtml(s: string): string { + return s + .replace(/&/g, `&`) + .replace(//g, `>`) + .replace(/"/g, `"`); +} + +function escapeAttr(s: string): string { + return s + .replace(/&/g, `&`) + .replace(/"/g, `"`) + .replace(//g, `>`); +} + +function postUrlFromUri(uri: string, handle: string): string { + const rkey = uri.split(`/`).pop(); + return `https://bsky.app/profile/${handle}/post/${rkey}`; +} + +export async function fetchSkeets(handle: string, limit = 5): Promise { + try { + const url = `${API_BASE}/app.bsky.feed.getAuthorFeed?actor=${encodeURIComponent(handle)}&limit=${limit * 2}`; + + const res = await fetch(url); + if (!res.ok) return []; + + const data = await res.json() as { feed: BskyFeedItem[] }; + + return data.feed + .filter(item => !item.reason && !item.post.record.reply) + .slice(0, limit) + .map(({post}) => { + const sortDate = new Date(post.record.createdAt); + return { + avatarUrl: post.author.avatar, + name: post.author.displayName, + handle: `@${post.author.handle}`, + sortDate, + date: sortDate.toLocaleDateString(`en-US`, {month: `short`, day: `numeric`}), + text: renderFacets(post.record.text, post.record.facets), + postUrl: postUrlFromUri(post.uri, post.author.handle), + likeCount: post.likeCount, + }; + }); + } catch { + return []; + } +} diff --git a/website/src/utils/cli.ts b/website/src/utils/cli.ts new file mode 100644 index 00000000..9e9611ac --- /dev/null +++ b/website/src/utils/cli.ts @@ -0,0 +1,107 @@ +import type {BaseData} from '@clipanion/astro'; + +type DataEntry = {id: string; data: BaseData; filePath?: string}; + +type OptionComponent = { + type: `option`; + primaryName: string; + aliases: string[]; + documentation: {description: string; details: string | null} | null; + isHidden: boolean; + allowBinding: boolean; + allowBoolean: boolean; +}; + +type PositionalComponent = { + type: `positional`; + positionalType: `keyword` | `dynamic`; + name?: string; + expected?: string; + documentation?: {description: string; details: string | null} | null; + extra_len?: number | null; +}; + +function escapeDirective(s: string): string { + return s.replace(/\\/g, `\\\\`).replace(/\[/g, `\\[`).replace(/\]/g, `\\]`); +} + +function formatOptionNames(option: OptionComponent): string { + const names = [option.primaryName, ...option.aliases]; + return names.join(`, `); +} + +function inferOptionType(option: OptionComponent): string { + if (!option.allowBinding && !option.allowBoolean) + return `boolean`; + + return `string`; +} + +function buildUsageLine(entry: DataEntry): string { + const {binaryName, commandSpec} = entry.data; + const parts = [binaryName, ...commandSpec.primaryPath]; + + for (const component of commandSpec.components) { + if (component.type !== `positional`) continue; + const pos = component as PositionalComponent; + if (pos.positionalType === `keyword`) { + parts.push(pos.expected!); + } else { + const name = pos.name || `arg`; + const suffix = pos.extra_len !== 0 ? `…` : ``; + parts.push(`<${name}${suffix}>`); + } + } + + return parts.join(` `); +} + +export function cliBody(entry: DataEntry): string { + const {commandSpec} = entry.data; + const lines: string[] = []; + + lines.push(`\`\`\`terminal`); + lines.push(buildUsageLine(entry)); + lines.push(`\`\`\``); + + if (commandSpec.documentation?.details) { + lines.push(``); + lines.push(commandSpec.documentation.details); + } + + const options = commandSpec.components.filter( + (c): c is OptionComponent => c.type === `option` && !(c as OptionComponent).isHidden, + ) as OptionComponent[]; + + if (options.length > 0) { + for (const option of options) { + const names = formatOptionNames(option); + const type = inferOptionType(option); + const pills = `:type[${escapeDirective(type)}]`; + + lines.push(``); + lines.push(`### \`${names}\` ${pills}`); + + if (option.documentation?.description) + lines.push(``, option.documentation.description); + } + } + + if (commandSpec.examples.length > 0) { + lines.push(``); + lines.push(`## Examples`); + + for (const example of commandSpec.examples) { + lines.push(``); + if (example.description) + lines.push(`**${example.description}**`); + + lines.push(``); + lines.push(`\`\`\`terminal`); + lines.push(example.command); + lines.push(`\`\`\``); + } + } + + return lines.join(`\n`); +} diff --git a/website/src/utils/highlight.ts b/website/src/utils/highlight.ts new file mode 100644 index 00000000..f0e491bf --- /dev/null +++ b/website/src/utils/highlight.ts @@ -0,0 +1,53 @@ +import { createHighlighter, createCssVariablesTheme } from 'shiki'; + +const cssVarsTheme = createCssVariablesTheme({ + name: 'css-variables', + variablePrefix: '--shiki-', + variableDefaults: {}, + fontStyle: true, +}); + +let highlighter: Awaited> | undefined; + +async function getHighlighter() { + if (highlighter) return highlighter; + + highlighter = await createHighlighter({ + themes: [cssVarsTheme], + langs: ['javascript', 'typescript', 'json', 'yaml', 'bash', 'html', 'css', 'jsx', 'tsx', 'diff', 'shell'], + }); + + return highlighter; +} + +const LANG_ALIASES: Record = { + js: 'javascript', + ts: 'typescript', + sh: 'shell', +}; + +export async function highlight(code: string, lang: string): Promise { + if (!lang) return escapeHtml(code); + + const resolved = LANG_ALIASES[lang] || lang; + const hl = await getHighlighter(); + + const loaded = hl.getLoadedLanguages(); + if (!loaded.includes(resolved)) return escapeHtml(code); + + const html = hl.codeToHtml(code, { + lang: resolved, + theme: 'css-variables', + }); + + const match = html.match(/(.+?)<\/code>/s); + return match ? match[1] : escapeHtml(code); +} + +function escapeHtml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} diff --git a/website/src/utils/render-markdown.ts b/website/src/utils/render-markdown.ts new file mode 100644 index 00000000..854513ea --- /dev/null +++ b/website/src/utils/render-markdown.ts @@ -0,0 +1,22 @@ +import {unified} from 'unified'; +import remarkParse from 'remark-parse'; +import remarkDirective from 'remark-directive'; +import remarkDocs from '../../plugins/remark-docs.mjs'; +import remarkRehype from 'remark-rehype'; +import rehypeRaw from 'rehype-raw'; +import rehypeDocs from '../../plugins/rehype-docs.mjs'; +import rehypeStringify from 'rehype-stringify'; + +const processor = unified() + .use(remarkParse) + .use(remarkDirective) + .use(remarkDocs) + .use(remarkRehype, {allowDangerousHtml: true}) + .use(rehypeRaw) + .use(rehypeDocs) + .use(rehypeStringify); + +export async function renderDocsMarkdown(md: string): Promise { + const result = await processor.process(md); + return String(result); +} diff --git a/website/src/utils/schema.ts b/website/src/utils/schema.ts new file mode 100644 index 00000000..33a7d17f --- /dev/null +++ b/website/src/utils/schema.ts @@ -0,0 +1,90 @@ +function escapeDirective(s: string): string { + return s.replace(/\\/g, `\\\\`).replace(/\[/g, `\\[`).replace(/\]/g, `\\]`); +} + +function formatType(prop: Record): string { + if (Array.isArray(prop.type)) + return prop.type.join(` | `); + + if (prop.enum) + return prop.enum.map((v: any) => typeof v === `string` ? `"${v}"` : String(v)).join(` | `); + + if (prop.type === `array`) + return `${prop.items?.type || `any`}[]`; + + return prop.type || `any`; +} + +function propertyToMarkdown(name: string, prop: Record): string { + const pills = [`:type[${escapeDirective(formatType(prop))}]`]; + + if (prop.default !== undefined) + pills.push(`:default[${escapeDirective(JSON.stringify(prop.default))}]`); + + const lines = [`### \`${name}\` ${pills.join(` `)}`]; + + if (prop.title) + lines.push(``, `**${prop.title}**`); + + if (prop.description) + lines.push(``, prop.description); + + return lines.join(`\n`); +} + +function flattenToMarkdown(properties: Record, prefix = ``): string[] { + const sections: string[] = []; + + for (const [key, prop] of Object.entries(properties)) { + const name = prefix + key; + sections.push(propertyToMarkdown(name, prop as Record)); + + if ((prop as any).properties) + sections.push(...flattenToMarkdown((prop as any).properties, `${name}.`)); + + if ((prop as any).patternProperties) { + for (const patternProp of Object.values((prop as any).patternProperties) as any[]) { + if (patternProp.properties) + sections.push(...flattenToMarkdown(patternProp.properties, `${name}[name].`)); + } + } + } + + return sections; +} + +export function schemaFieldNames(schema: Record): string[] { + return flattenFieldNames(schema.properties); +} + +function flattenFieldNames(properties: Record, prefix = ``): string[] { + const names: string[] = []; + + for (const [key, prop] of Object.entries(properties)) { + const name = prefix + key; + names.push(name); + + if ((prop as any).properties) + names.push(...flattenFieldNames((prop as any).properties, `${name}.`)); + + if ((prop as any).patternProperties) { + for (const patternProp of Object.values((prop as any).patternProperties) as any[]) { + if (patternProp.properties) + names.push(...flattenFieldNames(patternProp.properties, `${name}[name].`)); + } + } + } + + return names; +} + +export function schemaToMarkdown(schema: Record): string { + const parts: string[] = []; + + if (schema.description) + parts.push(schema.description); + + parts.push(...flattenToMarkdown(schema.properties)); + + return parts.join(`\n\n`); +} diff --git a/website/tsconfig.json b/website/tsconfig.json new file mode 100644 index 00000000..bcbf8b50 --- /dev/null +++ b/website/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "astro/tsconfigs/strict" +} diff --git a/yarn.lock b/yarn.lock index 3a26e038..1a5bf1f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,14 +3,15 @@ "version": 9 }, "workspaces": { - "@yarnpkg/documentation": "3a36e375c2f880ca4b3727f2db3a67968d85b7112518ce2cf2fdc9ce2cde237ab1010ab1320a074b1cece29e0c3b79b415b1b48bca4518c0f9157f0a3818e55d", - "@yarnpkg/monorepo": "f5e81432184d8250cce7f479f2ebb436a9b0900c22d0393bf1502e06cdad038193a88a7c6d3d6341963c6426eef186f390b1b44eff3242dc606b9f4a5cdb5846", - "@yarnpkg/zpm-constraints": "a602e3cc3ea1dd931ef50091753a9f1de3a06ff9ee0efb29c8222fb3491132ccbac1581220770e74dcf06e2c30189f636b1d7eb691bfdeff36012d3169630bc6", - "@yarnpkg/zpm-daemon-ui": "eeab3988f3f683099d2ac293259ca85accca0b08345680953771cb8aea311d570c309aa1dd19294742d23e95a728c70c96b985684fe454ac4a0415ae333e16ee", - "acceptance-tests": "e3fd8d082d2569f2db5632be90586713161177de887b3baef196e920aa6eb7a15447fb097a93339f72786bb1de1dab65e30bc9954296037e4fc72a9835d3e178", - "pkg-tests-core": "27dc1794f148dca7d11639e6ea0877781dcf88daf2e38af45bae8a0f9f259d4f49cb3f960e27849a3ee1e9d702da717d0060409c37fe6b26b23e509382d945e9", - "pkg-tests-fixtures": "634e2d39424349e30ef9ace3cbe374d6c2b404ab29491084908f538e4f9823567caa6d900e0c15e78c1515ed8b1418294c0e3063027fb7475982d0ddd62c2b64", - "pkg-tests-specs": "5cdd5861797fecf735eae28ef1480c96e7cbf11168c7459eaef247e8e9c8ab19a9940ba6436d7a982863a08286a2ca01d1a76cc926501a860822dda6f627ff4a" + "@yarnpkg/documentation": "91807b34e6dea592435ccae123ff1de2e4eb5915b5bf6f69e9b6108bb1fc47e614b3e6c095e90fddbc48070749ad63c372a7990a826f9c9c5da55a01875290a4", + "@yarnpkg/monorepo": "bb4a782d3d434fcd650e79b1367560cde2ac80cebaba1f906373cf370aa66aef412bac5d990d807b0e2c87734ac966a221b6442932a96bc0d774438e3fcafbd5", + "@yarnpkg/website": "c7ca7d61001453d8fceee2e83fe1309f41ab2c2c849e36a8577539fc20f25ae38dd2ce76d4b5722e6468f703fe9db4e23ee32981d0fcad89003d9497e2ee3709", + "@yarnpkg/zpm-constraints": "60af699f2119810820520bb8e3eb436c2b62987eae269d932eb44f73f96cb2274059fcbe7ec29bfb8c3c3022719b5b27829653a4c00162d7a4979963f63d6055", + "@yarnpkg/zpm-daemon-ui": "fa7a9a93084cb9be77a2ecc957130a65d371e4755518d7ca9ad0882d86965132a9629bb843f688f22c61cccecc5e746e572cbbfaad253e0cfca99f381fc16e00", + "acceptance-tests": "9d8edc097d9e560116c52345d9a48c0066f7b6f25e95554a612e01de95b19c720af054e6f98c4d5a3e00696ea566f8910687a0149f591c0911cc757bd5f2bad0", + "pkg-tests-core": "5a2339ba2265d216312aa34bbf0b7e0fd3a8724e6fe285562c70f2b27a76c4370832597316e2d20246c8cdc54f96df0eb083aeec824f489a8f68c9bf2efc9a55", + "pkg-tests-fixtures": "6f988cb36fe126bc5d41ce56ce3fd0ad485e201dfe8aaa5c70bbf8a3b64a0c0e4ca7f13215026f93dd8cb6c5c43be41574f171e84a345f67a47898fb8e1204cd", + "pkg-tests-specs": "12b6677dde1f354be6117229b4fd4c8714ec52269a28175843170cbed4cf0942b2634c01016de5df3f1e942126a0b0141b01180201e26aca444b51637714cfa0" }, "entries": { "@ai-sdk/gateway@npm:1.0.33": { @@ -73,6 +74,19 @@ ] } }, + "@algolia/abtesting@npm:1.18.1": { + "checksum": "16de29ecf6392ceda89cf26e65e6945c43bf4ddcf8f5653427aec2c056a038d804b83bf117907c9263e2e91d6b7732ae35ed9863b845b43a0d29de0ec33d0484", + "resolution": { + "resolution": "@algolia/abtesting@npm:1.18.1", + "version": "1.18.1", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + } + } + }, "@algolia/abtesting@npm:1.5.0": { "checksum": "93b88c22fc75a80baf07540fd21d8e98d012f041d2f7f33cb5b56386502062e8d625f3d42b05fb50191f91be17d334c1eace5a5c484878ca409254d0cc4a49e5", "resolution": { @@ -210,6 +224,19 @@ } } }, + "@algolia/client-abtesting@npm:5.52.1": { + "checksum": "58391be2df605a097d65e39a99014397499683f6d63a6eb0cac9f7ac8e978af06778d6a5e8ddfaa6ea6eccd63b0ed7dfe934452dd690ce676ed39024f991021f", + "resolution": { + "resolution": "@algolia/client-abtesting@npm:5.52.1", + "version": "5.52.1", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + } + } + }, "@algolia/client-account@npm:4.25.2": { "checksum": "8b37d5c2ee161dfcec27994ae10c90bf9e325ef531c92fd652c068126105c96a3e0ba867f21b6c192e0bc217a7841b16e6d40ec7d3e2a4b402e1abd9252dfdc6", "resolution": { @@ -248,6 +275,19 @@ } } }, + "@algolia/client-analytics@npm:5.52.1": { + "checksum": "dd847130f229d7e7ec200d90ca89ed061bbb8dd95954620b0d79b8326d2cedada4b2045fd43943e38dde3e9b0b9566538a7d460642fe4702ebe32d69caacc05b", + "resolution": { + "resolution": "@algolia/client-analytics@npm:5.52.1", + "version": "5.52.1", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + } + } + }, "@algolia/client-common@npm:4.25.2": { "checksum": "c82d161ed773e8686699dbd1fef78222bb294e92b41403ab78b22c0d25723843a606cb0b27e84cb2a2e7632b1e09617e695d30c602953f1eb840a154a5da942e", "resolution": { @@ -266,6 +306,13 @@ "version": "5.39.0" } }, + "@algolia/client-common@npm:5.52.1": { + "checksum": "c7e2bfa87228ad4902eea19fefc1bb53321a6410627a3d9265fa3488ff763d02c093c9d40e66a125edbfe1e81b4815188bbb47853d2f39439be86780b895ccb7", + "resolution": { + "resolution": "@algolia/client-common@npm:5.52.1", + "version": "5.52.1" + } + }, "@algolia/client-insights@npm:5.39.0": { "checksum": "4a03fe6530453b2e0e90c6f20b0a8117379e914487031b00f631a036baaab68e17cf5edfa77189c1d8000674001f782233147a5fd3b2825b2ca8778c3a5c1128", "resolution": { @@ -279,6 +326,19 @@ } } }, + "@algolia/client-insights@npm:5.52.1": { + "checksum": "eecdf510685cc9508834b6759fb2a09dc28d3622791a7cc4ab0683efbd36a6b48123265614568470a0375e4efd7d38ab176fb17ad2f8dda5cea0073625dab9e8", + "resolution": { + "resolution": "@algolia/client-insights@npm:5.52.1", + "version": "5.52.1", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + } + } + }, "@algolia/client-personalization@npm:4.25.2": { "checksum": "aed11f93340ab0d8a0866aa76f50cc4bb94faaaee3ff612f8b7f2c055c77bd56c6bdc07d490e9e5821edd5c067feee2423f6b9b9adbea72105e6e56f9a6b0deb", "resolution": { @@ -304,6 +364,19 @@ } } }, + "@algolia/client-personalization@npm:5.52.1": { + "checksum": "fdd63a9b7ab92ba0eddb045700b1ce9dabd127c94e77841b0b19afe8d1a68f48da46f48e82af4571f7387aea430f0c6f50ff824a86835c039eb127b984af9771", + "resolution": { + "resolution": "@algolia/client-personalization@npm:5.52.1", + "version": "5.52.1", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + } + } + }, "@algolia/client-query-suggestions@npm:5.39.0": { "checksum": "476dc98db4fa31bbdab94d4c4e9b84fcf02350e9aea51d3fb6ae9b812cfe5f0b167d98ae8e953861196b46b7150e71f81c5797b7a99cd74dbb9a2c3eb2cb8e47", "resolution": { @@ -317,6 +390,19 @@ } } }, + "@algolia/client-query-suggestions@npm:5.52.1": { + "checksum": "df810515a5825576d70a87d2c8d3e01caeec101b4369376e27148cc9a6e892bfc0982fe3ac29bb91b740f088040ceb3e9b59f880aaebe7d52af816ca832a8034", + "resolution": { + "resolution": "@algolia/client-query-suggestions@npm:5.52.1", + "version": "5.52.1", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + } + } + }, "@algolia/client-search@npm:4.25.2": { "checksum": "b05126f018485fa6c9bae46bc496b12c5c48e67d974415b0fb6db0284734df4b46cb1f9e4a43e3716457a72275cd8a7f73b703b87a45eeb423550ced74f6adee", "resolution": { @@ -342,6 +428,19 @@ } } }, + "@algolia/client-search@npm:5.52.1": { + "checksum": "cd64a489a35efd59e1b1a1ebe9a11583ccecb911a7261683ee8d9e223c2803ee594a3b3224d6d0d4bf37451a47fce15515142867a965c5d5969672be145ba98d", + "resolution": { + "resolution": "@algolia/client-search@npm:5.52.1", + "version": "5.52.1", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + } + } + }, "@algolia/events@npm:^4.0.1": { "checksum": "cf20b3bd9ede31e15fa246e660c8dd463e8c0f4c92112b5e649050b9379085dcb2cbf84c550c625fcc049c89e2a97260f6dab4b963cece54120c2cc733f2cff6", "resolution": { @@ -362,6 +461,19 @@ } } }, + "@algolia/ingestion@npm:1.52.1": { + "checksum": "3656d89eb0991e63ecaa0ac5ae66e6f92bdd8cb8dd4f2078f898bbe4eb722b70851c57e64c214c5fc23bd563785db1b3e4f7b307511af0702fa2ec39dc98fc08", + "resolution": { + "resolution": "@algolia/ingestion@npm:1.52.1", + "version": "1.52.1", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + } + } + }, "@algolia/logger-common@npm:4.25.2": { "checksum": "57fbfb8b3cd1db4f3c2463bdd5b8b2342b83996567ee7219faf1fc01e2cc4a59f005cb22cb13e281ac9f62956024edfb77e51ba17f820871dcfab67e805a710e", "resolution": { @@ -392,6 +504,19 @@ } } }, + "@algolia/monitoring@npm:1.52.1": { + "checksum": "4ed7ac7137271bc5649ab1eb22511a13dfed730360f0b1e6fe2a63cf0a7852c395332d751738ff4244465c9bf76da94c872ecc9baf4aec228c40f92711b9d12f", + "resolution": { + "resolution": "@algolia/monitoring@npm:1.52.1", + "version": "1.52.1", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + } + } + }, "@algolia/recommend@npm:4.25.2": { "checksum": "20268832b9104543ddcf53dbc2e3bd21c2e202344c6a98072e76a7d43068e5acfc3693da3e683d862feeee1fc2b62ccda734ec32a2e3175c1da0745f2f776b27", "resolution": { @@ -425,6 +550,19 @@ } } }, + "@algolia/recommend@npm:5.52.1": { + "checksum": "7b3e26ec8626c0156333ee0fe9649abb2bd051dbbdca39b2e46fab239d30de67f8e992cde47b333dda027dad357075c8b2bbcf5cd27f152af492fd0cc499984d", + "resolution": { + "resolution": "@algolia/recommend@npm:5.52.1", + "version": "5.52.1", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + } + } + }, "@algolia/requester-browser-xhr@npm:4.25.2": { "checksum": "22f8731c8c5762834b9ec0a0adf729a510507efc9d2a5d441f71ede05abe2171321947877d0c935371a5996b26e920c9df68d895cb87f9cf5f6f21da50cfb8e6", "resolution": { @@ -445,6 +583,16 @@ } } }, + "@algolia/requester-browser-xhr@npm:5.52.1": { + "checksum": "6eb5e61d475a8c796334a316c90ff846e152fd29c7290563a5488faaa01e7c0417c5317f0b3bf0e72714296b1da7c59987452960a51698990b476fce1af25999", + "resolution": { + "resolution": "@algolia/requester-browser-xhr@npm:5.52.1", + "version": "5.52.1", + "dependencies": { + "@algolia/client-common": "5.52.1" + } + } + }, "@algolia/requester-common@npm:4.25.2": { "checksum": "10e6310a8117ab62eafc0217ea6ec17e44f5ab3b879d033424ad4d0d8e45dfeb1b6510c98f22e494724c952f25a82f55213cd1ec9be3a3af9292ec70791e17bb", "resolution": { @@ -462,6 +610,16 @@ } } }, + "@algolia/requester-fetch@npm:5.52.1": { + "checksum": "deb2f9672321f31eaf609127478eb198d638cdd33d89a0e39ae238aaa6354a0ca375e696086f8baf6ff3fed55c32c2abf810e5864918aca4adf1d17059522bae", + "resolution": { + "resolution": "@algolia/requester-fetch@npm:5.52.1", + "version": "5.52.1", + "dependencies": { + "@algolia/client-common": "5.52.1" + } + } + }, "@algolia/requester-node-http@npm:4.25.2": { "checksum": "0bf95851d0b39b849189b6a3aecd657d6ee566998cabf162eeccb64768a6f39ad095178521b6e4fa67a5751c65affa778d856458d5620ae79c191171ad327c85", "resolution": { @@ -482,6 +640,16 @@ } } }, + "@algolia/requester-node-http@npm:5.52.1": { + "checksum": "8aae67373f0b8dcf238ab765ba9ae62e706020473ddc5232093ac0a23ec58c62b2d3ba526273f6c09b41fbc2b4bfdc161803b8f7d91c9c2fb22e7a92fdfe7e19", + "resolution": { + "resolution": "@algolia/requester-node-http@npm:5.52.1", + "version": "5.52.1", + "dependencies": { + "@algolia/client-common": "5.52.1" + } + } + }, "@algolia/transporter@npm:4.25.2": { "checksum": "f6761c2b6d8be51fb732b0387a893cbc749dcc999f1260d2585803f443da8f306b2b9a17b4737aa4d0d719839d71eaf0fa6ce691c3bf96ad891e33d64e88c158", "resolution": { @@ -494,6 +662,17 @@ } } }, + "@antfu/install-pkg@npm:^1.1.0": { + "checksum": "cb46f4a171eb71cc4d560656c08618201d162dc4e9ab54a8bb99ec0fb9e72d433d7822d17a6604cfc3ce0167be2827505af15f8a68d406e573132458dc2027fd", + "resolution": { + "resolution": "@antfu/install-pkg@npm:1.1.0", + "version": "1.1.0", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + } + } + }, "@arcanis/slice-ansi@npm:^1.1.1": { "checksum": "9b8481321d16b330ddb8fad48e6f60cdbd61de05793dfe8f26aa1c96ad8a874ae8030ee83e16379e1df6907d9ae0e533d5be9c06d24a6915b9d05724f1a30468", "resolution": { @@ -511,6 +690,13 @@ "version": "2.13.0" } }, + "@astrojs/compiler@npm:^2.13.0": { + "checksum": "a190b933e70af17c7708af4dd554fd227bf464a16299c40792a36390ad4febd9138136cbbe6e868db29c9495427759ebe08554df188f5f89f810e7104df4cb16", + "resolution": { + "resolution": "@astrojs/compiler@npm:2.13.1", + "version": "2.13.1" + } + }, "@astrojs/internal-helpers@npm:0.7.3": { "checksum": "cee54a2af88e728978e98b1772b91983cb27aa549e8b362a83972529778872d775a0b75e47a74a4c096bbe7f107b94bbdf5cf99182917e2185a8242dc5760b9f", "resolution": { @@ -518,6 +704,53 @@ "version": "0.7.3" } }, + "@astrojs/internal-helpers@npm:0.7.6": { + "checksum": "a1844f38eb37371bb0ad5bb91a70111e2773b90474341dee3f02ecc4f2156a55a115d665292c48893031db733183ae1ea33a2775d0bd03d3f98ccce51cabf932", + "resolution": { + "resolution": "@astrojs/internal-helpers@npm:0.7.6", + "version": "0.7.6" + } + }, + "@astrojs/internal-helpers@npm:0.9.1": { + "checksum": "34b12a54be4fc568ef330e97ff63caa861cbe5118acb09bcf0261ed63c0a206b4da9aeb45599c3d630ca7c793266856b56f2089bd36aec881dbebb3e809e3400", + "resolution": { + "resolution": "@astrojs/internal-helpers@npm:0.9.1", + "version": "0.9.1", + "dependencies": { + "picomatch": "^4.0.4" + } + } + }, + "@astrojs/markdown-remark@npm:6.3.11": { + "checksum": "bfa54b50a26d6b08273a32c99280e5e64686c7903b8bd4e23e61b31e31a00f5abb70195ab195a6fc202cb7123bfa497b767f9a744475c1e20ddd59b0a96d0d84", + "resolution": { + "resolution": "@astrojs/markdown-remark@npm:6.3.11", + "version": "6.3.11", + "dependencies": { + "@astrojs/internal-helpers": "0.7.6", + "@astrojs/prism": "3.3.0", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "import-meta-resolve": "^4.2.0", + "js-yaml": "^4.1.1", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "shiki": "^3.21.0", + "smol-toml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + } + }, "@astrojs/markdown-remark@npm:6.3.7, @astrojs/markdown-remark@npm:^6.3.1": { "checksum": "5ccb7b316f9a43362f7cb6873706b978b8313cd4d6d2ca4e3bc9af0ca901788f297fe7a22857ed3927dd4578765bc5d321049b0fe83d61d71184190f21d2335e", "resolution": { @@ -599,6 +832,26 @@ } } }, + "@astrojs/react@npm:^5.0.4": { + "checksum": "641dbba28e45aa1f3dd0d12969979db1a198a4d1d1b504b2041ecd0ac7cfa98c58820d5aa2db95972b995d11526659267817ab61298b33eb79e8856de3f0cb35", + "resolution": { + "resolution": "@astrojs/react@npm:5.0.5", + "version": "5.0.5", + "dependencies": { + "@astrojs/internal-helpers": "0.9.1", + "@vitejs/plugin-react": "^5.2.0", + "devalue": "^5.6.4", + "ultrahtml": "^1.6.0", + "vite": "^7.3.2" + }, + "peerDependencies": { + "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", + "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", + "react": "^17.0.2 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" + } + } + }, "@astrojs/sitemap@npm:^3.3.0": { "checksum": "a9caad1a83d649ebbac739401f837225212d9cf06d52dd9ec20fd054221ff14a7e3f5e43e919dda814316ce5fff40a635f4d72bd5eaa9497517d5f4ec27d45fb", "resolution": { @@ -611,6 +864,18 @@ } } }, + "@astrojs/sitemap@npm:^3.7.2": { + "checksum": "3aa0348fce1d2b344b489d27158f4c0b5bc916fbc43cf418469b2bbe6aaf4faba6b8b38db85804906d0a0461a124c2dd4220a43951e7bb5faaa1743014fe095a", + "resolution": { + "resolution": "@astrojs/sitemap@npm:3.7.2", + "version": "3.7.2", + "dependencies": { + "sitemap": "^9.0.0", + "stream-replace-string": "^2.0.0", + "zod": "^4.3.6" + } + } + }, "@astrojs/starlight@npm:^0.36.2": { "checksum": "d31b1c4fd6f2e61dc16bbfe060b776b15c10bb25e9462b1dced994ad725b03d0effa1722748f7279208ac874244258d9a0081518c4ef9ddcf5fb8529047d1dcb", "resolution": { @@ -1027,6 +1292,16 @@ } } }, + "@babel/parser@npm:^7.29.3": { + "checksum": "ca03ec670663ba1ff3cd0f1e210a74e329ceb0af763e6ca8be4bb658c21ae497cb0e82b2754740ffa66374415b39163582dcbba2e1fb32b659bafff48a41e42d", + "resolution": { + "resolution": "@babel/parser@npm:7.29.3", + "version": "7.29.3", + "dependencies": { + "@babel/types": "^7.29.0" + } + } + }, "@babel/plugin-syntax-async-generators@npm:^7.8.4": { "checksum": "77dc7e9d7c0e01f2fac6f0bcfcc472afcdfe36dfd9dc2868d76c87246636855b9225d6998f86e7644968ef22e49fb4a03dee02c7afb03904ef3a99e1efd01468", "resolution": { @@ -1468,6 +1743,13 @@ "version": "0.2.3" } }, + "@braintree/sanitize-url@npm:^7.1.1": { + "checksum": "535d8616c07c2428899e3ee4915186b65d21df4267c435fff9e030dae0de2a62c9e06e02fb6db73c7d633fc5cbf7cdc845d9b97198b857eb1f08c466551d9116", + "resolution": { + "resolution": "@braintree/sanitize-url@npm:7.1.2", + "version": "7.1.2" + } + }, "@capsizecss/unpack@npm:^2.4.0": { "checksum": "5975b3c177061de77dcd197d35e4ec01b0589ad1a0d39ec0bc297a4d03dd3270582f645dde13fd25255cd3414de3cbeac66cf39138a83537d04af3f9f34abb52", "resolution": { @@ -1480,6 +1762,23 @@ } } }, + "@capsizecss/unpack@npm:^4.0.0": { + "checksum": "52d4b4cb6d71c56df4b59e2f69b7351ee0b6f088643bd3e0d0b3c737638306d874d359864463ee76b986c4d69abc4e966103dd773fd7eb5672226d15c6d0ef6a", + "resolution": { + "resolution": "@capsizecss/unpack@npm:4.0.0", + "version": "4.0.0", + "dependencies": { + "fontkitten": "^1.0.0" + } + } + }, + "@chevrotain/types@npm:~11.1.1": { + "checksum": "7afe8b081c6ef72be00106217c47a387963deaa841e2758ce9b5350c0d59a82ba8a90eec7a077d654485cce6df9ea272b74a954524cd7dc91ae3803fc612bdf2", + "resolution": { + "resolution": "@chevrotain/types@npm:11.1.2", + "version": "11.1.2" + } + }, "@ctrl/tinycolor@npm:^4.0.4": { "checksum": "4a4cb32f6c43326267343b84d4fa89febb15c2d315764ef7161e2027b66dcded3fe563369ab37cfac03f2a51404ac90af064da1382d0ffd6136f510bfc2fdfed", "resolution": { @@ -1572,6 +1871,17 @@ ] } }, + "@emnapi/core@npm:1.10.0, @emnapi/core@npm:^1.10.0": { + "checksum": "79cf3a0650df9cb48a75f76de485693b198ea2f511129c120704f7b87490af1cba821e1b87e9f20c8128fe778d5f1fca492e43a2b96375d2ae4ffc71ad1b359d", + "resolution": { + "resolution": "@emnapi/core@npm:1.10.0", + "version": "1.10.0", + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + } + }, "@emnapi/core@npm:^1.5.0": { "checksum": "9089ce70035c923f3eef9b5daf4a3fb186bd01a4af2fed20759ac904e6282103fa020e18aee88d29c86ff508dd644ec5a758b30bd6ee3c651ce68be0b6bfaa0d", "resolution": { @@ -1583,6 +1893,16 @@ } } }, + "@emnapi/runtime@npm:1.10.0, @emnapi/runtime@npm:^1.10.0": { + "checksum": "ef3d63ba2d28c6c8aef4a450791ccfb39f2cdf71c818fc19b7f6c0572f394ca175a37235d5289047ccbb38d831cbcf4719d02f2cbac6a16903e857a8a6e85e74", + "resolution": { + "resolution": "@emnapi/runtime@npm:1.10.0", + "version": "1.10.0", + "dependencies": { + "tslib": "^2.4.0" + } + } + }, "@emnapi/runtime@npm:^1.5.0": { "checksum": "006452064f892a5e7350acd3c2afa8e7d9e950c4973a8e0896b9b5992b373dee82a0227dab3e53b391c2a3f63ac91a3d9d078b5eabf246af78cb94be6b6c4752", "resolution": { @@ -1603,6 +1923,16 @@ } } }, + "@emnapi/wasi-threads@npm:1.2.1, @emnapi/wasi-threads@npm:^1.2.1": { + "checksum": "1875308db5bcc486a9968004928bffe0226672a6aae1cd55fc86ef2721869499598a642424db553c5e4c11f044aa79bac8917dab317076fdbe1e2bf7334375d7", + "resolution": { + "resolution": "@emnapi/wasi-threads@npm:1.2.1", + "version": "1.2.1", + "dependencies": { + "tslib": "^2.4.0" + } + } + }, "@esbuild/aix-ppc64@npm:0.25.10": { "checksum": null, "resolution": { @@ -1633,6 +1963,21 @@ } } }, + "@esbuild/aix-ppc64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/aix-ppc64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "ppc64" + ], + "os": [ + "aix" + ] + } + } + }, "@esbuild/android-arm@npm:0.25.10": { "checksum": null, "resolution": { @@ -1663,6 +2008,21 @@ } } }, + "@esbuild/android-arm@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/android-arm@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "arm" + ], + "os": [ + "android" + ] + } + } + }, "@esbuild/android-arm64@npm:0.25.10": { "checksum": null, "resolution": { @@ -1693,6 +2053,21 @@ } } }, + "@esbuild/android-arm64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/android-arm64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "android" + ] + } + } + }, "@esbuild/android-x64@npm:0.25.10": { "checksum": null, "resolution": { @@ -1723,6 +2098,21 @@ } } }, + "@esbuild/android-x64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/android-x64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "android" + ] + } + } + }, "@esbuild/darwin-arm64@npm:0.25.10": { "checksum": null, "resolution": { @@ -1753,6 +2143,21 @@ } } }, + "@esbuild/darwin-arm64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/darwin-arm64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "darwin" + ] + } + } + }, "@esbuild/darwin-x64@npm:0.25.10": { "checksum": null, "resolution": { @@ -1783,6 +2188,21 @@ } } }, + "@esbuild/darwin-x64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/darwin-x64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "darwin" + ] + } + } + }, "@esbuild/freebsd-arm64@npm:0.25.10": { "checksum": null, "resolution": { @@ -1813,6 +2233,21 @@ } } }, + "@esbuild/freebsd-arm64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/freebsd-arm64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "freebsd" + ] + } + } + }, "@esbuild/freebsd-x64@npm:0.25.10": { "checksum": null, "resolution": { @@ -1843,22 +2278,37 @@ } } }, - "@esbuild/linux-arm@npm:0.25.10": { + "@esbuild/freebsd-x64@npm:0.27.7": { "checksum": null, "resolution": { - "resolution": "@esbuild/linux-arm@npm:0.25.10", - "version": "0.25.10", + "resolution": "@esbuild/freebsd-x64@npm:0.27.7", + "version": "0.27.7", "requirements": { "cpu": [ - "arm" + "x64" ], "os": [ - "linux" + "freebsd" ] } } }, - "@esbuild/linux-arm@npm:0.25.9": { + "@esbuild/linux-arm@npm:0.25.10": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/linux-arm@npm:0.25.10", + "version": "0.25.10", + "requirements": { + "cpu": [ + "arm" + ], + "os": [ + "linux" + ] + } + } + }, + "@esbuild/linux-arm@npm:0.25.9": { "checksum": null, "resolution": { "resolution": "@esbuild/linux-arm@npm:0.25.9", @@ -1873,6 +2323,21 @@ } } }, + "@esbuild/linux-arm@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/linux-arm@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "arm" + ], + "os": [ + "linux" + ] + } + } + }, "@esbuild/linux-arm64@npm:0.25.10": { "checksum": null, "resolution": { @@ -1903,6 +2368,21 @@ } } }, + "@esbuild/linux-arm64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/linux-arm64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "linux" + ] + } + } + }, "@esbuild/linux-ia32@npm:0.25.10": { "checksum": null, "resolution": { @@ -1933,6 +2413,21 @@ } } }, + "@esbuild/linux-ia32@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/linux-ia32@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "ia32" + ], + "os": [ + "linux" + ] + } + } + }, "@esbuild/linux-loong64@npm:0.25.10": { "checksum": null, "resolution": { @@ -1963,6 +2458,21 @@ } } }, + "@esbuild/linux-loong64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/linux-loong64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "loong64" + ], + "os": [ + "linux" + ] + } + } + }, "@esbuild/linux-mips64el@npm:0.25.10": { "checksum": null, "resolution": { @@ -1993,6 +2503,21 @@ } } }, + "@esbuild/linux-mips64el@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/linux-mips64el@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "mips64el" + ], + "os": [ + "linux" + ] + } + } + }, "@esbuild/linux-ppc64@npm:0.25.10": { "checksum": null, "resolution": { @@ -2023,6 +2548,21 @@ } } }, + "@esbuild/linux-ppc64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/linux-ppc64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "ppc64" + ], + "os": [ + "linux" + ] + } + } + }, "@esbuild/linux-riscv64@npm:0.25.10": { "checksum": null, "resolution": { @@ -2053,6 +2593,21 @@ } } }, + "@esbuild/linux-riscv64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/linux-riscv64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "riscv64" + ], + "os": [ + "linux" + ] + } + } + }, "@esbuild/linux-s390x@npm:0.25.10": { "checksum": null, "resolution": { @@ -2083,6 +2638,21 @@ } } }, + "@esbuild/linux-s390x@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/linux-s390x@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "s390x" + ], + "os": [ + "linux" + ] + } + } + }, "@esbuild/linux-x64@npm:0.25.10": { "checksum": null, "resolution": { @@ -2113,6 +2683,21 @@ } } }, + "@esbuild/linux-x64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/linux-x64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "linux" + ] + } + } + }, "@esbuild/netbsd-arm64@npm:0.25.10": { "checksum": null, "resolution": { @@ -2143,6 +2728,21 @@ } } }, + "@esbuild/netbsd-arm64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/netbsd-arm64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "netbsd" + ] + } + } + }, "@esbuild/netbsd-x64@npm:0.25.10": { "checksum": null, "resolution": { @@ -2173,6 +2773,21 @@ } } }, + "@esbuild/netbsd-x64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/netbsd-x64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "netbsd" + ] + } + } + }, "@esbuild/openbsd-arm64@npm:0.25.10": { "checksum": null, "resolution": { @@ -2203,6 +2818,21 @@ } } }, + "@esbuild/openbsd-arm64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/openbsd-arm64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "openbsd" + ] + } + } + }, "@esbuild/openbsd-x64@npm:0.25.10": { "checksum": null, "resolution": { @@ -2233,6 +2863,21 @@ } } }, + "@esbuild/openbsd-x64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/openbsd-x64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "openbsd" + ] + } + } + }, "@esbuild/openharmony-arm64@npm:0.25.10": { "checksum": null, "resolution": { @@ -2263,6 +2908,21 @@ } } }, + "@esbuild/openharmony-arm64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/openharmony-arm64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "openharmony" + ] + } + } + }, "@esbuild/sunos-x64@npm:0.25.10": { "checksum": null, "resolution": { @@ -2293,6 +2953,21 @@ } } }, + "@esbuild/sunos-x64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/sunos-x64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "sunos" + ] + } + } + }, "@esbuild/win32-arm64@npm:0.25.10": { "checksum": null, "resolution": { @@ -2323,6 +2998,21 @@ } } }, + "@esbuild/win32-arm64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/win32-arm64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "win32" + ] + } + } + }, "@esbuild/win32-ia32@npm:0.25.10": { "checksum": null, "resolution": { @@ -2353,6 +3043,21 @@ } } }, + "@esbuild/win32-ia32@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/win32-ia32@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "ia32" + ], + "os": [ + "win32" + ] + } + } + }, "@esbuild/win32-x64@npm:0.25.10": { "checksum": null, "resolution": { @@ -2383,6 +3088,21 @@ } } }, + "@esbuild/win32-x64@npm:0.27.7": { + "checksum": null, + "resolution": { + "resolution": "@esbuild/win32-x64@npm:0.27.7", + "version": "0.27.7", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "win32" + ] + } + } + }, "@eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.0": { "checksum": "1a22e55acc1dae62827c1b2f3c1bc805b0c3d23ff10208aadc1441e0e310c5605798ac10a57e32b0218b763e32481372750a269d646e82fe396005559b8df9b7", "resolution": { @@ -2605,6 +3325,45 @@ "version": "0.4.3" } }, + "@iconify-json/octicon@npm:^1.2.23": { + "checksum": "a4cecb19dd2a6ebb8fb6b2af57aee0de36a2f3ee41b17ced5cd7b0cfcd896dfb76251d5bea7cacc69f124f34c81e0eb572792edad1d4e9ddbec074f354780e06", + "resolution": { + "resolution": "@iconify-json/octicon@npm:1.2.24", + "version": "1.2.24", + "dependencies": { + "@iconify/types": "*" + } + } + }, + "@iconify-json/simple-icons@npm:^1.2.79": { + "checksum": "48485910bed1fec701b8a1d74f7c7e003c58ba25fcbd0e4a842fcceacae66ab6e3b27683d24e07bd3631c6761338b8225a69e57445ffb8be080ce39bdc4e1868", + "resolution": { + "resolution": "@iconify-json/simple-icons@npm:1.2.82", + "version": "1.2.82", + "dependencies": { + "@iconify/types": "*" + } + } + }, + "@iconify/types@npm:*, @iconify/types@npm:^2.0.0": { + "checksum": "9a411bc7e0bf59fa689305a1cd79bdd9799fe46f0394819b7826c8322a84dfcd3ad1a7761b97390984c5cb19948ec9a5eaa3d2a16bbf6398aecda566bc49cc51", + "resolution": { + "resolution": "@iconify/types@npm:2.0.0", + "version": "2.0.0" + } + }, + "@iconify/utils@npm:^3.0.2": { + "checksum": "be8fb2ac48b41c9b3b88d521032636f6bd5cbe096cf81771a2fbb3ca8d9d90027c257614aa13c6eae2769493de656e5951d370b8f5701e5bd8a78b5addee0820", + "resolution": { + "resolution": "@iconify/utils@npm:3.1.3", + "version": "3.1.3", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + } + }, "@img/colour@npm:^1.0.0": { "checksum": "2f80ea2df8370ca31b19c618da8e90c4951ba5c41ef0de511e113538848cefa8fc68e0d054e39265ad5460b20722cfac93d2fb5a4190f8e7b5c873de1b4d87dd", "resolution": { @@ -3441,11 +4200,31 @@ } } }, - "@monaco-editor/loader@npm:^1.4.0": { - "checksum": "e0b9ff1c82d79de69d31b5cfcf024b648a2ba3f2a07dc1447da83b301a9cdbe3f189c282adcbfc125898b662c00ecc8cd1af869e42ae0d7a95a78e40988a84eb", + "@mermaid-js/parser@npm:^1.1.1": { + "checksum": "0c61a3788d5d11eba17f1a214d6fca0b5cdbf1137bcf06c4c98fb4503c09e41cd297ae0175e67b431974981693d67728f291f0fd63768bc84cb5491f14b66755", "resolution": { - "resolution": "@monaco-editor/loader@npm:1.5.0", - "version": "1.5.0", + "resolution": "@mermaid-js/parser@npm:1.1.1", + "version": "1.1.1", + "dependencies": { + "@chevrotain/types": "~11.1.1" + } + } + }, + "@monaco-editor/loader@npm:^1.4.0": { + "checksum": "e0b9ff1c82d79de69d31b5cfcf024b648a2ba3f2a07dc1447da83b301a9cdbe3f189c282adcbfc125898b662c00ecc8cd1af869e42ae0d7a95a78e40988a84eb", + "resolution": { + "resolution": "@monaco-editor/loader@npm:1.5.0", + "version": "1.5.0", + "dependencies": { + "state-local": "^1.0.6" + } + } + }, + "@monaco-editor/loader@npm:^1.5.0": { + "checksum": "444ae06b6842291707e12864d6bfb6c675bb2c2f703b4b12a004f5e7895e31620f388b9d64789c872ceb74d05586939adb0c10c4f4c3fc5051d869f5a23ebf2f", + "resolution": { + "resolution": "@monaco-editor/loader@npm:1.7.0", + "version": "1.7.0", "dependencies": { "state-local": "^1.0.6" } @@ -3466,6 +4245,21 @@ } } }, + "@monaco-editor/react@npm:^4.7.0": { + "checksum": "ae51b244eed069e00504946ce38ea24a5c0e99ab01b46b7aa46bcd8dab6d9dec8169cc4c73a07a28ef0afc6f2d0fe00bc2b00432f479b1ba6533294d1309c3bc", + "resolution": { + "resolution": "@monaco-editor/react@npm:4.7.0", + "version": "4.7.0", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + } + }, "@napi-rs/wasm-runtime@npm:^1.0.5": { "checksum": "49e7f776ecb767659ff87a62de0b5ea6a65a06c5b51685fb95b67f8958dc2361eb41bfc7c064f09567eb6d0fe454bcbb5587119d7f5b4d651e41b74b994a6de7", "resolution": { @@ -3478,6 +4272,20 @@ } } }, + "@napi-rs/wasm-runtime@npm:^1.1.4": { + "checksum": "43c676861f990bfc84f080f975b755f9dc07c568bb1d4812c7a9cd5f89387448a6714f5497a4e9c58a2a5abe9a2144ab56bfe26b693b0bd57efde575d64b6171", + "resolution": { + "resolution": "@napi-rs/wasm-runtime@npm:1.1.4", + "version": "1.1.4", + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + } + }, "@nodelib/fs.scandir@npm:2.1.5": { "checksum": "0aa6555d3692980c7f316e702a7a6c95c44e9b9cc34e4978a22f3e11ee4df9d370b2ffd1ece6793eb02d1890e70604cdc8e77c6a46dab66a83effc806ff9fd5d", "resolution": { @@ -3870,6 +4678,13 @@ "version": "1.1.0" } }, + "@oxc-project/types@npm:=0.130.0": { + "checksum": "2501ee1b8052da46d7c965fdb733214b846f475ceb3c016d69dee48e3b7fd7479c1ce4465838ad47876e4bd1246252ebce4b4ace0e5e303a41cb5cdd1d47b17f", + "resolution": { + "resolution": "@oxc-project/types@npm:0.130.0", + "version": "0.130.0" + } + }, "@pagefind/darwin-arm64@npm:1.4.0": { "checksum": null, "resolution": { @@ -4067,6 +4882,22 @@ } } }, + "@puppeteer/browsers@npm:2.13.2": { + "checksum": "7ac2981208a4d5c8d5c1f28be312cb6ad8cea2af18eca090dda57117891e08897274cfa179bdb0511923df5a1a81c71b36a6a45ad4c4e497ba58fe3bf00e6987", + "resolution": { + "resolution": "@puppeteer/browsers@npm:2.13.2", + "version": "2.13.2", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + } + } + }, "@reduxjs/toolkit@npm:1.x.x || 2.x.x": { "checksum": "2b0016d48f91f8bf06b9dc7e22c11b85709c5c16101b9debc227240693c1b7e462a0888d54448ad9e177361ed9eef8d358e192f9429619a6c4b9ea1e57f02931", "resolution": { @@ -4113,6 +4944,251 @@ ] } }, + "@rolldown/binding-android-arm64@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-android-arm64@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "android" + ] + } + } + }, + "@rolldown/binding-darwin-arm64@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-darwin-arm64@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "darwin" + ] + } + } + }, + "@rolldown/binding-darwin-x64@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-darwin-x64@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "darwin" + ] + } + } + }, + "@rolldown/binding-freebsd-x64@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-freebsd-x64@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "freebsd" + ] + } + } + }, + "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "arm" + ], + "os": [ + "linux" + ] + } + } + }, + "@rolldown/binding-linux-arm64-gnu@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-linux-arm64-gnu@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "linux" + ], + "libc": [ + "glibc" + ] + } + } + }, + "@rolldown/binding-linux-arm64-musl@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-linux-arm64-musl@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "linux" + ], + "libc": [ + "musl" + ] + } + } + }, + "@rolldown/binding-linux-ppc64-gnu@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-linux-ppc64-gnu@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "ppc64" + ], + "os": [ + "linux" + ], + "libc": [ + "glibc" + ] + } + } + }, + "@rolldown/binding-linux-s390x-gnu@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-linux-s390x-gnu@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "s390x" + ], + "os": [ + "linux" + ], + "libc": [ + "glibc" + ] + } + } + }, + "@rolldown/binding-linux-x64-gnu@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-linux-x64-gnu@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "linux" + ], + "libc": [ + "glibc" + ] + } + } + }, + "@rolldown/binding-linux-x64-musl@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-linux-x64-musl@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "linux" + ], + "libc": [ + "musl" + ] + } + } + }, + "@rolldown/binding-openharmony-arm64@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-openharmony-arm64@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "openharmony" + ] + } + } + }, + "@rolldown/binding-wasm32-wasi@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-wasm32-wasi@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "wasm32" + ] + }, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + } + } + }, + "@rolldown/binding-win32-arm64-msvc@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-win32-arm64-msvc@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "win32" + ] + } + } + }, + "@rolldown/binding-win32-x64-msvc@npm:1.0.1": { + "checksum": null, + "resolution": { + "resolution": "@rolldown/binding-win32-x64-msvc@npm:1.0.1", + "version": "1.0.1", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "win32" + ] + } + } + }, "@rolldown/pluginutils@npm:1.0.0-rc.3": { "checksum": "5fe72e69eab36e9dac089241e360a8f04ab10e26fad12c61aae6b4ea5a80c5044bc7547c446454ae52e0916372d1150902b15e0fbcb5169088477cd2932cc29f", "resolution": { @@ -4120,6 +5196,13 @@ "version": "1.0.0-rc.3" } }, + "@rolldown/pluginutils@npm:^1.0.0": { + "checksum": "e684305beccec9cdc475ff58d0750eeacbd931ada04147a115dc76ab3a1ee453127f43915dac7f4202e2932205666582b3b1dcdcac9cd923ae4f1bc8a02cc084", + "resolution": { + "resolution": "@rolldown/pluginutils@npm:1.0.1", + "version": "1.0.1" + } + }, "@rollup/pluginutils@npm:^4.1.1, @rollup/pluginutils@npm:^4.2.1": { "checksum": "23cadac04bb58409831afdda1e9ef2e6b393a265f22f9092158ba3da2d1bc4ef0d21cd6dbc8e9fee3e1184f57fb5204fc58db82ee65bef67277b917ce9edaa36", "resolution": { @@ -4131,7 +5214,7 @@ } } }, - "@rollup/pluginutils@npm:^5.2.0": { + "@rollup/pluginutils@npm:^5.2.0, @rollup/pluginutils@npm:^5.3.0": { "checksum": "3a5130448ee4dcf861ea729b3ea7192d739dd95779aef538b46b70de15c50fdaecf8a0b433a422e1f213a75737f111b74aed7aef9cd1eb782404fa566988eecd", "resolution": { "resolution": "@rollup/pluginutils@npm:5.3.0", @@ -4525,6 +5608,33 @@ } } }, + "@shikijs/core@npm:3.23.0": { + "checksum": "b8beaf86f7f30d3ea1916c9946b5a327b7c72e4716fb6670a2c33236d6a589bf3c0cc0468cce88b47e80756a44895be661051d6b7739eb223744150e48f5a3d0", + "resolution": { + "resolution": "@shikijs/core@npm:3.23.0", + "version": "3.23.0", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + } + }, + "@shikijs/core@npm:4.0.2": { + "checksum": "78767dfe239b4fd75b43cb1e4330b57e0892f45cd10b54c38c3f671629b22fd58930d6c3158853d1110d0e10ba2d005a0ffee658d86f88ffbe25ad4c9a72ee45", + "resolution": { + "resolution": "@shikijs/core@npm:4.0.2", + "version": "4.0.2", + "dependencies": { + "@shikijs/primitive": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + } + }, "@shikijs/engine-javascript@npm:3.13.0": { "checksum": "cfc0e4bb83698809302ce2369e9d1e4965bad21076101b12f44a9d49e541d3ffc7055ad4d2e5f076fc1ccf89e4cf8cbfaf07ab75ac84836293183140ecbb3562", "resolution": { @@ -4537,6 +5647,30 @@ } } }, + "@shikijs/engine-javascript@npm:3.23.0": { + "checksum": "1db8ce29c15e265118939f351397d09052a775a2e9cee0c10b93f1a90b2c9692880c5f44e6353684f771daed3d4f03337519b7d7a352757f6d664dc9d284b9eb", + "resolution": { + "resolution": "@shikijs/engine-javascript@npm:3.23.0", + "version": "3.23.0", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + } + }, + "@shikijs/engine-javascript@npm:4.0.2": { + "checksum": "eb5178313e195ec4f33aa22be724fc56c89f75af50740942adbb34cd51898ff351d423c09461ab70e21ea0dbcf03576a98dfbf0f45cbf490e0135b6830c89bdc", + "resolution": { + "resolution": "@shikijs/engine-javascript@npm:4.0.2", + "version": "4.0.2", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + } + }, "@shikijs/engine-oniguruma@npm:3.13.0": { "checksum": "6b8bde8f482960c8e71175b4d0e86e2b6c0988d8de7cbb87757ba78d6c79e82103af85ed7a983d72486f105ef88a656648778ce66a3c6c812254e10fc6b72645", "resolution": { @@ -4548,6 +5682,28 @@ } } }, + "@shikijs/engine-oniguruma@npm:3.23.0": { + "checksum": "75e02698dad0152ded6b6eafa17eafe276315d6543e0e45677d0d6577785ec69baad56a799ebac9eac11cb8d148eb5c6292f6254f4274a354e1d9d06daa9a150", + "resolution": { + "resolution": "@shikijs/engine-oniguruma@npm:3.23.0", + "version": "3.23.0", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + } + }, + "@shikijs/engine-oniguruma@npm:4.0.2": { + "checksum": "d7e07ce13783d24825dec254d32c4bbe1c13a74712e57a00c2724bbc96562773fbb18e110a8e078c4392fdd0687bbfabb41ac2b3437eaaabc3dbe6cae4accf35", + "resolution": { + "resolution": "@shikijs/engine-oniguruma@npm:4.0.2", + "version": "4.0.2", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2" + } + } + }, "@shikijs/langs@npm:3.13.0": { "checksum": "c3df283e49cb5c03c8760edd05c0758a4b0eb423447488881c8d4ba612859f0e9d62e98622684636ad748ad90a9126a4f0363845ac16044376aac7d9e748013c", "resolution": { @@ -4558,6 +5714,38 @@ } } }, + "@shikijs/langs@npm:3.23.0": { + "checksum": "4490195af48d0827e8b54175a1aa1f7e10429bb7740e7053b47047ca47880decf4db90f5162400c7907e0b97fd6b492fca4aebf5ab47052d094d37c13fdb3026", + "resolution": { + "resolution": "@shikijs/langs@npm:3.23.0", + "version": "3.23.0", + "dependencies": { + "@shikijs/types": "3.23.0" + } + } + }, + "@shikijs/langs@npm:4.0.2": { + "checksum": "addb2fcedce4a76859cbd4018cd08a2a8c48c8ca34b93a6be667d42bb98991c983547c4c13cb18b2198045649f3042d72511614faa879cf07a968d87488c272f", + "resolution": { + "resolution": "@shikijs/langs@npm:4.0.2", + "version": "4.0.2", + "dependencies": { + "@shikijs/types": "4.0.2" + } + } + }, + "@shikijs/primitive@npm:4.0.2": { + "checksum": "b828d8878da4c8be5ebf751f1027cca0d66f9f339a93fcd0c1e7a8b0afc0c232c80535527101e888f6a725a3994b9bede64340be7becc788e845e4c4233ef656", + "resolution": { + "resolution": "@shikijs/primitive@npm:4.0.2", + "version": "4.0.2", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + } + }, "@shikijs/themes@npm:3.13.0": { "checksum": "2a3cf8b76c7ea34f01d5b032873b505e665dd2f8f51f86b72966b9eb54d8cb838f7d60f3ddf349da4442f94964f89b4bbcb10092ceb3580f3d3d75efed99e972", "resolution": { @@ -4568,6 +5756,26 @@ } } }, + "@shikijs/themes@npm:3.23.0": { + "checksum": "e83449ad9f0578257c03a364349947e30ad732775afe1b4fa087726a4407aeedd1a7b3c0ce10e09ce0670b7fa6e14b2962dcc2d26d21d274bdaf67a5b6ddd85c", + "resolution": { + "resolution": "@shikijs/themes@npm:3.23.0", + "version": "3.23.0", + "dependencies": { + "@shikijs/types": "3.23.0" + } + } + }, + "@shikijs/themes@npm:4.0.2": { + "checksum": "08cd186bd51b33ac12347e42921a3dc4d61d582e8be9432bdc6c04f1f6840bc69c5966b22ee8d4ac5e4a0b52881c53c32e0233706dfebeb0da7ec6faf16514ea", + "resolution": { + "resolution": "@shikijs/themes@npm:4.0.2", + "version": "4.0.2", + "dependencies": { + "@shikijs/types": "4.0.2" + } + } + }, "@shikijs/types@npm:3.13.0": { "checksum": "ebf9b567f50ee0aab3ba2147f78598314e09d3a5135dc476249b786bc88ad6695852d162cd96ce49f90151844671e7fbb8c7225044a51a77f75e61d97c8b1e39", "resolution": { @@ -4579,6 +5787,28 @@ } } }, + "@shikijs/types@npm:3.23.0": { + "checksum": "2db602f140f9f2f96fdb6d2e7164d3ceba0df8ba38a560388e105e1d1e1c840c55a53dee2f5e131464f23f69d1fbc99a626e7de7cf62151eb24539a885cb1646", + "resolution": { + "resolution": "@shikijs/types@npm:3.23.0", + "version": "3.23.0", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + } + }, + "@shikijs/types@npm:4.0.2": { + "checksum": "14a4538f3ab64c579f1ece3f8d649b84d248f4137258dfee2c5793f57b3c5b843f64ace04227e7b9e805bde4263d59f5689b9fea82e0ece45413a76983e9d015", + "resolution": { + "resolution": "@shikijs/types@npm:4.0.2", + "version": "4.0.2", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + } + }, "@shikijs/vscode-textmate@npm:^10.0.2": { "checksum": "7cc55038c667d5948c89e4957e8d7cb4edc3bc14ad87cff1f1544b3d031ca5d7ccac7fa6c4facbea0ad0071d800f8170586b5b0f36f3530a5fefaadb14580f38", "resolution": { @@ -4986,6 +6216,22 @@ } } }, + "@tailwindcss/node@npm:4.3.0": { + "checksum": "543a9057481e303651260d84dffe47d4bd0c3dc646528d19f072c24b413df51d2844de5d6a4888e19c3c90fae0d3ee1969384fdac56004f64d1366ffabb69cda", + "resolution": { + "resolution": "@tailwindcss/node@npm:4.3.0", + "version": "4.3.0", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + } + }, "@tailwindcss/oxide@npm:4.1.14": { "checksum": "180bcb90ad3c6367891b49966497feadd9ee7863eac9058a9aa12ca614bbfde79607f14ec2a5dbd10abd79fcae375b7068275aca860fb798b423ad26a5e864b9", "resolution": { @@ -5023,89 +6269,271 @@ ] } }, - "@tailwindcss/oxide-android-arm64@npm:4.1.14": { + "@tailwindcss/oxide@npm:4.3.0": { + "checksum": "8df1b05a04d13d5215398e260802451e8a711600e97c22225c13988c43ce2b9f1002864986be2180461972259aa8556633b6d72bbb84990d55d7c99be5b58c36", + "resolution": { + "resolution": "@tailwindcss/oxide@npm:4.3.0", + "version": "4.3.0", + "dependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + }, + "optionalDependencies": [ + "@tailwindcss/oxide-android-arm64", + "@tailwindcss/oxide-darwin-arm64", + "@tailwindcss/oxide-darwin-x64", + "@tailwindcss/oxide-freebsd-x64", + "@tailwindcss/oxide-linux-arm-gnueabihf", + "@tailwindcss/oxide-linux-arm64-gnu", + "@tailwindcss/oxide-linux-arm64-musl", + "@tailwindcss/oxide-linux-x64-gnu", + "@tailwindcss/oxide-linux-x64-musl", + "@tailwindcss/oxide-wasm32-wasi", + "@tailwindcss/oxide-win32-arm64-msvc", + "@tailwindcss/oxide-win32-x64-msvc" + ] + } + }, + "@tailwindcss/oxide-android-arm64@npm:4.1.14": { + "checksum": null, + "resolution": { + "resolution": "@tailwindcss/oxide-android-arm64@npm:4.1.14", + "version": "4.1.14", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "android" + ] + } + } + }, + "@tailwindcss/oxide-android-arm64@npm:4.3.0": { + "checksum": null, + "resolution": { + "resolution": "@tailwindcss/oxide-android-arm64@npm:4.3.0", + "version": "4.3.0", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "android" + ] + } + } + }, + "@tailwindcss/oxide-darwin-arm64@npm:4.1.14": { + "checksum": null, + "resolution": { + "resolution": "@tailwindcss/oxide-darwin-arm64@npm:4.1.14", + "version": "4.1.14", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "darwin" + ] + } + } + }, + "@tailwindcss/oxide-darwin-arm64@npm:4.3.0": { + "checksum": null, + "resolution": { + "resolution": "@tailwindcss/oxide-darwin-arm64@npm:4.3.0", + "version": "4.3.0", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "darwin" + ] + } + } + }, + "@tailwindcss/oxide-darwin-x64@npm:4.1.14": { + "checksum": null, + "resolution": { + "resolution": "@tailwindcss/oxide-darwin-x64@npm:4.1.14", + "version": "4.1.14", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "darwin" + ] + } + } + }, + "@tailwindcss/oxide-darwin-x64@npm:4.3.0": { + "checksum": null, + "resolution": { + "resolution": "@tailwindcss/oxide-darwin-x64@npm:4.3.0", + "version": "4.3.0", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "darwin" + ] + } + } + }, + "@tailwindcss/oxide-freebsd-x64@npm:4.1.14": { + "checksum": null, + "resolution": { + "resolution": "@tailwindcss/oxide-freebsd-x64@npm:4.1.14", + "version": "4.1.14", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "freebsd" + ] + } + } + }, + "@tailwindcss/oxide-freebsd-x64@npm:4.3.0": { + "checksum": null, + "resolution": { + "resolution": "@tailwindcss/oxide-freebsd-x64@npm:4.3.0", + "version": "4.3.0", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "freebsd" + ] + } + } + }, + "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.1.14": { + "checksum": null, + "resolution": { + "resolution": "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.1.14", + "version": "4.1.14", + "requirements": { + "cpu": [ + "arm" + ], + "os": [ + "linux" + ] + } + } + }, + "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.3.0": { "checksum": null, "resolution": { - "resolution": "@tailwindcss/oxide-android-arm64@npm:4.1.14", - "version": "4.1.14", + "resolution": "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.3.0", + "version": "4.3.0", "requirements": { "cpu": [ - "arm64" + "arm" ], "os": [ - "android" + "linux" ] } } }, - "@tailwindcss/oxide-darwin-arm64@npm:4.1.14": { + "@tailwindcss/oxide-linux-arm64-gnu@npm:4.1.14": { "checksum": null, "resolution": { - "resolution": "@tailwindcss/oxide-darwin-arm64@npm:4.1.14", + "resolution": "@tailwindcss/oxide-linux-arm64-gnu@npm:4.1.14", "version": "4.1.14", "requirements": { "cpu": [ "arm64" ], "os": [ - "darwin" + "linux" + ], + "libc": [ + "glibc" ] } } }, - "@tailwindcss/oxide-darwin-x64@npm:4.1.14": { + "@tailwindcss/oxide-linux-arm64-gnu@npm:4.3.0": { "checksum": null, "resolution": { - "resolution": "@tailwindcss/oxide-darwin-x64@npm:4.1.14", - "version": "4.1.14", + "resolution": "@tailwindcss/oxide-linux-arm64-gnu@npm:4.3.0", + "version": "4.3.0", "requirements": { "cpu": [ - "x64" + "arm64" ], "os": [ - "darwin" + "linux" + ], + "libc": [ + "glibc" ] } } }, - "@tailwindcss/oxide-freebsd-x64@npm:4.1.14": { + "@tailwindcss/oxide-linux-arm64-musl@npm:4.1.14": { "checksum": null, "resolution": { - "resolution": "@tailwindcss/oxide-freebsd-x64@npm:4.1.14", + "resolution": "@tailwindcss/oxide-linux-arm64-musl@npm:4.1.14", "version": "4.1.14", "requirements": { "cpu": [ - "x64" + "arm64" ], "os": [ - "freebsd" + "linux" + ], + "libc": [ + "musl" ] } } }, - "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.1.14": { + "@tailwindcss/oxide-linux-arm64-musl@npm:4.3.0": { "checksum": null, "resolution": { - "resolution": "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.1.14", - "version": "4.1.14", + "resolution": "@tailwindcss/oxide-linux-arm64-musl@npm:4.3.0", + "version": "4.3.0", "requirements": { "cpu": [ - "arm" + "arm64" ], "os": [ "linux" + ], + "libc": [ + "musl" ] } } }, - "@tailwindcss/oxide-linux-arm64-gnu@npm:4.1.14": { + "@tailwindcss/oxide-linux-x64-gnu@npm:4.1.14": { "checksum": null, "resolution": { - "resolution": "@tailwindcss/oxide-linux-arm64-gnu@npm:4.1.14", + "resolution": "@tailwindcss/oxide-linux-x64-gnu@npm:4.1.14", "version": "4.1.14", "requirements": { "cpu": [ - "arm64" + "x64" ], "os": [ "linux" @@ -5116,28 +6544,28 @@ } } }, - "@tailwindcss/oxide-linux-arm64-musl@npm:4.1.14": { + "@tailwindcss/oxide-linux-x64-gnu@npm:4.3.0": { "checksum": null, "resolution": { - "resolution": "@tailwindcss/oxide-linux-arm64-musl@npm:4.1.14", - "version": "4.1.14", + "resolution": "@tailwindcss/oxide-linux-x64-gnu@npm:4.3.0", + "version": "4.3.0", "requirements": { "cpu": [ - "arm64" + "x64" ], "os": [ "linux" ], "libc": [ - "musl" + "glibc" ] } } }, - "@tailwindcss/oxide-linux-x64-gnu@npm:4.1.14": { + "@tailwindcss/oxide-linux-x64-musl@npm:4.1.14": { "checksum": null, "resolution": { - "resolution": "@tailwindcss/oxide-linux-x64-gnu@npm:4.1.14", + "resolution": "@tailwindcss/oxide-linux-x64-musl@npm:4.1.14", "version": "4.1.14", "requirements": { "cpu": [ @@ -5147,16 +6575,16 @@ "linux" ], "libc": [ - "glibc" + "musl" ] } } }, - "@tailwindcss/oxide-linux-x64-musl@npm:4.1.14": { + "@tailwindcss/oxide-linux-x64-musl@npm:4.3.0": { "checksum": null, "resolution": { - "resolution": "@tailwindcss/oxide-linux-x64-musl@npm:4.1.14", - "version": "4.1.14", + "resolution": "@tailwindcss/oxide-linux-x64-musl@npm:4.3.0", + "version": "4.3.0", "requirements": { "cpu": [ "x64" @@ -5190,6 +6618,26 @@ } } }, + "@tailwindcss/oxide-wasm32-wasi@npm:4.3.0": { + "checksum": null, + "resolution": { + "resolution": "@tailwindcss/oxide-wasm32-wasi@npm:4.3.0", + "version": "4.3.0", + "requirements": { + "cpu": [ + "wasm32" + ] + }, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + } + } + }, "@tailwindcss/oxide-win32-arm64-msvc@npm:4.1.14": { "checksum": null, "resolution": { @@ -5205,6 +6653,21 @@ } } }, + "@tailwindcss/oxide-win32-arm64-msvc@npm:4.3.0": { + "checksum": null, + "resolution": { + "resolution": "@tailwindcss/oxide-win32-arm64-msvc@npm:4.3.0", + "version": "4.3.0", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "win32" + ] + } + } + }, "@tailwindcss/oxide-win32-x64-msvc@npm:4.1.14": { "checksum": null, "resolution": { @@ -5220,6 +6683,21 @@ } } }, + "@tailwindcss/oxide-win32-x64-msvc@npm:4.3.0": { + "checksum": null, + "resolution": { + "resolution": "@tailwindcss/oxide-win32-x64-msvc@npm:4.3.0", + "version": "4.3.0", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "win32" + ] + } + } + }, "@tailwindcss/vite@npm:^4.1.11": { "checksum": "0bff386ab160741019a794e18330f0c378c87248431d892a801838930ce97fcbeb851c1fa13e9dfd399a1ac48b54231c0fd91ea2e3e5bc13d42ccdb62f78e2f8", "resolution": { @@ -5235,6 +6713,21 @@ } } }, + "@tailwindcss/vite@npm:^4.2.4": { + "checksum": "6853e94b2fc6c8c87dc63917aee908b69c621ea533d55a5fccb58f2c54a9bf5eec5dacbda6615e1c66a112d140227e5dc502752da580d179278e726e5cf80f60", + "resolution": { + "resolution": "@tailwindcss/vite@npm:4.3.0", + "version": "4.3.0", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + } + }, "@tanstack/history@npm:1.161.6": { "checksum": "560eb61e17560c83c94aa2b7eda2674eef96b5f9d6a21080a07bfb387deb07fe18a532f9bd8a1972c269958eb517f6dea26f87634359b83cbf940964e52bfa40", "resolution": { @@ -5242,6 +6735,13 @@ "version": "1.161.6" } }, + "@tanstack/history@npm:1.162.0": { + "checksum": "b1e161a7aa59555a1850db457a60d413e944a5d9162e9862ae8a339f2201424ff3c0e6e5eb5ad718c4b35d9288f8ba716e0925a5dc49ecbd24ef6df9d3d357db", + "resolution": { + "resolution": "@tanstack/history@npm:1.162.0", + "version": "1.162.0" + } + }, "@tanstack/query-core@npm:5.90.2": { "checksum": "0cac18ed941ec38d65bd4ac81c111b47fbe60cab1344610c568e238dec0b1dedfe878617993c10d2a2b57c794fa163133b873667cf23f514cad170379161684a", "resolution": { @@ -5279,6 +6779,23 @@ } } }, + "@tanstack/react-router@npm:^1.169.1": { + "checksum": "c160c9362c26e8ba072ed332dcf5f6cd926d421adadef73b01bb35dd31a562ebdfdf1abf4ab8ea0b01f0fa76017f88df5723b27efd3e178e8396ad9e03152354", + "resolution": { + "resolution": "@tanstack/react-router@npm:1.170.4", + "version": "1.170.4", + "dependencies": { + "@tanstack/history": "1.162.0", + "@tanstack/react-store": "^0.9.3", + "@tanstack/router-core": "1.171.2", + "isbot": "^5.1.22" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + } + }, "@tanstack/react-store@npm:^0.9.3": { "checksum": "75d4ac3e90aca95e1444cf6e851830dbd354129284439726e22c599fc3ccb18f5c12364419551e231afead0a17cf030a9357c5717335f5c0c021630e1ced1a17", "resolution": { @@ -5307,6 +6824,19 @@ } } }, + "@tanstack/router-core@npm:1.171.2": { + "checksum": "a0b4bf226da8e41e7985e222f750b8d4945a24a35b002a0fe59c61e35e58c42b6f1b5be6f7733726d79813d5ea8967c383fc1c7d0f74df2f2f86373140a457ce", + "resolution": { + "resolution": "@tanstack/router-core@npm:1.171.2", + "version": "1.171.2", + "dependencies": { + "@tanstack/history": "1.162.0", + "cookie-es": "^3.0.0", + "seroval": "^1.5.4", + "seroval-plugins": "^1.5.4" + } + } + }, "@tanstack/store@npm:0.9.3": { "checksum": "94fbd9da29ef9847b576f1b0d6940b334ba10d9f8be79e8b721ce24a41e557a43fd4b21aeed975cf4200f4510cac6583a34573c0aae50d987fcc91cebb7992de", "resolution": { @@ -5314,6 +6844,13 @@ "version": "0.9.3" } }, + "@tootallnate/quickjs-emscripten@npm:^0.23.0": { + "checksum": "dccf0294df79f181b1befe7bf3854ba230d4ec4d0286d028757f6bc8a8a85fabfd093e7e97d2f8097e1a63ab4b9fc6d3dc1a074ec8ca3708a8eedba51b5eeca0", + "resolution": { + "resolution": "@tootallnate/quickjs-emscripten@npm:0.23.0", + "version": "0.23.0" + } + }, "@tufjs/canonical-json@npm:2.0.0": { "checksum": "ac1e5b4c916ffb4bf1b891ecb320e8e866e2e5e404e6cc21f9f5987a1a4242ee43cd092e6d7d8deccd91c480b8fd7ffd049ee64ebe8d3e7bdf04c9153826946c", "resolution": { @@ -5425,13 +6962,79 @@ } } }, - "@types/d3-array@npm:^3.0.3": { + "@types/d3@npm:^7.4.3": { + "checksum": "146ec398a38d8dc4b8128ffb9afdfd72a837a7dd7564c2f6aaf8ff60b0ae9b535e5a06a05b3405dd4ef8d30cd4b7a436987a26eb89ee8cf08c33720840fb7fc0", + "resolution": { + "resolution": "@types/d3@npm:7.4.3", + "version": "7.4.3", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + } + }, + "@types/d3-array@npm:*, @types/d3-array@npm:^3.0.3": { "checksum": "62cebfd02fcbf77e25d9050f28715db49513fac2d0470708163d44ee60c4a08c026a14beca423ff6fc9a45bb7492b6ec811d9e00f67b3e2306c933440238a59e", "resolution": { "resolution": "@types/d3-array@npm:3.2.2", "version": "3.2.2" } }, + "@types/d3-axis@npm:*": { + "checksum": "97aac6e032cd5e50681626cd9344e7ee03e00ca8daca52d0393e03e0b7c1177d4730cceb83d28372d6072967f26f683cdc91df7856051a28ad380e8aafcc2734", + "resolution": { + "resolution": "@types/d3-axis@npm:3.0.6", + "version": "3.0.6", + "dependencies": { + "@types/d3-selection": "*" + } + } + }, + "@types/d3-brush@npm:*": { + "checksum": "4fc5bd78fdc831e3629157f42c8c3881b5e72ae250278456ab18b21f05ad4ca2540d846db442205ada8182b3de5a1364f0ec61f4d6144f35225731285a831086", + "resolution": { + "resolution": "@types/d3-brush@npm:3.0.6", + "version": "3.0.6", + "dependencies": { + "@types/d3-selection": "*" + } + } + }, + "@types/d3-chord@npm:*": { + "checksum": "988115f353918871e69f517affe73026bfcce993cb659ce84651fdb6ccb831cf04647b92a27435f42667b5493075bbfa10683bd890ed2da0134a1fcbe0cad2da", + "resolution": { + "resolution": "@types/d3-chord@npm:3.0.6", + "version": "3.0.6" + } + }, "@types/d3-color@npm:*": { "checksum": "10d525fa8ffe2fd065d735c7a4e35629659a74d6c7bcba2cc17fcf710e70d5de252ad093713e35b46b4a1552f5c59a7de769dcaf00539b2ea5c3d16843e39c77", "resolution": { @@ -5439,14 +7042,97 @@ "version": "3.1.3" } }, - "@types/d3-ease@npm:^3.0.0": { + "@types/d3-contour@npm:*": { + "checksum": "da32c14c3f2638e44e0088acd116f17aecbdbf0d6c580049dd0c41e7a34539c9564010e57d368843a406af36872ab4df3f53f253141ac248a9cc62ddc111e32d", + "resolution": { + "resolution": "@types/d3-contour@npm:3.0.6", + "version": "3.0.6", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + } + }, + "@types/d3-delaunay@npm:*": { + "checksum": "df71d23f905b410a4283ccb9b7e2481abeb772283bfd33b63f61259b2aa8bed608b65bdea5516da6222c4de1e26abc5165336332dfcd8296031de7ef73311ee3", + "resolution": { + "resolution": "@types/d3-delaunay@npm:6.0.4", + "version": "6.0.4" + } + }, + "@types/d3-dispatch@npm:*": { + "checksum": "82fba3fba7ca2f96c8ea73eb96fb3ad965f9a5fc847c99d6d05d91c34ce69d98b779877bf02dd781331558e3aab1b494dfb158aa79e4c066f568199d2a564fd8", + "resolution": { + "resolution": "@types/d3-dispatch@npm:3.0.7", + "version": "3.0.7" + } + }, + "@types/d3-drag@npm:*": { + "checksum": "d2efdb8ba9eade4a7445b215ad5afbbe8fbbae106366255c1a2734755760f87ea7afe91d762a92b231bde054a83fa7797b5c35559bdb5da643330b826b6bdd47", + "resolution": { + "resolution": "@types/d3-drag@npm:3.0.7", + "version": "3.0.7", + "dependencies": { + "@types/d3-selection": "*" + } + } + }, + "@types/d3-dsv@npm:*": { + "checksum": "677a85cd8d332c43f50f324aacc94a5e213db2317e2f34aa1e641092d3bd088e7d3d149f422a0ed04490fabdd5accbaa6ab6e794adcedbfe8fbac37cb46910ec", + "resolution": { + "resolution": "@types/d3-dsv@npm:3.0.7", + "version": "3.0.7" + } + }, + "@types/d3-ease@npm:*, @types/d3-ease@npm:^3.0.0": { "checksum": "c22f4e85704c429bce900faa0ec30495cb1e1aa609504c629836b15f01df982965e69c9d670b3cffe7c5b6f04f7a9632d0d742c5cf77f71be2797b2a380045f2", "resolution": { "resolution": "@types/d3-ease@npm:3.0.2", "version": "3.0.2" } }, - "@types/d3-interpolate@npm:^3.0.1": { + "@types/d3-fetch@npm:*": { + "checksum": "227359f257e1702cb30070afc31a851e5d8f23c81c273e751b32ab40d607575d11003dde6b61d273b3bb69b4ac2c42c143e80e8e2799deb772a3ef6f226c971a", + "resolution": { + "resolution": "@types/d3-fetch@npm:3.0.7", + "version": "3.0.7", + "dependencies": { + "@types/d3-dsv": "*" + } + } + }, + "@types/d3-force@npm:*": { + "checksum": "7c1a4bdf0b758ff6d285f8a0c7a5ad7b0b6355b63d6752549bee8dc3ec7e3f1053eedd52a2db4f6280fc37b510b7ffe8dc9dcab9aac4b4c924593450438626ca", + "resolution": { + "resolution": "@types/d3-force@npm:3.0.10", + "version": "3.0.10" + } + }, + "@types/d3-format@npm:*": { + "checksum": "38d626251c203a05088541e92f2bfa0226f3a5a3160fd1b9e0a02afbfa0c4dea0705f13b6bdf904b4dc1b51f3f795de440c033ff7b696f165748de9ab64f7e9d", + "resolution": { + "resolution": "@types/d3-format@npm:3.0.4", + "version": "3.0.4" + } + }, + "@types/d3-geo@npm:*": { + "checksum": "a090736802e9e8f7c9ed817389d8d77a6c91395dcb180600901fb098790199854bb6431616c172ac618df21a0ba5d995c2694d3004757399e0a96dbec6b0dfc0", + "resolution": { + "resolution": "@types/d3-geo@npm:3.1.0", + "version": "3.1.0", + "dependencies": { + "@types/geojson": "*" + } + } + }, + "@types/d3-hierarchy@npm:*": { + "checksum": "2e5d9a7446a72ac1b2e8fac15378e3e78484147dfcf0a5e895c33a4f5bd7182a557146de74006e592a81f2c8f2481b837ba3424f08890e08dfa4383ce290878b", + "resolution": { + "resolution": "@types/d3-hierarchy@npm:3.1.7", + "version": "3.1.7" + } + }, + "@types/d3-interpolate@npm:*, @types/d3-interpolate@npm:^3.0.1": { "checksum": "0eff69578f98d123004f66b1d9c322c5c700a9079a7032839d8e36d5e806a215c3868c959ada1d94097d8c1468933c8fffff11b7f3e059b373731fa755549377", "resolution": { "resolution": "@types/d3-interpolate@npm:3.0.4", @@ -5463,7 +7149,28 @@ "version": "3.1.1" } }, - "@types/d3-scale@npm:^4.0.2": { + "@types/d3-polygon@npm:*": { + "checksum": "d2d9665b5a7b15aca3d56e89c7c33f9055f2c45d86d56231b6babe62f35652216ed80a4b503fb1196fbe872aff2862c9ebc8935722dc901b7ce1ef56f99f1ea9", + "resolution": { + "resolution": "@types/d3-polygon@npm:3.0.2", + "version": "3.0.2" + } + }, + "@types/d3-quadtree@npm:*": { + "checksum": "5f44c1efad2c0bc638eda2fc0314db79868a31cb51cd4776842204e5003f17652d265ff8e0ef970e00f0be42c8ce2a9c7b518a26ad0066fc6636840b91a6c74a", + "resolution": { + "resolution": "@types/d3-quadtree@npm:3.0.6", + "version": "3.0.6" + } + }, + "@types/d3-random@npm:*": { + "checksum": "5b280bb5727b8673c7aec7b395d884756815c0a05e6a7e0c414b30b387654bfa318ffb139a90aca39a803012b27f967fbaee33566977cba08e5afc8b60229a18", + "resolution": { + "resolution": "@types/d3-random@npm:3.0.3", + "version": "3.0.3" + } + }, + "@types/d3-scale@npm:*, @types/d3-scale@npm:^4.0.2": { "checksum": "4ef6ca1a1f09265a5ab7cfd60c2eaefc3faa954ab60319292adb28407a44c02e1d98013065744725fb4bf1ffac6e999c90f9227de5ba16e2de90481b7720789b", "resolution": { "resolution": "@types/d3-scale@npm:4.0.9", @@ -5473,6 +7180,30 @@ } } }, + "@types/d3-scale-chromatic@npm:*": { + "checksum": "c14b22cd91fd66b30fe80a556b0e11de6087a1d746b477ebec7a2063fc480b854ed9756b9c267b77c375f45974c64ad4eeacc06184171b4494e98450c9f64fbc", + "resolution": { + "resolution": "@types/d3-scale-chromatic@npm:3.1.0", + "version": "3.1.0" + } + }, + "@types/d3-selection@npm:*": { + "checksum": "f4c4cf7db058e6eda09456b024f45c8bf00559a1eb523f1e1ac0866a75d916f47e09df7aa4c81bd96f2a3b06e3f161d2be1dd8f2679c1d388c0a87a62e038fd9", + "resolution": { + "resolution": "@types/d3-selection@npm:3.0.11", + "version": "3.0.11" + } + }, + "@types/d3-shape@npm:*": { + "checksum": "95d821a28d94ff4851a5bc74d0b742ae196ef1a3088ea5d8962cefc190ea30d36445f251d57fe1d71512e4ee77d266be61cbdd6085e15f243c0cf1fd78c34ee5", + "resolution": { + "resolution": "@types/d3-shape@npm:3.1.8", + "version": "3.1.8", + "dependencies": { + "@types/d3-path": "*" + } + } + }, "@types/d3-shape@npm:^3.1.0": { "checksum": "df37f314c35bf5a5f927b7638a175144c5bf9a21a3fbefb92f5963eb55a6fa8a8404184489085073e90f23cec727584a8d64f37f1b731973af3bac1d76bd4930", "resolution": { @@ -5490,13 +7221,41 @@ "version": "3.0.4" } }, - "@types/d3-timer@npm:^3.0.0": { + "@types/d3-time-format@npm:*": { + "checksum": "7a910f32a34f8e3fe908b6caca677e021ba57ca0cbd0b4f821d166ce6e5391bb77ead9bb38b1dcccbff2c825424aeb2e34ba2b0b0fd0ceb027df3f1254158397", + "resolution": { + "resolution": "@types/d3-time-format@npm:4.0.3", + "version": "4.0.3" + } + }, + "@types/d3-timer@npm:*, @types/d3-timer@npm:^3.0.0": { "checksum": "f12cc65c36d43e1e958809518e7321fcd72bd7164f30fc8d75b66758dcdf8a440b6b58483cea7a92e4dd09eb4309fac4b9f776292d285b1be036a2ac38c21af7", "resolution": { "resolution": "@types/d3-timer@npm:3.0.2", "version": "3.0.2" } }, + "@types/d3-transition@npm:*": { + "checksum": "b8e8fd3ebd1c0b27c9dcabc9aa04b68f3862c78c17258a7a2735bc811ce8b436af93cbe4f2584050dbe0ed72241ccd78c46853488e960682661733ea0d69dea9", + "resolution": { + "resolution": "@types/d3-transition@npm:3.0.9", + "version": "3.0.9", + "dependencies": { + "@types/d3-selection": "*" + } + } + }, + "@types/d3-zoom@npm:*": { + "checksum": "f30148e8cc598d97a608e05ee29614ab0a240efbd8261de8f4f042efedfbda534a5453c9585a36316fd72dc611b4930070c3b92f4141bb759969f66e008d3863", + "resolution": { + "resolution": "@types/d3-zoom@npm:3.0.8", + "version": "3.0.8", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + } + }, "@types/debug@npm:^4.0.0": { "checksum": "2e5fe66fae7b85721074fed9cd37a2c5ea64a7d35730f4800bb3a84e5c666ba8b28d6a6009c7a41956435f3cf793fde97b303450d23c7c61f085eed6794d11d7", "resolution": { @@ -5578,6 +7337,13 @@ } } }, + "@types/geojson@npm:*": { + "checksum": "ee34763a26a5282aeea50c70a993e7472b0660cf1eb3c6be837407228a798160a05fce2115a512930b6317c6649e28f17e6b197fbc2b28c836725d881b588316", + "resolution": { + "resolution": "@types/geojson@npm:7946.0.16", + "version": "7946.0.16" + } + }, "@types/google.maps@npm:^3.55.12": { "checksum": "d51098783498d0b38dca6759f564f89184a522faac346b44a70f6d27e2958577da8f02b5a3c5bf0fe5838fd0bfd2ac7d84fdf57f6e5338854d2fc4da5ed11953", "resolution": { @@ -5810,7 +7576,7 @@ } } }, - "@types/react@npm:^19.2.2": { + "@types/react@npm:^19.2.14, @types/react@npm:^19.2.2": { "checksum": "1de55235d52f182bf5e4231bbe208d625f2d628b7d1d1733f547103558e1f8b5bfca2e6f3cd8d82e5c18284cef94898c23dc05d914d8ff02b640e654e7523c8f", "resolution": { "resolution": "@types/react@npm:19.2.14", @@ -5820,7 +7586,7 @@ } } }, - "@types/react-dom@npm:^19.2.2": { + "@types/react-dom@npm:^19.2.2, @types/react-dom@npm:^19.2.3": { "checksum": "2afc39f57726bc1d18c8f720d0deb508ad6dda4d5d1ad6385414f06435b7afd3e5f3d24cf6b9457e3b8d87b7728d577cdbcc1fb57ed8bd04d8e7f7eb31c503dd", "resolution": { "resolution": "@types/react-dom@npm:19.2.3", @@ -6023,6 +7789,16 @@ "version": "21.0.3" } }, + "@types/yauzl@npm:^2.9.1": { + "checksum": "9e64beb8f1359e08a7c1e1d68f41365fd0b0cf684d503fb59464969c68d6ef251215d6df319135f86219e29124b119d51c705d8a9ba43fd3700e400d5d38869a", + "resolution": { + "resolution": "@types/yauzl@npm:2.10.3", + "version": "2.10.3", + "dependencies": { + "@types/node": "*" + } + } + }, "@types/yoga-layout@npm:1.9.2": { "checksum": "0a8812e0e608b27fb6ce5b720291570b0d1b6649294de83e112e90813620a785776cc76b621e2555bbc135d46dd4ee5410858e9568be46db254b0be38b68f1ad", "resolution": { @@ -6189,6 +7965,21 @@ "version": "1.3.0" } }, + "@upsetjs/venn.js@npm:^2.0.0": { + "checksum": "433f88a3424ba92005d3e1fbeead14eddc421c37d3520762812582e98d63cfb6974e1e07fce92ab692e59fbdcef88831e68a181eda4bf2ac389110bfbd8abac4", + "resolution": { + "resolution": "@upsetjs/venn.js@npm:2.0.0", + "version": "2.0.0", + "dependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + }, + "optionalDependencies": [ + "d3-selection", + "d3-transition" + ] + } + }, "@vercel/oidc@npm:^3.0.1": { "checksum": "d14e0a004be95e2e8755d49ff56e34093734fef3641e580b5cf74acaff1ac3a09ca76a126fae508f8dccc8e599c411fa2edfd44b802d5a00c7a16b3b8ddcf35e", "resolution": { @@ -6196,7 +7987,7 @@ "version": "3.0.1" } }, - "@vitejs/plugin-react@npm:^5.1.0": { + "@vitejs/plugin-react@npm:^5.1.0, @vitejs/plugin-react@npm:^5.2.0": { "checksum": "9bc95a013cf42fa61b5d69fd8c70afa35715bbdb62b4a04ebf87b20c00c41867e088c257f73bbfbc2593d3b9d5e798684740f054d44051189e14459b9613fbaf", "resolution": { "resolution": "@vitejs/plugin-react@npm:5.2.0", @@ -7351,6 +9142,29 @@ } } }, + "algoliasearch@npm:^5.52.0": { + "checksum": "3398ae9604e532c976aa18951c481e3ec50e26e07eb211f0c5f82fe8105c1093ecaa7ca3b4f06e352ef477e1aa890f6668c4f501211f119e73f1c935166d7b81", + "resolution": { + "resolution": "algoliasearch@npm:5.52.1", + "version": "5.52.1", + "dependencies": { + "@algolia/abtesting": "1.18.1", + "@algolia/client-abtesting": "5.52.1", + "@algolia/client-analytics": "5.52.1", + "@algolia/client-common": "5.52.1", + "@algolia/client-insights": "5.52.1", + "@algolia/client-personalization": "5.52.1", + "@algolia/client-query-suggestions": "5.52.1", + "@algolia/client-search": "5.52.1", + "@algolia/ingestion": "1.52.1", + "@algolia/monitoring": "1.52.1", + "@algolia/recommend": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + } + } + }, "algoliasearch-helper@npm:3.26.0": { "checksum": "09d2fe196dbd394b55ab71bf91bf8a0e98672f292eb90de54c43c66134ecf54c858104557acb6f5d3080c7a30790a84f3c1f326dfe1a78744d7a0c4075d30d35", "resolution": { @@ -7591,6 +9405,16 @@ } } }, + "ast-types@npm:^0.13.4": { + "checksum": "4b422f01c2f157367eaf633d3ac19185b6bb17a8dde50fc23755797ee5116aef7693b5d3f272a33302c257711a26c1604e465b5610123cfdee38ea73ac46e4bb", + "resolution": { + "resolution": "ast-types@npm:0.13.4", + "version": "0.13.4", + "dependencies": { + "tslib": "^2.0.1" + } + } + }, "astral-regex@npm:^2.0.0": { "checksum": "5229fd46f2db09837265dc1c9d17c76d385b272353d38e4c4dd9528c5d3975459d6c086d5de549fc699c1b3c9d7208dd9108a07885751c449d3cf99d43ecad7d", "resolution": { @@ -7680,6 +9504,82 @@ ] } }, + "astro@npm:^5.9.3": { + "checksum": "42b3a4d7df5ee1eac373fbceae40383af65c731e04e2439b89d6b9e7f1ce2858e5e1101d12b06db45d4452bcbe2305608516912b69b111831b3797379e14e722", + "resolution": { + "resolution": "astro@npm:5.18.1", + "version": "5.18.1", + "dependencies": { + "@astrojs/compiler": "^2.13.0", + "@astrojs/internal-helpers": "0.7.6", + "@astrojs/markdown-remark": "6.3.11", + "@astrojs/telemetry": "3.3.0", + "@capsizecss/unpack": "^4.0.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.3.0", + "acorn": "^8.15.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "boxen": "8.0.1", + "ci-info": "^4.3.1", + "clsx": "^2.1.1", + "common-ancestor-path": "^1.0.1", + "cookie": "^1.1.1", + "cssesc": "^3.0.0", + "debug": "^4.4.3", + "deterministic-object-hash": "^2.0.2", + "devalue": "^5.6.2", + "diff": "^8.0.3", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "es-module-lexer": "^1.7.0", + "esbuild": "^0.27.3", + "estree-walker": "^3.0.3", + "flattie": "^1.1.1", + "fontace": "~0.4.0", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "import-meta-resolve": "^4.2.0", + "js-yaml": "^4.1.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.1", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "p-limit": "^6.2.0", + "p-queue": "^8.1.1", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.3", + "prompts": "^2.4.2", + "rehype": "^13.0.2", + "semver": "^7.7.3", + "sharp": "^0.34.0", + "shiki": "^3.21.0", + "smol-toml": "^1.6.0", + "svgo": "^4.0.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tsconfck": "^3.1.6", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.3", + "unist-util-visit": "^5.0.0", + "unstorage": "^1.17.4", + "vfile": "^6.0.3", + "vite": "^6.4.1", + "vitefu": "^1.1.1", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^21.1.1", + "yocto-spinner": "^0.2.3", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1", + "zod-to-ts": "^1.2.0" + }, + "optionalDependencies": [ + "sharp" + ] + } + }, "astro-expressive-code@npm:^0.41.1": { "checksum": "59c065f58d9acd43ffc1e266a66ac10b0c2c4fd535f16be7ff46328b74c52943afc868021ad99dc0b26536f1e8cf0bf853ddacb5d1efeeb3014ba584081d40ee", "resolution": { @@ -7941,6 +9841,13 @@ "version": "2.8.12" } }, + "basic-ftp@npm:^5.0.2": { + "checksum": "8fdb5fcd7d7d190915111480f8d0c600bb2d47841b6cc7f4444a0af4af5cf2ecda5896b1c84462e566f92d2e15ce8f1cc638e6b9af1e06e3ba4f78f17c0a88f4", + "resolution": { + "resolution": "basic-ftp@npm:5.3.1", + "version": "5.3.1" + } + }, "bcp-47@npm:^2.1.0": { "checksum": "de01f014e73e68b967569450f6c3dfd58ed3a6ef513a7ab0cdc20abd684ec5a8897600ffaecee755711200ccab31a0a8062919b0b521d095fa3447f8026378c1", "resolution": { @@ -8122,6 +10029,13 @@ "version": "1.1.0" } }, + "buffer-crc32@npm:~0.2.3": { + "checksum": "d89b84a216fa05c03d6e5ae08f95e99860a16649a029aaee438974c52d21e0eaf470b160c58b0fe2eb5ec33549fc22c8fcec13081ab608f9d963264d5bc1355b", + "resolution": { + "resolution": "buffer-crc32@npm:0.2.13", + "version": "0.2.13" + } + }, "buffer-fill@npm:^1.0.0": { "checksum": "95a1bfd16762b37c8252911979df37ac884fec5350406ddf16eb6e4c6aa1ba246dce9a30ffc61a83d46f862bb6b32915b82311f0cafb7b9e71d3f15d2f140fc3", "resolution": { @@ -8347,6 +10261,16 @@ } } }, + "chokidar@npm:^5.0.0": { + "checksum": "01cddcdfb4e57460091be40e62c3d14f6fcec23f08f7d4c64f0ea4b897fd3d0e6ab3952f23bae5daf55e2e529c74f99fe78910be60707b1fde93bcd9fb33a29b", + "resolution": { + "resolution": "chokidar@npm:5.0.0", + "version": "5.0.0", + "dependencies": { + "readdirp": "^5.0.0" + } + } + }, "chownr@npm:^1.0.1, chownr@npm:^1.1.1": { "checksum": "b36f18706e8079af4abde5028dde3c499a6f5d5053e310e5e908df965f07f51c24e9289f5300902e3cd3323963a2ee257cc863363eabf923b67182bfd3d956c5", "resolution": { @@ -8368,6 +10292,20 @@ "version": "3.0.0" } }, + "chromium-bidi@npm:14.0.0": { + "checksum": "cae5fd00b4075b9fe54e23472b3270cf9143bc92d0efb1deeb80a9a069598f7f79513b867beeb83032af957e488f793b35dd104cefe38a6ddc8f97fa8bc03c65", + "resolution": { + "resolution": "chromium-bidi@npm:14.0.0", + "version": "14.0.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + } + }, "ci-info@npm:^2.0.0": { "checksum": "f17aba757d9250bc0bd37b51307f4aa2a8c1fd6df1e0e2d5904cfe4660d07df45e5fceb7c4979dfa13afc1bfbc7d3fd026dd9815d872555a929f4f346d155011", "resolution": { @@ -8389,6 +10327,13 @@ "version": "4.3.1" } }, + "ci-info@npm:^4.3.1": { + "checksum": "87388db3c9180716e39ff3cb0240cdae74501ecfe7f501c7b6b7474edd50cb04435ef67652b549ba03aa95bda7cafadc794aca7c5e284d3e66441bf8a0fc95a0", + "resolution": { + "resolution": "ci-info@npm:4.4.0", + "version": "4.4.0" + } + }, "cjs-module-lexer@npm:^1.0.0": { "checksum": "7e83265a3197352eb10cc565b454ad09f06a4c8178b1aa6d470a9ab58b722edc0ddf33ce3ada165ac2238591ecdac413e05f39792dbdb3cd0b4599d27f9de641", "resolution": { @@ -8564,13 +10509,34 @@ "version": "2.0.3" } }, - "commander@npm:7.2.0": { + "commander@npm:2": { + "checksum": "edd96d1bb09763fff58198b16983cef56a4541e92d7dbefa76e2db84bb554af19a4d0262284cda3a47abe1860bf2ec069a88d9cecb5223a4ed203aa10e6755d3", + "resolution": { + "resolution": "commander@npm:2.20.3", + "version": "2.20.3" + } + }, + "commander@npm:7, commander@npm:7.2.0": { "checksum": "771e43585a19639bba83c00a573abb39ba38ce519210d95a1440b11a426e54cc9697421147d7835250bdbe5cd26d12b76358bc35ebec32e9545afed859692319", "resolution": { "resolution": "commander@npm:7.2.0", "version": "7.2.0" } }, + "commander@npm:^11.1.0": { + "checksum": "0643370fb68bae45bac5143c313616202e4e43f1d1aa42541614176bdfea111909c683b5a78c207f8a788521dcd89d9ff31f291052865087b602ed3738e62d9f", + "resolution": { + "resolution": "commander@npm:11.1.0", + "version": "11.1.0" + } + }, + "commander@npm:^8.3.0": { + "checksum": "36f5920bd3bef577f3e2af3b64261a98df2c154ff3fe8c697a3eaa58b9665b0e3b570ec0c72afd71c5174310af29e236b97c1bb42eb6822a12df210a310c47d8", + "resolution": { + "resolution": "commander@npm:8.3.0", + "version": "8.3.0" + } + }, "comment-json@npm:^2.2.0": { "checksum": "830ad7b422982257af7ac4258b4ef471b1aa81f0d3d43e4c00f397594fe62cd045b80d0b8b128a7230ea31cf11165729bfaa0cedb4027f702dcbb250aecfa184", "resolution": { @@ -8619,6 +10585,13 @@ "version": "1.0.2" } }, + "cookie@npm:^1.1.1": { + "checksum": "e5bc26821e3032e5d98cf2cfdf834c6e1e6a74ff6fcbdb2807f96815101aa77124902f7fe6467aeabe9cfe0bc0f998d207d51d5c80edc8e222f4c14c3ccd17cb", + "resolution": { + "resolution": "cookie@npm:1.1.1", + "version": "1.1.1" + } + }, "cookie-es@npm:^1.2.2": { "checksum": "6be66f1a51e99c36d5b9fb789b2307c64e9939302f22e35732acf9a3f43cc29ea48517a0eef7b3f112cf28d3dd347789e5bd66d57567f1ef7f6b2c686ae11e83", "resolution": { @@ -8626,6 +10599,13 @@ "version": "1.2.2" } }, + "cookie-es@npm:^1.2.3": { + "checksum": "20923e85708cabeaaa971649fd8a4a14f47d9007f38d999960cf942b05646896af9ef051e82feba81488b0bff8bd7b75c6f5cbedd9ce78d5e339489d65584583", + "resolution": { + "resolution": "cookie-es@npm:1.2.3", + "version": "1.2.3" + } + }, "cookie-es@npm:^3.0.0": { "checksum": "3fc160112f31ba8137542728afe401074ee1ad528d1bb338acc6f1fbe690932369cc86150911845996a1c49db582cb6f9e9fe9bf83503e17bb320dba1c711897", "resolution": { @@ -8640,6 +10620,26 @@ "version": "1.0.3" } }, + "cose-base@npm:^1.0.0": { + "checksum": "04aea582b312c46b14ffe43d88f428b5e8d4969cadb8831807bc06443425b10d645fd4178fd6dfae47142e8716c47b1f8c6b8ee38a677106613820fd4d3b6b6e", + "resolution": { + "resolution": "cose-base@npm:1.0.3", + "version": "1.0.3", + "dependencies": { + "layout-base": "^1.0.0" + } + } + }, + "cose-base@npm:^2.2.0": { + "checksum": "8791c1693a887bfc031ac6c487eabdbfbe33a466684c6820210664014f84fc99c77c729a327da3d941b8c85550f408195b6eb7d9de894436ca021446f13ac7ba", + "resolution": { + "resolution": "cose-base@npm:2.2.0", + "version": "2.2.0", + "dependencies": { + "layout-base": "^2.0.0" + } + } + }, "cosmiconfig@npm:^8.1.3": { "checksum": "c4d1dc231731e803a6822a1f4651ddb34aec4d35e241b5c373349f2b3e617fd2b9b426e978edd47a88351fb147452d7fa356c542857ac683cc403b779eb2b483", "resolution": { @@ -8659,6 +10659,25 @@ ] } }, + "cosmiconfig@npm:^9.0.0": { + "checksum": "2bf8031d0b38f78a17dd58ee3c8d015f3ffe90575ad75b6650fa1eae9e1f201ef287e86a28e8dfdd0ac0d4be3d48e684463dbf375c87ee3d1b15b087e7f2f7d9", + "resolution": { + "resolution": "cosmiconfig@npm:9.0.1", + "version": "9.0.1", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "optionalPeerDependencies": [ + "typescript" + ] + } + }, "create-jest@npm:^29.7.0": { "checksum": "1199089fa0fb502e2f3c1492862da4c0e9fb2f4bce6c504ae4bb9a4bd64dbc7fbb0e0f9f6f2b73054f7d8c4347b0edbb0fdd93a7b0398836e6674122ffd834c6", "resolution": { @@ -8746,75 +10765,347 @@ } } }, - "css-what@npm:^6.1.0": { - "checksum": "484f9f75cfbf65ceca573f063f1507e9ec02d6304981263d83025913d8b67f4a089b511ffcb28c90aa28dda32e59411090a82ef8e95ba8c3436812843599cdd2", + "css-tree@npm:^3.0.1, css-tree@npm:^3.1.0": { + "checksum": "d8ce918bcc5e4891ef30d69c60cf6ea92189601ac3043e88ca7dfba192b16a654df9c7c798a0f1cbded114df6fb02d003e3b98f8e86c9179b9b1ddaadc40cb93", + "resolution": { + "resolution": "css-tree@npm:3.2.1", + "version": "3.2.1", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + } + } + }, + "css-tree@npm:~2.2.0": { + "checksum": "02b718362d35bb9a7c6c69f36761c46652d4249b2d35d6baa9827020bc6a73425f11ac0d4e7f70dda2de2531f0ef41051e2a76be23d075076f8b5535d14c331c", + "resolution": { + "resolution": "css-tree@npm:2.2.1", + "version": "2.2.1", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + } + } + }, + "css-what@npm:^6.1.0": { + "checksum": "484f9f75cfbf65ceca573f063f1507e9ec02d6304981263d83025913d8b67f4a089b511ffcb28c90aa28dda32e59411090a82ef8e95ba8c3436812843599cdd2", + "resolution": { + "resolution": "css-what@npm:6.2.2", + "version": "6.2.2" + } + }, + "cssesc@npm:^3.0.0": { + "checksum": "12ac589d5559f5edb422cd721f693c725b1667e11e545024ac9ffc2bef1a0c45e4234428a88b213bff3d54794b18fc6dbfdbe5f2e898d69b1b901219f966a89f", + "resolution": { + "resolution": "cssesc@npm:3.0.0", + "version": "3.0.0" + } + }, + "csso@npm:^5.0.5": { + "checksum": "c236e072aacc8fb38888b395b5bb689c61120f68af0d81ed3257e6b40c73daa596c605b14c019d59a034cb23169480db4438ddba40a23424c852267fe746447f", + "resolution": { + "resolution": "csso@npm:5.0.5", + "version": "5.0.5", + "dependencies": { + "css-tree": "~2.2.0" + } + } + }, + "csstype@npm:^3.0.2": { + "checksum": "94e163c6127729a3a0c2be7881abe86b388c8e81cec405d128a0ac7c1fd02cb741c1554649ccd7f156bd1e1a3eab1d5a0bbcc2dfc59299787eadf9c8ff1e6d8e", + "resolution": { + "resolution": "csstype@npm:3.1.3", + "version": "3.1.3" + } + }, + "csstype@npm:^3.2.2": { + "checksum": "7ab13f89a707851e885ef163d1d8b38744bf41fbedb066c5af449c6ee0390e5de155cebad58de5672be80a54676ea64e8c145a44103e128c2fc1abf0b3016384", + "resolution": { + "resolution": "csstype@npm:3.2.3", + "version": "3.2.3" + } + }, + "cytoscape@npm:^3.33.1": { + "checksum": "041bc6dfa7db816c7016ae8298ca45e28fe1c910075fef335455d479960f283ea219e1d7f7cdb27d0a74a94d98d70acce0017d34fc4759428fbcbd00fc1a397f", + "resolution": { + "resolution": "cytoscape@npm:3.33.3", + "version": "3.33.3" + } + }, + "cytoscape-cose-bilkent@npm:^4.1.0": { + "checksum": "9893120292721acca45fb552555f23ff849893bb94f7306f21c66ad6573da84e8ae74091341532e2a7f2786f527a92fa6bf8b293aa988214f9150debcae3fab8", + "resolution": { + "resolution": "cytoscape-cose-bilkent@npm:4.1.0", + "version": "4.1.0", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + } + }, + "cytoscape-fcose@npm:^2.2.0": { + "checksum": "f9124afde64d702b487a1bafd7686e5e2c9289e2f0926a14e17fc7fd9e6deb56599c663bf316a390ae5d9a18767f89bffb5e80488d77d119f71a916ad3b4dfc9", + "resolution": { + "resolution": "cytoscape-fcose@npm:2.2.0", + "version": "2.2.0", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + } + }, + "d3@npm:^7.9.0": { + "checksum": "6a3a7fe6d3b98ab5ecdd7308c181540c3e8207c525b1f2b07810f0e8f64b2490631810d079292f560ce77d6b06ad570855209849fae4f4b4ffa28203ffdc7be6", + "resolution": { + "resolution": "d3@npm:7.9.0", + "version": "7.9.0", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + } + } + }, + "d3-array@npm:1 - 2": { + "checksum": "9880bb7aaf85815f7458a59b080ec43439018f4cd6ff7837fb7f78a6076b25f160e47de9adca6b899d2b1901f7566d5abff3403aedace3652c9ec836835c356a", + "resolution": { + "resolution": "d3-array@npm:1.2.4", + "version": "1.2.4" + } + }, + "d3-array@npm:2, d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:2.5.0 - 3": { + "checksum": "e83050247f184d2fc388d1ecb4bfc36c5caa980861b0c8d8a41bbacc2bef9eace4d235d461c9c3d056df3a249137c7b314e7ea16544e73066999a8701ecc0156", + "resolution": { + "resolution": "d3-array@npm:2.12.1", + "version": "2.12.1", + "dependencies": { + "internmap": "^1.0.0" + } + } + }, + "d3-array@npm:3, d3-array@npm:^3.1.6, d3-array@npm:^3.2.0": { + "checksum": "f7e171ab4ece1c931e2b8e016a0cbc8c201bf2d17cdd79a9540154f4e72c380c576e4297f299a040b7005a48783520370eb7faf24c3c24653a4ee3058ab8fbb1", + "resolution": { + "resolution": "d3-array@npm:3.2.4", + "version": "3.2.4", + "dependencies": { + "internmap": "1 - 2" + } + } + }, + "d3-axis@npm:3": { + "checksum": "30380491c8efce1e382393e01becdabe543c1e9522513c068ce0abeed2b2293375d1245067b8c9827adfb59fdb6ebb609a7651a79335e2f568e96e7308f017d2", + "resolution": { + "resolution": "d3-axis@npm:3.0.0", + "version": "3.0.0" + } + }, + "d3-brush@npm:3": { + "checksum": "a095e3628e4a1477ae4efdeddd69f245f903f8d242e19ae4ccba4bbb8e9d7c774bea00de27239d276bb96d170f89eefce2ddd26c401b4273e271697f0b9f137c", + "resolution": { + "resolution": "d3-brush@npm:3.0.0", + "version": "3.0.0", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + } + } + }, + "d3-chord@npm:3": { + "checksum": "da8369b700346800725cbf1c85777cbb393480b090df0c7b277f6124495be459748fc9c0eb5dff3809367565b7e33cccdf5f58f53e60e6fb5965946bd4114604", + "resolution": { + "resolution": "d3-chord@npm:3.0.1", + "version": "3.0.1", + "dependencies": { + "d3-path": "1 - 3" + } + } + }, + "d3-color@npm:1, d3-color@npm:1 - 2": { + "checksum": "d8383034c25361c09ef2e6bafe9e461f909cdaf1479a4ff721aeb7bf7bc93a67021ad666638e3e9771b7b0db0d3e4ab25f05cb1003a89f336115175a2fe0e3a8", + "resolution": { + "resolution": "d3-color@npm:1.4.1", + "version": "1.4.1" + } + }, + "d3-color@npm:1 - 3": { + "checksum": "e92a4f644097174b184088e8b41327b7975ecbf0b2a1e7b9fe7c111925f05f4b3d48da6167d592de6eb17de1b3f297417710dfb2f7854119ad073890b9b8f940", + "resolution": { + "resolution": "d3-color@npm:2.0.0", + "version": "2.0.0" + } + }, + "d3-color@npm:3": { + "checksum": "7176552af629fcc589e2d7b01b637e478bb4509b5be3abfb95ce018b6ec77d932181880e1e98fd7401113de2caada0f9967b358aacb27456b19e6e9821028567", + "resolution": { + "resolution": "d3-color@npm:3.1.0", + "version": "3.1.0" + } + }, + "d3-contour@npm:4": { + "checksum": "e4540f3fbe8a53196169b49b84fb6a257e1dabb14087b0e19ef3a05f5f5ceabf9b131d5d1b6d799d261a6d91171ae94c01b59fe499cea8c27b79fe80ebb5c9c6", + "resolution": { + "resolution": "d3-contour@npm:4.0.2", + "version": "4.0.2", + "dependencies": { + "d3-array": "^3.2.0" + } + } + }, + "d3-delaunay@npm:6": { + "checksum": "0d94494a4bf7e5a2f8405bcbdd95a5ec68dd72ddb80dc5f10914cc858044b11005ba7825a57b697d8d69f77c8b977f12bcccc730827ed3160a20e1bfb0bf2586", + "resolution": { + "resolution": "d3-delaunay@npm:6.0.4", + "version": "6.0.4", + "dependencies": { + "delaunator": "5" + } + } + }, + "d3-dispatch@npm:1 - 2": { + "checksum": "26b865ecdda84b360ca11a379f2b354048f5603a69350f9784450ad2d4bcb09704ffccac45a15d0b100754b232110878c31745b15f7119db38c09a1db9cadb9c", + "resolution": { + "resolution": "d3-dispatch@npm:1.0.6", + "version": "1.0.6" + } + }, + "d3-dispatch@npm:1 - 3": { + "checksum": "d840c07a7ef7a46c2308ae51e3c0092bfe00a24f808cbfdc0fbbafd8d818282d61eada960ae7f2076e326c9304826dd5ac9ac17c5e892237f66367e46aef1e1d", "resolution": { - "resolution": "css-what@npm:6.2.2", - "version": "6.2.2" + "resolution": "d3-dispatch@npm:2.0.0", + "version": "2.0.0" } }, - "cssesc@npm:^3.0.0": { - "checksum": "12ac589d5559f5edb422cd721f693c725b1667e11e545024ac9ffc2bef1a0c45e4234428a88b213bff3d54794b18fc6dbfdbe5f2e898d69b1b901219f966a89f", + "d3-dispatch@npm:3": { + "checksum": "49ece1d1f587354d34b8c51f52d929c14b8fdd2b4094f2ef466faa4a4e8e94afc38ffed437c07e3220a887f7da99e065d3970ab82cefc16fb54b2d7a8a32d6fd", "resolution": { - "resolution": "cssesc@npm:3.0.0", - "version": "3.0.0" + "resolution": "d3-dispatch@npm:3.0.1", + "version": "3.0.1" } }, - "csstype@npm:^3.0.2": { - "checksum": "94e163c6127729a3a0c2be7881abe86b388c8e81cec405d128a0ac7c1fd02cb741c1554649ccd7f156bd1e1a3eab1d5a0bbcc2dfc59299787eadf9c8ff1e6d8e", + "d3-drag@npm:2 - 3": { + "checksum": "51bc1e9f9808264afa025d7044ef26ed4346bd613f3b35b78c3b263fb471914b2783cc62719711887470bae03c068dbeb6262a5b0e30053e66ba4f4eff908eaf", "resolution": { - "resolution": "csstype@npm:3.1.3", - "version": "3.1.3" + "resolution": "d3-drag@npm:2.0.0", + "version": "2.0.0", + "dependencies": { + "d3-dispatch": "1 - 2", + "d3-selection": "2" + } } }, - "csstype@npm:^3.2.2": { - "checksum": "7ab13f89a707851e885ef163d1d8b38744bf41fbedb066c5af449c6ee0390e5de155cebad58de5672be80a54676ea64e8c145a44103e128c2fc1abf0b3016384", + "d3-drag@npm:3": { + "checksum": "3f9164d82f97fc14fd4f9f73c0cf70e7881ff6fc03a0b9f844710c818925429585a5c44ef115641b908df13314581e29b89aed8fef18ccffbc0f603f61ed1a55", "resolution": { - "resolution": "csstype@npm:3.2.3", - "version": "3.2.3" + "resolution": "d3-drag@npm:3.0.0", + "version": "3.0.0", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } } }, - "d3-array@npm:2, d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3": { - "checksum": "e83050247f184d2fc388d1ecb4bfc36c5caa980861b0c8d8a41bbacc2bef9eace4d235d461c9c3d056df3a249137c7b314e7ea16544e73066999a8701ecc0156", + "d3-dsv@npm:1 - 3": { + "checksum": "ff8fa5308bf743b407bc4d4c716117892cdb99d242f634603cf802cc27af97826d873c44a2bde45bda7a31c6271fd3209212cc98dd185aebb693a2f0f4493dd2", "resolution": { - "resolution": "d3-array@npm:2.12.1", - "version": "2.12.1", + "resolution": "d3-dsv@npm:2.0.0", + "version": "2.0.0", "dependencies": { - "internmap": "^1.0.0" + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" } } }, - "d3-array@npm:^3.1.6": { - "checksum": "f7e171ab4ece1c931e2b8e016a0cbc8c201bf2d17cdd79a9540154f4e72c380c576e4297f299a040b7005a48783520370eb7faf24c3c24653a4ee3058ab8fbb1", + "d3-dsv@npm:3": { + "checksum": "cafc6b70071b74145a92d683fe0ae64979de7627d9b87adb6df76028abe3b8aa44e0d098fe89efee3865c868f1bf2ad4fc03efaf2eba9ac807dd39e0750d7072", "resolution": { - "resolution": "d3-array@npm:3.2.4", - "version": "3.2.4", + "resolution": "d3-dsv@npm:3.0.1", + "version": "3.0.1", "dependencies": { - "internmap": "1 - 2" + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" } } }, - "d3-color@npm:1 - 2": { - "checksum": "d8383034c25361c09ef2e6bafe9e461f909cdaf1479a4ff721aeb7bf7bc93a67021ad666638e3e9771b7b0db0d3e4ab25f05cb1003a89f336115175a2fe0e3a8", + "d3-ease@npm:1 - 2": { + "checksum": "fd1332bd097af6efa9f101936567e0187a8d949badb83dc09220cef32de62cf86eec25c88ae621d5e51b4b202bb34f9eac1e089db690103c874f7bbbe612f1a3", "resolution": { - "resolution": "d3-color@npm:1.4.1", - "version": "1.4.1" + "resolution": "d3-ease@npm:1.0.7", + "version": "1.0.7" } }, - "d3-color@npm:1 - 3": { - "checksum": "e92a4f644097174b184088e8b41327b7975ecbf0b2a1e7b9fe7c111925f05f4b3d48da6167d592de6eb17de1b3f297417710dfb2f7854119ad073890b9b8f940", + "d3-ease@npm:1 - 3": { + "checksum": "91d09d809c6dd8ea4f2241ad313bc23f5b0b0d758f47c719d435e7166d961ce2623cb882aa8eb5161ec0c944805c10afaef66e67011d42e658c33e8fef14a909", "resolution": { - "resolution": "d3-color@npm:2.0.0", + "resolution": "d3-ease@npm:2.0.0", "version": "2.0.0" } }, - "d3-ease@npm:^3.0.1": { + "d3-ease@npm:3, d3-ease@npm:^3.0.1": { "checksum": "126757b36ab5fe84158045358f474aa1dde6ab5ab2f696e0fa84ab517bdc42f7d9db5db05809184907ab01247b0bda14f4fa7ac00204ffda21b65f9fb6df9e87", "resolution": { "resolution": "d3-ease@npm:3.0.1", "version": "3.0.1" } }, + "d3-fetch@npm:3": { + "checksum": "db35009adfd1213bd4aef88e7d83fcf0bb15f9ecba07fe44e20ede9cee89adb8992d4d8dd970584d28024ac9a8932ccc47e7b1d0508cb94a863e0f7f1aa72639", + "resolution": { + "resolution": "d3-fetch@npm:3.0.1", + "version": "3.0.1", + "dependencies": { + "d3-dsv": "1 - 3" + } + } + }, + "d3-force@npm:3": { + "checksum": "9c963eba79d51b4405b5570cd6295d7e1979096882632afae942ccf1d97b3d01a448558e5f3b5e5b7b774b9ff568c663130174c432b290295bf655a2bd0501e7", + "resolution": { + "resolution": "d3-force@npm:3.0.0", + "version": "3.0.0", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + } + } + }, "d3-format@npm:1 - 3": { "checksum": "07e43132ac6c0f0aff12e5527e970d3941c6c284af312b3232544286013c5f4a4309cdd9de2c8d4bbdfa709fc37791ffd235368f6ee4a9b9f97d495378255187", "resolution": { @@ -8822,7 +11113,41 @@ "version": "2.0.0" } }, - "d3-interpolate@npm:1.2.0 - 3": { + "d3-format@npm:3": { + "checksum": "d7ab106ff6c24d15dbebd0229c8356f878721b7c3e291f7b026a99d98661dde0d8ba2284456e72e3dc943f0492182b6dad4b8e4372cc2f5b902e7832135e9768", + "resolution": { + "resolution": "d3-format@npm:3.1.2", + "version": "3.1.2" + } + }, + "d3-geo@npm:3": { + "checksum": "a8f51308f1b6ebe784af4b3fb01738568c991d751dca94158e3b10117074ad029b6669e3e31f609d60c044d6d813d682436e38378b34b19e1a7509c829d7017a", + "resolution": { + "resolution": "d3-geo@npm:3.1.1", + "version": "3.1.1", + "dependencies": { + "d3-array": "2.5.0 - 3" + } + } + }, + "d3-hierarchy@npm:3": { + "checksum": "9406bc960e6d5ed440f084e8cdd89c87ea6725be40c6ae7af8fd61a5f1157030ff177be25f19591fce3c6d028a44cfd3122cb8f0351c9db1b3830df6f9085279", + "resolution": { + "resolution": "d3-hierarchy@npm:3.1.2", + "version": "3.1.2" + } + }, + "d3-interpolate@npm:1 - 2": { + "checksum": "289bc18e581fc558d6c10a878df73ed6a1ba91d431aedea43fc23d7480944040b7e8b54252598e061ccab3d224ccf63dc019af3509c4096ad83373689c4dd29c", + "resolution": { + "resolution": "d3-interpolate@npm:1.4.0", + "version": "1.4.0", + "dependencies": { + "d3-color": "1" + } + } + }, + "d3-interpolate@npm:1 - 3, d3-interpolate@npm:1.2.0 - 3": { "checksum": "481c953d1e25b5e940e630984827dce741609c9a6261ece4e4db3537d0670dcddb51623e00561dd1b76c57b2dc53b2a3f2583944a7d5ee49fedca8a02e671c7e", "resolution": { "resolution": "d3-interpolate@npm:2.0.1", @@ -8832,7 +11157,7 @@ } } }, - "d3-interpolate@npm:^3.0.1": { + "d3-interpolate@npm:3, d3-interpolate@npm:^3.0.1": { "checksum": "858f202fe7212901d7c12d67d7b964af7d41e814718d36d31ba15ac7ddd9c7b42347852eef595ed52eaadf4318984b3bebb60b424f3fdf48912d412f2a1fcde1", "resolution": { "resolution": "d3-interpolate@npm:3.0.1", @@ -8842,14 +11167,67 @@ } } }, - "d3-path@npm:^3.1.0": { + "d3-path@npm:1": { + "checksum": "c867409ed1621f495d4c219d575248204a7d51028c55c404425d3a78baf9e3c6909ff6fec20b17360ec9c7908a13effbd7a27c4e1451fb406dd70f446d92c223", + "resolution": { + "resolution": "d3-path@npm:1.0.9", + "version": "1.0.9" + } + }, + "d3-path@npm:1 - 3": { + "checksum": "d42d0d6bb1eb1aa4e96169278111bacce719525bee668dae108a0457c37f5708f1c0a88aa881404a0fbe8c82af6520bf23e59ccdac4a1ca4573e239f517aec61", + "resolution": { + "resolution": "d3-path@npm:2.0.0", + "version": "2.0.0" + } + }, + "d3-path@npm:3, d3-path@npm:^3.1.0": { "checksum": "1e716591d49b382dfefcbb21820c604bf71028332057df994082f9d040cecac5adb7ca09d84ce9bc73302d6f58e51807d30b5f8ddbef8b1510565140cc0a69e5", "resolution": { "resolution": "d3-path@npm:3.1.0", "version": "3.1.0" } }, - "d3-scale@npm:^4.0.2": { + "d3-polygon@npm:3": { + "checksum": "5487c8376bcb003ab7ab3dfc0cee8431c5f23a9593a8260e108b720fb06ef9bdcb172b3df7d23f082e9e8e4be13feaa9642a2d53b826ef1202abf496e19d4b8a", + "resolution": { + "resolution": "d3-polygon@npm:3.0.1", + "version": "3.0.1" + } + }, + "d3-quadtree@npm:1 - 3": { + "checksum": "9b0cdedeb8f7d27ba38cddfbe997ba0eb532877a19d0211bb2a0f310e458e0db750f84c0c4df6487f6f21c7c1b41fb718796adbc40a2b99cb11076b4df97e510", + "resolution": { + "resolution": "d3-quadtree@npm:2.0.0", + "version": "2.0.0" + } + }, + "d3-quadtree@npm:3": { + "checksum": "4ef6cef42c4844968f8859ee2931f1614561f12ef52c944421028c489bdfec17fe9e35e2547982e87b4287bde7d179ccdd5c908cba0f4b4568ce781f74d5e107", + "resolution": { + "resolution": "d3-quadtree@npm:3.0.1", + "version": "3.0.1" + } + }, + "d3-random@npm:3": { + "checksum": "2683ca087b784831ceec5ee8768367c8e74d930b36ca5ef6c5239a5d48d0d9b5b99d76fbda972cde66acc2ded5372796db974b01b398b749b958da850f271aea", + "resolution": { + "resolution": "d3-random@npm:3.0.1", + "version": "3.0.1" + } + }, + "d3-sankey@npm:^0.12.3": { + "checksum": "ee15e73af344cf5505cb75245644921fa3b2c8ff599725f77a9e121b6ca571669e14a247f736ce945996c21f4993e049f6d46e156904ea7c42622e7129825951", + "resolution": { + "resolution": "d3-sankey@npm:0.12.3", + "version": "0.12.3", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + } + }, + "d3-scale@npm:4, d3-scale@npm:^4.0.2": { "checksum": "fa8c3aeb9274d2afd3c6d439fbd0f2d12e401da75212ae19590a3786ae37cb229266d5ec358679468915014d84627ce5268ae5b658d1706e9a985c6f2b76a20c", "resolution": { "resolution": "d3-scale@npm:4.0.2", @@ -8863,7 +11241,32 @@ } } }, - "d3-shape@npm:^3.1.0": { + "d3-scale-chromatic@npm:3": { + "checksum": "142b269abd96d62112c27edac138c0eb8d1a5e477bb78f056db64b7ac201165b84d47f59cf11114ca426f36c47dab6e792c8a0a26d53176bfe0ad7dcfd90d628", + "resolution": { + "resolution": "d3-scale-chromatic@npm:3.1.0", + "version": "3.1.0", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + } + } + }, + "d3-selection@npm:2, d3-selection@npm:2 - 3": { + "checksum": "4abe936c65e40e6dfaa1d3642305df62a5071df0ab6eaa66eca5b6ed7629822c74a01e772ac98f3ba7937b68dfe1199bc30b2b205ea763993c74ed95ea6417ba", + "resolution": { + "resolution": "d3-selection@npm:2.0.0", + "version": "2.0.0" + } + }, + "d3-selection@npm:3, d3-selection@npm:^3.0.0": { + "checksum": "3888e2c62384c341a16c9ad6a787015a960cc90683f16cfe5d9e5184a922db9f4fa52cd037c11a6ad33568aa6dc6b970dd192bbd2fceaeefbf091d0997fa1968", + "resolution": { + "resolution": "d3-selection@npm:3.0.0", + "version": "3.0.0" + } + }, + "d3-shape@npm:3, d3-shape@npm:^3.1.0": { "checksum": "5f93ca4c092cb79e7d0b5babe04d3137a8046216772abbf43347620b88579a1dbb0734f6c531cf107bf91d39d9b9095d9e930bee3a5509aae9da3c0731f2e497", "resolution": { "resolution": "d3-shape@npm:3.2.0", @@ -8873,6 +11276,16 @@ } } }, + "d3-shape@npm:^1.2.0": { + "checksum": "f19c47d2171811c56594a929de037ada63b86f75696efda554a189198d4c0dcefff0762ed1c0c608f8c53fe2262b95b566621600687ea1c012ac98a496c7a7c2", + "resolution": { + "resolution": "d3-shape@npm:1.3.7", + "version": "1.3.7", + "dependencies": { + "d3-path": "1" + } + } + }, "d3-time@npm:1 - 2": { "checksum": "4bde32a1000fdf9029ee530619385f3566fb5679c77988d4b26ee7f180d369b824eb6eb48439c750e084528f8fbe51b9a52e13b0ca679c592c3bfa4ffd92158b", "resolution": { @@ -8880,7 +11293,7 @@ "version": "1.1.0" } }, - "d3-time@npm:2.1.1 - 3": { + "d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3": { "checksum": "6d2571a7860216b34b262655a016f632043c0913466498304114e6a4d36c91f8f64f4d49aa990a18b99b8156f252c2af9887b9bcf879927cd6e6759221a96cbf", "resolution": { "resolution": "d3-time@npm:2.1.1", @@ -8890,7 +11303,7 @@ } } }, - "d3-time@npm:^3.0.0": { + "d3-time@npm:3, d3-time@npm:^3.0.0": { "checksum": "4429aa55cf8e5b05c86b67b298701659abd597da558b26d53a92dbd8b35ff26e835870365c91619f597da1ac9a57a6471030e491f584fb14f3c7c05cc1c4626d", "resolution": { "resolution": "d3-time@npm:3.1.0", @@ -8910,13 +11323,103 @@ } } }, - "d3-timer@npm:^3.0.1": { + "d3-time-format@npm:4": { + "checksum": "a460033c8ca4bf1015a9515d6e2d5976ee30ced683454115a7f085f01981bea92b862528416325e784fe869baa9082b4fe1496ec19e525837fba643c4472d587", + "resolution": { + "resolution": "d3-time-format@npm:4.1.0", + "version": "4.1.0", + "dependencies": { + "d3-time": "1 - 3" + } + } + }, + "d3-timer@npm:1 - 2": { + "checksum": "19d386149a3e0b9bc0681999cc21a737998f86286e3f6a958a9ede54824701396db2bbd0eb8415755d70dd1e6b0fd0fe62afa036b91d529232549b3c5a34223c", + "resolution": { + "resolution": "d3-timer@npm:1.0.10", + "version": "1.0.10" + } + }, + "d3-timer@npm:1 - 3": { + "checksum": "a82abb9b373a0212a05bf646ce99344ef4903df699a75f847f4a6ccef1ba9a54c618a3f6d1cdea02a1dfa882eb801b8735da5db3dec2d19cfa0de5d44ce8f891", + "resolution": { + "resolution": "d3-timer@npm:2.0.0", + "version": "2.0.0" + } + }, + "d3-timer@npm:3, d3-timer@npm:^3.0.1": { "checksum": "5ff804395daa9afb4dd4fafa5635c46da11cd22dabe882cf728b0668a43412837abce71add4c25c11b48e4a636bf4e8cfe4c957ae650477ec5a1e88cde73532a", "resolution": { "resolution": "d3-timer@npm:3.0.1", "version": "3.0.1" } }, + "d3-transition@npm:2 - 3": { + "checksum": "214d2d32061fb82c7c44e9c23796a7c247dd2a3734a9107a1bd93ee0d5a2aece7ac29b425e9f85e4ea183de624b8150860f4bc78ac602a77e49a5de8e7493564", + "resolution": { + "resolution": "d3-transition@npm:2.0.0", + "version": "2.0.0", + "dependencies": { + "d3-color": "1 - 2", + "d3-dispatch": "1 - 2", + "d3-ease": "1 - 2", + "d3-interpolate": "1 - 2", + "d3-timer": "1 - 2" + }, + "peerDependencies": { + "d3-selection": "2" + } + } + }, + "d3-transition@npm:3, d3-transition@npm:^3.0.1": { + "checksum": "7bcbf2ba323801e9feb9e53d6692252f5d8d92092d8132faf5ab6794e8b554c8c5d48ed1db0ad90691f8fab844b3665404eb5fde100d9f9430445490b92afb87", + "resolution": { + "resolution": "d3-transition@npm:3.0.1", + "version": "3.0.1", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + } + }, + "d3-zoom@npm:3": { + "checksum": "df7ef80597c285e492371a8b7e183da79ec64f183d2ff15e03c07242de5db0087af785b319154cbf297b99b37292a038b6f8c2b5016847bc0927f2a3e1af47c8", + "resolution": { + "resolution": "d3-zoom@npm:3.0.0", + "version": "3.0.0", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + } + } + }, + "dagre-d3-es@npm:7.0.14": { + "checksum": "3b4265e3300326e978180f40ad47e67e936058474ca767eb0f4900ce09abb1000f8b5c8343db08e2b870bbe4353e2c974cc98ddb91713c9c1ff7b3c9856e00ac", + "resolution": { + "resolution": "dagre-d3-es@npm:7.0.14", + "version": "7.0.14", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + } + }, + "data-uri-to-buffer@npm:^6.0.2": { + "checksum": "1a11b1580a588cf0e86e69d5b22665fd7a7e208cc35a2ff8260f3e022ad70a21b985c84b2d75d55207259eb3cb1a7d4d9d38880a8198239bd5836ed69ec7a144", + "resolution": { + "resolution": "data-uri-to-buffer@npm:6.0.2", + "version": "6.0.2" + } + }, "data-view-buffer@npm:^1.0.2": { "checksum": "0c68dd3ec0671c4badc540068ec3bae868a14af361226c1347758fa328c281cfb7ad45722becea8c37288458e4fafa79d75b1ce5f37375ddf2df5699191cb930", "resolution": { @@ -8960,6 +11463,13 @@ "version": "1.11.18" } }, + "dayjs@npm:^1.11.19": { + "checksum": "aab8a93f6fc9804275753d2717138b764bd28dff3773681295b94ebefd8a6eae631f3a74e739a9c8f84c7bd3e6ce91280ad2469f2c8b7454f23e1c99ba4b1d4f", + "resolution": { + "resolution": "dayjs@npm:1.11.20", + "version": "1.11.20" + } + }, "debug@npm:2.6.9": { "checksum": "645fda7bad898f02130d83a48626830ced34a4e99a9c8c0dde1c2210a262b000853425a1db0be8228f7f13ba7ebf3732f36c492f59459eeacaaae4893c86ee28", "resolution": { @@ -9098,6 +11608,35 @@ "version": "6.1.4" } }, + "defu@npm:^6.1.6": { + "checksum": "466bcfa4551c6340b9bdfcee5d0437f523a7f314bc8742e9475ab10119d5a70f98d90e5dd00cdc0d841e5ef7c34a8bb26ebc390d875b26ede17c2c24249c1baf", + "resolution": { + "resolution": "defu@npm:6.1.7", + "version": "6.1.7" + } + }, + "degenerator@npm:^5.0.0": { + "checksum": "8275b85eb6d64d896c3b27bb044626c8f105a5a08fdefbd3b4af0c33e0f6c5dd04fb0e466033002cd88f1eac29ddf349c26f58a0221a1a1881f1d2d4bac89f24", + "resolution": { + "resolution": "degenerator@npm:5.0.1", + "version": "5.0.1", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + } + } + }, + "delaunator@npm:5": { + "checksum": "bb97ffb16e1276a26073238d0ddf6ae23193a95362f71e94ed1ee226c7666a06d716bd74c805d00a02197b525ab93a3646e1c7698d4f4495bb3af90931356ce3", + "resolution": { + "resolution": "delaunator@npm:5.1.0", + "version": "5.1.0", + "dependencies": { + "robust-predicates": "^3.0.2" + } + } + }, "depd@npm:2.0.0, depd@npm:~2.0.0": { "checksum": "00585fa49f9739eac8b44c8be3c98ecc231893c03451f130a545a9305c5761322233b41cd1aa134a85844d06aeaa4aa577bc2e9efedc19aa1bc781ef17c74af8", "resolution": { @@ -9157,6 +11696,13 @@ "version": "5.3.2" } }, + "devalue@npm:^5.6.2, devalue@npm:^5.6.4": { + "checksum": "52ee8881ed2e57f4cbcebfa126f281e9ce8c8359d83c509f186e769f123d8615af6f379ad0bde54b6c44990a12246b1fe84339027e940259255882db7968ab2a", + "resolution": { + "resolution": "devalue@npm:5.8.1", + "version": "5.8.1" + } + }, "devlop@npm:^1.0.0, devlop@npm:^1.1.0": { "checksum": "730e6cd95ae46e3d5c12284a3ad60f38c32b1cdfdd2aa6a3757c475fa432b57176715c65e4648ac36521cac3614090db2541c8517044f9545e34ec75aa35a631", "resolution": { @@ -9167,6 +11713,13 @@ } } }, + "devtools-protocol@npm:0.0.1608973": { + "checksum": "daac9a5a08c8bd6e7e3264d4889194005e45ab559494ad955031c4a9c46b553f5774a6a187bc9a8f9a1a2673a16ceea31997842fe449ba9e54a47ea0197f5830", + "resolution": { + "resolution": "devtools-protocol@npm:0.0.1608973", + "version": "0.0.1608973" + } + }, "dfa@npm:^1.2.0": { "checksum": "83c6c577da521d48fd8c129e6223396b5388af86c5eb6307ae31743f2348063b6f9749c586cbc335f3be43856a1e2dfa370ae5f3ce935fc54d29b740c767576f", "resolution": { @@ -9181,6 +11734,13 @@ "version": "5.2.0" } }, + "diff@npm:^8.0.3": { + "checksum": "3ef21212521e878b6a9e224422a603befa056962131bd046e798b1232f31c7b2cb1b4dab53f530c4c94bd29b6c0526902875d7f159410b31d56248546e3b133e", + "resolution": { + "resolution": "diff@npm:8.0.4", + "version": "8.0.4" + } + }, "diff-sequences@npm:^29.6.3": { "checksum": "c94d9a01882500d89fd7ed14804875a65eeb935ff6a997dc0a3ceff443ed4499e6f6663b901187d6b47d29f4f61e8beb4b5498a1ea4af89ff331a76f99523519", "resolution": { @@ -9254,6 +11814,19 @@ ] } }, + "dompurify@npm:^3.3.1": { + "checksum": "fba976014da5fea134a18be6e1ec921069bdc881cd2260a8400a86924135a1829885ac7c1bc3d56cde112a2abccdccfc378efae6ef2e2219c3ed163fa2e39e38", + "resolution": { + "resolution": "dompurify@npm:3.4.4", + "version": "3.4.4", + "dependencies": { + "@types/trusted-types": "^2.0.7" + }, + "optionalDependencies": [ + "@types/trusted-types" + ] + } + }, "domutils@npm:^3.0.1": { "checksum": "e937fa07d6e0d4570a2d546055e5cfd4ac7f7a5c33610e6c9467800641a10f9c9c08834faa1539ab2dfe59e41d76ecc29798a895f0321373a1e0f5333281c423", "resolution": { @@ -9390,6 +11963,17 @@ } } }, + "enhanced-resolve@npm:^5.21.0": { + "checksum": "06e5f52829111449a8eb0f6e6733199b84cfe2196abd22694951d0d5c351e6554d2cecde1c46a9c9b0e63a7f0ca304ec822e97f6c0e369c7d7405f985b78e9f8", + "resolution": { + "resolution": "enhanced-resolve@npm:5.21.3", + "version": "5.21.3", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + } + } + }, "enquirer@npm:^2.3.6": { "checksum": "0191cf75ffbc66f55b5637938498d0a676f9a945803084a8afe58fa52bd878abf9c92f89b71960fd2654ca64abe5fe2c336c3571888709432cfff0c59adbc708", "resolution": { @@ -9415,7 +11999,7 @@ "version": "6.0.1" } }, - "env-paths@npm:^2.2.0": { + "env-paths@npm:^2.2.0, env-paths@npm:^2.2.1": { "checksum": "f1740c5cfa1f277cb8f0c9751eec990828d430a524f70f770757cfdf569257a8ffbc43e1e6a724bda5fd96680f2a19ba02a647ea69364785804960e03ded0cdd", "resolution": { "resolution": "env-paths@npm:2.2.1", @@ -9600,6 +12184,13 @@ "version": "1.39.10" } }, + "es-toolkit@npm:^1.45.1": { + "checksum": "085e77376c94fef8fbf0868d55e89caed2f84d72872d4e202bc19c639c2a07e2955dc59924f88eda7d3549f7b653c362d9c815ff93883a932ccfe9760e1d133a", + "resolution": { + "resolution": "es-toolkit@npm:1.46.1", + "version": "1.46.1" + } + }, "es6-promisify@npm:^7.0.0": { "checksum": "0f247317726b30dae839a6cb4b24bcb8a37491b019eba1c1261bcc7e04019985a202746ecc7888f8d8cc192ab5e9ead88b4b0b877a432bb76607d9904460f9b6", "resolution": { @@ -9759,6 +12350,69 @@ ] } }, + "esbuild@npm:^0.27.0, esbuild@npm:^0.27.3": { + "checksum": "7c07567c23f0bf35b37449f83084c661bbbb3876e48084273525f4472fcd78a772792da86c6e74ea92a3ee9cb2291c752d998d7f7ceab93dec2b5cbe7c25bf49", + "resolution": { + "resolution": "esbuild@npm:0.27.7", + "version": "0.27.7", + "dependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + }, + "optionalDependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-arm64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/openharmony-arm64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ] + } + }, "esbuild-wasm@npm:^0.23.0": { "checksum": "000cca3a7a3f260048cdd6c7c3a562bb8b036ad51fa105e65a9a8035272c07d1485956e25c03ce428f1926d984e79dd84db23b57f0bde77eba3831431dedf21d", "resolution": { @@ -9801,6 +12455,22 @@ "version": "5.0.0" } }, + "escodegen@npm:^2.1.0": { + "checksum": "696edbfc8f6c14f4cc83e28e40e189e4e7532095b4eae136525a63ad963833dbd19a21d851a8dd9a6570c3eb24e51a1c4c0b8688bedeb46f54243ed60ed55c43", + "resolution": { + "resolution": "escodegen@npm:2.1.0", + "version": "2.1.0", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "source-map": "~0.6.1" + }, + "optionalDependencies": [ + "source-map" + ] + } + }, "eslint@npm:^9.36.0": { "checksum": "c9af0e86da6e2685fb51a65af9cad93e44841b1460c254bfb535462da99844d3ae7a6c56504d21871e065e90ec9e2996fc407c917ef6e38f554fb005ff546f6f", "resolution": { @@ -10151,6 +12821,22 @@ "version": "3.0.2" } }, + "extract-zip@npm:^2.0.1": { + "checksum": "f342b2ef1f64792daaa4aaef50f7b9fba0f55cdece6cd67ae22e852a6320de5f9bc3be99114ea48f79b29612e4e8936974ed152e908261638c44ae50b3e63e7d", + "resolution": { + "resolution": "extract-zip@npm:2.0.1", + "version": "2.0.1", + "dependencies": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "optionalDependencies": [ + "@types/yauzl" + ] + } + }, "fast-content-type-parse@npm:^3.0.0": { "checksum": "0a82e67a343754e0ebb52b2e9ac41bb953b2b837e935728b82523c821e728e2f0cff36e326c8c201ea66940e4f10f4977e9922aafcf90cc1c88250a5d018ba25", "resolution": { @@ -10220,6 +12906,16 @@ } } }, + "fd-slicer@npm:~1.1.0": { + "checksum": "1b3f5fe1b972f0ebb3064fead4e2fd101053d60335b2e4d3ec4abf3ae1725da87dab7864a21630c94c71ee5290b39db6760c25926cc6e64fb990f3b0ce278a54", + "resolution": { + "resolution": "fd-slicer@npm:1.1.0", + "version": "1.1.0", + "dependencies": { + "pend": "~1.2.0" + } + } + }, "fdir@npm:^6.4.4, fdir@npm:^6.5.0": { "checksum": "27dcdf00d6a81710385dea5a61417897e092ea5e7a156ab8a9a4a417e1a29f2032c268854e4c48e72e614df8591734088f3f164fc7b5cd5c3ad13c6c6a324525", "resolution": { @@ -10334,6 +13030,16 @@ } } }, + "fontace@npm:~0.4.0": { + "checksum": "c7343ea4a7cea1f2e7f0f0e9960d2d57ebd38e4db2cb4708fd5b03281b80ab5af684a144dacf8163b98d5d7691deb32022f26b2dd7d1498b25c738d91588b366", + "resolution": { + "resolution": "fontace@npm:0.4.1", + "version": "0.4.1", + "dependencies": { + "fontkitten": "^1.0.2" + } + } + }, "fontkit@npm:^2.0.2, fontkit@npm:^2.0.4": { "checksum": "071b37c2262c2722fad4fd3412f5474be31f2d6f9e09223ae86a1abf197b7c0e5be65c70240a9176f5761b3cb0a03ea83f982019f3c9478237b74dfcee0a13e4", "resolution": { @@ -10352,6 +13058,16 @@ } } }, + "fontkitten@npm:^1.0.0, fontkitten@npm:^1.0.2": { + "checksum": "67853dd174a2a11cbbd1a587d7693aa47c5bb78377e211d0b5dee5e6678111394fc2f6b0e05178b1bfdf76feff8726e19de3483864c84c5eaf9df400709fd530", + "resolution": { + "resolution": "fontkitten@npm:1.0.3", + "version": "1.0.3", + "dependencies": { + "tiny-inflate": "^1.0.3" + } + } + }, "for-each@npm:^0.3.3, for-each@npm:^0.3.5": { "checksum": "b77b83757ffbe368545b5bad71aec1311069fb00b26c70a96d01902a2177ad6df0effa0b9a77c4bbd2f49e5be12b9165d03ba3131c5aca9d384494faa4cce3ae", "resolution": { @@ -10552,6 +13268,18 @@ } } }, + "get-uri@npm:^6.0.1": { + "checksum": "6dc6ea787b7a529f7c844255180ccdea9850ea8f3fecabe057d16a7b42d6ee30418a22a373e6d782c55f2312d0e4b25df7b5555b66c7fcf5c49303dddeb3d4dc", + "resolution": { + "resolution": "get-uri@npm:6.0.5", + "version": "6.0.5", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + } + } + }, "git-up@npm:^7.0.0": { "checksum": "b27b6dd5968f969d15cc7b556105fbe1c8a2e232a1dceaf9278de871a8223282cfe5ce397dcf5d94f6ba19cf4bd229b58727ca095d21a673756f9f93fc5784c3", "resolution": { @@ -10779,6 +13507,24 @@ "version": "1.4.0" } }, + "h3@npm:^1.15.10": { + "checksum": "7dfd93a353fa9d3ad55e412f9b744123390327b9941aa5b93519f716e295656ad14c22f8db4974fe22685a47ce3abed9f01ad33976fa77821ce3ecfd588c367e", + "resolution": { + "resolution": "h3@npm:1.15.11", + "version": "1.15.11", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + } + }, "h3@npm:^1.15.4": { "checksum": "2c6741bdeaba3551c8992b65e93b32d1d12f376a28f553b2b198d4aeabe30435267ee739cdc509c642a2ae38caa6c587c466c139fb963547ecb62395183693de", "resolution": { @@ -10797,6 +13543,13 @@ } } }, + "hachure-fill@npm:^0.5.2": { + "checksum": "ff7f239169a8c562a028d562dbdc7fcf42ac081d5cd7c62861a13d6c5f1d0334d7f2391d1fcacc715ea04e2b92bb8603e63daec92ccc5f996e92e5706191d6b6", + "resolution": { + "resolution": "hachure-fill@npm:0.5.2", + "version": "0.5.2" + } + }, "has-bigints@npm:^1.0.2": { "checksum": "337cb81962041b52b298da9f77084cd706d20da7ebf669d12727a2ce39b5efaa52d5ea7ce2372942aaf192a60e0ab64c5ca02d3023b73fbcc09e5c6d34e343a2", "resolution": { @@ -11271,7 +14024,7 @@ } } }, - "http-proxy-agent@npm:^7.0.0": { + "http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1": { "checksum": "f8a937aaa255bdf2fa50e9d66ff5b98fa2149c24c2351e382a2fc731ed73709cf44eb5d4c59a7422f1ff3a03c935e7737afe9ae95cdd3c4706ee151a06c9f4dd", "resolution": { "resolution": "http-proxy-agent@npm:7.0.2", @@ -11293,7 +14046,7 @@ } } }, - "https-proxy-agent@npm:^7.0.1": { + "https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6": { "checksum": "5a5ac38ab7a4589f65c559de7c1ad8cd86fc88e945fc189b39c1d8804a6b0af4fe71827bcdf820d231422fdaad22395cb3e958ddf4ca716c7dd3abb6aa7c01c9", "resolution": { "resolution": "https-proxy-agent@npm:7.0.6", @@ -11321,7 +14074,17 @@ } } }, - "iconv-lite@npm:^0.6.2": { + "iconv-lite@npm:0.4": { + "checksum": "05f1a0c1b7a95f11eaa14f2776906c1b98289e6bbb2fb293e479756b848521ad7cdcf89d99b59d5533561b4ef922aa73113afc9ce77a32d23229898419247b83", + "resolution": { + "resolution": "iconv-lite@npm:0.4.24", + "version": "0.4.24", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + }, + "iconv-lite@npm:0.6, iconv-lite@npm:^0.6.2": { "checksum": "d3e5ff3b2ce637d6fd12400d96ed4de4f45751db643925c8234ac47a1c6629a98b2c27481c69e0688c631faf0c393c092efc735450a8df4ed725dd0dc6e8c39f", "resolution": { "resolution": "iconv-lite@npm:0.6.3", @@ -12600,6 +15363,13 @@ "version": "2.6.1" } }, + "jiti@npm:^2.6.1": { + "checksum": "73dcc28c5d20632ee4df7731b6826ddf08c02a7b4cf31558cfffe998c9ceec5b8239713f2ad3951665bf345de024b75e9f5669cff8eb6cd158541c21d47dd8ce", + "resolution": { + "resolution": "jiti@npm:2.7.0", + "version": "2.7.0" + } + }, "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": { "checksum": "5d3454c9f43eaca7897c678687f59728805c15e643214bd1027418e7cb5dce3e6f77967e9afdec5092f2f9a8f45f978c288bda1a98103de8e723b27ba24e0f62", "resolution": { @@ -12639,6 +15409,16 @@ } } }, + "js-yaml@npm:^4.1.1": { + "checksum": "807914890da23d90bf1805ea6eea8ac903bb413e950c91b5619c33ae449b02f427cf2ea3067bc0f2cd027cb305bc71f3e5302ec46699957f051c3a6dca32b015", + "resolution": { + "resolution": "js-yaml@npm:4.1.1", + "version": "4.1.1", + "dependencies": { + "argparse": "^2.0.1" + } + } + }, "jsesc@npm:^3.0.2": { "checksum": "760a8e49473ffbdc641cf453418dc10e0bafd1a5af7f13035760bd8435edf9c6114f0b2a86069242c12775f51bd80291fc8ab385550872422a6320558d63a0d9", "resolution": { @@ -12708,6 +15488,16 @@ } } }, + "katex@npm:^0.16.25": { + "checksum": "5d07245edeb92f73ff29e2162ea41076b3a0229c70c0f940f55a7f935f2087f524e9b4e8a960f511528546f04facbdb59c83ca959770e4364a5e443c77bc1cc2", + "resolution": { + "resolution": "katex@npm:0.16.47", + "version": "0.16.47", + "dependencies": { + "commander": "^8.3.0" + } + } + }, "keyv@npm:^4.0.0, keyv@npm:^4.5.4": { "checksum": "1ecb41ae61773a53024d49a47dde7eba90b51e671db1e76bdf0b1ce8ae1bac4670c609e3156f24fac7df52831a50c440399e9e64c5d9ff8096620a6f46c84ac3", "resolution": { @@ -12718,6 +15508,13 @@ } } }, + "khroma@npm:^2.1.0": { + "checksum": "184baba494f35b3648392071b25bf08e7935ee745e94e7d675b95642e2145b7a57e5126f1bff19f28af8d098df18d26673665b33137d68e4e0889794cb863437", + "resolution": { + "resolution": "khroma@npm:2.1.0", + "version": "2.1.0" + } + }, "kleur@npm:^3.0.3": { "checksum": "699f68485283cd147d110375ecc58b865e1ccae066f50f48410d4a3e7db4a61688adca3476cefef167595b3e3462d66b0f190189598c194270cfd65eb8f1c836", "resolution": { @@ -12746,6 +15543,20 @@ "version": "1.8.0" } }, + "layout-base@npm:^1.0.0": { + "checksum": "73e0079f260af3973681aefd129bb5b64d61aa94cf3366f340458cd6ccc867d42715922fbdad9e51ed5c7a83d28f8bfa4744fd39437db2f0f1c276a86ccb92c4", + "resolution": { + "resolution": "layout-base@npm:1.0.2", + "version": "1.0.2" + } + }, + "layout-base@npm:^2.0.0": { + "checksum": "1aab8874f0dfb0a89ba65ff7fb6df2092995efed538c53c2ddc114800966a936cc847224daa7ec4703a5627e1ca4002e805ecc11fe9a8d016ce05476ced592fc", + "resolution": { + "resolution": "layout-base@npm:2.0.1", + "version": "2.0.1" + } + }, "leven@npm:^3.1.0": { "checksum": "bf3fdc565c9b394420060e62c91265d9f5f08d8a7732cfffc786b91e27d042897c24282a94f51933bdf8dca936dc2070a12ee2bff49aee1fdf8d41b1c4d415e8", "resolution": { @@ -12796,6 +15607,55 @@ ] } }, + "lightningcss@npm:1.32.0, lightningcss@npm:^1.32.0": { + "checksum": "fbce06548eed2df7853983f101bf647b75867cfcc4aaa5b920447c80c0f5192e9c3365a04e7a8e01e04363558c0ec6a8f01929c8705faf2aecad924e75c215df", + "resolution": { + "resolution": "lightningcss@npm:1.32.0", + "version": "1.32.0", + "dependencies": { + "detect-libc": "^2.0.3", + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + }, + "optionalDependencies": [ + "lightningcss-android-arm64", + "lightningcss-darwin-arm64", + "lightningcss-darwin-x64", + "lightningcss-freebsd-x64", + "lightningcss-linux-arm-gnueabihf", + "lightningcss-linux-arm64-gnu", + "lightningcss-linux-arm64-musl", + "lightningcss-linux-x64-gnu", + "lightningcss-linux-x64-musl", + "lightningcss-win32-arm64-msvc", + "lightningcss-win32-x64-msvc" + ] + } + }, + "lightningcss-android-arm64@npm:1.32.0": { + "checksum": null, + "resolution": { + "resolution": "lightningcss-android-arm64@npm:1.32.0", + "version": "1.32.0", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "android" + ] + } + } + }, "lightningcss-darwin-arm64@npm:1.30.1": { "checksum": null, "resolution": { @@ -12811,6 +15671,21 @@ } } }, + "lightningcss-darwin-arm64@npm:1.32.0": { + "checksum": null, + "resolution": { + "resolution": "lightningcss-darwin-arm64@npm:1.32.0", + "version": "1.32.0", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "darwin" + ] + } + } + }, "lightningcss-darwin-x64@npm:1.30.1": { "checksum": null, "resolution": { @@ -12826,6 +15701,21 @@ } } }, + "lightningcss-darwin-x64@npm:1.32.0": { + "checksum": null, + "resolution": { + "resolution": "lightningcss-darwin-x64@npm:1.32.0", + "version": "1.32.0", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "darwin" + ] + } + } + }, "lightningcss-freebsd-x64@npm:1.30.1": { "checksum": null, "resolution": { @@ -12841,6 +15731,21 @@ } } }, + "lightningcss-freebsd-x64@npm:1.32.0": { + "checksum": null, + "resolution": { + "resolution": "lightningcss-freebsd-x64@npm:1.32.0", + "version": "1.32.0", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "freebsd" + ] + } + } + }, "lightningcss-linux-arm-gnueabihf@npm:1.30.1": { "checksum": null, "resolution": { @@ -12856,6 +15761,21 @@ } } }, + "lightningcss-linux-arm-gnueabihf@npm:1.32.0": { + "checksum": null, + "resolution": { + "resolution": "lightningcss-linux-arm-gnueabihf@npm:1.32.0", + "version": "1.32.0", + "requirements": { + "cpu": [ + "arm" + ], + "os": [ + "linux" + ] + } + } + }, "lightningcss-linux-arm64-gnu@npm:1.30.1": { "checksum": null, "resolution": { @@ -12874,6 +15794,24 @@ } } }, + "lightningcss-linux-arm64-gnu@npm:1.32.0": { + "checksum": null, + "resolution": { + "resolution": "lightningcss-linux-arm64-gnu@npm:1.32.0", + "version": "1.32.0", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "linux" + ], + "libc": [ + "glibc" + ] + } + } + }, "lightningcss-linux-arm64-musl@npm:1.30.1": { "checksum": null, "resolution": { @@ -12892,6 +15830,24 @@ } } }, + "lightningcss-linux-arm64-musl@npm:1.32.0": { + "checksum": null, + "resolution": { + "resolution": "lightningcss-linux-arm64-musl@npm:1.32.0", + "version": "1.32.0", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "linux" + ], + "libc": [ + "musl" + ] + } + } + }, "lightningcss-linux-x64-gnu@npm:1.30.1": { "checksum": null, "resolution": { @@ -12910,6 +15866,24 @@ } } }, + "lightningcss-linux-x64-gnu@npm:1.32.0": { + "checksum": null, + "resolution": { + "resolution": "lightningcss-linux-x64-gnu@npm:1.32.0", + "version": "1.32.0", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "linux" + ], + "libc": [ + "glibc" + ] + } + } + }, "lightningcss-linux-x64-musl@npm:1.30.1": { "checksum": null, "resolution": { @@ -12928,6 +15902,24 @@ } } }, + "lightningcss-linux-x64-musl@npm:1.32.0": { + "checksum": null, + "resolution": { + "resolution": "lightningcss-linux-x64-musl@npm:1.32.0", + "version": "1.32.0", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "linux" + ], + "libc": [ + "musl" + ] + } + } + }, "lightningcss-win32-arm64-msvc@npm:1.30.1": { "checksum": null, "resolution": { @@ -12943,6 +15935,21 @@ } } }, + "lightningcss-win32-arm64-msvc@npm:1.32.0": { + "checksum": null, + "resolution": { + "resolution": "lightningcss-win32-arm64-msvc@npm:1.32.0", + "version": "1.32.0", + "requirements": { + "cpu": [ + "arm64" + ], + "os": [ + "win32" + ] + } + } + }, "lightningcss-win32-x64-msvc@npm:1.30.1": { "checksum": null, "resolution": { @@ -12958,6 +15965,21 @@ } } }, + "lightningcss-win32-x64-msvc@npm:1.32.0": { + "checksum": null, + "resolution": { + "resolution": "lightningcss-win32-x64-msvc@npm:1.32.0", + "version": "1.32.0", + "requirements": { + "cpu": [ + "x64" + ], + "os": [ + "win32" + ] + } + } + }, "lines-and-columns@npm:^1.1.6": { "checksum": "33803d745fbb383fa0991990d18c93bcdaac87445db0e39408a289c2bd83d2e4f3fec902410edec754159527c8550cbbb7921a31b405313a225839f07150faed", "resolution": { @@ -12992,6 +16014,13 @@ "version": "4.17.21" } }, + "lodash-es@npm:^4.17.21": { + "checksum": "91ef9399fab82e3f28692e8210b38e1ebb2b825003e278a36e8dd2f5b04f8dce30a448cc60dcbe3157fe0211a1a0af4994e6a7c5ead77c68d254c49dc33f4ead", + "resolution": { + "resolution": "lodash-es@npm:4.18.1", + "version": "4.18.1" + } + }, "lodash.debounce@npm:^4.0.8": { "checksum": "0ad221b78f3964b7eda3d03333f00e2cd2240efb1445ecea593b678591753745727c1a363171c13e0bd33ba3fa8a7e09e8d8607b630a08333c0dfacc912316a0", "resolution": { @@ -13061,6 +16090,13 @@ "version": "11.2.4" } }, + "lru-cache@npm:^11.2.7": { + "checksum": "dc09440a9a62ef88e5f36da51cb042ac218e5e1ae5ca0ec1b12de36dc188bdcc570ef2068df4e1e18bf899a631c7e0840209922302cf8ba6ad698da86ed30ee3", + "resolution": { + "resolution": "lru-cache@npm:11.3.6", + "version": "11.3.6" + } + }, "lru-cache@npm:^5.1.1": { "checksum": "fd6afdf74fefa8a56ddffb638b4157f8a91164a71e2077b558120d493ca3cc239f638054012d59d9a282bfeafdfb18013f62f47caabadecf1af76387c512e3fa", "resolution": { @@ -13071,11 +16107,28 @@ } } }, - "magic-string@npm:0.x >= 0.26.0, magic-string@npm:^0.30.18, magic-string@npm:^0.30.19": { - "checksum": "a295c76d597e345cfac9526519b33c8917772fb76f610c6cc278e9e9d54486c10246c156e52772dd6d043d9d6be0ebe68a52e9004c106f26267cecec4c065f4b", + "lru-cache@npm:^7.14.1": { + "checksum": "a3bd91b1fc8895e11924cfcad9678069e8c0d34c435dd8e77b28109a00d8bbea92b29a9ef0fc3b3a1c9a074bb4d56aa4b10bf3745fa32903e719cfb824e6c15a", + "resolution": { + "resolution": "lru-cache@npm:7.18.3", + "version": "7.18.3" + } + }, + "magic-string@npm:0.x >= 0.26.0, magic-string@npm:^0.30.18, magic-string@npm:^0.30.19": { + "checksum": "a295c76d597e345cfac9526519b33c8917772fb76f610c6cc278e9e9d54486c10246c156e52772dd6d043d9d6be0ebe68a52e9004c106f26267cecec4c065f4b", + "resolution": { + "resolution": "magic-string@npm:0.30.19", + "version": "0.30.19", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + } + }, + "magic-string@npm:^0.30.21": { + "checksum": "aca784e571fb9609dbed834920cfa79f342458802a1643c382bf9e3e3bc19a16eec5b19e801b6ac811d9ea9116e1e6b0cb0987658560cd73abb401e49e6eef06", "resolution": { - "resolution": "magic-string@npm:0.30.19", - "version": "0.30.19", + "resolution": "magic-string@npm:0.30.21", + "version": "0.30.21", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } @@ -13093,6 +16146,18 @@ } } }, + "magicast@npm:^0.5.1": { + "checksum": "6fbe80fc565fe608da75bb55284d2984f5249c4821117ce87c2e9fac39bfb2319e47edbd7cd0b03c56de1290610a718c629cd44038f80bbbfdaebbe90c53aeaa", + "resolution": { + "resolution": "magicast@npm:0.5.3", + "version": "0.5.3", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + } + }, "make-dir@npm:^4.0.0": { "checksum": "0f1cad8a95267d800407e7f4e91349a21da2e2fa8ee089f8a44de4175a19224839ea24615a66f9fcf4068ce89d9968d8cc3f0a824c5e96842e77c5a10d994e75", "resolution": { @@ -13462,6 +16527,13 @@ } } }, + "mdn-data@npm:2.0.28": { + "checksum": "433f39f1ae3ab597bcacb3f083498bee41760e1604bd4d0d1a3ce523dcce78520b0574060e664ba2fbfddd6b47260cb96b1082d956d746b9a66f3090cce390eb", + "resolution": { + "resolution": "mdn-data@npm:2.0.28", + "version": "2.0.28" + } + }, "mdn-data@npm:2.12.2": { "checksum": "be1cf3df764d2a266d68787cb52426836bfac21d6a5e812ae2332841ed9333fe2e216cf5e0414d7ef3d31e88143500fab5e2e153b787e484aa89e30fc1e14a70", "resolution": { @@ -13469,6 +16541,13 @@ "version": "2.12.2" } }, + "mdn-data@npm:2.27.1": { + "checksum": "40bb5a14890686575acb66f76bfe1fa6b8f308d4127043c3e59f0958f24690190e7d26ece6563f80a29aea122cb9c7c3b744ae3696127888355e5590f43a05de", + "resolution": { + "resolution": "mdn-data@npm:2.27.1", + "version": "2.27.1" + } + }, "merge-stream@npm:^2.0.0": { "checksum": "6f9d948302dcf04200294137f91dee6ee8905d0fc4190ebd3015fc9110cac686bdbae46a57a0a9a61de0259aa272536159153f7a82e309a0c42158cd2ca71885", "resolution": { @@ -13483,6 +16562,36 @@ "version": "1.4.1" } }, + "mermaid@npm:^11.0.0": { + "checksum": "524783b15d33f8a4132617e854ab34e2581c7521cb819c669dce48bc5d510d00beaedc061723b917cfe7c86c224b156aede58dcd877d084aa15ecf280afff72d", + "resolution": { + "resolution": "mermaid@npm:11.15.0", + "version": "11.15.0", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.1.1", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "es-toolkit": "^1.45.1", + "katex": "^0.16.25", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + } + }, "meshoptimizer@npm:~0.22.0": { "checksum": "e3195b774c5abe43b301688a168a911dcc12514dfd6a5808eb2240e003a2d6dde74d836c059aed78bca3d1b0abdb1bc3f423b5089cdb31f8e09e73fa02eedbd4", "resolution": { @@ -13557,6 +16666,22 @@ } } }, + "micromark-extension-directive@npm:^4.0.0": { + "checksum": "2ccc571e4f14e712dc903bcb1cdf7cce4ac79150daa1b4b5985c37b983c0c8c7a8c11517e2f878ecf6c29c6e7ab8cec480c2aa96548d7a11d2edc0339693550d", + "resolution": { + "resolution": "micromark-extension-directive@npm:4.0.0", + "version": "4.0.0", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + } + } + }, "micromark-extension-gfm@npm:^3.0.0": { "checksum": "61c9745d2e79fdc563b876275308ca14d8d9ba87f0b5180f017ca5bcbb9f1c8ece90b79a09f2e315dc75cb6893480b9e80ddae2cf3ce22500acdf4e001de6984", "resolution": { @@ -14184,6 +17309,13 @@ } } }, + "mitt@npm:^3.0.1": { + "checksum": "7d26a58cf291844bc9ef6cd13c2f7fa4adc0e8964eb02514a2b7687454182f89bf8d2199377b0021ef1e91c6b845cf026c2d3619c23b911b784944fd3a500cd4", + "resolution": { + "resolution": "mitt@npm:3.0.1", + "version": "3.0.1" + } + }, "mkdirp@npm:0.3.0": { "checksum": "d6c431ab547fc5be4da9d1d1842db4b4be817cefbfbd1274150678b782cb52ed7870d60b34cf50f8518c6e580c9f6382753867f319f1f430c58a101ff76e336d", "resolution": { @@ -14271,6 +17403,13 @@ "version": "0.6.18" } }, + "netmask@npm:^2.0.2": { + "checksum": "1e9622f57c5a3e56b4bd6073e1841c5448a679b4f2b7185582f537d84da7ca567d8ac8b1ab8f15540bedfaceaedcd765e8a505cfe35500473862f02ea4ad5874", + "resolution": { + "resolution": "netmask@npm:2.1.1", + "version": "2.1.1" + } + }, "nlcst-to-string@npm:^4.0.0": { "checksum": "ee29cfaa732abdf8a6dd9dbc690bb5171948c8dc6eba2f34c18dbbfd7bc6a469a26f420cc3ad59dd95201eb54df49e26c79d8e3beedc73b3a6a33f8d3d243386", "resolution": { @@ -14379,6 +17518,13 @@ "version": "1.0.3" } }, + "node-mock-http@npm:^1.0.4": { + "checksum": "ff5bd08d0119c65edb1f2fafdc7f87f0e32fdc0c8c2c68f48dbc860d2f0be5bde9faae0174275da9491ba6b00514e76c8b9a8622bc839081835ca1a530777333", + "resolution": { + "resolution": "node-mock-http@npm:1.0.4", + "version": "1.0.4" + } + }, "node-releases@npm:^2.0.21": { "checksum": "3edf8e3f765ed7f35c87af7322f11cfeff2a6e27adbe43c89073f648418e61cbed25f1709dcd2d8b9501b8ac9d73394ca1c243ac78054d936f753aae09d9612e", "resolution": { @@ -14554,7 +17700,19 @@ } } }, - "ohash@npm:^2.0.0": { + "ofetch@npm:^1.5.1": { + "checksum": "78f76334a5abf7a17e1bb69d3de3ca7e064339a346800c9568781f57f4cc99ddb368e4a08aea6979843d06b383e432ac8d97b047622df7f9d8d32c64f19ca910", + "resolution": { + "resolution": "ofetch@npm:1.5.1", + "version": "1.5.1", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + } + }, + "ohash@npm:^2.0.0, ohash@npm:^2.0.11": { "checksum": "07bec79855fa0a0e136034d016c32fda25ac76551326494044df5bf007711bcf8c8ea4a01aaf4f492fa5d63efc729c05014ef2ba3a70f642c465ea5905031135", "resolution": { "resolution": "ohash@npm:2.0.11", @@ -14598,6 +17756,13 @@ "version": "0.12.1" } }, + "oniguruma-parser@npm:^0.12.2": { + "checksum": "d21d3dc5552e65675eb612f22c04192509f286fc90d899fda2c725f9816a98d137294201b830c73c9f4d775bb2826a7fbe0fc24f3cd793214f8627b4e2b03833", + "resolution": { + "resolution": "oniguruma-parser@npm:0.12.2", + "version": "0.12.2" + } + }, "oniguruma-to-es@npm:^4.3.3": { "checksum": "df0d78f564bad964053717ef952fc61a5b1243e0b783885164ae8fcb3f7407377f2f47f392a29fcd8d26280981ad4274c0c0187f1b454918658c3b793306b8e1", "resolution": { @@ -14610,6 +17775,18 @@ } } }, + "oniguruma-to-es@npm:^4.3.4": { + "checksum": "e95fd0eaeb7fe835480b7a3faafbf3fc90a4f1bcf9321b9d3320d81a46999d445f857685ab2907750331e5e3ead1158eea8cea0b06960e557988c54eff624fbc", + "resolution": { + "resolution": "oniguruma-to-es@npm:4.3.6", + "version": "4.3.6", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + } + }, "optionator@npm:^0.9.3": { "checksum": "6df8225033dcef0e2f526c86df9611be8ac502700f3b20edba759fe3d3fd0f6f556f352c0f7a3daeb5fff9aaccd6b57778f4a423834138b2d59993a03da3895d", "resolution": { @@ -14708,7 +17885,7 @@ "version": "7.0.3" } }, - "p-queue@npm:^8.1.0": { + "p-queue@npm:^8.1.0, p-queue@npm:^8.1.1": { "checksum": "80ec8e1d3e8b8c17834dda1ce3ea3c5bc7d53636c93b40c715cc2189129cc68c7898f922cfd4846eb40d7705cb6ea036f26bc7f50985ba37c7ded5360c7ffef5", "resolution": { "resolution": "p-queue@npm:8.1.1", @@ -14733,6 +17910,34 @@ "version": "2.2.0" } }, + "pac-proxy-agent@npm:^7.1.0": { + "checksum": "a574fb97633f9c78cdc6f9131c542c32c96a12b03636f65227b6fa7396cc00f642c4ca0fea0aaec4fb8f199ef26e76696be2db22d057a0ab5ba018f7c1bce474", + "resolution": { + "resolution": "pac-proxy-agent@npm:7.2.0", + "version": "7.2.0", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + } + } + }, + "pac-resolver@npm:^7.0.1": { + "checksum": "9adab7dc8a2cebfa18b0301b61d7d0d980d6e7b70f6c5ce07da53fa61e14f0f1ffd66602fecbb3b2fd854b55f155eb9dd687712d965ac339ee08623bc68bc78f", + "resolution": { + "resolution": "pac-resolver@npm:7.0.1", + "version": "7.0.1", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + } + } + }, "package-json-from-dist@npm:^1.0.0": { "checksum": "6afa421103f83f8b8e399d0f368a7415e2395d21ba4d908ce99ee918769c792809bcf0c5cd3235a80d42e06fb11c0271d27e3143f3dc7fa02677548a6050052e", "resolution": { @@ -14747,6 +17952,13 @@ "version": "1.4.0" } }, + "package-manager-detector@npm:^1.6.0": { + "checksum": "9032bf89a4e10ca61d2f23f665cd288bb8bf255b25f868b0e246011aed37eb5218017267a1f86d45ac77b144833c28c11804ffeed95fcdf65f352341dc31cb3c", + "resolution": { + "resolution": "package-manager-detector@npm:1.6.0", + "version": "1.6.0" + } + }, "pagefind@npm:^1.3.0": { "checksum": "cdf015a8934ac5bc168564d0c6d14b5fea069656bbf2eb55ab01c28380a2d01eca806b77962ccb41f7c627c3c40fea8cd30d2993da4b306b5a75619d63081fec", "resolution": { @@ -14897,6 +18109,13 @@ } } }, + "path-data-parser@npm:0.1.0, path-data-parser@npm:^0.1.0": { + "checksum": "2cdd0330126edededb4163620b6e30fb8c626df63c21cced6a0d54c200b2d865c7adc5df9a36615256957cbe4595a1d62ee1443f4cbadcbbd755c188bb933940", + "resolution": { + "resolution": "path-data-parser@npm:0.1.0", + "version": "0.1.0" + } + }, "path-exists@npm:^4.0.0": { "checksum": "7a2f40d5a364e59cdcc69fcbe55b562991838ef5539fb72b994360f463f7ac4cbb2952f509e4c52a892570bfe6d4cdeb1b9194cad81b06a0988d06753203a528", "resolution": { @@ -14967,6 +18186,20 @@ } } }, + "pend@npm:~1.2.0": { + "checksum": "0cc9f0c5ce05fd80002b23a8995768fb183adb14c9a549226c750bfd39b76a5761ca55680fa015ecb0f0317dd67f6f15d0220931e9289ba6f3216afc703b7714", + "resolution": { + "resolution": "pend@npm:1.2.0", + "version": "1.2.0" + } + }, + "piccolore@npm:^0.1.3": { + "checksum": "0e3ba5440a25625d420f659782792218fd70a6efab8b4b23e3d80d3302e3cdcbd3df3acc24a75f1743146d4907d83e43715532879553c157d2880ca7840f301e", + "resolution": { + "resolution": "piccolore@npm:0.1.3", + "version": "0.1.3" + } + }, "picocolors@npm:^1.1.1": { "checksum": "62072f51e4012b4e9914672ed44a5514838f8debe4f3a09bea6877e3c7f35424cfea2a97d8c29ff166ca995634a89127a04a16816191c61aac03c73e55d95498", "resolution": { @@ -14988,6 +18221,13 @@ "version": "4.0.3" } }, + "picomatch@npm:^4.0.4": { + "checksum": "8078451b2c25e3c0768a48dffe06efadf8b304241b560733df6e9f188c51ba1844444741408bef1c7a79e6220299bf16e757f96ca1e2dff7bfe373acef8414bd", + "resolution": { + "resolution": "picomatch@npm:4.0.4", + "version": "4.0.4" + } + }, "pirates@npm:^4.0.4": { "checksum": "ed72c7f37b71c5ca559adddd548d43c4ad0d0d5691e48203c8ca0d18fd5f89e16a65fb57b774f9e3808fb9a2c527a302715bc16c5215502befa8af8dc389bd68", "resolution": { @@ -15005,6 +18245,24 @@ } } }, + "points-on-curve@npm:0.2.0, points-on-curve@npm:^0.2.0": { + "checksum": "2958b14a40ced7adc9522192ba25c0d9690bc974ed980126df457fff0a2d574b3c1ba2caa98f3c592d60295257486de5ca9f9e5c418f41c8ba32bed7d280d09e", + "resolution": { + "resolution": "points-on-curve@npm:0.2.0", + "version": "0.2.0" + } + }, + "points-on-path@npm:^0.2.1": { + "checksum": "29bca90fba5714e0b1d42ad0f0e4d67c2d69f20f608741e5a4935291ec6d9631b453aa74697a34c35c31877e4159b106782046559d7a8b6ab773e958b90852dd", + "resolution": { + "resolution": "points-on-path@npm:0.2.1", + "version": "0.2.1", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + } + }, "possible-typed-array-names@npm:^1.0.0": { "checksum": "f48493a85ebe9abe872d7f0a7e7662d613497e9b7f9f35223fb9dd12aec9fecbd72ca057d914a4bd41b0e5973ae1953ff72128814a6d095c4fe0c4ef10446c0b", "resolution": { @@ -15024,6 +18282,18 @@ } } }, + "postcss@npm:^8.5.14": { + "checksum": "1dbc0462fd8251d8be285ead3794aad387d28e3660bb777f81958f1da2e5784999ba3bf95f211841a3a1f68793573712a07fdbd12dd101984c0ca7e0fa35609c", + "resolution": { + "resolution": "postcss@npm:8.5.14", + "version": "8.5.14", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + } + }, "postcss-nested@npm:^6.0.1": { "checksum": "e69bbc196079ed36d5b4e7b2d6c1ad8811df71489e02710803d0ac6fed0d588f4e185a35440c04aa9305775187db8bb4eba9e9b85ee8a83647e3f153f5ee6078", "resolution": { @@ -15104,6 +18374,13 @@ "version": "1.2.1" } }, + "prettier@npm:^3.5.0": { + "checksum": "f085db460c4e2d3203be291a7b34455c6304be4dc4b76ae22ec7eefc01a5254fbe48ea1e37dffcc156854987311f9ffe91aee641f3697f482dc9db9b2b577796", + "resolution": { + "resolution": "prettier@npm:3.8.3", + "version": "3.8.3" + } + }, "pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": { "checksum": "57596383af06dbe45d0a3c25dea4ebce471742d01d619e543a2c68d7fad914eb3b924cf87e643baa603c7fb99ec93a930fe97a764cbfcfa0d0f994167f073d01", "resolution": { @@ -15151,6 +18428,13 @@ "version": "2.0.1" } }, + "progress@npm:^2.0.3": { + "checksum": "e6a7d88be50b12741da9506bdb61bed338346136bd4d9dbe05f3ab56a398463ccc4656dafa8af1a583844d054c5bf6ba9634499da30200b5c88f2c3634af99d4", + "resolution": { + "resolution": "progress@npm:2.0.3", + "version": "2.0.3" + } + }, "promise-retry@npm:^2.0.1": { "checksum": "bda7c3542f522575e29adb6a1737ff60475fae860a5425772d2385d03b6e07947fbafbce971790e3a2b915db209621fd3b52ee9b48738e5253ee3dfd7d948481", "resolution": { @@ -15206,6 +18490,30 @@ "version": "2.0.2" } }, + "proxy-agent@npm:^6.5.0": { + "checksum": "04c3ddfeec796fc4533b8c8ccd502a05209339e5e6f0619993158efe1180ff6cf80143d777f4a4ff46b65225618790d9364dccbf6f4d4b74ae8e20c202272c78", + "resolution": { + "resolution": "proxy-agent@npm:6.5.0", + "version": "6.5.0", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + } + } + }, + "proxy-from-env@npm:^1.1.0": { + "checksum": "dc63eea0a2132a877d33f656b3b8b049e5bf205667af759da1a66e6823675a31052ee460ee237f827988e087a4331cdb79cb2e0e4bfb9a798ed3cbe9310b0ad6", + "resolution": { + "resolution": "proxy-from-env@npm:1.1.0", + "version": "1.1.0" + } + }, "pump@npm:^1.0.0": { "checksum": "ea557fb51feb9acd8cd8dca98362da78fb80bfbaadb57b0f74674bb0a78929b076d8b75b25ad58e28fb6782e9314f5482b76f46ab3e981cf10b204ce51abd2db", "resolution": { @@ -15235,6 +18543,37 @@ "version": "2.3.1" } }, + "puppeteer@npm:^24.42.0": { + "checksum": "0c155e53bad7afee3f1f780a22fb19442dffba4c0599bd7ea0d990de1b1fc8e24bdb0f2c55827a5905e5c7c334763de7960b72cc4b5ddd36a0aa03497c13afe0", + "resolution": { + "resolution": "puppeteer@npm:24.43.1", + "version": "24.43.1", + "dependencies": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1608973", + "puppeteer-core": "24.43.1", + "typed-query-selector": "^2.12.2" + } + } + }, + "puppeteer-core@npm:24.43.1": { + "checksum": "cc5342da72cb6c5a0b433ef1e275dda102f27fde35fba4e1253aa2f65ff5a564e8a738161687408a5ffd0d2d8755b397013bb32f143679859d66bcd0103e0398", + "resolution": { + "resolution": "puppeteer-core@npm:24.43.1", + "version": "24.43.1", + "dependencies": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" + } + } + }, "pure-rand@npm:^6.0.0": { "checksum": "c61a32aaffb10e86e32ff687e92b87bf15d85739194a58c28af97663adc1801de8fcef0390a4e62fdc8fa30aee721662483395553ddc104b83664ac7697177e2", "resolution": { @@ -15323,6 +18662,13 @@ } } }, + "react@npm:^19.2.5": { + "checksum": "7522c126ffa4219b317087a606ce50f91f70dc54a0bf1ea64bc8ab43d565eb43a3d1460b3078928bd19d5a178391fdc953e8696471fded64ff0acdb7c6f742b0", + "resolution": { + "resolution": "react@npm:19.2.6", + "version": "19.2.6" + } + }, "react-devtools-core@npm:^4.19.1": { "checksum": "deaf373110ffba404a133edf31214ff21bf7380783675e303eb8599a7fb9a71d7116e2027299a0b7023252bf477013d26e8117a5f59e7fe89e3b002234cad496", "resolution": { @@ -15348,6 +18694,19 @@ } } }, + "react-dom@npm:^19.2.5": { + "checksum": "d05a0823690bacca2b4fd684d4b3aed3f7e60d7092e09055ff86d11016ecaf11b93b16c66bef89333a2f1e004c746b07f51d2aedde8dce6e540a2e7081a16b20", + "resolution": { + "resolution": "react-dom@npm:19.2.6", + "version": "19.2.6", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + } + }, "react-instantsearch@npm:^7.16.0": { "checksum": "3be2970ec6f647847f80a50c243b84a58ac79a290f5211746c9946c77cff0d853d248d6ce21351cbbdd8e8ebfdd9e05a21d11e98d09c7f0a90b8e0b56f58c1cd", "resolution": { @@ -15515,6 +18874,13 @@ "version": "4.1.2" } }, + "readdirp@npm:^5.0.0": { + "checksum": "cf2af08e50628c61109ffef9d6b17dfc6e01f1a8f7236809a04bd159936a5210beb05bed36bff3f4ef3151eb168647d1c07de6ac2ba871a35a398af3c8b51b6f", + "resolution": { + "resolution": "readdirp@npm:5.0.0", + "version": "5.0.0" + } + }, "reading-time@npm:^1.5.0": { "checksum": "6dfba46e8073c19d2e6d474c24c1cc2814f8ef43350b7ce1d7dc687b3d9bb179947ca6f121f74d7e7384a5bdfa71dd3a7f61b323b02f73ea11511c06e9384000", "resolution": { @@ -15653,6 +19019,16 @@ } } }, + "regex@npm:^6.1.0": { + "checksum": "b2eae1bb8d8317abf79c42aa393dc795349fb3a55eca76b3f636a6f62d0d411eb74c138b83394398dc6730f79ecd5625b074de1e61a923fa1cc14550f9936236", + "resolution": { + "resolution": "regex@npm:6.1.0", + "version": "6.1.0", + "dependencies": { + "regex-utilities": "^2.3.0" + } + } + }, "regex-recursion@npm:^6.0.2": { "checksum": "3bc7a2ba3ca9bc4a94d34d0caafbb8fa5217f466031d96b3e1146ce69664ec6aa95dc63e6d8e9852f20d0fcce89d9c0a9ef403d27aa48a50a69da38383dd80bf", "resolution": { @@ -15854,6 +19230,19 @@ } } }, + "remark-directive@npm:^4.0.0": { + "checksum": "4a0cb315e9e5cf29772fbb2d0f18a5f7368a7e4c728d0750c304d6ee099f2f62cff44705272b7ed9ec0e7aea2acb054eea7302eb33a556e2911663833c69fd74", + "resolution": { + "resolution": "remark-directive@npm:4.0.0", + "version": "4.0.0", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^4.0.0", + "unified": "^11.0.0" + } + } + }, "remark-gfm@npm:^4.0.1": { "checksum": "d5f4ce9bfa122f93b56823d89eb74f0228c914183cea488730d98418fbb55e707b004b393a23b75a9d7e6edfd509a2a31013f502a5c432b08b05bac30a76edc9", "resolution": { @@ -16118,6 +19507,56 @@ "version": "1.1.0" } }, + "robust-predicates@npm:^3.0.2": { + "checksum": "17161d16dbd7839d87982e375a3c165a934dbddf5d7c1c8caaeb0c202687cbfe8a1a7712a07b9c1dcf60f4d924f2ecf48640facbacc9c6e97fb4002694a4bf55", + "resolution": { + "resolution": "robust-predicates@npm:3.0.3", + "version": "3.0.3" + } + }, + "rolldown@npm:1.0.1": { + "checksum": "afb97e5cd50170eb9fe7b6f6ba53c5190756747684a86dc5623d13eae3aeca1011664cfe197fa0e9c4f7f156fb84fab1944b2f7d4117df2c76c7e0494c191f51", + "resolution": { + "resolution": "rolldown@npm:1.0.1", + "version": "1.0.1", + "dependencies": { + "@oxc-project/types": "=0.130.0", + "@rolldown/binding-android-arm64": "1.0.1", + "@rolldown/binding-darwin-arm64": "1.0.1", + "@rolldown/binding-darwin-x64": "1.0.1", + "@rolldown/binding-freebsd-x64": "1.0.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.1", + "@rolldown/binding-linux-arm64-musl": "1.0.1", + "@rolldown/binding-linux-ppc64-gnu": "1.0.1", + "@rolldown/binding-linux-s390x-gnu": "1.0.1", + "@rolldown/binding-linux-x64-gnu": "1.0.1", + "@rolldown/binding-linux-x64-musl": "1.0.1", + "@rolldown/binding-openharmony-arm64": "1.0.1", + "@rolldown/binding-wasm32-wasi": "1.0.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.1", + "@rolldown/binding-win32-x64-msvc": "1.0.1", + "@rolldown/pluginutils": "^1.0.0" + }, + "optionalDependencies": [ + "@rolldown/binding-android-arm64", + "@rolldown/binding-darwin-arm64", + "@rolldown/binding-darwin-x64", + "@rolldown/binding-freebsd-x64", + "@rolldown/binding-linux-arm-gnueabihf", + "@rolldown/binding-linux-arm64-gnu", + "@rolldown/binding-linux-arm64-musl", + "@rolldown/binding-linux-ppc64-gnu", + "@rolldown/binding-linux-s390x-gnu", + "@rolldown/binding-linux-x64-gnu", + "@rolldown/binding-linux-x64-musl", + "@rolldown/binding-openharmony-arm64", + "@rolldown/binding-wasm32-wasi", + "@rolldown/binding-win32-arm64-msvc", + "@rolldown/binding-win32-x64-msvc" + ] + } + }, "rollup@npm:^4.34.9, rollup@npm:^4.43.0": { "checksum": "2cf94e91e828644c9ad5f244ffff93e8f4768b671486903e14b3c3fec818ae8676023e3f8c278e0c3a79d2d8ac566d12f842d79a67fe75b57740f8a3c62dccb6", "resolution": { @@ -16176,6 +19615,19 @@ ] } }, + "roughjs@npm:^4.6.6": { + "checksum": "d1b3b11c5354a816f93f7ddb639f13460a921ad93a4741235d1e858bf64386acda6ccf67df5bde3e876e40488e6272914c782792f1d0c038214aa7f692be5bb4", + "resolution": { + "resolution": "roughjs@npm:4.6.6", + "version": "4.6.6", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + } + }, "run-parallel@npm:^1.1.9": { "checksum": "93fbfc4a290b4dcccd7703e7f6730ed473060a7d81445a37752bd6c95bf401661561be30e423ff69002c5c8882a7066fce7ff337631da15c41f498451f26e653", "resolution": { @@ -16186,6 +19638,13 @@ } } }, + "rw@npm:1": { + "checksum": "869b97e27256ecbb4f5a0cec1368e34195e4f2f03cf4008de5c6d35e676444bec798547c3221abf4860972a4452adf6c17c8da39886d701d8c54cec40f2cac98", + "resolution": { + "resolution": "rw@npm:1.3.3", + "version": "1.3.3" + } + }, "safe-array-concat@npm:^1.1.3": { "checksum": "3c5733fd84d4326744f6fc516eb3325b8737f7c89529488bf232326db3e26732b97820a0f2c316c9ac259bf2745a509e0af323e9e85ef1b25cf6090c950a8771", "resolution": { @@ -16237,7 +19696,7 @@ } } }, - "safer-buffer@npm:>= 2.1.2 < 3.0.0": { + "safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": { "checksum": "ba55b2164fe11138b4d9b8657de709e0865c5026e027be5ed63bde3752f2fc0bdea010945af988da4cdeeea1c56d461627f26e620f158de28cc86676772292ee", "resolution": { "resolution": "safer-buffer@npm:2.1.2", @@ -16251,6 +19710,13 @@ "version": "1.4.1" } }, + "sax@npm:^1.4.1, sax@npm:^1.5.0": { + "checksum": "3d46c61bd8c9efe95d62481fa27fde8052bf9f8dd9502bc149126288a70f50ba074447a9dbc8af941f1afd8b6cb05a0316bbe187ca4b70355ecc67979582c032", + "resolution": { + "resolution": "sax@npm:1.6.0", + "version": "1.6.0" + } + }, "scheduler@npm:^0.20.2": { "checksum": "5d7e247d4779f3a2a445dc4ff10559423a71477f2ff82e2cda44719efff6ab897e06cd205f70976064675acfd04b11c79146adb9cd251e41325a1034a226bb95", "resolution": { @@ -16272,6 +19738,13 @@ } } }, + "scheduler@npm:^0.27.0": { + "checksum": "ffb4eec9248b93b62c6841a03a5cefeeadc3ca2e2629c04c7445009f713c011e16b30ba730968f7cf6610f7ebb1cd3ecd396b5a3aa2683a6de52882e5784751a", + "resolution": { + "resolution": "scheduler@npm:0.27.0", + "version": "0.27.0" + } + }, "search-insights@npm:^2.17.2": { "checksum": "26996ffb1b511cc0c774bcd39cf0cfdde4ede721bf450cb6a32acb53f422450bb1149ba75a397161592d3e4fb0bc9ee4c4f2d73389dd549e5f6121a21872246f", "resolution": { @@ -16300,6 +19773,13 @@ "version": "7.7.3" } }, + "semver@npm:^7.7.3, semver@npm:^7.7.4": { + "checksum": "1b73375272e48c7a0eb5383ae2e0767571e0ce75ccccbe884ca23405bb360c02ba32cdaa00f8f1fac7adc202353548752f0f3a4abb4303a3c735b84a131cc19f", + "resolution": { + "resolution": "semver@npm:7.8.0", + "version": "7.8.0" + } + }, "send@npm:~0.19.1": { "checksum": "f726cad655675cf3b2650007b4893af2e25a4e3f9b2e180536d69428e32a06d99418563e8633f445aacc1cefc16c0f449faf4a2792d75d72d3446cc300562b9b", "resolution": { @@ -16329,6 +19809,13 @@ "version": "1.5.2" } }, + "seroval@npm:^1.5.4": { + "checksum": "3711595e7faec93061241401eea1a240979887c5d5a305f16da3b9d3696d32ab633f9b52ea24420914d2822ea21bc720183c4c43e832e393374774565ba6a2f0", + "resolution": { + "resolution": "seroval@npm:1.5.4", + "version": "1.5.4" + } + }, "seroval-plugins@npm:^1.5.0": { "checksum": "193da9194b4622129c8293a9a7f9e8bf1a78e5870e58339c898d7fd936927fafbea71c192b40eb6fca30626e1d58885dba9749c897d8b2ca0d672a3fe2c603d5", "resolution": { @@ -16339,6 +19826,16 @@ } } }, + "seroval-plugins@npm:^1.5.4": { + "checksum": "ac01ceea8c8ebce0ee5323b92e74dd9f56374c448f5876159cab9fe958a22a48bc03206a275662948e0731d05717a566ce87f7abff6a52da1bd75820994c4695", + "resolution": { + "resolution": "seroval-plugins@npm:1.5.4", + "version": "1.5.4", + "peerDependencies": { + "seroval": "^1.0" + } + } + }, "serve-static@npm:^1.14.1": { "checksum": "f3f357874b415ac7952891e003a8643e0d152a5594f5ad910cb4ff41418ace6e85cb8f2850307e01e01a30d989c2490d14a8289590fcbb602aed92e758eff83a", "resolution": { @@ -16516,6 +20013,40 @@ } } }, + "shiki@npm:^3.21.0": { + "checksum": "2611369952c269f1a85acaa76d9b93a7d3b863c83cbd1edcd1c32d3cc9665a67bd0a64947da025c13f4708b9d4c9c5518b9c3e01c19593b4829697595d034102", + "resolution": { + "resolution": "shiki@npm:3.23.0", + "version": "3.23.0", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/engine-javascript": "3.23.0", + "@shikijs/engine-oniguruma": "3.23.0", + "@shikijs/langs": "3.23.0", + "@shikijs/themes": "3.23.0", + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + } + }, + "shiki@npm:^4.0.2": { + "checksum": "2280a33b187fcd770c0fc88fd235909526c0825f41d3059f7458f5a702aa5d796d55119bf157786291b4d03af5aa159659102102ddee63a3068ad4ff12cb09f8", + "resolution": { + "resolution": "shiki@npm:4.0.2", + "version": "4.0.2", + "dependencies": { + "@shikijs/core": "4.0.2", + "@shikijs/engine-javascript": "4.0.2", + "@shikijs/engine-oniguruma": "4.0.2", + "@shikijs/langs": "4.0.2", + "@shikijs/themes": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + } + }, "side-channel@npm:^1.1.0": { "checksum": "ac71658729abbcc911f1413811961754ac73a5441e4f361616ac0b348e52e007a226b1384c792f7635ab4e6b846d79671ae37115fc92ecfbcf68098638c70e28", "resolution": { @@ -16671,6 +20202,19 @@ } } }, + "sitemap@npm:^9.0.0": { + "checksum": "1b5dfbb94ceb076a0661a30c91405980c3f51bbe2423c06ac37d0dc1a4e1f51e7e65f7348543a3165df7d2269a3df2b92e8f8431e714a85fb7acdd10a85c286f", + "resolution": { + "resolution": "sitemap@npm:9.0.1", + "version": "9.0.1", + "dependencies": { + "@types/node": "^24.9.2", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + } + } + }, "slash@npm:^3.0.0": { "checksum": "eb76b05e7d28b5d6ed412cbb563da6e694d9bc3c145fe20e9cf41d5531d047257f9a29e07ff80af12d96c11fe3dba7a3106b707c242f764954bd1075030bcd96", "resolution": { @@ -16704,6 +20248,13 @@ "version": "1.4.2" } }, + "smol-toml@npm:^1.6.0": { + "checksum": "f800b199c192b1374bb72f0909bb8bfe85d7e91bcfcd59cc62bb4e7d9ba3fe1cef39aef3b0539d176c9847fb7c8f90caa7a39e3acf24a3474a65f274a591e007", + "resolution": { + "resolution": "smol-toml@npm:1.6.1", + "version": "1.6.1" + } + }, "snake-case@npm:^3.0.4": { "checksum": "14df35a73844726b5093ad55f716c9ec18f9269e675239ea681c683cc9219555df7673e98e4ba435ff1f5304813b5775a50ba6b64a31608fbd783047f76b6c9c", "resolution": { @@ -16726,7 +20277,7 @@ } } }, - "socks-proxy-agent@npm:^8.0.3": { + "socks-proxy-agent@npm:^8.0.3, socks-proxy-agent@npm:^8.0.5": { "checksum": "05c6dad880d7cdcc4d132f93c5b227dd5d102cc08d9a56979b3384a314eb64237a0093db8e471951c851272b2aba3e8dfb0c630670fac09b500d62a000c83895", "resolution": { "resolution": "socks-proxy-agent@npm:8.0.5", @@ -16738,7 +20289,7 @@ } } }, - "source-map@npm:^0.6.0, source-map@npm:^0.6.1": { + "source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.1": { "checksum": "d4afdaaea31a9ec1adc9e64cedca00ce80de0bd06c6389b7fb6892fde6d851860e6ca7b534c78f604d5f2556e487e080ddbac2f36680bec66446565d2ddf3d91", "resolution": { "resolution": "source-map@npm:0.6.1", @@ -17108,6 +20659,13 @@ } } }, + "stylis@npm:^4.3.6": { + "checksum": "7f19420020311cbe64ad9e7de4a9bad4f577010e939c96c89019112d2aad1799082af875cc5328ffa5c3c023d2ca35e896ff957cd7df63a2c8611fc3f9474f12", + "resolution": { + "resolution": "stylis@npm:4.4.0", + "version": "4.4.0" + } + }, "super-resolve@npm:^1.0.0": { "checksum": "812f9074a5948a68f6d308551a20008c004deb4b927301e00c7b2c8c922bcef3b0d7f7896be7655f90ade6b556ca28a447c2bef3f5832397412ca83a2ace4c61", "resolution": { @@ -17155,6 +20713,22 @@ "version": "2.0.4" } }, + "svgo@npm:^4.0.0": { + "checksum": "e4172f03915bc3b56ff0cc858171a89c1078a42b2aff0f51b116adda9bc8976b7a162e4b403587deabfbbdbe9429d46487b0714a38a2a5373c8d309cf312af41", + "resolution": { + "resolution": "svgo@npm:4.0.1", + "version": "4.0.1", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.5.0" + } + } + }, "swr@npm:^2.2.5": { "checksum": "a3b3763dc93e0743101bfe9a973cb7a8750396330e3347ffb33fbf5b0e0e59c7866bf2ba4511a52f5821c77570d05547418f66a9620d428c7d9a6854f9240f35", "resolution": { @@ -17190,6 +20764,13 @@ "version": "4.1.14" } }, + "tailwindcss@npm:4.3.0, tailwindcss@npm:^4.2.4": { + "checksum": "82b3bf7a55f90341fbd4c6208a4deae3fb9a3824c5effbd00b85ec9263b6d0e0004e3706ae3140d8453e360d36a08dfbce98431de41c005b31b38f3dcf51a5e9", + "resolution": { + "resolution": "tailwindcss@npm:4.3.0", + "version": "4.3.0" + } + }, "tailwindcss-scoped-preflight@npm:^4.0.1": { "checksum": "891b17ceaa3b78889a5494b855d81bafc1841d23e67cd476e3927ddd0d74619d5e976bb3116b2864295fbce53fdbc40247eec3ddd17fb3b0cbd3472c323ad9d5", "resolution": { @@ -17208,6 +20789,13 @@ "version": "2.3.0" } }, + "tapable@npm:^2.3.3": { + "checksum": "86acaca85652d33157bcd8f8506e266b7d9c7781e706c77ca7fdcd85d3b3a8cac9b6332c60162bf0d0b7fb7e00e00d62124ec3e742ff15fcb98de2b16417491e", + "resolution": { + "resolution": "tapable@npm:2.3.3", + "version": "2.3.3" + } + }, "tar@npm:^6.0.5": { "checksum": "f24bbad32515cb88c617dc6f80e248bbbc80439902d0c4cb33b2a9d359d9fbc65a43330c51a9f90abe3e20b079d430e01eb5f31454f886f72b2e328f4ac90f7f", "resolution": { @@ -17280,6 +20868,23 @@ ] } }, + "tar-fs@npm:^3.1.1": { + "checksum": "498a9a2ccf2e89eb035cca83a02a5c90cc884ea16be4d5659066eca9309a9290345e9b82ffed75bc5fade455943b9da4c11fa2915e8621c875f32c23e7bf82d3", + "resolution": { + "resolution": "tar-fs@npm:3.1.2", + "version": "3.1.2", + "dependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": [ + "bare-fs", + "bare-path" + ] + } + }, "tar-stream@npm:^1.1.2": { "checksum": "955412124e7be6789dbe290e04e5bbd490fac5774ef3b023587b6e024604d6d289800a79ca1e411cbea9b5501a5c3389ccd05b8b8d75ec4da6a7cb99a94cdcf9", "resolution": { @@ -17401,6 +21006,13 @@ "version": "0.3.2" } }, + "tinyexec@npm:^1.0.1, tinyexec@npm:^1.0.2": { + "checksum": "1e09c4df2e9da64382604d666805467ecf97c56147704e669c107ba75d66dbc004709565c2d8f03ee2405db7ddc9bfd21d9f34458ef0860f079b44680da71a45", + "resolution": { + "resolution": "tinyexec@npm:1.1.2", + "version": "1.1.2" + } + }, "tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": { "checksum": "57467da2137c5d1ae705ef8efee6edbe0558142d5d87bc35d09edde2325aa607e3a3f98c625bf7ab05de7e2f7f5bcd058178d5896232fa0525e47a3e499a8466", "resolution": { @@ -17412,6 +21024,17 @@ } } }, + "tinyglobby@npm:^0.2.16": { + "checksum": "3bca80c1157a64b0092b39c823bd16a93e70d7c6ea19a7bbedc8fdf3c5b105b6686696bf7f84bad5efe4f5308071ff85f038ce1ee98df69842a35a9c40d9c3d2", + "resolution": { + "resolution": "tinyglobby@npm:0.2.16", + "version": "0.2.16", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + } + } + }, "tinylogic@npm:^2.0.0": { "checksum": "3c1afbf41b70a2171fbdd7dff4b17f193a04e9bf0a0ff2e36afd98e95c2463aa5491494d24a54f0a03ba1cab99ed9bab896aac905be650e8a6290a900805eb0b", "resolution": { @@ -17500,6 +21123,13 @@ } } }, + "ts-dedent@npm:^2.2.0": { + "checksum": "a4c0c5db2049d7314bd1a1421b0d94553b6f505ce346b09cf73241680eb3d2a645aa00745093326e766eb6285719d90821bac89ba758e4f38e76b3200b193577", + "resolution": { + "resolution": "ts-dedent@npm:2.2.0", + "version": "2.2.0" + } + }, "tsconfck@npm:^3.1.6": { "checksum": "936952cb243fdafdc0f191e94abb2083502b99104d530c7c6e97ab25531c0c2275125d01d701adc703e41392bc806b1cb6284803e0e93f1164d95ebfd5d959ed", "resolution": { @@ -17513,7 +21143,7 @@ ] } }, - "tslib@npm:^2.0.3, tslib@npm:^2.4.0, tslib@npm:^2.8.0": { + "tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.4.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1": { "checksum": "93bfec26e84243600f2fd0e9426bfca813cc6a22ecafecce1b938b4fd53f9fd0f1a569686908e7b79c6bd3bdb2ff87c7238994e5ffaf924ab3ffbfaca45c2e5d", "resolution": { "resolution": "tslib@npm:2.8.1", @@ -17663,6 +21293,13 @@ } } }, + "typed-query-selector@npm:^2.12.2": { + "checksum": "3065b7cf5438d926fee96bf2e5d47b60d3763903df4cfdff62ad4f90569789fcf169778dadef18775ee758b1ae7ad2c4d42d540617c565888e2d29d5b0bba46a", + "resolution": { + "resolution": "typed-query-selector@npm:2.12.2", + "version": "2.12.2" + } + }, "typescript@npm:^5.9.3": { "checksum": "26beee1db38cf838f7d6c16ac74325c7952be3714903bff712af2c9493d64a0882c841f72d7b66a18ae586975c445085333f63f150acb66d74cd47d47c6b7193", "resolution": { @@ -17677,6 +21314,13 @@ "version": "1.6.1" } }, + "ufo@npm:^1.6.3": { + "checksum": "676eef24f6cfdd4d8e6d90c188cf3f1d267287dc0770d451ed046a6ca0e5d0fc7acc7d9cf53bdc16266e7bdd5f8722da6a46ed8576898855621260492bbcfdb0", + "resolution": { + "resolution": "ufo@npm:1.6.4", + "version": "1.6.4" + } + }, "ultrahtml@npm:^1.6.0": { "checksum": "4a8aaee6b0a29b7e42a05d6b712579dd1a3a061d2120c102c4011062d8ee72520695fb591a35e8e1fdcad5a08c83cb1b204bceff00b7244aa2fcea2a8c3921a5", "resolution": { @@ -17761,6 +21405,18 @@ } } }, + "unifont@npm:~0.7.3": { + "checksum": "f4a64f1c1fcf461855b252c8e951ca629eb54e1272f3fe93dc33b04a7149911c1d6855ae15e9a605c999f09245f1cb5c547bdad2aa7835225330b4d9b642f830", + "resolution": { + "resolution": "unifont@npm:0.7.4", + "version": "0.7.4", + "dependencies": { + "css-tree": "^3.1.0", + "ofetch": "^1.5.1", + "ohash": "^2.0.11" + } + } + }, "unique-filename@npm:^4.0.0": { "checksum": "0138a9552813448bee41a1fa10ae54847805b7730960c1ef5bb9139313db0bbb3bc7d8631e55782e7c8ad24fdf7d404cca0790999347f99c6ae7c479c8e9ee07", "resolution": { @@ -17893,6 +21549,18 @@ } } }, + "unist-util-visit@npm:^5.1.0": { + "checksum": "855276dfb96c58c8f810968724e2229e028708fbef56d4a046c2a2739b8db02de11391b2bfd23d6dcc0c9372ddcbb60bf358375a0c3e30f96ec955b8a6b321e7", + "resolution": { + "resolution": "unist-util-visit@npm:5.1.0", + "version": "5.1.0", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + } + } + }, "unist-util-visit-children@npm:^3.0.0": { "checksum": "662fcee7ec53f1abf12d9436cb3ac91fe8c3b1598f1ce8061b4b691a905a2de7c707ada90d096a0c15db464584091dae082996d89affbd655ae5be69b750759d", "resolution": { @@ -17914,6 +21582,17 @@ } } }, + "unist-util-visit-parents@npm:^6.0.2": { + "checksum": "154aeb996f78864748db1addba1beba7d07302dd2d37903723da1f1eb4cd9ff7eb032c81e0a51657b08468729036cf637cf3b535524c7c182f60b120f22a1c73", + "resolution": { + "resolution": "unist-util-visit-parents@npm:6.0.2", + "version": "6.0.2", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + } + } + }, "universal-github-app-jwt@npm:^2.2.0": { "checksum": "383a842f1b62e3dccc2e3cc36e6e84ed49ade196e1e04a08c83eb2b6e93d33a62bb7698c03f97de39dfffe6f72e63282185db9cc52c9d7f2ab3f8f4fe65e24d8", "resolution": { @@ -17994,6 +21673,65 @@ ] } }, + "unstorage@npm:^1.17.4": { + "checksum": "7adcf32e2a2923f6be417167441abc545293d8f4521f612b90f82aa7b6826630b8433c0c0362cd717afffd941c5eb21edecbcf9668da8f187dc062572aef6fd6", + "resolution": { + "resolution": "unstorage@npm:1.17.5", + "version": "1.17.5", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "optionalPeerDependencies": [ + "@azure/app-configuration", + "@azure/cosmos", + "@azure/data-tables", + "@azure/identity", + "@azure/keyvault-secrets", + "@azure/storage-blob", + "@capacitor/preferences", + "@deno/kv", + "@netlify/blobs", + "@planetscale/database", + "@upstash/redis", + "@vercel/blob", + "@vercel/functions", + "@vercel/kv", + "aws4fetch", + "db0", + "idb-keyval", + "ioredis", + "uploadthing" + ] + } + }, "update-browserslist-db@npm:^1.1.3": { "checksum": "054445521f0ba8c09c98dedef888ba12e234c5304275946d7c8b2ab9eaf7ecc7358172cd8f5c302a6285ea24bc6a4e8f83edb56eb7d1d60d1ff61d4eed9e636a", "resolution": { @@ -18058,6 +21796,13 @@ "version": "1.0.2" } }, + "uuid@npm:^11.1.0 || ^12 || ^13 || ^14.0.0": { + "checksum": "c50394346c05917923d79fb27fd9dab82bc1b2522093c920ae7b7cf9a6a41380013cba9b263b0d2c5318d6dbd23f4d298c9f0dead3bcdb373db6a94a2fd62ae5", + "resolution": { + "resolution": "uuid@npm:14.0.0", + "version": "14.0.0" + } + }, "uuid@npm:^8.3.2": { "checksum": "762d4cb86a0e0baa4c6979097ede3fbfe14d71da8b7e04f52699bc1033e52a7db527dd88f44c16b066135b9b2bf191c739d3bce033b092e3755db8cbe5cc42a3", "resolution": { @@ -18178,6 +21923,51 @@ ] } }, + "vite@npm:^6.4.1": { + "checksum": "5a374b8f8ececc605fa83b49edb0f1582018179955a617e4178820ac2afed227b6aeb783aeea6e5b294f26edece4420a7a331e6bc6eda0503f179fb997f31405", + "resolution": { + "resolution": "vite@npm:6.4.2", + "version": "6.4.2", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "fsevents": "~2.3.3", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "optionalDependencies": [ + "fsevents" + ], + "optionalPeerDependencies": [ + "@types/node", + "jiti", + "less", + "lightningcss", + "sass", + "sass-embedded", + "stylus", + "sugarss", + "terser", + "tsx", + "yaml" + ] + } + }, "vite@npm:^7.1.7": { "checksum": "fe228751780c586144283ba05f6ed9def06a6697d98c8c27ddeb3152f4f02df78bc34d037ba8ea963968a8d0d7aa9a34e9e9381671782f475954098a41422483", "resolution": { @@ -18223,6 +22013,97 @@ ] } }, + "vite@npm:^7.3.2": { + "checksum": "c21d2662a4acf4e0ceca9230e3bb809f055812db01412a4b331c3d850fa35bfaa0648fd8700f73ca683087aa6d2e5f0ef483e010248d07beaad83e513bbdfc25", + "resolution": { + "resolution": "vite@npm:7.3.3", + "version": "7.3.3", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "fsevents": "~2.3.3", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "optionalDependencies": [ + "fsevents" + ], + "optionalPeerDependencies": [ + "@types/node", + "jiti", + "less", + "lightningcss", + "sass", + "sass-embedded", + "stylus", + "sugarss", + "terser", + "tsx", + "yaml" + ] + } + }, + "vite@npm:^8.0.9": { + "checksum": "16bf995f7b42eec39524c070a42ff71de7b9a7ff155c234b808ec8a1b79f5fd18c3e7e02bdd2c346779f8d072e3af6c141fe7417b177d90b456b999be0dd0916", + "resolution": { + "resolution": "vite@npm:8.0.13", + "version": "8.0.13", + "dependencies": { + "fsevents": "~2.3.3", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "optionalDependencies": [ + "fsevents" + ], + "optionalPeerDependencies": [ + "@types/node", + "@vitejs/devtools", + "esbuild", + "jiti", + "less", + "sass", + "sass-embedded", + "stylus", + "sugarss", + "terser", + "tsx", + "yaml" + ] + } + }, "vite-plugin-svgr@npm:^4.3.0": { "checksum": "0b6f8e787eaff6863c98e7f2ce1646f86e144401496d86c24e0fd6d4453eec4f431fd89358b989e59360f72426d14b794e6e0baecb90f6b2acc8e65710056920", "resolution": { @@ -18286,6 +22167,13 @@ "version": "2.0.1" } }, + "webdriver-bidi-protocol@npm:0.4.1": { + "checksum": "2202df872967b45e869006893dfe6eb01606ea7c364a6afa0cacfe711ed3d19ade2075df87e967c531b07a9fa8ad01205db01060c1073bbf851b469074fef3ec", + "resolution": { + "resolution": "webdriver-bidi-protocol@npm:0.4.1", + "version": "0.4.1" + } + }, "webidl-conversions@npm:^3.0.0": { "checksum": "0754c4a1e17fed9ad2459d324b9ac7974eb04b78f60af3a73272fb4fe93c66cf468fd7d89dc130285c87b11b743f97a1fb4689d932640934f328b1acf2802201", "resolution": { @@ -18504,6 +22392,21 @@ ] } }, + "ws@npm:^8.20.0": { + "checksum": "5d930c49351ee5dc4869d81f4e6a91de9ea1dc164095afeaf5e51dace622ff8af30d037567914f23f63dc50d0af77ebd76805dcb3fd7a2d33a2d8bf753bb59fb", + "resolution": { + "resolution": "ws@npm:8.20.1", + "version": "8.20.1", + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "optionalPeerDependencies": [ + "bufferutil", + "utf-8-validate" + ] + } + }, "xtend@npm:^4.0.0": { "checksum": "a94e826182d2f9cfcbb914b7e0b8651731b747818c2f7c6cddb5ceeda32b1909c8edf33f0db99d7a8f7c61b14ed0783f200c510dcac09fa3c6ff2c7080185c5f", "resolution": { @@ -18546,7 +22449,7 @@ "version": "5.0.0" } }, - "yargs@npm:^17.3.1": { + "yargs@npm:^17.3.1, yargs@npm:^17.7.2": { "checksum": "90285acb8f982850e4ad0a208aba09e68b63b2ef6e8d5b8f4119c405769b5d72d42a99c9889d092fc8a6b468b46ba0e12cc6b383da0158439a86a556843edbce", "resolution": { "resolution": "yargs@npm:17.7.2", @@ -18569,6 +22472,17 @@ "version": "21.1.1" } }, + "yauzl@npm:^2.10.0": { + "checksum": "209102714cd0a703123747e5da5fd7c06cc393b8bdba2eb7620067206ed73f13ad08a64bb7986c4fb34f222fee7ac0c877bcd1d6c7551a96d2aef97d39bd558d", + "resolution": { + "resolution": "yauzl@npm:2.10.0", + "version": "2.10.0", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + }, "yocto-queue@npm:^0.1.0": { "checksum": "c368954f443451dd07f100a06a38ae10182a6ec8e9a2865ab0b9c9d3d4ab66d675288c2f136ef1a26104ae7020da75ea6e15dd2978ffef595ed27ff85d93e54d", "resolution": { @@ -18610,7 +22524,7 @@ } } }, - "zod@npm:^3.25.76": { + "zod@npm:^3.24.1, zod@npm:^3.25.76": { "checksum": "bc70ea2c1d4b2bae7172084b16d964e58edb05a2c8e3df63079c0f64c592e80013a7d7d4462a0bf186426f052e57780b4c1afe388543dc6630780ebe850125b5", "resolution": { "resolution": "zod@npm:3.25.76", @@ -18624,6 +22538,13 @@ "version": "4.1.12" } }, + "zod@npm:^4.3.6": { + "checksum": "8d5f746b783362f5fd0942816a6a9ac7166a0e2d8362e72b58cef2c4b5de0bb477fa64a76f0304ed8580ffa761b57d3519787fb0ea64660804619ff9cd5b6a16", + "resolution": { + "resolution": "zod@npm:4.4.3", + "version": "4.4.3" + } + }, "zod-to-json-schema@npm:^3.24.6": { "checksum": "39036649c38d3fccd115f38b13a669d00ba8fca1caf0c098fc5381a3e5890b4b7452c7ee815cd5f0a4351edf53d5e26768ae87486c62413fbccae7ea5e0f2761", "resolution": { @@ -18634,6 +22555,16 @@ } } }, + "zod-to-json-schema@npm:^3.25.1": { + "checksum": "5ebb28a39e30ed2d928b07d6c81b7cd59b95e4cf98158dc130ca23dc1ba9a16360b81c5194369d9a9047e74f8d0e9be25448f1dd082fe66c3ee1c476c152296a", + "resolution": { + "resolution": "zod-to-json-schema@npm:3.25.2", + "version": "3.25.2", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + }, "zod-to-ts@npm:^1.2.0": { "checksum": "307ad0fa78122e016987d00b15b166874b3bd78da4502317c448f7da8cdf76be2989f67b3b0318ae1101bec1bbf427901ae1e0d0088d09a5c9e70d45fcfe3649", "resolution": {