This document provides guidance for AI coding assistants (Claude, GitHub Copilot, etc.) and developers working on linkinator. It documents architectural decisions, common pitfalls, and important considerations to prevent regressions.
- Build & Package Structure
- Dual Runtime Targets
- Critical Files & Dependencies
- Common Pitfalls
- Testing Requirements
- Release Process
linkinator/
├── src/ # TypeScript source files
│ ├── cli.ts # CLI entry point
│ ├── index.ts # Library entry point
│ └── ...
├── build/ # Compiled JavaScript output (gitignored)
│ ├── package.json # AUTO-GENERATED copy from root
│ ├── src/
│ │ ├── cli.js # Compiled CLI
│ │ └── ...
│ └── test/
├── package.json # Root package metadata
└── test/ # Test files
The project uses TypeScript with these key settings (tsconfig.json):
{
"compilerOptions": {
"rootDir": ".",
"outDir": "build",
"resolveJsonModule": true // ⚠️ IMPORTANT: Copies JSON imports to build/
}
}Important: When TypeScript sees import foo from './bar.json', it:
- Copies
bar.jsonto the build directory (maintaining relative structure) - Generates a
.d.tsdeclaration file for type safety
The files array in package.json controls what gets published to npm:
{
"files": [
"build/src", // All compiled source
"build/package.json" // REQUIRED for CLI version info
]
}- Root
package.json build/test/directory- Any files outside
build/src/andbuild/package.json
Linkinator supports two distinct runtime environments:
npm install linkinator
linkinator ./docsHow it works:
- User installs via npm
- Gets contents specified in
filesarray - Runs
node_modules/linkinator/build/src/cli.js - Imports resolve to files in
node_modules/linkinator/
./linkinator-linux ./docsHow it works:
- Created via
bun build --compile - Bundles all JavaScript and dependencies into a single executable
- No access to filesystem for imports (everything is embedded)
- Uses embedded JSON imports from build time
File: src/cli.ts:6
import packageJson from '../package.json' with { type: 'json' };Why this exists:
- Originally added in #760 to fix
--versionflag in compiled binaries - Before this, meow tried to read package.json at runtime, which failed in binaries
How it works:
- During build: TypeScript copies
package.json→build/package.json - Compiled to:
import packageJson from '../package.json'(relative path unchanged) - At runtime (npm):
build/src/cli.jsimportsbuild/package.json✅ - At compile time (binaries): Bundler embeds the JSON content ✅
build/package.jsonMUST be included in the npm package- Without it, npm users get:
Cannot find module '/node_modules/linkinator/build/package.json' - This caused issue #763
When modifying packaging, verify:
-
"build/package.json"is in thefilesarray - The import path in
cli.tsis../package.json(not../../) -
tsconfig.jsonhasresolveJsonModule: true -
npm pack --dry-runincludesbuild/package.json
What happens: NPM users can't run the CLI
Error: Cannot find module '/node_modules/linkinator/build/package.json'
Why: The CLI imports from ../package.json relative to build/src/cli.js
How to avoid: Never remove build/package.json from the files array without changing how the CLI gets version info
What happens: Works for npm, breaks for compiled binaries Why:
- From
src/cli.ts,../../package.jsonwould look outside the project - TypeScript won't copy a file from outside the project
- Compiled binaries won't have the package.json embedded
How to avoid: Always import from
../package.json(one level up from src/)
What happens: --version shows wrong version
Why:
- Someone edited
package.jsonwithout rebuilding build/package.jsonhas stale version How to avoid:- Always run
npm run buildafter version changes - The release workflow rebuilds automatically
- Test
test.cli.tsverifies version consistency
Scenario: You add import config from '../config.json' in the CLI
What happens:
- TypeScript copies it to
build/config.json✅ - Works locally ✅
- NPM package doesn't include
build/config.json❌ - NPM users get import error ❌
How to avoid: When adding new JSON imports used by the CLI:
- Add the file to the
filesarray:"build/config.json" - Test with
npm pack --dry-run - Verify the file is listed in the tarball contents
Scenario: You use runtime file system access:
const version = JSON.parse(fs.readFileSync('package.json', 'utf8')).version;What happens:
- Works for npm ✅
- Breaks in compiled binaries ❌ (no filesystem)
How to avoid:
- Use static imports for data needed in both environments
- Test changes with
npm run build-binaries - Run the binary and verify it works
If you modify:
package.jsonfilesarray- Imports in
cli.ts - Build configuration (
tsconfig.json) - Package metadata
You must verify:
- All tests pass:
npm test - Version consistency test passes: Ensures
build/package.jsonmatches root - NPM package contents:
npm pack --dry-run | grep build/ - Compiled binary works:
npm run build-binaries ./build/binaries/linkinator-macos --version
- Local CLI works:
node build/src/cli.js --version
test/test.cli.ts- CLI behavior, version flag, packagingtest/test.index.ts- Core library functionalitytest/test.*.ts- Feature-specific tests
File: test/test.cli.ts:65-80
it('should have build/package.json with matching version', () => {
const rootPkg = JSON.parse(
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
);
const buildPkg = JSON.parse(
fs.readFileSync(new URL('../build/package.json', import.meta.url), 'utf8'),
);
assert.strictEqual(
buildPkg.version,
rootPkg.version,
'build/package.json version should match root package.json version',
);
});This test catches stale builds after version bumps.
File: .github/workflows/release.yaml
The workflow:
- release-please: Creates release PR with version bumps
- publish job (when PR merged):
- run: npm ci # Installs deps, runs prepare hook - run: npm run build # Explicit rebuild (ensure fresh build) - run: npm publish # Publishes to npm - run: npm run build-binaries # Creates platform binaries - run: gh release upload ... # Uploads binaries to GitHub release
- Release-please creates PR with version change
- Merging PR triggers publish workflow
- Workflow rebuilds from scratch
- Publishes with fresh
build/package.jsonmatching root version
- Don't manually publish without rebuilding
- Don't edit version in
build/package.jsondirectly (auto-generated) - Trust the CI/CD process - it rebuilds correctly
If you must publish manually:
# 1. Ensure clean state
git status # Should be clean
# 2. Bump version
npm version patch|minor|major
# 3. MUST rebuild
npm run build
# 4. Verify package contents
npm pack --dry-run | grep build/package.json
# 5. Publish
npm publish
# 6. Build and upload binaries
npm run build-binaries
gh release upload <tag> build/binaries/*Context: CLI needs to display version via --version flag in both npm and binary distributions.
Decision: Include build/package.json in npm packages and import it in CLI.
Alternatives considered:
- ❌ Use
import.meta.urlto read at runtime - doesn't work in binaries - ❌ Generate
version.tsfile - extra build complexity - ❌ Use bundler for constant injection - requires switching from
tsc - ✅ Import JSON and include in package - simple, works everywhere
Consequences:
- ✅ Works for npm and compiled binaries
- ✅ Minimal code changes
⚠️ Adds 2.5kB to package (duplicate package.json)⚠️ Must keepfilesarray in sync
References: #759, #760, #763
Context: Users want both:
- Easy npm installation for Node.js projects
- Standalone binaries for CI/CD and non-Node environments
Decision: Maintain both distribution methods.
Consequences:
- Must test both environments
- Can't use npm-only features (like reading node_modules at runtime)
- Import statements must work when bundled
- Adds complexity to CI/CD
# Build TypeScript
npm run build
# Run tests
npm test
# Test CLI locally
node build/src/cli.js <args>
# Check package contents
npm pack --dry-run
# Build platform binaries
npm run build-binaries
# Test binary
./build/binaries/linkinator-macos --version-
package.json-filesarray,binentry -
tsconfig.json-rootDir,outDir,resolveJsonModule -
src/cli.ts- Import paths for JSON -
test/test.cli.ts- Add tests for new imports -
.github/workflows/release.yaml- CI/CD build steps
Watch out for these in code reviews:
- ❌ Removing items from
filesarray without testing npm pack - ❌ Adding JSON imports without updating
filesarray - ❌ Using
fs.readFileSync()in code that runs in binaries - ❌ Changing import paths without testing both npm and binaries
- ❌ Modifying
build/directory manually (it's auto-generated) - ❌ Committing
build/directory to git (it's gitignored for a reason)
- #759 (Dec 2024):
--versionflag didn't work in compiled binaries - #760 (Dec 2024): Added
import packageJsonto fix binaries⚠️ Regression: Broke npm installs (missing build/package.json)
- #763 (Dec 2024): Users reported "Cannot find module" error
- #764 (Dec 2024): Added
build/package.jsontofilesarray
- Test both distribution methods: npm install AND compiled binaries
- Verify npm package contents: Use
npm pack --dry-run - Understand TypeScript JSON handling:
resolveJsonModulecopies files - Document architectural decisions: Hence this file!
If you're unsure about a change:
- Read this file - especially Common Pitfalls
- Test locally:
npm run build && npm test - Check package:
npm pack --dry-run - Test binary:
npm run build-binaries && ./build/binaries/linkinator-macos --version - Ask in PR: Tag maintainers if uncertain
Remember: It's better to ask than to introduce a regression! 🙂
Last Updated: 2025-12-27 Maintainer: @JustinBeckwith Contributors: Claude (learned the hard way 😅)