Skip to content

Commit ea88e58

Browse files
committed
feat: add-ons and overlays
1 parent 968fae8 commit ea88e58

File tree

3 files changed

+226
-73
lines changed

3 files changed

+226
-73
lines changed

src/cli.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,17 @@ export function cli() {
3333
})
3434

3535
program
36-
.command('init-add-on')
37-
.description('Initialize a new add-on from the current project')
36+
.command('update-add-on')
37+
.description('Create or update an add-on from the current project')
3838
.action(async () => {
39-
await initAddOn()
39+
await initAddOn('add-on')
40+
})
41+
42+
program
43+
.command('update-overlay')
44+
.description('Create or update a project overlay from the current project')
45+
.action(async () => {
46+
await initAddOn('overlay')
4047
})
4148

4249
program // 104 22

src/custom-add-on.ts

Lines changed: 215 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
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'
44
import chalk from 'chalk'
55

66
import { createMemoryEnvironment } from './environment.js'
@@ -11,6 +11,96 @@ import { finalizeAddOns } from './add-ons.js'
1111
import type { Options } from './types.js'
1212
import 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+
/import { createFileRoute } from '@tanstack\/react-router'/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+
/export\s+const\s+Route\s*=\s*createFileRoute\(['"]([^'"]+)['"]\)\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(/^demo/, '')
98+
.replace('.', ' ')
99+
.trim()
100+
101+
return { url: path, code, name }
102+
}
103+
14104
async 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

52141
async 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
}

templates/react/add-on/form/assets/src/routes/demo.form.address.tsx.ejs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { <% if (fileRouter) { %>createFileRoute<% } else { %>createRoute<% } %>
33
import { useAppForm } from '../hooks/demo.form'
44

55
<% if (codeRouter) { %>
6-
import type { RootRoute } from '@tanstack/react-router'
6+
import type { RootRoute } from '@tanstack/react-router'
77
<% } else { %>
88
export const Route = createFileRoute('/demo/form')({
99
component: AddressForm,

0 commit comments

Comments
 (0)