@tanstack/cli 0.0.8 → 0.48.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/dist/bin.js +7 -0
- package/dist/cli.js +481 -0
- package/dist/command-line.js +174 -0
- package/dist/dev-watch.js +290 -0
- package/dist/file-syncer.js +148 -0
- package/dist/index.js +1 -0
- package/dist/mcp/api.js +31 -0
- package/dist/mcp/tools.js +250 -0
- package/dist/mcp/types.js +37 -0
- package/dist/mcp.js +121 -0
- package/dist/options.js +162 -0
- package/dist/types/bin.d.ts +2 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/command-line.d.ts +10 -0
- package/dist/types/dev-watch.d.ts +27 -0
- package/dist/types/file-syncer.d.ts +18 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mcp/api.d.ts +4 -0
- package/dist/types/mcp/tools.d.ts +2 -0
- package/dist/types/mcp/types.d.ts +217 -0
- package/dist/types/mcp.d.ts +6 -0
- package/dist/types/options.d.ts +8 -0
- package/dist/types/types.d.ts +25 -0
- package/dist/types/ui-environment.d.ts +2 -0
- package/dist/types/ui-prompts.d.ts +12 -0
- package/dist/types/utils.d.ts +8 -0
- package/dist/types.js +1 -0
- package/dist/ui-environment.js +52 -0
- package/dist/ui-prompts.js +244 -0
- package/dist/utils.js +30 -0
- package/package.json +46 -46
- package/src/bin.ts +6 -93
- package/src/cli.ts +692 -0
- package/src/command-line.ts +236 -0
- package/src/dev-watch.ts +430 -0
- package/src/file-syncer.ts +205 -0
- package/src/index.ts +1 -85
- package/src/mcp.ts +190 -0
- package/src/options.ts +260 -0
- package/src/types.ts +27 -0
- package/src/ui-environment.ts +74 -0
- package/src/ui-prompts.ts +322 -0
- package/src/utils.ts +38 -0
- package/tests/command-line.test.ts +304 -0
- package/tests/index.test.ts +9 -0
- package/tests/mcp.test.ts +225 -0
- package/tests/options.test.ts +304 -0
- package/tests/setupVitest.ts +6 -0
- package/tests/ui-environment.test.ts +97 -0
- package/tests/ui-prompts.test.ts +238 -0
- package/tsconfig.json +17 -0
- package/vitest.config.js +7 -0
- package/dist/bin.cjs +0 -769
- package/dist/bin.d.cts +0 -1
- package/dist/bin.d.mts +0 -1
- package/dist/bin.mjs +0 -768
- package/dist/fetch-CbFFGJEw.cjs +0 -3
- package/dist/fetch-DG5dLrsb.cjs +0 -522
- package/dist/fetch-DhlVXS6S.mjs +0 -390
- package/dist/fetch-I_OVg8JX.mjs +0 -3
- package/dist/index.cjs +0 -37
- package/dist/index.d.cts +0 -1172
- package/dist/index.d.mts +0 -1172
- package/dist/index.mjs +0 -4
- package/dist/template-Szi7-AZJ.mjs +0 -2202
- package/dist/template-lWrIZhCQ.cjs +0 -2314
- package/src/api/fetch.test.ts +0 -114
- package/src/api/fetch.ts +0 -278
- package/src/cache/index.ts +0 -89
- package/src/commands/create.ts +0 -470
- package/src/commands/mcp.test.ts +0 -152
- package/src/commands/mcp.ts +0 -211
- package/src/engine/compile-with-addons.test.ts +0 -302
- package/src/engine/compile.test.ts +0 -404
- package/src/engine/compile.ts +0 -569
- package/src/engine/config-file.test.ts +0 -118
- package/src/engine/config-file.ts +0 -61
- package/src/engine/custom-addons/integration.ts +0 -323
- package/src/engine/custom-addons/shared.test.ts +0 -98
- package/src/engine/custom-addons/shared.ts +0 -281
- package/src/engine/custom-addons/template.test.ts +0 -288
- package/src/engine/custom-addons/template.ts +0 -124
- package/src/engine/template.test.ts +0 -256
- package/src/engine/template.ts +0 -269
- package/src/engine/types.ts +0 -336
- package/src/parse-gitignore.d.ts +0 -5
- package/src/templates/base.ts +0 -883
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utilities for custom integration/template creation
|
|
3
|
-
* Based on Jack's implementation in create-tsrouter-app
|
|
4
|
-
*/
|
|
5
|
-
import { readdir } from 'node:fs/promises'
|
|
6
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
7
|
-
import { basename, extname, resolve } from 'node:path'
|
|
8
|
-
import ignore from 'ignore'
|
|
9
|
-
import parseGitignore from 'parse-gitignore'
|
|
10
|
-
|
|
11
|
-
import { compile } from '../compile.js'
|
|
12
|
-
import { readConfigFile } from '../config-file.js'
|
|
13
|
-
import { fetchIntegrations } from '../../api/fetch.js'
|
|
14
|
-
|
|
15
|
-
import type { PersistedOptions } from '../config-file.js'
|
|
16
|
-
import type { CompileOptions, CompileOutput, IntegrationCompiled } from '../types.js'
|
|
17
|
-
|
|
18
|
-
// Files to always ignore (from Jack's IGNORE_FILES)
|
|
19
|
-
const IGNORE_FILES = [
|
|
20
|
-
'.template',
|
|
21
|
-
'.integration',
|
|
22
|
-
'.tanstack.json',
|
|
23
|
-
'.git',
|
|
24
|
-
'integration-info.json',
|
|
25
|
-
'integration.json',
|
|
26
|
-
'build',
|
|
27
|
-
'bun.lock',
|
|
28
|
-
'bun.lockb',
|
|
29
|
-
'deno.lock',
|
|
30
|
-
'dist',
|
|
31
|
-
'node_modules',
|
|
32
|
-
'package-lock.json',
|
|
33
|
-
'pnpm-lock.yaml',
|
|
34
|
-
'template.json',
|
|
35
|
-
'template-info.json',
|
|
36
|
-
'yarn.lock',
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
const PROJECT_FILES = ['package.json']
|
|
40
|
-
|
|
41
|
-
const BINARY_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico']
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Check if a file is binary based on extension
|
|
45
|
-
*/
|
|
46
|
-
function isBinaryFile(path: string): boolean {
|
|
47
|
-
return BINARY_EXTENSIONS.includes(extname(path))
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Read file contents, handling binary files with base64 encoding
|
|
52
|
-
*/
|
|
53
|
-
export function readFileHelper(path: string): string {
|
|
54
|
-
if (isBinaryFile(path)) {
|
|
55
|
-
return `base64::${readFileSync(path).toString('base64')}`
|
|
56
|
-
}
|
|
57
|
-
return readFileSync(path, 'utf-8')
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Create an ignore function that respects .gitignore and standard ignore patterns
|
|
62
|
-
* Ported from Jack's createIgnore in file-helpers.ts
|
|
63
|
-
*/
|
|
64
|
-
export function createIgnore(
|
|
65
|
-
path: string,
|
|
66
|
-
includeProjectFiles = true,
|
|
67
|
-
): (filePath: string) => boolean {
|
|
68
|
-
const gitignorePath = resolve(path, '.gitignore')
|
|
69
|
-
const ignoreList = existsSync(gitignorePath)
|
|
70
|
-
? (
|
|
71
|
-
parseGitignore(readFileSync(gitignorePath)) as unknown as {
|
|
72
|
-
patterns: Array<string>
|
|
73
|
-
}
|
|
74
|
-
).patterns
|
|
75
|
-
: []
|
|
76
|
-
|
|
77
|
-
const ig = ignore().add(ignoreList)
|
|
78
|
-
|
|
79
|
-
return (filePath: string) => {
|
|
80
|
-
const fileName = basename(filePath)
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
IGNORE_FILES.includes(fileName) ||
|
|
84
|
-
(includeProjectFiles && PROJECT_FILES.includes(fileName))
|
|
85
|
-
) {
|
|
86
|
-
return true
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const nameWithoutDotSlash = fileName.replace(/^\.\//, '')
|
|
90
|
-
return ig.ignores(nameWithoutDotSlash)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Create package.json additions by comparing original and current
|
|
96
|
-
* Ported from Jack's createPackageAdditions
|
|
97
|
-
*/
|
|
98
|
-
export function createPackageAdditions(
|
|
99
|
-
originalPackageJson: Record<string, unknown>,
|
|
100
|
-
currentPackageJson: Record<string, unknown>,
|
|
101
|
-
): {
|
|
102
|
-
scripts?: Record<string, string>
|
|
103
|
-
dependencies?: Record<string, string>
|
|
104
|
-
devDependencies?: Record<string, string>
|
|
105
|
-
} {
|
|
106
|
-
const packageAdditions: {
|
|
107
|
-
scripts?: Record<string, string>
|
|
108
|
-
dependencies?: Record<string, string>
|
|
109
|
-
devDependencies?: Record<string, string>
|
|
110
|
-
} = {}
|
|
111
|
-
|
|
112
|
-
const origScripts = (originalPackageJson.scripts || {}) as Record<string, string>
|
|
113
|
-
const currScripts = (currentPackageJson.scripts || {}) as Record<string, string>
|
|
114
|
-
const scripts: Record<string, string> = {}
|
|
115
|
-
for (const script of Object.keys(currScripts)) {
|
|
116
|
-
const currValue = currScripts[script]
|
|
117
|
-
if (currValue && origScripts[script] !== currValue) {
|
|
118
|
-
scripts[script] = currValue
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (Object.keys(scripts).length) {
|
|
122
|
-
packageAdditions.scripts = scripts
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const origDeps = (originalPackageJson.dependencies || {}) as Record<string, string>
|
|
126
|
-
const currDeps = (currentPackageJson.dependencies || {}) as Record<string, string>
|
|
127
|
-
const dependencies: Record<string, string> = {}
|
|
128
|
-
for (const dep of Object.keys(currDeps)) {
|
|
129
|
-
const currValue = currDeps[dep]
|
|
130
|
-
if (currValue && origDeps[dep] !== currValue) {
|
|
131
|
-
dependencies[dep] = currValue
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (Object.keys(dependencies).length) {
|
|
135
|
-
packageAdditions.dependencies = dependencies
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const origDevDeps = (originalPackageJson.devDependencies || {}) as Record<string, string>
|
|
139
|
-
const currDevDeps = (currentPackageJson.devDependencies || {}) as Record<string, string>
|
|
140
|
-
const devDependencies: Record<string, string> = {}
|
|
141
|
-
for (const dep of Object.keys(currDevDeps)) {
|
|
142
|
-
const currValue = currDevDeps[dep]
|
|
143
|
-
if (currValue && origDevDeps[dep] !== currValue) {
|
|
144
|
-
devDependencies[dep] = currValue
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (Object.keys(devDependencies).length) {
|
|
148
|
-
packageAdditions.devDependencies = devDependencies
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return packageAdditions
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export async function createCompileOptionsFromPersisted(
|
|
155
|
-
persisted: PersistedOptions,
|
|
156
|
-
integrationsPath?: string,
|
|
157
|
-
): Promise<CompileOptions> {
|
|
158
|
-
let chosenIntegrations: Array<IntegrationCompiled> = []
|
|
159
|
-
if (persisted.chosenIntegrations.length > 0) {
|
|
160
|
-
chosenIntegrations = await fetchIntegrations(persisted.chosenIntegrations, integrationsPath)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
projectName: persisted.projectName,
|
|
165
|
-
framework: persisted.framework,
|
|
166
|
-
mode: persisted.mode,
|
|
167
|
-
typescript: persisted.typescript,
|
|
168
|
-
tailwind: persisted.tailwind,
|
|
169
|
-
packageManager: persisted.packageManager,
|
|
170
|
-
chosenIntegrations,
|
|
171
|
-
integrationOptions: {},
|
|
172
|
-
customTemplate: undefined,
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function runCompile(options: CompileOptions): CompileOutput {
|
|
177
|
-
return compile(options)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Compare files recursively between current project and original output
|
|
182
|
-
* Ported from Jack's compareFilesRecursively
|
|
183
|
-
*/
|
|
184
|
-
export async function compareFilesRecursively(
|
|
185
|
-
basePath: string,
|
|
186
|
-
ignoreFn: (filePath: string) => boolean,
|
|
187
|
-
original: Record<string, string>,
|
|
188
|
-
changedFiles: Record<string, string>,
|
|
189
|
-
): Promise<void> {
|
|
190
|
-
await compareFilesRecursivelyHelper(basePath, '.', ignoreFn, original, changedFiles)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function compareFilesRecursivelyHelper(
|
|
194
|
-
basePath: string,
|
|
195
|
-
relativePath: string,
|
|
196
|
-
ignoreFn: (filePath: string) => boolean,
|
|
197
|
-
original: Record<string, string>,
|
|
198
|
-
changedFiles: Record<string, string>,
|
|
199
|
-
): Promise<void> {
|
|
200
|
-
const fullPath = resolve(basePath, relativePath)
|
|
201
|
-
const entries = await readdir(fullPath, { withFileTypes: true })
|
|
202
|
-
|
|
203
|
-
for (const entry of entries) {
|
|
204
|
-
const entryRelativePath = relativePath === '.' ? entry.name : `${relativePath}/${entry.name}`
|
|
205
|
-
const entryFullPath = resolve(basePath, entryRelativePath)
|
|
206
|
-
|
|
207
|
-
if (ignoreFn(entry.name)) {
|
|
208
|
-
continue
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (entry.isDirectory()) {
|
|
212
|
-
await compareFilesRecursivelyHelper(basePath, entryRelativePath, ignoreFn, original, changedFiles)
|
|
213
|
-
} else {
|
|
214
|
-
const contents = readFileHelper(entryFullPath)
|
|
215
|
-
// Original files use paths without ./ prefix
|
|
216
|
-
const originalKey = entryRelativePath
|
|
217
|
-
|
|
218
|
-
if (!original[originalKey] || original[originalKey] !== contents) {
|
|
219
|
-
changedFiles[entryRelativePath] = contents
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export async function readCurrentProjectOptions(
|
|
226
|
-
targetDir: string,
|
|
227
|
-
): Promise<PersistedOptions> {
|
|
228
|
-
const persisted = await readConfigFile(targetDir)
|
|
229
|
-
if (!persisted) {
|
|
230
|
-
throw new Error(
|
|
231
|
-
`No .tanstack.json file found in ${targetDir}.\n` +
|
|
232
|
-
`This project may have been created with an older version of the CLI, ` +
|
|
233
|
-
`or was not created with the TanStack CLI.`,
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
return persisted
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Recursively gather files from a directory
|
|
241
|
-
* Ported from Jack's recursivelyGatherFiles
|
|
242
|
-
*/
|
|
243
|
-
export async function recursivelyGatherFiles(
|
|
244
|
-
path: string,
|
|
245
|
-
includeProjectFiles = true,
|
|
246
|
-
): Promise<Record<string, string>> {
|
|
247
|
-
const ignoreFn = createIgnore(path, includeProjectFiles)
|
|
248
|
-
const files: Record<string, string> = {}
|
|
249
|
-
|
|
250
|
-
if (!existsSync(path)) {
|
|
251
|
-
return files
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
await gatherFilesHelper(path, '.', files, ignoreFn)
|
|
255
|
-
return files
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async function gatherFilesHelper(
|
|
259
|
-
basePath: string,
|
|
260
|
-
relativePath: string,
|
|
261
|
-
files: Record<string, string>,
|
|
262
|
-
ignoreFn: (filePath: string) => boolean,
|
|
263
|
-
): Promise<void> {
|
|
264
|
-
const fullPath = resolve(basePath, relativePath)
|
|
265
|
-
const entries = await readdir(fullPath, { withFileTypes: true })
|
|
266
|
-
|
|
267
|
-
for (const entry of entries) {
|
|
268
|
-
if (ignoreFn(entry.name)) {
|
|
269
|
-
continue
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const entryRelativePath = relativePath === '.' ? entry.name : `${relativePath}/${entry.name}`
|
|
273
|
-
const entryFullPath = resolve(basePath, entryRelativePath)
|
|
274
|
-
|
|
275
|
-
if (entry.isDirectory()) {
|
|
276
|
-
await gatherFilesHelper(basePath, entryRelativePath, files, ignoreFn)
|
|
277
|
-
} else {
|
|
278
|
-
files[entryRelativePath] = readFileHelper(entryFullPath)
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { fetchIntegrations } from '../../api/fetch.js'
|
|
4
|
-
import { compile } from '../compile.js'
|
|
5
|
-
import { CustomTemplateCompiledSchema } from '../types.js'
|
|
6
|
-
import type { CustomTemplateCompiled } from '../types.js'
|
|
7
|
-
|
|
8
|
-
const INTEGRATIONS_PATH = resolve(__dirname, '../../../../../integrations')
|
|
9
|
-
|
|
10
|
-
describe('Custom template schema', () => {
|
|
11
|
-
it('should validate a minimal template', () => {
|
|
12
|
-
const template = {
|
|
13
|
-
id: 'my-template',
|
|
14
|
-
name: 'My Template',
|
|
15
|
-
description: 'A simple template',
|
|
16
|
-
framework: 'react',
|
|
17
|
-
mode: 'file-router',
|
|
18
|
-
typescript: true,
|
|
19
|
-
tailwind: true,
|
|
20
|
-
integrations: [],
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const result = CustomTemplateCompiledSchema.safeParse(template)
|
|
24
|
-
expect(result.success).toBe(true)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('should validate a template with integrations', () => {
|
|
28
|
-
const template = {
|
|
29
|
-
id: 'saas-template',
|
|
30
|
-
name: 'SaaS Template',
|
|
31
|
-
description: 'Complete SaaS setup',
|
|
32
|
-
framework: 'react',
|
|
33
|
-
mode: 'file-router',
|
|
34
|
-
typescript: true,
|
|
35
|
-
tailwind: true,
|
|
36
|
-
integrations: ['tanstack-query', 'clerk', 'drizzle', 'shadcn'],
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const result = CustomTemplateCompiledSchema.safeParse(template)
|
|
40
|
-
expect(result.success).toBe(true)
|
|
41
|
-
if (result.success) {
|
|
42
|
-
expect(result.data.integrations).toHaveLength(4)
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('should validate a template with integration options', () => {
|
|
47
|
-
const template = {
|
|
48
|
-
id: 'db-template',
|
|
49
|
-
name: 'Database Template',
|
|
50
|
-
description: 'Template with database preset',
|
|
51
|
-
framework: 'react',
|
|
52
|
-
mode: 'file-router',
|
|
53
|
-
typescript: true,
|
|
54
|
-
tailwind: true,
|
|
55
|
-
integrations: ['drizzle'],
|
|
56
|
-
integrationOptions: {
|
|
57
|
-
drizzle: {
|
|
58
|
-
database: 'postgres',
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const result = CustomTemplateCompiledSchema.safeParse(template)
|
|
64
|
-
expect(result.success).toBe(true)
|
|
65
|
-
if (result.success) {
|
|
66
|
-
expect(result.data.integrationOptions?.drizzle).toEqual({ database: 'postgres' })
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('should validate a template with banner', () => {
|
|
71
|
-
const template = {
|
|
72
|
-
id: 'branded-template',
|
|
73
|
-
name: 'Branded Template',
|
|
74
|
-
description: 'A branded template',
|
|
75
|
-
framework: 'react',
|
|
76
|
-
mode: 'file-router',
|
|
77
|
-
typescript: true,
|
|
78
|
-
tailwind: false,
|
|
79
|
-
integrations: [],
|
|
80
|
-
banner: 'https://example.com/banner.png',
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const result = CustomTemplateCompiledSchema.safeParse(template)
|
|
84
|
-
expect(result.success).toBe(true)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('should reject template without required fields', () => {
|
|
88
|
-
const template = {
|
|
89
|
-
id: 'incomplete',
|
|
90
|
-
name: 'Incomplete',
|
|
91
|
-
// missing: description, framework, mode, typescript, tailwind, integrations
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const result = CustomTemplateCompiledSchema.safeParse(template)
|
|
95
|
-
expect(result.success).toBe(false)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('should reject template with invalid mode', () => {
|
|
99
|
-
const template = {
|
|
100
|
-
id: 'invalid-mode',
|
|
101
|
-
name: 'Invalid Mode',
|
|
102
|
-
description: 'Has invalid mode',
|
|
103
|
-
framework: 'react',
|
|
104
|
-
mode: 'invalid-mode', // should be 'file-router' or 'code-router'
|
|
105
|
-
typescript: true,
|
|
106
|
-
tailwind: true,
|
|
107
|
-
integrations: [],
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const result = CustomTemplateCompiledSchema.safeParse(template)
|
|
111
|
-
expect(result.success).toBe(false)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('should allow code-router mode', () => {
|
|
115
|
-
const template = {
|
|
116
|
-
id: 'code-router-template',
|
|
117
|
-
name: 'Code Router Template',
|
|
118
|
-
description: 'Uses code router',
|
|
119
|
-
framework: 'react',
|
|
120
|
-
mode: 'code-router',
|
|
121
|
-
typescript: true,
|
|
122
|
-
tailwind: true,
|
|
123
|
-
integrations: [],
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const result = CustomTemplateCompiledSchema.safeParse(template)
|
|
127
|
-
expect(result.success).toBe(true)
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
describe('Custom template as integration preset', () => {
|
|
132
|
-
it('templates should NOT have files property', () => {
|
|
133
|
-
const templateWithFiles = {
|
|
134
|
-
id: 'files-template',
|
|
135
|
-
name: 'Files Template',
|
|
136
|
-
description: 'Tries to have files',
|
|
137
|
-
framework: 'react',
|
|
138
|
-
mode: 'file-router',
|
|
139
|
-
typescript: true,
|
|
140
|
-
tailwind: true,
|
|
141
|
-
integrations: [],
|
|
142
|
-
files: { 'src/custom.ts': 'export const x = 1' }, // should be rejected
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const result = CustomTemplateCompiledSchema.safeParse(templateWithFiles)
|
|
146
|
-
// Zod strips unknown keys by default, so this passes but files is removed
|
|
147
|
-
expect(result.success).toBe(true)
|
|
148
|
-
if (result.success) {
|
|
149
|
-
expect((result.data as Record<string, unknown>).files).toBeUndefined()
|
|
150
|
-
}
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
it('templates should NOT have packageAdditions property', () => {
|
|
154
|
-
const templateWithPackages = {
|
|
155
|
-
id: 'packages-template',
|
|
156
|
-
name: 'Packages Template',
|
|
157
|
-
description: 'Tries to have packages',
|
|
158
|
-
framework: 'react',
|
|
159
|
-
mode: 'file-router',
|
|
160
|
-
typescript: true,
|
|
161
|
-
tailwind: true,
|
|
162
|
-
integrations: [],
|
|
163
|
-
packageAdditions: { dependencies: { foo: '1.0.0' } }, // should be stripped
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const result = CustomTemplateCompiledSchema.safeParse(templateWithPackages)
|
|
167
|
-
expect(result.success).toBe(true)
|
|
168
|
-
if (result.success) {
|
|
169
|
-
expect((result.data as Record<string, unknown>).packageAdditions).toBeUndefined()
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
describe('Custom template end-to-end flow', () => {
|
|
175
|
-
it('should resolve template integrations and compile project', async () => {
|
|
176
|
-
// Simulate what the create command does
|
|
177
|
-
const template: CustomTemplateCompiled = {
|
|
178
|
-
id: 'test-template',
|
|
179
|
-
name: 'Test Template',
|
|
180
|
-
description: 'Test template with real integrations',
|
|
181
|
-
framework: 'react',
|
|
182
|
-
mode: 'file-router',
|
|
183
|
-
typescript: true,
|
|
184
|
-
tailwind: true,
|
|
185
|
-
integrations: ['tanstack-query', 'tanstack-form'],
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Fetch the integrations specified in the template
|
|
189
|
-
const chosenIntegrations = await fetchIntegrations(template.integrations, INTEGRATIONS_PATH)
|
|
190
|
-
expect(chosenIntegrations).toHaveLength(2)
|
|
191
|
-
|
|
192
|
-
// Compile with the template
|
|
193
|
-
const output = compile({
|
|
194
|
-
projectName: 'template-test-project',
|
|
195
|
-
framework: template.framework,
|
|
196
|
-
mode: template.mode,
|
|
197
|
-
typescript: template.typescript,
|
|
198
|
-
tailwind: template.tailwind,
|
|
199
|
-
packageManager: 'pnpm',
|
|
200
|
-
chosenIntegrations,
|
|
201
|
-
integrationOptions: template.integrationOptions ?? {},
|
|
202
|
-
customTemplate: template,
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
// Verify base files exist
|
|
206
|
-
expect(output.files).toHaveProperty('package.json')
|
|
207
|
-
expect(output.files).toHaveProperty('vite.config.ts')
|
|
208
|
-
|
|
209
|
-
// Verify integration files are included
|
|
210
|
-
expect(output.files).toHaveProperty('src/integrations/query/provider.tsx')
|
|
211
|
-
expect(output.files).toHaveProperty('src/routes/demo/query.tsx')
|
|
212
|
-
expect(output.files).toHaveProperty('src/routes/demo/form.tsx')
|
|
213
|
-
|
|
214
|
-
// Verify integration dependencies are merged
|
|
215
|
-
const pkg = JSON.parse(output.files['package.json']!)
|
|
216
|
-
expect(pkg.dependencies).toHaveProperty('@tanstack/react-query')
|
|
217
|
-
expect(pkg.dependencies).toHaveProperty('@tanstack/react-form')
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('should respect template integration options', async () => {
|
|
221
|
-
const template: CustomTemplateCompiled = {
|
|
222
|
-
id: 'options-template',
|
|
223
|
-
name: 'Options Template',
|
|
224
|
-
description: 'Template with preset options',
|
|
225
|
-
framework: 'react',
|
|
226
|
-
mode: 'file-router',
|
|
227
|
-
typescript: true,
|
|
228
|
-
tailwind: true,
|
|
229
|
-
integrations: ['drizzle'],
|
|
230
|
-
integrationOptions: {
|
|
231
|
-
drizzle: {
|
|
232
|
-
database: 'sqlite',
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const chosenIntegrations = await fetchIntegrations(template.integrations, INTEGRATIONS_PATH)
|
|
238
|
-
|
|
239
|
-
const output = compile({
|
|
240
|
-
projectName: 'options-test',
|
|
241
|
-
framework: template.framework,
|
|
242
|
-
mode: template.mode,
|
|
243
|
-
typescript: template.typescript,
|
|
244
|
-
tailwind: template.tailwind,
|
|
245
|
-
packageManager: 'pnpm',
|
|
246
|
-
chosenIntegrations,
|
|
247
|
-
integrationOptions: template.integrationOptions ?? {},
|
|
248
|
-
customTemplate: template,
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
// The integration options should be available for template processing
|
|
252
|
-
// (actual option handling depends on the integration's template files)
|
|
253
|
-
expect(output.files).toHaveProperty('package.json')
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('should work with empty integration list', () => {
|
|
257
|
-
const template: CustomTemplateCompiled = {
|
|
258
|
-
id: 'minimal-template',
|
|
259
|
-
name: 'Minimal Template',
|
|
260
|
-
description: 'Just the defaults',
|
|
261
|
-
framework: 'react',
|
|
262
|
-
mode: 'file-router',
|
|
263
|
-
typescript: true,
|
|
264
|
-
tailwind: false, // no tailwind
|
|
265
|
-
integrations: [],
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const output = compile({
|
|
269
|
-
projectName: 'minimal-test',
|
|
270
|
-
framework: template.framework,
|
|
271
|
-
mode: template.mode,
|
|
272
|
-
typescript: template.typescript,
|
|
273
|
-
tailwind: template.tailwind,
|
|
274
|
-
packageManager: 'pnpm',
|
|
275
|
-
chosenIntegrations: [],
|
|
276
|
-
integrationOptions: {},
|
|
277
|
-
customTemplate: template,
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
// Base files should exist
|
|
281
|
-
expect(output.files).toHaveProperty('package.json')
|
|
282
|
-
expect(output.files).toHaveProperty('vite.config.ts')
|
|
283
|
-
|
|
284
|
-
// Tailwind should NOT be in the output
|
|
285
|
-
const viteConfig = output.files['vite.config.ts']!
|
|
286
|
-
expect(viteConfig).not.toContain('tailwindcss')
|
|
287
|
-
})
|
|
288
|
-
})
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'node:fs/promises'
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
3
|
-
import { resolve } from 'node:path'
|
|
4
|
-
|
|
5
|
-
import { CustomTemplateCompiledSchema } from '../types.js'
|
|
6
|
-
import { readCurrentProjectOptions } from './shared.js'
|
|
7
|
-
|
|
8
|
-
import type { PersistedOptions } from '../config-file.js'
|
|
9
|
-
import type { CustomTemplateCompiled, CustomTemplateInfo } from '../types.js'
|
|
10
|
-
|
|
11
|
-
const INFO_FILE = 'template-info.json'
|
|
12
|
-
const COMPILED_FILE = 'template.json'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Generate default template info from project options
|
|
16
|
-
* Custom templates are just integration presets - they capture which integrations are selected
|
|
17
|
-
*/
|
|
18
|
-
async function readOrGenerateTemplateInfo(
|
|
19
|
-
options: PersistedOptions,
|
|
20
|
-
targetDir: string,
|
|
21
|
-
): Promise<CustomTemplateInfo> {
|
|
22
|
-
const infoPath = resolve(targetDir, INFO_FILE)
|
|
23
|
-
|
|
24
|
-
if (existsSync(infoPath)) {
|
|
25
|
-
const content = await readFile(infoPath, 'utf-8')
|
|
26
|
-
return JSON.parse(content)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
id: `${options.projectName}-template`,
|
|
31
|
-
name: `${options.projectName} Template`,
|
|
32
|
-
description: 'A curated project template',
|
|
33
|
-
|
|
34
|
-
framework: options.framework,
|
|
35
|
-
mode: options.mode,
|
|
36
|
-
typescript: options.typescript,
|
|
37
|
-
tailwind: options.tailwind,
|
|
38
|
-
|
|
39
|
-
integrations: options.chosenIntegrations,
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Compile a custom template from the current project
|
|
45
|
-
* Custom templates are just integration presets - they specify project defaults and which integrations to include
|
|
46
|
-
*/
|
|
47
|
-
export async function compileTemplate(
|
|
48
|
-
targetDir: string,
|
|
49
|
-
): Promise<void> {
|
|
50
|
-
const persistedOptions = await readCurrentProjectOptions(targetDir)
|
|
51
|
-
const info = await readOrGenerateTemplateInfo(persistedOptions, targetDir)
|
|
52
|
-
|
|
53
|
-
const compiledInfo: CustomTemplateCompiled = {
|
|
54
|
-
...info,
|
|
55
|
-
id: info.id || `${persistedOptions.projectName}-template`,
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
await writeFile(
|
|
59
|
-
resolve(targetDir, COMPILED_FILE),
|
|
60
|
-
JSON.stringify(compiledInfo, null, 2),
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
console.log(`Compiled template written to ${COMPILED_FILE}`)
|
|
64
|
-
console.log(`\nIncluded integrations: ${compiledInfo.integrations.length > 0 ? compiledInfo.integrations.join(', ') : '(none)'}`)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function initTemplate(
|
|
68
|
-
targetDir: string,
|
|
69
|
-
): Promise<void> {
|
|
70
|
-
const persistedOptions = await readCurrentProjectOptions(targetDir)
|
|
71
|
-
const info = await readOrGenerateTemplateInfo(persistedOptions, targetDir)
|
|
72
|
-
|
|
73
|
-
// Write the info file for editing
|
|
74
|
-
await writeFile(
|
|
75
|
-
resolve(targetDir, INFO_FILE),
|
|
76
|
-
JSON.stringify(info, null, 2),
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
// Compile the template
|
|
80
|
-
await compileTemplate(targetDir)
|
|
81
|
-
|
|
82
|
-
console.log(`
|
|
83
|
-
Custom template initialized successfully!
|
|
84
|
-
|
|
85
|
-
Files created:
|
|
86
|
-
${INFO_FILE} - Template metadata (edit this to customize)
|
|
87
|
-
${COMPILED_FILE} - Compiled template (distribute this)
|
|
88
|
-
|
|
89
|
-
Custom templates are integration presets. They capture:
|
|
90
|
-
- Project defaults (framework, mode, typescript, tailwind)
|
|
91
|
-
- Which integrations to include
|
|
92
|
-
- Preset integration options (if any)
|
|
93
|
-
|
|
94
|
-
Next steps:
|
|
95
|
-
1. Edit ${INFO_FILE} to customize name, description, and integrations
|
|
96
|
-
2. Run 'tanstack template compile' to rebuild after changes
|
|
97
|
-
3. Share ${COMPILED_FILE} or host it publicly
|
|
98
|
-
4. Users can use: tanstack create --template <url-to-template.json>
|
|
99
|
-
`)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Load a remote custom template from a URL
|
|
104
|
-
*/
|
|
105
|
-
export async function loadTemplate(url: string): Promise<CustomTemplateCompiled> {
|
|
106
|
-
const response = await fetch(url)
|
|
107
|
-
if (!response.ok) {
|
|
108
|
-
throw new Error(`Failed to fetch template from ${url}: ${response.statusText}`)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const jsonContent = await response.json()
|
|
112
|
-
|
|
113
|
-
const result = CustomTemplateCompiledSchema.safeParse(jsonContent)
|
|
114
|
-
if (!result.success) {
|
|
115
|
-
throw new Error(`Invalid template at ${url}: ${result.error.message}`)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const template = result.data
|
|
119
|
-
if (!template.id) {
|
|
120
|
-
template.id = url
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return template
|
|
124
|
-
}
|