metaowl 0.5.0 → 0.6.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.
- package/CHANGELOG.md +28 -0
- package/README.md +255 -2
- package/build/runtime/index.js +5 -2
- package/build/runtime/modules/app-mounter.js +9 -1
- package/build/runtime/modules/constants.js +38 -0
- package/build/runtime/modules/file-router.js +12 -10
- package/build/runtime/modules/fonts.js +172 -0
- package/build/runtime/modules/i18n.js +13 -0
- package/build/runtime/modules/image.js +175 -0
- package/build/runtime/modules/layouts.js +54 -3
- package/build/runtime/modules/link.js +6 -6
- package/build/runtime/modules/odoo-rpc.js +4 -3
- package/build/runtime/modules/pwa.js +12 -2
- package/build/runtime/modules/router.js +4 -9
- package/build/runtime/modules/seo.js +4 -4
- package/build/runtime/modules/store.js +5 -3
- package/build/runtime/vite/plugin.js +23 -9
- package/package.json +26 -26
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2026-04-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Nested layouts support** — layouts can now have parent-child relationships using `setParentLayout()`, `getParentLayout()`, and `getLayoutChain()`. The `createNestedLayoutWrapper()` function creates wrapper components for multi-level nesting. The `defineNestedLayout()` decorator simplifies declaring nested layouts on components.
|
|
13
|
+
- **Image optimization module** — new `modules/image.ts` with `generateSrcSet()`, `calculateAspectRatio()`, `generateSizesAttribute()`, `createResponsiveImage()`, `prefetchImage()`, `prefetchImages()`, `isImageLoaded()`, `getImageDimensions()`, `observeImageVisibility()`, `swapImageSource()`, and `generateDominantColorPlaceholder()`.
|
|
14
|
+
- **Fonts optimization module** — new `modules/fonts.ts` with `defineFontFace()`, `loadFont()`, `loadFontFamily()`, `isFontLoaded()`, `preloadFont()`, `removeFontPreload()`, `createFontFaceRule()`, `injectFontFaceRules()`, `measureTextWidth()`, `estimateFontMetrics()`, `adjustFontForFout()`, and `getFontLoadStatus()`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **TypeScript migration completed** — the framework source, Vite integration, CLI entrypoints,
|
|
19
|
+
and test suite now use TypeScript as the primary source of truth while preserving the existing
|
|
20
|
+
public API and runtime behavior.
|
|
21
|
+
- **Runtime build output separated from source** — publishable JavaScript is now emitted to
|
|
22
|
+
`build/runtime`, and package `main`, `exports`, and `bin` entries resolve from that generated
|
|
23
|
+
runtime output.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
- **Redundant source JavaScript files** — legacy hand-maintained `.js` source files in `modules/`,
|
|
28
|
+
`bin/`, `vite/`, and the root entrypoint were removed in favor of the TypeScript sources and the
|
|
29
|
+
generated runtime build.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **Release build workflow** — added dedicated runtime build and release helper scripts for
|
|
34
|
+
clean runtime generation, release checks, and package dry-run validation.
|
|
35
|
+
|
|
8
36
|
## [0.5.0] - 2026-04-24
|
|
9
37
|
|
|
10
38
|
### Changed
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ metaowl is a complete solution for building OWL applications with everything you
|
|
|
17
17
|
|
|
18
18
|
**Developer Experience:** Composables for common patterns (auth, localStorage, fetching), form handling with validation, error boundaries, and internationalization.
|
|
19
19
|
|
|
20
|
-
**SEO & PWA:** Sitemap/robots.txt generation, structured data support, service worker integration, web app manifest, and push notifications.
|
|
20
|
+
**SEO & PWA:** Sitemap/robots.txt generation, structured data support, image optimization with srcset generation, font optimization with preloading, service worker integration, web app manifest, and push notifications.
|
|
21
21
|
|
|
22
22
|
**Testing & Quality:** Mock stores, router mocking, component testing utilities, plus bundled ESLint and PostCSS configs.
|
|
23
23
|
|
|
@@ -44,6 +44,8 @@ All powered by a batteries-included Vite plugin that handles the build pipeline,
|
|
|
44
44
|
- [Auto-Import](#auto-import)
|
|
45
45
|
- [Odoo JSON-RPC Service](#odoo-json-rpc-service)
|
|
46
46
|
- [Composables / Hooks](#composables--hooks)
|
|
47
|
+
- [Image Optimization](#image-optimization)
|
|
48
|
+
- [Font Optimization](#font-optimization)
|
|
47
49
|
- [CLI Reference](#cli-reference)
|
|
48
50
|
- [API Reference](#api-reference)
|
|
49
51
|
- [boot](#bootroutes)
|
|
@@ -61,6 +63,8 @@ All powered by a batteries-included Vite plugin that handles the build pipeline,
|
|
|
61
63
|
- [Forms](#forms-api)
|
|
62
64
|
- [OdooService](#odooservice-api)
|
|
63
65
|
- [Composables](#composables-api)
|
|
66
|
+
- [Image API](#image-api)
|
|
67
|
+
- [Fonts API](#fonts-api)
|
|
64
68
|
- [Vite Plugin](#vite-plugin)
|
|
65
69
|
- [metaowlPlugin](#metaowlpluginoptions)
|
|
66
70
|
- [metaowlConfig](#metaowlconfigoptions)
|
|
@@ -93,6 +97,8 @@ All powered by a batteries-included Vite plugin that handles the build pipeline,
|
|
|
93
97
|
- **Composables** — reusable hooks for auth, localStorage, fetching, and more
|
|
94
98
|
- **Testing Utilities** — mock store, router mocking, component mount helpers
|
|
95
99
|
- **SEO Utils** — sitemap, robots.txt, JSON-LD, Open Graph, Twitter Cards
|
|
100
|
+
- **Image Optimization** — responsive srcset generation, lazy loading, placeholder support, dominant color extraction
|
|
101
|
+
- **Font Optimization** — FontFace creation, font preloading, @font-face CSS generation, FOUT handling
|
|
96
102
|
- **PWA Support** — service worker, manifest generation, push notifications
|
|
97
103
|
- **SSG generator** — statically pre-renders HTML pages with correct meta tags at build time
|
|
98
104
|
- **Vite plugin** — handles `COMPONENTS` injection, XML template copying, CSS auto-import, chunk splitting, and env filtering
|
|
@@ -389,6 +395,37 @@ If no layout is specified, the `default` layout is used automatically.
|
|
|
389
395
|
</templates>
|
|
390
396
|
```
|
|
391
397
|
|
|
398
|
+
### Nested Layouts
|
|
399
|
+
|
|
400
|
+
Layouts can be nested in a parent-child hierarchy. Use `setParentLayout()` to define relationships:
|
|
401
|
+
|
|
402
|
+
```js
|
|
403
|
+
import { setParentLayout, getLayoutChain } from 'metaowl'
|
|
404
|
+
|
|
405
|
+
// Define layout hierarchy
|
|
406
|
+
setParentLayout('inner', 'middle')
|
|
407
|
+
setParentLayout('middle', 'outer')
|
|
408
|
+
|
|
409
|
+
// Get the full chain for a layout
|
|
410
|
+
const chain = getLayoutChain('inner')
|
|
411
|
+
// ['inner', 'middle', 'outer']
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Or use the `defineNestedLayout()` decorator:
|
|
415
|
+
|
|
416
|
+
```js
|
|
417
|
+
import { defineNestedLayout } from 'metaowl'
|
|
418
|
+
|
|
419
|
+
export class AdminDashboard extends Component {
|
|
420
|
+
static template = 'AdminDashboard'
|
|
421
|
+
static layout = 'admin-dashboard'
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
defineNestedLayout('admin-dashboard', 'admin-base')(AdminDashboard)
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
When a page uses `admin-dashboard` layout, it's rendered with: `outer > middle > admin-dashboard > page`.
|
|
428
|
+
|
|
392
429
|
---
|
|
393
430
|
|
|
394
431
|
## Navigation Guards
|
|
@@ -901,6 +938,98 @@ class MyComponent extends Component {
|
|
|
901
938
|
|
|
902
939
|
---
|
|
903
940
|
|
|
941
|
+
## Image Optimization
|
|
942
|
+
|
|
943
|
+
Responsive image handling with srcset generation, lazy loading, and placeholder support:
|
|
944
|
+
|
|
945
|
+
```js
|
|
946
|
+
import { Image } from 'metaowl'
|
|
947
|
+
|
|
948
|
+
// Generate srcset for responsive images
|
|
949
|
+
const srcset = Image.generateSrcSet('https://example.com/image.jpg', [320, 640, 960, 1280])
|
|
950
|
+
// "https://example.com/image.jpg?width=320&quality=80 320w, ..."
|
|
951
|
+
|
|
952
|
+
// Create a fully configured responsive image object
|
|
953
|
+
const responsive = Image.createResponsiveImage({
|
|
954
|
+
src: 'https://example.com/hero.jpg',
|
|
955
|
+
alt: 'Hero image',
|
|
956
|
+
widths: [640, 1024, 1920],
|
|
957
|
+
lazy: true,
|
|
958
|
+
placeholder: true
|
|
959
|
+
})
|
|
960
|
+
// Returns: { src, srcset, loading: 'lazy', decoding: 'async', placeholder, ... }
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
**Image utilities:**
|
|
964
|
+
|
|
965
|
+
| Function | Description |
|
|
966
|
+
|---|---|
|
|
967
|
+
| `generateSrcSet(src, widths, options)` | Generates responsive srcset string |
|
|
968
|
+
| `calculateAspectRatio(width, height)` | Computes aspect ratio as `W/H` |
|
|
969
|
+
| `generateSizesAttribute(src, breakpoints)` | Generates sizes attribute with breakpoints |
|
|
970
|
+
| `createResponsiveImage(options)` | Creates responsive image object |
|
|
971
|
+
| `prefetchImage(src)` / `prefetchImages(sources)` | Prefetch images for faster loading |
|
|
972
|
+
| `isImageLoaded(img)` | Check if image has finished loading |
|
|
973
|
+
| `getImageDimensions(src)` | Get natural width/height of image |
|
|
974
|
+
| `observeImageVisibility(img, callback)` | IntersectionObserver for lazy loading |
|
|
975
|
+
| `swapImageSource(img, newSrc, newSrcset)` | Swap src/srcset atomically |
|
|
976
|
+
| `generateDominantColorPlaceholder(src)` | Extract dominant color as placeholder |
|
|
977
|
+
|
|
978
|
+
---
|
|
979
|
+
|
|
980
|
+
## Font Optimization
|
|
981
|
+
|
|
982
|
+
Font loading and management with preloading and FOUT (Flash of Unstyled Text) handling:
|
|
983
|
+
|
|
984
|
+
```js
|
|
985
|
+
import { Fonts } from 'metaowl'
|
|
986
|
+
|
|
987
|
+
// Define and load a font
|
|
988
|
+
const fontFace = await Fonts.loadFont({
|
|
989
|
+
family: 'Inter',
|
|
990
|
+
src: 'https://fonts.example.com/inter.woff2',
|
|
991
|
+
weight: 'normal',
|
|
992
|
+
display: 'swap'
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
// Preload font for critical text
|
|
996
|
+
Fonts.preloadFont('Inter', 'https://fonts.example.com/inter.woff2')
|
|
997
|
+
|
|
998
|
+
// Generate @font-face CSS rule
|
|
999
|
+
const cssRule = Fonts.createFontFaceRule({
|
|
1000
|
+
family: 'Inter',
|
|
1001
|
+
src: 'https://fonts.example.com/inter.woff2',
|
|
1002
|
+
weight: 'bold',
|
|
1003
|
+
display: 'optional'
|
|
1004
|
+
})
|
|
1005
|
+
// @font-face { font-family: 'Inter'; src: url("..."); font-weight: bold; font-display: optional; }
|
|
1006
|
+
|
|
1007
|
+
// Inject font faces into document
|
|
1008
|
+
Fonts.injectFontFaceRules({
|
|
1009
|
+
family: 'Inter',
|
|
1010
|
+
src: ['woff2', 'woff'].map(f => `https://fonts.example.com/${f}`),
|
|
1011
|
+
weight: '400 700'
|
|
1012
|
+
})
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
**Font utilities:**
|
|
1016
|
+
|
|
1017
|
+
| Function | Description |
|
|
1018
|
+
|---|---|
|
|
1019
|
+
| `defineFontFace(options)` | Creates a FontFace object |
|
|
1020
|
+
| `loadFont(options)` | Loads font and tracks it |
|
|
1021
|
+
| `loadFontFamily(family, variants)` | Load multiple variants |
|
|
1022
|
+
| `isFontLoaded(family, weight?)` | Check if font is loaded |
|
|
1023
|
+
| `preloadFont(family, src, options)` | Add preload link for font |
|
|
1024
|
+
| `removeFontPreload(family, weight?)` | Remove preload link |
|
|
1025
|
+
| `createFontFaceRule(options)` | Generate @font-face CSS |
|
|
1026
|
+
| `injectFontFaceRules(options)` | Inject @font-face rules |
|
|
1027
|
+
| `measureTextWidth(text, font, size)` | Measure rendered text width |
|
|
1028
|
+
| `adjustFontForFout(el, fallback, timeout)` | Handle FOUT gracefully |
|
|
1029
|
+
| `getFontLoadStatus()` | Get loaded fonts status |
|
|
1030
|
+
|
|
1031
|
+
---
|
|
1032
|
+
|
|
904
1033
|
## CLI Reference
|
|
905
1034
|
|
|
906
1035
|
metaowl ships four CLI commands that use its own bundled Vite, Prettier, and ESLint binaries — no need to install them separately in your project.
|
|
@@ -1119,11 +1248,24 @@ Functions for layout management.
|
|
|
1119
1248
|
|
|
1120
1249
|
| Function | Description |
|
|
1121
1250
|
|---|---|
|
|
1122
|
-
| `registerLayout(name, Component)` | Register a layout |
|
|
1251
|
+
| `registerLayout(name, Component, options?)` | Register a layout |
|
|
1252
|
+
| `unregisterLayout(name)` | Remove a layout |
|
|
1123
1253
|
| `getLayout(name)` | Get layout component by name |
|
|
1254
|
+
| `hasLayout(name)` | Check if layout exists |
|
|
1255
|
+
| `getLayoutNames()` | Get all registered layout names |
|
|
1124
1256
|
| `setDefaultLayout(name)` | Set default layout |
|
|
1257
|
+
| `getDefaultLayout()` | Get default layout name |
|
|
1125
1258
|
| `resolveLayout(Component, path?)` | Resolve layout for component |
|
|
1259
|
+
| `setRouteLayout(routePath, layoutName)` | Set layout for a route |
|
|
1260
|
+
| `getRouteLayout(routePath)` | Get layout for a route |
|
|
1261
|
+
| `setParentLayout(layoutName, parentName)` | Set parent-child relationship |
|
|
1262
|
+
| `getParentLayout(layoutName)` | Get parent layout name |
|
|
1263
|
+
| `getLayoutChain(layoutName)` | Get full layout chain |
|
|
1264
|
+
| `createNestedLayoutWrapper(layouts, page, props)` | Create nested layout wrapper |
|
|
1126
1265
|
| `subscribeToLayouts(callback)` | Listen to layout events |
|
|
1266
|
+
| `clearLayouts()` | Clear all layouts |
|
|
1267
|
+
| `defineLayout(name, options?)` | Decorator for layout |
|
|
1268
|
+
| `defineNestedLayout(name, parent, options?)` | Decorator for nested layout |
|
|
1127
1269
|
|
|
1128
1270
|
**Component Layout Property:**
|
|
1129
1271
|
|
|
@@ -1458,6 +1600,117 @@ const { value, set, get, remove, clear } = useCache('user-prefs', {})
|
|
|
1458
1600
|
|
|
1459
1601
|
---
|
|
1460
1602
|
|
|
1603
|
+
### `Image API`
|
|
1604
|
+
|
|
1605
|
+
Image optimization utilities.
|
|
1606
|
+
|
|
1607
|
+
```ts
|
|
1608
|
+
import { Image } from 'metaowl'
|
|
1609
|
+
|
|
1610
|
+
// Generate srcset for responsive images
|
|
1611
|
+
Image.generateSrcSet('https://example.com/image.jpg', [320, 640, 960])
|
|
1612
|
+
|
|
1613
|
+
// Calculate aspect ratio
|
|
1614
|
+
Image.calculateAspectRatio(1920, 1080) // "16/9"
|
|
1615
|
+
|
|
1616
|
+
// Generate sizes attribute
|
|
1617
|
+
Image.generateSizesAttribute('https://example.com/image.jpg', {
|
|
1618
|
+
'(min-width: 1024px)': 1000,
|
|
1619
|
+
'(min-width: 768px)': 800
|
|
1620
|
+
})
|
|
1621
|
+
|
|
1622
|
+
// Create responsive image object
|
|
1623
|
+
const responsive = Image.createResponsiveImage({
|
|
1624
|
+
src: 'https://example.com/hero.jpg',
|
|
1625
|
+
alt: 'Hero',
|
|
1626
|
+
widths: [320, 640, 1024],
|
|
1627
|
+
lazy: true,
|
|
1628
|
+
placeholder: true
|
|
1629
|
+
})
|
|
1630
|
+
|
|
1631
|
+
// Prefetch images
|
|
1632
|
+
await Image.prefetchImages(['/img1.jpg', '/img2.jpg'])
|
|
1633
|
+
|
|
1634
|
+
// Check if loaded
|
|
1635
|
+
Image.isImageLoaded(imgElement)
|
|
1636
|
+
|
|
1637
|
+
// Get dimensions
|
|
1638
|
+
const dims = await Image.getImageDimensions('https://example.com/image.jpg')
|
|
1639
|
+
|
|
1640
|
+
// Observe visibility
|
|
1641
|
+
const observer = Image.observeImageVisibility(imgElement, (visible) => {
|
|
1642
|
+
if (visible) loadHighResImage()
|
|
1643
|
+
})
|
|
1644
|
+
|
|
1645
|
+
// Swap source
|
|
1646
|
+
Image.swapImageSource(imgElement, '/new-image.jpg', '/new-image-2x.jpg 2x')
|
|
1647
|
+
|
|
1648
|
+
// Generate dominant color placeholder
|
|
1649
|
+
const placeholder = await Image.generateDominantColorPlaceholder(src)
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
---
|
|
1653
|
+
|
|
1654
|
+
### `Fonts API`
|
|
1655
|
+
|
|
1656
|
+
Font optimization utilities.
|
|
1657
|
+
|
|
1658
|
+
```ts
|
|
1659
|
+
import { Fonts } from 'metaowl'
|
|
1660
|
+
|
|
1661
|
+
// Define a font face
|
|
1662
|
+
const fontFace = Fonts.defineFontFace({
|
|
1663
|
+
family: 'Inter',
|
|
1664
|
+
src: 'https://fonts.example.com/inter.woff2',
|
|
1665
|
+
weight: '400',
|
|
1666
|
+
style: 'normal',
|
|
1667
|
+
display: 'swap'
|
|
1668
|
+
})
|
|
1669
|
+
|
|
1670
|
+
// Load and track a font
|
|
1671
|
+
await Fonts.loadFont({
|
|
1672
|
+
family: 'Inter',
|
|
1673
|
+
src: 'https://fonts.example.com/inter.woff2'
|
|
1674
|
+
})
|
|
1675
|
+
|
|
1676
|
+
// Check if loaded
|
|
1677
|
+
Fonts.isFontLoaded('Inter') // true
|
|
1678
|
+
|
|
1679
|
+
// Preload a font
|
|
1680
|
+
Fonts.preloadFont('Inter', 'https://fonts.example.com/inter.woff2')
|
|
1681
|
+
|
|
1682
|
+
// Remove preload link
|
|
1683
|
+
Fonts.removeFontPreload('Inter')
|
|
1684
|
+
|
|
1685
|
+
// Generate @font-face CSS rule
|
|
1686
|
+
const rule = Fonts.createFontFaceRule({
|
|
1687
|
+
family: 'Inter',
|
|
1688
|
+
src: 'https://fonts.example.com/inter.woff2',
|
|
1689
|
+
weight: 'bold'
|
|
1690
|
+
})
|
|
1691
|
+
|
|
1692
|
+
// Inject @font-face rules into document
|
|
1693
|
+
Fonts.injectFontFaceRules({
|
|
1694
|
+
family: 'Inter',
|
|
1695
|
+
src: ['woff2', 'woff'].map(f => `https://fonts.example.com/${f}`),
|
|
1696
|
+
weight: '400 700'
|
|
1697
|
+
})
|
|
1698
|
+
|
|
1699
|
+
// Measure text width
|
|
1700
|
+
Fonts.measureTextWidth('Hello', 'Inter', 16)
|
|
1701
|
+
|
|
1702
|
+
// Adjust for FOUT
|
|
1703
|
+
await Fonts.adjustFontForFout(element, 'sans-serif', 3000)
|
|
1704
|
+
|
|
1705
|
+
// Get loaded font status
|
|
1706
|
+
Fonts.getFontLoadStatus() // { 'Inter': true }
|
|
1707
|
+
|
|
1708
|
+
// Clear all loaded fonts
|
|
1709
|
+
Fonts.clearLoadedFonts()
|
|
1710
|
+
```
|
|
1711
|
+
|
|
1712
|
+
---
|
|
1713
|
+
|
|
1461
1714
|
## Vite Plugin
|
|
1462
1715
|
|
|
1463
1716
|
### `metaowlPlugin(options)`
|
package/build/runtime/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mountApp, configureOwl } from './modules/app-mounter.js';
|
|
2
|
-
import { buildLayouts, clearLayouts, createLayoutWrapper, defineLayout, discoverLayouts, getCurrentLayout, getDefaultLayout, getLayout, getLayoutNames, getRouteLayout, hasLayout, layout, mountWithLayout, registerLayout, resolveLayout, setDefaultLayout, setRouteLayout, subscribeToLayouts, unregisterLayout } from './modules/layouts.js';
|
|
2
|
+
import { buildLayouts, clearLayouts, createLayoutWrapper, createNestedLayoutWrapper, defineLayout, defineNestedLayout, discoverLayouts, getCurrentLayout, getDefaultLayout, getLayout, getLayoutChain, getLayoutNames, getParentLayout, getRouteLayout, hasLayout, layout, mountWithLayout, registerLayout, resolveLayout, setDefaultLayout, setParentLayout, setRouteLayout, subscribeToLayouts, unregisterLayout } from './modules/layouts.js';
|
|
3
3
|
import { afterEach, _setSpaNavigationCallback, back, beforeEach, cancelNavigation, forward, getCurrentRoute, getPreviousRoute, go, isNavigating, isSpaMode, navigate, navigateTo, processRoutes, push, replace, router, setSpaMode } from './modules/router.js';
|
|
4
4
|
import { Link, registerLinkTemplate } from './modules/link.js';
|
|
5
5
|
import { buildRoutes, createCatchAllRoute, createRedirectRoute, defineRoute, findRoute, generateUrl, isDynamicRoute, matchRoute, route, validateRouteParams } from './modules/file-router.js';
|
|
@@ -13,6 +13,8 @@ import { createCanonicalUrl, generateOpenGraph, generateRobotsTxt, generateSitem
|
|
|
13
13
|
import { cache, checkCapabilities, generateManifest, getStorageInfo, isOnline, isStandalone, PWA, registerServiceWorker, requestPersistentStorage, showNotification, subscribeToConnectivity, subscribeToPush, sync, unregisterServiceWorker, unsubscribeFromPush } from './modules/pwa.js';
|
|
14
14
|
import Cache from './modules/cache.js';
|
|
15
15
|
import Fetch from './modules/fetch.js';
|
|
16
|
+
import * as Fonts from './modules/fonts.js';
|
|
17
|
+
import { ImageOptimizer } from './modules/image.js';
|
|
16
18
|
import * as Meta from './modules/meta.js';
|
|
17
19
|
import { Store, createPersistencePlugin, createStore } from './modules/store.js';
|
|
18
20
|
let appRoutes = null;
|
|
@@ -127,7 +129,7 @@ export async function boot(routesOrModules = {}, layoutsOrModules = null, option
|
|
|
127
129
|
await mountApp(resolvedRoute);
|
|
128
130
|
}
|
|
129
131
|
export { Fetch, Cache, configureOwl, Meta, buildRoutes, Store, createPersistencePlugin, createStore };
|
|
130
|
-
export { registerLayout, unregisterLayout, getLayout, hasLayout, getLayoutNames, setDefaultLayout, getDefaultLayout, resolveLayout, setRouteLayout, getRouteLayout, createLayoutWrapper, mountWithLayout, getCurrentLayout, subscribeToLayouts, clearLayouts, layout, defineLayout, buildLayouts, discoverLayouts };
|
|
132
|
+
export { registerLayout, unregisterLayout, getLayout, hasLayout, getLayoutNames, setDefaultLayout, getDefaultLayout, resolveLayout, setRouteLayout, getRouteLayout, createLayoutWrapper, createNestedLayoutWrapper, mountWithLayout, getCurrentLayout, subscribeToLayouts, clearLayouts, layout, defineLayout, defineNestedLayout, buildLayouts, discoverLayouts, setParentLayout, getParentLayout, getLayoutChain };
|
|
131
133
|
export { processRoutes, beforeEach, afterEach, getCurrentRoute, getPreviousRoute, isNavigating, cancelNavigation, navigate, navigateTo, push, replace, back, forward, go, router, setSpaMode, isSpaMode };
|
|
132
134
|
export { Link, registerLinkTemplate };
|
|
133
135
|
export { matchRoute, isDynamicRoute, findRoute, generateUrl, validateRouteParams, createCatchAllRoute, createRedirectRoute, defineRoute, route };
|
|
@@ -139,3 +141,4 @@ export { useAuth, useLocalStorage, useFetch, useDebounce, useThrottle, useWindow
|
|
|
139
141
|
export { createMockStore, mockRouter, mountComponent, wait, nextTick, flushPromises, userEvent, dom, TestUtils };
|
|
140
142
|
export { generateSitemap, generateRobotsTxt, jsonLd, createCanonicalUrl, generateOpenGraph, generateTwitterCard, validateSitemap, getPriorityByDepth, generateSitemapIndex, SEO };
|
|
141
143
|
export { generateManifest, registerServiceWorker, unregisterServiceWorker, isStandalone, isOnline, subscribeToConnectivity, requestPersistentStorage, getStorageInfo, sync, subscribeToPush, unsubscribeFromPush, showNotification, cache, checkCapabilities, PWA };
|
|
144
|
+
export { ImageOptimizer as Image, Fonts };
|
|
@@ -16,7 +16,15 @@ let config = { ...defaults };
|
|
|
16
16
|
let currentApp = null;
|
|
17
17
|
let cachedTemplates = null;
|
|
18
18
|
export function configureOwl(nextConfig) {
|
|
19
|
-
config = {
|
|
19
|
+
config = {
|
|
20
|
+
...defaults,
|
|
21
|
+
...nextConfig,
|
|
22
|
+
translatableAttributes: nextConfig.translatableAttributes
|
|
23
|
+
? Array.isArray(nextConfig.translatableAttributes)
|
|
24
|
+
? nextConfig.translatableAttributes
|
|
25
|
+
: [nextConfig.translatableAttributes]
|
|
26
|
+
: defaults.translatableAttributes
|
|
27
|
+
};
|
|
20
28
|
}
|
|
21
29
|
export async function mountApp(route) {
|
|
22
30
|
const components = typeof COMPONENTS !== 'undefined' ? COMPONENTS : [];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const MAGIC_STRINGS = {
|
|
2
|
+
STORE_SESSION_KEY: 'metaowl:odoo:session',
|
|
3
|
+
STORE_CSRF_KEY: 'metaowl:odoo:csrf',
|
|
4
|
+
MOUNT_ELEMENT_ID: 'metaowl',
|
|
5
|
+
LINK_TEMPLATE_NAME: 'Link'
|
|
6
|
+
};
|
|
7
|
+
export const ROUTE_PATTERN_CONFIG = {
|
|
8
|
+
optionalParam: /\/:([^/(]+)\?/g,
|
|
9
|
+
catchAll: /\/:([^/(]+)\(\.\*\)/g,
|
|
10
|
+
namedParam: /:([^/(?\s]+)/g,
|
|
11
|
+
wildcard: /\*/g,
|
|
12
|
+
separator: /\//g
|
|
13
|
+
};
|
|
14
|
+
export function buildRouteRegexPattern(path) {
|
|
15
|
+
const pattern = path
|
|
16
|
+
.replace(ROUTE_PATTERN_CONFIG.separator, '\\/')
|
|
17
|
+
.replace(ROUTE_PATTERN_CONFIG.catchAll, '/(.*)')
|
|
18
|
+
.replace(ROUTE_PATTERN_CONFIG.optionalParam, '(?:/([^/]+))?')
|
|
19
|
+
.replace(ROUTE_PATTERN_CONFIG.namedParam, '([^/]+)')
|
|
20
|
+
.replace(ROUTE_PATTERN_CONFIG.wildcard, '(.*)');
|
|
21
|
+
return '^' + pattern + '$';
|
|
22
|
+
}
|
|
23
|
+
export function buildSimpleRoutePattern(routePath) {
|
|
24
|
+
const pattern = routePath
|
|
25
|
+
.replace(ROUTE_PATTERN_CONFIG.separator, '\\/')
|
|
26
|
+
.replace(ROUTE_PATTERN_CONFIG.catchAll, '(.*)')
|
|
27
|
+
.replace(ROUTE_PATTERN_CONFIG.optionalParam, '(?:/([^/]+))?')
|
|
28
|
+
.replace(ROUTE_PATTERN_CONFIG.namedParam, '([^/]+)')
|
|
29
|
+
.replace(ROUTE_PATTERN_CONFIG.wildcard, '(.*)');
|
|
30
|
+
return '^' + pattern + '$';
|
|
31
|
+
}
|
|
32
|
+
export function normalizeRoutePath(path) {
|
|
33
|
+
return (path.replace(/\/$/, '') || '/');
|
|
34
|
+
}
|
|
35
|
+
export function normalizePathForComparison(path) {
|
|
36
|
+
return normalizeRoutePath(path);
|
|
37
|
+
}
|
|
38
|
+
export const EXTERNAL_URL_REGEX = /^(https?:|\/\/|mailto:|tel:|ftp:|file:|javascript:)/i;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* File-based routing with dynamic route parameter support.
|
|
5
5
|
*/
|
|
6
|
+
import { buildRouteRegexPattern } from './constants.js';
|
|
6
7
|
export function pathFromKey(key) {
|
|
7
8
|
const relativePath = key.replace(/^\.\/pages\//, '');
|
|
8
9
|
const parts = relativePath.split('/');
|
|
@@ -43,11 +44,7 @@ function extractParamNames(filePath) {
|
|
|
43
44
|
return params;
|
|
44
45
|
}
|
|
45
46
|
function buildRegexPattern(path) {
|
|
46
|
-
|
|
47
|
-
pattern = pattern.replace(/:([^/(]+)\(\.\*\)/g, '([^/]+(?:/[^/]+)*)');
|
|
48
|
-
pattern = pattern.replace(/\/:([^/(]+)\?/g, '(?:/([^/]+))?');
|
|
49
|
-
pattern = pattern.replace(/:([^/(\s]+)/g, '([^/]+)');
|
|
50
|
-
return '^' + pattern + '$';
|
|
47
|
+
return buildRouteRegexPattern(path);
|
|
51
48
|
}
|
|
52
49
|
export function matchRoute(pattern, path) {
|
|
53
50
|
const paramNames = [];
|
|
@@ -74,24 +71,29 @@ export function isDynamicRoute(path) {
|
|
|
74
71
|
return path.includes(':');
|
|
75
72
|
}
|
|
76
73
|
function componentFromModule(mod, key) {
|
|
77
|
-
if (typeof mod.default === 'function')
|
|
74
|
+
if (typeof mod.default === 'function') {
|
|
78
75
|
return mod.default;
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
}
|
|
77
|
+
const funcs = Object.values(mod).filter((v) => typeof v === 'function');
|
|
78
|
+
if (funcs.length === 0) {
|
|
81
79
|
throw new Error(`[metaowl] No component export found in "${key}"`);
|
|
82
80
|
}
|
|
83
|
-
return
|
|
81
|
+
return funcs[0];
|
|
84
82
|
}
|
|
85
83
|
export function buildRoutes(modules) {
|
|
86
84
|
const routes = [];
|
|
85
|
+
const nameCounts = {};
|
|
87
86
|
for (const [key, mod] of Object.entries(modules)) {
|
|
88
87
|
const derivedPath = pathFromKey(key);
|
|
89
88
|
const component = componentFromModule(mod, key);
|
|
90
89
|
const routeConfig = component.route || {};
|
|
91
90
|
const routePath = typeof routeConfig.path === 'string' ? routeConfig.path : derivedPath;
|
|
92
|
-
const
|
|
91
|
+
const baseName = routePath === '/'
|
|
93
92
|
? 'index'
|
|
94
93
|
: routePath.slice(1).replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
94
|
+
const routeName = nameCounts[baseName] !== undefined
|
|
95
|
+
? `${baseName}-${++nameCounts[baseName]}`
|
|
96
|
+
: (nameCounts[baseName] = 0, baseName);
|
|
95
97
|
const route = {
|
|
96
98
|
name: routeName,
|
|
97
99
|
path: [routePath],
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Fonts
|
|
3
|
+
*
|
|
4
|
+
* Font optimization utilities for metaowl applications.
|
|
5
|
+
* Provides font loading strategies, font face declarations, and font display optimization.
|
|
6
|
+
*/
|
|
7
|
+
const loadedFonts = new Map();
|
|
8
|
+
const fontPreloadLinks = new Map();
|
|
9
|
+
export function defineFontFace(options) {
|
|
10
|
+
const { family, src, weight = 'normal', style = 'normal', display = 'swap', unicodeRange } = options;
|
|
11
|
+
const srcString = Array.isArray(src) ? src.map((s) => `url("${s}")`).join(', ') : `url("${src}")`;
|
|
12
|
+
const weightStr = Array.isArray(weight) ? weight.join(' ') : weight;
|
|
13
|
+
const descriptors = {
|
|
14
|
+
weight: weightStr,
|
|
15
|
+
style,
|
|
16
|
+
display,
|
|
17
|
+
unicodeRange: unicodeRange || 'U+0-FFFF'
|
|
18
|
+
};
|
|
19
|
+
const fontFace = new FontFace(`${family}-${weightStr}`, srcString, descriptors);
|
|
20
|
+
return fontFace;
|
|
21
|
+
}
|
|
22
|
+
export async function loadFont(options) {
|
|
23
|
+
const { family, weight = 'normal' } = options;
|
|
24
|
+
const key = `${family}-${weight}`;
|
|
25
|
+
if (!loadedFonts.has(family)) {
|
|
26
|
+
loadedFonts.set(family, new Set());
|
|
27
|
+
}
|
|
28
|
+
const fontFace = defineFontFace(options);
|
|
29
|
+
try {
|
|
30
|
+
await fontFace.load();
|
|
31
|
+
document.fonts.add(fontFace);
|
|
32
|
+
const familySet = loadedFonts.get(family);
|
|
33
|
+
familySet.add(key);
|
|
34
|
+
return fontFace;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.warn(`[metaowl] Failed to load font ${family}:`, error);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function loadFontFamily(family, variants) {
|
|
42
|
+
return Promise.all(variants.map((variant) => loadFont({ ...variant, family })));
|
|
43
|
+
}
|
|
44
|
+
export function isFontLoaded(family, weight) {
|
|
45
|
+
if (!loadedFonts.has(family)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (weight) {
|
|
49
|
+
return loadedFonts.get(family).has(`${family}-${weight}`);
|
|
50
|
+
}
|
|
51
|
+
return loadedFonts.get(family).size > 0;
|
|
52
|
+
}
|
|
53
|
+
export function preloadFont(family, src, options = {}) {
|
|
54
|
+
const { weight, as = 'font', type = 'font/woff2' } = options;
|
|
55
|
+
const linkId = `metaowl-font-preload-${family}-${weight || 'normal'}`;
|
|
56
|
+
if (document.getElementById(linkId)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const link = document.createElement('link');
|
|
60
|
+
link.id = linkId;
|
|
61
|
+
link.rel = 'preload';
|
|
62
|
+
link.as = as;
|
|
63
|
+
link.href = src;
|
|
64
|
+
link.crossOrigin = 'anonymous';
|
|
65
|
+
if (type) {
|
|
66
|
+
link.type = type;
|
|
67
|
+
}
|
|
68
|
+
document.head.appendChild(link);
|
|
69
|
+
fontPreloadLinks.set(linkId, link);
|
|
70
|
+
}
|
|
71
|
+
export function removeFontPreload(family, weight) {
|
|
72
|
+
const linkId = `metaowl-font-preload-${family}-${weight || 'normal'}`;
|
|
73
|
+
const link = fontPreloadLinks.get(linkId);
|
|
74
|
+
if (link) {
|
|
75
|
+
link.remove();
|
|
76
|
+
fontPreloadLinks.delete(linkId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function generateFontDisplayStyle(display) {
|
|
80
|
+
return `font-display: ${display};`;
|
|
81
|
+
}
|
|
82
|
+
export function createFontFaceRule(options) {
|
|
83
|
+
const { family, src, weight = 'normal', style = 'normal', display = 'swap', unicodeRange } = options;
|
|
84
|
+
const srcString = Array.isArray(src) ? src.map((s) => `url("${s}")`).join(', ') : `url("${src}")`;
|
|
85
|
+
const weightStr = Array.isArray(weight) ? weight.join(' ') : weight;
|
|
86
|
+
let rule = '@font-face {\n';
|
|
87
|
+
rule += ` font-family: '${family}';\n`;
|
|
88
|
+
rule += ` src: ${srcString};\n`;
|
|
89
|
+
rule += ` font-weight: ${weightStr};\n`;
|
|
90
|
+
rule += ` font-style: ${style};\n`;
|
|
91
|
+
rule += ` font-display: ${display};\n`;
|
|
92
|
+
if (unicodeRange) {
|
|
93
|
+
rule += ` unicode-range: ${unicodeRange};\n`;
|
|
94
|
+
}
|
|
95
|
+
rule += '}';
|
|
96
|
+
return rule;
|
|
97
|
+
}
|
|
98
|
+
export function injectFontFaceRules(options) {
|
|
99
|
+
const rules = Array.isArray(options) ? options : [options];
|
|
100
|
+
const styleId = 'metaowl-font-faces';
|
|
101
|
+
let styleEl = document.getElementById(styleId);
|
|
102
|
+
if (!styleEl) {
|
|
103
|
+
styleEl = document.createElement('style');
|
|
104
|
+
styleEl.id = styleId;
|
|
105
|
+
document.head.appendChild(styleEl);
|
|
106
|
+
}
|
|
107
|
+
const cssText = rules.map(createFontFaceRule).join('\n\n');
|
|
108
|
+
styleEl.textContent += cssText;
|
|
109
|
+
}
|
|
110
|
+
export function measureTextWidth(text, font, size = 16) {
|
|
111
|
+
const canvas = document.createElement('canvas');
|
|
112
|
+
const ctx = canvas.getContext('2d');
|
|
113
|
+
if (!ctx) {
|
|
114
|
+
const numericSize = typeof size === 'string' ? parseInt(size, 10) : size;
|
|
115
|
+
return text.length * numericSize * 0.5;
|
|
116
|
+
}
|
|
117
|
+
const fontString = typeof size === 'number' ? `${size}px ${font}` : `${size} ${font}`;
|
|
118
|
+
ctx.font = fontString;
|
|
119
|
+
return ctx.measureText(text).width;
|
|
120
|
+
}
|
|
121
|
+
export function estimateFontMetrics(el) {
|
|
122
|
+
const style = window.getComputedStyle(el);
|
|
123
|
+
const family = style.fontFamily;
|
|
124
|
+
const fontMetricsMap = {
|
|
125
|
+
'Arial': { family: 'Arial', ascent: 0.8, descent: 0.2, lineGap: 0, unitsPerEm: 2048 },
|
|
126
|
+
'Helvetica': { family: 'Helvetica', ascent: 0.8, descent: 0.2, lineGap: 0, unitsPerEm: 2048 },
|
|
127
|
+
'Times New Roman': { family: 'Times New Roman', ascent: 0.8, descent: 0.2, lineGap: 0, unitsPerEm: 2048 },
|
|
128
|
+
'Georgia': { family: 'Georgia', ascent: 0.8, descent: 0.2, lineGap: 0, unitsPerEm: 2048 },
|
|
129
|
+
'system-ui': { family: 'system-ui', ascent: 0.8, descent: 0.2, lineGap: 0, unitsPerEm: 2048 }
|
|
130
|
+
};
|
|
131
|
+
return fontMetricsMap[family] || null;
|
|
132
|
+
}
|
|
133
|
+
export function adjustFontForFout(el, _fallbackFont = 'sans-serif', timeout = 3000) {
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
el.style.setProperty('font-display', 'block');
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
el.classList.add('metaowl-font-fout');
|
|
138
|
+
resolve();
|
|
139
|
+
}, timeout);
|
|
140
|
+
document.fonts.ready.then(() => {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
el.classList.add('metaowl-font-loaded');
|
|
143
|
+
resolve();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
export function getFontLoadStatus() {
|
|
148
|
+
const status = {};
|
|
149
|
+
for (const [family, weights] of loadedFonts.entries()) {
|
|
150
|
+
status[family] = weights.size > 0;
|
|
151
|
+
}
|
|
152
|
+
return status;
|
|
153
|
+
}
|
|
154
|
+
export function clearLoadedFonts() {
|
|
155
|
+
loadedFonts.clear();
|
|
156
|
+
}
|
|
157
|
+
export const Fonts = {
|
|
158
|
+
defineFontFace,
|
|
159
|
+
loadFont,
|
|
160
|
+
loadFontFamily,
|
|
161
|
+
isFontLoaded,
|
|
162
|
+
preloadFont,
|
|
163
|
+
removeFontPreload,
|
|
164
|
+
generateFontDisplayStyle,
|
|
165
|
+
createFontFaceRule,
|
|
166
|
+
injectFontFaceRules,
|
|
167
|
+
measureTextWidth,
|
|
168
|
+
estimateFontMetrics,
|
|
169
|
+
adjustFontForFout,
|
|
170
|
+
getFontLoadStatus,
|
|
171
|
+
clearLoadedFonts
|
|
172
|
+
};
|
|
@@ -57,6 +57,17 @@ export async function loadLocaleMessages(locale, messages) {
|
|
|
57
57
|
state.loading = false;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
export async function load(options) {
|
|
61
|
+
const { locale, messages, fallbackLocale } = options;
|
|
62
|
+
if (fallbackLocale) {
|
|
63
|
+
state.fallbackLocale = fallbackLocale;
|
|
64
|
+
}
|
|
65
|
+
state.locale = locale;
|
|
66
|
+
document.documentElement.lang = locale;
|
|
67
|
+
if (messages) {
|
|
68
|
+
await loadLocaleMessages(locale, messages);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
60
71
|
export function t(key, values = {}, defaultMessage) {
|
|
61
72
|
const locale = state.locale;
|
|
62
73
|
const fallbackLocale = state.fallbackLocale;
|
|
@@ -138,6 +149,8 @@ export const i18n = {
|
|
|
138
149
|
get messages() { return state.messages; },
|
|
139
150
|
configure: configureI18n,
|
|
140
151
|
setLocale,
|
|
152
|
+
load,
|
|
153
|
+
loadLocaleMessages,
|
|
141
154
|
t,
|
|
142
155
|
formatDate,
|
|
143
156
|
formatNumber,
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Image
|
|
3
|
+
*
|
|
4
|
+
* Image optimization utilities for metaowl applications.
|
|
5
|
+
* Provides lazy loading, responsive srcset generation, and placeholder support.
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_WIDTHS = [320, 640, 960, 1280, 1600, 1920];
|
|
8
|
+
const DEFAULT_QUALITY = 80;
|
|
9
|
+
export function generateSrcSet(baseSrc, widths = DEFAULT_WIDTHS, options = {}) {
|
|
10
|
+
const { format = 'original', quality = DEFAULT_QUALITY } = options;
|
|
11
|
+
const srcsetParts = [];
|
|
12
|
+
for (const width of widths) {
|
|
13
|
+
const url = buildOptimizedUrl(baseSrc, width, format, quality);
|
|
14
|
+
srcsetParts.push(`${url} ${width}w`);
|
|
15
|
+
}
|
|
16
|
+
return srcsetParts.join(', ');
|
|
17
|
+
}
|
|
18
|
+
function buildOptimizedUrl(baseSrc, width, format, quality) {
|
|
19
|
+
try {
|
|
20
|
+
const url = new URL(baseSrc);
|
|
21
|
+
if (format !== 'original') {
|
|
22
|
+
url.searchParams.set('format', format);
|
|
23
|
+
}
|
|
24
|
+
url.searchParams.set('width', String(width));
|
|
25
|
+
url.searchParams.set('quality', String(quality));
|
|
26
|
+
return url.toString();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
const separator = baseSrc.includes('?') ? '&' : '?';
|
|
30
|
+
return `${baseSrc}${separator}width=${width}&quality=${quality}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function calculateAspectRatio(width, height) {
|
|
34
|
+
const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
|
|
35
|
+
const divisor = gcd(width, height);
|
|
36
|
+
return `${width / divisor}/${height / divisor}`;
|
|
37
|
+
}
|
|
38
|
+
export function generateSizesAttribute(src, breakpoints = {}) {
|
|
39
|
+
const defaultBreakpoints = {
|
|
40
|
+
'(min-width: 1280px)': 1200,
|
|
41
|
+
'(min-width: 1024px)': 1000,
|
|
42
|
+
'(min-width: 768px)': 720,
|
|
43
|
+
'(min-width: 480px)': 480
|
|
44
|
+
};
|
|
45
|
+
const merged = { ...defaultBreakpoints, ...breakpoints };
|
|
46
|
+
const parts = [];
|
|
47
|
+
for (const [condition, size] of Object.entries(merged)) {
|
|
48
|
+
parts.push(`${condition} ${size}px`);
|
|
49
|
+
}
|
|
50
|
+
parts.push(`${Math.min(...Object.values(merged))}px`);
|
|
51
|
+
return parts.join(', ');
|
|
52
|
+
}
|
|
53
|
+
export function createResponsiveImage(options) {
|
|
54
|
+
const { src, alt = '', widths = DEFAULT_WIDTHS, format = 'original', quality = DEFAULT_QUALITY, lazy = true, placeholder = false, placeholderType = 'blur' } = options;
|
|
55
|
+
const srcset = generateSrcSet(src, widths, { format, quality });
|
|
56
|
+
const filteredWidths = widths.filter((w) => w <= 1920);
|
|
57
|
+
const defaultWidth = filteredWidths.length > 0 ? filteredWidths[0] : widths[0];
|
|
58
|
+
const result = {
|
|
59
|
+
src: buildOptimizedUrl(src, defaultWidth, format, quality),
|
|
60
|
+
srcset,
|
|
61
|
+
width: defaultWidth,
|
|
62
|
+
height: 0,
|
|
63
|
+
alt,
|
|
64
|
+
loading: lazy ? 'lazy' : 'eager',
|
|
65
|
+
decoding: 'async'
|
|
66
|
+
};
|
|
67
|
+
if (placeholder) {
|
|
68
|
+
result.placeholder = placeholderType === 'blur' ? buildPlaceholderBlur(src) : undefined;
|
|
69
|
+
result.blurDataURL = result.placeholder;
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
function buildPlaceholderBlur(src) {
|
|
74
|
+
try {
|
|
75
|
+
const url = new URL(src);
|
|
76
|
+
url.searchParams.set('w', '10');
|
|
77
|
+
url.searchParams.set('q', '10');
|
|
78
|
+
url.searchParams.set('blur', '10');
|
|
79
|
+
return url.toString();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return src;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function prefetchImage(src) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const img = new Image();
|
|
88
|
+
img.onload = () => resolve();
|
|
89
|
+
img.onerror = reject;
|
|
90
|
+
img.src = src;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export async function prefetchImages(sources) {
|
|
94
|
+
await Promise.all(sources.map(prefetchImage));
|
|
95
|
+
}
|
|
96
|
+
export function isImageLoaded(img) {
|
|
97
|
+
return img.complete && img.naturalHeight > 0;
|
|
98
|
+
}
|
|
99
|
+
export function getImageDimensions(src) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const img = new Image();
|
|
102
|
+
img.onload = () => {
|
|
103
|
+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
|
104
|
+
};
|
|
105
|
+
img.onerror = reject;
|
|
106
|
+
img.src = src;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export function observeImageVisibility(img, callback, options = {}) {
|
|
110
|
+
const defaultOptions = {
|
|
111
|
+
root: null,
|
|
112
|
+
rootMargin: '50px',
|
|
113
|
+
threshold: 0.01,
|
|
114
|
+
...options
|
|
115
|
+
};
|
|
116
|
+
const observer = new IntersectionObserver((entries) => {
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
callback(entry.isIntersecting);
|
|
119
|
+
}
|
|
120
|
+
}, defaultOptions);
|
|
121
|
+
observer.observe(img);
|
|
122
|
+
return observer;
|
|
123
|
+
}
|
|
124
|
+
export function swapImageSource(img, newSrc, newSrcset) {
|
|
125
|
+
if (newSrcset) {
|
|
126
|
+
img.srcset = newSrcset;
|
|
127
|
+
}
|
|
128
|
+
if (newSrc) {
|
|
129
|
+
img.src = newSrc;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function generateDominantColorPlaceholder(src) {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
const img = new Image();
|
|
135
|
+
img.crossOrigin = 'anonymous';
|
|
136
|
+
img.onload = () => {
|
|
137
|
+
const canvas = document.createElement('canvas');
|
|
138
|
+
const ctx = canvas.getContext('2d');
|
|
139
|
+
if (!ctx) {
|
|
140
|
+
resolve({ type: 'solid', value: '#cccccc' });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
canvas.width = 1;
|
|
144
|
+
canvas.height = 1;
|
|
145
|
+
ctx.drawImage(img, 0, 0, 1, 1);
|
|
146
|
+
try {
|
|
147
|
+
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
|
148
|
+
resolve({
|
|
149
|
+
type: 'dominant',
|
|
150
|
+
value: `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
resolve({ type: 'solid', value: '#cccccc' });
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
img.onerror = () => {
|
|
158
|
+
resolve({ type: 'solid', value: '#cccccc' });
|
|
159
|
+
};
|
|
160
|
+
img.src = src;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
export const ImageOptimizer = {
|
|
164
|
+
generateSrcSet,
|
|
165
|
+
calculateAspectRatio,
|
|
166
|
+
generateSizesAttribute,
|
|
167
|
+
createResponsiveImage,
|
|
168
|
+
prefetchImage,
|
|
169
|
+
prefetchImages,
|
|
170
|
+
isImageLoaded,
|
|
171
|
+
getImageDimensions,
|
|
172
|
+
observeImageVisibility,
|
|
173
|
+
swapImageSource,
|
|
174
|
+
generateDominantColorPlaceholder
|
|
175
|
+
};
|
|
@@ -63,6 +63,32 @@ export function setRouteLayout(routePath, layoutName) {
|
|
|
63
63
|
export function getRouteLayout(routePath) {
|
|
64
64
|
return routeLayouts.get(routePath);
|
|
65
65
|
}
|
|
66
|
+
export function setParentLayout(layoutName, parentLayoutName) {
|
|
67
|
+
const layout = getLayout(layoutName);
|
|
68
|
+
if (!layout) {
|
|
69
|
+
console.warn(`[metaowl] Cannot set parent for unregistered layout "${layoutName}"`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
;
|
|
73
|
+
layout.parentLayout = parentLayoutName;
|
|
74
|
+
}
|
|
75
|
+
export function getParentLayout(layoutName) {
|
|
76
|
+
const layout = getLayout(layoutName);
|
|
77
|
+
return layout?.parentLayout;
|
|
78
|
+
}
|
|
79
|
+
export function getLayoutChain(layoutName) {
|
|
80
|
+
const chain = [];
|
|
81
|
+
let current = layoutName;
|
|
82
|
+
while (current) {
|
|
83
|
+
if (chain.includes(current)) {
|
|
84
|
+
console.warn(`[metaowl] Circular layout hierarchy detected for "${current}"`);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
chain.push(current);
|
|
88
|
+
current = getParentLayout(current);
|
|
89
|
+
}
|
|
90
|
+
return chain;
|
|
91
|
+
}
|
|
66
92
|
export function createLayoutWrapper(layoutComponent, pageComponent, props = {}) {
|
|
67
93
|
const LayoutClass = layoutComponent;
|
|
68
94
|
const PageClass = pageComponent;
|
|
@@ -84,15 +110,32 @@ export function createLayoutWrapper(layoutComponent, pageComponent, props = {})
|
|
|
84
110
|
}
|
|
85
111
|
};
|
|
86
112
|
}
|
|
113
|
+
export function createNestedLayoutWrapper(layoutChain, pageComponent, props = {}) {
|
|
114
|
+
if (layoutChain.length === 0) {
|
|
115
|
+
return pageComponent;
|
|
116
|
+
}
|
|
117
|
+
const [outerLayout, ...innerChain] = layoutChain;
|
|
118
|
+
if (innerChain.length === 0) {
|
|
119
|
+
return createLayoutWrapper(outerLayout, pageComponent, props);
|
|
120
|
+
}
|
|
121
|
+
return createLayoutWrapper(outerLayout, createNestedLayoutWrapper(innerChain, pageComponent, props), props);
|
|
122
|
+
}
|
|
87
123
|
export async function mountWithLayout(pageComponent, target, options = {}, config = {}) {
|
|
88
124
|
const { routePath, props = {}, templates } = options;
|
|
89
125
|
const layoutName = resolveLayout(pageComponent, routePath);
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
126
|
+
const layoutChain = getLayoutChain(layoutName);
|
|
127
|
+
if (layoutChain.length === 0) {
|
|
128
|
+
console.warn(`[metaowl] Layout "${layoutName}" not found, mounting page without layout`);
|
|
129
|
+
return await mount(pageComponent, target, { ...config, props, templates });
|
|
130
|
+
}
|
|
131
|
+
const layoutClasses = layoutChain
|
|
132
|
+
.map((name) => getLayout(name))
|
|
133
|
+
.filter((l) => !!l);
|
|
134
|
+
if (layoutClasses.length === 0) {
|
|
92
135
|
console.warn(`[metaowl] Layout "${layoutName}" not found, mounting page without layout`);
|
|
93
136
|
return await mount(pageComponent, target, { ...config, props, templates });
|
|
94
137
|
}
|
|
95
|
-
const WrapperClass =
|
|
138
|
+
const WrapperClass = createNestedLayoutWrapper(layoutClasses, pageComponent, props);
|
|
96
139
|
const instance = await mount(WrapperClass, target, { ...config, templates });
|
|
97
140
|
currentLayout = instance;
|
|
98
141
|
for (const listener of listeners) {
|
|
@@ -131,6 +174,14 @@ export function defineLayout(name, options = {}) {
|
|
|
131
174
|
return componentClass;
|
|
132
175
|
};
|
|
133
176
|
}
|
|
177
|
+
export function defineNestedLayout(name, parentLayout, options = {}) {
|
|
178
|
+
return function decorator(componentClass) {
|
|
179
|
+
componentClass.layout = name;
|
|
180
|
+
componentClass.parentLayout = parentLayout;
|
|
181
|
+
componentClass.layoutOptions = options;
|
|
182
|
+
return componentClass;
|
|
183
|
+
};
|
|
184
|
+
}
|
|
134
185
|
export function buildLayouts(modules) {
|
|
135
186
|
const discoveredLayouts = {};
|
|
136
187
|
for (const [key, mod] of Object.entries(modules)) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SPA Link component for metaowl with automatic external link detection.
|
|
5
5
|
*/
|
|
6
6
|
import { Component, onMounted, onWillUnmount, useState } from '@odoo/owl';
|
|
7
|
-
|
|
7
|
+
import { EXTERNAL_URL_REGEX } from './constants.js';
|
|
8
8
|
function isExternalUrl(url) {
|
|
9
9
|
if (!url || typeof url !== 'string')
|
|
10
10
|
return false;
|
|
@@ -42,6 +42,11 @@ export class Link extends Component {
|
|
|
42
42
|
this.state = useState({
|
|
43
43
|
isActive: false
|
|
44
44
|
});
|
|
45
|
+
this._updateActiveState = () => {
|
|
46
|
+
if (this.props.activeClass) {
|
|
47
|
+
this.state.isActive = isActiveLink(this.props.to, document.location.pathname);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
45
50
|
onMounted(() => {
|
|
46
51
|
this._updateActiveState();
|
|
47
52
|
window.addEventListener('popstate', this._updateActiveState);
|
|
@@ -49,11 +54,6 @@ export class Link extends Component {
|
|
|
49
54
|
onWillUnmount(() => {
|
|
50
55
|
window.removeEventListener('popstate', this._updateActiveState);
|
|
51
56
|
});
|
|
52
|
-
this._updateActiveState = () => {
|
|
53
|
-
if (this.props.activeClass) {
|
|
54
|
-
this.state.isActive = isActiveLink(this.props.to, document.location.pathname);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
57
|
}
|
|
58
58
|
get linkClasses() {
|
|
59
59
|
const classes = [];
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Odoo JSON-RPC Service for MetaOwl applications.
|
|
5
5
|
*/
|
|
6
|
+
import { MAGIC_STRINGS } from './constants.js';
|
|
6
7
|
let config = null;
|
|
7
8
|
let session = null;
|
|
8
9
|
let csrfToken = null;
|
|
9
10
|
const authListeners = [];
|
|
10
|
-
const SESSION_KEY =
|
|
11
|
-
const CSRF_KEY =
|
|
11
|
+
const SESSION_KEY = MAGIC_STRINGS.STORE_SESSION_KEY;
|
|
12
|
+
const CSRF_KEY = MAGIC_STRINGS.STORE_CSRF_KEY;
|
|
12
13
|
export function configure(nextConfig) {
|
|
13
14
|
config = {
|
|
14
15
|
persistSession: true,
|
|
@@ -99,7 +100,7 @@ async function jsonRpc(service, method, args = []) {
|
|
|
99
100
|
}
|
|
100
101
|
const setCookie = response.headers.get('set-cookie');
|
|
101
102
|
if (setCookie?.includes('csrf_token')) {
|
|
102
|
-
const match = setCookie.match(/csrf_token=([
|
|
103
|
+
const match = setCookie.match(/csrf_token=([a-zA-Z0-9_-]+)/);
|
|
103
104
|
if (match) {
|
|
104
105
|
csrfToken = match[1] ?? null;
|
|
105
106
|
saveSession();
|
|
@@ -232,12 +232,22 @@ export const cache = {
|
|
|
232
232
|
return info;
|
|
233
233
|
}
|
|
234
234
|
};
|
|
235
|
-
export function checkCapabilities() {
|
|
235
|
+
export async function checkCapabilities() {
|
|
236
|
+
let backgroundSync = false;
|
|
237
|
+
if ('serviceWorker' in navigator) {
|
|
238
|
+
try {
|
|
239
|
+
const registration = await navigator.serviceWorker.ready;
|
|
240
|
+
backgroundSync = 'sync' in registration;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// Service worker not available
|
|
244
|
+
}
|
|
245
|
+
}
|
|
236
246
|
return {
|
|
237
247
|
serviceWorker: 'serviceWorker' in navigator,
|
|
238
248
|
push: 'PushManager' in window,
|
|
239
249
|
notifications: 'Notification' in window,
|
|
240
|
-
backgroundSync
|
|
250
|
+
backgroundSync,
|
|
241
251
|
persistentStorage: Boolean(navigator.storage?.persist),
|
|
242
252
|
addToHomeScreen: !isStandalone(),
|
|
243
253
|
offline: 'serviceWorker' in navigator
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Enhanced router with navigation guards support.
|
|
5
5
|
*/
|
|
6
|
+
import { buildSimpleRoutePattern } from './constants.js';
|
|
6
7
|
let currentRoute = null;
|
|
7
8
|
let previousRoute = null;
|
|
8
9
|
const beforeEachGuards = [];
|
|
@@ -45,17 +46,11 @@ class Router {
|
|
|
45
46
|
}
|
|
46
47
|
pathMatches(routePath, currentPath) {
|
|
47
48
|
if (!routePath.includes(':') && !routePath.includes('*')) {
|
|
48
|
-
const normalizedRoute = routePath.replace(/\/$/, '') || '/';
|
|
49
|
-
const normalizedCurrent = currentPath.replace(/\/$/, '') || '/';
|
|
49
|
+
const normalizedRoute = (routePath.replace(/\/$/, '') || '/');
|
|
50
|
+
const normalizedCurrent = (currentPath.replace(/\/$/, '') || '/');
|
|
50
51
|
return normalizedRoute === normalizedCurrent;
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
-
.replace(/\//g, '\\/')
|
|
54
|
-
.replace(/:([^/(]+)\(\.\*\)/g, '(.*)')
|
|
55
|
-
.replace(/\/:([^/(]+)\?/g, '(?:/([^/]+))?')
|
|
56
|
-
.replace(/:([^/(?\s]+)/g, '([^/]+)')
|
|
57
|
-
.replace(/\*/g, '(.*)');
|
|
58
|
-
pattern = '^' + pattern + '$';
|
|
53
|
+
const pattern = buildSimpleRoutePattern(routePath);
|
|
59
54
|
return new RegExp(pattern).test(currentPath);
|
|
60
55
|
}
|
|
61
56
|
extractParams(route, path) {
|
|
@@ -166,10 +166,10 @@ export function generateSitemapIndex(sitemaps) {
|
|
|
166
166
|
}
|
|
167
167
|
function escapeXml(str) {
|
|
168
168
|
return str
|
|
169
|
-
.replace(/&/g, '&')
|
|
170
|
-
.replace(/</g, '
|
|
171
|
-
.replace(/>/g, '
|
|
172
|
-
.replace(/"/g, '
|
|
169
|
+
.replace(/&/g, '&')
|
|
170
|
+
.replace(/</g, '<')
|
|
171
|
+
.replace(/>/g, '>')
|
|
172
|
+
.replace(/"/g, '"')
|
|
173
173
|
.replace(/'/g, ''');
|
|
174
174
|
}
|
|
175
175
|
export const SEO = {
|
|
@@ -105,9 +105,11 @@ export class Store {
|
|
|
105
105
|
reset() {
|
|
106
106
|
if (this._config.state) {
|
|
107
107
|
const initialState = this._config.state();
|
|
108
|
-
Object.keys(this._state)
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
const keys = Object.keys(this._state);
|
|
109
|
+
for (const key of keys) {
|
|
110
|
+
;
|
|
111
|
+
this._state[key] = undefined;
|
|
112
|
+
}
|
|
111
113
|
Object.assign(this._state, initialState);
|
|
112
114
|
}
|
|
113
115
|
}
|
|
@@ -7,6 +7,18 @@ import { config as dotenvConfig } from 'dotenv';
|
|
|
7
7
|
import { globSync } from 'glob';
|
|
8
8
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
|
+
function isPathSafe(path, root) {
|
|
11
|
+
const resolved = resolve(root, path);
|
|
12
|
+
return resolved.startsWith(resolve(root));
|
|
13
|
+
}
|
|
14
|
+
function sanitizeDirOption(path, root, fallback) {
|
|
15
|
+
if (!path)
|
|
16
|
+
return fallback;
|
|
17
|
+
if (isPathSafe(path, root))
|
|
18
|
+
return path;
|
|
19
|
+
console.warn(`[metaowl] Path "${path}" escapes root, using default "${fallback}"`);
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
10
22
|
function resolveOwlPath() {
|
|
11
23
|
return require.resolve('@odoo/owl/dist/owl.es.js', {
|
|
12
24
|
paths: [process.cwd(), dirname(fileURLToPath(import.meta.url))]
|
|
@@ -30,7 +42,9 @@ function mergeXmlFiles(xmlPaths) {
|
|
|
30
42
|
return '<templates>' + templates + '</templates>';
|
|
31
43
|
}
|
|
32
44
|
export async function metaowlPlugin(options = {}) {
|
|
33
|
-
const
|
|
45
|
+
const rootDir = options.root ?? 'src';
|
|
46
|
+
const resolvedRoot = resolve(process.cwd(), rootDir);
|
|
47
|
+
const { outDir = '../dist', publicDir = '../public', componentsDir = sanitizeDirOption(options.componentsDir ?? 'src/components', resolvedRoot, 'src/components'), pagesDir = sanitizeDirOption(options.pagesDir ?? 'src/pages', resolvedRoot, 'src/pages'), layoutsDir = sanitizeDirOption(options.layoutsDir ?? 'src/layouts', resolvedRoot, 'src/layouts'), frameworkEntry = './node_modules/metaowl/index.js', vendorPackages = ['@odoo/owl'], autoImport = {}, envPrefix } = options;
|
|
34
48
|
const componentXml = collectXml(`${componentsDir}/**/*.xml`);
|
|
35
49
|
const pageXml = collectXml(`${pagesDir}/**/*.xml`);
|
|
36
50
|
const layoutXml = collectXml(`${layoutsDir}/**/*.xml`);
|
|
@@ -76,7 +90,7 @@ export async function metaowlPlugin(options = {}) {
|
|
|
76
90
|
COMPONENTS: JSON.stringify(isDev ? allComponents : ['/templates.xml']),
|
|
77
91
|
'process.env': safeEnv
|
|
78
92
|
};
|
|
79
|
-
cfg.root = cfg.root ??
|
|
93
|
+
cfg.root = cfg.root ?? rootDir;
|
|
80
94
|
cfg.publicDir = cfg.publicDir ?? publicDir;
|
|
81
95
|
cfg.appType = cfg.appType ?? 'spa';
|
|
82
96
|
const owlPath = resolveOwlPath();
|
|
@@ -94,7 +108,7 @@ export async function metaowlPlugin(options = {}) {
|
|
|
94
108
|
chunkSizeWarningLimit: 1024,
|
|
95
109
|
target: 'esnext',
|
|
96
110
|
rollupOptions: {
|
|
97
|
-
input: resolve(
|
|
111
|
+
input: resolve(rootDir, 'index.html'),
|
|
98
112
|
output: {
|
|
99
113
|
manualChunks: {
|
|
100
114
|
vendor: vendorPackages,
|
|
@@ -118,8 +132,8 @@ export async function metaowlPlugin(options = {}) {
|
|
|
118
132
|
transform(code, id) {
|
|
119
133
|
if (!id.endsWith('/metaowl.js'))
|
|
120
134
|
return null;
|
|
121
|
-
const pagesRel = pagesDir.replace(new RegExp(`^${
|
|
122
|
-
const layoutsRel = layoutsDir.replace(new RegExp(`^${
|
|
135
|
+
const pagesRel = pagesDir.replace(new RegExp(`^${rootDir}[\\/]`), '');
|
|
136
|
+
const layoutsRel = layoutsDir.replace(new RegExp(`^${rootDir}[\\/]`), '');
|
|
123
137
|
return {
|
|
124
138
|
code: code.replace(/boot\(\s*\)/, `boot(import.meta.glob('./${pagesRel}/**/*.js', { eager: true }), import.meta.glob('./${layoutsRel}/**/*.js', { eager: true }))`),
|
|
125
139
|
map: null
|
|
@@ -131,9 +145,9 @@ export async function metaowlPlugin(options = {}) {
|
|
|
131
145
|
transform(code, id) {
|
|
132
146
|
if (!id.endsWith('/css.js'))
|
|
133
147
|
return null;
|
|
134
|
-
const compRel = componentsDir.replace(new RegExp(`^${
|
|
135
|
-
const pagesRel = pagesDir.replace(new RegExp(`^${
|
|
136
|
-
const layoutsRel = layoutsDir.replace(new RegExp(`^${
|
|
148
|
+
const compRel = componentsDir.replace(new RegExp(`^${rootDir}[\\/]`), '');
|
|
149
|
+
const pagesRel = pagesDir.replace(new RegExp(`^${rootDir}[\\/]`), '');
|
|
150
|
+
const layoutsRel = layoutsDir.replace(new RegExp(`^${rootDir}[\\/]`), '');
|
|
137
151
|
return {
|
|
138
152
|
code: code + '\n' +
|
|
139
153
|
`import.meta.glob('/${compRel}/**/*.{css,scss}', { eager: true })\n` +
|
|
@@ -162,7 +176,7 @@ export async function metaowlPlugin(options = {}) {
|
|
|
162
176
|
writeFileSync(file, content.replace(/\/templates\.xml/g, `/${hashedFilename}`), 'utf-8');
|
|
163
177
|
}
|
|
164
178
|
}
|
|
165
|
-
const srcImages = resolve(projectRoot,
|
|
179
|
+
const srcImages = resolve(projectRoot, rootDir, 'assets', 'images');
|
|
166
180
|
if (existsSync(srcImages)) {
|
|
167
181
|
cpSync(srcImages, resolve(outDirResolved, 'assets', 'images'), { recursive: true });
|
|
168
182
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metaowl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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
|
+
"author": "Dennis Schott",
|
|
6
|
+
"license": "LGPL-3.0-only",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dennisschott/metaowl"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://metaowl.org",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/dennisschott/metaowl/issues"
|
|
14
|
+
},
|
|
5
15
|
"type": "module",
|
|
6
16
|
"main": "./build/runtime/index.js",
|
|
7
17
|
"exports": {
|
|
@@ -17,6 +27,21 @@
|
|
|
17
27
|
"metaowl-generate": "./build/runtime/bin/metaowl-generate.js",
|
|
18
28
|
"metaowl-lint": "./build/runtime/bin/metaowl-lint.js"
|
|
19
29
|
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"clean:runtime": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('build/runtime', { recursive: true, force: true })\"",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest",
|
|
34
|
+
"lint": "eslint . --ext .js,.ts",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
36
|
+
"release:check": "npm run typecheck && npm test",
|
|
37
|
+
"build:runtime": "npm run clean:runtime && tsc -p tsconfig.build.json && node --input-type=module -e \"import { chmodSync, readdirSync } from 'node:fs'; for (const entry of readdirSync('build/runtime/bin')) { if (entry.endsWith('.js')) chmodSync('build/runtime/bin/' + entry, 0o755); }\"",
|
|
38
|
+
"release:build": "npm run release:check && npm run build:runtime",
|
|
39
|
+
"release:pack": "npm run release:check && npm pack --dry-run",
|
|
40
|
+
"prepack": "npm run build:runtime"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.0.0"
|
|
44
|
+
},
|
|
20
45
|
"files": [
|
|
21
46
|
"build/runtime",
|
|
22
47
|
"config",
|
|
@@ -33,17 +58,7 @@
|
|
|
33
58
|
"vite",
|
|
34
59
|
"meta-framework"
|
|
35
60
|
],
|
|
36
|
-
"author": "Dennis Schott",
|
|
37
|
-
"license": "LGPL-3.0-only",
|
|
38
61
|
"sideEffects": false,
|
|
39
|
-
"repository": {
|
|
40
|
-
"type": "git",
|
|
41
|
-
"url": "https://github.com/dennisschott/metaowl"
|
|
42
|
-
},
|
|
43
|
-
"homepage": "https://metaowl.org",
|
|
44
|
-
"bugs": {
|
|
45
|
-
"url": "https://github.com/dennisschott/metaowl/issues"
|
|
46
|
-
},
|
|
47
62
|
"dependencies": {
|
|
48
63
|
"@eslint/js": "^9.20.1",
|
|
49
64
|
"@odoo/owl": "^2.8.2",
|
|
@@ -60,21 +75,6 @@
|
|
|
60
75
|
"vite": "^8.0.0",
|
|
61
76
|
"vite-tsconfig-paths": "^6.1.1"
|
|
62
77
|
},
|
|
63
|
-
"engines": {
|
|
64
|
-
"node": ">=20.0.0"
|
|
65
|
-
},
|
|
66
|
-
"scripts": {
|
|
67
|
-
"clean:runtime": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('build/runtime', { recursive: true, force: true })\"",
|
|
68
|
-
"test": "vitest run",
|
|
69
|
-
"test:watch": "vitest",
|
|
70
|
-
"lint": "eslint . --ext .js,.ts",
|
|
71
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
72
|
-
"release:check": "npm run typecheck && npm test",
|
|
73
|
-
"build:runtime": "npm run clean:runtime && tsc -p tsconfig.build.json && node --input-type=module -e \"import { chmodSync, readdirSync } from 'node:fs'; for (const entry of readdirSync('build/runtime/bin')) { if (entry.endsWith('.js')) chmodSync('build/runtime/bin/' + entry, 0o755); }\"",
|
|
74
|
-
"release:build": "npm run release:check && npm run build:runtime",
|
|
75
|
-
"release:pack": "npm run release:check && npm pack --dry-run",
|
|
76
|
-
"prepack": "npm run build:runtime"
|
|
77
|
-
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@types/jsdom": "^28.0.1",
|
|
80
80
|
"@types/node": "^25.6.0",
|