create-mantiq 0.5.18 → 0.5.20
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/package.json +1 -1
- package/skeleton/config/vite.ts +33 -0
- package/skeleton/tests/feature/api.test.ts +14 -0
- package/skeleton/tests/feature/home.test.ts +17 -0
- package/skeleton/tests/unit/example.test.ts +11 -0
- package/src/index.ts +13 -3
- package/src/templates.ts +2 -0
- package/stubs/api-only/tests/feature/token-auth.test.ts.stub +69 -0
- package/stubs/manifest.json +22 -0
- package/stubs/shared/config/vite.ts.stub +8 -0
- package/stubs/shared/tests/feature/auth.test.ts.stub +69 -0
- package/stubs/shared/tests/feature/users.test.ts.stub +90 -0
package/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
| React Fast Refresh
|
|
6
|
+
|--------------------------------------------------------------------------
|
|
7
|
+
|
|
|
8
|
+
| Injects the React Refresh preamble into dev mode HTML so that HMR
|
|
9
|
+
| works correctly. Required for React — without it, components throw
|
|
10
|
+
| "can't detect preamble" and the page renders blank.
|
|
11
|
+
|
|
|
12
|
+
| Set to false for Vue/Svelte (they don't need this preamble).
|
|
13
|
+
|
|
|
14
|
+
*/
|
|
15
|
+
reactRefresh: false,
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
|--------------------------------------------------------------------------
|
|
19
|
+
| Server-Side Rendering
|
|
20
|
+
|--------------------------------------------------------------------------
|
|
21
|
+
|
|
|
22
|
+
| SSR renders pages on the server so users see content immediately
|
|
23
|
+
| instead of a blank shell. The entry file exports a render() function.
|
|
24
|
+
|
|
|
25
|
+
| In dev: Vite's ssrLoadModule() loads the entry with HMR.
|
|
26
|
+
| In prod: the pre-built bundle at bootstrap/ssr/ is used.
|
|
27
|
+
|
|
|
28
|
+
*/
|
|
29
|
+
// ssr: {
|
|
30
|
+
// entry: 'src/ssr.tsx',
|
|
31
|
+
// bundle: 'bootstrap/ssr/ssr.js',
|
|
32
|
+
// },
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, test } from 'bun:test'
|
|
2
|
+
import { TestCase } from '@mantiq/testing'
|
|
3
|
+
|
|
4
|
+
const t = new TestCase()
|
|
5
|
+
t.setup()
|
|
6
|
+
|
|
7
|
+
describe('API', () => {
|
|
8
|
+
test('GET /api/ping returns ok', async () => {
|
|
9
|
+
const res = await t.client.get('/api/ping')
|
|
10
|
+
res.assertOk()
|
|
11
|
+
await res.assertJson({ status: 'ok' })
|
|
12
|
+
await res.assertJsonHasKey('timestamp')
|
|
13
|
+
})
|
|
14
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, test } from 'bun:test'
|
|
2
|
+
import { TestCase } from '@mantiq/testing'
|
|
3
|
+
|
|
4
|
+
const t = new TestCase()
|
|
5
|
+
t.setup()
|
|
6
|
+
|
|
7
|
+
describe('Home', () => {
|
|
8
|
+
test('GET / returns 200', async () => {
|
|
9
|
+
const res = await t.client.get('/')
|
|
10
|
+
res.assertOk()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('GET / returns HTML', async () => {
|
|
14
|
+
const res = await t.client.get('/')
|
|
15
|
+
res.assertHeader('content-type')
|
|
16
|
+
})
|
|
17
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -167,15 +167,24 @@ if (kit) {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
// Shared stubs (routes, controllers,
|
|
170
|
+
// Shared stubs (routes, controllers, config — overwrites skeleton versions)
|
|
171
171
|
if (sharedManifest?.files) {
|
|
172
|
-
|
|
172
|
+
// Build placeholder map from manifest
|
|
173
|
+
const placeholders: Record<string, string> = {}
|
|
174
|
+
if (sharedManifest.placeholders) {
|
|
175
|
+
for (const [key, values] of Object.entries(sharedManifest.placeholders)) {
|
|
176
|
+
placeholders[key] = (values as Record<string, string>)[kit!] ?? ''
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
for (const { stub, target } of sharedManifest.files) {
|
|
174
181
|
const src = resolve(stubsDir, 'shared', stub)
|
|
175
182
|
const dest = resolve(projectDir, target)
|
|
176
183
|
mkdirSync(dirname(dest), { recursive: true })
|
|
177
184
|
let content = await Bun.file(src).text()
|
|
178
|
-
|
|
185
|
+
for (const [key, value] of Object.entries(placeholders)) {
|
|
186
|
+
content = content.replaceAll(key, value)
|
|
187
|
+
}
|
|
179
188
|
await Bun.write(dest, content)
|
|
180
189
|
fileCount++
|
|
181
190
|
}
|
|
@@ -194,6 +203,7 @@ if (kit) {
|
|
|
194
203
|
{ stub: 'shared/app/Http/Requests/UpdateUserRequest.ts.stub', target: 'app/Http/Requests/UpdateUserRequest.ts' },
|
|
195
204
|
{ stub: 'shared/database/seeders/DatabaseSeeder.ts.stub', target: 'database/seeders/DatabaseSeeder.ts' },
|
|
196
205
|
{ stub: 'shared/database/factories/UserFactory.ts.stub', target: 'database/factories/UserFactory.ts' },
|
|
206
|
+
{ stub: 'api-only/tests/feature/token-auth.test.ts.stub', target: 'tests/feature/token-auth.test.ts' },
|
|
197
207
|
]
|
|
198
208
|
for (const { stub, target } of apiOnlyFiles) {
|
|
199
209
|
const src = resolve(stubsDir, stub)
|
package/src/templates.ts
CHANGED
|
@@ -40,6 +40,7 @@ export function getTemplates(ctx: TemplateContext): Record<string, string> {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const baseDevDeps: Record<string, string> = {
|
|
43
|
+
'@mantiq/testing': '^0.5.0',
|
|
43
44
|
'bun-types': 'latest',
|
|
44
45
|
'typescript': '^5.7.0',
|
|
45
46
|
}
|
|
@@ -48,6 +49,7 @@ export function getTemplates(ctx: TemplateContext): Record<string, string> {
|
|
|
48
49
|
dev: 'bun run --watch index.ts',
|
|
49
50
|
start: 'bun run index.ts',
|
|
50
51
|
mantiq: 'bun run mantiq.ts',
|
|
52
|
+
test: 'bun test tests/',
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
if (ctx.kit) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { TestCase } from '@mantiq/testing'
|
|
3
|
+
|
|
4
|
+
const t = new TestCase()
|
|
5
|
+
t.refreshDatabase = true
|
|
6
|
+
t.setup()
|
|
7
|
+
|
|
8
|
+
const user = {
|
|
9
|
+
name: 'API User',
|
|
10
|
+
email: 'api@example.com',
|
|
11
|
+
password: 'password123',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('Token Authentication', () => {
|
|
15
|
+
test('can register and receive a token', async () => {
|
|
16
|
+
const res = await t.client.post('/api/register', user)
|
|
17
|
+
res.assertCreated()
|
|
18
|
+
await res.assertJsonHasKey('token')
|
|
19
|
+
const data = await res.json()
|
|
20
|
+
expect(data.token).toContain('|')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('can login and receive a token', async () => {
|
|
24
|
+
await t.client.post('/api/register', user)
|
|
25
|
+
const res = await t.client.post('/api/login', {
|
|
26
|
+
email: user.email,
|
|
27
|
+
password: user.password,
|
|
28
|
+
})
|
|
29
|
+
res.assertOk()
|
|
30
|
+
await res.assertJsonHasKey('token')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('cannot login with wrong credentials', async () => {
|
|
34
|
+
await t.client.post('/api/register', user)
|
|
35
|
+
const res = await t.client.post('/api/login', {
|
|
36
|
+
email: user.email,
|
|
37
|
+
password: 'wrong',
|
|
38
|
+
})
|
|
39
|
+
res.assertUnauthorized()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('can access protected route with bearer token', async () => {
|
|
43
|
+
const regRes = await t.client.post('/api/register', user)
|
|
44
|
+
const { token } = await regRes.json()
|
|
45
|
+
|
|
46
|
+
t.client.withToken(token)
|
|
47
|
+
const res = await t.client.get('/api/user')
|
|
48
|
+
res.assertOk()
|
|
49
|
+
await res.assertJsonPath('user.email', user.email)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('cannot access protected route without token', async () => {
|
|
53
|
+
const res = await t.client.get('/api/user')
|
|
54
|
+
res.assertUnauthorized()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('can logout (revoke token)', async () => {
|
|
58
|
+
const regRes = await t.client.post('/api/register', user)
|
|
59
|
+
const { token } = await regRes.json()
|
|
60
|
+
|
|
61
|
+
t.client.withToken(token)
|
|
62
|
+
const logoutRes = await t.client.post('/api/logout')
|
|
63
|
+
logoutRes.assertOk()
|
|
64
|
+
|
|
65
|
+
// Token should be revoked
|
|
66
|
+
const userRes = await t.client.get('/api/user')
|
|
67
|
+
userRes.assertUnauthorized()
|
|
68
|
+
})
|
|
69
|
+
})
|
package/stubs/manifest.json
CHANGED
|
@@ -1426,9 +1426,31 @@
|
|
|
1426
1426
|
{
|
|
1427
1427
|
"stub": "config/app.ts.stub",
|
|
1428
1428
|
"target": "config/app.ts"
|
|
1429
|
+
},
|
|
1430
|
+
{
|
|
1431
|
+
"stub": "config/vite.ts.stub",
|
|
1432
|
+
"target": "config/vite.ts"
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
"stub": "tests/feature/auth.test.ts.stub",
|
|
1436
|
+
"target": "tests/feature/auth.test.ts"
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
"stub": "tests/feature/users.test.ts.stub",
|
|
1440
|
+
"target": "tests/feature/users.test.ts"
|
|
1429
1441
|
}
|
|
1430
1442
|
],
|
|
1431
1443
|
"placeholders": {
|
|
1444
|
+
"{{REACT_REFRESH}}": {
|
|
1445
|
+
"react": "true",
|
|
1446
|
+
"vue": "false",
|
|
1447
|
+
"svelte": "false"
|
|
1448
|
+
},
|
|
1449
|
+
"{{SSR_ENTRY}}": {
|
|
1450
|
+
"react": "src/ssr.tsx",
|
|
1451
|
+
"vue": "src/ssr.ts",
|
|
1452
|
+
"svelte": "src/ssr.ts"
|
|
1453
|
+
},
|
|
1432
1454
|
"{{MAIN_ENTRY}}": {
|
|
1433
1455
|
"react": "src/main.tsx",
|
|
1434
1456
|
"vue": "src/main.ts",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, test, beforeAll } from 'bun:test'
|
|
2
|
+
import { TestCase } from '@mantiq/testing'
|
|
3
|
+
|
|
4
|
+
const t = new TestCase()
|
|
5
|
+
t.refreshDatabase = true
|
|
6
|
+
t.setup()
|
|
7
|
+
|
|
8
|
+
const user = {
|
|
9
|
+
name: 'Test User',
|
|
10
|
+
email: 'test@example.com',
|
|
11
|
+
password: 'password123',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('Authentication', () => {
|
|
15
|
+
test('can register a new user', async () => {
|
|
16
|
+
await t.client.initSession()
|
|
17
|
+
const res = await t.client.post('/register', user)
|
|
18
|
+
res.assertCreated()
|
|
19
|
+
await res.assertJson({ message: 'Registered.' })
|
|
20
|
+
await res.assertJsonMissingKey('password')
|
|
21
|
+
await t.assertDatabaseHas('users', { email: user.email })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('cannot register with duplicate email', async () => {
|
|
25
|
+
await t.client.initSession()
|
|
26
|
+
await t.client.post('/register', user)
|
|
27
|
+
const res = await t.client.post('/register', user)
|
|
28
|
+
res.assertUnprocessable()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('can login with valid credentials', async () => {
|
|
32
|
+
await t.client.initSession()
|
|
33
|
+
await t.client.post('/register', user)
|
|
34
|
+
t.client.flushCookies()
|
|
35
|
+
|
|
36
|
+
await t.client.initSession()
|
|
37
|
+
const res = await t.client.post('/login', {
|
|
38
|
+
email: user.email,
|
|
39
|
+
password: user.password,
|
|
40
|
+
})
|
|
41
|
+
res.assertOk()
|
|
42
|
+
await res.assertJson({ message: 'Logged in.' })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('cannot login with wrong password', async () => {
|
|
46
|
+
await t.client.initSession()
|
|
47
|
+
await t.client.post('/register', user)
|
|
48
|
+
t.client.flushCookies()
|
|
49
|
+
|
|
50
|
+
await t.client.initSession()
|
|
51
|
+
const res = await t.client.post('/login', {
|
|
52
|
+
email: user.email,
|
|
53
|
+
password: 'wrong',
|
|
54
|
+
})
|
|
55
|
+
res.assertUnauthorized()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('can logout', async () => {
|
|
59
|
+
await t.client.initSession()
|
|
60
|
+
await t.client.post('/register', user)
|
|
61
|
+
const logoutRes = await t.client.post('/logout')
|
|
62
|
+
logoutRes.assertOk()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('protected routes require authentication', async () => {
|
|
66
|
+
const res = await t.client.get('/api/users')
|
|
67
|
+
res.assertUnauthorized()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, test } from 'bun:test'
|
|
2
|
+
import { TestCase } from '@mantiq/testing'
|
|
3
|
+
|
|
4
|
+
const t = new TestCase()
|
|
5
|
+
t.refreshDatabase = true
|
|
6
|
+
t.setup()
|
|
7
|
+
|
|
8
|
+
const admin = {
|
|
9
|
+
name: 'Admin',
|
|
10
|
+
email: 'admin@example.com',
|
|
11
|
+
password: 'password123',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Register and login before CRUD tests. */
|
|
15
|
+
async function login() {
|
|
16
|
+
await t.client.initSession()
|
|
17
|
+
await t.client.post('/register', admin)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('Users CRUD', () => {
|
|
21
|
+
test('can list users', async () => {
|
|
22
|
+
await login()
|
|
23
|
+
const res = await t.client.get('/api/users')
|
|
24
|
+
res.assertOk()
|
|
25
|
+
await res.assertJsonHasKey('data', 'meta')
|
|
26
|
+
await res.assertJsonPath('meta.page', 1)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('can create a user', async () => {
|
|
30
|
+
await login()
|
|
31
|
+
const res = await t.client.post('/api/users', {
|
|
32
|
+
name: 'New User',
|
|
33
|
+
email: 'new@example.com',
|
|
34
|
+
password: 'secret123',
|
|
35
|
+
})
|
|
36
|
+
res.assertCreated()
|
|
37
|
+
await res.assertJsonPath('data.name', 'New User')
|
|
38
|
+
await res.assertJsonPath('data.email', 'new@example.com')
|
|
39
|
+
await t.assertDatabaseHas('users', { email: 'new@example.com' })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('cannot create user with missing fields', async () => {
|
|
43
|
+
await login()
|
|
44
|
+
const res = await t.client.post('/api/users', { name: 'No Email' })
|
|
45
|
+
res.assertUnprocessable()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('can update a user', async () => {
|
|
49
|
+
await login()
|
|
50
|
+
const createRes = await t.client.post('/api/users', {
|
|
51
|
+
name: 'Update Me',
|
|
52
|
+
email: 'update@example.com',
|
|
53
|
+
password: 'secret123',
|
|
54
|
+
})
|
|
55
|
+
const userId = (await createRes.json()).data.id
|
|
56
|
+
|
|
57
|
+
const res = await t.client.put(`/api/users/${userId}`, { name: 'Updated' })
|
|
58
|
+
res.assertOk()
|
|
59
|
+
await res.assertJsonPath('data.name', 'Updated')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('can delete a user', async () => {
|
|
63
|
+
await login()
|
|
64
|
+
const createRes = await t.client.post('/api/users', {
|
|
65
|
+
name: 'Delete Me',
|
|
66
|
+
email: 'delete@example.com',
|
|
67
|
+
password: 'secret123',
|
|
68
|
+
})
|
|
69
|
+
const userId = (await createRes.json()).data.id
|
|
70
|
+
|
|
71
|
+
const res = await t.client.delete(`/api/users/${userId}`)
|
|
72
|
+
res.assertOk()
|
|
73
|
+
await t.assertDatabaseMissing('users', { email: 'delete@example.com' })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('can search users', async () => {
|
|
77
|
+
await login()
|
|
78
|
+
const res = await t.client.get('/api/users?search=Admin')
|
|
79
|
+
res.assertOk()
|
|
80
|
+
const data = await res.json()
|
|
81
|
+
expect(data.data.length).toBeGreaterThan(0)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('can paginate users', async () => {
|
|
85
|
+
await login()
|
|
86
|
+
const res = await t.client.get('/api/users?page=1&per_page=1')
|
|
87
|
+
res.assertOk()
|
|
88
|
+
await res.assertJsonPath('meta.per_page', 1)
|
|
89
|
+
})
|
|
90
|
+
})
|