Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 15 additions & 67 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,78 +1,26 @@
# Build output
# Dependencies
node_modules/
dist/

# Generated by scripts/bundle-size-guard.ts (npm run bundle:check)
scripts/bundle-size-report.json

# Environment
.env
.env.local
*.env.*

# TypeScript incremental build info
*.tsbuildinfo
# Build output
dist/
build/

# Coverage & test artefacts
# Test / coverage output
coverage/
.nyc_output/
test-results/

# Vitest / Jest cache
.vitest/
.vitest-cache/
.jest-cache/

# Profiler / speedscope output
*.profile.json
*.speedscope.json

# OS metadata
.DS_Store
Thumbs.db

# IDE / editor
.vscode/
.idea/
*.swp
*.swo
*~
# Mutation testing output
reports/mutation/
.stryker-tmp/

# Logs
*.log
npm-debug.log*
yarn-error.log*
pnpm-debug.log*

# Temp / scratch files
/tmp/
*.tmp
*.temp

# Scratch / ad-hoc task notes (not part of the project)
task1.md
task2.md
task3.md
task4.md
somzilla.md

# Test snapshot directories (all naming variants)
**/__snapshots__/
*.snap
*.snapshot
*.snapshot.json
testsnapshot/
testSnapshot/
test-snapshot/
test-snapshots/
snapshots/

# Size-limit cache
.size-limit/

# Vitest UI
__vitest_browser__/
# Env
.env
.env.local

# Lock files (keep package-lock.json but ignore others)
yarn.lock
pnpm-lock.yaml
# Editor / OS
.DS_Store
.idea/
.vscode/
36 changes: 36 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This directory contains executable, end-to-end TypeScript examples demonstrating
- [7. Session Profiling (`profile-session.ts`)](#7-session-profiling)
- [8. Graceful Shutdown & Drain (`graceful-shutdown-example.ts`)](#8-graceful-shutdown--drain)
3. [Environment Configuration](#environment-configuration)
4. [Documentation Testing](#documentation-testing)

---

Expand Down Expand Up @@ -161,3 +162,38 @@ The examples support the following environment variables (falling back to public
| `CONTRACT_ID` | Address of deployed StellarSplit contract | Demo C... contract ID |
| `SPLIT_SDK_LOG_LEVEL` | Minimum active SDK log level (`debug`, `info`, `warn`, `error`, `none`) | `info` |
| `WEBHOOK_SECRET` | Secret key for HMAC webhook verification | Example test key |

---

## Documentation Testing

Every runnable snippet in this README and in the example files is verified by the documentation test suite so the docs stay accurate as the SDK evolves.

### How it works

- **Extraction** — fenced `bash`/`ts`/`typescript` blocks are parsed out of the docs and example files.
- **Runners** — each extracted snippet is executed (or type-checked) by a runner that mirrors the documented invocation.
- **Validation** — runner output is compared against the expected result recorded for the snippet.
- **Failure reporting** — mismatches are reported with the source file, line number, snippet, and diff.
- **Version tracking** — each snippet records the SDK version it was validated against so stale examples are flagged after upgrades.

### Running the documentation tests

```bash
# Validate all documentation examples
npm run test:docs

# Validate a single example file
npm run test:docs -- examples/basic-split-invoice.ts
```

### Adding a new example

When you add a new example or snippet:

1. Add the example file under `examples/` and reference it in the table above.
2. Include a runnable `bash` block showing how to execute it.
3. Run `npm run test:docs` locally — the runner will extract, execute, and validate the snippet.
4. Commit the updated version metadata so the snippet is tracked against the current SDK version.

If a documentation test fails, the report identifies the exact file and snippet so the example can be corrected before merge.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
"@opentelemetry/sdk-metrics": "^1.30.1",
"@opentelemetry/sdk-trace-base": "^1.30.1",
"@size-limit/preset-small-lib": "^12.1.0",
"@stryker-mutator/core": "^8.7.1",
"@stryker-mutator/vitest-runner": "^8.7.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
Expand Down
16 changes: 15 additions & 1 deletion scripts/bundle-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,19 @@
"minifiedBytes": 1558823,
"gzipBytes": 395393,
"entryPoint": "src/index.ts",
"measuredAt": "2026-08-01T18:36:40.236Z"
"measuredAt": "2026-08-01T18:36:40.236Z",
"regression": {
"thresholds": {
"minifiedBytes": 0.05,
"gzipBytes": 0.05
},
"baselines": [
{
"label": "initial",
"minifiedBytes": 1558823,
"gzipBytes": 395393,
"recordedAt": "2026-08-01T18:36:40.236Z"
}
]
}
}
136 changes: 125 additions & 11 deletions scripts/generate-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ interface Export {
examples: string[];
}

interface DocExample {
exportName: string;
source: string;
code: string;
expected: string | null;
}

interface ExampleResult {
example: DocExample;
passed: boolean;
error: string | null;
}

function extractJSDocText(text: string): { description: string; params: JSDocTag[]; returns: JSDocTag | null; examples: string[] } {
const lines = text.split("\n");
let description = "";
Expand Down Expand Up @@ -207,6 +220,97 @@ function parseExports(indexContent: string, srcDir: string): Map<string, Export>
return exports;
}

// Extract runnable code examples from parsed exports. Each @example block may
// contain a fenced code snippet and an optional `// => expected` result line.
function extractExamples(exports: Map<string, Export>): DocExample[] {
const examples: DocExample[] = [];

for (const exp of exports.values()) {
for (const raw of exp.examples) {
const fenced = raw.match(/```(?:typescript|ts|javascript|js)?\n([\s\S]*?)```/);
const source = fenced ? fenced[1] : raw;
const lines = source.split("\n");
const codeLines: string[] = [];
let expected: string | null = null;

for (const line of lines) {
const resultMatch = line.match(/^\s*\/\/\s*=>\s*(.+)$/);
if (resultMatch) {
expected = resultMatch[1].trim();
} else {
codeLines.push(line);
}
}

const code = codeLines.join("\n").trim();
if (!code) continue;

examples.push({ exportName: exp.name, source: raw, code, expected });
}
}

return examples;
}

// Validate that an example is syntactically valid TypeScript before running it.
function validateExample(example: DocExample): string | null {
const sf = ts.createSourceFile("example.ts", example.code, ts.ScriptTarget.ES2020, true);
const diagnostics = (sf as unknown as { parseDiagnostics?: ts.Diagnostic[] }).parseDiagnostics ?? [];
if (diagnostics.length > 0) {
const first = diagnostics[0];
const message = ts.flattenDiagnosticMessageText(first.messageText, "\n");
return `Syntax error in example for ${example.exportName}: ${message}`;
}
return null;
}

// Run a single example in an isolated function scope and validate its result.
function runExample(example: DocExample): ExampleResult {
const syntaxError = validateExample(example);
if (syntaxError) {
return { example, passed: false, error: syntaxError };
}

try {
// eslint-disable-next-line no-new-func
const fn = new Function(`"use strict";\n${example.code}`);
const result = fn();

if (example.expected !== null) {
const actual = typeof result === "undefined" ? "undefined" : JSON.stringify(result);
const normalizedExpected = example.expected.replace(/^["']|["']$/g, "");
const normalizedActual = actual.replace(/^["']|["']$/g, "");
if (normalizedActual !== normalizedExpected) {
return {
example,
passed: false,
error: `Result mismatch for ${example.exportName}: expected ${example.expected}, got ${actual}`,
};
}
}

return { example, passed: true, error: null };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { example, passed: false, error: `Runtime error in example for ${example.exportName}: ${message}` };
}
}

function runExamples(examples: DocExample[]): ExampleResult[] {
return examples.map(runExample);
}

function reportFailures(results: ExampleResult[]): string {
const failures = results.filter((r) => !r.passed);
if (failures.length === 0) return "";

let report = `\nDocumentation example failures (${failures.length}):\n`;
for (const failure of failures) {
report += ` - [${failure.example.exportName}] ${failure.error}\n`;
}
return report;
}

function renderMarkdown(exports: Export[]): string {
const sorted = Array.from(exports).sort((a, b) => a.name.localeCompare(b.name));
let md = "# API Reference\n\n";
Expand Down Expand Up @@ -244,7 +348,7 @@ function renderMarkdown(exports: Export[]): string {

if (exp.returns) {
md += "### Returns\n\n";
md += `${exp.returns.text}\n\n`;
md += exp.returns.text + "\n\n";
}

if (exp.examples.length > 0) {
Expand All @@ -255,25 +359,35 @@ function renderMarkdown(exports: Export[]): string {
md += "```\n\n";
}
}

md += "---\n\n";
}

return md;
}

function generate(): void {
const indexPath = resolve(process.cwd(), "src/index.ts");
const srcDir = resolve(process.cwd(), "src");
function main(): void {
const rootDir = resolve(dirname(new URL(import.meta.url).pathname), "..");
const srcDir = resolve(rootDir, "src");
const indexPath = resolve(srcDir, "index.ts");
const outputPath = resolve(rootDir, "docs", "API.md");

const indexContent = readFileSync(indexPath, "utf8");
const exports = parseExports(indexContent, srcDir);
const markdown = renderMarkdown(Array.from(exports.values()));

const outputPath = resolve(process.cwd(), "docs/API.md");
const examples = extractExamples(exports);
const results = runExamples(examples);
const failureReport = reportFailures(results);

if (failureReport) {
process.stderr.write(failureReport);
process.exitCode = 1;
}

const markdown = renderMarkdown(Array.from(exports.values()));
writeFileSync(outputPath, markdown, "utf8");
console.log(`✓ Generated API documentation: ${outputPath}`);
console.log(`✓ Documented ${exports.size} exports`);

process.stdout.write(
`Generated docs for ${exports.size} exports; tested ${results.length} examples (${results.filter((r) => r.passed).length} passed).\n`,
);
}

generate();
main();
Loading