Skip to content
Merged
97 changes: 97 additions & 0 deletions openspec/changes/add-internal-types-package/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
## Context

Currently, the `InferType` type is duplicated in two locations:
- `packages/arkenv/src/create-env.ts` (internal, not exported)
- `packages/vite-plugin/src/types.ts` (used for `ImportMetaEnvAugmented`)

This creates maintenance burden and risk of divergence. We need a way to share common types between packages without:
- Exposing internal types in the public API of `arkenv`
- Creating circular dependencies
- Adding unnecessary complexity

## Goals / Non-Goals

**Goals:**
- Eliminate duplication of `InferType` and enable sharing of other common types
- Keep the core `arkenv` package focused on its public API
- Maintain bundle size constraints (types-only package, no runtime code)
- Enable future sharing of additional common types

**Non-Goals:**
- Publishing the internal types package to npm
- Creating a full-featured shared utilities package (types only)
- Breaking existing functionality or public APIs

## Decisions

### Decision: Create Internal Types Package

**What:** Create `packages/internal/types/` as a new workspace package that exports common TypeScript types.

**Why:**
- Provides a single source of truth for shared types
- Keeps internal types separate from public APIs
- Follows monorepo best practices for shared code
- Easy to extend with additional types in the future

**Alternatives considered:**
1. **Export from arkenv package** - Rejected because it would expose internal types in the public API
2. **Keep duplication** - Rejected because it creates maintenance burden and risk of divergence
3. **Put in vite-plugin and import from there** - Rejected because it creates an awkward dependency (arkenv would depend on vite-plugin)

### Decision: Use Workspace Protocol, Not Publish

**What:** The internal types package will use `workspace:*` protocol and will NOT be published to npm.

**Why:**
- It's an internal implementation detail, not part of the public API
- No need for external users to depend on it
- Keeps the package ecosystem simpler

**Alternatives considered:**
1. **Publish as `@arkenv/internal-types`** - Rejected because it's not part of the public API and would add unnecessary complexity
2. **Publish as `@arkenv/types`** - Rejected for same reasons, plus the name suggests it's a public API
3. **Use `@repo/types`** - Chosen to follow the monorepo's naming convention for internal packages

### Decision: Package Structure

**What:** The package will have a minimal structure (no build step needed):
- `index.ts` - Main entry point exporting all types
- `infer-type.ts` - `InferType` type definition
- `tsconfig.json` - TypeScript configuration for type checking only

**Why:**
- Types-only package, no runtime code, so no build step needed
- Simple structure is easier to maintain
- No `src/` folder needed since we're not building anything
- Easy to extend with additional types in the future

**Alternatives considered:**
1. **Single file** - Rejected because it will be easier to maintain as the package grows
2. **More granular structure with src/** - Rejected because we don't need a build step, so no src folder needed
3. **Build configuration** - Rejected because types-only packages don't need to be built

## Risks / Trade-offs

### Risks
- **Additional package complexity** - Mitigation: Minimal structure, types-only, no runtime code
- **Build time impact** - Mitigation: Types-only package builds quickly, Turborepo caching helps
- **Dependency management** - Mitigation: Workspace protocol is well-supported, no external dependencies

### Trade-offs
- **Simplicity vs. Reusability**: We're adding a package for better code organization, but it's a small overhead for the benefit of eliminating duplication
- **Internal vs. Public**: Keeping it internal maintains clean public APIs but requires workspace dependencies

## Migration Plan

1. Create the internal types package structure
2. Extract `InferType` to the new package
3. Update `arkenv` and `vite-plugin` to import from internal types
4. Remove duplicate definitions
5. Test to ensure everything works correctly
6. No breaking changes - this is purely internal refactoring

## Open Questions

None - the approach is straightforward and well-defined.

32 changes: 32 additions & 0 deletions openspec/changes/add-internal-types-package/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Change: Add Internal Types Package

## Why

Currently, common TypeScript types like `InferType` are duplicated across multiple packages:
- `packages/arkenv/src/create-env.ts` defines `InferType` internally (not exported)
- `packages/vite-plugin/src/types.ts` defines the same `InferType` type

This duplication creates maintenance burden and risk of divergence. Creating an internal types package will:
- Eliminate code duplication
- Provide a single source of truth for shared types
- Enable easier sharing of additional common types in the future
- Keep the core `arkenv` package focused on its public API without exposing internal types

## What Changes

- **ADDED**: New internal types package `@repo/types` (not published to npm)
- **ADDED**: Export `InferType` from the internal types package
- **MODIFIED**: `packages/arkenv/src/create-env.ts` to import `InferType` from internal types package
- **MODIFIED**: `packages/vite-plugin/src/types.ts` to import `InferType` from internal types package
- **ADDED**: Package configuration for internal types package (package.json, tsconfig.json for type checking only, no build config needed)

## Impact

- **Affected specs**: New capability `internal-types`
- **Affected code**:
- New package: `packages/internal/types/`
- `packages/arkenv/src/create-env.ts` - Import from internal types
- `packages/vite-plugin/src/types.ts` - Import from internal types
- **User-facing**: No breaking changes; this is an internal refactoring
- **Bundle size**: Minimal impact (types-only package, no runtime code)

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Internal Types Specification

## ADDED Requirements

### Requirement: Internal Types Package

The project SHALL provide an internal types package (`@repo/types`) that exports common TypeScript types shared across multiple packages. This package SHALL NOT be published to npm and is intended for internal use only within the monorepo.

#### Scenario: InferType is exported from internal types package
- **WHEN** a package needs to use the `InferType` type
- **THEN** it can import `InferType` from `@repo/types`
- **AND** the type definition is consistent across all packages using it
- **AND** there is a single source of truth for the type definition

#### Scenario: Internal types package is not published
- **WHEN** the internal types package is built
- **THEN** it is not included in npm publishing
- **AND** it is only available within the monorepo via workspace protocol
- **AND** external users cannot depend on it

#### Scenario: Types are properly exported
- **WHEN** the internal types package is built
- **THEN** all exported types are available via the package's main entry point
- **AND** TypeScript can properly resolve and use the types
- **AND** the types work correctly with workspace protocol dependencies

#### Scenario: Internal types package is included in workflows
- **WHEN** changes are made to `packages/internal/types/`
- **THEN** relevant workflows (build, test, typecheck) are triggered
- **AND** the package is built and tested as part of the CI/CD pipeline
- **AND** the package is excluded from npm publishing workflows

36 changes: 36 additions & 0 deletions openspec/changes/add-internal-types-package/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## 1. Create Internal Types Package Structure
- [x] 1.1 Create `packages/internal/types/` directory
- [x] 1.2 Create `package.json` with workspace configuration (not published)
- [x] 1.3 Create `tsconfig.json` with appropriate TypeScript configuration for type checking only
- [x] 1.4 Create `index.ts` as the main entry point (no build needed, types-only)

## 2. Extract and Export Common Types
- [x] 2.1 Create `infer-type.ts` with `InferType` type definition
- [x] 2.2 Export `InferType` from `index.ts`
- [x] 2.3 Add JSDoc comments documenting the type

## 3. Update Packages to Use Internal Types
- [x] 3.1 Add `@repo/types` dependency to `packages/arkenv/package.json`
- [x] 3.2 Update `packages/arkenv/src/create-env.ts` to import `InferType` from internal types
- [x] 3.3 Add `@repo/types` dependency to `packages/vite-plugin/package.json`
- [x] 3.4 Update `packages/vite-plugin/src/types.ts` to import `InferType` from internal types
- [x] 3.5 Remove duplicate `InferType` definitions from both packages

## 4. Update Workflows
- [x] 4.1 Review `.github/workflows/size-limit.yml` to ensure `packages/internal/**` is included (should be covered by `packages/**`)
- [x] 4.2 Review `.github/workflows/pkg-pr-new.yml` to ensure internal package is excluded from publishing (should not be published)
- [x] 4.3 Review `.github/workflows/test.yml` to ensure internal package is built and tested
- [x] 4.4 Verify all workflows that reference `packages/**` properly handle `packages/internal/**`
- [x] 4.5 Add `@repo/types` to `.changeset/config.json` ignore list
- [x] 4.6 Add `packages/internal/*` to `pnpm-workspace.yaml`

## 5. Build and Test
- [x] 5.1 Update Turborepo configuration if needed (no build task needed, types-only)
- [x] 5.2 Run type checking to ensure imports work correctly
- [x] 5.3 Run existing tests to ensure no regressions
- [x] 5.4 Verify no bundle size impact (types-only, no runtime code)

## 6. Documentation
- [x] 6.1 Add README.md to internal types package explaining its purpose
- [x] 6.2 Document that this is an internal package (not published)

1 change: 1 addition & 0 deletions packages/arkenv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"author": "Yam Borodetsky <yam@yam.codes>",
"devDependencies": {
"@ark/schema": "0.54.0",
"@repo/types": "workspace:*",
"@size-limit/esbuild-why": "11.2.0",
"@size-limit/preset-small-lib": "11.2.0",
"@types/node": "24.10.1",
Expand Down
15 changes: 1 addition & 14 deletions packages/arkenv/src/create-env.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { InferType } from "@repo/types";
import { type distill, type } from "arktype";
import { ArkEnvError } from "./errors";
import { $ } from "./scope";
Expand All @@ -6,20 +7,6 @@ type RuntimeEnvironment = Record<string, string | undefined>;

export type EnvSchema<def> = type.validate<def, (typeof $)["t"]>;

/**
* Extract the inferred type from an ArkType type definition by checking its call signature
* When a type definition is called, it returns either the validated value or type.errors
*/
type InferType<T> = T extends (
value: Record<string, string | undefined>,
) => infer R
? R extends type.errors
? never
: R
: T extends type.Any<infer U, infer _Scope>
? U
: never;

/**
* TODO: If possible, find a better type than "const T extends Record<string, unknown>",
* and be as close as possible to the type accepted by ArkType's `type`.
Expand Down
3 changes: 3 additions & 0 deletions packages/arkenv/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ export default defineConfig({
format: ["esm", "cjs"],
minify: true,
fixedExtension: false,
dts: {
resolve: ["@repo/types"],
},
});
29 changes: 29 additions & 0 deletions packages/internal/types/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# @repo/types

Internal TypeScript types shared across ArkEnv packages.

## Purpose

This package provides common TypeScript types used internally by multiple packages in the ArkEnv monorepo. It eliminates code duplication and provides a single source of truth for shared type definitions.

## Usage

This package is **internal only** and is not published to npm. It's intended for use within the monorepo via workspace protocol:

```typescript
import type { InferType } from "@repo/types";
```

## Available Types

### `InferType<T>`

Extracts the inferred type from an ArkType type definition by checking its call signature. When a type definition is called, it returns either the validated value or `type.errors`.

## Important Notes

- **Not published**: This package is marked as `private: true` and will not be published to npm
- **Internal use only**: Do not depend on this package from external projects
- **No build step**: This is a types-only package with no runtime code, so no build is needed
- **Workspace dependency**: Use `workspace:*` protocol when adding this as a dependency

8 changes: 8 additions & 0 deletions packages/internal/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Internal TypeScript types shared across ArkEnv packages.
*
* This package is not published to npm and is intended for internal use only
* within the monorepo.
*/

export type { InferType } from "./infer-type";
17 changes: 17 additions & 0 deletions packages/internal/types/infer-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { type } from "arktype";

/**
* Extract the inferred type from an ArkType type definition by checking its call signature.
* When a type definition is called, it returns either the validated value or type.errors.
*
* @template T - The ArkType type definition to infer from
*/
export type InferType<T> = T extends (
value: Record<string, string | undefined>,
) => infer R
? R extends type.errors
? never
: R
: T extends type.Any<infer U, infer _Scope>
? U
: never;
25 changes: 25 additions & 0 deletions packages/internal/types/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@repo/types",
"version": "0.0.0",
"type": "module",
"private": true,
"description": "Internal TypeScript types shared across ArkEnv packages",
"main": "./index.ts",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts"
}
},
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc"
},
"devDependencies": {
"arktype": "2.1.27",
"typescript": "5.9.3"
},
"peerDependencies": {
"arktype": "^2.1.22"
}
}
18 changes: 18 additions & 0 deletions packages/internal/types/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es2020",
"module": "preserve",
"strict": true,
"esModuleInterop": true,
"moduleResolution": "bundler",
"skipLibCheck": true,
"exactOptionalPropertyTypes": true,
"noImplicitAny": true,
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"outDir": "./dist",
"resolveJsonModule": true
},
"include": ["*.ts"]
}
1 change: 1 addition & 0 deletions packages/vite-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"arkenv": "workspace:*"
},
"devDependencies": {
"@repo/types": "workspace:*",
"@size-limit/preset-small-lib": "11.2.0",
"arktype": "2.1.27",
"size-limit": "11.2.0",
Expand Down
15 changes: 1 addition & 14 deletions packages/vite-plugin/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
import type { InferType } from "@repo/types";
import type { type } from "arktype";

/**
* Extract the inferred type from an ArkType type definition.
* When a type definition is called, it returns either the validated value or type.errors.
*/
type InferType<T> = T extends (
value: Record<string, string | undefined>,
) => infer R
? R extends type.errors
? never
: R
: T extends type.Any<infer U, infer _Scope>
? U
: never;

/**
* Filter environment variables to only include those that start with the given prefix.
* This ensures only client-exposed variables (e.g., VITE_*) are included in import.meta.env.
Expand Down
3 changes: 3 additions & 0 deletions packages/vite-plugin/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ export default defineConfig({
format: ["esm", "cjs"],
minify: true,
fixedExtension: false,
dts: {
resolve: ["@repo/types"],
},
});
Loading
Loading