metaowl 0.3.4 → 0.3.7
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 +61 -15
- package/bin/metaowl-lint.js +2 -2
- package/eslint.js +5 -2
- package/index.js +22 -1
- package/modules/auto-import.js +1 -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
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/metaowl)
|
|
6
6
|
[](LICENSE)
|
|
7
|
-
[](https://nodejs.org)
|
|
8
8
|
[](https://github.com/dennisschott/metaowl/issues)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
> ⚠️ **Work in progress:** metaowl is not production-ready yet. APIs may change without notice, and features may still break between releases.
|
|
11
|
+
|
|
12
|
+
metaowl is a complete solution for building OWL applications with everything you need out of the box:
|
|
11
13
|
|
|
12
14
|
**Core Infrastructure:** File-based routing with dynamic routes, layout system, navigation guards, Pinia-inspired state management, and zero-config app mounting.
|
|
13
15
|
|
|
@@ -99,7 +101,7 @@ All powered by a batteries-included Vite plugin that handles the build pipeline,
|
|
|
99
101
|
|
|
100
102
|
| Dependency | Version |
|
|
101
103
|
|---|---|
|
|
102
|
-
| Node.js | `>=
|
|
104
|
+
| Node.js | `>=20` |
|
|
103
105
|
| `@odoo/owl` | bundled |
|
|
104
106
|
|
|
105
107
|
---
|
|
@@ -267,7 +269,7 @@ File-based routing supports dynamic segments using bracket notation. The router
|
|
|
267
269
|
| `pages/product/[category]/[slug]/Product.js` | `/product/:category/:slug` | `/product/tech/hello` | `{ category: 'tech', slug: 'hello' }` |
|
|
268
270
|
| `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
271
|
| `pages/docs/[...path]/Docs.js` | `/docs/:path(.*)` | `/docs/api/routing` | `{ path: 'api/routing' }` |
|
|
270
|
-
| `pages/[...
|
|
272
|
+
| `pages/[...path]/NotFound.js` | `/:path(.*)` | `/any/unknown/path` | `{ path: 'any/unknown/path' }` |
|
|
271
273
|
|
|
272
274
|
**Param Types:**
|
|
273
275
|
|
|
@@ -275,25 +277,55 @@ File-based routing supports dynamic segments using bracket notation. The router
|
|
|
275
277
|
- `[param?]` — Optional parameter, may be omitted
|
|
276
278
|
- `[...param]` — Catch-all parameter, matches any number of segments
|
|
277
279
|
|
|
278
|
-
Access parameters in your component
|
|
280
|
+
Access parameters in your component via `getCurrentRoute()`:
|
|
279
281
|
|
|
280
282
|
```js
|
|
281
|
-
import { Component
|
|
283
|
+
import { Component } from '@odoo/owl'
|
|
284
|
+
import { getCurrentRoute } from 'metaowl'
|
|
282
285
|
|
|
283
|
-
export class UserPage extends Component {
|
|
284
|
-
static template =
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
static props = ['params']
|
|
286
|
+
export default class UserPage extends Component {
|
|
287
|
+
static template = 'UserPage'
|
|
288
|
+
|
|
289
|
+
setup() {
|
|
290
|
+
const route = getCurrentRoute()
|
|
291
|
+
this.id = route?.params?.id
|
|
292
|
+
}
|
|
292
293
|
}
|
|
293
294
|
```
|
|
294
295
|
|
|
295
296
|
---
|
|
296
297
|
|
|
298
|
+
### 404 / Not Found Pages
|
|
299
|
+
|
|
300
|
+
Create a catch-all route at `pages/[...path]/` to handle unknown URLs:
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
src/pages/
|
|
304
|
+
[...path]/
|
|
305
|
+
NotFound.js ← rendered for any unmatched URL
|
|
306
|
+
NotFound.xml
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```js
|
|
310
|
+
// pages/[...path]/NotFound.js
|
|
311
|
+
import { Component } from '@odoo/owl'
|
|
312
|
+
import { Meta } from 'metaowl'
|
|
313
|
+
|
|
314
|
+
export default class NotFound extends Component {
|
|
315
|
+
static template = 'NotFound'
|
|
316
|
+
|
|
317
|
+
setup() {
|
|
318
|
+
Meta.title('404 – Page Not Found')
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
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.
|
|
324
|
+
|
|
325
|
+
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.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
297
329
|
## Layouts
|
|
298
330
|
|
|
299
331
|
Layouts provide shared page structures. Create a `layouts/` directory alongside your `pages/`:
|
|
@@ -1434,6 +1466,20 @@ npx serve -s dist
|
|
|
1434
1466
|
|
|
1435
1467
|
---
|
|
1436
1468
|
|
|
1469
|
+
## Changelog
|
|
1470
|
+
|
|
1471
|
+
### v0.3.7 (2026-03-24)
|
|
1472
|
+
|
|
1473
|
+
**Fixed:**
|
|
1474
|
+
|
|
1475
|
+
- **bin/metaowl-lint.js**: Fixed inconsistent default lint paths. Changed from `src/owl/pages/**` and `src/owl/components/**` to `src/pages/**` and `src/components/**` to match the documented project structure.
|
|
1476
|
+
|
|
1477
|
+
- **eslint.js**: Fixed `ignores` configuration placement. Moved `ignores` to a separate configuration object as required by ESLint Flat Config format. Also added `.metaowl/**` to the ignore list for the auto-generated component declarations.
|
|
1478
|
+
|
|
1479
|
+
- **modules/auto-import.js**: Fixed missing `node:` prefix for Node.js built-in module import.
|
|
1480
|
+
|
|
1481
|
+
---
|
|
1482
|
+
|
|
1437
1483
|
## Contributing
|
|
1438
1484
|
|
|
1439
1485
|
Contributions are welcome! Please open an issue before submitting a pull request so we can discuss the change.
|
package/bin/metaowl-lint.js
CHANGED
package/eslint.js
CHANGED
|
@@ -39,11 +39,14 @@ export const eslintConfig = [
|
|
|
39
39
|
'quotes': ['error', 'single'],
|
|
40
40
|
'comma-dangle': ['error', 'never'],
|
|
41
41
|
'no-undef': 'off'
|
|
42
|
-
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
43
45
|
ignores: [
|
|
44
46
|
'node_modules/**',
|
|
45
47
|
'dist/**',
|
|
46
|
-
'build/**'
|
|
48
|
+
'build/**',
|
|
49
|
+
'.metaowl/**'
|
|
47
50
|
]
|
|
48
51
|
}
|
|
49
52
|
]
|
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/auto-import.js
CHANGED
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.7",
|
|
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')
|