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/seo.js DELETED
@@ -1,501 +0,0 @@
1
- /**
2
- * @module SEO
3
- *
4
- * SEO utilities for MetaOwl applications.
5
- *
6
- * Features:
7
- * - Sitemap generation
8
- * - robots.txt generation
9
- * - Meta tag helpers
10
- * - Structured data (JSON-LD) helpers
11
- * - Canonical URL generation
12
- * - Open Graph utilities
13
- *
14
- * Usage:
15
- * import { SEO } from 'metaowl'
16
- *
17
- * // Generate sitemap
18
- * const sitemap = SEO.generateSitemap([
19
- * { url: '/', priority: 1.0, changefreq: 'daily' },
20
- * { url: '/about', priority: 0.8 }
21
- * ], {
22
- * baseUrl: 'https://myapp.com'
23
- * })
24
- *
25
- * // Generate robots.txt
26
- * const robots = SEO.generateRobotsTxt({
27
- * userAgent: '*',
28
- * allow: ['/'],
29
- * sitemap: 'https://myapp.com/sitemap.xml'
30
- * })
31
- *
32
- * // JSON-LD structured data
33
- * const schema = SEO.jsonLd({
34
- * '@type': 'Organization',
35
- * name: 'My Company',
36
- * url: 'https://myapp.com'
37
- * })
38
- */
39
-
40
- /**
41
- * @typedef {Object} SitemapEntry
42
- * @property {string} url - URL path (e.g., '/about')
43
- * @property {string} [lastmod] - Last modification date (ISO 8601)
44
- * @property {number} [priority] - Priority (0.0 to 1.0)
45
- * @property {string} [changefreq] - Change frequency (always, hourly, daily, weekly, monthly, yearly, never)
46
- * @property {string} [image] - Image URL
47
- */
48
-
49
- /**
50
- * @typedef {Object} RobotsConfig
51
- * @property {string} [userAgent='*'] - User agent
52
- * @property {string[]} [allow=[]] - Allowed paths
53
- * @property {string[]} [disallow=[]] - Disallowed paths
54
- * @property {number} [crawlDelay] - Crawl delay in seconds
55
- * @property {string} [sitemap] - Sitemap URL
56
- * @property {string} [host] - Preferred host
57
- */
58
-
59
- /**
60
- * @typedef {Object} JsonLdSchema
61
- * @property {string} '@context' - Schema context (usually https://schema.org)
62
- * @property {string} '@type' - Schema type
63
- */
64
-
65
- /**
66
- * Generate XML sitemap.
67
- *
68
- * @param {SitemapEntry[]} entries - Sitemap entries
69
- * @param {Object} options - Options
70
- * @param {string} options.baseUrl - Base URL for the site
71
- * @returns {string} XML sitemap content
72
- *
73
- * @example
74
- * const sitemap = generateSitemap([
75
- * { url: '/', priority: 1.0, changefreq: 'daily' },
76
- * { url: '/products', priority: 0.8, changefreq: 'weekly' },
77
- * { url: '/about', priority: 0.5 }
78
- * ], { baseUrl: 'https://myapp.com' })
79
- *
80
- * console.log(sitemap)
81
- * // <?xml version="1.0" encoding="UTF-8"?>
82
- * // <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
83
- * // <url>
84
- * // <loc>https://myapp.com/</loc>
85
- * // <priority>1.0</priority>
86
- * // <changefreq>daily</changefreq>
87
- * // </url>
88
- * // ...
89
- * // </urlset>
90
- */
91
- export function generateSitemap(entries, options = {}) {
92
- const { baseUrl } = options
93
-
94
- if (!baseUrl) {
95
- throw new Error('[SEO] baseUrl is required for sitemap generation')
96
- }
97
-
98
- // Normalize base URL (remove trailing slash)
99
- const normalizedBase = baseUrl.replace(/\/$/, '')
100
-
101
- const urls = entries.map(entry => {
102
- const loc = entry.url.startsWith('http')
103
- ? entry.url
104
- : `${normalizedBase}${entry.url.startsWith('/') ? entry.url : '/' + entry.url}`
105
-
106
- let urlXml = ` <url>\n <loc>${escapeXml(loc)}</loc>\n`
107
-
108
- if (entry.lastmod) {
109
- urlXml += ` <lastmod>${entry.lastmod}</lastmod>\n`
110
- }
111
-
112
- if (entry.changefreq) {
113
- const validFreqs = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never']
114
- if (validFreqs.includes(entry.changefreq)) {
115
- urlXml += ` <changefreq>${entry.changefreq}</changefreq>\n`
116
- }
117
- }
118
-
119
- if (entry.priority !== undefined) {
120
- const priority = Math.max(0, Math.min(1, entry.priority)).toFixed(1)
121
- urlXml += ` <priority>${priority}</priority>\n`
122
- }
123
-
124
- if (entry.image) {
125
- urlXml += ` <image:image xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">\n`
126
- urlXml += ` <image:loc>${escapeXml(entry.image)}</image:loc>\n`
127
- urlXml += ` </image:image>\n`
128
- }
129
-
130
- urlXml += ' </url>'
131
- return urlXml
132
- })
133
-
134
- return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.join('\n')}\n</urlset>`
135
- }
136
-
137
- /**
138
- * Generate robots.txt content.
139
- *
140
- * @param {RobotsConfig|RobotsConfig[]} config - Robots configuration
141
- * @returns {string} robots.txt content
142
- *
143
- * @example
144
- * const robots = generateRobotsTxt({
145
- * userAgent: '*',
146
- * allow: ['/'],
147
- * disallow: ['/admin/', '/private/'],
148
- * sitemap: 'https://myapp.com/sitemap.xml'
149
- * })
150
- *
151
- * console.log(robots)
152
- * // User-agent: *
153
- * // Allow: /
154
- * // Disallow: /admin/
155
- * // Disallow: /private/
156
- * //
157
- * // Sitemap: https://myapp.com/sitemap.xml
158
- */
159
- export function generateRobotsTxt(config = {}) {
160
- const configs = Array.isArray(config) ? config : [config]
161
-
162
- const sections = configs.map(cfg => {
163
- const {
164
- userAgent = '*',
165
- allow = [],
166
- disallow = [],
167
- crawlDelay,
168
- sitemap,
169
- host
170
- } = cfg
171
-
172
- let section = `User-agent: ${userAgent}\n`
173
-
174
- for (const path of allow) {
175
- section += `Allow: ${path}\n`
176
- }
177
-
178
- for (const path of disallow) {
179
- section += `Disallow: ${path}\n`
180
- }
181
-
182
- if (crawlDelay !== undefined && crawlDelay > 0) {
183
- section += `Crawl-delay: ${crawlDelay}\n`
184
- }
185
-
186
- return section.trim()
187
- })
188
-
189
- // Add global sitemap and host at the end
190
- const globalConfig = configs.find(c => c.sitemap || c.host)
191
- if (globalConfig?.sitemap) {
192
- sections.push(`Sitemap: ${globalConfig.sitemap}`)
193
- }
194
- if (globalConfig?.host) {
195
- sections.push(`Host: ${globalConfig.host}`)
196
- }
197
-
198
- return sections.join('\n\n')
199
- }
200
-
201
- /**
202
- * Create JSON-LD structured data script tag content.
203
- *
204
- * @param {Object} schema - JSON-LD schema object
205
- * @param {string} [schema['@context']='https://schema.org'] - Schema context
206
- * @returns {string} JSON-LD script content (without script tags)
207
- *
208
- * @example
209
- * const schema = jsonLd({
210
- * '@type': 'Organization',
211
- * name: 'My Company',
212
- * url: 'https://myapp.com',
213
- * logo: 'https://myapp.com/logo.png'
214
- * })
215
- *
216
- * // In template:
217
- * // <script type="application/ld+json">${schema}</script>
218
- */
219
- export function jsonLd(schema) {
220
- const fullSchema = {
221
- '@context': 'https://schema.org',
222
- ...schema
223
- }
224
- return JSON.stringify(fullSchema, null, 2)
225
- }
226
-
227
- /**
228
- * Create canonical URL.
229
- *
230
- * @param {string} baseUrl - Base site URL
231
- * @param {string} path - Path (can include query string)
232
- * @param {Object} options - Options
233
- * @param {boolean} [options.removeQueryParams=false] - Remove query parameters
234
- * @param {string[]} [options.allowedParams=[]] - Query parameters to keep
235
- * @returns {string} Canonical URL
236
- *
237
- * @example
238
- * const canonical = createCanonicalUrl('https://myapp.com', '/products?id=123&sort=price')
239
- * // 'https://myapp.com/products?id=123&sort=price'
240
- *
241
- * const cleanCanonical = createCanonicalUrl(
242
- * 'https://myapp.com',
243
- * '/products?id=123&utm_source=email',
244
- * { allowedParams: ['id'] }
245
- * )
246
- * // 'https://myapp.com/products?id=123'
247
- */
248
- export function createCanonicalUrl(baseUrl, path, options = {}) {
249
- const { removeQueryParams = false, allowedParams = [] } = options
250
-
251
- // Normalize base URL
252
- const normalizedBase = baseUrl.replace(/\/$/, '')
253
-
254
- // Parse path and query
255
- const [pathname, queryString] = path.split('?')
256
- const normalizedPath = pathname.startsWith('/') ? pathname : '/' + pathname
257
-
258
- if (!queryString || removeQueryParams) {
259
- return `${normalizedBase}${normalizedPath}`
260
- }
261
-
262
- // Filter query parameters
263
- if (allowedParams.length > 0) {
264
- const params = new URLSearchParams(queryString)
265
- const filtered = new URLSearchParams()
266
-
267
- for (const key of allowedParams) {
268
- if (params.has(key)) {
269
- filtered.set(key, params.get(key))
270
- }
271
- }
272
-
273
- const filteredQuery = filtered.toString()
274
- return filteredQuery
275
- ? `${normalizedBase}${normalizedPath}?${filteredQuery}`
276
- : `${normalizedBase}${normalizedPath}`
277
- }
278
-
279
- return `${normalizedBase}${normalizedPath}?${queryString}`
280
- }
281
-
282
- /**
283
- * Generate Open Graph meta tags object.
284
- *
285
- * @param {Object} data - Open Graph data
286
- * @param {string} data.title - Title
287
- * @param {string} data.description - Description
288
- * @param {string} [data.type='website'] - Type
289
- * @param {string} [data.url] - URL
290
- * @param {string} [data.image] - Image URL
291
- * @param {string} [data.siteName] - Site name
292
- * @returns {Object} Open Graph meta tags
293
- *
294
- * @example
295
- * const og = generateOpenGraph({
296
- * title: 'My Page',
297
- * description: 'Page description',
298
- * url: 'https://myapp.com/page',
299
- * image: 'https://myapp.com/image.png',
300
- * siteName: 'My App'
301
- * })
302
- *
303
- * // Returns:
304
- * // {
305
- * // 'og:title': 'My Page',
306
- * // 'og:description': 'Page description',
307
- * // 'og:type': 'website',
308
- * // 'og:url': 'https://myapp.com/page',
309
- * // 'og:image': 'https://myapp.com/image.png',
310
- * // 'og:site_name': 'My App'
311
- * // }
312
- */
313
- export function generateOpenGraph(data) {
314
- const {
315
- title,
316
- description,
317
- type = 'website',
318
- url,
319
- image,
320
- siteName
321
- } = data
322
-
323
- const tags = {
324
- 'og:title': title,
325
- 'og:type': type
326
- }
327
-
328
- if (description) tags['og:description'] = description
329
- if (url) tags['og:url'] = url
330
- if (image) tags['og:image'] = image
331
- if (siteName) tags['og:site_name'] = siteName
332
-
333
- return tags
334
- }
335
-
336
- /**
337
- * Generate Twitter Card meta tags object.
338
- *
339
- * @param {Object} data - Twitter Card data
340
- * @param {string} data.title - Title
341
- * @param {string} data.description - Description
342
- * @param {string} [data.card='summary_large_image'] - Card type
343
- * @param {string} [data.image] - Image URL
344
- * @param {string} [data.site] - Twitter handle
345
- * @returns {Object} Twitter Card meta tags
346
- *
347
- * @example
348
- * const twitter = generateTwitterCard({
349
- * title: 'My Page',
350
- * description: 'Page description',
351
- * image: 'https://myapp.com/image.png',
352
- * site: '@myapp'
353
- * })
354
- */
355
- export function generateTwitterCard(data) {
356
- const {
357
- title,
358
- description,
359
- card = 'summary_large_image',
360
- image,
361
- site
362
- } = data
363
-
364
- const tags = {
365
- 'twitter:card': card,
366
- 'twitter:title': title
367
- }
368
-
369
- if (description) tags['twitter:description'] = description
370
- if (image) tags['twitter:image'] = image
371
- if (site) tags['twitter:site'] = site
372
-
373
- return tags
374
- }
375
-
376
- /**
377
- * Validate sitemap entries.
378
- *
379
- * @param {SitemapEntry[]} entries - Sitemap entries
380
- * @returns {Object} Validation result
381
- * @returns {boolean} result.valid - Whether all entries are valid
382
- * @returns {string[]} result.errors - List of error messages
383
- */
384
- export function validateSitemap(entries) {
385
- const errors = []
386
-
387
- for (let i = 0; i < entries.length; i++) {
388
- const entry = entries[i]
389
-
390
- if (!entry.url) {
391
- errors.push(`Entry ${i}: Missing required 'url'`)
392
- }
393
-
394
- if (entry.priority !== undefined) {
395
- if (entry.priority < 0 || entry.priority > 1) {
396
- errors.push(`Entry ${i}: Priority must be between 0 and 1`)
397
- }
398
- }
399
-
400
- if (entry.changefreq) {
401
- const validFreqs = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never']
402
- if (!validFreqs.includes(entry.changefreq)) {
403
- errors.push(`Entry ${i}: Invalid changefreq '${entry.changefreq}'`)
404
- }
405
- }
406
-
407
- if (entry.lastmod) {
408
- // Validate ISO 8601 date format
409
- const date = new Date(entry.lastmod)
410
- if (isNaN(date.getTime())) {
411
- errors.push(`Entry ${i}: Invalid lastmod date '${entry.lastmod}'`)
412
- }
413
- }
414
- }
415
-
416
- return {
417
- valid: errors.length === 0,
418
- errors
419
- }
420
- }
421
-
422
- /**
423
- * Calculate sitemap priority based on URL depth.
424
- *
425
- * @param {string} url - URL path
426
- * @param {Object} options - Options
427
- * @param {number} [options.maxDepth=3] - Maximum depth for priority calculation
428
- * @returns {number} Priority (0.1 to 1.0)
429
- *
430
- * @example
431
- * getPriorityByDepth('/') // 1.0
432
- * getPriorityByDepth('/products') // 0.8
433
- * getPriorityByDepth('/products/electronics/phones') // 0.5
434
- */
435
- export function getPriorityByDepth(url, options = {}) {
436
- const { maxDepth = 3 } = options
437
-
438
- const depth = url.split('/').filter(Boolean).length
439
- const priority = Math.max(0.1, 1 - (depth / maxDepth) * 0.3)
440
-
441
- return Math.round(priority * 10) / 10
442
- }
443
-
444
- /**
445
- * Generate sitemap index file.
446
- *
447
- * @param {Object[]} sitemaps - Sitemap references
448
- * @param {string} sitemaps[].loc - Sitemap URL
449
- * @param {string} [sitemaps[].lastmod] - Last modification date
450
- * @returns {string} Sitemap index XML
451
- *
452
- * @example
453
- * const index = generateSitemapIndex([
454
- * { loc: 'https://myapp.com/sitemap-products.xml', lastmod: '2024-01-01' },
455
- * { loc: 'https://myapp.com/sitemap-blog.xml' }
456
- * ])
457
- */
458
- export function generateSitemapIndex(sitemaps) {
459
- const entries = sitemaps.map(sitemap => {
460
- let entry = ` <sitemap>\n <loc>${escapeXml(sitemap.loc)}</loc>\n`
461
- if (sitemap.lastmod) {
462
- entry += ` <lastmod>${sitemap.lastmod}</lastmod>\n`
463
- }
464
- entry += ' </sitemap>'
465
- return entry
466
- })
467
-
468
- return `<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries.join('\n')}\n</sitemapindex>`
469
- }
470
-
471
- /**
472
- * Escape special XML characters.
473
- *
474
- * @param {string} str - String to escape
475
- * @returns {string} Escaped string
476
- */
477
- function escapeXml(str) {
478
- return str
479
- .replace(/&/g, '&')
480
- .replace(/</g, '<')
481
- .replace(/>/g, '>')
482
- .replace(/"/g, '"')
483
- .replace(/'/g, '&#x27;')
484
- }
485
-
486
- /**
487
- * SEO namespace for convenient access.
488
- */
489
- export const SEO = {
490
- generateSitemap,
491
- generateRobotsTxt,
492
- jsonLd,
493
- createCanonicalUrl,
494
- generateOpenGraph,
495
- generateTwitterCard,
496
- validateSitemap,
497
- getPriorityByDepth,
498
- generateSitemapIndex
499
- }
500
-
501
- export default SEO