MyBotBoxMyBotBox

How @yarlisai packages are consumed

The dual-mode architecture: bun workspace source consumption internally, npm dist tarballs externally.

Every @yarlisai/* package is consumed in two completely different ways at the same time. Understanding both modes explains why deploys never touch npm, why there is no build step in local dev, and why publishing is a parallel track rather than a deploy dependency.

The exports map

Both modes hang off the same conditional exports map, present in every package:

{
  "exports": {
    ".": {
      "bun": "./src/index.ts",
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "default": "./dist/index.js"
    }
  }
}

The bun condition resolves first under the Bun runtime and points straight at TypeScript source. Every other consumer (Node, bundlers honoring import/default) gets the compiled dist/ output.

Internal mode — workspace source

Inside the monorepo, apps declare packages as "@yarlisai/<pkg>": "workspace:*". Bun workspaces symlink node_modules/@yarlisai/<pkg> to packages/<pkg>/, and the bun export condition resolves imports directly to src/index.ts:

  • No build step in dev. Editing packages/email/src/ hot-reloads the app immediately — there is no dist/ to rebuild or stale artifact to chase.
  • Docker / CD builds from source. The production image (Dockerfile.cloudrun) copies the workspace, builds with turbo, and Next.js standalone output tracing bundles exactly the package files the app imports.
  • npm is never contacted in deploys. A registry outage, a yanked version, or a broken publish cannot break a deploy — the app ships whatever is in the git tree.

External mode — npm tarballs

External consumers install from npm and get the published tarball:

  • The tarball contains dist/ (plus README.md and LICENSE) — never src/. Resolution goes through the import/default conditions to dist/index.js, with types from dist/index.d.ts.
  • Builds run tsc then tsc-alias --resolve-full-paths, so emitted imports carry explicit .js extensions — Node ESM is strict about this even though bundlers are not.
  • At publish time, workspace:* dependency ranges are rewritten to real semver ranges (^x.y.z) by the publish workflow. Without that rewrite, npm install would fail on invalid semver.
  • Packages are currently published as restricted to the Yarlis npm org; public access is planned.

The external path is exercised in CI by an external-consumer smoke test that packs every package, installs the tarballs into a throwaway project with plain npm, and imports every export subpath under plain Node — no Bun, no workspace symlinks. The bun condition would otherwise mask broken dist/ artifacts from every internal consumer.

Why both

  • Deploys are immune to npm. Publishing problems (and npm outages) never block shipping the product — the app consumes source via the workspace.
  • Instant dev iteration. Source-level consumption means cross-package changes are a single hot reload, not a build-link-restart loop.
  • The framework track is decoupled. Versioning, changelogs, and publishing run through changesets and dedicated publish workflows on their own cadence, without coordinating with app releases.

The cost of the dual mode is that internal consumption can hide external breakage — which is exactly what the publint gate and the external smoke test exist to catch.

See also