create-start-app 0.5.0 → 0.6.2
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/README.md +8 -0
- package/dist/cli.js +11 -1
- package/dist/create-app.js +86 -67
- package/dist/environment.js +32 -0
- package/dist/mcp.js +22 -1
- package/dist/options.js +22 -1
- package/dist/toolchain.js +2 -0
- package/package.json +4 -2
- package/src/cli.ts +21 -1
- package/src/create-app.ts +134 -77
- package/src/environment.ts +53 -0
- package/src/mcp.ts +24 -1
- package/src/options.ts +22 -1
- package/src/toolchain.ts +3 -0
- package/src/types.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 +11 -1
- package/templates/react/base/_dot_vscode/settings.biome.json +38 -0
- package/templates/react/base/package.biome.json +10 -0
- package/templates/react/base/toolchain/biome.json +31 -0
- package/templates/react/code-router/src/main.tsx.ejs +2 -2
- package/templates/react/example/tanchat/info.json +1 -1
- package/templates/react/file-router/src/main.tsx.ejs +2 -2
- 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/templates/solid/base/_dot_vscode/settings.biome.json +38 -0
- package/templates/solid/base/package.biome.json +10 -0
- package/templates/solid/base/toolchain/biome.json +31 -0
- package/templates/solid/code-router/src/main.tsx.ejs +4 -2
- package/templates/solid/file-router/src/main.tsx.ejs +4 -2
- package/templates/solid/file-router/src/routes/__root.tsx.ejs +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/README.md
CHANGED
|
@@ -30,6 +30,7 @@ This will start an interactive CLI that guides you through the setup process, al
|
|
|
30
30
|
- TypeScript support
|
|
31
31
|
- Tailwind CSS integration
|
|
32
32
|
- Package manager
|
|
33
|
+
- Toolchain
|
|
33
34
|
- Git initialization
|
|
34
35
|
|
|
35
36
|
## Command Line Options
|
|
@@ -45,6 +46,7 @@ Available options:
|
|
|
45
46
|
- `--template <type>`: Choose between `file-router`, `typescript`, or `javascript`
|
|
46
47
|
- `--tailwind`: Enable Tailwind CSS
|
|
47
48
|
- `--package-manager`: Specify your preferred package manager (`npm`, `yarn`, `pnpm`, `bun`, or `deno`)
|
|
49
|
+
- `--toolchain`: Specify your toolchain solution for formatting/linting (`biome`)
|
|
48
50
|
- `--no-git`: Do not initialize a git repository
|
|
49
51
|
- `--add-ons`: Enable add-on selection or specify add-ons to install
|
|
50
52
|
|
|
@@ -94,6 +96,12 @@ Choose your preferred package manager (`npm`, `bun`, `yarn`, `pnpm`, or `deno`)
|
|
|
94
96
|
|
|
95
97
|
Extensive documentation on using the TanStack Router, migrating to a File Base Routing approach, as well as integrating [@tanstack/react-query](https://tanstack.com/query/latest) and [@tanstack/store](https://tanstack.com/store/latest) can be found in the generated `README.md` for your project.
|
|
96
98
|
|
|
99
|
+
### Toolchain
|
|
100
|
+
|
|
101
|
+
Choose your preferred solution for formatting and linting either through the interactive CLI or using the `--toolchain` flag.
|
|
102
|
+
|
|
103
|
+
Setting this flag to `biome` will configure it as your toolchain of choice, adding a `biome.json` to the root of the project. Consult the [biome documentation](https://biomejs.dev/guides/getting-started/) for further customization.
|
|
104
|
+
|
|
97
105
|
## Add-ons (experimental)
|
|
98
106
|
|
|
99
107
|
You can enable add-on selection:
|
package/dist/cli.js
CHANGED
|
@@ -3,9 +3,11 @@ import { intro, log } from '@clack/prompts';
|
|
|
3
3
|
import { createApp } from './create-app.js';
|
|
4
4
|
import { normalizeOptions, promptForOptions } from './options.js';
|
|
5
5
|
import { SUPPORTED_PACKAGE_MANAGERS } from './package-manager.js';
|
|
6
|
+
import { SUPPORTED_TOOLCHAINS } from './toolchain.js';
|
|
6
7
|
import runServer from './mcp.js';
|
|
7
8
|
import { listAddOns } from './add-ons.js';
|
|
8
9
|
import { DEFAULT_FRAMEWORK, SUPPORTED_FRAMEWORKS } from './constants.js';
|
|
10
|
+
import { createDefaultEnvironment } from './environment.js';
|
|
9
11
|
export function cli() {
|
|
10
12
|
const program = new Command();
|
|
11
13
|
program
|
|
@@ -32,6 +34,12 @@ export function cli() {
|
|
|
32
34
|
throw new InvalidArgumentError(`Invalid package manager: ${value}. The following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(', ')}`);
|
|
33
35
|
}
|
|
34
36
|
return value;
|
|
37
|
+
})
|
|
38
|
+
.option(`--toolchain <${SUPPORTED_TOOLCHAINS.join('|')}>`, `Explicitly tell the CLI to use this toolchain`, (value) => {
|
|
39
|
+
if (!SUPPORTED_TOOLCHAINS.includes(value)) {
|
|
40
|
+
throw new InvalidArgumentError(`Invalid toolchain: ${value}. The following are allowed: ${SUPPORTED_TOOLCHAINS.join(', ')}`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
35
43
|
})
|
|
36
44
|
.option('--tailwind', 'add Tailwind CSS', false)
|
|
37
45
|
.option('--add-ons [...add-ons]', 'pick from a list of available add-ons (comma separated list)', (value) => {
|
|
@@ -65,7 +73,9 @@ export function cli() {
|
|
|
65
73
|
intro("Let's configure your TanStack application");
|
|
66
74
|
finalOptions = await promptForOptions(cliOptions);
|
|
67
75
|
}
|
|
68
|
-
await createApp(finalOptions
|
|
76
|
+
await createApp(finalOptions, {
|
|
77
|
+
environment: createDefaultEnvironment(),
|
|
78
|
+
});
|
|
69
79
|
}
|
|
70
80
|
catch (error) {
|
|
71
81
|
log.error(error instanceof Error
|
package/dist/create-app.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { appendFile, copyFile, mkdir, readFile, writeFile, } from 'node:fs/promises';
|
|
3
|
-
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
4
1
|
import { basename, dirname, resolve } from 'node:path';
|
|
5
2
|
import { fileURLToPath } from 'node:url';
|
|
6
3
|
import { log, outro, spinner } from '@clack/prompts';
|
|
7
|
-
import { execa } from 'execa';
|
|
8
4
|
import { render } from 'ejs';
|
|
9
5
|
import { format } from 'prettier';
|
|
10
6
|
import chalk from 'chalk';
|
|
@@ -17,11 +13,17 @@ function sortObject(obj) {
|
|
|
17
13
|
return acc;
|
|
18
14
|
}, {});
|
|
19
15
|
}
|
|
20
|
-
function createCopyFiles(targetDir) {
|
|
21
|
-
return async function copyFiles(templateDir, files
|
|
16
|
+
function createCopyFiles(environment, targetDir) {
|
|
17
|
+
return async function copyFiles(templateDir, files,
|
|
18
|
+
// optionally copy files from a folder to the root
|
|
19
|
+
toRoot) {
|
|
22
20
|
for (const file of files) {
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
let targetFileName = file.replace('.tw', '');
|
|
22
|
+
if (toRoot) {
|
|
23
|
+
const fileNoPath = targetFileName.split('/').pop();
|
|
24
|
+
targetFileName = fileNoPath ? `./${fileNoPath}` : targetFileName;
|
|
25
|
+
}
|
|
26
|
+
await environment.copyFile(resolve(templateDir, file), resolve(targetDir, targetFileName));
|
|
25
27
|
}
|
|
26
28
|
};
|
|
27
29
|
}
|
|
@@ -31,13 +33,14 @@ function jsSafeName(name) {
|
|
|
31
33
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
32
34
|
.join('');
|
|
33
35
|
}
|
|
34
|
-
function createTemplateFile(projectName, options, targetDir) {
|
|
36
|
+
function createTemplateFile(environment, projectName, options, targetDir) {
|
|
35
37
|
return async function templateFile(templateDir, file, targetFileName, extraTemplateValues) {
|
|
36
38
|
const templateValues = {
|
|
37
39
|
packageManager: options.packageManager,
|
|
38
40
|
projectName: projectName,
|
|
39
41
|
typescript: options.typescript,
|
|
40
42
|
tailwind: options.tailwind,
|
|
43
|
+
toolchain: options.toolchain,
|
|
41
44
|
js: options.typescript ? 'ts' : 'js',
|
|
42
45
|
jsx: options.typescript ? 'tsx' : 'jsx',
|
|
43
46
|
fileRouter: options.mode === FILE_ROUTER,
|
|
@@ -49,7 +52,7 @@ function createTemplateFile(projectName, options, targetDir) {
|
|
|
49
52
|
addOns: options.chosenAddOns,
|
|
50
53
|
...extraTemplateValues,
|
|
51
54
|
};
|
|
52
|
-
const template = await readFile(resolve(templateDir, file), 'utf-8');
|
|
55
|
+
const template = await environment.readFile(resolve(templateDir, file), 'utf-8');
|
|
53
56
|
let content = '';
|
|
54
57
|
try {
|
|
55
58
|
content = render(template, templateValues);
|
|
@@ -68,17 +71,14 @@ function createTemplateFile(projectName, options, targetDir) {
|
|
|
68
71
|
parser: 'typescript',
|
|
69
72
|
});
|
|
70
73
|
}
|
|
71
|
-
await
|
|
72
|
-
recursive: true,
|
|
73
|
-
});
|
|
74
|
-
await writeFile(resolve(targetDir, target), content);
|
|
74
|
+
await environment.writeFile(resolve(targetDir, target), content);
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
-
async function createPackageJSON(projectName, options, templateDir, routerDir, targetDir, addOns) {
|
|
78
|
-
let packageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.json'), 'utf8'));
|
|
77
|
+
async function createPackageJSON(environment, projectName, options, templateDir, routerDir, targetDir, addOns) {
|
|
78
|
+
let packageJSON = JSON.parse(await environment.readFile(resolve(templateDir, 'package.json'), 'utf8'));
|
|
79
79
|
packageJSON.name = projectName;
|
|
80
80
|
if (options.typescript) {
|
|
81
|
-
const tsPackageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.ts.json'), 'utf8'));
|
|
81
|
+
const tsPackageJSON = JSON.parse(await environment.readFile(resolve(templateDir, 'package.ts.json'), 'utf8'));
|
|
82
82
|
packageJSON = {
|
|
83
83
|
...packageJSON,
|
|
84
84
|
devDependencies: {
|
|
@@ -88,7 +88,7 @@ async function createPackageJSON(projectName, options, templateDir, routerDir, t
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
if (options.tailwind) {
|
|
91
|
-
const twPackageJSON = JSON.parse(await readFile(resolve(templateDir, 'package.tw.json'), 'utf8'));
|
|
91
|
+
const twPackageJSON = JSON.parse(await environment.readFile(resolve(templateDir, 'package.tw.json'), 'utf8'));
|
|
92
92
|
packageJSON = {
|
|
93
93
|
...packageJSON,
|
|
94
94
|
dependencies: {
|
|
@@ -97,8 +97,22 @@ async function createPackageJSON(projectName, options, templateDir, routerDir, t
|
|
|
97
97
|
},
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
|
+
if (options.toolchain === 'biome') {
|
|
101
|
+
const biomePackageJSON = JSON.parse(await environment.readFile(resolve(templateDir, 'package.biome.json'), 'utf8'));
|
|
102
|
+
packageJSON = {
|
|
103
|
+
...packageJSON,
|
|
104
|
+
scripts: {
|
|
105
|
+
...packageJSON.scripts,
|
|
106
|
+
...biomePackageJSON.scripts,
|
|
107
|
+
},
|
|
108
|
+
devDependencies: {
|
|
109
|
+
...packageJSON.devDependencies,
|
|
110
|
+
...biomePackageJSON.devDependencies,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
100
114
|
if (options.mode === FILE_ROUTER) {
|
|
101
|
-
const frPackageJSON = JSON.parse(await readFile(resolve(routerDir, 'package.fr.json'), 'utf8'));
|
|
115
|
+
const frPackageJSON = JSON.parse(await environment.readFile(resolve(routerDir, 'package.fr.json'), 'utf8'));
|
|
102
116
|
packageJSON = {
|
|
103
117
|
...packageJSON,
|
|
104
118
|
dependencies: {
|
|
@@ -126,16 +140,15 @@ async function createPackageJSON(projectName, options, templateDir, routerDir, t
|
|
|
126
140
|
}
|
|
127
141
|
packageJSON.dependencies = sortObject(packageJSON.dependencies);
|
|
128
142
|
packageJSON.devDependencies = sortObject(packageJSON.devDependencies);
|
|
129
|
-
await writeFile(resolve(targetDir, 'package.json'), JSON.stringify(packageJSON, null, 2));
|
|
143
|
+
await environment.writeFile(resolve(targetDir, 'package.json'), JSON.stringify(packageJSON, null, 2));
|
|
130
144
|
}
|
|
131
|
-
async function copyFilesRecursively(source, target,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const files = readdirSync(source);
|
|
145
|
+
async function copyFilesRecursively(environment, source, target, templateFile) {
|
|
146
|
+
if (environment.isDirectory(source)) {
|
|
147
|
+
const files = environment.readdir(source);
|
|
135
148
|
for (const file of files) {
|
|
136
149
|
const sourceChild = resolve(source, file);
|
|
137
150
|
const targetChild = resolve(target, file);
|
|
138
|
-
await copyFilesRecursively(sourceChild, targetChild,
|
|
151
|
+
await copyFilesRecursively(environment, sourceChild, targetChild, templateFile);
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
154
|
else {
|
|
@@ -151,42 +164,42 @@ async function copyFilesRecursively(source, target, copyFile, templateFile) {
|
|
|
151
164
|
isAppend = true;
|
|
152
165
|
}
|
|
153
166
|
const targetPath = resolve(dirname(target), targetFile);
|
|
154
|
-
await mkdir(dirname(targetPath), {
|
|
155
|
-
recursive: true,
|
|
156
|
-
});
|
|
157
167
|
if (isTemplate) {
|
|
158
168
|
await templateFile(source, targetPath);
|
|
159
169
|
}
|
|
160
170
|
else {
|
|
161
171
|
if (isAppend) {
|
|
162
|
-
await appendFile(targetPath, (await readFile(source)).toString());
|
|
172
|
+
await environment.appendFile(targetPath, (await environment.readFile(source)).toString());
|
|
163
173
|
}
|
|
164
174
|
else {
|
|
165
|
-
await copyFile(source, targetPath);
|
|
175
|
+
await environment.copyFile(source, targetPath);
|
|
166
176
|
}
|
|
167
177
|
}
|
|
168
178
|
}
|
|
169
179
|
}
|
|
170
|
-
export async function createApp(options, { silent = false,
|
|
180
|
+
export async function createApp(options, { silent = false, environment, }) {
|
|
171
181
|
const templateDirBase = fileURLToPath(new URL(`../templates/${options.framework}/base`, import.meta.url));
|
|
172
182
|
const templateDirRouter = fileURLToPath(new URL(`../templates/${options.framework}/${options.mode}`, import.meta.url));
|
|
173
183
|
const targetDir = resolve(process.cwd(), options.projectName);
|
|
174
|
-
if (
|
|
184
|
+
if (environment.exists(targetDir)) {
|
|
175
185
|
if (!silent) {
|
|
176
186
|
log.error(`Directory "${options.projectName}" already exists`);
|
|
177
187
|
}
|
|
178
188
|
return;
|
|
179
189
|
}
|
|
180
|
-
const copyFiles = createCopyFiles(targetDir);
|
|
181
|
-
const templateFile = createTemplateFile(options.projectName, options, targetDir);
|
|
190
|
+
const copyFiles = createCopyFiles(environment, targetDir);
|
|
191
|
+
const templateFile = createTemplateFile(environment, options.projectName, options, targetDir);
|
|
182
192
|
const isAddOnEnabled = (id) => options.chosenAddOns.find((a) => a.id === id);
|
|
183
|
-
// Make the root directory
|
|
184
|
-
await mkdir(targetDir, { recursive: true });
|
|
185
193
|
// Setup the .vscode directory
|
|
186
|
-
|
|
187
|
-
|
|
194
|
+
switch (options.toolchain) {
|
|
195
|
+
case 'biome':
|
|
196
|
+
await environment.copyFile(resolve(templateDirBase, '_dot_vscode/settings.biome.json'), resolve(targetDir, '.vscode/settings.json'));
|
|
197
|
+
break;
|
|
198
|
+
case 'none':
|
|
199
|
+
default:
|
|
200
|
+
await environment.copyFile(resolve(templateDirBase, '_dot_vscode/settings.json'), resolve(targetDir, '.vscode/settings.json'));
|
|
201
|
+
}
|
|
188
202
|
// Fill the public directory
|
|
189
|
-
await mkdir(resolve(targetDir, 'public'), { recursive: true });
|
|
190
203
|
copyFiles(templateDirBase, [
|
|
191
204
|
'./public/robots.txt',
|
|
192
205
|
'./public/favicon.ico',
|
|
@@ -194,15 +207,9 @@ export async function createApp(options, { silent = false, } = {}) {
|
|
|
194
207
|
'./public/logo192.png',
|
|
195
208
|
'./public/logo512.png',
|
|
196
209
|
]);
|
|
197
|
-
// Make the src directory
|
|
198
|
-
await mkdir(resolve(targetDir, 'src'), { recursive: true });
|
|
199
|
-
if (options.mode === FILE_ROUTER) {
|
|
200
|
-
await mkdir(resolve(targetDir, 'src/routes'), { recursive: true });
|
|
201
|
-
await mkdir(resolve(targetDir, 'src/components'), { recursive: true });
|
|
202
|
-
}
|
|
203
210
|
// Check for a .cursorrules file
|
|
204
|
-
if (
|
|
205
|
-
await copyFile(resolve(templateDirBase, '.cursorrules'), resolve(targetDir, '.cursorrules'));
|
|
211
|
+
if (environment.exists(resolve(templateDirBase, '.cursorrules'))) {
|
|
212
|
+
await environment.copyFile(resolve(templateDirBase, '.cursorrules'), resolve(targetDir, '.cursorrules'));
|
|
206
213
|
}
|
|
207
214
|
// Copy in Vite and Tailwind config and CSS
|
|
208
215
|
if (!options.tailwind) {
|
|
@@ -211,6 +218,9 @@ export async function createApp(options, { silent = false, } = {}) {
|
|
|
211
218
|
await templateFile(templateDirBase, './vite.config.js.ejs');
|
|
212
219
|
await templateFile(templateDirBase, './src/styles.css.ejs');
|
|
213
220
|
copyFiles(templateDirBase, ['./src/logo.svg']);
|
|
221
|
+
if (options.toolchain === 'biome') {
|
|
222
|
+
copyFiles(templateDirBase, ['./toolchain/biome.json'], true);
|
|
223
|
+
}
|
|
214
224
|
// Setup the main, reportWebVitals and index.html files
|
|
215
225
|
if (!isAddOnEnabled('start') && options.framework === 'react') {
|
|
216
226
|
if (options.typescript) {
|
|
@@ -227,21 +237,19 @@ export async function createApp(options, { silent = false, } = {}) {
|
|
|
227
237
|
if (options.typescript) {
|
|
228
238
|
await templateFile(templateDirBase, './tsconfig.json.ejs', './tsconfig.json');
|
|
229
239
|
}
|
|
230
|
-
// Setup the package.json file, optionally with typescript and
|
|
231
|
-
await createPackageJSON(options.projectName, options, templateDirBase, templateDirRouter, targetDir, options.chosenAddOns.map((addOn) => addOn.packageAdditions));
|
|
240
|
+
// Setup the package.json file, optionally with typescript, tailwind and biome
|
|
241
|
+
await createPackageJSON(environment, options.projectName, options, templateDirBase, templateDirRouter, targetDir, options.chosenAddOns.map((addOn) => addOn.packageAdditions));
|
|
232
242
|
// Copy all the asset files from the addons
|
|
233
243
|
const s = silent ? null : spinner();
|
|
234
244
|
for (const phase of ['setup', 'add-on', 'example']) {
|
|
235
245
|
for (const addOn of options.chosenAddOns.filter((addOn) => addOn.phase === phase)) {
|
|
236
246
|
s?.start(`Setting up ${addOn.name}...`);
|
|
237
247
|
const addOnDir = resolve(addOn.directory, 'assets');
|
|
238
|
-
if (
|
|
239
|
-
await copyFilesRecursively(addOnDir, targetDir,
|
|
248
|
+
if (environment.exists(addOnDir)) {
|
|
249
|
+
await copyFilesRecursively(environment, addOnDir, targetDir, async (file, targetFileName) => templateFile(addOnDir, file, targetFileName));
|
|
240
250
|
}
|
|
241
251
|
if (addOn.command) {
|
|
242
|
-
await
|
|
243
|
-
cwd: targetDir,
|
|
244
|
-
});
|
|
252
|
+
await environment.execute(addOn.command.command, addOn.command.args || [], targetDir);
|
|
245
253
|
}
|
|
246
254
|
s?.stop(`${addOn.name} setup complete`);
|
|
247
255
|
}
|
|
@@ -257,31 +265,29 @@ export async function createApp(options, { silent = false, } = {}) {
|
|
|
257
265
|
}
|
|
258
266
|
if (shadcnComponents.size > 0) {
|
|
259
267
|
s?.start(`Installing shadcn components (${Array.from(shadcnComponents).join(', ')})...`);
|
|
260
|
-
await
|
|
261
|
-
cwd: targetDir,
|
|
262
|
-
});
|
|
268
|
+
await environment.execute('npx', ['shadcn@canary', 'add', ...shadcnComponents], targetDir);
|
|
263
269
|
s?.stop(`Installed shadcn components`);
|
|
264
270
|
}
|
|
265
271
|
}
|
|
266
272
|
const integrations = [];
|
|
267
|
-
if (
|
|
268
|
-
for (const integration of
|
|
273
|
+
if (environment.exists(resolve(targetDir, 'src/integrations'))) {
|
|
274
|
+
for (const integration of environment.readdir(resolve(targetDir, 'src/integrations'))) {
|
|
269
275
|
const integrationName = jsSafeName(integration);
|
|
270
|
-
if (
|
|
276
|
+
if (environment.exists(resolve(targetDir, 'src/integrations', integration, 'layout.tsx'))) {
|
|
271
277
|
integrations.push({
|
|
272
278
|
type: 'layout',
|
|
273
279
|
name: `${integrationName}Layout`,
|
|
274
280
|
path: `integrations/${integration}/layout`,
|
|
275
281
|
});
|
|
276
282
|
}
|
|
277
|
-
if (
|
|
283
|
+
if (environment.exists(resolve(targetDir, 'src/integrations', integration, 'provider.tsx'))) {
|
|
278
284
|
integrations.push({
|
|
279
285
|
type: 'provider',
|
|
280
286
|
name: `${integrationName}Provider`,
|
|
281
287
|
path: `integrations/${integration}/provider`,
|
|
282
288
|
});
|
|
283
289
|
}
|
|
284
|
-
if (
|
|
290
|
+
if (environment.exists(resolve(targetDir, 'src/integrations', integration, 'header-user.tsx'))) {
|
|
285
291
|
integrations.push({
|
|
286
292
|
type: 'header-user',
|
|
287
293
|
name: `${integrationName}Header`,
|
|
@@ -291,8 +297,8 @@ export async function createApp(options, { silent = false, } = {}) {
|
|
|
291
297
|
}
|
|
292
298
|
}
|
|
293
299
|
const routes = [];
|
|
294
|
-
if (
|
|
295
|
-
for (const file of
|
|
300
|
+
if (environment.exists(resolve(targetDir, 'src/routes'))) {
|
|
301
|
+
for (const file of environment.readdir(resolve(targetDir, 'src/routes'))) {
|
|
296
302
|
const name = file.replace(/\.tsx?|\.jsx?/, '');
|
|
297
303
|
const safeRouteName = jsSafeName(name);
|
|
298
304
|
routes.push({
|
|
@@ -343,21 +349,34 @@ export async function createApp(options, { silent = false, } = {}) {
|
|
|
343
349
|
}
|
|
344
350
|
}
|
|
345
351
|
// Add .gitignore
|
|
346
|
-
await copyFile(resolve(templateDirBase, '_dot_gitignore'), resolve(targetDir, '.gitignore'));
|
|
352
|
+
await environment.copyFile(resolve(templateDirBase, '_dot_gitignore'), resolve(targetDir, '.gitignore'));
|
|
347
353
|
// Create the README.md
|
|
348
354
|
await templateFile(templateDirBase, 'README.md.ejs');
|
|
349
355
|
// Install dependencies
|
|
350
356
|
s?.start(`Installing dependencies via ${options.packageManager}...`);
|
|
351
|
-
await
|
|
357
|
+
await environment.execute(options.packageManager, ['install'], targetDir);
|
|
352
358
|
s?.stop(`Installed dependencies`);
|
|
353
359
|
if (warnings.length > 0) {
|
|
354
360
|
if (!silent) {
|
|
355
361
|
log.warn(chalk.red(warnings.join('\n')));
|
|
356
362
|
}
|
|
357
363
|
}
|
|
364
|
+
if (options.toolchain === 'biome') {
|
|
365
|
+
s?.start(`Applying toolchain ${options.toolchain}...`);
|
|
366
|
+
switch (options.packageManager) {
|
|
367
|
+
case 'pnpm':
|
|
368
|
+
// pnpm automatically forwards extra arguments
|
|
369
|
+
await environment.execute(options.packageManager, ['run', 'check', '--fix'], targetDir);
|
|
370
|
+
break;
|
|
371
|
+
default:
|
|
372
|
+
await environment.execute(options.packageManager, ['run', 'check', '--', '--fix'], targetDir);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
s?.stop(`Applied toolchain ${options.toolchain}...`);
|
|
376
|
+
}
|
|
358
377
|
if (options.git) {
|
|
359
378
|
s?.start(`Initializing git repository...`);
|
|
360
|
-
await
|
|
379
|
+
await environment.execute('git', ['init'], targetDir);
|
|
361
380
|
s?.stop(`Initialized git repository`);
|
|
362
381
|
}
|
|
363
382
|
if (!silent) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { appendFile, copyFile, mkdir, readFile, writeFile, } from 'node:fs/promises';
|
|
2
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
export function createDefaultEnvironment() {
|
|
6
|
+
return {
|
|
7
|
+
appendFile: async (path, contents) => {
|
|
8
|
+
await mkdir(dirname(path), { recursive: true });
|
|
9
|
+
return appendFile(path, contents);
|
|
10
|
+
},
|
|
11
|
+
copyFile: async (from, to) => {
|
|
12
|
+
await mkdir(dirname(to), { recursive: true });
|
|
13
|
+
return copyFile(from, to);
|
|
14
|
+
},
|
|
15
|
+
writeFile: async (path, contents) => {
|
|
16
|
+
await mkdir(dirname(path), { recursive: true });
|
|
17
|
+
return writeFile(path, contents);
|
|
18
|
+
},
|
|
19
|
+
execute: async (command, args, cwd) => {
|
|
20
|
+
await execa(command, args, {
|
|
21
|
+
cwd,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
readFile: (path, encoding) => readFile(path, { encoding: encoding || 'utf8' }),
|
|
25
|
+
exists: (path) => existsSync(path),
|
|
26
|
+
readdir: (path) => readdirSync(path),
|
|
27
|
+
isDirectory: (path) => {
|
|
28
|
+
const stat = statSync(path);
|
|
29
|
+
return stat.isDirectory();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -5,6 +5,7 @@ import express from 'express';
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { createApp } from './create-app.js';
|
|
7
7
|
import { finalizeAddOns } from './add-ons.js';
|
|
8
|
+
import { createDefaultEnvironment } from './environment.js';
|
|
8
9
|
const server = new McpServer({
|
|
9
10
|
name: 'Demo',
|
|
10
11
|
version: '1.0.0',
|
|
@@ -46,6 +47,10 @@ const tanStackReactAddOns = [
|
|
|
46
47
|
id: 'store',
|
|
47
48
|
description: 'Enable the TanStack Store state management library',
|
|
48
49
|
},
|
|
50
|
+
{
|
|
51
|
+
id: 'tanchat',
|
|
52
|
+
description: 'Add an AI chatbot example to the application',
|
|
53
|
+
},
|
|
49
54
|
];
|
|
50
55
|
server.tool('listTanStackReactAddOns', {}, () => {
|
|
51
56
|
return {
|
|
@@ -68,6 +73,7 @@ server.tool('createTanStackReactApplication', {
|
|
|
68
73
|
'start',
|
|
69
74
|
'store',
|
|
70
75
|
'tanstack-query',
|
|
76
|
+
'tanchat',
|
|
71
77
|
]))
|
|
72
78
|
.describe('The IDs of the add-ons to install'),
|
|
73
79
|
}, async ({ projectName, addOns, cwd }) => {
|
|
@@ -80,6 +86,7 @@ server.tool('createTanStackReactApplication', {
|
|
|
80
86
|
typescript: true,
|
|
81
87
|
tailwind: true,
|
|
82
88
|
packageManager: 'pnpm',
|
|
89
|
+
toolchain: 'none',
|
|
83
90
|
mode: 'file-router',
|
|
84
91
|
addOns: true,
|
|
85
92
|
chosenAddOns,
|
|
@@ -87,6 +94,7 @@ server.tool('createTanStackReactApplication', {
|
|
|
87
94
|
variableValues: {},
|
|
88
95
|
}, {
|
|
89
96
|
silent: true,
|
|
97
|
+
environment: createDefaultEnvironment(),
|
|
90
98
|
});
|
|
91
99
|
return {
|
|
92
100
|
content: [{ type: 'text', text: 'Application created successfully' }],
|
|
@@ -121,6 +129,10 @@ const tanStackSolidAddOns = [
|
|
|
121
129
|
id: 'tanstack-query',
|
|
122
130
|
description: 'Enable TanStack Query for data fetching',
|
|
123
131
|
},
|
|
132
|
+
{
|
|
133
|
+
id: 'tanchat',
|
|
134
|
+
description: 'Add an AI chatbot example to the application',
|
|
135
|
+
},
|
|
124
136
|
];
|
|
125
137
|
server.tool('listTanStackSolidAddOns', {}, () => {
|
|
126
138
|
return {
|
|
@@ -133,7 +145,14 @@ server.tool('createTanStackSolidApplication', {
|
|
|
133
145
|
.describe('The package.json module name of the application (will also be the directory name)'),
|
|
134
146
|
cwd: z.string().describe('The directory to create the application in'),
|
|
135
147
|
addOns: z
|
|
136
|
-
.array(z.enum([
|
|
148
|
+
.array(z.enum([
|
|
149
|
+
'solid-ui',
|
|
150
|
+
'form',
|
|
151
|
+
'sentry',
|
|
152
|
+
'store',
|
|
153
|
+
'tanstack-query',
|
|
154
|
+
'tanchat',
|
|
155
|
+
]))
|
|
137
156
|
.describe('The IDs of the add-ons to install'),
|
|
138
157
|
}, async ({ projectName, addOns, cwd }) => {
|
|
139
158
|
try {
|
|
@@ -145,6 +164,7 @@ server.tool('createTanStackSolidApplication', {
|
|
|
145
164
|
typescript: true,
|
|
146
165
|
tailwind: true,
|
|
147
166
|
packageManager: 'pnpm',
|
|
167
|
+
toolchain: 'none',
|
|
148
168
|
mode: 'file-router',
|
|
149
169
|
addOns: true,
|
|
150
170
|
chosenAddOns,
|
|
@@ -152,6 +172,7 @@ server.tool('createTanStackSolidApplication', {
|
|
|
152
172
|
variableValues: {},
|
|
153
173
|
}, {
|
|
154
174
|
silent: true,
|
|
175
|
+
environment: createDefaultEnvironment(),
|
|
155
176
|
});
|
|
156
177
|
return {
|
|
157
178
|
content: [{ type: 'text', text: 'Application created successfully' }],
|
package/dist/options.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cancel, confirm, isCancel, multiselect, select, text, } from '@clack/prompts';
|
|
2
2
|
import { DEFAULT_PACKAGE_MANAGER, SUPPORTED_PACKAGE_MANAGERS, getPackageManager, } from './package-manager.js';
|
|
3
|
+
import { DEFAULT_TOOLCHAIN, SUPPORTED_TOOLCHAINS } from './toolchain.js';
|
|
3
4
|
import { CODE_ROUTER, DEFAULT_FRAMEWORK, FILE_ROUTER } from './constants.js';
|
|
4
5
|
import { finalizeAddOns, getAllAddOns } from './add-ons.js';
|
|
5
6
|
// If all CLI options are provided, use them directly
|
|
@@ -26,6 +27,7 @@ export async function normalizeOptions(cliOptions) {
|
|
|
26
27
|
typescript,
|
|
27
28
|
tailwind,
|
|
28
29
|
packageManager: cliOptions.packageManager || DEFAULT_PACKAGE_MANAGER,
|
|
30
|
+
toolchain: cliOptions.toolchain || DEFAULT_TOOLCHAIN,
|
|
29
31
|
mode: cliOptions.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER,
|
|
30
32
|
git: !!cliOptions.git,
|
|
31
33
|
addOns,
|
|
@@ -145,7 +147,7 @@ export async function promptForOptions(cliOptions) {
|
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
// Tailwind selection
|
|
148
|
-
if (cliOptions.tailwind
|
|
150
|
+
if (!cliOptions.tailwind && options.framework === 'react') {
|
|
149
151
|
const tailwind = await confirm({
|
|
150
152
|
message: 'Would you like to use Tailwind CSS?',
|
|
151
153
|
initialValue: true,
|
|
@@ -184,6 +186,25 @@ export async function promptForOptions(cliOptions) {
|
|
|
184
186
|
else {
|
|
185
187
|
options.packageManager = cliOptions.packageManager;
|
|
186
188
|
}
|
|
189
|
+
// Toolchain selection
|
|
190
|
+
if (cliOptions.toolchain === undefined) {
|
|
191
|
+
const tc = await select({
|
|
192
|
+
message: 'Select toolchain',
|
|
193
|
+
options: SUPPORTED_TOOLCHAINS.map((tc) => ({
|
|
194
|
+
value: tc,
|
|
195
|
+
label: tc,
|
|
196
|
+
})),
|
|
197
|
+
initialValue: DEFAULT_TOOLCHAIN,
|
|
198
|
+
});
|
|
199
|
+
if (isCancel(tc)) {
|
|
200
|
+
cancel('Operation cancelled.');
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
options.toolchain = tc;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
options.toolchain = cliOptions.toolchain;
|
|
207
|
+
}
|
|
187
208
|
options.chosenAddOns = [];
|
|
188
209
|
if (Array.isArray(cliOptions.addOns)) {
|
|
189
210
|
options.chosenAddOns = await finalizeAddOns(options.framework, options.mode, cliOptions.addOns);
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-start-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Tanstack Application Builder",
|
|
5
5
|
"bin": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"start": "tsc && node dist/index.js",
|
|
10
|
-
"test": "npm run test:lint",
|
|
10
|
+
"test": "npm run test:lint && vitest --run",
|
|
11
|
+
"test:watch": "vitest",
|
|
11
12
|
"cipublish": "node scripts/publish.js",
|
|
12
13
|
"test:lint": "eslint ./src",
|
|
13
14
|
"mcp": "tsc && npx @modelcontextprotocol/inspector dist/index.js --mcp"
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"execa": "^9.5.2",
|
|
40
41
|
"express": "^4.21.2",
|
|
41
42
|
"prettier": "^3.5.0",
|
|
43
|
+
"vitest": "^3.0.8",
|
|
42
44
|
"zod": "^3.24.2"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
package/src/cli.ts
CHANGED
|
@@ -4,12 +4,16 @@ import { intro, log } from '@clack/prompts'
|
|
|
4
4
|
import { createApp } from './create-app.js'
|
|
5
5
|
import { normalizeOptions, promptForOptions } from './options.js'
|
|
6
6
|
import { SUPPORTED_PACKAGE_MANAGERS } from './package-manager.js'
|
|
7
|
+
import { SUPPORTED_TOOLCHAINS } from './toolchain.js'
|
|
7
8
|
|
|
8
9
|
import runServer from './mcp.js'
|
|
9
10
|
import { listAddOns } from './add-ons.js'
|
|
10
11
|
import { DEFAULT_FRAMEWORK, SUPPORTED_FRAMEWORKS } from './constants.js'
|
|
11
12
|
|
|
13
|
+
import { createDefaultEnvironment } from './environment.js'
|
|
14
|
+
|
|
12
15
|
import type { PackageManager } from './package-manager.js'
|
|
16
|
+
import type { ToolChain } from './toolchain.js'
|
|
13
17
|
import type { CliOptions, Framework } from './types.js'
|
|
14
18
|
|
|
15
19
|
export function cli() {
|
|
@@ -65,6 +69,20 @@ export function cli() {
|
|
|
65
69
|
return value as PackageManager
|
|
66
70
|
},
|
|
67
71
|
)
|
|
72
|
+
.option<ToolChain>(
|
|
73
|
+
`--toolchain <${SUPPORTED_TOOLCHAINS.join('|')}>`,
|
|
74
|
+
`Explicitly tell the CLI to use this toolchain`,
|
|
75
|
+
(value) => {
|
|
76
|
+
if (!SUPPORTED_TOOLCHAINS.includes(value as ToolChain)) {
|
|
77
|
+
throw new InvalidArgumentError(
|
|
78
|
+
`Invalid toolchain: ${value}. The following are allowed: ${SUPPORTED_TOOLCHAINS.join(
|
|
79
|
+
', ',
|
|
80
|
+
)}`,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
return value as ToolChain
|
|
84
|
+
},
|
|
85
|
+
)
|
|
68
86
|
.option('--tailwind', 'add Tailwind CSS', false)
|
|
69
87
|
.option<Array<string> | boolean>(
|
|
70
88
|
'--add-ons [...add-ons]',
|
|
@@ -99,7 +117,9 @@ export function cli() {
|
|
|
99
117
|
intro("Let's configure your TanStack application")
|
|
100
118
|
finalOptions = await promptForOptions(cliOptions)
|
|
101
119
|
}
|
|
102
|
-
await createApp(finalOptions
|
|
120
|
+
await createApp(finalOptions, {
|
|
121
|
+
environment: createDefaultEnvironment(),
|
|
122
|
+
})
|
|
103
123
|
} catch (error) {
|
|
104
124
|
log.error(
|
|
105
125
|
error instanceof Error
|