metaowl 0.4.0 → 0.5.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 (79) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +13 -15
  3. package/build/runtime/bin/metaowl-build.js +10 -0
  4. package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
  5. package/build/runtime/bin/metaowl-dev.js +10 -0
  6. package/build/runtime/bin/metaowl-generate.js +231 -0
  7. package/build/runtime/bin/metaowl-lint.js +58 -0
  8. package/build/runtime/bin/utils.js +68 -0
  9. package/build/runtime/index.js +141 -0
  10. package/build/runtime/modules/app-mounter.js +65 -0
  11. package/build/runtime/modules/auto-import.js +140 -0
  12. package/build/runtime/modules/cache.js +49 -0
  13. package/build/runtime/modules/composables.js +353 -0
  14. package/build/runtime/modules/error-boundary.js +116 -0
  15. package/build/runtime/modules/fetch.js +31 -0
  16. package/build/runtime/modules/file-router.js +205 -0
  17. package/build/runtime/modules/forms.js +193 -0
  18. package/build/runtime/modules/i18n.js +167 -0
  19. package/build/runtime/modules/layouts.js +163 -0
  20. package/build/runtime/modules/link.js +141 -0
  21. package/build/runtime/modules/meta.js +117 -0
  22. package/build/runtime/modules/odoo-rpc.js +264 -0
  23. package/build/runtime/modules/pwa.js +262 -0
  24. package/build/runtime/modules/router.js +389 -0
  25. package/build/runtime/modules/seo.js +186 -0
  26. package/build/runtime/modules/store.js +196 -0
  27. package/build/runtime/modules/templates-manager.js +52 -0
  28. package/build/runtime/modules/test-utils.js +238 -0
  29. package/build/runtime/vite/plugin.js +183 -0
  30. package/eslint.js +29 -0
  31. package/package.json +29 -11
  32. package/CONTRIBUTING.md +0 -49
  33. package/bin/metaowl-build.js +0 -12
  34. package/bin/metaowl-dev.js +0 -12
  35. package/bin/metaowl-generate.js +0 -339
  36. package/bin/metaowl-lint.js +0 -71
  37. package/bin/utils.js +0 -82
  38. package/index.js +0 -328
  39. package/modules/app-mounter.js +0 -104
  40. package/modules/auto-import.js +0 -225
  41. package/modules/cache.js +0 -59
  42. package/modules/composables.js +0 -600
  43. package/modules/error-boundary.js +0 -228
  44. package/modules/fetch.js +0 -51
  45. package/modules/file-router.js +0 -478
  46. package/modules/forms.js +0 -353
  47. package/modules/i18n.js +0 -333
  48. package/modules/layouts.js +0 -431
  49. package/modules/link.js +0 -255
  50. package/modules/meta.js +0 -119
  51. package/modules/odoo-rpc.js +0 -511
  52. package/modules/pwa.js +0 -515
  53. package/modules/router.js +0 -769
  54. package/modules/seo.js +0 -501
  55. package/modules/store.js +0 -409
  56. package/modules/templates-manager.js +0 -89
  57. package/modules/test-utils.js +0 -532
  58. package/test/auto-import.test.js +0 -110
  59. package/test/cache.test.js +0 -55
  60. package/test/composables.test.js +0 -103
  61. package/test/dynamic-routes.test.js +0 -469
  62. package/test/error-boundary.test.js +0 -126
  63. package/test/fetch.test.js +0 -100
  64. package/test/file-router.test.js +0 -55
  65. package/test/forms.test.js +0 -203
  66. package/test/i18n.test.js +0 -188
  67. package/test/layouts.test.js +0 -395
  68. package/test/link.test.js +0 -189
  69. package/test/meta.test.js +0 -146
  70. package/test/odoo-rpc.test.js +0 -547
  71. package/test/pwa.test.js +0 -154
  72. package/test/router-guards.test.js +0 -229
  73. package/test/router.test.js +0 -77
  74. package/test/seo.test.js +0 -353
  75. package/test/store.test.js +0 -476
  76. package/test/templates-manager.test.js +0 -83
  77. package/test/test-utils.test.js +0 -314
  78. package/vite/plugin.js +0 -277
  79. package/vitest.config.js +0 -8
@@ -1,478 +0,0 @@
1
- /**
2
- * @module FileRouter
3
- *
4
- * File-based routing with dynamic route parameter support.
5
- *
6
- * Convention (mirrors Nuxt/Next.js):
7
- * File: pages/index/Index.js → URL: /
8
- * File: pages/about/About.js → URL: /about
9
- * File: pages/blog/post/Post.js → URL: /blog/post
10
- *
11
- * Dynamic Routes:
12
- * File: pages/user/[id]/User.js → URL: /user/:id
13
- * File: pages/product/[category]/[slug].js → URL: /product/:category/:slug
14
- * File: pages/docs/[...path].js → URL: /docs/:path(.*)
15
- *
16
- * Optional Parameters:
17
- * File: pages/blog/[id]/[slug]?/Blog.js → URL: /blog/:id/:slug?
18
- *
19
- * Catch-all Routes:
20
- * File: pages/[...404].js → URL: /:path(.*)
21
- */
22
-
23
- /**
24
- * Pattern types for route segments.
25
- */
26
- const PATTERNS = {
27
- // [param] → required parameter
28
- PARAM: /^\[([^?]+)\]$/,
29
- // [param]? → optional parameter
30
- OPTIONAL: /^\[([^?]+)\?\]$/,
31
- // [...param] → catch-all parameter
32
- CATCH_ALL: /^\.\.\.(.*)$/,
33
- // Regular segment
34
- NORMAL: /^[^\[]+$/
35
- }
36
-
37
- /**
38
- * Converts a file path segment to a route pattern segment.
39
- *
40
- * @param {string} segment - Path segment (e.g., [id], about, [...slug])
41
- * @returns {string} Route pattern segment (e.g., :id, about, :slug(.*))
42
- */
43
- function segmentToPattern(segment) {
44
- // Check for catch-all [...something]
45
- const insideBrackets = segment.match(/^\[(.+)\]$/)
46
- if (insideBrackets) {
47
- const content = insideBrackets[1]
48
-
49
- // Check for spread: [...param]
50
- if (content.startsWith('...')) {
51
- const paramName = content.slice(3) || 'path'
52
- return `:${paramName}(.*)`
53
- }
54
-
55
- // Check for optional: [param?]
56
- if (content.endsWith('?')) {
57
- const paramName = content.slice(0, -1)
58
- return `:${paramName}?`
59
- }
60
-
61
- // Regular parameter: [param]
62
- return `:${content}`
63
- }
64
-
65
- // Normal segment
66
- return segment
67
- }
68
-
69
- /**
70
- * Checks if a segment is a dynamic parameter.
71
- *
72
- * @param {string} segment - Path segment
73
- * @returns {boolean}
74
- */
75
- function isDynamicSegment(segment) {
76
- return segment.startsWith('[') && segment.endsWith(']')
77
- }
78
-
79
- /**
80
- * Checks if a segment is an optional parameter.
81
- *
82
- * @param {string} segment - Path segment
83
- * @returns {boolean}
84
- */
85
- function isOptionalSegment(segment) {
86
- return segment.startsWith('[') && segment.endsWith('?]')
87
- }
88
-
89
- /**
90
- * Extracts parameter names from a file path.
91
- *
92
- * @param {string} filePath - Relative file path
93
- * @returns {string[]} Parameter names in order
94
- */
95
- function extractParamNames(filePath) {
96
- const params = []
97
- const parts = filePath.split('/')
98
-
99
- for (const part of parts) {
100
- const match = part.match(/^\[([^?\]]+)\??\]$|^\[\.\.\.([^\]]+)\]$/)
101
- if (match) {
102
- params.push(match[1] || match[2] || 'path')
103
- }
104
- }
105
-
106
- return params
107
- }
108
-
109
- /**
110
- * Derives a URL path from an import.meta.glob key.
111
- *
112
- * Convention (mirrors Nuxt/Next.js file-based routing):
113
- * Key: ./pages/index/Index.js → URL: /
114
- * Key: ./pages/about/About.js → URL: /about
115
- * Key: ./pages/about/bla/Bla.js → URL: /about/bla
116
- * Key: ./pages/user/[id]/User.js → URL: /user/:id
117
- * Key: ./pages/[...404].js → URL: /:path(.*)
118
- *
119
- * Rule: the *directory* path relative to pages/ becomes the URL.
120
- * A top-level directory named 'index' maps to '/'.
121
- * Dynamic segments use [param] syntax and become route parameters.
122
- *
123
- * @param {string} key - import.meta.glob key, e.g. './pages/about/About.js'
124
- * @returns {string} URL pattern
125
- */
126
- export function pathFromKey(key) {
127
- // Strip leading './' and 'pages/' prefix
128
- const rel = key.replace(/^\.\/pages\//, '')
129
- // Get all segments
130
- const parts = rel.split('/')
131
- // Remove filename (last segment)
132
- parts.pop()
133
-
134
- if (parts.length === 0) {
135
- return '/'
136
- }
137
-
138
- // Check for single 'index'
139
- if (parts.length === 1 && parts[0] === 'index') {
140
- return '/'
141
- }
142
-
143
- // Convert segments to route patterns
144
- const routeParts = parts.map(segmentToPattern)
145
-
146
- return '/' + routeParts.join('/')
147
- }
148
-
149
- /**
150
- * Builds a display path for documentation (without parameter syntax).
151
- *
152
- * @param {string} pattern - Route pattern like '/user/:id'
153
- * @returns {string} Display path like '/user/[id]'
154
- */
155
- function patternToDisplay(pattern) {
156
- return pattern
157
- .replace(/:([^/(]+)\?/g, '[$1?]')
158
- .replace(/:\(([^)]+)\)/g, '[]')
159
- .replace(/:([^/(]+)(?:\([^)]*\))?/g, '[$1]')
160
- }
161
-
162
- /**
163
- * Builds a regex pattern string from a route path.
164
- *
165
- * Supports:
166
- * - Static segments: /about
167
- * - Required params: /user/:id
168
- * - Optional params: /user/:id?
169
- * - Catch-all: /docs/:path(.*)
170
- *
171
- * @param {string} path - Route path pattern
172
- * @returns {string} Regex pattern string
173
- */
174
- function buildRegexPattern(path) {
175
- // Escape forward slashes
176
- let pattern = path.replace(/\//g, '\\/')
177
-
178
- // Replace catch-all :name(.*) → capture everything
179
- pattern = pattern.replace(/:([^/(]+)\(\.\*\)/g, '([^/]+(?:/[^/]+)*)')
180
-
181
- // Replace optional params :name? → optional capture
182
- pattern = pattern.replace(/\/:([^/(]+)\?/g, '(?:/([^/]+))?')
183
-
184
- // Replace required params :name → capture
185
- pattern = pattern.replace(/:([^/(\s]+)/g, '([^/]+)')
186
-
187
- return '^' + pattern + '$'
188
- }
189
-
190
- /**
191
- * Matches a URL path against a route pattern.
192
- *
193
- * @param {string} pattern - Route pattern (e.g., '/user/:id')
194
- * @param {string} path - URL path (e.g., '/user/123')
195
- * @returns {object|null} Matched params or null
196
- */
197
- export function matchRoute(pattern, path) {
198
- // Extract parameter names
199
- const paramNames = []
200
- const paramRegex = /:([^/?(]+)/g
201
- let match
202
- while ((match = paramRegex.exec(pattern)) !== null) {
203
- paramNames.push(match[1])
204
- }
205
-
206
- // Build regex pattern
207
- const regexPattern = buildRegexPattern(pattern)
208
- const regex = new RegExp(regexPattern)
209
-
210
- const matches = path.match(regex)
211
- if (!matches) {
212
- return null
213
- }
214
-
215
- // Extract parameter values
216
- const params = {}
217
- for (let i = 0; i < paramNames.length; i++) {
218
- if (matches[i + 1] !== undefined) {
219
- params[paramNames[i]] = matches[i + 1]
220
- }
221
- }
222
-
223
- return { params, pattern }
224
- }
225
-
226
- /**
227
- * Checks if a route path is dynamic.
228
- *
229
- * @param {string} path - Route path
230
- * @returns {boolean}
231
- */
232
- export function isDynamicRoute(path) {
233
- return path.includes(':')
234
- }
235
-
236
- /**
237
- * Extracts the page component from an eagerly-imported module.
238
- * Prefers default export, falls back to the first function/class export.
239
- *
240
- * @param {object} mod - Eagerly imported module
241
- * @param {string} key - Glob key (for error messages)
242
- * @returns {Function}
243
- */
244
- function componentFromModule(mod, key) {
245
- if (typeof mod.default === 'function') return mod.default
246
- const named = Object.values(mod).find(v => typeof v === 'function')
247
- if (!named) throw new Error(`[metaowl] No component export found in "${key}"`)
248
- return named
249
- }
250
-
251
- /**
252
- * Builds a metaowl route table from an import.meta.glob result.
253
- *
254
- * @param {Record<string, object>} modules - Result of import.meta.glob with eager: true
255
- * @returns {object[]} Route table for processRoutes()
256
- */
257
- export function buildRoutes(modules) {
258
- const routes = []
259
-
260
- for (const [key, mod] of Object.entries(modules)) {
261
- const path = pathFromKey(key)
262
- const name = path === '/' ? 'index' : path.slice(1).replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
263
- const component = componentFromModule(mod, key)
264
- const params = extractParamNames(key)
265
-
266
- const route = {
267
- name,
268
- path: [path],
269
- component,
270
- params,
271
- meta: component.route?.meta || {}
272
- }
273
-
274
- // Copy any route configuration from component
275
- if (component.route) {
276
- Object.assign(route, component.route)
277
- }
278
-
279
- routes.push(route)
280
- }
281
-
282
- // Sort routes: static first, then dynamic (fewer params first), catch-all last
283
- routes.sort((a, b) => {
284
- const aPath = a.path[0]
285
- const bPath = b.path[0]
286
-
287
- // Catch-all :name(.*) routes always come last
288
- const aIsCatchAll = aPath.includes('(.*)')
289
- const bIsCatchAll = bPath.includes('(.*)')
290
- if (!aIsCatchAll && bIsCatchAll) return -1
291
- if (aIsCatchAll && !bIsCatchAll) return 1
292
-
293
- // Static routes before dynamic routes
294
- const aIsDynamic = isDynamicRoute(aPath)
295
- const bIsDynamic = isDynamicRoute(bPath)
296
-
297
- if (!aIsDynamic && bIsDynamic) return -1
298
- if (aIsDynamic && !bIsDynamic) return 1
299
-
300
- // Among dynamic routes, more specific (longer path) comes first
301
- if (aIsDynamic && bIsDynamic) {
302
- const aSegments = aPath.split('/').length
303
- const bSegments = bPath.split('/').length
304
- if (aSegments !== bSegments) return bSegments - aSegments
305
-
306
- const aParamCount = a.params?.length || 0
307
- const bParamCount = b.params?.length || 0
308
- return aParamCount - bParamCount
309
- }
310
-
311
- return 0
312
- })
313
-
314
- return routes
315
- }
316
-
317
- /**
318
- * Finds a matching route for a given URL path.
319
- *
320
- * @param {object[]} routes - Route table
321
- * @param {string} path - URL path
322
- * @returns {object|null} Matched route with params
323
- */
324
- export function findRoute(routes, path) {
325
- for (const route of routes) {
326
- for (const routePath of route.path) {
327
- const match = matchRoute(routePath, path)
328
- if (match) {
329
- return {
330
- ...route,
331
- matchedPath: routePath,
332
- params: match.params
333
- }
334
- }
335
- }
336
- }
337
- return null
338
- }
339
-
340
- /**
341
- * Generates URL from route name and params.
342
- *
343
- * @param {string} name - Route name
344
- * @param {object} [params] - Route parameters
345
- * @returns {string} Generated URL
346
- * @throws {Error} If route not found
347
- *
348
- * Example:
349
- * generateUrl(routes, 'user', { id: '123' }) // returns '/user/123'
350
- * generateUrl(routes, 'blog-post', { category: 'tech', slug: 'hello' }) // returns '/blog/tech/hello'
351
- */
352
- export function generateUrl(routes, name, params = {}) {
353
- const route = routes.find(r => r.name === name)
354
- if (!route) {
355
- throw new Error(`[metaowl] Route "${name}" not found`)
356
- }
357
-
358
- let path = route.path[0]
359
-
360
- // Replace params in path
361
- for (const [key, value] of Object.entries(params)) {
362
- path = path.replace(`:${key}`, value)
363
- path = path.replace(`:${key}?`, value)
364
- }
365
-
366
- // Remove remaining optional params and trailing ?
367
- path = path.replace(/\/:[^/]+\?/g, '').replace(/\?$/, '')
368
-
369
- return path
370
- }
371
-
372
- /**
373
- * Validates route parameters.
374
- *
375
- * @param {object} route - Route definition
376
- * @param {object} params - Parameters to validate
377
- * @returns {object} Validation result { valid: boolean, missing: string[], extra: string[] }
378
- */
379
- export function validateRouteParams(route, params) {
380
- const required = route.params || []
381
- const provided = Object.keys(params)
382
-
383
- const missing = required.filter(p => !provided.includes(p))
384
- const extra = provided.filter(p => !required.includes(p))
385
-
386
- return {
387
- valid: missing.length === 0,
388
- missing,
389
- extra
390
- }
391
- }
392
-
393
- /**
394
- * Parses current URL and returns route info.
395
- *
396
- * @param {object[]} routes - Route table
397
- * @returns {object|null} Current route info
398
- */
399
- export function parseCurrentRoute(routes) {
400
- const path = document.location.pathname
401
- return findRoute(routes, path)
402
- }
403
-
404
- /**
405
- * Route configuration helper for components.
406
- *
407
- * @param {object} config - Route configuration
408
- * @param {string} [config.path] - Route path override
409
- * @param {object} [config.meta] - Route metadata
410
- * @param {Function} [config.beforeEnter] - Per-route guard
411
- * @returns {object} Route configuration
412
- *
413
- * Example in a component file:
414
- * export class UserPage extends Component {
415
- * static route = defineRoute({
416
- * path: '/custom/:id',
417
- * meta: { requiresAuth: true },
418
- * beforeEnter: (to, from, next) => { ... }
419
- * })
420
- * }
421
- */
422
- export function defineRoute(config) {
423
- return config
424
- }
425
-
426
- /**
427
- * Route decorator (works with class decorator syntax).
428
- *
429
- * @param {object} config - Route configuration
430
- * @returns {Function} Class decorator
431
- *
432
- * Example:
433
- * @route({ meta: { requiresAuth: true } })
434
- * export class UserPage extends Component {
435
- * // ...
436
- * }
437
- */
438
- export function route(config) {
439
- return function decorator(ComponentClass) {
440
- ComponentClass.route = config
441
- return ComponentClass
442
- }
443
- }
444
-
445
- /**
446
- * Helper to create a catch-all route.
447
- *
448
- * @param {Function} component - 404 component
449
- * @param {object} [options] - Additional options
450
- * @returns {object} Catch-all route definition
451
- */
452
- export function createCatchAllRoute(component, options = {}) {
453
- return {
454
- name: options.name || '404',
455
- path: ['/:path(.*)'],
456
- component,
457
- params: ['path'],
458
- meta: { ...options.meta, catchAll: true }
459
- }
460
- }
461
-
462
- /**
463
- * Helper to create a redirect route.
464
- *
465
- * @param {string} from - From path
466
- * @param {string} to - To path (can contain params)
467
- * @returns {object} Redirect route definition
468
- */
469
- export function createRedirectRoute(from, to) {
470
- // Remove leading slash and convert to dash-separated name
471
- const name = from.replace(/^\//, '').replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-')
472
- return {
473
- name: `redirect-${name}`,
474
- path: [from],
475
- redirect: to,
476
- component: null
477
- }
478
- }