metaowl 0.4.1 → 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 (80) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +12 -0
  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 +28 -10
  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/eslint.config.js +0 -3
  39. package/index.js +0 -328
  40. package/modules/app-mounter.js +0 -104
  41. package/modules/auto-import.js +0 -225
  42. package/modules/cache.js +0 -59
  43. package/modules/composables.js +0 -600
  44. package/modules/error-boundary.js +0 -228
  45. package/modules/fetch.js +0 -51
  46. package/modules/file-router.js +0 -478
  47. package/modules/forms.js +0 -353
  48. package/modules/i18n.js +0 -333
  49. package/modules/layouts.js +0 -431
  50. package/modules/link.js +0 -255
  51. package/modules/meta.js +0 -119
  52. package/modules/odoo-rpc.js +0 -511
  53. package/modules/pwa.js +0 -515
  54. package/modules/router.js +0 -769
  55. package/modules/seo.js +0 -501
  56. package/modules/store.js +0 -409
  57. package/modules/templates-manager.js +0 -89
  58. package/modules/test-utils.js +0 -532
  59. package/test/auto-import.test.js +0 -110
  60. package/test/cache.test.js +0 -55
  61. package/test/composables.test.js +0 -103
  62. package/test/dynamic-routes.test.js +0 -469
  63. package/test/error-boundary.test.js +0 -126
  64. package/test/fetch.test.js +0 -100
  65. package/test/file-router.test.js +0 -55
  66. package/test/forms.test.js +0 -203
  67. package/test/i18n.test.js +0 -188
  68. package/test/layouts.test.js +0 -395
  69. package/test/link.test.js +0 -189
  70. package/test/meta.test.js +0 -146
  71. package/test/odoo-rpc.test.js +0 -547
  72. package/test/pwa.test.js +0 -154
  73. package/test/router-guards.test.js +0 -229
  74. package/test/router.test.js +0 -77
  75. package/test/seo.test.js +0 -353
  76. package/test/store.test.js +0 -476
  77. package/test/templates-manager.test.js +0 -83
  78. package/test/test-utils.test.js +0 -314
  79. package/vite/plugin.js +0 -290
  80. 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
- }