-
Notifications
You must be signed in to change notification settings - Fork 6
(v1) Internal vs. Published Packages Strategy for Build-time and Runtime Code #1202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
yamcodes
merged 22 commits into
v1
from
1199-distribution-implement-standard-mode-packaging-strategy
Jun 23, 2026
Merged
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
552a9a8
feat: add turborepo skill and ADRs 0007 & 0008 from analyze-zod-expor…
yamcodes 8defbe3
docs: remove ADR 8 (belongs to issue 1200)
yamcodes 1c6fc7f
docs: add ADR 0010 for runtime shared logic strategy
yamcodes 8e35d3c
docs: standardize product name as ArkEnv in ADR and CLI changelog
yamcodes 6d55945
docs: add ADR 0008 to document the migration from createEnv to arkenv…
yamcodes cc0b227
Please provide the list of changes or the files modified so I can gen…
yamcodes 45d9f4f
Merge branch 'v1' into 1199-distribution-implement-standard-mode-pack…
yamcodes ba951a2
Merge branch 'v1' into 1199-distribution-implement-standard-mode-pack…
yamcodes bca2fe0
feat: implement hybrid versioning by decoupling plugins and enforcing…
yamcodes 9f20282
feat: move arkenv to peer dependencies and update changeset documenta…
yamcodes 26663c3
docs: add README for @arkenv/build and clarify runtime strategy in AD…
yamcodes 036127b
docs: clarify independent versioning and fix package name references
yamcodes 185c89a
docs: add rule for future core/standard split in versioning strategy
yamcodes 641b4e2
docs(adr): document peer dependency rationale and CLI onboarding flow
yamcodes f8ef94b
docs(adr): resolve review feedback on independent versioning examples…
yamcodes d88cbe8
refactor: decouple plugins from core engine by replacing workspace pe…
yamcodes 0f5e61a
docs: add enterprise precedent for peer dependency strategy in ADR 0011
yamcodes 61340e1
Merge remote-tracking branch 'origin/dev' into 1199-distribution-impl…
yamcodes a970f14
Merge branch 'v1' into 1199-distribution-implement-standard-mode-pack…
yamcodes 7820207
docs: resolve changeset peerDependency range review comment and AutoT…
yamcodes a710669
Merge branch 'v1' into 1199-distribution-implement-standard-mode-pack…
yamcodes 57b30a5
refactor: rename nuxt internal file to arkenv-internal
yamcodes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| --- | ||
| "@arkenv/nextjs": major | ||
| "@arkenv/nuxt": major | ||
| "@arkenv/vite-plugin": major | ||
| "@arkenv/bun-plugin": major | ||
| --- | ||
|
|
||
| #### Move `arkenv` to peer dependencies in framework plugins | ||
|
|
||
| Framework plugins no longer declare `arkenv` as a regular dependency. `arkenv` is now declared as a `peerDependency` with a caret range (`^1.0.0-alpha.1`), ensuring a single shared instance across all plugins and the host application. | ||
|
pullfrog[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| This change prevents duplicate instances of `arkenv` in `node_modules`, which could break ArkType structural typing and schema validation at runtime. | ||
|
|
||
| Plugins affected: | ||
|
|
||
| - `@arkenv/nextjs` | ||
| - `@arkenv/nuxt` | ||
| - `@arkenv/vite-plugin` | ||
| - `@arkenv/bun-plugin` | ||
|
|
||
| Before: | ||
|
|
||
| ```bash | ||
| npm install @arkenv/nextjs | ||
| ``` | ||
|
|
||
| After: | ||
|
|
||
| ```bash | ||
| npm install arkenv @arkenv/nextjs | ||
| ``` | ||
|
|
||
| **BREAKING CHANGE:** Users must now install `arkenv` alongside the plugin. Previously, `arkenv` was automatically pulled in as a regular dependency. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # Standard Mode Packaging Strategy | ||
|
|
||
| ## Context | ||
|
|
||
| With the introduction of the "Standard Schema" implementation (which strictly removes ArkType dependencies) and the migration to the `@arkenv` scope, we face a decision on how to distribute the "Standard Mode" across the core library and its framework plugins. | ||
|
|
||
| The decision is asymmetric: | ||
|
|
||
| 1. **The Core Engine:** We are splitting the core into two separate packages (`@arkenv/core` for ArkType, and `@arkenv/standard` for Standard Schema). | ||
| 2. **The Framework Plugins:** We are **not** splitting the plugins into separate packages (e.g., we will not create `@arkenv/vite-plugin-standard`). Instead, a single plugin package will support both engines via subpath exports (e.g., `import arkenv from "@arkenv/vite-plugin/standard"`). | ||
|
|
||
| ## Decision: Why the Asymmetry? | ||
|
|
||
| We have chosen this asymmetric packaging strategy for three primary reasons: | ||
|
|
||
| ### 1. The N-Multiplier Effect | ||
|
|
||
| If we mandate that every engine requires a separate package, adding support for a new framework requires creating two new packages (e.g., `@arkenv/bun-plugin` and `@arkenv/bun-plugin-standard`). If we ever add a third engine, we'd need three packages per framework. By using subpath exports in the plugins, we maintain a 1:1 ratio between frameworks and plugin packages. This dramatically reduces repository and npm namespace bloat. | ||
|
|
||
| ### 2. Perfect Sibling Symmetry & Discoverability | ||
|
|
||
| By extracting `@arkenv/standard` into its own package, we achieve perfect sibling symmetry: `@arkenv/core` represents the primary ArkType engine, and `@arkenv/standard` represents the alternative Standard Schema engine. This elevates Standard Mode to a first-class citizen, providing a massive discoverability benefit for Zod and Valibot users who no longer have to install a package whose primary identity is built around ArkType. | ||
|
|
||
| ### 3. Zero Peer Dependency Confusion | ||
|
|
||
| The engines have fundamentally different dependency requirements. The core requires `arktype`, while the standard mode is dependency-free. Splitting them allows `@arkenv/core` to list `arktype` as a *required* peer dependency, while `@arkenv/standard` has zero peer dependencies. This is much cleaner for package managers than relying on complex `peerDependenciesMeta: { optional: true }` configurations at a unified core level. | ||
|
|
||
| ### 4. Shared Internals (Enabler) | ||
|
|
||
| This split is made architecturally feasible because we are already adopting an "inlined internal packages" strategy. We can easily extract the shared `parse-standard` and `guards` logic into an internal `@repo/utils` package, and bundle that into both `@arkenv/core` and `@arkenv/standard` at publish time without creating dual-package hazards. | ||
|
|
||
| ### 5. Plugins are Thin Routers | ||
|
|
||
| Framework plugins do not implement parsing or evaluation logic; they are essentially dependency-injectors that bridge the build tool to the engine. Because they are lightweight, a single plugin package can safely list both `@arkenv/core` and `@arkenv/standard` as optional peer dependencies. The user installs the engine they want, imports the correct plugin subpath, and the bundler tree-shakes the unused path. There is no risk of leaking heavy dependencies. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - **Positive:** The npm ecosystem remains clean (fewer packages). | ||
| - **Positive:** Users only download the dependencies they actually need (ArkType vs Standard). | ||
| - **Negative:** Plugin maintainers must ensure their `package.json` correctly exposes multiple `"exports"` and manages dual optional peer dependencies. |
|
yamcodes marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # Runtime Shared Logic Strategy | ||
|
|
||
| > **Originating RFC:** [Issue #1197 — Internal vs. Published Packages Strategy](https://github.com/yamcodes/arkenv/issues/1197) | ||
|
|
||
| ## Context | ||
|
|
||
| ArkEnv relies on several internal utility functions and runtime configurations that must be shared across our core engine (`arkenv`) as well as framework plugins (e.g., `@arkenv/nextjs`, `@arkenv/nuxt`). This creates an architectural decision regarding how to distribute and share this logic effectively without compromising developer experience, type safety, or build environments. | ||
|
|
||
| When comparing code-sharing strategies, two traditional approaches are often cited: | ||
|
|
||
| 1. **The NestJS Peer Dependency Model:** Extensive use of `peerDependencies` where heavy singletons are hoisted to the application root. | ||
| 2. **The Published Utility Package:** Creating a public `@arkenv/utils` package published to npm that all other packages depend on. | ||
|
|
||
| Both of these standard approaches introduce critical flaws for ArkEnv's unique requirements as a structural typing utility. | ||
|
|
||
| ## Decision | ||
|
|
||
| We will avoid peer dependencies for **runtime** code sharing and avoid publishing an external utility package for runtime logic. | ||
|
|
||
| Instead, we will adopt an **Inlined Internal Packages Strategy** (`@repo/*`) combined with **Subpath Exports** for sharing runtime logic. | ||
|
|
||
| We distinguish between two categories of shared logic: | ||
|
|
||
| ### Runtime Shared Logic | ||
|
|
||
| Runtime logic must remain 100% dependency-free and must not suffer from version or singleton skew. | ||
|
|
||
| 1. **Stateless Logic (Helpers/Parsers):** Shared stateless logic will live in an internal monorepo package (e.g., `@repo/utils`). Using our bundler (`tsdown`), this logic will be physically inlined into the distributables of the consuming published packages (`arkenv`, `@arkenv/cli`). | ||
| 2. **Stateful Logic (Singletons/Schemas):** Any stateful logic or singleton configuration will be managed via Subpath Exports directly from the core `arkenv` package, rather than using peer dependencies to enforce a single instance. The core package exposes named entry points such as `arkenv/standard` and `arkenv/core` via its `exports` field. Future stateful internals (e.g., shared ArkType scopes) may be exposed through additional subpaths like `arkenv/internal` if needed. | ||
|
|
||
| ### Build-Time Shared Logic | ||
|
|
||
| Build-time logic is Node-only and does not impact the runtime footprint. This includes AST parsing, file-watching, and codegen utilities. | ||
|
|
||
| Build-time shared logic is centralized in the published `@arkenv/build` package (see [ADR 0009: Shared Build Package](./0009-shared-build-package.md)). Framework plugins list `@arkenv/build` as a regular dependency. Because it is consumed only during development and build phases, it does not compromise Edge runtime compatibility. | ||
|
|
||
| ## Rationale | ||
|
|
||
| ### 1. Why Not The NestJS Peer Dependency Model? | ||
|
|
||
| NestJS is a heavy, opinionated framework. It extensively uses `peerDependencies` (e.g., `@nestjs/core`, `@nestjs/common`) and relies on the user's package manager (npm/pnpm/yarn) to hoist these dependencies to the root of the project, theoretically ensuring a single instance of the framework's IoC container across the app. | ||
|
|
||
| This works for NestJS because users expect a rigid, complex dependency graph within a tightly controlled application boundary. | ||
|
|
||
| However, this is fundamentally incompatible with ArkEnv for several reasons: | ||
|
|
||
| - **Version Skew and Phantom Dependencies:** Monorepo package managers notoriously struggle with strict peer dependency hoisting. If a user installs a slightly different version of a sub-package, the package manager may duplicate the dependency in `node_modules`. | ||
| - **Fatal for Structural Typing:** ArkEnv relies heavily on ArkType, which evaluates schemas using structural typing, `instanceof` checks, and strict object identity under the hood. If a duplicated instance of the core engine or ArkType is loaded due to peer dependency failure, schema evaluation and type scoping break silently at runtime. We cannot afford to trust hoisting for singleton integrity. | ||
|
|
||
| ### 2. Why Not a Published `@arkenv/utils` Package? | ||
|
|
||
| The standard alternative to peer dependencies is a centralized utility package. We could create an `@arkenv/utils` package and publish it to npm, having `arkenv` and framework plugins depend on it normally. | ||
|
|
||
| This introduces unacceptable friction: | ||
|
|
||
| - **Public API Surface Bloat:** We would be forced to publish purely internal implementation details to npm. | ||
| - **Dependency Graph Bloat:** Users must download additional packages, slowing down install times and increasing the overall node_modules footprint. | ||
| - **Cascading Version Bumps:** Updating a single internal helper function would require bumping `@arkenv/utils`, which then requires bumping `arkenv` and all framework plugins, turning minor internal refactors into massive versioning chores. | ||
|
|
||
| ### 3. The Power of Inlined Internal Packages | ||
|
|
||
| By keeping `@repo/utils` purely as a local workspace package, we achieve the best of both worlds: | ||
|
|
||
| - **Zero Distribution Cost:** Using `tsdown`, we inline the specific functions we need directly into the published bundles. At runtime, the user downloads zero external utility packages. The logic is fully self-contained. | ||
| - **Excellent Monorepo DX:** We maintain the developer experience of sharing typed code across our monorepo during development. | ||
| - **Build Tool Stability:** Inlined code avoids the issue where "build tools crash on Edge runtimes." If we used external packages that inadvertently pulled in Node-native modules (like `fs` or `path`), Edge deployment platforms (like Vercel) would crash. Inlining allows us to strictly control the exact code entering the runtime payload. | ||
|
|
||
| ### 4. Separation of Build-Time Concerns | ||
|
|
||
| By routing build-time shared logic through `@arkenv/build` (a published but internal-only package), we: | ||
|
|
||
| - Keep the core `arkenv` runtime 100% dependency-free and Edge-compatible. | ||
| - Avoid duplicating AST parsers, file watchers, and codegen logic across framework plugins. | ||
| - Allow build utilities to depend on Node-native modules (`fs`, `path`, `chokidar`) without risking runtime crashes. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - **Positive:** No version skew or runtime duplication risks for core logic. | ||
| - **Positive:** Faster user installs with fewer runtime packages. | ||
| - **Positive:** Reduced version bump cascading for internal runtime helpers. | ||
| - **Positive:** Clean separation between runtime and build-time concerns. | ||
| - **Negative:** The compiled bundle size of the core packages might be trivially larger due to inlined helpers, but this is mitigated by tree-shaking and the lightweight nature of the utilities. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| # Independent Versioning Strategy | ||
|
|
||
| ## Context | ||
|
|
||
| ArkEnv is a monorepo containing a core engine, a CLI codegen tool, multiple framework plugins, and build utilities. Choosing a single versioning strategy for all packages leads to unnecessary releases and coupling. | ||
|
|
||
| For example: | ||
|
|
||
| - A bug fix in `@arkenv/nextjs` should not force a new version of `arkenv`. | ||
| - A breaking change in `arkenv` should be reflected immediately in `@arkenv/cli` to prevent schema/codegen version mismatches. | ||
|
yamcodes marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Decision | ||
|
|
||
| We will adopt an **independent versioning model** where all packages in the monorepo float independently. There is no locked inner circle because ArkEnv has a single core engine package (`arkenv`), and all other packages (including `@arkenv/cli`) act as independent consumers or utilities. | ||
|
|
||
| ### Independent Outer Ring | ||
|
|
||
| The following packages float independently: | ||
|
|
||
| - The CLI codegen tool: `@arkenv/cli` | ||
| - Framework plugins: `@arkenv/nextjs`, `@arkenv/nuxt`, `@arkenv/vite-plugin`, `@arkenv/bun-plugin` | ||
| - Build utilities: `@arkenv/build`, `@arkenv/fumadocs-ui` | ||
|
|
||
| **Rationale:** These packages integrate with external frameworks and tools that evolve on their own schedules. A local integration fix (e.g., adapting to a new Next.js API) or a tweak to a CLI prompt should not trigger a core engine release. Independent versioning allows us to ship plugin-specific and tooling-specific fixes without cascading version bumps to the entire ecosystem. The CLI (`@arkenv/cli`) itself is merely a scaffolding utility that writes configuration files; it does not depend on deep `arkenv` internals, and therefore does not need to be version-locked to the core engine. | ||
|
|
||
| ### The Glue (Strict Peer Dependencies) | ||
|
|
||
| All framework plugins declare `arkenv` as a **strict peer dependency**: | ||
|
|
||
| - This enforces compatibility at the package manager level. | ||
| - Users are warned (or blocked) if they install an incompatible plugin/core combination. | ||
| - It prevents silent version skew that could break structural typing at runtime. | ||
|
|
||
| #### Monorepo Protocol: `workspace:*` vs `workspace:^` | ||
|
|
||
| Because this is a monorepo, plugins reference the local `arkenv` package during development. We use two different workspace protocols depending on the dependency field: | ||
|
|
||
| **`devDependencies`: `"arkenv": "workspace:*"`** | ||
|
|
||
| - During local development, this links directly to the workspace package (the folder in `packages/arkenv`), regardless of its current version. | ||
| - When published to npm, pnpm rewrites `workspace:*` to the **exact current version** (e.g., `"1.0.0-alpha.1"`). | ||
| - This is correct for `devDependencies` because they are not installed by end users. | ||
|
|
||
| **`peerDependencies`: `"arkenv": "workspace:^"`** | ||
|
|
||
| - During local development, this also links to the workspace package. | ||
| - When published to npm, pnpm rewrites `workspace:^` to a **caret range** (e.g., `"^1.0.0-alpha.1"`). | ||
| - This is **mandatory** for `peerDependencies`. Using `workspace:*` here would publish an exact pin, meaning every patch release of `arkenv` would trigger an unmet peer dependency warning for all users until they manually update every plugin. The caret range allows users to upgrade `arkenv` within the major version without friction. | ||
|
|
||
| **`dependencies` (Highly Coupled Internals): `"arkenv": "workspace:~"`** | ||
|
|
||
| - For internal packages that rely on undocumented or deep-level APIs of `arkenv` (e.g., a future `@arkenv/parser` or codegen utility that reaches into internal schema shapes), `workspace:~` is the appropriate protocol. | ||
| - When published, pnpm rewrites `workspace:~` to a **tilde range** (e.g., `"~1.0.0-alpha.1"`), which allows patch updates but locks the minor version. | ||
| - This ensures that a minor feature update to `arkenv` cannot silently break the coupled internal tool. If `arkenv` bumps its minor version, the dependent package must also be released and bumped. | ||
| - **Not currently used** in the ArkEnv monorepo, but reserved for future highly-coupled internal tools. | ||
|
|
||
| | Field | Protocol | Local Dev | Published | | ||
| | ---------------------- | ------------- | ------------------ | ------------------------------- | | ||
| | `devDependencies` | `workspace:*` | Links to workspace | Exact version (`1.0.0-alpha.1`) | | ||
| | `peerDependencies` | `workspace:^` | Links to workspace | Caret range (`^1.0.0-alpha.1`) | | ||
| | `dependencies` (tight) | `workspace:~` | Links to workspace | Tilde range (`~1.0.0-alpha.1`) | | ||
|
|
||
| > **Reference:** See [pnpm documentation on publishing workspace packages](https://pnpm.io/workspaces#publishing-workspace-packages) for full details on how `workspace:` protocols are rewritten during publish. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - **Positive:** Core engine stability. Inner-circle releases are deliberate and reflect genuine engine or codegen changes. | ||
|
yamcodes marked this conversation as resolved.
Outdated
|
||
| - **Positive:** Plugin agility. Framework integrations can iterate and release quickly without coordinating with core releases. | ||
| - **Positive:** Clear contract. Users understand that `arkenv` and `@arkenv/cli` move together, while plugins move at their own pace. | ||
|
yamcodes marked this conversation as resolved.
Outdated
|
||
| - **Positive:** Peer dependency enforcement catches mismatched installations early. | ||
| - **Negative:** Slightly more complex release management (Changesets handles this via the `fixed` and `independent` configuration mix). | ||
|
yamcodes marked this conversation as resolved.
Outdated
|
||
| - **Negative:** Plugin authors must ensure peer dependency ranges accurately reflect tested compatibility. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # @arkenv/build | ||
|
|
||
| > **Internal Package — Not for direct use.** | ||
|
|
||
| This package is published to npm solely to support the ArkEnv framework plugins (`@arkenv/nextjs`, `@arkenv/nuxt`, etc.). It contains build-time utilities that are not suitable for runtime or Edge environments. | ||
|
|
||
| ## What it does | ||
|
|
||
| - **Layout resolution:** Detects simple vs. strict multi-file schema layouts. | ||
| - **Key extraction:** Statically parses schema files to extract environment variable keys without executing them. | ||
| - **File watching:** Watches schema files during development and triggers codegen. | ||
|
|
||
| ## Why it exists | ||
|
|
||
| Build-time tools require Node-native modules (`fs`, `path`) and heavy dependencies (`chokidar`). These cannot be inlined into the runtime bundles of framework plugins without breaking Edge runtime compatibility (Vercel, Cloudflare Workers). A published package is the only way to share this logic across plugins while keeping the core `arkenv` runtime 100% dependency-free. | ||
|
|
||
| ## Stability | ||
|
|
||
| The API surface of `@arkenv/build` is **unstable** and may change without a major version bump. Do not depend on it directly in your application. It is an implementation detail of the official ArkEnv framework integrations. | ||
|
|
||
| ## Related | ||
|
|
||
| - [ADR 0009: Shared Build Package](../../docs/adr/0009-shared-build-package.md) | ||
| - [ADR 0010: Runtime Shared Logic Strategy](../../docs/adr/0010-runtime-shared-logic-strategy.md) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.