diff --git a/.claude/skills/ci-prep/SKILL.md b/.claude/skills/ci-prep/SKILL.md new file mode 100644 index 0000000..68d0c45 --- /dev/null +++ b/.claude/skills/ci-prep/SKILL.md @@ -0,0 +1,136 @@ +--- +name: ci-prep +description: Prepares the current branch for CI by running the exact same steps locally and fixing issues. If CI is already failing, fetches the GH Actions logs first to diagnose. Use before pushing, when CI is red, or when the user says "fix ci". +argument-hint: "[--failing] [optional job name to focus on]" +--- + + + +# CI Prep + +Prepare the current state for CI. If CI is already failing, fetch and analyze the logs first. + +> **Repo context.** This is `eleventy-plugin-techdoc` — a plain-JavaScript (ES modules, Node >= 18) Eleventy (11ty) plugin/theme published to **npm**, plus a scaffolder CLI (`bin/init.js`, run as `npx eleventy-plugin-techdoc`). Build is a cross-platform **Makefile** with the 7 standard targets: `make build` (`npm pack --dry-run`), `make test` (CLI `--version` contract + Playwright e2e + `coverage-thresholds.json` gate, fail-fast), `make lint` (ESLint + Prettier `--check` + Shipwright manifest validation), `make fmt` (Prettier write; `CHECK=1` for check), `make clean`, `make ci` (lint + test + build), `make setup` (`npm ci` + playwright install). CI (`.github/workflows/ci.yml`) calls `npm ci` then `make lint → make test → make build`. Tests are **Playwright** e2e in `tests/` driving the `sample_website/` fixture. Lint/format are **ESLint** (`eslint .`, via `npm run lint`) and **Prettier** (`npm run fmt` / `fmt:check`). + +## Arguments + +- `--failing` — Indicates a GitHub Actions run is already failing. When present, you MUST execute **Step 1** before doing anything else. +- Any other argument is treated as a job name to focus on (but all failures are still reported). + +If `--failing` is NOT passed, skip directly to **Step 2**. + +## Step 1 — Fetch failed CI logs (only when `--failing`) + +You MUST do this before any other work. + +```bash +BRANCH=$(git branch --show-current) +PR_JSON=$(gh pr list --head "$BRANCH" --state open --json number,title,url --limit 1) +``` + +If the JSON array is empty, **stop immediately**: + +> No open PR found for branch `$BRANCH`. Create a PR first. + +Otherwise fetch the logs: + +```bash +PR_NUMBER=$(echo "$PR_JSON" | jq -r '.[0].number') +gh pr checks "$PR_NUMBER" +RUN_ID=$(gh run list --branch "$BRANCH" --limit 1 --json databaseId --jq '.[0].databaseId') +gh run view "$RUN_ID" +gh run view "$RUN_ID" --log-failed +``` + +Read **every line** of `--log-failed` output. For each failure note the exact file, line, and error message. If a job name argument was provided, prioritize that job but still report all failures. + +## Step 2 — Analyze the CI workflow + +1. Find the CI workflow file. It is `.github/workflows/ci.yml` (job `verify`). Also look for any other workflow triggered on `pull_request` or `push`. +2. Read the workflow file completely. Parse every job and every step. +3. Extract the ordered list of commands the CI actually runs. For this repo `.github/workflows/ci.yml` runs, in order: + - `npm ci` — frozen install (never bare `npm install`). + - `make lint` — `npm run lint` (`eslint .`) + `npm run fmt:check` (`prettier --check .`) + `npx --yes @nimblesite/shipwright-validate-manifest shipwright.json`. + - `make test` — the private `_cli_contract` recipe (`node bin/init.js --version` must print exactly `eleventy-plugin-techdoc `; `node bin/init.js --version --json` must conform to `manifestVersion: 1`, `name`, `kind: "cli"`, `version`, `language`), then Playwright e2e (**only if both `tests/` and `sample_website/` exist**, else skipped with a notice), then the private `_coverage_check` recipe (threshold from `coverage-thresholds.json`). + - `make build` — `npm pack --dry-run` (validates the publishable package assembles). +4. Note any environment variables, matrix strategies, or conditional steps that affect execution (e.g. the `tests/` + `sample_website/` guard inside `make test`; `CI=true` forces Playwright `forbidOnly`/retries/single worker via `playwright.config.js`). + +**Do NOT assume the steps.** Extract what `.github/workflows/ci.yml` _actually does_ and run exactly those commands (today: `npm ci` then `make lint`, `make test`, `make build`). If you find extra targets beyond the 7 standard Makefile targets, flag them in your final report. + +### Release workflow blocker scan + +`.github/workflows/release.yml` exists (tag-triggered npm publish via Trusted Publisher OIDC). Scan it before broad local CI. These are critical blockers and must be fixed before release work is considered CI-ready: + +- Tag-triggered jobs checking out `ref: main` instead of the tagged SHA. (This repo checks out the tagged ref and stamps version in the runner working tree only — `package.json` and `shipwright.json` are stamped from the tag and **never committed**.) +- Any `git commit`, `git push`, branch mutation, or tag mutation during release. +- Version bump commits after the tag already exists. +- Ad hoc `sed` version stamping of structured files instead of stamping via `npm version --no-git-tag-version` / a `node` rewrite of `shipwright.json`. +- A release that publishes without the pre-publish CLI version gate (`node bin/init.js --version` must equal `eleventy-plugin-techdoc `). +- A publish step that uses an `NPM_TOKEN` instead of Trusted Publisher OIDC (`id-token: write`, `--provenance`). + +## Step 3 — Run each CI step locally, in order + +Work through failures in this priority order: + +1. **Formatting** — run Prettier first to clear noise (`make fmt`, i.e. `npm run fmt`) +2. **Lint violations** — fix the code pattern (`eslint .` via `npm run lint`) +3. **Contract/validation failures** — Shipwright manifest validation and the CLI `--version` / `--version --json` contracts must pass (the latter two are the `_cli_contract` recipe inside `make test`) +4. **Runtime / test failures** — fix source code to satisfy the Playwright e2e tests + +For each command extracted from the CI workflow: + +1. Run the command exactly as CI would run it (adjusting only for local environment differences like not needing `actions/checkout`). +2. If the step fails, **stop and fix the issues** before continuing to the next step. +3. After fixing, re-run the same step to confirm it passes. +4. Move to the next step only after the current one succeeds. + +Concretely, run locally, in CI order: + +```bash +npm ci # frozen install (matches CI) +make fmt # auto-format first to clear noise (npm run fmt) +make lint # eslint . + prettier --check . + Shipwright manifest validation +make test # _cli_contract (--version / --version --json) + Playwright e2e + coverage gate +make build # npm pack --dry-run +# or run all three gates at once: +make ci # lint + test + build +``` + +### Hard constraints + +- **NEVER modify test files** — fix the source code, not the tests +- **NEVER add suppressions** (`// eslint-disable`, `/* eslint-disable */`) +- **NEVER delete or ignore failing tests** +- **NEVER remove assertions** + +If stuck on the same failure after 5 attempts, ask the user for help. + +## Step 4 — Report + +- List every step that was run and its result (pass/fail/fixed). +- If any step could not be fixed, report what failed and why. +- Confirm whether the branch is ready to push. + +## Step 5 — Remote CI follow-up (only when `--failing`) + +Once all CI steps pass locally: + +1. Report the local fixes and exact commands that now pass. +2. Do not commit or push. The user owns source-control writes. +3. If the user pushes, monitor the new run until completion or failure. +4. Upon failure, go back to Step 1. + +## Rules + +- **Always read the CI workflow first.** Never assume what commands CI runs. +- Do not commit or push from this skill. +- Fix issues found in each step before moving to the next +- Never skip steps or suppress errors +- If the CI workflow has multiple jobs, run all of them (respecting dependency order) +- Skip steps that are CI-infrastructure-only (checkout, setup-node actions, cache steps, artifact uploads) — focus on the actual `make lint` / `make test` / `make build` commands + +## Success criteria + +- Every command that CI runs has been executed locally and passed +- All fixes are applied to the working tree +- The CI passes successfully (if you are correcting an existing failure) diff --git a/.claude/skills/code-dedup/SKILL.md b/.claude/skills/code-dedup/SKILL.md new file mode 100644 index 0000000..020298b --- /dev/null +++ b/.claude/skills/code-dedup/SKILL.md @@ -0,0 +1,85 @@ +--- +name: code-dedup +description: Searches for duplicate code, duplicate tests, and dead code, then safely merges or removes them. Use when the user says "deduplicate", "find duplicates", "remove dead code", "DRY up", or "code dedup". Requires test coverage — refuses to touch untested code. +--- + + + +# Code Dedup + +Carefully search for duplicate code, duplicate tests, and dead code across the repo. Merge duplicates and delete dead code — but only when test coverage proves the change is safe. + +> **Repo context — READ THIS FIRST.** `eleventy-plugin-techdoc` is **plain, untyped JavaScript (ES modules, Node >= 18)** — there is no TypeScript, no `tsconfig.json`, and no static type checker. The source lives in `lib/` (`lib/index.js`, `lib/filters/`, `lib/plugins/`, `lib/shortcodes/`, `lib/virtual-templates.js`), the CLI in `bin/init.js`, plus `templates/` (`.njk`) and `assets/` (`css/`, `js/`). Tests are **Playwright** e2e run via `make test` (driving `sample_website/`); lint is `make lint` (ESLint + Prettier check + Shipwright manifest validation). The coverage gate reads `coverage-thresholds.json` inside `make test`. Because the codebase is **untyped JavaScript**, the hard gate below (Prerequisite 3) forces this skill to **REFUSE to dedup**. Do not work around that gate. + +## Prerequisites — hard gate + +Before touching ANY code, verify these conditions. If any fail, stop and report why. + +1. Run `make test` — the CLI version contract, all Playwright e2e tests, and the coverage gate must pass. If anything fails, stop. Do not dedup a broken codebase. (Note: `make test` only runs Playwright when the `tests/` and `sample_website/` fixtures exist; if they are absent there is no e2e coverage at all, so dedup is not safe — stop.) +2. Re-confirm the green baseline from `make test`. The coverage gate reads `coverage-thresholds.json` (`default_threshold` is currently `0` because per-line instrumentation is not wired yet — Playwright e2e is the only coverage). A `0%` floor means the gate cannot catch a coverage regression, which makes silent breakage even harder to detect. Treat the effectively-absent coverage floor as a reason to be _more_ conservative, not less. +3. Verify the project uses **static typing**. This repo is **untyped JavaScript**, so: **STOP. Refuse to dedup.** Print exactly: + > "This codebase has no static type checking. Deduplication without types is reckless — too high a risk of silent breakage. Add type checking first." +4. **Confirm the deslop MCP server is available** (see "Required tooling" below). The deslop `language` filter supports only `csharp`, `rust`, and `python` — it does **not** support JavaScript. So even setting aside Prerequisite 3, a real structural duplicate scan is not available for this repo; any scan would be best-effort grep only. See the fallback rule under "Required tooling". + +## Required tooling — the deslop MCP server + +This skill is built around the **deslop** MCP server. **Supported languages: `csharp`, `rust`, `python` only.** For a JavaScript project like this one, deslop's clustering is **not available**; you MUST report this limitation up front rather than pretending you ran a real duplicate scan. Fall back to careful symbol-level grep and ESLint output, and clearly label the scan as "best-effort, no deslop". + +When deslop is available for a supported language, the canonical workflow calls these tools in order: `mcp__deslop__session-config` → `mcp__deslop__rescan` → `mcp__deslop__top-offenders` → `mcp__deslop__report-query` → `mcp__deslop__cluster-by-id` → `mcp__deslop__report-for-file` / `mcp__deslop__report-for-range` → `mcp__deslop__find-similar` (before writing any replacement) → `mcp__deslop__rescan` (after each change). **None of that applies here because this repo is JavaScript** — deslop cannot index it. + +**Bucket interpretation (for reference only):** `identical` > `nearly_identical` > `loosely_similar` > `same_behavior`. + +## Steps + +For THIS repo, the hard gate at Prerequisite 3 means the only correct outcome is to **refuse and report**. The steps below describe the general procedure; do not execute the mutating steps on this untyped-JavaScript codebase. + +``` +Dedup Progress: +- [ ] Step 1: Prerequisites evaluated (tests green? typed? deslop supported?) — for this repo: untyped JS → REFUSE +- [ ] Step 2: Dead code scan (best-effort, no deslop) — report only, do not delete +- [ ] Step 3: Duplicate code scan (no deslop for JS) — report only, do not merge +- [ ] Step 4: Duplicate test scan (no deslop for JS) — report only, do not delete +- [ ] Step 5: SKIPPED — refusing to mutate untyped JavaScript +- [ ] Step 6: SKIPPED +``` + +### Step 1 — Inventory test coverage + +1. Run `make test` to confirm a green Playwright baseline. The recorded coverage floor is the `default_threshold` from `coverage-thresholds.json` (currently `0`, since per-line instrumentation is not wired yet). +2. Identify which files/modules in `lib/`, `bin/`, `templates/`, and `assets/` are exercised by the e2e tests in `tests/`. Only files exercised by tests are even candidates for dedup — and even then, see the untyped-JS refusal. + +### Step 2 — Scan for dead code (best-effort, report only) + +Search for code that is never called, never imported, never referenced. + +1. Look for unused exports, unused functions, unused variables in `lib/`, `bin/init.js`, and `assets/js/`. +2. Use ESLint output where available (`npx eslint .`) — it flags unused variables/imports. +3. For each candidate: **grep the entire codebase** for references (including `tests/`, `templates/*.njk`, `sample_website/`, config files). Only mark as dead if truly zero references. Remember `.njk` templates reference filters/shortcodes/collections by string name, and `assets/` JS is loaded by templates — a symbol with zero JS-import references may still be live via a template. +4. List all dead code found with file paths and line numbers. Do NOT delete (untyped JS → refusing to mutate). + +### Step 3 — Scan for duplicate code (best-effort, no deslop) + +deslop does not support JavaScript, so state that up front and run a **best-effort** scan only: symbol-level grep for repeated function/method bodies across `lib/` and `bin/init.js`, plus ESLint output. Flag every finding as `(no-deslop fallback)`. Do NOT merge anything — untyped JS. + +### Step 4 — Scan for duplicate tests (best-effort, no deslop) + +Best-effort grep across `tests/**/*.spec.js` for near-identical Playwright test bodies. Flag findings as `(no-deslop fallback)`. Do NOT delete anything — untyped JS. + +### Step 5 — Apply changes + +**SKIPPED for this repo.** Untyped JavaScript fails the Prerequisite 3 hard gate; do not remove dead code, merge duplicates, or delete duplicate tests. + +### Step 6 — Final verification + +Not applicable — no changes were made. Re-running `make lint` and `make test` should show the same green baseline as Step 1. + +## Rules + +- **Untyped code = refuse to dedup.** This repo is untyped JavaScript, which is too dangerous to dedup. Types are the safety net that catches breakage at compile time; without them, silent runtime errors are near-certain. This is the governing rule for this repo. +- **Deslop does not support JavaScript = best-effort scan, declared up front.** Say so in the first message of any report and label every finding as `(no-deslop fallback)`. Never pretend a structural scan ran. +- **No test coverage = do not touch.** If a file is not exercised by the e2e tests, leave it alone entirely. +- **Coverage must not drop.** The `coverage-thresholds.json` floor is `0` (no per-line instrumentation), so the gate cannot prove coverage stayed flat — another reason to refuse mutation. +- **One change at a time.** (Only relevant on a typed repo.) Make one dedup change, run `make test`, verify, never batch. +- **When in doubt, leave it.** False dedup is worse than duplication. +- **Preserve public API surface.** Do not change exported plugin/filter/shortcode/collection names or the CLI's behavior — sites and the scaffolder depend on them. `.njk` templates and `sample_website/` bind to these by name. +- **Three similar lines is fine.** Do not abstract trivial duplication. Only substantial shared logic (>10 lines) or 3+ copies would ever justify a merge — and even then, not on untyped JS. diff --git a/.claude/skills/fix-bug/SKILL.md b/.claude/skills/fix-bug/SKILL.md new file mode 100644 index 0000000..b179a6a --- /dev/null +++ b/.claude/skills/fix-bug/SKILL.md @@ -0,0 +1,70 @@ +--- +name: fix-bug +description: Fix a bug using test-driven development. Use when the user reports a bug, describes unexpected behavior, wants to fix a defect, or says something is broken. Enforces a strict test-first workflow where a failing test must be written and verified before any fix is attempted. +argument-hint: "[bug description]" +allowed-tools: Read, Grep, Glob, Edit, Write, Bash +--- + + + +# Bug Fix Skill — Test-First Workflow + +> **Repo context (added).** This is `eleventy-plugin-techdoc`, a plain-JavaScript (ES modules, Node >= 18) Eleventy theme/plugin plus a scaffolder CLI (`bin/init.js`). The existing test framework is **Playwright** e2e (config in `playwright.config.js`), with tests in `tests/` driving the `sample_website/` fixture. Use that framework and those conventions when writing the failing test. Source under test lives in `lib/`, `bin/init.js`, `templates/`, and `assets/`. "Run ONLY the new test" (Steps 3 and 6) = `npx playwright test -g ""`. "Run the full test suite" (Step 7) = `make test` (which also runs the CLI `--version` contract and the `coverage-thresholds.json` gate) or plain `npx playwright test`. None of the additions below remove or alter any step. + +You MUST follow this exact workflow. Do NOT skip steps. Do NOT fix the bug before writing a failing test. + +## Step 1: Understand the Bug + +- Read the bug description: $ARGUMENTS +- Investigate the codebase to understand the relevant code +- Identify the root cause (or narrow down candidates) +- Summarize your understanding of the bug to the user before proceeding + +## Step 2: Write a Failing Test + +- Write a test that **directly exercises the buggy behavior** +- The test must assert the **correct/expected** behavior — so it FAILS against the current broken code +- The test name should clearly describe the bug (e.g., `test_orange_color_not_applied_to_head`) +- Use the project's existing test framework and conventions + +## Step 3: Run the Test — Confirm It FAILS + +- Run ONLY the new test (not the full suite) +- **Verify the test FAILS** and that it fails **because of the bug**, not for some other reason (typo, import error, wrong selector, etc.) +- If the test passes: your test does not capture the bug. Go back to Step 2 +- If the test fails for the wrong reason: fix the test, not the code. Go back to Step 2 +- **Repeat until the test fails specifically because of the bug** + +## Step 4: Show Failure to User + +- Show the user the test code and the failure output +- Explicitly ask: "This test fails because of the bug. Can you confirm this captures the issue before I fix it?" +- **STOP and WAIT for user acknowledgment before proceeding** +- Do NOT continue to Step 5 until the user confirms + +## Step 5: Fix the Bug + +- Make the **minimum change** needed to fix the bug +- Do not refactor, clean up, or "improve" surrounding code +- Do not change the test + +## Step 6: Run the Test — Confirm It PASSES + +- Run the new test again +- **Verify it PASSES** +- If it fails: go back to Step 5 and adjust the fix +- **Repeat until the test passes** + +## Step 7: Run the Full Test Suite + +- Run ALL tests to make sure nothing else broke +- If other tests fail: fix the regression without breaking the new test +- Report the final result to the user + +## Rules + +- NEVER fix the bug before the failing test is written and confirmed +- NEVER skip asking the user to acknowledge the test failure +- NEVER modify the test to make it pass — modify the source code +- If you cannot write a test for the bug, explain why and ask the user how to proceed +- Keep the fix minimal — one bug, one fix, one test diff --git a/.claude/skills/spec-check/SKILL.md b/.claude/skills/spec-check/SKILL.md new file mode 100644 index 0000000..d051632 --- /dev/null +++ b/.claude/skills/spec-check/SKILL.md @@ -0,0 +1,356 @@ +--- +name: spec-check +description: Audit spec/plan documents against the codebase. Ensures every spec section has implementing code, tests, and matching logic. Use when the user says "check specs", "spec audit", or "verify specs". +argument-hint: "[optional spec ID or filename filter]" +--- + + + +# spec-check + +> **Portable skill.** This skill adapts to the current repository. The agent MUST inspect the repo structure and use judgment to apply these instructions appropriately. + +> **Repo context (added).** `eleventy-plugin-techdoc` keeps its spec at **`docs/specs/SPEC.md`** (the techdoc theme specification — matched by the `docs/**/*.md` and `specs/*.md` globs in Step 2), and the codebase/CI also carry Shipwright spec IDs of the form `[SWR-*]` (e.g. `[SWR-GATE-CI]`, `[SWR-SEC-OIDC-PUBLISH]`, `[SWR-VERSION-BUILD-STAMPING]`) embedded as comments in `.github/workflows/ci.yml`, `.github/workflows/release.yml`, and `shipwright.json`. Code files are plain JavaScript (`lib/`, `bin/init.js`) using `//` comments; templates are `.njk` (HTML/XML `` comments); assets are CSS (`/* */`) and JS (`//`); workflows/config are YAML/JSON (`#` comments, or no comments for JSON). Tests are **Playwright** e2e under `tests/` (`*.spec.js`). Note that Check A excludes `docs/` from the _code_ search, which is correct — `docs/specs/SPEC.md` is the spec source, not implementing code. None of this context removes or alters any step, check, URL, or report format below. + +Audit spec/plan documents against the codebase. Ensures every spec section has implementing code, tests, and that the code logic matches the spec. + +## Arguments + +- `$ARGUMENTS` — optional spec name or ID to check (e.g., `AUTH-TOKEN-VERIFY` or `repo-standards`). If empty, check ALL specs. Spec IDs are descriptive slugs, NEVER numbered (see Step 1). + +## Instructions + +Follow these steps exactly. Be strict and pedantic. Stop on the first failure. + +--- + +### Step 1: Validate spec ID structure + +Before checking code/test references, verify that the specs themselves are well-formed. + +1. Find all spec documents (see locations in Step 2). +2. Extract every section ID using the regex `\[([A-Z][A-Z0-9]*(-[A-Z0-9]+)+)\]`. +3. **Flag invalid IDs:** + - Numbered IDs (`[SPEC-001]`, `[REQ-003]`, `[CI-004]`) — must be renamed to descriptive hierarchical slugs. + - Single-word IDs (`[TIMEOUT]`) — must have a group prefix. + - IDs with trailing numbers (`[FEAT-AUTH-01]`) — the number is meaningless, remove it. +4. **Check group clustering:** The first word of each ID is its group. All sections in the same group MUST appear together (adjacent) in the document. If they're scattered, flag it. +5. **Check for missing IDs:** Any heading that defines a requirement or behavior should have an ID. Flag headings in spec files that look like they define behavior but lack an ID. + +If any ID violations are found, report them all and **STOP**: + +``` +SPEC ID VIOLATIONS: + +- docs/specs/AUTH-SPEC.md line 12: [SPEC-001] → rename to descriptive ID (e.g., [AUTH-LOGIN]) +- docs/specs/AUTH-SPEC.md line 30: [AUTH-TOKEN-VERIFY] and [AUTH-LOGIN] are not adjacent (scattered group) +- docs/specs/CI-SPEC.md line 5: "## Coverage thresholds" has no spec ID + +Fix spec IDs first, then re-run spec-check. +``` + +If all IDs are valid, proceed to Step 2. + +--- + +### Step 2: Find all spec/plan documents + +Search for markdown files that contain spec sections with IDs. Look in these locations: + +- `docs/*.md` +- `docs/**/*.md` +- `SPEC.md` +- `PLAN.md` +- `specs/*.md` + +Use Glob to find candidate files, then use Grep to confirm they contain spec IDs. + +**Spec ID patterns** — IDs appear in square brackets, typically at the start of a heading or section line. Match this regex pattern: + +``` +\[([A-Z][A-Z0-9]*(-[A-Z0-9]+)+)\] +``` + +Spec IDs are **hierarchical descriptive slugs, NEVER numbered.** The format is `[GROUP-TOPIC]` or `[GROUP-TOPIC-DETAIL]`. The first word is the **group** — all sections sharing the same group MUST appear together in the spec's table of contents. IDs are uppercase, hyphen-separated, unique across the repo, and MUST NOT contain sequential numbers. + +The hierarchy depth varies by repo: two words for simple repos (`[AUTH-LOGIN]`), three for most (`[AUTH-TOKEN-VERIFY]`), four for complex domains (`[AUTH-OAUTH-REFRESH-FLOW]`). The hierarchy mirrors the spec document's heading structure. + +Examples of valid spec IDs (note how groups cluster): + +- `[AUTH-LOGIN]`, `[AUTH-TOKEN-VERIFY]`, `[AUTH-TOKEN-REFRESH]` — all in the AUTH group +- `[CI-TIMEOUT]`, `[CI-LINT]`, `[CI-RELEASE]` — all in the CI group +- `[LINT-ESLINT]`, `[LINT-RUFF]` — all in the LINT group +- `[FEAT-DARK-MODE]`, `[FEAT-SEARCH-FILTER]` — all in the FEAT group + +Examples of INVALID spec IDs: + +- `[SPEC-001]` — numbered, meaningless +- `[FEAT-AUTH-01]` — trailing number +- `[REQ-003]` — sequential index, no group hierarchy +- `[CI-004]` — numbered, tells the reader nothing +- `[TIMEOUT]` — no group prefix, ungrouped + +For each file, extract every spec ID and its associated section title (the heading text after the ID) and the full section content (everything until the next heading of equal or higher level). + +--- + +### Step 3: Filter specs + +- If `$ARGUMENTS` is non-empty, filter the discovered specs: + - If it matches a spec ID exactly (e.g., `AUTH-TOKEN-VERIFY`), check only that spec. + - If it matches a partial name (e.g., `repo-standards`), check all specs in files whose path contains that string. +- If `$ARGUMENTS` is empty, process ALL discovered specs. + +If filtering produces zero specs, report an error: + +``` +ERROR: No specs found matching "$ARGUMENTS". Discovered spec files: [list them] +``` + +--- + +### Step 4: Check each spec section + +For EACH spec section that has an ID, perform checks A, B, and C below. **Stop on the first failure.** + +#### Check A: Code references the spec ID + +Search the entire codebase for the spec ID string, **excluding** these directories: + +- `docs/` +- `node_modules/` +- `.git/` +- `*.md` files (markdown is docs, not code) + +Use Grep with the literal spec ID (e.g., `[AUTH-TOKEN-VERIFY]`) to find references in code files. + +Code files should contain comments referencing the spec ID. The search must catch **all** comment styles across languages: + +**C-style `//` comments** (JavaScript, TypeScript, Rust, C#, F#, Java, Kotlin, Go, Swift, Dart): + +- `// Implements [AUTH-TOKEN-VERIFY]` +- `// [AUTH-TOKEN-VERIFY]` +- `// Tests [AUTH-TOKEN-VERIFY]` (also counts as a code reference) +- `/// Implements [AUTH-TOKEN-VERIFY]` (doc comments) + +**Hash `#` comments** (Python, Ruby, Shell/Bash, YAML, TOML): + +- `# Implements [AUTH-TOKEN-VERIFY]` +- `# [AUTH-TOKEN-VERIFY]` +- `# Tests [AUTH-TOKEN-VERIFY]` + +**HTML/XML comments** (HTML, CSS, SVG, XML, XAML, JSX templates): + +- `` +- `` + +**ML-style comments** (F#, OCaml): + +- `(* Implements [AUTH-TOKEN-VERIFY] *)` + +**Lua comments:** + +- `-- Implements [AUTH-TOKEN-VERIFY]` + +**CSS comments:** + +- `/* Implements [AUTH-TOKEN-VERIFY] */` + +**The key rule:** any comment in any language containing the exact spec ID string (e.g., `[AUTH-TOKEN-VERIFY]`) counts as a valid code reference. The Grep search uses the literal spec ID string, so it naturally matches all comment styles. Do NOT restrict the search to specific comment prefixes — just search for the spec ID string itself. + +**If NO code files reference the spec ID:** + +``` +SPEC VIOLATION: [AUTH-TOKEN-VERIFY] "Section Title" has no implementing code. + +Every spec section must have at least one code file that references it via a comment +containing the spec ID (e.g., `// Implements [AUTH-TOKEN-VERIFY]`). + +ACTION REQUIRED: Add a comment referencing [AUTH-TOKEN-VERIFY] in the file(s) that implement +this spec section, then re-run spec-check. +``` + +**STOP HERE. Do not continue to other checks.** + +#### Check B: Tests reference the spec ID + +Search test files for the spec ID. Test files are found in: + +- `test/` +- `tests/` +- `**/*.test.*` +- `**/*.spec.*` +- `**/*_test.*` +- `**/test_*.*` +- `**/*Tests.*` +- `**/*Test.*` + +Use Grep to search these locations for the literal spec ID string. + +Tests should contain the spec ID in comments, test names, or annotations. The search must catch **all** test frameworks across languages: + +**JavaScript/TypeScript** (Jest, Mocha, Vitest, Playwright): + +- `// Tests [AUTH-TOKEN-VERIFY]` +- `describe('[AUTH-TOKEN-VERIFY] Authentication flow', () => ...)` +- `test('[AUTH-TOKEN-VERIFY] should verify token', () => ...)` +- `it('[AUTH-TOKEN-VERIFY] verifies token', () => ...)` + +**Python** (pytest, unittest): + +- `# Tests [AUTH-TOKEN-VERIFY]` +- `def test_auth_token_verify_flow():` +- `class TestAuthTokenVerify:` + +**Rust:** + +- `// Tests [AUTH-TOKEN-VERIFY]` +- `#[test] // Tests [AUTH-TOKEN-VERIFY]` + +**C#** (xUnit, NUnit, MSTest): + +- `// Tests [AUTH-TOKEN-VERIFY]` +- `[Fact] // Tests [AUTH-TOKEN-VERIFY]` +- `[Test] // Tests [AUTH-TOKEN-VERIFY]` +- `[TestMethod] // Tests [AUTH-TOKEN-VERIFY]` + +**F#** (xUnit, Expecto): + +- `// Tests [AUTH-TOKEN-VERIFY]` +- `[] // Tests [AUTH-TOKEN-VERIFY]` +- `testCase "[AUTH-TOKEN-VERIFY] description" <| fun () ->` + +**Java/Kotlin** (JUnit, TestNG): + +- `// Tests [AUTH-TOKEN-VERIFY]` +- `@Test // Tests [AUTH-TOKEN-VERIFY]` + +**Go:** + +- `// Tests [AUTH-TOKEN-VERIFY]` +- `func TestAuthTokenVerify(t *testing.T) { // Tests [AUTH-TOKEN-VERIFY]` + +**Swift** (XCTest): + +- `// Tests [AUTH-TOKEN-VERIFY]` +- `func testAuthTokenVerify() { // Tests [AUTH-TOKEN-VERIFY]` + +**Dart** (flutter_test): + +- `// Tests [AUTH-TOKEN-VERIFY]` +- `test('[AUTH-TOKEN-VERIFY] description', () { ... });` + +**Ruby** (RSpec, Minitest): + +- `# Tests [AUTH-TOKEN-VERIFY]` +- `describe '[AUTH-TOKEN-VERIFY] Authentication' do` +- `it '[AUTH-TOKEN-VERIFY] verifies token' do` + +**Shell** (bats, shunit2): + +- `# Tests [AUTH-TOKEN-VERIFY]` +- `@test "[AUTH-TOKEN-VERIFY] description" {` + +**The key rule:** same as Check A — search for the literal spec ID string in test files. Any occurrence of the exact spec ID in a test file counts. Do NOT restrict to specific patterns — just search for the spec ID string itself. + +**If NO test files reference the spec ID:** + +``` +SPEC VIOLATION: [AUTH-TOKEN-VERIFY] "Section Title" has no tests. + +Every spec section must have corresponding tests that reference the spec ID. + +ACTION REQUIRED: Add tests for [AUTH-TOKEN-VERIFY] with a comment or test name containing +the spec ID, then re-run spec-check. +``` + +**STOP HERE. Do not continue to other checks.** + +#### Check C: Code logic matches the spec + +This is the most critical check. You must: + +1. **Read the spec section content carefully.** Understand exactly what behavior, logic, ordering, conditions, and constraints the spec describes. + +2. **Read the implementing code.** Use the references found in Check A to locate the implementing files. Read the relevant functions/sections. + +3. **Compare spec vs. code.** Be SENSITIVE and PEDANTIC. Check for: + - **Ordering violations** — If the spec says A happens before B, the code must do A before B. + - **Missing conditions** — If the spec says "only when X", the code must have that condition. + - **Extra behavior** — If the code does something the spec doesn't mention, flag it only if it contradicts the spec. + - **Wrong logic** — If the spec says "greater than" but code uses "greater than or equal", that's a violation. + - **Missing steps** — If the spec describes 5 steps but code only implements 3, that's a violation. + - **Wrong defaults** — If the spec says "default to X" but code defaults to Y, that's a violation. + +4. **If the code deviates from the spec**, report a detailed error: + +``` +SPEC VIOLATION: [AUTH-TOKEN-VERIFY] Code does not match spec. + +SPEC SAYS: +> "The authentication flow must verify the token expiry before checking permissions" +> (from docs/specs/AUTH-SPEC.md, line 42) + +CODE DOES: +> `if (hasPermission(user)) { verifyToken(token); }` (src/auth.ts:42) + +DEVIATION: The code checks permissions BEFORE verifying token expiry. +The spec explicitly requires token expiry verification FIRST. + +ACTION REQUIRED: Reorder the logic in src/auth.ts to verify token expiry +before checking permissions, as specified in [AUTH-TOKEN-VERIFY]. +``` + +**STOP HERE. Do not continue to other specs.** + +5. **If the code matches the spec**, this check passes. Move to the next spec. + +--- + +### Step 5: Report results + +#### On failure (any check fails): + +Output ONLY the first violation found. Use the exact error format shown above. Do not summarize other specs. Do not offer to fix the code. Just report the violation. + +End with: + +``` +spec-check FAILED. Fix the violation above and re-run. +``` + +#### On success (all specs pass): + +Output a summary table: + +``` +spec-check PASSED. All specs verified. + +| Spec ID | Title | Code References | Test References | Logic Match | +|----------------|--------------------------|-----------------|-----------------|-------------| +| [AUTH-TOKEN-VERIFY] | Authentication flow | src/auth.ts | tests/auth.test.ts | PASS | +| [RATE-LIMIT-CONFIG] | Rate limiting | src/rate.ts | tests/rate.test.ts | PASS | +| ... | ... | ... | ... | ... | + +Checked N spec sections across M files. All have implementing code, tests, and matching logic. +``` + +--- + +## Search strategy summary + +1. **Validate spec IDs:** Check all IDs are hierarchical, descriptive, grouped, and non-numbered +2. **Find spec files:** Glob for `docs/**/*.md`, `SPEC.md`, `PLAN.md`, `specs/**/*.md` +3. **Extract spec IDs:** Grep for `\[[A-Z][A-Z0-9]*(-[A-Z0-9]+)+\]` in those files +4. **Find code refs:** Grep for the literal spec ID in all files, excluding `docs/`, `node_modules/`, `.git/`, `*.md` +5. **Find test refs:** Grep for the literal spec ID in test directories and test file patterns +6. **Read and compare:** Read the spec section content and the implementing code, compare logic + +## Key principles + +- **Fail fast.** Stop on the first violation. One fix at a time. +- **Be pedantic.** If the spec says it, the code must do it. No "close enough". +- **Quote everything.** Always quote the spec text and the code in error messages so the developer sees exactly what's wrong. +- **Be actionable.** Every error must tell the developer what file to change and what to do. +- **Exclude docs from code search.** Markdown files are documentation, not implementation. Only search actual code files for spec references. +- **No numbered IDs.** Spec IDs are hierarchical descriptive slugs (`[AUTH-TOKEN-VERIFY]`), NEVER sequential numbers (`[SPEC-001]`). The first word is the group — sections sharing a group must be adjacent in the TOC. If you encounter numbered or ungrouped IDs, flag them as a violation. diff --git a/.claude/skills/submit-pr/SKILL.md b/.claude/skills/submit-pr/SKILL.md new file mode 100644 index 0000000..a16121f --- /dev/null +++ b/.claude/skills/submit-pr/SKILL.md @@ -0,0 +1,53 @@ +--- +name: submit-pr +description: Creates a pull request with a well-structured description after verifying CI passes. Use when the user asks to submit, create, or open a pull request. +disable-model-invocation: true +--- + + + +# Submit PR + +Create a pull request for the current branch with a well-structured description. + +> **Repo context (added).** `eleventy-plugin-techdoc` has a cross-platform **Makefile**, so `make ci` works literally here: it runs `make lint` (ESLint + Prettier `--check` + Shipwright manifest validation) + `make test` (CLI `--version` contract + Playwright e2e when `tests/` and `sample_website/` exist + the `coverage-thresholds.json` gate) + `make build` (`npm pack --dry-run`) — the same gate `.github/workflows/ci.yml` runs (after `npm ci`). You can also invoke the `ci-prep` skill. Step 4 references `.github/pull_request_template.md`; this repo now ships that template — use it. This context does not remove or alter any step, command, URL, or rule below. + +## Steps + +_NOTE: if you already ran make ci in this session and it passed, you can skip step 1._ + +1. Run `make ci` — must pass completely before creating PR +2. **Generate the diff against main.** Run `git diff main...HEAD > /tmp/pr-diff.txt` to capture the full diff between the current branch and the head of main. This is the ONLY source of truth for what the PR contains. **Warning:** the diff can be very large. If the diff file exceeds context limits, process it in chunks (e.g., read sections with `head`/`tail` or split by file) rather than trying to load it all at once. +3. **Derive the PR title and description SOLELY from the diff.** Read the diff output and summarize what changed. Ignore commit messages, branch names, and any other metadata — only the actual code/content diff matters. +4. Write PR body using the template in `.github/pull_request_template.md` +5. Fill in (based on the diff analysis from step 3): + - TLDR: one sentence + - What Was Added: new files, features, deps + - What Was Changed/Deleted: modified behaviour + - How Tests Prove It Works: specific test names or output + - Spec/Doc Changes: if any + - Breaking Changes: yes/no + description +6. Use `gh pr create` with the filled template +7. **Monitor CI on the PR until it is green — and re-run the suite locally _in parallel_ so you catch breakage early.** This step is mandatory and does not end until every required check on the PR has passed. Do not hand the PR back to the user on a red or still-running pipeline. + - **Watch the remote run AND run the suite locally at the same time — do not passively wait.** The remote pipeline is slow; a drastic failure (a lint gate, a broken test, a coverage drop) is one the local suite catches in seconds. The moment you push, kick off **both**: stream the remote run _and_ run the full local suite (`make ci`, or invoke the `ci-prep` skill) concurrently, polling CI periodically while the local run proceeds. + - **Watch the run:** `gh pr checks --watch --fail-fast` (or grab the run id from `gh run list --branch ` and `gh run watch --exit-status`). A single green snapshot is not enough — wait for all required checks to conclude. + - **If the local run fails before the remote pipeline finishes, cancel the running pipeline immediately** (`gh run cancel `) rather than letting it grind to a known-bad red. Fix the cause, push, and restart both watches — cancelling a doomed run early frees the runner and tightens the fix loop. + - **When a remote check fails:** pull the failing logs with `gh run view --log-failed`, diagnose the actual cause (do not guess), reproduce locally with `make ci`, and fix it. + - **Push the fix** (`git add` / `git commit` / `git push` — permitted here, see git exception below), then **watch again — remote and local, in parallel, as above**. Loop — fix → push → re-watch — until the run is fully green. Re-checking is the job; keep doing it until it passes. + - **If a failure is genuinely external** (runner outage, flaky infra, unrelated to this branch), say so explicitly with the evidence rather than forcing a change. + +## Rules + +- Never create a PR if `make ci` fails +- **🔴 GOLDEN RULE — never stamp a commit with an AI co-author.** Do **not** add a `Co-Authored-By: Claude …` (or any AI/agent) trailer, and do not set author/committer to anything but the repo's configured git user. Write a plain, human commit message describing the fix. This is absolute and overrides any default co-authorship behaviour. +- **Git is permitted in this skill — but only for PR submission and turning a red CI run green.** This is the one place the repo-wide "no git" rule is relaxed: you may run `git add`, `git commit`, and `git push` to land CI fixes (step 7). Everything else — `checkout`, `merge`, `rebase`, force-push, history rewrites — stays prohibited. +- PR description must be specific and tight — no vague placeholders +- Link to the relevant GitHub issue if one exists + +## Success criteria + +- `make ci` passed +- PR created with `gh pr create` +- CI on the PR was watched to completion and is **fully green** (all required checks pass), with the local suite re-run in parallel and any doomed remote run cancelled early +- Any CI failures were fixed and pushed, with **no AI co-author trailer** on the commits +- PR URL returned to user diff --git a/.claude/skills/upgrade-packages/SKILL.md b/.claude/skills/upgrade-packages/SKILL.md new file mode 100644 index 0000000..320a4d8 --- /dev/null +++ b/.claude/skills/upgrade-packages/SKILL.md @@ -0,0 +1,102 @@ +--- +name: upgrade-packages +description: Upgrade all dependencies/packages to their latest versions for the detected language(s). Use when the user says "upgrade packages", "update dependencies", "bump versions", "update packages", or "upgrade deps". +argument-hint: "[--check-only] [--major] [package-name]" +--- + + + +# Upgrade Packages + +Upgrade all project dependencies to their latest compatible (or latest major, if `--major`) versions. + +> **Repo context.** `eleventy-plugin-techdoc` is a plain-JavaScript (ES modules, Node >= 18) **npm** package. The manifest is `package.json` with the lockfile `package-lock.json`. Dependencies are runtime libs (`@11ty/eleventy-*`, `markdown-it*`, `@inquirer/prompts`), a peer dependency (`@11ty/eleventy ^3.1.2`), and a dev dependency (`@playwright/test`). This repo is JavaScript end to end, so this skill operates on **npm only**. + +## Arguments + +- `--check-only` — List outdated packages without upgrading. Stop after Step 2. +- `--major` — Include major version bumps (breaking changes). Without this flag, stay within semver-compatible ranges. +- Any other argument is treated as a specific package name to upgrade (instead of all packages). + +## Step 1 — Confirm the package manager + +The manifest is `package.json` at the repo root and the lockfile is `package-lock.json`, so the package manager is **npm**. (If a `yarn.lock` or `pnpm-lock.yaml` ever appears, switch to that tool — but today it is npm.) + +## Step 2 — List outdated packages + +Run this BEFORE upgrading anything. Show the user what will change. + +```bash +npm outdated +``` + +**Read the docs:** https://docs.npmjs.com/cli/v10/commands/npm-update + +If `--check-only` was passed, **stop here** and report the outdated list. + +## Step 3 — Read the official upgrade docs + +**Before running any upgrade command, you MUST fetch and read the official npm documentation** (https://docs.npmjs.com/cli/v10/commands/npm-update) with WebFetch. This ensures you use the correct flags and understand the behavior. Do not guess at flags or options from memory. + +## Step 4 — Upgrade packages + +Run the upgrade. If a specific package name was given as an argument, upgrade only that package. + +```bash +npm update # semver-compatible (within package.json ranges) +# --major flag: +npx npm-check-updates -u && npm install # bump package.json to latest majors +``` + +Notes specific to this repo: + +- `@11ty/eleventy` is a **peer dependency** (`^3.1.2`), not a regular dependency. Do not move it into `dependencies`; bump the peer range deliberately and only when intentionally targeting a new Eleventy major. A peer bump is a contract change for consumers — flag it explicitly. +- `@playwright/test` is the only `devDependency`. After upgrading it, you may need `npx playwright install` to fetch matching browsers before the e2e suite will run. +- If a specific package name was given, prefer `npm install @latest` (or `npx npm-check-updates -u `) for that single package. + +## Step 5 — Verify the upgrade + +Run the project's full gate via the Makefile (this is exactly what CI runs — `npm ci` then `make lint → make test → make build`): + +```bash +npm ci # frozen install against the updated lockfile +make ci # make lint + make test + make build +``` + +`make lint` runs ESLint + Prettier `--check` + Shipwright manifest validation; `make test` runs the CLI `--version` contract + Playwright e2e (when `tests/` + `sample_website/` exist) + the `coverage-thresholds.json` gate; `make build` runs `npm pack --dry-run`. + +If tests fail: + +1. Read the failure output carefully. +2. Check the changelog / migration guide for the upgraded packages (fetch the release notes URL if available) — Eleventy 3.x and `markdown-it` majors are the most likely sources of breakage here. +3. Fix breaking changes in the code (`lib/`, `bin/init.js`, `templates/`, `assets/`). +4. Re-run tests. +5. If stuck after 3 attempts on the same failure, report it to the user with the error details and the package that caused it. + +## Step 6 — Report + +Provide a summary: + +- Packages upgraded (old version -> new version) +- Packages skipped (and why, e.g., major version bump without `--major` flag, or a deliberate hold on the `@11ty/eleventy` peer range) +- Build/test result after upgrade +- Any breaking changes that were fixed +- Any packages that could not be upgraded (with error details) + +## Rules + +- **Always list outdated packages first** with `npm outdated` before upgrading anything +- **Always read the official npm docs** before running upgrade commands +- **Always run tests after upgrading** to catch breakage immediately +- **Never remove packages** unless they were explicitly deprecated and replaced +- **Never downgrade packages** unless rolling back a broken upgrade +- **Never modify the lockfile manually** (`package-lock.json`) — let npm regenerate it +- **Treat the `@11ty/eleventy` peer-dependency range as a public contract** — bump it deliberately, never as a side effect +- **Commit nothing** — leave changes in the working tree for the user to review + +## Success criteria + +- All outdated packages upgraded to latest compatible (or latest major if `--major`) +- `npm ci` succeeds against the regenerated lockfile +- `make ci` passes (lint incl. Shipwright manifest validation, the CLI `--version` contracts, Playwright e2e, and `npm pack --dry-run`) +- User has a clear summary of what changed diff --git a/.claude/skills/website-audit/SKILL.md b/.claude/skills/website-audit/SKILL.md new file mode 100644 index 0000000..97dd94c --- /dev/null +++ b/.claude/skills/website-audit/SKILL.md @@ -0,0 +1,185 @@ +--- +name: website-audit +description: Audits a website for SEO, AI search performance, structured data, mobile usability, broken links, and social media cards. Fixes issues found. Use when the user mentions "audit website", "SEO", "fix search ranking", "AI search", "structured data", "social media cards", or "website performance". +--- + + + +# Website Audit + +Performs a comprehensive website audit and fixes issues affecting search visibility and AI discoverability. + +> **Repo context (added).** `eleventy-plugin-techdoc` IS a website theme — an **Eleventy (11ty)** plugin/theme that emits the HTML, SEO metadata, RSS/Atom feed, sitemap, robots.txt, and `llms.txt` that this audit inspects. The "static content templates" are the Nunjucks files in `templates/layouts/` (`base.njk`, `docs.njk`, `blog.njk`, `api.njk`) and `templates/pages/` (`feed.njk`, `sitemap.njk`, `robots.txt.njk`, `llms.txt.njk`, plus the blog index/tags/categories pages), with structural CSS in `assets/css/` and structural JS in `assets/js/` (`theme-toggle.js`, `mobile-menu.js`, `language-switcher.js`, `main.js`). Fix issues in those source templates/assets and in `lib/` (e.g. `lib/virtual-templates.js`, `lib/plugins/collections.js`, `lib/filters/`) — **never** in generated output (`_site` / `sample_website/_site`). The rendered output to inspect is what the **Playwright `sample_website/` fixture** builds and serves (the dev server `cd sample_website && npx @11ty/eleventy --serve --port=8080`, per `playwright.config.js`). This is a _structural_ theme that ships no colors; respect that when auditing design. None of this context removes or alters any step, URL, checklist item, or the report format below. + +Copy this checklist and track your progress: + +``` +Audit Progress: +- [ ] Step 1: Read guidelines +- [ ] Step 2: Audit AI search readiness +- [ ] Step 3: Audit SEO and keywords +- [ ] Step 4: Audit crawling and indexing +- [ ] Step 5: Audit broken links and canonicalization +- [ ] Step 6: Audit mobile usability +- [ ] Step 7: Audit structured data +- [ ] Step 8: Audit social media cards +- [ ] Step 9: Audit For Unsubstantiated Claims +- [ ] Step 10: Audit Design Compliance +- [ ] Step 11: Test with Playwright +- [ ] Step 12: Report findings +``` + +- Check the outputted HTML/CSS/JavaScript AFTER the website is generated by the static content generator. - Don't just check the static content before the website is generated. +- Fix issues at the core where the static content templates are stored - not in the outputted HTML (e.g. \_site) +- Never manually edit the generated website content directly +- ENSURE THE FOOTER HAS A copyright link to nimblesite.co + +## Step 1 — Read guidelines + +Fetch and read each of these before auditing. These are the authoritative references for every step that follows. + +- [Google's guidance on using generative AI content](https://developers.google.com/search/docs/fundamentals/using-gen-ai-content) +- [Top ways to ensure content performs well in Google's AI experiences](https://developers.google.com/search/blog/2025/05/succeeding-in-ai-search) +- [SEO Starter Guide](https://developers.google.com/search/docs/fundamentals/seo-starter-guide) + +If the repo has a business plan doc, take it into account + +Identify the website source files in the repo. Determine the framework (static site generator, Next.js, Hugo, etc.) so you know where to find templates, metadata, and content. (For this repo: the framework is **Eleventy (11ty)**; templates live in `templates/layouts/` and `templates/pages/`, assets in `assets/`, theme logic in `lib/`, and the sample/rendered site is built by the `sample_website/` fixture.) + +## Step 2 — Audit AI search readiness + +Apply the guidance from the AI search article. Check: + +1. **Content quality** — Is content original, expert-level, and comprehensive? Flag thin or duplicated pages. +2. **Clear structure** — Do pages use descriptive headings, lists, and concise answers to likely questions? +3. **Entity clarity** — Are key terms, products, and concepts defined clearly so AI can extract them? +4. **Freshness signals** — Are dates, update timestamps, and authorship present? + +Fix issues directly in the source files. For each fix, note what changed and why. + +## Step 3 — Audit SEO and keywords + +1. Search [Google Trends](https://trends.google.com/home) for trending keywords related to the website's content. +2. Review each page's ``, `<meta name="description">`, and `<h1>` tags. +3. Check for keyword opportunities — can trending terms be naturally inserted into headings, descriptions, or body content? +4. Verify each page has a unique, descriptive title (50-60 chars) and meta description (150-160 chars). +5. Check image `alt` attributes describe the image content and include relevant keywords where natural. + +Apply the [SEO Starter Guide](https://developers.google.com/search/docs/fundamentals/seo-starter-guide) principles. Fix issues directly. + +## Step 4 — Audit crawling and indexing + +Reference: [Overview of crawling and indexing topics](https://developers.google.com/search/docs/crawling-indexing) + +1. **robots.txt** — Locate and review it. Verify it doesn't block important pages. Reference: [robots.txt spec](https://developers.google.com/search/docs/crawling-indexing/robots-txt) +2. **Sitemap** — Locate the sitemap (or sitemap index). Verify all important pages are listed and no dead URLs are included. Reference: [Sitemap guidelines](https://developers.google.com/search/docs/crawling-indexing/sitemaps/large-sitemaps) +3. **Meta robots tags** — Check for unintended `noindex` or `nofollow` directives on pages that should be indexed. + +Note: robots.txt and sitemaps are often auto-generated. If so, check the generator config rather than the output file. (In this repo they ARE generated — from `templates/pages/robots.txt.njk` and `templates/pages/sitemap.njk` (and `templates/pages/llms.txt.njk` for AI crawlers). Fix those templates, not the rendered files.) + +## Step 5 — Audit broken links and canonicalization + +Reference: [What is canonicalization](https://developers.google.com/search/docs/crawling-indexing/canonicalization) + +1. Check all internal links resolve to valid pages (no 404s). +2. Verify `<link rel="canonical">` tags are present and point to the correct URL. +3. Check for duplicate content accessible via multiple URLs (with/without trailing slash, www vs non-www). +4. Verify redirects use 301 (permanent) not 302 (temporary) where appropriate. + +## Step 6 — Audit mobile usability + +Reference: [Mobile-first indexing best practices](https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing) + +1. Verify the `<meta name="viewport">` tag is present and correctly configured. +2. Check that content is identical between mobile and desktop (mobile-first indexing requires this). +3. Verify touch targets are adequately sized (min 48x48px). +4. Check font sizes are readable without zooming (min 16px body text). + +## Step 7 — Audit structured data + +Reference: [Structured data guidelines](https://developers.google.com/search/docs/appearance/structured-data/sd-policies) + +1. Check for existing JSON-LD `<script type="application/ld+json">` blocks. +2. Verify the structured data matches the page content (no misleading markup). +3. Add missing structured data where appropriate: + - **Organization/Person** on the homepage + - **Article/BlogPosting** on blog posts (with author, datePublished, dateModified) + - **BreadcrumbList** for navigation + - **FAQ** for pages with question/answer content +4. Validate JSON-LD syntax is correct. + +## Step 8 — Audit social media cards + +Reference: [Implementing Social Media Preview Cards](https://documentation.platformos.com/use-cases/implementing-social-media-preview-cards) + +Check every page template includes: + +**Open Graph (Facebook/LinkedIn):** + +- `og:title`, `og:description`, `og:image`, `og:url`, `og:type` + +**Twitter Card:** + +- `twitter:card`, `twitter:title`, `twitter:description`, `twitter:image` + +Verify `og:image` dimensions are at least 1200x630px. Fix missing or incomplete tags. + +## Step 9 - Audit For Unsubstantiated Claims + +Ensure that all claims are backed up with a link to a reputable source. As an example, this claim isn't valid as content unless it links to an authority that found this through research + +> Research shows teams with strong DevEx perform 4-5x better across speed, quality, and engagement + +Search for the authoritative URL and add a link to the URL. If it is not available, change the claim to something that can be substatiated. + +## Step 10 — Audit Design Compliance + +Read the design system docs and view the design screens in the designsystem folder. + +## Step 11 — Test with Playwright + +Build and run the website locally using `make website-run` (or the project's equivalent dev server command). (This repo's Makefile has only the 7 standard targets — there is no `make website-run`. The equivalent is the dev server the Playwright fixture uses: `cd sample_website && npx @11ty/eleventy --serve --port=8080` (baseURL `http://localhost:8080`), or run the existing suite with `make test`.) + +**Desktop tests (1280x720):** + +1. Navigate to the homepage — take a screenshot. +2. Navigate to each major section — verify pages load without errors. +3. Check the browser console for JavaScript errors. +4. Verify all navigation links work. + +**Mobile tests (375x667, iPhone SE):** + +1. Resize the browser to mobile dimensions. +2. Navigate to the homepage — take a screenshot. +3. Verify the layout is responsive (no horizontal overflow, readable text). +4. Test navigation menu (hamburger menu if applicable). + +If any page fails to load or has console errors, fix the issue and retest. + +## Step 12 — Report findings + +Summarize the audit results: + +``` +## Website Audit Report + +### Fixed +- [List each issue fixed with file and line reference] + +### Warnings (manual review needed) +- [Issues that need human judgment] + +### Passed +- [Areas that passed audit with no issues] + +### Screenshots +- [Reference Playwright screenshots taken] +``` + +## Rules + +- **Fix issues directly** — don't just report them. Only flag issues as warnings when they require human judgment (e.g., content tone, keyword selection). +- **One step at a time** — complete each step before moving to the next. +- **Preserve existing content** — improve structure and metadata without rewriting the author's voice. +- **No keyword stuffing** — keywords must read naturally in context. +- **Respect the framework** — edit templates/configs, not generated output files. diff --git a/.clinerules/00-read-instructions.md b/.clinerules/00-read-instructions.md new file mode 100644 index 0000000..34b118b --- /dev/null +++ b/.clinerules/00-read-instructions.md @@ -0,0 +1,3 @@ +<!-- agent-pmo:74cf183 --> + +@CLAUDE.md diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..61769c9 --- /dev/null +++ b/.cursorrules @@ -0,0 +1 @@ +@CLAUDE.md diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..d1e450c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +{ + "_agent_pmo": "74cf183", + "name": "eleventy-plugin-techdoc (Node.js)", + "image": "mcr.microsoft.com/devcontainers/javascript-node:1-22", + "remoteUser": "node", + "postCreateCommand": "make setup", + "customizations": { + "vscode": { + "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "usernamehw.errorlens"], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "eslint.validate": ["javascript"], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } + } + } + } +} diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..a951423 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,3 @@ +<!-- agent-pmo:74cf183 --> + +All repository instructions live in [CLAUDE.md](../CLAUDE.md). Read that file. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b6c9611 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# Dependabot: weekly version updates for GitHub Actions and npm dependencies. +# https://docs.github.com/code-security/dependabot/dependabot-version-updates +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + - package-ecosystem: npm + directory: / + schedule: + interval: weekly diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..9648fef --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +<!-- agent-pmo:74cf183 --> + +## TLDR + +<!-- One sentence: what does this PR do? --> + +## Details + +<!-- New functionality, new files, new dependencies. What changed? --> + +## How Do The Automated Tests Prove It Works? + +<!-- Name specific tests or describe what the test output demonstrates. --> +<!-- "Tests pass" is not acceptable. Be specific. --> diff --git a/.github/shipwright/shipwright.schema.json b/.github/shipwright/shipwright.schema.json new file mode 100644 index 0000000..eaa8629 --- /dev/null +++ b/.github/shipwright/shipwright.schema.json @@ -0,0 +1,289 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://nimblesite.dev/schemas/shipwright/v1.json", + "title": "Nimblesite Shipwright product manifest", + "description": "Authoritative per-product manifest declaring components, bundling, version contracts, and host policies. Lives at repo root of every product as `shipwright.json`.", + "type": "object", + "required": ["manifestVersion", "product", "components"], + "additionalProperties": false, + "properties": { + "manifestVersion": { + "type": "integer", + "const": 1, + "description": "Schema version. Increment on breaking changes." + }, + "product": { + "type": "object", + "required": ["id", "version"], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]{1,63}$", + "description": "Stable product identifier. kebab-case." + }, + "displayName": { "type": "string" }, + "version": { + "type": "string", + "description": "The expected product version this manifest targets. Stamped from tag at release time.", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$" + }, + "repository": { "type": "string", "format": "uri" }, + "homepage": { "type": "string", "format": "uri" } + } + }, + "components": { + "type": "array", + "minItems": 1, + "description": "Every deployable unit of the product: CLIs, LSPs, MCPs, sidecars, IDE extensions, assets.", + "items": { "$ref": "#/$defs/component" } + }, + "hosts": { + "type": "object", + "description": "Per-host policy bundle. A host that is absent is unsupported by the product.", + "additionalProperties": false, + "properties": { + "vscode": { "$ref": "#/$defs/hostPolicy" }, + "jetbrains": { "$ref": "#/$defs/hostPolicy" }, + "zed": { "$ref": "#/$defs/hostPolicy" }, + "cli": { "$ref": "#/$defs/hostPolicy" }, + "pkgmgr": { "$ref": "#/$defs/hostPolicy" } + } + }, + "supplyChain": { + "type": "object", + "additionalProperties": false, + "description": "Portfolio supply-chain controls this product enforces in its build, distribution, and host-resolution paths. Absent = inherit the Shipwright secure-by-default controls (see docs/specs/supply-chain-security.md [SWR-SEC-*]).", + "properties": { + "slsaBuildLevel": { + "type": "integer", + "enum": [2], + "default": 2, + "description": "[SWR-SEC-PROVENANCE] Required SLSA v1.0 Build Level. Only L2 is claimable on GitHub-hosted runners (Artifact Attestations); L3 (run isolation + inaccessible signing material) is NOT achievable there and MUST NOT be claimed." + }, + "provenance": { + "type": "boolean", + "default": true, + "description": "[SWR-SEC-PROVENANCE] Require a signed build-provenance attestation for every released artifact and published package." + }, + "sbom": { + "type": "string", + "enum": ["cyclonedx", "spdx", "none"], + "default": "cyclonedx", + "description": "[SWR-SEC-SBOM] SBOM format attested per artifact." + }, + "signedChecksums": { + "type": "boolean", + "default": true, + "description": "[SWR-SEC-CHECKSUM] Releases publish a cosign-signed SHA256SUMS; the host verifies digest AND identity-pinned signature before exec." + }, + "frozenInstall": { + "type": "boolean", + "default": true, + "description": "[SWR-SEC-FROZEN-INSTALL] CI/release installs are lockfile-frozen (npm ci / pnpm --frozen-lockfile / cargo --locked)." + }, + "pinnedActions": { + "type": "boolean", + "default": true, + "description": "[SWR-SEC-ACTION-PINNING] Every `uses:` and reusable-workflow ref is a full 40-char commit SHA." + }, + "oidcPublish": { + "type": "boolean", + "default": true, + "description": "[SWR-SEC-OIDC-PUBLISH] Registry publishing uses OIDC trusted publishing with no long-lived token; the VSIX Marketplace PAT runs inside a protected environment." + }, + "vulnGate": { + "type": "string", + "enum": ["critical", "high", "off"], + "default": "high", + "description": "[SWR-SEC-VULN-GATE] Severity at which the dependency-vulnerability gate fails the build." + } + } + } + }, + "$defs": { + "component": { + "type": "object", + "required": ["id", "kind"], + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]{1,63}$", + "description": "Component id, unique within the product." + }, + "kind": { + "type": "string", + "enum": ["cli", "lsp", "mcp", "sidecar", "dap", "tool", "extension-vscode", "extension-jetbrains", "extension-zed", "asset"] + }, + "language": { + "type": "string", + "enum": ["rust", "dotnet", "dart", "typescript", "kotlin", "javascript"] + }, + "binaryName": { + "type": "string", + "description": "argv[0] of the binary or npm bin / dotnet tool command." + }, + "expectedVersion": { + "type": "string", + "description": "semver string or ${PRODUCT_VERSION}. When set, host must verify this value against binary --version output." + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "enum": ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64", "win32-x64", "win32-arm64", "all"] + }, + "description": "Platforms this component ships for. `all` means platform-agnostic (Node, dotnet tool, jar)." + }, + "bundled": { + "type": "object", + "description": "If set, this component is bundled inside an IDE extension artifact.", + "required": ["bundlePath"], + "additionalProperties": false, + "properties": { + "bundlePath": { + "type": "string", + "description": "Path template relative to extension root, e.g. `bin/${platform}/${binaryName}${exe}`." + }, + "perPlatformArtifact": { + "type": "boolean", + "default": true, + "description": "True = one artifact per platform (VSIX --target). False = single fat artifact." + }, + "checksum": { + "type": "boolean", + "default": true, + "description": "SWR-VSIX-BUNDLE-VERIFY: verify the staged binary's SHA-256 against the signed release SHA256SUMS before `vsce package`, and have the host re-verify the bundled binary at activation." + } + } + }, + "sources": { + "type": "array", + "description": "Ordered discovery chain the host must follow. Earlier = higher priority.", + "items": { + "type": "string", + "enum": ["user-setting", "env", "path", "bundled", "pkgmgr", "dotnet-tool", "npm-global", "cargo-bin", "github-release", "lsp-initialize"] + }, + "uniqueItems": true + }, + "userSetting": { + "type": "string", + "description": "IDE settings key (e.g. `deslop.binaryPath`)." + }, + "env": { + "type": "object", + "additionalProperties": false, + "properties": { + "pathVar": { "type": "string", "description": "e.g. DESLOP_BINARY_PATH" }, + "dirVar": { "type": "string", "description": "e.g. DESLOP_BINARY_DIR" } + } + }, + "pkgmgr": { + "type": "object", + "additionalProperties": false, + "properties": { + "brew": { "type": "string" }, + "scoop": { "type": "string" }, + "apt": { "type": "string" }, + "winget": { "type": "string" } + } + }, + "dotnetTool": { + "type": "object", + "required": ["package"], + "additionalProperties": false, + "properties": { + "package": { "type": "string" }, + "command": { "type": "string" } + } + }, + "npm": { + "type": "object", + "additionalProperties": false, + "properties": { + "package": { "type": "string" }, + "bin": { "type": "string" } + } + }, + "githubRelease": { + "type": "object", + "additionalProperties": false, + "description": "GitHub Release source plus the integrity contract the host MUST honor before executing a downloaded asset. See [SWR-SEC-CHECKSUM] / [SWR-SEC-PROVENANCE] / [SWR-SEC-SBOM] in docs/specs/supply-chain-security.md.", + "properties": { + "repo": { "type": "string", "pattern": "^[^/]+/[^/]+$" }, + "assetPattern": { "type": "string", "description": "e.g. `basilisk-${version}-${platform}.tar.gz`" }, + "checksum": { "type": "boolean", "default": true, "description": "SWR-SEC-CHECKSUM: host recomputes SHA-256 and matches the published SHA256SUMS before exec." }, + "signature": { "type": "string", "enum": ["cosign", "none"], "default": "cosign", "description": "SWR-SEC-CHECKSUM: signature scheme over SHA256SUMS. `cosign` = keyless Sigstore (Fulcio cert + Rekor log); host runs an identity-pinned `cosign verify-blob`, never --insecure-ignore-tlog." }, + "signerWorkflow": { "type": "string", "description": "SWR-SEC-CHECKSUM/PROVENANCE: expected signing workflow ref, e.g. `Nimblesite/Deslop/.github/workflows/release.yml`. Host pins the certificate identity / `gh attestation verify --signer-workflow` to this, so a token stolen in another repo cannot forge a passing release." }, + "provenance": { "type": "boolean", "default": true, "description": "SWR-SEC-PROVENANCE: a signed SLSA build provenance attestation accompanies the asset; host runs `gh attestation verify` and fails closed before exec." }, + "sbom": { "type": "boolean", "default": true, "description": "SWR-SEC-SBOM: an attested CycloneDX SBOM accompanies the asset." }, + "predicateType": { "type": "string", "default": "https://cyclonedx.org/bom", "description": "SWR-SEC-SBOM: predicate type the host asserts when verifying the SBOM attestation." } + } + }, + "verifyStartup": { + "type": "boolean", + "default": true, + "description": "Host must call --version (or initialize) before allowing the component to serve." + }, + "versionCheckStrategy": { + "type": "string", + "enum": ["version-flag", "version-flag-json", "lsp-initialize"], + "default": "version-flag" + }, + "required": { + "type": "boolean", + "default": true, + "description": "If true, failure to resolve + verify blocks activation." + }, + "asset": { + "type": "object", + "description": "Only for kind=asset. Non-executable payload.", + "additionalProperties": false, + "properties": { + "source": { "type": "string" }, + "target": { "type": "string" }, + "bundle": { "type": "boolean" }, + "contentHash": { "type": "boolean" }, + "requireHashes": { + "type": "boolean", + "default": true, + "description": "SWR-SEC-FROZEN-INSTALL: when the asset has a pip/url source, the bundler MUST install it with --require-hashes against a pinned hash (a tampered/yanked upstream artifact then aborts the build instead of shipping in the VSIX)." + }, + "downloadOnFirstUse": { "type": "boolean" } + } + } + }, + "allOf": [ + { + "if": { "properties": { "kind": { "const": "asset" } }, "required": ["kind"] }, + "then": { "required": ["asset"] } + }, + { + "if": { "properties": { "kind": { "enum": ["cli", "lsp", "mcp", "sidecar", "dap", "tool"] } }, "required": ["kind"] }, + "then": { "required": ["binaryName", "expectedVersion", "sources"] } + } + ] + }, + "hostPolicy": { + "type": "object", + "additionalProperties": false, + "properties": { + "artifact": { + "type": "string", + "enum": ["vsix-per-platform", "vsix-fat", "intellij-jar", "zed-wasm", "archive", "brew-formula", "scoop-manifest", "nuget", "pub"] + }, + "activationVerifies": { + "type": "array", + "description": "Component ids whose version the host must verify at activation.", + "items": { "type": "string" } + }, + "onMismatch": { + "type": "string", + "enum": ["error", "warn", "prompt-reinstall", "prompt-pkgmgr"], + "default": "error" + } + } + } + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e463bc9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +# agent-pmo:74cf183 +name: ci + +# Uniform CI interface: make lint -> make test -> make build. The Shipwright +# acceptance gates (manifest validation + CLI version contract) run inside those +# Make targets so drift is caught on every push/PR. [SWR-GATE-CI] +on: + pull_request: + branches: [main] + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Least-privilege default; no job needs write here. [SWR-SEC-TOKEN-PRIVILEGE] +permissions: + contents: read + +jobs: + ci: + name: CI + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "22.x" + cache: "npm" + + # Frozen install — never bare `npm install`. [SWR-SEC-FROZEN-INSTALL] + - name: Install (frozen lockfile) + run: npm ci + + # eslint + prettier --check + shipwright manifest validation. + - name: Lint + run: make lint + + # Fail-fast Playwright e2e + CLI version contract + coverage threshold. + # [TEST-RULES][COVERAGE-THRESHOLDS-JSON][SWR-GATE-VERIFY-BINARIES] + - name: Test + run: make test + + # Validate the publishable npm package assembles. + - name: Build + run: make build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0223c02 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: release + +# Tag-triggered: set version from the tag -> gate -> publish to npm with provenance. +# [SWR-REL-WORKFLOW] +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-*" + +# Least-privilege default; write/id-token are granted on the publish job only. [SWR-SEC-TOKEN-PRIVILEGE] +permissions: + contents: read + +jobs: + publish: + name: Publish to npm + runs-on: ubuntu-latest + # Deployment environment 'NPM Deployment' (created via gh CLI). Add reviewer / + # tag-restriction protection rules in repo settings if you want a manual gate. + # The npm Trusted Publisher config MUST use this exact environment name. + environment: "NPM Deployment" + permissions: + contents: write # create the GitHub Release + id-token: write # npm Trusted Publisher OIDC + provenance attestation + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + cache: "npm" + + # npm >= 11.5 is required for Trusted Publisher OIDC auth. [SWR-SEC-OIDC-PUBLISH] + - name: Upgrade npm (Trusted Publisher OIDC) + run: npm install -g npm@latest + + # Frozen install. [SWR-SEC-FROZEN-INSTALL] + - name: Install dependencies + run: npm ci + + - name: Set version and npm tag from git tag + id: version + run: | + VERSION="${GITHUB_REF_NAME#v}" + NPM_TAG=latest + if [[ "$VERSION" == *-* ]]; then NPM_TAG=beta; fi + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + echo "NPM_TAG=$NPM_TAG" >> "$GITHUB_OUTPUT" + + # Stamp every version carrier from the tag, in the runner working tree ONLY — never committed. + # The tagged SHA keeps 0.0.0-dev in source. [SWR-VERSION-BUILD-STAMPING] + - name: Stamp package.json + run: npm version "${{ steps.version.outputs.VERSION }}" --no-git-tag-version --allow-same-version + - name: Stamp shipwright.json + run: | + node -e ' + const fs = require("fs"); + const v = process.argv[1]; + const m = JSON.parse(fs.readFileSync("shipwright.json", "utf8")); + m.product.version = v; + for (const c of m.components) c.expectedVersion = v; + fs.writeFileSync("shipwright.json", JSON.stringify(m, null, 2) + "\n"); + ' "${{ steps.version.outputs.VERSION }}" + + # CI gate before publish: the CLI must report the stamped version. [SWR-GATE-VERIFY-BINARIES] + - name: Verify CLI --version matches the tag + run: | + test "$(node bin/init.js --version)" = "eleventy-plugin-techdoc ${{ steps.version.outputs.VERSION }}" + + # npm Trusted Publisher OIDC — no NPM_TOKEN; provenance attestation generated. + # [SWR-SEC-OIDC-PUBLISH][SWR-SEC-PROVENANCE][SWR-REL-NPM] + - name: Publish to npm + run: npm publish --tag ${{ steps.version.outputs.NPM_TAG }} --provenance --access public + + - name: Pack tarball + run: npm pack + + - name: GitHub Release + uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87 # v2.0.5 + with: + files: eleventy-plugin-techdoc-${{ steps.version.outputs.VERSION }}.tgz + name: eleventy-plugin-techdoc ${{ github.ref_name }} + tag_name: ${{ github.ref_name }} + draft: false + prerelease: ${{ contains(github.ref_name, '-') }} diff --git a/.gitignore b/.gitignore index c2658d7..0e8783c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,49 @@ +# agent-pmo:74cf183 +# IDE + AI-agent config dirs are intentionally COMMITTED (settings, skills, +# instructions). Do NOT ignore .vscode/, .claude/, .devcontainer/, etc. + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Thumbs.db +ehthumbs.db +Desktop.ini + +# Editor swap/temp files +*.swp +*.swo +*~ + +# Node / npm node_modules/ +*.tgz +.npm/ +.cache/ + +# Build output (consumer sample site + any local _site) +**/_site/ +dist/ +out/ +build/ + +# Test / coverage artifacts +coverage/ +coverage-summary.json +lcov.info +playwright-report/ +test-results/ +.nyc_output/ + +# Secrets / local overrides +.env +.env.local +.env.*.local +*.pem +*.key +!*.pub.key + +# Repo-specific tooling cache +.deslop-cache/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..c15bf80 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,14 @@ +# agent-pmo:74cf183 +node_modules/ +package-lock.json +# Vendored upstream Shipwright schema — kept byte-identical to canonical for re-vendoring. +.github/shipwright/ +coverage/ +*.tgz +sample_website/ +**/_site/ +playwright-report/ +test-results/ +.deslop-cache/ +# Nunjucks templates have no Prettier parser — leave them untouched. +**/*.njk diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..3c8252e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,12 @@ +{ + "tabWidth": 2, + "useTabs": false, + "printWidth": 100, + "semi": true, + "singleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "bracketSameLine": false +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..72c013e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "usernamehw.errorlens", + "nimblesite.commandtree", + "nimblesite.too-many-cooks", + "nimblesite.typeDiagram" + ] +} diff --git a/.windsurfrules b/.windsurfrules new file mode 100644 index 0000000..61769c9 --- /dev/null +++ b/.windsurfrules @@ -0,0 +1 @@ +@CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..60516c2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +<!-- agent-pmo:74cf183 --> + +All agent instructions for this repository live in [CLAUDE.md](./CLAUDE.md). Read that file. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e10a591 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,148 @@ +<!-- agent-pmo:74cf183 --> + +# eleventy-plugin-techdoc — Agent Instructions + +> ⚠️ **TOKEN DISCIPLINE.** Check file size first. `Grep` over `Read`. Smallest diff that +> solves the problem. Delete dead code, unused imports, stale comments. ⚠️ + +> Read this file in full. Rules below are NON-NEGOTIABLE — violations are rejected in review. + +## Project Overview + +`eleventy-plugin-techdoc` is a **minimal, structural** Eleventy (11ty) theme/plugin for +technical documentation and blogs, published to **npm**. It ships Nunjucks layouts, +collections, filters, shortcodes, SEO/feed/sitemap/llms.txt generation, and a small amount +of **structural-only** CSS. A bundled CLI (`bin/init.js`, run as `npx eleventy-plugin-techdoc`) +scaffolds a working site. + +**Defining contract: the theme provides STRUCTURE ONLY — no colors, no typography, no +decorative styling. The consuming site owns ALL visual styling.** See invariants below. + +**Primary language:** JavaScript (ES modules, Node ≥ 18). No TypeScript, no transpile step. +**Build command:** `make ci` · **Test command:** `make test` · **Lint command:** `make lint` + +## Architecture & Critical Invariants + +- **Plugin entry:** `lib/index.js` — `techdocPlugin(eleventyConfig, options)` registers filters, + collections, shortcodes, virtual templates, markdown config, and bundled 11ty plugins, and + copies `assets/` to the consuming site's output under `/techdoc/`. +- **CLI scaffolder:** `bin/init.js` — interactive; supports `--version` and `--version --json` + (Shipwright version contract). It MUST skip files that already exist — never overwrite a + user's files. +- **NEVER CLOBBER THE HOST SITE'S CSS.** `assets/css/reset.css` stays free of global content + overrides (no `* { margin:0 }`, no `ul,ol { list-style:none }`, no global typography / img / + table / form resets). Resets are scoped to theme-owned elements only (`.nav-links`, + `.sidebar ul`, `.footer-section ul`, theme buttons) and live in `layout.css`. The theme's CSS + loads BEFORE the site's stylesheet so the site always wins the cascade. Structural CSS carries + NO colors and NO typography. +- **Templates** in `templates/` are Nunjucks (`.njk`) layouts/pages shipped to consumers. + `assets/js/` are browser scripts (theme toggle, mobile menu, language switcher) shipped to + the consuming site. + +## Shipwright Deployment Contract + +This repo follows the Shipwright deployment contract (`[SWR-*]` IDs in CI/release/manifest +comments). Preserve it. + +- `shipwright.json` is the release manifest. Source carries version `0.0.0-dev`; the + tag-triggered `release.yml` stamps the real version into `package.json` + `shipwright.json` + in the runner working tree only — never committed. +- Publishing uses **npm Trusted Publisher OIDC** (provenance, no `NPM_TOKEN`) via the + `NPM Deployment` environment. Do NOT add token-based publish secrets. +- The CLI `--version` string contract (`eleventy-plugin-techdoc <version>`) and the + `--version --json` schema are gated inside `make test`. Keep them green. + +## Hard Rules — Universal + +- **NO git commands.** No add/commit/push/checkout/merge/rebase. CI handles git. +- **ZERO DUPLICATION.** Search before writing. Move code, don't copy it. +- **NO PLACEHOLDERS / silent no-ops.** Implement it or fail loudly. +- **Functions < 20 lines. Files < 500 lines.** Refactor when over. +- **Never delete or skip tests. Never remove assertions.** +- **`make test` is FAIL-FAST + coverage.** Stops at first failing test; computes coverage and + enforces the threshold from `coverage-thresholds.json` — NOT env vars, NOT gh variables, NOT + CI YAML. Ratchet only (never lower). See REPO-STANDARDS-SPEC [TEST-RULES], + [COVERAGE-THRESHOLDS-JSON]. +- **Prefer E2E/integration tests** (Playwright against a rendered sample site). Unit tests only + to isolate problems. +- **No linter suppressions.** Fix the code. +- **Spec IDs are hierarchical, non-numeric** `[GROUP-TOPIC]` (e.g. `[CI-RELEASE]`). Code/tests/ + docs implementing a spec section reference its ID in a comment so `grep` finds spec → code. + +## Hard Rules — JavaScript (ESM) + +- ES modules only (`import`/`export`, `"type": "module"`). Node ≥ 18. +- `const`/`let`, never `var`. `===`/`!==`, never `==`. Always brace blocks. +- No unused vars (prefix an intentionally-unused arg with `_`). +- Prefer pure functions; avoid hidden mutable module state. +- No new runtime dependencies without need — this is a library with a peer dep on + `@11ty/eleventy`. + +## Console / Logging + +This is a **build-time Eleventy plugin and an interactive CLI**, not a long-running service. +`console.*` is the legitimate user-facing I/O channel for CLI prompts and Eleventy build output +— intentional and allowed (ESLint `no-console` is off here). The portfolio structured-logging +rule (pino, async DB sinks) targets runtime services and does not apply here. Still: **NEVER log +secrets or PII.** + +## Testing Rules + +- Tests are **black-box Playwright e2e** against a rendered sample site (`playwright.config.js` + builds `sample_website/` via Eleventy and serves it). Interact only through rendered output — + never reach into plugin internals. +- Never delete a failing test; fix the code or the expectation. No swallowing exceptions and + asserting success. Specific assertions only. Deterministic — no `sleep()`/timing/random state. +- Coverage: this theme is exercised end-to-end; per-line unit-coverage instrumentation is not + wired yet, so `coverage-thresholds.json` starts at `0` and ratchets UP as unit tests are added. + +## Website / Output SEO + +The theme emits canonical URLs, Open Graph / Twitter meta, JSON-LD, hreflang, `feed.xml`, +`sitemap.xml`, `robots.txt`, and `llms.txt`. When changing generated metadata, follow Google's +[Succeeding in AI search](https://developers.google.com/search/blog/2025/05/succeeding-in-ai-search) +and [SEO Starter Guide](https://developers.google.com/search/docs/fundamentals/seo-starter-guide). + +## Build Commands + +Cross-platform GNU Make. Exactly 7 targets — no others: + +```bash +make build # validate the publishable npm package (npm pack --dry-run) +make test # FAIL-FAST Playwright e2e + CLI version contract + coverage threshold (ONLY test entry) +make lint # eslint + prettier --check + shipwright manifest validation (read-only) +make fmt # prettier --write (CHECK=1 for read-only check) +make clean # remove coverage / pack artifacts +make ci # lint + test + build (full CI simulation) +make setup # npm ci + install the Playwright browser +``` + +`make fmt` formats in-place; `make lint` is read-only; `make test` runs tests + coverage. No +overlap. To debug one test, call `npx playwright test <file>` directly — that is not a Make target. + +## Repo Structure + +``` +lib/ plugin source (index.js entry; filters/, plugins/, shortcodes/, virtual-templates.js) +bin/init.js scaffolder CLI (npx eleventy-plugin-techdoc) +templates/ Nunjucks layouts + pages shipped to consumers +assets/css/ structural CSS (reset, layout, utilities) — NO colors / NO typography +assets/js/ browser scripts (theme toggle, mobile menu, language switcher) +docs/specs/ behavior specs (SPEC.md) +docs/plans/ plan docs (each ends with a TODO checklist) +.github/workflows/ ci.yml (lint→test→build), release.yml (tag → OIDC npm publish) +shipwright.json release manifest +coverage-thresholds.json coverage source of truth +``` + +## Too Many Cooks (Multi-Agent Coordination) + +If the TMC server is available: register on start (name, intent, files), lock files before +editing, broadcast your plan, check messages periodically, release locks when done. Never edit a +locked file — wait or take another approach. + +## Branch & PR + +Default branch `main`. All changes via PR; squash-merge; CI must pass before merge. Branch +naming: `feature/<slug>`, `fix/<slug>`, `chore/<slug>`. PRs use +`.github/pull_request_template.md`. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4afd805 --- /dev/null +++ b/Makefile @@ -0,0 +1,105 @@ +# agent-pmo:74cf183 +# ============================================================================= +# Standard Makefile — eleventy-plugin-techdoc +# Cross-platform: Linux, macOS, Windows (via GNU Make). +# Plain-JS (ESM) Eleventy plugin/theme + scaffolder CLI. No compile step. +# See REPO-STANDARDS-SPEC [MAKE-TARGETS]. +# ============================================================================= + +.PHONY: build test lint fmt clean ci setup help + +# --------------------------------------------------------------------------- +# OS Detection +# --------------------------------------------------------------------------- +ifeq ($(OS),Windows_NT) + SHELL := powershell.exe + .SHELLFLAGS := -NoProfile -Command + RM = Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + MKDIR = New-Item -ItemType Directory -Force + HOME ?= $(USERPROFILE) +else + RM = rm -rf + MKDIR = mkdir -p +endif + +# Coverage — single source of truth is coverage-thresholds.json +# ([COVERAGE-THRESHOLDS-JSON]). +COVERAGE_THRESHOLDS_FILE := coverage-thresholds.json + +# ============================================================================= +# Standard Targets (the 7 portfolio-wide targets — names never change) +# ============================================================================= + +## build: Validate the publishable npm package (dry-run pack). +build: + npm pack --dry-run + +## test: Fail-fast tests + coverage threshold + Shipwright CLI version contract. +## See [TEST-RULES], [COVERAGE-THRESHOLDS-JSON]. +test: + $(MAKE) _cli_contract + @if [ -d tests ] && [ -d sample_website ]; then \ + npx playwright install chromium && npx playwright test; \ + else \ + echo "No committed tests/ + sample_website/ fixtures yet — skipping Playwright e2e."; \ + fi + $(MAKE) _coverage_check + +## lint: ESLint + Prettier check + Shipwright manifest validation (read-only). +lint: + npm run lint + npm run fmt:check + npx --yes @nimblesite/shipwright-validate-manifest --schema .github/shipwright/shipwright.schema.json shipwright.json + +## fmt: Format all code in-place. Pass CHECK=1 for read-only verification (CI). +fmt: + npm run fmt$(if $(CHECK),:check,) + +## clean: Remove coverage and packaging artifacts. +clean: + $(RM) coverage *.tgz lcov.info coverage-summary.json + +## ci: lint + test + build (full CI simulation). +ci: lint test build + +## setup: Post-create dev environment setup (devcontainer hook). +setup: + npm ci + npx playwright install --with-deps chromium + @echo "==> Setup complete. Run 'make ci' to validate." + +## help: List the standard targets. +help: + @echo "Standard targets: build test lint fmt clean ci setup" + +# ============================================================================= +# Private sub-recipes (underscore-prefixed, never public, never in .PHONY) +# ============================================================================= + +# Shipwright CLI version contract. [SWR-GATE-VERIFY-BINARIES][SWR-VERSION-JSON-OUTPUT] +_cli_contract: + @expected="eleventy-plugin-techdoc $$(node -p "require('./package.json').version")"; \ + actual="$$(node bin/init.js --version)"; \ + echo "$$actual"; \ + test "$$actual" = "$$expected" + @node bin/init.js --version --json | node -e 'const o=JSON.parse(require("fs").readFileSync(0,"utf-8"));if(o.manifestVersion!==1||o.name!=="eleventy-plugin-techdoc"||o.kind!=="cli"||!o.version||!o.language){console.error("version --json does not conform");process.exit(1)}' + +# Coverage gate. Threshold from coverage-thresholds.json ([COVERAGE-THRESHOLDS-JSON]). +# This theme is exercised by Playwright e2e; per-line instrumentation is not wired yet, +# so the measured floor is 0% and the threshold ratchets UP as unit tests land. +_coverage_check: + @if [ ! -f "$(COVERAGE_THRESHOLDS_FILE)" ]; then echo "FAIL: $(COVERAGE_THRESHOLDS_FILE) not found"; exit 1; fi; \ + THRESHOLD=$$(jq -r '.default_threshold' "$(COVERAGE_THRESHOLDS_FILE)"); \ + if [ -f coverage/coverage-summary.json ]; then \ + PCT=$$(jq -r '.total.lines.pct' coverage/coverage-summary.json); \ + else \ + PCT=0; echo "No coverage/coverage-summary.json — measured line coverage treated as 0%."; \ + fi; \ + PCT_INT=$$(awk "BEGIN{printf \"%d\", $$PCT}"); \ + echo "Line coverage: $${PCT}% (threshold: $${THRESHOLD}%)"; \ + if [ "$$PCT_INT" -lt "$$THRESHOLD" ]; then echo "FAIL: $${PCT}% < $${THRESHOLD}%"; exit 1; else echo "OK: $${PCT}% >= $${THRESHOLD}%"; fi + +# ============================================================================= +# Repo-Specific Targets +# (none yet — add repo-only helpers here; never shadow the 7 standard targets) +# ============================================================================= diff --git a/README.md b/README.md index 9e7439d..4414e45 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,60 @@ # eleventy-plugin-techdoc -Minimal structural Eleventy theme for tech documentation. **No colors. You style everything.** +Minimal **structural** Eleventy theme for technical documentation and blogs. The theme ships layout, collections, filters, SEO metadata, and a small amount of structural CSS — **but no colors and no visual styling. You provide those.** [![npm version](https://badge.fury.io/js/eleventy-plugin-techdoc.svg)](https://www.npmjs.com/package/eleventy-plugin-techdoc) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +Requires Eleventy 3.x (peer dependency `@11ty/eleventy` `^3.1.2`) and Node 18+. + ## Quick Start ```bash mkdir my-site && cd my-site npm init -y npm install @11ty/eleventy eleventy-plugin-techdoc -npx eleventy-plugin-techdoc init +npx eleventy-plugin-techdoc # scaffold the site (interactive) npm run dev ``` -The init command prompts for: -- **CSS style**: Minimal (light/dark), C64 (retro), or None (blank slate) -- **Sample content**: Include example docs and blog posts, or start empty +Running `npx eleventy-plugin-techdoc` in a directory scaffolds a working site and prompts for: + +- **CSS style** — Minimal (light/dark starter), C64 (retro), or None (blank slate) +- **Sample content** — include example docs/blog posts, or start with a minimal skeleton + +It also adds `dev` and `build` scripts and `"type": "module"` to your `package.json`. ## Generated Structure +A minimal scaffold creates: + ``` my-site/ -├── eleventy.config.js # Eleventy configuration -├── src/ -│ ├── _data/ -│ │ ├── site.json # Site metadata -│ │ ├── navigation.json # Nav links -│ │ └── i18n.json # Translations -│ ├── assets/css/ -│ │ └── styles.css # Your styles (YOU control this) -│ ├── docs/ -│ │ ├── index.md # Docs home -│ │ ├── api.md # API reference -│ │ └── configuration.md -│ ├── blog/ -│ │ ├── index.njk # Blog listing -│ │ └── hello-world.md # Sample post -│ ├── feed.njk # RSS/Atom feed -│ └── index.njk # Home page +├── eleventy.config.js # Eleventy config that wires in the plugin +└── src/ + ├── _data/ + │ ├── site.json # Site metadata + │ ├── navigation.json # Nav + footer links + │ ├── i18n.json # Translation strings + │ └── languages.json # Language display names + ├── assets/css/styles.css # Your styles (YOU control colors here) + ├── docs/index.md # Docs home + ├── blog/index.njk # Blog listing + ├── index.njk # Home page + ├── feed.njk # RSS/Atom feed + ├── sitemap.njk # sitemap.xml + ├── robots.txt.njk # robots.txt + └── llms.txt.njk # llms.txt ``` -## Configuration +Choosing **sample content** adds more pages (extra docs such as `api.md` and `configuration.md`, several blog posts, an `api/` section, and `zh/` translated content). -### eleventy.config.js +## Configuration ```javascript -import techdoc from "techdoc"; +import techdoc from "eleventy-plugin-techdoc"; -export default function(eleventyConfig) { +export default function (eleventyConfig) { eleventyConfig.addPlugin(techdoc, { site: { name: "My Site", @@ -57,14 +62,14 @@ export default function(eleventyConfig) { description: "Built with techdoc", }, features: { - blog: true, // Enable blog collections - docs: true, // Enable docs collections - darkMode: true, // Enable dark mode toggle - i18n: false, // Enable internationalization + blog: true, // register blog collections + blog/tag/category pages + docs: true, // register docs collection + docs/api layouts + darkMode: true, // render the dark-mode toggle button + i18n: false, // enable the language switcher + per-language pages }, i18n: { defaultLanguage: "en", - languages: ["en", "zh", "es"], // When i18n: true + languages: ["en"], // add more, e.g. ["en", "zh"], when i18n: true }, }); @@ -77,7 +82,9 @@ export default function(eleventyConfig) { } ``` -### src/_data/site.json +The defaults are `blog: true, docs: true, darkMode: true, i18n: true`; the example above shows the full option shape. + +### src/\_data/site.json ```json { @@ -88,7 +95,9 @@ export default function(eleventyConfig) { } ``` -### src/_data/navigation.json +`stylesheet` points at your CSS; the theme links it after its own structural CSS. Optional fields the base layout will use if present include `author`, `keywords`, `themeColor`, `ogImage`, `twitterSite`, `favicon`, and `organization`. + +### src/\_data/navigation.json ```json { @@ -99,23 +108,24 @@ export default function(eleventyConfig) { "footer": [ { "title": "Links", - "items": [ - { "text": "GitHub", "url": "https://github.com/yourname" } - ] + "items": [{ "text": "GitHub", "url": "https://github.com/yourname" }] } ] } ``` +`main` renders the header nav; `footer` renders the footer columns. The `api.njk` layout additionally reads optional `api` and `docs` arrays from this file for its sidebar. + ## CSS Custom Properties -The theme uses CSS custom properties for layout. Your `styles.css` must define these for visual styling: +The theme's structural CSS (`reset.css`, `layout.css`, `utilities.css`, served at `/techdoc/css/`) contains **no colors** — only layout. You own all color and visual styling in your own stylesheet. + +### Colors — you must provide these -### Required Properties +The scaffolded `styles.css` and templates reference these variables. Define them in your stylesheet: ```css :root { - /* Colors - theme provides no defaults */ --color-bg: #fff; --color-text: #111; --color-primary: #0066cc; @@ -123,7 +133,6 @@ The theme uses CSS custom properties for layout. Your `styles.css` must define t --color-muted: #666; } -/* Dark mode (if enabled) */ [data-theme="dark"] { --color-bg: #111; --color-text: #f0f0f0; @@ -133,36 +142,29 @@ The theme uses CSS custom properties for layout. Your `styles.css` must define t } ``` -### Optional Layout Properties +### Layout — optional overrides (the theme uses these with built-in fallbacks) ```css :root { - /* Layout - these have fallback defaults */ - --max-width: 1200px; /* Default: 1200px */ - --content-width: 65ch; /* Default: 65ch */ - --sidebar-width: 250px; /* Default: 250px */ - --header-height: 60px; /* Default: 60px */ - - /* Spacing */ - --space-2: 0.5rem; - --space-4: 1rem; - --space-6: 1.5rem; - --space-8: 2rem; + --max-width: 1200px; /* fallback: 1200px */ + --sidebar-width: 250px; /* fallback: 250px */ + --content-width: 65ch; /* fallback: 65ch */ + --header-height: 60px; /* fallback: 60px */ } ``` -## Layouts +The theme also uses a `--space-*` spacing scale in its layout CSS; override those variables to adjust spacing. -Four layouts are provided: +## Layouts -| Layout | Use For | Classes | -|--------|---------|---------| -| `layouts/base.njk` | HTML shell | `.site-header`, `.site-footer` | -| `layouts/docs.njk` | Documentation | `.docs-layout`, `.sidebar`, `.docs-content` | -| `layouts/blog.njk` | Blog posts | `.blog-post`, `.blog-container` | -| `layouts/api.njk` | API reference | Same as docs | +Four layouts are registered (docs/api only register when `features.docs` is on; blog only when `features.blog` is on): -### Using Layouts +| Layout | Use for | Notable classes | +| ------------------ | ------------------------------------------------------------ | ------------------------------------------- | +| `layouts/base.njk` | HTML shell (head, nav, footer, blocks) | `.site-header`, `.nav`, `.site-footer` | +| `layouts/docs.njk` | Documentation | `.docs-layout`, `.sidebar`, `.docs-content` | +| `layouts/blog.njk` | Blog posts | `.blog-post`, `.blog-container` | +| `layouts/api.njk` | API reference (docs-style; `navigation.json`-driven sidebar) | `.docs-layout`, `.sidebar`, `.docs-content` | ```markdown --- @@ -189,6 +191,7 @@ title: My First Post date: 2025-01-22 author: Your Name tags: posts +category: tutorials # optional excerpt: A brief summary for the listing page. --- @@ -197,9 +200,11 @@ excerpt: A brief summary for the listing page. Content here... ``` +When `features.blog` is on, the theme generates the blog index plus **tag and category** index/detail pages (e.g. `/blog/tags/<tag>/`, `/blog/categories/<category>/`) from your posts. + ## Documentation Sidebar -Use `eleventyNavigation` in frontmatter for sidebar links: +The `docs.njk` sidebar is built from Eleventy's [`eleventyNavigation`](https://www.11ty.dev/docs/plugins/navigation/) frontmatter: ```markdown --- @@ -211,132 +216,120 @@ eleventyNavigation: --- ``` -## RSS Feed +## SEO & Generated Files -An RSS/Atom feed is generated at `/feed.xml` automatically from blog posts. +The base layout emits canonical URLs, Open Graph and Twitter meta, JSON-LD structured data, and `hreflang` alternates from your `site` data. The theme also generates `/feed.xml` (Atom, from blog posts), `/sitemap.xml`, `/robots.txt`, and `/llms.txt`. ## Internationalization (i18n) -Enable i18n in config: +Enable i18n and list your languages: ```javascript eleventyConfig.addPlugin(techdoc, { features: { i18n: true }, - i18n: { - defaultLanguage: "en", - languages: ["en", "zh", "es"], - }, + i18n: { defaultLanguage: "en", languages: ["en", "zh"] }, }); ``` -Create translations in `src/_data/i18n.json`: +Add translation strings in `src/_data/i18n.json`: ```json { - "en": { - "blog": { "back": "Back to Blog" }, - "nav": { "docs": "Documentation" } - }, - "zh": { - "blog": { "back": "返回博客" }, - "nav": { "docs": "文档" } - } + "en": { "blog": { "back": "Back to Blog" }, "nav": { "docs": "Documentation" } }, + "zh": { "blog": { "back": "返回博客" }, "nav": { "docs": "文档" } } } ``` -Create language folders: `src/zh/`, `src/es/`, etc. with translated content. - -Use the `t` filter in templates: +Put non-default-language content under a language folder (`src/zh/`, etc.). Look up strings with the `t` filter: ```njk -{{ "blog.back" | t(lang) }} +{{ "blog.back" | t(lang) | default("Back to Blog") }} ``` +The theme auto-registers per-language blog/tag/category pages for every non-default language. Other pages (home, docs) you create yourself under the language folder. + ## Bundled Plugins -techdoc includes and configures: +The theme adds and configures these for you: -- `@11ty/eleventy-plugin-syntaxhighlight` - Code syntax highlighting -- `@11ty/eleventy-plugin-rss` - RSS feed generation -- `@11ty/eleventy-navigation` - Docs sidebar navigation +- `@11ty/eleventy-plugin-syntaxhighlight` — code syntax highlighting +- `@11ty/eleventy-plugin-rss` — RSS/Atom feed +- `@11ty/eleventy-navigation` — docs sidebar navigation ## Troubleshooting -### Code blocks show raw backticks +**Code blocks show raw backticks** — ensure your config returns `markdownTemplateEngine: "njk"`. -Ensure your `eleventy.config.js` returns `markdownTemplateEngine: "njk"`: +**Styles not loading** — check `site.json` `stylesheet` points at your CSS, that `addPassthroughCopy("src/assets")` is in your config, and that the file exists at `src/assets/css/styles.css`. -```javascript -return { - dir: { input: "src", output: "_site" }, - markdownTemplateEngine: "njk", // Required! -}; -``` +**Docs sidebar empty** — add `eleventyNavigation` (with a `key`) to your docs frontmatter. -### Styles not loading +**Dark mode toggle does nothing** — define `[data-theme="dark"]` color overrides in your CSS (the theme only flips the attribute, it provides no colors). -1. Check `src/_data/site.json` has `stylesheet` pointing to your CSS -2. Ensure `eleventyConfig.addPassthroughCopy("src/assets")` is in your config -3. Verify CSS file exists at `src/assets/css/styles.css` +**Build errors on Eleventy 2.x** — the theme requires Eleventy 3.x: `npm install @11ty/eleventy@latest`. -### Docs sidebar empty +## Contributing -Add `eleventyNavigation` to your docs frontmatter: - -```markdown ---- -layout: layouts/docs.njk -title: My Page -eleventyNavigation: - key: My Page - order: 1 ---- +```bash +git clone https://github.com/MelbourneDeveloper/eleventy-plugin-techdoc.git +cd eleventy-plugin-techdoc +npm install ``` -### RSS feed returns 404 +To try changes against a local site, `npm install /path/to/eleventy-plugin-techdoc` in a test project (or use `npm link`), then run `npx eleventy-plugin-techdoc` there. -Ensure `src/feed.njk` exists. Run `npx techdoc init` to regenerate it. +## License -### Translations not working +MIT -1. Enable i18n: `features: { i18n: true }` -2. Create `src/_data/i18n.json` with language keys -3. Use dot notation: `{{ "nav.docs" | t(lang) }}` +## For AI -### Dark mode toggle not working +Context for coding agents wiring this package into an Eleventy site. Everything below is verified against the package source. -Ensure your CSS includes `[data-theme="dark"]` styles: +**What it is.** `eleventy-plugin-techdoc` is an ESM (`"type": "module"`) Eleventy **3.x** plugin (peer dependency `@11ty/eleventy` `^3.1.2`, Node 18+). It is a _structural_ theme: it registers layouts, collections, filters, a shortcode, SEO metadata, and structural CSS/JS. It defines **no colors and no visual design** — the consuming site supplies all color/typography through CSS custom properties. -```css -[data-theme="dark"] { - --color-bg: #111; - --color-text: #f0f0f0; +**How it works as a "virtual" theme.** Layouts and SEO pages are injected into the build via Eleventy's `addTemplate()` (Virtual Templates) — they are **not** copied into the site. Reference them as `layout: layouts/<name>.njk`; they resolve from inside the npm package, so `npm update eleventy-plugin-techdoc` picks up new versions with no file syncing. The theme's `assets/` are passthrough-copied to the site at `/techdoc/`, so its CSS loads from `/techdoc/css/*` and JS from `/techdoc/js/main.js`. + +**Wiring (prefer this over the interactive CLI).** `npx eleventy-plugin-techdoc` is interactive; an agent should create files directly instead. + +`eleventy.config.js`: + +```javascript +import techdoc from "eleventy-plugin-techdoc"; + +export default function (eleventyConfig) { + eleventyConfig.addPlugin(techdoc, { + site: { name: "My Site", url: "https://example.com", description: "…" }, + features: { blog: true, docs: true, darkMode: true, i18n: false }, + i18n: { defaultLanguage: "en", languages: ["en"] }, + }); + eleventyConfig.addPassthroughCopy("src/assets"); + return { dir: { input: "src", output: "_site" }, markdownTemplateEngine: "njk" }; } ``` -### Build errors with Eleventy 2.x +**Options (full shape; defaults).** `site: { name:"", url:"", description:"" }`; `features: { blog:true, docs:true, darkMode:true, i18n:true }`; `i18n: { defaultLanguage:"en", languages:["en"] }`. `features.docs` gates the `docs`/`api` layouts and the `docs` collection; `features.blog` gates the blog layout, the blog/tag/category pages, and the blog collections; `features.darkMode` renders the toggle button; `features.i18n` renders the language switcher and per-language pages. -techdoc requires Eleventy 3.x. Upgrade: +**Data files the site must provide** (under `src/_data/`): -```bash -npm install @11ty/eleventy@latest -``` +- `site.json` — `{ title, description, url, stylesheet }`. Optional fields `base.njk` uses if present: `name, author, keywords, themeColor, ogImage, twitterSite, twitterCreator, favicon, appleTouchIcon, organization{name,logo,sameAs}, searchUrl`. +- `navigation.json` — `main` (header nav: `[{ text, url, external?, i18nKey? }]`) and `footer` (`[{ title, items: [{ text, url }] }]`). `api.njk` also reads optional `api` and `docs` arrays. +- For i18n: `i18n.json` (nested per-language strings) and `languages.json` (`{ "en": { "code": "en", "nativeName": "English" }, … }`). -## Local Development +**Layouts** (`layout: layouts/<name>.njk`): `base.njk` (HTML shell — head meta, Open Graph/Twitter, JSON-LD, hreflang, header, footer; blocks `head`/`content`/`scripts`), `docs.njk` (sidebar from `eleventyNavigation`; classes `.docs-layout`/`.sidebar`/`.docs-content`), `blog.njk` (`.blog-post`/`.blog-container`), `api.njk` (docs-style; sidebar from `navigation.json`). -For contributing to techdoc: +**Content conventions.** -```bash -mkdir my-site && cd my-site -npm init -y -npm install @11ty/eleventy ../techdoc -node ../techdoc/bin/init.js -# Or if published: -# npm install @11ty/eleventy eleventy-plugin-techdoc -# npx eleventy-plugin-techdoc init -npm run dev -``` +- Docs: markdown in `src/docs/**/*.md`, `layout: layouts/docs.njk`, add `eleventyNavigation: { key, order }` for the sidebar. +- Blog: markdown in `src/blog/*.md`, `layout: layouts/blog.njk`, frontmatter `tags: posts` (required) plus optional `category`, `excerpt`, `author`, `date`. +- i18n: place non-default-language content under `src/<lang>/…`; per-language blog/tag/category pages are auto-registered. -## License +**Collections** (`collections.*`): `posts`, `tagList`, `categoryList`, `postsByTag`, `postsByCategory`, `docs`. For each non-default language `<lang>`: `<lang>posts`, `<lang>tagList`, `<lang>categoryList`, `<lang>postsByTag`, `<lang>postsByCategory`, `<lang>Docs`. -MIT +**Filters:** `dateFormat`, `isoDate`, `dateToRfc3339`, `limit`, `capitalize`, `slugify`, `t` (i18n; returns `undefined` when missing, so chain `| default("…")`), `altLangUrl`, `extractLangFromUrl`, `toOgLocale`. **Shortcode:** `{% year %}`. + +**Auto-generated output** (no files needed): `/feed.xml` (Atom, from `posts`), `/sitemap.xml`, `/robots.txt`, `/llms.txt`; the blog index and `/blog/tags/<tag>/`, `/blog/categories/<category>/` (plus per-language variants). + +**CSS contract.** The theme loads `/techdoc/css/{reset,layout,utilities}.css`, then the site's `site.stylesheet`. The structural CSS has **no colors**. The site's stylesheet MUST define the color variables the templates use: `--color-bg`, `--color-text`, `--color-primary`, `--color-border`, `--color-muted` — and a `[data-theme="dark"]` block (`base.njk` sets `data-theme` from `localStorage`/`prefers-color-scheme`; the dark-mode button toggles it). Layout variables are optional (the theme uses built-in fallbacks): `--max-width` (1200px), `--sidebar-width` (250px), `--content-width` (65ch), `--header-height` (60px), plus a `--space-*` scale. + +**Minimal viable site an agent can generate:** the `eleventy.config.js` above + `src/_data/site.json` + `src/_data/navigation.json` + `src/assets/css/styles.css` (defining the color variables) + `src/index.njk` (`layout: layouts/base.njk`) + a `src/docs/*.md` (`layout: layouts/docs.njk` with `eleventyNavigation`) + optionally `src/blog/*.md` (`tags: posts`). Then run `npx @11ty/eleventy --serve`. diff --git a/SPEC.md b/SPEC.md deleted file mode 100644 index 45400fd..0000000 --- a/SPEC.md +++ /dev/null @@ -1,347 +0,0 @@ -# techdoc - Eleventy Theme Specification - -## Overview - -Minimal structural Eleventy theme for tech documentation. **No colors. Sites style everything.** - -## Version Requirements - -- Eleventy: **3.1.2+** (latest stable) -- Node.js: **18+** - -## Philosophy - -The theme provides **structure only**: -- Layouts (base, docs, blog, api) -- Collections (blog posts, docs pages) -- Filters (dateFormat, slugify, t() for i18n) -- JavaScript (theme toggle, mobile menu, language switcher) -- Minimal structural CSS (reset, layout, utilities) - -Sites provide **all visual styling**: -- Colors (CSS custom properties) -- Typography (fonts, sizes) -- Decorative styles (shadows, borders, gradients) -- Branding/logos - -## Installation - -### New Site (Recommended) - -```bash -mkdir my-site && cd my-site -npm init -y -npm install @11ty/eleventy techdoc -npx techdoc init -``` - -The `init` command scaffolds a complete site and prompts for CSS style choice. - -### CSS Style Choices - -When running `npx techdoc init`, users choose from: - -1. **Minimal** (default) - - Basic CSS custom properties - - Simple light/dark mode - - No framework dependencies - -2. **C64** - - Basic C64 style - - Blocky - - Block monospace font - - C64 blues - -3. **None** - - No CSS generated - - User provides all styling from scratch - -## Package Structure - -``` -techdoc/ -├── package.json -├── lib/ -│ ├── index.js # Main plugin entry -│ ├── virtual-templates.js # Registers layouts via addTemplate() -│ ├── filters/index.js # All filters -│ ├── plugins/ -│ │ ├── collections.js # Blog/docs collections -│ │ └── markdown.js # Markdown config -│ └── shortcodes/index.js # Shortcodes (year, etc.) -├── templates/layouts/ -│ ├── base.njk # HTML shell -│ ├── docs.njk # Two-column with sidebar -│ ├── blog.njk # Blog post layout -│ └── api.njk # API reference -├── assets/ -│ ├── css/ -│ │ ├── reset.css # Minimal CSS reset -│ │ ├── layout.css # Structural grid/flexbox -│ │ └── utilities.css # Accessibility helpers -│ └── js/ -│ ├── main.js # Entry point -│ ├── theme-toggle.js # Dark/light mode -│ ├── mobile-menu.js # Mobile navigation -│ └── language-switcher.js -└── bin/ - └── init.js # CLI scaffolding tool -``` - -## Configuration - -Config goes in eleventy.config.js - -## Layouts - -### base.njk -Minimal HTML shell with: -- Proper `<head>` with meta tags, Open Graph -- Skip link for accessibility -- Header with nav -- Main content area -- Footer -- Blocks: `{% block head %}`, `{% block content %}`, `{% block scripts %}` - -### docs.njk -Extends base. Two-column layout: -- Sidebar with docs navigation -- Main content area -- Responsive (stacks on mobile) - -### blog.njk -Extends base. Article layout: -- Post metadata (date, author) -- Back to blog link -- Article content - -### api.njk -Extends base. API reference: -- Sidebar with API nav -- Main content area - -## API Documentation Generation - -**CRITICAL: API documentation MUST be 100% generated. No hand-written API docs in git.** - -The `api.njk` layout displays generated API documentation. The generation process: - -1. **Source**: Language-specific documentation (C# XML docs, Dart doc comments, JSDoc, etc.) -2. **Generator**: Each site implements a generator script that parses source docs -3. **Output**: Markdown files in `src/api/` directory (gitignored) -4. **Build**: Generator runs before Eleventy build - -### Generation Requirements - -- API markdown files (`src/api/*.md`) MUST be gitignored -- Generator script MUST be in `scripts/` or `tools/` directory -- Build command MUST run generator before Eleventy: `npm run generate-api && npx @11ty/eleventy` -- Each class gets a summary page (just member list with links) -- Each member (method, property, type) gets its own detail page - -### Example: C# XML Docs - -For C# projects like RestClient.Net: - -```bash -# .gitignore -src/api/*.md -!src/api/_index.md # Optional hand-written index - -# package.json scripts -{ - "scripts": { - "generate-api": "node scripts/generate-api-docs.js", - "build": "npm run generate-api && npx @11ty/eleventy", - "dev": "npm run generate-api && npx @11ty/eleventy --serve" - } -} -``` - -Generator reads `.xml` doc files from C# build output and generates: -- One markdown file per namespace -- One markdown file per class (summary only - member list with links) -- One markdown file per member (full details: signature, parameters, returns, examples) - -### Example: Dart Doc Comments - -For Dart projects: - -```bash -# Generate JSON from dartdoc -dart doc --output=doc-json --format=json - -# Generator parses doc-json/ and creates src/api/*.md -node scripts/generate-api-docs.js -``` - -### Page Structure - -**Class summary page** (`src/api/httpclient-extensions.md`): -```markdown -# HttpClientExtensions Class - -Extension methods for HttpClient. - -## Namespace -`RestClient.Net` - -## Methods -| Method | Description | -|--------|-------------| -| [GetAsync](/api/getasync/) | Make a type-safe GET request | -| [PostAsync](/api/postasync/) | Make a type-safe POST request | -``` - -**Member detail page** (`src/api/getasync.md`): -```markdown -# GetAsync Method - -Make a type-safe GET request. - -## Signature -\`\`\`csharp -public static async Task<Result<TSuccess, HttpError<TError>>> GetAsync<TSuccess, TError>(...) -\`\`\` - -## Parameters -| Parameter | Type | Description | -|-----------|------|-------------| -| url | AbsoluteUrl | The request URL | -... - -## Returns -... - -## Example -... -``` - -## Filters - -| Filter | Usage | Description | -|--------|-------|-------------| -| `dateFormat` | `{{ date \| dateFormat }}` | Localized date (respects `lang`) | -| `isoDate` | `{{ date \| isoDate }}` | ISO 8601 for JSON-LD | -| `limit` | `{{ posts \| limit(5) }}` | Limit array length | -| `capitalize` | `{{ str \| capitalize }}` | Capitalize first letter | -| `slugify` | `{{ str \| slugify }}` | URL-safe slug | -| `t` | `{{ "key" \| t(lang) }}` | Translation lookup | -| `altLangUrl` | `{{ url \| altLangUrl(currentLang, targetLang) }}` | Language URL switching | - -## Required Site Data - -### src/_data/site.json -```json -{ - "title": "My Site", - "description": "Site description", - "url": "https://example.com" -} -``` - -### src/_data/navigation.json -```json -{ - "main": [ - { "text": "Docs", "url": "/docs/" }, - { "text": "Blog", "url": "/blog/" } - ], - "footer": [ - { - "title": "Resources", - "items": [ - { "text": "GitHub", "url": "https://github.com/..." } - ] - } - ], - "docs": [ - { "text": "Getting Started", "url": "/docs/" }, - { "text": "Installation", "url": "/docs/installation/" } - ] -} -``` - -## CSS Custom Properties - -Sites MUST define these for the theme to render properly: - -```css -:root { - /* Required */ - --color-bg: #fff; - --color-text: #111; - --color-primary: #0066cc; - --color-border: #e5e5e5; - - /* Optional (have sensible defaults) */ - --sidebar-width: 250px; - --content-width: 65ch; - --space-1: 0.25rem; - --space-2: 0.5rem; - --space-4: 1rem; - --space-8: 2rem; -} - -[data-theme="dark"] { - --color-bg: #111; - --color-text: #f0f0f0; - --color-primary: #66b3ff; - --color-border: #333; -} -``` - -## Tests - -There are Playwright tests for the sample website. They are at techdoc/tests - -## i18n Support - -Enable with: -```javascript -{ - features: { i18n: true }, - i18n: { languages: ["en", "zh"] } -} -``` - -Create `src/_data/i18n.json`: -```json -{ - "en": { - "nav": { "docs": "Documentation", "blog": "Blog" }, - "blog": { "back": "Back to Blog" } - }, - "zh": { - "nav": { "docs": "文档", "blog": "博客" }, - "blog": { "back": "返回博客" } - } -} -``` - -Create language-specific content in `src/zh/`. - -## Bundled Plugins - -The theme includes and configures: -- `@11ty/eleventy-plugin-syntaxhighlight` - Code syntax highlighting -- `@11ty/eleventy-plugin-rss` - RSS feed generation -- `@11ty/eleventy-navigation` - Navigation plugin - -## Updates Flow Automatically - -Layouts are registered via Eleventy's Virtual Templates API (`addTemplate()`). When you `npm update techdoc`, new layouts are used immediately - no manual syncing required. - -## Verification Checklist - -- [ ] `npx techdoc init` scaffolds working site -- [ ] `npm run dev` serves site successfully -- [ ] All 4 layouts render (base, docs, blog, api) -- [ ] Dark mode toggle works -- [ ] Mobile menu works -- [ ] Theme CSS has NO colors (sites must provide) -- [ ] i18n works when enabled -- [ ] RSS feed generates -- [ ] Syntax highlighting works -- [ ] API docs are 100% generated (no `src/api/*.md` in git) -- [ ] `npm run build` runs generator before Eleventy diff --git a/assets/css/layout.css b/assets/css/layout.css index e12c788..06ccd91 100644 --- a/assets/css/layout.css +++ b/assets/css/layout.css @@ -21,6 +21,8 @@ /* Site structure */ body { + margin: 0; + min-height: 100vh; display: flex; flex-direction: column; } @@ -29,6 +31,22 @@ main { flex: 1; } +/* Theme-owned resets — scoped so reset.css can stay global-free and never + clobber the consuming site's content lists, buttons, or spacing. */ +.nav-links, +.sidebar ul, +.footer-section ul { + list-style: none; + margin: 0; + padding: 0; +} + +.theme-toggle, +.language-btn, +.mobile-menu-toggle { + font: inherit; +} + /* Header */ .site-header { position: sticky; @@ -79,6 +97,13 @@ main { max-width: var(--content-width, 65ch); } +/* Keep content media inside the theme's content columns. Scoped to theme + containers so the host site's images elsewhere are never affected. */ +.docs-content img, +.blog-post img { + max-width: 100%; +} + /* Blog layout */ .blog-post { max-width: var(--content-width, 65ch); diff --git a/assets/css/reset.css b/assets/css/reset.css index 2efa57f..bce8768 100644 --- a/assets/css/reset.css +++ b/assets/css/reset.css @@ -11,54 +11,31 @@ * ║ 3. Use CSS custom properties (--var-name) for theming ║ * ║ ║ * ║ Example in your styles.css: ║ - * ║ .nav { background: var(--color-bg); } /* Override nav bg */ + * ║ .nav { background: var(--color-bg); } // override nav bg ║ * ╚══════════════════════════════════════════════════════════════════════════╝ * * techdoc reset.css - * Minimal CSS reset NO colors, NO typography choices + * Deliberately minimal so the theme NEVER clobbers the consuming site's CSS. + * + * This file intentionally contains NO global content overrides: + * - NO `* { margin: 0; padding: 0 }` (would wipe the host site's spacing) + * - NO `ul, ol { list-style: none }` (would strip bullets from host content) + * - NO line-height / font-smoothing / overflow-wrap (typography is the host's) + * - NO img / table / form element overrides (content is the host's) + * + * The theme resets ONLY the structural elements it actually owns (nav, sidebar, + * footer lists, theme buttons). Those scoped resets live in layout.css. The + * single global rule below is box-sizing, which every consuming site expects, + * is required by techdoc's grid/flex layout, and is overridable per-element. */ -*, *::before, *::after { +*, +*::before, +*::after { box-sizing: border-box; } -* { - margin: 0; - padding: 0; -} - +/* Prevent iOS text-size inflation. Not a visual/typographic choice. */ html { -webkit-text-size-adjust: 100%; } - -body { - min-height: 100vh; - line-height: 1.5; - -webkit-font-smoothing: antialiased; -} - -img, picture, video, canvas, svg { - display: block; - max-width: 100%; -} - -input, button, textarea, select { - font: inherit; -} - -p, h1, h2, h3, h4, h5, h6 { - overflow-wrap: break-word; -} - -a { - text-decoration-skip-ink: auto; -} - -ul, ol { - list-style: none; -} - -table { - border-collapse: collapse; - border-spacing: 0; -} diff --git a/assets/js/language-switcher.js b/assets/js/language-switcher.js index 3b76113..3e5fdee 100644 --- a/assets/js/language-switcher.js +++ b/assets/js/language-switcher.js @@ -12,17 +12,19 @@ * Language switcher - handles dropdown toggle and outside click */ export function initLanguageSwitcher() { - const btn = document.querySelector('.language-btn'); - const dropdown = document.querySelector('.language-dropdown'); - if (!btn || !dropdown) return; - btn.addEventListener('click', () => { - const expanded = btn.getAttribute('aria-expanded') === 'true'; - btn.setAttribute('aria-expanded', !expanded); + const btn = document.querySelector(".language-btn"); + const dropdown = document.querySelector(".language-dropdown"); + if (!btn || !dropdown) { + return; + } + btn.addEventListener("click", () => { + const expanded = btn.getAttribute("aria-expanded") === "true"; + btn.setAttribute("aria-expanded", !expanded); }); - document.addEventListener('click', (e) => { - if (!e.target.closest('.language-switcher')) { - btn.setAttribute('aria-expanded', 'false'); + document.addEventListener("click", (e) => { + if (!e.target.closest(".language-switcher")) { + btn.setAttribute("aria-expanded", "false"); } }); } diff --git a/assets/js/main.js b/assets/js/main.js index d66b8fe..6b79e24 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -16,9 +16,9 @@ * ║ Then run: ./update-techdoc.sh to sync changes ║ * ╚══════════════════════════════════════════════════════════════════════════╝ */ -import { initThemeToggle } from './theme-toggle.js'; -import { initMobileMenu } from './mobile-menu.js'; -import { initLanguageSwitcher } from './language-switcher.js'; +import { initThemeToggle } from "./theme-toggle.js"; +import { initMobileMenu } from "./mobile-menu.js"; +import { initLanguageSwitcher } from "./language-switcher.js"; initThemeToggle(); initMobileMenu(); diff --git a/assets/js/mobile-menu.js b/assets/js/mobile-menu.js index 801a907..572af5c 100644 --- a/assets/js/mobile-menu.js +++ b/assets/js/mobile-menu.js @@ -12,16 +12,18 @@ * Mobile menu toggle - handles sidebar visibility on mobile */ export function initMobileMenu() { - const toggle = document.getElementById('mobile-menu-toggle'); - const sidebar = document.querySelector('.sidebar, .docs-sidebar'); - const navLinks = document.querySelector('.nav-links'); - if (!toggle) return; + const toggle = document.getElementById("mobile-menu-toggle"); + const sidebar = document.querySelector(".sidebar, .docs-sidebar"); + const navLinks = document.querySelector(".nav-links"); + if (!toggle) { + return; + } - toggle.addEventListener('click', () => { - const expanded = toggle.getAttribute('aria-expanded') === 'true'; - toggle.setAttribute('aria-expanded', String(!expanded)); - sidebar?.classList.toggle('open'); - navLinks?.classList.toggle('open'); - document.body.classList.toggle('menu-open'); + toggle.addEventListener("click", () => { + const expanded = toggle.getAttribute("aria-expanded") === "true"; + toggle.setAttribute("aria-expanded", String(!expanded)); + sidebar?.classList.toggle("open"); + navLinks?.classList.toggle("open"); + document.body.classList.toggle("menu-open"); }); } diff --git a/assets/js/theme-toggle.js b/assets/js/theme-toggle.js index 55e1ce9..7f3b1ce 100644 --- a/assets/js/theme-toggle.js +++ b/assets/js/theme-toggle.js @@ -12,13 +12,15 @@ * Theme toggle - switches between light/dark modes */ export function initThemeToggle() { - const toggle = document.getElementById('theme-toggle'); - if (!toggle) return; + const toggle = document.getElementById("theme-toggle"); + if (!toggle) { + return; + } - toggle.addEventListener('click', () => { - const current = document.documentElement.getAttribute('data-theme'); - const next = current === 'dark' ? 'light' : 'dark'; - document.documentElement.setAttribute('data-theme', next); - localStorage.setItem('theme', next); + toggle.addEventListener("click", () => { + const current = document.documentElement.getAttribute("data-theme"); + const next = current === "dark" ? "light" : "dark"; + document.documentElement.setAttribute("data-theme", next); + localStorage.setItem("theme", next); }); } diff --git a/bin/init.js b/bin/init.js index dd74eee..c65d1b9 100755 --- a/bin/init.js +++ b/bin/init.js @@ -1,10 +1,56 @@ #!/usr/bin/env node -import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'fs'; -import { join } from 'path'; -import { select, input } from '@inquirer/prompts'; +import { writeFileSync, readFileSync, mkdirSync, existsSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +// `@inquirer/prompts` is imported lazily inside main() so `--version`/`--help` start no +// runtime and load no heavy dependencies. [SWR-VERSION-CLI-OUTPUT] const cwd = process.cwd(); +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// --- Shipwright binary version contract [SWR-VERSION-CLI-OUTPUT][SWR-VERSION-JSON-OUTPUT] --- +// `--version` prints exactly "<binaryName> <version>", exits 0, starts no runtime, touches no network. +// Version derives from package metadata, never a hard-coded string [SWR-VERSION-BINDINGS]. +function readVersion() { + const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")); + return pkg.version; +} + +function handleCliArgs(argv) { + const args = new Set(argv); + + if (args.has("--help") || args.has("-h")) { + console.log( + `eleventy-plugin-techdoc ${readVersion()}\n\n` + + "Usage:\n" + + " eleventy-plugin-techdoc Scaffold a techdoc Eleventy site in the current directory\n" + + " eleventy-plugin-techdoc --version Print the version and exit\n" + + " eleventy-plugin-techdoc --help Show this help and exit" + ); + process.exit(0); + } + + if (args.has("--version") || args.has("-v")) { + const version = readVersion(); + if (args.has("--json")) { + process.stdout.write( + JSON.stringify({ + manifestVersion: 1, + name: "eleventy-plugin-techdoc", + version, + kind: "cli", + language: "javascript", + }) + "\n" + ); + } else { + console.log(`eleventy-plugin-techdoc ${version}`); + } + process.exit(0); + } +} + +handleCliArgs(process.argv.slice(2)); // CSS style templates const cssStyles = { @@ -276,83 +322,94 @@ pre { --color-primary: #00f; --color-border: #ccc; } -` +`, }; // Handle package.json separately - merge, don't skip function updatePackageJson() { - const pkgPath = join(cwd, 'package.json'); + const pkgPath = join(cwd, "package.json"); const additions = { - type: 'module', + type: "module", scripts: { - dev: 'npx @11ty/eleventy --serve', - build: 'npx @11ty/eleventy' - } + dev: "npx @11ty/eleventy --serve", + build: "npx @11ty/eleventy", + }, }; if (existsSync(pkgPath)) { - const existing = JSON.parse(readFileSync(pkgPath, 'utf-8')); + const existing = JSON.parse(readFileSync(pkgPath, "utf-8")); existing.type = additions.type; existing.scripts = { ...existing.scripts, ...additions.scripts }; - writeFileSync(pkgPath, JSON.stringify(existing, null, 2) + '\n'); - console.log(' update: package.json (added type, scripts)'); + writeFileSync(pkgPath, JSON.stringify(existing, null, 2) + "\n"); + console.log(" update: package.json (added type, scripts)"); } else { const fresh = { - name: 'my-site', - version: '1.0.0', + name: "my-site", + version: "1.0.0", ...additions, dependencies: { - '@11ty/eleventy': '^3.1.2', - 'eleventy-plugin-techdoc': '^0.1.0' - } + "@11ty/eleventy": "^3.1.2", + "eleventy-plugin-techdoc": "^0.1.0", + }, }; - writeFileSync(pkgPath, JSON.stringify(fresh, null, 2) + '\n'); - console.log(' create: package.json'); + writeFileSync(pkgPath, JSON.stringify(fresh, null, 2) + "\n"); + console.log(" create: package.json"); } } // Core files always created (minimal structure) function getCoreFiles(cssStyle, siteConfig = {}) { - const { name = 'My Site', description = 'Built with techdoc', author = '', githubUrl = '', twitterHandle = '', discordUrl = '' } = siteConfig; + const { + name = "My Site", + description = "Built with techdoc", + author = "", + githubUrl = "", + twitterHandle = "", + discordUrl = "", + } = siteConfig; // Build footer sections - proper multi-column footer like real sites const footerSections = [ { - title: 'Documentation', + title: "Documentation", items: [ - { text: 'Getting Started', url: '/docs/' }, - { text: 'Blog', url: '/blog/' } - ] + { text: "Getting Started", url: "/docs/" }, + { text: "Blog", url: "/blog/" }, + ], }, { - title: 'Community', + title: "Community", items: [ - ...(githubUrl ? [{ text: 'GitHub', url: githubUrl }] : []), - ...(discordUrl ? [{ text: 'Discord', url: discordUrl }] : []), - ...(twitterHandle ? [{ text: 'Twitter', url: `https://twitter.com/${twitterHandle.replace('@', '')}` }] : []) - ].filter(item => item.url) + ...(githubUrl ? [{ text: "GitHub", url: githubUrl }] : []), + ...(discordUrl ? [{ text: "Discord", url: discordUrl }] : []), + ...(twitterHandle + ? [{ text: "Twitter", url: `https://twitter.com/${twitterHandle.replace("@", "")}` }] + : []), + ].filter((item) => item.url), }, { - title: 'More', - items: [ - { text: 'Eleventy', url: 'https://www.11ty.dev' } - ] - } - ].filter(section => section.items.length > 0); // Remove empty sections + title: "More", + items: [{ text: "Eleventy", url: "https://www.11ty.dev" }], + }, + ].filter((section) => section.items.length > 0); // Remove empty sections // Build site.json with optional fields const siteJson = { title: name, description: description, - url: 'https://example.com', - stylesheet: '/assets/css/styles.css' + url: "https://example.com", + stylesheet: "/assets/css/styles.css", }; - if (author) siteJson.author = author; - if (twitterHandle) siteJson.twitterSite = twitterHandle; + if (author) { + siteJson.author = author; + } + if (twitterHandle) { + siteJson.twitterSite = twitterHandle; + } return { - 'eleventy.config.js': `import techdoc from "eleventy-plugin-techdoc"; + "eleventy.config.js": `import techdoc from "eleventy-plugin-techdoc"; export default function(eleventyConfig) { eleventyConfig.addPlugin(techdoc, { @@ -378,19 +435,24 @@ export default function(eleventyConfig) { } `, - 'src/_data/site.json': JSON.stringify(siteJson, null, 2) + '\n', - - 'src/_data/navigation.json': JSON.stringify({ - main: [ - { text: 'Docs', url: '/docs/' }, - { text: 'API', url: '/api/' }, - { text: 'Blog', url: '/blog/' }, - ...(githubUrl ? [{ text: 'GitHub', url: githubUrl, external: true }] : []) - ], - footer: footerSections - }, null, 2) + '\n', - - 'src/_data/i18n.json': `{ + "src/_data/site.json": JSON.stringify(siteJson, null, 2) + "\n", + + "src/_data/navigation.json": + JSON.stringify( + { + main: [ + { text: "Docs", url: "/docs/" }, + { text: "API", url: "/api/" }, + { text: "Blog", url: "/blog/" }, + ...(githubUrl ? [{ text: "GitHub", url: githubUrl, external: true }] : []), + ], + footer: footerSections, + }, + null, + 2 + ) + "\n", + + "src/_data/i18n.json": `{ "en": { "blog": { "back": "← Back to Blog", @@ -428,9 +490,9 @@ export default function(eleventyConfig) { } `, - 'src/assets/css/styles.css': cssStyles[cssStyle], + "src/assets/css/styles.css": cssStyles[cssStyle], - 'src/feed.njk': `---json + "src/feed.njk": `---json { "permalink": "feed.xml", "eleventyExcludeFromCollections": true @@ -455,7 +517,7 @@ export default function(eleventyConfig) { </feed> `, - 'src/sitemap.njk': `---json + "src/sitemap.njk": `---json { "permalink": "sitemap.xml", "eleventyExcludeFromCollections": true @@ -474,7 +536,7 @@ export default function(eleventyConfig) { </urlset> `, - 'src/robots.txt.njk': `---json + "src/robots.txt.njk": `---json { "permalink": "robots.txt", "eleventyExcludeFromCollections": true @@ -486,7 +548,7 @@ Allow: / Sitemap: {{ site.url }}/sitemap.xml `, - 'src/llms.txt.njk': `---json + "src/llms.txt.njk": `---json { "permalink": "llms.txt", "eleventyExcludeFromCollections": true @@ -515,14 +577,14 @@ This site contains documentation and resources. - RSS Feed: {{ site.url }}/feed.xml `, - 'src/_data/languages.json': `{ + "src/_data/languages.json": `{ "en": { "code": "en", "nativeName": "English" }, "zh": { "code": "zh", "nativeName": "中文" }, "es": { "code": "es", "nativeName": "Español" } } `, - 'src/index.njk': `--- + "src/index.njk": `--- layout: layouts/base.njk title: Home --- @@ -531,7 +593,7 @@ title: Home <p>Edit <code>src/index.njk</code> to customize this page.</p> `, - 'src/docs/index.md': `--- + "src/docs/index.md": `--- layout: layouts/docs.njk title: Documentation eleventyNavigation: @@ -544,7 +606,7 @@ eleventyNavigation: Your documentation goes here. `, - 'src/blog/index.njk': `--- + "src/blog/index.njk": `--- layout: layouts/base.njk title: Blog --- @@ -574,12 +636,16 @@ title: Blog // Sample content files (optional) function getSampleContentFiles(siteConfig = {}) { - const { name = 'My Site', author = '', description = 'Documentation and blog powered by techdoc' } = siteConfig; - const authorName = author || 'Your Name'; + const { + name = "My Site", + author = "", + description = "Documentation and blog powered by techdoc", + } = siteConfig; + const authorName = author || "Your Name"; return { // Override eleventy.config.js with i18n enabled - 'eleventy.config.js': `import techdoc from "eleventy-plugin-techdoc"; + "eleventy.config.js": `import techdoc from "eleventy-plugin-techdoc"; export default function(eleventyConfig) { eleventyConfig.addPlugin(techdoc, { @@ -609,7 +675,7 @@ export default function(eleventyConfig) { } `, - 'src/index.njk': `--- + "src/index.njk": `--- layout: layouts/base.njk title: Home lang: en @@ -642,7 +708,7 @@ permalink: / `, // Chinese home page - 'src/zh/index.njk': `--- + "src/zh/index.njk": `--- layout: layouts/base.njk title: 首页 lang: zh @@ -674,7 +740,7 @@ permalink: /zh/ </div> `, - 'src/docs/index.md': `--- + "src/docs/index.md": `--- layout: layouts/docs.njk title: Getting Started eleventyNavigation: @@ -721,7 +787,7 @@ app.doSomething(); - Visit the [Blog](/blog/) for tutorials and updates `, - 'src/docs/api.md': `--- + "src/docs/api.md": `--- layout: layouts/docs.njk title: API Reference eleventyNavigation: @@ -770,7 +836,7 @@ instance.destroy(); | \`error\` | \`{ message, code }\` | Fired on error | `, - 'src/docs/configuration.md': `--- + "src/docs/configuration.md": `--- layout: layouts/docs.njk title: Configuration eleventyNavigation: @@ -828,7 +894,7 @@ export default { \`\`\` `, - 'src/blog/index.njk': `--- + "src/blog/index.njk": `--- layout: layouts/base.njk title: Blog --- @@ -852,10 +918,10 @@ title: Blog </div> `, - 'src/blog/hello-world.md': `--- + "src/blog/hello-world.md": `--- layout: layouts/blog.njk title: Hello World -date: ${new Date().toISOString().split('T')[0]} +date: ${new Date().toISOString().split("T")[0]} author: ${authorName} tags: posts excerpt: Welcome to your new blog! This is a sample post to get you started. @@ -899,10 +965,14 @@ You can use all standard Markdown formatting: 3. Customize the styling in \`src/assets/css/styles.css\` `, - 'src/blog/getting-started-guide.md': `--- + "src/blog/getting-started-guide.md": `--- layout: layouts/blog.njk title: Getting Started with techdoc -date: ${(() => { const d = new Date(); d.setDate(d.getDate() - 7); return d.toISOString().split('T')[0]; })()} +date: ${(() => { + const d = new Date(); + d.setDate(d.getDate() - 7); + return d.toISOString().split("T")[0]; + })()} author: ${authorName} tags: posts excerpt: A quick guide to customizing your new documentation site. @@ -956,7 +1026,7 @@ All visual styling lives in \`src/assets/css/styles.css\`. The theme provides st `, // Chinese docs - 'src/zh/docs/index.md': `--- + "src/zh/docs/index.md": `--- layout: layouts/docs.njk title: 入门指南 lang: zh @@ -991,7 +1061,7 @@ app.doSomething(); \`\`\` `, - 'src/zh/docs/api.md': `--- + "src/zh/docs/api.md": `--- layout: layouts/docs.njk title: API 参考 lang: zh @@ -1022,7 +1092,7 @@ function create(options: Options): Instance; `, // Chinese blog - 'src/zh/blog/index.njk': `--- + "src/zh/blog/index.njk": `--- layout: layouts/base.njk title: 博客 lang: zh @@ -1046,10 +1116,10 @@ permalink: /zh/blog/ </div> `, - 'src/zh/blog/hello-world.md': `--- + "src/zh/blog/hello-world.md": `--- layout: layouts/blog.njk title: 你好世界 -date: ${new Date().toISOString().split('T')[0]} +date: ${new Date().toISOString().split("T")[0]} author: 你的名字 tags: posts lang: zh @@ -1091,7 +1161,7 @@ greet('世界'); `, // API section - 'src/api/index.njk': `--- + "src/api/index.njk": `--- layout: layouts/base.njk title: API Reference permalink: /api/ @@ -1109,7 +1179,7 @@ permalink: /api/ </div> `, - 'src/zh/api/index.njk': `--- + "src/zh/api/index.njk": `--- layout: layouts/base.njk title: API 参考 lang: zh @@ -1131,47 +1201,49 @@ permalink: /zh/api/ } async function main() { - console.log('Scaffolding techdoc site...\n'); + const { select, input } = await import("@inquirer/prompts"); + + console.log("Scaffolding techdoc site...\n"); // Prompt for CSS style const cssStyle = await select({ - message: 'Choose a CSS style:', + message: "Choose a CSS style:", choices: [ { - name: 'Minimal (default)', - value: 'minimal', - description: 'Basic CSS custom properties with simple light/dark mode' + name: "Minimal (default)", + value: "minimal", + description: "Basic CSS custom properties with simple light/dark mode", }, { - name: 'C64', - value: 'c64', - description: 'Commodore 64 retro style with blocky monospace font' + name: "C64", + value: "c64", + description: "Commodore 64 retro style with blocky monospace font", }, { - name: 'None', - value: 'none', - description: 'No CSS generated - you provide all styling from scratch' - } + name: "None", + value: "none", + description: "No CSS generated - you provide all styling from scratch", + }, ], - default: 'minimal' + default: "minimal", }); // Prompt for sample content const includeSampleContent = await select({ - message: 'Include sample content?', + message: "Include sample content?", choices: [ { - name: 'Yes (recommended)', + name: "Yes (recommended)", value: true, - description: 'Include sample docs, blog posts, and example config' + description: "Include sample docs, blog posts, and example config", }, { - name: 'No', + name: "No", value: false, - description: 'Minimal files only - configure from scratch' - } + description: "Minimal files only - configure from scratch", + }, ], - default: true + default: true, }); let siteConfig; @@ -1179,41 +1251,41 @@ async function main() { if (includeSampleContent) { // Sample content = use complete example config (no questions!) siteConfig = { - name: 'My Awesome Project', - description: 'Documentation and blog powered by techdoc', - author: 'Your Name', - githubUrl: 'https://github.com/your-username/your-project', - twitterHandle: '@yourhandle', - discordUrl: 'https://discord.gg/example' + name: "My Awesome Project", + description: "Documentation and blog powered by techdoc", + author: "Your Name", + githubUrl: "https://github.com/your-username/your-project", + twitterHandle: "@yourhandle", + discordUrl: "https://discord.gg/example", }; - console.log('\nUsing sample configuration. Edit src/_data/ files to customize.\n'); + console.log("\nUsing sample configuration. Edit src/_data/ files to customize.\n"); } else { // No sample = ask questions for real config - console.log('\nConfiguring your site:\n'); + console.log("\nConfiguring your site:\n"); const siteName = await input({ - message: 'Site name:', - default: 'My Site' + message: "Site name:", + default: "My Site", }); const siteDescription = await input({ - message: 'Site description:', - default: 'Built with techdoc' + message: "Site description:", + default: "Built with techdoc", }); const authorName = await input({ - message: 'Author name (optional):', - default: '' + message: "Author name (optional):", + default: "", }); const githubUrl = await input({ - message: 'GitHub URL (optional):', - default: '' + message: "GitHub URL (optional):", + default: "", }); const twitterHandle = await input({ - message: 'Twitter handle (optional, e.g. @username):', - default: '' + message: "Twitter handle (optional, e.g. @username):", + default: "", }); siteConfig = { @@ -1221,12 +1293,12 @@ async function main() { description: siteDescription, author: authorName, githubUrl: githubUrl, - twitterHandle: twitterHandle + twitterHandle: twitterHandle, }; } console.log(`Using ${cssStyle} CSS style`); - console.log(`Sample content: ${includeSampleContent ? 'yes' : 'no'}\n`); + console.log(`Sample content: ${includeSampleContent ? "yes" : "no"}\n`); updatePackageJson(); @@ -1238,7 +1310,7 @@ async function main() { for (const [path, content] of Object.entries(files)) { const fullPath = join(cwd, path); - const dir = fullPath.substring(0, fullPath.lastIndexOf('/')); + const dir = fullPath.substring(0, fullPath.lastIndexOf("/")); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); @@ -1252,7 +1324,7 @@ async function main() { } } - console.log('\nDone! Run: npm run dev'); + console.log("\nDone! Run: npm run dev"); } main().catch(console.error); diff --git a/coverage-thresholds.json b/coverage-thresholds.json new file mode 100644 index 0000000..68b7e65 --- /dev/null +++ b/coverage-thresholds.json @@ -0,0 +1,5 @@ +{ + "_agent_pmo": "74cf183", + "_doc": "Single source of truth for coverage thresholds. See REPO-STANDARDS-SPEC [COVERAGE-THRESHOLDS-JSON]. Read by the private _coverage_check recipe inside `make test`. NO GitHub repo variables, NO env vars, NO public `make coverage-check` target. This theme is validated with Playwright e2e tests; per-line unit-coverage instrumentation is not wired yet, so the measured floor is 0 and the threshold ratchets UP (never down) as unit tests are added.", + "default_threshold": 0 +} diff --git a/docs/plans/README.md b/docs/plans/README.md new file mode 100644 index 0000000..bef4521 --- /dev/null +++ b/docs/plans/README.md @@ -0,0 +1,3 @@ +<!-- agent-pmo:74cf183 --> + +Plan documents live here. Each plan MUST end with a TODO checklist. diff --git a/docs/specs/SPEC.md b/docs/specs/SPEC.md new file mode 100644 index 0000000..b35656b --- /dev/null +++ b/docs/specs/SPEC.md @@ -0,0 +1,184 @@ +# eleventy-plugin-techdoc — Specification + +## Overview + +Minimal **structural** Eleventy theme for technical documentation and blogs. The theme provides layout, collections, filters, SEO metadata, and structural CSS. **It ships no colors and no visual styling — sites provide those.** + +- Eleventy: **3.x** (peer dependency `@11ty/eleventy` `^3.1.2`) +- Node.js: **18+** + +## Philosophy + +The theme provides **structure only**: + +- Layouts (base, docs, blog, api) +- Collections (blog posts, tags, categories, docs) +- Filters (dates, slugify, i18n `t`, language-URL helpers) +- Structural JavaScript (theme toggle, mobile menu, language switcher) +- Structural CSS (reset, layout, accessibility utilities) — no colors + +Sites provide **all visual styling**: colors, typography, decorative styles, and branding. + +## Installation + +```bash +mkdir my-site && cd my-site +npm init -y +npm install @11ty/eleventy eleventy-plugin-techdoc +npx eleventy-plugin-techdoc # scaffold (interactive) +``` + +Running the package binary in a directory scaffolds a working site, adds `dev`/`build` scripts and `"type": "module"` to `package.json`, and prompts for a CSS style and whether to include sample content. There is no `init` subcommand — the bin scaffolds when run. + +### CSS style choices + +1. **Minimal** (default) — basic CSS custom properties, simple light/dark mode. +2. **C64** — blocky Commodore-64 retro style with a monospace font. +3. **None** — no CSS generated; you provide all styling. + +## Package Structure + +``` +eleventy-plugin-techdoc/ +├── package.json +├── index.js # re-exports lib/index.js +├── lib/ +│ ├── index.js # plugin entry point +│ ├── virtual-templates.js # registers layouts + pages via addTemplate() +│ ├── filters/index.js # filters +│ ├── plugins/ +│ │ ├── collections.js # blog/docs/tag/category collections +│ │ └── markdown.js # markdown-it configuration +│ └── shortcodes/index.js # shortcodes +├── templates/ +│ ├── layouts/ +│ │ ├── base.njk # HTML shell +│ │ ├── docs.njk # two-column with sidebar +│ │ ├── blog.njk # blog post layout +│ │ └── api.njk # API reference (docs-style) +│ └── pages/ # feed, sitemap, robots, llms.txt, blog/* pages +├── assets/ +│ ├── css/{reset,layout,utilities}.css +│ └── js/{main,theme-toggle,mobile-menu,language-switcher}.js +└── bin/init.js # scaffolding CLI (the `eleventy-plugin-techdoc` bin) +``` + +At build time the theme copies `assets/` into the site at `/techdoc/` (via `addPassthroughCopy`), so its CSS/JS load from `/techdoc/css/` and `/techdoc/js/`. + +## Layouts + +Layouts are registered into the site under `_includes/layouts/` via Eleventy's Virtual Templates API (`addTemplate()`), so sites reference them as `layout: layouts/<name>.njk`. `base.njk` is always registered; `docs.njk`/`api.njk` register when `features.docs` is on; `blog.njk` when `features.blog` is on. + +### base.njk + +HTML shell with: `<head>` meta (title, description, canonical, Open Graph, Twitter, JSON-LD structured data, `hreflang` alternates), an inline theme-preference script, a skip link, header nav (`navigation.main`), optional language switcher and dark-mode toggle, footer (`navigation.footer`), and the blocks `{% block head %}`, `{% block content %}`, `{% block scripts %}`. + +### docs.njk + +Extends base. Two-column layout (`.docs-layout`): a sidebar built from `eleventyNavigation` frontmatter and a `.docs-content` article. Supports optional `prevPage`/`nextPage` footer links. Responsive. + +### blog.njk + +Extends base. Article layout (`.blog-post`) with post metadata (date, author, category), tag/category links, content, and a "back to blog" link. + +### api.njk + +Extends base. A **docs-style** two-column layout whose sidebar is built from optional `api` and `docs` arrays in `src/_data/navigation.json`. It renders the page's markdown/content — **the theme does not generate API docs; you author the content (or generate it yourself) and the layout displays it.** + +## Filters + +| Filter | Usage | Description | +| -------------------- | -------------------------------------------------- | ------------------------------------------------------------------------ | +| `dateFormat` | `{{ date \| dateFormat }}` | Localized long date (respects page `lang`) | +| `isoDate` | `{{ date \| isoDate }}` | ISO 8601 timestamp | +| `dateToRfc3339` | `{{ date \| dateToRfc3339 }}` | RFC 3339 timestamp (feeds/JSON-LD) | +| `limit` | `{{ posts \| limit(5) }}` | Limit array length | +| `capitalize` | `{{ str \| capitalize }}` | Capitalize first letter | +| `slugify` | `{{ str \| slugify }}` | URL-safe slug | +| `t` | `{{ "key.path" \| t(lang) }}` | i18n lookup (returns `undefined` when missing so `default(...)` applies) | +| `altLangUrl` | `{{ url \| altLangUrl(currentLang, targetLang) }}` | Map a URL to another language | +| `extractLangFromUrl` | `{{ url \| extractLangFromUrl(defaultLang) }}` | Detect language prefix in a URL | +| `toOgLocale` | `{{ lang \| toOgLocale }}` | Map a language code to an `og:locale` (e.g. `en` → `en_US`) | + +## Shortcodes + +| Shortcode | Output | +| ------------ | ------------------------------------------ | +| `{% year %}` | Current year (e.g. for a footer copyright) | + +## Collections + +When `features.blog` is on, for the default language: `posts`, `tagList`, `categoryList`, `postsByTag`, `postsByCategory` (from `src/blog/*.md`). When `features.docs` is on: `docs` (from `src/docs/**/*.md`). + +For each non-default language `<lang>`, the same collections are registered with a language prefix (e.g. `zhposts`, `zhtagList`, `zhDocs`) from `src/<lang>/blog/*.md` and `src/<lang>/docs/**/*.md`. + +## Required Site Data + +### src/\_data/site.json + +```json +{ + "title": "My Site", + "description": "Site description", + "url": "https://example.com", + "stylesheet": "/assets/css/styles.css" +} +``` + +### src/\_data/navigation.json + +```json +{ + "main": [ + { "text": "Docs", "url": "/docs/" }, + { "text": "Blog", "url": "/blog/" } + ], + "footer": [ + { "title": "Resources", "items": [{ "text": "GitHub", "url": "https://github.com/..." }] } + ] +} +``` + +`api.njk` additionally reads optional `api` and `docs` arrays for its sidebar. + +## CSS Custom Properties + +The theme's structural CSS contains no colors. Sites define color variables in their own stylesheet; the scaffolded `styles.css` and templates reference them: + +```css +:root { + --color-bg: #fff; + --color-text: #111; + --color-primary: #0066cc; + --color-border: #e5e5e5; + --color-muted: #666; +} +[data-theme="dark"] { + /* your dark overrides */ +} +``` + +The theme's layout CSS uses these layout variables **with built-in fallback defaults**, so overriding them is optional: `--max-width` (1200px), `--sidebar-width` (250px), `--content-width` (65ch), `--header-height` (60px), plus a `--space-*` spacing scale. + +## i18n + +```javascript +{ + features: { i18n: true }, + i18n: { defaultLanguage: "en", languages: ["en", "zh"] } +} +``` + +Define strings in `src/_data/i18n.json`, put non-default-language content under `src/<lang>/`, and look strings up with the `t` filter. Per-language blog/tag/category pages are auto-registered; other pages are authored per language. + +## Bundled Plugins + +The theme adds and configures `@11ty/eleventy-plugin-syntaxhighlight`, `@11ty/eleventy-plugin-rss`, and `@11ty/eleventy-navigation`. + +## Updates Flow Automatically + +Layouts and SEO pages are registered through `addTemplate()` rather than copied into the site, so running `npm update eleventy-plugin-techdoc` picks up new versions without manual file syncing. + +## Tests + +`npm test` is configured to run Playwright (see `playwright.config.js`), which serves a `sample_website/` build and runs specs from `tests/`. Those fixtures are **not committed in this repository**, so `npm test` has nothing to run until they are added. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..7e81d6d --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,58 @@ +// agent-pmo:74cf183 +// ESLint flat config for a plain-JS (ESM) Eleventy plugin + browser theme scripts. +import js from "@eslint/js"; +import globals from "globals"; + +export default [ + js.configs.recommended, + { + // Node-side plugin + CLI source (ES modules). + files: ["lib/**/*.js", "bin/**/*.js", "index.js", "*.config.js", "*.config.mjs"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { ...globals.node }, + }, + rules: { + // `== null` / `!= null` is the idiomatic null-and-undefined check; keep + // strict equality everywhere else. + eqeqeq: ["error", "always", { null: "ignore" }], + curly: ["error", "all"], + "no-var": "error", + "prefer-const": "error", + "no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + // This is a build-time plugin + interactive CLI: console.* is intentional user I/O. + "no-console": "off", + }, + }, + { + // Browser-side theme scripts shipped to the consuming site (ES modules, + // loaded via <script type="module">). + files: ["assets/**/*.js"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { ...globals.browser }, + }, + rules: { + // `== null` / `!= null` is the idiomatic null-and-undefined check; keep + // strict equality everywhere else. + eqeqeq: ["error", "always", { null: "ignore" }], + curly: ["error", "all"], + "no-var": "error", + "prefer-const": "error", + "no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + }, + }, + { + ignores: [ + "node_modules/", + "coverage/", + "sample_website/", + "**/_site/", + "playwright-report/", + "test-results/", + ".deslop-cache/", + ], + }, +]; diff --git a/index.js b/index.js new file mode 100644 index 0000000..bc0a95d --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +export { default } from "./lib/index.js"; diff --git a/lib/filters/index.js b/lib/filters/index.js index c18ed6e..f276d5c 100644 --- a/lib/filters/index.js +++ b/lib/filters/index.js @@ -11,12 +11,12 @@ export function registerFilters(eleventyConfig, options) { const defaultLang = options.i18n.defaultLanguage; // Date formatting - respects page language for i18n sites - eleventyConfig.addFilter("dateFormat", function(dateObj, lang) { - const locale = lang || this.ctx?.lang || defaultLang || 'en'; + eleventyConfig.addFilter("dateFormat", function (dateObj, lang) { + const locale = lang || this.ctx?.lang || defaultLang || "en"; return new Date(dateObj).toLocaleDateString(locale, { - year: 'numeric', - month: 'long', - day: 'numeric' + year: "numeric", + month: "long", + day: "numeric", }); }); @@ -37,38 +37,50 @@ export function registerFilters(eleventyConfig, options) { // Capitalize first letter eleventyConfig.addFilter("capitalize", (str) => { - return str ? str.charAt(0).toUpperCase() + str.slice(1) : ''; + return str ? str.charAt(0).toUpperCase() + str.slice(1) : ""; }); // Slugify string eleventyConfig.addFilter("slugify", (str) => { - return str ? str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, '') : ''; + return str + ? str + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^\w-]+/g, "") + : ""; }); - // i18n translation filter - accesses i18n data from template context - // Usage in templates: {{ "key.path" | t(lang) }} - // The i18n data must be provided via _data/i18n.json in the site - eleventyConfig.addFilter("t", function(key, lang) { + // i18n translation filter - accesses i18n data from template context. + // Usage in templates: {{ "key.path" | t(lang) | default("Fallback") }} + // + // Returns `undefined` for ANY missing translation - no i18n data at all, an + // unknown language, or an absent key. That lets the template's `default(...)` + // supply the human-readable fallback. Returning the raw key here (the old + // behaviour) leaked strings like "blog.tagsTitle" into pages, because a + // truthy key string silently defeats the `default` filter. + eleventyConfig.addFilter("t", function (key, lang) { const actualLang = lang || this.ctx?.lang || defaultLang; - const i18n = this.ctx?.i18n; - if (!i18n) return key; - const langData = i18n[actualLang] || i18n[defaultLang]; - if (!langData) return key; - const keys = key.split('.'); + const langData = this.ctx?.i18n?.[actualLang] || this.ctx?.i18n?.[defaultLang]; + if (!langData) { + return undefined; + } let value = langData; - for (const k of keys) { - value = value?.[k]; + for (const part of key.split(".")) { + value = value?.[part]; } - return value != null ? value : undefined; + // Only scalar leaves are renderable text. A partial path that lands on a + // nested object (or array) must fall through to `default(...)` rather than + // stringify as "[object Object]". + return value != null && typeof value !== "object" ? value : undefined; }); // Get alternate language URL eleventyConfig.addFilter("altLangUrl", (url, currentLang, targetLang) => { - if (currentLang === 'en' && targetLang !== 'en') { + if (currentLang === "en" && targetLang !== "en") { return `/${targetLang}${url}`; - } else if (currentLang !== 'en' && targetLang === 'en') { - return url.replace(`/${currentLang}`, '') || '/'; - } else if (currentLang !== 'en' && targetLang !== 'en') { + } else if (currentLang !== "en" && targetLang === "en") { + return url.replace(`/${currentLang}`, "") || "/"; + } else if (currentLang !== "en" && targetLang !== "en") { return url.replace(`/${currentLang}`, `/${targetLang}`); } return url; @@ -76,10 +88,14 @@ export function registerFilters(eleventyConfig, options) { // Extract language from URL (for filtering navigation items) eleventyConfig.addFilter("extractLangFromUrl", (url, defaultLanguage) => { - if (!url) return defaultLanguage; - const supportedNonDefaultLangs = options.i18n.languages.filter(l => l !== defaultLanguage); + if (!url) { + return defaultLanguage; + } + const supportedNonDefaultLangs = options.i18n.languages.filter((l) => l !== defaultLanguage); for (const lang of supportedNonDefaultLangs) { - if (url.startsWith(`/${lang}/`)) return lang; + if (url.startsWith(`/${lang}/`)) { + return lang; + } } return defaultLanguage; }); @@ -87,19 +103,19 @@ export function registerFilters(eleventyConfig, options) { // Convert language code to og:locale format (e.g., en -> en_US, zh -> zh_CN) eleventyConfig.addFilter("toOgLocale", (langCode) => { const localeMap = { - 'en': 'en_US', - 'zh': 'zh_CN', - 'es': 'es_ES', - 'fr': 'fr_FR', - 'de': 'de_DE', - 'ja': 'ja_JP', - 'ko': 'ko_KR', - 'pt': 'pt_BR', - 'ru': 'ru_RU', - 'it': 'it_IT', - 'nl': 'nl_NL', - 'ar': 'ar_SA', - 'hi': 'hi_IN', + en: "en_US", + zh: "zh_CN", + es: "es_ES", + fr: "fr_FR", + de: "de_DE", + ja: "ja_JP", + ko: "ko_KR", + pt: "pt_BR", + ru: "ru_RU", + it: "it_IT", + nl: "nl_NL", + ar: "ar_SA", + hi: "hi_IN", }; return localeMap[langCode] || `${langCode}_${langCode.toUpperCase()}`; }); diff --git a/lib/index.js b/lib/index.js index 2bcf62d..1728055 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,24 +5,24 @@ * Sites must provide ALL visual styling via CSS custom properties. */ -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { registerFilters } from './filters/index.js'; -import { registerCollections } from './plugins/collections.js'; -import { registerVirtualTemplates } from './virtual-templates.js'; -import { registerShortcodes } from './shortcodes/index.js'; -import { configureMarkdown } from './plugins/markdown.js'; -import syntaxHighlight from '@11ty/eleventy-plugin-syntaxhighlight'; -import rss from '@11ty/eleventy-plugin-rss'; -import navigation from '@11ty/eleventy-navigation'; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import { registerFilters } from "./filters/index.js"; +import { registerCollections } from "./plugins/collections.js"; +import { registerVirtualTemplates } from "./virtual-templates.js"; +import { registerShortcodes } from "./shortcodes/index.js"; +import { configureMarkdown } from "./plugins/markdown.js"; +import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight"; +import rss from "@11ty/eleventy-plugin-rss"; +import navigation from "@11ty/eleventy-navigation"; const __dirname = dirname(fileURLToPath(import.meta.url)); const defaultOptions = { site: { - name: '', - url: '', - description: '', + name: "", + url: "", + description: "", }, features: { blog: true, @@ -31,8 +31,8 @@ const defaultOptions = { i18n: true, }, i18n: { - defaultLanguage: 'en', - languages: ['en'], + defaultLanguage: "en", + languages: ["en"], }, }; @@ -79,8 +79,8 @@ export default function techdocPlugin(eleventyConfig, userOptions = {}) { registerVirtualTemplates(eleventyConfig, options); // Add passthrough copy for theme assets (structural CSS/JS) - const assetsDir = join(__dirname, '..', 'assets'); - eleventyConfig.addPassthroughCopy({ [assetsDir]: 'techdoc' }); + const assetsDir = join(__dirname, "..", "assets"); + eleventyConfig.addPassthroughCopy({ [assetsDir]: "techdoc" }); // Add bundled plugins eleventyConfig.addPlugin(syntaxHighlight); diff --git a/lib/plugins/collections.js b/lib/plugins/collections.js index f271bd4..a058aaa 100644 --- a/lib/plugins/collections.js +++ b/lib/plugins/collections.js @@ -37,7 +37,8 @@ const groupByTag = (posts) => { const groupByCategory = (posts) => { const postsByCategory = {}; posts.forEach((post) => { - post.data.category && (postsByCategory[post.data.category] = postsByCategory[post.data.category] || []).push(post); + post.data.category && + (postsByCategory[post.data.category] = postsByCategory[post.data.category] || []).push(post); }); Object.values(postsByCategory).forEach((arr) => arr.sort((a, b) => b.date - a.date)); return postsByCategory; @@ -80,7 +81,9 @@ function registerBlogCollections(eleventyConfig, defaultLang, languages) { // Non-default languages (with prefix) languages .filter((lang) => lang !== defaultLang) - .forEach((lang) => registerBlogCollectionsForLang(eleventyConfig, `src/${lang}/blog/*.md`, `${lang}`)); + .forEach((lang) => + registerBlogCollectionsForLang(eleventyConfig, `src/${lang}/blog/*.md`, `${lang}`) + ); } function registerDocsCollections(eleventyConfig, defaultLang, languages) { @@ -89,6 +92,8 @@ function registerDocsCollections(eleventyConfig, defaultLang, languages) { languages .filter((lang) => lang !== defaultLang) .forEach((lang) => { - eleventyConfig.addCollection(`${lang}Docs`, (api) => api.getFilteredByGlob(`src/${lang}/docs/**/*.md`)); + eleventyConfig.addCollection(`${lang}Docs`, (api) => + api.getFilteredByGlob(`src/${lang}/docs/**/*.md`) + ); }); } diff --git a/lib/plugins/markdown.js b/lib/plugins/markdown.js index 9793019..4f5ce2e 100644 --- a/lib/plugins/markdown.js +++ b/lib/plugins/markdown.js @@ -19,7 +19,11 @@ export function configureMarkdown(eleventyConfig) { const mdAnchorOptions = { permalink: markdownItAnchor.permalink.headerLink(), - slugify: (s) => s.toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]+/g, ""), + slugify: (s) => + s + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^\w-]+/g, ""), level: [1, 2, 3, 4], }; diff --git a/lib/virtual-templates.js b/lib/virtual-templates.js index 9b40a63..f747f3f 100644 --- a/lib/virtual-templates.js +++ b/lib/virtual-templates.js @@ -118,33 +118,38 @@ export function registerVirtualTemplates(eleventyConfig, options) { ); // Register i18n blog pages for each non-default language - const defaultLang = options.i18n?.defaultLanguage || 'en'; - const languages = options.i18n?.languages || ['en']; + const defaultLang = options.i18n?.defaultLanguage || "en"; + const languages = options.i18n?.languages || ["en"]; for (const lang of languages) { - if (lang === defaultLang) continue; + if (lang === defaultLang) { + continue; + } // Blog index for this language eleventyConfig.addTemplate( `${lang}/blog/index.njk`, readFileSync(join(templatesDir, "pages/blog/index.njk"), "utf-8") - .replace('permalink: /blog/', `permalink: /${lang}/blog/`) - .replace('title: Blog', `title: Blog\nlang: ${lang}`) + .replace("permalink: /blog/", `permalink: /${lang}/blog/`) + .replace("title: Blog", `title: Blog\nlang: ${lang}`) ); // Tags index for this language eleventyConfig.addTemplate( `${lang}/blog/tags.njk`, readFileSync(join(templatesDir, "pages/blog/tags.njk"), "utf-8") - .replace('permalink: /blog/tags/', `permalink: /${lang}/blog/tags/`) - .replace('title: Tags', `title: Tags\nlang: ${lang}`) + .replace("permalink: /blog/tags/", `permalink: /${lang}/blog/tags/`) + .replace("title: Tags", `title: Tags\nlang: ${lang}`) ); // Tag pages for this language eleventyConfig.addTemplate( `${lang}/blog/tags-pages.njk`, readFileSync(join(templatesDir, "pages/blog/tags-pages.njk"), "utf-8") - .replace(/permalink: \/blog\/tags\/\{\{ tag \| slugify \}\}\//g, `permalink: /${lang}/blog/tags/{{ tag | slugify }}/`) + .replace( + /permalink: \/blog\/tags\/\{\{ tag \| slugify \}\}\//g, + `permalink: /${lang}/blog/tags/{{ tag | slugify }}/` + ) .replace(/data: collections\.tagList/g, `data: collections.${lang}tagList`) .replace(/collections\.postsByTag/g, `collections.${lang}postsByTag`) ); @@ -153,15 +158,18 @@ export function registerVirtualTemplates(eleventyConfig, options) { eleventyConfig.addTemplate( `${lang}/blog/categories.njk`, readFileSync(join(templatesDir, "pages/blog/categories.njk"), "utf-8") - .replace('permalink: /blog/categories/', `permalink: /${lang}/blog/categories/`) - .replace('title: Categories', `title: Categories\nlang: ${lang}`) + .replace("permalink: /blog/categories/", `permalink: /${lang}/blog/categories/`) + .replace("title: Categories", `title: Categories\nlang: ${lang}`) ); // Category pages for this language eleventyConfig.addTemplate( `${lang}/blog/categories-pages.njk`, readFileSync(join(templatesDir, "pages/blog/categories-pages.njk"), "utf-8") - .replace(/permalink: \/blog\/categories\/\{\{ category \| slugify \}\}\//g, `permalink: /${lang}/blog/categories/{{ category | slugify }}/`) + .replace( + /permalink: \/blog\/categories\/\{\{ category \| slugify \}\}\//g, + `permalink: /${lang}/blog/categories/{{ category | slugify }}/` + ) .replace(/data: collections\.categoryList/g, `data: collections.${lang}categoryList`) .replace(/collections\.postsByCategory/g, `collections.${lang}postsByCategory`) ); diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..d6dfcb8 --- /dev/null +++ b/opencode.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://opencode.ai/config.json", + "_agent_pmo": "74cf183", + "instructions": ["CLAUDE.md"] +} diff --git a/package-lock.json b/package-lock.json index 8d58efc..e16dae9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "techdoc", - "version": "0.1.0", + "name": "eleventy-plugin-techdoc", + "version": "0.0.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "techdoc", - "version": "0.1.0", + "name": "eleventy-plugin-techdoc", + "version": "0.0.0-dev", "license": "MIT", "dependencies": { "@11ty/eleventy-navigation": "^0.3.5", @@ -17,10 +17,15 @@ "markdown-it-anchor": "^9.2.0" }, "bin": { - "techdoc": "bin/init.js" + "eleventy-plugin-techdoc": "bin/init.js" }, "devDependencies": { - "@playwright/test": "^1.40.0" + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.40.0", + "c8": "^11.0.0", + "eslint": "^10.4.1", + "globals": "^17.6.0", + "prettier": "^3.8.3" }, "engines": { "node": ">=18" @@ -263,6 +268,249 @@ "node": ">=18" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@inquirer/ansi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", @@ -597,6 +845,44 @@ } } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@playwright/test": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", @@ -646,6 +932,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -679,11 +993,10 @@ "peer": true }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -691,6 +1004,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", @@ -704,6 +1027,23 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -897,6 +1237,40 @@ "node": ">=8" } }, + "node_modules/c8": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz", + "integrity": "sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^8.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": "20 || >=22" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, "node_modules/chardet": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", @@ -937,6 +1311,39 @@ "node": ">= 12" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -972,6 +1379,28 @@ "license": "MIT", "peer": true }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -989,6 +1418,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1122,6 +1558,16 @@ "errno": "cli.js" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1142,17 +1588,188 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esm-import-transformer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/esm-import-transformer/-/esm-import-transformer-3.0.5.tgz", - "integrity": "sha512-1GKLvfuMnnpI75l8c6sHoz0L3Z872xL5akGuBudgqTDPv4Vy6f2Ec7jEMKTxlqWl/3kSvNbHELeimJtnqgYniw==", + "node_modules/eslint": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", + "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "acorn": "^8.15.0" - } - }, - "node_modules/esprima": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/esm-import-transformer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/esm-import-transformer/-/esm-import-transformer-3.0.5.tgz", + "integrity": "sha512-1GKLvfuMnnpI75l8c6sHoz0L3Z872xL5akGuBudgqTDPv4Vy6f2Ec7jEMKTxlqWl/3kSvNbHELeimJtnqgYniw==", + "license": "MIT", + "peer": true, + "dependencies": { + "acorn": "^8.15.0" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", @@ -1166,6 +1783,52 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1198,6 +1861,27 @@ "node": ">=0.10.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1216,6 +1900,19 @@ } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/filesize": { "version": "10.1.6", "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", @@ -1275,6 +1972,61 @@ "license": "MIT", "peer": true }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -1300,6 +2052,34 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1313,6 +2093,58 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gray-matter": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", @@ -1353,6 +2185,23 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/htmlparser2": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", @@ -1430,6 +2279,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1502,7 +2371,6 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -1521,7 +2389,6 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", - "peer": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -1545,6 +2412,13 @@ "node": ">=0.12.0" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/iso-639-1": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.5.tgz", @@ -1555,6 +2429,45 @@ "node": ">=6.0" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -1568,6 +2481,27 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/junk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", @@ -1578,6 +2512,16 @@ "node": ">=8" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -1598,6 +2542,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -1634,6 +2592,32 @@ "integrity": "sha512-+dAZZ2mM+/m+vY9ezfoueVvrgnHIGi5FvgSymbIgJOFwiznWyA59mav95L+Mc6xPtL3s9gm5eNTlNtxJLbNM1g==", "license": "MIT" }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", @@ -1644,6 +2628,22 @@ "node": ">=12" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", @@ -1769,11 +2769,10 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "peer": true, + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -1807,6 +2806,13 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-retrieve-globals": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/node-retrieve-globals/-/node-retrieve-globals-6.0.1.tgz", @@ -1878,6 +2884,56 @@ "node": ">= 0.8" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-srcset": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", @@ -1894,6 +2950,43 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -2014,6 +3107,32 @@ "node": ">=12" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -2030,6 +3149,16 @@ "license": "MIT", "peer": true }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -2075,6 +3204,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -2100,7 +3239,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", - "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -2149,6 +3287,29 @@ "license": "ISC", "peer": true }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -2247,6 +3408,73 @@ "node": ">=0.10.0" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", + "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^13.0.6", + "minimatch": "^10.2.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2287,6 +3515,19 @@ "node": ">=0.6" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", @@ -2303,6 +3544,16 @@ "node": ">= 0.8" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/urlpattern-polyfill": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", @@ -2310,6 +3561,47 @@ "license": "MIT", "peer": true }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -2346,6 +3638,58 @@ } } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors-cjs": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", diff --git a/package.json b/package.json index fe6cca3..697555c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eleventy-plugin-techdoc", - "version": "0.1.0", + "version": "0.0.0-dev", "description": "Minimal structural Eleventy theme for tech documentation - no colors, no styling, just structure", "type": "module", "main": "lib/index.js", @@ -11,7 +11,12 @@ "eleventy-plugin-techdoc": "./bin/init.js" }, "scripts": { - "test": "playwright test" + "build": "npm pack --dry-run", + "test": "playwright test", + "lint": "eslint .", + "fmt": "prettier --write .", + "fmt:check": "prettier --check .", + "clean": "rm -rf coverage *.tgz lcov.info coverage-summary.json" }, "keywords": [ "eleventy", @@ -42,15 +47,20 @@ "@11ty/eleventy": "^3.1.2" }, "dependencies": { - "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", - "@11ty/eleventy-plugin-rss": "^2.0.2", "@11ty/eleventy-navigation": "^0.3.5", + "@11ty/eleventy-plugin-rss": "^2.0.2", + "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", "@inquirer/prompts": "^7.0.0", "markdown-it": "^14.1.0", "markdown-it-anchor": "^9.2.0" }, "devDependencies": { - "@playwright/test": "^1.40.0" + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.40.0", + "c8": "^11.0.0", + "eslint": "^10.4.1", + "globals": "^17.6.0", + "prettier": "^3.8.3" }, "files": [ "lib/", diff --git a/playwright.config.js b/playwright.config.js index cdd37b3..4376b58 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,19 +1,19 @@ -import { defineConfig } from '@playwright/test'; +import { defineConfig } from "@playwright/test"; export default defineConfig({ - testDir: './tests', + testDir: "./tests", fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, - reporter: 'list', + reporter: "list", use: { - baseURL: 'http://localhost:8080', - trace: 'on-first-retry', + baseURL: "http://localhost:8080", + trace: "on-first-retry", }, webServer: { - command: 'cd sample_website && npx @11ty/eleventy --serve --port=8080', - url: 'http://localhost:8080', + command: "cd sample_website && npx @11ty/eleventy --serve --port=8080", + url: "http://localhost:8080", reuseExistingServer: !process.env.CI, timeout: 120000, }, diff --git a/shipwright.json b/shipwright.json new file mode 100644 index 0000000..398baf9 --- /dev/null +++ b/shipwright.json @@ -0,0 +1,20 @@ +{ + "manifestVersion": 1, + "product": { + "id": "eleventy-plugin-techdoc", + "displayName": "eleventy-plugin-techdoc", + "version": "0.0.0-dev" + }, + "components": [ + { + "id": "eleventy-plugin-techdoc-cli", + "kind": "cli", + "language": "javascript", + "binaryName": "eleventy-plugin-techdoc", + "expectedVersion": "0.0.0-dev", + "platforms": ["all"], + "sources": ["bundled"], + "required": true + } + ] +} diff --git a/templates/layouts/api.njk b/templates/layouts/api.njk index 30f163a..f1f1ea7 100644 --- a/templates/layouts/api.njk +++ b/templates/layouts/api.njk @@ -8,7 +8,7 @@ layout: layouts/base.njk <aside class="sidebar" id="docs-sidebar"> {% if navigation.api %} <div class="sidebar-section"> - <h4>{{ "nav.apiReference" | t(lang) }}</h4> + <h4>{{ "nav.apiReference" | t(lang) | default("API Reference") }}</h4> <ul> {% for item in navigation.api %} <li> @@ -23,7 +23,7 @@ layout: layouts/base.njk {% if navigation.docs %} <div class="sidebar-section"> - <h4>{{ "nav.docs" | t(lang) }}</h4> + <h4>{{ "nav.docs" | t(lang) | default("Docs") }}</h4> <ul> {% for section in navigation.docs %} {% for item in section.items | limit(3) %} diff --git a/templates/layouts/base.njk b/templates/layouts/base.njk index 60216bf..9202dd0 100644 --- a/templates/layouts/base.njk +++ b/templates/layouts/base.njk @@ -155,7 +155,7 @@ {% endif %} <a href="{{ navUrl }}" {% if item.external %}target="_blank" rel="noopener noreferrer"{% endif %} class="nav-link {% if (item.url == '/' and (page.url == '/' or page.url == '/index.html' or page.url == ('/' + currentLang + '/') or page.url == ('/' + currentLang + '/index.html'))) or (item.url != '/' and item.url | length > 1 and (page.url.startsWith(item.url) or page.url.startsWith(navUrl))) %}active{% endif %}"> - {{ item.text }} + {% if item.i18nKey and currentLang != defaultLanguage %}{{ item.i18nKey | t(currentLang) | default(item.text) }}{% else %}{{ item.text }}{% endif %} </a> </li> {% endfor %} @@ -208,14 +208,14 @@ {% set currentLang = lang | default('en') %} {% for section in navigation.footer %} <div class="footer-section"> - <h3>{{ section.title }}</h3> + <h3>{% if section.i18nKey and currentLang != defaultLanguage %}{{ section.i18nKey | t(currentLang) | default(section.title) }}{% else %}{{ section.title }}{% endif %}</h3> <ul> {% for item in section.items %} {% set footerUrl = item.url %} {% if item.url.startsWith('/') and currentLang != defaultLanguage %} {% set footerUrl = item.url | altLangUrl('en', currentLang) %} {% endif %} - <li><a href="{{ footerUrl }}">{{ item.text }}</a></li> + <li><a href="{{ footerUrl }}">{% if item.i18nKey and currentLang != defaultLanguage %}{{ item.i18nKey | t(currentLang) | default(item.text) }}{% else %}{{ item.text }}{% endif %}</a></li> {% endfor %} </ul> </div> diff --git a/templates/layouts/blog.njk b/templates/layouts/blog.njk index bc24582..ffe4f46 100644 --- a/templates/layouts/blog.njk +++ b/templates/layouts/blog.njk @@ -61,7 +61,7 @@ layout: layouts/base.njk </div> <footer class="blog-post-footer"> - <a href="{{ langPrefix }}/blog/" class="btn">{{ "blog.back" | t(lang) }}</a> + <a href="{{ langPrefix }}/blog/" class="btn">{{ "blog.back" | t(lang) | default("Back to Blog") }}</a> </footer> </div> </article>