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
@@ -1,476 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest'
2
- import { Store, createPersistencePlugin, createStore } from '../modules/store.js'
3
-
4
- describe('Store', () => {
5
- beforeEach(() => {
6
- Store.clear()
7
- })
8
-
9
- describe('Store.define()', () => {
10
- it('creates a store factory function', () => {
11
- const useCounterStore = Store.define('counter', {
12
- state: () => ({ count: 0 })
13
- })
14
-
15
- expect(typeof useCounterStore).toBe('function')
16
- })
17
-
18
- it('returns singleton instance on multiple calls', () => {
19
- const useCounterStore = Store.define('counter', {
20
- state: () => ({ count: 0 })
21
- })
22
-
23
- const store1 = useCounterStore()
24
- const store2 = useCounterStore()
25
-
26
- expect(store1).toBe(store2)
27
- })
28
-
29
- it('creates reactive state', () => {
30
- const useCounterStore = Store.define('counter', {
31
- state: () => ({ count: 0 })
32
- })
33
-
34
- const store = useCounterStore()
35
-
36
- expect(store.state.count).toBe(0)
37
- })
38
- })
39
-
40
- describe('state management', () => {
41
- let useCounterStore
42
-
43
- beforeEach(() => {
44
- useCounterStore = Store.define('counter', {
45
- state: () => ({ count: 0, name: 'test' }),
46
- mutations: {
47
- increment: (state) => state.count++,
48
- setName: (state, name) => { state.name = name },
49
- add: (state, amount) => { state.count += amount }
50
- }
51
- })
52
- })
53
-
54
- it('commits mutations synchronously', () => {
55
- const store = useCounterStore()
56
-
57
- store.commit('increment')
58
-
59
- expect(store.state.count).toBe(1)
60
- })
61
-
62
- it('commits mutations with payload', () => {
63
- const store = useCounterStore()
64
-
65
- store.commit('setName', 'new name')
66
-
67
- expect(store.state.name).toBe('new name')
68
- })
69
-
70
- it('commits mutations with numeric payload', () => {
71
- const store = useCounterStore()
72
-
73
- store.commit('add', 5)
74
-
75
- expect(store.state.count).toBe(5)
76
- })
77
-
78
- it('throws on unknown mutation', () => {
79
- const store = useCounterStore()
80
-
81
- expect(() => store.commit('unknown')).toThrow('Mutation "unknown" not found')
82
- })
83
- })
84
-
85
- describe('getters', () => {
86
- it('computes getter values', () => {
87
- const useCounterStore = Store.define('counter', {
88
- state: () => ({ count: 5 }),
89
- getters: {
90
- double: (state) => state.count * 2,
91
- triple: (state) => state.count * 3
92
- }
93
- })
94
-
95
- const store = useCounterStore()
96
-
97
- expect(store.getters.double).toBe(10)
98
- expect(store.getters.triple).toBe(15)
99
- })
100
-
101
- it('updates getters when state changes', () => {
102
- const useCounterStore = Store.define('counter', {
103
- state: () => ({ count: 5 }),
104
- getters: {
105
- double: (state) => state.count * 2
106
- },
107
- mutations: {
108
- increment: (state) => state.count++
109
- }
110
- })
111
-
112
- const store = useCounterStore()
113
- expect(store.getters.double).toBe(10)
114
-
115
- store.commit('increment')
116
- expect(store.getters.double).toBe(12)
117
- })
118
- })
119
-
120
- describe('actions', () => {
121
- it('dispatches async actions', async () => {
122
- const useCounterStore = Store.define('counter', {
123
- state: () => ({ count: 0 }),
124
- mutations: {
125
- increment: (state) => state.count++
126
- },
127
- actions: {
128
- async incrementAsync({ commit }) {
129
- await new Promise(resolve => setTimeout(resolve, 10))
130
- commit('increment')
131
- }
132
- }
133
- })
134
-
135
- const store = useCounterStore()
136
-
137
- await store.dispatch('incrementAsync')
138
-
139
- expect(store.state.count).toBe(1)
140
- })
141
-
142
- it('passes payload to actions', async () => {
143
- const useCounterStore = Store.define('counter', {
144
- state: () => ({ count: 0 }),
145
- mutations: {
146
- add: (state, amount) => { state.count += amount }
147
- },
148
- actions: {
149
- async addAsync({ commit }, amount) {
150
- commit('add', amount)
151
- }
152
- }
153
- })
154
-
155
- const store = useCounterStore()
156
-
157
- await store.dispatch('addAsync', 10)
158
-
159
- expect(store.state.count).toBe(10)
160
- })
161
-
162
- it('provides context to actions', async () => {
163
- const actionContext = {}
164
-
165
- const useCounterStore = Store.define('counter2', {
166
- state: () => ({ count: 0 }),
167
- getters: { double: (state) => state.count * 2 },
168
- mutations: { increment: (state) => state.count++ },
169
- actions: {
170
- async testContext(context) {
171
- Object.assign(actionContext, {
172
- hasState: 'state' in context,
173
- hasGetters: 'getters' in context,
174
- hasCommit: typeof context.commit === 'function',
175
- hasDispatch: typeof context.dispatch === 'function'
176
- })
177
- }
178
- }
179
- })
180
-
181
- const store = useCounterStore()
182
- await store.dispatch('testContext')
183
-
184
- expect(actionContext.hasState).toBe(true)
185
- expect(actionContext.hasGetters).toBe(true)
186
- expect(actionContext.hasCommit).toBe(true)
187
- expect(actionContext.hasDispatch).toBe(true)
188
- })
189
-
190
- it('throws on unknown action', async () => {
191
- const useCounterStore = Store.define('counter', {
192
- state: () => ({ count: 0 })
193
- })
194
-
195
- const store = useCounterStore()
196
-
197
- await expect(store.dispatch('unknown')).rejects.toThrow('Action "unknown" not found')
198
- })
199
-
200
- it('returns action result', async () => {
201
- const useCounterStore = Store.define('counter', {
202
- state: () => ({ count: 5 }),
203
- actions: {
204
- async fetchData() {
205
- return { data: 'test' }
206
- }
207
- }
208
- })
209
-
210
- const store = useCounterStore()
211
- const result = await store.dispatch('fetchData')
212
-
213
- expect(result).toEqual({ data: 'test' })
214
- })
215
- })
216
-
217
- describe('subscriptions', () => {
218
- it('notifies subscribers on mutation', () => {
219
- const useCounterStore = Store.define('counter', {
220
- state: () => ({ count: 0 }),
221
- mutations: { increment: (state) => state.count++ }
222
- })
223
-
224
- const store = useCounterStore()
225
- const subscriber = { fn: () => {} }
226
- const spy = { ...subscriber, fn: (mutation, state, prevState) => {} }
227
-
228
- let receivedMutation, receivedState, receivedPrevState
229
- store.subscribe((mutation, state, prevState) => {
230
- receivedMutation = mutation
231
- receivedState = state
232
- receivedPrevState = prevState
233
- })
234
-
235
- store.commit('increment')
236
-
237
- expect(receivedMutation.type).toBe('increment')
238
- expect(receivedState.count).toBe(1)
239
- expect(receivedPrevState.count).toBe(0)
240
- })
241
-
242
- it('allows unsubscribing', () => {
243
- const useCounterStore = Store.define('counter', {
244
- state: () => ({ count: 0 }),
245
- mutations: { increment: (state) => state.count++ }
246
- })
247
-
248
- const store = useCounterStore()
249
- let callCount = 0
250
- const unsubscribe = store.subscribe(() => callCount++)
251
-
252
- store.commit('increment')
253
- expect(callCount).toBe(1)
254
-
255
- unsubscribe()
256
- store.commit('increment')
257
- expect(callCount).toBe(1)
258
- })
259
- })
260
-
261
- describe('action subscriptions', () => {
262
- it('notifies action subscribers', async () => {
263
- const useCounterStore = Store.define('counter', {
264
- state: () => ({ count: 0 }),
265
- actions: {
266
- async testAction() {
267
- return 'result'
268
- }
269
- }
270
- })
271
-
272
- const store = useCounterStore()
273
- const events = []
274
-
275
- store.subscribeAction((action, status, result) => {
276
- events.push({ action, status, result })
277
- })
278
-
279
- await store.dispatch('testAction')
280
-
281
- expect(events.length).toBe(2)
282
- expect(events[0].status).toBe('before')
283
- expect(events[1].status).toBe('after')
284
- expect(events[1].result).toBe('result')
285
- })
286
-
287
- it('notifies on action error', async () => {
288
- const useCounterStore = Store.define('counter', {
289
- actions: {
290
- async failingAction() {
291
- throw new Error('test error')
292
- }
293
- }
294
- })
295
-
296
- const store = useCounterStore()
297
- const events = []
298
-
299
- store.subscribeAction((action, status, result) => {
300
- events.push({ action, status, result })
301
- })
302
-
303
- try {
304
- await store.dispatch('failingAction')
305
- } catch (e) {
306
- // expected
307
- }
308
-
309
- expect(events.length).toBe(2)
310
- expect(events[0].status).toBe('before')
311
- expect(events[1].status).toBe('error')
312
- })
313
- })
314
-
315
- describe('reset', () => {
316
- it('resets state to initial values', () => {
317
- const useCounterStore = Store.define('counter', {
318
- state: () => ({ count: 0, name: 'initial' }),
319
- mutations: {
320
- setCount: (state, val) => { state.count = val },
321
- setName: (state, val) => { state.name = val }
322
- }
323
- })
324
-
325
- const store = useCounterStore()
326
- store.commit('setCount', 100)
327
- store.commit('setName', 'changed')
328
-
329
- store.reset()
330
-
331
- expect(store.state.count).toBe(0)
332
- expect(store.state.name).toBe('initial')
333
- })
334
- })
335
-
336
- describe('static methods', () => {
337
- it('Store.get() retrieves store by id', () => {
338
- const useCounterStore = Store.define('counter', {
339
- state: () => ({ count: 0 })
340
- })
341
-
342
- const store = useCounterStore()
343
- const retrieved = Store.get('counter')
344
-
345
- expect(retrieved).toBe(store)
346
- })
347
-
348
- it('Store.has() checks store existence', () => {
349
- const useCounterStore = Store.define('counter', {
350
- state: () => ({ count: 0 })
351
- })
352
-
353
- useCounterStore() // Initialize the store
354
-
355
- expect(Store.has('counter')).toBe(true)
356
- expect(Store.has('unknown')).toBe(false)
357
- })
358
-
359
- it('Store.remove() removes store', () => {
360
- const useCounterStore = Store.define('counter', {
361
- state: () => ({ count: 0 })
362
- })
363
-
364
- useCounterStore()
365
- expect(Store.has('counter')).toBe(true)
366
-
367
- Store.remove('counter')
368
- expect(Store.has('counter')).toBe(false)
369
- })
370
-
371
- it('Store.storeIds() returns all store IDs', () => {
372
- const useStore1 = Store.define('store1', { state: () => ({}) })
373
- const useStore2 = Store.define('store2', { state: () => ({}) })
374
-
375
- useStore1()
376
- useStore2()
377
-
378
- const ids = Store.storeIds()
379
- expect(ids).toContain('store1')
380
- expect(ids).toContain('store2')
381
- })
382
- })
383
-
384
- describe('persistence plugin', () => {
385
- it('persists state to storage', () => {
386
- const storage = {
387
- data: {},
388
- getItem(key) { return this.data[key] || null },
389
- setItem(key, value) { this.data[key] = value }
390
- }
391
-
392
- Store.use(createPersistencePlugin({ storage }))
393
-
394
- const useCounterStore = Store.define('counter', {
395
- state: () => ({ count: 0 }),
396
- mutations: { increment: (state) => state.count++ }
397
- })
398
-
399
- const store = useCounterStore()
400
- store.commit('increment')
401
-
402
- expect(storage.data['metaowl:store:counter']).toContain('"count":1')
403
- })
404
-
405
- it('restores state from storage', () => {
406
- const storage = {
407
- data: { 'metaowl:store:counter': '{"count":42}' },
408
- getItem(key) { return this.data[key] || null },
409
- setItem(key, value) { this.data[key] = value }
410
- }
411
-
412
- Store.use(createPersistencePlugin({ storage }))
413
-
414
- const useCounterStore = Store.define('counter', {
415
- state: () => ({ count: 0 }),
416
- mutations: { increment: (state) => state.count++ }
417
- })
418
-
419
- const store = useCounterStore()
420
-
421
- expect(store.state.count).toBe(42)
422
- })
423
-
424
- it('persists only specified paths', () => {
425
- const storage = {
426
- data: {},
427
- getItem(key) { return this.data[key] || null },
428
- setItem(key, value) { this.data[key] = value }
429
- }
430
-
431
- Store.use(createPersistencePlugin({ storage, paths: ['persistent'] }))
432
-
433
- const useStore = Store.define('test', {
434
- state: () => ({ persistent: 'value', temporary: 'data' }),
435
- mutations: {
436
- setPersistent: (state, val) => { state.persistent = val },
437
- setTemporary: (state, val) => { state.temporary = val }
438
- }
439
- })
440
-
441
- const store = useStore()
442
- store.commit('setPersistent', 'new value')
443
- store.commit('setTemporary', 'new temp')
444
-
445
- const saved = JSON.parse(storage.data['metaowl:store:test'])
446
- expect(saved.persistent).toBe('new value')
447
- expect(saved.temporary).toBeUndefined()
448
- })
449
- })
450
- })
451
-
452
- describe('createStore', () => {
453
- it('creates reactive state object', () => {
454
- const state = createStore({ count: 0 })
455
-
456
- expect(state.count).toBe(0)
457
- })
458
-
459
- it('has $patch method for batch updates', () => {
460
- const state = createStore({ count: 0, name: 'test' })
461
-
462
- state.$patch({ count: 10, name: 'updated' })
463
-
464
- expect(state.count).toBe(10)
465
- expect(state.name).toBe('updated')
466
- })
467
-
468
- it('has $reset method to restore initial state', () => {
469
- const state = createStore({ count: 0 })
470
-
471
- state.count = 100
472
- state.$reset()
473
-
474
- expect(state.count).toBe(0)
475
- })
476
- })
@@ -1,83 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
- import { mergeTemplates, getInternalTemplates } from '../modules/templates-manager.js'
3
-
4
- vi.mock('@odoo/owl', () => ({
5
- loadFile: vi.fn(),
6
- }))
7
-
8
- import { loadFile } from '@odoo/owl'
9
-
10
- beforeEach(() => {
11
- vi.clearAllMocks()
12
- })
13
-
14
- describe('mergeTemplates', () => {
15
- it('returns wrapped templates string from a single file', async () => {
16
- loadFile.mockResolvedValue('<t t-name="Comp"><div/></t>')
17
- const result = await mergeTemplates(['/components/Comp.xml'])
18
- expect(result).toContain('<templates>')
19
- expect(result).toContain('<t t-name="Comp"><div/></t>')
20
- expect(result).toContain('</templates>')
21
- expect(result).toContain('t-name="Link"') // Internal Link template
22
- })
23
-
24
- it('concatenates multiple template files', async () => {
25
- loadFile
26
- .mockResolvedValueOnce('<t t-name="A"><div/></t>')
27
- .mockResolvedValueOnce('<t t-name="B"><span/></t>')
28
- const result = await mergeTemplates(['/A.xml', '/B.xml'])
29
- expect(result).toContain('<templates>')
30
- expect(result).toContain('<t t-name="A"><div/></t>')
31
- expect(result).toContain('<t t-name="B"><span/></t>')
32
- expect(result).toContain('</templates>')
33
- expect(result).toContain('t-name="Link"') // Internal Link template
34
- })
35
-
36
- it('returns templates with internal components for empty array', async () => {
37
- const result = await mergeTemplates([])
38
- expect(result).toContain('<templates>')
39
- expect(result).toContain('</templates>')
40
- expect(result).toContain('t-name="Link"') // Internal Link template
41
- })
42
-
43
- it('skips failed files and logs error', async () => {
44
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
45
- loadFile.mockRejectedValue(new Error('404'))
46
- const result = await mergeTemplates(['/missing.xml'])
47
- expect(result).toContain('<templates>')
48
- expect(result).toContain('</templates>')
49
- expect(result).toContain('t-name="Link"') // Internal Link template despite error
50
- expect(consoleSpy).toHaveBeenCalledWith(
51
- expect.stringContaining('[metaowl] Failed to load template: /missing.xml'),
52
- expect.any(Error)
53
- )
54
- consoleSpy.mockRestore()
55
- })
56
-
57
- it('collects successful files even when one fails', async () => {
58
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
59
- loadFile
60
- .mockResolvedValueOnce('<t t-name="Good"><div/></t>')
61
- .mockRejectedValueOnce(new Error('404'))
62
- .mockResolvedValueOnce('<t t-name="Also"><span/></t>')
63
- const result = await mergeTemplates(['/good.xml', '/missing.xml', '/also.xml'])
64
- expect(result).toContain('<t t-name="Good"><div/></t>')
65
- expect(result).toContain('<t t-name="Also"><span/></t>')
66
- expect(result).toContain('t-name="Link"') // Internal Link template
67
- consoleSpy.mockRestore()
68
- })
69
-
70
- it('includes internal Link component template', async () => {
71
- const templates = getInternalTemplates()
72
- expect(templates.length).toBeGreaterThan(0)
73
- expect(templates[0]).toContain('t-name="Link"')
74
- expect(templates[0]).toContain('<a')
75
- expect(templates[0]).toContain('t-att-href')
76
- })
77
-
78
- it('calls loadFile once per path', async () => {
79
- loadFile.mockResolvedValue('<t/>')
80
- await mergeTemplates(['/a.xml', '/b.xml', '/c.xml'])
81
- expect(loadFile).toHaveBeenCalledTimes(3)
82
- })
83
- })