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
5 changes: 5 additions & 0 deletions .bumpy/source-first-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
mdcode-ts: patch
---

Explained the source-first workflow in the package README: examples live in source or test files that your tools check, and mdcode copies them into Markdown. Added a comparison with snippet type-checkers such as Kiira.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

[![npm version](https://img.shields.io/npm/v/mdcode-ts)](https://www.npmjs.com/package/mdcode-ts)

mdcode keeps the code blocks in your Markdown docs in sync with real source files. You write and test examples as ordinary code, point a code block at the file (or a `#region` inside it), and `mdcode update` copies the current code into the document. Your README can't drift from code that compiles and passes its tests.
mdcode keeps the code blocks in your Markdown docs in sync with real source files. Your examples live in ordinary source or test files, so your linter, type checker and tests run on them like any other code. Point a code block at the file (or a `#region` inside it), and `mdcode update` copies the current code into the document. Run your checks first and the README shows the code that passed them.

It also works the other way: `mdcode extract` writes code blocks out to files, and `mdcode run` runs a command against each block. It is a TypeScript port of [szkiba/mdcode](https://github.com/szkiba/mdcode), compatible with its CLI, and adds transform functions, a library API and a versioned `--json` output.

mdcode copies code into Markdown. It doesn't type-check or lint the code inside a fence. If you'd rather write snippets in the Markdown and check them there, use a snippet checker such as [Kiira](https://github.com/AlemTuzlak/kiira). See [mdcode and Snippet Checkers](packages/mdcode/README.md#mdcode-and-snippet-checkers) for how the two differ.

## Install

```bash
Expand All @@ -29,6 +31,8 @@ export function greet(name: string): string {
console.log(greet("docs"));
```

It's an ordinary file, so your existing lint, type check and tests already cover it.

In your README, add an empty code block that names the file and region:

````markdown
Expand All @@ -54,7 +58,14 @@ export function greet(name: string): string {
```
````

When `src/greet.ts` changes, run `mdcode update --apply README.md` again. In CI, `mdcode update --check README.md` exits 1 when a block has drifted from its source, without writing anything. To check several documents and tell drift apart from a broken `file=`, copy [`examples/ci/check-docs-sync.mjs`](examples/ci/check-docs-sync.mjs); see [Checking Docs in CI](packages/mdcode/README.md#checking-docs-in-ci).
When `src/greet.ts` changes, run your checks, then `mdcode update --apply README.md` again. CI runs the same two steps in the same order, with `--check` instead of `--apply`:

```bash
npm run lint && npm test # check the code where it lives
npx mdcode update --check README.md # exit 1 if a block has drifted from it
```

`--check` never writes. To check several documents and tell drift apart from a broken `file=`, copy [`examples/ci/check-docs-sync.mjs`](examples/ci/check-docs-sync.mjs); see [Checking Docs in CI](packages/mdcode/README.md#checking-docs-in-ci).

To keep several documents in sync without repeating their paths, list them in `mdcode.config.json` and run `mdcode update --project --check`. See [Project Configuration](packages/mdcode/README.md#project-configuration).

Expand Down
75 changes: 48 additions & 27 deletions packages/mdcode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,49 +40,70 @@ Documentation examples often become outdated. You write great examples in your R

### The Solution

**mdcode** solves this by making your documentation executable and testable:
**mdcode** keeps each example in a source or test file and copies it into the Markdown:

1. **Write code examples directly in your markdown** with metadata
2. **Extract them to files** for testing and development
3. **Run them as part of your test suite** to ensure they actually work
4. **Update your markdown** when the code changes
1. **Write the example as ordinary code**, marking the part to show with a `#region` if it's only part of a file
2. **Point a code block at it**, for example ```` ```ts file=src/greet.ts region=greet ````
3. **Lint and test the code** with your project's usual tools, like any other file
4. **Copy it into the Markdown** with `mdcode update --apply`, and fail CI with `mdcode update --check` when a block has drifted

The source file stays authoritative. The code block is a copy, so it shows whatever passed your checks.

### Key Benefits

**Test Your Documentation**
**Keep Examples Fresh**
```bash
# Change the code and check it as usual
nano src/calculator.js
npm test

# Copy the change into the README
mdcode update --apply README.md
```

**Catch Drift in CI**
```bash
npm run lint && npm test # check the code where it lives
mdcode update --check README.md # exit 1 if a block has drifted from it
```

**One Copy to Maintain**
- The code lives in one file, which your tools already lint, type-check and test
- The README holds a copy that `mdcode update` refreshes
- A block can show a whole file, one `#region` of it, or an outline of its regions

### Writing Examples in the Markdown

mdcode also works the other way. Write a block in the Markdown, then `extract` it to a file and `run` a command on it:

```bash file=block-1.sh
# Extract examples from README
mdcode extract README.md -d ./examples

# Run them as tests
mdcode run --allow-shell -l js "node {file}" README.md

# They work? Great! They fail? Fix them before users see broken examples.
```

**Keep Examples Fresh**
```bash file=tests/examples/base-1.sh
# Update your source code
nano src/calculator.js

# Sync changes back to README
mdcode update --apply README.md
```

**Single Source of Truth**
- Write examples once in your README
- Extract to files for actual implementation
- Bidirectional sync keeps everything in sync
- No duplicate code to maintain

**Documentation-Driven Development**
1. Write your README with examples first (TDD for docs)
This suits documentation-driven development:
1. Write your README with examples first
2. Extract code blocks to create skeleton files
3. Implement the functionality
4. Update README from working code
5. Your docs are always accurate because they **are** the code

If your README examples don't work, the build fails. Simple.
Once the files exist, they become the source and `mdcode update` keeps the README in step. To run Markdown-only blocks in CI, mark them `runnable=true`; see [Validating Runnable Snippets in CI](#validating-runnable-snippets-in-ci).

### mdcode and Snippet Checkers

mdcode copies code between source files and Markdown. It doesn't type-check, lint or compile the code in a fence itself, and it has no editor integration. In the source-first workflow, your project's own tools check the source file before mdcode copies it.

Snippet checkers such as [Kiira](https://github.com/AlemTuzlak/kiira) treat the fence as the source. Kiira extracts TypeScript and JavaScript fences from Markdown and MDX, type-checks them against your project and reports errors on the fence's line, in your editor, on the command line and in CI.

They suit different snippets:

- An example that should be complete, tested code belongs in a source or test file, and mdcode copies it into the docs.
- A fragment that only makes sense in the prose, such as a single call or part of a config, can stay in the Markdown, where a snippet checker type-checks it.

mdcode can also run your own type checker over blocks written in the Markdown. [`validate-snippets.mjs`](#validating-runnable-snippets-in-ci) extracts the `runnable=true` blocks into a temporary workspace and runs a command such as `tsc --noEmit` there. A failure names the block on the command line; nothing shows in your editor.

## Installation

Expand Down
Loading