@tanstack/cta-engine 0.28.0 → 0.29.0

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/types.js CHANGED
@@ -1,4 +1,18 @@
1
1
  import z from 'zod';
2
+ export const AddOnSelectOptionSchema = z.object({
3
+ type: z.literal('select'),
4
+ label: z.string(),
5
+ description: z.string().optional(),
6
+ default: z.string(),
7
+ options: z.array(z.object({
8
+ value: z.string(),
9
+ label: z.string(),
10
+ })),
11
+ });
12
+ export const AddOnOptionSchema = z.discriminatedUnion('type', [
13
+ AddOnSelectOptionSchema,
14
+ ]);
15
+ export const AddOnOptionsSchema = z.record(z.string(), AddOnOptionSchema);
2
16
  export const AddOnBaseSchema = z.object({
3
17
  id: z.string(),
4
18
  name: z.string(),
@@ -36,6 +50,8 @@ export const AddOnBaseSchema = z.object({
36
50
  logo: z.string().optional(),
37
51
  addOnSpecialSteps: z.array(z.string()).optional(),
38
52
  createSpecialSteps: z.array(z.string()).optional(),
53
+ postInitSpecialSteps: z.array(z.string()).optional(),
54
+ options: AddOnOptionsSchema.optional(),
39
55
  default: z.boolean().optional(),
40
56
  });
41
57
  export const StarterSchema = AddOnBaseSchema.extend({
@@ -65,4 +81,5 @@ export const AddOnInfoSchema = AddOnBaseSchema.extend({
65
81
  export const AddOnCompiledSchema = AddOnInfoSchema.extend({
66
82
  files: z.record(z.string(), z.string()),
67
83
  deletedFiles: z.array(z.string()),
84
+ packageTemplate: z.string().optional(),
68
85
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cta-engine",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Tanstack Application Builder Engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/add-ons.ts CHANGED
@@ -47,3 +47,21 @@ export async function finalizeAddOns(
47
47
  function loadAddOn(addOn: AddOn): AddOn {
48
48
  return addOn
49
49
  }
50
+
51
+ export function populateAddOnOptionsDefaults(
52
+ chosenAddOns: Array<AddOn>
53
+ ): Record<string, Record<string, any>> {
54
+ const addOnOptions: Record<string, Record<string, any>> = {}
55
+
56
+ for (const addOn of chosenAddOns) {
57
+ if (addOn.options) {
58
+ const defaults: Record<string, any> = {}
59
+ for (const [optionKey, optionDef] of Object.entries(addOn.options)) {
60
+ defaults[optionKey] = optionDef.default
61
+ }
62
+ addOnOptions[addOn.id] = defaults
63
+ }
64
+ }
65
+
66
+ return addOnOptions
67
+ }
package/src/add-to-app.ts CHANGED
@@ -181,7 +181,9 @@ export async function runNewCommands(
181
181
  type: 'command',
182
182
  message: `Running ${formatCommand({ command: command.command, args: command.args })}...`,
183
183
  })
184
- await environment.execute(command.command, command.args, cwd)
184
+ await environment.execute(command.command, command.args, cwd, {
185
+ inherit: true,
186
+ })
185
187
  environment.finishStep('run-commands', 'Setup commands complete')
186
188
  }
187
189
  }
@@ -260,6 +262,23 @@ export async function addToApp(
260
262
  s.stop(`Installed dependencies`)
261
263
  environment.finishStep('install-dependencies', 'Dependencies installed')
262
264
 
265
+ // Run any post-init special steps for the new add-ons
266
+ const postInitSpecialSteps = new Set<string>([])
267
+ for (const addOn of newOptions.chosenAddOns) {
268
+ for (const step of addOn.postInitSpecialSteps || []) {
269
+ if (addOns.includes(addOn.id)) {
270
+ postInitSpecialSteps.add(step)
271
+ }
272
+ }
273
+ }
274
+ if (postInitSpecialSteps.size) {
275
+ await runSpecialSteps(
276
+ environment,
277
+ newOptions,
278
+ Array.from(postInitSpecialSteps),
279
+ )
280
+ }
281
+
263
282
  // Handle new commands
264
283
 
265
284
  await runNewCommands(environment, persistedOptions, cwd, output)
package/src/create-app.ts CHANGED
@@ -147,6 +147,21 @@ async function runCommandsAndInstallDependencies(
147
147
  environment.finishStep('install-dependencies', 'Installed dependencies')
148
148
  s.stop(`Installed dependencies`)
149
149
 
150
+ // Run any post-init special steps for the new add-ons
151
+ const postInitSpecialSteps = new Set<string>([])
152
+ for (const addOn of options.chosenAddOns) {
153
+ for (const step of addOn.postInitSpecialSteps || []) {
154
+ postInitSpecialSteps.add(step)
155
+ }
156
+ }
157
+ if (postInitSpecialSteps.size) {
158
+ await runSpecialSteps(
159
+ environment,
160
+ options,
161
+ Array.from(postInitSpecialSteps),
162
+ )
163
+ }
164
+
150
165
  for (const phase of ['setup', 'add-on', 'example']) {
151
166
  for (const addOn of options.chosenAddOns.filter(
152
167
  (addOn) =>
@@ -166,6 +181,7 @@ async function runCommandsAndInstallDependencies(
166
181
  addOn.command!.command,
167
182
  addOn.command!.args || [],
168
183
  options.targetDir,
184
+ { inherit: true },
169
185
  )
170
186
  environment.finishStep('run-commands', 'Setup commands complete')
171
187
  s.stop(`${addOn.name} commands complete`)
@@ -193,6 +209,7 @@ async function runCommandsAndInstallDependencies(
193
209
  options.starter.command.command,
194
210
  options.starter.command.args || [],
195
211
  options.targetDir,
212
+ { inherit: true },
196
213
  )
197
214
 
198
215
  environment.finishStep('run-starter-command', 'Starter command complete')
@@ -2,7 +2,7 @@ import { readdir } from 'node:fs/promises'
2
2
  import { resolve } from 'node:path'
3
3
  import { createApp } from '../create-app.js'
4
4
  import { createMemoryEnvironment } from '../environment.js'
5
- import { finalizeAddOns } from '../add-ons.js'
5
+ import { finalizeAddOns, populateAddOnOptionsDefaults } from '../add-ons.js'
6
6
  import { getFrameworkById } from '../frameworks.js'
7
7
  import { readConfigFileFromEnvironment } from '../config-file.js'
8
8
  import { readFileHelper } from '../file-helpers.js'
@@ -66,6 +66,9 @@ export async function createAppOptionsFromPersisted(
66
66
  const { version, ...rest } = json
67
67
  /* eslint-enable unused-imports/no-unused-vars */
68
68
  const framework = getFrameworkById(rest.framework)
69
+ const chosenAddOns = await finalizeAddOns(framework!, json.mode!, [
70
+ ...json.chosenAddOns,
71
+ ])
69
72
  return {
70
73
  ...rest,
71
74
  mode: json.mode!,
@@ -77,9 +80,8 @@ export async function createAppOptionsFromPersisted(
77
80
  targetDir: '',
78
81
  framework: framework!,
79
82
  starter: json.starter ? await loadStarter(json.starter) : undefined,
80
- chosenAddOns: await finalizeAddOns(framework!, json.mode!, [
81
- ...json.chosenAddOns,
82
- ]),
83
+ chosenAddOns,
84
+ addOnOptions: populateAddOnOptionsDefaults(chosenAddOns),
83
85
  }
84
86
  }
85
87
 
@@ -100,6 +102,7 @@ export function createSerializedOptionsFromPersisted(
100
102
  targetDir: '',
101
103
  framework: json.framework,
102
104
  starter: json.starter,
105
+ addOnOptions: {},
103
106
  }
104
107
  }
105
108
 
@@ -46,12 +46,22 @@ export function createDefaultEnvironment(): Environment {
46
46
  await mkdir(dirname(path), { recursive: true })
47
47
  return writeFile(path, getBinaryFile(base64Contents) as string)
48
48
  },
49
- execute: async (command: string, args: Array<string>, cwd: string) => {
49
+ execute: async (command: string, args: Array<string>, cwd: string, options?: { inherit?: boolean }) => {
50
50
  try {
51
- const result = await execa(command, args, {
52
- cwd,
53
- })
54
- return { stdout: result.stdout }
51
+ if (options?.inherit) {
52
+ // For commands that should show output directly to the user
53
+ await execa(command, args, {
54
+ cwd,
55
+ stdio: 'inherit',
56
+ })
57
+ return { stdout: '' }
58
+ } else {
59
+ // For commands where we need to capture output
60
+ const result = await execa(command, args, {
61
+ cwd,
62
+ })
63
+ return { stdout: result.stdout }
64
+ }
55
65
  } catch {
56
66
  errors.push(
57
67
  `Command "${command} ${args.join(' ')}" did not run successfully. Please run this manually in your project.`,
package/src/frameworks.ts CHANGED
@@ -56,11 +56,15 @@ export function scanAddOnDirectories(addOnsDirectories: Array<string>) {
56
56
  const fileContent = readFileSync(filePath, 'utf-8')
57
57
  const info = JSON.parse(fileContent)
58
58
 
59
- let packageAdditions: Record<string, string> = {}
59
+ let packageAdditions: Record<string, any> = {}
60
+ let packageTemplate: string | undefined = undefined
61
+
60
62
  if (existsSync(resolve(addOnsBase, dir, 'package.json'))) {
61
63
  packageAdditions = JSON.parse(
62
64
  readFileSync(resolve(addOnsBase, dir, 'package.json'), 'utf-8'),
63
65
  )
66
+ } else if (existsSync(resolve(addOnsBase, dir, 'package.json.ejs'))) {
67
+ packageTemplate = readFileSync(resolve(addOnsBase, dir, 'package.json.ejs'), 'utf-8')
64
68
  }
65
69
 
66
70
  let readme: string | undefined
@@ -97,6 +101,7 @@ export function scanAddOnDirectories(addOnsDirectories: Array<string>) {
97
101
  ...info,
98
102
  id: dir,
99
103
  packageAdditions,
104
+ packageTemplate,
100
105
  readme,
101
106
  files,
102
107
  smallLogo,
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { createApp } from './create-app.js'
2
2
  export { addToApp } from './add-to-app.js'
3
3
 
4
- export { finalizeAddOns, getAllAddOns } from './add-ons.js'
4
+ export { finalizeAddOns, getAllAddOns, populateAddOnOptionsDefaults } from './add-ons.js'
5
5
 
6
6
  export { loadRemoteAddOn } from './custom-add-ons/add-on.js'
7
7
  export { loadStarter } from './custom-add-ons/starter.js'
@@ -73,6 +73,10 @@ export {
73
73
 
74
74
  export type {
75
75
  AddOn,
76
+ AddOnOption,
77
+ AddOnOptions,
78
+ AddOnSelectOption,
79
+ AddOnSelection,
76
80
  Environment,
77
81
  FileBundleHandler,
78
82
  Framework,
@@ -1,3 +1,4 @@
1
+ import { render } from 'ejs'
1
2
  import { sortObject } from './utils.js'
2
3
 
3
4
  import type { Options } from './types.js'
@@ -8,6 +9,7 @@ export function mergePackageJSON(
8
9
  ) {
9
10
  return {
10
11
  ...packageJSON,
12
+ ...(overlayPackageJSON || {}),
11
13
  dependencies: {
12
14
  ...packageJSON.dependencies,
13
15
  ...(overlayPackageJSON?.dependencies || {}),
@@ -42,10 +44,44 @@ export function createPackageJSON(options: Options) {
42
44
  packageJSON = mergePackageJSON(packageJSON, addition)
43
45
  }
44
46
 
45
- for (const addOn of options.chosenAddOns.map(
46
- (addOn) => addOn.packageAdditions,
47
- )) {
48
- packageJSON = mergePackageJSON(packageJSON, addOn)
47
+ for (const addOn of options.chosenAddOns) {
48
+ let addOnPackageJSON = addOn.packageAdditions
49
+
50
+ // Process EJS template if present
51
+ if (addOn.packageTemplate) {
52
+ const templateValues = {
53
+ packageManager: options.packageManager,
54
+ projectName: options.projectName,
55
+ typescript: options.typescript,
56
+ tailwind: options.tailwind,
57
+ js: options.typescript ? 'ts' : 'js',
58
+ jsx: options.typescript ? 'tsx' : 'jsx',
59
+ fileRouter: options.mode === 'file-router',
60
+ codeRouter: options.mode === 'code-router',
61
+ addOnEnabled: options.chosenAddOns.reduce<Record<string, boolean>>(
62
+ (acc, addon) => {
63
+ acc[addon.id] = true
64
+ return acc
65
+ },
66
+ {},
67
+ ),
68
+ addOnOption: options.addOnOptions,
69
+ addOns: options.chosenAddOns,
70
+ }
71
+
72
+ try {
73
+ const renderedTemplate = render(addOn.packageTemplate, templateValues)
74
+ addOnPackageJSON = JSON.parse(renderedTemplate)
75
+ } catch (error) {
76
+ console.error(
77
+ `Error processing package.json.ejs for add-on ${addOn.id}:`,
78
+ error,
79
+ )
80
+ // Fall back to packageAdditions if template processing fails
81
+ }
82
+ }
83
+
84
+ packageJSON = mergePackageJSON(packageJSON, addOnPackageJSON)
49
85
  }
50
86
 
51
87
  if (options.starter) {
@@ -1,4 +1,5 @@
1
1
  import { rimrafNodeModules } from './rimraf-node-modules.js'
2
+ import { postInitScript } from './post-init-script.js'
2
3
 
3
4
  import type { Environment, Options } from '../types.js'
4
5
 
@@ -7,6 +8,7 @@ const specialStepsLookup: Record<
7
8
  (environment: Environment, options: Options) => Promise<void>
8
9
  > = {
9
10
  'rimraf-node-modules': rimrafNodeModules,
11
+ 'post-init-script': postInitScript,
10
12
  }
11
13
 
12
14
  export async function runSpecialSteps(
@@ -18,7 +20,7 @@ export async function runSpecialSteps(
18
20
  environment.startStep({
19
21
  id: 'special-steps',
20
22
  type: 'command',
21
- message: 'Removing node_modules...',
23
+ message: 'Running special steps...',
22
24
  })
23
25
 
24
26
  for (const step of specialSteps) {
@@ -0,0 +1,52 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ import { getPackageManagerScriptCommand } from '../package-manager.js'
5
+
6
+ import type { Environment, Options } from '../types.js'
7
+
8
+ export async function postInitScript(
9
+ environment: Environment,
10
+ options: Options,
11
+ ) {
12
+ const packageJsonPath = resolve(options.targetDir, 'package.json')
13
+
14
+ if (!environment.exists(packageJsonPath)) {
15
+ environment.warn(
16
+ 'Warning',
17
+ 'No package.json found, skipping post-cta-init script',
18
+ )
19
+ return
20
+ }
21
+
22
+ try {
23
+ const packageJsonContent = readFileSync(packageJsonPath, 'utf-8')
24
+ const packageJson = JSON.parse(packageJsonContent)
25
+
26
+ if (!packageJson.scripts || !packageJson.scripts['post-cta-init']) {
27
+ // No post-cta-init script found, skip silently
28
+ return
29
+ }
30
+
31
+ environment.startStep({
32
+ id: 'post-init-script',
33
+ type: 'command',
34
+ message: 'Running post-cta-init script...',
35
+ })
36
+
37
+ const { command, args } = getPackageManagerScriptCommand(
38
+ options.packageManager,
39
+ ['post-cta-init'],
40
+ )
41
+
42
+ await environment.execute(command, args, options.targetDir, {
43
+ inherit: true,
44
+ })
45
+
46
+ environment.finishStep('post-init-script', 'Post-cta-init script complete')
47
+ } catch (error) {
48
+ environment.error(
49
+ `Failed to run post-cta-init script: ${error instanceof Error ? error.message : String(error)}`,
50
+ )
51
+ }
52
+ }
@@ -95,6 +95,7 @@ export function createTemplateFile(environment: Environment, options: Options) {
95
95
  fileRouter: options.mode === 'file-router',
96
96
  codeRouter: options.mode === 'code-router',
97
97
  addOnEnabled,
98
+ addOnOption: options.addOnOptions,
98
99
  addOns: options.chosenAddOns,
99
100
  integrations,
100
101
  routes,
@@ -135,6 +136,13 @@ export function createTemplateFile(environment: Environment, options: Options) {
135
136
 
136
137
  let target = convertDotFilesAndPaths(file.replace('.ejs', ''))
137
138
 
139
+ // Strip option prefixes from filename (e.g., __postgres__schema.prisma -> schema.prisma)
140
+ const prefixMatch = target.match(/^(.+\/)?__([^_]+)__(.+)$/)
141
+ if (prefixMatch) {
142
+ const [, directory, , filename] = prefixMatch
143
+ target = (directory || '') + filename
144
+ }
145
+
138
146
  let append = false
139
147
  if (target.endsWith('.append')) {
140
148
  append = true
package/src/types.ts CHANGED
@@ -9,6 +9,25 @@ export type StatusStepType =
9
9
  | 'package-manager'
10
10
  | 'other'
11
11
 
12
+ export const AddOnSelectOptionSchema = z.object({
13
+ type: z.literal('select'),
14
+ label: z.string(),
15
+ description: z.string().optional(),
16
+ default: z.string(),
17
+ options: z.array(
18
+ z.object({
19
+ value: z.string(),
20
+ label: z.string(),
21
+ }),
22
+ ),
23
+ })
24
+
25
+ export const AddOnOptionSchema = z.discriminatedUnion('type', [
26
+ AddOnSelectOptionSchema,
27
+ ])
28
+
29
+ export const AddOnOptionsSchema = z.record(z.string(), AddOnOptionSchema)
30
+
12
31
  export const AddOnBaseSchema = z.object({
13
32
  id: z.string(),
14
33
  name: z.string(),
@@ -48,6 +67,8 @@ export const AddOnBaseSchema = z.object({
48
67
  logo: z.string().optional(),
49
68
  addOnSpecialSteps: z.array(z.string()).optional(),
50
69
  createSpecialSteps: z.array(z.string()).optional(),
70
+ postInitSpecialSteps: z.array(z.string()).optional(),
71
+ options: AddOnOptionsSchema.optional(),
51
72
  default: z.boolean().optional(),
52
73
  })
53
74
 
@@ -82,8 +103,15 @@ export const AddOnInfoSchema = AddOnBaseSchema.extend({
82
103
  export const AddOnCompiledSchema = AddOnInfoSchema.extend({
83
104
  files: z.record(z.string(), z.string()),
84
105
  deletedFiles: z.array(z.string()),
106
+ packageTemplate: z.string().optional(),
85
107
  })
86
108
 
109
+ export type AddOnSelectOption = z.infer<typeof AddOnSelectOptionSchema>
110
+
111
+ export type AddOnOption = z.infer<typeof AddOnOptionSchema>
112
+
113
+ export type AddOnOptions = z.infer<typeof AddOnOptionsSchema>
114
+
87
115
  export type Integration = z.infer<typeof IntegrationSchema>
88
116
 
89
117
  export type AddOnBase = z.infer<typeof AddOnBaseSchema>
@@ -96,13 +124,19 @@ export type AddOnInfo = z.infer<typeof AddOnInfoSchema>
96
124
 
97
125
  export type AddOnCompiled = z.infer<typeof AddOnCompiledSchema>
98
126
 
127
+ export interface AddOnSelection {
128
+ id: string
129
+ enabled: boolean
130
+ options: Record<string, any>
131
+ }
132
+
99
133
  export type FileBundleHandler = {
100
134
  getFiles: () => Promise<Array<string>>
101
135
  getFileContents: (path: string) => Promise<string>
102
136
  getDeletedFiles: () => Promise<Array<string>>
103
137
  }
104
138
 
105
- export type AddOn = AddOnInfo & FileBundleHandler
139
+ export type AddOn = AddOnCompiled & FileBundleHandler
106
140
 
107
141
  export type Starter = StarterCompiled & FileBundleHandler
108
142
 
@@ -146,6 +180,7 @@ export interface Options {
146
180
  git: boolean
147
181
 
148
182
  chosenAddOns: Array<AddOn>
183
+ addOnOptions: Record<string, Record<string, any>>
149
184
  starter?: Starter | undefined
150
185
  }
151
186
 
@@ -173,6 +208,7 @@ type FileEnvironment = {
173
208
  command: string,
174
209
  args: Array<string>,
175
210
  cwd: string,
211
+ options?: { inherit?: boolean },
176
212
  ) => Promise<{ stdout: string }>
177
213
  deleteFile: (path: string) => Promise<void>
178
214