diff --git a/.github/workflows/sdk-reference-generator-test.yml b/.github/workflows/sdk-reference-generator-test.yml new file mode 100644 index 0000000..b09b760 --- /dev/null +++ b/.github/workflows/sdk-reference-generator-test.yml @@ -0,0 +1,36 @@ +name: Test SDK Reference Generator + +on: + pull_request: + paths: + - "sdk-reference-generator/**" + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + working-directory: sdk-reference-generator + run: pnpm install --frozen-lockfile + + - name: Run tests + working-directory: sdk-reference-generator + run: pnpm test + + - name: TypeScript check + working-directory: sdk-reference-generator + run: npx tsc --noEmit diff --git a/.github/workflows/sdk-reference-sync.yml b/.github/workflows/sdk-reference-sync.yml new file mode 100644 index 0000000..796a507 --- /dev/null +++ b/.github/workflows/sdk-reference-sync.yml @@ -0,0 +1,174 @@ +name: Sync SDK Reference Documentation + +on: + workflow_dispatch: + inputs: + sdk: + description: "SDK to generate (see sdks.config.ts for available SDKs, or use 'all')" + required: true + default: "all" + type: string + version: + description: "Version to generate (all, latest, or specific like v2.9.0)" + required: true + default: "latest" + type: string + limit: + description: "Limit number of versions to generate (default: 5)" + required: false + default: 5 + type: number + force: + description: "Force regeneration of existing versions" + required: false + default: false + type: boolean + + repository_dispatch: + types: [sdk-release] + +# prevent concurrent runs that could conflict +concurrency: + group: sdk-reference-${{ github.ref }} + cancel-in-progress: false + +jobs: + generate: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + + steps: + - name: Checkout docs repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Cache pnpm dependencies + uses: actions/cache@v4 + with: + path: | + ~/.pnpm-store + sdk-reference-generator/node_modules + key: ${{ runner.os }}-pnpm-${{ hashFiles('sdk-reference-generator/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + + - name: Install generator dependencies + working-directory: sdk-reference-generator + run: pnpm install --frozen-lockfile + + - name: Install Python dependencies + run: pip install -r requirements.txt + + - name: Determine SDK and Version + id: params + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_SDK: ${{ github.event.inputs.sdk }} + INPUT_VERSION: ${{ github.event.inputs.version }} + INPUT_LIMIT: ${{ github.event.inputs.limit }} + INPUT_FORCE: ${{ github.event.inputs.force }} + PAYLOAD_SDK: ${{ github.event.client_payload.sdk }} + PAYLOAD_VERSION: ${{ github.event.client_payload.version }} + PAYLOAD_LIMIT: ${{ github.event.client_payload.limit }} + run: | + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + SDK="$INPUT_SDK" + VERSION="$INPUT_VERSION" + LIMIT="$INPUT_LIMIT" + FORCE="$INPUT_FORCE" + elif [[ "$EVENT_NAME" == "repository_dispatch" ]]; then + SDK="$PAYLOAD_SDK" + VERSION="${PAYLOAD_VERSION:-latest}" + LIMIT="${PAYLOAD_LIMIT:-5}" + FORCE="false" + fi + + echo "sdk=${SDK:-all}" >> $GITHUB_OUTPUT + echo "version=${VERSION:-latest}" >> $GITHUB_OUTPUT + echo "limit=${LIMIT:-5}" >> $GITHUB_OUTPUT + echo "force=${FORCE:-false}" >> $GITHUB_OUTPUT + + - name: Generate SDK Reference + working-directory: sdk-reference-generator + env: + SDK_NAME: ${{ steps.params.outputs.sdk }} + SDK_VERSION: ${{ steps.params.outputs.version }} + LIMIT: ${{ steps.params.outputs.limit }} + FORCE: ${{ steps.params.outputs.force }} + FORCE_COLOR: "2" + run: | + ARGS="--sdk $SDK_NAME --version $SDK_VERSION" + + if [[ -n "$LIMIT" && "$LIMIT" != "0" ]]; then + ARGS="$ARGS --limit $LIMIT" + fi + + if [[ "$FORCE" == "true" ]]; then + ARGS="$ARGS --force" + fi + + pnpm run generate $ARGS + + - name: Commit and push changes + id: commit + env: + SDK_NAME: ${{ steps.params.outputs.sdk }} + SDK_VERSION: ${{ steps.params.outputs.version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add docs/sdk-reference/ + git add docs.json + + if git diff --staged --quiet; then + echo "No changes to commit" + echo "changes=false" >> $GITHUB_OUTPUT + else + git commit -m "docs: update SDK reference for $SDK_NAME $SDK_VERSION" + git push + echo "changes=true" >> $GITHUB_OUTPUT + fi + + - name: Summary + env: + SDK_NAME: ${{ steps.params.outputs.sdk }} + SDK_VERSION: ${{ steps.params.outputs.version }} + LIMIT: ${{ steps.params.outputs.limit }} + CHANGES: ${{ steps.commit.outputs.changes }} + run: | + echo "## SDK Reference Generation Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Parameter | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-----------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| SDK | $SDK_NAME |" >> $GITHUB_STEP_SUMMARY + echo "| Version | $SDK_VERSION |" >> $GITHUB_STEP_SUMMARY + echo "| Limit | ${LIMIT:-No limit} |" >> $GITHUB_STEP_SUMMARY + echo "| Changes committed | $CHANGES |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "$CHANGES" == "true" ]]; then + echo "### Generated Files" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "Total MDX files: $(find docs/sdk-reference -type f -name '*.mdx' | wc -l)" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi diff --git a/.gitignore b/.gitignore index 090a1f0..ac322bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ .idea .DS_Store + +node_modules + +# Generated SDK navigation (intermediate file) +sdk_navigation.json \ No newline at end of file diff --git a/.mintignore b/.mintignore new file mode 100644 index 0000000..8881a60 --- /dev/null +++ b/.mintignore @@ -0,0 +1,3 @@ +sdk-reference-generator/ + +scripts/ \ No newline at end of file diff --git a/docs.json b/docs.json index f1410c9..04a7a24 100644 --- a/docs.json +++ b/docs.json @@ -168,18 +168,24 @@ }, { "group": "Deployment", - "pages": ["docs/byoc"] + "pages": [ + "docs/byoc" + ] }, { "group": "Migration", - "pages": ["docs/migration/v2"] + "pages": [ + "docs/migration/v2" + ] }, { "group": "Troubleshooting", "pages": [ { "group": "SDKs", - "pages": ["docs/troubleshooting/sdks/workers-edge-runtime"] + "pages": [ + "docs/troubleshooting/sdks/workers-edge-runtime" + ] }, { "group": "Templates", @@ -195,7 +201,359 @@ { "anchor": "SDK Reference", "icon": "brackets-curly", - "href": "https://e2b.dev/docs/sdk-reference" + "dropdowns": [ + { + "dropdown": "SDK (JavaScript)", + "icon": "square-js", + "versions": [ + { + "version": "v2.9.0", + "default": true, + "pages": [ + "docs/sdk-reference/js-sdk/v2.9.0/errors", + "docs/sdk-reference/js-sdk/v2.9.0/sandbox", + "docs/sdk-reference/js-sdk/v2.9.0/sandbox-commands", + "docs/sdk-reference/js-sdk/v2.9.0/sandbox-filesystem", + "docs/sdk-reference/js-sdk/v2.9.0/template", + "docs/sdk-reference/js-sdk/v2.9.0/template-logger", + "docs/sdk-reference/js-sdk/v2.9.0/template-readycmd" + ] + }, + { + "version": "v2.8.4", + "default": false, + "pages": [ + "docs/sdk-reference/js-sdk/v2.8.4/errors", + "docs/sdk-reference/js-sdk/v2.8.4/sandbox", + "docs/sdk-reference/js-sdk/v2.8.4/sandbox-commands", + "docs/sdk-reference/js-sdk/v2.8.4/sandbox-filesystem", + "docs/sdk-reference/js-sdk/v2.8.4/template", + "docs/sdk-reference/js-sdk/v2.8.4/template-logger", + "docs/sdk-reference/js-sdk/v2.8.4/template-readycmd" + ] + }, + { + "version": "v2.8.3", + "default": false, + "pages": [ + "docs/sdk-reference/js-sdk/v2.8.3/errors", + "docs/sdk-reference/js-sdk/v2.8.3/sandbox", + "docs/sdk-reference/js-sdk/v2.8.3/sandbox-commands", + "docs/sdk-reference/js-sdk/v2.8.3/sandbox-filesystem", + "docs/sdk-reference/js-sdk/v2.8.3/template", + "docs/sdk-reference/js-sdk/v2.8.3/template-logger", + "docs/sdk-reference/js-sdk/v2.8.3/template-readycmd" + ] + }, + { + "version": "v2.8.2", + "default": false, + "pages": [ + "docs/sdk-reference/js-sdk/v2.8.2/errors", + "docs/sdk-reference/js-sdk/v2.8.2/sandbox", + "docs/sdk-reference/js-sdk/v2.8.2/sandbox-commands", + "docs/sdk-reference/js-sdk/v2.8.2/sandbox-filesystem", + "docs/sdk-reference/js-sdk/v2.8.2/template", + "docs/sdk-reference/js-sdk/v2.8.2/template-logger", + "docs/sdk-reference/js-sdk/v2.8.2/template-readycmd" + ] + }, + { + "version": "v1.0.0", + "default": false, + "pages": [ + "docs/sdk-reference/js-sdk/1.0.0/errors", + "docs/sdk-reference/js-sdk/1.0.0/sandbox", + "docs/sdk-reference/js-sdk/1.0.0/sandbox-filesystem" + ] + } + ] + }, + { + "dropdown": "SDK (Python)", + "icon": "python", + "versions": [ + { + "version": "v2.9.0", + "default": true, + "pages": [ + "docs/sdk-reference/python-sdk/v2.9.0/exceptions", + "docs/sdk-reference/python-sdk/v2.9.0/logger", + "docs/sdk-reference/python-sdk/v2.9.0/readycmd", + "docs/sdk-reference/python-sdk/v2.9.0/sandbox_async", + "docs/sdk-reference/python-sdk/v2.9.0/sandbox_sync", + "docs/sdk-reference/python-sdk/v2.9.0/template", + "docs/sdk-reference/python-sdk/v2.9.0/template_async", + "docs/sdk-reference/python-sdk/v2.9.0/template_sync" + ] + }, + { + "version": "v2.8.1", + "default": false, + "pages": [ + "docs/sdk-reference/python-sdk/v2.8.1/exceptions", + "docs/sdk-reference/python-sdk/v2.8.1/logger", + "docs/sdk-reference/python-sdk/v2.8.1/readycmd", + "docs/sdk-reference/python-sdk/v2.8.1/sandbox_async", + "docs/sdk-reference/python-sdk/v2.8.1/sandbox_sync", + "docs/sdk-reference/python-sdk/v2.8.1/template", + "docs/sdk-reference/python-sdk/v2.8.1/template_async", + "docs/sdk-reference/python-sdk/v2.8.1/template_sync" + ] + }, + { + "version": "v2.8.0", + "default": false, + "pages": [ + "docs/sdk-reference/python-sdk/v2.8.0/exceptions", + "docs/sdk-reference/python-sdk/v2.8.0/logger", + "docs/sdk-reference/python-sdk/v2.8.0/readycmd", + "docs/sdk-reference/python-sdk/v2.8.0/sandbox_async", + "docs/sdk-reference/python-sdk/v2.8.0/sandbox_sync", + "docs/sdk-reference/python-sdk/v2.8.0/template", + "docs/sdk-reference/python-sdk/v2.8.0/template_async", + "docs/sdk-reference/python-sdk/v2.8.0/template_sync" + ] + }, + { + "version": "v2.7.0", + "default": false, + "pages": [ + "docs/sdk-reference/python-sdk/v2.7.0/exceptions", + "docs/sdk-reference/python-sdk/v2.7.0/logger", + "docs/sdk-reference/python-sdk/v2.7.0/readycmd", + "docs/sdk-reference/python-sdk/v2.7.0/sandbox_async", + "docs/sdk-reference/python-sdk/v2.7.0/sandbox_sync", + "docs/sdk-reference/python-sdk/v2.7.0/template", + "docs/sdk-reference/python-sdk/v2.7.0/template_async", + "docs/sdk-reference/python-sdk/v2.7.0/template_sync" + ] + }, + { + "version": "v1.0.0", + "default": false, + "pages": [ + "docs/sdk-reference/python-sdk/1.0.0/exceptions", + "docs/sdk-reference/python-sdk/1.0.0/sandbox_async", + "docs/sdk-reference/python-sdk/1.0.0/sandbox_sync" + ] + } + ] + }, + { + "dropdown": "Code Interpreter SDK (JavaScript)", + "icon": "square-js", + "versions": [ + { + "version": "v2.3.3", + "default": true, + "pages": [ + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.3/charts", + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.3/consts", + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.3/messaging", + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.3/sandbox" + ] + }, + { + "version": "v2.3.2", + "default": false, + "pages": [ + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.2/charts", + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.2/consts", + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.2/messaging", + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.2/sandbox" + ] + }, + { + "version": "v2.3.1", + "default": false, + "pages": [ + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.1/charts", + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.1/consts", + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.1/messaging", + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.1/sandbox" + ] + }, + { + "version": "v2.3.0", + "default": false, + "pages": [ + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.0/charts", + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.0/consts", + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.0/messaging", + "docs/sdk-reference/code-interpreter-js-sdk/v2.3.0/sandbox" + ] + } + ] + }, + { + "dropdown": "Code Interpreter SDK (Python)", + "icon": "python", + "versions": [ + { + "version": "v2.4.1", + "default": true, + "pages": [ + "docs/sdk-reference/code-interpreter-python-sdk/v2.4.1/code_interpreter" + ] + }, + { + "version": "v2.4.0", + "default": false, + "pages": [ + "docs/sdk-reference/code-interpreter-python-sdk/v2.4.0/code_interpreter" + ] + }, + { + "version": "v2.3.0", + "default": false, + "pages": [ + "docs/sdk-reference/code-interpreter-python-sdk/v2.3.0/code_interpreter" + ] + }, + { + "version": "v2.2.1", + "default": false, + "pages": [ + "docs/sdk-reference/code-interpreter-python-sdk/v2.2.1/code_interpreter" + ] + }, + { + "version": "v1.0.4", + "default": false, + "pages": [ + "docs/sdk-reference/code-interpreter-python-sdk/1.0.4/code_interpreter" + ] + }, + { + "version": "v1.0.1", + "default": false, + "pages": [ + "docs/sdk-reference/code-interpreter-python-sdk/1.0.1/code_interpreter" + ] + } + ] + }, + { + "dropdown": "Desktop SDK (JavaScript)", + "icon": "square-js", + "versions": [ + { + "version": "v2.2.2", + "default": true, + "pages": [ + "docs/sdk-reference/desktop-js-sdk/v2.2.2/sandbox" + ] + }, + { + "version": "v2.2.1", + "default": false, + "pages": [ + "docs/sdk-reference/desktop-js-sdk/v2.2.1/sandbox" + ] + }, + { + "version": "v2.2.0", + "default": false, + "pages": [ + "docs/sdk-reference/desktop-js-sdk/v2.2.0/sandbox" + ] + }, + { + "version": "v2.1.0", + "default": false, + "pages": [ + "docs/sdk-reference/desktop-js-sdk/v2.1.0/sandbox" + ] + } + ] + }, + { + "dropdown": "Desktop SDK (Python)", + "icon": "python", + "versions": [ + { + "version": "v2.2.0", + "default": true, + "pages": [ + "docs/sdk-reference/desktop-python-sdk/v2.2.0/desktop" + ] + }, + { + "version": "v2.1.0", + "default": false, + "pages": [ + "docs/sdk-reference/desktop-python-sdk/v2.1.0/desktop" + ] + }, + { + "version": "v2.0.1", + "default": false, + "pages": [ + "docs/sdk-reference/desktop-python-sdk/v2.0.1/desktop" + ] + }, + { + "version": "v2.0.0", + "default": false, + "pages": [ + "docs/sdk-reference/desktop-python-sdk/v2.0.0/desktop" + ] + } + ] + }, + { + "dropdown": "CLI", + "icon": "terminal", + "versions": [ + { + "version": "v2.4.2", + "default": true, + "pages": [ + "docs/sdk-reference/cli/v2.4.2/auth", + "docs/sdk-reference/cli/v2.4.2/sandbox", + "docs/sdk-reference/cli/v2.4.2/template" + ] + }, + { + "version": "v2.4.1", + "default": false, + "pages": [ + "docs/sdk-reference/cli/v2.4.1/auth", + "docs/sdk-reference/cli/v2.4.1/sandbox", + "docs/sdk-reference/cli/v2.4.1/template" + ] + }, + { + "version": "v2.4.0", + "default": false, + "pages": [ + "docs/sdk-reference/cli/v2.4.0/auth", + "docs/sdk-reference/cli/v2.4.0/sandbox", + "docs/sdk-reference/cli/v2.4.0/template" + ] + }, + { + "version": "v2.3.3", + "default": false, + "pages": [ + "docs/sdk-reference/cli/v2.3.3/auth", + "docs/sdk-reference/cli/v2.3.3/sandbox", + "docs/sdk-reference/cli/v2.3.3/template" + ] + }, + { + "version": "v1.0.0", + "default": false, + "pages": [ + "docs/sdk-reference/cli/1.0.0/auth", + "docs/sdk-reference/cli/1.0.0/sandbox", + "docs/sdk-reference/cli/1.0.0/template" + ] + } + ] + } + ] } ], "global": {} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e83d282 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +# Python dependencies for SDK reference generation +pydoc-markdown>=4.8.2 +poetry>=1.8.0 + diff --git a/scripts/generate-mcp-servers.sh b/scripts/generate-mcp-servers.sh old mode 100644 new mode 100755 diff --git a/sdk-reference-generator/.gitignore b/sdk-reference-generator/.gitignore new file mode 100644 index 0000000..94973cf --- /dev/null +++ b/sdk-reference-generator/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log + diff --git a/sdk-reference-generator/README.md b/sdk-reference-generator/README.md new file mode 100644 index 0000000..790626a --- /dev/null +++ b/sdk-reference-generator/README.md @@ -0,0 +1,201 @@ +# SDK Reference Documentation Generator + +TypeScript-based documentation generator for E2B SDKs with automatic versioning, caching, and CI/CD integration. + +## Features + +- **Multi-SDK Support**: JS, Python, CLI, Code Interpreter, Desktop SDKs +- **Automatic Version Discovery**: Detects and generates missing versions +- **Intelligent Caching**: Skips reinstalling dependencies when lockfile unchanged +- **Idempotent**: Safe to run repeatedly, only generates what's missing +- **Full Visibility**: Complete logging of all subcommands for debugging +- **Verification**: Validates generated docs before finalizing +- **CI/CD Ready**: GitHub Actions integration with safety checks + +## Usage + +```bash +# generate all SDKs, all versions +pnpm run generate + +# generate specific SDK, latest version +pnpm run generate --sdk js-sdk --version latest + +# generate specific version +pnpm run generate --sdk python-sdk --version v2.8.0 + +# limit to last N versions (useful for testing) +pnpm run generate --sdk all --version all --limit 5 + +# force regenerate existing versions (useful after config changes) +pnpm run generate --sdk js-sdk --version all --force + +# combine flags +pnpm run generate --sdk all --version all --limit 3 --force +``` + +## Architecture + +``` +src/ +├── cli.ts # Entry point with CLI argument parsing +├── generator.ts # Core SDK generation orchestration +├── navigation.ts # Mintlify navigation builder +├── types.ts # TypeScript interfaces +├── lib/ +│ ├── constants.ts # Centralized magic strings +│ ├── utils.ts # Pure utility functions +│ ├── git.ts # Git operations (clone, tags) +│ ├── checkout.ts # Manages repo checkouts and version switching +│ ├── versions.ts # Version comparison and filtering +│ ├── files.ts # Markdown processing and flattening +│ ├── install.ts # Package manager abstraction +│ └── verify.ts # Post-generation validation +└── generators/ + ├── typedoc.ts # JavaScript/TypeScript docs + ├── pydoc.ts # Python docs + └── cli.ts # CLI command docs +``` + +## Configuration + +SDKs are configured in `sdks.json`: + +```json +{ + "sdks": { + "js-sdk": { + "displayName": "SDK (JavaScript)", + "icon": "square-js", + "order": 1, + "repo": "https://github.com/e2b-dev/e2b.git", + "tagPattern": "e2b@", + "tagFormat": "e2b@{version}", + "generator": "typedoc", + "required": true, + "minVersion": "1.0.0", + "sdkPath": "packages/js-sdk" + } + } +} +``` + +## Error Handling + +### Strict Safety Model + +1. **Required SDKs**: Any failure aborts workflow +2. **Optional SDKs**: All versions failing aborts workflow +3. **Partial failures**: Non-required SDK with some successes continues +4. **Verification**: Post-generation validation ensures quality + +### Progressive Dependency Resolution + +For maximum compatibility across SDK versions: + +1. **Try pnpm install** - Primary package manager with caching +2. **Try npm fallback** - Uses npm with `--force` and `--legacy-peer-deps` + +Each strategy visible in logs for debugging. If both strategies fail, workflow aborts. + +### What Gets Logged + +- ✅ Package manager output (pnpm/npm/poetry/pip) +- ✅ Build tool output (TypeDoc, pydoc-markdown, CLI builds) +- ✅ File operations (copying, flattening) +- ✅ Validation results (empty files, missing frontmatter) +- ✅ Final statistics (files, SDKs, versions) + +## Verification Checks + +Before finalizing, the generator verifies: + +1. **Generated Files**: No empty MDX files +2. **Frontmatter**: All files have proper frontmatter +3. **Structure**: Valid directory structure +4. **docs.json**: Valid JSON with correct navigation structure + +## Testing + +```bash +# run unit tests +pnpm test + +# run with watch mode +pnpm test:watch + +# type check +npx tsc --noEmit +``` + +Tests cover: +- Version comparison and filtering (10 tests) +- File operations and title casing (5 tests) +- Verification logic (7 tests) + +## CI/CD Integration + +The generator runs in GitHub Actions on: +- Manual workflow dispatch +- Automatic repository dispatch from SDK repos on release + +### Manual Trigger (GitHub UI) + +1. Go to **Actions** → **Sync SDK Reference Documentation** +2. Click **Run workflow** +3. Fill in: + - **SDK**: `all`, or specific SDK key (e.g., `js-sdk`, `python-sdk`, `cli`) + - **Version**: `all`, `latest`, or specific version (e.g., `v2.9.0`) + +### Manual Trigger (GitHub CLI) + +```bash +# generate all SDKs, all versions +gh workflow run sdk-reference-sync.yml -f sdk=all -f version=all + +# generate specific SDK, latest version +gh workflow run sdk-reference-sync.yml -f sdk=js-sdk -f version=latest + +# generate specific version +gh workflow run sdk-reference-sync.yml -f sdk=python-sdk -f version=v2.8.0 +``` + +### Repository Dispatch (from SDK repos) + +SDK repositories can trigger doc generation on release: + +```bash +curl -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/e2b-dev/docs/dispatches \ + -d '{"event_type": "sdk-release", "client_payload": {"sdk": "js-sdk", "version": "v2.9.0"}}' +``` + +### Safety Features + +- Validates all generated files before committing +- Only commits if changes detected +- Skips CI on doc updates (`[skip ci]` in commit message) +- Full logging visible in workflow runs +- User inputs passed via environment variables (prevents script injection) + +## Performance + +- **Checkout Reuse**: Repository cloned once, versions switched via git checkout +- **Version Deduplication**: Batch comparison skips already-generated versions +- **Parallel Generation**: Could process multiple versions concurrently (future enhancement) + +## Development + +```bash +# install dependencies +pnpm install + +# run generator locally +pnpm run generate --sdk js-sdk --limit 1 + +# run tests +pnpm test +``` + diff --git a/sdk-reference-generator/configs/typedoc-theme.cjs b/sdk-reference-generator/configs/typedoc-theme.cjs new file mode 100644 index 0000000..b1cb1de --- /dev/null +++ b/sdk-reference-generator/configs/typedoc-theme.cjs @@ -0,0 +1,63 @@ +/** + * Custom TypeDoc markdown theme for E2B SDK reference docs. + * Cleans up generated markdown for Mintlify compatibility. + */ +const { MarkdownPageEvent } = require('typedoc-plugin-markdown') + +function load(app) { + // listen to the render event + app.renderer.on(MarkdownPageEvent.END, (page) => { + // process markdown content + page.contents = removeMarkdownLinks( + removeFirstNLines( + convertH5toH3(removeLinesWithConditions(page.contents)), + 6 + ) + ) + }) +} + +// makes methods in the sdk reference look more prominent +function convertH5toH3(text) { + return text.replace(/^##### (.*)$/gm, '### $1') +} + +// removes markdown-style links, keeps link text +function removeMarkdownLinks(text) { + return text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') +} + +function removeFirstNLines(text, n) { + return text.split('\n').slice(n).join('\n') +} + +// removes "Extends", "Overrides", "Inherited from" sections +function removeLinesWithConditions(text) { + const lines = text.split('\n') + const filteredLines = [] + + for (let i = 0; i < lines.length; i++) { + if ( + lines[i].startsWith('#### Extends') || + lines[i].startsWith('###### Overrides') || + lines[i].startsWith('###### Inherited from') + ) { + // skip this line and the next three + i += 3 + continue + } + + if (lines[i].startsWith('##### new')) { + // avoid promoting constructors + i += 1 + continue + } + + filteredLines.push(lines[i]) + } + + return filteredLines.join('\n') +} + +module.exports = { load } + diff --git a/sdk-reference-generator/package.json b/sdk-reference-generator/package.json new file mode 100644 index 0000000..5692b09 --- /dev/null +++ b/sdk-reference-generator/package.json @@ -0,0 +1,29 @@ +{ + "name": "e2b-sdk-reference-generator", + "version": "1.0.0", + "type": "module", + "scripts": { + "generate": "tsx src/cli.ts", + "rebuild-docs-json": "tsx src/rebuild-docs-json.ts", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "chalk": "^5.4.1", + "commander": "^12.1.0", + "execa": "^9.5.2", + "fs-extra": "^11.2.0", + "glob": "^11.0.0", + "semver": "^7.6.3", + "simple-git": "^3.27.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/node": "^22.10.2", + "@types/semver": "^7.5.8", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + } +} diff --git a/sdk-reference-generator/pnpm-lock.yaml b/sdk-reference-generator/pnpm-lock.yaml new file mode 100644 index 0000000..62c9eaf --- /dev/null +++ b/sdk-reference-generator/pnpm-lock.yaml @@ -0,0 +1,1703 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + chalk: + specifier: ^5.4.1 + version: 5.6.2 + commander: + specifier: ^12.1.0 + version: 12.1.0 + execa: + specifier: ^9.5.2 + version: 9.6.1 + fs-extra: + specifier: ^11.2.0 + version: 11.3.3 + glob: + specifier: ^11.0.0 + version: 11.1.0 + semver: + specifier: ^7.6.3 + version: 7.7.3 + simple-git: + specifier: ^3.27.0 + version: 3.30.0 + zod: + specifier: ^3.24.1 + version: 3.25.76 + devDependencies: + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 + '@types/node': + specifier: ^22.10.2 + version: 22.19.3 + '@types/semver': + specifier: ^7.5.8 + version: 7.7.1 + tsx: + specifier: ^4.19.2 + version: 4.21.0 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.19.3) + +packages: + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + + '@kwsites/promise-deferred@1.1.1': + resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + + '@rollup/rollup-android-arm-eabi@4.55.1': + resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.55.1': + resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.55.1': + resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.55.1': + resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.55.1': + resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.55.1': + resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.55.1': + resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.55.1': + resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.55.1': + resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.55.1': + resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.55.1': + resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.55.1': + resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.55.1': + resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.55.1': + resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.55.1': + resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.55.1': + resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.55.1': + resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.55.1': + resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.55.1': + resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.55.1': + resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.55.1': + resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + + '@types/node@22.19.3': + resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} + + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + hasBin: true + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + engines: {node: 20 || >=22} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} + engines: {node: 20 || >=22} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} + engines: {node: 20 || >=22} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rollup@4.55.1: + resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-git@3.30.0: + resolution: {integrity: sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@kwsites/promise-deferred@1.1.1': {} + + '@rollup/rollup-android-arm-eabi@4.55.1': + optional: true + + '@rollup/rollup-android-arm64@4.55.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.55.1': + optional: true + + '@rollup/rollup-darwin-x64@4.55.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.55.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.55.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.55.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.55.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.55.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.55.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.55.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.55.1': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@types/estree@1.0.8': {} + + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 22.19.3 + + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 22.19.3 + + '@types/node@22.19.3': + dependencies: + undici-types: 6.21.0 + + '@types/semver@7.7.1': {} + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.3))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.19.3) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + assertion-error@2.0.1: {} + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@5.6.2: {} + + check-error@2.1.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@12.1.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + expect-type@1.3.0: {} + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fs-extra@11.3.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.1.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.1 + + graceful-fs@4.2.11: {} + + human-signals@8.0.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-plain-obj@4.1.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.1.0: {} + + isexe@2.0.0: {} + + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + loupe@3.2.1: {} + + lru-cache@11.2.4: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minimatch@10.1.1: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + + minipass@7.1.2: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + package-json-from-dist@1.0.1: {} + + parse-ms@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.4 + minipass: 7.1.2 + + pathe@1.1.2: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + resolve-pkg-maps@1.0.0: {} + + rollup@4.55.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.55.1 + '@rollup/rollup-android-arm64': 4.55.1 + '@rollup/rollup-darwin-arm64': 4.55.1 + '@rollup/rollup-darwin-x64': 4.55.1 + '@rollup/rollup-freebsd-arm64': 4.55.1 + '@rollup/rollup-freebsd-x64': 4.55.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 + '@rollup/rollup-linux-arm-musleabihf': 4.55.1 + '@rollup/rollup-linux-arm64-gnu': 4.55.1 + '@rollup/rollup-linux-arm64-musl': 4.55.1 + '@rollup/rollup-linux-loong64-gnu': 4.55.1 + '@rollup/rollup-linux-loong64-musl': 4.55.1 + '@rollup/rollup-linux-ppc64-gnu': 4.55.1 + '@rollup/rollup-linux-ppc64-musl': 4.55.1 + '@rollup/rollup-linux-riscv64-gnu': 4.55.1 + '@rollup/rollup-linux-riscv64-musl': 4.55.1 + '@rollup/rollup-linux-s390x-gnu': 4.55.1 + '@rollup/rollup-linux-x64-gnu': 4.55.1 + '@rollup/rollup-linux-x64-musl': 4.55.1 + '@rollup/rollup-openbsd-x64': 4.55.1 + '@rollup/rollup-openharmony-arm64': 4.55.1 + '@rollup/rollup-win32-arm64-msvc': 4.55.1 + '@rollup/rollup-win32-ia32-msvc': 4.55.1 + '@rollup/rollup-win32-x64-gnu': 4.55.1 + '@rollup/rollup-win32-x64-msvc': 4.55.1 + fsevents: 2.3.3 + + semver@7.7.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + simple-git@3.30.0: + dependencies: + '@kwsites/file-exists': 1.1.1 + '@kwsites/promise-deferred': 1.1.1 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-final-newline@4.0.0: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.2 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + unicorn-magic@0.3.0: {} + + universalify@2.0.1: {} + + vite-node@2.1.9(@types/node@22.19.3): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.19.3) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@22.19.3): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.55.1 + optionalDependencies: + '@types/node': 22.19.3 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@22.19.3): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.3)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.19.3) + vite-node: 2.1.9(@types/node@22.19.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.3 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + yoctocolors@2.1.2: {} + + zod@3.25.76: {} diff --git a/sdk-reference-generator/sdks.config.ts b/sdk-reference-generator/sdks.config.ts new file mode 100644 index 0000000..f69ea37 --- /dev/null +++ b/sdk-reference-generator/sdks.config.ts @@ -0,0 +1,176 @@ +import type { SDKConfig } from "./src/types.js"; + +const sdks = { + cli: { + displayName: "CLI", + icon: "terminal", + order: 7, + repo: "https://github.com/e2b-dev/e2b.git", + tagPattern: "@e2b/cli@", + tagFormat: "@e2b/cli@{version}", + sdkPath: "packages/cli", + generator: "cli", + required: true, + minVersion: "1.0.0", + }, + + "js-sdk": { + displayName: "SDK (JavaScript)", + icon: "square-js", + order: 1, + repo: "https://github.com/e2b-dev/e2b.git", + tagPattern: "e2b@", + tagFormat: "e2b@{version}", + sdkPath: "packages/js-sdk", + generator: "typedoc", + required: true, + minVersion: "1.0.0", + + defaultConfig: { + entryPoints: [ + "src/sandbox/index.ts", + "src/sandbox/filesystem/index.ts", + "src/sandbox/process/index.ts", + "src/sandbox/commands/index.ts", + "src/errors.ts", + "src/template/index.ts", + "src/template/readycmd.ts", + "src/template/logger.ts", + ], + }, + + configOverrides: { + "1.0.0": { + entryPoints: [ + "src/sandbox/index.ts", + "src/sandbox/filesystem/index.ts", + "src/sandbox/process/index.ts", + "src/sandbox/pty.ts", + "src/errors.ts", + ], + }, + + ">=1.1.0 <2.3.0": { + entryPoints: [ + "src/sandbox/index.ts", + "src/sandbox/filesystem/index.ts", + "src/sandbox/process/index.ts", + "src/sandbox/commands/index.ts", + "src/errors.ts", + ], + }, + }, + }, + + "python-sdk": { + displayName: "SDK (Python)", + icon: "python", + order: 2, + repo: "https://github.com/e2b-dev/e2b.git", + tagPattern: "@e2b/python-sdk@", + tagFormat: "@e2b/python-sdk@{version}", + sdkPath: "packages/python-sdk", + generator: "pydoc", + required: true, + minVersion: "1.0.0", + + defaultConfig: { + allowedPackages: [ + "e2b.sandbox_sync", + "e2b.sandbox_async", + "e2b.exceptions", + "e2b.template", + "e2b.template_sync", + "e2b.template_async", + "e2b.template.logger", + "e2b.template.readycmd", + ], + }, + + configOverrides: { + ">=1.0.0 <2.1.0": { + allowedPackages: [ + "e2b.sandbox_sync", + "e2b.sandbox_async", + "e2b.exceptions", + ], + }, + }, + }, + + "code-interpreter-js-sdk": { + displayName: "Code Interpreter SDK (JavaScript)", + icon: "square-js", + order: 3, + repo: "https://github.com/e2b-dev/code-interpreter.git", + tagPattern: "@e2b/code-interpreter@", + tagFormat: "@e2b/code-interpreter@{version}", + sdkPaths: ["js"], + generator: "typedoc", + required: false, + minVersion: "1.0.0", + + defaultConfig: { + entryPoints: [ + "src/index.ts", + "src/charts.ts", + "src/consts.ts", + "src/messaging.ts", + "src/sandbox.ts", + ], + }, + }, + + "code-interpreter-python-sdk": { + displayName: "Code Interpreter SDK (Python)", + icon: "python", + order: 4, + repo: "https://github.com/e2b-dev/code-interpreter.git", + tagPattern: "@e2b/code-interpreter-python@", + tagFormat: "@e2b/code-interpreter-python@{version}", + sdkPaths: ["python"], + generator: "pydoc", + required: false, + minVersion: "1.0.0", + + defaultConfig: { + allowedPackages: ["e2b_code_interpreter"], + }, + }, + + "desktop-js-sdk": { + displayName: "Desktop SDK (JavaScript)", + icon: "square-js", + order: 5, + repo: "https://github.com/e2b-dev/desktop.git", + tagPattern: "@e2b/desktop@", + tagFormat: "@e2b/desktop@{version}", + sdkPaths: ["packages/js-sdk"], + generator: "typedoc", + required: false, + minVersion: "1.0.0", + + defaultConfig: { + entryPoints: ["src/index.ts", "src/sandbox.ts"], + }, + }, + + "desktop-python-sdk": { + displayName: "Desktop SDK (Python)", + icon: "python", + order: 6, + repo: "https://github.com/e2b-dev/desktop.git", + tagPattern: "@e2b/desktop-python@", + tagFormat: "@e2b/desktop-python@{version}", + sdkPaths: ["packages/python-sdk"], + generator: "pydoc", + required: false, + minVersion: "1.0.0", + + defaultConfig: { + allowedPackages: ["e2b_desktop"], + }, + }, +} as const satisfies Record; + +export default sdks; diff --git a/sdk-reference-generator/src/__tests__/checkout.test.ts b/sdk-reference-generator/src/__tests__/checkout.test.ts new file mode 100644 index 0000000..3db07e4 --- /dev/null +++ b/sdk-reference-generator/src/__tests__/checkout.test.ts @@ -0,0 +1,180 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { CheckoutManager } from "../lib/checkout.js"; + +vi.mock("../lib/git.js", () => ({ + cloneAtTag: vi.fn(), + checkoutTag: vi.fn(), +})); + +vi.mock("fs-extra", () => ({ + default: { + remove: vi.fn(), + }, +})); + +import { cloneAtTag, checkoutTag } from "../lib/git.js"; +import fs from "fs-extra"; + +const mockCloneAtTag = vi.mocked(cloneAtTag); +const mockCheckoutTag = vi.mocked(checkoutTag); +const mockRemove = vi.mocked(fs.remove); + +describe("CheckoutManager", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("getOrClone", () => { + it("clones repo on first call", async () => { + const mgr = new CheckoutManager(); + + const repoDir = await mgr.getOrClone( + "test-sdk", + "https://github.com/test/repo.git", + "v1.0.0", + "/tmp" + ); + + expect(repoDir).toBe("/tmp/shared-test-sdk"); + expect(mockCloneAtTag).toHaveBeenCalledWith( + "https://github.com/test/repo.git", + "v1.0.0", + "/tmp/shared-test-sdk" + ); + }); + + it("returns cached dir on subsequent calls", async () => { + const mgr = new CheckoutManager(); + + await mgr.getOrClone( + "test-sdk", + "https://github.com/test/repo.git", + "v1.0.0", + "/tmp" + ); + + mockCloneAtTag.mockClear(); + + const repoDir = await mgr.getOrClone( + "test-sdk", + "https://github.com/test/repo.git", + "v2.0.0", + "/tmp" + ); + + expect(repoDir).toBe("/tmp/shared-test-sdk"); + expect(mockCloneAtTag).not.toHaveBeenCalled(); + }); + + it("handles multiple SDKs independently", async () => { + const mgr = new CheckoutManager(); + + const dir1 = await mgr.getOrClone( + "sdk-a", + "https://github.com/test/a.git", + "v1.0.0", + "/tmp" + ); + + const dir2 = await mgr.getOrClone( + "sdk-b", + "https://github.com/test/b.git", + "v2.0.0", + "/tmp" + ); + + expect(dir1).toBe("/tmp/shared-sdk-a"); + expect(dir2).toBe("/tmp/shared-sdk-b"); + expect(mockCloneAtTag).toHaveBeenCalledTimes(2); + }); + }); + + describe("switchVersion", () => { + it("switches to new tag in existing checkout", async () => { + const mgr = new CheckoutManager(); + + await mgr.getOrClone( + "test-sdk", + "https://github.com/test/repo.git", + "v1.0.0", + "/tmp" + ); + + await mgr.switchVersion("test-sdk", "v2.0.0"); + + expect(mockCheckoutTag).toHaveBeenCalledWith( + "/tmp/shared-test-sdk", + "v2.0.0" + ); + }); + + it("throws if checkout not initialized", async () => { + const mgr = new CheckoutManager(); + + await expect(mgr.switchVersion("unknown-sdk", "v1.0.0")).rejects.toThrow( + "Checkout not initialized for unknown-sdk" + ); + }); + }); + + describe("getRepoDir", () => { + it("returns undefined for unknown SDK", () => { + const mgr = new CheckoutManager(); + expect(mgr.getRepoDir("unknown")).toBeUndefined(); + }); + + it("returns path for initialized SDK", async () => { + const mgr = new CheckoutManager(); + + await mgr.getOrClone( + "test-sdk", + "https://github.com/test/repo.git", + "v1.0.0", + "/tmp" + ); + + expect(mgr.getRepoDir("test-sdk")).toBe("/tmp/shared-test-sdk"); + }); + }); + + describe("cleanup", () => { + it("removes all checkout directories", async () => { + const mgr = new CheckoutManager(); + + await mgr.getOrClone( + "sdk-a", + "https://github.com/test/a.git", + "v1.0.0", + "/tmp" + ); + + await mgr.getOrClone( + "sdk-b", + "https://github.com/test/b.git", + "v1.0.0", + "/tmp" + ); + + await mgr.cleanup(); + + expect(mockRemove).toHaveBeenCalledWith("/tmp/shared-sdk-a"); + expect(mockRemove).toHaveBeenCalledWith("/tmp/shared-sdk-b"); + }); + + it("clears internal state after cleanup", async () => { + const mgr = new CheckoutManager(); + + await mgr.getOrClone( + "test-sdk", + "https://github.com/test/repo.git", + "v1.0.0", + "/tmp" + ); + + await mgr.cleanup(); + + expect(mgr.getRepoDir("test-sdk")).toBeUndefined(); + }); + }); +}); + diff --git a/sdk-reference-generator/src/__tests__/config.test.ts b/sdk-reference-generator/src/__tests__/config.test.ts new file mode 100644 index 0000000..66b5e9d --- /dev/null +++ b/sdk-reference-generator/src/__tests__/config.test.ts @@ -0,0 +1,187 @@ +import { describe, it, expect } from "vitest"; +import { resolveConfig, buildTypedocConfig, TYPEDOC_FORMATTING } from "../lib/config.js"; +import type { TypedocConfig, PydocConfig } from "../types.js"; + +describe("resolveConfig", () => { + describe("with TypedocConfig", () => { + const defaultConfig: TypedocConfig = { + entryPoints: ["src/index.ts"], + }; + + it("returns defaultConfig when no overrides", () => { + const result = resolveConfig(defaultConfig, undefined, "v1.0.0"); + expect(result).toEqual(defaultConfig); + }); + + it("returns defaultConfig when no matching range", () => { + const overrides = { + ">=2.0.0": { entryPoints: ["src/v2/index.ts"] }, + }; + const result = resolveConfig(defaultConfig, overrides, "v1.5.0"); + expect(result).toEqual(defaultConfig); + }); + + it("returns merged config when version matches range", () => { + const overrides = { + ">=1.0.0 <2.0.0": { entryPoints: ["src/v1/index.ts"] }, + }; + const result = resolveConfig(defaultConfig, overrides, "v1.5.0"); + expect(result).toEqual({ entryPoints: ["src/v1/index.ts"] }); + }); + + it("handles version with v prefix", () => { + const overrides = { + ">=1.0.0": { entryPoints: ["src/new/index.ts"] }, + }; + const result = resolveConfig(defaultConfig, overrides, "v1.2.3"); + expect(result).toEqual({ entryPoints: ["src/new/index.ts"] }); + }); + + it("handles version without v prefix", () => { + const overrides = { + ">=1.0.0": { entryPoints: ["src/new/index.ts"] }, + }; + const result = resolveConfig(defaultConfig, overrides, "1.2.3"); + expect(result).toEqual({ entryPoints: ["src/new/index.ts"] }); + }); + + it("uses first matching range when multiple match", () => { + const overrides = { + ">=1.0.0 <1.5.0": { entryPoints: ["src/v1-early/index.ts"] }, + ">=1.0.0": { entryPoints: ["src/v1/index.ts"] }, + }; + const result = resolveConfig(defaultConfig, overrides, "v1.2.0"); + expect(result).toEqual({ entryPoints: ["src/v1-early/index.ts"] }); + }); + + it("merges partial overrides with defaults", () => { + const configWithExclude: TypedocConfig = { + entryPoints: ["src/index.ts"], + exclude: ["**/*.test.ts"], + }; + const overrides = { + ">=1.0.0": { entryPoints: ["src/new/index.ts"] }, + }; + const result = resolveConfig(configWithExclude, overrides, "v1.0.0"); + expect(result).toEqual({ + entryPoints: ["src/new/index.ts"], + exclude: ["**/*.test.ts"], + }); + }); + }); + + describe("with PydocConfig", () => { + const defaultConfig: PydocConfig = { + allowedPackages: ["e2b"], + }; + + it("returns defaultConfig when no overrides", () => { + const result = resolveConfig(defaultConfig, undefined, "v1.0.0"); + expect(result).toEqual(defaultConfig); + }); + + it("returns merged config when version matches range", () => { + const overrides = { + ">=1.0.0": { allowedPackages: ["e2b.sandbox_sync", "e2b.sandbox_async"] as const }, + }; + const result = resolveConfig(defaultConfig, overrides, "v1.5.0"); + expect(result.allowedPackages).toEqual(["e2b.sandbox_sync", "e2b.sandbox_async"]); + }); + }); + + describe("semver range matching", () => { + const defaultConfig: TypedocConfig = { entryPoints: ["default.ts"] }; + + it("matches exact version", () => { + const overrides = { "1.0.0": { entryPoints: ["exact.ts"] } }; + const result = resolveConfig(defaultConfig, overrides, "v1.0.0"); + expect(result.entryPoints).toEqual(["exact.ts"]); + }); + + it("matches >= range", () => { + const overrides = { ">=1.0.0": { entryPoints: ["gte.ts"] } }; + expect(resolveConfig(defaultConfig, overrides, "v1.0.0").entryPoints).toEqual(["gte.ts"]); + expect(resolveConfig(defaultConfig, overrides, "v2.0.0").entryPoints).toEqual(["gte.ts"]); + expect(resolveConfig(defaultConfig, overrides, "v0.9.0").entryPoints).toEqual(["default.ts"]); + }); + + it("matches < range", () => { + const overrides = { "<2.0.0": { entryPoints: ["lt.ts"] } }; + expect(resolveConfig(defaultConfig, overrides, "v1.9.9").entryPoints).toEqual(["lt.ts"]); + expect(resolveConfig(defaultConfig, overrides, "v2.0.0").entryPoints).toEqual(["default.ts"]); + }); + + it("matches compound range", () => { + const overrides = { ">=1.0.0 <2.0.0": { entryPoints: ["compound.ts"] } }; + expect(resolveConfig(defaultConfig, overrides, "v1.0.0").entryPoints).toEqual(["compound.ts"]); + expect(resolveConfig(defaultConfig, overrides, "v1.9.9").entryPoints).toEqual(["compound.ts"]); + expect(resolveConfig(defaultConfig, overrides, "v0.9.9").entryPoints).toEqual(["default.ts"]); + expect(resolveConfig(defaultConfig, overrides, "v2.0.0").entryPoints).toEqual(["default.ts"]); + }); + + it("matches tilde range", () => { + const overrides = { "~1.2.0": { entryPoints: ["tilde.ts"] } }; + expect(resolveConfig(defaultConfig, overrides, "v1.2.0").entryPoints).toEqual(["tilde.ts"]); + expect(resolveConfig(defaultConfig, overrides, "v1.2.9").entryPoints).toEqual(["tilde.ts"]); + expect(resolveConfig(defaultConfig, overrides, "v1.3.0").entryPoints).toEqual(["default.ts"]); + }); + + it("matches caret range", () => { + const overrides = { "^1.2.0": { entryPoints: ["caret.ts"] } }; + expect(resolveConfig(defaultConfig, overrides, "v1.2.0").entryPoints).toEqual(["caret.ts"]); + expect(resolveConfig(defaultConfig, overrides, "v1.9.9").entryPoints).toEqual(["caret.ts"]); + expect(resolveConfig(defaultConfig, overrides, "v2.0.0").entryPoints).toEqual(["default.ts"]); + }); + }); +}); + +describe("buildTypedocConfig", () => { + it("merges TYPEDOC_FORMATTING with resolved config", () => { + const resolved: TypedocConfig = { + entryPoints: ["src/sandbox/index.ts"], + }; + const result = buildTypedocConfig(resolved); + + expect(result.entryPoints).toEqual(["src/sandbox/index.ts"]); + expect(result.out).toBe(TYPEDOC_FORMATTING.out); + expect(result.excludeExternals).toBe(true); + expect(result.hidePageTitle).toBe(true); + }); + + it("includes exclude when provided", () => { + const resolved: TypedocConfig = { + entryPoints: ["src/index.ts"], + exclude: ["**/*.test.ts", "**/__tests__/**"], + }; + const result = buildTypedocConfig(resolved); + + expect(result.exclude).toEqual(["**/*.test.ts", "**/__tests__/**"]); + }); + + it("does not include exclude when not provided", () => { + const resolved: TypedocConfig = { + entryPoints: ["src/index.ts"], + }; + const result = buildTypedocConfig(resolved); + + // exclude comes from TYPEDOC_FORMATTING default + expect(result.exclude).toEqual(TYPEDOC_FORMATTING.exclude); + }); +}); + +describe("TYPEDOC_FORMATTING", () => { + it("has all required formatting options", () => { + expect(TYPEDOC_FORMATTING.out).toBe("sdk_ref"); + expect(TYPEDOC_FORMATTING.excludeExternals).toBe(true); + expect(TYPEDOC_FORMATTING.excludeInternal).toBe(true); + expect(TYPEDOC_FORMATTING.excludePrivate).toBe(true); + expect(TYPEDOC_FORMATTING.excludeProtected).toBe(true); + expect(TYPEDOC_FORMATTING.outputFileStrategy).toBe("modules"); + expect(TYPEDOC_FORMATTING.readme).toBe("none"); + expect(TYPEDOC_FORMATTING.disableSources).toBe(true); + expect(TYPEDOC_FORMATTING.classPropertiesFormat).toBe("table"); + expect(TYPEDOC_FORMATTING.hidePageTitle).toBe(true); + expect(TYPEDOC_FORMATTING.hideBreadcrumbs).toBe(true); + }); +}); + diff --git a/sdk-reference-generator/src/__tests__/files-processing.test.ts b/sdk-reference-generator/src/__tests__/files-processing.test.ts new file mode 100644 index 0000000..217c0ed --- /dev/null +++ b/sdk-reference-generator/src/__tests__/files-processing.test.ts @@ -0,0 +1,434 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import fs from "fs-extra"; +import path from "path"; +import os from "os"; +import { flattenMarkdown, copyToDocs, locateSDKDir } from "../lib/files.js"; +import { CONSTANTS } from "../lib/constants.js"; + +describe("flattenMarkdown", () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "flatten-test-")); + }); + + afterEach(async () => { + await fs.remove(tempDir); + }); + + describe("removeUnwantedFiles", () => { + it("removes README.md, index.md, and index.mdx", async () => { + await fs.writeFile(path.join(tempDir, "README.md"), "# Readme"); + await fs.writeFile(path.join(tempDir, "index.md"), "# Index"); + await fs.writeFile(path.join(tempDir, "index.mdx"), "# Index MDX"); + await fs.writeFile(path.join(tempDir, "Sandbox.md"), "# Keep this"); + + await flattenMarkdown(tempDir); + + expect(await fs.pathExists(path.join(tempDir, "README.md"))).toBe(false); + expect(await fs.pathExists(path.join(tempDir, "index.md"))).toBe(false); + expect(await fs.pathExists(path.join(tempDir, "index.mdx"))).toBe(false); + expect(await fs.pathExists(path.join(tempDir, "Sandbox.mdx"))).toBe(true); + }); + }); + + describe("flattenNestedFiles", () => { + it("flattens nested md files to top level", async () => { + const nestedDir = path.join(tempDir, "modules", "sandbox"); + await fs.ensureDir(nestedDir); + await fs.writeFile(path.join(nestedDir, "Sandbox.md"), "# Sandbox"); + + await flattenMarkdown(tempDir); + + // nested file should be flattened with path prefix + expect( + await fs.pathExists(path.join(tempDir, "modules-sandbox-Sandbox.mdx")) + ).toBe(true); + // original nested directory should be removed + expect(await fs.pathExists(nestedDir)).toBe(false); + }); + + it("renames page.md to parent directory name", async () => { + const nestedDir = path.join(tempDir, "sandbox"); + await fs.ensureDir(nestedDir); + await fs.writeFile(path.join(nestedDir, "page.md"), "# Page content"); + + await flattenMarkdown(tempDir); + + expect(await fs.pathExists(path.join(tempDir, "sandbox.mdx"))).toBe(true); + }); + + it("renames index.md in nested dirs to parent directory name", async () => { + const nestedDir = path.join(tempDir, "filesystem"); + await fs.ensureDir(nestedDir); + await fs.writeFile( + path.join(nestedDir, "index.md"), + "# Filesystem index" + ); + + await flattenMarkdown(tempDir); + + expect(await fs.pathExists(path.join(tempDir, "filesystem.mdx"))).toBe( + true + ); + }); + + it("handles deeply nested files", async () => { + const deepDir = path.join(tempDir, "a", "b", "c"); + await fs.ensureDir(deepDir); + await fs.writeFile(path.join(deepDir, "Deep.md"), "# Deep file"); + + await flattenMarkdown(tempDir); + + expect(await fs.pathExists(path.join(tempDir, "a-b-c-Deep.mdx"))).toBe( + true + ); + }); + }); + + describe("convertMdToMdx", () => { + it("converts .md files to .mdx with frontmatter", async () => { + await fs.writeFile(path.join(tempDir, "Test.md"), "# Test content"); + + await flattenMarkdown(tempDir); + + const mdxPath = path.join(tempDir, "Test.mdx"); + expect(await fs.pathExists(mdxPath)).toBe(true); + expect(await fs.pathExists(path.join(tempDir, "Test.md"))).toBe(false); + + const content = await fs.readFile(mdxPath, "utf-8"); + expect(content).toContain("---"); + expect(content).toContain('sidebarTitle: "Test"'); + expect(content).toContain("# Test content"); + }); + + it("generates correct title from snake_case filenames", async () => { + await fs.writeFile(path.join(tempDir, "sandbox_sync.md"), "# Content"); + + await flattenMarkdown(tempDir); + + const content = await fs.readFile( + path.join(tempDir, "sandbox_sync.mdx"), + "utf-8" + ); + expect(content).toContain('sidebarTitle: "Sandbox Sync"'); + }); + }); + + describe("ensureFrontmatter", () => { + it("adds frontmatter to mdx files without it", async () => { + // create mdx file without frontmatter + await fs.writeFile( + path.join(tempDir, "NeedsFrontmatter.mdx"), + "# No frontmatter" + ); + + await flattenMarkdown(tempDir); + + const content = await fs.readFile( + path.join(tempDir, "NeedsFrontmatter.mdx"), + "utf-8" + ); + expect(content.startsWith("---")).toBe(true); + expect(content).toContain('sidebarTitle: "NeedsFrontmatter"'); + }); + + it("does not duplicate frontmatter if already present", async () => { + const existingFrontmatter = `--- +sidebarTitle: "Existing" +--- + +# Content`; + await fs.writeFile( + path.join(tempDir, "HasFrontmatter.mdx"), + existingFrontmatter + ); + + await flattenMarkdown(tempDir); + + const content = await fs.readFile( + path.join(tempDir, "HasFrontmatter.mdx"), + "utf-8" + ); + // should still have exactly one frontmatter block + const frontmatterCount = (content.match(/---/g) || []).length; + expect(frontmatterCount).toBe(2); // opening and closing --- + expect(content).toContain('sidebarTitle: "Existing"'); + }); + }); + + describe("full workflow", () => { + it("processes complex directory structure correctly", async () => { + // create complex structure + await fs.writeFile(path.join(tempDir, "README.md"), "# Remove me"); + await fs.writeFile(path.join(tempDir, "TopLevel.md"), "# Top level"); + + const modulesDir = path.join(tempDir, "modules"); + await fs.ensureDir(modulesDir); + await fs.writeFile(path.join(modulesDir, "page.md"), "# Modules page"); + await fs.writeFile(path.join(modulesDir, "Helper.md"), "# Helper"); + + const sandboxDir = path.join(modulesDir, "sandbox"); + await fs.ensureDir(sandboxDir); + await fs.writeFile( + path.join(sandboxDir, "Sandbox.md"), + "# Sandbox class" + ); + + await flattenMarkdown(tempDir); + + // check results + expect(await fs.pathExists(path.join(tempDir, "README.md"))).toBe(false); + expect(await fs.pathExists(path.join(tempDir, "TopLevel.mdx"))).toBe( + true + ); + expect(await fs.pathExists(path.join(tempDir, "modules.mdx"))).toBe(true); + expect( + await fs.pathExists(path.join(tempDir, "modules-Helper.mdx")) + ).toBe(true); + expect( + await fs.pathExists(path.join(tempDir, "modules-sandbox-Sandbox.mdx")) + ).toBe(true); + + // verify all have frontmatter + const files = await fs.readdir(tempDir); + for (const file of files) { + if (file.endsWith(".mdx")) { + const content = await fs.readFile(path.join(tempDir, file), "utf-8"); + expect(content.startsWith("---")).toBe(true); + } + } + + // verify directories are removed + expect(await fs.pathExists(modulesDir)).toBe(false); + }); + }); +}); + +describe("locateSDKDir", () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "locate-sdk-test-")); + }); + + afterEach(async () => { + await fs.remove(tempDir); + }); + + it("returns sdkPath when it exists", async () => { + const sdkDir = path.join(tempDir, "packages", "sdk"); + await fs.ensureDir(sdkDir); + + const result = await locateSDKDir(tempDir, "packages/sdk"); + + expect(result).toBe(sdkDir); + }); + + it("returns null when sdkPath does not exist", async () => { + const result = await locateSDKDir(tempDir, "nonexistent/path"); + + expect(result).toBeNull(); + }); + + it("returns first existing path from sdkPaths array", async () => { + const secondPath = path.join(tempDir, "js"); + await fs.ensureDir(secondPath); + + const result = await locateSDKDir(tempDir, undefined, [ + "python", + "js", + "go", + ]); + + expect(result).toBe(secondPath); + }); + + it("returns null when no sdkPaths exist", async () => { + const result = await locateSDKDir(tempDir, undefined, [ + "nonexistent1", + "nonexistent2", + ]); + + expect(result).toBeNull(); + }); + + it("returns repoDir when no paths specified", async () => { + const result = await locateSDKDir(tempDir); + + expect(result).toBe(tempDir); + }); + + it("sdkPath takes priority over sdkPaths", async () => { + const sdkPathDir = path.join(tempDir, "primary"); + const sdkPathsDir = path.join(tempDir, "fallback"); + await fs.ensureDir(sdkPathDir); + await fs.ensureDir(sdkPathsDir); + + const result = await locateSDKDir(tempDir, "primary", ["fallback"]); + + expect(result).toBe(sdkPathDir); + }); +}); + +describe("copyToDocs", () => { + let srcDir: string; + let destDir: string; + + beforeEach(async () => { + srcDir = await fs.mkdtemp(path.join(os.tmpdir(), "copy-src-")); + destDir = await fs.mkdtemp(path.join(os.tmpdir(), "copy-dest-")); + }); + + afterEach(async () => { + await fs.remove(srcDir); + await fs.remove(destDir); + }); + + it("copies non-empty mdx files to destination", async () => { + await fs.writeFile( + path.join(srcDir, "Test.mdx"), + '---\nsidebarTitle: "Test"\n---\n\n# Content' + ); + await fs.writeFile( + path.join(srcDir, "Another.mdx"), + '---\nsidebarTitle: "Another"\n---\n\n# More' + ); + + const result = await copyToDocs(srcDir, destDir, "SDK", "v1.0.0"); + + expect(result).toBe(true); + expect(await fs.pathExists(path.join(destDir, "Test.mdx"))).toBe(true); + expect(await fs.pathExists(path.join(destDir, "Another.mdx"))).toBe(true); + }); + + it("returns false when no mdx files exist", async () => { + // srcDir is empty + + const result = await copyToDocs(srcDir, destDir, "SDK", "v1.0.0"); + + expect(result).toBe(false); + }); + + it("skips empty mdx files", async () => { + await fs.writeFile(path.join(srcDir, "Empty.mdx"), ""); + await fs.writeFile( + path.join(srcDir, "Valid.mdx"), + '---\nsidebarTitle: "Valid"\n---\n\n# Content' + ); + + const result = await copyToDocs(srcDir, destDir, "SDK", "v1.0.0"); + + expect(result).toBe(true); + expect(await fs.pathExists(path.join(destDir, "Valid.mdx"))).toBe(true); + expect(await fs.pathExists(path.join(destDir, "Empty.mdx"))).toBe(false); + }); + + it("returns false when only empty mdx files exist", async () => { + await fs.writeFile(path.join(srcDir, "Empty1.mdx"), ""); + await fs.writeFile(path.join(srcDir, "Empty2.mdx"), ""); + + const result = await copyToDocs(srcDir, destDir, "SDK", "v1.0.0"); + + expect(result).toBe(false); + }); + + it("creates destination directory if it does not exist", async () => { + const newDest = path.join(destDir, "nested", "path"); + await fs.writeFile( + path.join(srcDir, "Test.mdx"), + '---\nsidebarTitle: "Test"\n---\n\n# Content' + ); + + const result = await copyToDocs(srcDir, newDest, "SDK", "v1.0.0"); + + expect(result).toBe(true); + expect(await fs.pathExists(newDest)).toBe(true); + expect(await fs.pathExists(path.join(newDest, "Test.mdx"))).toBe(true); + }); + + it("ignores non-mdx files", async () => { + await fs.writeFile(path.join(srcDir, "readme.txt"), "text file"); + await fs.writeFile(path.join(srcDir, "config.json"), "{}"); + await fs.writeFile( + path.join(srcDir, "Valid.mdx"), + '---\nsidebarTitle: "Valid"\n---\n\n# Content' + ); + + const result = await copyToDocs(srcDir, destDir, "SDK", "v1.0.0"); + + expect(result).toBe(true); + expect(await fs.pathExists(path.join(destDir, "Valid.mdx"))).toBe(true); + expect(await fs.pathExists(path.join(destDir, "readme.txt"))).toBe(false); + expect(await fs.pathExists(path.join(destDir, "config.json"))).toBe(false); + }); + + it("removes stale files from previous generation", async () => { + // simulate previous generation with 3 files + await fs.writeFile( + path.join(destDir, "Sandbox.mdx"), + '---\nsidebarTitle: "Sandbox"\n---\n\n# Old content' + ); + await fs.writeFile( + path.join(destDir, "Template.mdx"), + '---\nsidebarTitle: "Template"\n---\n\n# Old content' + ); + await fs.writeFile( + path.join(destDir, "OldAPI.mdx"), + '---\nsidebarTitle: "Old API"\n---\n\n# Removed in new version' + ); + + // new generation only has 2 files (OldAPI.mdx was removed) + await fs.writeFile( + path.join(srcDir, "Sandbox.mdx"), + '---\nsidebarTitle: "Sandbox"\n---\n\n# New content' + ); + await fs.writeFile( + path.join(srcDir, "Template.mdx"), + '---\nsidebarTitle: "Template"\n---\n\n# New content' + ); + + const result = await copyToDocs(srcDir, destDir, "SDK", "v2.0.0"); + + expect(result).toBe(true); + expect(await fs.pathExists(path.join(destDir, "Sandbox.mdx"))).toBe(true); + expect(await fs.pathExists(path.join(destDir, "Template.mdx"))).toBe(true); + // stale file should be removed + expect(await fs.pathExists(path.join(destDir, "OldAPI.mdx"))).toBe(false); + + // verify content was updated (not just appended) + const sandboxContent = await fs.readFile( + path.join(destDir, "Sandbox.mdx"), + "utf-8" + ); + expect(sandboxContent).toContain("New content"); + expect(sandboxContent).not.toContain("Old content"); + }); + + it("cleans entire destination directory before copying", async () => { + // create various files and subdirectories in destination + await fs.writeFile(path.join(destDir, "File1.mdx"), "content"); + await fs.writeFile(path.join(destDir, "File2.mdx"), "content"); + await fs.writeFile(path.join(destDir, "random.txt"), "text"); + const subdir = path.join(destDir, "subdir"); + await fs.ensureDir(subdir); + await fs.writeFile(path.join(subdir, "nested.mdx"), "nested"); + + // new generation has different files + await fs.writeFile( + path.join(srcDir, "NewFile.mdx"), + '---\nsidebarTitle: "New"\n---\n\n# Content' + ); + + const result = await copyToDocs(srcDir, destDir, "SDK", "v1.0.0"); + + expect(result).toBe(true); + // only new file should exist + expect(await fs.pathExists(path.join(destDir, "NewFile.mdx"))).toBe(true); + // all old files should be gone + expect(await fs.pathExists(path.join(destDir, "File1.mdx"))).toBe(false); + expect(await fs.pathExists(path.join(destDir, "File2.mdx"))).toBe(false); + expect(await fs.pathExists(path.join(destDir, "random.txt"))).toBe(false); + expect(await fs.pathExists(subdir)).toBe(false); + }); +}); diff --git a/sdk-reference-generator/src/__tests__/files.test.ts b/sdk-reference-generator/src/__tests__/files.test.ts new file mode 100644 index 0000000..63ac8c4 --- /dev/null +++ b/sdk-reference-generator/src/__tests__/files.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from "vitest"; +import { toTitleCase, extractTitle } from "../lib/files.js"; + +describe("toTitleCase", () => { + it("converts snake_case to Title Case", () => { + expect(toTitleCase("sandbox_sync")).toBe("Sandbox Sync"); + expect(toTitleCase("sandbox_async")).toBe("Sandbox Async"); + expect(toTitleCase("template_async")).toBe("Template Async"); + }); + + it("capitalizes single words", () => { + expect(toTitleCase("sandbox")).toBe("Sandbox"); + expect(toTitleCase("exceptions")).toBe("Exceptions"); + }); + + it("handles already capitalized words", () => { + expect(toTitleCase("Sandbox")).toBe("Sandbox"); + expect(toTitleCase("SANDBOX")).toBe("SANDBOX"); + }); + + it("handles empty string", () => { + expect(toTitleCase("")).toBe(""); + }); + + it("handles multiple underscores", () => { + expect(toTitleCase("a_b_c")).toBe("A B C"); + }); +}); + +describe("extractTitle", () => { + it("strips directory prefix from flattened filenames", () => { + expect(extractTitle("modules-Sandbox")).toBe("Sandbox"); + expect(extractTitle("classes-MyClass")).toBe("MyClass"); + expect(extractTitle("interfaces-IUser")).toBe("IUser"); + expect(extractTitle("types-CustomType")).toBe("CustomType"); + }); + + it("handles nested directory prefixes", () => { + expect(extractTitle("modules-sandbox-Sandbox")).toBe("Sandbox"); + expect(extractTitle("classes-internal-MyClass")).toBe("MyClass"); + }); + + it("handles snake_case after prefix removal", () => { + expect(extractTitle("modules-sandbox_sync")).toBe("Sandbox Sync"); + expect(extractTitle("classes-my_class")).toBe("My Class"); + }); + + it("handles files without directory prefix", () => { + expect(extractTitle("Sandbox")).toBe("Sandbox"); + expect(extractTitle("sandbox_sync")).toBe("Sandbox Sync"); + }); + + it("handles empty string", () => { + expect(extractTitle("")).toBe(""); + }); + + it("handles edge cases correctly", () => { + // files with hyphenated prefixes get the prefix stripped + expect(extractTitle("some-file")).toBe("File"); + // files without hyphens are processed as-is + expect(extractTitle("MyClass")).toBe("MyClass"); + expect(extractTitle("simple")).toBe("Simple"); + }); +}); diff --git a/sdk-reference-generator/src/__tests__/generator-failures.test.ts b/sdk-reference-generator/src/__tests__/generator-failures.test.ts new file mode 100644 index 0000000..2c27d7d --- /dev/null +++ b/sdk-reference-generator/src/__tests__/generator-failures.test.ts @@ -0,0 +1,378 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import fs from "fs-extra"; +import path from "path"; +import os from "os"; +import type { SDKConfig, GenerationResult } from "../types.js"; +import { handleGenerationFailures } from "../generator.js"; +import { CONSTANTS } from "../lib/constants.js"; + +describe("handleGenerationFailures", () => { + // required SDK config + const requiredConfig: SDKConfig = { + displayName: "Required SDK", + icon: "square-js", + order: 1, + repo: "https://github.com/test/repo.git", + tagPattern: "test@", + tagFormat: "test@{version}", + generator: "typedoc", + required: true, + defaultConfig: { + entryPoints: ["src/index.ts"], + }, + }; + + // optional SDK config + const optionalConfig: SDKConfig = { + displayName: "Optional SDK", + icon: "python", + order: 2, + repo: "https://github.com/test/repo.git", + tagPattern: "@test/python@", + tagFormat: "@test/python@{version}", + generator: "typedoc", + required: false, + defaultConfig: { + entryPoints: ["src/index.ts"], + }, + }; + + describe("required SDK", () => { + it("throws error when any version fails", () => { + const result: GenerationResult = { + generated: 5, + failed: 1, + failedVersions: ["v1.0.0"], + }; + + expect(() => handleGenerationFailures(requiredConfig, result)).toThrow( + "Generation aborted: Required SDK has failures" + ); + }); + + it("throws error when all versions fail", () => { + const result: GenerationResult = { + generated: 0, + failed: 3, + failedVersions: ["v1.0.0", "v2.0.0", "v3.0.0"], + }; + + expect(() => handleGenerationFailures(requiredConfig, result)).toThrow( + "Generation aborted: Required SDK has failures" + ); + }); + + it("does not throw when no failures", () => { + const result: GenerationResult = { + generated: 3, + failed: 0, + failedVersions: [], + }; + + expect(() => + handleGenerationFailures(requiredConfig, result) + ).not.toThrow(); + }); + }); + + describe("optional SDK", () => { + it("throws error when all versions fail (generated === 0)", () => { + const result: GenerationResult = { + generated: 0, + failed: 3, + failedVersions: ["v1.0.0", "v2.0.0", "v3.0.0"], + }; + + expect(() => handleGenerationFailures(optionalConfig, result)).toThrow( + "Generation aborted: All versions failed" + ); + }); + + it("does not throw when partial success (some generated)", () => { + const result: GenerationResult = { + generated: 2, + failed: 1, + failedVersions: ["v1.0.0"], + }; + + expect(() => + handleGenerationFailures(optionalConfig, result) + ).not.toThrow(); + }); + + it("does not throw when no failures", () => { + const result: GenerationResult = { + generated: 3, + failed: 0, + failedVersions: [], + }; + + expect(() => + handleGenerationFailures(optionalConfig, result) + ).not.toThrow(); + }); + }); + + describe("edge cases", () => { + it("does not throw for zero generated and zero failed", () => { + const result: GenerationResult = { + generated: 0, + failed: 0, + failedVersions: [], + }; + + // no failures means nothing to abort + expect(() => + handleGenerationFailures(optionalConfig, result) + ).not.toThrow(); + expect(() => + handleGenerationFailures(requiredConfig, result) + ).not.toThrow(); + }); + }); +}); + +describe("version discovery integration", () => { + let tempDir: string; + + // mock the config and git modules + vi.mock("../../sdks.config.js", () => ({ + default: { + "test-sdk": { + displayName: "Test SDK", + icon: "square-js", + order: 1, + repo: "https://github.com/test/repo.git", + tagPattern: "test@", + tagFormat: "test@{version}", + generator: "typedoc", + required: true, + minVersion: "1.0.0", + defaultConfig: { + entryPoints: ["src/index.ts"], + }, + }, + "optional-sdk": { + displayName: "Optional SDK", + icon: "python", + order: 2, + repo: "https://github.com/test/optional.git", + tagPattern: "@optional@", + tagFormat: "@optional@{version}", + generator: "typedoc", + required: false, + defaultConfig: { + entryPoints: ["src/index.ts"], + }, + }, + }, + })); + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "gen-test-")); + }); + + afterEach(async () => { + await fs.remove(tempDir); + vi.restoreAllMocks(); + }); + + describe("generateSDK with unknown SDK", () => { + it("returns failure result for unknown SDK key", async () => { + const { generateSDK } = await import("../generator.js"); + + const result = await generateSDK("unknown-sdk", "v1.0.0", { + tempDir, + docsDir: tempDir, + configsDir: tempDir, + }); + + expect(result.generated).toBe(0); + expect(result.failed).toBe(1); + expect(result.failedVersions).toContain("unknown-sdk"); + }); + }); +}); + +describe("versionExists", () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "version-exists-test-")); + }); + + afterEach(async () => { + await fs.remove(tempDir); + }); + + it("returns true for existing version with mdx files", async () => { + const { versionExists } = await import("../lib/versions.js"); + + const versionDir = path.join( + tempDir, + CONSTANTS.DOCS_SDK_REF_PATH, + "test-sdk", + "v1.0.0" + ); + await fs.ensureDir(versionDir); + await fs.writeFile(path.join(versionDir, "Test.mdx"), "# Content"); + + const result = await versionExists("test-sdk", "v1.0.0", tempDir); + + expect(result).toBe(true); + }); + + it("returns true for version without v prefix", async () => { + const { versionExists } = await import("../lib/versions.js"); + + // create directory without "v" prefix + const versionDir = path.join( + tempDir, + CONSTANTS.DOCS_SDK_REF_PATH, + "test-sdk", + "1.0.0" + ); + await fs.ensureDir(versionDir); + await fs.writeFile(path.join(versionDir, "Test.mdx"), "# Content"); + + // query with "v" prefix + const result = await versionExists("test-sdk", "v1.0.0", tempDir); + + expect(result).toBe(true); + }); + + it("returns false for non-existing version", async () => { + const { versionExists } = await import("../lib/versions.js"); + + const result = await versionExists("test-sdk", "v99.0.0", tempDir); + + expect(result).toBe(false); + }); + + it("returns false for version directory without mdx files", async () => { + const { versionExists } = await import("../lib/versions.js"); + + const versionDir = path.join( + tempDir, + CONSTANTS.DOCS_SDK_REF_PATH, + "test-sdk", + "v1.0.0" + ); + await fs.ensureDir(versionDir); + // no mdx files, only other files + await fs.writeFile(path.join(versionDir, "readme.txt"), "text"); + + const result = await versionExists("test-sdk", "v1.0.0", tempDir); + + expect(result).toBe(false); + }); +}); + +describe("fetchLocalVersions", () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "local-versions-test-")); + }); + + afterEach(async () => { + await fs.remove(tempDir); + }); + + it("returns empty array when SDK directory does not exist", async () => { + const { fetchLocalVersions } = await import("../lib/versions.js"); + + const result = await fetchLocalVersions("nonexistent-sdk", tempDir); + + expect(result).toEqual([]); + }); + + it("returns versions sorted descending", async () => { + const { fetchLocalVersions } = await import("../lib/versions.js"); + + const sdkDir = path.join( + tempDir, + CONSTANTS.DOCS_SDK_REF_PATH, + "test-sdk" + ); + + // create versions in random order + for (const version of ["v1.0.0", "v2.0.0", "v1.5.0"]) { + const versionDir = path.join(sdkDir, version); + await fs.ensureDir(versionDir); + await fs.writeFile(path.join(versionDir, "Test.mdx"), "# Content"); + } + + const result = await fetchLocalVersions("test-sdk", tempDir); + + expect(result).toEqual(["v2.0.0", "v1.5.0", "v1.0.0"]); + }); + + it("normalizes versions without v prefix", async () => { + const { fetchLocalVersions } = await import("../lib/versions.js"); + + const sdkDir = path.join( + tempDir, + CONSTANTS.DOCS_SDK_REF_PATH, + "test-sdk" + ); + + // create version without "v" prefix + const versionDir = path.join(sdkDir, "1.0.0"); + await fs.ensureDir(versionDir); + await fs.writeFile(path.join(versionDir, "Test.mdx"), "# Content"); + + const result = await fetchLocalVersions("test-sdk", tempDir); + + expect(result).toEqual(["v1.0.0"]); + }); + + it("ignores non-version directories", async () => { + const { fetchLocalVersions } = await import("../lib/versions.js"); + + const sdkDir = path.join( + tempDir, + CONSTANTS.DOCS_SDK_REF_PATH, + "test-sdk" + ); + + // valid version + const validDir = path.join(sdkDir, "v1.0.0"); + await fs.ensureDir(validDir); + await fs.writeFile(path.join(validDir, "Test.mdx"), "# Content"); + + // invalid versions + await fs.ensureDir(path.join(sdkDir, "main")); + await fs.writeFile(path.join(sdkDir, "main", "Test.mdx"), "# Content"); + await fs.ensureDir(path.join(sdkDir, "latest")); + + const result = await fetchLocalVersions("test-sdk", tempDir); + + expect(result).toEqual(["v1.0.0"]); + }); + + it("ignores version directories without mdx files", async () => { + const { fetchLocalVersions } = await import("../lib/versions.js"); + + const sdkDir = path.join( + tempDir, + CONSTANTS.DOCS_SDK_REF_PATH, + "test-sdk" + ); + + // version with mdx files + const withMdx = path.join(sdkDir, "v2.0.0"); + await fs.ensureDir(withMdx); + await fs.writeFile(path.join(withMdx, "Test.mdx"), "# Content"); + + // version without mdx files + const withoutMdx = path.join(sdkDir, "v1.0.0"); + await fs.ensureDir(withoutMdx); + await fs.writeFile(path.join(withoutMdx, "readme.txt"), "text"); + + const result = await fetchLocalVersions("test-sdk", tempDir); + + expect(result).toEqual(["v2.0.0"]); + }); +}); + diff --git a/sdk-reference-generator/src/__tests__/git.test.ts b/sdk-reference-generator/src/__tests__/git.test.ts new file mode 100644 index 0000000..3d585d9 --- /dev/null +++ b/sdk-reference-generator/src/__tests__/git.test.ts @@ -0,0 +1,143 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const mockClone = vi.fn(); +const mockListRemote = vi.fn(); + +// mock simple-git before importing our module +vi.mock('simple-git', () => ({ + simpleGit: vi.fn(() => ({ + clone: mockClone, + listRemote: mockListRemote, + })), +})); + +// mock timers to avoid actual delays in tests +vi.useFakeTimers(); + +// now import our module after mocking +const { cloneAtTag } = await import('../lib/git.js'); + +describe('cloneAtTag', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockClone.mockReset(); + mockListRemote.mockReset(); + }); + + it('succeeds on first attempt', async () => { + mockClone.mockResolvedValueOnce(undefined as any); + + await expect( + cloneAtTag('https://github.com/test/repo', 'v1.0.0', '/tmp/test') + ).resolves.toBeUndefined(); + + expect(mockClone).toHaveBeenCalledTimes(1); + expect(mockClone).toHaveBeenCalledWith( + 'https://github.com/test/repo', + '/tmp/test', + ['--depth', '1', '--branch', 'v1.0.0'] + ); + }); + + it('retries 3 times for tag-not-found errors', async () => { + const tagNotFoundError = new Error( + "fatal: Remote branch v1.0.0 not found in upstream origin" + ); + + mockClone + .mockRejectedValueOnce(tagNotFoundError) + .mockRejectedValueOnce(tagNotFoundError) + .mockRejectedValueOnce(tagNotFoundError); + + const promise = expect( + cloneAtTag('https://github.com/test/repo', 'v1.0.0', '/tmp/test') + ).rejects.toThrow(/Tag v1\.0\.0 not found in repository.*after 3 attempts/); + + // advance timers for all retry delays + await vi.runAllTimersAsync(); + await promise; + + expect(mockClone).toHaveBeenCalledTimes(3); + }); + + it('succeeds on second attempt after tag-not-found retry', async () => { + const tagNotFoundError = new Error( + "fatal: Remote branch v1.0.0 not found in upstream origin" + ); + + mockClone + .mockRejectedValueOnce(tagNotFoundError) + .mockResolvedValueOnce(undefined as any); + + const promise = expect( + cloneAtTag('https://github.com/test/repo', 'v1.0.0', '/tmp/test') + ).resolves.toBeUndefined(); + + // advance timers for retry delay + await vi.runAllTimersAsync(); + await promise; + + expect(mockClone).toHaveBeenCalledTimes(2); + }); + + it('throws immediately for network errors without retrying', async () => { + const networkError = new Error( + "fatal: unable to access 'https://github.com/test/repo': Could not resolve host" + ); + + mockClone.mockRejectedValueOnce(networkError); + + await expect( + cloneAtTag('https://github.com/test/repo', 'v1.0.0', '/tmp/test') + ).rejects.toThrow(/Failed to clone repository.*network, authentication, or system error/); + + // should NOT retry for network errors + expect(mockClone).toHaveBeenCalledTimes(1); + }); + + it('throws immediately for authentication errors without retrying', async () => { + const authError = new Error( + "fatal: Authentication failed for 'https://github.com/test/repo'" + ); + + mockClone.mockRejectedValueOnce(authError); + + await expect( + cloneAtTag('https://github.com/test/repo', 'v1.0.0', '/tmp/test') + ).rejects.toThrow(/Failed to clone repository.*network, authentication, or system error/); + + expect(mockClone).toHaveBeenCalledTimes(1); + }); + + it('recognizes various tag-not-found error formats', async () => { + const errorFormats = [ + "fatal: Remote branch v1.0.0 not found in upstream origin", + "fatal: couldn't find remote ref v1.0.0", + "error: invalid refspec 'v1.0.0'", + "fatal: reference is not a tree: v1.0.0", + ]; + + for (const errorMessage of errorFormats) { + vi.clearAllMocks(); + mockClone.mockReset(); + const error = new Error(errorMessage); + + mockClone + .mockRejectedValueOnce(error) + .mockRejectedValueOnce(error) + .mockRejectedValueOnce(error); + + const promise = expect( + cloneAtTag('https://github.com/test/repo', 'v1.0.0', '/tmp/test') + ).rejects.toThrow(/Tag v1\.0\.0 not found/); + + // advance timers for all retry delays + await vi.runAllTimersAsync(); + await promise; + + // should retry for all tag-not-found formats + expect(mockClone).toHaveBeenCalledTimes(3); + } + }); +}); + diff --git a/sdk-reference-generator/src/__tests__/navigation.test.ts b/sdk-reference-generator/src/__tests__/navigation.test.ts new file mode 100644 index 0000000..9a2a58d --- /dev/null +++ b/sdk-reference-generator/src/__tests__/navigation.test.ts @@ -0,0 +1,413 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import fs from "fs-extra"; +import path from "path"; +import os from "os"; +import { CONSTANTS } from "../lib/constants.js"; + +// mock the sdks config before importing navigation +vi.mock("../../sdks.config.js", () => ({ + default: { + "test-js-sdk": { + displayName: "Test SDK (JavaScript)", + icon: "square-js", + order: 1, + repo: "https://github.com/test/repo.git", + tagPattern: "test@", + tagFormat: "test@{version}", + generator: "typedoc", + required: true, + }, + "test-py-sdk": { + displayName: "Test SDK (Python)", + icon: "python", + order: 2, + repo: "https://github.com/test/repo.git", + tagPattern: "@test/python@", + tagFormat: "@test/python@{version}", + generator: "pydoc", + required: false, + }, + }, +})); + +// import after mocking +const { buildNavigation, mergeNavigation } = await import("../navigation.js"); + +describe("buildNavigation", () => { + let tempDir: string; + let sdkRefDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "nav-test-")); + sdkRefDir = path.join(tempDir, CONSTANTS.DOCS_SDK_REF_PATH); + }); + + afterEach(async () => { + await fs.remove(tempDir); + }); + + it("returns empty array when sdk-reference directory does not exist", async () => { + const result = await buildNavigation(tempDir); + + expect(result).toEqual([]); + }); + + it("skips SDKs that have no directory", async () => { + await fs.ensureDir(sdkRefDir); + // don't create any SDK directories + + const result = await buildNavigation(tempDir); + + expect(result).toEqual([]); + }); + + it("skips SDKs with no valid versions", async () => { + const sdkDir = path.join(sdkRefDir, "test-js-sdk"); + await fs.ensureDir(sdkDir); + // create invalid version directories + await fs.ensureDir(path.join(sdkDir, "main")); + await fs.ensureDir(path.join(sdkDir, "latest")); + + const result = await buildNavigation(tempDir); + + expect(result).toEqual([]); + }); + + it("builds navigation for SDKs with valid versions", async () => { + const sdkDir = path.join(sdkRefDir, "test-js-sdk"); + const versionDir = path.join(sdkDir, "v1.0.0"); + await fs.ensureDir(versionDir); + await fs.writeFile( + path.join(versionDir, "Sandbox.mdx"), + '---\nsidebarTitle: "Sandbox"\n---\n\n# Content' + ); + + const result = await buildNavigation(tempDir); + + expect(result).toHaveLength(1); + expect(result[0].dropdown).toBe("Test SDK (JavaScript)"); + expect(result[0].icon).toBe("square-js"); + expect(result[0].versions).toHaveLength(1); + expect(result[0].versions[0].version).toBe("v1.0.0"); + expect(result[0].versions[0].default).toBe(true); + expect(result[0].versions[0].pages).toContain( + "docs/sdk-reference/test-js-sdk/v1.0.0/Sandbox" + ); + }); + + it("sorts versions descending (latest first)", async () => { + const sdkDir = path.join(sdkRefDir, "test-js-sdk"); + + // create multiple versions + for (const version of ["v1.0.0", "v2.0.0", "v1.5.0"]) { + const versionDir = path.join(sdkDir, version); + await fs.ensureDir(versionDir); + await fs.writeFile( + path.join(versionDir, "Test.mdx"), + '---\nsidebarTitle: "Test"\n---\n\n# Content' + ); + } + + const result = await buildNavigation(tempDir); + + expect(result[0].versions[0].version).toBe("v2.0.0"); + expect(result[0].versions[1].version).toBe("v1.5.0"); + expect(result[0].versions[2].version).toBe("v1.0.0"); + }); + + it("marks first version as default", async () => { + const sdkDir = path.join(sdkRefDir, "test-js-sdk"); + + for (const version of ["v1.0.0", "v2.0.0"]) { + const versionDir = path.join(sdkDir, version); + await fs.ensureDir(versionDir); + await fs.writeFile(path.join(versionDir, "Test.mdx"), "# Content"); + } + + const result = await buildNavigation(tempDir); + + expect(result[0].versions[0].default).toBe(true); + expect(result[0].versions[1].default).toBe(false); + }); + + it("normalizes versions without v prefix", async () => { + const sdkDir = path.join(sdkRefDir, "test-js-sdk"); + const versionDir = path.join(sdkDir, "1.0.0"); // no "v" prefix + await fs.ensureDir(versionDir); + await fs.writeFile(path.join(versionDir, "Test.mdx"), "# Content"); + + const result = await buildNavigation(tempDir); + + expect(result[0].versions[0].version).toBe("v1.0.0"); + }); + + it("sorts SDKs by order from config", async () => { + // create both SDKs + for (const sdk of ["test-js-sdk", "test-py-sdk"]) { + const versionDir = path.join(sdkRefDir, sdk, "v1.0.0"); + await fs.ensureDir(versionDir); + await fs.writeFile(path.join(versionDir, "Test.mdx"), "# Content"); + } + + const result = await buildNavigation(tempDir); + + expect(result).toHaveLength(2); + expect(result[0].dropdown).toBe("Test SDK (JavaScript)"); // order: 1 + expect(result[1].dropdown).toBe("Test SDK (Python)"); // order: 2 + }); + + it("builds correct page paths with multiple modules", async () => { + const versionDir = path.join(sdkRefDir, "test-js-sdk", "v1.0.0"); + await fs.ensureDir(versionDir); + await fs.writeFile(path.join(versionDir, "Sandbox.mdx"), "# Sandbox"); + await fs.writeFile(path.join(versionDir, "Filesystem.mdx"), "# Filesystem"); + await fs.writeFile(path.join(versionDir, "Commands.mdx"), "# Commands"); + + const result = await buildNavigation(tempDir); + + const pages = result[0].versions[0].pages; + expect(pages).toHaveLength(3); + // pages should be sorted alphabetically + expect(pages[0]).toBe("docs/sdk-reference/test-js-sdk/v1.0.0/Commands"); + expect(pages[1]).toBe("docs/sdk-reference/test-js-sdk/v1.0.0/Filesystem"); + expect(pages[2]).toBe("docs/sdk-reference/test-js-sdk/v1.0.0/Sandbox"); + }); + + it("ignores non-mdx files in version directories", async () => { + const versionDir = path.join(sdkRefDir, "test-js-sdk", "v1.0.0"); + await fs.ensureDir(versionDir); + await fs.writeFile(path.join(versionDir, "Valid.mdx"), "# Valid"); + await fs.writeFile(path.join(versionDir, "readme.md"), "# Readme"); + await fs.writeFile(path.join(versionDir, "config.json"), "{}"); + + const result = await buildNavigation(tempDir); + + expect(result[0].versions[0].pages).toHaveLength(1); + expect(result[0].versions[0].pages[0]).toContain("Valid"); + }); + + it("filters out invalid version directories", async () => { + const sdkDir = path.join(sdkRefDir, "test-js-sdk"); + + // valid versions + await fs.ensureDir(path.join(sdkDir, "v1.0.0")); + await fs.writeFile(path.join(sdkDir, "v1.0.0", "Test.mdx"), "# Content"); + + // invalid versions + await fs.ensureDir(path.join(sdkDir, "main")); + await fs.writeFile(path.join(sdkDir, "main", "Test.mdx"), "# Content"); + await fs.ensureDir(path.join(sdkDir, "latest")); + await fs.writeFile(path.join(sdkDir, "latest", "Test.mdx"), "# Content"); + + const result = await buildNavigation(tempDir); + + expect(result[0].versions).toHaveLength(1); + expect(result[0].versions[0].version).toBe("v1.0.0"); + }); +}); + +describe("mergeNavigation", () => { + let tempDir: string; + let docsJsonPath: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "merge-nav-test-")); + docsJsonPath = path.join(tempDir, "docs.json"); + }); + + afterEach(async () => { + await fs.remove(tempDir); + }); + + it("throws when docs.json does not exist", async () => { + const navigation = [ + { + dropdown: "Test SDK", + icon: "square-js", + versions: [{ version: "v1.0.0", default: true, pages: ["test"] }], + }, + ]; + + await expect(mergeNavigation(navigation, tempDir)).rejects.toThrow( + "docs.json not found" + ); + }); + + it("throws when docs.json has no anchors", async () => { + await fs.writeJSON(docsJsonPath, { navigation: {} }); + + const navigation = [ + { + dropdown: "Test SDK", + icon: "square-js", + versions: [{ version: "v1.0.0", default: true, pages: ["test"] }], + }, + ]; + + await expect(mergeNavigation(navigation, tempDir)).rejects.toThrow( + "No anchors found" + ); + }); + + it("creates new SDK Reference anchor when missing", async () => { + await fs.writeJSON(docsJsonPath, { + navigation: { + anchors: [{ anchor: "Documentation", groups: [] }], + }, + }); + + const navigation = [ + { + dropdown: "Test SDK", + icon: "square-js", + versions: [{ version: "v1.0.0", default: true, pages: ["test/page"] }], + }, + ]; + + await mergeNavigation(navigation, tempDir); + + const result = await fs.readJSON(docsJsonPath); + expect(result.navigation.anchors).toHaveLength(2); + expect(result.navigation.anchors[1].anchor).toBe("SDK Reference"); + expect(result.navigation.anchors[1].dropdowns).toHaveLength(1); + }); + + it("updates existing SDK Reference anchor", async () => { + await fs.writeJSON(docsJsonPath, { + navigation: { + anchors: [ + { anchor: "Documentation", groups: [] }, + { anchor: "SDK Reference", icon: "brackets-curly", dropdowns: [] }, + ], + }, + }); + + const navigation = [ + { + dropdown: "Test SDK", + icon: "square-js", + versions: [{ version: "v1.0.0", default: true, pages: ["test/page"] }], + }, + ]; + + await mergeNavigation(navigation, tempDir); + + const result = await fs.readJSON(docsJsonPath); + expect(result.navigation.anchors).toHaveLength(2); + expect(result.navigation.anchors[1].dropdowns).toHaveLength(1); + expect(result.navigation.anchors[1].dropdowns[0].dropdown).toBe("Test SDK"); + }); + + it("preserves other anchors in docs.json", async () => { + await fs.writeJSON(docsJsonPath, { + navigation: { + anchors: [ + { anchor: "Documentation", groups: ["group1"] }, + { anchor: "API Reference", groups: ["group2"] }, + ], + }, + }); + + const navigation = [ + { + dropdown: "Test SDK", + icon: "square-js", + versions: [{ version: "v1.0.0", default: true, pages: ["test/page"] }], + }, + ]; + + await mergeNavigation(navigation, tempDir); + + const result = await fs.readJSON(docsJsonPath); + expect(result.navigation.anchors).toHaveLength(3); + expect(result.navigation.anchors[0].anchor).toBe("Documentation"); + expect(result.navigation.anchors[0].groups).toEqual(["group1"]); + expect(result.navigation.anchors[1].anchor).toBe("API Reference"); + }); + + it("filters out SDKs with empty versions", async () => { + await fs.writeJSON(docsJsonPath, { + navigation: { anchors: [] }, + }); + + const navigation = [ + { + dropdown: "Valid SDK", + icon: "square-js", + versions: [{ version: "v1.0.0", default: true, pages: ["page"] }], + }, + { + dropdown: "Empty SDK", + icon: "python", + versions: [], + }, + ]; + + await mergeNavigation(navigation, tempDir); + + const result = await fs.readJSON(docsJsonPath); + const sdkRefAnchor = result.navigation.anchors.find( + (a: { anchor: string }) => a.anchor === "SDK Reference" + ); + expect(sdkRefAnchor.dropdowns).toHaveLength(1); + expect(sdkRefAnchor.dropdowns[0].dropdown).toBe("Valid SDK"); + }); + + it("does not modify docs.json when no valid SDK versions exist", async () => { + const originalContent = { + navigation: { + anchors: [{ anchor: "Documentation", groups: [] }], + }, + }; + await fs.writeJSON(docsJsonPath, originalContent); + + const navigation = [ + { dropdown: "Empty SDK", icon: "square-js", versions: [] }, + ]; + + await mergeNavigation(navigation, tempDir); + + const result = await fs.readJSON(docsJsonPath); + expect(result).toEqual(originalContent); + }); + + it("writes JSON with proper formatting (2 spaces)", async () => { + await fs.writeJSON(docsJsonPath, { + navigation: { anchors: [] }, + }); + + const navigation = [ + { + dropdown: "Test SDK", + icon: "square-js", + versions: [{ version: "v1.0.0", default: true, pages: ["page"] }], + }, + ]; + + await mergeNavigation(navigation, tempDir); + + const content = await fs.readFile(docsJsonPath, "utf-8"); + // check for 2-space indentation + expect(content).toContain(' "navigation"'); + }); + + it("ensures newline at end of file", async () => { + await fs.writeJSON(docsJsonPath, { + navigation: { anchors: [] }, + }); + + const navigation = [ + { + dropdown: "Test SDK", + icon: "square-js", + versions: [{ version: "v1.0.0", default: true, pages: ["page"] }], + }, + ]; + + await mergeNavigation(navigation, tempDir); + + const content = await fs.readFile(docsJsonPath, "utf-8"); + expect(content.endsWith("\n")).toBe(true); + }); +}); + diff --git a/sdk-reference-generator/src/__tests__/sdks-config.test.ts b/sdk-reference-generator/src/__tests__/sdks-config.test.ts new file mode 100644 index 0000000..ec11357 --- /dev/null +++ b/sdk-reference-generator/src/__tests__/sdks-config.test.ts @@ -0,0 +1,138 @@ +import { describe, it, expect } from "vitest"; +import sdks from "../../sdks.config.js"; +import { resolveConfig } from "../lib/config.js"; +import type { TypedocSDKConfig, PydocSDKConfig } from "../types.js"; + +describe("sdks.config - js-sdk version overrides", () => { + const jsSdkConfig = sdks["js-sdk"] as TypedocSDKConfig; + + it("should use pty.ts for v1.0.0", () => { + const resolved = resolveConfig( + jsSdkConfig.defaultConfig, + jsSdkConfig.configOverrides, + "v1.0.0" + ); + + expect(resolved.entryPoints).toContain("src/sandbox/pty.ts"); + expect(resolved.entryPoints).not.toContain("src/sandbox/commands/index.ts"); + expect(resolved.entryPoints).not.toContain("src/template/index.ts"); + }); + + it("should use commands.ts without template for v1.1.0 - v2.2.x", () => { + const versions = ["v1.1.0", "v1.5.0", "v2.0.0", "v2.2.10"]; + + for (const version of versions) { + const resolved = resolveConfig( + jsSdkConfig.defaultConfig, + jsSdkConfig.configOverrides, + version + ); + + expect(resolved.entryPoints).toContain("src/sandbox/commands/index.ts"); + expect(resolved.entryPoints).not.toContain("src/sandbox/pty.ts"); + expect(resolved.entryPoints).not.toContain("src/template/index.ts"); + } + }); + + it("should include template modules for v2.3.0+", () => { + const versions = ["v2.3.0", "v2.5.0", "v2.10.1"]; + + for (const version of versions) { + const resolved = resolveConfig( + jsSdkConfig.defaultConfig, + jsSdkConfig.configOverrides, + version + ); + + expect(resolved.entryPoints).toContain("src/sandbox/commands/index.ts"); + expect(resolved.entryPoints).toContain("src/template/index.ts"); + expect(resolved.entryPoints).toContain("src/template/readycmd.ts"); + expect(resolved.entryPoints).toContain("src/template/logger.ts"); + expect(resolved.entryPoints).not.toContain("src/sandbox/pty.ts"); + } + }); + + it("should have correct entry point counts per version range", () => { + const v1_0_0 = resolveConfig( + jsSdkConfig.defaultConfig, + jsSdkConfig.configOverrides, + "v1.0.0" + ); + expect(v1_0_0.entryPoints).toHaveLength(5); // sandbox, filesystem, process, pty, errors + + const v1_5_0 = resolveConfig( + jsSdkConfig.defaultConfig, + jsSdkConfig.configOverrides, + "v1.5.0" + ); + expect(v1_5_0.entryPoints).toHaveLength(5); // sandbox, filesystem, process, commands, errors + + const v2_10_1 = resolveConfig( + jsSdkConfig.defaultConfig, + jsSdkConfig.configOverrides, + "v2.10.1" + ); + expect(v2_10_1.entryPoints).toHaveLength(8); // + 3 template modules + }); +}); + +describe("sdks.config - python-sdk version overrides", () => { + const pythonSdkConfig = sdks["python-sdk"] as PydocSDKConfig; + + it("should only include basic sandbox packages for v1.0.0 - v2.0.x", () => { + const versions = ["v1.0.0", "v1.5.0", "v2.0.0", "v2.0.3"]; + + for (const version of versions) { + const resolved = resolveConfig( + pythonSdkConfig.defaultConfig, + pythonSdkConfig.configOverrides, + version + ); + + expect(resolved.allowedPackages).toContain("e2b.sandbox_sync"); + expect(resolved.allowedPackages).toContain("e2b.sandbox_async"); + expect(resolved.allowedPackages).toContain("e2b.exceptions"); + expect(resolved.allowedPackages).not.toContain("e2b.template"); + expect(resolved.allowedPackages).not.toContain("e2b.template_sync"); + expect(resolved.allowedPackages).toHaveLength(3); + } + }); + + it("should include template packages for v2.1.0+", () => { + const versions = ["v2.1.0", "v2.3.0", "v2.5.0", "v2.10.1"]; + + for (const version of versions) { + const resolved = resolveConfig( + pythonSdkConfig.defaultConfig, + pythonSdkConfig.configOverrides, + version + ); + + expect(resolved.allowedPackages).toContain("e2b.sandbox_sync"); + expect(resolved.allowedPackages).toContain("e2b.sandbox_async"); + expect(resolved.allowedPackages).toContain("e2b.exceptions"); + expect(resolved.allowedPackages).toContain("e2b.template"); + expect(resolved.allowedPackages).toContain("e2b.template_sync"); + expect(resolved.allowedPackages).toContain("e2b.template_async"); + expect(resolved.allowedPackages).toContain("e2b.template.logger"); + expect(resolved.allowedPackages).toContain("e2b.template.readycmd"); + expect(resolved.allowedPackages).toHaveLength(8); + } + }); + + it("should have correct package counts per version range", () => { + const v1_5_0 = resolveConfig( + pythonSdkConfig.defaultConfig, + pythonSdkConfig.configOverrides, + "v1.5.0" + ); + expect(v1_5_0.allowedPackages).toHaveLength(3); // basic sandbox packages only + + const v2_10_1 = resolveConfig( + pythonSdkConfig.defaultConfig, + pythonSdkConfig.configOverrides, + "v2.10.1" + ); + expect(v2_10_1.allowedPackages).toHaveLength(8); // + 5 template packages + }); +}); diff --git a/sdk-reference-generator/src/__tests__/utils.test.ts b/sdk-reference-generator/src/__tests__/utils.test.ts new file mode 100644 index 0000000..8b91d1d --- /dev/null +++ b/sdk-reference-generator/src/__tests__/utils.test.ts @@ -0,0 +1,189 @@ +import { describe, it, expect } from "vitest"; +import path from "path"; +import { + normalizeVersion, + stripVersionPrefix, + isValidVersion, + sortVersionsDescending, + createFrontmatter, + buildSDKPath, +} from "../lib/utils.js"; +import { CONSTANTS } from "../lib/constants.js"; + +describe("normalizeVersion", () => { + it("adds v prefix when missing", () => { + expect(normalizeVersion("1.0.0")).toBe("v1.0.0"); + expect(normalizeVersion("2.5.3")).toBe("v2.5.3"); + expect(normalizeVersion("10.20.30")).toBe("v10.20.30"); + }); + + it("keeps v prefix when already present", () => { + expect(normalizeVersion("v1.0.0")).toBe("v1.0.0"); + expect(normalizeVersion("v2.5.3")).toBe("v2.5.3"); + }); + + it("handles prerelease versions", () => { + expect(normalizeVersion("1.0.0-beta.1")).toBe("v1.0.0-beta.1"); + expect(normalizeVersion("v1.0.0-rc.2")).toBe("v1.0.0-rc.2"); + }); +}); + +describe("stripVersionPrefix", () => { + it("removes v prefix when present", () => { + expect(stripVersionPrefix("v1.0.0")).toBe("1.0.0"); + expect(stripVersionPrefix("v2.5.3")).toBe("2.5.3"); + }); + + it("keeps version unchanged when no v prefix", () => { + expect(stripVersionPrefix("1.0.0")).toBe("1.0.0"); + expect(stripVersionPrefix("10.20.30")).toBe("10.20.30"); + }); + + it("only removes leading v", () => { + expect(stripVersionPrefix("vvv1.0.0")).toBe("vv1.0.0"); + expect(stripVersionPrefix("1.0.0-v2")).toBe("1.0.0-v2"); + }); +}); + +describe("isValidVersion", () => { + it("accepts valid semver versions", () => { + expect(isValidVersion("1.0.0")).toBe(true); + expect(isValidVersion("v1.0.0")).toBe(true); + expect(isValidVersion("v2.9.0")).toBe(true); + expect(isValidVersion("10.20.30")).toBe(true); + expect(isValidVersion("v0.0.1")).toBe(true); + }); + + it("accepts versions with prerelease tags", () => { + expect(isValidVersion("1.0.0-beta")).toBe(true); + expect(isValidVersion("v2.0.0-rc.1")).toBe(true); + expect(isValidVersion("1.0.0-alpha.2.3")).toBe(true); + }); + + it("rejects non-semver strings", () => { + expect(isValidVersion("main")).toBe(false); + expect(isValidVersion("latest")).toBe(false); + expect(isValidVersion("develop")).toBe(false); + expect(isValidVersion("")).toBe(false); + }); + + it("rejects partial versions", () => { + expect(isValidVersion("1.0")).toBe(false); + expect(isValidVersion("1")).toBe(false); + expect(isValidVersion("v1")).toBe(false); + }); +}); + +describe("sortVersionsDescending", () => { + it("sorts versions from newest to oldest", () => { + const versions = ["v1.0.0", "v2.0.0", "v1.5.0"]; + const result = sortVersionsDescending(versions); + expect(result).toEqual(["v2.0.0", "v1.5.0", "v1.0.0"]); + }); + + it("handles versions without v prefix", () => { + const versions = ["1.0.0", "2.0.0", "1.5.0"]; + const result = sortVersionsDescending(versions); + expect(result).toEqual(["2.0.0", "1.5.0", "1.0.0"]); + }); + + it("handles mixed prefix versions", () => { + const versions = ["v1.0.0", "2.0.0", "v1.5.0"]; + const result = sortVersionsDescending(versions); + expect(result).toEqual(["2.0.0", "v1.5.0", "v1.0.0"]); + }); + + it("correctly sorts double-digit versions", () => { + const versions = ["v1.9.0", "v1.10.0", "v1.2.0"]; + const result = sortVersionsDescending(versions); + // semver sorts 1.10.0 > 1.9.0 > 1.2.0 + expect(result).toEqual(["v1.10.0", "v1.9.0", "v1.2.0"]); + }); + + it("handles empty array", () => { + expect(sortVersionsDescending([])).toEqual([]); + }); + + it("handles single version", () => { + expect(sortVersionsDescending(["v1.0.0"])).toEqual(["v1.0.0"]); + }); + + it("sorts prerelease versions correctly", () => { + const versions = ["v1.0.0", "v1.0.0-beta.1", "v1.0.0-alpha"]; + const result = sortVersionsDescending(versions); + // stable > beta > alpha + expect(result).toEqual(["v1.0.0", "v1.0.0-beta.1", "v1.0.0-alpha"]); + }); + + it("uses string comparison fallback for invalid semver", () => { + const versions = ["invalid", "also-invalid", "z-last"]; + const result = sortVersionsDescending(versions); + // lexicographic descending + expect(result).toEqual(["z-last", "invalid", "also-invalid"]); + }); +}); + +describe("createFrontmatter", () => { + it("creates frontmatter with title", () => { + const result = createFrontmatter("My Title"); + expect(result).toBe(`--- +sidebarTitle: "My Title" +--- + +`); + }); + + it("handles empty title", () => { + const result = createFrontmatter(""); + expect(result).toBe(`--- +sidebarTitle: "" +--- + +`); + }); + + it("handles titles with special characters", () => { + const result = createFrontmatter('Title "with" quotes'); + // the function doesn't escape - this is fine for most use cases + expect(result).toContain('sidebarTitle: "Title "with" quotes"'); + }); + + it("includes trailing newlines for content concatenation", () => { + const result = createFrontmatter("Test"); + // should end with double newline for clean content concatenation + expect(result.endsWith("\n\n")).toBe(true); + }); +}); + +describe("buildSDKPath", () => { + it("builds correct path with all components", () => { + const result = buildSDKPath("/docs", "js-sdk", "v1.0.0"); + expect(result).toBe( + path.join("/docs", CONSTANTS.DOCS_SDK_REF_PATH, "js-sdk", "v1.0.0") + ); + }); + + it("uses DOCS_SDK_REF_PATH constant", () => { + const result = buildSDKPath("/root", "test", "v2.0.0"); + expect(result).toContain("docs/sdk-reference"); + expect(result).toContain("test"); + expect(result).toContain("v2.0.0"); + }); + + it("handles nested docsDir paths", () => { + const result = buildSDKPath("/home/user/project/docs", "cli", "v3.0.0"); + expect(result).toBe( + path.join( + "/home/user/project/docs", + CONSTANTS.DOCS_SDK_REF_PATH, + "cli", + "v3.0.0" + ) + ); + }); + + it("handles SDK keys with hyphens", () => { + const result = buildSDKPath("/docs", "code-interpreter-js-sdk", "v1.0.0"); + expect(result).toContain("code-interpreter-js-sdk"); + }); +}); diff --git a/sdk-reference-generator/src/__tests__/verify.test.ts b/sdk-reference-generator/src/__tests__/verify.test.ts new file mode 100644 index 0000000..c31ea49 --- /dev/null +++ b/sdk-reference-generator/src/__tests__/verify.test.ts @@ -0,0 +1,158 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import fs from 'fs-extra'; +import path from 'path'; +import os from 'os'; +import { verifyGeneratedDocs } from '../lib/verify.js'; +import { CONSTANTS } from '../lib/constants.js'; + +describe('verifyGeneratedDocs', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'verify-test-')); + }); + + afterEach(async () => { + await fs.remove(tempDir); + }); + + it('validates correct SDK structure', async () => { + const sdkPath = path.join(tempDir, CONSTANTS.DOCS_SDK_REF_PATH, 'test-sdk', 'v1.0.0'); + await fs.ensureDir(sdkPath); + await fs.writeFile( + path.join(sdkPath, 'test.mdx'), + '---\nsidebarTitle: "Test"\n---\n\nContent' + ); + + // create valid docs.json + const docsJson = { + navigation: { + anchors: [ + { + anchor: CONSTANTS.SDK_REFERENCE_ANCHOR, + icon: 'brackets-curly', + dropdowns: [], + }, + ], + }, + }; + await fs.writeJSON(path.join(tempDir, 'docs.json'), docsJson); + + const result = await verifyGeneratedDocs(tempDir); + + expect(result.valid).toBe(true); + expect(result.docsJsonValid).toBe(true); + expect(result.errors).toHaveLength(0); + expect(result.stats.totalMdxFiles).toBe(1); + expect(result.stats.totalSDKs).toBe(1); + expect(result.stats.totalVersions).toBe(1); + }); + + it('detects empty MDX files', async () => { + const sdkPath = path.join(tempDir, CONSTANTS.DOCS_SDK_REF_PATH, 'test-sdk', 'v1.0.0'); + await fs.ensureDir(sdkPath); + await fs.writeFile(path.join(sdkPath, 'empty.mdx'), ''); + + const result = await verifyGeneratedDocs(tempDir); + + expect(result.valid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + expect(result.errors[0]).toContain('Empty file'); + }); + + it('detects missing frontmatter', async () => { + const sdkPath = path.join(tempDir, CONSTANTS.DOCS_SDK_REF_PATH, 'test-sdk', 'v1.0.0'); + await fs.ensureDir(sdkPath); + await fs.writeFile(path.join(sdkPath, 'no-frontmatter.mdx'), 'Just content'); + + const result = await verifyGeneratedDocs(tempDir); + + expect(result.valid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + expect(result.errors[0]).toContain('Missing frontmatter'); + }); + + it('warns about versions with no MDX files', async () => { + const sdkPath = path.join(tempDir, CONSTANTS.DOCS_SDK_REF_PATH, 'test-sdk', 'v1.0.0'); + await fs.ensureDir(sdkPath); + + const result = await verifyGeneratedDocs(tempDir); + + expect(result.warnings.length).toBeGreaterThan(0); + expect(result.warnings[0]).toContain('has no MDX files'); + }); +}); + +describe('verifyDocsJson via verifyGeneratedDocs', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'verify-test-')); + }); + + afterEach(async () => { + await fs.remove(tempDir); + }); + + it('validates correct docs.json structure', async () => { + const sdkPath = path.join(tempDir, CONSTANTS.DOCS_SDK_REF_PATH, 'test-sdk', 'v1.0.0'); + await fs.ensureDir(sdkPath); + await fs.writeFile( + path.join(sdkPath, 'test.mdx'), + '---\nsidebarTitle: "Test"\n---\n\nContent' + ); + + const docsJson = { + navigation: { + anchors: [ + { + anchor: CONSTANTS.SDK_REFERENCE_ANCHOR, + icon: 'brackets-curly', + dropdowns: [], + }, + ], + }, + }; + + await fs.writeJSON(path.join(tempDir, 'docs.json'), docsJson); + + const result = await verifyGeneratedDocs(tempDir); + expect(result.docsJsonValid).toBe(true); + expect(result.valid).toBe(true); + }); + + it('fails when docs.json missing', async () => { + const sdkPath = path.join(tempDir, CONSTANTS.DOCS_SDK_REF_PATH, 'test-sdk', 'v1.0.0'); + await fs.ensureDir(sdkPath); + await fs.writeFile( + path.join(sdkPath, 'test.mdx'), + '---\nsidebarTitle: "Test"\n---\n\nContent' + ); + + const result = await verifyGeneratedDocs(tempDir); + expect(result.docsJsonValid).toBe(false); + expect(result.valid).toBe(false); + }); + + it('fails when SDK Reference anchor missing', async () => { + const sdkPath = path.join(tempDir, CONSTANTS.DOCS_SDK_REF_PATH, 'test-sdk', 'v1.0.0'); + await fs.ensureDir(sdkPath); + await fs.writeFile( + path.join(sdkPath, 'test.mdx'), + '---\nsidebarTitle: "Test"\n---\n\nContent' + ); + + const docsJson = { + navigation: { + anchors: [{ anchor: 'Other', dropdowns: [] }], + }, + }; + + await fs.writeJSON(path.join(tempDir, 'docs.json'), docsJson); + + const result = await verifyGeneratedDocs(tempDir); + expect(result.docsJsonValid).toBe(false); + expect(result.valid).toBe(false); + }); +}); + diff --git a/sdk-reference-generator/src/__tests__/versions.test.ts b/sdk-reference-generator/src/__tests__/versions.test.ts new file mode 100644 index 0000000..6a928a2 --- /dev/null +++ b/sdk-reference-generator/src/__tests__/versions.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from "vitest"; +import { + isValidVersion, + versionGte, + filterByMinVersion, + diffVersions, +} from "../lib/versions.js"; + +describe("isValidVersion", () => { + it("accepts valid semver versions", () => { + expect(isValidVersion("1.0.0")).toBe(true); + expect(isValidVersion("v1.0.0")).toBe(true); + expect(isValidVersion("v2.9.0")).toBe(true); + expect(isValidVersion("10.20.30")).toBe(true); + }); + + it("rejects invalid versions", () => { + expect(isValidVersion("main")).toBe(false); + expect(isValidVersion("latest")).toBe(false); + expect(isValidVersion("1.0")).toBe(false); + expect(isValidVersion("")).toBe(false); + }); +}); + +describe("versionGte", () => { + it("compares versions correctly", () => { + expect(versionGte("2.0.0", "1.0.0")).toBe(true); + expect(versionGte("1.0.0", "1.0.0")).toBe(true); + expect(versionGte("1.0.0", "2.0.0")).toBe(false); + }); + + it("handles v prefix", () => { + expect(versionGte("v2.0.0", "1.0.0")).toBe(true); + expect(versionGte("2.0.0", "v1.0.0")).toBe(true); + expect(versionGte("v2.0.0", "v1.0.0")).toBe(true); + }); +}); + +describe("filterByMinVersion", () => { + it("filters versions below minimum", () => { + const versions = ["v0.9.0", "v1.0.0", "v1.5.0", "v2.0.0"]; + const result = filterByMinVersion(versions, "1.0.0"); + expect(result).toEqual(["v1.0.0", "v1.5.0", "v2.0.0"]); + }); + + it("returns all versions when no minimum specified", () => { + const versions = ["v0.9.0", "v1.0.0", "v2.0.0"]; + const result = filterByMinVersion(versions); + expect(result).toEqual(versions); + }); + + it("handles empty array", () => { + expect(filterByMinVersion([], "1.0.0")).toEqual([]); + }); +}); + +describe("diffVersions", () => { + it("finds versions in remote not in local", () => { + const remote = ["v1.0.0", "v2.0.0", "v3.0.0"]; + const local = ["v1.0.0", "v3.0.0"]; + expect(diffVersions(remote, local)).toEqual(["v2.0.0"]); + }); + + it("returns all remote when local is empty", () => { + const remote = ["v1.0.0", "v2.0.0"]; + expect(diffVersions(remote, [])).toEqual(remote); + }); + + it("returns empty when all versions exist locally", () => { + const versions = ["v1.0.0", "v2.0.0"]; + expect(diffVersions(versions, versions)).toEqual([]); + }); + + it("matches normalized local versions (both have v prefix after normalization)", () => { + // fetchLocalVersions now normalizes all versions to have "v" prefix + // so local versions like "1.0.0" become "v1.0.0" before diffVersions is called + const remote = ["v1.0.0", "v2.0.0"]; + const local = ["v1.0.0"]; // normalized from "1.0.0" by fetchLocalVersions + expect(diffVersions(remote, local)).toEqual(["v2.0.0"]); + }); +}); diff --git a/sdk-reference-generator/src/cli.ts b/sdk-reference-generator/src/cli.ts new file mode 100644 index 0000000..4c8381e --- /dev/null +++ b/sdk-reference-generator/src/cli.ts @@ -0,0 +1,138 @@ +#!/usr/bin/env node + +import { Command } from "commander"; +import fs from "fs-extra"; +import path from "path"; +import os from "os"; +import { fileURLToPath } from "url"; +import sdks from "../sdks.config.js"; +import { generateSDK } from "./generator.js"; +import { buildNavigation, mergeNavigation } from "./navigation.js"; +import { verifyGeneratedDocs } from "./lib/verify.js"; +import { log } from "./lib/log.js"; +import type { GenerationContext, GenerationResult } from "./types.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const SCRIPT_DIR = path.resolve(__dirname, ".."); +const DOCS_DIR = path.resolve(SCRIPT_DIR, ".."); +const CONFIGS_DIR = path.join(SCRIPT_DIR, "configs"); + +const program = new Command() + .name("generate-sdk-reference") + .description("Generate SDK reference documentation") + .option("--sdk ", 'SDK to generate (or "all")', "all") + .option( + "--version ", + 'Version to generate (or "all", "latest")', + "all" + ) + .option("--limit ", "Limit number of versions to generate", parseInt) + .option("--force", "Force regenerate existing versions") + .parse(); + +const opts = program.opts<{ + sdk: string; + version: string; + limit?: number; + force?: boolean; +}>(); + +async function main(): Promise { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "sdk-gen-")); + + log.header("SDK Reference Generator"); + log.stats([ + { label: "SDK", value: opts.sdk }, + { label: "Version", value: opts.version }, + ...(opts.limit + ? [{ label: "Limit", value: `${opts.limit} versions` }] + : []), + ...(opts.force ? [{ label: "Force", value: "true" }] : []), + { label: "Temp", value: tempDir }, + ]); + log.blank(); + + const context: GenerationContext = { + tempDir, + docsDir: DOCS_DIR, + configsDir: CONFIGS_DIR, + limit: opts.limit, + force: opts.force, + }; + + try { + const sdkKeys = opts.sdk === "all" ? Object.keys(sdks) : [opts.sdk]; + + const results: Map = new Map(); + + for (const sdkKey of sdkKeys) { + log.section(`Generating ${sdkKey}`); + const result = await generateSDK(sdkKey, opts.version, context); + results.set(sdkKey, result); + } + + log.blank(); + log.section("Building navigation"); + const navigation = await buildNavigation(DOCS_DIR); + + log.blank(); + log.section("Merging into docs.json"); + await mergeNavigation(navigation, DOCS_DIR); + + log.blank(); + log.section("Verifying documentation"); + const verification = await verifyGeneratedDocs(DOCS_DIR); + + if (verification.warnings.length > 0) { + log.warn("Warnings detected:"); + for (const warning of verification.warnings) { + log.data(`- ${warning}`, 1); + } + } + + if (!verification.valid) { + log.blank(); + log.error("Verification failed:"); + for (const error of verification.errors) { + log.data(`- ${error}`, 1); + } + if (!verification.docsJsonValid) { + log.error("docs.json validation failed"); + } + throw new Error("Documentation verification failed"); + } + + log.blank(); + log.success("SDK reference generation complete"); + + let totalGenerated = 0; + let totalFailed = 0; + + for (const [sdkKey, result] of results) { + totalGenerated += result.generated; + totalFailed += result.failed; + } + + log.blank(); + log.summary("Final Summary"); + log.stats( + [ + { label: "Generated", value: totalGenerated }, + ...(totalFailed > 0 ? [{ label: "Failed", value: totalFailed }] : []), + { label: "Total MDX files", value: verification.stats.totalMdxFiles }, + { label: "Total SDKs", value: verification.stats.totalSDKs }, + { label: "Total versions", value: verification.stats.totalVersions }, + ], + 0 + ); + } finally { + await fs.remove(tempDir); + } +} + +main().catch((error) => { + log.error(`Fatal error: ${error.message}`); + process.exit(1); +}); diff --git a/sdk-reference-generator/src/generator.ts b/sdk-reference-generator/src/generator.ts new file mode 100644 index 0000000..2e67560 --- /dev/null +++ b/sdk-reference-generator/src/generator.ts @@ -0,0 +1,335 @@ +import fs from "fs-extra"; +import path from "path"; +import type { + SDKConfig, + GenerationContext, + GenerationResult, + TypedocConfig, + PydocConfig, +} from "./types.js"; +import sdks from "../sdks.config.js"; +import { log } from "./lib/log.js"; +import { fetchRemoteTags, resolveLatestVersion } from "./lib/git.js"; +import { + fetchLocalVersions, + filterByMinVersion, + diffVersions, + versionExists, +} from "./lib/versions.js"; +import { flattenMarkdown, copyToDocs, locateSDKDir } from "./lib/files.js"; +import { installDependencies } from "./lib/install.js"; +import { generateTypedoc } from "./generators/typedoc.js"; +import { generatePydoc } from "./generators/pydoc.js"; +import { generateCli } from "./generators/cli.js"; +import { buildSDKPath } from "./lib/utils.js"; +import { CONSTANTS } from "./lib/constants.js"; +import { CheckoutManager } from "./lib/checkout.js"; +import { resolveConfig } from "./lib/config.js"; + +async function generateVersion( + sdkKey: string, + config: SDKConfig, + version: string, + context: GenerationContext, + checkoutMgr: CheckoutManager, + isFirstVersion: boolean +): Promise { + const tagName = config.tagFormat.replace( + "{version}", + version.replace(/^v/, "") + ); + + let repoDir: string; + + if (isFirstVersion) { + repoDir = await checkoutMgr.getOrClone( + sdkKey, + config.repo, + tagName, + context.tempDir + ); + } else { + await checkoutMgr.switchVersion(sdkKey, tagName); + repoDir = checkoutMgr.getRepoDir(sdkKey)!; + } + + const sdkDir = await locateSDKDir(repoDir, config.sdkPath, config.sdkPaths); + if (!sdkDir) { + throw new Error( + `SDK path not found: ${config.sdkPath || config.sdkPaths?.join(", ")}` + ); + } + + const sdkRefDir = path.join(sdkDir, CONSTANTS.SDK_REF_DIR); + await fs.remove(sdkRefDir); + + const installResult = await installDependencies(sdkDir, config.generator); + + let generatedDocsDir: string; + switch (config.generator) { + case "typedoc": { + const resolvedConfig = resolveConfig( + config.defaultConfig, + config.configOverrides, + version + ); + generatedDocsDir = await generateTypedoc( + sdkDir, + resolvedConfig, + context.configsDir + ); + break; + } + case "pydoc": { + const resolvedConfig = resolveConfig( + config.defaultConfig, + config.configOverrides, + version + ); + generatedDocsDir = await generatePydoc( + sdkDir, + resolvedConfig, + installResult.usePoetryRun + ); + break; + } + case "cli": + generatedDocsDir = await generateCli(sdkDir); + break; + } + + if (generatedDocsDir !== sdkRefDir) { + log.info(`Normalizing ${path.basename(generatedDocsDir)} to sdk_ref`, 1); + await fs.move(generatedDocsDir, sdkRefDir, { overwrite: true }); + } + + await flattenMarkdown(sdkRefDir); + + const destDir = buildSDKPath(context.docsDir, sdkKey, version); + const success = await copyToDocs( + sdkRefDir, + destDir, + config.displayName, + version + ); + + if (!success) { + throw new Error("Failed to copy generated files"); + } +} + +async function discoverAllVersions( + sdkKey: string, + config: SDKConfig, + context: GenerationContext +): Promise { + log.info("Discovering all versions...", 1); + + let remote = await fetchRemoteTags(config.repo, config.tagPattern); + + if (remote.length === 0) { + if (config.required) { + throw new Error(`No tags found for required SDK: ${sdkKey}`); + } + log.warn("No tags found, skipping...", 1); + return []; + } + + if (config.minVersion) { + remote = filterByMinVersion(remote, config.minVersion); + log.info(`Filtered to versions >= ${config.minVersion}`, 1); + } + + if (context.limit && context.limit > 0) { + remote = remote.slice(0, context.limit); + log.info(`Limited to last ${context.limit} versions`, 1); + } + + const local = await fetchLocalVersions(sdkKey, context.docsDir); + + log.blank(); + log.step("Version Discovery", 1); + log.stats( + [ + { label: "Remote", value: remote.length }, + { label: "Local", value: local.length }, + ], + 1 + ); + + const missing = context.force ? remote : diffVersions(remote, local); + + log.stats( + [ + { + label: context.force ? "To Generate (forced)" : "Missing", + value: missing.length, + }, + ], + 1 + ); + log.blank(); + + if (missing.length === 0) { + log.success("Nothing to generate", 1); + return []; + } + + if (context.force && local.length > 0) { + log.warn("FORCE MODE: Will regenerate existing versions", 1); + } + + return missing; +} + +async function resolveSpecificVersion( + sdkKey: string, + config: SDKConfig, + versionArg: string, + context: GenerationContext +): Promise { + const resolved = await resolveLatestVersion( + config.repo, + config.tagPattern, + versionArg + ); + + if (!resolved) { + if (config.required) { + throw new Error(`No tags found for required SDK: ${sdkKey}`); + } + log.warn("No tags found, skipping...", 1); + return []; + } + + if ( + !context.force && + (await versionExists(sdkKey, resolved, context.docsDir)) + ) { + log.success(`${resolved} already exists`, 1); + return []; + } + + if (context.force) { + log.warn("FORCE MODE: Will regenerate existing version", 1); + } + + return [resolved]; +} + +async function processVersionBatch( + sdkKey: string, + config: SDKConfig, + versions: string[], + context: GenerationContext +): Promise { + let generated = 0; + let failed = 0; + const failedVersions: string[] = []; + + const checkoutMgr = new CheckoutManager(); + + try { + for (let i = 0; i < versions.length; i++) { + const version = versions[i]; + const isFirstVersion = i === 0; + + log.blank(); + log.step(`Generating ${version}`, 1); + + try { + await generateVersion( + sdkKey, + config, + version, + context, + checkoutMgr, + isFirstVersion + ); + log.success(`Complete: ${version}`, 1); + generated++; + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + log.error(`Failed: ${version} - ${msg}`, 1); + failed++; + failedVersions.push(version); + } + } + } finally { + await checkoutMgr.cleanup(); + } + + return { generated, failed, failedVersions }; +} + +export function handleGenerationFailures( + config: SDKConfig, + result: GenerationResult +): void { + const { generated, failed, failedVersions } = result; + + log.blank(); + log.step("Summary", 1); + log.stats( + [ + { label: "Generated", value: generated }, + ...(failed > 0 + ? [ + { + label: "Failed", + value: `${failed} (${failedVersions.join(" ")})`, + }, + ] + : []), + ], + 1 + ); + + if (failed === 0) return; + + const shouldAbort = config.required || generated === 0; + if (shouldAbort) { + log.blank(); + const reason = config.required + ? "Required SDK has failures" + : "All versions failed"; + log.error(`WORKFLOW ABORTED: ${reason}`, 1); + log.error(`Failed: ${failedVersions.join(" ")}`, 1); + throw new Error(`Generation aborted: ${reason}`); + } +} + +export async function generateSDK( + sdkKey: string, + versionArg: string, + context: GenerationContext +): Promise { + const config = sdks[sdkKey as keyof typeof sdks]; + + if (!config) { + log.error(`SDK '${sdkKey}' not found in config`, 1); + return { generated: 0, failed: 1, failedVersions: [sdkKey] }; + } + + log.info(`${config.displayName} version: ${versionArg}`, 1); + + const versionsToProcess = + versionArg === "all" + ? await discoverAllVersions(sdkKey, config, context) + : await resolveSpecificVersion(sdkKey, config, versionArg, context); + + if (versionsToProcess.length === 0) { + return { generated: 0, failed: 0, failedVersions: [] }; + } + + const result = await processVersionBatch( + sdkKey, + config, + versionsToProcess, + context + ); + + handleGenerationFailures(config, result); + + return result; +} diff --git a/sdk-reference-generator/src/generators/cli.ts b/sdk-reference-generator/src/generators/cli.ts new file mode 100644 index 0000000..d6fc9d9 --- /dev/null +++ b/sdk-reference-generator/src/generators/cli.ts @@ -0,0 +1,71 @@ +import { execa } from "execa"; +import fs from "fs-extra"; +import path from "path"; +import { glob } from "glob"; +import { CONSTANTS } from "../lib/constants.js"; +import { log } from "../lib/log.js"; + +async function findCliOutputDir(sdkDir: string): Promise { + const possibleDirs = ["sdk_ref", "api_ref"]; + + for (const dir of possibleDirs) { + const fullPath = path.join(sdkDir, dir); + if (await fs.pathExists(fullPath)) { + const mdFiles = await glob(`*${CONSTANTS.MD_EXTENSION}`, { + cwd: fullPath, + }); + if (mdFiles.length > 0) { + log.info(`Found CLI docs in ${dir}/`, 1); + return fullPath; + } + } + } + + return null; +} + +export async function generateCli(sdkDir: string): Promise { + log.info("Building CLI...", 1); + + try { + await execa("pnpm", ["run", "build"], { + cwd: sdkDir, + stdio: "inherit", + }); + } catch (error) { + log.warn("pnpm build failed, trying tsup...", 1); + await execa("npx", ["tsup"], { + cwd: sdkDir, + stdio: "inherit", + }); + } + + log.info("Generating documentation...", 1); + + await execa("node", ["dist/index.js", "-cmd2md"], { + cwd: sdkDir, + env: { ...process.env, NODE_ENV: "development" }, + stdio: "inherit", + }); + + const outputDir = await findCliOutputDir(sdkDir); + + if (!outputDir) { + throw new Error( + "CLI generator did not create any markdown files in sdk_ref/ or api_ref/" + ); + } + + const mdFiles = await glob(`*${CONSTANTS.MD_EXTENSION}`, { cwd: outputDir }); + + for (const file of mdFiles) { + const srcPath = path.join(outputDir, file); + const destPath = srcPath.replace( + CONSTANTS.MD_EXTENSION, + CONSTANTS.MDX_EXTENSION + ); + await fs.move(srcPath, destPath); + } + + return outputDir; +} diff --git a/sdk-reference-generator/src/generators/pydoc.ts b/sdk-reference-generator/src/generators/pydoc.ts new file mode 100644 index 0000000..8c60d96 --- /dev/null +++ b/sdk-reference-generator/src/generators/pydoc.ts @@ -0,0 +1,101 @@ +import { execa } from "execa"; +import fs from "fs-extra"; +import path from "path"; +import { CONSTANTS } from "../lib/constants.js"; +import { log } from "../lib/log.js"; +import type { PydocConfig } from "../types.js"; + +async function processMdx(file: string): Promise { + let content = await fs.readFile(file, "utf-8"); + + content = content.replace(/]*>.*?<\/a>/g, ""); + content = content + .split("\n") + .filter((line) => !line.startsWith("# ")) + .join("\n"); + content = content.replace(/^(## .+) Objects$/gm, "$1"); + content = content.replace(/^####/gm, "###"); + + await fs.writeFile(file, content); +} + +async function processPackage( + pkg: string, + sdkDir: string, + usePoetryRun: boolean +): Promise { + const rawName = pkg.split(".").pop() || pkg; + const name = rawName.replace(/^e2b_/, ""); + + log.step(`Processing ${pkg}`, 2); + + const outputFile = path.join( + sdkDir, + CONSTANTS.SDK_REF_DIR, + `${name}${CONSTANTS.MDX_EXTENSION}` + ); + + try { + const cmd = usePoetryRun ? "poetry" : "pydoc-markdown"; + const args = usePoetryRun + ? ["run", "pydoc-markdown", "-p", pkg] + : ["-p", pkg]; + + const result = await execa(cmd, args, { + cwd: sdkDir, + stdio: "pipe", + }); + + const rawContent = result.stdout.trim(); + if (rawContent.length < 50) { + log.warn(`${pkg} generated no content - skipping`, 2); + return false; + } + + await fs.writeFile(outputFile, result.stdout); + await processMdx(outputFile); + + const stat = await fs.stat(outputFile); + if (stat.size < 100) { + log.warn(`${pkg} has no meaningful content - removing`, 2); + await fs.remove(outputFile); + return false; + } + + return true; + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + log.warn(`Failed to generate docs for ${pkg}: ${msg}`, 2); + await fs.remove(outputFile); + return false; + } +} + +export async function generatePydoc( + sdkDir: string, + resolvedConfig: PydocConfig, + usePoetryRun: boolean +): Promise { + const { allowedPackages } = resolvedConfig; + const outputDir = path.join(sdkDir, CONSTANTS.SDK_REF_DIR); + await fs.ensureDir(outputDir); + + log.info( + `Attempting to generate docs for ${allowedPackages.length} packages`, + 1 + ); + log.data(`Packages: ${allowedPackages.join(", ")}`, 1); + + let successful = 0; + for (const pkg of allowedPackages) { + const result = await processPackage(pkg, sdkDir, usePoetryRun); + if (result) successful++; + } + + log.step( + `Generated docs for ${successful}/${allowedPackages.length} packages`, + 1 + ); + + return outputDir; +} diff --git a/sdk-reference-generator/src/generators/typedoc.ts b/sdk-reference-generator/src/generators/typedoc.ts new file mode 100644 index 0000000..ff0fd01 --- /dev/null +++ b/sdk-reference-generator/src/generators/typedoc.ts @@ -0,0 +1,62 @@ +import { execa } from "execa"; +import fs from "fs-extra"; +import path from "path"; +import { log } from "../lib/log.js"; +import { buildTypedocConfig } from "../lib/config.js"; +import { CONSTANTS } from "../lib/constants.js"; +import type { TypedocConfig } from "../types.js"; + +const GENERATED_CONFIG_NAME = "typedoc.generated.json"; + +/** + * Removes any existing typedoc config from the repo to prevent interference. + */ +async function cleanRepoConfigs(sdkDir: string): Promise { + const configFiles = ["typedoc.json", "typedoc.config.js", "typedoc.config.cjs"]; + + for (const file of configFiles) { + const filePath = path.join(sdkDir, file); + if (await fs.pathExists(filePath)) { + log.info(`Removing repo config: ${file}`, 1); + await fs.remove(filePath); + } + } +} + +export async function generateTypedoc( + sdkDir: string, + resolvedConfig: TypedocConfig, + configsDir: string +): Promise { + // remove any existing repo configs to force our config + await cleanRepoConfigs(sdkDir); + + // build full config with formatting defaults + SDK-specific settings + const fullConfig = buildTypedocConfig(resolvedConfig); + + // write our generated config + const configPath = path.join(sdkDir, GENERATED_CONFIG_NAME); + await fs.writeJSON(configPath, fullConfig, { spaces: 2 }); + + log.info("Running TypeDoc with generated config...", 1); + log.data(`Entry points: ${resolvedConfig.entryPoints.join(", ")}`, 1); + + await execa( + "npx", + [ + "typedoc", + "--options", + `./${GENERATED_CONFIG_NAME}`, + "--plugin", + "typedoc-plugin-markdown", + "--plugin", + path.join(configsDir, "typedoc-theme.cjs"), + ], + { + cwd: sdkDir, + stdio: "inherit", + } + ); + + return path.join(sdkDir, CONSTANTS.SDK_REF_DIR); +} diff --git a/sdk-reference-generator/src/lib/checkout.ts b/sdk-reference-generator/src/lib/checkout.ts new file mode 100644 index 0000000..a95434d --- /dev/null +++ b/sdk-reference-generator/src/lib/checkout.ts @@ -0,0 +1,48 @@ +import fs from "fs-extra"; +import path from "path"; +import { cloneAtTag, checkoutTag } from "./git.js"; +import { log } from "./log.js"; + +export class CheckoutManager { + private checkouts = new Map(); + + async getOrClone( + sdkKey: string, + repo: string, + tag: string, + tempDir: string + ): Promise { + const existing = this.checkouts.get(sdkKey); + if (existing) { + return existing; + } + + const repoDir = path.join(tempDir, `shared-${sdkKey}`); + log.info(`Cloning ${sdkKey} repository...`, 1); + await cloneAtTag(repo, tag, repoDir); + this.checkouts.set(sdkKey, repoDir); + return repoDir; + } + + async switchVersion(sdkKey: string, tag: string): Promise { + const repoDir = this.checkouts.get(sdkKey); + if (!repoDir) { + throw new Error(`Checkout not initialized for ${sdkKey}`); + } + + log.info(`Switching to ${tag}...`, 1); + await checkoutTag(repoDir, tag); + } + + getRepoDir(sdkKey: string): string | undefined { + return this.checkouts.get(sdkKey); + } + + async cleanup(): Promise { + for (const [sdkKey, dir] of this.checkouts.entries()) { + log.info(`Cleaning up ${sdkKey}...`, 1); + await fs.remove(dir); + } + this.checkouts.clear(); + } +} diff --git a/sdk-reference-generator/src/lib/config.ts b/sdk-reference-generator/src/lib/config.ts new file mode 100644 index 0000000..4a7ac3a --- /dev/null +++ b/sdk-reference-generator/src/lib/config.ts @@ -0,0 +1,73 @@ +import semver from "semver"; +import { stripVersionPrefix } from "./utils.js"; +import { log } from "./log.js"; +import type { TypedocConfig, PydocConfig } from "../types.js"; + +// shared typedoc formatting settings (applied to all typedoc SDKs) +export const TYPEDOC_FORMATTING = { + out: "sdk_ref", + plugin: ["typedoc-plugin-markdown"], + exclude: ["**/*.spec.ts"], + excludeExternals: true, + excludeInternal: true, + excludePrivate: true, + excludeProtected: true, + navigation: { + includeGroups: false, + includeCategories: false, + }, + outputFileStrategy: "modules", + readme: "none", + disableSources: true, + classPropertiesFormat: "table", + typeDeclarationFormat: "table", + enumMembersFormat: "table", + parametersFormat: "table", + expandParameters: true, + useCodeBlocks: true, + hidePageTitle: true, + hideBreadcrumbs: true, +} as const; + +/** + * Resolves config by matching version against semver ranges in configOverrides. + * Returns merged defaultConfig with matching override, or defaultConfig if no match. + */ +export function resolveConfig( + defaultConfig: T, + configOverrides: Record> | undefined, + version: string +): T { + if (!configOverrides) { + log.info(`Using default config for ${version}`, 1); + return defaultConfig; + } + + const cleanVersion = stripVersionPrefix(version); + + for (const [range, override] of Object.entries(configOverrides)) { + if (semver.satisfies(cleanVersion, range)) { + log.info( + `Using config override for ${version} (matched range: ${range})`, + 1 + ); + return { ...defaultConfig, ...override } as T; + } + } + + log.info(`Using default config for ${version} (no override match)`, 1); + return defaultConfig; +} + +/** + * Builds the full typedoc config by merging formatting defaults with SDK-specific settings. + */ +export function buildTypedocConfig( + resolved: TypedocConfig +): Record { + return { + ...TYPEDOC_FORMATTING, + entryPoints: resolved.entryPoints, + ...(resolved.exclude && { exclude: resolved.exclude }), + }; +} diff --git a/sdk-reference-generator/src/lib/constants.ts b/sdk-reference-generator/src/lib/constants.ts new file mode 100644 index 0000000..22315ad --- /dev/null +++ b/sdk-reference-generator/src/lib/constants.ts @@ -0,0 +1,7 @@ +export const CONSTANTS = { + SDK_REF_DIR: "sdk_ref", + DOCS_SDK_REF_PATH: "docs/sdk-reference", + MDX_EXTENSION: ".mdx", + MD_EXTENSION: ".md", + SDK_REFERENCE_ANCHOR: "SDK Reference", +} as const; diff --git a/sdk-reference-generator/src/lib/files.ts b/sdk-reference-generator/src/lib/files.ts new file mode 100644 index 0000000..9b8e630 --- /dev/null +++ b/sdk-reference-generator/src/lib/files.ts @@ -0,0 +1,224 @@ +import fs from "fs-extra"; +import path from "path"; +import { glob } from "glob"; +import { createFrontmatter } from "./utils.js"; +import { CONSTANTS } from "./constants.js"; +import { log } from "./log.js"; + +export function toTitleCase(str: string): string { + if (!str) return ""; + + return str + .split("_") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} + +/** + * Extracts a clean title from a flattened filename. + * Removes directory prefixes added during flattening. + * + * @example + * extractTitle('modules-Sandbox') // 'Sandbox' + * extractTitle('classes-MyClass') // 'MyClass' + * extractTitle('sandbox_sync') // 'Sandbox Sync' + * extractTitle('modules-sandbox_sync') // 'Sandbox Sync' + */ +export function extractTitle(filename: string): string { + if (!filename) return ""; + + const prefixMatch = filename.match(/^([a-z]+-)+(.+)$/); + + if (prefixMatch) { + const withoutPrefix = prefixMatch[2]; + return toTitleCase(withoutPrefix); + } + + return toTitleCase(filename); +} + +export async function addFrontmatter( + file: string, + title: string +): Promise { + const content = await fs.readFile(file, "utf-8"); + + if (content.startsWith("---")) { + return; + } + + await fs.writeFile(file, createFrontmatter(title) + content); +} + +async function removeUnwantedFiles(refDir: string): Promise { + await fs.remove(path.join(refDir, "README.md")); + await fs.remove(path.join(refDir, "index.md")); + await fs.remove(path.join(refDir, `index${CONSTANTS.MDX_EXTENSION}`)); +} + +async function flattenNestedFiles(refDir: string): Promise { + const nestedFiles = await glob("**/*.md", { + cwd: refDir, + ignore: "*.md", + }); + + const targetFiles = new Set(); + const collisions: string[] = []; + const moves: Array<{ from: string; to: string }> = []; + + for (const file of nestedFiles) { + const filename = path.basename(file); + const parentDirName = path.basename(path.dirname(file)); + const dirPath = path.dirname(file).replace(/\//g, "-"); + + let targetName: string; + + if (filename === "page.md" || filename === "index.md") { + targetName = `${parentDirName}.md`; + } else { + const baseName = path.basename(filename, ".md"); + targetName = `${dirPath}-${baseName}.md`; + } + + if (targetFiles.has(targetName)) { + collisions.push(`${file} → ${targetName}`); + } + targetFiles.add(targetName); + + moves.push({ + from: path.join(refDir, file), + to: path.join(refDir, targetName), + }); + } + + if (collisions.length > 0) { + log.warn(`Detected ${collisions.length} filename collision(s):`, 1); + collisions.forEach((c) => log.data(c, 2)); + throw new Error( + `Cannot flatten files: ${collisions.length} filename collision(s) detected. ` + + `Different source files would overwrite each other.` + ); + } + + for (const { from, to } of moves) { + await fs.move(from, to, { overwrite: false }); + } +} + +async function removeEmptyDirectories(refDir: string): Promise { + const dirs = await glob("**/", { cwd: refDir }); + for (const dir of dirs.reverse()) { + const dirPath = path.join(refDir, dir); + try { + const files = await fs.readdir(dirPath); + if (files.length === 0) { + await fs.remove(dirPath); + } + } catch {} + } +} + +async function convertMdToMdx(refDir: string): Promise { + const mdFiles = await glob("*.md", { cwd: refDir }); + + for (const file of mdFiles) { + const fullPath = path.join(refDir, file); + const title = extractTitle(path.basename(file, CONSTANTS.MD_EXTENSION)); + const content = await fs.readFile(fullPath, "utf-8"); + + const mdxPath = fullPath.replace( + CONSTANTS.MD_EXTENSION, + CONSTANTS.MDX_EXTENSION + ); + await fs.writeFile(mdxPath, createFrontmatter(title) + content); + await fs.remove(fullPath); + } +} + +async function ensureFrontmatter(refDir: string): Promise { + const mdxFiles = await glob(`*${CONSTANTS.MDX_EXTENSION}`, { cwd: refDir }); + + for (const file of mdxFiles) { + const fullPath = path.join(refDir, file); + const content = await fs.readFile(fullPath, "utf-8"); + + if (!content.startsWith("---")) { + const title = extractTitle(path.basename(file, CONSTANTS.MDX_EXTENSION)); + await addFrontmatter(fullPath, title); + } + } +} + +export async function flattenMarkdown(refDir: string): Promise { + await removeUnwantedFiles(refDir); + await flattenNestedFiles(refDir); + await removeEmptyDirectories(refDir); + await convertMdToMdx(refDir); + await ensureFrontmatter(refDir); +} + +async function getNonEmptyMdxFiles(dir: string): Promise { + const allFiles = await glob(`*${CONSTANTS.MDX_EXTENSION}`, { cwd: dir }); + const nonEmptyFiles: string[] = []; + + for (const file of allFiles) { + const stat = await fs.stat(path.join(dir, file)); + if (stat.size > 0) { + nonEmptyFiles.push(file); + } + } + + return nonEmptyFiles; +} + +export async function copyToDocs( + srcDir: string, + destDir: string, + sdkName: string, + version: string +): Promise { + const files = await getNonEmptyMdxFiles(srcDir); + + if (files.length === 0) { + log.error("No MDX files generated - doc generator failed", 1); + return false; + } + + await fs.remove(destDir); + await fs.ensureDir(destDir); + + log.info(`Copying ${files.length} files to ${destDir}`, 1); + + for (const file of files) { + await fs.copy(path.join(srcDir, file), path.join(destDir, file)); + } + + log.success(`${sdkName} ${version} complete`, 1); + return true; +} + +export async function locateSDKDir( + repoDir: string, + sdkPath?: string, + sdkPaths?: string[] +): Promise { + if (sdkPath) { + const dir = path.join(repoDir, sdkPath); + if (await fs.pathExists(dir)) { + return dir; + } + return null; + } + + if (sdkPaths) { + for (const p of sdkPaths) { + const dir = path.join(repoDir, p); + if (await fs.pathExists(dir)) { + return dir; + } + } + return null; + } + + return repoDir; +} diff --git a/sdk-reference-generator/src/lib/git.ts b/sdk-reference-generator/src/lib/git.ts new file mode 100644 index 0000000..06d0f8e --- /dev/null +++ b/sdk-reference-generator/src/lib/git.ts @@ -0,0 +1,110 @@ +import { simpleGit, SimpleGit } from "simple-git"; +import { sortVersionsDescending, normalizeVersion } from "./utils.js"; +import { log } from "./log.js"; + +const git: SimpleGit = simpleGit(); + +export async function fetchRemoteTags( + repo: string, + tagPattern: string +): Promise { + const output = await git.listRemote(["--tags", "--refs", repo]); + + const versions = output + .split("\n") + .filter((line: string) => line.includes(`refs/tags/${tagPattern}`)) + .map((line: string) => { + const match = line.match(/refs\/tags\/(.+)$/); + if (!match) return null; + const tag = match[1]; + return "v" + tag.replace(tagPattern, ""); + }) + .filter((v: string | null): v is string => v !== null && v !== "v"); + + return sortVersionsDescending(versions); +} + +function isTagNotFoundError(error: unknown): boolean { + if (!(error instanceof Error)) return false; + + const message = error.message.toLowerCase(); + return ( + (message.includes("remote branch") && message.includes("not found")) || + message.includes("couldn't find remote ref") || + message.includes("invalid refspec") || + message.includes("reference is not a tree") + ); +} + +async function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export async function cloneAtTag( + repo: string, + tag: string, + targetDir: string +): Promise { + const maxRetries = 3; + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + await git.clone(repo, targetDir, ["--depth", "1", "--branch", tag]); + return; + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + + // only retry if it's a tag-not-found error + if (!isTagNotFoundError(error)) { + throw new Error( + `Failed to clone repository: ${lastError.message}. ` + + `This appears to be a network, authentication, or system error, not a missing tag.` + ); + } + + if (attempt < maxRetries) { + const backoffMs = attempt * 1000; + log.warn( + `Tag ${tag} not found (attempt ${attempt}/${maxRetries}), retrying in ${backoffMs}ms...`, + 1 + ); + await sleep(backoffMs); + } + } + } + + throw new Error( + `Tag ${tag} not found in repository ${repo} after ${maxRetries} attempts. ` + + `Cancelling generation to avoid publishing incorrect documentation. ` + + `Original error: ${lastError?.message}` + ); +} + +export async function resolveLatestVersion( + repo: string, + tagPattern: string, + version: string +): Promise { + if (version !== "latest") { + return normalizeVersion(version); + } + + const versions = await fetchRemoteTags(repo, tagPattern); + return versions[0] || null; +} + +export async function checkoutTag(repoDir: string, tag: string): Promise { + const repoGit = simpleGit(repoDir); + + await repoGit.fetch([ + "origin", + `refs/tags/${tag}:refs/tags/${tag}`, + "--depth", + "1", + ]); + + await repoGit.checkout(tag, ["--force"]); + + await repoGit.clean("f", ["-d"]); +} diff --git a/sdk-reference-generator/src/lib/install.ts b/sdk-reference-generator/src/lib/install.ts new file mode 100644 index 0000000..9fce918 --- /dev/null +++ b/sdk-reference-generator/src/lib/install.ts @@ -0,0 +1,68 @@ +import { execa } from "execa"; +import type { GeneratorType, InstallResult } from "../types.js"; +import { log } from "./log.js"; + +export async function installDependencies( + sdkDir: string, + generator: GeneratorType +): Promise { + log.info("Installing dependencies...", 1); + + switch (generator) { + case "typedoc": + case "cli": { + const isTypedoc = generator === "typedoc"; + const pnpmArgs = isTypedoc + ? ["install", "--ignore-scripts", "--prefer-offline"] + : ["install", "--prefer-offline"]; + + try { + await execa("pnpm", pnpmArgs, { + cwd: sdkDir, + stdio: "inherit", + }); + } catch { + log.warn("pnpm failed, falling back to npm...", 1); + await execa( + "npm", + ["install", "--legacy-peer-deps", "--force", "--prefer-offline"], + { + cwd: sdkDir, + stdio: "inherit", + } + ); + } + return { usePoetryRun: false }; + } + + case "pydoc": { + try { + await execa("poetry", ["install", "--no-interaction"], { + cwd: sdkDir, + stdio: "inherit", + }); + return { usePoetryRun: true }; + } catch { + log.warn("poetry failed, falling back to pip...", 1); + + log.info("Installing SDK package from local directory...", 1); + await execa("pip", ["install", "--break-system-packages", "."], { + cwd: sdkDir, + stdio: "inherit", + }); + + log.info("Installing pydoc-markdown...", 1); + await execa( + "pip", + ["install", "--break-system-packages", "pydoc-markdown"], + { + cwd: sdkDir, + stdio: "inherit", + } + ); + + return { usePoetryRun: false }; + } + } + } +} diff --git a/sdk-reference-generator/src/lib/log.ts b/sdk-reference-generator/src/lib/log.ts new file mode 100644 index 0000000..aa67bf4 --- /dev/null +++ b/sdk-reference-generator/src/lib/log.ts @@ -0,0 +1,59 @@ +import chalk from "chalk"; + +export const log = { + header(message: string): void { + console.log(chalk.bold.cyan(`\n▸ ${message.toUpperCase()}`)); + }, + + section(message: string): void { + console.log(chalk.bold(`\n${message}`)); + }, + + info(message: string, indent = 0): void { + const prefix = " ".repeat(indent); + console.log(`${prefix}${chalk.dim(">")} ${message}`); + }, + + success(message: string, indent = 0): void { + const prefix = " ".repeat(indent); + console.log(`${prefix}${chalk.green("✓")} ${message}`); + }, + + warn(message: string, indent = 0): void { + const prefix = " ".repeat(indent); + console.log(`${prefix}${chalk.yellow("!")} ${message}`); + }, + + error(message: string, indent = 0): void { + const prefix = " ".repeat(indent); + console.log(`${prefix}${chalk.red("✗")} ${message}`); + }, + + step(message: string, indent = 0): void { + const prefix = " ".repeat(indent); + console.log(`${prefix}${chalk.blue("-")} ${message}`); + }, + + stats( + items: Array<{ label: string; value: string | number }>, + indent = 0 + ): void { + const prefix = " ".repeat(indent); + for (const { label, value } of items) { + console.log(`${prefix} ${chalk.gray(label + ":")} ${value}`); + } + }, + + blank(): void { + console.log(""); + }, + + data(message: string, indent = 0): void { + const prefix = " ".repeat(indent); + console.log(`${prefix} ${chalk.dim(message)}`); + }, + + summary(title: string): void { + console.log(chalk.bold.white(`\n${title}`)); + }, +}; diff --git a/sdk-reference-generator/src/lib/utils.ts b/sdk-reference-generator/src/lib/utils.ts new file mode 100644 index 0000000..58a0eda --- /dev/null +++ b/sdk-reference-generator/src/lib/utils.ts @@ -0,0 +1,41 @@ +import semver from "semver"; +import path from "path"; +import { CONSTANTS } from "./constants.js"; + +export function normalizeVersion(version: string): string { + return version.startsWith("v") ? version : `v${version}`; +} + +export function stripVersionPrefix(version: string): string { + return version.replace(/^v/, ""); +} + +export function isValidVersion(version: string): boolean { + return /^v?\d+\.\d+\.\d+/.test(version); +} + +export function sortVersionsDescending(versions: string[]): string[] { + return versions.sort((a, b) => { + try { + return semver.rcompare(stripVersionPrefix(a), stripVersionPrefix(b)); + } catch { + return b.localeCompare(a); + } + }); +} + +export function createFrontmatter(title: string): string { + return `--- +sidebarTitle: "${title}" +--- + +`; +} + +export function buildSDKPath( + docsDir: string, + sdkKey: string, + version: string +): string { + return path.join(docsDir, CONSTANTS.DOCS_SDK_REF_PATH, sdkKey, version); +} diff --git a/sdk-reference-generator/src/lib/verify.ts b/sdk-reference-generator/src/lib/verify.ts new file mode 100644 index 0000000..130ff9f --- /dev/null +++ b/sdk-reference-generator/src/lib/verify.ts @@ -0,0 +1,132 @@ +import fs from 'fs-extra'; +import path from 'path'; +import { glob } from 'glob'; +import { CONSTANTS } from './constants.js'; +import { log } from './log.js'; + +export interface VerificationResult { + valid: boolean; + errors: string[]; + warnings: string[]; + stats: { + totalMdxFiles: number; + totalSDKs: number; + totalVersions: number; + }; + docsJsonValid: boolean; +} + +export async function verifyGeneratedDocs( + docsDir: string +): Promise { + const errors: string[] = []; + const warnings: string[] = []; + const stats = { totalMdxFiles: 0, totalSDKs: 0, totalVersions: 0 }; + + const sdkRefDir = path.join(docsDir, CONSTANTS.DOCS_SDK_REF_PATH); + + if (!(await fs.pathExists(sdkRefDir))) { + errors.push('SDK reference directory does not exist'); + return { valid: false, errors, warnings, stats, docsJsonValid: false }; + } + + const sdkDirs = await fs.readdir(sdkRefDir, { withFileTypes: true }); + + for (const sdkEntry of sdkDirs) { + if (!sdkEntry.isDirectory()) continue; + + stats.totalSDKs++; + const sdkPath = path.join(sdkRefDir, sdkEntry.name); + const versionDirs = await fs.readdir(sdkPath, { withFileTypes: true }); + + for (const versionEntry of versionDirs) { + if (!versionEntry.isDirectory()) continue; + if (!/^v?\d+\.\d+\.\d+/.test(versionEntry.name)) continue; + + stats.totalVersions++; + const versionPath = path.join(sdkPath, versionEntry.name); + + const mdxFiles = await glob(`*${CONSTANTS.MDX_EXTENSION}`, { + cwd: versionPath, + }); + + if (mdxFiles.length === 0) { + warnings.push( + `${sdkEntry.name}/${versionEntry.name} has no MDX files` + ); + continue; + } + + for (const file of mdxFiles) { + const filePath = path.join(versionPath, file); + const stat = await fs.stat(filePath); + + if (stat.size === 0) { + errors.push( + `Empty file: ${sdkEntry.name}/${versionEntry.name}/${file}` + ); + } else { + stats.totalMdxFiles++; + + const content = await fs.readFile(filePath, 'utf-8'); + if (!content.startsWith('---')) { + errors.push( + `Missing frontmatter: ${sdkEntry.name}/${versionEntry.name}/${file}` + ); + } + } + } + } + } + + // verify docs.json + const docsJsonValid = await verifyDocsJson(docsDir); + + return { + valid: errors.length === 0 && docsJsonValid, + errors, + warnings, + stats, + docsJsonValid, + }; +} + +async function verifyDocsJson(docsDir: string): Promise { + const docsJsonPath = path.join(docsDir, 'docs.json'); + + if (!(await fs.pathExists(docsJsonPath))) { + log.error('docs.json not found'); + return false; + } + + try { + const docsJson = await fs.readJSON(docsJsonPath); + + const anchors = docsJson.navigation?.anchors; + if (!Array.isArray(anchors)) { + log.error('Invalid docs.json: navigation.anchors is not an array'); + return false; + } + + const sdkRefAnchor = anchors.find( + (a: { anchor?: string }) => a.anchor === CONSTANTS.SDK_REFERENCE_ANCHOR + ); + + if (!sdkRefAnchor) { + log.error(`${CONSTANTS.SDK_REFERENCE_ANCHOR} anchor not found in docs.json`); + return false; + } + + if (!Array.isArray(sdkRefAnchor.dropdowns)) { + log.error('SDK Reference anchor has no dropdowns array'); + return false; + } + + return true; + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + log.error(`Failed to parse docs.json: ${msg}`); + return false; + } +} + diff --git a/sdk-reference-generator/src/lib/versions.ts b/sdk-reference-generator/src/lib/versions.ts new file mode 100644 index 0000000..2bbf4f6 --- /dev/null +++ b/sdk-reference-generator/src/lib/versions.ts @@ -0,0 +1,93 @@ +import fs from "fs-extra"; +import path from "path"; +import semver from "semver"; +import { + stripVersionPrefix, + sortVersionsDescending, + isValidVersion, + normalizeVersion, +} from "./utils.js"; +import { CONSTANTS } from "./constants.js"; + +export { isValidVersion }; + +export function versionGte(v1: string, v2: string): boolean { + try { + return semver.gte(stripVersionPrefix(v1), stripVersionPrefix(v2)); + } catch { + return stripVersionPrefix(v1) >= stripVersionPrefix(v2); + } +} + +export function filterByMinVersion( + versions: string[], + minVersion?: string +): string[] { + if (!minVersion) return versions; + return versions.filter((v) => versionGte(v, minVersion)); +} + +export async function fetchLocalVersions( + sdkKey: string, + docsDir: string +): Promise { + const sdkDir = path.join(docsDir, CONSTANTS.DOCS_SDK_REF_PATH, sdkKey); + + if (!(await fs.pathExists(sdkDir))) { + return []; + } + + const entries = await fs.readdir(sdkDir, { withFileTypes: true }); + const versions: string[] = []; + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (!isValidVersion(entry.name)) continue; + + const versionDir = path.join(sdkDir, entry.name); + const files = await fs.readdir(versionDir); + const hasMdx = files.some((f) => f.endsWith(CONSTANTS.MDX_EXTENSION)); + + if (hasMdx) { + // normalize to "v" prefix for consistent comparison with remote versions + versions.push(normalizeVersion(entry.name)); + } + } + + return sortVersionsDescending(versions); +} + +export function diffVersions(remote: string[], local: string[]): string[] { + const localSet = new Set(local); + return remote.filter((v) => !localSet.has(v)); +} + +export async function versionExists( + sdkKey: string, + version: string, + docsDir: string +): Promise { + const normalized = normalizeVersion(version); + const stripped = stripVersionPrefix(version); + + // check both "v1.0.0" and "1.0.0" directories for backward compatibility + const candidates = [normalized, stripped]; + + for (const candidate of candidates) { + const versionDir = path.join( + docsDir, + CONSTANTS.DOCS_SDK_REF_PATH, + sdkKey, + candidate + ); + + if (await fs.pathExists(versionDir)) { + const files = await fs.readdir(versionDir); + if (files.some((f) => f.endsWith(CONSTANTS.MDX_EXTENSION))) { + return true; + } + } + } + + return false; +} diff --git a/sdk-reference-generator/src/navigation.ts b/sdk-reference-generator/src/navigation.ts new file mode 100644 index 0000000..c6f39f5 --- /dev/null +++ b/sdk-reference-generator/src/navigation.ts @@ -0,0 +1,161 @@ +import fs from "fs-extra"; +import path from "path"; +import sdks from "../sdks.config.js"; +import { + sortVersionsDescending, + isValidVersion, + normalizeVersion, +} from "./lib/utils.js"; +import { CONSTANTS } from "./lib/constants.js"; +import { log } from "./lib/log.js"; +import type { + NavigationDropdown, + NavigationDropdownWithOrder, +} from "./types.js"; + +async function getVersions(sdkDir: string): Promise { + try { + const entries = await fs.readdir(sdkDir, { withFileTypes: true }); + + const versions = entries + .filter((e) => e.isDirectory() && isValidVersion(e.name)) + .map((e) => e.name); + + return sortVersionsDescending(versions); + } catch { + return []; + } +} + +async function getModules(versionDir: string): Promise { + try { + const entries = await fs.readdir(versionDir, { withFileTypes: true }); + return entries + .filter((e) => e.isFile() && e.name.endsWith(CONSTANTS.MDX_EXTENSION)) + .map((e) => e.name.replace(CONSTANTS.MDX_EXTENSION, "")) + .sort(); + } catch { + return []; + } +} + +export async function buildNavigation( + docsDir: string +): Promise { + const sdkRefDir = path.join(docsDir, CONSTANTS.DOCS_SDK_REF_PATH); + + if (!(await fs.pathExists(sdkRefDir))) { + log.warn(`SDK reference directory not found: ${sdkRefDir}`); + return []; + } + + const navigation: NavigationDropdownWithOrder[] = []; + + for (const [sdkKey, sdkConfig] of Object.entries(sdks)) { + const sdkDir = path.join(sdkRefDir, sdkKey); + + if (!(await fs.pathExists(sdkDir))) { + log.data(`Skipping ${sdkKey} (not found)`); + continue; + } + + const versions = await getVersions(sdkDir); + if (versions.length === 0) { + log.data(`Skipping ${sdkKey} (no versions)`); + continue; + } + + log.data(`Found ${sdkKey}: ${versions.length} versions`); + + const dropdown: NavigationDropdownWithOrder = { + dropdown: sdkConfig.displayName, + icon: sdkConfig.icon, + versions: await Promise.all( + versions.map(async (version, index) => { + const versionDir = path.join(sdkDir, version); + const modules = await getModules(versionDir); + const normalizedVersion = normalizeVersion(version); + + return { + version: normalizedVersion, + default: index === 0, + pages: modules.map( + (module) => + `${CONSTANTS.DOCS_SDK_REF_PATH}/${sdkKey}/${normalizedVersion}/${module}` + ), + }; + }) + ), + _order: sdkConfig.order, + }; + + navigation.push(dropdown); + } + + return navigation + .sort((a, b) => a._order - b._order) + .map(({ _order, ...rest }) => rest); +} + +export async function mergeNavigation( + navigation: NavigationDropdown[], + docsDir: string +): Promise { + const docsJsonPath = path.join(docsDir, "docs.json"); + + if (!(await fs.pathExists(docsJsonPath))) { + throw new Error("docs.json not found"); + } + + const docsJson = await fs.readJSON(docsJsonPath); + + const anchors = docsJson.navigation?.anchors; + if (!anchors) { + throw new Error("No anchors found in docs.json"); + } + + const validDropdowns = navigation.filter( + (d) => d.versions && d.versions.length > 0 + ); + + if (validDropdowns.length === 0) { + log.warn("No SDK versions found, keeping existing docs.json"); + return; + } + + const sdkRefAnchor = { + anchor: CONSTANTS.SDK_REFERENCE_ANCHOR, + icon: "brackets-curly", + dropdowns: validDropdowns, + }; + + const sdkRefIndex = anchors.findIndex( + (a: { anchor?: string }) => a.anchor === CONSTANTS.SDK_REFERENCE_ANCHOR + ); + + if (sdkRefIndex === -1) { + log.info(`Creating new ${CONSTANTS.SDK_REFERENCE_ANCHOR} anchor`, 1); + anchors.push(sdkRefAnchor); + } else { + anchors[sdkRefIndex] = sdkRefAnchor; + } + + await fs.writeJSON(docsJsonPath, docsJson, { spaces: 2 }); + const content = await fs.readFile(docsJsonPath, "utf-8"); + if (!content.endsWith("\n")) { + await fs.appendFile(docsJsonPath, "\n"); + } + + log.success(`Updated docs.json with ${validDropdowns.length} SDK dropdowns`); + + for (const dropdown of validDropdowns) { + const totalVersions = dropdown.versions.length; + const totalPages = dropdown.versions.reduce( + (sum, v) => sum + (v.pages?.length || 0), + 0 + ); + log.data( + `${dropdown.dropdown}: ${totalVersions} versions, ${totalPages} pages` + ); + } +} diff --git a/sdk-reference-generator/src/rebuild-docs-json.ts b/sdk-reference-generator/src/rebuild-docs-json.ts new file mode 100644 index 0000000..8547647 --- /dev/null +++ b/sdk-reference-generator/src/rebuild-docs-json.ts @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +import path from "path"; +import { fileURLToPath } from "url"; +import { buildNavigation, mergeNavigation } from "./navigation.js"; +import { verifyGeneratedDocs } from "./lib/verify.js"; +import { log } from "./lib/log.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const SCRIPT_DIR = path.resolve(__dirname, ".."); +const DOCS_DIR = path.resolve(SCRIPT_DIR, ".."); + +async function main(): Promise { + log.header("Rebuild docs.json from local SDK references"); + + log.section("Building navigation"); + const navigation = await buildNavigation(DOCS_DIR); + + if (navigation.length === 0) { + log.warn("No SDK references found locally"); + process.exit(1); + } + + log.blank(); + log.section("Merging into docs.json"); + await mergeNavigation(navigation, DOCS_DIR); + + log.blank(); + log.section("Verifying documentation"); + const verification = await verifyGeneratedDocs(DOCS_DIR); + + if (verification.warnings.length > 0) { + log.warn("Warnings detected:"); + for (const warning of verification.warnings) { + log.data(`- ${warning}`, 1); + } + } + + if (!verification.valid) { + log.blank(); + log.error("Verification failed:"); + for (const error of verification.errors) { + log.data(`- ${error}`, 1); + } + if (!verification.docsJsonValid) { + log.error("docs.json validation failed"); + } + process.exit(1); + } + + log.blank(); + log.success("docs.json rebuilt successfully"); + log.stats( + [ + { label: "Total SDKs", value: verification.stats.totalSDKs }, + { label: "Total versions", value: verification.stats.totalVersions }, + { label: "Total MDX files", value: verification.stats.totalMdxFiles }, + ], + 0 + ); +} + +main().catch((error) => { + log.error(`Fatal error: ${error.message}`); + process.exit(1); +}); diff --git a/sdk-reference-generator/src/types.ts b/sdk-reference-generator/src/types.ts new file mode 100644 index 0000000..21cb977 --- /dev/null +++ b/sdk-reference-generator/src/types.ts @@ -0,0 +1,82 @@ +type BaseSDKConfig = { + displayName: string; + icon: string; + order: number; + repo: string; + tagPattern: string; + tagFormat: string; + required: boolean; + minVersion?: string; + sdkPath?: string; + sdkPaths?: string[]; +}; + +// generator-specific config shapes +export type TypedocConfig = { + entryPoints: string[]; + exclude?: string[]; +}; + +export type PydocConfig = { + allowedPackages: readonly string[]; +}; + +// discriminated union - defaultConfig and configOverrides typed by generator +type TypedocSDKConfig = BaseSDKConfig & { + generator: "typedoc"; + defaultConfig: TypedocConfig; + configOverrides?: Record>; +}; + +type PydocSDKConfig = BaseSDKConfig & { + generator: "pydoc"; + defaultConfig: PydocConfig; + configOverrides?: Record>; +}; + +type CLISDKConfig = BaseSDKConfig & { + generator: "cli"; + // no defaultConfig/configOverrides - CLI is self-contained +}; + +export type SDKConfig = TypedocSDKConfig | PydocSDKConfig | CLISDKConfig; +export type { TypedocSDKConfig, PydocSDKConfig, CLISDKConfig }; +export type GeneratorType = SDKConfig["generator"]; + +export type ConfigFile = { + sdks: Record; +}; + +export interface GenerationContext { + tempDir: string; + docsDir: string; + configsDir: string; + limit?: number; + force?: boolean; +} + +export interface GenerationResult { + generated: number; + failed: number; + failedVersions: string[]; +} + +export interface InstallResult { + usePoetryRun: boolean; +} + +export interface NavigationVersion { + version: string; + default: boolean; + pages: string[]; +} + +export interface NavigationDropdown { + dropdown: string; + icon: string; + versions: NavigationVersion[]; +} + +export interface NavigationDropdownWithOrder extends NavigationDropdown { + _order: number; +} diff --git a/sdk-reference-generator/tsconfig.json b/sdk-reference-generator/tsconfig.json new file mode 100644 index 0000000..33193b1 --- /dev/null +++ b/sdk-reference-generator/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "declaration": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*", "sdks.config.ts"], + "exclude": ["node_modules", "dist"] +} + diff --git a/sdk-reference-generator/vitest.config.ts b/sdk-reference-generator/vitest.config.ts new file mode 100644 index 0000000..9fe1d91 --- /dev/null +++ b/sdk-reference-generator/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, +}); + diff --git a/style.css b/style.css index 490a779..e4c3329 100644 --- a/style.css +++ b/style.css @@ -61,3 +61,35 @@ code { .dark\:text-sky-200:is(.dark *) { color: rgb(255, 183, 102) !important; } + +/* Version switcher styles - subtle highlight */ +#sidebar ul:last-child:not(.sidebar-group) li button, +#sidebar ul:last-child:not(.sidebar-group) li select { + background-color: rgba(255, 136, 0, 0.1) !important; /* 10% orange tint */ + color: #ff8800 !important; /* Orange text */ + border: 1px solid rgba(255, 136, 0, 0.3) !important; /* 30% orange border */ + border-radius: 6px !important; + padding: 0.5rem 1rem !important; + font-weight: 500 !important; + transition: all 0.2s ease !important; +} + +#sidebar ul:last-child:not(.sidebar-group) li button:hover, +#sidebar ul:last-child:not(.sidebar-group) li select:hover { + background-color: rgba(255, 136, 0, 0.15) !important; /* Slightly darker on hover */ + border-color: rgba(255, 136, 0, 0.5) !important; +} + +/* Dark mode version switcher */ +.dark #sidebar ul:last-child:not(.sidebar-group) li button, +.dark #sidebar ul:last-child:not(.sidebar-group) li select { + background-color: rgba(255, 136, 0, 0.15) !important; /* Slightly more visible in dark */ + color: #ffaa44 !important; /* Lighter orange for contrast */ + border-color: rgba(255, 136, 0, 0.4) !important; +} + +.dark #sidebar ul:last-child:not(.sidebar-group) li button:hover, +.dark #sidebar ul:last-child:not(.sidebar-group) li select:hover { + background-color: rgba(255, 136, 0, 0.25) !important; + border-color: rgba(255, 136, 0, 0.6) !important; +}