Skip to content

RFC: Internal vs. Published Packages Strategy for Build-time and Runtime Code #1197

Description

@yamcodes

Architectural Strategy: Internal vs. Published Packages

Based on the recent decisions around @arkenv/standard, the build-tools ADR, and the context from PR #1162 (consolidating @repo/scope to avoid version skew), here is my recommendation.

The short answer is: You need a hybrid approach, but the deciding factor is whether the shared code is Build-Time or Runtime.

You shouldn't "double down" universally on either approach. Instead, apply the following matrix:

1. Build-Time Infrastructure (build-tools, codegen, AST parsers)

Recommendation: Migrate to Published NPM Packages (e.g., @arkenv/build-utils).

Tip

This perfectly aligns with the build-tools ADR (Sub-alternative B). Publish the package, but explicitly mark it as "internal use only" in the README.

Why this is the right call:

  • The Edge Constraint: Build tools require Node globals (fs, path) and heavy dependencies (chokidar, TypeScript AST). You absolutely cannot inline or export these from @arkenv/core without instantly breaking Edge compatibility (Vercel, Cloudflare) and destroying the "zero dependencies" promise.
  • The Monorepo Constraint: If you keep them as internal @repo/* packages and don't inline them, consuming projects (like Next.js apps on Vercel) will crash during deployment because they can't resolve the unpublished turborepo package.
  • The Bloat Constraint: If you do inline them into every framework plugin (@arkenv/nextjs, @arkenv/vite-plugin), you drastically bloat the size of every plugin.

Publishing @arkenv/build-utils as a public dependency cleanly solves all three. The cost of changesets/registry clutter is entirely worth the stability it brings to the deployment pipeline.


2. Shared Runtime Logic (types, keywords, scopes)

Recommendation: Double down on Internal Turborepo Packages (@repo/*) and bundle them, OR use Singleton Subpath Exports (like PR #1162). Do not publish these as separate public NPM packages.

Why this is the right call:

  • The Singleton Problem: As you discovered in PR (v1) Internal code sharing strategy #1162, ArkType scopes are stateful. If you publish @arkenv/scope or @arkenv/types as standalone packages, users will inevitably end up with mismatched versions (e.g., @arkenv/core uses v1.0.1 but @arkenv/standard uses v1.0.2). This leads to duplicate instances in node_modules and catastrophic "version skew" bugs where type guarding fails.
  • Registry Clutter: Publishing hyper-granular packages like @arkenv/types creates an explosion of packages to manage and version.

The Runtime Strategy:

For runtime code, you have two safe patterns depending on the statefulness of the code:

  1. For Stateful Logic (e.g., ArkType Scopes):
    Use the Singleton Subpath Export pattern you introduced in PR (v1) Internal code sharing strategy #1162.
    Host the canonical instance in @arkenv/core and expose it internally via "exports": { "./internal": "./dist/internal.js" }. Have your plugins import from @arkenv/core/internal. This guarantees exactly one instance of the scope is ever loaded in memory.

  2. For Pure Logic (e.g., utility functions, type definitions):
    Use the Inlined Internal turborepo Package pattern.
    Keep them in packages/internal/@repo/utils, and use tsup/tsdown to bundle/inline them directly into the dist output of @arkenv/core and @arkenv/standard. It remains completely invisible to the end user and NPM.


Summary Verdict

Shared Domain Code Type Strategy Reason
Build Tools Codegen, Watchers, AST Published NPM Package (@arkenv/build-utils) Prevents deployment crashes and plugin bloat while preserving Edge compatibility.
Stateful Runtime ArkType Scopes, Caches Subpath Export (@arkenv/core/internal) Prevents duplicate instances in memory and version skew.
Pure Runtime Type utilities, helpers Inlined Turborepo Package (@repo/utils) Keeps NPM clean; zero impact on end users.

Given the direction we set with @arkenv/standard, this hybrid model gives you the perfect balance: a rock-solid, zero-dependency runtime, and a decoupled, robust build-time toolchain.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs triageMaintainer needs to evaluate this issue

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions