@draekien/create-d9-app 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +9 -2
- package/dist/index.js +53 -0
- package/index.js +11 -0
- package/package.json +36 -3
- package/templates/base/.claude/settings.json +36 -0
- package/templates/base/.codex/hooks.json +30 -0
- package/templates/base/.intent/hooks/intent-claude-gate.mjs +278 -0
- package/templates/base/.intent/hooks/intent-codex-gate.mjs +278 -0
- package/templates/base/AGENTS.md +28 -0
- package/templates/base/CLAUDE.md +1 -0
- package/templates/base/_gitignore +7 -0
- package/templates/base/apps/web/package.json +38 -0
- package/templates/base/apps/web/src/examples/form.test.tsx +10 -0
- package/templates/base/apps/web/src/examples/form.tsx +35 -0
- package/templates/base/apps/web/src/examples/greeting.ts +11 -0
- package/templates/base/apps/web/src/examples/hotkeys.test.tsx +7 -0
- package/templates/base/apps/web/src/examples/hotkeys.tsx +10 -0
- package/templates/base/apps/web/src/examples/query.test.tsx +18 -0
- package/templates/base/apps/web/src/examples/query.tsx +8 -0
- package/templates/base/apps/web/src/examples/table.test.tsx +7 -0
- package/templates/base/apps/web/src/examples/table.tsx +47 -0
- package/templates/base/apps/web/src/home.test.tsx +18 -0
- package/templates/base/apps/web/src/home.tsx +21 -0
- package/templates/base/apps/web/src/routeTree.gen.ts +68 -0
- package/templates/base/apps/web/src/router.tsx +20 -0
- package/templates/base/apps/web/src/routes/__root.tsx +26 -0
- package/templates/base/apps/web/src/routes/index.tsx +10 -0
- package/templates/base/apps/web/tsconfig.json +12 -0
- package/templates/base/apps/web/vite.config.ts +20 -0
- package/templates/base/package.json +28 -0
- package/templates/base/pnpm-workspace.yaml +7 -0
- package/templates/base/scripts/check-comments.ts +100 -0
- package/templates/base/tsconfig.json +13 -0
- package/templates/base/turbo.json +23 -0
- package/templates/tools/biome/.biome/effect-cleanup.grit +8 -0
- package/templates/tools/biome/.biome/handler-naming.grit +9 -0
- package/templates/tools/biome/.biome/no-default-props.grit +5 -0
- package/templates/tools/biome/.biome/no-derived-state-effect.grit +7 -0
- package/templates/tools/biome/.biome/no-double-assertion.grit +5 -0
- package/templates/tools/biome/.biome/no-fetch-in-effect.grit +6 -0
- package/templates/tools/biome/.biome/no-impure-render.grit +8 -0
- package/templates/tools/biome/.biome/no-inline-query-key.grit +7 -0
- package/templates/tools/biome/.biome/no-query-client-in-render.grit +7 -0
- package/templates/tools/biome/.biome/no-refetch-on-success.grit +7 -0
- package/templates/tools/biome/.biome/no-try-around-jsx.grit +9 -0
- package/templates/tools/biome/.biome/query-client-stale-time.grit +8 -0
- package/templates/tools/biome/biome.json +156 -0
- package/templates/tools/biome/package.json +8 -0
- package/templates/tools/lefthook/lefthook.yml +6 -0
- package/templates/tools/lefthook/package.json +5 -0
- package/templates/tools/shadcn/packages/ui/components.json +22 -0
- package/templates/tools/shadcn/packages/ui/package.json +33 -0
- package/templates/tools/shadcn/packages/ui/src/components/button.tsx +56 -0
- package/templates/tools/shadcn/packages/ui/src/lib/utils.ts +1 -0
- package/templates/tools/shadcn/packages/ui/src/styles/globals.css +87 -0
- package/templates/tools/shadcn/packages/ui/tsconfig.json +10 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { execFileSync } from 'node:child_process'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { dirname, join, relative, sep } from 'node:path'
|
|
6
|
+
import { createHash } from 'node:crypto'
|
|
7
|
+
import { performance } from 'node:perf_hooks'
|
|
8
|
+
|
|
9
|
+
const AGENT = "codex"
|
|
10
|
+
const CATALOG_COMMAND = "pnpm exec intent list --json --no-notices"
|
|
11
|
+
const LOAD_COMMAND = "pnpm exec intent load <package>#<skill>"
|
|
12
|
+
const EDIT_TOOLS = new Set(["Edit","Write","apply_patch"])
|
|
13
|
+
const GATE_DENY_REASON = "Blocked: check TanStack guidance before editing. If a listed skill matches, load it, then retry the edit."
|
|
14
|
+
const INTENT_INVOCATION_PATTERN = /(?:^|&&|\|\||;|\|)\s*((?:bunx\s+--no-install\s+--package\s+@tanstack\/intent\s+intent)|(?:npm\s+exec\s+--no\s+--\s+intent)|(?:yarn\s+exec\s+intent)|(?:bunx\s+@tanstack\/intent(?:@latest)?)|(?:pnpm\s+exec\s+intent)|(?:pnpm\s+dlx\s+@tanstack\/intent(?:@latest)?)|(?:npx\s+@tanstack\/intent(?:@latest)?)|(?:yarn\s+dlx\s+@tanstack\/intent(?:@latest)?)|(?:(?:[^\s|;&]*[\\/])?intent))\s+(list|load)(?:\s+([^\s|;&]+))?/i
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await main()
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
process.exit(0)
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const event = readEventFromStdin()
|
|
25
|
+
|
|
26
|
+
if (isSessionStartEvent(event)) {
|
|
27
|
+
const stateFile = stateFileForEvent(event)
|
|
28
|
+
const additionalContext = await createSessionCatalogContext(rootForEvent(event))
|
|
29
|
+
appendObservation(stateFile, { action: 'list', raw: CATALOG_COMMAND })
|
|
30
|
+
if (additionalContext) {
|
|
31
|
+
process.stdout.write(JSON.stringify(sessionStartOutput(additionalContext)))
|
|
32
|
+
}
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const stateFile = stateFileForEvent(event)
|
|
37
|
+
const observation = observationFromEvent(event)
|
|
38
|
+
|
|
39
|
+
if (observation) {
|
|
40
|
+
appendObservation(stateFile, observation)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const toolName = event?.tool_name ?? event?.toolName
|
|
44
|
+
if (typeof toolName === 'string' && EDIT_TOOLS.has(toolName) && !hasIntentCheck(stateFile)) {
|
|
45
|
+
process.stdout.write(JSON.stringify(denyOutput()))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readEventFromStdin() {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(readFileSync(0, 'utf8'))
|
|
52
|
+
} catch {
|
|
53
|
+
return {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isSessionStartEvent(event) {
|
|
58
|
+
return (event?.hook_event_name ?? event?.hookEventName) === 'SessionStart'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rootForEvent(event) {
|
|
62
|
+
return typeof event?.cwd === 'string' && event.cwd ? event.cwd : process.cwd()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function createSessionCatalogContext(root) {
|
|
66
|
+
try {
|
|
67
|
+
const start = performance.now()
|
|
68
|
+
const localCli = resolveLocalIntentCli(root)
|
|
69
|
+
const result = readIntentList(root, localCli)
|
|
70
|
+
const durationMs = performance.now() - start
|
|
71
|
+
console.error(
|
|
72
|
+
`[intent-${AGENT}-session-catalog] listIntentSkills found ${result.skills.length} skills from ${result.packages.length} packages in ${formatDuration(durationMs)} (packageJsonReadCount=${result.debug?.scan.packageJsonReadCount ?? 'unknown'})`,
|
|
73
|
+
)
|
|
74
|
+
return formatSessionCatalog(result, loadCommandForLocalCli(root, localCli))
|
|
75
|
+
} catch {
|
|
76
|
+
return ''
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The package-manager runner in CATALOG_COMMAND (npx, pnpm dlx, ...) resolves
|
|
81
|
+
// @tanstack/intent@latest against the registry on every run, which costs one
|
|
82
|
+
// to four seconds per session start. When the project has the package
|
|
83
|
+
// installed, run its CLI directly with this Node binary instead. Returns the
|
|
84
|
+
// CLI path and the node_modules directory it was found in, or null.
|
|
85
|
+
function resolveLocalIntentCli(root) {
|
|
86
|
+
let dir = root
|
|
87
|
+
let prev
|
|
88
|
+
while (dir !== prev) {
|
|
89
|
+
const nodeModulesDir = join(dir, 'node_modules')
|
|
90
|
+
const packageJsonPath = join(nodeModulesDir, '@tanstack', 'intent', 'package.json')
|
|
91
|
+
if (existsSync(packageJsonPath)) {
|
|
92
|
+
try {
|
|
93
|
+
const bin = JSON.parse(readFileSync(packageJsonPath, 'utf8')).bin
|
|
94
|
+
const relativeBin = typeof bin === 'string' ? bin : bin && bin.intent
|
|
95
|
+
if (typeof relativeBin === 'string') {
|
|
96
|
+
const cli = join(dirname(packageJsonPath), relativeBin)
|
|
97
|
+
if (existsSync(cli)) return { cli, nodeModulesDir }
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
prev = dir
|
|
104
|
+
dir = dirname(dir)
|
|
105
|
+
}
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The load command shown to the agent. When the catalog came from a local
|
|
110
|
+
// install that also has a bin shim, suggest that shim (same installation, no
|
|
111
|
+
// registry lookup); otherwise suggest the package-manager runner.
|
|
112
|
+
function loadCommandForLocalCli(root, localCli) {
|
|
113
|
+
if (!localCli) return LOAD_COMMAND
|
|
114
|
+
const bin = join(localCli.nodeModulesDir, '.bin', 'intent')
|
|
115
|
+
if (!existsSync(bin)) return LOAD_COMMAND
|
|
116
|
+
return relative(root, bin).split(sep).join('/') + ' load <package>#<skill>'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readIntentList(root, localCli) {
|
|
120
|
+
const options = {
|
|
121
|
+
cwd: root,
|
|
122
|
+
encoding: 'utf8',
|
|
123
|
+
env: { ...process.env, INTENT_AUDIENCE: 'agent' },
|
|
124
|
+
maxBuffer: 1024 * 1024,
|
|
125
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
126
|
+
timeout: 9000,
|
|
127
|
+
}
|
|
128
|
+
const output = localCli
|
|
129
|
+
? execFileSync(process.execPath, [localCli.cli, 'list', '--json', '--no-notices'], options)
|
|
130
|
+
: execFileSync(CATALOG_COMMAND, { ...options, shell: true })
|
|
131
|
+
return JSON.parse(output)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatDuration(durationMs) {
|
|
135
|
+
return `${durationMs.toFixed(1)}ms`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatSessionCatalog(result, loadCommand) {
|
|
139
|
+
if (!Array.isArray(result.skills) || result.skills.length === 0) return ''
|
|
140
|
+
|
|
141
|
+
return [
|
|
142
|
+
'TanStack Intent skills are available in this repository.',
|
|
143
|
+
'',
|
|
144
|
+
'These are Intent skills, not native agent skills.',
|
|
145
|
+
'Load a matching skill with: `' + loadCommand + '`.',
|
|
146
|
+
'',
|
|
147
|
+
'Before substantial work, check whether one listed skill clearly matches the user task. If one clearly matches, load that full skill guidance with the Intent CLI before proceeding.',
|
|
148
|
+
'',
|
|
149
|
+
'If no skill clearly matches, continue normally. Do not load a skill just to improve phrasing or gather nonessential context.',
|
|
150
|
+
'',
|
|
151
|
+
'Available local Intent skills:',
|
|
152
|
+
formatSkillCatalog(result.skills),
|
|
153
|
+
formatWarnings(result),
|
|
154
|
+
]
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.join('\n')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatSkillCatalog(skills) {
|
|
160
|
+
return skills
|
|
161
|
+
.map((skill) => `- ${skill.use}: ${normalizeDescription(skill.description)}`)
|
|
162
|
+
.join('\n')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function normalizeDescription(description) {
|
|
166
|
+
return typeof description === 'string' ? description.replace(/\s+/g, ' ').trim() : ''
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatWarnings(result) {
|
|
170
|
+
const warnings = [
|
|
171
|
+
...(Array.isArray(result.warnings) ? result.warnings : []),
|
|
172
|
+
...(Array.isArray(result.conflicts)
|
|
173
|
+
? result.conflicts.map(
|
|
174
|
+
(conflict) =>
|
|
175
|
+
`Version conflict for ${conflict.packageName}; using ${conflict.chosen.version}`,
|
|
176
|
+
)
|
|
177
|
+
: []),
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
if (warnings.length === 0) return ''
|
|
181
|
+
return `\nWarnings:\n${warnings.map((warning) => `- ${warning}`).join('\n')}`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function sessionStartOutput(additionalContext) {
|
|
185
|
+
if (AGENT === 'copilot') {
|
|
186
|
+
return { additionalContext }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
hookSpecificOutput: {
|
|
191
|
+
hookEventName: 'SessionStart',
|
|
192
|
+
additionalContext,
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function stateFileForEvent(event) {
|
|
198
|
+
const sessionId = typeof event?.session_id === 'string' ? event.session_id : 'unknown'
|
|
199
|
+
const cwd = typeof event?.cwd === 'string' ? event.cwd : process.cwd()
|
|
200
|
+
const key = createHash('sha256').update(AGENT + '\0' + cwd + '\0' + sessionId).digest('hex')
|
|
201
|
+
return join(tmpdir(), 'tanstack-intent-hooks', key + '.jsonl')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function parseIntentInvocation(command) {
|
|
205
|
+
if (typeof command !== 'string') return undefined
|
|
206
|
+
const match = command.match(INTENT_INVOCATION_PATTERN)
|
|
207
|
+
if (!match?.[1] || !match[2]) return undefined
|
|
208
|
+
const action = match[2].toLowerCase()
|
|
209
|
+
if (action !== 'list' && action !== 'load') return undefined
|
|
210
|
+
const skillUse = action === 'load' ? match[3] : undefined
|
|
211
|
+
if (action === 'load' && !skillUse) return undefined
|
|
212
|
+
return action === 'load' ? { action, skillUse } : { action }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function observationFromEvent(event) {
|
|
216
|
+
if (!event || typeof event !== 'object') return undefined
|
|
217
|
+
const toolName = event.tool_name ?? event.toolName
|
|
218
|
+
const toolInput = event.tool_input ?? event.toolArgs
|
|
219
|
+
if (toolName !== 'Bash') return undefined
|
|
220
|
+
const command = typeof toolInput === 'string' ? safeCommandFromString(toolInput) : commandFromObject(toolInput)
|
|
221
|
+
const parsed = parseIntentInvocation(command)
|
|
222
|
+
if (!parsed || typeof command !== 'string') return undefined
|
|
223
|
+
return { action: parsed.action, skillUse: parsed.skillUse, raw: command }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function commandFromObject(value) {
|
|
227
|
+
return value && typeof value === 'object' ? value.command : undefined
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function safeCommandFromString(value) {
|
|
231
|
+
try {
|
|
232
|
+
const command = commandFromObject(JSON.parse(value))
|
|
233
|
+
return typeof command === 'string' ? command : value
|
|
234
|
+
} catch {
|
|
235
|
+
return value
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function appendObservation(stateFile, observation) {
|
|
240
|
+
try {
|
|
241
|
+
mkdirSync(dirname(stateFile), { recursive: true })
|
|
242
|
+
appendFileSync(stateFile, JSON.stringify({ ts: new Date().toISOString(), ...observation }) + '\n')
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function hasIntentCheck(stateFile) {
|
|
248
|
+
if (!existsSync(stateFile)) return false
|
|
249
|
+
try {
|
|
250
|
+
return readFileSync(stateFile, 'utf8')
|
|
251
|
+
.split('\n')
|
|
252
|
+
.filter(Boolean)
|
|
253
|
+
.some((line) => {
|
|
254
|
+
try {
|
|
255
|
+
const action = JSON.parse(line).action
|
|
256
|
+
return action === 'list' || action === 'load'
|
|
257
|
+
} catch {
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
} catch {
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function denyOutput() {
|
|
267
|
+
if (AGENT === 'copilot') {
|
|
268
|
+
return { permissionDecision: 'deny', permissionDecisionReason: GATE_DENY_REASON }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
hookSpecificOutput: {
|
|
273
|
+
hookEventName: 'PreToolUse',
|
|
274
|
+
permissionDecision: 'deny',
|
|
275
|
+
permissionDecisionReason: GATE_DENY_REASON,
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!-- intent-skills:start -->
|
|
2
|
+
## Skill Loading
|
|
3
|
+
|
|
4
|
+
Use the repository’s installed Intent. If it is unavailable, report the missing dependency instead of downloading a replacement.
|
|
5
|
+
Before editing files for a substantial task:
|
|
6
|
+
- Run `pnpm exec intent list` from the workspace root to see available local skills.
|
|
7
|
+
- If a listed skill matches the task, run `pnpm exec intent load <package>#<skill>` before changing files.
|
|
8
|
+
- Use the loaded `SKILL.md` guidance while making the change.
|
|
9
|
+
- Monorepos: when working across packages, run the skill check from the workspace root and prefer the local skill for the package being changed.
|
|
10
|
+
- Multiple matches: prefer the most specific local skill for the package or concern you are changing; load additional skills only when the task spans multiple packages or concerns.
|
|
11
|
+
<!-- intent-skills:end -->
|
|
12
|
+
|
|
13
|
+
# Project
|
|
14
|
+
|
|
15
|
+
- Turborepo monorepo managed with pnpm. Apps in `apps/`, shared packages in `packages/`.
|
|
16
|
+
- `apps/web`: TanStack Start on Vite, React, TypeScript, Vitest.
|
|
17
|
+
- Run tasks from the repo root: `pnpm build`, `pnpm check`, `pnpm typecheck`, `pnpm test`, `pnpm dev`.
|
|
18
|
+
- Dependency versions are exact. Never use `^`, `~` or `latest`.
|
|
19
|
+
- Routes live in `apps/web/src/routes`. `routeTree.gen.ts` is generated; do not edit it.
|
|
20
|
+
|
|
21
|
+
<!-- BEGIN:turborepo-agent-rules -->
|
|
22
|
+
|
|
23
|
+
# This is NOT the Turborepo you know
|
|
24
|
+
|
|
25
|
+
Turborepo configuration, task behavior, and CLI commands can vary between installed versions and may differ from your training data. Resolve the `turbo` package from this file's directory or relevant workspace; in monorepos, it may not be visible from the repository root. For example, run `node -p "require.resolve('turbo/package.json')"` from a workspace that depends on `turbo`.
|
|
26
|
+
|
|
27
|
+
Read `docs/README.md` inside that installed package first, then read the relevant pages from its `docs/` directory before changing Turborepo configuration or commands. Heed deprecation notices. These bundled docs match the installed package version and are available without network access.
|
|
28
|
+
<!-- END:turborepo-agent-rules -->
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "web",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite dev",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"test": "vitest run"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@tailwindcss/vite": "4.3.3",
|
|
13
|
+
"@tanstack/react-form": "1.33.5",
|
|
14
|
+
"@tanstack/react-hotkeys": "0.13.0",
|
|
15
|
+
"@tanstack/react-query": "5.104.1",
|
|
16
|
+
"@tanstack/react-router": "1.170.41",
|
|
17
|
+
"@tanstack/react-router-ssr-query": "1.167.3",
|
|
18
|
+
"@tanstack/react-start": "1.168.60",
|
|
19
|
+
"@tanstack/react-table": "9.2.8",
|
|
20
|
+
"@workspace/ui": "workspace:*",
|
|
21
|
+
"motion": "14.0.0",
|
|
22
|
+
"react": "19.3.0",
|
|
23
|
+
"react-dom": "19.3.0",
|
|
24
|
+
"tailwindcss": "4.3.3"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@babel/core": "8.0.7",
|
|
28
|
+
"@rolldown/plugin-babel": "0.2.4",
|
|
29
|
+
"@types/node": "22.20.5",
|
|
30
|
+
"@types/react": "19.3.0",
|
|
31
|
+
"@types/react-dom": "19.3.0",
|
|
32
|
+
"@vitejs/plugin-react": "6.1.2",
|
|
33
|
+
"babel-plugin-react-compiler": "1.0.0",
|
|
34
|
+
"typescript": "7.0.2",
|
|
35
|
+
"vite": "8.3.4",
|
|
36
|
+
"vitest": "5.0.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { renderToString } from "react-dom/server"
|
|
2
|
+
import { expect, it } from "vitest"
|
|
3
|
+
import { FormExample } from "./form.tsx"
|
|
4
|
+
|
|
5
|
+
it("renders the name field and submit button", () => {
|
|
6
|
+
const html = renderToString(<FormExample />)
|
|
7
|
+
|
|
8
|
+
expect(html).toContain('name="name"')
|
|
9
|
+
expect(html).toContain("Submit")
|
|
10
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useForm } from "@tanstack/react-form"
|
|
2
|
+
import { useState } from "react"
|
|
3
|
+
|
|
4
|
+
export function FormExample() {
|
|
5
|
+
const [submitted, setSubmitted] = useState("")
|
|
6
|
+
const form = useForm({
|
|
7
|
+
defaultValues: { name: "" },
|
|
8
|
+
onSubmit: ({ value }) => setSubmitted(value.name),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const handleSubmit = (event: React.SubmitEvent<HTMLFormElement>) => {
|
|
12
|
+
event.preventDefault()
|
|
13
|
+
form.handleSubmit()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<form onSubmit={handleSubmit} className="flex flex-col items-center gap-2">
|
|
18
|
+
<form.Field name="name">
|
|
19
|
+
{(field) => (
|
|
20
|
+
<label className="flex gap-2">
|
|
21
|
+
Name
|
|
22
|
+
<input
|
|
23
|
+
name={field.name}
|
|
24
|
+
value={field.state.value}
|
|
25
|
+
onChange={(event) => field.handleChange(event.target.value)}
|
|
26
|
+
className="border"
|
|
27
|
+
/>
|
|
28
|
+
</label>
|
|
29
|
+
)}
|
|
30
|
+
</form.Field>
|
|
31
|
+
<button type="submit">Submit</button>
|
|
32
|
+
{submitted ? <p>Submitted {submitted}</p> : null}
|
|
33
|
+
</form>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { queryOptions } from "@tanstack/react-query"
|
|
2
|
+
|
|
3
|
+
export const greetingKeys = {
|
|
4
|
+
all: ["greeting"] as const,
|
|
5
|
+
detail: (name: string) => [...greetingKeys.all, name] as const,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const greetingOptions = queryOptions({
|
|
9
|
+
queryKey: greetingKeys.detail("query"),
|
|
10
|
+
queryFn: () => Promise.resolve("Hello, query"),
|
|
11
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useHotkey } from "@tanstack/react-hotkeys"
|
|
2
|
+
import { useState } from "react"
|
|
3
|
+
|
|
4
|
+
export function HotkeysExample() {
|
|
5
|
+
const [count, setCount] = useState(0)
|
|
6
|
+
|
|
7
|
+
useHotkey("Mod+K", () => setCount((current) => current + 1))
|
|
8
|
+
|
|
9
|
+
return <p>Press Ctrl+K or Cmd+K ({count} presses)</p>
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
|
2
|
+
import { renderToString } from "react-dom/server"
|
|
3
|
+
import { expect, it } from "vitest"
|
|
4
|
+
import { greetingOptions } from "./greeting.ts"
|
|
5
|
+
import { QueryExample } from "./query.tsx"
|
|
6
|
+
|
|
7
|
+
it("renders the prefetched greeting", async () => {
|
|
8
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 } } })
|
|
9
|
+
await queryClient.prefetchQuery(greetingOptions)
|
|
10
|
+
|
|
11
|
+
const html = renderToString(
|
|
12
|
+
<QueryClientProvider client={queryClient}>
|
|
13
|
+
<QueryExample />
|
|
14
|
+
</QueryClientProvider>
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
expect(html).toContain("Hello, query")
|
|
18
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ColumnDef } from "@tanstack/react-table"
|
|
2
|
+
import { tableFeatures, useTable } from "@tanstack/react-table"
|
|
3
|
+
|
|
4
|
+
type Person = { name: string; role: string }
|
|
5
|
+
|
|
6
|
+
const features = tableFeatures({})
|
|
7
|
+
|
|
8
|
+
const data: Array<Person> = [
|
|
9
|
+
{ name: "Ada Lovelace", role: "Mathematician" },
|
|
10
|
+
{ name: "Grace Hopper", role: "Admiral" },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const columns: Array<ColumnDef<typeof features, Person>> = [
|
|
14
|
+
{ accessorKey: "name", header: "Name" },
|
|
15
|
+
{ accessorKey: "role", header: "Role" },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export function TableExample() {
|
|
19
|
+
const table = useTable({ features, columns, data })
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<table>
|
|
23
|
+
<thead>
|
|
24
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
25
|
+
<tr key={headerGroup.id}>
|
|
26
|
+
{headerGroup.headers.map((header) => (
|
|
27
|
+
<th key={header.id}>
|
|
28
|
+
<table.FlexRender header={header} />
|
|
29
|
+
</th>
|
|
30
|
+
))}
|
|
31
|
+
</tr>
|
|
32
|
+
))}
|
|
33
|
+
</thead>
|
|
34
|
+
<tbody>
|
|
35
|
+
{table.getRowModel().rows.map((row) => (
|
|
36
|
+
<tr key={row.id}>
|
|
37
|
+
{row.getAllCells().map((cell) => (
|
|
38
|
+
<td key={cell.id}>
|
|
39
|
+
<table.FlexRender cell={cell} />
|
|
40
|
+
</td>
|
|
41
|
+
))}
|
|
42
|
+
</tr>
|
|
43
|
+
))}
|
|
44
|
+
</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
|
2
|
+
import { renderToString } from "react-dom/server"
|
|
3
|
+
import { expect, it } from "vitest"
|
|
4
|
+
import { greetingOptions } from "./examples/greeting.ts"
|
|
5
|
+
import { Home } from "./home.tsx"
|
|
6
|
+
|
|
7
|
+
it("renders the greeting", async () => {
|
|
8
|
+
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 } } })
|
|
9
|
+
await queryClient.prefetchQuery(greetingOptions)
|
|
10
|
+
|
|
11
|
+
const html = renderToString(
|
|
12
|
+
<QueryClientProvider client={queryClient}>
|
|
13
|
+
<Home />
|
|
14
|
+
</QueryClientProvider>
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
expect(html).toContain("Hello from your new app")
|
|
18
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Button } from "@workspace/ui/components/button.tsx"
|
|
2
|
+
import { motion } from "motion/react"
|
|
3
|
+
import { FormExample } from "./examples/form.tsx"
|
|
4
|
+
import { HotkeysExample } from "./examples/hotkeys.tsx"
|
|
5
|
+
import { QueryExample } from "./examples/query.tsx"
|
|
6
|
+
import { TableExample } from "./examples/table.tsx"
|
|
7
|
+
|
|
8
|
+
export function Home() {
|
|
9
|
+
return (
|
|
10
|
+
<main className="flex min-h-screen flex-col items-center justify-center gap-6">
|
|
11
|
+
<h1 className="text-3xl font-semibold">Hello from your new app</h1>
|
|
12
|
+
<motion.div initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }}>
|
|
13
|
+
<Button>Get started</Button>
|
|
14
|
+
</motion.div>
|
|
15
|
+
<QueryExample />
|
|
16
|
+
<FormExample />
|
|
17
|
+
<TableExample />
|
|
18
|
+
<HotkeysExample />
|
|
19
|
+
</main>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
|
|
5
|
+
// noinspection JSUnusedGlobalSymbols
|
|
6
|
+
|
|
7
|
+
// This file was automatically generated by TanStack Router.
|
|
8
|
+
// You should NOT make any changes in this file as it will be overwritten.
|
|
9
|
+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
10
|
+
|
|
11
|
+
import { Route as rootRouteImport } from './routes/__root'
|
|
12
|
+
import { Route as IndexRouteImport } from './routes/index'
|
|
13
|
+
|
|
14
|
+
const IndexRoute = IndexRouteImport.update({
|
|
15
|
+
id: '/',
|
|
16
|
+
path: '/',
|
|
17
|
+
getParentRoute: () => rootRouteImport,
|
|
18
|
+
} as any)
|
|
19
|
+
|
|
20
|
+
export interface FileRoutesByFullPath {
|
|
21
|
+
'/': typeof IndexRoute
|
|
22
|
+
}
|
|
23
|
+
export interface FileRoutesByTo {
|
|
24
|
+
'/': typeof IndexRoute
|
|
25
|
+
}
|
|
26
|
+
export interface FileRoutesById {
|
|
27
|
+
__root__: typeof rootRouteImport
|
|
28
|
+
'/': typeof IndexRoute
|
|
29
|
+
}
|
|
30
|
+
export interface FileRouteTypes {
|
|
31
|
+
fileRoutesByFullPath: FileRoutesByFullPath
|
|
32
|
+
fullPaths: '/'
|
|
33
|
+
fileRoutesByTo: FileRoutesByTo
|
|
34
|
+
to: '/'
|
|
35
|
+
id: '__root__' | '/'
|
|
36
|
+
fileRoutesById: FileRoutesById
|
|
37
|
+
}
|
|
38
|
+
export interface RootRouteChildren {
|
|
39
|
+
IndexRoute: typeof IndexRoute
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare module '@tanstack/react-router' {
|
|
43
|
+
interface FileRoutesByPath {
|
|
44
|
+
'/': {
|
|
45
|
+
id: '/'
|
|
46
|
+
path: '/'
|
|
47
|
+
fullPath: '/'
|
|
48
|
+
preLoaderRoute: typeof IndexRouteImport
|
|
49
|
+
parentRoute: typeof rootRouteImport
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rootRouteChildren: RootRouteChildren = {
|
|
55
|
+
IndexRoute: IndexRoute,
|
|
56
|
+
}
|
|
57
|
+
export const routeTree = rootRouteImport
|
|
58
|
+
._addFileChildren(rootRouteChildren)
|
|
59
|
+
._addFileTypes<FileRouteTypes>()
|
|
60
|
+
|
|
61
|
+
import type { getRouter } from './router.tsx'
|
|
62
|
+
import type { createStart } from '@tanstack/react-start'
|
|
63
|
+
declare module '@tanstack/react-start' {
|
|
64
|
+
interface Register {
|
|
65
|
+
ssr: true
|
|
66
|
+
router: Awaited<ReturnType<typeof getRouter>>
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { QueryClient } from "@tanstack/react-query"
|
|
2
|
+
import { createRouter } from "@tanstack/react-router"
|
|
3
|
+
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query"
|
|
4
|
+
import { routeTree } from "./routeTree.gen.ts"
|
|
5
|
+
|
|
6
|
+
export function getRouter() {
|
|
7
|
+
const queryClient = new QueryClient({
|
|
8
|
+
defaultOptions: { queries: { staleTime: 30_000 } },
|
|
9
|
+
})
|
|
10
|
+
const router = createRouter({
|
|
11
|
+
routeTree,
|
|
12
|
+
context: { queryClient },
|
|
13
|
+
scrollRestoration: true,
|
|
14
|
+
defaultPreload: "intent",
|
|
15
|
+
defaultPreloadStaleTime: 0,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
setupRouterSsrQueryIntegration({ router, queryClient })
|
|
19
|
+
return router
|
|
20
|
+
}
|