create-start-app 0.6.1 → 0.6.3

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/src/create-app.ts CHANGED
@@ -1,23 +1,13 @@
1
- #!/usr/bin/env node
2
-
3
- import {
4
- appendFile,
5
- copyFile,
6
- mkdir,
7
- readFile,
8
- writeFile,
9
- } from 'node:fs/promises'
10
- import { existsSync, readdirSync, statSync } from 'node:fs'
11
1
  import { basename, dirname, resolve } from 'node:path'
12
2
  import { fileURLToPath } from 'node:url'
13
3
  import { log, outro, spinner } from '@clack/prompts'
14
- import { execa } from 'execa'
15
4
  import { render } from 'ejs'
16
5
  import { format } from 'prettier'
17
6
  import chalk from 'chalk'
18
7
 
19
8
  import { CODE_ROUTER, FILE_ROUTER } from './constants.js'
20
9
 
10
+ import type { Environment } from './environment.js'
21
11
  import type { Options } from './types.js'
22
12
 
23
13
  function sortObject(obj: Record<string, string>): Record<string, string> {
@@ -29,7 +19,7 @@ function sortObject(obj: Record<string, string>): Record<string, string> {
29
19
  }, {})
30
20
  }
31
21
 
32
- function createCopyFiles(targetDir: string) {
22
+ function createCopyFiles(environment: Environment, targetDir: string) {
33
23
  return async function copyFiles(
34
24
  templateDir: string,
35
25
  files: Array<string>,
@@ -42,7 +32,7 @@ function createCopyFiles(targetDir: string) {
42
32
  const fileNoPath = targetFileName.split('/').pop()
43
33
  targetFileName = fileNoPath ? `./${fileNoPath}` : targetFileName
44
34
  }
45
- await copyFile(
35
+ await environment.copyFile(
46
36
  resolve(templateDir, file),
47
37
  resolve(targetDir, targetFileName),
48
38
  )
@@ -58,6 +48,7 @@ function jsSafeName(name: string) {
58
48
  }
59
49
 
60
50
  function createTemplateFile(
51
+ environment: Environment,
61
52
  projectName: string,
62
53
  options: Required<Options>,
63
54
  targetDir: string,
@@ -89,7 +80,10 @@ function createTemplateFile(
89
80
  ...extraTemplateValues,
90
81
  }
91
82
 
92
- const template = await readFile(resolve(templateDir, file), 'utf-8')
83
+ const template = await environment.readFile(
84
+ resolve(templateDir, file),
85
+ 'utf-8',
86
+ )
93
87
  let content = ''
94
88
  try {
95
89
  content = render(template, templateValues)
@@ -109,15 +103,12 @@ function createTemplateFile(
109
103
  })
110
104
  }
111
105
 
112
- await mkdir(dirname(resolve(targetDir, target)), {
113
- recursive: true,
114
- })
115
-
116
- await writeFile(resolve(targetDir, target), content)
106
+ await environment.writeFile(resolve(targetDir, target), content)
117
107
  }
118
108
  }
119
109
 
120
110
  async function createPackageJSON(
111
+ environment: Environment,
121
112
  projectName: string,
122
113
  options: Required<Options>,
123
114
  templateDir: string,
@@ -130,12 +121,15 @@ async function createPackageJSON(
130
121
  }>,
131
122
  ) {
132
123
  let packageJSON = JSON.parse(
133
- await readFile(resolve(templateDir, 'package.json'), 'utf8'),
124
+ await environment.readFile(resolve(templateDir, 'package.json'), 'utf8'),
134
125
  )
135
126
  packageJSON.name = projectName
136
127
  if (options.typescript) {
137
128
  const tsPackageJSON = JSON.parse(
138
- await readFile(resolve(templateDir, 'package.ts.json'), 'utf8'),
129
+ await environment.readFile(
130
+ resolve(templateDir, 'package.ts.json'),
131
+ 'utf8',
132
+ ),
139
133
  )
140
134
  packageJSON = {
141
135
  ...packageJSON,
@@ -147,7 +141,10 @@ async function createPackageJSON(
147
141
  }
148
142
  if (options.tailwind) {
149
143
  const twPackageJSON = JSON.parse(
150
- await readFile(resolve(templateDir, 'package.tw.json'), 'utf8'),
144
+ await environment.readFile(
145
+ resolve(templateDir, 'package.tw.json'),
146
+ 'utf8',
147
+ ),
151
148
  )
152
149
  packageJSON = {
153
150
  ...packageJSON,
@@ -159,7 +156,10 @@ async function createPackageJSON(
159
156
  }
160
157
  if (options.toolchain === 'biome') {
161
158
  const biomePackageJSON = JSON.parse(
162
- await readFile(resolve(templateDir, 'package.biome.json'), 'utf8'),
159
+ await environment.readFile(
160
+ resolve(templateDir, 'package.biome.json'),
161
+ 'utf8',
162
+ ),
163
163
  )
164
164
  packageJSON = {
165
165
  ...packageJSON,
@@ -175,7 +175,7 @@ async function createPackageJSON(
175
175
  }
176
176
  if (options.mode === FILE_ROUTER) {
177
177
  const frPackageJSON = JSON.parse(
178
- await readFile(resolve(routerDir, 'package.fr.json'), 'utf8'),
178
+ await environment.readFile(resolve(routerDir, 'package.fr.json'), 'utf8'),
179
179
  )
180
180
  packageJSON = {
181
181
  ...packageJSON,
@@ -211,28 +211,27 @@ async function createPackageJSON(
211
211
  packageJSON.devDependencies as Record<string, string>,
212
212
  )
213
213
 
214
- await writeFile(
214
+ await environment.writeFile(
215
215
  resolve(targetDir, 'package.json'),
216
216
  JSON.stringify(packageJSON, null, 2),
217
217
  )
218
218
  }
219
219
 
220
220
  async function copyFilesRecursively(
221
+ environment: Environment,
221
222
  source: string,
222
223
  target: string,
223
- copyFile: (source: string, target: string) => Promise<void>,
224
224
  templateFile: (file: string, targetFileName?: string) => Promise<void>,
225
225
  ) {
226
- const sourceStat = statSync(source)
227
- if (sourceStat.isDirectory()) {
228
- const files = readdirSync(source)
226
+ if (environment.isDirectory(source)) {
227
+ const files = environment.readdir(source)
229
228
  for (const file of files) {
230
229
  const sourceChild = resolve(source, file)
231
230
  const targetChild = resolve(target, file)
232
231
  await copyFilesRecursively(
232
+ environment,
233
233
  sourceChild,
234
234
  targetChild,
235
- copyFile,
236
235
  templateFile,
237
236
  )
238
237
  }
@@ -251,17 +250,16 @@ async function copyFilesRecursively(
251
250
 
252
251
  const targetPath = resolve(dirname(target), targetFile)
253
252
 
254
- await mkdir(dirname(targetPath), {
255
- recursive: true,
256
- })
257
-
258
253
  if (isTemplate) {
259
254
  await templateFile(source, targetPath)
260
255
  } else {
261
256
  if (isAppend) {
262
- await appendFile(targetPath, (await readFile(source)).toString())
257
+ await environment.appendFile(
258
+ targetPath,
259
+ (await environment.readFile(source)).toString(),
260
+ )
263
261
  } else {
264
- await copyFile(source, targetPath)
262
+ await environment.copyFile(source, targetPath)
265
263
  }
266
264
  }
267
265
  }
@@ -271,10 +269,14 @@ export async function createApp(
271
269
  options: Required<Options>,
272
270
  {
273
271
  silent = false,
272
+ environment,
274
273
  }: {
275
274
  silent?: boolean
276
- } = {},
275
+ environment: Environment
276
+ },
277
277
  ) {
278
+ environment.startRun()
279
+
278
280
  const templateDirBase = fileURLToPath(
279
281
  new URL(`../templates/${options.framework}/base`, import.meta.url),
280
282
  )
@@ -286,15 +288,16 @@ export async function createApp(
286
288
  )
287
289
  const targetDir = resolve(process.cwd(), options.projectName)
288
290
 
289
- if (existsSync(targetDir)) {
291
+ if (environment.exists(targetDir)) {
290
292
  if (!silent) {
291
293
  log.error(`Directory "${options.projectName}" already exists`)
292
294
  }
293
295
  return
294
296
  }
295
297
 
296
- const copyFiles = createCopyFiles(targetDir)
298
+ const copyFiles = createCopyFiles(environment, targetDir)
297
299
  const templateFile = createTemplateFile(
300
+ environment,
298
301
  options.projectName,
299
302
  options,
300
303
  targetDir,
@@ -303,28 +306,23 @@ export async function createApp(
303
306
  const isAddOnEnabled = (id: string) =>
304
307
  options.chosenAddOns.find((a) => a.id === id)
305
308
 
306
- // Make the root directory
307
- await mkdir(targetDir, { recursive: true })
308
-
309
309
  // Setup the .vscode directory
310
- await mkdir(resolve(targetDir, '.vscode'), { recursive: true })
311
310
  switch (options.toolchain) {
312
311
  case 'biome':
313
- await copyFile(
312
+ await environment.copyFile(
314
313
  resolve(templateDirBase, '_dot_vscode/settings.biome.json'),
315
314
  resolve(targetDir, '.vscode/settings.json'),
316
315
  )
317
316
  break
318
317
  case 'none':
319
318
  default:
320
- await copyFile(
319
+ await environment.copyFile(
321
320
  resolve(templateDirBase, '_dot_vscode/settings.json'),
322
321
  resolve(targetDir, '.vscode/settings.json'),
323
322
  )
324
323
  }
325
324
 
326
325
  // Fill the public directory
327
- await mkdir(resolve(targetDir, 'public'), { recursive: true })
328
326
  copyFiles(templateDirBase, [
329
327
  './public/robots.txt',
330
328
  './public/favicon.ico',
@@ -333,16 +331,9 @@ export async function createApp(
333
331
  './public/logo512.png',
334
332
  ])
335
333
 
336
- // Make the src directory
337
- await mkdir(resolve(targetDir, 'src'), { recursive: true })
338
- if (options.mode === FILE_ROUTER) {
339
- await mkdir(resolve(targetDir, 'src/routes'), { recursive: true })
340
- await mkdir(resolve(targetDir, 'src/components'), { recursive: true })
341
- }
342
-
343
334
  // Check for a .cursorrules file
344
- if (existsSync(resolve(templateDirBase, '.cursorrules'))) {
345
- await copyFile(
335
+ if (environment.exists(resolve(templateDirBase, '.cursorrules'))) {
336
+ await environment.copyFile(
346
337
  resolve(templateDirBase, '.cursorrules'),
347
338
  resolve(targetDir, '.cursorrules'),
348
339
  )
@@ -388,6 +379,7 @@ export async function createApp(
388
379
 
389
380
  // Setup the package.json file, optionally with typescript, tailwind and biome
390
381
  await createPackageJSON(
382
+ environment,
391
383
  options.projectName,
392
384
  options,
393
385
  templateDirBase,
@@ -404,21 +396,24 @@ export async function createApp(
404
396
  )) {
405
397
  s?.start(`Setting up ${addOn.name}...`)
406
398
  const addOnDir = resolve(addOn.directory, 'assets')
407
- if (existsSync(addOnDir)) {
399
+ if (environment.exists(addOnDir)) {
408
400
  await copyFilesRecursively(
401
+ environment,
409
402
  addOnDir,
410
403
  targetDir,
411
- copyFile,
412
404
  async (file: string, targetFileName?: string) =>
413
405
  templateFile(addOnDir, file, targetFileName),
414
406
  )
415
407
  }
416
408
 
417
409
  if (addOn.command) {
418
- await execa(addOn.command.command, addOn.command.args || [], {
419
- cwd: targetDir,
420
- })
410
+ await environment.execute(
411
+ addOn.command.command,
412
+ addOn.command.args || [],
413
+ targetDir,
414
+ )
421
415
  }
416
+
422
417
  s?.stop(`${addOn.name} setup complete`)
423
418
  }
424
419
  }
@@ -437,9 +432,11 @@ export async function createApp(
437
432
  s?.start(
438
433
  `Installing shadcn components (${Array.from(shadcnComponents).join(', ')})...`,
439
434
  )
440
- await execa('npx', ['shadcn@canary', 'add', ...shadcnComponents], {
441
- cwd: targetDir,
442
- })
435
+ await environment.execute(
436
+ 'npx',
437
+ ['shadcn@canary', 'add', ...shadcnComponents],
438
+ targetDir,
439
+ )
443
440
  s?.stop(`Installed shadcn components`)
444
441
  }
445
442
  }
@@ -449,13 +446,13 @@ export async function createApp(
449
446
  name: string
450
447
  path: string
451
448
  }> = []
452
- if (existsSync(resolve(targetDir, 'src/integrations'))) {
453
- for (const integration of readdirSync(
449
+ if (environment.exists(resolve(targetDir, 'src/integrations'))) {
450
+ for (const integration of environment.readdir(
454
451
  resolve(targetDir, 'src/integrations'),
455
452
  )) {
456
453
  const integrationName = jsSafeName(integration)
457
454
  if (
458
- existsSync(
455
+ environment.exists(
459
456
  resolve(targetDir, 'src/integrations', integration, 'layout.tsx'),
460
457
  )
461
458
  ) {
@@ -466,7 +463,7 @@ export async function createApp(
466
463
  })
467
464
  }
468
465
  if (
469
- existsSync(
466
+ environment.exists(
470
467
  resolve(targetDir, 'src/integrations', integration, 'provider.tsx'),
471
468
  )
472
469
  ) {
@@ -477,7 +474,7 @@ export async function createApp(
477
474
  })
478
475
  }
479
476
  if (
480
- existsSync(
477
+ environment.exists(
481
478
  resolve(
482
479
  targetDir,
483
480
  'src/integrations',
@@ -499,8 +496,8 @@ export async function createApp(
499
496
  path: string
500
497
  name: string
501
498
  }> = []
502
- if (existsSync(resolve(targetDir, 'src/routes'))) {
503
- for (const file of readdirSync(resolve(targetDir, 'src/routes'))) {
499
+ if (environment.exists(resolve(targetDir, 'src/routes'))) {
500
+ for (const file of environment.readdir(resolve(targetDir, 'src/routes'))) {
504
501
  const name = file.replace(/\.tsx?|\.jsx?/, '')
505
502
  const safeRouteName = jsSafeName(name)
506
503
  routes.push({
@@ -588,7 +585,7 @@ export async function createApp(
588
585
  }
589
586
 
590
587
  // Add .gitignore
591
- await copyFile(
588
+ await environment.copyFile(
592
589
  resolve(templateDirBase, '_dot_gitignore'),
593
590
  resolve(targetDir, '.gitignore'),
594
591
  )
@@ -598,7 +595,7 @@ export async function createApp(
598
595
 
599
596
  // Install dependencies
600
597
  s?.start(`Installing dependencies via ${options.packageManager}...`)
601
- await execa(options.packageManager, ['install'], { cwd: targetDir })
598
+ await environment.execute(options.packageManager, ['install'], targetDir)
602
599
  s?.stop(`Installed dependencies`)
603
600
 
604
601
  if (warnings.length > 0) {
@@ -612,14 +609,18 @@ export async function createApp(
612
609
  switch (options.packageManager) {
613
610
  case 'pnpm':
614
611
  // pnpm automatically forwards extra arguments
615
- await execa(options.packageManager, ['run', 'check', '--fix'], {
616
- cwd: targetDir,
617
- })
612
+ await environment.execute(
613
+ options.packageManager,
614
+ ['run', 'check', '--fix'],
615
+ targetDir,
616
+ )
618
617
  break
619
618
  default:
620
- await execa(options.packageManager, ['run', 'check', '--', '--fix'], {
621
- cwd: targetDir,
622
- })
619
+ await environment.execute(
620
+ options.packageManager,
621
+ ['run', 'check', '--', '--fix'],
622
+ targetDir,
623
+ )
623
624
  break
624
625
  }
625
626
  s?.stop(`Applied toolchain ${options.toolchain}...`)
@@ -627,10 +628,21 @@ export async function createApp(
627
628
 
628
629
  if (options.git) {
629
630
  s?.start(`Initializing git repository...`)
630
- await execa('git', ['init'], { cwd: targetDir })
631
+ await environment.execute('git', ['init'], targetDir)
631
632
  s?.stop(`Initialized git repository`)
632
633
  }
633
634
 
635
+ environment.finishRun()
636
+
637
+ let errorStatement = ''
638
+ if (environment.getErrors().length) {
639
+ errorStatement = `
640
+
641
+ ${chalk.red('There were errors encountered during this process:')}
642
+
643
+ ${environment.getErrors().join('\n')}`
644
+ }
645
+
634
646
  if (!silent) {
635
647
  outro(`Created your new TanStack app in '${basename(targetDir)}'.
636
648
 
@@ -638,7 +650,6 @@ Use the following commands to start your app:
638
650
  % cd ${options.projectName}
639
651
  % ${options.packageManager === 'deno' ? 'deno start' : options.packageManager} ${isAddOnEnabled('start') ? 'dev' : 'start'}
640
652
 
641
- Please read README.md for more information on testing, styling, adding routes, react-query, etc.
642
- `)
653
+ Please read README.md for more information on testing, styling, adding routes, react-query, etc.${errorStatement}`)
643
654
  }
644
655
  }
@@ -0,0 +1,70 @@
1
+ import {
2
+ appendFile,
3
+ copyFile,
4
+ mkdir,
5
+ readFile,
6
+ writeFile,
7
+ } from 'node:fs/promises'
8
+ import { existsSync, readdirSync, statSync } from 'node:fs'
9
+ import { dirname } from 'node:path'
10
+ import { execa } from 'execa'
11
+
12
+ export type Environment = {
13
+ startRun: () => void
14
+ finishRun: () => void
15
+ getErrors: () => Array<string>
16
+
17
+ appendFile: (path: string, contents: string) => Promise<void>
18
+ copyFile: (from: string, to: string) => Promise<void>
19
+ writeFile: (path: string, contents: string) => Promise<void>
20
+ execute: (command: string, args: Array<string>, cwd: string) => Promise<void>
21
+
22
+ readFile: (path: string, encoding?: BufferEncoding) => Promise<string>
23
+ exists: (path: string) => boolean
24
+ readdir: (path: string) => Array<string>
25
+ isDirectory: (path: string) => boolean
26
+ }
27
+
28
+ export function createDefaultEnvironment(): Environment {
29
+ let errors: Array<string> = []
30
+ return {
31
+ startRun: () => {
32
+ errors = []
33
+ },
34
+ finishRun: () => {},
35
+ getErrors: () => errors,
36
+
37
+ appendFile: async (path: string, contents: string) => {
38
+ await mkdir(dirname(path), { recursive: true })
39
+ return appendFile(path, contents)
40
+ },
41
+ copyFile: async (from: string, to: string) => {
42
+ await mkdir(dirname(to), { recursive: true })
43
+ return copyFile(from, to)
44
+ },
45
+ writeFile: async (path: string, contents: string) => {
46
+ await mkdir(dirname(path), { recursive: true })
47
+ return writeFile(path, contents)
48
+ },
49
+ execute: async (command: string, args: Array<string>, cwd: string) => {
50
+ try {
51
+ await execa(command, args, {
52
+ cwd,
53
+ })
54
+ } catch {
55
+ errors.push(
56
+ `Command "${command} ${args.join(' ')}" did not run successfully. Please run this manually in your project.`,
57
+ )
58
+ }
59
+ },
60
+
61
+ readFile: (path: string, encoding?: BufferEncoding) =>
62
+ readFile(path, { encoding: encoding || 'utf8' }),
63
+ exists: (path: string) => existsSync(path),
64
+ readdir: (path) => readdirSync(path),
65
+ isDirectory: (path) => {
66
+ const stat = statSync(path)
67
+ return stat.isDirectory()
68
+ },
69
+ }
70
+ }
package/src/mcp.ts CHANGED
@@ -6,6 +6,7 @@ import { z } from 'zod'
6
6
 
7
7
  import { createApp } from './create-app.js'
8
8
  import { finalizeAddOns } from './add-ons.js'
9
+ import { createDefaultEnvironment } from './environment.js'
9
10
 
10
11
  const server = new McpServer({
11
12
  name: 'Demo',
@@ -112,6 +113,7 @@ server.tool(
112
113
  },
113
114
  {
114
115
  silent: true,
116
+ environment: createDefaultEnvironment(),
115
117
  },
116
118
  )
117
119
  return {
@@ -206,6 +208,7 @@ server.tool(
206
208
  },
207
209
  {
208
210
  silent: true,
211
+ environment: createDefaultEnvironment(),
209
212
  },
210
213
  )
211
214
  return {
@@ -0,0 +1,120 @@
1
+ import { useStore } from '@tanstack/react-form'
2
+ import { useFieldContext, useFormContext } from '../hooks/demo.form-context'
3
+
4
+ export function SubscribeButton({ label }: { label: string }) {
5
+ const form = useFormContext()
6
+ return (
7
+ <form.Subscribe selector={(state) => state.isSubmitting}>
8
+ {(isSubmitting) => (
9
+ <button
10
+ disabled={isSubmitting}
11
+ className="px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-colors disabled:opacity-50"
12
+ >
13
+ {label}
14
+ </button>
15
+ )}
16
+ </form.Subscribe>
17
+ )
18
+ }
19
+
20
+ function ErrorMessages({
21
+ errors,
22
+ }: {
23
+ errors: Array<string | { message: string }>
24
+ }) {
25
+ return (
26
+ <>
27
+ {errors.map((error) => (
28
+ <div
29
+ key={typeof error === 'string' ? error : error.message}
30
+ className="text-red-500 mt-1 font-bold"
31
+ >
32
+ {typeof error === 'string' ? error : error.message}
33
+ </div>
34
+ ))}
35
+ </>
36
+ )
37
+ }
38
+
39
+ export function TextField({
40
+ label,
41
+ placeholder,
42
+ }: {
43
+ label: string
44
+ placeholder?: string
45
+ }) {
46
+ const field = useFieldContext<string>()
47
+ const errors = useStore(field.store, (state) => state.meta.errors)
48
+
49
+ return (
50
+ <div>
51
+ <label htmlFor={label} className="block font-bold mb-1 text-xl">
52
+ {label}
53
+ <input
54
+ value={field.state.value}
55
+ placeholder={placeholder}
56
+ onBlur={field.handleBlur}
57
+ onChange={(e) => field.handleChange(e.target.value)}
58
+ className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
59
+ />
60
+ </label>
61
+ {field.state.meta.isTouched && <ErrorMessages errors={errors} />}
62
+ </div>
63
+ )
64
+ }
65
+
66
+ export function TextArea({
67
+ label,
68
+ rows = 3,
69
+ }: {
70
+ label: string
71
+ rows?: number
72
+ }) {
73
+ const field = useFieldContext<string>()
74
+ const errors = useStore(field.store, (state) => state.meta.errors)
75
+
76
+ return (
77
+ <div>
78
+ <label htmlFor={label} className="block font-bold mb-1 text-xl">
79
+ {label}
80
+ <textarea
81
+ value={field.state.value}
82
+ onBlur={field.handleBlur}
83
+ rows={rows}
84
+ onChange={(e) => field.handleChange(e.target.value)}
85
+ className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
86
+ />
87
+ </label>
88
+ {field.state.meta.isTouched && <ErrorMessages errors={errors} />}
89
+ </div>
90
+ )
91
+ }
92
+
93
+ export function Select({
94
+ label,
95
+ children,
96
+ }: {
97
+ label: string
98
+ children: React.ReactNode
99
+ }) {
100
+ const field = useFieldContext<string>()
101
+ const errors = useStore(field.store, (state) => state.meta.errors)
102
+
103
+ return (
104
+ <div>
105
+ <label htmlFor={label} className="block font-bold mb-1 text-xl">
106
+ {label}
107
+ </label>
108
+ <select
109
+ name={field.name}
110
+ value={field.state.value}
111
+ onBlur={field.handleBlur}
112
+ onChange={(e) => field.handleChange(e.target.value)}
113
+ className="w-full px-4 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
114
+ >
115
+ {children}
116
+ </select>
117
+ {field.state.meta.isTouched && <ErrorMessages errors={errors} />}
118
+ </div>
119
+ )
120
+ }
@@ -0,0 +1,4 @@
1
+ import { createFormHookContexts } from '@tanstack/react-form'
2
+
3
+ export const { fieldContext, useFieldContext, formContext, useFormContext } =
4
+ createFormHookContexts()
@@ -0,0 +1,22 @@
1
+ import { createFormHook } from '@tanstack/react-form'
2
+
3
+ import {
4
+ Select,
5
+ SubscribeButton,
6
+ TextArea,
7
+ TextField,
8
+ } from '../components/demo.FormComponents'
9
+ import { fieldContext, formContext } from './demo.form-context'
10
+
11
+ export const { useAppForm } = createFormHook({
12
+ fieldComponents: {
13
+ TextField,
14
+ Select,
15
+ TextArea,
16
+ },
17
+ formComponents: {
18
+ SubscribeButton,
19
+ },
20
+ fieldContext,
21
+ formContext,
22
+ })