1
1
import { readFile , readdir } from 'node:fs/promises'
2
- import { existsSync , writeFileSync } from 'node:fs'
3
- import { resolve } from 'node:path'
2
+ import { existsSync , mkdirSync , writeFileSync } from 'node:fs'
3
+ import { basename , dirname , resolve } from 'node:path'
4
4
import chalk from 'chalk'
5
5
6
6
import { createMemoryEnvironment } from './environment.js'
@@ -11,6 +11,96 @@ import { finalizeAddOns } from './add-ons.js'
11
11
import type { Options } from './types.js'
12
12
import type { PersistedOptions } from './config-file.js'
13
13
14
+ type AddOnMode = 'add-on' | 'overlay'
15
+
16
+ const INFO_FILE : Record < AddOnMode , string > = {
17
+ 'add-on' : '.add-on/info.json' ,
18
+ overlay : 'overlay-info.json' ,
19
+ }
20
+ const COMPILED_FILE : Record < AddOnMode , string > = {
21
+ 'add-on' : 'add-on.json' ,
22
+ overlay : 'overlay.json' ,
23
+ }
24
+
25
+ const ADD_ON_DIR = '.add-on'
26
+ const ASSETS_DIR = 'assets'
27
+
28
+ const IGNORE_FILES = [
29
+ ADD_ON_DIR ,
30
+ 'node_modules' ,
31
+ 'dist' ,
32
+ 'build' ,
33
+ '.git' ,
34
+ 'pnpm-lock.yaml' ,
35
+ 'package-lock.json' ,
36
+ 'yarn.lock' ,
37
+ 'bun.lockb' ,
38
+ 'bun.lock' ,
39
+ 'deno.lock' ,
40
+ 'add-on.json' ,
41
+ 'add-on-info.json' ,
42
+ 'package.json' ,
43
+ ]
44
+
45
+ const ADD_ON_IGNORE_FILES : Array < string > = [
46
+ 'main.jsx' ,
47
+ 'App.jsx' ,
48
+ 'main.tsx' ,
49
+ 'App.tsx' ,
50
+ 'routeTree.gen.ts' ,
51
+ ]
52
+
53
+ function templatize ( routeCode : string , routeFile : string ) {
54
+ let code = routeCode
55
+
56
+ // Replace the import
57
+ code = code . replace (
58
+ / i m p o r t { c r e a t e F i l e R o u t e } f r o m ' @ t a n s t a c k \/ r e a c t - r o u t e r ' / g,
59
+ `import { <% if (fileRouter) { %>createFileRoute<% } else { %>createRoute<% } %> } from '@tanstack/react-router'` ,
60
+ )
61
+
62
+ // Extract route path and definition, then transform the route declaration
63
+ const routeMatch = code . match (
64
+ / e x p o r t \s + c o n s t \s + R o u t e \s * = \s * c r e a t e F i l e R o u t e \( [ ' " ] ( [ ^ ' " ] + ) [ ' " ] \) \s * \( \{ ( [ ^ } ] + ) \} \) / ,
65
+ )
66
+
67
+ let path = ''
68
+
69
+ if ( routeMatch ) {
70
+ const fullMatch = routeMatch [ 0 ]
71
+ path = routeMatch [ 1 ]
72
+ const routeDefinition = routeMatch [ 2 ]
73
+ code = code . replace (
74
+ fullMatch ,
75
+ `<% if (codeRouter) { %>
76
+ import type { RootRoute } from '@tanstack/react-router'
77
+ <% } else { %>
78
+ export const Route = createFileRoute('${ path } ')({${ routeDefinition } })
79
+ <% } %>` ,
80
+ )
81
+
82
+ code += `
83
+ <% if (codeRouter) { %>
84
+ export default (parentRoute: RootRoute) => createRoute({
85
+ path: '${ path } ',
86
+ ${ routeDefinition }
87
+ getParentRoute: () => parentRoute,
88
+ })
89
+ <% } %>
90
+ `
91
+ } else {
92
+ console . error ( `No route found in the file: ${ routeFile } ` )
93
+ }
94
+
95
+ const name = basename ( path )
96
+ . replace ( '.tsx' , '' )
97
+ . replace ( / ^ d e m o / , '' )
98
+ . replace ( '.' , ' ' )
99
+ . trim ( )
100
+
101
+ return { url : path , code, name }
102
+ }
103
+
14
104
async function createOptions (
15
105
json : PersistedOptions ,
16
106
) : Promise < Required < Options > > {
@@ -32,34 +122,34 @@ async function runCreateApp(options: Required<Options>) {
32
122
return output
33
123
}
34
124
35
- const IGNORE_FILES = [
36
- 'node_modules' ,
37
- 'dist' ,
38
- 'build' ,
39
- '.add-ons' ,
40
- '.git' ,
41
- 'pnpm-lock.yaml' ,
42
- 'package-lock.json' ,
43
- 'yarn.lock' ,
44
- 'bun.lockb' ,
45
- 'bun.lock' ,
46
- 'deno.lock' ,
47
- 'add-on.json' ,
48
- 'add-on-info.json' ,
49
- 'package.json' ,
50
- ]
125
+ async function recursivelyGatherFiles (
126
+ path : string ,
127
+ files : Record < string , string > ,
128
+ ) {
129
+ const dirFiles = await readdir ( path , { withFileTypes : true } )
130
+ for ( const file of dirFiles ) {
131
+ if ( file . isDirectory ( ) ) {
132
+ await recursivelyGatherFiles ( resolve ( path , file . name ) , files )
133
+ } else {
134
+ files [ resolve ( path , file . name ) ] = (
135
+ await readFile ( resolve ( path , file . name ) )
136
+ ) . toString ( )
137
+ }
138
+ }
139
+ }
51
140
52
141
async function compareFiles (
53
142
path : string ,
143
+ ignore : Array < string > ,
54
144
original : Record < string , string > ,
55
145
changedFiles : Record < string , string > ,
56
146
) {
57
147
const files = await readdir ( path , { withFileTypes : true } )
58
148
for ( const file of files ) {
59
149
const filePath = `${ path } /${ file . name } `
60
- if ( ! IGNORE_FILES . includes ( file . name ) ) {
150
+ if ( ! ignore . includes ( file . name ) ) {
61
151
if ( file . isDirectory ( ) ) {
62
- await compareFiles ( filePath , original , changedFiles )
152
+ await compareFiles ( filePath , ignore , original , changedFiles )
63
153
} else {
64
154
const contents = ( await readFile ( filePath ) ) . toString ( )
65
155
const absolutePath = resolve ( process . cwd ( ) , filePath )
@@ -71,7 +161,7 @@ async function compareFiles(
71
161
}
72
162
}
73
163
74
- export async function initAddOn ( ) {
164
+ export async function initAddOn ( mode : AddOnMode ) {
75
165
const persistedOptions = await readConfigFile ( process . cwd ( ) )
76
166
if ( ! persistedOptions ) {
77
167
console . error ( `${ chalk . red ( 'There is no .cta.json file in your project.' ) }
@@ -80,33 +170,52 @@ This is probably because this was created with an older version of create-tsrout
80
170
return
81
171
}
82
172
83
- if ( ! existsSync ( 'add-on-info.json' ) ) {
84
- writeFileSync (
85
- 'add-on-info.json' ,
86
- JSON . stringify (
87
- {
88
- name : 'custom-add-on' ,
89
- version : '0.0.1' ,
90
- description : 'A custom add-on' ,
91
- author : 'John Doe' ,
92
- license : 'MIT' ,
93
- link : 'https://github.com/john-doe/custom-add-on' ,
94
- command : { } ,
95
- shadcnComponents : [ ] ,
96
- templates : [ persistedOptions . mode ] ,
97
- routes : [ ] ,
98
- warning : '' ,
99
- variables : { } ,
100
- phase : 'add-on' ,
101
- type : 'overlay' ,
102
- } ,
103
- null ,
104
- 2 ,
105
- ) ,
106
- )
173
+ if ( mode === 'add-on' ) {
174
+ if ( persistedOptions . mode !== 'file-router' ) {
175
+ console . error ( `${ chalk . red ( 'This project is not using file-router mode.' ) }
176
+
177
+ To create an add-on, the project must be created with the file-router mode.` )
178
+ return
179
+ }
180
+ if ( ! persistedOptions . tailwind ) {
181
+ console . error ( `${ chalk . red ( 'This project is not using Tailwind CSS.' ) }
182
+
183
+ To create an add-on, the project must be created with Tailwind CSS.` )
184
+ return
185
+ }
186
+ if ( ! persistedOptions . typescript ) {
187
+ console . error ( `${ chalk . red ( 'This project is not using TypeScript.' ) }
188
+
189
+ To create an add-on, the project must be created with TypeScript.` )
190
+ return
191
+ }
107
192
}
108
193
109
- const info = JSON . parse ( ( await readFile ( 'add-on-info.json' ) ) . toString ( ) )
194
+ const info = existsSync ( INFO_FILE [ mode ] )
195
+ ? JSON . parse ( ( await readFile ( INFO_FILE [ mode ] ) ) . toString ( ) )
196
+ : {
197
+ name : `${ persistedOptions . projectName } -${ mode } ` ,
198
+ version : '0.0.1' ,
199
+ description : mode === 'add-on' ? 'Add-on' : 'Project overlay' ,
200
+ author :
'Jane Smith <[email protected] >' ,
201
+ license : 'MIT' ,
202
+ link : `https://github.com/jane-smith/${ persistedOptions . projectName } -${ mode } ` ,
203
+ command : { } ,
204
+ shadcnComponents : [ ] ,
205
+ templates : [ persistedOptions . mode ] ,
206
+ routes : [ ] ,
207
+ warning : '' ,
208
+ variables : { } ,
209
+ phase : 'add-on' ,
210
+ type : mode ,
211
+ packageAdditions : {
212
+ scripts : { } ,
213
+ dependencies : { } ,
214
+ devDependencies : { } ,
215
+ } ,
216
+ }
217
+
218
+ const compiledInfo = JSON . parse ( JSON . stringify ( info ) )
110
219
111
220
const originalOutput = await runCreateApp (
112
221
await createOptions ( persistedOptions ) ,
@@ -119,17 +228,12 @@ This is probably because this was created with an older version of create-tsrout
119
228
( await readFile ( 'package.json' ) ) . toString ( ) ,
120
229
)
121
230
122
- info . packageAdditions = {
123
- scripts : { } ,
124
- dependencies : { } ,
125
- devDependencies : { } ,
126
- }
127
-
128
- if (
129
- JSON . stringify ( originalPackageJson . scripts ) !==
130
- JSON . stringify ( currentPackageJson . scripts )
131
- ) {
132
- info . packageAdditions . scripts = currentPackageJson . scripts
231
+ for ( const script of Object . keys ( currentPackageJson . scripts ) ) {
232
+ if (
233
+ originalPackageJson . scripts [ script ] !== currentPackageJson . scripts [ script ]
234
+ ) {
235
+ info . packageAdditions . scripts [ script ] = currentPackageJson . scripts [ script ]
236
+ }
133
237
}
134
238
135
239
const dependencies : Record < string , string > = { }
@@ -155,23 +259,65 @@ This is probably because this was created with an older version of create-tsrout
155
259
}
156
260
info . packageAdditions . devDependencies = devDependencies
157
261
262
+ // Find altered files
158
263
const changedFiles : Record < string , string > = { }
159
- await compareFiles ( '.' , originalOutput . files , changedFiles )
264
+ await compareFiles ( '.' , IGNORE_FILES , originalOutput . files , changedFiles )
265
+ if ( mode === 'overlay' ) {
266
+ compiledInfo . files = changedFiles
267
+ } else {
268
+ const assetsDir = resolve ( ADD_ON_DIR , ASSETS_DIR )
269
+ if ( ! existsSync ( assetsDir ) ) {
270
+ await compareFiles ( '.' , IGNORE_FILES , originalOutput . files , changedFiles )
271
+ for ( const file of Object . keys ( changedFiles ) . filter (
272
+ ( file ) => ! ADD_ON_IGNORE_FILES . includes ( basename ( file ) ) ,
273
+ ) ) {
274
+ mkdirSync ( dirname ( resolve ( assetsDir , file ) ) , {
275
+ recursive : true ,
276
+ } )
277
+ if ( file . includes ( '/routes/' ) ) {
278
+ const { url, code, name } = templatize ( changedFiles [ file ] , file )
279
+ info . routes . push ( {
280
+ url,
281
+ name,
282
+ } )
283
+ writeFileSync ( resolve ( assetsDir , `${ file } .ejs` ) , code )
284
+ } else {
285
+ writeFileSync ( resolve ( assetsDir , file ) , changedFiles [ file ] )
286
+ }
287
+ }
288
+ }
289
+ const addOnFiles : Record < string , string > = { }
290
+ await recursivelyGatherFiles ( assetsDir , addOnFiles )
291
+ compiledInfo . files = Object . keys ( addOnFiles ) . reduce (
292
+ ( acc , file ) => {
293
+ acc [ file . replace ( assetsDir , '.' ) ] = addOnFiles [ file ]
294
+ return acc
295
+ } ,
296
+ { } as Record < string , string > ,
297
+ )
298
+ }
160
299
161
- info . files = changedFiles
162
- info . deletedFiles = [ ]
300
+ compiledInfo . routes = info . routes
301
+ compiledInfo . framework = persistedOptions . framework
302
+ compiledInfo . addDependencies = persistedOptions . existingAddOns
163
303
164
- info . mode = persistedOptions . mode
165
- info . framework = persistedOptions . framework
166
- info . typescript = persistedOptions . typescript
167
- info . tailwind = persistedOptions . tailwind
168
- info . addDependencies = persistedOptions . existingAddOns
304
+ if ( mode === 'overlay' ) {
305
+ compiledInfo . mode = persistedOptions . mode
306
+ compiledInfo . typescript = persistedOptions . typescript
307
+ compiledInfo . tailwind = persistedOptions . tailwind
169
308
170
- for ( const file of Object . keys ( originalOutput . files ) ) {
171
- if ( ! existsSync ( file ) ) {
172
- info . deletedFiles . push ( file . replace ( process . cwd ( ) , '.' ) )
309
+ compiledInfo . deletedFiles = [ ]
310
+ for ( const file of Object . keys ( originalOutput . files ) ) {
311
+ if ( ! existsSync ( file ) ) {
312
+ compiledInfo . deletedFiles . push ( file . replace ( process . cwd ( ) , '.' ) )
313
+ }
173
314
}
174
315
}
175
316
176
- writeFileSync ( 'add-on.json' , JSON . stringify ( info , null , 2 ) )
317
+ if ( ! existsSync ( resolve ( INFO_FILE [ mode ] ) ) ) {
318
+ mkdirSync ( resolve ( dirname ( INFO_FILE [ mode ] ) ) , { recursive : true } )
319
+ writeFileSync ( INFO_FILE [ mode ] , JSON . stringify ( info , null , 2 ) )
320
+ }
321
+
322
+ writeFileSync ( COMPILED_FILE [ mode ] , JSON . stringify ( compiledInfo , null , 2 ) )
177
323
}
0 commit comments