metaowl 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +13 -15
  3. package/build/runtime/bin/metaowl-build.js +10 -0
  4. package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
  5. package/build/runtime/bin/metaowl-dev.js +10 -0
  6. package/build/runtime/bin/metaowl-generate.js +231 -0
  7. package/build/runtime/bin/metaowl-lint.js +58 -0
  8. package/build/runtime/bin/utils.js +68 -0
  9. package/build/runtime/index.js +141 -0
  10. package/build/runtime/modules/app-mounter.js +65 -0
  11. package/build/runtime/modules/auto-import.js +140 -0
  12. package/build/runtime/modules/cache.js +49 -0
  13. package/build/runtime/modules/composables.js +353 -0
  14. package/build/runtime/modules/error-boundary.js +116 -0
  15. package/build/runtime/modules/fetch.js +31 -0
  16. package/build/runtime/modules/file-router.js +205 -0
  17. package/build/runtime/modules/forms.js +193 -0
  18. package/build/runtime/modules/i18n.js +167 -0
  19. package/build/runtime/modules/layouts.js +163 -0
  20. package/build/runtime/modules/link.js +141 -0
  21. package/build/runtime/modules/meta.js +117 -0
  22. package/build/runtime/modules/odoo-rpc.js +264 -0
  23. package/build/runtime/modules/pwa.js +262 -0
  24. package/build/runtime/modules/router.js +389 -0
  25. package/build/runtime/modules/seo.js +186 -0
  26. package/build/runtime/modules/store.js +196 -0
  27. package/build/runtime/modules/templates-manager.js +52 -0
  28. package/build/runtime/modules/test-utils.js +238 -0
  29. package/build/runtime/vite/plugin.js +183 -0
  30. package/eslint.js +29 -0
  31. package/package.json +29 -11
  32. package/CONTRIBUTING.md +0 -49
  33. package/bin/metaowl-build.js +0 -12
  34. package/bin/metaowl-dev.js +0 -12
  35. package/bin/metaowl-generate.js +0 -339
  36. package/bin/metaowl-lint.js +0 -71
  37. package/bin/utils.js +0 -82
  38. package/index.js +0 -328
  39. package/modules/app-mounter.js +0 -104
  40. package/modules/auto-import.js +0 -225
  41. package/modules/cache.js +0 -59
  42. package/modules/composables.js +0 -600
  43. package/modules/error-boundary.js +0 -228
  44. package/modules/fetch.js +0 -51
  45. package/modules/file-router.js +0 -478
  46. package/modules/forms.js +0 -353
  47. package/modules/i18n.js +0 -333
  48. package/modules/layouts.js +0 -431
  49. package/modules/link.js +0 -255
  50. package/modules/meta.js +0 -119
  51. package/modules/odoo-rpc.js +0 -511
  52. package/modules/pwa.js +0 -515
  53. package/modules/router.js +0 -769
  54. package/modules/seo.js +0 -501
  55. package/modules/store.js +0 -409
  56. package/modules/templates-manager.js +0 -89
  57. package/modules/test-utils.js +0 -532
  58. package/test/auto-import.test.js +0 -110
  59. package/test/cache.test.js +0 -55
  60. package/test/composables.test.js +0 -103
  61. package/test/dynamic-routes.test.js +0 -469
  62. package/test/error-boundary.test.js +0 -126
  63. package/test/fetch.test.js +0 -100
  64. package/test/file-router.test.js +0 -55
  65. package/test/forms.test.js +0 -203
  66. package/test/i18n.test.js +0 -188
  67. package/test/layouts.test.js +0 -395
  68. package/test/link.test.js +0 -189
  69. package/test/meta.test.js +0 -146
  70. package/test/odoo-rpc.test.js +0 -547
  71. package/test/pwa.test.js +0 -154
  72. package/test/router-guards.test.js +0 -229
  73. package/test/router.test.js +0 -77
  74. package/test/seo.test.js +0 -353
  75. package/test/store.test.js +0 -476
  76. package/test/templates-manager.test.js +0 -83
  77. package/test/test-utils.test.js +0 -314
  78. package/vite/plugin.js +0 -277
  79. package/vitest.config.js +0 -8
package/modules/store.js DELETED
@@ -1,409 +0,0 @@
1
- /**
2
- * @module Store
3
- *
4
- * Lightweight state management for OWL applications, inspired by Pinia/Vuex.
5
- *
6
- * Features:
7
- * - Reactive state with OWL's reactivity system
8
- * - Synchronous mutations for state changes
9
- * - Asynchronous actions with context access
10
- * - Getter support for computed values
11
- * - Module composition via plugins
12
- * - DevTools integration support
13
- * - Persistence plugin for localStorage/sessionStorage
14
- *
15
- * @example
16
- * import { Store } from 'metaowl'
17
- *
18
- * const useUserStore = Store.define('user', {
19
- * state: () => ({ name: '', loggedIn: false }),
20
- * getters: {
21
- * displayName: (state) => state.name || 'Guest'
22
- * },
23
- * mutations: {
24
- * setName: (state, name) => { state.name = name },
25
- * setLoggedIn: (state, value) => { state.loggedIn = value }
26
- * },
27
- * actions: {
28
- * async login({ commit, state }, credentials) {
29
- * const result = await Fetch.url('/api/login', 'POST', credentials)
30
- * commit('setName', result.name)
31
- * commit('setLoggedIn', true)
32
- * return result
33
- * },
34
- * logout({ commit }) {
35
- * commit('setName', '')
36
- * commit('setLoggedIn', false)
37
- * }
38
- * }
39
- * })
40
- *
41
- * // In a component:
42
- * const store = useUserStore()
43
- * console.log(store.state.name)
44
- * console.log(store.getters.displayName)
45
- * store.commit('setName', 'John')
46
- * await store.dispatch('login', { email, password })
47
- */
48
-
49
- import { reactive } from '@odoo/owl'
50
-
51
- /**
52
- * Registry of all defined stores by their ID.
53
- * @type {Map<string, Store>}
54
- */
55
- const _stores = new Map()
56
-
57
- /**
58
- * Global plugins applied to all stores.
59
- * @type {Function[]}
60
- */
61
- const _plugins = []
62
-
63
- /**
64
- * Store class implementing the state management pattern.
65
- */
66
- export class Store {
67
- /**
68
- * Creates a new Store instance.
69
- *
70
- * @param {string} id - Unique identifier for the store
71
- * @param {object} config - Store configuration
72
- * @param {Function} config.state - Factory function returning initial state object
73
- * @param {Object.<string, Function>} [config.getters] - Computed property functions
74
- * @param {Object.<string, Function>} [config.mutations] - Synchronous state mutation functions
75
- * @param {Object.<string, Function>} [config.actions] - Asynchronous action functions
76
- */
77
- constructor(id, config) {
78
- this._id = id
79
- this._config = config
80
- this._state = reactive(config.state ? config.state() : {})
81
- this._getters = {}
82
- this._mutations = config.mutations || {}
83
- this._actions = config.actions || {}
84
- this._subscribers = []
85
- this._actionSubscribers = []
86
-
87
- // Initialize getters as getter functions
88
- if (config.getters) {
89
- for (const [name, fn] of Object.entries(config.getters)) {
90
- Object.defineProperty(this._getters, name, {
91
- get: () => fn(this._state, this._getters),
92
- enumerable: true,
93
- configurable: true
94
- })
95
- }
96
- }
97
-
98
- // Apply global plugins
99
- for (const plugin of _plugins) {
100
- plugin(this)
101
- }
102
- }
103
-
104
- /**
105
- * The store's unique identifier.
106
- * @returns {string}
107
- */
108
- get id() {
109
- return this._id
110
- }
111
-
112
- /**
113
- * Reactive state object. Direct mutation is discouraged; use commit() instead.
114
- * @returns {object}
115
- */
116
- get state() {
117
- return this._state
118
- }
119
-
120
- /**
121
- * Computed getters object.
122
- * @returns {object}
123
- */
124
- get getters() {
125
- return this._getters
126
- }
127
-
128
- /**
129
- * Commit a synchronous mutation to modify state.
130
- *
131
- * @param {string} type - Mutation name
132
- * @param {*} payload - Data passed to mutation handler
133
- * @returns {*} Return value from mutation
134
- * @throws {Error} If mutation is not defined
135
- */
136
- commit(type, payload) {
137
- const mutation = this._mutations[type]
138
- if (!mutation) {
139
- throw new Error(`[metaowl] Mutation "${type}" not found in store "${this._id}"`)
140
- }
141
-
142
- const prevState = JSON.parse(JSON.stringify(this._state))
143
- const result = mutation(this._state, payload)
144
-
145
- // Notify subscribers
146
- for (const subscriber of this._subscribers) {
147
- subscriber({ type, payload }, this._state, prevState)
148
- }
149
-
150
- return result
151
- }
152
-
153
- /**
154
- * Dispatch an asynchronous action.
155
- *
156
- * @param {string} type - Action name
157
- * @param {*} payload - Data passed to action handler
158
- * @returns {Promise<*>} Return value from action
159
- * @throws {Error} If action is not defined
160
- */
161
- async dispatch(type, payload) {
162
- const action = this._actions[type]
163
- if (!action) {
164
- throw new Error(`[metaowl] Action "${type}" not found in store "${this._id}"`)
165
- }
166
-
167
- const context = {
168
- state: this._state,
169
- getters: this._getters,
170
- commit: this.commit.bind(this),
171
- dispatch: this.dispatch.bind(this)
172
- }
173
-
174
- // Notify action subscribers (before)
175
- for (const subscriber of this._actionSubscribers) {
176
- subscriber({ type, payload }, 'before')
177
- }
178
-
179
- try {
180
- const result = await action(context, payload)
181
-
182
- // Notify action subscribers (after)
183
- for (const subscriber of this._actionSubscribers) {
184
- subscriber({ type, payload }, 'after', result)
185
- }
186
-
187
- return result
188
- } catch (error) {
189
- // Notify action subscribers (error)
190
- for (const subscriber of this._actionSubscribers) {
191
- subscriber({ type, payload }, 'error', error)
192
- }
193
- throw error
194
- }
195
- }
196
-
197
- /**
198
- * Subscribe to state changes. Callback receives mutation info and state snapshots.
199
- *
200
- * @param {Function} callback - (mutation, state, prevState) => void
201
- * @returns {Function} Unsubscribe function
202
- */
203
- subscribe(callback) {
204
- this._subscribers.push(callback)
205
- return () => {
206
- const index = this._subscribers.indexOf(callback)
207
- if (index > -1) this._subscribers.splice(index, 1)
208
- }
209
- }
210
-
211
- /**
212
- * Subscribe to action dispatches.
213
- *
214
- * @param {Function} callback - (action, status, result/error) => void
215
- * @returns {Function} Unsubscribe function
216
- */
217
- subscribeAction(callback) {
218
- this._actionSubscribers.push(callback)
219
- return () => {
220
- const index = this._actionSubscribers.indexOf(callback)
221
- if (index > -1) this._actionSubscribers.splice(index, 1)
222
- }
223
- }
224
-
225
- /**
226
- * Reset store state to initial values.
227
- */
228
- reset() {
229
- if (this._config.state) {
230
- const initialState = this._config.state()
231
- Object.keys(this._state).forEach(key => {
232
- delete this._state[key]
233
- })
234
- Object.assign(this._state, initialState)
235
- }
236
- }
237
-
238
- /**
239
- * Define a new store or retrieve existing one.
240
- *
241
- * @param {string} id - Unique store identifier
242
- * @param {object} config - Store configuration
243
- * @returns {Function} Hook function that returns store instance
244
- *
245
- * @example
246
- * const useCounterStore = Store.define('counter', {
247
- * state: () => ({ count: 0 }),
248
- * getters: { double: (state) => state.count * 2 },
249
- * mutations: { increment: (state) => state.count++ }
250
- * })
251
- *
252
- * const store = useCounterStore()
253
- * store.commit('increment')
254
- * console.log(store.getters.double.value) // 2
255
- */
256
- static define(id, config) {
257
- return function useStore() {
258
- if (!_stores.has(id)) {
259
- _stores.set(id, new Store(id, config))
260
- }
261
- return _stores.get(id)
262
- }
263
- }
264
-
265
- /**
266
- * Get a store by ID (for advanced use cases).
267
- *
268
- * @param {string} id - Store identifier
269
- * @returns {Store|undefined}
270
- */
271
- static get(id) {
272
- return _stores.get(id)
273
- }
274
-
275
- /**
276
- * Check if a store exists.
277
- *
278
- * @param {string} id - Store identifier
279
- * @returns {boolean}
280
- */
281
- static has(id) {
282
- return _stores.has(id)
283
- }
284
-
285
- /**
286
- * Remove a store from the registry.
287
- *
288
- * @param {string} id - Store identifier
289
- * @returns {boolean} True if store was removed
290
- */
291
- static remove(id) {
292
- const store = _stores.get(id)
293
- if (store) {
294
- store.reset()
295
- return _stores.delete(id)
296
- }
297
- return false
298
- }
299
-
300
- /**
301
- * Clear all stores (useful for testing).
302
- */
303
- static clear() {
304
- _stores.clear()
305
- }
306
-
307
- /**
308
- * Get all registered store IDs.
309
- *
310
- * @returns {string[]}
311
- */
312
- static storeIds() {
313
- return Array.from(_stores.keys())
314
- }
315
-
316
- /**
317
- * Register a global plugin applied to all new stores.
318
- *
319
- * @param {Function} plugin - (store) => void
320
- */
321
- static use(plugin) {
322
- _plugins.push(plugin)
323
- }
324
- }
325
-
326
- /**
327
- * Plugin for persisting store state to storage (localStorage/sessionStorage).
328
- *
329
- * @param {object} options
330
- * @param {Storage} [options.storage=localStorage] - Storage implementation
331
- * @param {string} [options.key] - Custom storage key (defaults to store id)
332
- * @param {string[]} [options.paths] - Specific state paths to persist (default: all)
333
- *
334
- * @example
335
- * Store.use(createPersistencePlugin({
336
- * storage: localStorage,
337
- * paths: ['user', 'preferences']
338
- * }))
339
- */
340
- export function createPersistencePlugin(options = {}) {
341
- const { storage = localStorage, key, paths } = options
342
-
343
- return function persistencePlugin(store) {
344
- const storageKey = key || `metaowl:store:${store.id}`
345
-
346
- // Restore state from storage on init
347
- try {
348
- const saved = storage.getItem(storageKey)
349
- if (saved) {
350
- const persisted = JSON.parse(saved)
351
- if (paths) {
352
- // Only restore specified paths
353
- for (const path of paths) {
354
- if (path in persisted && path in store.state) {
355
- store.state[path] = persisted[path]
356
- }
357
- }
358
- } else {
359
- // Restore all
360
- Object.assign(store.state, persisted)
361
- }
362
- }
363
- } catch (e) {
364
- console.warn('[metaowl] Failed to restore store from storage:', e)
365
- }
366
-
367
- // Subscribe to changes and persist
368
- store.subscribe((mutation, state) => {
369
- try {
370
- const toPersist = paths
371
- ? Object.fromEntries(paths.map(p => [p, state[p]]))
372
- : state
373
- storage.setItem(storageKey, JSON.stringify(toPersist))
374
- } catch (e) {
375
- console.warn('[metaowl] Failed to persist store:', e)
376
- }
377
- })
378
- }
379
- }
380
-
381
- /**
382
- * Simple store factory for basic use cases without full Store class overhead.
383
- *
384
- * @param {object} initialState - Initial state object
385
- * @returns {object} Reactive state object with $patch method
386
- *
387
- * @example
388
- * const counter = createStore({ count: 0 })
389
- * counter.count++ // reactive
390
- * counter.$patch({ count: 10 }) // batch update
391
- */
392
- export function createStore(initialState = {}) {
393
- const state = reactive({ ...initialState })
394
-
395
- state.$patch = (partialState) => {
396
- Object.assign(state, partialState)
397
- }
398
-
399
- state.$reset = () => {
400
- Object.keys(state).forEach(key => {
401
- if (!key.startsWith('$')) {
402
- delete state[key]
403
- }
404
- })
405
- Object.assign(state, initialState)
406
- }
407
-
408
- return state
409
- }
@@ -1,89 +0,0 @@
1
- /**
2
- * @module TemplatesManager
3
- *
4
- * Template loading and merging utilities for OWL applications.
5
- */
6
- import { loadFile } from '@odoo/owl'
7
-
8
- /**
9
- * Link component template.
10
- * Automatically added to all templates.
11
- * @type {string}
12
- */
13
- const LINK_COMPONENT_TEMPLATE = /* xml */ `
14
- <t t-name="Link">
15
- <a
16
- t-att="forwardedAttrs"
17
- t-att-href="props.to"
18
- t-att-class="linkClasses"
19
- t-att-target="props.target"
20
- t-att-rel="linkRel"
21
- t-att-title="props.title"
22
- t-att-download="props.download"
23
- t-on-click="onClick"
24
- >
25
- <t t-slot="default"/>
26
- </a>
27
- </t>
28
- `
29
-
30
- /**
31
- * Internal templates that are automatically added.
32
- * @type {string[]}
33
- */
34
- const INTERNAL_TEMPLATES = [
35
- LINK_COMPONENT_TEMPLATE
36
- ]
37
-
38
- /**
39
- * Loads OWL XML template(s) into a string ready to be passed to OWL's mount() options.
40
- *
41
- * If a single file is provided that already contains <templates> wrapper (merged file),
42
- * it's returned as-is. Otherwise, the content is wrapped in <templates>.
43
- *
44
- * @param {string|string[]} files - Array of URL-style XML paths or single path
45
- * @returns {Promise<string>}
46
- */
47
- export async function mergeTemplates(files) {
48
- // Normalize to array
49
- const fileArray = Array.isArray(files) ? files : [files]
50
-
51
- // If there's only one file, check if it's already wrapped
52
- if (fileArray.length === 1) {
53
- try {
54
- const content = await loadFile(fileArray[0])
55
- // If already wrapped (merged templates.xml), return as-is with internal templates
56
- if (content.trim().startsWith('<templates>')) {
57
- return content.replace('</templates>', INTERNAL_TEMPLATES.join('') + '</templates>')
58
- }
59
- // Otherwise wrap it with internal templates
60
- return '<templates>' + content + INTERNAL_TEMPLATES.join('') + '</templates>'
61
- } catch (e) {
62
- console.error(`[metaowl] Failed to load template: ${fileArray[0]}`, e)
63
- return '<templates>' + INTERNAL_TEMPLATES.join('') + '</templates>'
64
- }
65
- }
66
-
67
- // Multiple files: load each and wrap in <templates>
68
- const results = await Promise.all(
69
- fileArray.map(async (file) => {
70
- try {
71
- return await loadFile(file)
72
- } catch (e) {
73
- console.error(`[metaowl] Failed to load template: ${file}`, e)
74
- return ''
75
- }
76
- })
77
- )
78
- return '<templates>' + results.join('') + INTERNAL_TEMPLATES.join('') + '</templates>'
79
- }
80
-
81
- /**
82
- * Gibt die internen Templates zurück.
83
- * Nützlich für Testing oder manuelle Template-Registrierung.
84
- *
85
- * @returns {string[]}
86
- */
87
- export function getInternalTemplates() {
88
- return [...INTERNAL_TEMPLATES]
89
- }