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/dist/cli.js +4 -1
- package/dist/create-app.js +54 -74
- package/dist/environment.js +43 -0
- package/dist/mcp.js +3 -0
- package/package.json +4 -2
- package/src/cli.ts +5 -1
- package/src/create-app.ts +92 -81
- package/src/environment.ts +70 -0
- package/src/mcp.ts +3 -0
- package/templates/react/add-on/form/assets/src/components/demo.FormComponents.tsx +120 -0
- package/templates/react/add-on/form/assets/src/hooks/demo.form-context.ts +4 -0
- package/templates/react/add-on/form/assets/src/hooks/demo.form.ts +22 -0
- package/templates/react/add-on/form/assets/src/routes/demo.form.address.tsx.ejs +203 -0
- package/templates/react/add-on/form/assets/src/routes/demo.form.simple.tsx.ejs +79 -0
- package/templates/react/add-on/form/info.json +6 -2
- package/templates/react/add-on/form/package.json +2 -1
- package/templates/react/base/README.md.ejs +1 -1
- package/templates/react/example/tanchat/info.json +1 -1
- package/templates/solid/add-on/form/assets/src/routes/demo.form.tsx.ejs +310 -106
- package/templates/solid/add-on/form/package.json +1 -1
- package/tests/cra.test.ts +112 -0
- package/tests/snapshots/cra/cr-js-npm.json +34 -0
- package/tests/snapshots/cra/cr-ts-npm.json +35 -0
- package/tests/snapshots/cra/fr-ts-npm.json +35 -0
- package/tests/snapshots/cra/fr-ts-tw-npm.json +34 -0
- package/tests/test-utilities.ts +69 -0
- package/templates/react/add-on/form/assets/src/routes/demo.form.tsx.ejs +0 -62
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
227
|
-
|
|
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(
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
419
|
-
|
|
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
|
|
441
|
-
|
|
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 (
|
|
453
|
-
for (const integration of
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
503
|
-
for (const file of
|
|
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
|
|
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
|
|
616
|
-
|
|
617
|
-
|
|
612
|
+
await environment.execute(
|
|
613
|
+
options.packageManager,
|
|
614
|
+
['run', 'check', '--fix'],
|
|
615
|
+
targetDir,
|
|
616
|
+
)
|
|
618
617
|
break
|
|
619
618
|
default:
|
|
620
|
-
await
|
|
621
|
-
|
|
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
|
|
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,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
|
+
})
|