From 98da6ac47027c0ac87da8b85ea03dbc4c6aa0c5d Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Fri, 24 Jul 2026 12:12:19 +0500 Subject: [PATCH 01/15] fix: Forward-port fork Deploy-Preview secrets fix from #1481 (#1485) ## Summary - Ports [#1481](https://github.com/yamcodes/arkenv/pull/1481) (`dev`) to `v1`: `pull_request_target` for labeled PR previews so fork PRs receive Vercel secrets. - Keeps secret hygiene (checkout PR HEAD with `persist-credentials: false`; inject `VERCEL_*` only on Vercel CLI steps). - Updates CONTRIBUTING preview section to match; no package/changeset changes (workflows + docs only). ## Test plan - [ ] Same-repo / fork PRs targeting `v1` with `preview` get a green Actions Deploy-Preview after this merges - [ ] Unlabeled fork PRs do not deploy Made with [Cursor](https://cursor.com) Co-authored-by: Cursor --- .github/workflows/preview-www-default.yml | 5 +- .github/workflows/preview-www-labeled.yml | 3 +- .github/workflows/preview-www-reusable.yml | 60 +++++++++++++++------- docs/CONTRIBUTING.md | 2 +- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/.github/workflows/preview-www-default.yml b/.github/workflows/preview-www-default.yml index b6cee9371..be975c16c 100644 --- a/.github/workflows/preview-www-default.yml +++ b/.github/workflows/preview-www-default.yml @@ -1,7 +1,9 @@ name: Preview www (Default Events) on: - pull_request: + # pull_request_target so fork PRs receive repository secrets (VERCEL_*). + # Workflow YAML is taken from the base branch; we checkout PR HEAD in the reusable job. + pull_request_target: types: [opened, synchronize, ready_for_review] branches: - dev @@ -19,6 +21,7 @@ jobs: Deploy-Preview: # Branch previews (push to dev/v1) always deploy. PR previews are opt-in: # they only deploy when the PR carries the `preview` label, regardless of draft state. + # Fork authors cannot apply labels (triage+ required), so this is not self-serve spamable. if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'preview') uses: ./.github/workflows/preview-www-reusable.yml secrets: inherit diff --git a/.github/workflows/preview-www-labeled.yml b/.github/workflows/preview-www-labeled.yml index 8043e4714..13ba6fd8f 100644 --- a/.github/workflows/preview-www-labeled.yml +++ b/.github/workflows/preview-www-labeled.yml @@ -1,7 +1,8 @@ name: Preview www (Label Triggered) on: - pull_request: + # pull_request_target so fork PRs receive repository secrets when a maintainer applies `preview`. + pull_request_target: types: [labeled] branches: - dev diff --git a/.github/workflows/preview-www-reusable.yml b/.github/workflows/preview-www-reusable.yml index b65dd6daf..db9c2d4d9 100644 --- a/.github/workflows/preview-www-reusable.yml +++ b/.github/workflows/preview-www-reusable.yml @@ -3,25 +3,33 @@ name: Preview www Reusable on: workflow_call: +# Non-secret defaults only. Vercel credentials are injected solely on Vercel CLI steps +# so untrusted PR HEAD install/build scripts do not see them ambiently. env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} TZ: ${{ vars.TIMEZONE || 'Asia/Almaty' }} jobs: Deploy-Preview: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - uses: actions/checkout@v6 with: fetch-depth: 0 + # pull_request_target defaults to the base ref; use the base repo's pull head + # ref so fork commits resolve and base history remains available for turbo. + ref: ${{ github.event.pull_request.number && format('refs/pull/{0}/head', github.event.pull_request.number) || github.sha }} + persist-credentials: false - name: Set Turbo base/head for PRs + env: + PR_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} run: | - echo "TURBO_SCM_BASE=${{ github.event.pull_request.base.sha || github.event.before }}" >> $GITHUB_ENV - echo "TURBO_SCM_HEAD=${{ github.sha }}" >> $GITHUB_ENV + echo "TURBO_SCM_BASE=${PR_BASE_SHA}" >> "$GITHUB_ENV" + echo "TURBO_SCM_HEAD=${PR_HEAD_SHA}" >> "$GITHUB_ENV" - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v6 with: @@ -31,45 +39,61 @@ jobs: run: pnpm install - name: Check if www is affected id: affected + env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} run: | - BASE_SHA="${{ github.event.pull_request.base.sha || github.event.before }}" if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then BASE_SHA="HEAD~1" fi - echo "Comparing base ref: $BASE_SHA with head: ${{ github.sha }}" - - if pnpm exec turbo query affected --packages=www --base="$BASE_SHA" --head="${{ github.sha }}" --exit-code; then + echo "Comparing base ref: $BASE_SHA with head: $HEAD_SHA" + + if pnpm exec turbo query affected --packages=www --base="$BASE_SHA" --head="$HEAD_SHA" --exit-code; then echo "www is NOT affected." - echo "is_affected=false" >> $GITHUB_OUTPUT + echo "is_affected=false" >> "$GITHUB_OUTPUT" else echo "www IS affected." - echo "is_affected=true" >> $GITHUB_OUTPUT + echo "is_affected=true" >> "$GITHUB_OUTPUT" fi - name: Install Vercel CLI # Pin the CLI to avoid upstream breakage from newly published majors. run: npm install --global vercel@54.7.1 - name: Pull Vercel Environment Information if: steps.affected.outputs.is_affected == 'true' - run: node scripts/vercel-wrapper.cjs pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: node scripts/vercel-wrapper.cjs pull --yes --environment=preview --token="$VERCEL_TOKEN" - name: Build Project Artifacts if: steps.affected.outputs.is_affected == 'true' - run: node scripts/vercel-wrapper.cjs build --token=${{ secrets.VERCEL_TOKEN }} + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + run: node scripts/vercel-wrapper.cjs build --token="$VERCEL_TOKEN" - name: Deploy Project Artifacts to Vercel if: steps.affected.outputs.is_affected == 'true' id: deploy + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: | set -o pipefail - DEPLOYMENT_URL=$(node scripts/vercel-wrapper.cjs deploy --prebuilt --archive=tgz --token=${{ secrets.VERCEL_TOKEN }} | tail -n 1) - echo "DEPLOYMENT_URL=$DEPLOYMENT_URL" >> $GITHUB_OUTPUT + DEPLOYMENT_URL=$(node scripts/vercel-wrapper.cjs deploy --prebuilt --archive=tgz --token="$VERCEL_TOKEN" | tail -n 1) + echo "DEPLOYMENT_URL=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT" - name: Generate GitHub App Token id: generate-token uses: actions/create-github-app-token@v3 - if: env.APP_PRIVATE_KEY != '' + if: vars.APP_ID != '' with: client-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - name: Comment PR - if: github.event_name == 'pull_request' && steps.affected.outputs.is_affected == 'true' + if: github.event_name != 'push' && steps.affected.outputs.is_affected == 'true' uses: actions/github-script@v9 with: github-token: ${{ steps.generate-token.outputs.token || secrets.GITHUB_TOKEN }} diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 8768f0350..91802200e 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -148,7 +148,7 @@ When working on a massive marketing push, docs facelift, or breaking API changes ## Preview deployments -PR previews for the `www` app are opt-in. Apply the `preview` label to a PR to trigger a Vercel preview deployment on `opened`, `synchronize`, and `ready_for_review` events (a preview is only produced when the `www` app is actually affected). Pushes to `dev` or `v1` continue to deploy rolling branch previews automatically. +PR previews for the `www` app are opt-in. A maintainer (triage+) applies the `preview` label to trigger a Vercel preview deployment when the label is added, and again on subsequent `synchronize` / `ready_for_review` events while the label remains (a preview is only produced when the `www` app is actually affected). This works for same-repo and fork PRs; fork authors cannot self-serve the label. Pushes to `dev` or `v1` continue to deploy rolling branch previews automatically. ## Changesets From 01480f3197235cb830660b773fdab99df2fde1fd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 07:13:48 +0000 Subject: [PATCH 02/15] [autofix.ci] apply automated fixes --- .changeset/pre.json | 120 ++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index d6163b9a6..f52140d3f 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,62 +1,62 @@ { - "mode": "pre", - "tag": "alpha", - "initialVersions": { - "arkenv-cli-playground": "0.0.0", - "bun-react-playground": "0.1.0", - "vite-playground": "0.0.0", - "vite-legacy-playground": "0.0.0", - "@repo/playwright-www": "0.0.0", - "www": "0.0.0", - "arkenv": "0.11.1", - "@arkenv/bun-plugin": "0.1.7", - "@arkenv/cli": "0.2.10", - "@arkenv/fumadocs-ui": "0.0.8", - "@repo/scope": "0.1.3", - "@repo/types": "0.1.0", - "@arkenv/nextjs": "0.0.7", - "@arkenv/vite-plugin": "0.1.1", - "@arkenv/build": "0.0.1", - "@arkenv/nuxt": "0.0.1", - "@arkenv/core": "1.0.0-alpha.2", - "@repo/utils": "0.1.3", - "@arkenv/standard": "1.0.0-alpha.2", - "@repo/log": "0.0.1" - }, - "changesets": [ - "add-host-preset-add-command", - "add-host-presets-phase-4", - "add-host-strict-layouts-forward-port", - "align-missing-schema-throw", - "bun-env-module-transform", - "drop-example-e-alias", - "epg3nspi", - "fix-nuxt-proxy-coercion", - "forward-port-hosting-presets", - "forward-port-keywords-typetable", - "host-preset-alias", - "improve-npm-keywords", - "init-v1", - "injectable-logger-api", - "machine-actionable-error-codes", - "nuxt-augment-config-key", - "nuxt-boot-gate-thin-accessors", - "nuxt-flat-layout-cli-alignment", - "nuxt-module-options-jsdoc-v1", - "nuxt-skip-setup-before-boot-gate", - "nuxt-strict-auto-extend", - "nuxt-strict-client-shared-auto-extend", - "reconcile-v0-features", - "remove-framework-shared-exports", - "rename-create-env-to-arkenv", - "standard-isolation-guards", - "standard-mode-flat-layout", - "standard-mode-packaging", - "trim-short-flag-aliases", - "unified-error-normalization", - "unify-coercion", - "valibot-example-registry", - "vite-env-module-transform", - "walk-back-env-starters" - ] + "mode": "pre", + "tag": "alpha", + "initialVersions": { + "arkenv-cli-playground": "0.0.0", + "bun-react-playground": "0.1.0", + "vite-playground": "0.0.0", + "vite-legacy-playground": "0.0.0", + "@repo/playwright-www": "0.0.0", + "www": "0.0.0", + "arkenv": "0.11.1", + "@arkenv/bun-plugin": "0.1.7", + "@arkenv/cli": "0.2.10", + "@arkenv/fumadocs-ui": "0.0.8", + "@repo/scope": "0.1.3", + "@repo/types": "0.1.0", + "@arkenv/nextjs": "0.0.7", + "@arkenv/vite-plugin": "0.1.1", + "@arkenv/build": "0.0.1", + "@arkenv/nuxt": "0.0.1", + "@arkenv/core": "1.0.0-alpha.2", + "@repo/utils": "0.1.3", + "@arkenv/standard": "1.0.0-alpha.2", + "@repo/log": "0.0.1" + }, + "changesets": [ + "add-host-preset-add-command", + "add-host-presets-phase-4", + "add-host-strict-layouts-forward-port", + "align-missing-schema-throw", + "bun-env-module-transform", + "drop-example-e-alias", + "epg3nspi", + "fix-nuxt-proxy-coercion", + "forward-port-hosting-presets", + "forward-port-keywords-typetable", + "host-preset-alias", + "improve-npm-keywords", + "init-v1", + "injectable-logger-api", + "machine-actionable-error-codes", + "nuxt-augment-config-key", + "nuxt-boot-gate-thin-accessors", + "nuxt-flat-layout-cli-alignment", + "nuxt-module-options-jsdoc-v1", + "nuxt-skip-setup-before-boot-gate", + "nuxt-strict-auto-extend", + "nuxt-strict-client-shared-auto-extend", + "reconcile-v0-features", + "remove-framework-shared-exports", + "rename-create-env-to-arkenv", + "standard-isolation-guards", + "standard-mode-flat-layout", + "standard-mode-packaging", + "trim-short-flag-aliases", + "unified-error-normalization", + "unify-coercion", + "valibot-example-registry", + "vite-env-module-transform", + "walk-back-env-starters" + ] } From f86887c55c13c979451ff85d6d4ec5d3d69113dd Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Fri, 24 Jul 2026 15:13:10 +0500 Subject: [PATCH 03/15] fix: Forward-port split CLI `--help` Global/init options from dev (#1487) --- .changeset/split-help-global-init-options.md | 30 +++++++++ packages/arkenv/src/cli/commands/help.test.ts | 62 ++++++++++++++----- packages/arkenv/src/cli/commands/help.ts | 39 +++++++----- 3 files changed, 99 insertions(+), 32 deletions(-) create mode 100644 .changeset/split-help-global-init-options.md diff --git a/.changeset/split-help-global-init-options.md b/.changeset/split-help-global-init-options.md new file mode 100644 index 000000000..c44f18e03 --- /dev/null +++ b/.changeset/split-help-global-init-options.md @@ -0,0 +1,30 @@ +--- +"arkenv": patch +--- + +#### Split `--help` options into Global and `init` sections + +List shared flags under **Global options** and scaffolding flags under **init options**, matching the multi-command `/docs/cli` taxonomy. + +```bash +npx arkenv@next --help +``` + +```text +Usage: + arkenv init [project-name] ... + arkenv add host [provider] ... + +Global options: + --yes, -y Skip prompts and use defaults ... + --quiet, -q Quiet mode ... + --json, -j Output structured JSON ... + --agent Enable non-interactive, machine-readable mode ... + --help, -h Show this help message + +init options: + --example Specify an example name ... + --force, -f Bypass checks and force scaffolding + --no-codegen Disable automatic env.gen.ts code generation ... + --host-preset, -H Specify a hosting provider preset ... +``` diff --git a/packages/arkenv/src/cli/commands/help.test.ts b/packages/arkenv/src/cli/commands/help.test.ts index 137a23515..323250e50 100644 --- a/packages/arkenv/src/cli/commands/help.test.ts +++ b/packages/arkenv/src/cli/commands/help.test.ts @@ -5,7 +5,7 @@ import { version } from "../../../package.json"; import { HelpUseCase } from "./help"; describe("HelpUseCase", () => { - it("should display the help message with dynamic column alignment", async () => { + it("should display the help message with Global and init option sections", async () => { const logs: string[] = []; const logger = { log: vi.fn().mockImplementation((msg: string) => { @@ -37,27 +37,52 @@ describe("HelpUseCase", () => { " arkenv add host [provider] Add hosting provider preset (vercel, netlify, cloudflare, railway, render, fly) to schema", ); - // Options should be aligned based on the longest option (--host-preset, -H ) which is 26 chars - // leftPad (2) + longest flag (26) + colGap (4) = 32 characters start index for descriptions. + const globalHeaderIndex = logs.findIndex((l) => + l.includes(pc.bold("Global options:")), + ); + const initHeaderIndex = logs.findIndex((l) => + l.includes(pc.bold("init options:")), + ); + expect(globalHeaderIndex).toBeGreaterThan(-1); + expect(initHeaderIndex).toBeGreaterThan(globalHeaderIndex); + + // Global options align on the longest flag in that section (--quiet, -q = 11 chars) const yesOptionLog = logs.find((l) => l.includes("--yes, -y")); expect(yesOptionLog).toBeDefined(); - // "--yes, -y" is 9 chars. max (26) - 9 + colGap (4) = 21 spaces padding. + // "--yes, -y" is 9 chars. max (11) - 9 + colGap (4) = 6 spaces padding. expect(yesOptionLog).toBe( - " --yes, -y Skip prompts and use defaults (also passed to subprocesses)", + " --yes, -y Skip prompts and use defaults (also passed to subprocesses)", ); + expect(logs.indexOf(yesOptionLog as string)).toBeGreaterThan( + globalHeaderIndex, + ); + expect(logs.indexOf(yesOptionLog as string)).toBeLessThan(initHeaderIndex); + const agentOptionLog = logs.find((l) => l.includes("--agent")); + expect(agentOptionLog).toBeDefined(); + // "--agent" is 7 chars. max (11) - 7 + colGap (4) = 8 spaces padding. + expect(agentOptionLog).toBe( + " --agent Enable non-interactive, machine-readable mode for AI agents. Bypasses all prompts and outputs structured JSON. Macro for --yes --quiet --json", + ); + expect(logs.indexOf(agentOptionLog as string)).toBeLessThan( + initHeaderIndex, + ); + + const helpOptionLog = logs.find((l) => l.includes("--help, -h")); + expect(helpOptionLog).toBeDefined(); + // "--help, -h" is 10 chars. max (11) - 10 + colGap (4) = 5 spaces padding. + expect(helpOptionLog).toBe(" --help, -h Show this help message"); + expect(logs.indexOf(helpOptionLog as string)).toBeLessThan(initHeaderIndex); + + // Init options align on --host-preset, -H (26 chars) and must not appear under Global const exampleOptionLog = logs.find((l) => l.includes("--example")); expect(exampleOptionLog).toBeDefined(); // "--example" is 9 chars. max (26) - 9 + colGap (4) = 21 spaces padding. expect(exampleOptionLog).toBe( " --example Specify an example name when creating a new project", ); - - const agentOptionLog = logs.find((l) => l.includes("--agent")); - expect(agentOptionLog).toBeDefined(); - // "--agent" is 7 chars. max (26) - 7 + colGap (4) = 23 spaces padding. - expect(agentOptionLog).toBe( - " --agent Enable non-interactive, machine-readable mode for AI agents. Bypasses all prompts and outputs structured JSON. Macro for --yes --quiet --json", + expect(logs.indexOf(exampleOptionLog as string)).toBeGreaterThan( + initHeaderIndex, ); const noCodegenOptionLog = logs.find((l) => l.includes("--no-codegen")); @@ -66,6 +91,9 @@ describe("HelpUseCase", () => { expect(noCodegenOptionLog).toBe( " --no-codegen Disable automatic env.gen.ts code generation for Next.js", ); + expect(logs.indexOf(noCodegenOptionLog as string)).toBeGreaterThan( + initHeaderIndex, + ); const hostPresetOptionLog = logs.find((l) => l.includes("--host-preset, -H "), @@ -75,12 +103,14 @@ describe("HelpUseCase", () => { expect(hostPresetOptionLog).toBe( " --host-preset, -H Specify a hosting provider preset (none, vercel, netlify, cloudflare, railway, render, fly)", ); + expect(logs.indexOf(hostPresetOptionLog as string)).toBeGreaterThan( + initHeaderIndex, + ); - const helpOptionLog = logs.find((l) => l.includes("--help, -h")); - expect(helpOptionLog).toBeDefined(); - // "--help, -h" is 10 chars. max (26) - 10 + colGap (4) = 20 spaces padding. - expect(helpOptionLog).toBe( - " --help, -h Show this help message", + const forceOptionLog = logs.find((l) => l.includes("--force, -f")); + expect(forceOptionLog).toBeDefined(); + expect(logs.indexOf(forceOptionLog as string)).toBeGreaterThan( + initHeaderIndex, ); }); }); diff --git a/packages/arkenv/src/cli/commands/help.ts b/packages/arkenv/src/cli/commands/help.ts index e41da60a1..d970bf09a 100644 --- a/packages/arkenv/src/cli/commands/help.ts +++ b/packages/arkenv/src/cli/commands/help.ts @@ -45,15 +45,18 @@ export class HelpUseCase { }, ]; - const options: HelpItem[] = [ + const globalOptions: HelpItem[] = [ { left: "--yes, -y", right: "Skip prompts and use defaults (also passed to subprocesses)", }, { - left: "--force, -f", - right: - "Bypass technical requirement checks and dirty git working tree check, then force scaffolding", + left: "--quiet, -q", + right: "Quiet mode: Suppress output, capture logs on failure", + }, + { + left: "--json, -j", + right: "Output structured JSON to stdout", }, { left: "--agent", @@ -61,16 +64,20 @@ export class HelpUseCase { "Enable non-interactive, machine-readable mode for AI agents. Bypasses all prompts and outputs structured JSON. Macro for --yes --quiet --json", }, { - left: "--example", - right: "Specify an example name when creating a new project", + left: "--help, -h", + right: "Show this help message", }, + ]; + + const initOptions: HelpItem[] = [ { - left: "--quiet, -q", - right: "Quiet mode: Suppress output, capture logs on failure", + left: "--example", + right: "Specify an example name when creating a new project", }, { - left: "--json, -j", - right: "Output structured JSON to stdout", + left: "--force, -f", + right: + "Bypass technical requirement checks and dirty git working tree check, then force scaffolding", }, { left: "--no-codegen", @@ -81,10 +88,6 @@ export class HelpUseCase { right: "Specify a hosting provider preset (none, vercel, netlify, cloudflare, railway, render, fly)", }, - { - left: "--help, -h", - right: "Show this help message", - }, ]; this.logger.log(`ArkEnv CLI v${version}`); @@ -92,8 +95,12 @@ export class HelpUseCase { for (const line of formatColumns(commands)) { this.logger.log(line); } - this.logger.log(`\n${pc.bold("Options:")}`); - for (const line of formatColumns(options)) { + this.logger.log(`\n${pc.bold("Global options:")}`); + for (const line of formatColumns(globalOptions)) { + this.logger.log(line); + } + this.logger.log(`\n${pc.bold("init options:")}`); + for (const line of formatColumns(initOptions)) { this.logger.log(line); } } From 9d5bdbbeaf2fdddf69f5bcc47a7d79b15a51ece3 Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Fri, 24 Jul 2026 15:13:55 +0500 Subject: [PATCH 04/15] fix: Forward-port align missing-schema errors from #1482 (#1488) --- .changeset/align-missing-schema-host-hints.md | 9 ++++++ packages/bun-plugin/src/env-module.test.ts | 30 +++++++++++++++++++ packages/nextjs/src/config/setup.ts | 2 +- packages/nuxt/src/config.ts | 2 +- packages/nuxt/src/module.ts | 2 +- packages/vite-plugin/src/env-module-path.ts | 2 +- packages/vite-plugin/src/env-module.test.ts | 30 +++++++++++++++++++ 7 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 .changeset/align-missing-schema-host-hints.md diff --git a/.changeset/align-missing-schema-host-hints.md b/.changeset/align-missing-schema-host-hints.md new file mode 100644 index 000000000..87f803bc3 --- /dev/null +++ b/.changeset/align-missing-schema-host-hints.md @@ -0,0 +1,9 @@ +--- +"@arkenv/vite-plugin": patch +"@arkenv/nextjs": patch +"@arkenv/nuxt": patch +--- + +#### Align missing-schema errors with short, actionable host guidance + +Point missing-schema errors at checked paths / `schemaPath` and `arkenv init`, matching the Bun plugin style, without embedding starter `env.ts` modules. diff --git a/packages/bun-plugin/src/env-module.test.ts b/packages/bun-plugin/src/env-module.test.ts index 233011325..01f306382 100644 --- a/packages/bun-plugin/src/env-module.test.ts +++ b/packages/bun-plugin/src/env-module.test.ts @@ -297,3 +297,33 @@ describe("SPA mode regression", () => { delete process.env.BUN_PUBLIC_TEST; }); }); + +describe("missing-schema errors", () => { + const temps: string[] = []; + + afterEach(() => { + for (const dir of temps.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("throws a short discovery error without an env.ts starter", async () => { + const { resolveEnvModulePath } = await import("./env-module-path.js"); + const root = mkdtempSync(join(tmpdir(), "arkenv-bun-missing-schema-")); + temps.push(root); + + let message = ""; + try { + resolveEnvModulePath(root); + } catch (error) { + message = error instanceof Error ? error.message : String(error); + } + + expect(message).toMatch(/could not find an env module/); + expect(message).toMatch(/arkenv init/); + expect(message).not.toMatch(/Example `src\/env\.ts`/); + expect(message).not.toMatch(/```/); + expect(message).not.toMatch(/import \{ type \} from "arktype"/); + expect(message).not.toMatch(/from "zod"/); + }); +}); diff --git a/packages/nextjs/src/config/setup.ts b/packages/nextjs/src/config/setup.ts index a8efa6fc1..2591665b7 100644 --- a/packages/nextjs/src/config/setup.ts +++ b/packages/nextjs/src/config/setup.ts @@ -59,7 +59,7 @@ export function setupArkEnv( formatBuildError( `Could not find schema file at ${ options?.schemaPath || "src/env.ts or env.ts" - }. Please specify 'schemaPath' in setupArkEnv options.`, + }. Please specify 'schemaPath' in setupArkEnv options (or run \`arkenv init\`).`, ), ); } diff --git a/packages/nuxt/src/config.ts b/packages/nuxt/src/config.ts index 3eda354c5..c352dc89a 100644 --- a/packages/nuxt/src/config.ts +++ b/packages/nuxt/src/config.ts @@ -133,7 +133,7 @@ export function setupArkEnv( formatBuildError( `Could not find schema file at ${ options?.schemaPath || "src/env.ts or env.ts" - }. Please specify 'schemaPath' in ArkEnv options.`, + }. Please specify 'schemaPath' in ArkEnv options (or run \`arkenv init\`).`, ), ); } diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index c93f80d9c..10f3ed8d8 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -82,7 +82,7 @@ const module: NuxtModule = defineNuxtModule({ throw new Error( `[ArkEnv] Could not find schema file at ${ options.schemaPath || "src/env.ts or env.ts" - }. Please specify 'schemaPath' in ArkEnv options.`, + }. Please specify 'schemaPath' in ArkEnv options (or run \`arkenv init\`).`, ); } diff --git a/packages/vite-plugin/src/env-module-path.ts b/packages/vite-plugin/src/env-module-path.ts index ab1532a69..f474bc34a 100644 --- a/packages/vite-plugin/src/env-module-path.ts +++ b/packages/vite-plugin/src/env-module-path.ts @@ -64,7 +64,7 @@ export function resolveEnvModulePath( const discovered = findSchemaPath(root); if (!discovered) { throw new Error( - `ArkEnv Vite plugin: could not find an env module. Expected "src/env.ts" or "env.ts" under "${root}", or pass schemaPath.`, + `ArkEnv Vite plugin: could not find an env module. Expected "src/env.ts" or "env.ts" under "${root}", or pass schemaPath (or run \`arkenv init\`).`, ); } return discovered; diff --git a/packages/vite-plugin/src/env-module.test.ts b/packages/vite-plugin/src/env-module.test.ts index d47f96572..5c3855e66 100644 --- a/packages/vite-plugin/src/env-module.test.ts +++ b/packages/vite-plugin/src/env-module.test.ts @@ -244,3 +244,33 @@ describe("transform mode plugin", () => { expect(result?.code).not.toContain("@arkenv/core"); }); }); + +describe("missing-schema errors", () => { + const temps: string[] = []; + + afterEach(() => { + for (const dir of temps.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("throws a short discovery error without an env.ts starter", async () => { + const { resolveEnvModulePath } = await import("./env-module-path.js"); + const root = mkdtempSync(join(tmpdir(), "arkenv-vite-missing-schema-")); + temps.push(root); + + let message = ""; + try { + resolveEnvModulePath(root); + } catch (error) { + message = error instanceof Error ? error.message : String(error); + } + + expect(message).toMatch(/could not find an env module/); + expect(message).toMatch(/arkenv init/); + expect(message).not.toMatch(/Example `src\/env\.ts`/); + expect(message).not.toMatch(/```/); + expect(message).not.toMatch(/import \{ type \} from "arktype"/); + expect(message).not.toMatch(/from "zod"/); + }); +}); From 2049b3f5ef1641d0d3f275b363ecb5a688972a13 Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Fri, 24 Jul 2026 15:16:50 +0500 Subject: [PATCH 05/15] Update split-help-global-init-options.md --- .changeset/split-help-global-init-options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/split-help-global-init-options.md b/.changeset/split-help-global-init-options.md index c44f18e03..d8df262b5 100644 --- a/.changeset/split-help-global-init-options.md +++ b/.changeset/split-help-global-init-options.md @@ -7,7 +7,7 @@ List shared flags under **Global options** and scaffolding flags under **init options**, matching the multi-command `/docs/cli` taxonomy. ```bash -npx arkenv@next --help +npx arkenv@alpha --help ``` ```text From 97291a5d5dd6860e687308b5d6f910c93c68ae5f Mon Sep 17 00:00:00 2001 From: Yam C Borodetsky Date: Sat, 25 Jul 2026 17:57:41 +0500 Subject: [PATCH 06/15] chore: add deploy-probe.txt on v1 for #1460 verification Co-authored-by: Cursor --- apps/www/public/deploy-probe.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/www/public/deploy-probe.txt diff --git a/apps/www/public/deploy-probe.txt b/apps/www/public/deploy-probe.txt new file mode 100644 index 000000000..6e6e3b8b4 --- /dev/null +++ b/apps/www/public/deploy-probe.txt @@ -0,0 +1 @@ +1460-verify-20260725124821 From 586e31e3bacd49ef95241663c8ad8193701644b0 Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Sat, 25 Jul 2026 18:02:52 +0500 Subject: [PATCH 07/15] ci: forward-port Actions Vercel branch-domain wiring to v1 (#1460) --- .github/workflows/preview-www-reusable.yml | 58 +++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/.github/workflows/preview-www-reusable.yml b/.github/workflows/preview-www-reusable.yml index db9c2d4d9..b01c9983c 100644 --- a/.github/workflows/preview-www-reusable.yml +++ b/.github/workflows/preview-www-reusable.yml @@ -49,6 +49,13 @@ jobs: fi echo "Comparing base ref: $BASE_SHA with head: $HEAD_SHA" + # Deploy-path edits must still rebuild/alias branch domains (turbo won't mark www). + if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -qE '^(\.github/workflows/preview-www|scripts/vercel-wrapper\.cjs)'; then + echo "Preview deploy path changed; forcing www deploy." + echo "is_affected=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + if pnpm exec turbo query affected --packages=www --base="$BASE_SHA" --head="$HEAD_SHA" --exit-code; then echo "www is NOT affected." echo "is_affected=false" >> "$GITHUB_OUTPUT" @@ -81,10 +88,59 @@ jobs: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + # Git meta so CLI deploys associate with the correct branch (branch domains / env). + # See https://vercel.com/kb/guide/branch-variables-and-domains-not-linked-to-cli-deployments + GIT_ORG: ${{ github.repository_owner }} + GIT_REPO: ${{ github.event.repository.name }} + GIT_REF: ${{ github.event.pull_request.head.ref || github.ref_name }} + GIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + GIT_ACTOR: ${{ github.event.pull_request.head.user.login || github.actor }} + GIT_COMMIT_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message || github.sha }} + GIT_COMMIT_AUTHOR_NAME: ${{ github.event.head_commit.author.name || github.actor }} run: | set -o pipefail - DEPLOYMENT_URL=$(node scripts/vercel-wrapper.cjs deploy --prebuilt --archive=tgz --token="$VERCEL_TOKEN" | tail -n 1) + SHORT_MSG="$(printf '%s\n' "$GIT_COMMIT_MESSAGE" | head -n1)" + DEPLOYMENT_URL=$(node scripts/vercel-wrapper.cjs deploy --prebuilt --archive=tgz --token="$VERCEL_TOKEN" \ + -m githubDeployment=1 \ + -m githubOrg="$GIT_ORG" \ + -m githubRepo="$GIT_REPO" \ + -m githubCommitOrg="$GIT_ORG" \ + -m githubCommitRepo="$GIT_REPO" \ + -m githubCommitRef="$GIT_REF" \ + -m githubCommitSha="$GIT_SHA" \ + -m githubCommitMessage="$SHORT_MSG" \ + -m githubCommitAuthorName="$GIT_COMMIT_AUTHOR_NAME" \ + -m githubCommitAuthorLogin="$GIT_ACTOR" \ + | tail -n 1) echo "DEPLOYMENT_URL=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT" + # Explicit alias so branch domains stay current without native Vercel Git builds (#1460). + # Only on push to dev/v1 — labeled PR previews keep ephemeral URLs and must not steal these. + - name: Alias branch preview domain + if: >- + github.event_name == 'push' && + (github.ref_name == 'dev' || github.ref_name == 'v1') && + steps.affected.outputs.is_affected == 'true' + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + DEPLOYMENT_URL: ${{ steps.deploy.outputs.DEPLOYMENT_URL }} + BRANCH: ${{ github.ref_name }} + run: | + case "$BRANCH" in + dev) DOMAIN="arkenv-dev.vercel.app" ;; + v1) DOMAIN="arkenv-v1.vercel.app" ;; + *) + echo "No branch domain mapping for $BRANCH; skipping alias." + exit 0 + ;; + esac + if [ -z "$DEPLOYMENT_URL" ]; then + echo "No deployment URL; skipping alias." + exit 0 + fi + echo "Aliasing $DEPLOYMENT_URL -> $DOMAIN" + node scripts/vercel-wrapper.cjs alias set "$DEPLOYMENT_URL" "$DOMAIN" --token="$VERCEL_TOKEN" - name: Generate GitHub App Token id: generate-token uses: actions/create-github-app-token@v3 From f7ab30f98ce3ae7d40d6fbf307747293772b912f Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Sat, 25 Jul 2026 18:02:56 +0500 Subject: [PATCH 08/15] ci: forward-port Actions Vercel branch-domain wiring to v1 (#1460) From 928acfdb376471de7575368d859a61b1c60d5c55 Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Sat, 25 Jul 2026 18:02:56 +0500 Subject: [PATCH 09/15] ci: forward-port Actions Vercel branch-domain wiring to v1 (#1460) --- .github/workflows/deploy-www.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-www.yml b/.github/workflows/deploy-www.yml index e1183419a..22b25f3ce 100644 --- a/.github/workflows/deploy-www.yml +++ b/.github/workflows/deploy-www.yml @@ -42,4 +42,28 @@ jobs: - name: Build Project Artifacts run: node scripts/vercel-wrapper.cjs build --prod --token=${{ secrets.VERCEL_TOKEN }} - name: Deploy Project Artifacts to Vercel - run: node scripts/vercel-wrapper.cjs deploy --prebuilt --prod --archive=tgz --token=${{ secrets.VERCEL_TOKEN }} + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + GIT_ORG: ${{ github.repository_owner }} + GIT_REPO: ${{ github.event.repository.name }} + GIT_REF: ${{ github.ref_name }} + GIT_SHA: ${{ github.sha }} + GIT_ACTOR: ${{ github.actor }} + GIT_COMMIT_MESSAGE: ${{ github.event.head_commit.message || github.sha }} + GIT_COMMIT_AUTHOR_NAME: ${{ github.event.head_commit.author.name || github.actor }} + run: | + set -o pipefail + SHORT_MSG="$(printf '%s\n' "$GIT_COMMIT_MESSAGE" | head -n1)" + # --prod already assigns production domains (arkenv.js.org / arkenv.vercel.app). + # Git meta keeps the deployment linked to main in the Vercel dashboard. + node scripts/vercel-wrapper.cjs deploy --prebuilt --prod --archive=tgz --token="$VERCEL_TOKEN" \ + -m githubDeployment=1 \ + -m githubOrg="$GIT_ORG" \ + -m githubRepo="$GIT_REPO" \ + -m githubCommitOrg="$GIT_ORG" \ + -m githubCommitRepo="$GIT_REPO" \ + -m githubCommitRef="$GIT_REF" \ + -m githubCommitSha="$GIT_SHA" \ + -m githubCommitMessage="$SHORT_MSG" \ + -m githubCommitAuthorName="$GIT_COMMIT_AUTHOR_NAME" \ + -m githubCommitAuthorLogin="$GIT_ACTOR" From 4541e9894e32e6c7db270f4bbe172b7b52528caf Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Sat, 25 Jul 2026 18:03:23 +0500 Subject: [PATCH 10/15] ci: forward-port Actions Vercel branch-domain wiring to v1 (#1460) From 3785c6bfa27888a669900045b5b326e7baa1558b Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Sat, 25 Jul 2026 18:15:12 +0500 Subject: [PATCH 11/15] fix: Forward-port shared missing-schema errors from #1490 (#1495) ## Summary - Forward-port of [#1490](https://github.com/yamcodes/arkenv/pull/1490) (`dev`) onto `v1` - Centralize missing-schema text in `formatMissingSchemaError` (`@arkenv/build`) - Wire **Bun, Vite, Next, and Nuxt** through the shared helper (Vite included on `v1` because it has a discovery miss path; `dev` Vite does not) - Mock registry fetch in CLI init tests (same flake fix as #1490) ## Test plan - [x] `pnpm exec vitest run packages/build packages/bun-plugin/src/env-module.test.ts packages/vite-plugin/src/env-module.test.ts packages/nuxt/src/module.test.ts packages/arkenv/src/cli/commands/init.test.ts --run` - [x] `pnpm run typecheck` - [x] `pnpm run fix` - [x] `pnpm run test -- --run` (1066 passed) Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor --- .../align-v1-shared-missing-schema-errors.md | 17 +++++++ packages/arkenv/src/cli/commands/init.test.ts | 15 +++++- packages/build/src/core.test.ts | 29 ++++++++++- packages/build/src/core.ts | 22 ++++++-- packages/build/src/missing-schema-error.ts | 51 +++++++++++++++++++ packages/bun-plugin/src/env-module-path.ts | 12 ++++- packages/bun-plugin/src/env-module.test.ts | 5 +- packages/nextjs/src/config/setup.ts | 16 +++--- packages/nuxt/src/config.ts | 12 ++--- packages/nuxt/src/module.test.ts | 14 +++-- packages/nuxt/src/module.ts | 8 +-- packages/vite-plugin/src/env-module-path.ts | 12 ++++- packages/vite-plugin/src/env-module.test.ts | 5 +- 13 files changed, 185 insertions(+), 33 deletions(-) create mode 100644 .changeset/align-v1-shared-missing-schema-errors.md create mode 100644 packages/build/src/missing-schema-error.ts diff --git a/.changeset/align-v1-shared-missing-schema-errors.md b/.changeset/align-v1-shared-missing-schema-errors.md new file mode 100644 index 000000000..6e3e2409b --- /dev/null +++ b/.changeset/align-v1-shared-missing-schema-errors.md @@ -0,0 +1,17 @@ +--- +"@arkenv/build": patch +"@arkenv/bun-plugin": patch +"@arkenv/vite-plugin": patch +"@arkenv/nextjs": patch +"@arkenv/nuxt": patch +--- + +#### Make missing-schema errors short and actionable across hosts + +When a host cannot find an env schema, throw a consistent message that names the expected path / `schemaPath` and points to `npx arkenv@latest init`, without embedding a starter `env.ts` module. + +Example: + +```text +[ArkEnv] Could not find schema file at src/env.ts or env.ts. Please specify 'schemaPath' in ArkEnv options (or run `npx arkenv@latest init`). +``` diff --git a/packages/arkenv/src/cli/commands/init.test.ts b/packages/arkenv/src/cli/commands/init.test.ts index 2f49f511b..6869b3ab4 100644 --- a/packages/arkenv/src/cli/commands/init.test.ts +++ b/packages/arkenv/src/cli/commands/init.test.ts @@ -65,7 +65,20 @@ describe("InitUseCase", () => { checkGitStatus: vi.fn().mockResolvedValue({ status: "clean" }), } as unknown as ProjectScannerPort; - useCase = new InitUseCase(logger, workspace, prompt, scanner); + const registry = { + fetchRegistry: vi.fn().mockResolvedValue({ + examples: [ + { + id: "basic", + name: "Basic", + description: "A minimal ArkEnv setup in Node.js", + framework: "vanilla", + }, + ], + }), + }; + + useCase = new InitUseCase(logger, workspace, prompt, scanner, registry); }); it("should enter new project flow if no package.json and empty directory", async () => { diff --git a/packages/build/src/core.test.ts b/packages/build/src/core.test.ts index ca5cbd3e0..ed9d3a9c8 100644 --- a/packages/build/src/core.test.ts +++ b/packages/build/src/core.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { extractKeys, resolveLayout } from "./core"; +import { extractKeys, formatMissingSchemaError, resolveLayout } from "./core"; describe("@arkenv/build layout resolution", () => { it("treats flat as simple layout", () => { @@ -49,3 +49,30 @@ describe("@arkenv/build layout resolution", () => { expect(res.clientKeys).toEqual(["NUXT_PUBLIC_API_URL"]); }); }); + +describe("formatMissingSchemaError", () => { + it("formats a short host error with npx arkenv@latest init and no starter", () => { + const message = formatMissingSchemaError({ + optionsHint: "setupArkEnv options", + }); + + expect(message).toBe( + "[ArkEnv] Could not find schema file at src/env.ts or env.ts. Please specify 'schemaPath' in setupArkEnv options (or run `npx arkenv@latest init`).", + ); + expect(message).not.toMatch(/```/); + expect(message).not.toMatch(/Example/); + }); + + it("includes checked paths when provided", () => { + const message = formatMissingSchemaError({ + prefix: "ArkEnv Vite plugin:", + optionsHint: "plugin options", + checkedPaths: ["/proj/src/env.ts", "/proj/env.ts"], + }); + + expect(message).toContain("ArkEnv Vite plugin: Could not find schema file"); + expect(message).toContain("Checked paths:"); + expect(message).toContain(" - /proj/src/env.ts"); + expect(message).toContain("npx arkenv@latest init"); + }); +}); diff --git a/packages/build/src/core.ts b/packages/build/src/core.ts index 716285f63..ccd4a3407 100644 --- a/packages/build/src/core.ts +++ b/packages/build/src/core.ts @@ -8,6 +8,12 @@ import { } from "@repo/log"; import { watch as chokidarWatch, type FSWatcher } from "chokidar"; +export { + DEFAULT_SCHEMA_LOCATIONS, + type FormatMissingSchemaErrorOptions, + formatMissingSchemaError, +} from "./missing-schema-error"; + // Global watcher reference isolated to this bundle's scope let activeWatcher: FSWatcher | undefined; @@ -113,6 +119,16 @@ export function resolveLayout( return { layout: "simple", baseDir: schemaPath }; } +/** + * Return the default absolute schema file candidates for a project root. + * + * @param cwd The working directory to search from (defaults to process.cwd()) + * @returns Absolute paths for `src/env.ts` and `env.ts` + */ +export function getDefaultSchemaFileCandidates(cwd = process.cwd()): string[] { + return [path.join(cwd, "src", "env.ts"), path.join(cwd, "env.ts")]; +} + /** * Find the path to the schema file or directory in the project. * @@ -120,11 +136,7 @@ export function resolveLayout( * @returns The absolute path to the schema file/directory, or null if not found */ export function findSchemaPath(cwd = process.cwd()): string | null { - const possiblePaths = [ - path.join(cwd, "src", "env.ts"), - path.join(cwd, "env.ts"), - ]; - for (const p of possiblePaths) { + for (const p of getDefaultSchemaFileCandidates(cwd)) { if (fs.existsSync(p)) return p; } diff --git a/packages/build/src/missing-schema-error.ts b/packages/build/src/missing-schema-error.ts new file mode 100644 index 000000000..ea73353d8 --- /dev/null +++ b/packages/build/src/missing-schema-error.ts @@ -0,0 +1,51 @@ +/** + * Default relative schema locations hosts search when `schemaPath` is omitted. + */ +export const DEFAULT_SCHEMA_LOCATIONS = "src/env.ts or env.ts"; + +export type FormatMissingSchemaErrorOptions = { + /** + * Explicit `schemaPath` that was missing. + * Omit to refer to {@link DEFAULT_SCHEMA_LOCATIONS}. + */ + schemaPath?: string; + /** + * Where to set `schemaPath` (e.g. `"setupArkEnv options"`, `"ArkEnv options"`, + * `"plugin options"`). + */ + optionsHint: string; + /** + * Brand prefix for the error. + * @default "[ArkEnv]" + */ + prefix?: string; + /** + * Absolute paths that were checked during discovery. + * Appended as a `Checked paths` section when present. + */ + checkedPaths?: string[]; +}; + +/** + * Format a missing-schema error shared by host integrations. + * + * Owns the no-starter policy: hosts must use this helper instead of inlining + * example `env.ts` modules in thrown errors. + * + * @param options Host-specific labels and optional discovery paths + * @returns A short, actionable missing-schema error message + */ +export function formatMissingSchemaError( + options: FormatMissingSchemaErrorOptions, +): string { + const prefix = options.prefix ?? "[ArkEnv]"; + const location = options.schemaPath || DEFAULT_SCHEMA_LOCATIONS; + let message = `${prefix} Could not find schema file at ${location}. Please specify 'schemaPath' in ${options.optionsHint} (or run \`npx arkenv@latest init\`).`; + + if (options.checkedPaths?.length) { + const pathsList = options.checkedPaths.map((p) => ` - ${p}`).join("\n"); + message += `\n\nChecked paths:\n${pathsList}`; + } + + return message; +} diff --git a/packages/bun-plugin/src/env-module-path.ts b/packages/bun-plugin/src/env-module-path.ts index 18be2fff2..f8c50e5ab 100644 --- a/packages/bun-plugin/src/env-module-path.ts +++ b/packages/bun-plugin/src/env-module-path.ts @@ -1,6 +1,10 @@ import fs from "node:fs"; import path from "node:path"; -import { findSchemaPath } from "@arkenv/build"; +import { + findSchemaPath, + formatMissingSchemaError, + getDefaultSchemaFileCandidates, +} from "@arkenv/build"; /** * Strip query suffixes from a module path. @@ -61,7 +65,11 @@ export function resolveEnvModulePath( const discovered = findSchemaPath(root); if (!discovered) { throw new Error( - `ArkEnv Bun plugin: could not find an env module. Expected "src/env.ts" or "env.ts" under "${root}", or pass schemaPath (or run \`arkenv init\`).`, + formatMissingSchemaError({ + prefix: "ArkEnv Bun plugin:", + optionsHint: "plugin options", + checkedPaths: getDefaultSchemaFileCandidates(root), + }), ); } return discovered; diff --git a/packages/bun-plugin/src/env-module.test.ts b/packages/bun-plugin/src/env-module.test.ts index 01f306382..ef54034e5 100644 --- a/packages/bun-plugin/src/env-module.test.ts +++ b/packages/bun-plugin/src/env-module.test.ts @@ -319,8 +319,9 @@ describe("missing-schema errors", () => { message = error instanceof Error ? error.message : String(error); } - expect(message).toMatch(/could not find an env module/); - expect(message).toMatch(/arkenv init/); + expect(message).toMatch(/Could not find schema file/); + expect(message).toMatch(/npx arkenv@latest init/); + expect(message).toMatch(/Checked paths:/); expect(message).not.toMatch(/Example `src\/env\.ts`/); expect(message).not.toMatch(/```/); expect(message).not.toMatch(/import \{ type \} from "arktype"/); diff --git a/packages/nextjs/src/config/setup.ts b/packages/nextjs/src/config/setup.ts index 2591665b7..a6efa3eb5 100644 --- a/packages/nextjs/src/config/setup.ts +++ b/packages/nextjs/src/config/setup.ts @@ -1,7 +1,12 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { findSchemaPath, resolveLayout, watchSchema } from "@arkenv/build"; +import { + findSchemaPath, + formatMissingSchemaError, + resolveLayout, + watchSchema, +} from "@arkenv/build"; import { formatBuildError, resolveBuildLog, @@ -56,11 +61,10 @@ export function setupArkEnv( if (!schemaPath || !schemaPathExists(schemaPath)) { throw new Error( - formatBuildError( - `Could not find schema file at ${ - options?.schemaPath || "src/env.ts or env.ts" - }. Please specify 'schemaPath' in setupArkEnv options (or run \`arkenv init\`).`, - ), + formatMissingSchemaError({ + schemaPath: options?.schemaPath, + optionsHint: "setupArkEnv options", + }), ); } diff --git a/packages/nuxt/src/config.ts b/packages/nuxt/src/config.ts index c352dc89a..177b4c6b7 100644 --- a/packages/nuxt/src/config.ts +++ b/packages/nuxt/src/config.ts @@ -5,11 +5,11 @@ import { extractClientKeys, extractSharedKeys, findSchemaPath, + formatMissingSchemaError, resolveLayout, } from "@arkenv/build"; import { type BuildLogHelpers, - formatBuildError, type Logger, type LogLevel, resolveBuildLog, @@ -26,6 +26,7 @@ export { extractArkenvBlock, extractServerKeys, findSchemaPath, + formatMissingSchemaError, resolveLayout, } from "@arkenv/build"; export { extractClientKeys, extractSharedKeys, validateSchema }; @@ -130,11 +131,10 @@ export function setupArkEnv( if (!schemaPath || !exists) { throw new Error( - formatBuildError( - `Could not find schema file at ${ - options?.schemaPath || "src/env.ts or env.ts" - }. Please specify 'schemaPath' in ArkEnv options (or run \`arkenv init\`).`, - ), + formatMissingSchemaError({ + schemaPath: options?.schemaPath, + optionsHint: "ArkEnv options", + }), ); } diff --git a/packages/nuxt/src/module.test.ts b/packages/nuxt/src/module.test.ts index 6f9c24624..935bf3b22 100644 --- a/packages/nuxt/src/module.test.ts +++ b/packages/nuxt/src/module.test.ts @@ -545,11 +545,19 @@ describe("Nuxt module integration", () => { }; try { - expect(() => - (module as any).setup({ validate: false }, mockNuxt), - ).toThrow( + let message = ""; + try { + (module as any).setup({ validate: false }, mockNuxt); + } catch (error) { + message = error instanceof Error ? error.message : String(error); + } + + expect(message).toMatch( /\[ArkEnv\] Could not find schema file at src\/env\.ts or env\.ts/, ); + expect(message).toMatch(/npx arkenv@latest init/); + expect(message).not.toMatch(/Example `src\/env\.ts`/); + expect(message).not.toMatch(/```/); expect(mockNuxt.hook).not.toHaveBeenCalled(); expect(mockNuxt.options.runtimeConfig.DATABASE_URL).toBeUndefined(); } finally { diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 10f3ed8d8..f66b8e4f8 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -12,6 +12,7 @@ import { extractServerKeys, extractSharedKeys, findSchemaPath, + formatMissingSchemaError, normalizeLayout, resolveLayout, validateSchema, @@ -80,9 +81,10 @@ const module: NuxtModule = defineNuxtModule({ if (!schemaPath || !fs.existsSync(schemaPath)) { throw new Error( - `[ArkEnv] Could not find schema file at ${ - options.schemaPath || "src/env.ts or env.ts" - }. Please specify 'schemaPath' in ArkEnv options (or run \`arkenv init\`).`, + formatMissingSchemaError({ + schemaPath: options.schemaPath, + optionsHint: "ArkEnv options", + }), ); } diff --git a/packages/vite-plugin/src/env-module-path.ts b/packages/vite-plugin/src/env-module-path.ts index f474bc34a..c4ffbd0db 100644 --- a/packages/vite-plugin/src/env-module-path.ts +++ b/packages/vite-plugin/src/env-module-path.ts @@ -1,6 +1,10 @@ import fs from "node:fs"; import path from "node:path"; -import { findSchemaPath } from "@arkenv/build"; +import { + findSchemaPath, + formatMissingSchemaError, + getDefaultSchemaFileCandidates, +} from "@arkenv/build"; /** * Strip Vite virtual-module and query suffixes from a module id. @@ -64,7 +68,11 @@ export function resolveEnvModulePath( const discovered = findSchemaPath(root); if (!discovered) { throw new Error( - `ArkEnv Vite plugin: could not find an env module. Expected "src/env.ts" or "env.ts" under "${root}", or pass schemaPath (or run \`arkenv init\`).`, + formatMissingSchemaError({ + prefix: "ArkEnv Vite plugin:", + optionsHint: "plugin options", + checkedPaths: getDefaultSchemaFileCandidates(root), + }), ); } return discovered; diff --git a/packages/vite-plugin/src/env-module.test.ts b/packages/vite-plugin/src/env-module.test.ts index 5c3855e66..d27808b4d 100644 --- a/packages/vite-plugin/src/env-module.test.ts +++ b/packages/vite-plugin/src/env-module.test.ts @@ -266,8 +266,9 @@ describe("missing-schema errors", () => { message = error instanceof Error ? error.message : String(error); } - expect(message).toMatch(/could not find an env module/); - expect(message).toMatch(/arkenv init/); + expect(message).toMatch(/Could not find schema file/); + expect(message).toMatch(/npx arkenv@latest init/); + expect(message).toMatch(/Checked paths:/); expect(message).not.toMatch(/Example `src\/env\.ts`/); expect(message).not.toMatch(/```/); expect(message).not.toMatch(/import \{ type \} from "arktype"/); From 0f41e3f408686a0ca704db544ba7f55d91e7654a Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Sat, 25 Jul 2026 18:16:48 +0500 Subject: [PATCH 12/15] chore: remove #1460 deploy probe file --- apps/www/public/deploy-probe.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 apps/www/public/deploy-probe.txt diff --git a/apps/www/public/deploy-probe.txt b/apps/www/public/deploy-probe.txt deleted file mode 100644 index 6e6e3b8b4..000000000 --- a/apps/www/public/deploy-probe.txt +++ /dev/null @@ -1 +0,0 @@ -1460-verify-20260725124821 From df7a10bd73aade1cb68775dd86b5a7aafec534a9 Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Sat, 25 Jul 2026 22:47:09 +0500 Subject: [PATCH 13/15] ci: Forward-port manual SHA deploy escape hatch from #1502 (#1504) --- .github/workflows/deploy-www-manual.yml | 111 ++++++++++++++++++++++++ docs/CONTRIBUTING.md | 6 +- 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy-www-manual.yml diff --git a/.github/workflows/deploy-www-manual.yml b/.github/workflows/deploy-www-manual.yml new file mode 100644 index 000000000..f00b5bcc8 --- /dev/null +++ b/.github/workflows/deploy-www-manual.yml @@ -0,0 +1,111 @@ +name: Deploy www (manual SHA) + +# Maintainer escape hatch: deploy any commit to a stable www URL without +# pushing that commit to the target branch. Requires write access to run. +on: + workflow_dispatch: + inputs: + sha: + description: Full commit SHA to build and deploy + required: true + type: string + target: + description: Stable URL to update + required: true + type: choice + options: + - arkenv-dev.vercel.app + - arkenv-v1.vercel.app + - arkenv.js.org + +concurrency: + group: deploy-www-manual-${{ inputs.target }} + cancel-in-progress: true + +jobs: + Deploy-Manual: + runs-on: ubuntu-latest + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + TARGET: ${{ inputs.target }} + DEPLOY_SHA: ${{ inputs.sha }} + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.sha }} + fetch-depth: 0 + - name: Resolve git metadata for the deployed commit + id: meta + run: | + set -euo pipefail + case "$TARGET" in + arkenv-dev.vercel.app) echo "git_ref=dev" >> "$GITHUB_OUTPUT" ;; + arkenv-v1.vercel.app) echo "git_ref=v1" >> "$GITHUB_OUTPUT" ;; + arkenv.js.org) echo "git_ref=main" >> "$GITHUB_OUTPUT" ;; + *) + echo "Unknown target: $TARGET" >&2 + exit 1 + ;; + esac + echo "short_msg=$(git log -1 --format=%s "$DEPLOY_SHA" | head -n1)" >> "$GITHUB_OUTPUT" + echo "author_name=$(git log -1 --format=%an "$DEPLOY_SHA")" >> "$GITHUB_OUTPUT" + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v6 + with: + node-version: lts/* + cache: pnpm + - name: Install dependencies + run: pnpm install + - name: Install Vercel CLI + # Pin the CLI to avoid upstream breakage from newly published majors. + run: npm install --global vercel@54.7.1 + - name: Pull Vercel Environment Information + run: | + if [ "$TARGET" = "arkenv.js.org" ]; then + node scripts/vercel-wrapper.cjs pull --yes --environment=production --token="$VERCEL_TOKEN" + else + node scripts/vercel-wrapper.cjs pull --yes --environment=preview --token="$VERCEL_TOKEN" + fi + - name: Build Project Artifacts + run: | + if [ "$TARGET" = "arkenv.js.org" ]; then + node scripts/vercel-wrapper.cjs build --prod --token="$VERCEL_TOKEN" + else + node scripts/vercel-wrapper.cjs build --token="$VERCEL_TOKEN" + fi + - name: Deploy and attach to stable URL + env: + GIT_ORG: ${{ github.repository_owner }} + GIT_REPO: ${{ github.event.repository.name }} + GIT_REF: ${{ steps.meta.outputs.git_ref }} + GIT_SHA: ${{ inputs.sha }} + GIT_COMMIT_MESSAGE: ${{ steps.meta.outputs.short_msg }} + GIT_COMMIT_AUTHOR_NAME: ${{ steps.meta.outputs.author_name }} + GIT_COMMIT_AUTHOR_LOGIN: ${{ github.actor }} + run: | + set -o pipefail + META_ARGS=( + -m githubDeployment=1 + -m githubOrg="$GIT_ORG" + -m githubRepo="$GIT_REPO" + -m githubCommitOrg="$GIT_ORG" + -m githubCommitRepo="$GIT_REPO" + -m githubCommitRef="$GIT_REF" + -m githubCommitSha="$GIT_SHA" + -m githubCommitMessage="$GIT_COMMIT_MESSAGE" + -m githubCommitAuthorName="$GIT_COMMIT_AUTHOR_NAME" + -m githubCommitAuthorLogin="$GIT_COMMIT_AUTHOR_LOGIN" + ) + if [ "$TARGET" = "arkenv.js.org" ]; then + node scripts/vercel-wrapper.cjs deploy --prebuilt --prod --archive=tgz --token="$VERCEL_TOKEN" "${META_ARGS[@]}" + echo "Deployed $GIT_SHA to production (arkenv.js.org)." + else + DEPLOYMENT_URL=$(node scripts/vercel-wrapper.cjs deploy --prebuilt --archive=tgz --token="$VERCEL_TOKEN" "${META_ARGS[@]}" | tail -n 1) + echo "Deployment URL: $DEPLOYMENT_URL" + echo "Aliasing $DEPLOYMENT_URL -> $TARGET" + node scripts/vercel-wrapper.cjs alias set "$DEPLOYMENT_URL" "$TARGET" --token="$VERCEL_TOKEN" + fi diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 91802200e..34b0b77d0 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -148,7 +148,11 @@ When working on a massive marketing push, docs facelift, or breaking API changes ## Preview deployments -PR previews for the `www` app are opt-in. A maintainer (triage+) applies the `preview` label to trigger a Vercel preview deployment when the label is added, and again on subsequent `synchronize` / `ready_for_review` events while the label remains (a preview is only produced when the `www` app is actually affected). This works for same-repo and fork PRs; fork authors cannot self-serve the label. Pushes to `dev` or `v1` continue to deploy rolling branch previews automatically. +PR previews for the `www` app are opt-in. A maintainer (triage+) applies the `preview` label to trigger a Vercel preview deployment when the label is added, and again on subsequent `synchronize` / `ready_for_review` events while the label remains (a preview is only produced when the `www` app is actually affected). This works for same-repo and fork PRs; fork authors cannot self-serve the label. + +Pushes to `dev` or `v1` always deploy via GitHub Actions (Vercel CLI). Those deploys pass git metadata and alias the rolling branch domains (`https://arkenv-dev.vercel.app`, `https://arkenv-v1.vercel.app`) so the domains stay current without relying on native Vercel Git builds. Labeled PR previews keep ephemeral deployment URLs and do not take over those branch domains. + +To redeploy an older commit to a stable URL without moving the branch, maintainers can run **Actions → Deploy www (manual SHA)** and choose `arkenv-dev.vercel.app`, `arkenv-v1.vercel.app`, or production `arkenv.js.org`. ## Changesets From 9bfe1c4a6e278966ff2c0b2219d95e319888fb98 Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Sat, 25 Jul 2026 23:21:38 +0500 Subject: [PATCH 14/15] feat: (v1) Make env/internal/shared.ts optional in strict layout (#1505) --- .changeset/optional-strict-shared-v1.md | 18 +++ .../content/docs/nextjs/layouts/strict.mdx | 12 +- apps/www/content/docs/nuxt/configuration.mdx | 4 +- apps/www/content/docs/nuxt/layouts/strict.mdx | 16 +-- packages/build/src/core.test.ts | 123 +++++++++++++++++- packages/build/src/core.ts | 70 +++++++--- packages/bun-plugin/src/env-module-path.ts | 5 +- packages/bun-plugin/src/env-module.test.ts | 16 ++- packages/nextjs/src/config/types.ts | 2 +- packages/nuxt/src/boot-gate.ts | 32 +++-- packages/nuxt/src/config.ts | 6 +- packages/nuxt/src/empty-shared-schema.ts | 5 +- packages/nuxt/src/module.test.ts | 34 +++-- packages/nuxt/src/module.ts | 15 ++- packages/nuxt/src/strict-layout-hooks.ts | 8 +- packages/nuxt/src/strict-shared-schema.ts | 29 +---- packages/nuxt/src/validation.test.ts | 35 +++++ packages/vite-plugin/src/env-module-path.ts | 5 +- packages/vite-plugin/src/env-module.test.ts | 22 +++- 19 files changed, 346 insertions(+), 111 deletions(-) create mode 100644 .changeset/optional-strict-shared-v1.md diff --git a/.changeset/optional-strict-shared-v1.md b/.changeset/optional-strict-shared-v1.md new file mode 100644 index 000000000..4eafdded5 --- /dev/null +++ b/.changeset/optional-strict-shared-v1.md @@ -0,0 +1,18 @@ +--- +"@arkenv/build": patch +"@arkenv/nextjs": patch +"@arkenv/nuxt": patch +--- + +#### Make `env/internal/shared.ts` optional in strict layout + +Strict layout now works with just `client.ts` and `server.ts`. Omit `internal/shared.ts` when you have nothing to share — shared keys are treated as empty. + +```ts +// env/client.ts + env/server.ts alone is enough +export default withArkEnv(nextConfig, { + layout: "strict", +}); +``` + +The CLI still scaffolds `shared.ts` by default for convenience. diff --git a/apps/www/content/docs/nextjs/layouts/strict.mdx b/apps/www/content/docs/nextjs/layouts/strict.mdx index 12d20d430..a4878f0a9 100644 --- a/apps/www/content/docs/nextjs/layouts/strict.mdx +++ b/apps/www/content/docs/nextjs/layouts/strict.mdx @@ -4,13 +4,13 @@ icon: ShieldCheck description: Get the best security in Next.js using strictly separated client and server schemas. --- -In this setup, you define your client, server, and shared schemas in separate files. +In this setup, you define your client and server schemas in separate files. An optional `internal/shared.ts` holds variables shared by both. ```files src └── env ├── internal - │ └── shared.ts + │ └── shared.ts # optional ├── client.ts └── server.ts ``` @@ -38,7 +38,7 @@ npx arkenv@latest init --strict ## Your schema -When bootstrapping with the CLI, it generates three separate schema files under `src/env/`: +When bootstrapping with the CLI, it generates separate schema files under `src/env/` (including an optional `internal/shared.ts` with `NODE_ENV` for convenience): :::note By default, ArkEnv for Next.js uses an automatic code generation wrapper (`withArkEnv`) in your Next.js config to compile your `runtimeEnv` block. This is why the client-side schema imports from `./generated/env.gen` rather than `@arkenv/nextjs/client`. To opt out, simply import from `@arkenv/nextjs/client` directly and provide your own `runtimeEnv` mapping. @@ -127,9 +127,9 @@ export const env = arkenv( If you're using Standard Schema without ArkType, install `@arkenv/standard` instead of `@arkenv/core` — see [Zod, Valibot, and other Standard Schema validators](/docs/nextjs/using-other-validators). ::: - ### Define shared variables [step] [!toc] + ### Define shared variables (optional) [step] [!toc] - Create `env/internal/shared.ts` to define schema variables that are shared across both the client and the server. We treat this as a runtime type, so it is defined using PascalCase: + Optionally create `env/internal/shared.ts` to define schema variables that are shared across both the client and the server. Shared is not required for strict layout—omit the file when you have nothing to share. When present, we treat it as a runtime type, so it is defined using PascalCase: ```ts title="src/env/internal/shared.ts" twoslash import { type } from "@arkenv/core"; @@ -145,7 +145,7 @@ export const env = arkenv( ### Define client variables [step] [!toc] - Create `env/client.ts` importing `@arkenv/core` from the auto-generated `./generated/env.gen.ts` helper file. All local keys **must** be prefixed with `NEXT_PUBLIC_` and you must extend `SharedSchema`: + Create `env/client.ts` importing `@arkenv/core` from the auto-generated `./generated/env.gen.ts` helper file. All local keys **must** be prefixed with `NEXT_PUBLIC_`. When you define `SharedSchema`, extend it here: ```ts title="src/env/client.ts" twoslash // @filename: /env/internal/shared.ts diff --git a/apps/www/content/docs/nuxt/configuration.mdx b/apps/www/content/docs/nuxt/configuration.mdx index f26ae8dcc..f0cdc84e0 100644 --- a/apps/www/content/docs/nuxt/configuration.mdx +++ b/apps/www/content/docs/nuxt/configuration.mdx @@ -81,10 +81,10 @@ Thrown validation errors are separate from logged diagnostics — the logger onl In strict layout, the module automatically composes: -1. `env/internal/shared.ts` → client entry (as `SharedSchema`) +1. `env/internal/shared.ts` → client entry (as `SharedSchema`), when the file exists 2. Client env → server entry -The shared file must live at `internal/shared.ts` under your schema directory and export `SharedSchema`. That relative path is not configurable. To change composition — for example to extend additional schemas, or to skip the default merge — pass an explicit `extends` option in your client or server entry. See [Extending your schema](/docs/nuxt/layouts/strict#extending-your-schema). +Shared is optional: omit `internal/shared.ts` when you have nothing to share (empty merge). When present, the file must live at `internal/shared.ts` under your schema directory and export `SharedSchema`. That relative path is not configurable. To change composition — for example to extend additional schemas, or to skip the default merge — pass an explicit `extends` option in your client or server entry. See [Extending your schema](/docs/nuxt/layouts/strict#extending-your-schema). `schemaPath` only relocates the schema directory as a whole (for example `src/env` instead of `env`). It does not rename `internal/shared.ts` inside that directory. diff --git a/apps/www/content/docs/nuxt/layouts/strict.mdx b/apps/www/content/docs/nuxt/layouts/strict.mdx index dda56a89f..233c2f9c0 100644 --- a/apps/www/content/docs/nuxt/layouts/strict.mdx +++ b/apps/www/content/docs/nuxt/layouts/strict.mdx @@ -4,12 +4,12 @@ icon: ShieldCheck description: Get the best security in Nuxt using strictly separated client and server schemas. --- -In this setup, you define your client, server, and shared schemas in separate files. +In this setup, you define your client and server schemas in separate files. An optional `internal/shared.ts` holds variables shared by both. ```files env ├── internal -│ └── shared.ts +│ └── shared.ts # optional ├── client.ts └── server.ts ``` @@ -37,7 +37,7 @@ npx arkenv@latest init --strict ## Your schema -When bootstrapping with the CLI, it generates three separate schema files under `env/`: +When bootstrapping with the CLI, it generates separate schema files under `env/` (including an optional `internal/shared.ts` with `NODE_ENV` for convenience): ```ts twoslash title="env/internal/shared.ts" import { type } from "@arkenv/core"; @@ -85,10 +85,10 @@ export const env = arkenv({ With `@arkenv/nuxt/module` registered: -- The **client** entry includes `SharedSchema` from `env/internal/shared.ts` -- The **server** entry includes the composed client env (and therefore shared variables) +- The **client** entry includes `SharedSchema` from `env/internal/shared.ts` when that file exists (otherwise shared is empty) +- The **server** entry includes the composed client env (and therefore any shared variables) -`env/internal/shared.ts` is always part of the strict layout, even when you only define client and server variables. If nothing is shared, export an empty schema (`type({})`) — do not omit the file or the `SharedSchema` export. +Shared is not required for strict layout—omit `env/internal/shared.ts` when you have nothing to share. When the file is present, it must export `SharedSchema`. The shared schema path is fixed relative to your schema directory: `internal/shared.ts`. There is no module option to relocate it (for example to `env/hidden/shared.ts`). To compose different schemas — or to opt out of the default merge — pass an explicit `extends` list: @@ -157,9 +157,9 @@ See [Configuration & API](/docs/nuxt/configuration) for module options such as ` If you're using Standard Schema without ArkType, install `@arkenv/standard` instead of `@arkenv/core` — see [Zod, Valibot, and other Standard Schema validators](/docs/nuxt/using-other-validators). ::: - ### Define shared variables [step] [!toc] + ### Define shared variables (optional) [step] [!toc] - Create `env/internal/shared.ts` to define schema variables that are shared across both the client and the server. We treat this as a runtime type, so it is defined using PascalCase: + Optionally create `env/internal/shared.ts` to define schema variables that are shared across both the client and the server. Shared is not required for strict layout—omit the file when you have nothing to share. When present, we treat it as a runtime type, so it is defined using PascalCase: ```ts twoslash title="env/internal/shared.ts" import { type } from "@arkenv/core"; diff --git a/packages/build/src/core.test.ts b/packages/build/src/core.test.ts index ed9d3a9c8..efeff65b9 100644 --- a/packages/build/src/core.test.ts +++ b/packages/build/src/core.test.ts @@ -1,7 +1,15 @@ import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { extractKeys, formatMissingSchemaError, resolveLayout } from "./core"; +import { + assertFlatSchemaFile, + extractKeys, + findSchemaPath, + formatMissingSchemaError, + isStrictLayoutDir, + resolveLayout, +} from "./core"; describe("@arkenv/build layout resolution", () => { it("treats flat as simple layout", () => { @@ -18,6 +26,119 @@ describe("@arkenv/build layout resolution", () => { } }); + describe("strict layout detection", () => { + const makeTempDir = () => + fs.mkdtempSync(path.join(os.tmpdir(), "arkenv-build-layout-")); + + it("treats client.ts + server.ts as strict without internal/shared.ts", () => { + const tempDir = makeTempDir(); + try { + fs.writeFileSync( + path.join(tempDir, "client.ts"), + "export const env = {}", + ); + fs.writeFileSync( + path.join(tempDir, "server.ts"), + "export const env = {}", + ); + + expect(isStrictLayoutDir(tempDir)).toBe(true); + expect(resolveLayout(tempDir)).toEqual({ + layout: "strict", + baseDir: tempDir, + }); + expect(resolveLayout(path.join(tempDir, "client.ts"))).toEqual({ + layout: "strict", + baseDir: tempDir, + }); + expect( + resolveLayout(path.join(tempDir, "client.ts"), "strict"), + ).toEqual({ + layout: "strict", + baseDir: tempDir, + }); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("findSchemaPath discovers env/ without requiring shared.ts", () => { + const tempDir = makeTempDir(); + try { + const envDir = path.join(tempDir, "env"); + fs.mkdirSync(envDir, { recursive: true }); + fs.writeFileSync( + path.join(envDir, "client.ts"), + "export const env = {}", + ); + fs.writeFileSync( + path.join(envDir, "server.ts"), + "export const env = {}", + ); + + expect(findSchemaPath(tempDir)).toBe(envDir); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("findSchemaPath discovers src/env/ without requiring shared.ts", () => { + const tempDir = makeTempDir(); + try { + const envDir = path.join(tempDir, "src", "env"); + fs.mkdirSync(envDir, { recursive: true }); + fs.writeFileSync( + path.join(envDir, "client.ts"), + "export const env = {}", + ); + fs.writeFileSync( + path.join(envDir, "server.ts"), + "export const env = {}", + ); + + expect(findSchemaPath(tempDir)).toBe(envDir); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("throws for explicit strict when client.ts is missing", () => { + const tempDir = makeTempDir(); + try { + fs.writeFileSync( + path.join(tempDir, "server.ts"), + "export const env = {}", + ); + expect(() => + resolveLayout(path.join(tempDir, "missing.ts"), "strict"), + ).toThrow( + `[ArkEnv] Strict layout requires "${path.join(tempDir, "client.ts")}" to exist`, + ); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("assertFlatSchemaFile rejects strict layout directories", () => { + const tempDir = makeTempDir(); + try { + fs.writeFileSync( + path.join(tempDir, "client.ts"), + "export const env = {}", + ); + fs.writeFileSync( + path.join(tempDir, "server.ts"), + "export const env = {}", + ); + expect(() => + assertFlatSchemaFile(tempDir, "ArkEnv Vite plugin:"), + ).toThrow(/only supports a flat env module file/); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + }); + it("extracts keys from flat layout schema with NUXT_PUBLIC_ prefix", () => { const content = ` export const env = arkenv({ diff --git a/packages/build/src/core.ts b/packages/build/src/core.ts index ccd4a3407..82811f66a 100644 --- a/packages/build/src/core.ts +++ b/packages/build/src/core.ts @@ -27,23 +27,35 @@ export type ResolvedLayout = { baseDir: string; }; +/** + * Return whether a directory looks like a strict split layout. + * + * Strict auto-detection requires `client.ts` and `server.ts`. + * `internal/shared.ts` is optional and treated as empty when absent. + * + * @param dir Absolute path to a candidate env directory + * @returns `true` when both `client.ts` and `server.ts` exist + */ +export function isStrictLayoutDir(dir: string): boolean { + return ( + fs.existsSync(path.join(dir, "client.ts")) && + fs.existsSync(path.join(dir, "server.ts")) + ); +} + /** * Resolve the layout mode and base directory for a given schema file path. * * @param schemaPath The absolute path to the schema file or directory * @param layoutOption An optional explicit layout configuration ("flat", "simple", or "strict") * @returns An object containing the resolved layout mode and the base directory path - * @throws An error if explicit "strict" layout is requested but required split files are missing + * @throws An error if explicit "strict" layout is requested but `client.ts` is missing */ export function resolveLayout( schemaPath: string, layoutOption?: LayoutInput, ): ResolvedLayout { const layout = layoutOption === "flat" ? "simple" : layoutOption; - const checkStrict = (dir: string) => - fs.existsSync(path.join(dir, "internal", "shared.ts")) && - fs.existsSync(path.join(dir, "client.ts")) && - fs.existsSync(path.join(dir, "server.ts")); const resolveBaseDir = (p: string): string => { const ext = path.extname(p); @@ -60,7 +72,7 @@ export function resolveLayout( if (!layout) { const resolved = resolveBaseDir(schemaPath); if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) { - if (checkStrict(resolved)) { + if (isStrictLayoutDir(resolved)) { return { layout: "strict", baseDir: resolved }; } return { layout: "simple", baseDir: resolved }; @@ -72,16 +84,16 @@ export function resolveLayout( if ( fs.existsSync(baseWithoutExt) && fs.statSync(baseWithoutExt).isDirectory() && - checkStrict(baseWithoutExt) + isStrictLayoutDir(baseWithoutExt) ) { return { layout: "strict", baseDir: baseWithoutExt }; } - if (checkStrict(parent)) { + if (isStrictLayoutDir(parent)) { return { layout: "strict", baseDir: parent }; } if ( path.basename(parent) === "internal" && - checkStrict(path.dirname(parent)) + isStrictLayoutDir(path.dirname(parent)) ) { return { layout: "strict", baseDir: path.dirname(parent) }; } @@ -103,12 +115,11 @@ export function resolveLayout( } const clientPath = path.join(baseDir, "client.ts"); - const sharedPath = path.join(baseDir, "internal", "shared.ts"); - if (!fs.existsSync(clientPath) || !fs.existsSync(sharedPath)) { + if (!fs.existsSync(clientPath)) { throw new Error( formatBuildError( - `Strict layout requires "${clientPath}" and "${sharedPath}" to exist. ` + - `Ensure both files are present or remove the 'layout: "strict"' option to let ArkEnv auto-detect.`, + `Strict layout requires "${clientPath}" to exist. ` + + `Ensure it is present or remove the 'layout: "strict"' option to let ArkEnv auto-detect.`, ), ); } @@ -142,18 +153,39 @@ export function findSchemaPath(cwd = process.cwd()): string | null { const possibleDirs = [path.join(cwd, "src", "env"), path.join(cwd, "env")]; for (const d of possibleDirs) { - if ( - fs.existsSync(d) && - fs.existsSync(path.join(d, "internal", "shared.ts")) && - fs.existsSync(path.join(d, "client.ts")) && - fs.existsSync(path.join(d, "server.ts")) - ) { + if (fs.existsSync(d) && isStrictLayoutDir(d)) { return d; } } return null; } +/** + * Ensure a discovered schema path is a flat env module file. + * + * Vite/Bun plugins only support a single `env.ts` module. Strict layout + * directories discovered by {@link findSchemaPath} are rejected with a clear + * host-specific diagnostic. + * + * @param schemaPath Absolute path returned by discovery or plugin options + * @param prefix Brand prefix for the error (e.g. `"ArkEnv Vite plugin:"`) + * @returns The same `schemaPath` when it is an existing file + * @throws When `schemaPath` is a directory (typically a strict layout) + */ +export function assertFlatSchemaFile( + schemaPath: string, + prefix: string, +): string { + if (fs.existsSync(schemaPath) && fs.statSync(schemaPath).isDirectory()) { + throw new Error( + `${prefix} discovered a schema directory at "${schemaPath}". ` + + "This integration only supports a flat env module file (env.ts). " + + "Point schemaPath at that file, or use @arkenv/nextjs / @arkenv/nuxt for strict layout.", + ); + } + return schemaPath; +} + /** * Extract the schema and options arguments from an `arkenv` or `createEnv` call. */ diff --git a/packages/bun-plugin/src/env-module-path.ts b/packages/bun-plugin/src/env-module-path.ts index f8c50e5ab..8f646dd02 100644 --- a/packages/bun-plugin/src/env-module-path.ts +++ b/packages/bun-plugin/src/env-module-path.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { + assertFlatSchemaFile, findSchemaPath, formatMissingSchemaError, getDefaultSchemaFileCandidates, @@ -59,7 +60,7 @@ export function resolveEnvModulePath( `ArkEnv Bun plugin: schemaPath "${schemaPath}" does not exist (resolved to "${resolved}").`, ); } - return resolved; + return assertFlatSchemaFile(resolved, "ArkEnv Bun plugin:"); } const discovered = findSchemaPath(root); @@ -72,7 +73,7 @@ export function resolveEnvModulePath( }), ); } - return discovered; + return assertFlatSchemaFile(discovered, "ArkEnv Bun plugin:"); } /** diff --git a/packages/bun-plugin/src/env-module.test.ts b/packages/bun-plugin/src/env-module.test.ts index ef54034e5..2651b8a94 100644 --- a/packages/bun-plugin/src/env-module.test.ts +++ b/packages/bun-plugin/src/env-module.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; @@ -327,4 +327,18 @@ describe("missing-schema errors", () => { expect(message).not.toMatch(/import \{ type \} from "arktype"/); expect(message).not.toMatch(/from "zod"/); }); + + it("rejects a discovered strict layout directory", async () => { + const { resolveEnvModulePath } = await import("./env-module-path.js"); + const root = mkdtempSync(join(tmpdir(), "arkenv-bun-strict-dir-")); + temps.push(root); + const envDir = join(root, "env"); + mkdirSync(envDir, { recursive: true }); + writeFileSync(join(envDir, "client.ts"), "export const env = {}"); + writeFileSync(join(envDir, "server.ts"), "export const env = {}"); + + expect(() => resolveEnvModulePath(root)).toThrow( + /only supports a flat env module file/, + ); + }); }); diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index ee89f5faf..1646022e7 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -46,7 +46,7 @@ export type ArkEnvConfigOptions = { * Specify the configuration layout. * * - `"flat"` (default): A single `env.ts` schema file. - * - `"strict"`: A 3-file split schema layout (`env/internal/shared.ts`, `env/client.ts`, `env/server.ts`). + * - `"strict"`: A split schema layout (`env/client.ts`, `env/server.ts`, and optionally `env/internal/shared.ts`). * * @default "flat" */ diff --git a/packages/nuxt/src/boot-gate.ts b/packages/nuxt/src/boot-gate.ts index 1ffc7f67a..47e0ddf31 100644 --- a/packages/nuxt/src/boot-gate.ts +++ b/packages/nuxt/src/boot-gate.ts @@ -278,24 +278,22 @@ export function loadSchemaViaCapture( "internal", "shared.ts", ); - if (!fs.existsSync(strictUserSharedPath)) { - throw new Error( - `[arkenv] Strict layout requires "internal/shared.ts" with a usable SharedSchema export under "${config.baseDir}".`, - ); - } - - const sharedMod = jiti(strictUserSharedPath) as { - SharedSchema?: SchemaShape; - default?: { SharedSchema?: SchemaShape }; - }; - const sharedSchema = - sharedMod.SharedSchema ?? sharedMod.default?.SharedSchema; - if (sharedSchema === undefined || sharedSchema === null) { - throw new Error( - `[arkenv] Strict layout requires a usable SharedSchema export from "${strictUserSharedPath}".`, - ); + // Absent shared.ts → empty merge (aliases already point at + // empty-shared-schema). A present file must export SharedSchema. + if (fs.existsSync(strictUserSharedPath)) { + const sharedMod = jiti(strictUserSharedPath) as { + SharedSchema?: SchemaShape; + default?: { SharedSchema?: SchemaShape }; + }; + const sharedSchema = + sharedMod.SharedSchema ?? sharedMod.default?.SharedSchema; + if (sharedSchema === undefined || sharedSchema === null) { + throw new Error( + `[arkenv] Strict layout requires a usable SharedSchema export from "${strictUserSharedPath}".`, + ); + } + g.__ARKENV_SHARED_SCHEMA__ = sharedSchema; } - g.__ARKENV_SHARED_SCHEMA__ = sharedSchema; const strictUserClientPath = path.join(config.baseDir, "client.ts"); if (fs.existsSync(strictUserClientPath)) { diff --git a/packages/nuxt/src/config.ts b/packages/nuxt/src/config.ts index 177b4c6b7..175de2c48 100644 --- a/packages/nuxt/src/config.ts +++ b/packages/nuxt/src/config.ts @@ -73,12 +73,12 @@ export type ArkEnvConfigOptions = { * Specify the configuration layout. * * When omitted, the layout is auto-detected from the schema structure: it is - * `"strict"` when the split files (`env/internal/shared.ts`, `env/client.ts`, - * `env/server.ts`) are present, and falls back to `"flat"` (a single + * `"strict"` when `env/client.ts` and `env/server.ts` are present (with + * optional `env/internal/shared.ts`), and falls back to `"flat"` (a single * `env.ts`) otherwise. * * - `"flat"`: A single `env.ts` schema file. - * - `"strict"`: A multi-file split schema layout. + * - `"strict"`: A split schema layout (`env/client.ts`, `env/server.ts`, and optionally `env/internal/shared.ts`). */ layout?: | "flat" diff --git a/packages/nuxt/src/empty-shared-schema.ts b/packages/nuxt/src/empty-shared-schema.ts index 051ea3ad3..66b98c67d 100644 --- a/packages/nuxt/src/empty-shared-schema.ts +++ b/packages/nuxt/src/empty-shared-schema.ts @@ -1,6 +1,7 @@ /** * Empty shared schema used when `#arkenv/shared-schema` is imported outside - * strict layout (so the client entry can keep a static import without Vite - * failing to resolve the virtual specifier). + * strict layout, or in strict layout when `env/internal/shared.ts` is omitted + * (so the client entry can keep a static import without Vite failing to + * resolve the virtual specifier). */ export const SharedSchema = {}; diff --git a/packages/nuxt/src/module.test.ts b/packages/nuxt/src/module.test.ts index 935bf3b22..b42fa5366 100644 --- a/packages/nuxt/src/module.test.ts +++ b/packages/nuxt/src/module.test.ts @@ -340,13 +340,14 @@ describe("Nuxt module integration", () => { } }); - it("throws when strict layout is missing internal/shared.ts", async () => { + it("aliases #arkenv/shared-schema to empty stub when internal/shared.ts is omitted", async () => { const tempDir = path.resolve(__dirname, "temp-strict-missing-shared"); const envDir = path.join(tempDir, "env"); fs.mkdirSync(envDir, { recursive: true }); try { - fs.writeFileSync(path.join(envDir, "client.ts"), "export const env = {}"); + const clientPath = path.join(envDir, "client.ts"); + fs.writeFileSync(clientPath, "export const env = {}"); fs.writeFileSync(path.join(envDir, "server.ts"), "export const env = {}"); const mockNuxt: any = { @@ -355,20 +356,29 @@ describe("Nuxt module integration", () => { rootDir: tempDir, srcDir: tempDir, runtimeConfig: { public: {} }, + alias: {}, }, hook: vi.fn(), }; - expect(() => - (module as any).setup( - { - schemaPath: "./env", - layout: "strict", - validate: false, - }, - mockNuxt, - ), - ).toThrow(/internal\/shared\.ts/); + await (module as any).setup( + { + schemaPath: "./env", + layout: "strict", + validate: false, + }, + mockNuxt, + ); + + expect(mockNuxt.options.alias["#arkenv/client-env"]).toBe(clientPath); + const sharedAlias = mockNuxt.options.alias[ + "#arkenv/shared-schema" + ] as string; + expect(sharedAlias).toBeDefined(); + expect(sharedAlias).toMatch(/empty-shared-schema/); + expect(fs.existsSync(path.join(envDir, "internal", "shared.ts"))).toBe( + false, + ); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index f66b8e4f8..979d38fc4 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -23,7 +23,6 @@ import { registerStrictLayoutHooks, registerViteExtendHook, } from "./strict-layout-hooks"; -import { missingSharedTsError } from "./strict-shared-schema"; /** * Configuration options for the ArkEnv Nuxt module. @@ -131,19 +130,21 @@ const module: NuxtModule = defineNuxtModule({ nuxt.options.srcDir ?? nuxt.options.rootDir, ); + const emptySharedSchema = resolver.resolve("./empty-shared-schema"); + let strictClientPath: string | undefined; let strictSharedPath: string | undefined; + let userSharedPath: string | undefined; if (resolvedLayout === "strict" && baseDir) { const clientPath = path.join(baseDir, "client.ts"); if (!fs.existsSync(clientPath)) { throw new Error(missingClientTsError(clientPath, baseDir)); } const sharedPath = path.join(baseDir, "internal", "shared.ts"); - if (!fs.existsSync(sharedPath)) { - throw new Error(missingSharedTsError(sharedPath, baseDir)); - } + userSharedPath = fs.existsSync(sharedPath) ? sharedPath : undefined; strictClientPath = clientPath; - strictSharedPath = sharedPath; + // Missing shared.ts is intentional empty; alias to the package stub. + strictSharedPath = userSharedPath ?? emptySharedSchema; } if (nuxt.options.dev) { @@ -190,7 +191,9 @@ const module: NuxtModule = defineNuxtModule({ const serverPath = path.join(baseDir, "server.ts"); const clientContent = fs.readFileSync(strictClientPath, "utf-8"); - const sharedContent = fs.readFileSync(strictSharedPath, "utf-8"); + const sharedContent = userSharedPath + ? fs.readFileSync(userSharedPath, "utf-8") + : ""; const serverContent = fs.existsSync(serverPath) ? fs.readFileSync(serverPath, "utf-8") : ""; diff --git a/packages/nuxt/src/strict-layout-hooks.ts b/packages/nuxt/src/strict-layout-hooks.ts index f960a3066..d69a91954 100644 --- a/packages/nuxt/src/strict-layout-hooks.ts +++ b/packages/nuxt/src/strict-layout-hooks.ts @@ -28,7 +28,8 @@ declare module "@nuxt/schema" { * * @param nuxt The Nuxt instance * @param strictClientPath Absolute path to the project's `env/client.ts` - * @param strictSharedPath Absolute path to the project's `env/internal/shared.ts` + * @param strictSharedPath Absolute path to the project's `env/internal/shared.ts`, + * or the package `empty-shared-schema` stub when that file is omitted */ export function registerStrictLayoutHooks( nuxt: Nuxt, @@ -150,7 +151,7 @@ export function registerViteExtendHook( id === CLIENT_ENV_SPECIFIER || id === `\0${CLIENT_ENV_SPECIFIER}` ) { - if (strictClientPath && fs.existsSync(strictClientPath)) { + if (fs.existsSync(strictClientPath)) { return strictClientPath; } throw new Error(UNRESOLVED_CLIENT_ENV_ERROR); @@ -165,7 +166,8 @@ export function registerViteExtendHook( id === SHARED_SCHEMA_SPECIFIER || id === `\0${SHARED_SCHEMA_SPECIFIER}` ) { - if (strictSharedPath && fs.existsSync(strictSharedPath)) { + // `strictSharedPath` is either the user file or empty-shared-schema. + if (fs.existsSync(strictSharedPath)) { return strictSharedPath; } throw new Error(UNRESOLVED_SHARED_SCHEMA_ERROR); diff --git a/packages/nuxt/src/strict-shared-schema.ts b/packages/nuxt/src/strict-shared-schema.ts index 7bcd95904..0374b9221 100644 --- a/packages/nuxt/src/strict-shared-schema.ts +++ b/packages/nuxt/src/strict-shared-schema.ts @@ -1,11 +1,9 @@ -import { formatBuildError } from "@repo/log"; - export const SHARED_SCHEMA_SPECIFIER = "#arkenv/shared-schema"; export const UNRESOLVED_SHARED_SCHEMA_ERROR = "[arkenv] Could not resolve #arkenv/shared-schema.\n" + - "Ensure @arkenv/nuxt/module is registered in nuxt.config and env/internal/shared.ts\n" + - "exports SharedSchema, or pass extends: [SharedSchema] explicitly."; + "Ensure @arkenv/nuxt/module is registered in nuxt.config. When present,\n" + + "env/internal/shared.ts must export SharedSchema, or pass extends: [SharedSchema] explicitly."; type GlobalStrictState = { __ARKENV_SHARED_SCHEMA__?: unknown; @@ -16,7 +14,8 @@ type GlobalStrictState = { * * Prefers the Jiti validation injection on `globalThis`, then falls back to * the statically imported `#arkenv/shared-schema` module (aliased by the Nuxt - * module). Never uses `node:module` / `createRequire`. + * module to the project file or `empty-shared-schema` when absent). Never uses + * `node:module` / `createRequire`. * * @param importedSharedSchema The `SharedSchema` export from `#arkenv/shared-schema` * @returns The shared schema to pass through `extends` @@ -36,23 +35,3 @@ export function resolveStrictSharedSchema( throw new Error(UNRESOLVED_SHARED_SCHEMA_ERROR); } - -/** - * Build the fail-fast error for a missing strict-layout `internal/shared.ts`. - * - * @param sharedPath The absolute path where `internal/shared.ts` was expected - * @param baseDir The strict layout base directory - * @returns A formatted ArkEnv build error message - */ -export function missingSharedTsError( - sharedPath: string, - baseDir: string, -): string { - return formatBuildError( - `Strict layout requires "internal/shared.ts" but it was not found at "${sharedPath}".\n` + - `Expected strict layout structure under "${baseDir}":\n` + - " ├── internal/shared.ts\n" + - " ├── client.ts\n" + - " └── server.ts", - ); -} diff --git a/packages/nuxt/src/validation.test.ts b/packages/nuxt/src/validation.test.ts index 3156ea409..e0dceb6df 100644 --- a/packages/nuxt/src/validation.test.ts +++ b/packages/nuxt/src/validation.test.ts @@ -287,6 +287,41 @@ describe("build-time environment validation", () => { }).toThrow(/SharedSchema/); }); + it("should pass when internal/shared.ts is omitted", () => { + fs.writeFileSync( + clientPath, + ` + import arkenv from "@arkenv/nuxt/client"; + export const env = arkenv({ + NUXT_PUBLIC_API_URL: "string", + }); + `, + "utf-8", + ); + + fs.writeFileSync( + serverPath, + ` + import arkenv from "${path.resolve(__dirname, "./server.ts")}"; + export const env = arkenv({ + DATABASE_URL: "string", + }); + `, + "utf-8", + ); + + process.env.DATABASE_URL = "postgres://localhost/db"; + process.env.NUXT_PUBLIC_API_URL = "https://api.example.com"; + + expect(() => { + setupArkEnv({ + schemaPath: strictBaseDir, + layout: "strict", + validate: true, + }); + }).not.toThrow(); + }, 15_000); + it("should still honor explicit extends in strict layout validation", () => { fs.writeFileSync( sharedPath, diff --git a/packages/vite-plugin/src/env-module-path.ts b/packages/vite-plugin/src/env-module-path.ts index c4ffbd0db..abda94a41 100644 --- a/packages/vite-plugin/src/env-module-path.ts +++ b/packages/vite-plugin/src/env-module-path.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { + assertFlatSchemaFile, findSchemaPath, formatMissingSchemaError, getDefaultSchemaFileCandidates, @@ -62,7 +63,7 @@ export function resolveEnvModulePath( `ArkEnv Vite plugin: schemaPath "${schemaPath}" does not exist (resolved to "${resolved}").`, ); } - return resolved; + return assertFlatSchemaFile(resolved, "ArkEnv Vite plugin:"); } const discovered = findSchemaPath(root); @@ -75,7 +76,7 @@ export function resolveEnvModulePath( }), ); } - return discovered; + return assertFlatSchemaFile(discovered, "ArkEnv Vite plugin:"); } /** diff --git a/packages/vite-plugin/src/env-module.test.ts b/packages/vite-plugin/src/env-module.test.ts index d27808b4d..b6b399079 100644 --- a/packages/vite-plugin/src/env-module.test.ts +++ b/packages/vite-plugin/src/env-module.test.ts @@ -1,4 +1,10 @@ -import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import * as vite from "vite"; @@ -274,4 +280,18 @@ describe("missing-schema errors", () => { expect(message).not.toMatch(/import \{ type \} from "arktype"/); expect(message).not.toMatch(/from "zod"/); }); + + it("rejects a discovered strict layout directory", async () => { + const { resolveEnvModulePath } = await import("./env-module-path.js"); + const root = mkdtempSync(join(tmpdir(), "arkenv-vite-strict-dir-")); + temps.push(root); + const envDir = join(root, "env"); + mkdirSync(envDir, { recursive: true }); + writeFileSync(join(envDir, "client.ts"), "export const env = {}"); + writeFileSync(join(envDir, "server.ts"), "export const env = {}"); + + expect(() => resolveEnvModulePath(root)).toThrow( + /only supports a flat env module file/, + ); + }); }); From 5a2d91edfc6de2548e8d868900e83d030d0d3bd8 Mon Sep 17 00:00:00 2001 From: "arkenv-bot[bot]" <237618717+arkenv-bot[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:20:34 +0500 Subject: [PATCH 15/15] Version Packages (alpha) (#1489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to v1, this PR will be updated. ⚠️⚠️⚠️⚠️⚠️⚠️ `v1` is currently in **pre mode** so this branch has prereleases rather than normal releases. If you want to exit prereleases, run `changeset pre exit` on `v1`. ⚠️⚠️⚠️⚠️⚠️⚠️ # Releases ## arkenv@1.0.0-alpha.11 ### Patch Changes - #### Split `--help` options into Global and `init` sections _[`#1487`](https://github.com/yamcodes/arkenv/pull/1487) [`f86887c`](https://github.com/yamcodes/arkenv/commit/f86887c55c13c979451ff85d6d4ec5d3d69113dd) [@yamcodes](https://github.com/yamcodes)_ List shared flags under **Global options** and scaffolding flags under **init options**, matching the multi-command `/docs/cli` taxonomy. ```bash npx arkenv@alpha --help ``` ```text Usage: arkenv init [project-name] ... arkenv add host [provider] ... Global options: --yes, -y Skip prompts and use defaults ... --quiet, -q Quiet mode ... --json, -j Output structured JSON ... --agent Enable non-interactive, machine-readable mode ... --help, -h Show this help message init options: --example Specify an example name ... --force, -f Bypass checks and force scaffolding --no-codegen Disable automatic env.gen.ts code generation ... --host-preset, -H Specify a hosting provider preset ... ``` ## @arkenv/build@0.0.2-alpha.2 ### Patch Changes - #### Make missing-schema errors short and actionable across hosts _[`#1495`](https://github.com/yamcodes/arkenv/pull/1495) [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [@yamcodes](https://github.com/yamcodes)_ When a host cannot find an env schema, throw a consistent message that names the expected path / `schemaPath` and points to `npx arkenv@latest init`, without embedding a starter `env.ts` module. Example: ```text [ArkEnv] Could not find schema file at src/env.ts or env.ts. Please specify 'schemaPath' in ArkEnv options (or run `npx arkenv@latest init`). ``` - #### Make `env/internal/shared.ts` optional in strict layout _[`#1505`](https://github.com/yamcodes/arkenv/pull/1505) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) [@yamcodes](https://github.com/yamcodes)_ Strict layout now works with just `client.ts` and `server.ts`. Omit `internal/shared.ts` when you have nothing to share — shared keys are treated as empty. ```ts // env/client.ts + env/server.ts alone is enough export default withArkEnv(nextConfig, { layout: "strict", }); ``` The CLI still scaffolds `shared.ts` by default for convenience. ## @arkenv/bun-plugin@1.0.0-alpha.8 ### Patch Changes - #### Make missing-schema errors short and actionable across hosts _[`#1495`](https://github.com/yamcodes/arkenv/pull/1495) [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [@yamcodes](https://github.com/yamcodes)_ When a host cannot find an env schema, throw a consistent message that names the expected path / `schemaPath` and points to `npx arkenv@latest init`, without embedding a starter `env.ts` module. Example: ```text [ArkEnv] Could not find schema file at src/env.ts or env.ts. Please specify 'schemaPath' in ArkEnv options (or run `npx arkenv@latest init`). ```
Updated 1 dependency [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) - `@arkenv/build@0.0.2-alpha.2`
## @arkenv/nextjs@1.0.0-alpha.9 ### Patch Changes - #### Align missing-schema errors with short, actionable host guidance _[`#1488`](https://github.com/yamcodes/arkenv/pull/1488) [`9d5bdbb`](https://github.com/yamcodes/arkenv/commit/9d5bdbbeaf2fdddf69f5bcc47a7d79b15a51ece3) [@yamcodes](https://github.com/yamcodes)_ Point missing-schema errors at checked paths / `schemaPath` and `arkenv init`, matching the Bun plugin style, without embedding starter `env.ts` modules. - #### Make missing-schema errors short and actionable across hosts _[`#1495`](https://github.com/yamcodes/arkenv/pull/1495) [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [@yamcodes](https://github.com/yamcodes)_ When a host cannot find an env schema, throw a consistent message that names the expected path / `schemaPath` and points to `npx arkenv@latest init`, without embedding a starter `env.ts` module. Example: ```text [ArkEnv] Could not find schema file at src/env.ts or env.ts. Please specify 'schemaPath' in ArkEnv options (or run `npx arkenv@latest init`). ``` - #### Make `env/internal/shared.ts` optional in strict layout _[`#1505`](https://github.com/yamcodes/arkenv/pull/1505) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) [@yamcodes](https://github.com/yamcodes)_ Strict layout now works with just `client.ts` and `server.ts`. Omit `internal/shared.ts` when you have nothing to share — shared keys are treated as empty. ```ts // env/client.ts + env/server.ts alone is enough export default withArkEnv(nextConfig, { layout: "strict", }); ``` The CLI still scaffolds `shared.ts` by default for convenience.
Updated 1 dependency [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) - `@arkenv/build@0.0.2-alpha.2`
## @arkenv/nuxt@1.0.0-alpha.11 ### Patch Changes - #### Align missing-schema errors with short, actionable host guidance _[`#1488`](https://github.com/yamcodes/arkenv/pull/1488) [`9d5bdbb`](https://github.com/yamcodes/arkenv/commit/9d5bdbbeaf2fdddf69f5bcc47a7d79b15a51ece3) [@yamcodes](https://github.com/yamcodes)_ Point missing-schema errors at checked paths / `schemaPath` and `arkenv init`, matching the Bun plugin style, without embedding starter `env.ts` modules. - #### Make missing-schema errors short and actionable across hosts _[`#1495`](https://github.com/yamcodes/arkenv/pull/1495) [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [@yamcodes](https://github.com/yamcodes)_ When a host cannot find an env schema, throw a consistent message that names the expected path / `schemaPath` and points to `npx arkenv@latest init`, without embedding a starter `env.ts` module. Example: ```text [ArkEnv] Could not find schema file at src/env.ts or env.ts. Please specify 'schemaPath' in ArkEnv options (or run `npx arkenv@latest init`). ``` - #### Make `env/internal/shared.ts` optional in strict layout _[`#1505`](https://github.com/yamcodes/arkenv/pull/1505) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) [@yamcodes](https://github.com/yamcodes)_ Strict layout now works with just `client.ts` and `server.ts`. Omit `internal/shared.ts` when you have nothing to share — shared keys are treated as empty. ```ts // env/client.ts + env/server.ts alone is enough export default withArkEnv(nextConfig, { layout: "strict", }); ``` The CLI still scaffolds `shared.ts` by default for convenience.
Updated 1 dependency [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) - `@arkenv/build@0.0.2-alpha.2`
## @arkenv/vite-plugin@1.0.0-alpha.8 ### Patch Changes - #### Align missing-schema errors with short, actionable host guidance _[`#1488`](https://github.com/yamcodes/arkenv/pull/1488) [`9d5bdbb`](https://github.com/yamcodes/arkenv/commit/9d5bdbbeaf2fdddf69f5bcc47a7d79b15a51ece3) [@yamcodes](https://github.com/yamcodes)_ Point missing-schema errors at checked paths / `schemaPath` and `arkenv init`, matching the Bun plugin style, without embedding starter `env.ts` modules. - #### Make missing-schema errors short and actionable across hosts _[`#1495`](https://github.com/yamcodes/arkenv/pull/1495) [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [@yamcodes](https://github.com/yamcodes)_ When a host cannot find an env schema, throw a consistent message that names the expected path / `schemaPath` and points to `npx arkenv@latest init`, without embedding a starter `env.ts` module. Example: ```text [ArkEnv] Could not find schema file at src/env.ts or env.ts. Please specify 'schemaPath' in ArkEnv options (or run `npx arkenv@latest init`). ```
Updated 1 dependency [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) - `@arkenv/build@0.0.2-alpha.2`
--------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .changeset/pre.json | 4 +++ packages/arkenv/CHANGELOG.md | 31 ++++++++++++++++++++++ packages/arkenv/package.json | 2 +- packages/build/CHANGELOG.md | 27 +++++++++++++++++++ packages/build/package.json | 2 +- packages/bun-plugin/CHANGELOG.md | 26 +++++++++++++++++++ packages/bun-plugin/package.json | 2 +- packages/nextjs/CHANGELOG.md | 43 +++++++++++++++++++++++++++++++ packages/nextjs/package.json | 2 +- packages/nuxt/CHANGELOG.md | 43 +++++++++++++++++++++++++++++++ packages/nuxt/package.json | 2 +- packages/vite-plugin/CHANGELOG.md | 30 +++++++++++++++++++++ packages/vite-plugin/package.json | 2 +- 13 files changed, 210 insertions(+), 6 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index f52140d3f..520e6dfec 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -27,7 +27,9 @@ "add-host-preset-add-command", "add-host-presets-phase-4", "add-host-strict-layouts-forward-port", + "align-missing-schema-host-hints", "align-missing-schema-throw", + "align-v1-shared-missing-schema-errors", "bun-env-module-transform", "drop-example-e-alias", "epg3nspi", @@ -46,9 +48,11 @@ "nuxt-skip-setup-before-boot-gate", "nuxt-strict-auto-extend", "nuxt-strict-client-shared-auto-extend", + "optional-strict-shared-v1", "reconcile-v0-features", "remove-framework-shared-exports", "rename-create-env-to-arkenv", + "split-help-global-init-options", "standard-isolation-guards", "standard-mode-flat-layout", "standard-mode-packaging", diff --git a/packages/arkenv/CHANGELOG.md b/packages/arkenv/CHANGELOG.md index d284d19b1..75f1d5b05 100644 --- a/packages/arkenv/CHANGELOG.md +++ b/packages/arkenv/CHANGELOG.md @@ -1,5 +1,36 @@ # @arkenv/core +## 1.0.0-alpha.11 + +### Patch Changes + +- #### Split `--help` options into Global and `init` sections _[`#1487`](https://github.com/yamcodes/arkenv/pull/1487) [`f86887c`](https://github.com/yamcodes/arkenv/commit/f86887c55c13c979451ff85d6d4ec5d3d69113dd) [@yamcodes](https://github.com/yamcodes)_ + + List shared flags under **Global options** and scaffolding flags under **init options**, matching the multi-command `/docs/cli` taxonomy. + + ```bash + npx arkenv@alpha --help + ``` + + ```text + Usage: + arkenv init [project-name] ... + arkenv add host [provider] ... + + Global options: + --yes, -y Skip prompts and use defaults ... + --quiet, -q Quiet mode ... + --json, -j Output structured JSON ... + --agent Enable non-interactive, machine-readable mode ... + --help, -h Show this help message + + init options: + --example Specify an example name ... + --force, -f Bypass checks and force scaffolding + --no-codegen Disable automatic env.gen.ts code generation ... + --host-preset, -H Specify a hosting provider preset ... + ``` + ## 1.0.0-alpha.10 ### Minor Changes diff --git a/packages/arkenv/package.json b/packages/arkenv/package.json index bca739229..16086b48c 100644 --- a/packages/arkenv/package.json +++ b/packages/arkenv/package.json @@ -1,7 +1,7 @@ { "name": "arkenv", "type": "module", - "version": "1.0.0-alpha.10", + "version": "1.0.0-alpha.11", "description": "Interactive CLI for scaffolding ArkEnv projects", "bin": { "arkenv": "./dist/index.cjs" diff --git a/packages/build/CHANGELOG.md b/packages/build/CHANGELOG.md index b9569e1f2..8ae17e8fb 100644 --- a/packages/build/CHANGELOG.md +++ b/packages/build/CHANGELOG.md @@ -1,5 +1,32 @@ # @arkenv/build +## 0.0.2-alpha.2 + +### Patch Changes + +- #### Make missing-schema errors short and actionable across hosts _[`#1495`](https://github.com/yamcodes/arkenv/pull/1495) [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [@yamcodes](https://github.com/yamcodes)_ + + When a host cannot find an env schema, throw a consistent message that names the expected path / `schemaPath` and points to `npx arkenv@latest init`, without embedding a starter `env.ts` module. + + Example: + + ```text + [ArkEnv] Could not find schema file at src/env.ts or env.ts. Please specify 'schemaPath' in ArkEnv options (or run `npx arkenv@latest init`). + ``` + +- #### Make `env/internal/shared.ts` optional in strict layout _[`#1505`](https://github.com/yamcodes/arkenv/pull/1505) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) [@yamcodes](https://github.com/yamcodes)_ + + Strict layout now works with just `client.ts` and `server.ts`. Omit `internal/shared.ts` when you have nothing to share — shared keys are treated as empty. + + ```ts + // env/client.ts + env/server.ts alone is enough + export default withArkEnv(nextConfig, { + layout: "strict", + }); + ``` + + The CLI still scaffolds `shared.ts` by default for convenience. + ## 0.0.2-alpha.1 ### Patch Changes diff --git a/packages/build/package.json b/packages/build/package.json index b25e0f9e7..0ef7f6f43 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -1,6 +1,6 @@ { "name": "@arkenv/build", - "version": "0.0.2-alpha.1", + "version": "0.0.2-alpha.2", "description": "Shared build and codegen utilities for ArkEnv framework plugins", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/bun-plugin/CHANGELOG.md b/packages/bun-plugin/CHANGELOG.md index 9aec52911..3cd97f96a 100644 --- a/packages/bun-plugin/CHANGELOG.md +++ b/packages/bun-plugin/CHANGELOG.md @@ -1,5 +1,31 @@ # @arkenv/bun-plugin +## 1.0.0-alpha.8 + +### Patch Changes + +- #### Make missing-schema errors short and actionable across hosts _[`#1495`](https://github.com/yamcodes/arkenv/pull/1495) [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [@yamcodes](https://github.com/yamcodes)_ + + When a host cannot find an env schema, throw a consistent message that names the expected path / `schemaPath` and points to `npx arkenv@latest init`, without embedding a starter `env.ts` module. + + Example: + + ```text + [ArkEnv] Could not find schema file at src/env.ts or env.ts. Please specify 'schemaPath' in ArkEnv options (or run `npx arkenv@latest init`). + ``` + +
Updated 1 dependency + + + +[`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) + + + +- `@arkenv/build@0.0.2-alpha.2` + +
+ ## 1.0.0-alpha.7 ### Minor Changes diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json index ec933074a..8b90ffaaf 100644 --- a/packages/bun-plugin/package.json +++ b/packages/bun-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@arkenv/bun-plugin", - "version": "1.0.0-alpha.7", + "version": "1.0.0-alpha.8", "author": "Yam Borodetsky ", "repository": { "type": "git", diff --git a/packages/nextjs/CHANGELOG.md b/packages/nextjs/CHANGELOG.md index e73a5a1e0..97d2c9376 100644 --- a/packages/nextjs/CHANGELOG.md +++ b/packages/nextjs/CHANGELOG.md @@ -1,5 +1,48 @@ # @arkenv/nextjs +## 1.0.0-alpha.9 + +### Patch Changes + +- #### Align missing-schema errors with short, actionable host guidance _[`#1488`](https://github.com/yamcodes/arkenv/pull/1488) [`9d5bdbb`](https://github.com/yamcodes/arkenv/commit/9d5bdbbeaf2fdddf69f5bcc47a7d79b15a51ece3) [@yamcodes](https://github.com/yamcodes)_ + + Point missing-schema errors at checked paths / `schemaPath` and `arkenv init`, matching the Bun plugin style, without embedding starter `env.ts` modules. + +- #### Make missing-schema errors short and actionable across hosts _[`#1495`](https://github.com/yamcodes/arkenv/pull/1495) [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [@yamcodes](https://github.com/yamcodes)_ + + When a host cannot find an env schema, throw a consistent message that names the expected path / `schemaPath` and points to `npx arkenv@latest init`, without embedding a starter `env.ts` module. + + Example: + + ```text + [ArkEnv] Could not find schema file at src/env.ts or env.ts. Please specify 'schemaPath' in ArkEnv options (or run `npx arkenv@latest init`). + ``` + +- #### Make `env/internal/shared.ts` optional in strict layout _[`#1505`](https://github.com/yamcodes/arkenv/pull/1505) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) [@yamcodes](https://github.com/yamcodes)_ + + Strict layout now works with just `client.ts` and `server.ts`. Omit `internal/shared.ts` when you have nothing to share — shared keys are treated as empty. + + ```ts + // env/client.ts + env/server.ts alone is enough + export default withArkEnv(nextConfig, { + layout: "strict", + }); + ``` + + The CLI still scaffolds `shared.ts` by default for convenience. + +
Updated 1 dependency + + + +[`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) + + + +- `@arkenv/build@0.0.2-alpha.2` + +
+ ## 1.0.0-alpha.8 ### Patch Changes diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 41ed07e91..ab8077b1a 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@arkenv/nextjs", - "version": "1.0.0-alpha.8", + "version": "1.0.0-alpha.9", "author": "Yam Borodetsky ", "repository": { "type": "git", diff --git a/packages/nuxt/CHANGELOG.md b/packages/nuxt/CHANGELOG.md index 6578c3b8e..6966fc2ce 100644 --- a/packages/nuxt/CHANGELOG.md +++ b/packages/nuxt/CHANGELOG.md @@ -1,5 +1,48 @@ # @arkenv/nuxt +## 1.0.0-alpha.11 + +### Patch Changes + +- #### Align missing-schema errors with short, actionable host guidance _[`#1488`](https://github.com/yamcodes/arkenv/pull/1488) [`9d5bdbb`](https://github.com/yamcodes/arkenv/commit/9d5bdbbeaf2fdddf69f5bcc47a7d79b15a51ece3) [@yamcodes](https://github.com/yamcodes)_ + + Point missing-schema errors at checked paths / `schemaPath` and `arkenv init`, matching the Bun plugin style, without embedding starter `env.ts` modules. + +- #### Make missing-schema errors short and actionable across hosts _[`#1495`](https://github.com/yamcodes/arkenv/pull/1495) [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [@yamcodes](https://github.com/yamcodes)_ + + When a host cannot find an env schema, throw a consistent message that names the expected path / `schemaPath` and points to `npx arkenv@latest init`, without embedding a starter `env.ts` module. + + Example: + + ```text + [ArkEnv] Could not find schema file at src/env.ts or env.ts. Please specify 'schemaPath' in ArkEnv options (or run `npx arkenv@latest init`). + ``` + +- #### Make `env/internal/shared.ts` optional in strict layout _[`#1505`](https://github.com/yamcodes/arkenv/pull/1505) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) [@yamcodes](https://github.com/yamcodes)_ + + Strict layout now works with just `client.ts` and `server.ts`. Omit `internal/shared.ts` when you have nothing to share — shared keys are treated as empty. + + ```ts + // env/client.ts + env/server.ts alone is enough + export default withArkEnv(nextConfig, { + layout: "strict", + }); + ``` + + The CLI still scaffolds `shared.ts` by default for convenience. + +
Updated 1 dependency + + + +[`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) + + + +- `@arkenv/build@0.0.2-alpha.2` + +
+ ## 1.0.0-alpha.10 ### Major Changes diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index ccd6a0d0a..d6a36884d 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@arkenv/nuxt", - "version": "1.0.0-alpha.10", + "version": "1.0.0-alpha.11", "author": "Yam Borodetsky ", "repository": { "type": "git", diff --git a/packages/vite-plugin/CHANGELOG.md b/packages/vite-plugin/CHANGELOG.md index 50b49b902..c3a0b0a64 100644 --- a/packages/vite-plugin/CHANGELOG.md +++ b/packages/vite-plugin/CHANGELOG.md @@ -1,5 +1,35 @@ # @arkenv/vite-plugin +## 1.0.0-alpha.8 + +### Patch Changes + +- #### Align missing-schema errors with short, actionable host guidance _[`#1488`](https://github.com/yamcodes/arkenv/pull/1488) [`9d5bdbb`](https://github.com/yamcodes/arkenv/commit/9d5bdbbeaf2fdddf69f5bcc47a7d79b15a51ece3) [@yamcodes](https://github.com/yamcodes)_ + + Point missing-schema errors at checked paths / `schemaPath` and `arkenv init`, matching the Bun plugin style, without embedding starter `env.ts` modules. + +- #### Make missing-schema errors short and actionable across hosts _[`#1495`](https://github.com/yamcodes/arkenv/pull/1495) [`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [@yamcodes](https://github.com/yamcodes)_ + + When a host cannot find an env schema, throw a consistent message that names the expected path / `schemaPath` and points to `npx arkenv@latest init`, without embedding a starter `env.ts` module. + + Example: + + ```text + [ArkEnv] Could not find schema file at src/env.ts or env.ts. Please specify 'schemaPath' in ArkEnv options (or run `npx arkenv@latest init`). + ``` + +
Updated 1 dependency + + + +[`3785c6b`](https://github.com/yamcodes/arkenv/commit/3785c6bfa27888a669900045b5b326e7baa1558b) [`9bfe1c4`](https://github.com/yamcodes/arkenv/commit/9bfe1c4a6e278966ff2c0b2219d95e319888fb98) + + + +- `@arkenv/build@0.0.2-alpha.2` + +
+ ## 1.0.0-alpha.7 ### Minor Changes diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index d2735f4d1..df42434b6 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@arkenv/vite-plugin", - "version": "1.0.0-alpha.7", + "version": "1.0.0-alpha.8", "author": "Yam Borodetsky ", "repository": { "type": "git",