Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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 Jun 12, 2026
8defbe3
docs: remove ADR 8 (belongs to issue 1200)
yamcodes Jun 13, 2026
1c6fc7f
docs: add ADR 0010 for runtime shared logic strategy
yamcodes Jun 13, 2026
8e35d3c
docs: standardize product name as ArkEnv in ADR and CLI changelog
yamcodes Jun 16, 2026
6d55945
docs: add ADR 0008 to document the migration from createEnv to arkenv…
yamcodes Jun 16, 2026
cc0b227
Please provide the list of changes or the files modified so I can gen…
yamcodes Jun 16, 2026
45d9f4f
Merge branch 'v1' into 1199-distribution-implement-standard-mode-pack…
yamcodes Jun 16, 2026
ba951a2
Merge branch 'v1' into 1199-distribution-implement-standard-mode-pack…
yamcodes Jun 19, 2026
bca2fe0
feat: implement hybrid versioning by decoupling plugins and enforcing…
yamcodes Jun 19, 2026
9f20282
feat: move arkenv to peer dependencies and update changeset documenta…
yamcodes Jun 19, 2026
26663c3
docs: add README for @arkenv/build and clarify runtime strategy in AD…
yamcodes Jun 19, 2026
036127b
docs: clarify independent versioning and fix package name references
yamcodes Jun 20, 2026
185c89a
docs: add rule for future core/standard split in versioning strategy
yamcodes Jun 20, 2026
641b4e2
docs(adr): document peer dependency rationale and CLI onboarding flow
yamcodes Jun 20, 2026
f8ef94b
docs(adr): resolve review feedback on independent versioning examples…
yamcodes Jun 20, 2026
d88cbe8
refactor: decouple plugins from core engine by replacing workspace pe…
yamcodes Jun 20, 2026
0f5e61a
docs: add enterprise precedent for peer dependency strategy in ADR 0011
yamcodes Jun 20, 2026
61340e1
Merge remote-tracking branch 'origin/dev' into 1199-distribution-impl…
yamcodes Jun 23, 2026
a970f14
Merge branch 'v1' into 1199-distribution-implement-standard-mode-pack…
yamcodes Jun 23, 2026
7820207
docs: resolve changeset peerDependency range review comment and AutoT…
yamcodes Jun 23, 2026
a710669
Merge branch 'v1' into 1199-distribution-implement-standard-mode-pack…
yamcodes Jun 23, 2026
57b30a5
refactor: rename nuxt internal file to arkenv-internal
yamcodes Jun 23, 2026
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
33 changes: 33 additions & 0 deletions .changeset/epg3nspi.md
Comment thread
yamcodes marked this conversation as resolved.
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.
Comment thread
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.
40 changes: 40 additions & 0 deletions docs/adr/0007-standard-mode-packaging-strategy.md
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.
82 changes: 82 additions & 0 deletions docs/adr/0010-runtime-shared-logic-strategy.md
Comment thread
yamcodes marked this conversation as resolved.
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.
84 changes: 84 additions & 0 deletions docs/adr/0011-independent-versioning-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 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`.
- An update to `@arkenv/cli` (e.g., modifying its interactive prompt or code templates) should not require a new release of `arkenv`.

## 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.

### Future Core Package Split

If the `arkenv` package is ever split into `@arkenv/core` and `@arkenv/standard` in the future, those two packages **must be versioned together in lockstep** (added to the `fixed` group in the Changesets config). Because they would constitute the core runtime engine together, a version mismatch between them could cause critical compatibility errors or break structural typing. For now, since `arkenv` is a single package, the `fixed` group remains empty.

### 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 (e.g., `@arkenv/nextjs`, `@arkenv/bun-plugin`) declare `arkenv` as a **strict peer dependency** rather than a regular dependency.

#### Why not regular dependencies?

- **Avoid Duplication & Runtime Failures:** Wrapping `arkenv` as a regular dependency in plugins risks duplicating the core engine package in `node_modules` (due to varying version resolution matching or hoisting strategies of package managers).
- **Structural Typing & Singletons:** Duplication breaks ArkType's structural typing and `instanceof` checks (e.g., schema validation context, symbols, internal singletons). Having a single shared instance of the `arkenv` core engine across the user's codebase is critical.

#### Enforcing Peer Dependencies Without Sacrificing DX

While requiring peer dependencies can sometimes lead to extra manual installation steps, the developer experience (DX) is fully preserved:

- **CLI-Forward Scaffolding:** The starting point and primary onboarding path for ArkEnv projects is running `npx arkenv init`. This CLI-forward approach handles project initialization, dependency installation, and boilerplate generation automatically.
- **Auto-Installation:** Modern package managers (NPM v7+, PNPM, Yarn) automatically resolve and install peer dependencies by default, removing manual friction for the end user while maintaining runtime singleton safety.

#### 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": "^1.0.0"`**

- We use the **Wide Peer** strategy by hardcoding the absolute minimum supported version of `arkenv`. We completely drop the `workspace:` prefix here.
- This decoupling is necessary to solve the "Artificial Floor" problem where using `workspace:^` forces the published package to artificially require the exact version of the core engine present in the monorepo at publish time.
- Because it is a regular hardcoded range, anyone from `1.0.0` upwards can install the new plugin update without being forced to upgrade their core engine.

**`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` | `^1.0.0` | Standard resolution | Hardcoded range (`^1.0.0`) |
| `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. The core `arkenv` package is only released when there are genuine engine-level changes or bug fixes.
- **Positive:** Plugin agility. Framework integrations can iterate and release quickly without coordinating with core releases.
- **Positive:** Clear contract. Users understand that each tool (including `@arkenv/cli`) floats and releases on its own schedule.
- **Positive:** Peer dependency enforcement catches mismatched installations early.
- **Negative:** Release management requires tracking changelogs and versions across separate packages independently.
- **Negative:** Plugin authors must ensure peer dependency ranges accurately reflect tested compatibility.
24 changes: 24 additions & 0 deletions packages/build/README.md
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)
5 changes: 2 additions & 3 deletions packages/bun-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"dependencies": {
"arkenv": "workspace:*"
},
"devDependencies": {
"@repo/types": "workspace:*",
"@size-limit/esbuild-why": "catalog:",
"@size-limit/preset-small-lib": "catalog:",
"@types/bun": "catalog:",
"arkenv": "workspace:*",
"arktype": "catalog:",
"bun": "catalog:",
"rimraf": "catalog:",
Expand All @@ -25,6 +23,7 @@
"vitest": "catalog:"
},
"peerDependencies": {
"arkenv": "^1.0.0",
"arktype": "^2.1.22",
"bun": "^1.0.0"
},
Expand Down
Loading
Loading