@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.
Files changed (87) hide show
  1. package/dist/bin.js +7 -0
  2. package/dist/cli.js +481 -0
  3. package/dist/command-line.js +174 -0
  4. package/dist/dev-watch.js +290 -0
  5. package/dist/file-syncer.js +148 -0
  6. package/dist/index.js +1 -0
  7. package/dist/mcp/api.js +31 -0
  8. package/dist/mcp/tools.js +250 -0
  9. package/dist/mcp/types.js +37 -0
  10. package/dist/mcp.js +121 -0
  11. package/dist/options.js +162 -0
  12. package/dist/types/bin.d.ts +2 -0
  13. package/dist/types/cli.d.ts +16 -0
  14. package/dist/types/command-line.d.ts +10 -0
  15. package/dist/types/dev-watch.d.ts +27 -0
  16. package/dist/types/file-syncer.d.ts +18 -0
  17. package/dist/types/index.d.ts +1 -0
  18. package/dist/types/mcp/api.d.ts +4 -0
  19. package/dist/types/mcp/tools.d.ts +2 -0
  20. package/dist/types/mcp/types.d.ts +217 -0
  21. package/dist/types/mcp.d.ts +6 -0
  22. package/dist/types/options.d.ts +8 -0
  23. package/dist/types/types.d.ts +25 -0
  24. package/dist/types/ui-environment.d.ts +2 -0
  25. package/dist/types/ui-prompts.d.ts +12 -0
  26. package/dist/types/utils.d.ts +8 -0
  27. package/dist/types.js +1 -0
  28. package/dist/ui-environment.js +52 -0
  29. package/dist/ui-prompts.js +244 -0
  30. package/dist/utils.js +30 -0
  31. package/package.json +46 -46
  32. package/src/bin.ts +6 -93
  33. package/src/cli.ts +692 -0
  34. package/src/command-line.ts +236 -0
  35. package/src/dev-watch.ts +430 -0
  36. package/src/file-syncer.ts +205 -0
  37. package/src/index.ts +1 -85
  38. package/src/mcp.ts +190 -0
  39. package/src/options.ts +260 -0
  40. package/src/types.ts +27 -0
  41. package/src/ui-environment.ts +74 -0
  42. package/src/ui-prompts.ts +322 -0
  43. package/src/utils.ts +38 -0
  44. package/tests/command-line.test.ts +304 -0
  45. package/tests/index.test.ts +9 -0
  46. package/tests/mcp.test.ts +225 -0
  47. package/tests/options.test.ts +304 -0
  48. package/tests/setupVitest.ts +6 -0
  49. package/tests/ui-environment.test.ts +97 -0
  50. package/tests/ui-prompts.test.ts +238 -0
  51. package/tsconfig.json +17 -0
  52. package/vitest.config.js +7 -0
  53. package/dist/bin.cjs +0 -769
  54. package/dist/bin.d.cts +0 -1
  55. package/dist/bin.d.mts +0 -1
  56. package/dist/bin.mjs +0 -768
  57. package/dist/fetch-CbFFGJEw.cjs +0 -3
  58. package/dist/fetch-DG5dLrsb.cjs +0 -522
  59. package/dist/fetch-DhlVXS6S.mjs +0 -390
  60. package/dist/fetch-I_OVg8JX.mjs +0 -3
  61. package/dist/index.cjs +0 -37
  62. package/dist/index.d.cts +0 -1172
  63. package/dist/index.d.mts +0 -1172
  64. package/dist/index.mjs +0 -4
  65. package/dist/template-Szi7-AZJ.mjs +0 -2202
  66. package/dist/template-lWrIZhCQ.cjs +0 -2314
  67. package/src/api/fetch.test.ts +0 -114
  68. package/src/api/fetch.ts +0 -278
  69. package/src/cache/index.ts +0 -89
  70. package/src/commands/create.ts +0 -470
  71. package/src/commands/mcp.test.ts +0 -152
  72. package/src/commands/mcp.ts +0 -211
  73. package/src/engine/compile-with-addons.test.ts +0 -302
  74. package/src/engine/compile.test.ts +0 -404
  75. package/src/engine/compile.ts +0 -569
  76. package/src/engine/config-file.test.ts +0 -118
  77. package/src/engine/config-file.ts +0 -61
  78. package/src/engine/custom-addons/integration.ts +0 -323
  79. package/src/engine/custom-addons/shared.test.ts +0 -98
  80. package/src/engine/custom-addons/shared.ts +0 -281
  81. package/src/engine/custom-addons/template.test.ts +0 -288
  82. package/src/engine/custom-addons/template.ts +0 -124
  83. package/src/engine/template.test.ts +0 -256
  84. package/src/engine/template.ts +0 -269
  85. package/src/engine/types.ts +0 -336
  86. package/src/parse-gitignore.d.ts +0 -5
  87. 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
- }