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,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
- })