create-start-app 0.1.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/.gitattributes +2 -0
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/workflows/auto.yml +46 -0
- package/.github/workflows/ci.yml +43 -0
- package/.nvmrc +1 -0
- package/.prettierignore +3 -0
- package/CONTRIBUTING.md +34 -0
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/index.js +199 -0
- package/dist/utils/getPackageManager.js +15 -0
- package/eslint.config.js +35 -0
- package/package.json +48 -0
- package/prettier.config.js +10 -0
- package/scripts/publish.js +33 -0
- package/src/index.ts +335 -0
- package/src/utils/getPackageManager.ts +22 -0
- package/templates/base/.vscode/settings.json +11 -0
- package/templates/base/README.md.ejs +530 -0
- package/templates/base/gitignore +5 -0
- package/templates/base/index.html.ejs +20 -0
- package/templates/base/package.json +28 -0
- package/templates/base/package.ts.json +7 -0
- package/templates/base/package.tw.json +6 -0
- package/templates/base/public/favicon.ico +0 -0
- package/templates/base/public/logo192.png +0 -0
- package/templates/base/public/logo512.png +0 -0
- package/templates/base/public/manifest.json +25 -0
- package/templates/base/public/robots.txt +3 -0
- package/templates/base/src/App.css +38 -0
- package/templates/base/src/App.test.tsx.ejs +10 -0
- package/templates/base/src/App.tsx.ejs +74 -0
- package/templates/base/src/logo.svg +43 -0
- package/templates/base/src/reportWebVitals.ts.ejs +28 -0
- package/templates/base/src/styles.css.ejs +15 -0
- package/templates/base/tsconfig.json +24 -0
- package/templates/base/vite.config.js.ejs +15 -0
- package/templates/code-router/src/main.tsx.ejs +61 -0
- package/templates/file-router/package.fr.json +5 -0
- package/templates/file-router/src/main.tsx.ejs +35 -0
- package/templates/file-router/src/routes/__root.tsx +11 -0
- package/tsconfig.json +15 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { existsSync } from 'node:fs'
|
|
5
|
+
import { resolve } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { Command, InvalidArgumentError } from 'commander'
|
|
8
|
+
import { intro, outro, spinner, log } from '@clack/prompts'
|
|
9
|
+
import { execa } from 'execa'
|
|
10
|
+
import { render } from 'ejs'
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
SUPPORTED_PACKAGE_MANAGERS,
|
|
14
|
+
getPackageManager,
|
|
15
|
+
} from './utils/getPackageManager.js'
|
|
16
|
+
|
|
17
|
+
import type { PackageManager } from './utils/getPackageManager.js'
|
|
18
|
+
|
|
19
|
+
const program = new Command()
|
|
20
|
+
|
|
21
|
+
const CODE_ROUTER = 'code-router'
|
|
22
|
+
const FILE_ROUTER = 'file-router'
|
|
23
|
+
|
|
24
|
+
interface Options {
|
|
25
|
+
typescript: boolean
|
|
26
|
+
tailwind: boolean
|
|
27
|
+
packageManager: PackageManager
|
|
28
|
+
mode: typeof CODE_ROUTER | typeof FILE_ROUTER
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sortObject(obj: Record<string, string>): Record<string, string> {
|
|
32
|
+
return Object.keys(obj)
|
|
33
|
+
.sort()
|
|
34
|
+
.reduce<Record<string, string>>((acc, key) => {
|
|
35
|
+
acc[key] = obj[key]
|
|
36
|
+
return acc
|
|
37
|
+
}, {})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createCopyFile(targetDir: string) {
|
|
41
|
+
return async function copyFiles(templateDir: string, files: Array<string>) {
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
const targetFileName = file.replace('.tw', '')
|
|
44
|
+
await copyFile(
|
|
45
|
+
resolve(templateDir, file),
|
|
46
|
+
resolve(targetDir, targetFileName),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createTemplateFile(
|
|
53
|
+
projectName: string,
|
|
54
|
+
options: Required<Options>,
|
|
55
|
+
targetDir: string,
|
|
56
|
+
) {
|
|
57
|
+
return async function templateFile(
|
|
58
|
+
templateDir: string,
|
|
59
|
+
file: string,
|
|
60
|
+
targetFileName?: string,
|
|
61
|
+
) {
|
|
62
|
+
const templateValues = {
|
|
63
|
+
packageManager: options.packageManager,
|
|
64
|
+
projectName: projectName,
|
|
65
|
+
typescript: options.typescript,
|
|
66
|
+
tailwind: options.tailwind,
|
|
67
|
+
js: options.typescript ? 'ts' : 'js',
|
|
68
|
+
jsx: options.typescript ? 'tsx' : 'jsx',
|
|
69
|
+
fileRouter: options.mode === FILE_ROUTER,
|
|
70
|
+
codeRouter: options.mode === CODE_ROUTER,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const template = await readFile(resolve(templateDir, file), 'utf-8')
|
|
74
|
+
const content = render(template, templateValues)
|
|
75
|
+
const target = targetFileName ?? file.replace('.ejs', '')
|
|
76
|
+
await writeFile(resolve(targetDir, target), content)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function createPackageJSON(
|
|
81
|
+
projectName: string,
|
|
82
|
+
options: Required<Options>,
|
|
83
|
+
templateDir: string,
|
|
84
|
+
routerDir: string,
|
|
85
|
+
targetDir: string,
|
|
86
|
+
) {
|
|
87
|
+
let packageJSON = JSON.parse(
|
|
88
|
+
await readFile(resolve(templateDir, 'package.json'), 'utf8'),
|
|
89
|
+
)
|
|
90
|
+
packageJSON.name = projectName
|
|
91
|
+
if (options.typescript) {
|
|
92
|
+
const tsPackageJSON = JSON.parse(
|
|
93
|
+
await readFile(resolve(templateDir, 'package.ts.json'), 'utf8'),
|
|
94
|
+
)
|
|
95
|
+
packageJSON = {
|
|
96
|
+
...packageJSON,
|
|
97
|
+
devDependencies: {
|
|
98
|
+
...packageJSON.devDependencies,
|
|
99
|
+
...tsPackageJSON.devDependencies,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (options.tailwind) {
|
|
104
|
+
const twPackageJSON = JSON.parse(
|
|
105
|
+
await readFile(resolve(templateDir, 'package.tw.json'), 'utf8'),
|
|
106
|
+
)
|
|
107
|
+
packageJSON = {
|
|
108
|
+
...packageJSON,
|
|
109
|
+
dependencies: {
|
|
110
|
+
...packageJSON.dependencies,
|
|
111
|
+
...twPackageJSON.dependencies,
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (options.mode === FILE_ROUTER) {
|
|
116
|
+
const frPackageJSON = JSON.parse(
|
|
117
|
+
await readFile(resolve(routerDir, 'package.fr.json'), 'utf8'),
|
|
118
|
+
)
|
|
119
|
+
packageJSON = {
|
|
120
|
+
...packageJSON,
|
|
121
|
+
dependencies: {
|
|
122
|
+
...packageJSON.dependencies,
|
|
123
|
+
...frPackageJSON.dependencies,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
packageJSON.dependencies = sortObject(
|
|
128
|
+
packageJSON.dependencies as Record<string, string>,
|
|
129
|
+
)
|
|
130
|
+
packageJSON.devDependencies = sortObject(
|
|
131
|
+
packageJSON.devDependencies as Record<string, string>,
|
|
132
|
+
)
|
|
133
|
+
await writeFile(
|
|
134
|
+
resolve(targetDir, 'package.json'),
|
|
135
|
+
JSON.stringify(packageJSON, null, 2),
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function createApp(projectName: string, options: Required<Options>) {
|
|
140
|
+
const templateDirBase = fileURLToPath(
|
|
141
|
+
new URL('../templates/base', import.meta.url),
|
|
142
|
+
)
|
|
143
|
+
const templateDirRouter = fileURLToPath(
|
|
144
|
+
new URL(`../templates/${options.mode}`, import.meta.url),
|
|
145
|
+
)
|
|
146
|
+
const targetDir = resolve(process.cwd(), projectName)
|
|
147
|
+
|
|
148
|
+
if (existsSync(targetDir)) {
|
|
149
|
+
log.error(`Directory "${projectName}" already exists`)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const copyFiles = createCopyFile(targetDir)
|
|
154
|
+
const templateFile = createTemplateFile(projectName, options, targetDir)
|
|
155
|
+
|
|
156
|
+
intro(`Creating a new TanStack app in ${targetDir}...`)
|
|
157
|
+
|
|
158
|
+
// Make the root directory
|
|
159
|
+
await mkdir(targetDir, { recursive: true })
|
|
160
|
+
|
|
161
|
+
// Setup the .vscode directory
|
|
162
|
+
await mkdir(resolve(targetDir, '.vscode'), { recursive: true })
|
|
163
|
+
await copyFile(
|
|
164
|
+
resolve(templateDirBase, '.vscode/settings.json'),
|
|
165
|
+
resolve(targetDir, '.vscode/settings.json'),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
// Fill the public directory
|
|
169
|
+
await mkdir(resolve(targetDir, 'public'), { recursive: true })
|
|
170
|
+
copyFiles(templateDirBase, [
|
|
171
|
+
'./public/robots.txt',
|
|
172
|
+
'./public/favicon.ico',
|
|
173
|
+
'./public/manifest.json',
|
|
174
|
+
'./public/logo192.png',
|
|
175
|
+
'./public/logo512.png',
|
|
176
|
+
])
|
|
177
|
+
|
|
178
|
+
// Make the src directory
|
|
179
|
+
await mkdir(resolve(targetDir, 'src'), { recursive: true })
|
|
180
|
+
if (options.mode === FILE_ROUTER) {
|
|
181
|
+
await mkdir(resolve(targetDir, 'src/routes'), { recursive: true })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Copy in Vite and Tailwind config and CSS
|
|
185
|
+
if (!options.tailwind) {
|
|
186
|
+
await copyFiles(templateDirBase, ['./src/App.css'])
|
|
187
|
+
}
|
|
188
|
+
await templateFile(templateDirBase, './vite.config.js.ejs')
|
|
189
|
+
await templateFile(templateDirBase, './src/styles.css.ejs')
|
|
190
|
+
|
|
191
|
+
copyFiles(templateDirBase, ['./src/logo.svg'])
|
|
192
|
+
|
|
193
|
+
// Setup the app component. There are four variations, typescript/javascript and tailwind/non-tailwind.
|
|
194
|
+
if (options.mode === FILE_ROUTER) {
|
|
195
|
+
copyFiles(templateDirRouter, ['./src/routes/__root.tsx'])
|
|
196
|
+
await templateFile(
|
|
197
|
+
templateDirBase,
|
|
198
|
+
'./src/App.tsx.ejs',
|
|
199
|
+
'./src/routes/index.tsx',
|
|
200
|
+
)
|
|
201
|
+
} else {
|
|
202
|
+
await templateFile(
|
|
203
|
+
templateDirBase,
|
|
204
|
+
'./src/App.tsx.ejs',
|
|
205
|
+
options.typescript ? undefined : './src/App.jsx',
|
|
206
|
+
)
|
|
207
|
+
await templateFile(
|
|
208
|
+
templateDirBase,
|
|
209
|
+
'./src/App.test.tsx.ejs',
|
|
210
|
+
options.typescript ? undefined : './src/App.test.jsx',
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Create the main entry point
|
|
215
|
+
if (options.typescript) {
|
|
216
|
+
await templateFile(templateDirRouter, './src/main.tsx.ejs')
|
|
217
|
+
} else {
|
|
218
|
+
await templateFile(
|
|
219
|
+
templateDirRouter,
|
|
220
|
+
'./src/main.tsx.ejs',
|
|
221
|
+
'./src/main.jsx',
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Setup the main, reportWebVitals and index.html files
|
|
226
|
+
if (options.typescript) {
|
|
227
|
+
await templateFile(templateDirBase, './src/reportWebVitals.ts.ejs')
|
|
228
|
+
} else {
|
|
229
|
+
await templateFile(
|
|
230
|
+
templateDirBase,
|
|
231
|
+
'./src/reportWebVitals.ts.ejs',
|
|
232
|
+
'./src/reportWebVitals.js',
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
await templateFile(templateDirBase, './index.html.ejs')
|
|
236
|
+
|
|
237
|
+
// Setup tsconfig
|
|
238
|
+
if (options.typescript) {
|
|
239
|
+
await copyFiles(templateDirBase, ['./tsconfig.json'])
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Setup the package.json file, optionally with typescript and tailwind
|
|
243
|
+
await createPackageJSON(
|
|
244
|
+
projectName,
|
|
245
|
+
options,
|
|
246
|
+
templateDirBase,
|
|
247
|
+
templateDirRouter,
|
|
248
|
+
targetDir,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
// Add .gitignore
|
|
252
|
+
await copyFile(
|
|
253
|
+
resolve(templateDirBase, 'gitignore'),
|
|
254
|
+
resolve(targetDir, '.gitignore'),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
// Create the README.md
|
|
258
|
+
await templateFile(templateDirBase, 'README.md.ejs')
|
|
259
|
+
|
|
260
|
+
// Install dependencies
|
|
261
|
+
const s = spinner()
|
|
262
|
+
s.start(`Installing dependencies via ${options.packageManager}...`)
|
|
263
|
+
await execa(options.packageManager, ['install'], { cwd: targetDir })
|
|
264
|
+
s.stop(`Installed dependencies`)
|
|
265
|
+
|
|
266
|
+
outro(`Created your new TanStack app in ${targetDir}.
|
|
267
|
+
|
|
268
|
+
Use the following commands to start your app:
|
|
269
|
+
|
|
270
|
+
% cd ${projectName}
|
|
271
|
+
% ${options.packageManager} start
|
|
272
|
+
|
|
273
|
+
Please read README.md for more information on testing, styling, adding routes, react-query, etc.
|
|
274
|
+
`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
program
|
|
278
|
+
.name('create-tsrouter-app')
|
|
279
|
+
.description('CLI to create a new TanStack application')
|
|
280
|
+
.argument('<project-name>', 'name of the project')
|
|
281
|
+
.option<'typescript' | 'javascript' | 'file-router'>(
|
|
282
|
+
'--template <type>',
|
|
283
|
+
'project template (typescript, javascript, file-router)',
|
|
284
|
+
(value) => {
|
|
285
|
+
if (
|
|
286
|
+
value !== 'typescript' &&
|
|
287
|
+
value !== 'javascript' &&
|
|
288
|
+
value !== 'file-router'
|
|
289
|
+
) {
|
|
290
|
+
throw new InvalidArgumentError(
|
|
291
|
+
`Invalid template: ${value}. Only the following are allowed: typescript, javascript, file-router`,
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
return value
|
|
295
|
+
},
|
|
296
|
+
'javascript',
|
|
297
|
+
)
|
|
298
|
+
.option<PackageManager>(
|
|
299
|
+
`--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`,
|
|
300
|
+
`Explicitly tell the CLI to use this package manager`,
|
|
301
|
+
(value) => {
|
|
302
|
+
if (!SUPPORTED_PACKAGE_MANAGERS.includes(value as PackageManager)) {
|
|
303
|
+
throw new InvalidArgumentError(
|
|
304
|
+
`Invalid package manager: ${value}. Only the following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(
|
|
305
|
+
', ',
|
|
306
|
+
)}`,
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
return value as PackageManager
|
|
310
|
+
},
|
|
311
|
+
getPackageManager(),
|
|
312
|
+
)
|
|
313
|
+
.option('--tailwind', 'add Tailwind CSS', false)
|
|
314
|
+
.action(
|
|
315
|
+
(
|
|
316
|
+
projectName: string,
|
|
317
|
+
options: {
|
|
318
|
+
template: 'typescript' | 'javascript' | 'file-router'
|
|
319
|
+
tailwind: boolean
|
|
320
|
+
packageManager: PackageManager
|
|
321
|
+
},
|
|
322
|
+
) => {
|
|
323
|
+
const typescript =
|
|
324
|
+
options.template === 'typescript' || options.template === 'file-router'
|
|
325
|
+
|
|
326
|
+
createApp(projectName, {
|
|
327
|
+
typescript,
|
|
328
|
+
tailwind: options.tailwind,
|
|
329
|
+
packageManager: options.packageManager,
|
|
330
|
+
mode: options.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER,
|
|
331
|
+
})
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
program.parse()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const SUPPORTED_PACKAGE_MANAGERS = [
|
|
2
|
+
'npm',
|
|
3
|
+
'yarn',
|
|
4
|
+
'pnpm',
|
|
5
|
+
'bun',
|
|
6
|
+
] as const
|
|
7
|
+
export type PackageManager = (typeof SUPPORTED_PACKAGE_MANAGERS)[number]
|
|
8
|
+
export const DEFAULT_PACKAGE_MANAGER: PackageManager = 'npm'
|
|
9
|
+
|
|
10
|
+
export function getPackageManager(): PackageManager | undefined {
|
|
11
|
+
const userAgent = process.env.npm_config_user_agent
|
|
12
|
+
|
|
13
|
+
if (userAgent === undefined) {
|
|
14
|
+
return DEFAULT_PACKAGE_MANAGER
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const packageManager = SUPPORTED_PACKAGE_MANAGERS.find((manager) =>
|
|
18
|
+
userAgent.startsWith(manager),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return packageManager || DEFAULT_PACKAGE_MANAGER
|
|
22
|
+
}
|