metaowl 0.3.4 → 0.3.5
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/README.md +42 -12
- package/index.js +22 -1
- package/modules/file-router.js +13 -3
- package/modules/router.js +31 -17
- package/package.json +1 -1
- package/test/dynamic-routes.test.js +1 -1
package/README.md
CHANGED
|
@@ -267,7 +267,7 @@ File-based routing supports dynamic segments using bracket notation. The router
|
|
|
267
267
|
| `pages/product/[category]/[slug]/Product.js` | `/product/:category/:slug` | `/product/tech/hello` | `{ category: 'tech', slug: 'hello' }` |
|
|
268
268
|
| `pages/blog/[id]/[slug?]/Blog.js` | `/blog/:id/:slug?` | `/blog/123` or `/blog/123/my-post` | `{ id: '123' }` or `{ id: '123', slug: 'my-post' }` |
|
|
269
269
|
| `pages/docs/[...path]/Docs.js` | `/docs/:path(.*)` | `/docs/api/routing` | `{ path: 'api/routing' }` |
|
|
270
|
-
| `pages/[...
|
|
270
|
+
| `pages/[...path]/NotFound.js` | `/:path(.*)` | `/any/unknown/path` | `{ path: 'any/unknown/path' }` |
|
|
271
271
|
|
|
272
272
|
**Param Types:**
|
|
273
273
|
|
|
@@ -275,23 +275,53 @@ File-based routing supports dynamic segments using bracket notation. The router
|
|
|
275
275
|
- `[param?]` — Optional parameter, may be omitted
|
|
276
276
|
- `[...param]` — Catch-all parameter, matches any number of segments
|
|
277
277
|
|
|
278
|
-
Access parameters in your component
|
|
278
|
+
Access parameters in your component via `getCurrentRoute()`:
|
|
279
279
|
|
|
280
280
|
```js
|
|
281
|
-
import { Component
|
|
281
|
+
import { Component } from '@odoo/owl'
|
|
282
|
+
import { getCurrentRoute } from 'metaowl'
|
|
282
283
|
|
|
283
|
-
export class UserPage extends Component {
|
|
284
|
-
static template =
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
284
|
+
export default class UserPage extends Component {
|
|
285
|
+
static template = 'UserPage'
|
|
286
|
+
|
|
287
|
+
setup() {
|
|
288
|
+
const route = getCurrentRoute()
|
|
289
|
+
this.id = route?.params?.id
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
### 404 / Not Found Pages
|
|
297
|
+
|
|
298
|
+
Create a catch-all route at `pages/[...path]/` to handle unknown URLs:
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
src/pages/
|
|
302
|
+
[...path]/
|
|
303
|
+
NotFound.js ← rendered for any unmatched URL
|
|
304
|
+
NotFound.xml
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
// pages/[...path]/NotFound.js
|
|
309
|
+
import { Component } from '@odoo/owl'
|
|
310
|
+
import { Meta } from 'metaowl'
|
|
311
|
+
|
|
312
|
+
export default class NotFound extends Component {
|
|
313
|
+
static template = 'NotFound'
|
|
314
|
+
|
|
315
|
+
setup() {
|
|
316
|
+
Meta.title('404 – Page Not Found')
|
|
317
|
+
}
|
|
292
318
|
}
|
|
293
319
|
```
|
|
294
320
|
|
|
321
|
+
The catch-all directory can be named `[...path]`, `[...404]`, or any `[...name]` — the bracket-dot-dot-dot prefix is what makes it a catch-all regardless of name.
|
|
322
|
+
|
|
323
|
+
If no catch-all route exists and a URL cannot be matched, metaowl renders a minimal built-in 404 message so the page doesn't silently break.
|
|
324
|
+
|
|
295
325
|
---
|
|
296
326
|
|
|
297
327
|
## Layouts
|
package/index.js
CHANGED
|
@@ -188,6 +188,27 @@ export async function boot(routesOrModules = {}, layoutsOrModules = null) {
|
|
|
188
188
|
const routes = Array.isArray(routesOrModules)
|
|
189
189
|
? routesOrModules
|
|
190
190
|
: buildRoutes(routesOrModules)
|
|
191
|
-
|
|
191
|
+
|
|
192
|
+
let route
|
|
193
|
+
try {
|
|
194
|
+
route = await processRoutes(routes)
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error.message && error.message.startsWith('No route found')) {
|
|
197
|
+
console.warn('[metaowl]', error.message)
|
|
198
|
+
const el = document.getElementById('metaowl')
|
|
199
|
+
if (el) {
|
|
200
|
+
el.innerHTML = [
|
|
201
|
+
'<div style="font-family:sans-serif;padding:3rem;text-align:center">',
|
|
202
|
+
'<h1 style="font-size:4rem;font-weight:700;margin:0;color:#6b7280">404</h1>',
|
|
203
|
+
'<p style="font-size:1.25rem;color:#9ca3af;margin-top:0.5rem">Page not found</p>',
|
|
204
|
+
'<p style="margin-top:2rem"><a href="/" style="color:#3b82f6;text-decoration:none">← Go home</a></p>',
|
|
205
|
+
'</div>'
|
|
206
|
+
].join('')
|
|
207
|
+
}
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
throw error
|
|
211
|
+
}
|
|
212
|
+
|
|
192
213
|
await mountApp(route)
|
|
193
214
|
}
|
package/modules/file-router.js
CHANGED
|
@@ -279,20 +279,30 @@ export function buildRoutes(modules) {
|
|
|
279
279
|
routes.push(route)
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
// Sort routes: static
|
|
282
|
+
// Sort routes: static first, then dynamic (fewer params first), catch-all last
|
|
283
283
|
routes.sort((a, b) => {
|
|
284
284
|
const aPath = a.path[0]
|
|
285
285
|
const bPath = b.path[0]
|
|
286
286
|
|
|
287
|
-
//
|
|
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
|
|
288
294
|
const aIsDynamic = isDynamicRoute(aPath)
|
|
289
295
|
const bIsDynamic = isDynamicRoute(bPath)
|
|
290
296
|
|
|
291
297
|
if (!aIsDynamic && bIsDynamic) return -1
|
|
292
298
|
if (aIsDynamic && !bIsDynamic) return 1
|
|
293
299
|
|
|
294
|
-
// Among dynamic routes,
|
|
300
|
+
// Among dynamic routes, more specific (longer path) comes first
|
|
295
301
|
if (aIsDynamic && bIsDynamic) {
|
|
302
|
+
const aSegments = aPath.split('/').length
|
|
303
|
+
const bSegments = bPath.split('/').length
|
|
304
|
+
if (aSegments !== bSegments) return bSegments - aSegments
|
|
305
|
+
|
|
296
306
|
const aParamCount = a.params?.length || 0
|
|
297
307
|
const bParamCount = b.params?.length || 0
|
|
298
308
|
return aParamCount - bParamCount
|
package/modules/router.js
CHANGED
|
@@ -162,24 +162,25 @@ class Router {
|
|
|
162
162
|
* @returns {boolean}
|
|
163
163
|
*/
|
|
164
164
|
pathMatches(routePath, currentPath) {
|
|
165
|
-
//
|
|
165
|
+
// Simple exact match for static routes
|
|
166
166
|
if (!routePath.includes(':') && !routePath.includes('*')) {
|
|
167
|
-
// Simple exact match
|
|
168
167
|
const normalizedRoute = routePath.replace(/\/$/, '') || '/'
|
|
169
168
|
const normalizedCurrent = currentPath.replace(/\/$/, '') || '/'
|
|
170
169
|
return normalizedRoute === normalizedCurrent
|
|
171
170
|
}
|
|
172
171
|
|
|
173
|
-
//
|
|
172
|
+
// Build regex without pre-escaping the whole string (which would break : and * handling)
|
|
174
173
|
let pattern = routePath
|
|
175
|
-
// Escape
|
|
176
|
-
.replace(
|
|
177
|
-
// Replace
|
|
178
|
-
.replace(
|
|
179
|
-
// Replace
|
|
180
|
-
.replace(
|
|
181
|
-
// Replace
|
|
182
|
-
.replace(
|
|
174
|
+
// Escape forward slashes
|
|
175
|
+
.replace(/\//g, '\\/')
|
|
176
|
+
// Replace catch-all :name(.*) params — must come before required-param replacement
|
|
177
|
+
.replace(/:([^/(]+)\(\.\*\)/g, '(.*)')
|
|
178
|
+
// Replace optional params /:name?
|
|
179
|
+
.replace(/\/:([^/(]+)\?/g, '(?:/([^/]+))?')
|
|
180
|
+
// Replace required params :name
|
|
181
|
+
.replace(/:([^/(?\s]+)/g, '([^/]+)')
|
|
182
|
+
// Replace bare wildcards *
|
|
183
|
+
.replace(/\*/g, '(.*)')
|
|
183
184
|
|
|
184
185
|
pattern = '^' + pattern + '$'
|
|
185
186
|
const regex = new RegExp(pattern)
|
|
@@ -219,12 +220,23 @@ class Router {
|
|
|
219
220
|
return null
|
|
220
221
|
}
|
|
221
222
|
|
|
222
|
-
// Extract parameter names
|
|
223
|
+
// Extract parameter names in the correct order
|
|
223
224
|
const paramNames = []
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
|
|
226
|
+
// Build pattern: handle catch-all :name(.*) first, then optional, then required
|
|
227
|
+
let pattern = routePath
|
|
228
|
+
.replace(/:([^/(]+)\(\.\*\)/g, (match, name) => {
|
|
229
|
+
paramNames.push(name)
|
|
230
|
+
return '(.*)'
|
|
231
|
+
})
|
|
232
|
+
.replace(/\/:([^/(]+)\?/g, (match, name) => {
|
|
233
|
+
paramNames.push(name)
|
|
234
|
+
return '(?:/([^/]+))?'
|
|
235
|
+
})
|
|
236
|
+
.replace(/:([^/(?\s]+)/g, (match, name) => {
|
|
237
|
+
paramNames.push(name)
|
|
238
|
+
return '([^/]+)'
|
|
239
|
+
})
|
|
228
240
|
|
|
229
241
|
const regex = new RegExp('^' + pattern + '$')
|
|
230
242
|
const matches = currentPath.match(regex)
|
|
@@ -233,7 +245,9 @@ class Router {
|
|
|
233
245
|
|
|
234
246
|
const params = {}
|
|
235
247
|
for (let i = 0; i < paramNames.length; i++) {
|
|
236
|
-
|
|
248
|
+
if (matches[i + 1] !== undefined) {
|
|
249
|
+
params[paramNames[i]] = matches[i + 1]
|
|
250
|
+
}
|
|
237
251
|
}
|
|
238
252
|
|
|
239
253
|
return params
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metaowl",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Lightweight meta-framework for Odoo OWL — file-based routing, app mounting, Fetch helper, Cache, Meta tags, SSG generator, and a Vite plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -393,7 +393,7 @@ describe('Dynamic Routes', () => {
|
|
|
393
393
|
|
|
394
394
|
// Check that routes are sorted correctly
|
|
395
395
|
const blogRoute = routes.find(r => r.name === 'blog')
|
|
396
|
-
const categoryRoute = routes.find(r => r.name.includes('category'))
|
|
396
|
+
const categoryRoute = routes.find(r => r.name.includes('category') && !r.name.includes('slug'))
|
|
397
397
|
const postRoute = routes.find(r => r.name.includes('slug'))
|
|
398
398
|
|
|
399
399
|
expect(blogRoute.path[0]).toBe('/blog')
|