@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
@@ -0,0 +1,358 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { fs, vol } from 'memfs'
3
+
4
+ import { createMemoryEnvironment } from '../src/environment.js'
5
+ import {
6
+ addToApp,
7
+ getCurrentConfiguration,
8
+ hasPendingGitChanges,
9
+ runNewCommands,
10
+ writeFiles,
11
+ } from '../src/add-to-app.js'
12
+ import {
13
+ __testClearFrameworks,
14
+ __testRegisterFramework,
15
+ } from '../src/frameworks.js'
16
+
17
+ import type { PersistedOptions } from '../src/config-file.js'
18
+
19
+ vi.mock('node:fs', () => fs)
20
+ vi.mock('node:fs/promises', () => fs.promises)
21
+
22
+ const fakeCTAJSON: PersistedOptions = {
23
+ projectName: 'test',
24
+ targetDir: '/foo',
25
+ framework: 'test',
26
+ mode: 'code-router',
27
+ existingAddOns: [],
28
+ version: 1,
29
+ typescript: true,
30
+ tailwind: true,
31
+ packageManager: 'npm',
32
+ git: true,
33
+ }
34
+
35
+ beforeEach(() => {
36
+ const fakeFiles = {
37
+ './package.json': JSON.stringify({
38
+ name: 'test',
39
+ version: '1.0.0',
40
+ dependencies: {},
41
+ }),
42
+ }
43
+
44
+ vol.reset()
45
+ __testClearFrameworks()
46
+ __testRegisterFramework({
47
+ id: 'test',
48
+ name: 'Test',
49
+ description: 'Test',
50
+ version: '1.0.0',
51
+ baseDirectory: '/foo',
52
+ addOnsDirectories: [],
53
+ examplesDirectory: '',
54
+ basePackageJSON: {},
55
+ optionalPackages: {},
56
+ getAddOns: () => [
57
+ {
58
+ id: 'test',
59
+ name: 'Test',
60
+ description: 'Test',
61
+ version: '1.0.0',
62
+ type: 'add-on',
63
+ phase: 'add-on',
64
+ modes: ['code-router', 'file-router'],
65
+ command: {
66
+ command: 'echo',
67
+ args: ['baz'],
68
+ },
69
+ packageAdditions: {
70
+ dependencies: {
71
+ 'test-package': '1.0.0',
72
+ },
73
+ },
74
+ dependsOn: [],
75
+ getFiles: () => Promise.resolve(['./jack.txt']),
76
+ getFileContents: () => Promise.resolve('foo'),
77
+ getDeletedFiles: () => Promise.resolve([]),
78
+ },
79
+ ],
80
+ getFiles: () => Promise.resolve(Object.keys(fakeFiles)),
81
+ getFileContents: (path) => Promise.resolve(fakeFiles[path]),
82
+ getDeletedFiles: () => Promise.resolve([]),
83
+ })
84
+ })
85
+
86
+ const configFile: PersistedOptions = {
87
+ projectName: 'test',
88
+ targetDir: '/foo',
89
+ framework: 'test',
90
+ mode: 'code-router',
91
+ existingAddOns: [],
92
+ version: 1,
93
+ typescript: true,
94
+ tailwind: true,
95
+ packageManager: 'npm',
96
+ git: true,
97
+ }
98
+
99
+ describe('getCurrentConfiguration', () => {
100
+ it('should check for the config file', async () => {
101
+ const { environment } = createMemoryEnvironment()
102
+ const out = await getCurrentConfiguration(environment, '/foo')
103
+ expect(out).toBeUndefined()
104
+ })
105
+
106
+ it('should read the config file', async () => {
107
+ const { environment } = createMemoryEnvironment()
108
+
109
+ environment.writeFile('/foo/.cta.json', JSON.stringify(configFile, null, 2))
110
+
111
+ const out = await getCurrentConfiguration(environment, '/foo')
112
+ expect(out).toEqual(configFile)
113
+ })
114
+ })
115
+
116
+ describe('hasPendingGitChanges', () => {
117
+ it('should check for pending git changes', async () => {
118
+ const { environment } = createMemoryEnvironment()
119
+ environment.execute = () => Promise.resolve({ stdout: '' })
120
+ const out = await hasPendingGitChanges(environment, '/foo')
121
+ expect(out).toBe(false)
122
+ })
123
+
124
+ it('should check for pending git changes', async () => {
125
+ const { environment } = createMemoryEnvironment()
126
+ environment.execute = () => Promise.resolve({ stdout: 'M foo' })
127
+ const out = await hasPendingGitChanges(environment, '/foo')
128
+ expect(out).toBe(true)
129
+ })
130
+ })
131
+
132
+ describe('writeFiles', () => {
133
+ it('should prompt for confirmation when not forced', async () => {
134
+ const { environment } = createMemoryEnvironment('/foo')
135
+ environment.writeFile('/foo/bloop.txt', 'bloop')
136
+ environment.writeFile(
137
+ '/foo/package.json',
138
+ JSON.stringify(
139
+ {
140
+ name: 'test',
141
+ version: '1.0.0',
142
+ dependencies: {},
143
+ },
144
+ null,
145
+ 2,
146
+ ),
147
+ )
148
+ environment.writeFile('/foo/bar.txt', 'bar')
149
+ environment.confirm = () => Promise.resolve(false)
150
+ let thrown = false
151
+ writeFiles(
152
+ environment,
153
+ '/foo',
154
+ {
155
+ files: {
156
+ './bar.txt': 'baz',
157
+ './blarg.txt': 'blarg',
158
+ },
159
+ deletedFiles: [],
160
+ },
161
+ false,
162
+ )
163
+ .catch((e) => {
164
+ thrown = true
165
+ })
166
+ .finally(() => {
167
+ expect(thrown).toBe(true)
168
+ })
169
+ })
170
+
171
+ it('should not prompt for confirmation when forced', async () => {
172
+ const { environment, output } = createMemoryEnvironment('/foo')
173
+ environment.startRun()
174
+ environment.writeFile('/foo/.cta.json', JSON.stringify(configFile, null, 2))
175
+ environment.writeFile('/foo/blooop.txt', 'blooop')
176
+ await writeFiles(
177
+ environment,
178
+ '/foo',
179
+ {
180
+ files: {
181
+ './bar.txt': 'baz',
182
+ './blarg.txt': 'blarg',
183
+ },
184
+ deletedFiles: [],
185
+ },
186
+ true,
187
+ )
188
+ environment.finishRun()
189
+ expect(output.files).toEqual({
190
+ './blooop.txt': 'blooop',
191
+ './bar.txt': 'baz',
192
+ './blarg.txt': 'blarg',
193
+ })
194
+ })
195
+
196
+ it('should handle binary files', async () => {
197
+ const { environment, output } = createMemoryEnvironment('/foo')
198
+ environment.startRun()
199
+ environment.writeFile('/foo/.cta.json', JSON.stringify(configFile, null, 2))
200
+ environment.writeFile('/foo/unchanged.jpg', 'base64::foobaz')
201
+ environment.writeFile('/foo/changing.jpg', 'base64::foobaz')
202
+ await writeFiles(
203
+ environment,
204
+ '/foo',
205
+ {
206
+ files: {
207
+ './unchanged.jpg': 'base64::foobaz',
208
+ './changing.jpg': 'base64::aGVsbG8=',
209
+ './new.jpg': 'base64::aGVsbG8=',
210
+ },
211
+ deletedFiles: [],
212
+ },
213
+ true,
214
+ )
215
+ environment.finishRun()
216
+ // It's ok for unchanged.jpg not to be written, because it matches the existing file
217
+ expect(output.files).toEqual({
218
+ './unchanged.jpg': 'base64::foobaz',
219
+ './changing.jpg': 'base64::aGVsbG8=',
220
+ './new.jpg': 'base64::aGVsbG8=',
221
+ })
222
+ })
223
+
224
+ it('should handle package.json', async () => {
225
+ const { environment, output } = createMemoryEnvironment('/foo')
226
+ environment.startRun()
227
+ environment.writeFile(
228
+ '/foo/package.json',
229
+ JSON.stringify(
230
+ {
231
+ name: 'test',
232
+ version: '1.0.0',
233
+ scripts: {
234
+ dev: 'echo "test"',
235
+ },
236
+ dependencies: {
237
+ 'test-package-2': '1.0.0',
238
+ },
239
+ },
240
+ null,
241
+ 2,
242
+ ),
243
+ )
244
+ await writeFiles(
245
+ environment,
246
+ '/foo',
247
+ {
248
+ files: {
249
+ './package.json': JSON.stringify(
250
+ {
251
+ scripts: {
252
+ test: 'echo "test"',
253
+ },
254
+ dependencies: {
255
+ 'test-package': '1.0.0',
256
+ },
257
+ },
258
+ null,
259
+ 2,
260
+ ),
261
+ },
262
+ deletedFiles: [],
263
+ },
264
+ true,
265
+ )
266
+ environment.finishRun()
267
+ expect(output.files).toEqual({
268
+ './package.json': JSON.stringify(
269
+ {
270
+ name: 'test',
271
+ version: '1.0.0',
272
+ scripts: {
273
+ dev: 'echo "test"',
274
+ test: 'echo "test"',
275
+ },
276
+ dependencies: {
277
+ 'test-package-2': '1.0.0',
278
+ 'test-package': '1.0.0',
279
+ },
280
+ devDependencies: {},
281
+ },
282
+ null,
283
+ 2,
284
+ ),
285
+ })
286
+ })
287
+
288
+ it('should delete files', async () => {
289
+ const { environment, output } = createMemoryEnvironment('/foo')
290
+ environment.startRun()
291
+ environment.writeFile('/foo/bloop.txt', 'bloop')
292
+ await writeFiles(
293
+ environment,
294
+ '/foo',
295
+ { files: {}, deletedFiles: ['./bloop.txt'] },
296
+ true,
297
+ )
298
+ environment.finishRun()
299
+ expect(output.deletedFiles).toEqual(['./bloop.txt'])
300
+ })
301
+ })
302
+
303
+ describe('runNewCommands', () => {
304
+ it('should run new commands', async () => {
305
+ const { environment, output } = createMemoryEnvironment('/foo')
306
+ environment.startRun()
307
+ await runNewCommands(environment, fakeCTAJSON, '/foo', {
308
+ commands: [{ command: 'echo', args: ['bloop'] }],
309
+ })
310
+ environment.finishRun()
311
+ expect(output.commands).toEqual([{ command: 'echo', args: ['bloop'] }])
312
+ })
313
+ })
314
+
315
+ describe('addToApp', () => {
316
+ it('should add an add-on', async () => {
317
+ const { environment, output } = createMemoryEnvironment('/foo')
318
+ environment.startRun()
319
+ environment.writeFile(
320
+ '/foo/.cta.json',
321
+ JSON.stringify(fakeCTAJSON, null, 2),
322
+ )
323
+ environment.writeFile(
324
+ '/foo/package.json',
325
+ JSON.stringify(
326
+ {
327
+ name: 'test',
328
+ version: '1.0.0',
329
+ scripts: {},
330
+ dependencies: {},
331
+ devDependencies: {},
332
+ },
333
+ null,
334
+ 2,
335
+ ),
336
+ )
337
+ await addToApp(environment, ['test'], '/foo', {
338
+ forced: true,
339
+ })
340
+ environment.finishRun()
341
+ expect(output.files).toEqual({
342
+ './jack.txt': 'foo',
343
+ './package.json': JSON.stringify(
344
+ {
345
+ name: 'test',
346
+ version: '1.0.0',
347
+ scripts: {},
348
+ dependencies: {
349
+ 'test-package': '1.0.0',
350
+ },
351
+ devDependencies: {},
352
+ },
353
+ null,
354
+ 2,
355
+ ),
356
+ })
357
+ })
358
+ })
@@ -2,9 +2,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
2
  import { fs, vol } from 'memfs'
3
3
  import { resolve } from 'node:path'
4
4
 
5
- import { readConfigFile, writeConfigFile } from '../src/config-file.js'
5
+ import {
6
+ readConfigFileFromEnvironment,
7
+ writeConfigFileToEnvironment,
8
+ } from '../src/config-file.js'
6
9
  import { CONFIG_FILE } from '../src/constants.js'
7
-
10
+ import { createMemoryEnvironment } from '../src/environment.js'
8
11
  import type { AddOn, Environment, Framework, Options } from '../src/types.js'
9
12
 
10
13
  vi.mock('node:fs', () => fs)
@@ -16,6 +19,7 @@ beforeEach(() => {
16
19
 
17
20
  describe('writeConfigFile', () => {
18
21
  it('should write the config file', async () => {
22
+ const targetDir = 'test-dir'
19
23
  const options = {
20
24
  framework: {
21
25
  id: 'react-cra',
@@ -25,12 +29,11 @@ describe('writeConfigFile', () => {
25
29
  {
26
30
  id: 'add-on-1',
27
31
  description: 'Add-on 1',
28
- templates: ['file-router'],
32
+ modes: ['file-router'],
29
33
  } as AddOn,
30
34
  ],
31
- addOns: [],
35
+ targetDir,
32
36
  } as unknown as Options
33
- const targetDir = 'test-dir'
34
37
  const persistedOptions = {
35
38
  version: 1,
36
39
  framework: options.framework.id,
@@ -42,23 +45,24 @@ describe('writeConfigFile', () => {
42
45
  expect(optionsString).toEqual(JSON.stringify(persistedOptions, null, 2))
43
46
  },
44
47
  } as Environment
45
- await writeConfigFile(env, targetDir, options)
48
+ await writeConfigFileToEnvironment(env, options)
46
49
  })
47
50
  })
48
51
 
49
- describe('readConfigFile', () => {
52
+ describe('readConfigFileFromEnvironment', () => {
50
53
  it('should read the config file', async () => {
54
+ const targetDir = 'test-dir'
51
55
  const persistedOptions = {
52
56
  version: 1,
53
57
  framework: 'react-cra',
54
58
  existingAddOns: ['add-on-1'],
55
59
  }
56
- vol.mkdirSync('/test')
57
- vol.writeFileSync(
58
- resolve('/test', CONFIG_FILE),
60
+ const { environment } = createMemoryEnvironment()
61
+ environment.writeFile(
62
+ resolve(targetDir, CONFIG_FILE),
59
63
  JSON.stringify(persistedOptions, null, 2),
60
64
  )
61
- const config = await readConfigFile('/test')
65
+ const config = await readConfigFileFromEnvironment(environment, targetDir)
62
66
  expect(config).toEqual(persistedOptions)
63
67
  })
64
68
  })
@@ -1,6 +1,5 @@
1
- import { resolve } from 'node:path'
2
-
3
1
  import { describe, expect, it } from 'vitest'
2
+ import { resolve } from 'node:path'
4
3
 
5
4
  import { createApp } from '../src/create-app.js'
6
5
 
@@ -10,6 +9,7 @@ import { AddOn, Options } from '../src/types.js'
10
9
 
11
10
  const simpleOptions = {
12
11
  projectName: 'test',
12
+ targetDir: '/',
13
13
  framework: {
14
14
  id: 'test',
15
15
  name: 'Test',
@@ -37,6 +37,7 @@ const simpleOptions = {
37
37
  },
38
38
  getFiles: () => ['./src/test.txt'],
39
39
  getFileContents: () => 'Hello',
40
+ getDeletedFiles: () => [],
40
41
  },
41
42
  chosenAddOns: [],
42
43
  packageManager: 'pnpm',
@@ -49,25 +50,16 @@ const simpleOptions = {
49
50
  describe('createApp', () => {
50
51
  it('should create an app', async () => {
51
52
  const { environment, output } = createMemoryEnvironment()
52
- await createApp(simpleOptions, {
53
- silent: true,
54
- environment,
55
- name: 'Test',
56
- cwd: '/',
57
- appName: 'TanStack App',
58
- })
53
+ await createApp(environment, simpleOptions)
59
54
 
60
55
  expect(output.files['/src/test.txt']).toEqual('Hello')
61
56
  })
62
57
 
63
58
  it('should create an app - not silent', async () => {
64
59
  const { environment, output } = createMemoryEnvironment()
65
- await createApp(simpleOptions, {
66
- silent: false,
67
- environment,
68
- name: 'Test',
69
- cwd: '/foo/bar/baz',
70
- appName: 'TanStack App',
60
+ await createApp(environment, {
61
+ ...simpleOptions,
62
+ targetDir: '/foo/bar/baz',
71
63
  })
72
64
 
73
65
  const cwd = process.cwd()
@@ -79,26 +71,18 @@ describe('createApp', () => {
79
71
 
80
72
  it('should create an app - with a starter', async () => {
81
73
  const { environment, output } = createMemoryEnvironment()
82
- await createApp(
83
- {
84
- ...simpleOptions,
85
- starter: {
86
- command: {
87
- command: 'echo',
88
- args: ['Hello'],
89
- },
90
- getFiles: () => ['./src/test2.txt'],
91
- getFileContents: () => 'Hello-2',
92
- } as unknown as AddOn,
93
- },
94
- {
95
- silent: false,
96
- environment,
97
- name: 'Test',
98
- cwd: '/',
99
- appName: 'TanStack App',
100
- },
101
- )
74
+ await createApp(environment, {
75
+ ...simpleOptions,
76
+ starter: {
77
+ command: {
78
+ command: 'echo',
79
+ args: ['Hello'],
80
+ },
81
+ getFiles: () => ['./src/test2.txt'],
82
+ getFileContents: () => 'Hello-2',
83
+ getDeletedFiles: () => [],
84
+ } as unknown as AddOn,
85
+ })
102
86
 
103
87
  expect(output.files['/src/test2.txt']).toEqual('Hello-2')
104
88
  expect(output.commands.some(({ command }) => command === 'echo')).toBe(true)
@@ -106,39 +90,31 @@ describe('createApp', () => {
106
90
 
107
91
  it('should create an app - with a add-on', async () => {
108
92
  const { environment, output } = createMemoryEnvironment()
109
- await createApp(
110
- {
111
- ...simpleOptions,
112
- git: true,
113
- addOns: true,
114
- chosenAddOns: [
115
- {
116
- type: 'add-on',
117
- phase: 'add-on',
118
- warning: 'This is a warning',
119
- command: {
120
- command: 'echo',
121
- args: ['Hello'],
122
- },
123
- packageAdditions: {
124
- dependencies: {},
125
- devDependencies: {},
126
- },
127
- getFiles: () => ['./src/test2.txt', './public/foo.jpg'],
128
- getFileContents: () => 'base64::aGVsbG8=',
129
- } as unknown as AddOn,
130
- ],
131
- },
132
- {
133
- silent: false,
134
- environment,
135
- name: 'Test',
136
- cwd: '/',
137
- appName: 'TanStack App',
138
- },
139
- )
93
+ await createApp(environment, {
94
+ ...simpleOptions,
95
+ git: true,
96
+ chosenAddOns: [
97
+ {
98
+ type: 'add-on',
99
+ phase: 'add-on',
100
+ warning: 'This is a warning',
101
+ command: {
102
+ command: 'echo',
103
+ args: ['Hello'],
104
+ },
105
+ packageAdditions: {
106
+ dependencies: {},
107
+ devDependencies: {},
108
+ },
109
+ getFiles: () => ['./src/test2.txt', './public/foo.jpg'],
110
+ getFileContents: () => 'base64::aGVsbG8=',
111
+ getDeletedFiles: () => [],
112
+ } as unknown as AddOn,
113
+ ],
114
+ })
140
115
 
141
- expect(output.files['/src/test2.txt']).toEqual('hello')
116
+ // This is ok, we convert to binary right at the end of the process
117
+ expect(output.files['/src/test2.txt']).toEqual('base64::aGVsbG8=')
142
118
  expect(output.commands.some(({ command }) => command === 'echo')).toBe(true)
143
119
  })
144
120
  })
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { camelCase } from '../../src/custom-add-ons/add-on.js'
4
+
5
+ describe('camelCase', () => {
6
+ it('should convert a string to camel case', () => {
7
+ expect(camelCase('test-case')).toBe('TestCase')
8
+ expect(camelCase('demo.test-case')).toBe('DemoTestCase')
9
+ expect(camelCase('demo/test-case')).toBe('DemoTestCase')
10
+ expect(camelCase('demo/test/case')).toBe('DemoTestCase')
11
+ })
12
+ })