@basicbenframework/core 0.1.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.
Files changed (186) hide show
  1. package/.github/workflows/publish.yml +35 -0
  2. package/README.md +588 -0
  3. package/bin/cli.js +8 -0
  4. package/create-basicben-app/index.js +205 -0
  5. package/create-basicben-app/package.json +30 -0
  6. package/create-basicben-app/template/.env.example +24 -0
  7. package/create-basicben-app/template/README.md +59 -0
  8. package/create-basicben-app/template/basicben.config.js +33 -0
  9. package/create-basicben-app/template/index.html +54 -0
  10. package/create-basicben-app/template/migrations/001_create_users.js +15 -0
  11. package/create-basicben-app/template/migrations/002_create_posts.js +18 -0
  12. package/create-basicben-app/template/public/.gitkeep +0 -0
  13. package/create-basicben-app/template/seeds/01_users.js +29 -0
  14. package/create-basicben-app/template/seeds/02_posts.js +43 -0
  15. package/create-basicben-app/template/src/client/components/Alert.jsx +11 -0
  16. package/create-basicben-app/template/src/client/components/Avatar.jsx +11 -0
  17. package/create-basicben-app/template/src/client/components/BackLink.jsx +10 -0
  18. package/create-basicben-app/template/src/client/components/Button.jsx +19 -0
  19. package/create-basicben-app/template/src/client/components/Card.jsx +10 -0
  20. package/create-basicben-app/template/src/client/components/Empty.jsx +6 -0
  21. package/create-basicben-app/template/src/client/components/Input.jsx +12 -0
  22. package/create-basicben-app/template/src/client/components/Loading.jsx +6 -0
  23. package/create-basicben-app/template/src/client/components/Logo.jsx +40 -0
  24. package/create-basicben-app/template/src/client/components/Nav/DarkModeToggle.jsx +23 -0
  25. package/create-basicben-app/template/src/client/components/Nav/DesktopNav.jsx +32 -0
  26. package/create-basicben-app/template/src/client/components/Nav/MobileNav.jsx +107 -0
  27. package/create-basicben-app/template/src/client/components/NavLink.jsx +10 -0
  28. package/create-basicben-app/template/src/client/components/PageHeader.jsx +8 -0
  29. package/create-basicben-app/template/src/client/components/PostCard.jsx +19 -0
  30. package/create-basicben-app/template/src/client/components/Textarea.jsx +12 -0
  31. package/create-basicben-app/template/src/client/components/ThemeContext.jsx +5 -0
  32. package/create-basicben-app/template/src/client/contexts/ToastContext.jsx +94 -0
  33. package/create-basicben-app/template/src/client/layouts/AppLayout.jsx +60 -0
  34. package/create-basicben-app/template/src/client/layouts/AuthLayout.jsx +33 -0
  35. package/create-basicben-app/template/src/client/layouts/DocsLayout.jsx +60 -0
  36. package/create-basicben-app/template/src/client/layouts/RootLayout.jsx +25 -0
  37. package/create-basicben-app/template/src/client/pages/Auth.jsx +55 -0
  38. package/create-basicben-app/template/src/client/pages/Authentication.jsx +236 -0
  39. package/create-basicben-app/template/src/client/pages/Database.jsx +426 -0
  40. package/create-basicben-app/template/src/client/pages/Feed.jsx +34 -0
  41. package/create-basicben-app/template/src/client/pages/FeedPost.jsx +37 -0
  42. package/create-basicben-app/template/src/client/pages/GettingStarted.jsx +136 -0
  43. package/create-basicben-app/template/src/client/pages/Home.jsx +206 -0
  44. package/create-basicben-app/template/src/client/pages/PostForm.jsx +69 -0
  45. package/create-basicben-app/template/src/client/pages/Posts.jsx +59 -0
  46. package/create-basicben-app/template/src/client/pages/Profile.jsx +68 -0
  47. package/create-basicben-app/template/src/client/pages/Routing.jsx +207 -0
  48. package/create-basicben-app/template/src/client/pages/Testing.jsx +251 -0
  49. package/create-basicben-app/template/src/client/pages/Validation.jsx +210 -0
  50. package/create-basicben-app/template/src/controllers/AuthController.js +81 -0
  51. package/create-basicben-app/template/src/controllers/HomeController.js +17 -0
  52. package/create-basicben-app/template/src/controllers/PostController.js +86 -0
  53. package/create-basicben-app/template/src/controllers/ProfileController.js +66 -0
  54. package/create-basicben-app/template/src/helpers/api.js +24 -0
  55. package/create-basicben-app/template/src/main.jsx +9 -0
  56. package/create-basicben-app/template/src/middleware/auth.js +16 -0
  57. package/create-basicben-app/template/src/models/Post.js +63 -0
  58. package/create-basicben-app/template/src/models/User.js +42 -0
  59. package/create-basicben-app/template/src/routes/App.jsx +38 -0
  60. package/create-basicben-app/template/src/routes/api/auth.js +7 -0
  61. package/create-basicben-app/template/src/routes/api/posts.js +15 -0
  62. package/create-basicben-app/template/src/routes/api/profile.js +8 -0
  63. package/create-basicben-app/template/src/server/index.js +16 -0
  64. package/create-basicben-app/template/vite.config.js +18 -0
  65. package/database.sqlite +0 -0
  66. package/my-test-app/.env.example +24 -0
  67. package/my-test-app/README.md +59 -0
  68. package/my-test-app/basicben.config.js +33 -0
  69. package/my-test-app/database.sqlite-shm +0 -0
  70. package/my-test-app/database.sqlite-wal +0 -0
  71. package/my-test-app/index.html +54 -0
  72. package/my-test-app/migrations/001_create_users.js +15 -0
  73. package/my-test-app/migrations/002_create_posts.js +18 -0
  74. package/my-test-app/package-lock.json +2160 -0
  75. package/my-test-app/package.json +29 -0
  76. package/my-test-app/public/.gitkeep +0 -0
  77. package/my-test-app/seeds/01_users.js +29 -0
  78. package/my-test-app/seeds/02_posts.js +43 -0
  79. package/my-test-app/src/client/components/Alert.jsx +11 -0
  80. package/my-test-app/src/client/components/Avatar.jsx +11 -0
  81. package/my-test-app/src/client/components/BackLink.jsx +10 -0
  82. package/my-test-app/src/client/components/Button.jsx +19 -0
  83. package/my-test-app/src/client/components/Card.jsx +10 -0
  84. package/my-test-app/src/client/components/Empty.jsx +6 -0
  85. package/my-test-app/src/client/components/Input.jsx +12 -0
  86. package/my-test-app/src/client/components/Loading.jsx +6 -0
  87. package/my-test-app/src/client/components/Logo.jsx +40 -0
  88. package/my-test-app/src/client/components/Nav/DarkModeToggle.jsx +23 -0
  89. package/my-test-app/src/client/components/Nav/DesktopNav.jsx +32 -0
  90. package/my-test-app/src/client/components/Nav/MobileNav.jsx +107 -0
  91. package/my-test-app/src/client/components/NavLink.jsx +10 -0
  92. package/my-test-app/src/client/components/PageHeader.jsx +8 -0
  93. package/my-test-app/src/client/components/PostCard.jsx +19 -0
  94. package/my-test-app/src/client/components/Textarea.jsx +12 -0
  95. package/my-test-app/src/client/components/ThemeContext.jsx +5 -0
  96. package/my-test-app/src/client/contexts/AppContext.jsx +13 -0
  97. package/my-test-app/src/client/contexts/ToastContext.jsx +94 -0
  98. package/my-test-app/src/client/layouts/AppLayout.jsx +60 -0
  99. package/my-test-app/src/client/layouts/AuthLayout.jsx +33 -0
  100. package/my-test-app/src/client/layouts/DocsLayout.jsx +60 -0
  101. package/my-test-app/src/client/layouts/RootLayout.jsx +25 -0
  102. package/my-test-app/src/client/pages/Auth.jsx +55 -0
  103. package/my-test-app/src/client/pages/Authentication.jsx +236 -0
  104. package/my-test-app/src/client/pages/Database.jsx +426 -0
  105. package/my-test-app/src/client/pages/Feed.jsx +34 -0
  106. package/my-test-app/src/client/pages/FeedPost.jsx +37 -0
  107. package/my-test-app/src/client/pages/GettingStarted.jsx +136 -0
  108. package/my-test-app/src/client/pages/Home.jsx +206 -0
  109. package/my-test-app/src/client/pages/PostForm.jsx +69 -0
  110. package/my-test-app/src/client/pages/Posts.jsx +59 -0
  111. package/my-test-app/src/client/pages/Profile.jsx +68 -0
  112. package/my-test-app/src/client/pages/Routing.jsx +207 -0
  113. package/my-test-app/src/client/pages/Testing.jsx +251 -0
  114. package/my-test-app/src/client/pages/Validation.jsx +210 -0
  115. package/my-test-app/src/controllers/AuthController.js +81 -0
  116. package/my-test-app/src/controllers/HomeController.js +17 -0
  117. package/my-test-app/src/controllers/PostController.js +86 -0
  118. package/my-test-app/src/controllers/ProfileController.js +66 -0
  119. package/my-test-app/src/helpers/api.js +24 -0
  120. package/my-test-app/src/main.jsx +9 -0
  121. package/my-test-app/src/middleware/auth.js +16 -0
  122. package/my-test-app/src/models/Post.js +63 -0
  123. package/my-test-app/src/models/User.js +42 -0
  124. package/my-test-app/src/routes/App.jsx +38 -0
  125. package/my-test-app/src/routes/api/auth.js +7 -0
  126. package/my-test-app/src/routes/api/posts.js +15 -0
  127. package/my-test-app/src/routes/api/profile.js +8 -0
  128. package/my-test-app/src/server/index.js +16 -0
  129. package/my-test-app/vite.config.js +18 -0
  130. package/package.json +61 -0
  131. package/scripts/test-app.sh +59 -0
  132. package/src/auth/jwt.js +195 -0
  133. package/src/auth/password.js +132 -0
  134. package/src/cli/colors.js +31 -0
  135. package/src/cli/dispatcher.js +168 -0
  136. package/src/cli/parser.js +91 -0
  137. package/src/client/context.js +4 -0
  138. package/src/client/hooks.js +50 -0
  139. package/src/client/index.js +3 -0
  140. package/src/client/router.js +184 -0
  141. package/src/commands/build.js +155 -0
  142. package/src/commands/dev.js +206 -0
  143. package/src/commands/help.js +84 -0
  144. package/src/commands/make-controller.js +36 -0
  145. package/src/commands/make-middleware.js +44 -0
  146. package/src/commands/make-migration.js +51 -0
  147. package/src/commands/make-model.js +38 -0
  148. package/src/commands/make-route.js +36 -0
  149. package/src/commands/make-seed.js +38 -0
  150. package/src/commands/migrate-fresh.js +32 -0
  151. package/src/commands/migrate-rollback.js +30 -0
  152. package/src/commands/migrate-status.js +41 -0
  153. package/src/commands/migrate.js +30 -0
  154. package/src/commands/seed.js +47 -0
  155. package/src/commands/start.js +69 -0
  156. package/src/commands/test.js +46 -0
  157. package/src/db/Grammar.js +125 -0
  158. package/src/db/QueryBuilder.js +476 -0
  159. package/src/db/adapters/neon.js +170 -0
  160. package/src/db/adapters/planetscale.js +146 -0
  161. package/src/db/adapters/postgres.js +166 -0
  162. package/src/db/adapters/sqlite.js +125 -0
  163. package/src/db/adapters/turso.js +165 -0
  164. package/src/db/index.js +156 -0
  165. package/src/db/migrator.js +250 -0
  166. package/src/db/seeder.js +124 -0
  167. package/src/index.js +12 -0
  168. package/src/scaffolding/index.js +152 -0
  169. package/src/server/body-parser.js +159 -0
  170. package/src/server/cors.js +63 -0
  171. package/src/server/default-entry.js +13 -0
  172. package/src/server/http.js +221 -0
  173. package/src/server/index.js +168 -0
  174. package/src/server/loader.js +128 -0
  175. package/src/server/router.js +281 -0
  176. package/src/server/static.js +139 -0
  177. package/src/validation/index.js +436 -0
  178. package/src/vite/config.js +49 -0
  179. package/stubs/controller.stub +48 -0
  180. package/stubs/middleware-auth.stub +29 -0
  181. package/stubs/middleware.stub +9 -0
  182. package/stubs/migration.stub +17 -0
  183. package/stubs/model.stub +77 -0
  184. package/stubs/route.stub +13 -0
  185. package/stubs/seed.stub +16 -0
  186. package/stubs/vite.config.stub +18 -0
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Secure password hashing using node:crypto scrypt.
3
+ *
4
+ * Comparable to bcrypt/Argon2 used by Laravel and Next.js.
5
+ * - Deliberately slow (configurable cost)
6
+ * - Automatic random salt
7
+ * - Timing-safe comparison
8
+ */
9
+
10
+ import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto'
11
+ import { promisify } from 'node:util'
12
+
13
+ const scryptAsync = promisify(scrypt)
14
+
15
+ // Default parameters (similar to bcrypt cost factor 10)
16
+ const DEFAULT_KEY_LENGTH = 64
17
+ const DEFAULT_COST = 16384 // N - CPU/memory cost (2^14)
18
+ const DEFAULT_BLOCK_SIZE = 8 // r
19
+ const DEFAULT_PARALLELIZATION = 1 // p
20
+ const SALT_LENGTH = 16
21
+
22
+ /**
23
+ * Hash a password securely
24
+ *
25
+ * @param {string} password - Plain text password
26
+ * @param {Object} options - Optional parameters
27
+ * @returns {Promise<string>} - Hash in format: salt:hash:params
28
+ *
29
+ * @example
30
+ * const hash = await hashPassword('mysecretpassword')
31
+ * // Store hash in database
32
+ */
33
+ export async function hashPassword(password, options = {}) {
34
+ const {
35
+ keyLength = DEFAULT_KEY_LENGTH,
36
+ cost = DEFAULT_COST,
37
+ blockSize = DEFAULT_BLOCK_SIZE,
38
+ parallelization = DEFAULT_PARALLELIZATION
39
+ } = options
40
+
41
+ const salt = randomBytes(SALT_LENGTH)
42
+
43
+ const derivedKey = await scryptAsync(password, salt, keyLength, {
44
+ N: cost,
45
+ r: blockSize,
46
+ p: parallelization
47
+ })
48
+
49
+ // Format: base64(salt):base64(hash):N:r:p:keylen
50
+ const params = `${cost}:${blockSize}:${parallelization}:${keyLength}`
51
+ return `${salt.toString('base64')}:${derivedKey.toString('base64')}:${params}`
52
+ }
53
+
54
+ /**
55
+ * Verify a password against a hash
56
+ *
57
+ * @param {string} password - Plain text password to verify
58
+ * @param {string} hash - Stored hash from hashPassword()
59
+ * @returns {Promise<boolean>} - True if password matches
60
+ *
61
+ * @example
62
+ * const isValid = await verifyPassword('mysecretpassword', storedHash)
63
+ * if (!isValid) {
64
+ * // Invalid password
65
+ * }
66
+ */
67
+ export async function verifyPassword(password, hash) {
68
+ if (!password || !hash) {
69
+ return false
70
+ }
71
+
72
+ try {
73
+ const parts = hash.split(':')
74
+ if (parts.length !== 6) {
75
+ return false
76
+ }
77
+
78
+ const [saltB64, hashB64, costStr, blockSizeStr, parallelizationStr, keyLengthStr] = parts
79
+
80
+ const salt = Buffer.from(saltB64, 'base64')
81
+ const storedHash = Buffer.from(hashB64, 'base64')
82
+ const cost = parseInt(costStr, 10)
83
+ const blockSize = parseInt(blockSizeStr, 10)
84
+ const parallelization = parseInt(parallelizationStr, 10)
85
+ const keyLength = parseInt(keyLengthStr, 10)
86
+
87
+ const derivedKey = await scryptAsync(password, salt, keyLength, {
88
+ N: cost,
89
+ r: blockSize,
90
+ p: parallelization
91
+ })
92
+
93
+ // Timing-safe comparison to prevent timing attacks
94
+ return timingSafeEqual(storedHash, derivedKey)
95
+ } catch {
96
+ return false
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Check if a hash needs to be rehashed (e.g., cost factor increased)
102
+ *
103
+ * @param {string} hash - Stored hash
104
+ * @param {Object} options - Current desired parameters
105
+ * @returns {boolean} - True if hash should be regenerated
106
+ */
107
+ export function needsRehash(hash, options = {}) {
108
+ const {
109
+ cost = DEFAULT_COST,
110
+ blockSize = DEFAULT_BLOCK_SIZE,
111
+ parallelization = DEFAULT_PARALLELIZATION,
112
+ keyLength = DEFAULT_KEY_LENGTH
113
+ } = options
114
+
115
+ try {
116
+ const parts = hash.split(':')
117
+ if (parts.length !== 6) {
118
+ return true
119
+ }
120
+
121
+ const [, , costStr, blockSizeStr, parallelizationStr, keyLengthStr] = parts
122
+
123
+ return (
124
+ parseInt(costStr, 10) !== cost ||
125
+ parseInt(blockSizeStr, 10) !== blockSize ||
126
+ parseInt(parallelizationStr, 10) !== parallelization ||
127
+ parseInt(keyLengthStr, 10) !== keyLength
128
+ )
129
+ } catch {
130
+ return true
131
+ }
132
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Simple ANSI color helpers. No Chalk needed.
3
+ */
4
+
5
+ const isColorSupported = process.stdout.isTTY && !process.env.NO_COLOR
6
+
7
+ const code = (open, close) => {
8
+ if (!isColorSupported) return (str) => str
9
+ return (str) => `\x1b[${open}m${str}\x1b[${close}m`
10
+ }
11
+
12
+ export const bold = code(1, 22)
13
+ export const dim = code(2, 22)
14
+ export const italic = code(3, 23)
15
+ export const underline = code(4, 24)
16
+
17
+ export const red = code(31, 39)
18
+ export const green = code(32, 39)
19
+ export const yellow = code(33, 39)
20
+ export const blue = code(34, 39)
21
+ export const magenta = code(35, 39)
22
+ export const cyan = code(36, 39)
23
+ export const white = code(37, 39)
24
+ export const gray = code(90, 39)
25
+
26
+ // Semantic aliases
27
+ export const success = green
28
+ export const error = red
29
+ export const warn = yellow
30
+ export const info = cyan
31
+ export const muted = gray
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Command dispatcher. Maps command names to handlers.
3
+ */
4
+
5
+ import { bold, red, cyan, yellow, dim } from './colors.js'
6
+
7
+ // Command registry
8
+ const commands = {
9
+ // Development
10
+ dev: () => import('../commands/dev.js'),
11
+ build: () => import('../commands/build.js'),
12
+ start: () => import('../commands/start.js'),
13
+ test: () => import('../commands/test.js'),
14
+
15
+ // Scaffolding
16
+ 'make:controller': () => import('../commands/make-controller.js'),
17
+ 'make:model': () => import('../commands/make-model.js'),
18
+ 'make:route': () => import('../commands/make-route.js'),
19
+ 'make:migration': () => import('../commands/make-migration.js'),
20
+ 'make:middleware': () => import('../commands/make-middleware.js'),
21
+
22
+ // Database
23
+ migrate: () => import('../commands/migrate.js'),
24
+ 'migrate:rollback': () => import('../commands/migrate-rollback.js'),
25
+ 'migrate:fresh': () => import('../commands/migrate-fresh.js'),
26
+ 'migrate:status': () => import('../commands/migrate-status.js'),
27
+ 'migrate:make': () => import('../commands/make-migration.js'), // alias
28
+
29
+ // Seeding
30
+ seed: () => import('../commands/seed.js'),
31
+ 'db:seed': () => import('../commands/seed.js'), // alias
32
+ 'make:seed': () => import('../commands/make-seed.js'),
33
+
34
+ // Help
35
+ help: () => import('../commands/help.js')
36
+ }
37
+
38
+ // Command metadata for help display
39
+ export const commandMeta = {
40
+ dev: {
41
+ description: 'Start development server (Vite + Node with hot reload)',
42
+ usage: 'basicben dev',
43
+ options: {
44
+ '--port <port>': 'API server port (default: 3001)'
45
+ }
46
+ },
47
+ build: {
48
+ description: 'Build client and server for production',
49
+ usage: 'basicben build',
50
+ options: {
51
+ '--static': 'Build client only (for static hosts like CF Pages)'
52
+ }
53
+ },
54
+ start: {
55
+ description: 'Start production server',
56
+ usage: 'basicben start',
57
+ options: {
58
+ '--port <port>': 'Server port (default: 3000)'
59
+ }
60
+ },
61
+ test: {
62
+ description: 'Run tests with Vitest',
63
+ usage: 'basicben test [files]',
64
+ options: {
65
+ '--watch, -w': 'Watch mode (re-run on changes)',
66
+ '--coverage': 'Generate coverage report',
67
+ '--ui': 'Open Vitest UI'
68
+ }
69
+ },
70
+
71
+ 'make:controller': {
72
+ description: 'Generate a controller with CRUD methods',
73
+ usage: 'basicben make:controller <Name>',
74
+ example: 'basicben make:controller User'
75
+ },
76
+ 'make:model': {
77
+ description: 'Generate a model with common database methods',
78
+ usage: 'basicben make:model <Name>',
79
+ example: 'basicben make:model User'
80
+ },
81
+ 'make:route': {
82
+ description: 'Generate a route file with REST endpoints',
83
+ usage: 'basicben make:route <name>',
84
+ example: 'basicben make:route users'
85
+ },
86
+ 'make:migration': {
87
+ description: 'Generate a timestamped migration file',
88
+ usage: 'basicben make:migration <name>',
89
+ example: 'basicben make:migration create_users_table'
90
+ },
91
+ 'make:middleware': {
92
+ description: 'Generate middleware (includes auth template)',
93
+ usage: 'basicben make:middleware <name>',
94
+ example: 'basicben make:middleware auth'
95
+ },
96
+
97
+ migrate: {
98
+ description: 'Run all pending migrations',
99
+ usage: 'basicben migrate'
100
+ },
101
+ 'migrate:rollback': {
102
+ description: 'Roll back the last migration batch',
103
+ usage: 'basicben migrate:rollback'
104
+ },
105
+ 'migrate:fresh': {
106
+ description: 'Drop all tables and re-run all migrations',
107
+ usage: 'basicben migrate:fresh'
108
+ },
109
+ 'migrate:status': {
110
+ description: 'Show which migrations have run',
111
+ usage: 'basicben migrate:status'
112
+ },
113
+
114
+ seed: {
115
+ description: 'Run database seeders',
116
+ usage: 'basicben seed [name]',
117
+ example: 'basicben seed users'
118
+ },
119
+ 'make:seed': {
120
+ description: 'Generate a new seed file',
121
+ usage: 'basicben make:seed <name>',
122
+ example: 'basicben make:seed users'
123
+ },
124
+
125
+ help: {
126
+ description: 'Show help for a command',
127
+ usage: 'basicben help [command]'
128
+ }
129
+ }
130
+
131
+ export async function dispatch(command, args, flags) {
132
+ // Version flag (check first)
133
+ if (flags.version || flags.v) {
134
+ const pkg = await import('../../package.json', { with: { type: 'json' } })
135
+ console.log(pkg.default.version)
136
+ return
137
+ }
138
+
139
+ // No command or help flag
140
+ if (!command || flags.help || flags.h) {
141
+ const helpModule = await commands.help()
142
+ return helpModule.run(args, flags)
143
+ }
144
+
145
+ // Look up command
146
+ const loader = commands[command]
147
+
148
+ if (!loader) {
149
+ console.error(`\n${red('Error:')} Unknown command ${bold(command)}`)
150
+ console.error(`\nRun ${cyan('basicben help')} to see available commands.\n`)
151
+ process.exit(1)
152
+ }
153
+
154
+ try {
155
+ const module = await loader()
156
+ await module.run(args, flags)
157
+ } catch (err) {
158
+ // Handle module not found (command not implemented yet)
159
+ if (err.code === 'ERR_MODULE_NOT_FOUND') {
160
+ console.error(`\n${yellow('Not implemented:')} ${bold(command)}`)
161
+ console.error(`${dim('This command is coming soon.')}\n`)
162
+ process.exit(1)
163
+ }
164
+
165
+ // Re-throw other errors
166
+ throw err
167
+ }
168
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Custom argument parser. No Commander.js needed.
3
+ *
4
+ * Handles:
5
+ * - Commands: basicben dev, basicben make:controller
6
+ * - Positional args: basicben make:controller UserController
7
+ * - Flags: --port=3000, --verbose, -v
8
+ */
9
+
10
+ export function parseArgs(argv) {
11
+ const result = {
12
+ command: null,
13
+ args: [],
14
+ flags: {}
15
+ }
16
+
17
+ for (let i = 0; i < argv.length; i++) {
18
+ const arg = argv[i]
19
+
20
+ // Long flag with value: --port=3000
21
+ if (arg.startsWith('--') && arg.includes('=')) {
22
+ const [key, value] = arg.slice(2).split('=')
23
+ result.flags[key] = value
24
+ continue
25
+ }
26
+
27
+ // Long flag: --verbose (boolean) or --port 3000 (with next arg)
28
+ if (arg.startsWith('--')) {
29
+ const key = arg.slice(2)
30
+ const next = argv[i + 1]
31
+
32
+ // If next arg exists and isn't a flag, use it as value
33
+ if (next && !next.startsWith('-')) {
34
+ result.flags[key] = next
35
+ i++ // skip next
36
+ } else {
37
+ result.flags[key] = true
38
+ }
39
+ continue
40
+ }
41
+
42
+ // Short flags: -v, -p 3000
43
+ if (arg.startsWith('-') && arg.length === 2) {
44
+ const key = arg.slice(1)
45
+ const next = argv[i + 1]
46
+
47
+ if (next && !next.startsWith('-')) {
48
+ result.flags[key] = next
49
+ i++
50
+ } else {
51
+ result.flags[key] = true
52
+ }
53
+ continue
54
+ }
55
+
56
+ // Combined short flags: -abc (multiple booleans)
57
+ if (arg.startsWith('-') && arg.length > 2) {
58
+ for (const char of arg.slice(1)) {
59
+ result.flags[char] = true
60
+ }
61
+ continue
62
+ }
63
+
64
+ // First non-flag is the command
65
+ if (!result.command) {
66
+ result.command = arg
67
+ continue
68
+ }
69
+
70
+ // Everything else is a positional arg
71
+ result.args.push(arg)
72
+ }
73
+
74
+ return result
75
+ }
76
+
77
+ /**
78
+ * Expand flag aliases to full names
79
+ */
80
+ export function expandAliases(flags, aliases) {
81
+ const expanded = { ...flags }
82
+
83
+ for (const [short, long] of Object.entries(aliases)) {
84
+ if (expanded[short] !== undefined && expanded[long] === undefined) {
85
+ expanded[long] = expanded[short]
86
+ delete expanded[short]
87
+ }
88
+ }
89
+
90
+ return expanded
91
+ }
@@ -0,0 +1,4 @@
1
+ import { createContext } from 'react'
2
+
3
+ export const RouterContext = createContext(null)
4
+ export const AuthContext = createContext(null)
@@ -0,0 +1,50 @@
1
+ import { useContext } from 'react'
2
+ import { RouterContext, AuthContext } from './context.js'
3
+
4
+ /**
5
+ * Access auth state and methods
6
+ * @returns {{ user: object|null, setUser: function, logout: function, loading: boolean }}
7
+ */
8
+ export function useAuth() {
9
+ const context = useContext(AuthContext)
10
+ if (!context) {
11
+ throw new Error('useAuth must be used within createClientApp')
12
+ }
13
+ return context
14
+ }
15
+
16
+ /**
17
+ * Get navigation function
18
+ * @returns {function} navigate(path, options)
19
+ */
20
+ export function useNavigate() {
21
+ const context = useContext(RouterContext)
22
+ if (!context) {
23
+ throw new Error('useNavigate must be used within createClientApp')
24
+ }
25
+ return context.navigate
26
+ }
27
+
28
+ /**
29
+ * Get current route params
30
+ * @returns {object} params object (e.g. { id: '123' })
31
+ */
32
+ export function useParams() {
33
+ const context = useContext(RouterContext)
34
+ if (!context) {
35
+ throw new Error('useParams must be used within createClientApp')
36
+ }
37
+ return context.params
38
+ }
39
+
40
+ /**
41
+ * Get current path
42
+ * @returns {string} current pathname
43
+ */
44
+ export function usePath() {
45
+ const context = useContext(RouterContext)
46
+ if (!context) {
47
+ throw new Error('usePath must be used within createClientApp')
48
+ }
49
+ return context.path
50
+ }
@@ -0,0 +1,3 @@
1
+ export { createClientApp } from './router.js'
2
+ export { useAuth, useNavigate, useParams, usePath } from './hooks.js'
3
+ export { RouterContext, AuthContext } from './context.js'
@@ -0,0 +1,184 @@
1
+ import { useState, useEffect, useCallback, createElement } from 'react'
2
+ import { RouterContext, AuthContext } from './context.js'
3
+
4
+ /**
5
+ * Create a client-side React app with routing
6
+ *
7
+ * @param {object} config
8
+ * @param {object} config.routes - Route definitions { path: Component | { component, auth?, guest?, layout? } }
9
+ * @param {function} [config.layout] - Default layout wrapper
10
+ * @param {function} [config.api] - API function for auth check (default: fetch /api/user)
11
+ * @param {function} [config.Loading] - Loading component
12
+ * @returns {function} React component
13
+ */
14
+ export function createClientApp(config) {
15
+ const { routes, layout: DefaultLayout, api, Loading } = config
16
+
17
+ // Normalize routes to consistent format
18
+ const normalizedRoutes = Object.entries(routes).map(([path, value]) => {
19
+ const isSimple = typeof value === 'function'
20
+ return {
21
+ path,
22
+ pattern: pathToRegex(path),
23
+ component: isSimple ? value : value.component,
24
+ auth: isSimple ? false : value.auth || false,
25
+ guest: isSimple ? false : value.guest || false,
26
+ layout: isSimple ? null : value.layout || null,
27
+ }
28
+ })
29
+
30
+ function App() {
31
+ const [user, setUser] = useState(null)
32
+ const [loading, setLoading] = useState(true)
33
+ const [path, setPath] = useState(window.location.pathname)
34
+
35
+ // Match current route
36
+ const matchRoute = useCallback((pathname) => {
37
+ for (const route of normalizedRoutes) {
38
+ const match = pathname.match(route.pattern)
39
+ if (match) {
40
+ const routeParams = extractParams(route.path, match)
41
+ return { route, params: routeParams }
42
+ }
43
+ }
44
+ return null
45
+ }, [])
46
+
47
+ // Navigate function
48
+ const navigate = useCallback((to, options = {}) => {
49
+ const { replace = false } = options
50
+
51
+ if (replace) {
52
+ window.history.replaceState({}, '', to)
53
+ } else {
54
+ window.history.pushState({}, '', to)
55
+ }
56
+
57
+ setPath(to)
58
+ window.scrollTo(0, 0)
59
+ }, [])
60
+
61
+ // Logout function
62
+ const logout = useCallback(() => {
63
+ localStorage.removeItem('token')
64
+ setUser(null)
65
+ navigate('/')
66
+ }, [navigate])
67
+
68
+ // Auth check on mount
69
+ useEffect(() => {
70
+ const token = localStorage.getItem('token')
71
+ if (!token) {
72
+ setLoading(false)
73
+ return
74
+ }
75
+
76
+ const checkAuth = api || defaultApi
77
+ checkAuth('/api/user')
78
+ .then(data => setUser(data.user))
79
+ .catch(() => localStorage.removeItem('token'))
80
+ .finally(() => setLoading(false))
81
+ }, [])
82
+
83
+ // Handle browser back/forward
84
+ useEffect(() => {
85
+ const handlePopState = () => {
86
+ setPath(window.location.pathname)
87
+ }
88
+ window.addEventListener('popstate', handlePopState)
89
+ return () => window.removeEventListener('popstate', handlePopState)
90
+ }, [])
91
+
92
+ // Loading state
93
+ if (loading) {
94
+ if (Loading) return createElement(Loading)
95
+ return createElement('div', {
96
+ style: { display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }
97
+ }, 'Loading...')
98
+ }
99
+
100
+ // Find matching route
101
+ const matched = matchRoute(path)
102
+ if (!matched) {
103
+ return createElement('div', null, '404 - Not Found')
104
+ }
105
+
106
+ const { route, params } = matched
107
+
108
+ // Route guards
109
+ if (route.auth && !user) {
110
+ navigate('/login', { replace: true })
111
+ return null
112
+ }
113
+ if (route.guest && user) {
114
+ navigate('/', { replace: true })
115
+ return null
116
+ }
117
+
118
+ // Build page element
119
+ let wrapped = createElement(route.component)
120
+
121
+ // Apply layout: route-specific layout replaces default, or use default
122
+ const Layout = route.layout || DefaultLayout
123
+ if (Layout) {
124
+ wrapped = createElement(Layout, null, wrapped)
125
+ }
126
+
127
+ // Provide context
128
+ return createElement(
129
+ AuthContext.Provider,
130
+ { value: { user, setUser, logout, loading } },
131
+ createElement(
132
+ RouterContext.Provider,
133
+ { value: { path, params, navigate } },
134
+ wrapped
135
+ )
136
+ )
137
+ }
138
+
139
+ return App
140
+ }
141
+
142
+ /**
143
+ * Convert route path to regex
144
+ * /posts/:id -> /^\/posts\/([^/]+)$/
145
+ */
146
+ function pathToRegex(path) {
147
+ if (path === '*') return /^.*$/
148
+
149
+ const pattern = path
150
+ .replace(/\*/g, '.*')
151
+ .replace(/:(\w+)/g, '([^/]+)')
152
+ .replace(/\//g, '\\/')
153
+
154
+ return new RegExp(`^${pattern}$`)
155
+ }
156
+
157
+ /**
158
+ * Extract params from match
159
+ */
160
+ function extractParams(path, match) {
161
+ const params = {}
162
+ const paramNames = path.match(/:(\w+)/g) || []
163
+
164
+ paramNames.forEach((name, i) => {
165
+ params[name.slice(1)] = match[i + 1]
166
+ })
167
+
168
+ return params
169
+ }
170
+
171
+ /**
172
+ * Default API function
173
+ */
174
+ async function defaultApi(path) {
175
+ const token = localStorage.getItem('token')
176
+ const res = await fetch(path, {
177
+ headers: {
178
+ 'Content-Type': 'application/json',
179
+ ...(token ? { Authorization: `Bearer ${token}` } : {})
180
+ }
181
+ })
182
+ if (!res.ok) throw new Error('Request failed')
183
+ return res.json()
184
+ }