metaowl 0.4.1 → 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 (80) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +12 -0
  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 +28 -10
  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/eslint.config.js +0 -3
  39. package/index.js +0 -328
  40. package/modules/app-mounter.js +0 -104
  41. package/modules/auto-import.js +0 -225
  42. package/modules/cache.js +0 -59
  43. package/modules/composables.js +0 -600
  44. package/modules/error-boundary.js +0 -228
  45. package/modules/fetch.js +0 -51
  46. package/modules/file-router.js +0 -478
  47. package/modules/forms.js +0 -353
  48. package/modules/i18n.js +0 -333
  49. package/modules/layouts.js +0 -431
  50. package/modules/link.js +0 -255
  51. package/modules/meta.js +0 -119
  52. package/modules/odoo-rpc.js +0 -511
  53. package/modules/pwa.js +0 -515
  54. package/modules/router.js +0 -769
  55. package/modules/seo.js +0 -501
  56. package/modules/store.js +0 -409
  57. package/modules/templates-manager.js +0 -89
  58. package/modules/test-utils.js +0 -532
  59. package/test/auto-import.test.js +0 -110
  60. package/test/cache.test.js +0 -55
  61. package/test/composables.test.js +0 -103
  62. package/test/dynamic-routes.test.js +0 -469
  63. package/test/error-boundary.test.js +0 -126
  64. package/test/fetch.test.js +0 -100
  65. package/test/file-router.test.js +0 -55
  66. package/test/forms.test.js +0 -203
  67. package/test/i18n.test.js +0 -188
  68. package/test/layouts.test.js +0 -395
  69. package/test/link.test.js +0 -189
  70. package/test/meta.test.js +0 -146
  71. package/test/odoo-rpc.test.js +0 -547
  72. package/test/pwa.test.js +0 -154
  73. package/test/router-guards.test.js +0 -229
  74. package/test/router.test.js +0 -77
  75. package/test/seo.test.js +0 -353
  76. package/test/store.test.js +0 -476
  77. package/test/templates-manager.test.js +0 -83
  78. package/test/test-utils.test.js +0 -314
  79. package/vite/plugin.js +0 -290
  80. package/vitest.config.js +0 -8
@@ -1,100 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest'
2
- import Fetch from '../modules/fetch.js'
3
-
4
- beforeEach(() => {
5
- Fetch.configure({ baseUrl: '', onError: null })
6
- vi.restoreAllMocks()
7
- })
8
-
9
- describe('Fetch.configure', () => {
10
- it('sets baseUrl', () => {
11
- Fetch.configure({ baseUrl: 'https://api.example.com' })
12
- expect(Fetch._baseUrl).toBe('https://api.example.com')
13
- })
14
-
15
- it('sets onError callback', () => {
16
- const handler = vi.fn()
17
- Fetch.configure({ onError: handler })
18
- expect(Fetch._onError).toBe(handler)
19
- })
20
-
21
- it('defaults baseUrl to empty string', () => {
22
- Fetch.configure({})
23
- expect(Fetch._baseUrl).toBe('')
24
- })
25
- })
26
-
27
- describe('Fetch.url', () => {
28
- it('prepends baseUrl for internal requests', async () => {
29
- Fetch.configure({ baseUrl: 'https://api.example.com' })
30
- const mockFetch = vi.fn().mockResolvedValue({
31
- json: () => Promise.resolve({ ok: true })
32
- })
33
- vi.stubGlobal('fetch', mockFetch)
34
-
35
- await Fetch.url('/items')
36
- expect(mockFetch).toHaveBeenCalledWith(
37
- 'https://api.example.com/items',
38
- expect.objectContaining({ method: 'GET' })
39
- )
40
- })
41
-
42
- it('does not prepend baseUrl when internal=false', async () => {
43
- Fetch.configure({ baseUrl: 'https://api.example.com' })
44
- const mockFetch = vi.fn().mockResolvedValue({
45
- json: () => Promise.resolve({})
46
- })
47
- vi.stubGlobal('fetch', mockFetch)
48
-
49
- await Fetch.url('https://other.com/data', 'GET', null, false)
50
- expect(mockFetch).toHaveBeenCalledWith(
51
- 'https://other.com/data',
52
- expect.anything()
53
- )
54
- })
55
-
56
- it('sends JSON body for POST requests', async () => {
57
- const mockFetch = vi.fn().mockResolvedValue({
58
- json: () => Promise.resolve({})
59
- })
60
- vi.stubGlobal('fetch', mockFetch)
61
-
62
- await Fetch.url('/items', 'POST', { name: 'test' })
63
- expect(mockFetch).toHaveBeenCalledWith(
64
- '/items',
65
- expect.objectContaining({
66
- method: 'POST',
67
- body: JSON.stringify({ name: 'test' })
68
- })
69
- )
70
- })
71
-
72
- it('returns null and calls onError on network failure', async () => {
73
- const onError = vi.fn()
74
- Fetch.configure({ onError })
75
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')))
76
-
77
- const result = await Fetch.url('/fail')
78
- expect(result).toBeNull()
79
- expect(onError).toHaveBeenCalledOnce()
80
- })
81
-
82
- it('returns null and skips onError when triggerErrorHandler=false', async () => {
83
- const onError = vi.fn()
84
- Fetch.configure({ onError })
85
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')))
86
-
87
- const result = await Fetch.url('/fail', 'GET', null, true, false)
88
- expect(result).toBeNull()
89
- expect(onError).not.toHaveBeenCalled()
90
- })
91
-
92
- it('returns parsed JSON on success', async () => {
93
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
94
- json: () => Promise.resolve({ id: 42 })
95
- }))
96
-
97
- const result = await Fetch.url('/item/42')
98
- expect(result).toEqual({ id: 42 })
99
- })
100
- })
@@ -1,55 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { buildRoutes } from '../modules/file-router.js'
3
-
4
- const Comp = function IndexPage() {}
5
- const About = function AboutPage() {}
6
- const Deep = function DeepPage() {}
7
-
8
- describe('buildRoutes', () => {
9
- it('maps index directory to /', () => {
10
- const routes = buildRoutes({
11
- './pages/index/Index.js': { default: Comp }
12
- })
13
- expect(routes).toHaveLength(1)
14
- expect(routes[0].path).toContain('/')
15
- expect(routes[0].name).toBe('index')
16
- expect(routes[0].component).toBe(Comp)
17
- })
18
-
19
- it('maps a named directory to its URL path', () => {
20
- const routes = buildRoutes({
21
- './pages/about/About.js': { default: About }
22
- })
23
- expect(routes[0].path).toContain('/about')
24
- expect(routes[0].name).toBe('about')
25
- })
26
-
27
- it('maps nested directories to a slug name', () => {
28
- const routes = buildRoutes({
29
- './pages/blog/post/Post.js': { default: Deep }
30
- })
31
- expect(routes[0].path).toContain('/blog/post')
32
- expect(routes[0].name).toBe('blog-post')
33
- })
34
-
35
- it('falls back to first function export when no default', () => {
36
- const routes = buildRoutes({
37
- './pages/index/Index.js': { MyComp: Comp }
38
- })
39
- expect(routes[0].component).toBe(Comp)
40
- })
41
-
42
- it('throws when module has no function export', () => {
43
- expect(() =>
44
- buildRoutes({ './pages/index/Index.js': { value: 42 } })
45
- ).toThrow('[metaowl]')
46
- })
47
-
48
- it('builds multiple routes from multiple modules', () => {
49
- const routes = buildRoutes({
50
- './pages/index/Index.js': { default: Comp },
51
- './pages/about/About.js': { default: About }
52
- })
53
- expect(routes).toHaveLength(2)
54
- })
55
- })
@@ -1,203 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest'
2
- import { useForm, validators, createSchema } from '../modules/forms.js'
3
-
4
- describe('Forms', () => {
5
- describe('useForm', () => {
6
- it('creates form with initial values', () => {
7
- const form = useForm({
8
- name: { default: 'John' },
9
- email: { default: '' }
10
- })
11
-
12
- expect(form.fields.name).toBe('John')
13
- expect(form.fields.email).toBe('')
14
- })
15
-
16
- it('tracks dirty state', () => {
17
- const form = useForm({
18
- name: { default: '' }
19
- })
20
-
21
- expect(form.isDirty).toBe(false)
22
- form.setValue('name', 'Jane')
23
- expect(form.isDirty).toBe(true)
24
- })
25
-
26
- it('tracks touched state', () => {
27
- const form = useForm({
28
- name: { default: '' }
29
- })
30
-
31
- expect(form.isTouched).toBe(false)
32
- form.setTouched('name')
33
- expect(form.isTouched).toBe(true)
34
- })
35
-
36
- it('validates with sync validators', async () => {
37
- const form = useForm({
38
- name: {
39
- default: '',
40
- validation: (v) => v.length >= 3 || 'Min 3 chars'
41
- }
42
- })
43
-
44
- // Invalid
45
- form.setValue('name', 'Jo')
46
- const isValid1 = await form.validateField('name')
47
- expect(isValid1).toBe(false)
48
- expect(form.errors.name).toBe('Min 3 chars')
49
-
50
- // Valid
51
- form.setValue('name', 'John')
52
- const isValid2 = await form.validateField('name')
53
- expect(isValid2).toBe(true)
54
- expect(form.errors.name).toBe(null)
55
- })
56
-
57
- it('validates all fields', async () => {
58
- const form = useForm({
59
- name: {
60
- default: '',
61
- validation: (v) => !!v || 'Required'
62
- },
63
- email: {
64
- default: '',
65
- validation: (v) => !!v || 'Required'
66
- }
67
- })
68
-
69
- const isValid = await form.validate()
70
- expect(isValid).toBe(false)
71
- expect(form.errors.name).toBe('Required')
72
- expect(form.errors.email).toBe('Required')
73
- })
74
-
75
- it('handles submit', async () => {
76
- const onSubmit = vi.fn()
77
- const form = useForm({
78
- name: { default: 'John' }
79
- })
80
-
81
- const handleSubmit = form.handleSubmit(onSubmit)
82
- await handleSubmit()
83
-
84
- expect(onSubmit).toHaveBeenCalledWith({ name: 'John' })
85
- })
86
-
87
- it('validates before submit', async () => {
88
- const onSubmit = vi.fn()
89
- const form = useForm({
90
- name: {
91
- default: '',
92
- validation: (v) => !!v || 'Required'
93
- }
94
- })
95
-
96
- const handleSubmit = form.handleSubmit(onSubmit)
97
- await handleSubmit()
98
-
99
- expect(onSubmit).not.toHaveBeenCalled()
100
- expect(form.errors.name).toBe('Required')
101
- })
102
-
103
- it('resets form', () => {
104
- const form = useForm({
105
- name: { default: 'John' }
106
- })
107
-
108
- form.setValue('name', 'Jane')
109
- form.setTouched('name')
110
- form.reset()
111
-
112
- expect(form.fields.name).toBe('John')
113
- expect(form.isDirty).toBe(false)
114
- expect(form.isTouched).toBe(false)
115
- })
116
-
117
- it('supports register method', () => {
118
- const form = useForm({
119
- name: { default: '' }
120
- })
121
-
122
- const props = form.register('name')
123
-
124
- expect(props.value).toBe('')
125
- expect(typeof props.onChange).toBe('function')
126
- expect(typeof props.onBlur).toBe('function')
127
- })
128
- })
129
-
130
- describe('validators', () => {
131
- describe('required', () => {
132
- it('validates non-empty', () => {
133
- const validator = validators.required()
134
- expect(validator('')).toBe('Required')
135
- expect(validator('test')).toBe(true)
136
- })
137
-
138
- it('allows custom message', () => {
139
- const validator = validators.required('Field required')
140
- expect(validator('')).toBe('Field required')
141
- })
142
- })
143
-
144
- describe('minLength', () => {
145
- it('validates min length', () => {
146
- const validator = validators.minLength(3)
147
- expect(validator('ab')).toBe('Min 3 characters')
148
- expect(validator('abc')).toBe(true)
149
- })
150
- })
151
-
152
- describe('maxLength', () => {
153
- it('validates max length', () => {
154
- const validator = validators.maxLength(5)
155
- expect(validator('abcdef')).toBe('Max 5 characters')
156
- expect(validator('abc')).toBe(true)
157
- })
158
- })
159
-
160
- describe('email', () => {
161
- it('validates email format', () => {
162
- const validator = validators.email()
163
- expect(validator('invalid')).toBe('Invalid email')
164
- expect(validator('test@example.com')).toBe(true)
165
- })
166
- })
167
-
168
- describe('match', () => {
169
- it('validates field match', () => {
170
- const validator = validators.match('password', 'Passwords must match')
171
- expect(validator('secret', { password: 'different' })).toBe('Passwords must match')
172
- expect(validator('secret', { password: 'secret' })).toBe(true)
173
- })
174
- })
175
- })
176
-
177
- describe('createSchema', () => {
178
- it('creates form config from schema', () => {
179
- const schema = createSchema({
180
- name: [validators.required(), validators.minLength(3)],
181
- email: validators.email()
182
- })
183
-
184
- expect(schema.name).toBeDefined()
185
- expect(schema.email).toBeDefined()
186
-
187
- // Test validation
188
- const nameValidator = schema.name.validation
189
- expect(nameValidator('ab', {})).toBe('Min 3 characters')
190
- expect(nameValidator('john', {})).toBe(true)
191
- })
192
-
193
- it('accepts single validator', () => {
194
- const schema = createSchema({
195
- email: validators.email()
196
- })
197
-
198
- const emailValidator = schema.email.validation
199
- expect(emailValidator('test', {})).toBe('Invalid email')
200
- expect(emailValidator('test@example.com', {})).toBe(true)
201
- })
202
- })
203
- })
package/test/i18n.test.js DELETED
@@ -1,188 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest'
2
- import {
3
- configureI18n,
4
- t,
5
- getLocale,
6
- setLocale,
7
- i18n,
8
- loadLocaleMessages,
9
- formatDate,
10
- formatNumber,
11
- formatCurrency,
12
- formatRelativeTime,
13
- createNamespacedT
14
- } from '../modules/i18n.js'
15
-
16
- describe('i18n', () => {
17
- beforeEach(() => {
18
- configureI18n({
19
- locale: 'en',
20
- fallbackLocale: 'en',
21
- messages: {
22
- en: {
23
- hello: 'Hello',
24
- greeting: 'Hello {{name}}',
25
- count: {
26
- zero: 'No items',
27
- one: 'One item',
28
- other: '{{n}} items'
29
- }
30
- },
31
- de: {
32
- hello: 'Hallo',
33
- greeting: 'Hallo {{name}}'
34
- }
35
- }
36
- })
37
- })
38
-
39
- describe('t()', () => {
40
- it('translates simple key', () => {
41
- expect(t('hello')).toBe('Hello')
42
- })
43
-
44
- it('interpolates values', () => {
45
- expect(t('greeting', { name: 'World' })).toBe('Hello World')
46
- })
47
-
48
- it('returns key if translation not found', () => {
49
- expect(t('nonexistent.key')).toBe('nonexistent.key')
50
- })
51
-
52
- it('uses fallback locale', () => {
53
- configureI18n({ locale: 'fr', fallbackLocale: 'en' })
54
- expect(t('hello')).toBe('Hello') // Falls back to English
55
- })
56
- })
57
-
58
- describe('pluralization', () => {
59
- it('handles zero', () => {
60
- const result = t('count', { n: 0 })
61
- expect(result).toBe('No items')
62
- })
63
-
64
- it('handles one', () => {
65
- const result = t('count', { n: 1 })
66
- expect(result).toBe('One item')
67
- })
68
-
69
- it('handles other', () => {
70
- const result = t('count', { n: 5 })
71
- expect(result).toBe('5 items')
72
- })
73
-
74
- it('uses fallback when form not defined', () => {
75
- configureI18n({
76
- locale: 'de',
77
- messages: {
78
- de: {
79
- items: {
80
- one: 'Ein Artikel'
81
- // missing 'other'
82
- }
83
- }
84
- }
85
- })
86
- })
87
- })
88
-
89
- describe('locale switching', () => {
90
- it('sets and gets locale', async () => {
91
- await setLocale('de')
92
- expect(getLocale()).toBe('de')
93
- expect(document.documentElement.lang).toBe('de')
94
- })
95
-
96
- it('updates html lang attribute', async () => {
97
- await setLocale('de')
98
- expect(document.documentElement.lang).toBe('de')
99
- })
100
- })
101
-
102
- describe('loadLocaleMessages', () => {
103
- it('loads messages asynchronously', async () => {
104
- const messages = { goodbye: 'Farewell' }
105
- await loadLocaleMessages('en', messages)
106
-
107
- expect(t('goodbye')).toBe('Farewell')
108
- })
109
-
110
- it('merges with existing messages', async () => {
111
- await loadLocaleMessages('en', { newKey: 'New Value' })
112
-
113
- expect(t('hello')).toBe('Hello') // Still exists
114
- expect(t('newKey')).toBe('New Value') // Added
115
- })
116
- })
117
-
118
- describe('formatDate', () => {
119
- it('formats date according to locale', () => {
120
- const date = new Date('2024-01-15')
121
- const result = formatDate(date, { dateStyle: 'short' })
122
- expect(result).toContain('1')
123
- expect(result).toContain('15')
124
- })
125
- })
126
-
127
- describe('formatNumber', () => {
128
- it('formats number according to locale', () => {
129
- expect(formatNumber(1234.5)).toContain('1')
130
- expect(formatNumber(1234.5)).toContain('5')
131
- })
132
- })
133
-
134
- describe('formatCurrency', () => {
135
- it('formats currency', () => {
136
- const result = formatCurrency(1234.5, 'USD')
137
- expect(result).toContain('$')
138
- })
139
-
140
- it('formats EUR', () => {
141
- const result = formatCurrency(1234.5, 'EUR')
142
- expect(result).toContain('€')
143
- })
144
- })
145
-
146
- describe('formatRelativeTime', () => {
147
- it('formats relative time', () => {
148
- const future = new Date(Date.now() + 2 * 60 * 60 * 1000)
149
- const result = formatRelativeTime(future)
150
- expect(result).toContain('2')
151
- })
152
- })
153
-
154
- describe('createNamespacedT', () => {
155
- it('creates namespaced translator', () => {
156
- configureI18n({
157
- messages: {
158
- en: {
159
- forms: {
160
- name: { label: 'Name' },
161
- email: { label: 'Email' }
162
- }
163
- }
164
- }
165
- })
166
-
167
- const tf = createNamespacedT('forms')
168
- expect(tf('name.label')).toBe('Name')
169
- expect(tf('email.label')).toBe('Email')
170
- })
171
- })
172
-
173
- describe('i18n object', () => {
174
- it('exposes locale getter', () => {
175
- expect(i18n.locale).toBe('en')
176
- })
177
-
178
- it('exposes t function', () => {
179
- expect(i18n.t('hello')).toBe('Hello')
180
- })
181
-
182
- it('exposes format functions', () => {
183
- expect(typeof i18n.formatDate).toBe('function')
184
- expect(typeof i18n.formatNumber).toBe('function')
185
- expect(typeof i18n.formatCurrency).toBe('function')
186
- })
187
- })
188
- })