Skip to content

fix: publish only what consumers need, without the development condition - #560

Merged
V3RON merged 17 commits into
mainfrom
chore/package-files
Oct 12, 2026
Merged

V3RON merged 17 commits into
mainfrom
chore/package-files

Conversation

@V3RON

@V3RON V3RON commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Description

Published packages now ship only what consumers need, and their exports resolve in every bundler mode.

  • files allowlists: every publishable package has an explicit files field.

    • Plugins ship dist and CHANGELOG.md. require-profiler-plugin also ships src/metro/setup.js, which its Metro integration loads by path.
    • Libraries ship dist plus what their exports point at.
    • @rozenite/ui keeps src for Tailwind's @source, plus fonts/, fonts.css and styles.css.
    • Dev-host fixtures (rozenite.config.ts), tests, fixtures, configs and tsbuildinfo no longer ship.
  • No development condition in published manifests. A root .pnpmfile.cjs beforePacking hook strips it from every package's exports on pnpm pack and pnpm publish; changeset publish uses pnpm publish. The workspace keeps the condition, so tsconfig customConditions and the playground still resolve to source.

    • The 8 plugins' own ./sdk exports drop development: './sdk.ts' as well.
    • ./sdk still points at dist/sdk/sdk.js and sdk.d.ts, which ship as before.
  • CLI: rozenite build no longer writes development: './sdk.ts' into a plugin's ./sdk export, and removes that legacy value from existing plugins. An author's own development value is kept. If a plugin has publishConfig.exports, the managed entries are synced into it. Plugins created with rozenite generate publish ["dist"].

  • Dead references removed: the templates entries in metro and repack, and TEMPLATE_DIR in middleware, all pointed at files that don't exist.

  • Guard: every publishable package has a package-contents test, built on inspectPackedPackage in @rozenite/test-utils. It runs pnpm pack and checks that:

    • every exports, main, module, types, react-native and bin target, for every condition, is in the tarball;
    • published exports equal source exports minus development;
    • no workspace: dependency leaks;
    • no rozenite.config.*, test, fixture, tsbuildinfo or build config ships;
    • no shipped module imports a left-out file.

    .pnpmfile.cjs is a global Turborepo input, so editing the hook re-runs every guard.

  • Docs: agents/package-creation.md describes the allowlist, the development policy and the guard.

Related Issue

Closes #559

Context

Published size, all 33 packages, rebuilt from scratch on both sides:

Before After
Unpacked 66.5 MB 21.8 MB
.tgz 12.9 MB 6.7 MB

expo-atlas-plugin drops from 43 MB to 295 kB, because 4,222 Metro test fixtures no longer ship. Every package now ships its CHANGELOG.md.

The development bug. Before, a consumer project using webpack 5 in development mode could not resolve agent-bridge, agent-shared, agent-sdk, metro, repack, middleware or lynx-dev. Vite 7 dev threw ERR_RESOLVE_PACKAGE_ENTRY_FAIL. Plugin /sdk imports and vite-plugin resolved to .ts source inside node_modules. After this change, everything resolves to dist.

Why a pnpm hook. It replaces 16 hand-copied publishConfig.exports maps. pnpm records the hook's checksum in the lockfile, so editing it needs a pnpm install; otherwise frozen installs fail loudly. npm publish would ignore the hook, but releases go through changeset publish, which uses pnpm publish.

Behaviour note. In the playground's on-device e2e:network run, @rozenite/{network-activity,storage}-plugin/sdk now resolves to the built dist instead of sdk.ts. docs/agents/network-activity-e2e.md already says to rebuild the plugin before each run.

Not changed, noted:

  • @rozenite/web's .d.ts.map files point at src, which isn't published. Editors fall back to the .d.ts, as for lynx, plugin-bridge and testing.
  • packages/web/src/__tests__/webpack.test.ts was never wired up on main, and fails with "DefaultToolLauncher must be mocked". Web's test script now runs only the guard.

Testing

  • git fetch origin main && pnpm checks:affected && pnpm test:affected: passed, 72 test tasks. The guard was also run uncached across all 35 test tasks; the slowest guard took about 16 s against a 120 s timeout.
  • Before/after tarball comparison, every package rebuilt from a clean dist:
    • every removed file justified as dev-only;
    • 0 missing export targets and 0 import regressions;
    • 0 development conditions in any packed manifest.
  • Consumer smoke test outside the repo, from the packed tarballs:
    • 102 subpath resolutions with require and import;
    • real loads of the sdk entries, tools, the Metro, Re.Pack and webpack integrations, vite-plugin and lynx/rspeedy;
    • rozenite --help and rozenite skills list.
  • Bundler probe: Vite 7 dev and webpack 5 in dev and production modes resolve every package to dist.
  • Publish path: a real pnpm publish, and changeset publish --no-git-tag for all 33 packages, against a local mock registry. The published manifests have no development.
  • Guard mutation tests: each of these fails the guard: an extra file in files, a dropped dist/sdk, a shipped tsbuildinfo, a stale export, and the hook disabled.
  • Workspace build: rozenite build across all plugins leaves every package.json unchanged.
  • Review: an independent adversarial review ran twice; its findings are fixed.

V3RON added 17 commits October 12, 2026 18:15
Plugins had no `files` field, so npm shipped everything not in .npmignore:
rozenite.config.ts (dev-host fixtures and flows), tests, vite/tsconfig
files, and for expo-atlas-plugin 4k+ installed test fixtures (about 6 MB).

Publish an explicit allowlist instead: dist, CHANGELOG.md, and for plugins
whose ./sdk export has a development condition, sdk.ts and src minus
tests. require-profiler-plugin also keeps src/metro/setup.js, which its
Metro integration hands to Metro by path.
@rozenite/tools and @rozenite/web had no `files` field and shipped their
src, tsconfig*.json and build scripts. Drop test artifacts from the others:
tsc declaration stubs for tests in lynx, plugin-bridge and testing, tsbuildinfo
caches in agent-bridge and test files in the src that @rozenite/ui ships for
Tailwind source scanning.
The plugin template had no `files` field, so every generated plugin would
publish its rozenite.config.ts, tests and build configuration. rozenite
build adds the ./sdk export (development condition -> sdk.ts) on its own
when sdk.ts appears, so the allowlist includes sdk.ts and src up front.
Sixteen packages had a `development` condition in `exports` pointing at
TypeScript source (`./src/index.ts`, `./react-native.ts`, `./sdk.ts`). It
exists for this workspace (tsconfig customConditions, the playground's webpack
conditionNames), but webpack, Rspack and Vite also turn it on in dev mode, and
for agent-bridge, agent-sdk, agent-shared, lynx-dev, metro, middleware and
repack the target was not in the tarball: a consumer's dev build failed with
'Failed to resolve entry for package' / "Can't resolve". For the plugins'
./sdk it resolved to .ts under node_modules instead.

Repeat `exports` without the condition under `publishConfig.exports`; pnpm
pack and pnpm publish (which changeset publish uses) apply it.
With the development condition gone from the published exports, plugins no
longer need to ship sdk.ts and src to keep its target from dangling: their
allowlist is now dist and CHANGELOG.md (plus src/metro/setup.js for
require-profiler-plugin, which hands it to Metro by path).

tsc --incremental writes dist/*.tsbuildinfo whenever typecheck runs before a
pack, and release:check does exactly that, so exclude them from every package
that publishes dist.
metro and repack listed a `templates` directory in `files` that does not
exist, and middleware exported a TEMPLATE_DIR pointing at a nonexistent
entry-point.ejs. Nothing in the repository reads it (the CLI has its own
TEMPLATE_DIR).
rozenite build added `development: ./sdk.ts` to a plugin's ./sdk export. Anyone
publishing such a plugin either shipped sdk.ts and src to keep the target from
dangling or broke dev builds that enable the condition (webpack, Rspack, Vite).
The managed export is now just types and default; a development condition the
author wrote themselves is preserved, which is how the workspace plugins keep
theirs.

The plugin template therefore publishes only dist.
Each publishable package now owns a package-contents test that packs the
built package the way pnpm publish does and checks the tarball with
inspectPackedPackage from @rozenite/test-utils/packaging:

- every exports/main/module/types/react-native/bin target, for every
  condition, is in the tarball;
- the published exports equals the source exports minus `development`;
- no rozenite.config.*, test, fixture or build-configuration file ships;
- no shipped module imports a file that was left out.

It runs as part of the package's own test task, so test:affected runs it
whenever the package or something it depends on changes, and it builds only
what that package already needs. @rozenite/web gets a test script that runs
just this guard: its existing webpack suite was never wired up and fails on
an unhandled rejection.
Replace the sixteen copies of exports under publishConfig.exports with a
beforePacking hook in the root .pnpmfile.cjs that removes the development
condition from every package's exports. Verified for both entry points: pnpm
pack, and a real pnpm publish against a local registry that records the PUT
body (publishConfig.exports removed from the package for the test).
changeset publish runs pnpm publish from the package directory, so the
workspace pnpmfile applies. pnpm records the file in the lockfile as
pnpmfileChecksum.

The plugins' ./sdk exports also drop their development: ./sdk.ts. Nothing
resolved it in the workspace: agent-sdk maps the plugin SDKs to dist
declarations in its tsconfig and to source through a Vite alias, and the
network-activity e2e tsconfig clears customConditions.

The package-contents guard still asserts that the published exports equal the
source exports minus development.
…onfig.exports

rozenite build wrote development: ./sdk.ts into the managed ./sdk export from
1.10.0 until the previous commit stopped it, so plugins generated since then
still carry it and publish a target they do not ship. Drop that exact value on
the next build; any other development value was written by the author and is
kept.

Authors who hide a development condition with publishConfig.exports would
lose ./sdk or ./metro from the published package when those appear, because
only exports was managed. When publishConfig.exports exists, write the
managed entries there too (never with development) and report it as
updated; a package without one is left alone.
npm does not include CHANGELOG.md on its own, so the allowlists added earlier
left it out of the seventeen libraries, the CLI and electron-app that were
already on one. List it everywhere a CHANGELOG.md exists; the plugin template
has none.
Track .pnpmfile.cjs as a global Turborepo input, so editing the hook that
strips the development condition re-runs every package-contents test, and
strip the condition inside export arrays too.
@V3RON
V3RON merged commit a7985fa into main Oct 12, 2026
4 checks passed
@V3RON
V3RON deleted the chore/package-files branch October 12, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Published packages ship dev files and broken development exports

1 participant