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
66 changes: 66 additions & 0 deletions .changeset/published-packages-declare-exports.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
---
'@objectstack/cli': minor
'@objectstack/plugin-hono-server': minor
---

feat(cli,plugin-hono-server): declare `exports` maps, and make "a published package declares one" a gate (#12879)

**BREAKING** removal of reachable subpaths, shipped as `minor` under the repo's
launch-window convention for breaking changes.

**FROM.** Both packages declared `main` + `files` and no `exports`. Under Node's
resolution that leaves every module under `dist/` importable from outside the
package, whatever the entry barrel names — `@objectstack/cli/dist/utils/lower-callables.js`
resolved, and so did every other `dist/**` path in either package. They were the
only two of the 69 publishable packages here in that shape.

**TO.** Each declares exactly the entry it means to offer, and nothing else:

```jsonc
// @objectstack/cli — ESM-only (tsc, "type": "module")
"exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }

// @objectstack/plugin-hono-server — dual build (tsup esm+cjs)
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.js" } }
```

The root entry resolves to exactly what `main` / `types` already pointed at, so
`import … from '@objectstack/cli'` and `require('@objectstack/plugin-hono-server')`
are byte-for-byte unchanged. `@objectstack/cli` uses `default` rather than `import`
on purpose: it is ESM-only, and an `import`-condition-only map would ALSO refuse
CJS `require()`, which is a second break this change is not making.

The CLI's `bin` is untouched. `exports` gates specifier resolution only, and the
executable is reached by path — the `objectstack` / `os` shims, or
`node node_modules/@objectstack/cli/bin/run.js` as the Dockerfiles and the
showcase Playwright config spell it.

**Migration.** A consumer that deep-imports `@objectstack/cli/dist/**` or
`@objectstack/plugin-hono-server/dist/**` now gets `ERR_PACKAGE_PATH_NOT_EXPORTED`.
Import the symbol from the package root instead. If it is not exported there, it
was never an offered surface — the deep path resolved by omission, not by
decision, and the fix is an issue naming the use case rather than a map entry
that would ratify it (ratifying accidental reachability prices every later
internal refactor of these packages at a minor bump — the trap this change
exists to close, in the other direction).

Measured before the maps were written: **one** in-repo deep importer,
`packages/qa/dogfood/test/build-shaped-artifact.ts`, which now reads the CLI's
`lowerCallables` as SOURCE by relative path — the shape the rest of that suite
already uses for package-internal reads, and one that keeps the util internal
(#6293: reach the goal without growing `@objectstack/cli`'s public entry).
`@objectstack/plugin-hono-server` had **zero**. ⚠️ Deep importers OUTSIDE this
repo cannot be measured from inside it; they are the residual risk of this
release, and they break at the import rather than silently.

**The class, not the two instances.** `pnpm check:published-files` gains a sixth
invariant — GATED: a publishable package declares an `exports` map — turning a
convention that held for 69 of 71 into a written ratchet. It carries the census
control the ruling requires: the gate reads a POSITIVE signal off every manifest,
so a broken reading (no members enumerated, a key read under the wrong name, a
parse that drops manifests) would make "nobody violates GATED" true and green.
A census that finds nobody declaring `exports` therefore fails as an INSTRUMENT
error, in its own words, and the self-test holds the floor inside a band against
the live tree in both directions.

<!-- adr-0087: not-required (no-migration-prescription) A packaging-resolution narrowing: no metadata key, spec property or authorable surface is removed, renamed or re-shaped, so there is no tombstone and nothing for `objectstack migrate meta` to rewrite. The affected surface is a module specifier in a consumer's own source, the channel that reaches its author is Node's own `ERR_PACKAGE_PATH_NOT_EXPORTED` at the import, and the remedy (import from the package root, or ask for the surface) is a source edit no migration entry can perform. Measured in-repo population: one importer, changed in this PR; out-of-repo population is unmeasurable from here and is stated as the release's known risk. -->
6 changes: 6 additions & 0 deletions packages/cli/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,12 @@
"description": "Command Line Interface for ObjectStack Protocol",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"bin": {
"objectstack": "./bin/run.js",
"os": "./bin/run.js"
Expand Down
7 changes: 7 additions & 0 deletions packages/plugins/plugin-hono-server/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,13 @@
"description": "Standard Hono Server Adapter for ObjectStack Runtime",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"scripts": {
"build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs",
"test": "vitest run",
Expand Down
28 changes: 23 additions & 5 deletions packages/qa/dogfood/test/build-shaped-artifact.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,11 +68,29 @@
import { writeFileSync } from 'node:fs';

import { normalizeStackInput, ObjectStackDefinitionSchema } from '@objectstack/spec';
// The lowering itself, not a copy of it. `@objectstack/cli` declares no
// `exports` map, so this deep path is an ordinary internal import into a
// PRIVATE package — no published surface is added or widened by it (#6293's
// ruling: reach the goal without growing `@objectstack/cli`'s public entry).
import { lowerCallables } from '@objectstack/cli/dist/utils/lower-callables.js';
// The lowering itself, not a copy of it — reached as SOURCE, by relative path.
//
// It used to arrive as `@objectstack/cli/dist/utils/lower-callables.js`, which
// resolved only because that package declared no `exports` map: every `dist/**`
// module of it was importable from anywhere, whatever its entry barrel named.
// #12879 closed that hole, and this import is the one in-repo case it had to
// decide explicitly. Declaring the subpath in the CLI's `exports` was the other
// option and is the wrong one twice over: it would make an internal compiler
// util part of the published contract — which is #6293's ruling inverted (reach
// the goal WITHOUT growing `@objectstack/cli`'s public entry) — and it would
// ratify an accidental reachability nobody ever offered, pricing every later
// internal refactor of that package at a minor bump.
//
// So the module is reached the way every other package-internal read in this
// suite is: a relative path into the sibling package's SOURCE. It is the shape
// `route-ledger-live-mount-parity.dogfood.test.ts` uses for its five route
// ledgers, and the reason this package's `tsconfig.json` sets `rootDir` to the
// repo root (its comment there says so). Two things it buys that the `dist`
// path could not: the pin becomes a verdict about the checkout rather than
// about a build artifact (`check-test-source-alias`'s whole subject — #7668 is
// what a dist-resolved pin costs), and this suite no longer needs
// `@objectstack/cli` BUILT in order to run.
import { lowerCallables } from '../../../cli/src/utils/lower-callables.js';

type AnyFn = (...args: unknown[]) => unknown;

Expand Down
Loading
Loading