metaowl 0.4.1 → 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.
Files changed (83) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +267 -2
  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 +144 -0
  10. package/build/runtime/modules/app-mounter.js +73 -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/constants.js +38 -0
  15. package/build/runtime/modules/error-boundary.js +116 -0
  16. package/build/runtime/modules/fetch.js +31 -0
  17. package/build/runtime/modules/file-router.js +207 -0
  18. package/build/runtime/modules/fonts.js +172 -0
  19. package/build/runtime/modules/forms.js +193 -0
  20. package/build/runtime/modules/i18n.js +180 -0
  21. package/build/runtime/modules/image.js +175 -0
  22. package/build/runtime/modules/layouts.js +214 -0
  23. package/build/runtime/modules/link.js +141 -0
  24. package/build/runtime/modules/meta.js +117 -0
  25. package/build/runtime/modules/odoo-rpc.js +265 -0
  26. package/build/runtime/modules/pwa.js +272 -0
  27. package/build/runtime/modules/router.js +384 -0
  28. package/build/runtime/modules/seo.js +186 -0
  29. package/build/runtime/modules/store.js +198 -0
  30. package/build/runtime/modules/templates-manager.js +52 -0
  31. package/build/runtime/modules/test-utils.js +238 -0
  32. package/build/runtime/vite/plugin.js +197 -0
  33. package/eslint.js +29 -0
  34. package/package.json +45 -27
  35. package/CONTRIBUTING.md +0 -49
  36. package/bin/metaowl-build.js +0 -12
  37. package/bin/metaowl-dev.js +0 -12
  38. package/bin/metaowl-generate.js +0 -339
  39. package/bin/metaowl-lint.js +0 -71
  40. package/bin/utils.js +0 -82
  41. package/eslint.config.js +0 -3
  42. package/index.js +0 -328
  43. package/modules/app-mounter.js +0 -104
  44. package/modules/auto-import.js +0 -225
  45. package/modules/cache.js +0 -59
  46. package/modules/composables.js +0 -600
  47. package/modules/error-boundary.js +0 -228
  48. package/modules/fetch.js +0 -51
  49. package/modules/file-router.js +0 -478
  50. package/modules/forms.js +0 -353
  51. package/modules/i18n.js +0 -333
  52. package/modules/layouts.js +0 -431
  53. package/modules/link.js +0 -255
  54. package/modules/meta.js +0 -119
  55. package/modules/odoo-rpc.js +0 -511
  56. package/modules/pwa.js +0 -515
  57. package/modules/router.js +0 -769
  58. package/modules/seo.js +0 -501
  59. package/modules/store.js +0 -409
  60. package/modules/templates-manager.js +0 -89
  61. package/modules/test-utils.js +0 -532
  62. package/test/auto-import.test.js +0 -110
  63. package/test/cache.test.js +0 -55
  64. package/test/composables.test.js +0 -103
  65. package/test/dynamic-routes.test.js +0 -469
  66. package/test/error-boundary.test.js +0 -126
  67. package/test/fetch.test.js +0 -100
  68. package/test/file-router.test.js +0 -55
  69. package/test/forms.test.js +0 -203
  70. package/test/i18n.test.js +0 -188
  71. package/test/layouts.test.js +0 -395
  72. package/test/link.test.js +0 -189
  73. package/test/meta.test.js +0 -146
  74. package/test/odoo-rpc.test.js +0 -547
  75. package/test/pwa.test.js +0 -154
  76. package/test/router-guards.test.js +0 -229
  77. package/test/router.test.js +0 -77
  78. package/test/seo.test.js +0 -353
  79. package/test/store.test.js +0 -476
  80. package/test/templates-manager.test.js +0 -83
  81. package/test/test-utils.test.js +0 -314
  82. package/vite/plugin.js +0 -290
  83. package/vitest.config.js +0 -8
package/bin/utils.js DELETED
@@ -1,82 +0,0 @@
1
- /**
2
- * Shared CLI utilities for metaowl bin scripts.
3
- * Uses ANSI escape codes only when stdout is a TTY (no color when piped).
4
- */
5
- import { existsSync, readFileSync } from 'node:fs'
6
- import { resolve, dirname } from 'node:path'
7
- import { fileURLToPath } from 'node:url'
8
- import { execSync } from 'node:child_process'
9
-
10
- export const metaowlRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
11
- export const bin = resolve(metaowlRoot, 'node_modules/.bin')
12
- export const cwd = process.cwd()
13
- const cwdBin = resolve(cwd, 'node_modules/.bin')
14
-
15
- const { version } = JSON.parse(readFileSync(resolve(metaowlRoot, 'package.json'), 'utf-8'))
16
- export { version }
17
-
18
- /**
19
- * Resolve an executable path with fallback for hoisted installs.
20
- * Priority:
21
- * 1) metaowl-local node_modules/.bin
22
- * 2) project node_modules/.bin
23
- * 3) command name (PATH lookup by shell)
24
- *
25
- * @param {string} name
26
- * @returns {string}
27
- */
28
- export function resolveBin(name) {
29
- const local = resolve(bin, name)
30
- if (existsSync(local)) return local
31
-
32
- const project = resolve(cwdBin, name)
33
- if (existsSync(project)) return project
34
-
35
- return name
36
- }
37
-
38
- const TTY = Boolean(process.stdout.isTTY)
39
- const a = (str, code) => TTY ? `\x1b[${code}m${str}\x1b[0m` : str
40
-
41
- /** Print a styled header for the current command. */
42
- export function banner(command) {
43
- console.log()
44
- console.log(` ${a('metaowl', '1;36')} ${a(command, '1')} ${a(`v${version}`, '2')}`)
45
- console.log()
46
- }
47
-
48
- /** Print a step indicator: " › message" */
49
- export function step(msg) {
50
- console.log(` ${a('›', '36')} ${msg}`)
51
- }
52
-
53
- /** Print a success line: " ✓ message" */
54
- export function success(msg) {
55
- console.log(` ${a('✓', '32')} ${a(msg, '2')}`)
56
- }
57
-
58
- /** Print an error line: " ✗ message" */
59
- export function failure(msg) {
60
- console.error(` ${a('✗', '31')} ${msg}`)
61
- }
62
-
63
- /**
64
- * Run a shell command, printing a step label before and a blank line after.
65
- * Exits the process with code 1 on failure.
66
- *
67
- * @param {string} label - Human-readable step description.
68
- * @param {string} cmd - Shell command to execute.
69
- * @param {object} [opts] - Additional options forwarded to execSync.
70
- */
71
- export function run(label, cmd, opts = {}) {
72
- step(label)
73
- console.log()
74
- try {
75
- execSync(cmd, { stdio: 'inherit', cwd, ...opts })
76
- } catch {
77
- console.log()
78
- failure(`${label} failed`)
79
- process.exit(1)
80
- }
81
- console.log()
82
- }
package/eslint.config.js DELETED
@@ -1,3 +0,0 @@
1
- import { eslintConfig } from './eslint.js'
2
-
3
- export default eslintConfig
package/index.js DELETED
@@ -1,328 +0,0 @@
1
- import { mountApp } from './modules/app-mounter.js'
2
- import { buildRoutes } from './modules/file-router.js'
3
- import { processRoutes, setSpaMode, _setSpaNavigationCallback } from './modules/router.js'
4
- import { discoverLayouts, buildLayouts, setDefaultLayout } from './modules/layouts.js'
5
-
6
- export { default as Fetch } from './modules/fetch.js'
7
- export { default as Cache } from './modules/cache.js'
8
- export { configureOwl } from './modules/app-mounter.js'
9
- export * as Meta from './modules/meta.js'
10
- export { buildRoutes }
11
- export { Store, createPersistencePlugin, createStore } from './modules/store.js'
12
- export {
13
- registerLayout,
14
- unregisterLayout,
15
- getLayout,
16
- hasLayout,
17
- getLayoutNames,
18
- setDefaultLayout,
19
- getDefaultLayout,
20
- resolveLayout,
21
- setRouteLayout,
22
- getRouteLayout,
23
- createLayoutWrapper,
24
- mountWithLayout,
25
- getCurrentLayout,
26
- subscribeToLayouts,
27
- clearLayouts,
28
- layout,
29
- defineLayout,
30
- buildLayouts,
31
- discoverLayouts
32
- } from './modules/layouts.js'
33
- export {
34
- processRoutes,
35
- beforeEach,
36
- afterEach,
37
- getCurrentRoute,
38
- getPreviousRoute,
39
- isNavigating,
40
- cancelNavigation,
41
- navigate,
42
- navigateTo,
43
- push,
44
- replace,
45
- back,
46
- forward,
47
- go,
48
- router,
49
- setSpaMode,
50
- isSpaMode
51
- } from './modules/router.js'
52
- export { Link, registerLinkTemplate } from './modules/link.js'
53
- export {
54
- matchRoute,
55
- isDynamicRoute,
56
- findRoute,
57
- generateUrl,
58
- validateRouteParams,
59
- createCatchAllRoute,
60
- createRedirectRoute,
61
- defineRoute,
62
- route
63
- } from './modules/file-router.js'
64
- export {
65
- onError,
66
- setErrorContext,
67
- getErrorContext,
68
- clearErrorContext,
69
- captureError,
70
- initGlobalErrorHandling,
71
- errorBoundary
72
- } from './modules/error-boundary.js'
73
- export {
74
- configureI18n,
75
- t,
76
- getLocale,
77
- setLocale,
78
- i18n,
79
- loadLocaleMessages,
80
- formatDate,
81
- formatNumber,
82
- formatCurrency,
83
- formatRelativeTime,
84
- createNamespacedT
85
- } from './modules/i18n.js'
86
- export {
87
- useForm,
88
- validators,
89
- createSchema,
90
- fieldProps
91
- } from './modules/forms.js'
92
- export {
93
- OdooService,
94
- configure,
95
- authenticate,
96
- logout,
97
- searchRead,
98
- call,
99
- read,
100
- create,
101
- write,
102
- unlink,
103
- searchCount,
104
- listDatabases,
105
- versionInfo,
106
- isAuthenticated,
107
- getSession,
108
- onAuthChange
109
- } from './modules/odoo-rpc.js'
110
- export {
111
- useAuth,
112
- useLocalStorage,
113
- useFetch,
114
- useDebounce,
115
- useThrottle,
116
- useWindowSize,
117
- useOnlineStatus,
118
- useAsyncState,
119
- useCache,
120
- Composables
121
- } from './modules/composables.js'
122
- export {
123
- createMockStore,
124
- mockRouter,
125
- mountComponent,
126
- wait,
127
- nextTick,
128
- flushPromises,
129
- userEvent,
130
- dom,
131
- TestUtils
132
- } from './modules/test-utils.js'
133
- export {
134
- generateSitemap,
135
- generateRobotsTxt,
136
- jsonLd,
137
- createCanonicalUrl,
138
- generateOpenGraph,
139
- generateTwitterCard,
140
- validateSitemap,
141
- getPriorityByDepth,
142
- generateSitemapIndex,
143
- SEO
144
- } from './modules/seo.js'
145
- export {
146
- generateManifest,
147
- registerServiceWorker,
148
- unregisterServiceWorker,
149
- isStandalone,
150
- isOnline,
151
- subscribeToConnectivity,
152
- requestPersistentStorage,
153
- getStorageInfo,
154
- sync,
155
- subscribeToPush,
156
- unsubscribeFromPush,
157
- showNotification,
158
- cache,
159
- checkCapabilities,
160
- PWA
161
- } from './modules/pwa.js'
162
-
163
- /**
164
- * Global routes reference for SPA navigation.
165
- * @type {object[]|null}
166
- */
167
- let _appRoutes = null
168
-
169
- /**
170
- * Monotonically-increasing navigation counter.
171
- * Incremented on every navigation attempt; lets us discard stale navigations
172
- * that complete AFTER a newer one was already triggered.
173
- * @type {number}
174
- */
175
- let _navSeq = 0
176
-
177
- /**
178
- * Promise of the currently-running mountApp call.
179
- * Used to serialize mounts: a new navigation waits for the in-progress mount
180
- * to finish, then checks if it is still the latest before mounting itself.
181
- * This prevents concurrent OWL App instances on the same element.
182
- * @type {Promise<void>|null}
183
- */
184
- let _mountingPromise = null
185
-
186
- function _handle404() {
187
- const el = document.getElementById('metaowl')
188
- if (el) {
189
- el.innerHTML = [
190
- '<div style="font-family:sans-serif;padding:3rem;text-align:center">',
191
- '<h1 style="font-size:4rem;font-weight:700;margin:0;color:#6b7280">404</h1>',
192
- '<p style="font-size:1.25rem;color:#9ca3af;margin-top:0.5rem">Page not found</p>',
193
- '<p style="margin-top:2rem"><a href="/" style="color:#3b82f6;text-decoration:none">← Go home</a></p>',
194
- '</div>'
195
- ].join('')
196
- }
197
- }
198
-
199
- /**
200
- * SPA navigation callback.
201
- * Called when navigateTo() is used.
202
- *
203
- * @param {string} path - The target path
204
- * @returns {Promise<void>}
205
- */
206
- async function _spaNavigate(path) {
207
- if (!_appRoutes) {
208
- console.error('[metaowl] Routes not available for SPA navigation')
209
- return
210
- }
211
-
212
- const seq = ++_navSeq
213
-
214
- let route
215
- try {
216
- route = await processRoutes(_appRoutes, path)
217
- } catch (error) {
218
- if (seq !== _navSeq) return
219
- if (error.message && error.message.startsWith('No route found')) {
220
- console.warn('[metaowl]', error.message)
221
- _handle404()
222
- } else {
223
- throw error
224
- }
225
- return
226
- }
227
-
228
- // Bail early if a newer navigation overtook us while processRoutes was running
229
- if (seq !== _navSeq || !route) return
230
-
231
- // Wait for any in-progress mount to finish before starting our own.
232
- // This is the key serialization: it ensures only one OWL App mounts at a time.
233
- if (_mountingPromise) {
234
- await _mountingPromise.catch(() => {})
235
- // After waiting, check again — a newer navigation may have started
236
- if (seq !== _navSeq) return
237
- }
238
-
239
- // Claim the mount slot
240
- _mountingPromise = mountApp(route)
241
- try {
242
- await _mountingPromise
243
- } finally {
244
- _mountingPromise = null
245
- }
246
- }
247
-
248
- /**
249
- * Boots the metaowl application.
250
- *
251
- * When called without arguments inside a Vite project, the `metaowl:app`
252
- * plugin transform automatically rewrites `boot()` to
253
- * `boot(import.meta.glob('./pages/**\/*.js', { eager: true }))` at build time.
254
- *
255
- * Can also be called explicitly with:
256
- * - An import.meta.glob result (file-based routing):
257
- * boot(import.meta.glob('./pages/**\/*.js', { eager: true }))
258
- * - A manual route array:
259
- * boot([{ name: 'index', path: ['/'], component: IndexPage }])
260
- *
261
- * @param {Record<string, object>|object[]} [routesOrModules]
262
- * @param {object} [options] - Boot options
263
- * @param {boolean} [options.spa=true] - Enable SPA navigation mode
264
- */
265
- export async function boot(routesOrModules = {}, layoutsOrModules = null, options = {}) {
266
- const { spa = true } = options
267
-
268
- // Auto-discover layouts
269
- try {
270
- if (layoutsOrModules && typeof layoutsOrModules === 'object' && !Array.isArray(layoutsOrModules)) {
271
- // Use layouts provided by Vite plugin transformation
272
- buildLayouts(layoutsOrModules)
273
- setDefaultLayout('default')
274
- } else if (typeof layoutsOrModules === 'object' && layoutsOrModules?.spa !== undefined) {
275
- // Options object passed as second argument
276
- Object.assign(options, layoutsOrModules)
277
- } else {
278
- await discoverLayouts()
279
- }
280
- } catch (e) {
281
- console.warn('[metaowl] Could not auto-discover layouts:', e.message)
282
- }
283
-
284
- const routes = Array.isArray(routesOrModules)
285
- ? routesOrModules
286
- : buildRoutes(routesOrModules)
287
-
288
- // Store routes for SPA navigation
289
- _appRoutes = routes
290
-
291
- // Enable SPA mode
292
- if (spa) {
293
- setSpaMode(true)
294
- _setSpaNavigationCallback(_spaNavigate)
295
-
296
- // Register global navigateTo handler for Link component
297
- window.__metaowlNavigate = _spaNavigate
298
-
299
- // Listen to PopState events (Browser Back/Forward)
300
- window.addEventListener('popstate', (event) => {
301
- const path = document.location.pathname
302
- _spaNavigate(path)
303
- })
304
- }
305
-
306
- let route
307
- try {
308
- route = await processRoutes(routes)
309
- } catch (error) {
310
- if (error.message && error.message.startsWith('No route found')) {
311
- console.warn('[metaowl]', error.message)
312
- const el = document.getElementById('metaowl')
313
- if (el) {
314
- el.innerHTML = [
315
- '<div style="font-family:sans-serif;padding:3rem;text-align:center">',
316
- '<h1 style="font-size:4rem;font-weight:700;margin:0;color:#6b7280">404</h1>',
317
- '<p style="font-size:1.25rem;color:#9ca3af;margin-top:0.5rem">Page not found</p>',
318
- '<p style="margin-top:2rem"><a href="/" style="color:#3b82f6;text-decoration:none">← Go home</a></p>',
319
- '</div>'
320
- ].join('')
321
- }
322
- return
323
- }
324
- throw error
325
- }
326
-
327
- await mountApp(route)
328
- }
@@ -1,104 +0,0 @@
1
- /**
2
- * @module AppMounter
3
- *
4
- * OWL application mounting with template merging.
5
- * Handles the low-level mounting of components into the DOM with
6
- * merged XML templates from the build process.
7
- */
8
- import { mount } from '@odoo/owl'
9
- import { mergeTemplates } from './templates-manager.js'
10
- import { resolveLayout, getLayout, mountWithLayout } from './layouts.js'
11
- import { Link } from './link.js'
12
-
13
- const _defaults = {
14
- warnIfNoStaticProps: true,
15
- willStartTimeout: 10000,
16
- translatableAttributes: ['title', 'placeholder', 'label', 'alt']
17
- }
18
-
19
- let _config = { ..._defaults }
20
-
21
- /**
22
- * Reference to the currently mounted OWL App instance.
23
- * Destroyed before each new mount to prevent zombie app accumulation.
24
- * @type {import('@odoo/owl').App|null}
25
- */
26
- let _currentApp = null
27
-
28
- /**
29
- * Override or extend the default OWL mount configuration.
30
- * Call before boot() in your project's metaowl.js.
31
- *
32
- * @param {object} config - Partial OWL config merged over the defaults.
33
- */
34
- export function configureOwl(config) {
35
- _config = { ..._defaults, ...config }
36
- }
37
-
38
- /**
39
- * Mount the resolved route's OWL component into `#app`.
40
- *
41
- * Loads and merges all XML templates (collected at build time by the
42
- * metaowl Vite plugin via the `COMPONENTS` define), then mounts the component
43
- * using the active OWL config.
44
- *
45
- * @param {object[]} route - Single-element array returned by `processRoutes()`.
46
- * @returns {Promise<void>}
47
- */
48
- /**
49
- * Cached merged templates string. Computed once on first navigation;
50
- * COMPONENTS (the list of XML files) never changes at runtime so the
51
- * result is the same for every mount.
52
- * @type {string|null}
53
- */
54
- let _cachedTemplates = null
55
-
56
- export async function mountApp(route) {
57
- // Load and cache templates on first call; reuse on every subsequent navigation.
58
- // Without caching, every navigation re-fetches all XML template files.
59
- const components = typeof COMPONENTS !== 'undefined' ? COMPONENTS : []
60
- if (!_cachedTemplates) {
61
- _cachedTemplates = await mergeTemplates(components)
62
- }
63
- const templates = _cachedTemplates
64
- const mountElement = document.getElementById('metaowl')
65
-
66
- // Destroy the previous OWL App before mounting a new one.
67
- // Without this, every navigation leaves a zombie app running in the background
68
- // (scheduler, reactive effects, event listeners) that accumulates and causes freezes.
69
- if (_currentApp) {
70
- try { _currentApp.destroy() } catch (_) {}
71
- _currentApp = null
72
- }
73
- mountElement.innerHTML = ''
74
-
75
- const pageComponent = route[0].component
76
- const pagePath = route[0].path
77
-
78
- // Check for layout
79
- const layoutName = resolveLayout(pageComponent, pagePath)
80
- const LayoutClass = getLayout(layoutName)
81
-
82
- // Base mount configuration with built-in components
83
- const baseConfig = {
84
- ..._config,
85
- templates,
86
- components: {
87
- Link,
88
- 't-link': Link
89
- }
90
- }
91
-
92
- let instance
93
- if (LayoutClass) {
94
- // Mount with layout
95
- instance = await mountWithLayout(pageComponent, mountElement, { routePath: pagePath, ...baseConfig })
96
- } else {
97
- // Mount without layout
98
- instance = await mount(pageComponent, mountElement, baseConfig)
99
- }
100
-
101
- // Store OWL App reference so we can destroy it before the next navigation.
102
- // instance.__owl__.app is the underlying App object that owns the scheduler.
103
- _currentApp = instance?.__owl__?.app ?? null
104
- }