@agjs/tsforge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tsforge.js +2 -0
- package/package.json +35 -0
- package/src/agent/agent.constants.ts +382 -0
- package/src/agent/agent.types.ts +34 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/model-agent.ts +297 -0
- package/src/agent/tool-repair.ts +194 -0
- package/src/agent/tools.ts +190 -0
- package/src/browser/checks.ts +96 -0
- package/src/browser/index.ts +8 -0
- package/src/browser/oracle.ts +303 -0
- package/src/classify.ts +48 -0
- package/src/cli.ts +1333 -0
- package/src/config/config.constants.ts +9 -0
- package/src/config/flags.ts +32 -0
- package/src/config/index.ts +8 -0
- package/src/config/tsforge-config.ts +301 -0
- package/src/constitution/baseline.ts +257 -0
- package/src/detect-gate.ts +498 -0
- package/src/eval/eval.types.ts +36 -0
- package/src/eval/index.ts +3 -0
- package/src/eval/judge.ts +62 -0
- package/src/eval/score.ts +39 -0
- package/src/files/create.ts +22 -0
- package/src/files/edit.ts +193 -0
- package/src/files/files.constants.ts +11 -0
- package/src/files/files.types.ts +81 -0
- package/src/files/hashline-format.ts +110 -0
- package/src/files/hashline.ts +689 -0
- package/src/files/index.ts +19 -0
- package/src/index.ts +8 -0
- package/src/inference/index.ts +6 -0
- package/src/inference/inference.constants.ts +34 -0
- package/src/inference/inference.types.ts +123 -0
- package/src/inference/openai-compatible.ts +113 -0
- package/src/inference/stream-guard.ts +161 -0
- package/src/inference/stream.ts +370 -0
- package/src/inference/transport.ts +78 -0
- package/src/inference/wire.ts +0 -0
- package/src/lib/fs/fs.ts +126 -0
- package/src/lib/fs/fs.types.ts +5 -0
- package/src/lib/fs/index.ts +3 -0
- package/src/lib/fs/process.ts +146 -0
- package/src/lib/guards/guards.ts +9 -0
- package/src/lib/guards/index.ts +1 -0
- package/src/lib/json/index.ts +1 -0
- package/src/lib/json/json.ts +12 -0
- package/src/lib/scope/index.ts +2 -0
- package/src/lib/scope/scope.constants.ts +3 -0
- package/src/lib/scope/scope.ts +40 -0
- package/src/loop/astgrep-fix.ts +228 -0
- package/src/loop/feedback/feedback.ts +138 -0
- package/src/loop/feedback/index.ts +8 -0
- package/src/loop/feedback/meta-rule-docs.ts +41 -0
- package/src/loop/feedback/meta-rule-feedback.ts +61 -0
- package/src/loop/feedback/rule-docs.generated.json +112 -0
- package/src/loop/feedback/rule-docs.ts +342 -0
- package/src/loop/index.ts +19 -0
- package/src/loop/loop.constants.ts +68 -0
- package/src/loop/loop.types.ts +99 -0
- package/src/loop/prompt/index.ts +2 -0
- package/src/loop/prompt/project-map.ts +69 -0
- package/src/loop/prompt/prompt.ts +107 -0
- package/src/loop/quality.ts +174 -0
- package/src/loop/rule-docs.generated.json +367 -0
- package/src/loop/run-spec.ts +88 -0
- package/src/loop/run.ts +400 -0
- package/src/loop/session.ts +1410 -0
- package/src/loop/tools/add-dependency.ts +71 -0
- package/src/loop/tools/condense.ts +498 -0
- package/src/loop/tools/edit-hashline.ts +80 -0
- package/src/loop/tools/execute-tool.ts +80 -0
- package/src/loop/tools/file-ops.ts +323 -0
- package/src/loop/tools/index.ts +2 -0
- package/src/loop/tools/lsp-ops.ts +222 -0
- package/src/loop/tools/scaffold-routes.ts +68 -0
- package/src/loop/tools/scaffold-ui.ts +62 -0
- package/src/loop/tools/scaffold-web.ts +35 -0
- package/src/loop/tools/tool-context.ts +126 -0
- package/src/loop/ttsr-defaults.ts +53 -0
- package/src/loop/ttsr.ts +322 -0
- package/src/loop/turn.ts +856 -0
- package/src/lsp/index.ts +2 -0
- package/src/lsp/lsp.types.ts +56 -0
- package/src/lsp/service.ts +500 -0
- package/src/meta-rules/context.ts +195 -0
- package/src/meta-rules/index.ts +9 -0
- package/src/meta-rules/meta-rules.types.ts +47 -0
- package/src/meta-rules/parsers/package-json-parser.ts +51 -0
- package/src/meta-rules/registry.ts +37 -0
- package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
- package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
- package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
- package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
- package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
- package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
- package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
- package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
- package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
- package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
- package/src/meta-rules/runner.ts +64 -0
- package/src/models-config.ts +196 -0
- package/src/render/ansi.ts +289 -0
- package/src/render/banner.ts +113 -0
- package/src/render/box.ts +134 -0
- package/src/render/index.ts +7 -0
- package/src/render/markdown.ts +123 -0
- package/src/render/render.types.ts +21 -0
- package/src/render/stream-markdown.ts +128 -0
- package/src/render/style.ts +26 -0
- package/src/rule-packs/bullmq/index.ts +39 -0
- package/src/rule-packs/bullmq/rules/index.ts +7 -0
- package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
- package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
- package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
- package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
- package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
- package/src/rule-packs/bullmq/utils.ts +334 -0
- package/src/rule-packs/code-flow/index.ts +25 -0
- package/src/rule-packs/code-flow/rules/index.ts +3 -0
- package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
- package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
- package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
- package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
- package/src/rule-packs/comment-hygiene/index.ts +25 -0
- package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
- package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
- package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
- package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
- package/src/rule-packs/create-rule.ts +9 -0
- package/src/rule-packs/drizzle/index.ts +41 -0
- package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
- package/src/rule-packs/drizzle/rules/index.ts +8 -0
- package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
- package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
- package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
- package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
- package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
- package/src/rule-packs/drizzle/utils.ts +115 -0
- package/src/rule-packs/elysia/index.ts +43 -0
- package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
- package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
- package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
- package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
- package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
- package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
- package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
- package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
- package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
- package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
- package/src/rule-packs/env-access/index.ts +23 -0
- package/src/rule-packs/env-access/rules/index.ts +2 -0
- package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
- package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
- package/src/rule-packs/i18n-keys/index.ts +19 -0
- package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
- package/src/rule-packs/index.ts +139 -0
- package/src/rule-packs/jwt-cookies/index.ts +25 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
- package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
- package/src/rule-packs/jwt-cookies/utils.ts +188 -0
- package/src/rule-packs/oauth-security/index.ts +25 -0
- package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
- package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
- package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
- package/src/rule-packs/oauth-security/utils.ts +127 -0
- package/src/rule-packs/react-component-architecture/index.ts +35 -0
- package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
- package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
- package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
- package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
- package/src/rule-packs/react-component-architecture/utils.ts +47 -0
- package/src/rule-packs/rule-packs.types.ts +18 -0
- package/src/rule-packs/structured-logging/index.ts +26 -0
- package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
- package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
- package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
- package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
- package/src/rule-packs/tanstack-query/index.ts +20 -0
- package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
- package/src/rule-packs/test-conventions/index.ts +23 -0
- package/src/rule-packs/test-conventions/rules/index.ts +2 -0
- package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
- package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
- package/src/rule-packs/utils.ts +142 -0
- package/src/session-store.ts +359 -0
- package/src/spec/generate-tests.ts +213 -0
- package/src/spec/index.ts +5 -0
- package/src/spec/parse.ts +152 -0
- package/src/spec/review-tests.ts +162 -0
- package/src/spec/spec.constants.ts +13 -0
- package/src/spec/spec.types.ts +79 -0
- package/src/stack-detection/detect.ts +246 -0
- package/src/stack-detection/index.ts +3 -0
- package/src/stack-detection/packs.ts +174 -0
- package/src/stack-detection/stack-detection.types.ts +47 -0
- package/src/validate/accept.ts +49 -0
- package/src/validate/errors.ts +35 -0
- package/src/validate/index.ts +12 -0
- package/src/validate/parse.ts +148 -0
- package/src/validate/run-tests.ts +59 -0
- package/src/validate/validate.ts +40 -0
- package/src/validate/validate.types.ts +52 -0
- package/src/web-components.ts +638 -0
- package/src/web-coverage.ts +89 -0
- package/src/web-routes.ts +151 -0
- package/src/web-templates.ts +1011 -0
- package/strict.eslint.config.mjs +84 -0
- package/strict.web.eslint.config.mjs +185 -0
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opinionated, MODERN web scaffolds — the file sets tsforge lays down so the
|
|
3
|
+
* model fills a known-good, building project instead of improvising. Every stack
|
|
4
|
+
* is Vite-based (real bundler oracle), strict-TS, and proven green end-to-end
|
|
5
|
+
* (vite build + tsc strict + eslint + headless browser render) before shipping.
|
|
6
|
+
*
|
|
7
|
+
* react — the FULL kit: Vite + React 19 + Tailwind v4 + shadcn/ui (cn, base
|
|
8
|
+
* components, theme tokens) + TanStack Router (file-based, route-tree
|
|
9
|
+
* codegen) + TanStack Query. The opinionated default.
|
|
10
|
+
* vanilla — Vite + TypeScript + Tailwind, no UI framework. The neutral choice.
|
|
11
|
+
*
|
|
12
|
+
* shadcn components + the generated routeTree.gen.ts are VENDORED/GENERATED — not
|
|
13
|
+
* the model's output — so each stack lists `eslintIgnore` globs that exempt them
|
|
14
|
+
* from the bundled strict eslint (they're still type-checked by tsc + vite build).
|
|
15
|
+
*/
|
|
16
|
+
export type WebFramework = "react" | "vanilla";
|
|
17
|
+
|
|
18
|
+
export interface IWebTemplate {
|
|
19
|
+
/** Short label for the banner. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** Relative path → file content, written non-destructively at scaffold time. */
|
|
22
|
+
files: Record<string, string>;
|
|
23
|
+
/** eslint --ignore-pattern globs for vendored/generated code the model didn't write. */
|
|
24
|
+
eslintIgnore: string[];
|
|
25
|
+
/** System-prompt guidance describing the structure + conventions for this stack. */
|
|
26
|
+
guidance: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const VITE_ENV_DTS = `/// <reference types="vite/client" />
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
// Keep prettier (and so the gate's prettier --check) off vendored/generated code —
|
|
33
|
+
// same scope as eslintIgnore. The model's own files are the only ones formatted.
|
|
34
|
+
const PRETTIER_IGNORE = `node_modules
|
|
35
|
+
dist
|
|
36
|
+
src/components/ui
|
|
37
|
+
src/lib
|
|
38
|
+
*.gen.ts
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
// ─── react: the full kit ─────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const REACT_PACKAGE_JSON = `{
|
|
44
|
+
"name": "app",
|
|
45
|
+
"private": true,
|
|
46
|
+
"type": "module",
|
|
47
|
+
"scripts": {
|
|
48
|
+
"dev": "vite",
|
|
49
|
+
"build": "vite build",
|
|
50
|
+
"preview": "vite preview"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@dnd-kit/core": "^6.3.1",
|
|
54
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
55
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
56
|
+
"@faker-js/faker": "^9.5.0",
|
|
57
|
+
"@radix-ui/react-slot": "^1.1.1",
|
|
58
|
+
"@tanstack/react-query": "^5.62.0",
|
|
59
|
+
"@tanstack/react-router": "^1.95.0",
|
|
60
|
+
"class-variance-authority": "^0.7.1",
|
|
61
|
+
"clsx": "^2.1.1",
|
|
62
|
+
"lucide-react": "^0.469.0",
|
|
63
|
+
"react": "^19.0.0",
|
|
64
|
+
"react-dom": "^19.0.0",
|
|
65
|
+
"recharts": "^2.15.0",
|
|
66
|
+
"tailwind-merge": "^3.0.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
70
|
+
"@tanstack/router-plugin": "^1.95.0",
|
|
71
|
+
"@types/react": "^19.0.0",
|
|
72
|
+
"@types/react-dom": "^19.0.0",
|
|
73
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
74
|
+
"tailwindcss": "^4.0.0",
|
|
75
|
+
"tw-animate-css": "^1.0.0",
|
|
76
|
+
"typescript": "^5.7.0",
|
|
77
|
+
"vite": "^6.0.0",
|
|
78
|
+
"vite-tsconfig-paths": "^5.1.0"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const REACT_VITE_CONFIG = `import { defineConfig } from "vite";
|
|
84
|
+
import react from "@vitejs/plugin-react";
|
|
85
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
86
|
+
import tsconfigPaths from "vite-tsconfig-paths";
|
|
87
|
+
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
|
88
|
+
|
|
89
|
+
export default defineConfig({
|
|
90
|
+
plugins: [
|
|
91
|
+
tanstackRouter({ target: "react", autoCodeSplitting: true }),
|
|
92
|
+
react(),
|
|
93
|
+
tailwindcss(),
|
|
94
|
+
tsconfigPaths(),
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
const REACT_TSCONFIG = `{
|
|
100
|
+
"compilerOptions": {
|
|
101
|
+
"target": "ES2022",
|
|
102
|
+
"module": "ESNext",
|
|
103
|
+
"moduleResolution": "bundler",
|
|
104
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
105
|
+
"jsx": "react-jsx",
|
|
106
|
+
"strict": true,
|
|
107
|
+
"noUncheckedIndexedAccess": true,
|
|
108
|
+
"noImplicitOverride": true,
|
|
109
|
+
"noFallthroughCasesInSwitch": true,
|
|
110
|
+
"esModuleInterop": true,
|
|
111
|
+
"forceConsistentCasingInFileNames": true,
|
|
112
|
+
"skipLibCheck": true,
|
|
113
|
+
"noEmit": true,
|
|
114
|
+
"paths": { "@/*": ["./src/*"] }
|
|
115
|
+
},
|
|
116
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
117
|
+
"exclude": ["node_modules", "dist", "build"]
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
const REACT_HTML = `<!doctype html>
|
|
122
|
+
<html lang="en">
|
|
123
|
+
<head>
|
|
124
|
+
<meta charset="utf-8" />
|
|
125
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
126
|
+
<title>app</title>
|
|
127
|
+
</head>
|
|
128
|
+
<body>
|
|
129
|
+
<div id="root"></div>
|
|
130
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
131
|
+
</body>
|
|
132
|
+
</html>
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
const COMPONENTS_JSON = `{
|
|
136
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
137
|
+
"style": "new-york",
|
|
138
|
+
"rsc": false,
|
|
139
|
+
"tsx": true,
|
|
140
|
+
"tailwind": {
|
|
141
|
+
"config": "",
|
|
142
|
+
"css": "src/index.css",
|
|
143
|
+
"baseColor": "neutral",
|
|
144
|
+
"cssVariables": true,
|
|
145
|
+
"prefix": ""
|
|
146
|
+
},
|
|
147
|
+
"aliases": {
|
|
148
|
+
"components": "@/components",
|
|
149
|
+
"utils": "@/lib/utils",
|
|
150
|
+
"ui": "@/components/ui",
|
|
151
|
+
"lib": "@/lib",
|
|
152
|
+
"hooks": "@/hooks"
|
|
153
|
+
},
|
|
154
|
+
"iconLibrary": "lucide"
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
const CN_UTILS = `import { clsx, type ClassValue } from "clsx";
|
|
159
|
+
import { twMerge } from "tailwind-merge";
|
|
160
|
+
|
|
161
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
162
|
+
return twMerge(clsx(inputs));
|
|
163
|
+
}
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
const REACT_INDEX_CSS = `@import "tailwindcss";
|
|
167
|
+
@import "tw-animate-css";
|
|
168
|
+
|
|
169
|
+
@custom-variant dark (&:is(.dark *));
|
|
170
|
+
|
|
171
|
+
:root {
|
|
172
|
+
--radius: 0.625rem;
|
|
173
|
+
--background: oklch(1 0 0);
|
|
174
|
+
--foreground: oklch(0.145 0 0);
|
|
175
|
+
--card: oklch(1 0 0);
|
|
176
|
+
--card-foreground: oklch(0.145 0 0);
|
|
177
|
+
--popover: oklch(1 0 0);
|
|
178
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
179
|
+
--primary: oklch(0.205 0 0);
|
|
180
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
181
|
+
--secondary: oklch(0.97 0 0);
|
|
182
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
183
|
+
--muted: oklch(0.97 0 0);
|
|
184
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
185
|
+
--accent: oklch(0.97 0 0);
|
|
186
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
187
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
188
|
+
--border: oklch(0.922 0 0);
|
|
189
|
+
--input: oklch(0.922 0 0);
|
|
190
|
+
--ring: oklch(0.708 0 0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.dark {
|
|
194
|
+
--background: oklch(0.145 0 0);
|
|
195
|
+
--foreground: oklch(0.985 0 0);
|
|
196
|
+
--card: oklch(0.205 0 0);
|
|
197
|
+
--card-foreground: oklch(0.985 0 0);
|
|
198
|
+
--popover: oklch(0.205 0 0);
|
|
199
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
200
|
+
--primary: oklch(0.922 0 0);
|
|
201
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
202
|
+
--secondary: oklch(0.269 0 0);
|
|
203
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
204
|
+
--muted: oklch(0.269 0 0);
|
|
205
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
206
|
+
--accent: oklch(0.269 0 0);
|
|
207
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
208
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
209
|
+
--border: oklch(1 0 0 / 10%);
|
|
210
|
+
--input: oklch(1 0 0 / 15%);
|
|
211
|
+
--ring: oklch(0.556 0 0);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@theme inline {
|
|
215
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
216
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
217
|
+
--radius-lg: var(--radius);
|
|
218
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
219
|
+
--color-background: var(--background);
|
|
220
|
+
--color-foreground: var(--foreground);
|
|
221
|
+
--color-card: var(--card);
|
|
222
|
+
--color-card-foreground: var(--card-foreground);
|
|
223
|
+
--color-popover: var(--popover);
|
|
224
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
225
|
+
--color-primary: var(--primary);
|
|
226
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
227
|
+
--color-secondary: var(--secondary);
|
|
228
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
229
|
+
--color-muted: var(--muted);
|
|
230
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
231
|
+
--color-accent: var(--accent);
|
|
232
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
233
|
+
--color-destructive: var(--destructive);
|
|
234
|
+
--color-border: var(--border);
|
|
235
|
+
--color-input: var(--input);
|
|
236
|
+
--color-ring: var(--ring);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@layer base {
|
|
240
|
+
* {
|
|
241
|
+
@apply border-border outline-ring/50;
|
|
242
|
+
}
|
|
243
|
+
body {
|
|
244
|
+
@apply bg-background text-foreground;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
`;
|
|
248
|
+
|
|
249
|
+
const BUTTON_TSX = `import { Slot } from "@radix-ui/react-slot";
|
|
250
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
251
|
+
import type { ComponentProps } from "react";
|
|
252
|
+
|
|
253
|
+
import { cn } from "@/lib/utils";
|
|
254
|
+
|
|
255
|
+
const buttonVariants = cva(
|
|
256
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
257
|
+
{
|
|
258
|
+
variants: {
|
|
259
|
+
variant: {
|
|
260
|
+
default:
|
|
261
|
+
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
262
|
+
destructive:
|
|
263
|
+
"bg-destructive text-white shadow-xs hover:bg-destructive/90",
|
|
264
|
+
outline:
|
|
265
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
|
|
266
|
+
secondary:
|
|
267
|
+
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
268
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
269
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
270
|
+
},
|
|
271
|
+
size: {
|
|
272
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
273
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
274
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
275
|
+
icon: "size-9",
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
defaultVariants: {
|
|
279
|
+
variant: "default",
|
|
280
|
+
size: "default",
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
function Button({
|
|
286
|
+
className,
|
|
287
|
+
variant,
|
|
288
|
+
size,
|
|
289
|
+
asChild = false,
|
|
290
|
+
...props
|
|
291
|
+
}: ComponentProps<"button"> &
|
|
292
|
+
VariantProps<typeof buttonVariants> & {
|
|
293
|
+
asChild?: boolean;
|
|
294
|
+
}) {
|
|
295
|
+
const Comp = asChild ? Slot : "button";
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<Comp
|
|
299
|
+
data-slot="button"
|
|
300
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
301
|
+
{...props}
|
|
302
|
+
/>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export { Button, buttonVariants };
|
|
307
|
+
`;
|
|
308
|
+
|
|
309
|
+
const ROOT_ROUTE_TSX = `import { createRootRoute, Outlet } from "@tanstack/react-router";
|
|
310
|
+
|
|
311
|
+
export const Route = createRootRoute({
|
|
312
|
+
component: () => <Outlet />,
|
|
313
|
+
});
|
|
314
|
+
`;
|
|
315
|
+
|
|
316
|
+
const INDEX_ROUTE_TSX = `import { createFileRoute } from "@tanstack/react-router";
|
|
317
|
+
|
|
318
|
+
import { Button } from "@/components/ui/button";
|
|
319
|
+
|
|
320
|
+
export const Route = createFileRoute("/")({
|
|
321
|
+
component: Home,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
function Home() {
|
|
325
|
+
return (
|
|
326
|
+
<main className="flex min-h-screen flex-col items-center justify-center gap-6 bg-background text-foreground">
|
|
327
|
+
<h1 className="text-3xl font-bold">app</h1>
|
|
328
|
+
<Button>Get started</Button>
|
|
329
|
+
</main>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
`;
|
|
333
|
+
|
|
334
|
+
const REACT_MAIN_TSX = `import { StrictMode } from "react";
|
|
335
|
+
import { createRoot } from "react-dom/client";
|
|
336
|
+
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
|
337
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
338
|
+
|
|
339
|
+
import { routeTree } from "./routeTree.gen";
|
|
340
|
+
import "./index.css";
|
|
341
|
+
|
|
342
|
+
const router = createRouter({ routeTree });
|
|
343
|
+
|
|
344
|
+
declare module "@tanstack/react-router" {
|
|
345
|
+
interface Register {
|
|
346
|
+
router: typeof router;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const queryClient = new QueryClient();
|
|
351
|
+
|
|
352
|
+
const rootElement = document.getElementById("root");
|
|
353
|
+
|
|
354
|
+
if (rootElement === null) {
|
|
355
|
+
throw new Error("missing #root element");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
createRoot(rootElement).render(
|
|
359
|
+
<StrictMode>
|
|
360
|
+
<QueryClientProvider client={queryClient}>
|
|
361
|
+
<RouterProvider router={router} />
|
|
362
|
+
</QueryClientProvider>
|
|
363
|
+
</StrictMode>
|
|
364
|
+
);
|
|
365
|
+
`;
|
|
366
|
+
|
|
367
|
+
// A STUB of TanStack Router's generated route tree, registering only the stock "/"
|
|
368
|
+
// route — shipped so the scaffold TYPECHECKS from turn 1. Without it, the tsc-only
|
|
369
|
+
// stages (the design-phase type-gate and the incremental check, which do NOT run a
|
|
370
|
+
// Vite build) can't resolve \`./routeTree.gen\`, so the stock main.tsx/index.tsx
|
|
371
|
+
// throw two unfixable errors ("Cannot find module './routeTree.gen'" + "createFileRoute
|
|
372
|
+
// ('/') not assignable to 'undefined'") that pin every interim check at a 2-error
|
|
373
|
+
// floor — the model never sees 0 errors, never settles, and burns its whole turn
|
|
374
|
+
// budget chasing them (observed on large multi-route apps stuck on exactly this).
|
|
375
|
+
// The Vite build overwrites this with the real tree (all routes) on every gate run.
|
|
376
|
+
// @ts-nocheck + eslint-disabled (matches what the generator emits; *.gen.ts is ignored).
|
|
377
|
+
const ROUTE_TREE_GEN = `/* eslint-disable */
|
|
378
|
+
|
|
379
|
+
// @ts-nocheck
|
|
380
|
+
|
|
381
|
+
// This file was automatically generated by TanStack Router.
|
|
382
|
+
// You should NOT make any changes in this file as it will be overwritten.
|
|
383
|
+
|
|
384
|
+
import { Route as rootRouteImport } from './routes/__root'
|
|
385
|
+
import { Route as IndexRouteImport } from './routes/index'
|
|
386
|
+
|
|
387
|
+
const IndexRoute = IndexRouteImport.update({
|
|
388
|
+
id: '/',
|
|
389
|
+
path: '/',
|
|
390
|
+
getParentRoute: () => rootRouteImport,
|
|
391
|
+
} as any)
|
|
392
|
+
|
|
393
|
+
export interface FileRoutesByFullPath {
|
|
394
|
+
'/': typeof IndexRoute
|
|
395
|
+
}
|
|
396
|
+
export interface FileRoutesByTo {
|
|
397
|
+
'/': typeof IndexRoute
|
|
398
|
+
}
|
|
399
|
+
export interface FileRoutesById {
|
|
400
|
+
__root__: typeof rootRouteImport
|
|
401
|
+
'/': typeof IndexRoute
|
|
402
|
+
}
|
|
403
|
+
export interface FileRouteTypes {
|
|
404
|
+
fileRoutesByFullPath: FileRoutesByFullPath
|
|
405
|
+
fullPaths: '/'
|
|
406
|
+
fileRoutesByTo: FileRoutesByTo
|
|
407
|
+
to: '/'
|
|
408
|
+
id: '__root__' | '/'
|
|
409
|
+
fileRoutesById: FileRoutesById
|
|
410
|
+
}
|
|
411
|
+
export interface RootRouteChildren {
|
|
412
|
+
IndexRoute: typeof IndexRoute
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
declare module '@tanstack/react-router' {
|
|
416
|
+
interface FileRoutesByPath {
|
|
417
|
+
'/': {
|
|
418
|
+
id: '/'
|
|
419
|
+
path: '/'
|
|
420
|
+
fullPath: '/'
|
|
421
|
+
preLoaderRoute: typeof IndexRouteImport
|
|
422
|
+
parentRoute: typeof rootRouteImport
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const rootRouteChildren: RootRouteChildren = {
|
|
428
|
+
IndexRoute: IndexRoute,
|
|
429
|
+
}
|
|
430
|
+
export const routeTree = rootRouteImport
|
|
431
|
+
._addFileChildren(rootRouteChildren)
|
|
432
|
+
._addFileTypes<FileRouteTypes>()
|
|
433
|
+
`;
|
|
434
|
+
|
|
435
|
+
const REACT_GUIDANCE = [
|
|
436
|
+
"This is a Vite + React 19 + TypeScript + Tailwind v4 app with shadcn/ui and",
|
|
437
|
+
"TanStack (Router + Query) — ALREADY scaffolded and its dependencies INSTALLED.",
|
|
438
|
+
"Build the app by adding/editing files under src/. Do NOT touch vite.config.ts,",
|
|
439
|
+
"index.html, tsconfig.json, components.json, or the build setup.",
|
|
440
|
+
"FILE LAYOUT — boringstack, ONE thing per file (judged on this, not just",
|
|
441
|
+
"compiling). NEVER put more than one component in a file, and NEVER inline types",
|
|
442
|
+
"or constants in a component file:",
|
|
443
|
+
" • Organize by DOMAIN: each feature/area of the app is its own folder under",
|
|
444
|
+
" src/, named for the domain it holds. Inside a domain folder <d>/ put:",
|
|
445
|
+
" – <d>.types.ts — that domain's interfaces/types (I-prefixed)",
|
|
446
|
+
" – <d>.constants.ts — its `as const` registries/config",
|
|
447
|
+
" – one component per .tsx file (PascalCase, named for the component)",
|
|
448
|
+
" – (NO <d>.hooks.ts query wrapper — the SDK's useCollection IS the data hook;",
|
|
449
|
+
" add a hook file ONLY for genuine derived/computed state, never to fetch)",
|
|
450
|
+
" – index.ts — a barrel re-exporting the folder's public surface",
|
|
451
|
+
" • Types ALWAYS live in a `.types.ts`, constants in a `.constants.ts` — never",
|
|
452
|
+
" inline, and never one mega src/types.ts for the whole app. Types shared",
|
|
453
|
+
" across domains → src/shared/shared.types.ts.",
|
|
454
|
+
" • NO RUNTIME VALIDATION / PARSING — there is NO backend, network, or uploaded",
|
|
455
|
+
" data here; EVERY value originates from your own typed code + seed, so TypeScript",
|
|
456
|
+
" has already proven its shape. The TYPE SYSTEM is the only validation. NEVER create",
|
|
457
|
+
" a `*.validators.ts`; NEVER write a `parse<X>` / `validate<X>` / `is<X>` function",
|
|
458
|
+
" that takes `unknown` or `Record<string, unknown>` and checks fields with `typeof`",
|
|
459
|
+
" / `in` — that is dead ceremony for data the compiler already guarantees, and it is",
|
|
460
|
+
" REJECTED. Instead: type the value correctly at its source and use it directly; for",
|
|
461
|
+
" a literal use `x satisfies IType`; narrow a discriminated union with a plain",
|
|
462
|
+
" `switch (x.kind)`. If you typed it right, there is nothing to validate.",
|
|
463
|
+
" • ROUTES ARE THE APP — every PAGE the user asked for must be a real route, and a",
|
|
464
|
+
" component never mounted in a route is DEAD CODE. CREATE THEM ALL AT ONCE with the",
|
|
465
|
+
" `scaffold_routes` tool: call it ONCE (right after your types + services) listing",
|
|
466
|
+
" EVERY page — list pages, detail pages ($param, e.g. /accounts/$accountId), and",
|
|
467
|
+
" create/edit pages (e.g. /deals/create). It writes every src/routes/*.tsx stub AND",
|
|
468
|
+
" the real home at src/routes/index.tsx and regenerates the route tree, so the whole",
|
|
469
|
+
" app navigates and every <Link to>/navigate target type-checks from that point on.",
|
|
470
|
+
" NEVER hand-write or hand-edit route files or createFileRoute paths. THEN fill each",
|
|
471
|
+
" route's placeholder component with the real UI, ONE feature at a time.",
|
|
472
|
+
" – shadcn/ui primitives stay in @/components/ui (Button exists; add more there",
|
|
473
|
+
" following cva + cn() + tokens).",
|
|
474
|
+
" • src/routeTree.gen.ts is AUTO-GENERATED by the Vite build from your route",
|
|
475
|
+
" files — NEVER create or edit it, and never try to fix it. If you see",
|
|
476
|
+
" `Cannot find module './routeTree.gen'` or `createFileRoute('/...')` →",
|
|
477
|
+
" `not assignable to parameter of type 'undefined'`, that is NOT a routeTree",
|
|
478
|
+
" problem: it means one of your src/routes/*.tsx files is malformed (bad",
|
|
479
|
+
" createRootRoute/createFileRoute call, wrong path string, or a syntax/type",
|
|
480
|
+
" error). FIX THE ROUTE FILE — the generated tree then resolves on the next",
|
|
481
|
+
" build. Every route file must `export const Route = createFileRoute('/path')`.",
|
|
482
|
+
" • LINKING to a DYNAMIC route — <Link>/navigate are TYPED against the route tree.",
|
|
483
|
+
" For a param route (src/routes/profile.$handle.tsx) pass the param SEPARATELY:",
|
|
484
|
+
' `<Link to="/profile/$handle" params={{ handle }}>` (and',
|
|
485
|
+
' `navigate({ to: "/tweet/$tweetId", params: { tweetId } })`). NEVER interpolate',
|
|
486
|
+
" the value into `to` — an interpolated template-literal string is NOT assignable",
|
|
487
|
+
" to the typed route union (TS2322). `to` is ALWAYS the static $param pattern; the",
|
|
488
|
+
" runtime value ALWAYS goes in `params`. The `params` KEY must match the route's",
|
|
489
|
+
" $segment EXACTLY (route /card/$cardId → params={{ cardId }}, not {{ id }}), and",
|
|
490
|
+
" `to` must name the route that HAS that param. A TS2353 `'X' does not exist in",
|
|
491
|
+
" type 'ParamsReducerFn<…>'` means your `to`/`params` disagree — fix `to` to the",
|
|
492
|
+
" right $param route (it is NOT a routeTree problem), do not restructure params.",
|
|
493
|
+
" • EVERY <Link to=…> / navigate({ to }) TARGET MUST BE A ROUTE FILE THAT EXISTS.",
|
|
494
|
+
" The typed route union is built ONLY from your src/routes/*.tsx files. An error",
|
|
495
|
+
" `Type '\"/x/create\"' is not assignable to '\"/\" | …'` (or `… not assignable to",
|
|
496
|
+
" parameter of type 'keyof FileRoutesByPath'`) does NOT mean edit the link string —",
|
|
497
|
+
' it means NO src/routes/x.create.tsx exists. A "New/Create X" button needs a real',
|
|
498
|
+
" route: create src/routes/<name>.create.tsx (and an edit page =",
|
|
499
|
+
" src/routes/<name>.$<id>.edit.tsx) BEFORE you link to it — OR render the form inline",
|
|
500
|
+
" / in a dialog and don't navigate at all. NEVER leave a Link pointing at a route you",
|
|
501
|
+
" have not created: it is an UNFIXABLE type error (you'll burn the whole turn budget",
|
|
502
|
+
" re-editing the string) until that route file exists. Pick one — make the route, or",
|
|
503
|
+
" don't link. And do NOT invent router hooks (there is no useRouteContext): read route",
|
|
504
|
+
" data only via the Route object — Route.useParams() / Route.useSearch() / useNavigate().",
|
|
505
|
+
" • UI BUILDING BLOCKS — call `scaffold_ui` ONCE near the start to generate what",
|
|
506
|
+
" the app needs, themed to its vibe (minimal | warm | futuristic, from the",
|
|
507
|
+
" user's request). Two tiers, BOTH from @/components/ui: PRIMITIVES (button,",
|
|
508
|
+
" card, input, label, textarea, select, badge, separator, table) AND COMPOSITION",
|
|
509
|
+
" BLOCKS — app-shell (sidebar+nav layout, renders <Outlet/>), page-header, field",
|
|
510
|
+
" (label+control+error), form-actions, toolbar, empty-state. COMPOSE these:",
|
|
511
|
+
" layout = app-shell; a list view = page-header + toolbar + table + empty-state;",
|
|
512
|
+
" a form = field × N + form-actions. NEVER hand-roll a component OR this view",
|
|
513
|
+
" chrome — it wastes time and breaks theme coherence. Write only domain wiring.",
|
|
514
|
+
" • HARNESS SDK — USE IT, do NOT hand-roll the data layer (this is the biggest",
|
|
515
|
+
" speed+quality lever). A tested generic toolkit is already in src/lib/:",
|
|
516
|
+
" – createCollection(key, SEED) [from @/lib/collection] IS a domain's whole",
|
|
517
|
+
" service: typed async CRUD + Result + latency. <d>.service.ts is ONE line:",
|
|
518
|
+
" `export const items = createCollection('items', SEED_ITEMS)`.",
|
|
519
|
+
" – useCollection(collection) [from @/lib/use-collection] IS the data hook:",
|
|
520
|
+
" cached list, isLoading/error, and create/update/remove mutations WITH",
|
|
521
|
+
" optimistic updates + rollback. Do NOT write a <d>.hooks.ts query wrapper.",
|
|
522
|
+
" – useForm({ initial, validate, submit }) [from @/lib/use-form] IS form state:",
|
|
523
|
+
" values, per-field errors, async submit status. Do NOT hand-roll form state.",
|
|
524
|
+
" – SEED DATA — GENERATE with faker. NEVER hand-write literal arrays, and NEVER",
|
|
525
|
+
" index (no Array.from((_,i)=>…), no `arr[i]`, no `id:`item-${i}``, no `pickX(i)`",
|
|
526
|
+
" helpers). Indexing is the root of all the garbage: `arr[i]` is T | undefined",
|
|
527
|
+
" under noUncheckedIndexedAccess, which then forces `if (x===undefined) throw`",
|
|
528
|
+
" guards. There is NO index. Build the seed INDEX-FREE with two faker helpers:",
|
|
529
|
+
" • `faker.helpers.multiple(factory, { count: N })` — runs the factory N times,",
|
|
530
|
+
" no counter. The factory's RETURN-TYPE annotation is the whole validation.",
|
|
531
|
+
" • `faker.helpers.arrayElement(arr)` — picks one element, returns T (NOT T |",
|
|
532
|
+
" undefined), for fixed sets / string-literal unions / RELATED seed arrays.",
|
|
533
|
+
" `faker.string.uuid()` for ids. Pattern:",
|
|
534
|
+
" `faker.seed(42);`",
|
|
535
|
+
" `export const SEED_NOTIFS: readonly INotif[] = faker.helpers.multiple(`",
|
|
536
|
+
" ` (): INotif => ({ id: faker.string.uuid(), kind: faker.helpers.arrayElement(`",
|
|
537
|
+
" ` ['like','reply','follow']), from: faker.helpers.arrayElement(SEED_USERS),`",
|
|
538
|
+
" ` text: faker.lorem.sentence() }), { count: 15 });`",
|
|
539
|
+
" No `i`, no `arr[i % len]`, no undefined-guards, no parser — the type system +",
|
|
540
|
+
" the factory return type ARE the validation. There is NO backend/localStorage,",
|
|
541
|
+
" so NEVER write a runtime parser/validator (no parse<X>, no pObject, no `typeof`",
|
|
542
|
+
" guards, no `as` casts).",
|
|
543
|
+
" – Result/ok/err [from @/lib/result] for any fallible op.",
|
|
544
|
+
" – objectKeys(x)/objectEntries(x) [from @/lib/object] for TYPED keys of an",
|
|
545
|
+
" `as const` object. NEVER write `Object.keys(x) as (keyof typeof x)[]` —",
|
|
546
|
+
" the gate REJECTS that `as` cast; call objectKeys(x) instead.",
|
|
547
|
+
" – sortBy(rows, key, dir) [from @/lib/sort] for sortable tables/lists: pass",
|
|
548
|
+
" the column key as a plain STRING, get a sorted copy. NEVER write",
|
|
549
|
+
" `[...rows].sort((a, b) => a[sortKey] - b[sortKey])` — a string can't index",
|
|
550
|
+
" an entity (TS7053) and the `as` to silence it is banned. sortBy does it safely.",
|
|
551
|
+
" – LABEL / LOOKUP MAPS (status→label, kind→color, etc.) keyed by a union: TYPE",
|
|
552
|
+
" the map `Record<TheUnion, V>` so indexing by a value of that union is CAST-FREE.",
|
|
553
|
+
" e.g. `const KIND_LABEL: Record<ActivityKind, string> = { call: 'Call', … }` →",
|
|
554
|
+
" `KIND_LABEL[activity.kind]` needs NO cast. NEVER write the map as a bare",
|
|
555
|
+
" `as const` and then index it `MAP[key as keyof typeof MAP]` — that `as` is",
|
|
556
|
+
" REJECTED. The map's KEY type, not a cast, is what makes the lookup type-check.",
|
|
557
|
+
" So a domain is mostly: <d>.types.ts + a `satisfies`-typed SEED const + one-line",
|
|
558
|
+
" createCollection + components that call useCollection/useForm. Far fewer lines,",
|
|
559
|
+
" fewer bugs. Only write a custom service/hook if the SDK genuinely can't express",
|
|
560
|
+
" it. A QueryClientProvider is already wired in src/main.tsx.",
|
|
561
|
+
" • Style with Tailwind classes via className using theme tokens",
|
|
562
|
+
" (bg-background, text-foreground, border-border), not raw colors.",
|
|
563
|
+
" • Need charts? `recharts` is installed — import from 'recharts'. Need drag-and-",
|
|
564
|
+
" drop? `@dnd-kit/core` + `@dnd-kit/sortable` are installed. Do NOT add other",
|
|
565
|
+
" deps (only these + the scaffold's are installed; the build can't fetch more).",
|
|
566
|
+
"Imports use the @/ alias (e.g. @/<domain>/<domain>.types, @/components/ui/button).",
|
|
567
|
+
"Do NOT write a checks.json or any browser interaction test. The gate already",
|
|
568
|
+
"builds the app with Vite and renders it in a real browser, FAILING on any",
|
|
569
|
+
"runtime/console error — that IS the acceptance. Spend your effort on a working,",
|
|
570
|
+
"clean app that renders without errors, not on test assertions.",
|
|
571
|
+
"Do NOT run `tsc`, `eslint`, `vite build`, or the gate command yourself to check",
|
|
572
|
+
"your work — the harness type-checks each file the moment you write it and runs",
|
|
573
|
+
"the full gate automatically, feeding back the exact errors concisely. Running",
|
|
574
|
+
"them yourself just floods the conversation with output and wastes time. Just",
|
|
575
|
+
"write and fix files; the harness tells you what's wrong. In particular the gate",
|
|
576
|
+
"AUTO-FIXES all mechanical style for you — blank lines (padding-line-between-",
|
|
577
|
+
"statements), `if` braces (curly), string→template (prefer-template), import order,",
|
|
578
|
+
"and ALL formatting. NEVER re-run eslint/prettier or hand-fix those; they are not",
|
|
579
|
+
"your job and you can ignore them entirely even if you happen to see them.",
|
|
580
|
+
"WORK IN SMALL COHERENT SLICES — write ONE feature's few files per response (or a",
|
|
581
|
+
"single file if it's large), then END the turn and let the harness check before you",
|
|
582
|
+
"continue. This model is slow (~20 tokens/sec) and a response that runs past the time",
|
|
583
|
+
"limit is CUT OFF and ALL of its work is LOST — so NEVER dump the whole app (all",
|
|
584
|
+
"routes/components/seeds) into one giant response. A feature-sized slice finishes and",
|
|
585
|
+
"accumulates; a huge turn fails. Build feature by feature — it is fine to take many",
|
|
586
|
+
"turns. (Routes are the exception: create them ALL at once via `scaffold_routes`,",
|
|
587
|
+
"since stubs are tiny and the complete route set must exist before you wire links.)",
|
|
588
|
+
].join("\n");
|
|
589
|
+
|
|
590
|
+
// ─── vanilla: Vite + TS + Tailwind, no framework ─────────────────────────────
|
|
591
|
+
|
|
592
|
+
const VANILLA_PACKAGE_JSON = `{
|
|
593
|
+
"name": "app",
|
|
594
|
+
"private": true,
|
|
595
|
+
"type": "module",
|
|
596
|
+
"scripts": {
|
|
597
|
+
"dev": "vite",
|
|
598
|
+
"build": "vite build",
|
|
599
|
+
"preview": "vite preview"
|
|
600
|
+
},
|
|
601
|
+
"devDependencies": {
|
|
602
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
603
|
+
"tailwindcss": "^4.0.0",
|
|
604
|
+
"typescript": "^5.7.0",
|
|
605
|
+
"vite": "^6.0.0"
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
`;
|
|
609
|
+
|
|
610
|
+
const VANILLA_VITE_CONFIG = `import { defineConfig } from "vite";
|
|
611
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
612
|
+
|
|
613
|
+
export default defineConfig({
|
|
614
|
+
plugins: [tailwindcss()],
|
|
615
|
+
});
|
|
616
|
+
`;
|
|
617
|
+
|
|
618
|
+
const VANILLA_TSCONFIG = `{
|
|
619
|
+
"compilerOptions": {
|
|
620
|
+
"target": "ES2022",
|
|
621
|
+
"module": "ESNext",
|
|
622
|
+
"moduleResolution": "bundler",
|
|
623
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
624
|
+
"strict": true,
|
|
625
|
+
"noUncheckedIndexedAccess": true,
|
|
626
|
+
"noImplicitOverride": true,
|
|
627
|
+
"noFallthroughCasesInSwitch": true,
|
|
628
|
+
"esModuleInterop": true,
|
|
629
|
+
"forceConsistentCasingInFileNames": true,
|
|
630
|
+
"skipLibCheck": true,
|
|
631
|
+
"noEmit": true
|
|
632
|
+
},
|
|
633
|
+
"include": ["**/*.ts"],
|
|
634
|
+
"exclude": ["node_modules", "dist", "build"]
|
|
635
|
+
}
|
|
636
|
+
`;
|
|
637
|
+
|
|
638
|
+
const VANILLA_HTML = `<!doctype html>
|
|
639
|
+
<html lang="en">
|
|
640
|
+
<head>
|
|
641
|
+
<meta charset="utf-8" />
|
|
642
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
643
|
+
<title>app</title>
|
|
644
|
+
</head>
|
|
645
|
+
<body>
|
|
646
|
+
<div id="app"></div>
|
|
647
|
+
<script type="module" src="/src/main.ts"></script>
|
|
648
|
+
</body>
|
|
649
|
+
</html>
|
|
650
|
+
`;
|
|
651
|
+
|
|
652
|
+
const VANILLA_MAIN_TS = `import "./style.css";
|
|
653
|
+
|
|
654
|
+
const app = document.getElementById("app");
|
|
655
|
+
|
|
656
|
+
if (app === null) {
|
|
657
|
+
throw new Error("missing #app element");
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const heading = document.createElement("h1");
|
|
661
|
+
heading.className = "text-3xl font-bold";
|
|
662
|
+
heading.textContent = "app";
|
|
663
|
+
|
|
664
|
+
const main = document.createElement("main");
|
|
665
|
+
main.className =
|
|
666
|
+
"flex min-h-screen flex-col items-center justify-center gap-4 bg-gray-50 text-gray-900";
|
|
667
|
+
main.append(heading);
|
|
668
|
+
app.append(main);
|
|
669
|
+
`;
|
|
670
|
+
|
|
671
|
+
const VANILLA_CSS = `@import "tailwindcss";
|
|
672
|
+
`;
|
|
673
|
+
|
|
674
|
+
const VANILLA_GUIDANCE = [
|
|
675
|
+
"This is a Vite + TypeScript + Tailwind app (no UI framework) — ALREADY",
|
|
676
|
+
"scaffolded and its dependencies INSTALLED. The entry is src/main.ts; it imports",
|
|
677
|
+
"./style.css. Do NOT change vite.config.ts, index.html, or the build setup.",
|
|
678
|
+
"STRUCTURE IT PROPERLY — small, single-purpose modules, NOT one big file:",
|
|
679
|
+
" • src/types.ts — shared types/interfaces",
|
|
680
|
+
" • src/store.ts — state + business logic (pure, NO DOM access)",
|
|
681
|
+
" • src/view.ts — DOM rendering (build/update elements with createElement)",
|
|
682
|
+
" • src/main.ts — the entry that wires store + view + events into #app",
|
|
683
|
+
"Style with Tailwind utility classes. Keep functions small and single-purpose.",
|
|
684
|
+
"Do NOT write a checks.json or browser interaction test. The gate builds with",
|
|
685
|
+
"Vite and renders the app in a real browser, failing on any runtime error — that",
|
|
686
|
+
"is the acceptance. Focus on a working app that renders cleanly.",
|
|
687
|
+
].join("\n");
|
|
688
|
+
|
|
689
|
+
// ── Harness SDK primitives (vendored): the toolkit the model COMPOSES with
|
|
690
|
+
// instead of hand-writing per-domain services/hooks/forms/validators. One
|
|
691
|
+
// tested generic each — quality UP, tokens DOWN. *.gen-style vendored code.
|
|
692
|
+
const SDK_RESULT_TS = `// A Result type + constructors — the harness SDK's error spine. Fallible operations
|
|
693
|
+
// return Result instead of throwing, so callers handle failure explicitly.
|
|
694
|
+
export type Result<T, E> =
|
|
695
|
+
| { readonly ok: true; readonly value: T }
|
|
696
|
+
| { readonly ok: false; readonly error: E };
|
|
697
|
+
|
|
698
|
+
export function ok<T>(value: T): Result<T, never> {
|
|
699
|
+
return { ok: true, value };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export function err<E>(error: E): Result<never, E> {
|
|
703
|
+
return { ok: false, error };
|
|
704
|
+
}
|
|
705
|
+
`;
|
|
706
|
+
|
|
707
|
+
const SDK_OBJECT_TS = `// Typed Object helpers. This vendored, lint-exempt file is the ONE sanctioned home
|
|
708
|
+
// for the Object.keys cast. In YOUR code use objectKeys(x) instead of
|
|
709
|
+
// \`Object.keys(x) as (keyof typeof x)[]\` — the strict gate rejects that cast.
|
|
710
|
+
export function objectKeys<T extends object>(obj: T): (keyof T)[] {
|
|
711
|
+
return Object.keys(obj) as (keyof T)[];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export function objectEntries<T extends object>(obj: T): [keyof T, T[keyof T]][] {
|
|
715
|
+
return Object.entries(obj) as [keyof T, T[keyof T]][];
|
|
716
|
+
}
|
|
717
|
+
`;
|
|
718
|
+
|
|
719
|
+
const SDK_SORT_TS = `// Typed sorting for tables/lists. The strict gate rejects \`row[sortKey]\` when
|
|
720
|
+
// sortKey is a string (TS7053: a string can't index an entity), and bans the \`as\`
|
|
721
|
+
// cast that would silence it. This vendored, lint-exempt helper does the indexing
|
|
722
|
+
// safely ONCE here: pass the column key as a plain string and get a sorted COPY
|
|
723
|
+
// (handles readonly input → mutable output). In YOUR code:
|
|
724
|
+
// const rows = sortBy(transactions, sortKey, sortDir) // sortKey: string is fine
|
|
725
|
+
// — never write \`[...rows].sort((a, b) => a[sortKey] - b[sortKey])\`.
|
|
726
|
+
export function sortBy<T extends object>(
|
|
727
|
+
rows: readonly T[],
|
|
728
|
+
key: string,
|
|
729
|
+
direction: "asc" | "desc" = "asc"
|
|
730
|
+
): T[] {
|
|
731
|
+
const dir = direction === "asc" ? 1 : -1;
|
|
732
|
+
|
|
733
|
+
return [...rows].sort((a, b) => {
|
|
734
|
+
const av = (a as Record<string, unknown>)[key];
|
|
735
|
+
const bv = (b as Record<string, unknown>)[key];
|
|
736
|
+
|
|
737
|
+
if (typeof av === "number" && typeof bv === "number") {
|
|
738
|
+
return (av - bv) * dir;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return String(av ?? "").localeCompare(String(bv ?? "")) * dir;
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
`;
|
|
745
|
+
|
|
746
|
+
const SDK_COLLECTION_TS = `// createCollection — one tested generic that IS a domain's data layer: typed async
|
|
747
|
+
// CRUD over an in-memory store, seeded from a typed const, with simulated latency +
|
|
748
|
+
// a Result return. A domain's service becomes one line:
|
|
749
|
+
// export const items = createCollection("items", SEED_ITEMS)
|
|
750
|
+
import type { Result } from "@/lib/result";
|
|
751
|
+
import { err, ok } from "@/lib/result";
|
|
752
|
+
|
|
753
|
+
export interface IEntity {
|
|
754
|
+
readonly id: string;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export interface ICollection<T extends IEntity> {
|
|
758
|
+
readonly key: string;
|
|
759
|
+
list: () => Promise<Result<readonly T[], string>>;
|
|
760
|
+
get: (id: string) => Promise<Result<T, string>>;
|
|
761
|
+
create: (draft: Omit<T, "id">) => Promise<Result<T, string>>;
|
|
762
|
+
update: (id: string, patch: Partial<Omit<T, "id">>) => Promise<Result<T, string>>;
|
|
763
|
+
remove: (id: string) => Promise<Result<true, string>>;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const LATENCY_MS = 120;
|
|
767
|
+
|
|
768
|
+
function delay(): Promise<void> {
|
|
769
|
+
return new Promise((resolve) => setTimeout(resolve, LATENCY_MS));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export function createCollection<T extends IEntity>(
|
|
773
|
+
key: string,
|
|
774
|
+
seed: readonly T[]
|
|
775
|
+
): ICollection<T> {
|
|
776
|
+
const store = new Map<string, T>();
|
|
777
|
+
for (const entity of seed) {
|
|
778
|
+
store.set(entity.id, entity);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
let counter = store.size;
|
|
782
|
+
const nextId = (): string => {
|
|
783
|
+
counter += 1;
|
|
784
|
+
return key + "-" + String(counter);
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
return {
|
|
788
|
+
key,
|
|
789
|
+
async list() {
|
|
790
|
+
await delay();
|
|
791
|
+
return ok([...store.values()]);
|
|
792
|
+
},
|
|
793
|
+
async get(id) {
|
|
794
|
+
await delay();
|
|
795
|
+
const found = store.get(id);
|
|
796
|
+
return found === undefined ? err(key + " " + id + " not found") : ok(found);
|
|
797
|
+
},
|
|
798
|
+
async create(draft) {
|
|
799
|
+
await delay();
|
|
800
|
+
const entity = { ...draft, id: nextId() } as T;
|
|
801
|
+
store.set(entity.id, entity);
|
|
802
|
+
return ok(entity);
|
|
803
|
+
},
|
|
804
|
+
async update(id, patch) {
|
|
805
|
+
await delay();
|
|
806
|
+
const current = store.get(id);
|
|
807
|
+
if (current === undefined) {
|
|
808
|
+
return err(key + " " + id + " not found");
|
|
809
|
+
}
|
|
810
|
+
const updated = { ...current, ...patch };
|
|
811
|
+
store.set(id, updated);
|
|
812
|
+
return ok(updated);
|
|
813
|
+
},
|
|
814
|
+
async remove(id) {
|
|
815
|
+
await delay();
|
|
816
|
+
return store.delete(id) ? ok(true) : err(key + " " + id + " not found");
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
`;
|
|
821
|
+
|
|
822
|
+
const SDK_USE_COLLECTION_TS = `// useCollection — the TanStack Query layer for a collection, once: cached list,
|
|
823
|
+
// loading/error state, and create/update/remove mutations with OPTIMISTIC updates
|
|
824
|
+
// + rollback + invalidation built in. A domain's hooks file becomes one line.
|
|
825
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
826
|
+
import type { ICollection, IEntity } from "@/lib/collection";
|
|
827
|
+
|
|
828
|
+
export interface IMutationApi<T extends IEntity> {
|
|
829
|
+
create: (draft: Omit<T, "id">) => void;
|
|
830
|
+
update: (input: { readonly id: string; readonly patch: Partial<Omit<T, "id">> }) => void;
|
|
831
|
+
remove: (id: string) => void;
|
|
832
|
+
isPending: boolean;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export interface ICollectionApi<T extends IEntity> {
|
|
836
|
+
items: readonly T[];
|
|
837
|
+
isLoading: boolean;
|
|
838
|
+
error: string | undefined;
|
|
839
|
+
refetch: () => void;
|
|
840
|
+
mutations: IMutationApi<T>;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function unwrap<T>(promise: Promise<{ ok: true; value: T } | { ok: false; error: string }>): Promise<T> {
|
|
844
|
+
const result = await promise;
|
|
845
|
+
if (!result.ok) {
|
|
846
|
+
throw new Error(result.error);
|
|
847
|
+
}
|
|
848
|
+
return result.value;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export function useCollection<T extends IEntity>(collection: ICollection<T>): ICollectionApi<T> {
|
|
852
|
+
const client = useQueryClient();
|
|
853
|
+
const queryKey = [collection.key];
|
|
854
|
+
|
|
855
|
+
const query = useQuery({
|
|
856
|
+
queryKey,
|
|
857
|
+
queryFn: () => unwrap(collection.list()),
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
const invalidate = (): void => {
|
|
861
|
+
void client.invalidateQueries({ queryKey });
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
const create = useMutation({
|
|
865
|
+
mutationFn: (draft: Omit<T, "id">) => unwrap(collection.create(draft)),
|
|
866
|
+
onSettled: invalidate,
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
const update = useMutation({
|
|
870
|
+
mutationFn: (input: { readonly id: string; readonly patch: Partial<Omit<T, "id">> }) =>
|
|
871
|
+
unwrap(collection.update(input.id, input.patch)),
|
|
872
|
+
onMutate: async (input) => {
|
|
873
|
+
await client.cancelQueries({ queryKey });
|
|
874
|
+
const previous = client.getQueryData<readonly T[]>(queryKey);
|
|
875
|
+
if (previous !== undefined) {
|
|
876
|
+
client.setQueryData<readonly T[]>(
|
|
877
|
+
queryKey,
|
|
878
|
+
previous.map((item) => (item.id === input.id ? { ...item, ...input.patch } : item))
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
return { previous };
|
|
882
|
+
},
|
|
883
|
+
onError: (_error, _input, context) => {
|
|
884
|
+
if (context?.previous !== undefined) {
|
|
885
|
+
client.setQueryData(queryKey, context.previous);
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
onSettled: invalidate,
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const remove = useMutation({
|
|
892
|
+
mutationFn: (id: string) => unwrap(collection.remove(id)),
|
|
893
|
+
onSettled: invalidate,
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
items: query.data ?? [],
|
|
898
|
+
isLoading: query.isPending,
|
|
899
|
+
error: query.error === null ? undefined : query.error.message,
|
|
900
|
+
refetch: () => {
|
|
901
|
+
void query.refetch();
|
|
902
|
+
},
|
|
903
|
+
mutations: {
|
|
904
|
+
create: create.mutate,
|
|
905
|
+
update: update.mutate,
|
|
906
|
+
remove: remove.mutate,
|
|
907
|
+
isPending: create.isPending || update.isPending || remove.isPending,
|
|
908
|
+
},
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
`;
|
|
912
|
+
|
|
913
|
+
const SDK_USE_FORM_TS = `// useForm — declarative form state: values, per-field errors, async submit with
|
|
914
|
+
// loading/success/error status. A form becomes initial + validate + submit; the
|
|
915
|
+
// plumbing (touched, submitting, error/success handling) lives here, once.
|
|
916
|
+
import { useCallback, useState } from "react";
|
|
917
|
+
import type { Result } from "@/lib/result";
|
|
918
|
+
|
|
919
|
+
export type TFormStatus = "idle" | "submitting" | "success" | "error";
|
|
920
|
+
export type TFieldErrors<T> = Partial<Record<keyof T, string>>;
|
|
921
|
+
|
|
922
|
+
export interface IFormApi<T> {
|
|
923
|
+
values: T;
|
|
924
|
+
errors: TFieldErrors<T>;
|
|
925
|
+
status: TFormStatus;
|
|
926
|
+
submitError: string | undefined;
|
|
927
|
+
setField: <K extends keyof T>(key: K, value: T[K]) => void;
|
|
928
|
+
handleSubmit: () => Promise<void>;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export interface IFormOptions<T> {
|
|
932
|
+
readonly initial: T;
|
|
933
|
+
readonly validate: (values: T) => TFieldErrors<T>;
|
|
934
|
+
readonly submit: (values: T) => Promise<Result<unknown, string>>;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
export function useForm<T>(options: IFormOptions<T>): IFormApi<T> {
|
|
938
|
+
const [values, setValues] = useState<T>(options.initial);
|
|
939
|
+
const [errors, setErrors] = useState<TFieldErrors<T>>({});
|
|
940
|
+
const [status, setStatus] = useState<TFormStatus>("idle");
|
|
941
|
+
const [submitError, setSubmitError] = useState<string | undefined>(undefined);
|
|
942
|
+
|
|
943
|
+
const setField = useCallback(<K extends keyof T>(key: K, value: T[K]): void => {
|
|
944
|
+
setValues((prev) => ({ ...prev, [key]: value }));
|
|
945
|
+
}, []);
|
|
946
|
+
|
|
947
|
+
const handleSubmit = useCallback(async (): Promise<void> => {
|
|
948
|
+
const found = options.validate(values);
|
|
949
|
+
setErrors(found);
|
|
950
|
+
if (Object.keys(found).length > 0) {
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
setStatus("submitting");
|
|
954
|
+
setSubmitError(undefined);
|
|
955
|
+
const result = await options.submit(values);
|
|
956
|
+
if (result.ok) {
|
|
957
|
+
setStatus("success");
|
|
958
|
+
} else {
|
|
959
|
+
setStatus("error");
|
|
960
|
+
setSubmitError(result.error);
|
|
961
|
+
}
|
|
962
|
+
}, [options, values]);
|
|
963
|
+
|
|
964
|
+
return { values, errors, status, submitError, setField, handleSubmit };
|
|
965
|
+
}
|
|
966
|
+
`;
|
|
967
|
+
|
|
968
|
+
export const WEB_TEMPLATES: Record<WebFramework, IWebTemplate> = {
|
|
969
|
+
react: {
|
|
970
|
+
label: "Vite + React + shadcn/ui + TanStack",
|
|
971
|
+
files: {
|
|
972
|
+
"package.json": REACT_PACKAGE_JSON,
|
|
973
|
+
"vite.config.ts": REACT_VITE_CONFIG,
|
|
974
|
+
"tsconfig.json": REACT_TSCONFIG,
|
|
975
|
+
"index.html": REACT_HTML,
|
|
976
|
+
"components.json": COMPONENTS_JSON,
|
|
977
|
+
"src/vite-env.d.ts": VITE_ENV_DTS,
|
|
978
|
+
".prettierignore": PRETTIER_IGNORE,
|
|
979
|
+
"src/lib/utils.ts": CN_UTILS,
|
|
980
|
+
"src/lib/result.ts": SDK_RESULT_TS,
|
|
981
|
+
"src/lib/object.ts": SDK_OBJECT_TS,
|
|
982
|
+
"src/lib/sort.ts": SDK_SORT_TS,
|
|
983
|
+
"src/lib/collection.ts": SDK_COLLECTION_TS,
|
|
984
|
+
"src/lib/use-collection.ts": SDK_USE_COLLECTION_TS,
|
|
985
|
+
"src/lib/use-form.ts": SDK_USE_FORM_TS,
|
|
986
|
+
"src/index.css": REACT_INDEX_CSS,
|
|
987
|
+
"src/components/ui/button.tsx": BUTTON_TSX,
|
|
988
|
+
"src/routes/__root.tsx": ROOT_ROUTE_TSX,
|
|
989
|
+
"src/routes/index.tsx": INDEX_ROUTE_TSX,
|
|
990
|
+
"src/routeTree.gen.ts": ROUTE_TREE_GEN,
|
|
991
|
+
"src/main.tsx": REACT_MAIN_TSX,
|
|
992
|
+
},
|
|
993
|
+
eslintIgnore: ["src/components/ui/**", "src/lib/**", "**/*.gen.ts"],
|
|
994
|
+
guidance: REACT_GUIDANCE,
|
|
995
|
+
},
|
|
996
|
+
vanilla: {
|
|
997
|
+
label: "Vite + TypeScript + Tailwind",
|
|
998
|
+
files: {
|
|
999
|
+
"package.json": VANILLA_PACKAGE_JSON,
|
|
1000
|
+
"vite.config.ts": VANILLA_VITE_CONFIG,
|
|
1001
|
+
"tsconfig.json": VANILLA_TSCONFIG,
|
|
1002
|
+
"index.html": VANILLA_HTML,
|
|
1003
|
+
"src/vite-env.d.ts": VITE_ENV_DTS,
|
|
1004
|
+
".prettierignore": PRETTIER_IGNORE,
|
|
1005
|
+
"src/main.ts": VANILLA_MAIN_TS,
|
|
1006
|
+
"src/style.css": VANILLA_CSS,
|
|
1007
|
+
},
|
|
1008
|
+
eslintIgnore: [],
|
|
1009
|
+
guidance: VANILLA_GUIDANCE,
|
|
1010
|
+
},
|
|
1011
|
+
};
|