diff --git a/.github/workflows/validate-sdk-compliance.yml b/.github/workflows/validate-sdk-compliance.yml index aae8d2d..35354c4 100644 --- a/.github/workflows/validate-sdk-compliance.yml +++ b/.github/workflows/validate-sdk-compliance.yml @@ -23,6 +23,10 @@ on: description: Comma-separated search paths for griffe relative to repo root (python only) type: string default: "" + typedoc-packages: + description: Comma-separated package dirs (relative to SDK root), each defining a `docs:json` script. Required when language is javascript. + type: string + default: "" jobs: validate: @@ -99,6 +103,42 @@ jobs: run: npm ci working-directory: _sdk-spec/scripts/capability-matrix + # TypeScript (pnpm): the only TS SDK is the supabase-js pnpm monorepo. + # Install it, run each package's `docs:json` to emit TypeDoc JSON (each + # package owns its own entrypoints there), then normalize + merge all + # packages into one ParseResult per branch. The package list comes from + # the caller via `typedoc-packages`. + - name: Enable corepack (TypeScript) + if: inputs.language == 'javascript' + run: corepack enable + + - name: Generate and normalize TypeScript symbols + if: inputs.language == 'javascript' + env: + TYPEDOC_PACKAGES: ${{ inputs.typedoc-packages }} + run: | + set -euo pipefail + if [ -z "${TYPEDOC_PACKAGES:-}" ]; then + echo "::error::typedoc-packages input is required when language is javascript" + exit 1 + fi + IFS=',' read -ra PKGS <<< "$(echo "$TYPEDOC_PACKAGES" | tr -d '[:space:]')" + for b in pr base; do + ROOT="$GITHUB_WORKSPACE/_sdk-$b" + ( cd "$ROOT" && pnpm install --frozen-lockfile ) + INPUTS=() + for pkg in "${PKGS[@]}"; do + [ -z "$pkg" ] && continue + ( cd "$ROOT" && pnpm -C "$pkg" run docs:json ) + # TypeDoc emits repo-root-relative file paths, so the merged + # output stays unambiguous without any prefixing. + INPUTS+=("$ROOT/$pkg/docs/v2/spec.json") + done + ( cd "$GITHUB_WORKSPACE/_sdk-spec/scripts/capability-matrix" \ + && npm run --silent normalize-typedoc -- \ + --out "$GITHUB_WORKSPACE/$b-symbols.json" "${INPUTS[@]}" ) + done + - name: Set up Python if: inputs.language == 'python' uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 @@ -184,15 +224,6 @@ jobs: done working-directory: _sdk-spec/scripts/capability-matrix - - name: Parse JavaScript symbols - if: inputs.language == 'javascript' - run: | - for b in pr base; do - npm run --silent parse-ts -- "$GITHUB_WORKSPACE/_sdk-$b" \ - > "$GITHUB_WORKSPACE/$b-symbols.json" - done - working-directory: _sdk-spec/scripts/capability-matrix - - name: Check new symbols against capability matrix run: | npm run check-api-symbols -- \ diff --git a/CLAUDE.md b/CLAUDE.md index 50455f9..0272cc9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,19 +59,18 @@ capabilities/*.yaml → validate (AJV schema) → aggregate (GitHub API fetc - `aggregate.ts` — Fetches compliance files from all SDK repos via Octokit - `generate-site.ts` — Builds the static HTML matrix site - `report.ts` — Calculates parity percentages per feature/area/language -- `ts-parser.ts` — Syntactic TypeScript AST walker; extracts public symbols without requiring `node_modules` - `swift-parser.ts` — Line-by-line Swift scanner; extracts public/open symbols from classes, structs, actors, enums, extensions -- `parse-ts.ts` — CLI wrapper for `ts-parser.ts`; takes an SDK root path and emits `ParseResult` JSON -- `parse-swift.ts` — CLI wrapper for `swift-parser.ts`; same contract as `parse-ts.ts` +- `normalize-typedoc.ts` — TypeDoc JSON normalizer; maps TypeDoc reflection kinds to `ParseResult`; defines `ParsedSymbol` and `ParseResult` types +- `normalize-typedoc-cli.ts` — CLI wrapper. Legacy form ` ` normalizes one project; merge form `--out …` normalizes and concatenates several TypeDoc JSONs into one `ParseResult` (for monorepos; TypeDoc emits repo-relative paths so no rewriting is needed) - `scripts/dart_symbol_extractor/` (sibling Dart package) — Small `package:analyzer` tool that walks `lib/**.dart` syntactically and emits the same `ParseResult` JSON; run directly with `dart run bin/extract.dart `. Parses without `pub get`; supports extension types and enhanced enums -- `parse-ignore.ts` — Loads `.sdk-parse-ignore` (gitignore syntax) to exclude paths from symbol parsing +- `parse-ignore.ts` — Loads `.sdk-parse-ignore` (gitignore syntax) to exclude paths from Swift symbol parsing (TypeScript uses TypeDoc entrypoint resolution instead) - `api-check.ts` — Diff logic: `checkNewSymbols(base, pr, compliance)` returns symbols added in PR not in the compliance file - `check-api-symbols.ts` — CLI; compares two `ParseResult` files against `sdk-compliance.yaml`, exits 1 with a clear error on uncovered symbols ### CI Workflows - `validate-capabilities.yml` — Runs on push to main, PRs, and nightly; Tier 1: schema/tests/typecheck/structural; Tier 2 (PRs + nightly): reference checks against GitHub -- `validate-sdk-compliance.yml` — **Reusable workflow** called by SDK repos; validates `sdk-compliance.yaml` and blocks PRs that add public symbols not registered in the compliance file (requires `language` input: `swift`, `javascript`, or `dart`) +- `validate-sdk-compliance.yml` — **Reusable workflow** called by SDK repos; validates `sdk-compliance.yaml` and blocks PRs that add public symbols not registered in the compliance file (requires `language` input: `swift`, `javascript`, `python`, or `dart`). For `javascript` (the supabase-js pnpm monorepo) pass `typedoc-packages` — comma-separated package dirs, each with a `docs:json` script that owns its TypeDoc entrypoints; the JS path installs with pnpm and merges all packages - `aggregate-capabilities.yml` — Hourly cron that fetches all SDK compliance data and rebuilds the site - `deploy-pages.yml` — Deploys to GitHub Pages on main push diff --git a/scripts/capability-matrix/package.json b/scripts/capability-matrix/package.json index 8eb0dbb..2f75e49 100644 --- a/scripts/capability-matrix/package.json +++ b/scripts/capability-matrix/package.json @@ -8,7 +8,7 @@ "validate": "tsx src/cli.ts validate", "validate:online": "tsx src/cli.ts validate --online", "validate-compliance": "tsx src/compliance-cli.ts", - "parse-ts": "tsx src/parse-ts.ts", + "normalize-typedoc": "tsx src/normalize-typedoc-cli.ts", "normalize-symbolgraph": "tsx src/normalize-symbolgraph-cli.ts", "normalize-griffe": "tsx src/normalize-griffe-cli.ts", "check-api-symbols": "tsx src/check-api-symbols.ts", diff --git a/scripts/capability-matrix/src/api-check.ts b/scripts/capability-matrix/src/api-check.ts index 19485b9..495941c 100644 --- a/scripts/capability-matrix/src/api-check.ts +++ b/scripts/capability-matrix/src/api-check.ts @@ -1,6 +1,6 @@ import { buildSymbolIndex } from "./compliance.js"; import type { RawCompliance } from "./compliance.js"; -import type { ParsedSymbol } from "./ts-parser.js"; +import type { ParsedSymbol } from "./normalize-typedoc.js"; export interface CheckResult { newSymbols: string[]; diff --git a/scripts/capability-matrix/src/check-api-symbols.ts b/scripts/capability-matrix/src/check-api-symbols.ts index 62ebe1f..11fe4b9 100644 --- a/scripts/capability-matrix/src/check-api-symbols.ts +++ b/scripts/capability-matrix/src/check-api-symbols.ts @@ -3,7 +3,7 @@ import { resolve } from "node:path"; import { parse } from "yaml"; import { checkNewSymbols, formatErrorMessage, formatRemovedMessage } from "./api-check.js"; import type { RawCompliance } from "./compliance.js"; -import type { ParseResult } from "./ts-parser.js"; +import type { ParseResult } from "./normalize-typedoc.js"; async function main(): Promise { const [prFile, baseFile, compliancePath] = process.argv.slice(2); diff --git a/scripts/capability-matrix/src/normalize-griffe.ts b/scripts/capability-matrix/src/normalize-griffe.ts index 562ddc5..5cb3bcc 100644 --- a/scripts/capability-matrix/src/normalize-griffe.ts +++ b/scripts/capability-matrix/src/normalize-griffe.ts @@ -1,5 +1,5 @@ import { basename, relative } from "node:path"; -import type { ParsedSymbol, ParseResult } from "./ts-parser.js"; +import type { ParsedSymbol, ParseResult } from "./normalize-typedoc.js"; import { loadIgnore, type Ignore } from "./parse-ignore.js"; export type { ParsedSymbol, ParseResult }; diff --git a/scripts/capability-matrix/src/normalize-symbolgraph.ts b/scripts/capability-matrix/src/normalize-symbolgraph.ts index 4b2d644..3aeeac7 100644 --- a/scripts/capability-matrix/src/normalize-symbolgraph.ts +++ b/scripts/capability-matrix/src/normalize-symbolgraph.ts @@ -1,5 +1,5 @@ import { relative } from "node:path"; -import type { ParsedSymbol, ParseResult } from "./ts-parser.js"; +import type { ParsedSymbol, ParseResult } from "./normalize-typedoc.js"; export type { ParsedSymbol, ParseResult }; export interface SymbolGraphSymbol { diff --git a/scripts/capability-matrix/src/normalize-typedoc-cli.ts b/scripts/capability-matrix/src/normalize-typedoc-cli.ts new file mode 100644 index 0000000..9238db1 --- /dev/null +++ b/scripts/capability-matrix/src/normalize-typedoc-cli.ts @@ -0,0 +1,41 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { mergeProjects } from "./normalize-typedoc.js"; + +// Usage: +// normalize-typedoc (single project) +// normalize-typedoc --out ... (merge N projects) +// +// The merge form concatenates the symbols of several TypeDoc project JSONs into +// one ParseResult — used for monorepos where each package is documented +// separately. TypeDoc emits repo-root-relative file paths, so no path rewriting +// is needed. + +const argv = process.argv.slice(2); +const outIdx = argv.indexOf("--out"); + +let inputs: string[]; +let outputPath: string | undefined; + +if (outIdx !== -1) { + outputPath = argv[outIdx + 1]; + inputs = argv.filter((_, i) => i !== outIdx && i !== outIdx + 1); +} else { + // Legacy single-project form: + const [inputPath, legacyOut] = argv; + inputs = inputPath ? [inputPath] : []; + outputPath = legacyOut; +} + +if (!outputPath || inputs.length === 0) { + console.error( + [ + "Usage:", + " normalize-typedoc ", + " normalize-typedoc --out ...", + ].join("\n"), + ); + process.exit(1); +} + +const projects = inputs.map((path) => JSON.parse(readFileSync(path, "utf8"))); +writeFileSync(outputPath, JSON.stringify(mergeProjects(projects), null, 2)); diff --git a/scripts/capability-matrix/src/normalize-typedoc.ts b/scripts/capability-matrix/src/normalize-typedoc.ts new file mode 100644 index 0000000..3096b2c --- /dev/null +++ b/scripts/capability-matrix/src/normalize-typedoc.ts @@ -0,0 +1,113 @@ +export interface ParsedSymbol { + name: string; + kind: "class" | "method" | "property" | "function" | "variable"; + file: string; + line?: number; +} + +export interface ParseResult { + symbols: ParsedSymbol[]; +} + +const Kind = { + Module: 2, + Namespace: 4, + Enum: 8, + EnumMember: 16, + Variable: 32, + Function: 64, + Class: 128, + Interface: 256, + Constructor: 512, + Property: 1024, + Method: 2048, + Accessor: 262144, + TypeAlias: 2097152, + Reference: 4194304, +} as const; + +interface TdReflection { + name: string; + kind: number; + flags?: { isPrivate?: boolean; isProtected?: boolean }; + sources?: Array<{ fileName: string; line?: number }>; + children?: TdReflection[]; +} + +function sourceOf(r: TdReflection): { file: string; line?: number } { + const src = r.sources?.[0]; + return { file: src?.fileName ?? "", line: src?.line }; +} + +function isExcluded(r: TdReflection): boolean { + return !!(r.flags?.isPrivate || r.flags?.isProtected); +} + +function extractMembers( + parent: string, + children: TdReflection[], + out: ParsedSymbol[], +): void { + for (const child of children) { + if (isExcluded(child)) continue; + if (child.kind === Kind.Constructor) continue; + const qualName = `${parent}.${child.name}`; + const { file, line } = sourceOf(child); + if (child.kind === Kind.Method) { + out.push({ name: qualName, kind: "method", file, line }); + } else if (child.kind === Kind.Property) { + out.push({ name: qualName, kind: "property", file, line }); + } else if (child.kind === Kind.Accessor) { + out.push({ name: qualName, kind: "method", file, line }); + } else if (child.kind === Kind.EnumMember) { + out.push({ name: qualName, kind: "property", file, line }); + } + } +} + +function extractDeclarations( + children: TdReflection[], + out: ParsedSymbol[], +): void { + for (const child of children) { + if (isExcluded(child)) continue; + const { file, line } = sourceOf(child); + if (child.kind === Kind.Module || child.kind === Kind.Namespace) { + if (child.children) extractDeclarations(child.children, out); + } else if (child.kind === Kind.Reference) { + continue; + } else if ( + child.kind === Kind.Class || + child.kind === Kind.Interface || + child.kind === Kind.Enum + ) { + out.push({ name: child.name, kind: "class", file, line }); + if (child.children) extractMembers(child.name, child.children, out); + } else if (child.kind === Kind.Function) { + out.push({ name: child.name, kind: "function", file, line }); + } else if (child.kind === Kind.Variable || child.kind === Kind.TypeAlias) { + out.push({ name: child.name, kind: "variable", file, line }); + } + } +} + +export function normalize(json: unknown): ParseResult { + const project = json as TdReflection; + const symbols: ParsedSymbol[] = []; + if (project.children) extractDeclarations(project.children, symbols); + return { symbols }; +} + +/** + * Normalize one or more TypeDoc project JSONs into a single `ParseResult` by + * concatenating their symbols. Used for monorepos where each package is + * documented separately. TypeDoc already emits repo-root-relative `fileName`s, + * so no path rewriting is needed. Symbol names are kept as-is (the API-check + * diff is name-based), so this is a plain concatenation — no cross-package + * deduping; duplicate re-exports are collapsed by name downstream. + */ +export function mergeProjects(projects: unknown[]): ParseResult { + const symbols: ParsedSymbol[] = []; + for (const json of projects) symbols.push(...normalize(json).symbols); + return { symbols }; +} diff --git a/scripts/capability-matrix/src/parse-ts.ts b/scripts/capability-matrix/src/parse-ts.ts deleted file mode 100644 index ce92c70..0000000 --- a/scripts/capability-matrix/src/parse-ts.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { parseTypeScriptProject } from "./ts-parser.js"; - -async function main(): Promise { - const projectPath = process.argv[2]; - if (!projectPath) { - console.error("Usage: parse-ts "); - process.exit(1); - } - - try { - const result = parseTypeScriptProject(projectPath); - console.log(JSON.stringify(result, null, 2)); - } catch (e) { - console.error(`Error: ${(e as Error).message}`); - process.exit(1); - } -} - -main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/scripts/capability-matrix/src/ts-parser.ts b/scripts/capability-matrix/src/ts-parser.ts deleted file mode 100644 index 5846ba7..0000000 --- a/scripts/capability-matrix/src/ts-parser.ts +++ /dev/null @@ -1,145 +0,0 @@ -import ts from "typescript"; -import { readFileSync, readdirSync, existsSync } from "node:fs"; -import { join, resolve, relative } from "node:path"; -import { loadIgnore, type Ignore } from "./parse-ignore.js"; - -export interface ParsedSymbol { - name: string; - kind: "class" | "method" | "property" | "function" | "variable"; - file: string; - line?: number; -} - -export interface ParseResult { - symbols: ParsedSymbol[]; -} - -// --------------------------------------------------------------------------- -// Internal helpers -// --------------------------------------------------------------------------- - -function findSourceFiles(dir: string, root: string, ig: Ignore): string[] { - const results: string[] = []; - try { - for (const entry of readdirSync(dir, { withFileTypes: true })) { - if (entry.name.startsWith(".")) continue; - const full = join(dir, entry.name); - const rel = relative(root, full); - if (entry.isDirectory()) { - if (ig.ignores(rel + "/")) continue; - results.push(...findSourceFiles(full, root, ig)); - } else if (entry.isFile() && entry.name.endsWith(".ts")) { - if (ig.ignores(rel)) continue; - results.push(full); - } - } - } catch { /* ignore unreadable dirs */ } - return results; -} - -function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean { - if (!ts.canHaveModifiers(node)) return false; - const mods = ts.getModifiers(node); - return mods?.some((m) => m.kind === kind) ?? false; -} - -function isExported(node: ts.Node): boolean { - return hasModifier(node, ts.SyntaxKind.ExportKeyword); -} - -function isPublicMember(member: ts.ClassElement): boolean { - if (member.name?.kind === ts.SyntaxKind.PrivateIdentifier) return false; - if (hasModifier(member, ts.SyntaxKind.PrivateKeyword)) return false; - if (hasModifier(member, ts.SyntaxKind.ProtectedKeyword)) return false; - return true; -} - -function memberIdentifierName(member: ts.ClassElement): string | undefined { - const n = member.name; - if (!n) return undefined; - if (ts.isIdentifier(n)) return n.text; - if (ts.isStringLiteral(n)) return n.text; - return undefined; -} - -function extractClassMembers( - className: string, - node: ts.ClassDeclaration, - relPath: string, - sf: ts.SourceFile, - out: ParsedSymbol[], -): void { - for (const member of node.members) { - if (ts.isConstructorDeclaration(member)) continue; - if (!isPublicMember(member)) continue; - - const name = memberIdentifierName(member); - if (!name) continue; - - const kind = - ts.isMethodDeclaration(member) || - ts.isGetAccessorDeclaration(member) || - ts.isSetAccessorDeclaration(member) - ? "method" - : "property"; - - const line = sf.getLineAndCharacterOfPosition(member.getStart(sf)).line + 1; - out.push({ name: `${className}.${name}`, kind, file: relPath, line }); - } -} - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -export function extractFromSource( - source: string, - relPath: string, -): ParsedSymbol[] { - const sf = ts.createSourceFile(relPath, source, ts.ScriptTarget.Latest, true); - const symbols: ParsedSymbol[] = []; - - for (const stmt of sf.statements) { - if (ts.isClassDeclaration(stmt) && isExported(stmt)) { - const className = stmt.name?.text; - if (className) { - const line = sf.getLineAndCharacterOfPosition(stmt.getStart(sf)).line + 1; - symbols.push({ name: className, kind: "class", file: relPath, line }); - extractClassMembers(className, stmt, relPath, sf, symbols); - } - } else if (ts.isFunctionDeclaration(stmt) && isExported(stmt)) { - const name = stmt.name?.text; - if (name) { - const line = sf.getLineAndCharacterOfPosition(stmt.getStart(sf)).line + 1; - symbols.push({ name, kind: "function", file: relPath, line }); - } - } else if (ts.isVariableStatement(stmt) && isExported(stmt)) { - for (const decl of stmt.declarationList.declarations) { - if (ts.isIdentifier(decl.name)) { - const line = sf.getLineAndCharacterOfPosition(decl.getStart(sf)).line + 1; - symbols.push({ name: decl.name.text, kind: "variable", file: relPath, line }); - } - } - } - } - - return symbols; -} - -export function parseTypeScriptProject(projectRoot: string): ParseResult { - const root = resolve(projectRoot); - const ig = loadIgnore(root); - const srcDir = join(root, "src"); - const scanRoot = existsSync(srcDir) ? srcDir : root; - - const files = findSourceFiles(scanRoot, root, ig); - const symbols: ParsedSymbol[] = []; - - for (const file of files) { - const source = readFileSync(file, "utf8"); - const relPath = relative(root, file); - symbols.push(...extractFromSource(source, relPath)); - } - - return { symbols }; -} diff --git a/scripts/capability-matrix/test/api-check.test.ts b/scripts/capability-matrix/test/api-check.test.ts index 41f180b..4f8aff1 100644 --- a/scripts/capability-matrix/test/api-check.test.ts +++ b/scripts/capability-matrix/test/api-check.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import { checkNewSymbols, formatErrorMessage, formatRemovedMessage } from "../src/api-check"; -import type { ParsedSymbol } from "../src/ts-parser"; +import type { ParsedSymbol } from "../src/normalize-typedoc"; function sym(name: string, line?: number): ParsedSymbol { const s: ParsedSymbol = { name, kind: "method", file: "src/index.ts" }; diff --git a/scripts/capability-matrix/test/fixtures/ts-sample/src/index.ts b/scripts/capability-matrix/test/fixtures/ts-sample/src/index.ts deleted file mode 100644 index c49da69..0000000 --- a/scripts/capability-matrix/test/fixtures/ts-sample/src/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -export class AuthClient { - public signUp(email: string, password: string): Promise { - return Promise.resolve(); - } - - public signIn(email: string): Promise { - return Promise.resolve(); - } - - get session(): string | null { - return null; - } - - private _token: string | null = null; - - protected _refresh(): void {} - - #privateField = "hidden"; -} - -export class StorageClient { - public upload(path: string): Promise { - return Promise.resolve(); - } -} - -export function createClient(url: string, key: string): AuthClient { - return new AuthClient(); -} - -export const version = "1.0.0"; - -// Not exported — must not appear in output -class InternalHelper { - public doSomething(): void {} -} - -function internalUtil(): void {} diff --git a/scripts/capability-matrix/test/fixtures/typedoc-sample.json b/scripts/capability-matrix/test/fixtures/typedoc-sample.json new file mode 100644 index 0000000..5ad5e8e --- /dev/null +++ b/scripts/capability-matrix/test/fixtures/typedoc-sample.json @@ -0,0 +1,675 @@ +{ + "id": 0, + "name": "fixture", + "variant": "project", + "kind": 1, + "flags": {}, + "children": [ + { + "id": 20, + "name": "UserRole", + "variant": "declaration", + "kind": 8, + "flags": {}, + "children": [ + { + "id": 21, + "name": "Admin", + "variant": "declaration", + "kind": 16, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 15, + "character": 2 + } + ], + "type": { + "type": "literal", + "value": "admin" + } + }, + { + "id": 22, + "name": "User", + "variant": "declaration", + "kind": 16, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 16, + "character": 2 + } + ], + "type": { + "type": "literal", + "value": "user" + } + } + ], + "groups": [ + { + "title": "Enumeration Members", + "children": [ + 21, + 22 + ] + } + ], + "sources": [ + { + "fileName": "index.ts", + "line": 14, + "character": 12 + } + ] + }, + { + "id": 4, + "name": "AuthClient", + "variant": "declaration", + "kind": 128, + "flags": {}, + "children": [ + { + "id": 5, + "name": "constructor", + "variant": "declaration", + "kind": 512, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 2, + "character": 2 + } + ], + "signatures": [ + { + "id": 6, + "name": "AuthClient", + "variant": "signature", + "kind": 16384, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 2, + "character": 2 + } + ], + "parameters": [ + { + "id": 7, + "name": "url", + "variant": "param", + "kind": 32768, + "flags": {}, + "type": { + "type": "intrinsic", + "name": "string" + } + } + ], + "type": { + "type": "reference", + "target": 4, + "name": "AuthClient", + "package": "fixture" + } + } + ] + }, + { + "id": 14, + "name": "session", + "variant": "declaration", + "kind": 262144, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 5, + "character": 6 + } + ], + "getSignature": { + "id": 15, + "name": "session", + "variant": "signature", + "kind": 524288, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 5, + "character": 6 + } + ], + "type": { + "type": "intrinsic", + "name": "string" + } + } + }, + { + "id": 12, + "name": "signIn", + "variant": "declaration", + "kind": 2048, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 4, + "character": 2 + } + ], + "signatures": [ + { + "id": 13, + "name": "signIn", + "variant": "signature", + "kind": 4096, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 4, + "character": 2 + } + ], + "type": { + "type": "intrinsic", + "name": "void" + } + } + ] + }, + { + "id": 9, + "name": "signUp", + "variant": "declaration", + "kind": 2048, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 3, + "character": 2 + } + ], + "signatures": [ + { + "id": 10, + "name": "signUp", + "variant": "signature", + "kind": 4096, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 3, + "character": 2 + } + ], + "parameters": [ + { + "id": 11, + "name": "email", + "variant": "param", + "kind": 32768, + "flags": {}, + "type": { + "type": "intrinsic", + "name": "string" + } + } + ], + "type": { + "type": "intrinsic", + "name": "void" + } + } + ] + } + ], + "groups": [ + { + "title": "Constructors", + "children": [ + 5 + ] + }, + { + "title": "Accessors", + "children": [ + 14 + ] + }, + { + "title": "Methods", + "children": [ + 12, + 9 + ] + } + ], + "sources": [ + { + "fileName": "index.ts", + "line": 1, + "character": 13 + } + ] + }, + { + "id": 17, + "name": "Session", + "variant": "declaration", + "kind": 256, + "flags": {}, + "children": [ + { + "id": 19, + "name": "expires", + "variant": "declaration", + "kind": 1024, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 11, + "character": 2 + } + ], + "type": { + "type": "reference", + "target": { + "sourceFileName": "node_modules/typescript/lib/lib.es5.d.ts", + "qualifiedName": "Date" + }, + "name": "Date", + "package": "typescript" + } + }, + { + "id": 18, + "name": "user", + "variant": "declaration", + "kind": 1024, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 10, + "character": 2 + } + ], + "type": { + "type": "intrinsic", + "name": "string" + } + } + ], + "groups": [ + { + "title": "Properties", + "children": [ + 19, + 18 + ] + } + ], + "sources": [ + { + "fileName": "index.ts", + "line": 9, + "character": 17 + } + ] + }, + { + "id": 23, + "name": "AuthResponse", + "variant": "declaration", + "kind": 2097152, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 19, + "character": 12 + } + ], + "type": { + "type": "reflection", + "declaration": { + "id": 24, + "name": "__type", + "variant": "declaration", + "kind": 65536, + "flags": {}, + "children": [ + { + "id": 25, + "name": "data", + "variant": "declaration", + "kind": 1024, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 19, + "character": 29 + } + ], + "type": { + "type": "intrinsic", + "name": "unknown" + } + }, + { + "id": 26, + "name": "error", + "variant": "declaration", + "kind": 1024, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 19, + "character": 44 + } + ], + "type": { + "type": "union", + "types": [ + { + "type": "reference", + "target": { + "sourceFileName": "node_modules/typescript/lib/lib.es5.d.ts", + "qualifiedName": "Error" + }, + "name": "Error", + "package": "typescript" + }, + { + "type": "literal", + "value": null + } + ] + } + } + ], + "groups": [ + { + "title": "Properties", + "children": [ + 25, + 26 + ] + } + ], + "sources": [ + { + "fileName": "index.ts", + "line": 19, + "character": 27 + } + ] + } + } + }, + { + "id": 27, + "name": "VERSION", + "variant": "declaration", + "kind": 32, + "flags": { + "isConst": true + }, + "sources": [ + { + "fileName": "index.ts", + "line": 25, + "character": 13 + } + ], + "type": { + "type": "literal", + "value": "1.0.0" + }, + "defaultValue": "\"1.0.0\"" + }, + { + "id": 1, + "name": "createClient", + "variant": "declaration", + "kind": 64, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 21, + "character": 16 + } + ], + "signatures": [ + { + "id": 2, + "name": "createClient", + "variant": "signature", + "kind": 4096, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 21, + "character": 16 + } + ], + "parameters": [ + { + "id": 3, + "name": "url", + "variant": "param", + "kind": 32768, + "flags": {}, + "type": { + "type": "intrinsic", + "name": "string" + } + } + ], + "type": { + "type": "reference", + "target": 4, + "name": "AuthClient", + "package": "fixture" + } + } + ] + }, + { + "id": 28, + "name": "Client", + "variant": "reference", + "kind": 4194304, + "flags": {}, + "sources": [ + { + "fileName": "index.ts", + "line": 27, + "character": 23 + } + ], + "target": 4 + } + ], + "groups": [ + { + "title": "Enumerations", + "children": [ + 20 + ] + }, + { + "title": "Classes", + "children": [ + 4 + ] + }, + { + "title": "Interfaces", + "children": [ + 17 + ] + }, + { + "title": "Type Aliases", + "children": [ + 23 + ] + }, + { + "title": "Variables", + "children": [ + 27 + ] + }, + { + "title": "Functions", + "children": [ + 1 + ] + }, + { + "title": "References", + "children": [ + 28 + ] + } + ], + "packageName": "fixture", + "symbolIdMap": { + "0": { + "sourceFileName": "src/index.ts", + "qualifiedName": "" + }, + "1": { + "sourceFileName": "src/index.ts", + "qualifiedName": "createClient" + }, + "2": { + "sourceFileName": "src/index.ts", + "qualifiedName": "createClient" + }, + "3": { + "sourceFileName": "src/index.ts", + "qualifiedName": "url" + }, + "4": { + "sourceFileName": "src/index.ts", + "qualifiedName": "AuthClient" + }, + "5": { + "sourceFileName": "src/index.ts", + "qualifiedName": "AuthClient.__constructor" + }, + "6": { + "sourceFileName": "src/index.ts", + "qualifiedName": "AuthClient" + }, + "7": { + "sourceFileName": "src/index.ts", + "qualifiedName": "url" + }, + "9": { + "sourceFileName": "src/index.ts", + "qualifiedName": "AuthClient.signUp" + }, + "10": { + "sourceFileName": "src/index.ts", + "qualifiedName": "AuthClient.signUp" + }, + "11": { + "sourceFileName": "src/index.ts", + "qualifiedName": "email" + }, + "12": { + "sourceFileName": "src/index.ts", + "qualifiedName": "AuthClient.signIn" + }, + "13": { + "sourceFileName": "src/index.ts", + "qualifiedName": "AuthClient.signIn" + }, + "14": { + "sourceFileName": "src/index.ts", + "qualifiedName": "AuthClient.session" + }, + "15": { + "sourceFileName": "src/index.ts", + "qualifiedName": "AuthClient.session" + }, + "17": { + "sourceFileName": "src/index.ts", + "qualifiedName": "Session" + }, + "18": { + "sourceFileName": "src/index.ts", + "qualifiedName": "Session.user" + }, + "19": { + "sourceFileName": "src/index.ts", + "qualifiedName": "Session.expires" + }, + "20": { + "sourceFileName": "src/index.ts", + "qualifiedName": "UserRole" + }, + "21": { + "sourceFileName": "src/index.ts", + "qualifiedName": "UserRole.Admin" + }, + "22": { + "sourceFileName": "src/index.ts", + "qualifiedName": "UserRole.User" + }, + "23": { + "sourceFileName": "src/index.ts", + "qualifiedName": "AuthResponse" + }, + "24": { + "sourceFileName": "src/index.ts", + "qualifiedName": "__type" + }, + "25": { + "sourceFileName": "src/index.ts", + "qualifiedName": "__type.data" + }, + "26": { + "sourceFileName": "src/index.ts", + "qualifiedName": "__type.error" + }, + "27": { + "sourceFileName": "src/index.ts", + "qualifiedName": "VERSION" + }, + "28": { + "sourceFileName": "src/index.ts", + "qualifiedName": "Client" + } + }, + "files": { + "entries": { + "1": "src/index.ts" + }, + "reflections": { + "1": 0 + } + } +} diff --git a/scripts/capability-matrix/test/normalize-typedoc.test.ts b/scripts/capability-matrix/test/normalize-typedoc.test.ts new file mode 100644 index 0000000..92aa505 --- /dev/null +++ b/scripts/capability-matrix/test/normalize-typedoc.test.ts @@ -0,0 +1,258 @@ +import { describe, it, expect } from "vitest"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { readFileSync } from "node:fs"; +import { normalize, mergeProjects } from "../src/normalize-typedoc.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function project(...children: object[]) { + return { kind: 1, name: "test", children }; +} +function mod(name: string, ...children: object[]) { + return { kind: 2, name, flags: {}, children }; +} +function cls(name: string, file: string, ...members: object[]) { + return { kind: 128, name, flags: {}, sources: [{ fileName: file }], children: members }; +} +function iface(name: string, file: string, ...members: object[]) { + return { kind: 256, name, flags: {}, sources: [{ fileName: file }], children: members }; +} +function enumDecl(name: string, file: string, ...members: object[]) { + return { kind: 8, name, flags: {}, sources: [{ fileName: file }], children: members }; +} +function method(name: string, file: string) { + return { kind: 2048, name, flags: {}, sources: [{ fileName: file }] }; +} +function prop(name: string, file: string) { + return { kind: 1024, name, flags: {}, sources: [{ fileName: file }] }; +} +function accessor(name: string, file: string) { + return { kind: 262144, name, flags: {}, sources: [{ fileName: file }] }; +} +function ctor(file: string) { + return { kind: 512, name: "constructor", flags: {}, sources: [{ fileName: file }] }; +} +function enumMember(name: string, file: string) { + return { kind: 16, name, flags: {}, sources: [{ fileName: file }] }; +} +function fn(name: string, file: string) { + return { kind: 64, name, flags: {}, sources: [{ fileName: file }] }; +} +function variable(name: string, file: string) { + return { kind: 32, name, flags: {}, sources: [{ fileName: file }] }; +} +function typeAlias(name: string, file: string) { + return { kind: 2097152, name, flags: {}, sources: [{ fileName: file }] }; +} +function ref(name: string) { + return { kind: 4194304, name, flags: {} }; +} +function privateFlag(base: object): object { + return { ...base, flags: { isPrivate: true } }; +} +function protectedFlag(base: object): object { + return { ...base, flags: { isProtected: true } }; +} + +describe("normalize — class", () => { + it("emits class symbol", () => { + const result = normalize(project(cls("AuthClient", "src/auth.ts"))); + expect(result.symbols).toContainEqual({ name: "AuthClient", kind: "class", file: "src/auth.ts" }); + }); + + it("emits class method", () => { + const result = normalize(project(cls("AuthClient", "src/auth.ts", method("signUp", "src/auth.ts")))); + expect(result.symbols).toContainEqual({ name: "AuthClient.signUp", kind: "method", file: "src/auth.ts" }); + }); + + it("emits class property", () => { + const result = normalize(project(cls("AuthClient", "src/auth.ts", prop("session", "src/auth.ts")))); + expect(result.symbols).toContainEqual({ name: "AuthClient.session", kind: "property", file: "src/auth.ts" }); + }); + + it("emits accessor as method kind", () => { + const result = normalize(project(cls("AuthClient", "src/auth.ts", accessor("token", "src/auth.ts")))); + expect(result.symbols).toContainEqual({ name: "AuthClient.token", kind: "method", file: "src/auth.ts" }); + }); + + it("skips constructor", () => { + const result = normalize(project(cls("Foo", "src/foo.ts", ctor("src/foo.ts"), method("bar", "src/foo.ts")))); + const names = result.symbols.map(s => s.name); + expect(names).not.toContain("Foo.constructor"); + expect(names).toContain("Foo.bar"); + }); +}); + +describe("normalize — interface", () => { + it("emits interface as class kind", () => { + const result = normalize(project(iface("Session", "src/session.ts"))); + expect(result.symbols).toContainEqual({ name: "Session", kind: "class", file: "src/session.ts" }); + }); + + it("emits interface members as property", () => { + const result = normalize(project(iface("Session", "src/session.ts", prop("user", "src/session.ts")))); + expect(result.symbols).toContainEqual({ name: "Session.user", kind: "property", file: "src/session.ts" }); + }); +}); + +describe("normalize — enum", () => { + it("emits enum as class kind", () => { + const result = normalize(project(enumDecl("UserRole", "src/role.ts"))); + expect(result.symbols).toContainEqual({ name: "UserRole", kind: "class", file: "src/role.ts" }); + }); + + it("emits enum member as property kind", () => { + const result = normalize(project(enumDecl("UserRole", "src/role.ts", enumMember("Admin", "src/role.ts")))); + expect(result.symbols).toContainEqual({ name: "UserRole.Admin", kind: "property", file: "src/role.ts" }); + }); +}); + +describe("normalize — top-level declarations", () => { + it("emits exported function", () => { + const result = normalize(project(fn("createClient", "src/index.ts"))); + expect(result.symbols).toContainEqual({ name: "createClient", kind: "function", file: "src/index.ts" }); + }); + + it("emits exported variable", () => { + const result = normalize(project(variable("VERSION", "src/index.ts"))); + expect(result.symbols).toContainEqual({ name: "VERSION", kind: "variable", file: "src/index.ts" }); + }); + + it("emits type alias as variable kind", () => { + const result = normalize(project(typeAlias("AuthResponse", "src/types.ts"))); + expect(result.symbols).toContainEqual({ name: "AuthResponse", kind: "variable", file: "src/types.ts" }); + }); + + it("skips Reference kind", () => { + const result = normalize(project(cls("AuthClient", "src/auth.ts"), ref("Client"))); + const names = result.symbols.map(s => s.name); + expect(names).not.toContain("Client"); + expect(names).toContain("AuthClient"); + }); +}); + +describe("normalize — traversal", () => { + it("walks into Module wrapper (kind 2)", () => { + const result = normalize(project(mod("src/auth", cls("AuthClient", "src/auth.ts")))); + expect(result.symbols).toContainEqual({ name: "AuthClient", kind: "class", file: "src/auth.ts" }); + }); + + it("walks into Namespace wrapper (kind 4)", () => { + const ns = { kind: 4, name: "Utils", flags: {}, children: [fn("helper", "src/utils.ts")] }; + const result = normalize(project(ns)); + expect(result.symbols).toContainEqual({ name: "helper", kind: "function", file: "src/utils.ts" }); + }); + + it("captures file path from sources[0].fileName", () => { + const result = normalize(project(fn("foo", "packages/core/src/index.ts"))); + expect(result.symbols[0]?.file).toBe("packages/core/src/index.ts"); + }); +}); + +describe("normalize — privacy (defensive filter)", () => { + it("skips member with isPrivate flag", () => { + const result = normalize(project( + cls("Foo", "src/foo.ts", privateFlag(prop("secret", "src/foo.ts"))) + )); + expect(result.symbols.map(s => s.name)).not.toContain("Foo.secret"); + }); + + it("skips member with isProtected flag", () => { + const result = normalize(project( + cls("Foo", "src/foo.ts", protectedFlag(prop("internal", "src/foo.ts"))) + )); + expect(result.symbols.map(s => s.name)).not.toContain("Foo.internal"); + }); +}); + +describe("normalize (fixture — real TypeDoc 0.27 output)", () => { + const fixture = JSON.parse( + readFileSync(join(__dirname, "fixtures/typedoc-sample.json"), "utf8") + ); + + it("finds AuthClient class", () => { + const names = normalize(fixture).symbols.map(s => s.name); + expect(names).toContain("AuthClient"); + }); + + it("finds AuthClient.signUp method", () => { + const names = normalize(fixture).symbols.map(s => s.name); + expect(names).toContain("AuthClient.signUp"); + }); + + it("finds Session interface as class kind", () => { + const sym = normalize(fixture).symbols.find(s => s.name === "Session"); + expect(sym?.kind).toBe("class"); + }); + + it("finds UserRole enum as class kind", () => { + const sym = normalize(fixture).symbols.find(s => s.name === "UserRole"); + expect(sym?.kind).toBe("class"); + }); + + it("finds AuthResponse type alias as variable kind", () => { + const sym = normalize(fixture).symbols.find(s => s.name === "AuthResponse"); + expect(sym?.kind).toBe("variable"); + }); + + it("finds createClient function", () => { + const sym = normalize(fixture).symbols.find(s => s.name === "createClient"); + expect(sym?.kind).toBe("function"); + }); + + it("finds VERSION variable", () => { + const sym = normalize(fixture).symbols.find(s => s.name === "VERSION"); + expect(sym?.kind).toBe("variable"); + }); + + it("does not emit Client (re-export Reference)", () => { + const names = normalize(fixture).symbols.map(s => s.name); + expect(names).not.toContain("Client"); + }); + + it("does not emit constructor", () => { + const names = normalize(fixture).symbols.map(s => s.name); + expect(names).not.toContain("AuthClient.constructor"); + }); + + it("emits AuthClient.session accessor as method kind from fixture", () => { + const sym = normalize(fixture).symbols.find(s => s.name === "AuthClient.session"); + expect(sym?.kind).toBe("method"); + }); +}); + +describe("mergeProjects", () => { + it("behaves like normalize for a single project (back-compat)", () => { + const json = project(cls("AuthClient", "src/auth.ts", method("signUp", "src/auth.ts"))); + expect(mergeProjects([json])).toEqual(normalize(json)); + }); + + it("concatenates symbols from multiple projects", () => { + const a = project(cls("AuthClient", "packages/core/auth-js/src/index.ts")); + const b = project(cls("StorageClient", "packages/core/storage-js/src/index.ts")); + const names = mergeProjects([a, b]).symbols.map(s => s.name); + expect(names).toContain("AuthClient"); + expect(names).toContain("StorageClient"); + }); + + it("preserves each project's (repo-relative) file paths", () => { + const a = project(cls("AuthClient", "packages/core/auth-js/src/index.ts")); + const b = project(cls("StorageClient", "packages/core/storage-js/src/index.ts")); + const merged = mergeProjects([a, b]); + expect(merged.symbols.find(s => s.name === "AuthClient")?.file) + .toBe("packages/core/auth-js/src/index.ts"); + expect(merged.symbols.find(s => s.name === "StorageClient")?.file) + .toBe("packages/core/storage-js/src/index.ts"); + }); + + it("keeps duplicate re-exported names (deduped by name downstream)", () => { + // supabase-js re-exports FunctionsClient from functions-js, so the same + // name legitimately appears in two projects. mergeProjects keeps both; + // checkNewSymbols collapses them by name. + const fns = project(cls("FunctionsClient", "packages/core/functions-js/src/index.ts")); + const sb = project(cls("FunctionsClient", "packages/core/functions-js/dist/types.d.ts")); + const names = mergeProjects([fns, sb]).symbols.map(s => s.name).filter(n => n === "FunctionsClient"); + expect(names).toHaveLength(2); + }); +}); diff --git a/scripts/capability-matrix/test/ts-parser.test.ts b/scripts/capability-matrix/test/ts-parser.test.ts deleted file mode 100644 index 94a9f55..0000000 --- a/scripts/capability-matrix/test/ts-parser.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { join, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import { tmpdir } from "node:os"; -import { writeFileSync, cpSync } from "node:fs"; -import { extractFromSource, parseTypeScriptProject } from "../src/ts-parser"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const FIXTURE = join(__dirname, "fixtures", "ts-sample"); - -describe("extractFromSource", () => { - it("extracts exported class and its public methods", () => { - const source = ` - export class AuthClient { - public signUp(email: string): void {} - public signIn(): void {} - } - `; - const symbols = extractFromSource(source, "src/index.ts"); - const names = symbols.map((s) => s.name); - expect(names).toContain("AuthClient"); - expect(names).toContain("AuthClient.signUp"); - expect(names).toContain("AuthClient.signIn"); - }); - - it("excludes private and protected members", () => { - const source = ` - export class Foo { - public pub(): void {} - private priv(): void {} - protected prot(): void {} - #hard = 1; - } - `; - const symbols = extractFromSource(source, "src/index.ts"); - const names = symbols.map((s) => s.name); - expect(names).toContain("Foo.pub"); - expect(names).not.toContain("Foo.priv"); - expect(names).not.toContain("Foo.prot"); - expect(names).not.toContain("Foo.#hard"); - }); - - it("excludes non-exported classes", () => { - const source = ` - class Internal { - public method(): void {} - } - `; - const symbols = extractFromSource(source, "src/index.ts"); - expect(symbols).toHaveLength(0); - }); - - it("extracts exported functions", () => { - const source = `export function createClient(url: string): void {}`; - const symbols = extractFromSource(source, "src/index.ts"); - expect(symbols).toEqual([{ name: "createClient", kind: "function", file: "src/index.ts", line: 1 }]); - }); - - it("extracts exported variables", () => { - const source = `export const version = "1.0.0";`; - const symbols = extractFromSource(source, "src/index.ts"); - expect(symbols).toEqual([{ name: "version", kind: "variable", file: "src/index.ts", line: 1 }]); - }); - - it("records line numbers for class and its members", () => { - const source = [ - "export class AuthClient {", - " public signUp(email: string): void {}", - " public signIn(): void {}", - "}", - ].join("\n"); - const symbols = extractFromSource(source, "src/index.ts"); - const byName = Object.fromEntries(symbols.map((s) => [s.name, s])); - expect(byName["AuthClient"].line).toBe(1); - expect(byName["AuthClient.signUp"].line).toBe(2); - expect(byName["AuthClient.signIn"].line).toBe(3); - }); - - it("skips constructor", () => { - const source = ` - export class Foo { - constructor(private x: number) {} - public bar(): void {} - } - `; - const symbols = extractFromSource(source, "src/index.ts"); - const names = symbols.map((s) => s.name); - expect(names).not.toContain("Foo.constructor"); - expect(names).toContain("Foo.bar"); - }); - - it("includes getter as property kind", () => { - const source = ` - export class Foo { - get session(): string { return ""; } - } - `; - const symbols = extractFromSource(source, "src/index.ts"); - const s = symbols.find((x) => x.name === "Foo.session"); - expect(s).toBeDefined(); - expect(s?.kind).toBe("method"); - }); -}); - -describe("parseTypeScriptProject (fixture)", () => { - it("parses the fixture project and finds expected symbols", () => { - const result = parseTypeScriptProject(FIXTURE); - const names = result.symbols.map((s) => s.name); - - expect(names).toContain("AuthClient"); - expect(names).toContain("AuthClient.signUp"); - expect(names).toContain("AuthClient.signIn"); - expect(names).toContain("AuthClient.session"); - expect(names).toContain("StorageClient"); - expect(names).toContain("StorageClient.upload"); - expect(names).toContain("createClient"); - expect(names).toContain("version"); - }); - - it("excludes private and internal symbols from fixture", () => { - const result = parseTypeScriptProject(FIXTURE); - const names = result.symbols.map((s) => s.name); - - expect(names).not.toContain("AuthClient._token"); - expect(names).not.toContain("AuthClient._refresh"); - expect(names).not.toContain("InternalHelper"); - expect(names).not.toContain("internalUtil"); - }); -}); - -describe("parseTypeScriptProject — .sdk-parse-ignore", () => { - it("excludes files matched by .sdk-parse-ignore", () => { - // Copy fixture to a temp dir so we can add an ignore file without - // polluting the committed fixture. - const dir = join(tmpdir(), `ts-parser-ignore-test-${process.pid}`); - cpSync(FIXTURE, dir, { recursive: true }); - // The fixture has src/index.ts which exports AuthClient. - // Ignore the entire src/ directory. - writeFileSync(join(dir, ".sdk-parse-ignore"), "src/\n"); - const result = parseTypeScriptProject(dir); - expect(result.symbols).toHaveLength(0); - }); - - it("does not exclude files when .sdk-parse-ignore is absent", () => { - // FIXTURE has no .sdk-parse-ignore — should parse normally. - const result = parseTypeScriptProject(FIXTURE); - expect(result.symbols.map((s) => s.name)).toContain("AuthClient"); - }); -});