@tanstack/cta-engine 0.27.1 → 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.
@@ -0,0 +1,418 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { createPackageJSON, mergePackageJSON } from '../src/package-json.js'
4
+ import type { Options, Framework } from '../src/types.js'
5
+
6
+ describe('Conditional Package Dependencies', () => {
7
+ const baseFramework = {
8
+ basePackageJSON: {
9
+ version: '1.0.0',
10
+ type: 'module'
11
+ },
12
+ optionalPackages: {}
13
+ } as unknown as Framework
14
+
15
+ const baseOptions = {
16
+ projectName: 'test-app',
17
+ framework: baseFramework,
18
+ mode: 'file-router',
19
+ typescript: true,
20
+ tailwind: false,
21
+ packageManager: 'pnpm',
22
+ chosenAddOns: [],
23
+ addOnOptions: {}
24
+ } as unknown as Options
25
+
26
+ describe('EJS Template Processing', () => {
27
+ it('should process packageTemplate with conditional dependencies', () => {
28
+ const options = {
29
+ ...baseOptions,
30
+ chosenAddOns: [
31
+ {
32
+ id: 'testAddon',
33
+ name: 'Drizzle ORM',
34
+ packageTemplate: `{
35
+ "dependencies": {
36
+ "testAddon-orm": "^0.29.0"<% if (addOnOption.testAddon.database === 'postgres') { %>,
37
+ "postgres": "^3.4.0"<% } %><% if (addOnOption.testAddon.database === 'mysql') { %>,
38
+ "mysql2": "^3.6.0"<% } %><% if (addOnOption.testAddon.database === 'sqlite') { %>,
39
+ "better-sqlite3": "^8.7.0"<% } %>
40
+ },
41
+ "devDependencies": {<% if (addOnOption.testAddon.database === 'postgres') { %>
42
+ "@types/postgres": "^3.0.0"<% } %><% if (addOnOption.testAddon.database === 'mysql') { %>
43
+ "@types/mysql2": "^3.0.0"<% } %><% if (addOnOption.testAddon.database === 'sqlite') { %>
44
+ "@types/better-sqlite3": "^7.6.0"<% } %>
45
+ }
46
+ }`
47
+ }
48
+ ],
49
+ addOnOptions: {
50
+ testAddon: {
51
+ database: 'postgres'
52
+ }
53
+ }
54
+ }
55
+
56
+ const packageJSON = createPackageJSON(options)
57
+
58
+ expect(packageJSON.dependencies).toEqual({
59
+ 'testAddon-orm': '^0.29.0',
60
+ 'postgres': '^3.4.0'
61
+ })
62
+ expect(packageJSON.devDependencies).toEqual({
63
+ '@types/postgres': '^3.0.0'
64
+ })
65
+ // MySQL and SQLite dependencies should not be included
66
+ expect(packageJSON.dependencies).not.toHaveProperty('mysql2')
67
+ expect(packageJSON.dependencies).not.toHaveProperty('better-sqlite3')
68
+ expect(packageJSON.devDependencies).not.toHaveProperty('@types/mysql2')
69
+ expect(packageJSON.devDependencies).not.toHaveProperty('@types/better-sqlite3')
70
+ })
71
+
72
+ it('should handle different database options correctly', () => {
73
+ const options = {
74
+ ...baseOptions,
75
+ chosenAddOns: [
76
+ {
77
+ id: 'testAddon',
78
+ name: 'Drizzle ORM',
79
+ packageTemplate: `{
80
+ "dependencies": {
81
+ "testAddon-orm": "^0.29.0"<% if (addOnOption.testAddon.database === 'postgres') { %>,
82
+ "postgres": "^3.4.0"<% } %><% if (addOnOption.testAddon.database === 'mysql') { %>,
83
+ "mysql2": "^3.6.0"<% } %><% if (addOnOption.testAddon.database === 'sqlite') { %>,
84
+ "better-sqlite3": "^8.7.0"<% } %>
85
+ }
86
+ }`
87
+ }
88
+ ],
89
+ addOnOptions: {
90
+ testAddon: {
91
+ database: 'mysql'
92
+ }
93
+ }
94
+ }
95
+
96
+ const packageJSON = createPackageJSON(options)
97
+
98
+ expect(packageJSON.dependencies).toEqual({
99
+ 'testAddon-orm': '^0.29.0',
100
+ 'mysql2': '^3.6.0'
101
+ })
102
+ // PostgreSQL and SQLite dependencies should not be included
103
+ expect(packageJSON.dependencies).not.toHaveProperty('postgres')
104
+ expect(packageJSON.dependencies).not.toHaveProperty('better-sqlite3')
105
+ })
106
+
107
+ it('should handle SQLite option correctly', () => {
108
+ const options = {
109
+ ...baseOptions,
110
+ chosenAddOns: [
111
+ {
112
+ id: 'testAddon',
113
+ name: 'Drizzle ORM',
114
+ packageTemplate: `{
115
+ "dependencies": {
116
+ "testAddon-orm": "^0.29.0"<% if (addOnOption.testAddon.database === 'postgres') { %>,
117
+ "postgres": "^3.4.0"<% } %><% if (addOnOption.testAddon.database === 'mysql') { %>,
118
+ "mysql2": "^3.6.0"<% } %><% if (addOnOption.testAddon.database === 'sqlite') { %>,
119
+ "better-sqlite3": "^8.7.0"<% } %>
120
+ },
121
+ "devDependencies": {<% if (addOnOption.testAddon.database === 'sqlite') { %>
122
+ "@types/better-sqlite3": "^7.6.0"<% } %>
123
+ }
124
+ }`
125
+ }
126
+ ],
127
+ addOnOptions: {
128
+ testAddon: {
129
+ database: 'sqlite'
130
+ }
131
+ }
132
+ }
133
+
134
+ const packageJSON = createPackageJSON(options)
135
+
136
+ expect(packageJSON.dependencies).toEqual({
137
+ 'testAddon-orm': '^0.29.0',
138
+ 'better-sqlite3': '^8.7.0'
139
+ })
140
+ expect(packageJSON.devDependencies).toEqual({
141
+ '@types/better-sqlite3': '^7.6.0'
142
+ })
143
+ })
144
+
145
+ it('should handle multiple add-ons with options', () => {
146
+ const options = {
147
+ ...baseOptions,
148
+ chosenAddOns: [
149
+ {
150
+ id: 'testAddon',
151
+ name: 'Drizzle ORM',
152
+ packageTemplate: `{
153
+ "dependencies": {
154
+ "testAddon-orm": "^0.29.0"<% if (addOnOption.testAddon.database === 'postgres') { %>,
155
+ "postgres": "^3.4.0"<% } %>
156
+ }
157
+ }`
158
+ },
159
+ {
160
+ id: 'auth',
161
+ name: 'Authentication',
162
+ packageTemplate: `{
163
+ "dependencies": {<% if (addOnOption.auth.provider === 'auth0') { %>
164
+ "@auth0/nextjs-auth0": "^3.0.0"<% } %><% if (addOnOption.auth.provider === 'supabase') { %>
165
+ "@supabase/supabase-js": "^2.0.0"<% } %>
166
+ }
167
+ }`
168
+ }
169
+ ],
170
+ addOnOptions: {
171
+ testAddon: {
172
+ database: 'postgres'
173
+ },
174
+ auth: {
175
+ provider: 'auth0'
176
+ }
177
+ }
178
+ }
179
+
180
+ const packageJSON = createPackageJSON(options)
181
+
182
+ expect(packageJSON.dependencies).toEqual({
183
+ 'testAddon-orm': '^0.29.0',
184
+ 'postgres': '^3.4.0',
185
+ '@auth0/nextjs-auth0': '^3.0.0'
186
+ })
187
+ expect(packageJSON.dependencies).not.toHaveProperty('@supabase/supabase-js')
188
+ })
189
+
190
+ it('should handle complex conditional logic', () => {
191
+ const options = {
192
+ ...baseOptions,
193
+ chosenAddOns: [
194
+ {
195
+ id: 'ui',
196
+ name: 'UI Library',
197
+ packageTemplate: `{
198
+ "dependencies": {<% if (addOnOption.ui.library === 'chakra') { %>
199
+ "@chakra-ui/react": "^2.0.0",
200
+ "@emotion/react": "^11.0.0",
201
+ "@emotion/styled": "^11.0.0"<% } else if (addOnOption.ui.library === 'mui') { %>
202
+ "@mui/material": "^5.0.0",
203
+ "@emotion/react": "^11.0.0",
204
+ "@emotion/styled": "^11.0.0"<% } else if (addOnOption.ui.library === 'mantine') { %>
205
+ "@mantine/core": "^7.0.0",
206
+ "@mantine/hooks": "^7.0.0"<% } %>
207
+ }
208
+ }`
209
+ }
210
+ ],
211
+ addOnOptions: {
212
+ ui: {
213
+ library: 'mantine'
214
+ }
215
+ }
216
+ }
217
+
218
+ const packageJSON = createPackageJSON(options)
219
+
220
+ expect(packageJSON.dependencies).toEqual({
221
+ '@mantine/core': '^7.0.0',
222
+ '@mantine/hooks': '^7.0.0'
223
+ })
224
+ // Other UI library dependencies should not be included
225
+ expect(packageJSON.dependencies).not.toHaveProperty('@chakra-ui/react')
226
+ expect(packageJSON.dependencies).not.toHaveProperty('@mui/material')
227
+ })
228
+
229
+ it('should handle scripts conditionally', () => {
230
+ const options = {
231
+ ...baseOptions,
232
+ chosenAddOns: [
233
+ {
234
+ id: 'testing',
235
+ name: 'Testing Setup',
236
+ packageTemplate: `{
237
+ "scripts": {<% if (addOnOption.testing.framework === 'jest') { %>
238
+ "test": "jest",
239
+ "test:watch": "jest --watch"<% } else if (addOnOption.testing.framework === 'vitest') { %>
240
+ "test": "vitest",
241
+ "test:ui": "vitest --ui"<% } %>
242
+ },
243
+ "devDependencies": {<% if (addOnOption.testing.framework === 'jest') { %>
244
+ "jest": "^29.0.0",
245
+ "@types/jest": "^29.0.0"<% } else if (addOnOption.testing.framework === 'vitest') { %>
246
+ "vitest": "^1.0.0",
247
+ "@vitest/ui": "^1.0.0"<% } %>
248
+ }
249
+ }`
250
+ }
251
+ ],
252
+ addOnOptions: {
253
+ testing: {
254
+ framework: 'vitest'
255
+ }
256
+ }
257
+ }
258
+
259
+ const packageJSON = createPackageJSON(options)
260
+
261
+ expect(packageJSON.scripts).toEqual({
262
+ 'test': 'vitest',
263
+ 'test:ui': 'vitest --ui'
264
+ })
265
+ expect(packageJSON.devDependencies).toEqual({
266
+ 'vitest': '^1.0.0',
267
+ '@vitest/ui': '^1.0.0'
268
+ })
269
+ // Jest-specific scripts and dependencies should not be included
270
+ expect(packageJSON.scripts).not.toHaveProperty('test:watch')
271
+ expect(packageJSON.devDependencies).not.toHaveProperty('jest')
272
+ expect(packageJSON.devDependencies).not.toHaveProperty('@types/jest')
273
+ })
274
+
275
+ it('should fallback to packageAdditions on template error', () => {
276
+ const options = {
277
+ ...baseOptions,
278
+ chosenAddOns: [
279
+ {
280
+ id: 'broken',
281
+ name: 'Broken Template',
282
+ packageTemplate: `{
283
+ "dependencies": {
284
+ "valid-package": "^1.0.0"
285
+ <% this will cause a syntax error %>
286
+ }
287
+ }`,
288
+ packageAdditions: {
289
+ dependencies: {
290
+ 'fallback-package': '^1.0.0'
291
+ }
292
+ }
293
+ }
294
+ ],
295
+ addOnOptions: {}
296
+ }
297
+
298
+ const packageJSON = createPackageJSON(options)
299
+
300
+ // Should use fallback packageAdditions
301
+ expect(packageJSON.dependencies).toEqual({
302
+ 'fallback-package': '^1.0.0'
303
+ })
304
+ expect(packageJSON.dependencies).not.toHaveProperty('valid-package')
305
+ })
306
+
307
+ it('should handle empty or missing addOnOptions', () => {
308
+ const options = {
309
+ ...baseOptions,
310
+ chosenAddOns: [
311
+ {
312
+ id: 'simple',
313
+ name: 'Simple Add-on',
314
+ packageTemplate: `{
315
+ "dependencies": {
316
+ "always-included": "^1.0.0"<% if (addOnOption.simple && addOnOption.simple.feature) { %>,
317
+ "conditional-package": "^1.0.0"<% } %>
318
+ }
319
+ }`
320
+ }
321
+ ],
322
+ addOnOptions: {} // No options for this add-on
323
+ }
324
+
325
+ const packageJSON = createPackageJSON(options)
326
+
327
+ expect(packageJSON.dependencies).toEqual({
328
+ 'always-included': '^1.0.0'
329
+ })
330
+ expect(packageJSON.dependencies).not.toHaveProperty('conditional-package')
331
+ })
332
+
333
+ it('should preserve dependency sorting after template processing', () => {
334
+ const options = {
335
+ ...baseOptions,
336
+ chosenAddOns: [
337
+ {
338
+ id: 'sorting-test',
339
+ name: 'Sorting Test',
340
+ packageTemplate: `{
341
+ "dependencies": {
342
+ "z-package": "^1.0.0",
343
+ "a-package": "^1.0.0",
344
+ "m-package": "^1.0.0"
345
+ }
346
+ }`
347
+ }
348
+ ],
349
+ addOnOptions: {}
350
+ }
351
+
352
+ const packageJSON = createPackageJSON(options)
353
+ const dependencyKeys = Object.keys(packageJSON.dependencies)
354
+
355
+ // Dependencies should be sorted alphabetically
356
+ expect(dependencyKeys).toEqual(['a-package', 'm-package', 'z-package'])
357
+ })
358
+ })
359
+
360
+ describe('mergePackageJSON', () => {
361
+ it('should merge dependencies correctly', () => {
362
+ const base = {
363
+ dependencies: {
364
+ 'react': '^18.0.0',
365
+ 'lodash': '^4.0.0'
366
+ },
367
+ devDependencies: {
368
+ 'typescript': '^5.0.0'
369
+ }
370
+ }
371
+
372
+ const overlay = {
373
+ dependencies: {
374
+ 'axios': '^1.0.0',
375
+ 'lodash': '^4.17.0' // Should override
376
+ },
377
+ devDependencies: {
378
+ 'jest': '^29.0.0'
379
+ }
380
+ }
381
+
382
+ const result = mergePackageJSON(base, overlay)
383
+
384
+ expect(result.dependencies).toEqual({
385
+ 'react': '^18.0.0',
386
+ 'lodash': '^4.17.0', // Overridden version
387
+ 'axios': '^1.0.0'
388
+ })
389
+ expect(result.devDependencies).toEqual({
390
+ 'typescript': '^5.0.0',
391
+ 'jest': '^29.0.0'
392
+ })
393
+ })
394
+
395
+ it('should handle missing sections gracefully', () => {
396
+ const base = {
397
+ dependencies: {
398
+ 'react': '^18.0.0'
399
+ }
400
+ }
401
+
402
+ const overlay = {
403
+ devDependencies: {
404
+ 'jest': '^29.0.0'
405
+ }
406
+ }
407
+
408
+ const result = mergePackageJSON(base, overlay)
409
+
410
+ expect(result.dependencies).toEqual({
411
+ 'react': '^18.0.0'
412
+ })
413
+ expect(result.devDependencies).toEqual({
414
+ 'jest': '^29.0.0'
415
+ })
416
+ })
417
+ })
418
+ })
@@ -0,0 +1,275 @@
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 { 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('Filename Processing - Prefix Stripping', () => {
24
+ it('should strip single prefix from filename', async () => {
25
+ const { environment, output } = createMemoryEnvironment()
26
+ const templateFile = createTemplateFile(environment, {
27
+ ...simpleOptions,
28
+ addOnOptions: {
29
+ testAddon: { database: 'postgres' }
30
+ }
31
+ })
32
+ environment.startRun()
33
+ await templateFile(
34
+ './__postgres__testAddon.config.ts.ejs',
35
+ '<% if (addOnOption.testAddon.database !== "postgres") { ignoreFile() } %>\n// PostgreSQL config\nexport default { driver: "postgres" }'
36
+ )
37
+ environment.finishRun()
38
+
39
+ // File should be created with prefix stripped
40
+ expect(output.files['/test/testAddon.config.ts']).toBeDefined()
41
+ expect(output.files['/test/testAddon.config.ts'].trim()).toEqual('// PostgreSQL config\nexport default { driver: \'postgres\' }')
42
+
43
+ // Original prefixed filename should not exist
44
+ expect(output.files['/test/__postgres__testAddon.config.ts']).toBeUndefined()
45
+ })
46
+
47
+ it('should strip prefix from nested directory paths', async () => {
48
+ const { environment, output } = createMemoryEnvironment()
49
+ const templateFile = createTemplateFile(environment, {
50
+ ...simpleOptions,
51
+ addOnOptions: {
52
+ testAddon: { database: 'mysql' }
53
+ }
54
+ })
55
+ environment.startRun()
56
+ await templateFile(
57
+ './src/db/__mysql__connection.ts.ejs',
58
+ '<% if (addOnOption.testAddon.database !== "mysql") { ignoreFile() } %>\n// MySQL connection\nexport const connection = "mysql"'
59
+ )
60
+ environment.finishRun()
61
+
62
+ // File should be created with prefix stripped, preserving directory structure
63
+ expect(output.files['/test/src/db/connection.ts']).toBeDefined()
64
+ expect(output.files['/test/src/db/connection.ts'].trim()).toEqual('// MySQL connection\nexport const connection = \'mysql\'')
65
+
66
+ // Original prefixed path should not exist
67
+ expect(output.files['/test/src/db/__mysql__connection.ts']).toBeUndefined()
68
+ })
69
+
70
+ it('should handle multiple prefixed files in same directory', async () => {
71
+ const { environment, output } = createMemoryEnvironment()
72
+ const templateFile = createTemplateFile(environment, {
73
+ ...simpleOptions,
74
+ addOnOptions: {
75
+ testAddon: { database: 'sqlite' }
76
+ }
77
+ })
78
+ environment.startRun()
79
+ await templateFile(
80
+ './__postgres__testAddon.config.ts.ejs',
81
+ '<% if (addOnOption.testAddon.database !== "postgres") { ignoreFile() } %>\n// PostgreSQL config'
82
+ )
83
+ await templateFile(
84
+ './__mysql__testAddon.config.ts.ejs',
85
+ '<% if (addOnOption.testAddon.database !== "mysql") { ignoreFile() } %>\n// MySQL config'
86
+ )
87
+ await templateFile(
88
+ './__sqlite__testAddon.config.ts.ejs',
89
+ '<% if (addOnOption.testAddon.database !== "sqlite") { ignoreFile() } %>\n// SQLite config'
90
+ )
91
+ environment.finishRun()
92
+
93
+ // Only SQLite file should exist (others ignored via ignoreFile())
94
+ expect(output.files['/test/testAddon.config.ts']).toBeDefined()
95
+ expect(output.files['/test/testAddon.config.ts'].trim()).toEqual('// SQLite config')
96
+
97
+ // Prefixed filenames should not exist
98
+ expect(output.files['/test/__postgres__testAddon.config.ts']).toBeUndefined()
99
+ expect(output.files['/test/__mysql__testAddon.config.ts']).toBeUndefined()
100
+ expect(output.files['/test/__sqlite__testAddon.config.ts']).toBeUndefined()
101
+ })
102
+
103
+ it('should handle complex filename patterns', async () => {
104
+ const { environment, output } = createMemoryEnvironment()
105
+ const templateFile = createTemplateFile(environment, {
106
+ ...simpleOptions,
107
+ addOnOptions: {
108
+ auth: { provider: 'auth0' }
109
+ }
110
+ })
111
+ environment.startRun()
112
+ await templateFile(
113
+ './__auth0__auth.config.js.ejs',
114
+ '<% if (addOnOption.auth.provider !== "auth0") { ignoreFile() } %>\n// Auth0 configuration\nmodule.exports = { provider: "auth0" }'
115
+ )
116
+ environment.finishRun()
117
+
118
+ // File should be created with prefix stripped
119
+ expect(output.files['/test/auth.config.js']).toBeDefined()
120
+ expect(output.files['/test/auth.config.js'].trim()).toEqual('// Auth0 configuration\nmodule.exports = { provider: "auth0" }')
121
+ })
122
+
123
+ it('should handle prefixed files with .append.ejs extension', async () => {
124
+ const { environment, output } = createMemoryEnvironment()
125
+ const templateFile = createTemplateFile(environment, {
126
+ ...simpleOptions,
127
+ addOnOptions: {
128
+ testAddon: { database: 'postgres' }
129
+ }
130
+ })
131
+ environment.startRun()
132
+ // Create base file first
133
+ await templateFile(
134
+ './.env.ejs',
135
+ 'BASE_VAR=value\n'
136
+ )
137
+ // Then append with prefixed filename
138
+ await templateFile(
139
+ './__postgres__.env.append.ejs',
140
+ '<% if (addOnOption.testAddon.database !== "postgres") { ignoreFile() } %>\nDATABASE_URL=postgresql://localhost:5432/mydb\n'
141
+ )
142
+ environment.finishRun()
143
+
144
+ // File should be created with prefix stripped and content appended
145
+ expect(output.files['/test/.env']).toBeDefined()
146
+ expect(output.files['/test/.env']).toEqual('BASE_VAR=value\n\nDATABASE_URL=postgresql://localhost:5432/mydb\n')
147
+ })
148
+
149
+ it('should handle files without prefixes normally', async () => {
150
+ const { environment, output } = createMemoryEnvironment()
151
+ const templateFile = createTemplateFile(environment, simpleOptions)
152
+ environment.startRun()
153
+ await templateFile(
154
+ './regular-file.ts.ejs',
155
+ 'export const config = "normal"'
156
+ )
157
+ environment.finishRun()
158
+
159
+ // File should be created with normal filename processing
160
+ expect(output.files['/test/regular-file.ts']).toBeDefined()
161
+ expect(output.files['/test/regular-file.ts']).toEqual('export const config = \'normal\'\n')
162
+ })
163
+
164
+ it('should handle malformed prefixes gracefully', async () => {
165
+ const { environment, output } = createMemoryEnvironment()
166
+ const templateFile = createTemplateFile(environment, simpleOptions)
167
+ environment.startRun()
168
+ await templateFile(
169
+ './__malformed_prefix.ts.ejs',
170
+ 'export const config = "malformed"'
171
+ )
172
+ await templateFile(
173
+ './__only_one_underscore.ts.ejs',
174
+ 'export const config = "malformed2"'
175
+ )
176
+ await templateFile(
177
+ './____.ts.ejs',
178
+ 'export const config = "empty"'
179
+ )
180
+ environment.finishRun()
181
+
182
+ // Files with malformed prefixes should be created as-is (minus .ejs extension)
183
+ expect(output.files['/test/__malformed_prefix.ts']).toBeDefined()
184
+ expect(output.files['/test/__only_one_underscore.ts']).toBeDefined()
185
+ expect(output.files['/test/____.ts']).toBeDefined()
186
+ })
187
+
188
+ it('should handle deeply nested prefixed files', async () => {
189
+ const { environment, output } = createMemoryEnvironment()
190
+ const templateFile = createTemplateFile(environment, {
191
+ ...simpleOptions,
192
+ addOnOptions: {
193
+ styling: { framework: 'tailwind' }
194
+ }
195
+ })
196
+ environment.startRun()
197
+ await templateFile(
198
+ './src/styles/components/__tailwind__button.css.ejs',
199
+ '<% if (addOnOption.styling.framework !== "tailwind") { ignoreFile() } %>\n@tailwind base;\n@tailwind components;\n@tailwind utilities;'
200
+ )
201
+ environment.finishRun()
202
+
203
+ // File should be created with prefix stripped, preserving deep directory structure
204
+ expect(output.files['/test/src/styles/components/button.css']).toBeDefined()
205
+ expect(output.files['/test/src/styles/components/button.css'].trim()).toEqual('@tailwind base;\n@tailwind components;\n@tailwind utilities;')
206
+ })
207
+
208
+ it('should handle prefix stripping with different option values', async () => {
209
+ const { environment, output } = createMemoryEnvironment()
210
+ const templateFile = createTemplateFile(environment, {
211
+ ...simpleOptions,
212
+ addOnOptions: {
213
+ ui: { library: 'chakra' }
214
+ }
215
+ })
216
+ environment.startRun()
217
+ await templateFile(
218
+ './__chakra__theme.ts.ejs',
219
+ '<% if (addOnOption.ui.library !== "chakra") { ignoreFile() } %>\n// Chakra UI theme\nexport const theme = { colors: {} }'
220
+ )
221
+ await templateFile(
222
+ './__mui__theme.ts.ejs',
223
+ '<% if (addOnOption.ui.library !== "mui") { ignoreFile() } %>\n// Material-UI theme\nexport const theme = { palette: {} }'
224
+ )
225
+ environment.finishRun()
226
+
227
+ // Only Chakra file should exist (MUI ignored via ignoreFile())
228
+ expect(output.files['/test/theme.ts']).toBeDefined()
229
+ expect(output.files['/test/theme.ts'].trim()).toEqual('// Chakra UI theme\nexport const theme = { colors: {} }')
230
+
231
+ // Prefixed filenames should not exist
232
+ expect(output.files['/test/__chakra__theme.ts']).toBeUndefined()
233
+ expect(output.files['/test/__mui__theme.ts']).toBeUndefined()
234
+ })
235
+
236
+ it('should handle complex prefix with special characters', async () => {
237
+ const { environment, output } = createMemoryEnvironment()
238
+ const templateFile = createTemplateFile(environment, {
239
+ ...simpleOptions,
240
+ addOnOptions: {
241
+ deployment: { platform: 'vercel-edge' }
242
+ }
243
+ })
244
+ environment.startRun()
245
+ await templateFile(
246
+ './__vercel-edge__api.ts.ejs',
247
+ '<% if (addOnOption.deployment.platform !== "vercel-edge") { ignoreFile() } %>\n// Vercel Edge API\nexport const runtime = "edge"'
248
+ )
249
+ environment.finishRun()
250
+
251
+ // File should be created with prefix stripped
252
+ expect(output.files['/test/api.ts']).toBeDefined()
253
+ expect(output.files['/test/api.ts'].trim()).toEqual('// Vercel Edge API\nexport const runtime = \'edge\'')
254
+ })
255
+
256
+ it('should handle multiple prefixes in same filename (edge case)', async () => {
257
+ const { environment, output } = createMemoryEnvironment()
258
+ const templateFile = createTemplateFile(environment, {
259
+ ...simpleOptions,
260
+ addOnOptions: {
261
+ test: { value: 'postgres' }
262
+ }
263
+ })
264
+ environment.startRun()
265
+ await templateFile(
266
+ './__postgres__file__with__underscores.ts.ejs',
267
+ '<% if (addOnOption.test.value !== "postgres") { ignoreFile() } %>\n// File with underscores\nexport const value = "test"'
268
+ )
269
+ environment.finishRun()
270
+
271
+ // Should only strip the first valid prefix pattern
272
+ expect(output.files['/test/file__with__underscores.ts']).toBeDefined()
273
+ expect(output.files['/test/file__with__underscores.ts'].trim()).toEqual('// File with underscores\nexport const value = \'test\'')
274
+ })
275
+ })