@tanstack/cta-engine 0.10.0-alpha.19 → 0.10.0-alpha.21

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 (69) hide show
  1. package/dist/add-ons.js +5 -14
  2. package/dist/add-to-app.js +118 -74
  3. package/dist/config-file.js +9 -7
  4. package/dist/create-app.js +112 -34
  5. package/dist/custom-add-ons/add-on.js +175 -0
  6. package/dist/custom-add-ons/shared.js +117 -0
  7. package/dist/custom-add-ons/starter.js +84 -0
  8. package/dist/environment.js +59 -12
  9. package/dist/file-helpers.js +108 -2
  10. package/dist/frameworks.js +15 -1
  11. package/dist/index.js +12 -5
  12. package/dist/integrations/shadcn.js +10 -4
  13. package/dist/options.js +9 -0
  14. package/dist/package-json.js +7 -4
  15. package/dist/special-steps/index.js +24 -0
  16. package/dist/special-steps/rimraf-node-modules.js +16 -0
  17. package/dist/template-file.js +3 -13
  18. package/dist/types/add-ons.d.ts +3 -4
  19. package/dist/types/add-to-app.d.ts +16 -3
  20. package/dist/types/config-file.d.ts +4 -3
  21. package/dist/types/create-app.d.ts +1 -7
  22. package/dist/types/custom-add-ons/add-on.d.ts +69 -0
  23. package/dist/types/custom-add-ons/shared.d.ts +15 -0
  24. package/dist/types/custom-add-ons/starter.d.ts +7 -0
  25. package/dist/types/environment.d.ts +2 -1
  26. package/dist/types/file-helpers.d.ts +10 -0
  27. package/dist/types/frameworks.d.ts +2 -0
  28. package/dist/types/index.d.ts +13 -6
  29. package/dist/types/integrations/shadcn.d.ts +1 -1
  30. package/dist/types/options.d.ts +2 -0
  31. package/dist/types/package-json.d.ts +5 -0
  32. package/dist/types/package-manager.d.ts +6 -2
  33. package/dist/types/special-steps/index.d.ts +2 -0
  34. package/dist/types/special-steps/rimraf-node-modules.d.ts +2 -0
  35. package/dist/types/template-file.d.ts +1 -1
  36. package/dist/types/types.d.ts +752 -70
  37. package/dist/types.js +65 -1
  38. package/package.json +9 -3
  39. package/src/add-ons.ts +7 -19
  40. package/src/add-to-app.ts +196 -102
  41. package/src/config-file.ts +16 -13
  42. package/src/create-app.ts +129 -75
  43. package/src/custom-add-ons/add-on.ts +261 -0
  44. package/src/custom-add-ons/shared.ts +161 -0
  45. package/src/custom-add-ons/starter.ts +126 -0
  46. package/src/environment.ts +70 -11
  47. package/src/file-helpers.ts +164 -2
  48. package/src/frameworks.ts +21 -1
  49. package/src/index.ts +46 -11
  50. package/src/integrations/shadcn.ts +14 -4
  51. package/src/options.ts +11 -0
  52. package/src/package-json.ts +13 -6
  53. package/src/special-steps/index.ts +36 -0
  54. package/src/special-steps/rimraf-node-modules.ts +25 -0
  55. package/src/template-file.ts +3 -18
  56. package/src/types.ts +143 -85
  57. package/tests/add-ons.test.ts +5 -5
  58. package/tests/add-to-app.test.ts +358 -0
  59. package/tests/config-file.test.ts +15 -11
  60. package/tests/create-app.test.ts +43 -67
  61. package/tests/custom-add-ons/add-on.test.ts +12 -0
  62. package/tests/custom-add-ons/shared.test.ts +257 -0
  63. package/tests/custom-add-ons/starter.test.ts +58 -0
  64. package/tests/environment.test.ts +19 -0
  65. package/tests/integrations/shadcn.test.ts +48 -63
  66. package/tests/options.test.ts +42 -0
  67. package/tests/setupVitest.ts +6 -0
  68. package/tests/template-file.test.ts +54 -91
  69. package/vitest.config.ts +2 -0
package/dist/types.js CHANGED
@@ -1 +1,65 @@
1
- export {};
1
+ import z from 'zod';
2
+ export const AddOnBaseSchema = z.object({
3
+ id: z.string(),
4
+ name: z.string(),
5
+ description: z.string(),
6
+ author: z.string().optional(),
7
+ version: z.string().optional(),
8
+ link: z.string().optional(),
9
+ license: z.string().optional(),
10
+ warning: z.string().optional(),
11
+ type: z.enum(['add-on', 'example', 'starter', 'toolchain']),
12
+ command: z
13
+ .object({
14
+ command: z.string(),
15
+ args: z.array(z.string()).optional(),
16
+ })
17
+ .optional(),
18
+ routes: z
19
+ .array(z.object({
20
+ url: z.string().optional(),
21
+ name: z.string().optional(),
22
+ path: z.string(),
23
+ jsName: z.string(),
24
+ }))
25
+ .optional(),
26
+ packageAdditions: z
27
+ .object({
28
+ dependencies: z.record(z.string(), z.string()).optional(),
29
+ devDependencies: z.record(z.string(), z.string()).optional(),
30
+ scripts: z.record(z.string(), z.string()).optional(),
31
+ })
32
+ .optional(),
33
+ shadcnComponents: z.array(z.string()).optional(),
34
+ dependsOn: z.array(z.string()).optional(),
35
+ smallLogo: z.string().optional(),
36
+ logo: z.string().optional(),
37
+ addOnSpecialSteps: z.array(z.string()).optional(),
38
+ createSpecialSteps: z.array(z.string()).optional(),
39
+ });
40
+ export const StarterSchema = AddOnBaseSchema.extend({
41
+ framework: z.string(),
42
+ mode: z.string(),
43
+ typescript: z.boolean(),
44
+ tailwind: z.boolean(),
45
+ banner: z.string().optional(),
46
+ });
47
+ export const StarterCompiledSchema = StarterSchema.extend({
48
+ files: z.record(z.string(), z.string()),
49
+ deletedFiles: z.array(z.string()),
50
+ });
51
+ export const IntegrationSchema = z.object({
52
+ type: z.string(),
53
+ path: z.string(),
54
+ jsName: z.string(),
55
+ });
56
+ export const AddOnInfoSchema = AddOnBaseSchema.extend({
57
+ modes: z.array(z.string()),
58
+ integrations: z.array(IntegrationSchema).optional(),
59
+ phase: z.enum(['setup', 'add-on']),
60
+ readme: z.string().optional(),
61
+ });
62
+ export const AddOnCompiledSchema = AddOnInfoSchema.extend({
63
+ files: z.record(z.string(), z.string()),
64
+ deletedFiles: z.array(z.string()),
65
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cta-engine",
3
- "version": "0.10.0-alpha.19",
3
+ "version": "0.10.0-alpha.21",
4
4
  "description": "Tanstack Application Builder Engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -24,17 +24,23 @@
24
24
  "dependencies": {
25
25
  "ejs": "^3.1.10",
26
26
  "execa": "^9.5.2",
27
+ "ignore": "^7.0.3",
27
28
  "memfs": "^4.17.0",
28
- "prettier": "^3.5.0"
29
+ "parse-gitignore": "^2.0.0",
30
+ "prettier": "^3.5.0",
31
+ "rimraf": "^6.0.1",
32
+ "zod": "^3.24.2"
29
33
  },
30
34
  "devDependencies": {
31
35
  "@tanstack/config": "^0.16.2",
32
36
  "@types/ejs": "^3.1.5",
33
37
  "@types/node": "^22.13.4",
38
+ "@types/parse-gitignore": "^1.0.2",
34
39
  "@vitest/coverage-v8": "3.1.1",
35
40
  "eslint": "^9.20.0",
36
41
  "typescript": "^5.6.3",
37
- "vitest": "^3.0.8"
42
+ "vitest": "^3.0.8",
43
+ "vitest-fetch-mock": "^0.4.5"
38
44
  },
39
45
  "scripts": {}
40
46
  }
package/src/add-ons.ts CHANGED
@@ -1,21 +1,20 @@
1
- import type { AddOn, Framework } from './types.js'
1
+ import { loadRemoteAddOn } from './custom-add-ons/add-on.js'
2
2
 
3
- export function getAllAddOns(
4
- framework: Framework,
5
- template: string,
6
- ): Array<AddOn> {
7
- return framework.getAddOns().filter((a) => a.templates.includes(template))
3
+ import type { AddOn, Framework, Mode } from './types.js'
4
+
5
+ export function getAllAddOns(framework: Framework, mode: Mode): Array<AddOn> {
6
+ return framework.getAddOns().filter((a) => a.modes.includes(mode))
8
7
  }
9
8
 
10
9
  // Turn the list of chosen add-on IDs into a final list of add-ons by resolving dependencies
11
10
  export async function finalizeAddOns(
12
11
  framework: Framework,
13
- template: string,
12
+ mode: Mode,
14
13
  chosenAddOnIDs: Array<string>,
15
14
  ): Promise<Array<AddOn>> {
16
15
  const finalAddOnIDs = new Set(chosenAddOnIDs)
17
16
 
18
- const addOns = getAllAddOns(framework, template)
17
+ const addOns = getAllAddOns(framework, mode)
19
18
 
20
19
  for (const addOnID of finalAddOnIDs) {
21
20
  let addOn: AddOn | undefined
@@ -48,14 +47,3 @@ export async function finalizeAddOns(
48
47
  function loadAddOn(addOn: AddOn): AddOn {
49
48
  return addOn
50
49
  }
51
-
52
- export async function loadRemoteAddOn(url: string): Promise<AddOn> {
53
- const response = await fetch(url)
54
- const fileContent = await response.json()
55
- fileContent.id = url
56
- return {
57
- ...fileContent,
58
- getFiles: () => Promise.resolve(Object.keys(fileContent.files)),
59
- getFileContents: (path: string) => Promise.resolve(fileContent.files[path]),
60
- }
61
- }
package/src/add-to-app.ts CHANGED
@@ -1,180 +1,274 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises'
2
- import { existsSync, statSync } from 'node:fs'
3
- import { basename, dirname, resolve } from 'node:path'
4
- import { execa, execaSync } from 'execa'
1
+ import { basename, resolve } from 'node:path'
5
2
 
6
3
  import { CONFIG_FILE } from './constants.js'
7
4
  import { finalizeAddOns } from './add-ons.js'
8
5
  import { getFrameworkById } from './frameworks.js'
9
- import {
10
- createDefaultEnvironment,
11
- createMemoryEnvironment,
12
- } from './environment.js'
6
+ import { createMemoryEnvironment } from './environment.js'
13
7
  import { createApp } from './create-app.js'
14
- import { readConfigFile, writeConfigFile } from './config-file.js'
15
- import { sortObject } from './utils.js'
8
+ import {
9
+ readConfigFileFromEnvironment,
10
+ writeConfigFileToEnvironment,
11
+ } from './config-file.js'
12
+ import { formatCommand } from './utils.js'
13
+ import { packageManagerInstall } from './package-manager.js'
14
+ import {
15
+ isBase64,
16
+ recursivelyGatherFilesFromEnvironment,
17
+ } from './file-helpers.js'
18
+ import { mergePackageJSON } from './package-json.js'
19
+ import { runSpecialSteps } from './special-steps/index.js'
16
20
 
17
- import type { Environment, Options } from './types.js'
21
+ import type { Environment, Mode, Options } from './types.js'
18
22
  import type { PersistedOptions } from './config-file.js'
19
23
 
20
- function isDirectory(path: string) {
21
- return statSync(path).isDirectory()
22
- }
23
-
24
- async function hasPendingGitChanges() {
25
- const status = await execaSync('git', ['status', '--porcelain'])
26
- return status.stdout.length > 0
24
+ export async function hasPendingGitChanges(
25
+ environment: Environment,
26
+ cwd: string,
27
+ ) {
28
+ const { stdout } = await environment.execute(
29
+ 'git',
30
+ ['status', '--porcelain'],
31
+ cwd,
32
+ )
33
+ return stdout.length > 0
27
34
  }
28
35
 
29
36
  async function createOptions(
30
37
  json: PersistedOptions,
31
38
  addOns: Array<string>,
32
- ): Promise<Required<Options>> {
39
+ targetDir: string,
40
+ ): Promise<Options> {
33
41
  const framework = getFrameworkById(json.framework)
34
42
 
43
+ // TODO: Load the starter
44
+
35
45
  return {
36
46
  ...json,
37
47
  framework,
38
48
  tailwind: true,
39
49
  addOns: true,
40
- chosenAddOns: await finalizeAddOns(framework!, json.mode as string, [
50
+ chosenAddOns: await finalizeAddOns(framework!, json.mode as Mode, [
41
51
  ...json.existingAddOns,
42
52
  ...addOns,
43
53
  ]),
44
- } as Required<Options>
54
+ targetDir,
55
+ } as Options
45
56
  }
46
57
 
47
58
  async function runCreateApp(options: Required<Options>) {
48
- const { environment, output } = createMemoryEnvironment()
49
- await createApp(options, {
50
- silent: true,
51
- environment,
52
- cwd: process.cwd(),
53
- name: 'create-tsrouter-app',
54
- })
59
+ const { environment, output } = createMemoryEnvironment(options.targetDir)
60
+ await createApp(environment, options)
55
61
  return output
56
62
  }
57
63
 
58
- export async function addToApp(
59
- addOns: Array<string>,
60
- {
61
- silent = false,
62
- }: {
63
- silent?: boolean
64
- } = {},
64
+ export async function getCurrentConfiguration(
65
65
  environment: Environment,
66
+ cwd: string,
66
67
  ) {
67
- const persistedOptions = await readConfigFile(process.cwd())
68
+ const persistedOptions = await readConfigFileFromEnvironment(environment, cwd)
68
69
  if (!persistedOptions) {
69
70
  environment.error(
70
71
  'There is no .cta.json file in your project.',
71
72
  'This is probably because this was created with an older version of create-tsrouter-app.',
72
73
  )
73
- return
74
- }
75
-
76
- if (!silent) {
77
- environment.intro(`Adding ${addOns.join(', ')} to the project...`)
78
- }
79
-
80
- if (await hasPendingGitChanges()) {
81
- environment.error(
82
- 'You have pending git changes.',
83
- 'Please commit or stash them before adding add-ons.',
84
- )
85
- return
74
+ return undefined
86
75
  }
87
76
 
88
- const newOptions = await createOptions(persistedOptions, addOns)
77
+ return persistedOptions
78
+ }
89
79
 
90
- const output = await runCreateApp(newOptions)
80
+ export async function writeFiles(
81
+ environment: Environment,
82
+ cwd: string,
83
+ output: {
84
+ files: Record<string, string>
85
+ deletedFiles: Array<string>
86
+ },
87
+ forced: boolean,
88
+ ) {
89
+ const currentFiles = await recursivelyGatherFilesFromEnvironment(
90
+ environment,
91
+ cwd,
92
+ false,
93
+ )
91
94
 
92
95
  const overwrittenFiles: Array<string> = []
93
96
  const changedFiles: Array<string> = []
94
- const contentMap = new Map<string, string>()
95
97
  for (const file of Object.keys(output.files)) {
96
- const relativeFile = file.replace(process.cwd(), '')
97
- if (existsSync(file)) {
98
- if (!isDirectory(file)) {
99
- const contents = (await readFile(file)).toString()
100
- if (
101
- ['package.json', CONFIG_FILE].includes(basename(file)) ||
102
- contents !== output.files[file]
103
- ) {
104
- overwrittenFiles.push(relativeFile)
105
- contentMap.set(relativeFile, output.files[file])
106
- }
98
+ const relativeFile = file.replace(cwd, '')
99
+ if (currentFiles[relativeFile]) {
100
+ if (currentFiles[relativeFile] !== output.files[file]) {
101
+ overwrittenFiles.push(relativeFile)
107
102
  }
108
103
  } else {
109
104
  changedFiles.push(relativeFile)
110
- contentMap.set(relativeFile, output.files[file])
111
105
  }
112
106
  }
113
107
 
114
- if (overwrittenFiles.length > 0 && !silent) {
108
+ if (!forced && overwrittenFiles.length) {
115
109
  environment.warn(
116
- 'The following will be overwritten:',
117
- overwrittenFiles.join('\n'),
110
+ 'The following will be overwritten',
111
+ [...overwrittenFiles, ...output.deletedFiles].join('\n'),
118
112
  )
119
113
  const shouldContinue = await environment.confirm('Do you want to continue?')
120
114
  if (!shouldContinue) {
121
- process.exit(0)
115
+ throw new Error('User cancelled')
122
116
  }
123
117
  }
124
118
 
119
+ for (const file of output.deletedFiles) {
120
+ if (environment.exists(resolve(cwd, file))) {
121
+ await environment.deleteFile(resolve(cwd, file))
122
+ }
123
+ }
124
+
125
+ environment.startStep({
126
+ id: 'write-files',
127
+ type: 'file',
128
+ message: 'Writing add-on files...',
129
+ })
130
+
125
131
  for (const file of [...changedFiles, ...overwrittenFiles]) {
126
- const targetFile = `.${file}`
127
132
  const fName = basename(file)
128
- const contents = contentMap.get(file)!
133
+ const contents = output.files[file]
129
134
  if (fName === 'package.json') {
130
135
  const currentJson = JSON.parse(
131
- (await readFile(resolve(fName), 'utf-8')).toString(),
136
+ await environment.readFile(resolve(cwd, file)),
137
+ )
138
+ const newJSON = mergePackageJSON(currentJson, JSON.parse(contents))
139
+ environment.writeFile(
140
+ resolve(cwd, file),
141
+ JSON.stringify(newJSON, null, 2),
132
142
  )
133
- const newJson = JSON.parse(contents)
134
-
135
- currentJson.scripts = newJson.scripts
136
- currentJson.dependencies = sortObject({
137
- ...currentJson.dependencies,
138
- ...newJson.dependencies,
139
- })
140
- currentJson.devDependencies = sortObject({
141
- ...currentJson.devDependencies,
142
- ...newJson.devDependencies,
143
- })
144
-
145
- await writeFile(targetFile, JSON.stringify(currentJson, null, 2))
146
143
  } else if (fName !== CONFIG_FILE) {
147
- await mkdir(resolve(dirname(targetFile)), { recursive: true })
148
- await writeFile(resolve(targetFile), contents)
144
+ if (isBase64(contents)) {
145
+ await environment.writeFileBase64(resolve(cwd, file), contents)
146
+ } else {
147
+ await environment.writeFile(resolve(cwd, file), contents)
148
+ }
149
149
  }
150
150
  }
151
151
 
152
- // Handle commands
153
- const originalOutput = await runCreateApp(
154
- await createOptions(persistedOptions, []),
155
- )
152
+ environment.finishStep('write-files', 'Add-on files written')
153
+ }
154
+
155
+ export async function runNewCommands(
156
+ environment: Environment,
157
+ originalOptions: PersistedOptions,
158
+ cwd: string,
159
+ output: {
160
+ commands: Array<{
161
+ command: string
162
+ args: Array<string>
163
+ }>
164
+ },
165
+ ) {
166
+ const originalOutput = await runCreateApp({
167
+ ...(await createOptions(originalOptions, [], cwd)),
168
+ } as Required<Options>)
169
+
156
170
  const originalCommands = new Set(
157
171
  originalOutput.commands.map((c) => [c.command, ...c.args].join(' ')),
158
172
  )
173
+
159
174
  for (const command of output.commands) {
160
175
  const commandString = [command.command, ...command.args].join(' ')
161
176
  if (!originalCommands.has(commandString)) {
162
- await execa(command.command, command.args)
177
+ environment.startStep({
178
+ id: 'run-commands',
179
+ type: 'command',
180
+ message: `Running ${formatCommand({ command: command.command, args: command.args })}...`,
181
+ })
182
+ await environment.execute(command.command, command.args, cwd)
183
+ environment.finishStep('run-commands', 'Setup commands complete')
163
184
  }
164
185
  }
165
- const realEnvironment = createDefaultEnvironment()
166
- writeConfigFile(realEnvironment, process.cwd(), newOptions)
186
+ }
167
187
 
168
- const s = silent ? null : environment.spinner()
169
- s?.start(`Installing dependencies via ${newOptions.packageManager}...`)
170
- await realEnvironment.execute(
171
- newOptions.packageManager,
172
- ['install'],
173
- resolve(process.cwd()),
188
+ export async function addToApp(
189
+ environment: Environment,
190
+ addOns: Array<string>,
191
+ cwd: string,
192
+ options?: {
193
+ forced?: boolean
194
+ },
195
+ ) {
196
+ const persistedOptions = await getCurrentConfiguration(environment, cwd)
197
+ if (!persistedOptions) {
198
+ return
199
+ }
200
+
201
+ if (!options?.forced && (await hasPendingGitChanges(environment, cwd))) {
202
+ environment.error(
203
+ 'You have pending git changes.',
204
+ 'Please commit or stash them before adding add-ons.',
205
+ )
206
+ return
207
+ }
208
+
209
+ environment.intro(`Adding ${addOns.join(', ')} to the project...`)
210
+ environment.startStep({
211
+ id: 'processing-new-app-setup',
212
+ type: 'file',
213
+ message: 'Processing new app setup...',
214
+ })
215
+
216
+ const newOptions = await createOptions(persistedOptions, addOns, cwd)
217
+
218
+ const output = await runCreateApp({
219
+ ...newOptions,
220
+ targetDir: cwd,
221
+ } as Required<Options>)
222
+
223
+ await writeFiles(environment, cwd, output, !!options?.forced)
224
+
225
+ environment.finishStep(
226
+ 'processing-new-app-setup',
227
+ 'Application files written',
174
228
  )
175
- s?.stop(`Installed dependencies`)
176
229
 
177
- if (!silent) {
178
- environment.outro('Add-ons added successfully!')
230
+ // Run any special steps for the new add-ons
231
+
232
+ const specialSteps = new Set<string>([])
233
+ for (const addOn of newOptions.chosenAddOns) {
234
+ for (const step of addOn.addOnSpecialSteps || []) {
235
+ if (addOns.includes(addOn.id)) {
236
+ specialSteps.add(step)
237
+ }
238
+ }
179
239
  }
240
+ if (specialSteps.size) {
241
+ await runSpecialSteps(environment, newOptions, Array.from(specialSteps))
242
+ }
243
+
244
+ // Install dependencies
245
+
246
+ environment.startStep({
247
+ id: 'install-dependencies',
248
+ type: 'package-manager',
249
+ message: `Installing dependencies via ${newOptions.packageManager}...`,
250
+ })
251
+ const s = environment.spinner()
252
+ s.start(`Installing dependencies via ${newOptions.packageManager}...`)
253
+ await packageManagerInstall(
254
+ environment,
255
+ newOptions.targetDir,
256
+ newOptions.packageManager,
257
+ )
258
+ s.stop(`Installed dependencies`)
259
+ environment.finishStep('install-dependencies', 'Dependencies installed')
260
+
261
+ // Handle new commands
262
+
263
+ await runNewCommands(environment, persistedOptions, cwd, output)
264
+
265
+ environment.startStep({
266
+ id: 'write-config-file',
267
+ type: 'file',
268
+ message: 'Writing config file...',
269
+ })
270
+ writeConfigFileToEnvironment(environment, newOptions)
271
+ environment.finishStep('write-config-file', 'Config file written')
272
+
273
+ environment.outro('Add-ons added successfully!')
180
274
  }
@@ -1,4 +1,3 @@
1
- import { readFile } from 'node:fs/promises'
2
1
  import { resolve } from 'node:path'
3
2
 
4
3
  import { CONFIG_FILE } from './constants.js'
@@ -7,40 +6,44 @@ import type { Environment, Options } from './types.js'
7
6
 
8
7
  export type PersistedOptions = Omit<
9
8
  Partial<Options>,
10
- 'addOns' | 'chosenAddOns' | 'framework'
9
+ 'addOns' | 'chosenAddOns' | 'framework' | 'starter' | 'targetDir'
11
10
  > & {
12
11
  framework: string
13
12
  version: number
14
13
  existingAddOns: Array<string>
14
+ starter?: string
15
15
  }
16
16
 
17
- export async function writeConfigFile(
18
- environment: Environment,
19
- targetDir: string,
20
- options: Options,
21
- ) {
17
+ function createPersistedOptions(options: Options): PersistedOptions {
22
18
  /* eslint-disable unused-imports/no-unused-vars */
23
- const { addOns, chosenAddOns, framework, ...rest } = options
19
+ const { chosenAddOns, framework, targetDir, ...rest } = options
24
20
  /* eslint-enable unused-imports/no-unused-vars */
25
- const persistedOptions: PersistedOptions = {
21
+ return {
26
22
  ...rest,
27
23
  version: 1,
28
24
  framework: options.framework.id,
29
25
  existingAddOns: options.chosenAddOns.map((addOn) => addOn.id),
26
+ starter: options.starter?.id ?? undefined,
30
27
  }
28
+ }
31
29
 
30
+ export async function writeConfigFileToEnvironment(
31
+ environment: Environment,
32
+ options: Options,
33
+ ) {
32
34
  await environment.writeFile(
33
- resolve(targetDir, CONFIG_FILE),
34
- JSON.stringify(persistedOptions, null, 2),
35
+ resolve(options.targetDir, CONFIG_FILE),
36
+ JSON.stringify(createPersistedOptions(options), null, 2),
35
37
  )
36
38
  }
37
39
 
38
- export async function readConfigFile(
40
+ export async function readConfigFileFromEnvironment(
41
+ environment: Environment,
39
42
  targetDir: string,
40
43
  ): Promise<PersistedOptions | null> {
41
44
  try {
42
45
  const configFile = resolve(targetDir, CONFIG_FILE)
43
- const config = await readFile(configFile, 'utf8')
46
+ const config = await environment.readFile(configFile)
44
47
 
45
48
  // TODO: Look for old config files and convert them to the new format
46
49