@opensaas/stack-cli 0.1.1 → 0.1.2
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +8 -0
- package/README.md +65 -5
- package/dist/commands/init.d.ts +9 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +27 -338
- package/dist/commands/init.js.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/commands/init.ts +28 -361
- package/src/index.ts +8 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/src/commands/init.test.ts +0 -308
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
-
import { initCommand } from './init.js'
|
|
3
|
-
import * as fs from 'fs'
|
|
4
|
-
import * as path from 'path'
|
|
5
|
-
import * as os from 'os'
|
|
6
|
-
|
|
7
|
-
// Mock prompts module
|
|
8
|
-
vi.mock('prompts', () => ({
|
|
9
|
-
default: vi.fn(),
|
|
10
|
-
}))
|
|
11
|
-
|
|
12
|
-
// Mock ora module
|
|
13
|
-
vi.mock('ora', () => ({
|
|
14
|
-
default: vi.fn(() => ({
|
|
15
|
-
start: vi.fn().mockReturnThis(),
|
|
16
|
-
succeed: vi.fn().mockReturnThis(),
|
|
17
|
-
fail: vi.fn().mockReturnThis(),
|
|
18
|
-
text: '',
|
|
19
|
-
})),
|
|
20
|
-
}))
|
|
21
|
-
|
|
22
|
-
// Mock chalk module
|
|
23
|
-
vi.mock('chalk', () => ({
|
|
24
|
-
default: {
|
|
25
|
-
bold: {
|
|
26
|
-
cyan: vi.fn((str) => str),
|
|
27
|
-
green: vi.fn((str) => str),
|
|
28
|
-
},
|
|
29
|
-
cyan: vi.fn((str) => str),
|
|
30
|
-
gray: vi.fn((str) => str),
|
|
31
|
-
red: vi.fn((str) => str),
|
|
32
|
-
yellow: vi.fn((str) => str),
|
|
33
|
-
green: vi.fn((str) => str),
|
|
34
|
-
},
|
|
35
|
-
}))
|
|
36
|
-
|
|
37
|
-
describe('Init Command', () => {
|
|
38
|
-
let tempDir: string
|
|
39
|
-
let originalCwd: string
|
|
40
|
-
let originalExit: typeof process.exit
|
|
41
|
-
let exitCode: number | undefined
|
|
42
|
-
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
// Create temp directory for testing
|
|
45
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'init-test-'))
|
|
46
|
-
originalCwd = process.cwd()
|
|
47
|
-
process.chdir(tempDir)
|
|
48
|
-
|
|
49
|
-
// Mock process.exit
|
|
50
|
-
originalExit = process.exit
|
|
51
|
-
exitCode = undefined
|
|
52
|
-
process.exit = vi.fn((code?: number) => {
|
|
53
|
-
exitCode = code
|
|
54
|
-
throw new Error(`process.exit(${code})`)
|
|
55
|
-
}) as never
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
afterEach(() => {
|
|
59
|
-
// Restore
|
|
60
|
-
process.chdir(originalCwd)
|
|
61
|
-
process.exit = originalExit
|
|
62
|
-
|
|
63
|
-
// Clean up
|
|
64
|
-
if (fs.existsSync(tempDir)) {
|
|
65
|
-
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
describe('initCommand', () => {
|
|
70
|
-
it('should create project structure with provided name', async () => {
|
|
71
|
-
const projectName = 'test-project'
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
await initCommand(projectName)
|
|
75
|
-
} catch {
|
|
76
|
-
// Expected to throw due to mocked process.exit
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const projectPath = path.join(tempDir, projectName)
|
|
80
|
-
expect(fs.existsSync(projectPath)).toBe(true)
|
|
81
|
-
expect(fs.existsSync(path.join(projectPath, 'app'))).toBe(true)
|
|
82
|
-
expect(fs.existsSync(path.join(projectPath, 'lib'))).toBe(true)
|
|
83
|
-
expect(fs.existsSync(path.join(projectPath, 'prisma'))).toBe(true)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('should create package.json with correct content', async () => {
|
|
87
|
-
const projectName = 'test-project'
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
await initCommand(projectName)
|
|
91
|
-
} catch {
|
|
92
|
-
// Expected to throw due to mocked process.exit
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const projectPath = path.join(tempDir, projectName)
|
|
96
|
-
const packageJson = JSON.parse(
|
|
97
|
-
fs.readFileSync(path.join(projectPath, 'package.json'), 'utf-8'),
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
expect(packageJson.name).toBe(projectName)
|
|
101
|
-
expect(packageJson.scripts.generate).toBe('opensaas generate')
|
|
102
|
-
expect(packageJson.scripts['db:push']).toBe('prisma db push')
|
|
103
|
-
expect(packageJson.dependencies['@opensaas/stack-core']).toBeDefined()
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('should create tsconfig.json', async () => {
|
|
107
|
-
const projectName = 'test-project'
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
await initCommand(projectName)
|
|
111
|
-
} catch {
|
|
112
|
-
// Expected to throw
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const projectPath = path.join(tempDir, projectName)
|
|
116
|
-
const tsConfig = JSON.parse(fs.readFileSync(path.join(projectPath, 'tsconfig.json'), 'utf-8'))
|
|
117
|
-
|
|
118
|
-
expect(tsConfig.compilerOptions.target).toBe('ES2022')
|
|
119
|
-
expect(tsConfig.compilerOptions.moduleResolution).toBe('bundler')
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('should create opensaas.config.ts with User list', async () => {
|
|
123
|
-
const projectName = 'test-project'
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
await initCommand(projectName)
|
|
127
|
-
} catch {
|
|
128
|
-
// Expected to throw
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const projectPath = path.join(tempDir, projectName)
|
|
132
|
-
const config = fs.readFileSync(path.join(projectPath, 'opensaas.config.ts'), 'utf-8')
|
|
133
|
-
|
|
134
|
-
expect(config).toContain('config({')
|
|
135
|
-
expect(config).toContain('lists: {')
|
|
136
|
-
expect(config).toContain('User: list({')
|
|
137
|
-
expect(config).toContain('name: text(')
|
|
138
|
-
expect(config).toContain('email: text(')
|
|
139
|
-
expect(config).toContain('password: password(')
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('should create .env file', async () => {
|
|
143
|
-
const projectName = 'test-project'
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
await initCommand(projectName)
|
|
147
|
-
} catch {
|
|
148
|
-
// Expected to throw
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const projectPath = path.join(tempDir, projectName)
|
|
152
|
-
const env = fs.readFileSync(path.join(projectPath, '.env'), 'utf-8')
|
|
153
|
-
|
|
154
|
-
expect(env).toContain('DATABASE_URL')
|
|
155
|
-
expect(env).toContain('file:./dev.db')
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('should create .gitignore file', async () => {
|
|
159
|
-
const projectName = 'test-project'
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
await initCommand(projectName)
|
|
163
|
-
} catch {
|
|
164
|
-
// Expected to throw
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const projectPath = path.join(tempDir, projectName)
|
|
168
|
-
const gitignore = fs.readFileSync(path.join(projectPath, '.gitignore'), 'utf-8')
|
|
169
|
-
|
|
170
|
-
expect(gitignore).toContain('node_modules')
|
|
171
|
-
expect(gitignore).toContain('.opensaas')
|
|
172
|
-
expect(gitignore).toContain('prisma/dev.db')
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('should create lib/context.ts', async () => {
|
|
176
|
-
const projectName = 'test-project'
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
await initCommand(projectName)
|
|
180
|
-
} catch {
|
|
181
|
-
// Expected to throw
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const projectPath = path.join(tempDir, projectName)
|
|
185
|
-
const context = fs.readFileSync(path.join(projectPath, 'lib', 'context.ts'), 'utf-8')
|
|
186
|
-
|
|
187
|
-
expect(context).toContain('import { PrismaClient }')
|
|
188
|
-
expect(context).toContain('export async function getContext(')
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
it('should create app/page.tsx', async () => {
|
|
192
|
-
const projectName = 'test-project'
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
await initCommand(projectName)
|
|
196
|
-
} catch {
|
|
197
|
-
// Expected to throw
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const projectPath = path.join(tempDir, projectName)
|
|
201
|
-
const page = fs.readFileSync(path.join(projectPath, 'app', 'page.tsx'), 'utf-8')
|
|
202
|
-
|
|
203
|
-
expect(page).toContain('export default function Home(')
|
|
204
|
-
expect(page).toContain('Welcome to OpenSaas')
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
it('should create app/layout.tsx', async () => {
|
|
208
|
-
const projectName = 'test-project'
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
await initCommand(projectName)
|
|
212
|
-
} catch {
|
|
213
|
-
// Expected to throw
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const projectPath = path.join(tempDir, projectName)
|
|
217
|
-
const layout = fs.readFileSync(path.join(projectPath, 'app', 'layout.tsx'), 'utf-8')
|
|
218
|
-
|
|
219
|
-
expect(layout).toContain('export default function RootLayout(')
|
|
220
|
-
expect(layout).toContain('export const metadata')
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
it('should create README.md', async () => {
|
|
224
|
-
const projectName = 'test-project'
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
await initCommand(projectName)
|
|
228
|
-
} catch {
|
|
229
|
-
// Expected to throw
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const projectPath = path.join(tempDir, projectName)
|
|
233
|
-
const readme = fs.readFileSync(path.join(projectPath, 'README.md'), 'utf-8')
|
|
234
|
-
|
|
235
|
-
expect(readme).toContain(`# ${projectName}`)
|
|
236
|
-
expect(readme).toContain('Getting Started')
|
|
237
|
-
expect(readme).toContain('npm run generate')
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
it('should create next.config.js', async () => {
|
|
241
|
-
const projectName = 'test-project'
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
await initCommand(projectName)
|
|
245
|
-
} catch {
|
|
246
|
-
// Expected to throw
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const projectPath = path.join(tempDir, projectName)
|
|
250
|
-
const nextConfig = fs.readFileSync(path.join(projectPath, 'next.config.js'), 'utf-8')
|
|
251
|
-
|
|
252
|
-
expect(nextConfig).toContain('serverComponentsExternalPackages')
|
|
253
|
-
expect(nextConfig).toContain('@opensaas/stack-core')
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
it('should fail if directory already exists', async () => {
|
|
257
|
-
const projectName = 'test-project'
|
|
258
|
-
|
|
259
|
-
// Create directory first
|
|
260
|
-
fs.mkdirSync(path.join(tempDir, projectName))
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
await initCommand(projectName)
|
|
264
|
-
} catch {
|
|
265
|
-
// Expected to throw
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
expect(exitCode).toBe(1)
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
it('should validate project name format', async () => {
|
|
272
|
-
const prompts = await import('prompts')
|
|
273
|
-
|
|
274
|
-
// Test with a valid name that should succeed
|
|
275
|
-
const validName = 'test-project-valid'
|
|
276
|
-
|
|
277
|
-
vi.mocked(prompts.default).mockResolvedValue({
|
|
278
|
-
name: validName,
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
await initCommand(undefined)
|
|
283
|
-
} catch {
|
|
284
|
-
// Expected to throw due to mocked process.exit
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Project should be created with valid name
|
|
288
|
-
expect(fs.existsSync(path.join(tempDir, validName))).toBe(true)
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
it('should verify cleanup behavior exists', () => {
|
|
292
|
-
// This test verifies the cleanup logic is present in the code
|
|
293
|
-
// Full end-to-end testing of cleanup is difficult in ESM environment
|
|
294
|
-
// The actual cleanup logic in init.ts catches errors and removes the directory
|
|
295
|
-
|
|
296
|
-
const projectName = 'test-project-cleanup'
|
|
297
|
-
const projectPath = path.join(tempDir, projectName)
|
|
298
|
-
|
|
299
|
-
// Create a test directory
|
|
300
|
-
fs.mkdirSync(projectPath)
|
|
301
|
-
expect(fs.existsSync(projectPath)).toBe(true)
|
|
302
|
-
|
|
303
|
-
// Verify cleanup can work
|
|
304
|
-
fs.rmSync(projectPath, { recursive: true, force: true })
|
|
305
|
-
expect(fs.existsSync(projectPath)).toBe(false)
|
|
306
|
-
})
|
|
307
|
-
})
|
|
308
|
-
})
|