Skip to content

Commit c066ecd

Browse files
committed
chore: starting to get some place with the refactoring of create-app
1 parent b4db443 commit c066ecd

File tree

15 files changed

+186
-198
lines changed

15 files changed

+186
-198
lines changed

packages/cta-core/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"scripts": {
99
"build": "tsc",
1010
"dev": "tsc --watch",
11-
"test": "eslint ./src"
11+
"test": "eslint ./src",
12+
"test:watch": "vitest"
1213
},
1314
"repository": {
1415
"type": "git",
@@ -31,7 +32,8 @@
3132
"ejs": "^3.1.10",
3233
"execa": "^9.5.2",
3334
"memfs": "^4.17.0",
34-
"prettier": "^3.5.0"
35+
"prettier": "^3.5.0",
36+
"vitest": "^3.1.1"
3537
},
3638
"devDependencies": {
3739
"@tanstack/config": "^0.16.2",

packages/cta-core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export {
2222
getFrameworkByName,
2323
getFrameworks,
2424
} from './frameworks.js'
25-
export { jsSafeName, sortObject } from './utils.js'
25+
export { jsSafeName, relativePath, sortObject } from './utils.js'
2626
export { writeConfigFile, readConfigFile } from './config-file.js'
2727
export { readFileHelper } from './file-helper.js'
2828

packages/cta-core/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export type AddOn = {
9898
type: 'add-on' | 'example' | 'starter'
9999
link: string
100100
templates: Array<string>
101-
routes: Array<{
101+
routes?: Array<{
102102
url: string
103103
name: string
104104
path: string

packages/cta-core/src/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { basename } from 'node:path'
2+
;``
13
export function sortObject(
24
obj: Record<string, string>,
35
): Record<string, string> {
@@ -15,3 +17,34 @@ export function jsSafeName(name: string) {
1517
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
1618
.join('')
1719
}
20+
21+
export function relativePath(from: string, to: string) {
22+
const fromSegments = from.replace(/^./, '').split('/')
23+
const toSegments = to.replace(/^./, '').split('/')
24+
25+
fromSegments.pop()
26+
toSegments.pop()
27+
28+
let commonIndex = 0
29+
while (
30+
commonIndex < fromSegments.length &&
31+
commonIndex < toSegments.length &&
32+
fromSegments[commonIndex] === toSegments[commonIndex]
33+
) {
34+
commonIndex++
35+
}
36+
37+
const upLevels = fromSegments.length - commonIndex
38+
const downLevels = toSegments.slice(commonIndex)
39+
40+
if (upLevels === 0 && downLevels.length === 0) {
41+
return `./${basename(to)}`
42+
} else if (upLevels === 0 && downLevels.length > 0) {
43+
return `./${downLevels.join('/')}/${basename(to)}`
44+
} else {
45+
const relativePath = [...Array(upLevels).fill('..'), ...downLevels].join(
46+
'/',
47+
)
48+
return `${relativePath}/${basename(to)}`
49+
}
50+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { relativePath } from '../src/utils'
4+
5+
describe('relativePath', () => {
6+
it('relative path with the same directory', () => {
7+
expect(relativePath('src/utils.ts', 'src/index.ts')).toBe('./index.ts')
8+
})
9+
it('relative from a subdirectory', () => {
10+
expect(relativePath('src/something/utils.ts', 'src/index.ts')).toBe(
11+
'../index.ts',
12+
)
13+
})
14+
it('relative to a subdirectory', () => {
15+
expect(relativePath('src/utils.ts', 'src/something/index.ts')).toBe(
16+
'./something/index.ts',
17+
)
18+
})
19+
it('same deep directory', () => {
20+
expect(
21+
relativePath('src/foo/bar/baz/utils.ts', 'src/foo/bar/baz/index.ts'),
22+
).toBe('./index.ts')
23+
})
24+
it('up several levels and down several levels', () => {
25+
expect(relativePath('src/bar/baz/utils.ts', 'src/foo/bar/index.ts')).toBe(
26+
'../../foo/bar/index.ts',
27+
)
28+
})
29+
})

packages/cta-engine/src/create-app.ts

Lines changed: 56 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { basename, resolve } from 'node:path'
22

33
import {
44
copyAddOnFile,
5-
jsSafeName,
65
packageManagerExecute,
76
writeConfigFile,
87
} from '@tanstack/cta-core'
@@ -71,41 +70,9 @@ export async function createApp(
7170
)
7271
}
7372

74-
const isAddOnEnabled = (id: string) =>
75-
options.chosenAddOns.find((a) => a.id === id)
76-
77-
async function runAddOn(addOn: AddOn) {
78-
if (addOn.files) {
79-
for (const file of Object.keys(addOn.files)) {
80-
await copyAddOnFile(
81-
environment,
82-
addOn.files[file],
83-
file,
84-
resolve(targetDir, file),
85-
(content, targetFileName) =>
86-
templateFileFromContent(targetFileName, content),
87-
)
88-
}
89-
}
90-
if (addOn.deletedFiles) {
91-
for (const file of addOn.deletedFiles) {
92-
await environment.deleteFile(resolve(targetDir, file))
93-
}
94-
}
95-
96-
if (addOn.command && addOn.command.command) {
97-
await environment.execute(
98-
addOn.command.command,
99-
addOn.command.args || [],
100-
resolve(targetDir),
101-
)
102-
}
103-
}
104-
105-
// Setup the .vscode directory
73+
// Setup the base files
10674
await templateFile(templateDirBase, '_dot_vscode/settings.json.ejs')
10775

108-
// Fill the public directory
10976
copyFiles(templateDirBase, [
11077
'./public/robots.txt',
11178
'./public/favicon.ico',
@@ -114,15 +81,13 @@ export async function createApp(
11481
'./public/logo512.png',
11582
])
11683

117-
// Check for a .cursorrules file
11884
if (environment.exists(resolve(templateDirBase, '_dot_cursorrules'))) {
11985
await environment.copyFile(
12086
resolve(templateDirBase, '_dot_cursorrules'),
12187
resolve(targetDir, '.cursorrules'),
12288
)
12389
}
12490

125-
// Copy in Vite and Tailwind config and CSS
12691
if (!options.tailwind) {
12792
await copyFiles(templateDirBase, ['./src/App.css'])
12893
}
@@ -137,24 +102,42 @@ export async function createApp(
137102
await templateFile(templateDirBase, 'eslint.config.js.ejs')
138103
await templateFile(templateDirBase, 'prettier.config.js.ejs')
139104

140-
// Setup reportWebVitals
141-
// TODO: This is a bit of a hack to check if the framework is react
142105
if (
143106
environment.exists(resolve(templateDirBase, './src/reportWebVitals.ts.ejs'))
144107
) {
145108
await templateFile(templateDirBase, './src/reportWebVitals.ts.ejs')
146109
}
147110
await templateFile(templateDirBase, './index.html.ejs')
148111

149-
// Add .gitignore
150112
await environment.copyFile(
151113
resolve(templateDirBase, '_dot_gitignore'),
152114
resolve(targetDir, '.gitignore'),
153115
)
154116

155-
// Setup tsconfig
156117
await templateFile(templateDirBase, './tsconfig.json.ejs')
157118

119+
await templateFile(templateDirBase, './src/main.tsx.ejs', './src/main.tsx')
120+
121+
await templateFile(
122+
templateDirBase,
123+
'./src/routes/__root.tsx.ejs',
124+
'./src/routes/__root.tsx',
125+
)
126+
127+
if (options.framework.id === 'react-cra') {
128+
await templateFile(templateDirBase, './src/App.test.tsx.ejs')
129+
}
130+
await templateFile(templateDirBase, './src/App.tsx.ejs')
131+
await templateFile(templateDirBase, './src/routes/index.tsx.ejs')
132+
133+
await templateFile(
134+
templateDirBase,
135+
'./src/components/Header.tsx.ejs',
136+
'./src/components/Header.tsx',
137+
)
138+
139+
await templateFile(templateDirBase, 'README.md.ejs')
140+
158141
// Setup the package.json file, optionally with typescript, tailwind and formatter/linter
159142
await createPackageJSON(
160143
environment,
@@ -165,6 +148,39 @@ export async function createApp(
165148
options.chosenAddOns.map((addOn) => addOn.packageAdditions),
166149
)
167150

151+
// Setup the add-ons
152+
153+
const isAddOnEnabled = (id: string) =>
154+
options.chosenAddOns.find((a) => a.id === id)
155+
156+
async function runAddOn(addOn: AddOn) {
157+
if (addOn.files) {
158+
for (const file of Object.keys(addOn.files)) {
159+
await copyAddOnFile(
160+
environment,
161+
addOn.files[file],
162+
file,
163+
resolve(targetDir, file),
164+
(content, targetFileName) =>
165+
templateFileFromContent(targetFileName, content),
166+
)
167+
}
168+
}
169+
if (addOn.deletedFiles) {
170+
for (const file of addOn.deletedFiles) {
171+
await environment.deleteFile(resolve(targetDir, file))
172+
}
173+
}
174+
175+
if (addOn.command && addOn.command.command) {
176+
await environment.execute(
177+
addOn.command.command,
178+
addOn.command.args || [],
179+
resolve(targetDir),
180+
)
181+
}
182+
}
183+
168184
// Copy all the asset files from the addons
169185
const s = silent ? null : environment.spinner()
170186
for (const type of ['add-on', 'example']) {
@@ -211,128 +227,13 @@ export async function createApp(
211227
}
212228
}
213229

214-
const integrations: Array<{
215-
type: 'layout' | 'provider' | 'root-provider' | 'header-user'
216-
name: string
217-
path: string
218-
}> = []
219-
if (environment.exists(resolve(targetDir, 'src/integrations'))) {
220-
for (const integration of environment.readdir(
221-
resolve(targetDir, 'src/integrations'),
222-
)) {
223-
const integrationName = jsSafeName(integration)
224-
if (
225-
environment.exists(
226-
resolve(targetDir, 'src/integrations', integration, 'layout.tsx'),
227-
)
228-
) {
229-
integrations.push({
230-
type: 'layout',
231-
name: `${integrationName}Layout`,
232-
path: `integrations/${integration}/layout`,
233-
})
234-
}
235-
if (
236-
environment.exists(
237-
resolve(targetDir, 'src/integrations', integration, 'provider.tsx'),
238-
)
239-
) {
240-
integrations.push({
241-
type: 'provider',
242-
name: `${integrationName}Provider`,
243-
path: `integrations/${integration}/provider`,
244-
})
245-
}
246-
if (
247-
environment.exists(
248-
resolve(
249-
targetDir,
250-
'src/integrations',
251-
integration,
252-
'root-provider.tsx',
253-
),
254-
)
255-
) {
256-
integrations.push({
257-
type: 'root-provider',
258-
name: integrationName,
259-
path: `integrations/${integration}/root-provider`,
260-
})
261-
}
262-
if (
263-
environment.exists(
264-
resolve(
265-
targetDir,
266-
'src/integrations',
267-
integration,
268-
'header-user.tsx',
269-
),
270-
)
271-
) {
272-
integrations.push({
273-
type: 'header-user',
274-
name: `${integrationName}Header`,
275-
path: `integrations/${integration}/header-user`,
276-
})
277-
}
278-
}
279-
}
280-
281-
const routes: Array<{
282-
path: string
283-
name: string
284-
}> = []
285-
if (environment.exists(resolve(targetDir, 'src/routes'))) {
286-
for (const file of environment.readdir(resolve(targetDir, 'src/routes'))) {
287-
const name = file.replace(/\.tsx?|\.jsx?/, '')
288-
const safeRouteName = jsSafeName(name)
289-
routes.push({
290-
path: `./routes/${name}`,
291-
name: safeRouteName,
292-
})
293-
}
294-
}
295-
296-
await templateFile(templateDirBase, './src/main.tsx.ejs', './src/main.tsx', {
297-
routes,
298-
integrations,
299-
})
300-
301-
await templateFile(
302-
templateDirBase,
303-
'./src/routes/__root.tsx.ejs',
304-
'./src/routes/__root.tsx',
305-
{
306-
integrations,
307-
},
308-
)
309-
310-
if (options.framework.id === 'react-cra') {
311-
await templateFile(templateDirBase, './src/App.test.tsx.ejs')
312-
}
313-
await templateFile(templateDirBase, './src/App.tsx.ejs')
314-
await templateFile(templateDirBase, './src/routes/index.tsx.ejs')
315-
316-
await templateFile(
317-
templateDirBase,
318-
'./src/components/Header.tsx.ejs',
319-
'./src/components/Header.tsx',
320-
{
321-
integrations,
322-
routes,
323-
},
324-
)
325-
326230
const warnings: Array<string> = []
327231
for (const addOn of options.chosenAddOns) {
328232
if (addOn.warning) {
329233
warnings.push(addOn.warning)
330234
}
331235
}
332236

333-
// Create the README.md
334-
await templateFile(templateDirBase, 'README.md.ejs')
335-
336237
// Adding starter
337238
if (options.starter) {
338239
s?.start(`Setting up starter ${options.starter.name}...`)

0 commit comments

Comments
 (0)