11import { 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'
44import chalk from 'chalk'
55
66import { createMemoryEnvironment } from './environment.js'
@@ -11,6 +11,96 @@ import { finalizeAddOns } from './add-ons.js'
1111import type { Options } from './types.js'
1212import type { PersistedOptions } from './config-file.js'
1313
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+
14104async function createOptions (
15105 json : PersistedOptions ,
16106) : Promise < Required < Options > > {
@@ -32,34 +122,34 @@ async function runCreateApp(options: Required<Options>) {
32122 return output
33123}
34124
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+ }
51140
52141async function compareFiles (
53142 path : string ,
143+ ignore : Array < string > ,
54144 original : Record < string , string > ,
55145 changedFiles : Record < string , string > ,
56146) {
57147 const files = await readdir ( path , { withFileTypes : true } )
58148 for ( const file of files ) {
59149 const filePath = `${ path } /${ file . name } `
60- if ( ! IGNORE_FILES . includes ( file . name ) ) {
150+ if ( ! ignore . includes ( file . name ) ) {
61151 if ( file . isDirectory ( ) ) {
62- await compareFiles ( filePath , original , changedFiles )
152+ await compareFiles ( filePath , ignore , original , changedFiles )
63153 } else {
64154 const contents = ( await readFile ( filePath ) ) . toString ( )
65155 const absolutePath = resolve ( process . cwd ( ) , filePath )
@@ -71,7 +161,7 @@ async function compareFiles(
71161 }
72162}
73163
74- export async function initAddOn ( ) {
164+ export async function initAddOn ( mode : AddOnMode ) {
75165 const persistedOptions = await readConfigFile ( process . cwd ( ) )
76166 if ( ! persistedOptions ) {
77167 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
80170 return
81171 }
82172
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+ }
107192 }
108193
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 ) )
110219
111220 const originalOutput = await runCreateApp (
112221 await createOptions ( persistedOptions ) ,
@@ -119,17 +228,12 @@ This is probably because this was created with an older version of create-tsrout
119228 ( await readFile ( 'package.json' ) ) . toString ( ) ,
120229 )
121230
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+ }
133237 }
134238
135239 const dependencies : Record < string , string > = { }
@@ -155,23 +259,65 @@ This is probably because this was created with an older version of create-tsrout
155259 }
156260 info . packageAdditions . devDependencies = devDependencies
157261
262+ // Find altered files
158263 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+ }
160299
161- info . files = changedFiles
162- info . deletedFiles = [ ]
300+ compiledInfo . routes = info . routes
301+ compiledInfo . framework = persistedOptions . framework
302+ compiledInfo . addDependencies = persistedOptions . existingAddOns
163303
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
169308
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+ }
173314 }
174315 }
175316
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 ) )
177323}
0 commit comments