diff --git a/.github/workflows/check-docs.yaml b/.github/workflows/check-docs.yaml index bca45dd6..8e198a3f 100644 --- a/.github/workflows/check-docs.yaml +++ b/.github/workflows/check-docs.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Check markdown links uses: gaurav-nelson/github-action-markdown-link-check@v1 # use to skip next line diff --git a/.github/workflows/check-link-snapshot.yaml b/.github/workflows/check-link-snapshot.yaml new file mode 100644 index 00000000..d9f69847 --- /dev/null +++ b/.github/workflows/check-link-snapshot.yaml @@ -0,0 +1,23 @@ +name: Check link snapshot + +on: + pull_request: + branches: + - main + push: + branches: + - main + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "22" + cache: "npm" + - run: npm ci + - name: Verify snapshot contract + run: npm run snapshot:check diff --git a/.github/workflows/check-notebooks.yaml b/.github/workflows/check-notebooks.yaml index 6641df63..76429735 100644 --- a/.github/workflows/check-notebooks.yaml +++ b/.github/workflows/check-notebooks.yaml @@ -27,10 +27,10 @@ jobs: working-directory: notebooks steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' diff --git a/.github/workflows/run-notebooks.yaml b/.github/workflows/run-notebooks.yaml index 0312ae06..3d4171a3 100644 --- a/.github/workflows/run-notebooks.yaml +++ b/.github/workflows/run-notebooks.yaml @@ -22,7 +22,7 @@ jobs: notebooks: ${{ steps.list.outputs.notebooks }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: List notebooks id: list @@ -35,10 +35,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' @@ -63,10 +63,10 @@ jobs: notebook: ${{ fromJSON(needs.discover-notebooks.outputs.notebooks) }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..e85391ff --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +# Regenerate link-snapshot.yaml when docs structure may have changed. +if git diff --cached --name-only | grep -qE '^(docs\.json|.*\.mdx?$)'; then + npm run snapshot:update --silent || exit 1 + git add link-snapshot.yaml +fi diff --git a/CLAUDE.md b/CLAUDE.md index d5206beb..8ea7d63d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -182,6 +182,23 @@ grep -r '/>' --include="*.mdx" --include="*.md" . 3. Check if you need to target light/dark mode specifically 4. For icons using mask-image, set `background-color` instead of `color` +## Link Snapshot Contract + +`link-snapshot.yaml` is an append-only ledger of every URL the docs site has promised to keep resolving. The pre-commit hook keeps it in sync; [`check-link-snapshot.yaml`](.github/workflows/check-link-snapshot.yaml) fails any PR that would make a recorded URL stop resolving. Run `npm install` once to install the hook. + +A URL in `active` resolves if any of these are true: +1. It is in the `docs.json` navigation tree. +2. A matching `.mdx`/`.md` file exists under `lfm/`, `leap/`, `examples/`, or `deployment/`. +3. It matches a `redirects[*].source` (literal or `:slug*` prefix) whose destination itself resolves (recursive, max depth 5). + +When CI flags a URL, pick one (in order of preference): + +1. **Add a redirect** in `docs.json` — best when a substitute page exists. +2. **Deprecate in place** — remove the page from `docs.json` navigation but leave the `.mdx` on disk (the URL stays served, just undiscoverable). Add a `` deprecation banner. +3. **Mark deleted** — move the URL from `active` to `deleted` in `link-snapshot.yaml`. Bare URLs are fine; commit history is the record. Use only when no substitute exists. + +Don't edit `active` by hand — moving a URL to `deleted` causes the next `snapshot:update` (which the pre-commit hook runs automatically) to drop it from `active`. Manual commands: `npm run snapshot:update` to regenerate, `npm run snapshot:check` to run what CI runs. Both call `scripts/generateLinkSnapshot.ts` via `tsx`; new TypeScript scripts should follow the same `tsx scripts/.ts` pattern. + ## Git Commits - Never create a single monolithic commit for multi-step work. Break commits into logical units (e.g., infrastructure/scaffolding, feature A, feature B, fixes). diff --git a/README.md b/README.md index fcd0b218..955ffc03 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ This is the **official documentation repository** for Liquid AI. It contains com - [Making Changes](#making-changes) - [Submitting Changes](#submitting-changes) - [Link Check](#link-check) + - [Link Snapshot](#link-snapshot) - [License](#license) --- @@ -148,6 +149,7 @@ Navigate to the docs directory and start the development server: ```bash cd docs +npm i mintlify dev ``` @@ -173,8 +175,22 @@ For more details on Mintlify setup and configuration, visit the [official Mintli The [`check-docs.yaml`](.github/workflows/check-docs.yaml) workflow has a `check-link` job that examine markdown links. Customize the config in [`link-check.json`](./link-check.json). If a link cannot be accessed (e.g. Github private repo), add the URL pattern to the `ignorePatterns` array. +### Link Snapshot + +[`link-snapshot.yaml`](./link-snapshot.yaml) records every URL the docs site has served. The [`check-link-snapshot.yaml`](.github/workflows/check-link-snapshot.yaml) workflow fails any PR that would make a recorded URL stop resolving. Run `npm install` once to install the pre-commit hook that keeps the snapshot in sync. + +How it works: + +- `link-snapshot.yaml` has two sections: `active:` (URLs the site is currently committed to keep serving) and `deleted:` (URLs intentionally retired). +- The pre-commit hook regenerates the file whenever `docs.json` or any `.mdx`/`.md` file is staged. +- The `active:` section is **append-only** — new URLs are added when pages are created, but existing entries are never silently dropped, even after you delete the underlying page. +- CI re-checks every URL in `active:` on each PR. A URL is satisfied if it's still in the navigation, still on disk, or reachable via a redirect chain. +- The only way to retire a URL is to move it from `active:` to `deleted:` by hand. This forces every removal to be a deliberate choice — redirect, deprecate-but-keep, or formally retire. + +See [CLAUDE.md](./CLAUDE.md#link-snapshot-contract) for the remediation paths and the full resolution algorithm. + --- ## License -This documentation is licensed under [Attribution-ShareAlike 4.0 International](./LICENSE). \ No newline at end of file +This documentation is licensed under [Attribution-ShareAlike 4.0 International](./LICENSE). diff --git a/link-snapshot.yaml b/link-snapshot.yaml new file mode 100644 index 00000000..7b89d11c --- /dev/null +++ b/link-snapshot.yaml @@ -0,0 +1,175 @@ +# Auto-managed by scripts/generateLinkSnapshot.ts. +# `active` is append-only — pre-commit adds new URLs but never removes them. +# To intentionally retire a URL without a redirect, move it from `active` to +# `deleted`. The CI check skips entries listed under `deleted`. +active: + - /customization/finetuning-frameworks/datasets + - /customization/finetuning-frameworks/leap-finetune + - /customization/finetuning-frameworks/trl + - /customization/finetuning-frameworks/unsloth + - /customization/getting-started/connect-ai-tools + - /customization/getting-started/welcome + - /customization/tools/workbench + - /deployment/getting-started/connect-ai-tools + - /deployment/getting-started/welcome + - /deployment/gpu-inference/baseten + - /deployment/gpu-inference/fal + - /deployment/gpu-inference/modal + - /deployment/gpu-inference/sglang + - /deployment/gpu-inference/transformers + - /deployment/gpu-inference/vllm + - /deployment/on-device/android/advanced-features + - /deployment/on-device/android/ai-agent-usage-guide + - /deployment/on-device/android/android-quick-start-guide + - /deployment/on-device/android/cloud-ai-comparison + - /deployment/on-device/android/constrained-generation + - /deployment/on-device/android/conversation-generation + - /deployment/on-device/android/function-calling + - /deployment/on-device/android/messages-content + - /deployment/on-device/android/model-loading + - /deployment/on-device/android/openai-client + - /deployment/on-device/android/utilities + - /deployment/on-device/android/voice-assistant + - /deployment/on-device/ios/advanced-features + - /deployment/on-device/ios/ai-agent-usage-guide + - /deployment/on-device/ios/cloud-ai-comparison + - /deployment/on-device/ios/constrained-generation + - /deployment/on-device/ios/conversation-generation + - /deployment/on-device/ios/function-calling + - /deployment/on-device/ios/ios-quick-start-guide + - /deployment/on-device/ios/messages-content + - /deployment/on-device/ios/model-loading + - /deployment/on-device/ios/openai-client + - /deployment/on-device/ios/utilities + - /deployment/on-device/ios/voice-assistant + - /deployment/on-device/leap-sdk-changelog + - /deployment/on-device/llama-cpp + - /deployment/on-device/lm-studio + - /deployment/on-device/mlx + - /deployment/on-device/ollama + - /deployment/on-device/onnx + - /deployment/on-device/sdk/advanced-features + - /deployment/on-device/sdk/ai-agent-usage-guide + - /deployment/on-device/sdk/cloud-ai-comparison + - /deployment/on-device/sdk/constrained-generation + - /deployment/on-device/sdk/conversation-generation + - /deployment/on-device/sdk/desktop-platforms + - /deployment/on-device/sdk/function-calling + - /deployment/on-device/sdk/messages-content + - /deployment/on-device/sdk/model-loading + - /deployment/on-device/sdk/openai-client + - /deployment/on-device/sdk/quick-start + - /deployment/on-device/sdk/utilities + - /deployment/on-device/sdk/voice-assistant + - /deployment/tools/model-bundling/authentication + - /deployment/tools/model-bundling/bundle-creation + - /deployment/tools/model-bundling/bundle-management + - /deployment/tools/model-bundling/changelog + - /deployment/tools/model-bundling/configuration + - /deployment/tools/model-bundling/data-privacy + - /deployment/tools/model-bundling/download + - /deployment/tools/model-bundling/quick-start + - /deployment/tools/model-bundling/reference + - /docs/fine-tuning/datasets + - /docs/fine-tuning/trl + - /docs/fine-tuning/unsloth + - /docs/fine-tuning/workbench + - /docs/getting-started/connect-ai-tools + - /docs/getting-started/welcome + - /docs/inference/baseten-deployment + - /docs/inference/fal-deployment + - /docs/inference/llama-cpp + - /docs/inference/lm-studio + - /docs/inference/mlx + - /docs/inference/modal-deployment + - /docs/inference/ollama + - /docs/inference/onnx + - /docs/inference/sglang + - /docs/inference/transformers + - /docs/inference/vllm + - /examples/android/leap-koog-agent + - /examples/android/recipe-generator-constrained-output + - /examples/android/slogan-generator + - /examples/android/vision-language-model-example + - /examples/android/web-content-summarizer + - /examples/connect-ai-tools + - /examples/customize-models/car-maker-identification + - /examples/customize-models/home-assistant + - /examples/customize-models/satellite-vlm + - /examples/customize-models/wildfire-prevention + - /examples/index + - /examples/laptop-examples/audio-car-cockpit + - /examples/laptop-examples/audio-to-text-in-real-time + - /examples/laptop-examples/browser-control + - /examples/laptop-examples/flight-search-assistant + - /examples/laptop-examples/invoice-extractor-tool-with-liquid-nanos + - /examples/laptop-examples/lfm2-english-to-korean + - /examples/laptop-examples/meeting-summarization + - /examples/web/audio-webgpu-demo + - /examples/web/hand-voice-racer + - /examples/web/vl-webgpu-demo + - /leap/edge-sdk/overview + - /lfm/fine-tuning + - /lfm/fine-tuning/datasets + - /lfm/fine-tuning/trl + - /lfm/fine-tuning/unsloth + - /lfm/getting-started/connect-ai-tools + - /lfm/getting-started/model-license + - /lfm/getting-started/welcome + - /lfm/help/connect-ai-tools + - /lfm/help/contributing + - /lfm/help/faqs + - /lfm/help/model-license + - /lfm/help/troubleshooting + - /lfm/inference + - /lfm/inference/llama-cpp + - /lfm/inference/lm-studio + - /lfm/inference/mlx + - /lfm/inference/ollama + - /lfm/inference/transformers + - /lfm/inference/vllm + - /lfm/key-concepts/chat-template + - /lfm/key-concepts/text-generation-and-prompting + - /lfm/key-concepts/tool-use + - /lfm/models/audio-models + - /lfm/models/complete-library + - /lfm/models/lfm2-1.2b + - /lfm/models/lfm2-1.2b-extract + - /lfm/models/lfm2-1.2b-rag + - /lfm/models/lfm2-1.2b-tool + - /lfm/models/lfm2-2.6b + - /lfm/models/lfm2-2.6b-exp + - /lfm/models/lfm2-2.6b-transcript + - /lfm/models/lfm2-24b-a2b + - /lfm/models/lfm2-350m + - /lfm/models/lfm2-350m-enjp-mt + - /lfm/models/lfm2-350m-extract + - /lfm/models/lfm2-350m-math + - /lfm/models/lfm2-350m-pii-extract-jp + - /lfm/models/lfm2-700m + - /lfm/models/lfm2-8b-a1b + - /lfm/models/lfm2-audio-1.5b + - /lfm/models/lfm2-colbert-350m + - /lfm/models/lfm2-vl-1.6b + - /lfm/models/lfm2-vl-3b + - /lfm/models/lfm2-vl-450m + - /lfm/models/lfm25-1.2b-base + - /lfm/models/lfm25-1.2b-instruct + - /lfm/models/lfm25-1.2b-jp + - /lfm/models/lfm25-1.2b-thinking + - /lfm/models/lfm25-350m + - /lfm/models/lfm25-audio-1.5b + - /lfm/models/lfm25-vl-1.6b + - /lfm/models/lfm25-vl-450m + - /lfm/models/liquid-nanos + - /lfm/models/text-models + - /lfm/models/vision-models +# Examples of `deleted` entries (uncomment and adapt): +# +# deleted: +# # Minimal form — git history is the canonical record of why this was retired: +# - /lfm/old/experimental-thing +# # Or, if you want the reason inline: +# - url: /lfm/another-old-thing +# reason: "Page retired in DOC-12; no substitute exists." +deleted: [] diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..2a6c796b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,611 @@ +{ + "name": "liquid-docs", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "liquid-docs", + "version": "0.0.1", + "devDependencies": { + "@types/node": "^22.0.0", + "commander": "^14.0.0", + "husky": "^9.1.0", + "tsx": "^4.21.0", + "typescript": "^5.9.0", + "yaml": "^2.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/tsx": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz", + "integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..58ebdd8c --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "liquid-docs", + "version": "0.0.1", + "private": true, + "scripts": { + "snapshot:update": "tsx scripts/generateLinkSnapshot.ts --update", + "snapshot:check": "tsx scripts/generateLinkSnapshot.ts --check", + "prepare": "husky" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "commander": "^14.0.0", + "husky": "^9.1.0", + "tsx": "^4.21.0", + "typescript": "^5.9.0", + "yaml": "^2.7.0" + } +} diff --git a/scripts/generateLinkSnapshot.ts b/scripts/generateLinkSnapshot.ts new file mode 100644 index 00000000..cb93fa4a --- /dev/null +++ b/scripts/generateLinkSnapshot.ts @@ -0,0 +1,281 @@ +import { Command } from 'commander'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as YAML from 'yaml'; + +const REPO_ROOT = path.resolve(__dirname, '..'); +const DOCS_JSON = path.join(REPO_ROOT, 'docs.json'); +const SNAPSHOT_FILE = path.join(REPO_ROOT, 'link-snapshot.yaml'); + +const SNAPSHOT_HEADER = `# Auto-managed by scripts/generateLinkSnapshot.ts. +# \`active\` is append-only — pre-commit adds new URLs but never removes them. +# To intentionally retire a URL without a redirect, move it from \`active\` to +# \`deleted\`. The CI check skips entries listed under \`deleted\`. +`; + +const DELETED_PREAMBLE = `# Examples of \`deleted\` entries (uncomment and adapt): +# +# deleted: +# # Minimal form — git history is the canonical record of why this was retired: +# - /lfm/old/experimental-thing +# # Or, if you want the reason inline: +# - url: /lfm/another-old-thing +# reason: "Page retired in DOC-12; no substitute exists." +`; + +const DELETED_EXAMPLE = ` deleted: + # Minimal form — git history is the canonical record of why this URL was retired: + - /lfm/old/experimental-thing + # Or, if you want the reason inline: + - url: /lfm/another-old-thing + reason: "Page retired in DOC-12; no substitute exists."`; + +// Directories whose .mdx/.md files map to docs URLs. snippets/ is excluded +// because it holds reusable fragments, not pages. +const PAGE_DIRS = ['lfm', 'leap', 'examples', 'deployment']; + +interface DocsJson { + navigation?: { tabs?: NavNode[] }; + redirects?: { source: string; destination: string }[]; +} + +type NavNode = + | string + | { + tab?: string; + group?: string; + root?: string; + pages?: NavNode[]; + groups?: NavNode[]; + tabs?: NavNode[]; + }; + +// A deleted entry is either a bare URL string (minimal form — commit history +// is the record of why) or an object with `url` plus optional `reason` / +// `retired_at` fields if the contributor wants the rationale inline. +type DeletedEntry = string | { url: string; reason?: string; retired_at?: string }; + +function deletedUrl(entry: DeletedEntry): string { + return typeof entry === 'string' ? entry : entry.url; +} + +interface Snapshot { + active: string[]; + deleted: DeletedEntry[]; +} + +function loadDocsJson(): DocsJson { + return JSON.parse(fs.readFileSync(DOCS_JSON, 'utf8')); +} + +function* walkPages(node: NavNode): Generator { + if (typeof node === 'string') { + yield '/' + node; + return; + } + if (!node || typeof node !== 'object') return; + if (typeof node.root === 'string') yield '/' + node.root; + for (const list of [node.pages, node.groups, node.tabs] as (NavNode[] | undefined)[]) { + if (Array.isArray(list)) { + for (const child of list) yield* walkPages(child); + } + } +} + +function navUrls(docs: DocsJson): Set { + const urls = new Set(); + for (const tab of docs.navigation?.tabs ?? []) { + for (const url of walkPages(tab)) urls.add(url); + } + return urls; +} + +function redirectSources(docs: DocsJson): string[] { + // Skip wildcard sources (e.g. "/docs/models/:slug*") — they're patterns, not + // URLs that anyone visits directly. They stay in docs.json and still match + // incoming requests via matchesRedirectSource at check time. + return (docs.redirects ?? []) + .map((r) => normalizeUrl(r.source)) + .filter((src) => !src.includes(':')); +} + +function normalizeUrl(url: string): string { + if (!url.startsWith('/')) return '/' + url; + return url; +} + +function diskPageUrls(): Set { + const urls = new Set(); + for (const dir of PAGE_DIRS) { + const abs = path.join(REPO_ROOT, dir); + if (!fs.existsSync(abs)) continue; + walkDir(abs, (file) => { + if (file.endsWith('.mdx') || file.endsWith('.md')) { + const rel = path.relative(REPO_ROOT, file).replace(/\\/g, '/'); + const noExt = rel.replace(/\.(mdx|md)$/, ''); + urls.add('/' + noExt); + } + }); + } + return urls; +} + +function walkDir(dir: string, visit: (file: string) => void): void { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walkDir(full, visit); + else visit(full); + } +} + +function loadSnapshot(): Snapshot { + if (!fs.existsSync(SNAPSHOT_FILE)) return { active: [], deleted: [] }; + const raw = fs.readFileSync(SNAPSHOT_FILE, 'utf8'); + const parsed = YAML.parse(raw) ?? {}; + return { + active: Array.isArray(parsed.active) ? parsed.active.map(String) : [], + deleted: Array.isArray(parsed.deleted) ? (parsed.deleted as DeletedEntry[]) : [], + }; +} + +function serializeSnapshot(snap: Snapshot): string { + const activeYaml = YAML.stringify({ active: [...snap.active].sort() }, { lineWidth: 0 }); + const deletedYaml = YAML.stringify({ deleted: snap.deleted }, { lineWidth: 0 }); + return SNAPSHOT_HEADER + activeYaml + DELETED_PREAMBLE + deletedYaml; +} + +function computeUpdatedSnapshot(docs: DocsJson, prev: Snapshot): Snapshot { + const fromNav = navUrls(docs); + const fromRedirects = redirectSources(docs); + const fromDisk = diskPageUrls(); + const deletedUrls = new Set(prev.deleted.map(deletedUrl)); + // Start from prior active, minus anything the user has since moved to `deleted`. + const merged = new Set([...prev.active].filter((url) => !deletedUrls.has(url))); + for (const url of fromNav) if (!deletedUrls.has(url)) merged.add(url); + for (const url of fromRedirects) if (!deletedUrls.has(url)) merged.add(url); + for (const url of fromDisk) if (!deletedUrls.has(url)) merged.add(url); + return { + active: [...merged].sort(), + deleted: prev.deleted, + }; +} + +// Match a `:slug*`-style wildcard source against a candidate URL. +function matchesRedirectSource(source: string, candidate: string): boolean { + if (source === candidate) return true; + const wildcardIdx = source.indexOf(':'); + if (wildcardIdx === -1) return false; + const prefix = source.slice(0, wildcardIdx); + return candidate.startsWith(prefix); +} + +function resolveDestination(source: string, destination: string, candidate: string): string { + const wildcardIdx = source.indexOf(':'); + if (wildcardIdx === -1) return destination; + const prefix = source.slice(0, wildcardIdx); + const tail = candidate.slice(prefix.length); + // destination typically ends in `/:slug*`; strip that and append the tail. + const destWildcardIdx = destination.indexOf(':'); + const destPrefix = destWildcardIdx === -1 ? destination : destination.slice(0, destWildcardIdx); + return destPrefix + tail; +} + +interface ResolveContext { + navSet: Set; + diskSet: Set; + redirects: { source: string; destination: string }[]; +} + +function urlResolves(url: string, ctx: ResolveContext, visited = new Set(), depth = 0): boolean { + if (depth > 5) return false; + if (visited.has(url)) return false; + visited.add(url); + if (ctx.navSet.has(url)) return true; + if (ctx.diskSet.has(url)) return true; + for (const r of ctx.redirects) { + const normSource = normalizeUrl(r.source); + if (matchesRedirectSource(normSource, url)) { + const dest = normalizeUrl(resolveDestination(normSource, normalizeUrl(r.destination), url)); + if (urlResolves(dest, ctx, visited, depth + 1)) return true; + } + } + return false; +} + +function checkContract(docs: DocsJson, snap: Snapshot): { ok: boolean; failures: string[] } { + const ctx: ResolveContext = { + navSet: navUrls(docs), + diskSet: diskPageUrls(), + redirects: (docs.redirects ?? []).map((r) => ({ + source: normalizeUrl(r.source), + destination: normalizeUrl(r.destination), + })), + }; + const deleted = new Set(snap.deleted.map(deletedUrl)); + const failures: string[] = []; + for (const url of snap.active) { + if (deleted.has(url)) continue; + if (!urlResolves(url, ctx)) failures.push(url); + } + return { ok: failures.length === 0, failures }; +} + +function main(): void { + const program = new Command(); + program + .option('--update', 'Append new URLs to link-snapshot.yaml') + .option('--check', 'Verify snapshot contract; non-zero on failure') + .parse(process.argv); + const opts = program.opts<{ update?: boolean; check?: boolean }>(); + + if (!opts.update && !opts.check) { + console.error('Pass --update or --check.'); + process.exit(2); + } + + const docs = loadDocsJson(); + const prev = loadSnapshot(); + const next = computeUpdatedSnapshot(docs, prev); + const serialized = serializeSnapshot(next); + const onDisk = fs.existsSync(SNAPSHOT_FILE) ? fs.readFileSync(SNAPSHOT_FILE, 'utf8') : ''; + + if (opts.update) { + if (serialized !== onDisk) { + fs.writeFileSync(SNAPSHOT_FILE, serialized); + console.log(`Updated ${path.relative(REPO_ROOT, SNAPSHOT_FILE)} (${next.active.length} active, ${next.deleted.length} deleted).`); + } else { + console.log('Snapshot already up to date.'); + } + return; + } + + // --check mode + const stale = serialized !== onDisk; + const { ok, failures } = checkContract(docs, prev); + + if (!ok) { + console.error('Link snapshot contract violation. The following URLs no longer resolve:'); + for (const url of failures) console.error(` - ${url}`); + console.error(''); + console.error('Remediation options for each URL:'); + console.error(' 1. Add a redirect entry under `redirects` in docs.json pointing to a current page.'); + console.error(' 2. Keep the underlying .mdx file on disk but remove it from docs.json navigation'); + console.error(' (the URL stays served but undiscoverable — mark the page as deprecated).'); + console.error(' 3. Move the URL from `active` to `deleted` in link-snapshot.yaml. The'); + console.error(' minimal form is just the URL string; add an inline `reason` if you want'); + console.error(' it embedded next to the entry (otherwise the commit history is the'); + console.error(' record). Example:'); + console.error(''); + console.error(DELETED_EXAMPLE); + process.exit(1); + } + if (stale) { + console.error('link-snapshot.yaml is out of date relative to docs.json + on-disk pages.'); + console.error('Run `npm run snapshot:update` and commit the result.'); + process.exit(1); + } + console.log(`Snapshot OK: ${prev.active.length} active URLs verified.`); +} + +main(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..5392bffa --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["scripts/**/*.ts"] +}