@tanstack/cta-engine 0.28.0 → 0.29.1

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.
@@ -44,13 +44,13 @@ describe('createPackageJSON', () => {
44
44
 
45
45
  const expected = {
46
46
  name: 'test',
47
+ devDependencies: {
48
+ typescript: '^5.0.0',
49
+ },
47
50
  dependencies: {
48
51
  'file-router': '^1.0.0',
49
52
  tailwindcss: '^3.0.0',
50
53
  },
51
- devDependencies: {
52
- typescript: '^5.0.0',
53
- },
54
54
  scripts: {
55
55
  dev: 'file-router dev',
56
56
  },
@@ -0,0 +1,165 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import * as fs from 'node:fs'
3
+ import { postInitScript } from '../src/special-steps/post-init-script.js'
4
+ import { rimrafNodeModules } from '../src/special-steps/rimraf-node-modules.js'
5
+ import { runSpecialSteps } from '../src/special-steps/index.js'
6
+
7
+ import type { Environment, Options } from '../src/types.js'
8
+
9
+ vi.mock('node:fs')
10
+
11
+ describe('Special Steps', () => {
12
+ let mockEnvironment: Environment
13
+ let mockOptions: Options
14
+
15
+ beforeEach(() => {
16
+ mockEnvironment = {
17
+ exists: vi.fn(),
18
+ execute: vi.fn(),
19
+ startStep: vi.fn(),
20
+ finishStep: vi.fn(),
21
+ error: vi.fn(),
22
+ warn: vi.fn(),
23
+ info: vi.fn(),
24
+ rimraf: vi.fn(),
25
+ deleteFile: vi.fn(),
26
+ readFile: vi.fn(),
27
+ } as unknown as Environment
28
+
29
+ mockOptions = {
30
+ targetDir: '/test/project',
31
+ packageManager: 'npm',
32
+ chosenAddOns: [],
33
+ } as Options
34
+ })
35
+
36
+ describe('postInitScript', () => {
37
+ it('should run post-cta-init script when it exists', async () => {
38
+ const mockPackageJson = {
39
+ scripts: {
40
+ 'post-cta-init': 'echo "Running post-cta-init"',
41
+ },
42
+ }
43
+
44
+ vi.spyOn(mockEnvironment, 'exists').mockReturnValue(true)
45
+ vi.mocked(fs.readFileSync).mockReturnValue(
46
+ JSON.stringify(mockPackageJson),
47
+ )
48
+
49
+ await postInitScript(mockEnvironment, mockOptions)
50
+
51
+ expect(mockEnvironment.startStep).toHaveBeenCalledWith({
52
+ id: 'post-init-script',
53
+ type: 'command',
54
+ message: 'Running post-cta-init script...',
55
+ })
56
+
57
+ expect(mockEnvironment.execute).toHaveBeenCalledWith(
58
+ 'npm',
59
+ ['run', 'post-cta-init'],
60
+ '/test/project',
61
+ { inherit: true },
62
+ )
63
+
64
+ expect(mockEnvironment.finishStep).toHaveBeenCalledWith(
65
+ 'post-init-script',
66
+ 'Post-cta-init script complete',
67
+ )
68
+ })
69
+
70
+ it('should skip when package.json does not exist', async () => {
71
+ vi.spyOn(mockEnvironment, 'exists').mockReturnValue(false)
72
+
73
+ await postInitScript(mockEnvironment, mockOptions)
74
+
75
+ expect(mockEnvironment.warn).toHaveBeenCalledWith(
76
+ 'Warning',
77
+ 'No package.json found, skipping post-cta-init script',
78
+ )
79
+ expect(mockEnvironment.execute).not.toHaveBeenCalled()
80
+ })
81
+
82
+ it('should skip when post-cta-init script does not exist', async () => {
83
+ const mockPackageJson = {
84
+ scripts: {
85
+ build: 'echo "Building"',
86
+ },
87
+ }
88
+
89
+ vi.spyOn(mockEnvironment, 'exists').mockReturnValue(true)
90
+ vi.mocked(fs.readFileSync).mockReturnValue(
91
+ JSON.stringify(mockPackageJson),
92
+ )
93
+
94
+ await postInitScript(mockEnvironment, mockOptions)
95
+
96
+ // Note: The function now skips silently without calling info()
97
+ expect(mockEnvironment.execute).not.toHaveBeenCalled()
98
+ })
99
+
100
+ it('should handle different package managers', async () => {
101
+ const packageManagers = ['yarn', 'pnpm', 'bun', 'deno'] as const
102
+ const expectedCommands = {
103
+ yarn: { command: 'yarn', args: ['run', 'post-cta-init'] },
104
+ pnpm: { command: 'pnpm', args: ['post-cta-init'] },
105
+ bun: { command: 'bun', args: ['--bun', 'run', 'post-cta-init'] },
106
+ deno: { command: 'deno', args: ['task', 'post-cta-init'] },
107
+ }
108
+
109
+ for (const pm of packageManagers) {
110
+ mockOptions.packageManager = pm
111
+ vi.clearAllMocks()
112
+ vi.spyOn(mockEnvironment, 'exists').mockReturnValue(true)
113
+ vi.mocked(fs.readFileSync).mockReturnValue(
114
+ JSON.stringify({ scripts: { 'post-cta-init': 'test' } }),
115
+ )
116
+
117
+ await postInitScript(mockEnvironment, mockOptions)
118
+
119
+ const expected = expectedCommands[pm]
120
+ expect(mockEnvironment.execute).toHaveBeenCalledWith(
121
+ expected.command,
122
+ expected.args,
123
+ '/test/project',
124
+ { inherit: true },
125
+ )
126
+ }
127
+ })
128
+ })
129
+
130
+ describe('runSpecialSteps', () => {
131
+ it('should run multiple special steps in sequence', async () => {
132
+ const specialSteps = ['rimraf-node-modules', 'post-init-script']
133
+
134
+ vi.spyOn(mockEnvironment, 'exists').mockReturnValue(true)
135
+ vi.mocked(fs.readFileSync).mockReturnValue(
136
+ JSON.stringify({ scripts: { 'post-cta-init': 'test' } }),
137
+ )
138
+
139
+ await runSpecialSteps(mockEnvironment, mockOptions, specialSteps)
140
+
141
+ expect(mockEnvironment.startStep).toHaveBeenCalledWith({
142
+ id: 'special-steps',
143
+ type: 'command',
144
+ message: 'Running special steps...',
145
+ })
146
+
147
+ expect(mockEnvironment.rimraf).toHaveBeenCalled()
148
+ expect(mockEnvironment.execute).toHaveBeenCalled()
149
+ expect(mockEnvironment.finishStep).toHaveBeenCalledWith(
150
+ 'special-steps',
151
+ 'Special steps complete',
152
+ )
153
+ })
154
+
155
+ it('should handle unknown special steps', async () => {
156
+ const specialSteps = ['unknown-step']
157
+
158
+ await runSpecialSteps(mockEnvironment, mockOptions, specialSteps)
159
+
160
+ expect(mockEnvironment.error).toHaveBeenCalledWith(
161
+ 'Special step unknown-step not found',
162
+ )
163
+ })
164
+ })
165
+ })
@@ -0,0 +1,314 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { createMemoryEnvironment } from '../src/environment.js'
4
+ import { createTemplateFile } from '../src/template-file.js'
5
+
6
+ import type { AddOn, Options } from '../src/types.js'
7
+
8
+ const simpleOptions = {
9
+ projectName: 'test',
10
+ targetDir: '/test',
11
+ framework: {
12
+ id: 'test',
13
+ name: 'Test',
14
+ },
15
+ chosenAddOns: [],
16
+ packageManager: 'pnpm',
17
+ typescript: true,
18
+ tailwind: true,
19
+ mode: 'file-router',
20
+ addOnOptions: {},
21
+ } as unknown as Options
22
+
23
+ describe('Template Context - Add-on Options', () => {
24
+ it('should provide addOnOption context variable', async () => {
25
+ const { environment, output } = createMemoryEnvironment()
26
+ const templateFile = createTemplateFile(environment, {
27
+ ...simpleOptions,
28
+ addOnOptions: {
29
+ testAddon: {
30
+ database: 'postgres'
31
+ }
32
+ }
33
+ })
34
+ environment.startRun()
35
+ await templateFile('./test.txt.ejs', 'Database: <%= addOnOption.testAddon.database %>')
36
+ environment.finishRun()
37
+
38
+ expect(output.files['/test/test.txt']).toEqual('Database: postgres')
39
+ })
40
+
41
+ it('should handle multiple add-on options', async () => {
42
+ const { environment, output } = createMemoryEnvironment()
43
+ const templateFile = createTemplateFile(environment, {
44
+ ...simpleOptions,
45
+ addOnOptions: {
46
+ testAddon: {
47
+ database: 'mysql'
48
+ },
49
+ shadcn: {
50
+ theme: 'slate'
51
+ }
52
+ }
53
+ })
54
+ environment.startRun()
55
+ await templateFile(
56
+ './test.txt.ejs',
57
+ 'Drizzle: <%= addOnOption.testAddon.database %>, shadcn: <%= addOnOption.shadcn.theme %>'
58
+ )
59
+ environment.finishRun()
60
+
61
+ expect(output.files['/test/test.txt']).toEqual('Drizzle: mysql, shadcn: slate')
62
+ })
63
+
64
+ it('should handle multiple options per add-on', async () => {
65
+ const { environment, output } = createMemoryEnvironment()
66
+ const templateFile = createTemplateFile(environment, {
67
+ ...simpleOptions,
68
+ addOnOptions: {
69
+ 'complex-addon': {
70
+ database: 'postgres',
71
+ theme: 'dark',
72
+ port: 5432
73
+ }
74
+ }
75
+ })
76
+ environment.startRun()
77
+ await templateFile(
78
+ './test.txt.ejs',
79
+ 'DB: <%= addOnOption["complex-addon"].database %>, Theme: <%= addOnOption["complex-addon"].theme %>, Port: <%= addOnOption["complex-addon"].port %>'
80
+ )
81
+ environment.finishRun()
82
+
83
+ expect(output.files['/test/test.txt']).toEqual('DB: postgres, Theme: dark, Port: 5432')
84
+ })
85
+
86
+ it('should handle conditional logic with addOnOption', async () => {
87
+ const { environment, output } = createMemoryEnvironment()
88
+ const templateFile = createTemplateFile(environment, {
89
+ ...simpleOptions,
90
+ addOnOptions: {
91
+ testAddon: {
92
+ database: 'postgres'
93
+ }
94
+ }
95
+ })
96
+ environment.startRun()
97
+ await templateFile(
98
+ './test.txt.ejs',
99
+ `<% if (addOnOption.testAddon.database === 'postgres') { %>
100
+ PostgreSQL configuration
101
+ <% } else if (addOnOption.testAddon.database === 'mysql') { %>
102
+ MySQL configuration
103
+ <% } else { %>
104
+ SQLite configuration
105
+ <% } %>`
106
+ )
107
+ environment.finishRun()
108
+
109
+ expect(output.files['/test/test.txt'].trim()).toEqual('PostgreSQL configuration')
110
+ })
111
+
112
+ it('should handle ignoreFile() with option conditions', async () => {
113
+ const { environment, output } = createMemoryEnvironment()
114
+ const templateFile = createTemplateFile(environment, {
115
+ ...simpleOptions,
116
+ addOnOptions: {
117
+ testAddon: {
118
+ database: 'postgres'
119
+ }
120
+ }
121
+ })
122
+ environment.startRun()
123
+ await templateFile(
124
+ './postgres-config.ts.ejs',
125
+ '<% if (addOnOption.testAddon.database !== "postgres") { ignoreFile() } %>\n// PostgreSQL configuration\nexport const config = "postgres"'
126
+ )
127
+ await templateFile(
128
+ './mysql-config.ts.ejs',
129
+ '<% if (addOnOption.testAddon.database !== "mysql") { ignoreFile() } %>\n// MySQL configuration\nexport const config = "mysql"'
130
+ )
131
+ environment.finishRun()
132
+
133
+ expect(output.files['/test/postgres-config.ts']).toBeDefined()
134
+ expect(output.files['/test/postgres-config.ts'].trim()).toEqual('// PostgreSQL configuration\nexport const config = \'postgres\'')
135
+ expect(output.files['/test/mysql-config.ts']).toBeUndefined()
136
+ })
137
+
138
+ it('should handle empty addOnOptions', async () => {
139
+ const { environment, output } = createMemoryEnvironment()
140
+ const templateFile = createTemplateFile(environment, {
141
+ ...simpleOptions,
142
+ addOnOptions: {}
143
+ })
144
+ environment.startRun()
145
+ await templateFile(
146
+ './test.txt.ejs',
147
+ 'Options: <%= JSON.stringify(addOnOption) %>'
148
+ )
149
+ environment.finishRun()
150
+
151
+ expect(output.files['/test/test.txt']).toEqual('Options: {}')
152
+ })
153
+
154
+ it('should handle undefined option values', async () => {
155
+ const { environment, output } = createMemoryEnvironment()
156
+ const templateFile = createTemplateFile(environment, {
157
+ ...simpleOptions,
158
+ addOnOptions: {
159
+ testAddon: {
160
+ database: undefined
161
+ }
162
+ }
163
+ })
164
+ environment.startRun()
165
+ await templateFile(
166
+ './test.txt.ejs',
167
+ 'Database: <%= addOnOption.testAddon.database || "not set" %>'
168
+ )
169
+ environment.finishRun()
170
+
171
+ expect(output.files['/test/test.txt']).toEqual('Database: not set')
172
+ })
173
+
174
+ it('should work alongside existing template variables', async () => {
175
+ const { environment, output } = createMemoryEnvironment()
176
+ const templateFile = createTemplateFile(environment, {
177
+ ...simpleOptions,
178
+ projectName: 'my-app',
179
+ chosenAddOns: [
180
+ {
181
+ id: 'testAddon',
182
+ name: 'Drizzle ORM',
183
+ } as AddOn
184
+ ],
185
+ addOnOptions: {
186
+ testAddon: {
187
+ database: 'postgres'
188
+ }
189
+ }
190
+ })
191
+ environment.startRun()
192
+ await templateFile(
193
+ './test.txt.ejs',
194
+ 'Project: <%= projectName %>, Add-ons: <%= Object.keys(addOnEnabled).join(", ") %>, Database: <%= addOnOption.testAddon.database %>'
195
+ )
196
+ environment.finishRun()
197
+
198
+ expect(output.files['/test/test.txt']).toEqual('Project: my-app, Add-ons: testAddon, Database: postgres')
199
+ })
200
+
201
+ it('should handle nested object access safely', async () => {
202
+ const { environment, output } = createMemoryEnvironment()
203
+ const templateFile = createTemplateFile(environment, {
204
+ ...simpleOptions,
205
+ addOnOptions: {
206
+ testAddon: {
207
+ database: 'postgres'
208
+ }
209
+ }
210
+ })
211
+ environment.startRun()
212
+ await templateFile(
213
+ './test.txt.ejs',
214
+ 'Exists: <%= addOnOption.testAddon ? "yes" : "no" %>, Non-existent: <%= addOnOption.nonexistent ? "yes" : "no" %>'
215
+ )
216
+ environment.finishRun()
217
+
218
+ expect(output.files['/test/test.txt']).toEqual('Exists: yes, Non-existent: no')
219
+ })
220
+
221
+ it('should handle option-based conditional imports', async () => {
222
+ const { environment, output } = createMemoryEnvironment()
223
+ const templateFile = createTemplateFile(environment, {
224
+ ...simpleOptions,
225
+ addOnOptions: {
226
+ testAddon: {
227
+ database: 'postgres'
228
+ }
229
+ }
230
+ })
231
+ environment.startRun()
232
+ await templateFile(
233
+ './db-config.ts.ejs',
234
+ `<% if (addOnOption.testAddon.database === 'postgres') { %>
235
+ import { testAddon } from 'testAddon-orm/postgres-js'
236
+ import postgres from 'postgres'
237
+ <% } else if (addOnOption.testAddon.database === 'mysql') { %>
238
+ import { testAddon } from 'testAddon-orm/mysql2'
239
+ import mysql from 'mysql2/promise'
240
+ <% } else if (addOnOption.testAddon.database === 'sqlite') { %>
241
+ import { testAddon } from 'testAddon-orm/better-sqlite3'
242
+ import Database from 'better-sqlite3'
243
+ <% } %>
244
+
245
+ export const db = testAddon(/* connection */)`
246
+ )
247
+ environment.finishRun()
248
+
249
+ expect(output.files['/test/db-config.ts']).toContain("import { testAddon } from 'testAddon-orm/postgres-js'")
250
+ expect(output.files['/test/db-config.ts']).toContain("import postgres from 'postgres'")
251
+ expect(output.files['/test/db-config.ts']).not.toContain("import mysql from 'mysql2/promise'")
252
+ expect(output.files['/test/db-config.ts']).not.toContain("import Database from 'better-sqlite3'")
253
+ })
254
+
255
+ it('should handle filename prefix stripping', async () => {
256
+ const { environment, output } = createMemoryEnvironment()
257
+ const templateFile = createTemplateFile(environment, {
258
+ ...simpleOptions,
259
+ addOnOptions: {
260
+ testAddon: {
261
+ database: 'postgres'
262
+ }
263
+ }
264
+ })
265
+ environment.startRun()
266
+ await templateFile(
267
+ './__postgres__testAddon.config.ts.ejs',
268
+ '<% if (addOnOption.testAddon.database !== "postgres") { ignoreFile() } %>\n// PostgreSQL Drizzle config\nexport default { driver: "postgres" }'
269
+ )
270
+ await templateFile(
271
+ './__mysql__testAddon.config.ts.ejs',
272
+ '<% if (addOnOption.testAddon.database !== "mysql") { ignoreFile() } %>\n// MySQL Drizzle config\nexport default { driver: "mysql" }'
273
+ )
274
+ environment.finishRun()
275
+
276
+ // File should be created with prefix stripped
277
+ expect(output.files['/test/testAddon.config.ts']).toBeDefined()
278
+ expect(output.files['/test/testAddon.config.ts'].trim()).toEqual('// PostgreSQL Drizzle config\nexport default { driver: \'postgres\' }')
279
+
280
+ // Prefixed filename should not exist
281
+ expect(output.files['/test/__postgres__testAddon.config.ts']).toBeUndefined()
282
+ expect(output.files['/test/__mysql__testAddon.config.ts']).toBeUndefined()
283
+ })
284
+
285
+ it('should handle nested directory with prefixed files', async () => {
286
+ const { environment, output } = createMemoryEnvironment()
287
+ const templateFile = createTemplateFile(environment, {
288
+ ...simpleOptions,
289
+ addOnOptions: {
290
+ testAddon: {
291
+ database: 'sqlite'
292
+ }
293
+ }
294
+ })
295
+ environment.startRun()
296
+ await templateFile(
297
+ './src/db/__sqlite__index.ts.ejs',
298
+ '<% if (addOnOption.testAddon.database !== "sqlite") { ignoreFile() } %>\n// SQLite database connection\nexport const db = "sqlite"'
299
+ )
300
+ await templateFile(
301
+ './src/db/__postgres__index.ts.ejs',
302
+ '<% if (addOnOption.testAddon.database !== "postgres") { ignoreFile() } %>\n// PostgreSQL database connection\nexport const db = "postgres"'
303
+ )
304
+ environment.finishRun()
305
+
306
+ // SQLite file should be created with prefix stripped
307
+ expect(output.files['/test/src/db/index.ts']).toBeDefined()
308
+ expect(output.files['/test/src/db/index.ts'].trim()).toEqual('// SQLite database connection\nexport const db = \'sqlite\'')
309
+
310
+ // Prefixed filenames should not exist
311
+ expect(output.files['/test/src/db/__sqlite__index.ts']).toBeUndefined()
312
+ expect(output.files['/test/src/db/__postgres__index.ts']).toBeUndefined()
313
+ })
314
+ })