swallowkit 1.0.0-beta.5 → 1.0.0-beta.7

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 (97) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +251 -242
  3. package/README.md +252 -243
  4. package/dist/__tests__/fixtures.d.ts +14 -0
  5. package/dist/__tests__/fixtures.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures.js +85 -0
  7. package/dist/__tests__/fixtures.js.map +1 -0
  8. package/dist/cli/commands/create-model.js +14 -14
  9. package/dist/cli/commands/dev.d.ts +8 -0
  10. package/dist/cli/commands/dev.d.ts.map +1 -1
  11. package/dist/cli/commands/dev.js +238 -30
  12. package/dist/cli/commands/dev.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts +5 -0
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +2507 -1664
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/cli/commands/scaffold.d.ts +3 -0
  18. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  19. package/dist/cli/commands/scaffold.js +281 -117
  20. package/dist/cli/commands/scaffold.js.map +1 -1
  21. package/dist/cli/index.js +2 -0
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/core/config.d.ts +2 -1
  24. package/dist/core/config.d.ts.map +1 -1
  25. package/dist/core/config.js +28 -0
  26. package/dist/core/config.js.map +1 -1
  27. package/dist/core/scaffold/functions-generator.d.ts +5 -0
  28. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  29. package/dist/core/scaffold/functions-generator.js +649 -218
  30. package/dist/core/scaffold/functions-generator.js.map +1 -1
  31. package/dist/core/scaffold/model-parser.d.ts +1 -1
  32. package/dist/core/scaffold/model-parser.js +99 -99
  33. package/dist/core/scaffold/nextjs-generator.js +181 -181
  34. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  35. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  36. package/dist/core/scaffold/openapi-generator.js +190 -0
  37. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  38. package/dist/core/scaffold/ui-generator.js +656 -656
  39. package/dist/database/base-model.d.ts +3 -3
  40. package/dist/database/base-model.js +3 -3
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +2 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/types/index.d.ts +4 -0
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/dist/utils/package-manager.d.ts +2 -1
  48. package/dist/utils/package-manager.d.ts.map +1 -1
  49. package/dist/utils/package-manager.js +14 -10
  50. package/dist/utils/package-manager.js.map +1 -1
  51. package/package.json +81 -74
  52. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +445 -0
  53. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
  54. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +524 -0
  55. package/src/__tests__/config.test.ts +122 -0
  56. package/src/__tests__/dev.test.ts +42 -0
  57. package/src/__tests__/fixtures.ts +83 -0
  58. package/src/__tests__/functions-generator.test.ts +101 -0
  59. package/src/__tests__/init.test.ts +59 -0
  60. package/src/__tests__/nextjs-generator.test.ts +97 -0
  61. package/src/__tests__/openapi-generator.test.ts +43 -0
  62. package/src/__tests__/package-manager.test.ts +189 -0
  63. package/src/__tests__/scaffold.test.ts +39 -0
  64. package/src/__tests__/string-utils.test.ts +75 -0
  65. package/src/__tests__/ui-generator.test.ts +144 -0
  66. package/src/cli/commands/create-model.ts +141 -0
  67. package/src/cli/commands/dev.ts +794 -0
  68. package/src/cli/commands/index.ts +8 -0
  69. package/src/cli/commands/init.ts +3363 -0
  70. package/src/cli/commands/provision.ts +193 -0
  71. package/src/cli/commands/scaffold.ts +786 -0
  72. package/src/cli/index.ts +73 -0
  73. package/src/core/config.ts +244 -0
  74. package/src/core/scaffold/functions-generator.ts +674 -0
  75. package/src/core/scaffold/model-parser.ts +627 -0
  76. package/src/core/scaffold/nextjs-generator.ts +217 -0
  77. package/src/core/scaffold/openapi-generator.ts +212 -0
  78. package/src/core/scaffold/ui-generator.ts +945 -0
  79. package/src/database/base-model.ts +184 -0
  80. package/src/database/client.ts +140 -0
  81. package/src/database/repository.ts +104 -0
  82. package/src/database/runtime-check.ts +25 -0
  83. package/src/index.ts +27 -0
  84. package/src/types/index.ts +45 -0
  85. package/src/utils/package-manager.ts +229 -0
  86. package/dist/cli/commands/build.d.ts +0 -6
  87. package/dist/cli/commands/build.d.ts.map +0 -1
  88. package/dist/cli/commands/build.js +0 -177
  89. package/dist/cli/commands/build.js.map +0 -1
  90. package/dist/cli/commands/deploy.d.ts +0 -3
  91. package/dist/cli/commands/deploy.d.ts.map +0 -1
  92. package/dist/cli/commands/deploy.js +0 -147
  93. package/dist/cli/commands/deploy.js.map +0 -1
  94. package/dist/cli/commands/setup.d.ts +0 -6
  95. package/dist/cli/commands/setup.d.ts.map +0 -1
  96. package/dist/cli/commands/setup.js +0 -254
  97. package/dist/cli/commands/setup.js.map +0 -1
@@ -0,0 +1,524 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`generateDetailPage generates detail page (snapshot) 1`] = `
4
+ "'use client';
5
+
6
+ import { useEffect, useState } from 'react';
7
+ import { useParams, useRouter } from 'next/navigation';
8
+ import Link from 'next/link';
9
+ import { z } from 'zod/v4';
10
+ import { todoSchema } from '@myapp/shared';
11
+
12
+ type Todo = z.infer<typeof todoSchema>;
13
+
14
+ export default function TodoDetailPage() {
15
+ const params = useParams();
16
+ const router = useRouter();
17
+ const [todo, setTodo] = useState<Todo | null>(null);
18
+ const [loading, setLoading] = useState(true);
19
+ const [error, setError] = useState<string | null>(null);
20
+
21
+
22
+ useEffect(() => {
23
+ const id = params?.id as string;
24
+ if (!id) return;
25
+
26
+ fetch(\`/api/todo/\${id}\`)
27
+ .then((res) => {
28
+ if (!res.ok) throw new Error('Failed to fetch todo');
29
+ return res.json();
30
+ })
31
+ .then((data) => {
32
+ setTodo(data);
33
+ setLoading(false);
34
+ })
35
+ .catch((err) => {
36
+ setError(err.message);
37
+ setLoading(false);
38
+ });
39
+
40
+
41
+ }, [params]);
42
+
43
+ const handleDelete = async () => {
44
+ if (!confirm('Are you sure you want to delete this item?')) return;
45
+
46
+ try {
47
+ const res = await fetch(\`/api/todo/\${params?.id}\`, {
48
+ method: 'DELETE',
49
+ });
50
+
51
+ if (!res.ok) throw new Error('Failed to delete todo');
52
+
53
+ router.push('/todo');
54
+ } catch (err: any) {
55
+ alert(\`Error: \${err.message}\`);
56
+ }
57
+ };
58
+
59
+ if (loading) {
60
+ return (
61
+ <div className="flex items-center justify-center min-h-screen">
62
+ <div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ if (error || !todo) {
68
+ return (
69
+ <div className="flex items-center justify-center min-h-screen">
70
+ <div className="text-red-600 dark:text-red-400">Error: {error || 'Todo not found'}</div>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ return (
76
+ <div className="container mx-auto px-4 py-8">
77
+ <div className="max-w-2xl mx-auto">
78
+ <div className="flex justify-between items-center mb-6">
79
+ <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Todo Details</h1>
80
+ <div className="space-x-2">
81
+ <Link
82
+ href={\`/todo/\${todo.id}/edit\`}
83
+ className="inline-flex items-center bg-green-600 hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600 text-white px-4 py-2 rounded"
84
+ >
85
+ Edit
86
+ </Link>
87
+ <button
88
+ onClick={handleDelete}
89
+ className="inline-flex items-center bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 text-white px-4 py-2 rounded"
90
+ >
91
+ Delete
92
+ </button>
93
+ </div>
94
+ </div>
95
+
96
+ <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
97
+ <dl className="space-y-4">
98
+ <div>
99
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">id</dt>
100
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(todo.id)}</dd>
101
+ </div>
102
+ <div>
103
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">title</dt>
104
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(todo.title)}</dd>
105
+ </div>
106
+ <div>
107
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">description</dt>
108
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(todo.description)}</dd>
109
+ </div>
110
+ <div>
111
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">completed</dt>
112
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(todo.completed)}</dd>
113
+ </div>
114
+ <div>
115
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">createdAt</dt>
116
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(todo.createdAt)}</dd>
117
+ </div>
118
+ <div>
119
+ <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">updatedAt</dt>
120
+ <dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(todo.updatedAt)}</dd>
121
+ </div>
122
+ </dl>
123
+ </div>
124
+
125
+ <div className="mt-6">
126
+ <Link
127
+ href="/todo"
128
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300"
129
+ >
130
+ &larr; Back to list
131
+ </Link>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ );
136
+ }
137
+ "
138
+ `;
139
+
140
+ exports[`generateEditPage generates edit page (snapshot) 1`] = `
141
+ "'use client';
142
+
143
+ import { useEffect, useState } from 'react';
144
+ import { useParams } from 'next/navigation';
145
+ import TodoForm from '../../_components/TodoForm';
146
+ import { z } from 'zod/v4';
147
+ import { todoSchema } from '@myapp/shared';
148
+
149
+ type Todo = z.infer<typeof todoSchema>;
150
+
151
+ export default function EditTodoPage() {
152
+ const params = useParams();
153
+ const [todo, setTodo] = useState<Todo | null>(null);
154
+ const [loading, setLoading] = useState(true);
155
+
156
+ useEffect(() => {
157
+ const id = params?.id as string;
158
+ if (!id) return;
159
+
160
+ fetch(\`/api/todo/\${id}\`)
161
+ .then((res) => res.json())
162
+ .then((data) => {
163
+ setTodo(data);
164
+ setLoading(false);
165
+ })
166
+ .catch(() => setLoading(false));
167
+ }, [params]);
168
+
169
+ if (loading) {
170
+ return (
171
+ <div className="flex items-center justify-center min-h-screen">
172
+ <div className="text-lg">Loading...</div>
173
+ </div>
174
+ );
175
+ }
176
+
177
+ if (!todo) {
178
+ return (
179
+ <div className="flex items-center justify-center min-h-screen">
180
+ <div className="text-red-600">Todo not found</div>
181
+ </div>
182
+ );
183
+ }
184
+
185
+ return (
186
+ <div className="container mx-auto px-4 py-8">
187
+ <div className="max-w-2xl mx-auto">
188
+ <h1 className="text-3xl font-bold mb-6">Edit Todo</h1>
189
+ <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
190
+ <TodoForm initialData={todo} isEdit={true} />
191
+ </div>
192
+ </div>
193
+ </div>
194
+ );
195
+ }
196
+ "
197
+ `;
198
+
199
+ exports[`generateFormComponent generates form component (snapshot) 1`] = `
200
+ "'use client';
201
+
202
+ import { useState, FormEvent } from 'react';
203
+ import { useRouter } from 'next/navigation';
204
+ import { z } from 'zod/v4';
205
+ import { todoSchema } from '@myapp/shared';
206
+
207
+ // Input schema: SwallowKit-managed fields (id, createdAt, updatedAt) are optional
208
+ // These fields are ignored by the backend and auto-managed
209
+ const TodoInputSchema = todoSchema.partial({ id: true, createdAt: true, updatedAt: true });
210
+
211
+ type Todo = z.infer<typeof todoSchema>;
212
+
213
+ interface TodoFormProps {
214
+ initialData?: Todo;
215
+ isEdit?: boolean;
216
+ }
217
+
218
+ export default function TodoForm({ initialData, isEdit = false }: TodoFormProps) {
219
+ const router = useRouter();
220
+ const [loading, setLoading] = useState(false);
221
+ const [errors, setErrors] = useState<Record<string, string>>({});
222
+
223
+
224
+
225
+ const [formData, setFormData] = useState({
226
+ title: initialData?.title ?? '',
227
+ description: initialData?.description ?? '',
228
+ completed: initialData?.completed ?? false,
229
+ });
230
+
231
+ const handleSubmit = async (e: FormEvent) => {
232
+ e.preventDefault();
233
+ setLoading(true);
234
+ setErrors({});
235
+
236
+ try {
237
+ // Array フィールドをカンマ区切りから配列に変換
238
+ const submitData: any = { ...formData };
239
+
240
+
241
+
242
+
243
+
244
+ // Validate input (excluding SwallowKit-managed fields)
245
+ TodoInputSchema.parse(submitData);
246
+
247
+ const url = isEdit ? \`/api/todo/\${initialData!.id}\` : '/api/todo';
248
+ const method = isEdit ? 'PUT' : 'POST';
249
+
250
+ const res = await fetch(url, {
251
+ method,
252
+ headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify(submitData),
254
+ });
255
+
256
+ if (!res.ok) {
257
+ const errorData = await res.json();
258
+ throw new Error(errorData.error || 'Failed to save todo');
259
+ }
260
+
261
+ router.push('/todo');
262
+ } catch (err: any) {
263
+ if (err.issues) {
264
+ // Zod validation errors
265
+ const fieldErrors: Record<string, string> = {};
266
+ err.issues.forEach((error: any) => {
267
+ const field = error.path[0];
268
+ fieldErrors[field] = error.message;
269
+ });
270
+ setErrors(fieldErrors);
271
+ } else {
272
+ alert(\`Error: \${err.message}\`);
273
+ }
274
+ setLoading(false);
275
+ }
276
+ };
277
+
278
+ return (
279
+ <form onSubmit={handleSubmit} className="space-y-6">
280
+ <div>
281
+ <label htmlFor="title" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
282
+ title *
283
+ </label>
284
+ <input
285
+ type="text"
286
+ id="title"
287
+ name="title"
288
+ value={formData.title}
289
+ onChange={(e) => setFormData({ ...formData, title: e.target.value })}
290
+ className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
291
+ required
292
+ />
293
+ {errors.title && (
294
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.title}</p>
295
+ )}
296
+ </div>
297
+
298
+ <div>
299
+ <label htmlFor="description" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
300
+ description
301
+ </label>
302
+ <input
303
+ type="text"
304
+ id="description"
305
+ name="description"
306
+ value={formData.description}
307
+ onChange={(e) => setFormData({ ...formData, description: e.target.value })}
308
+ className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
309
+
310
+ />
311
+ {errors.description && (
312
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.description}</p>
313
+ )}
314
+ </div>
315
+
316
+ <div className="flex items-center">
317
+ <input
318
+ type="checkbox"
319
+ id="completed"
320
+ name="completed"
321
+ checked={formData.completed}
322
+ onChange={(e) => setFormData({ ...formData, completed: e.target.checked })}
323
+ className="h-4 w-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 dark:focus:ring-blue-400 border border-gray-300 dark:border-gray-600 rounded"
324
+ />
325
+ <label htmlFor="completed" className="ml-2 block text-sm text-gray-700 dark:text-gray-300">
326
+ completed
327
+ </label>
328
+ {errors.completed && (
329
+ <p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.completed}</p>
330
+ )}
331
+ </div>
332
+
333
+ <div className="flex gap-4">
334
+ <button
335
+ type="submit"
336
+ disabled={loading}
337
+ className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded disabled:opacity-50"
338
+ >
339
+ {loading ? 'Saving...' : isEdit ? 'Update' : 'Create'}
340
+ </button>
341
+ <button
342
+ type="button"
343
+ onClick={() => router.push('/todo')}
344
+ className="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded"
345
+ >
346
+ Cancel
347
+ </button>
348
+ </div>
349
+ </form>
350
+ );
351
+ }
352
+ "
353
+ `;
354
+
355
+ exports[`generateListPage generates list page for basic model (snapshot) 1`] = `
356
+ "'use client';
357
+
358
+ import { useEffect, useState } from 'react';
359
+ import Link from 'next/link';
360
+ import { z } from 'zod/v4';
361
+ import { todoSchema } from '@myapp/shared';
362
+
363
+ type Todo = z.infer<typeof todoSchema>;
364
+
365
+ export default function TodoListPage() {
366
+ const [todos, setTodos] = useState<Todo[]>([]);
367
+ const [loading, setLoading] = useState(true);
368
+ const [error, setError] = useState<string | null>(null);
369
+
370
+
371
+ useEffect(() => {
372
+ fetch('/api/todo')
373
+ .then((res) => {
374
+ if (!res.ok) throw new Error('Failed to fetch todos');
375
+ return res.json();
376
+ })
377
+ .then((data) => {
378
+ setTodos(data);
379
+ setLoading(false);
380
+ })
381
+ .catch((err) => {
382
+ setError(err.message);
383
+ setLoading(false);
384
+ });
385
+
386
+
387
+ }, []);
388
+
389
+ const handleDelete = async (id: string) => {
390
+ if (!confirm('Are you sure you want to delete this item?')) return;
391
+
392
+ try {
393
+ const res = await fetch(\`/api/todo/\${id}\`, {
394
+ method: 'DELETE',
395
+ });
396
+
397
+ if (!res.ok) throw new Error('Failed to delete todo');
398
+
399
+ setTodos(todos.filter((item) => item.id !== id));
400
+ } catch (err: any) {
401
+ alert(\`Error: \${err.message}\`);
402
+ }
403
+ };
404
+
405
+ if (loading) {
406
+ return (
407
+ <div className="flex items-center justify-center min-h-screen">
408
+ <div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
409
+ </div>
410
+ );
411
+ }
412
+
413
+ if (error) {
414
+ return (
415
+ <div className="flex items-center justify-center min-h-screen">
416
+ <div className="text-red-600 dark:text-red-400">Error: {error}</div>
417
+ </div>
418
+ );
419
+ }
420
+
421
+ return (
422
+ <div className="container mx-auto px-4 py-8">
423
+ <div className="mb-4">
424
+ <Link
425
+ href="/"
426
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 text-sm"
427
+ >
428
+ ← Home
429
+ </Link>
430
+ </div>
431
+ <div className="flex justify-between items-center mb-6">
432
+ <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Todo</h1>
433
+ <Link
434
+ href="/todo/new"
435
+ className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded"
436
+ >
437
+ Create New
438
+ </Link>
439
+ </div>
440
+
441
+ {todos.length === 0 ? (
442
+ <div className="text-center py-12 text-gray-500 dark:text-gray-400">
443
+ No todos found. Create your first one!
444
+ </div>
445
+ ) : (
446
+ <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden">
447
+ <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
448
+ <thead className="bg-gray-50 dark:bg-gray-900">
449
+ <tr>
450
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
451
+ title
452
+ </th>
453
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
454
+ description
455
+ </th>
456
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
457
+ completed
458
+ </th>
459
+ <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
460
+ Actions
461
+ </th>
462
+ </tr>
463
+ </thead>
464
+ <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
465
+ {todos.map((item) => (
466
+ <tr key={item.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
467
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
468
+ {String(item.title)}
469
+ </td>
470
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
471
+ {String(item.description)}
472
+ </td>
473
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
474
+ {String(item.completed)}
475
+ </td>
476
+ <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
477
+ <Link
478
+ href={\`/todo/\${item.id}\`}
479
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 mr-4"
480
+ >
481
+ View
482
+ </Link>
483
+ <Link
484
+ href={\`/todo/\${item.id}/edit\`}
485
+ className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 mr-4"
486
+ >
487
+ Edit
488
+ </Link>
489
+ <button
490
+ onClick={() => handleDelete(item.id)}
491
+ className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
492
+ >
493
+ Delete
494
+ </button>
495
+ </td>
496
+ </tr>
497
+ ))}
498
+ </tbody>
499
+ </table>
500
+ </div>
501
+ )}
502
+ </div>
503
+ );
504
+ }
505
+ "
506
+ `;
507
+
508
+ exports[`generateNewPage generates new page (snapshot) 1`] = `
509
+ "import TodoForm from '../_components/TodoForm';
510
+
511
+ export default function NewTodoPage() {
512
+ return (
513
+ <div className="container mx-auto px-4 py-8">
514
+ <div className="max-w-2xl mx-auto">
515
+ <h1 className="text-3xl font-bold mb-6">Create New Todo</h1>
516
+ <div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
517
+ <TodoForm />
518
+ </div>
519
+ </div>
520
+ </div>
521
+ );
522
+ }
523
+ "
524
+ `;
@@ -0,0 +1,122 @@
1
+ import { validateConfig, loadConfigFromEnv } from "../core/config";
2
+ import { SwallowKitConfig } from "../types";
3
+
4
+ describe("validateConfig", () => {
5
+ it("returns valid for a complete config", () => {
6
+ const config: SwallowKitConfig = {
7
+ database: { connectionString: "AccountEndpoint=https://..." },
8
+ backend: { language: "typescript" },
9
+ api: { endpoint: "/api/_swallowkit" },
10
+ };
11
+ const result = validateConfig(config);
12
+ expect(result.valid).toBe(true);
13
+ expect(result.errors).toHaveLength(0);
14
+ });
15
+
16
+ it("returns error when connectionString is missing", () => {
17
+ const config: SwallowKitConfig = {
18
+ database: {},
19
+ backend: { language: "typescript" },
20
+ api: { endpoint: "/api/_swallowkit" },
21
+ };
22
+ const result = validateConfig(config);
23
+ expect(result.valid).toBe(false);
24
+ expect(result.errors).toContain("Cosmos DB connection string is required");
25
+ });
26
+
27
+ it("returns error when endpoint does not start with /", () => {
28
+ const config: SwallowKitConfig = {
29
+ database: { connectionString: "AccountEndpoint=https://..." },
30
+ backend: { language: "typescript" },
31
+ api: { endpoint: "api/_swallowkit" },
32
+ };
33
+ const result = validateConfig(config);
34
+ expect(result.valid).toBe(false);
35
+ expect(result.errors).toContain("API endpoint must start with '/'");
36
+ });
37
+
38
+ it("returns multiple errors for multiple issues", () => {
39
+ const config: SwallowKitConfig = {
40
+ database: {},
41
+ backend: { language: "typescript" },
42
+ api: { endpoint: "bad-endpoint" },
43
+ };
44
+ const result = validateConfig(config);
45
+ expect(result.valid).toBe(false);
46
+ expect(result.errors).toHaveLength(2);
47
+ });
48
+
49
+ it("returns error when backend language is invalid", () => {
50
+ const config: SwallowKitConfig = {
51
+ backend: { language: "ruby" as never },
52
+ };
53
+ const result = validateConfig(config);
54
+ expect(result.valid).toBe(false);
55
+ expect(result.errors).toContain("Backend language must be one of: typescript, csharp, python");
56
+ });
57
+
58
+ it("accepts config without database or api (no validation errors for absent sections)", () => {
59
+ const config: SwallowKitConfig = {};
60
+ const result = validateConfig(config);
61
+ // database is undefined → no connectionString check triggered
62
+ expect(result.errors.filter((e) => e.includes("endpoint"))).toHaveLength(0);
63
+ });
64
+ });
65
+
66
+ describe("loadConfigFromEnv", () => {
67
+ const originalEnv = process.env;
68
+
69
+ beforeEach(() => {
70
+ process.env = { ...originalEnv };
71
+ });
72
+
73
+ afterAll(() => {
74
+ process.env = originalEnv;
75
+ });
76
+
77
+ it("returns empty config when no env vars set", () => {
78
+ delete process.env.SWALLOWKIT_DB_CONNECTION_STRING;
79
+ delete process.env.SWALLOWKIT_DB_NAME;
80
+ delete process.env.SWALLOWKIT_API_ENDPOINT;
81
+
82
+ const config = loadConfigFromEnv();
83
+ expect(config.database).toBeUndefined();
84
+ expect(config.api).toBeUndefined();
85
+ });
86
+
87
+ it("reads database connection string from env", () => {
88
+ process.env.SWALLOWKIT_DB_CONNECTION_STRING = "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=xxx;";
89
+ const config = loadConfigFromEnv();
90
+ expect(config.database?.connectionString).toBe(
91
+ "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=xxx;"
92
+ );
93
+ });
94
+
95
+ it("reads database name from env", () => {
96
+ process.env.SWALLOWKIT_DB_NAME = "MyTestDB";
97
+ const config = loadConfigFromEnv();
98
+ expect(config.database?.databaseName).toBe("MyTestDB");
99
+ });
100
+
101
+ it("reads API endpoint from env", () => {
102
+ process.env.SWALLOWKIT_API_ENDPOINT = "/api/custom";
103
+ const config = loadConfigFromEnv();
104
+ expect(config.api?.endpoint).toBe("/api/custom");
105
+ });
106
+
107
+ it("reads backend language from env", () => {
108
+ process.env.SWALLOWKIT_BACKEND_LANGUAGE = "python";
109
+ const config = loadConfigFromEnv();
110
+ expect(config.backend?.language).toBe("python");
111
+ });
112
+
113
+ it("reads all env vars together", () => {
114
+ process.env.SWALLOWKIT_DB_CONNECTION_STRING = "conn";
115
+ process.env.SWALLOWKIT_DB_NAME = "db";
116
+ process.env.SWALLOWKIT_API_ENDPOINT = "/api/v2";
117
+ const config = loadConfigFromEnv();
118
+ expect(config.database?.connectionString).toBe("conn");
119
+ expect(config.database?.databaseName).toBe("db");
120
+ expect(config.api?.endpoint).toBe("/api/v2");
121
+ });
122
+ });
@@ -0,0 +1,42 @@
1
+ import * as path from "path";
2
+ import {
3
+ buildFunctionsStartArgs,
4
+ buildNextDevArgs,
5
+ buildPythonFunctionsEnv,
6
+ getPythonVirtualEnvPaths,
7
+ } from "../cli/commands/dev";
8
+
9
+ describe("dev command helpers", () => {
10
+ it("passes the requested port to Azure Functions Core Tools", () => {
11
+ expect(buildFunctionsStartArgs("7076")).toEqual(["start", "--port", "7076"]);
12
+ });
13
+
14
+ it("uses webpack mode for npm-based Next.js dev", () => {
15
+ expect(buildNextDevArgs("npm", "3012")).toEqual(["next", "dev", "--port", "3012", "--webpack"]);
16
+ });
17
+
18
+ it("uses pnpm exec for pnpm-based Next.js dev", () => {
19
+ expect(buildNextDevArgs("pnpm", "3012")).toEqual(["exec", "next", "dev", "--port", "3012", "--webpack"]);
20
+ });
21
+
22
+ it("builds Python virtual environment paths under functions/.venv", () => {
23
+ const functionsDir = path.join("C:\\repo", "functions");
24
+ const paths = getPythonVirtualEnvPaths(functionsDir);
25
+
26
+ expect(paths.venvDir).toBe(path.join(functionsDir, ".venv"));
27
+ expect(paths.binDir.endsWith(process.platform === "win32" ? path.join(".venv", "Scripts") : path.join(".venv", "bin"))).toBe(true);
28
+ expect(paths.pythonExecutable.endsWith(process.platform === "win32" ? path.join("Scripts", "python.exe") : path.join("bin", "python"))).toBe(true);
29
+ });
30
+
31
+ it("injects Python virtual environment settings for Functions", () => {
32
+ const functionsDir = path.join("C:\\repo", "functions");
33
+ const env = buildPythonFunctionsEnv({ PATH: "C:\\Windows\\System32" }, functionsDir);
34
+ const expectedBinDir = getPythonVirtualEnvPaths(functionsDir).binDir;
35
+
36
+ expect(env.VIRTUAL_ENV).toBe(path.join(functionsDir, ".venv"));
37
+ expect(env.languageWorkers__python__defaultExecutablePath).toBe(
38
+ getPythonVirtualEnvPaths(functionsDir).pythonExecutable
39
+ );
40
+ expect(env.PATH?.startsWith(`${expectedBinDir}${path.delimiter}`)).toBe(true);
41
+ });
42
+ });