Skip to content

Commit 7d1579f

Browse files
committed
feat(tanstack-addon): make demo route optional via user-select prompt
1 parent 01ef1bc commit 7d1579f

9 files changed

Lines changed: 253 additions & 116 deletions

File tree

apps/www/content/docs/frameworks/tanstack-start.mdx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,22 @@ Generate a new TanStack Start project with the ArkEnv add-on:
3131
npx @tanstack/cli create my-app --add-ons https://arkenv.js.org/tanstack/info.json
3232
```
3333

34-
The CLI prompts you to choose your preferred validation engine:
34+
The CLI prompts you with configuration choices:
3535

36-
- **ArkType** (default) — installs `@arkenv/core` and `arktype`
37-
- **Zod** — installs `@arkenv/standard` and `zod`
38-
- **Valibot** — installs `@arkenv/standard` and `valibot`
36+
- **Validator Engine**:
37+
- **ArkType** (default) — installs `@arkenv/core` and `arktype`
38+
- **Zod** — installs `@arkenv/standard` and `zod`
39+
- **Valibot** — installs `@arkenv/standard` and `valibot`
40+
- **Interactive Demo Route**:
41+
- **Include `/demo/arkenv`** (default) — scaffolds an interactive secret leak test showcasing server-only isolation in the browser
42+
- **Skip demo route** — scaffolds only the schema definition and plugin configuration
3943

4044
The add-on automatically:
4145

4246
- Installs the required validator runtime and dev dependencies
4347
- Configures `arkenvVitePlugin()` in `vite.config.ts`
4448
- Generates `src/env.ts` with your selected validator engine
45-
- Adds an interactive demo route at `/demo/arkenv` showcasing server-only secret isolation and runtime safety
49+
- Pre-populates declared keys in `.env.example`
4650

4751
<Callout type="info" title="Adding to an existing TanStack project">
4852
If you already have a TanStack application, add ArkEnv directly with the TanStack CLI:
Lines changed: 73 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,74 @@
11
{
2-
"id": "arkenv",
3-
"name": "ArkEnv",
4-
"version": "1.0.0-alpha.1",
5-
"description": "Typesafe environment variable validation with build-time validation and runtime leak protection.",
6-
"type": "add-on",
7-
"phase": "add-on",
8-
"category": "tooling",
9-
"color": "#06B6D4",
10-
"priority": 28,
11-
"link": "https://arkenv.js.org",
12-
"modes": ["file-router", "code-router"],
13-
"options": {
14-
"validator": {
15-
"type": "select",
16-
"label": "Validator Engine",
17-
"default": "arktype",
18-
"options": [
19-
{
20-
"value": "arktype",
21-
"label": "ArkType (@arkenv/core) - Recommended"
22-
},
23-
{
24-
"value": "zod",
25-
"label": "Zod (@arkenv/standard)"
26-
},
27-
{
28-
"value": "valibot",
29-
"label": "Valibot (@arkenv/standard)"
30-
}
31-
]
32-
}
33-
},
34-
"routes": [
35-
{
36-
"url": "/demo/arkenv",
37-
"name": "ArkEnv Demo",
38-
"path": "src/routes/demo/arkenv.tsx",
39-
"jsName": "ArkEnvDemo"
40-
}
41-
],
42-
"integrations": [
43-
{
44-
"type": "vite-plugin",
45-
"import": "import arkenv from '@arkenv/vite-plugin'",
46-
"code": "arkenv()"
47-
}
48-
],
49-
"packageTemplate": "<%\n const arkenvOption = (typeof addOnOption !== 'undefined' && (\n addOnOption['arkenv'] ||\n Object.entries(addOnOption).find(([k]) => k.includes('arkenv') || k.includes('info.json'))?.[1]\n )) || {};\n const validator = arkenvOption.validator || 'arktype';\n-%>\n{\n \"dependencies\": {\n<% if (validator === 'zod') { -%>\n \"@arkenv/standard\": \"^1.0.0-alpha.1\",\n \"zod\": \"^3.24.2\"\n<% } else if (validator === 'valibot') { -%>\n \"@arkenv/standard\": \"^1.0.0-alpha.1\",\n \"valibot\": \"^1.0.0\"\n<% } else { -%>\n \"@arkenv/core\": \"^1.0.0-alpha.1\",\n \"arktype\": \"^2.2.0\"\n<% } -%>\n },\n \"devDependencies\": {\n \"@arkenv/vite-plugin\": \"^1.0.0-alpha.1\"\n }\n}\n",
50-
"files": {
51-
"_dot_env.example": "# Port for the dev/preview server\nPORT=3000\n\n# Public API URL (inlined into client bundle)\nVITE_API_URL=https://api.example.com\n\n# Server-only database connection URL (protected from client access)\nDATABASE_URL=postgresql://postgres:postgres@localhost:5432/db\n\n# Environment mode\nNODE_ENV=development\n",
52-
"src/env.ts.ejs": "<%\n const arkenvOption = (typeof addOnOption !== 'undefined' && (\n addOnOption['arkenv'] ||\n Object.entries(addOnOption).find(([k]) => k.includes('arkenv') || k.includes('info.json'))?.[1]\n )) || {};\n const validator = arkenvOption.validator || 'arktype';\n-%>\n<% if (validator === 'zod') { -%>\nimport arkenv from \"@arkenv/standard\";\nimport { z } from \"zod\";\n\nexport const env = arkenv({\n PORT: z.coerce.number().int().min(1).max(65535).default(3000),\n VITE_API_URL: z.string().url().default(\"https://api.example.com\"),\n DATABASE_URL: z.string().url().default(\"postgresql://postgres:postgres@localhost:5432/db\"),\n NODE_ENV: z.enum([\"development\", \"production\", \"test\"]).default(\"development\"),\n});\n<% } else if (validator === 'valibot') { -%>\nimport arkenv from \"@arkenv/standard\";\nimport * as v from \"valibot\";\n\nexport const env = arkenv({\n PORT: v.optional(v.pipe(v.unknown(), v.transform(Number), v.integer()), 3000),\n VITE_API_URL: v.optional(v.pipe(v.string(), v.url()), \"https://api.example.com\"),\n DATABASE_URL: v.optional(v.pipe(v.string(), v.url()), \"postgresql://postgres:postgres@localhost:5432/db\"),\n NODE_ENV: v.optional(v.picklist([\"development\", \"production\", \"test\"]), \"development\"),\n});\n<% } else { -%>\nimport arkenv from \"@arkenv/core\";\n\nexport const env = arkenv({\n PORT: \"number.port = 3000\",\n VITE_API_URL: \"string = 'https://api.example.com'\",\n DATABASE_URL: \"string = 'postgresql://postgres:postgres@localhost:5432/db'\",\n NODE_ENV: \"'development' | 'production' | 'test' = 'development'\",\n});\n<% } -%>\n",
53-
"src/routes/demo/arkenv.tsx": "import { createFileRoute } from \"@tanstack/react-router\";\nimport { createServerFn } from \"@tanstack/react-start\";\nimport { useState } from \"react\";\nimport { env } from \"../../env\";\n\nconst getDatabaseConfig = createServerFn({ method: \"GET\" }).handler(() => {\n\t// Server-only key: safely read on the server during SSR / RPC\n\ttry {\n\t\tconst url = new URL(env.DATABASE_URL);\n\t\treturn { host: url.host, protocol: url.protocol };\n\t} catch {\n\t\treturn { host: \"localhost:5432\", protocol: \"postgresql:\" };\n\t}\n});\n\nexport const Route = createFileRoute(\"/demo/arkenv\")({\n\tcomponent: ArkEnvDemo,\n\tloader: () => getDatabaseConfig(),\n});\n\nfunction LeakedSecret() {\n\t// Accessing server-only DATABASE_URL directly on the client throws at runtime\n\treturn <p>Server key leaked: {env.DATABASE_URL}</p>;\n}\n\nfunction ArkEnvDemo() {\n\tconst dbConfig = Route.useLoaderData();\n\tconst [attemptLeak, setAttemptLeak] = useState(false);\n\n\treturn (\n\t\t<div className=\"p-6 max-w-xl mx-auto space-y-4 font-sans\">\n\t\t\t<h1 className=\"text-2xl font-bold\">ArkEnv Demo</h1>\n\t\t\t<p className=\"text-sm text-gray-600\">\n\t\t\t\tTypesafe environment variables with build-time validation and runtime\n\t\t\t\tleak protection.\n\t\t\t</p>\n\n\t\t\t<div className=\"p-4 bg-gray-100 dark:bg-gray-800 rounded-md space-y-2\">\n\t\t\t\t<h2 className=\"font-semibold text-lg\">Public Client Variables</h2>\n\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-300\">\n\t\t\t\t\tInlined safely into client bundles:\n\t\t\t\t</p>\n\t\t\t\t<code className=\"block p-2 bg-white dark:bg-black rounded border text-xs font-mono\">\n\t\t\t\t\tenv.VITE_API_URL: {env.VITE_API_URL}\n\t\t\t\t</code>\n\t\t\t</div>\n\n\t\t\t<div className=\"p-4 bg-gray-100 dark:bg-gray-800 rounded-md space-y-2\">\n\t\t\t\t<h2 className=\"font-semibold text-lg\">Server-Only Variables</h2>\n\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-300\">\n\t\t\t\t\tAccessible inside createServerFn handlers:\n\t\t\t\t</p>\n\t\t\t\t<code className=\"block p-2 bg-white dark:bg-black rounded border text-xs font-mono\">\n\t\t\t\t\tDatabase Host: {dbConfig.host} ({dbConfig.protocol})\n\t\t\t\t</code>\n\t\t\t</div>\n\n\t\t\t<div className=\"p-4 bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 rounded-md space-y-2\">\n\t\t\t\t<h2 className=\"font-semibold text-red-800 dark:text-red-300 text-lg\">\n\t\t\t\t\tSecret Leak Protection\n\t\t\t\t</h2>\n\t\t\t\t<p className=\"text-sm text-red-700 dark:text-red-400\">\n\t\t\t\t\tClicking the button below attempts to access the server secret{\" \"}\n\t\t\t\t\t<code>env.DATABASE_URL</code> on the client, which ArkEnv blocks:\n\t\t\t\t</p>\n\t\t\t\t{attemptLeak ? (\n\t\t\t\t\t<LeakedSecret />\n\t\t\t\t) : (\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclassName=\"px-3 py-1.5 bg-red-600 text-white rounded text-sm hover:bg-red-700 cursor-pointer\"\n\t\t\t\t\t\tonClick={() => setAttemptLeak(true)}\n\t\t\t\t\t>\n\t\t\t\t\t\tAttempt client access to DATABASE_URL\n\t\t\t\t\t</button>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
54-
},
55-
"deletedFiles": []
56-
}
2+
"id": "arkenv",
3+
"name": "ArkEnv",
4+
"version": "1.0.0-alpha.1",
5+
"description": "Typesafe environment variable validation with build-time validation and runtime leak protection.",
6+
"type": "add-on",
7+
"phase": "add-on",
8+
"category": "tooling",
9+
"color": "#06B6D4",
10+
"priority": 28,
11+
"link": "https://arkenv.js.org",
12+
"modes": [
13+
"file-router",
14+
"code-router"
15+
],
16+
"options": {
17+
"validator": {
18+
"type": "select",
19+
"label": "Validator Engine",
20+
"default": "arktype",
21+
"options": [
22+
{
23+
"value": "arktype",
24+
"label": "ArkType (@arkenv/core) - Recommended"
25+
},
26+
{
27+
"value": "zod",
28+
"label": "Zod (@arkenv/standard)"
29+
},
30+
{
31+
"value": "valibot",
32+
"label": "Valibot (@arkenv/standard)"
33+
}
34+
]
35+
},
36+
"demo": {
37+
"type": "select",
38+
"label": "Interactive Demo Route",
39+
"default": "true",
40+
"options": [
41+
{
42+
"value": "true",
43+
"label": "Include /demo/arkenv (recommended)"
44+
},
45+
{
46+
"value": "false",
47+
"label": "Skip demo route"
48+
}
49+
]
50+
}
51+
},
52+
"routes": [
53+
{
54+
"url": "/demo/arkenv",
55+
"name": "ArkEnv Demo",
56+
"path": "src/routes/demo/arkenv.tsx",
57+
"jsName": "ArkEnvDemo"
58+
}
59+
],
60+
"integrations": [
61+
{
62+
"type": "vite-plugin",
63+
"import": "import arkenv from '@arkenv/vite-plugin'",
64+
"code": "arkenv()"
65+
}
66+
],
67+
"packageTemplate": "<%\n const arkenvOption = (typeof addOnOption !== 'undefined' && (\n addOnOption['arkenv'] ||\n Object.entries(addOnOption).find(([k]) => k.includes('arkenv') || k.includes('info.json'))?.[1]\n )) || {};\n const validator = arkenvOption.validator || 'arktype';\n-%>\n{\n \"dependencies\": {\n<% if (validator === 'zod') { -%>\n \"@arkenv/standard\": \"^1.0.0-alpha.1\",\n \"zod\": \"^3.24.2\"\n<% } else if (validator === 'valibot') { -%>\n \"@arkenv/standard\": \"^1.0.0-alpha.1\",\n \"valibot\": \"^1.0.0\"\n<% } else { -%>\n \"@arkenv/core\": \"^1.0.0-alpha.1\",\n \"arktype\": \"^2.2.0\"\n<% } -%>\n },\n \"devDependencies\": {\n \"@arkenv/vite-plugin\": \"^1.0.0-alpha.1\"\n }\n}\n",
68+
"files": {
69+
"_dot_env.example": "# Port for the dev/preview server\nPORT=3000\n\n# Public API URL (inlined into client bundle)\nVITE_API_URL=https://api.example.com\n\n# Server-only database connection URL (protected from client access)\nDATABASE_URL=postgresql://postgres:postgres@localhost:5432/db\n\n# Environment mode\nNODE_ENV=development\n",
70+
"src/env.ts.ejs": "<%\n const arkenvOption = (typeof addOnOption !== 'undefined' && (\n addOnOption['arkenv'] ||\n Object.entries(addOnOption).find(([k]) => k.includes('arkenv') || k.includes('info.json'))?.[1]\n )) || {};\n const validator = arkenvOption.validator || 'arktype';\n-%>\n<% if (validator === 'zod') { -%>\nimport arkenv from \"@arkenv/standard\";\nimport { z } from \"zod\";\n\nexport const env = arkenv({\n PORT: z.coerce.number().int().min(1).max(65535).default(3000),\n VITE_API_URL: z.string().url().default(\"https://api.example.com\"),\n DATABASE_URL: z.string().url().default(\"postgresql://postgres:postgres@localhost:5432/db\"),\n NODE_ENV: z.enum([\"development\", \"production\", \"test\"]).default(\"development\"),\n});\n<% } else if (validator === 'valibot') { -%>\nimport arkenv from \"@arkenv/standard\";\nimport * as v from \"valibot\";\n\nexport const env = arkenv({\n PORT: v.optional(v.pipe(v.unknown(), v.transform(Number), v.integer()), 3000),\n VITE_API_URL: v.optional(v.pipe(v.string(), v.url()), \"https://api.example.com\"),\n DATABASE_URL: v.optional(v.pipe(v.string(), v.url()), \"postgresql://postgres:postgres@localhost:5432/db\"),\n NODE_ENV: v.optional(v.picklist([\"development\", \"production\", \"test\"]), \"development\"),\n});\n<% } else { -%>\nimport arkenv from \"@arkenv/core\";\n\nexport const env = arkenv({\n PORT: \"number.port = 3000\",\n VITE_API_URL: \"string = 'https://api.example.com'\",\n DATABASE_URL: \"string = 'postgresql://postgres:postgres@localhost:5432/db'\",\n NODE_ENV: \"'development' | 'production' | 'test' = 'development'\",\n});\n<% } -%>\n",
71+
"src/routes/demo/arkenv.tsx.ejs": "<%\n const arkenvOption = (typeof addOnOption !== 'undefined' && (\n addOnOption['arkenv'] ||\n Object.entries(addOnOption).find(([k]) => k.includes('arkenv') || k.includes('info.json'))?.[1]\n )) || {};\n const includeDemo = typeof includeExamples !== 'undefined' ? includeExamples : true;\n if (arkenvOption.demo === 'false' || !includeDemo) {\n if (typeof ignoreFile === 'function') {\n ignoreFile();\n return;\n }\n }\n-%>\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { createServerFn } from \"@tanstack/react-start\";\nimport { useState } from \"react\";\nimport { env } from \"../../env\";\n\nconst getDatabaseConfig = createServerFn({ method: \"GET\" }).handler(() => {\n\t// Server-only key: safely read on the server during SSR / RPC\n\ttry {\n\t\tconst url = new URL(env.DATABASE_URL);\n\t\treturn { host: url.host, protocol: url.protocol };\n\t} catch {\n\t\treturn { host: \"localhost:5432\", protocol: \"postgresql:\" };\n\t}\n});\n\nexport const Route = createFileRoute(\"/demo/arkenv\")({\n\tcomponent: ArkEnvDemo,\n\tloader: () => getDatabaseConfig(),\n});\n\nfunction LeakedSecret() {\n\t// Accessing server-only DATABASE_URL directly on the client throws at runtime\n\treturn <p>Server key leaked: {env.DATABASE_URL}</p>;\n}\n\nfunction ArkEnvDemo() {\n\tconst dbConfig = Route.useLoaderData();\n\tconst [attemptLeak, setAttemptLeak] = useState(false);\n\n\treturn (\n\t\t<div className=\"p-6 max-w-xl mx-auto space-y-4 font-sans\">\n\t\t\t<h1 className=\"text-2xl font-bold\">ArkEnv Demo</h1>\n\t\t\t<p className=\"text-sm text-gray-600\">\n\t\t\t\tTypesafe environment variables with build-time validation and runtime\n\t\t\t\tleak protection.\n\t\t\t</p>\n\n\t\t\t<div className=\"p-4 bg-gray-100 dark:bg-gray-800 rounded-md space-y-2\">\n\t\t\t\t<h2 className=\"font-semibold text-lg\">Public Client Variables</h2>\n\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-300\">\n\t\t\t\t\tInlined safely into client bundles:\n\t\t\t\t</p>\n\t\t\t\t<code className=\"block p-2 bg-white dark:bg-black rounded border text-xs font-mono\">\n\t\t\t\t\tenv.VITE_API_URL: {env.VITE_API_URL}\n\t\t\t\t</code>\n\t\t\t</div>\n\n\t\t\t<div className=\"p-4 bg-gray-100 dark:bg-gray-800 rounded-md space-y-2\">\n\t\t\t\t<h2 className=\"font-semibold text-lg\">Server-Only Variables</h2>\n\t\t\t\t<p className=\"text-sm text-gray-600 dark:text-gray-300\">\n\t\t\t\t\tAccessible inside createServerFn handlers:\n\t\t\t\t</p>\n\t\t\t\t<code className=\"block p-2 bg-white dark:bg-black rounded border text-xs font-mono\">\n\t\t\t\t\tDatabase Host: {dbConfig.host} ({dbConfig.protocol})\n\t\t\t\t</code>\n\t\t\t</div>\n\n\t\t\t<div className=\"p-4 bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 rounded-md space-y-2\">\n\t\t\t\t<h2 className=\"font-semibold text-red-800 dark:text-red-300 text-lg\">\n\t\t\t\t\tSecret Leak Protection\n\t\t\t\t</h2>\n\t\t\t\t<p className=\"text-sm text-red-700 dark:text-red-400\">\n\t\t\t\t\tClicking the button below attempts to access the server secret{\" \"}\n\t\t\t\t\t<code>env.DATABASE_URL</code> on the client, which ArkEnv blocks:\n\t\t\t\t</p>\n\t\t\t\t{attemptLeak ? (\n\t\t\t\t\t<LeakedSecret />\n\t\t\t\t) : (\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclassName=\"px-3 py-1.5 bg-red-600 text-white rounded text-sm hover:bg-red-700 cursor-pointer\"\n\t\t\t\t\t\tonClick={() => setAttemptLeak(true)}\n\t\t\t\t\t>\n\t\t\t\t\t\tAttempt client access to DATABASE_URL\n\t\t\t\t\t</button>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
72+
},
73+
"deletedFiles": []
74+
}

apps/www/public/tanstack/assets/src/routes/demo/arkenv.tsx renamed to apps/www/public/tanstack/assets/src/routes/demo/arkenv.tsx.ejs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
<%
2+
const arkenvOption = (typeof addOnOption !== 'undefined' && (
3+
addOnOption['arkenv'] ||
4+
Object.entries(addOnOption).find(([k]) => k.includes('arkenv') || k.includes('info.json'))?.[1]
5+
)) || {};
6+
const includeDemo = typeof includeExamples !== 'undefined' ? includeExamples : true;
7+
if (arkenvOption.demo === 'false' || !includeDemo) {
8+
if (typeof ignoreFile === 'function') {
9+
ignoreFile();
10+
return;
11+
}
12+
}
13+
-%>
114
import { createFileRoute } from "@tanstack/react-router";
215
import { createServerFn } from "@tanstack/react-start";
316
import { useState } from "react";

0 commit comments

Comments
 (0)