# Internal package golden path

This document is the source of truth for Node.js libraries created under
`packages/` for use inside the Ghost monorepo. Start from [`_template`](./_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.

## Package contract

New internal packages are private, TypeScript-only ESM libraries:

- use an `@tryghost/<name>` package name;
- set `"version": "0.0.0"` and `"private": true`;
- set `"ghostPackage": {"goldenPath": "compliant"}`;
- set `"type": "module"`;
- keep authored code in `src/**/*.ts` and tests in `test/**/*.ts`;
- compile production code to `build/` with `tsc`;
- publish no independent npm releases.

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.

### Golden path status

Every private package under `packages/` declares its lifecycle state in
`ghostPackage.goldenPath`:

- `compliant` means the package is mechanically checked against this document;
- `migration` is temporary while a history-preserving import awaits a separate
  modernization PR;
- `exempt` records an intentional long-term exception such as a test-only or
  multi-runtime package.

Both `migration` and `exempt` require a non-empty `ghostPackage.reason`.
Public, independently versioned packages do not declare this metadata because
this internal-only contract does not apply to them. Run `pnpm lint:packages` to
validate the status and all mechanically enforceable rules.

## Package metadata

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:

```json
{
  "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.

## Shared configuration

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.

## Scripts

Use the standard script surface so Nx, CI and agents can operate on every
package consistently:

```json
{
  "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.

## Dependencies

- Use `workspace:*` for dependencies on another Ghost workspace package.
- Use `catalog:` for external dependencies managed by the root catalog.
- Declare every dependency the package imports; do not rely on root hoisting.
- Keep build, lint, test and type tooling in `devDependencies`.
- Keep runtime imports in `dependencies`.

## TypeScript and ESM

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.

## Runtime assets

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.

## Verification

For package changes, run at least from the package directory:

```bash
pnpm build
pnpm test
pnpm lint
```

Also verify:

- representative consumers through the same import or `require()` path they use
  in production;
- both raw `source` resolution and compiled output when a consumer uses both;
- the packed package or Ghost archive when runtime assets or exports change;
- `pnpm build` when build graph or packaging behavior changes.

## Existing packages and exceptions

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.
