packages/README.md
This document is the source of truth for Node.js libraries created under
packages/ for use inside the Ghost monorepo. Start from _template
and keep the resulting package aligned with these rules throughout its lifetime.
These defaults do not automatically apply to browser applications, test-only source helpers, or packages that Ghost intentionally publishes for third-party consumers. Record and justify exceptions in the package README and configuration.
New internal packages are private, TypeScript-only ESM libraries:
@tryghost/<name> package name;"version": "0.0.0" and "private": true;"type": "module";src/**/*.ts and tests in test/**/*.ts;build/ with tsc;Making a package public or independently versioned is a product and maintenance
decision, not a packaging convenience. Establish its compatibility, release and
support policy before removing private or adding publishing automation.
Use the template's repository, author and license metadata. Point repository metadata at the package's actual directory.
Expose each entry point with the source, types and default conditions, in
that order:
{
"type": "module",
"exports": {
".": {
"source": "./src/index.ts",
"types": "./build/index.d.ts",
"default": "./build/index.js"
}
},
"main": "build/index.js",
"types": "build/index.d.ts",
"files": ["build"]
}
Add explicit export entries for additional public modules. Do not expose broad filesystem patterns unless consumers genuinely need them.
The source condition gives Ghost development and tests build-free access to
raw TypeScript. Plain Node.js ignores it and loads compiled ESM from build/.
Keep src/ out of files so release components cannot depend on source-only
behavior accidentally.
Use the standard configuration packages as workspace:* dev dependencies:
@internal/cfg-eslint with nodeLibConfig();@internal/cfg-typescript with esm.json;@internal/cfg-vitest with createVitestConfig().Keep package config files minimal. Add overrides only for behavior that differs from the shared contract, and explain non-obvious exceptions next to the override.
The source TypeScript config should set rootDir to src, outDir to build
and include only production source. Use a separate test/tsconfig.json that
extends the source config, sets noEmit, and includes both source and tests.
Use the standard script surface so Nx, CI and agents can operate on every package consistently:
{
"scripts": {
"build": "tsc",
"test:unit": "NODE_ENV=testing vitest run --coverage",
"test:types": "tsc --noEmit -p test/tsconfig.json",
"test": "pnpm run '/^test:/'",
"lint:code": "eslint src/ --cache",
"lint:test": "eslint test/ --cache",
"lint": "pnpm run '/^lint:/'"
},
"nx": {
"targets": {
"build": {
"outputs": ["{projectRoot}/build"]
}
}
}
}
Add a dev watcher only when it is useful. Do not add aliases around these
commands without a concrete need.
workspace:* for dependencies on another Ghost workspace package.catalog: for external dependencies managed by the root catalog.devDependencies.dependencies.The shared TypeScript config uses NodeNext semantics. Relative imports in
TypeScript source must include their real .ts extension; the compiler rewrites
it to .js on emit.
Ghost Core is CommonJS but runs on Node versions that support require(esm).
Internal ESM packages may therefore serve both import and require() consumers
from one build. This requires the entire imported module graph to avoid top-level
await; ESLint enforces that restriction.
Do not add a CommonJS build or forwarding shim by default. Add multiple formats only when a verified consumer cannot use the standard ESM output.
Production behavior must work from build/ alone. Import JSON or other assets
from TypeScript when the compiler can copy them. Otherwise add an explicit,
portable build step and include the emitted asset path in package exports or
files as required.
Never rely on files that are present in the repository but absent from the packed package or Ghost release component.
For package changes, run at least:
pnpm --filter @tryghost/<name> build
pnpm --filter @tryghost/<name> test
pnpm --filter @tryghost/<name> lint
Also verify:
require() path they use
in production;source resolution and compiled output when a consumer uses both;pnpm build when build graph or packaging behavior changes.The golden path is the default for new and modernized internal libraries. Some existing packages have intentional public-release, browser, dual-format or test-only contracts. Do not mechanically rewrite them to match the template.
When touching a divergent package, determine whether the difference is required by a current consumer. Remove accidental drift in a focused change; document and test necessary exceptions.