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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mantiq",
3
- "version": "0.5.18",
3
+ "version": "0.5.20",
4
4
  "description": "Scaffold a new MantiqJS application",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
+ })
@@ -0,0 +1,11 @@
1
+ import { describe, test, expect } from 'bun:test'
2
+
3
+ describe('Example', () => {
4
+ test('true is truthy', () => {
5
+ expect(true).toBe(true)
6
+ })
7
+
8
+ test('math works', () => {
9
+ expect(1 + 1).toBe(2)
10
+ })
11
+ })
package/src/index.ts CHANGED
@@ -167,15 +167,24 @@ if (kit) {
167
167
  }
168
168
  }
169
169
 
170
- // Shared stubs (routes, controllers, seeder — overwrites skeleton routes)
170
+ // Shared stubs (routes, controllers, config — overwrites skeleton versions)
171
171
  if (sharedManifest?.files) {
172
- const mainEntry = kitManifest?.mainEntry ?? 'src/main.ts'
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
- content = content.replace(/\{\{MAIN_ENTRY\}\}/g, mainEntry)
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
+ })
@@ -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,8 @@
1
+ export default {
2
+ reactRefresh: {{REACT_REFRESH}},
3
+
4
+ ssr: {
5
+ entry: '{{SSR_ENTRY}}',
6
+ bundle: 'bootstrap/ssr/ssr.js',
7
+ },
8
+ }
@@ -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
+ })