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
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * metaowl generate — SSG production build.
4
+ */
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { resolve } from 'node:path';
7
+ import { globSync } from 'glob';
8
+ import { banner, cwd, resolveBin, resolveOwnRuntimeBin, run, step, success } from './utils.js';
9
+ banner('generate');
10
+ function escapeAttr(value) {
11
+ return value.replace(/&/g, '&').replace(/"/g, '"');
12
+ }
13
+ function extractMetaFromJs(src) {
14
+ const meta = {};
15
+ const fns = [
16
+ 'title', 'description', 'keywords', 'author', 'canonical',
17
+ 'ogTitle', 'ogDescription', 'ogImage', 'ogUrl', 'ogType', 'ogSiteName'
18
+ ];
19
+ for (const fn of fns) {
20
+ const match = src.match(new RegExp(`Meta\\.${fn}\\s*\\(\\s*(['"\`])([^'"\`]+)\\1\\s*\\)`));
21
+ if (match?.[2])
22
+ meta[fn] = match[2];
23
+ }
24
+ return meta;
25
+ }
26
+ function injectMeta(html, meta) {
27
+ let nextHtml = html;
28
+ if (meta.title) {
29
+ nextHtml = nextHtml.replace(/<title>[^<]*<\/title>/, `<title>${escapeAttr(meta.title)}</title>`);
30
+ }
31
+ const injectTag = (selector, tag) => {
32
+ nextHtml = nextHtml.replace(new RegExp(`\\s*${selector}[^>]*>\\s*`, 'gi'), '');
33
+ nextHtml = nextHtml.replace('</head>', ` ${tag}\n </head>`);
34
+ };
35
+ if (meta.description)
36
+ injectTag('<meta\\s+name="description"', `<meta name="description" content="${escapeAttr(meta.description)}">`);
37
+ if (meta.keywords)
38
+ injectTag('<meta\\s+name="keywords"', `<meta name="keywords" content="${escapeAttr(meta.keywords)}">`);
39
+ if (meta.author)
40
+ injectTag('<meta\\s+name="author"', `<meta name="author" content="${escapeAttr(meta.author)}">`);
41
+ if (meta.canonical)
42
+ injectTag('<link\\s+rel="canonical"', `<link rel="canonical" href="${escapeAttr(meta.canonical)}">`);
43
+ if (meta.ogTitle)
44
+ injectTag('<meta\\s+property="og:title"', `<meta property="og:title" content="${escapeAttr(meta.ogTitle)}">`);
45
+ if (meta.ogDescription)
46
+ injectTag('<meta\\s+property="og:description"', `<meta property="og:description" content="${escapeAttr(meta.ogDescription)}">`);
47
+ if (meta.ogImage)
48
+ injectTag('<meta\\s+property="og:image"', `<meta property="og:image" content="${escapeAttr(meta.ogImage)}">`);
49
+ if (meta.ogUrl)
50
+ injectTag('<meta\\s+property="og:url"', `<meta property="og:url" content="${escapeAttr(meta.ogUrl)}">`);
51
+ if (meta.ogType)
52
+ injectTag('<meta\\s+property="og:type"', `<meta property="og:type" content="${escapeAttr(meta.ogType)}">`);
53
+ if (meta.ogSiteName)
54
+ injectTag('<meta\\s+property="og:site_name"', `<meta property="og:site_name" content="${escapeAttr(meta.ogSiteName)}">`);
55
+ return nextHtml;
56
+ }
57
+ const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf-8'));
58
+ const metaowlConfig = pkg.metaowl ?? {};
59
+ const pagesDir = metaowlConfig.pagesDir ?? 'src/pages';
60
+ const outDir = metaowlConfig.outDir ?? 'dist';
61
+ function deriveRoute(pageFile) {
62
+ const rel = pageFile.replace(new RegExp(`^${pagesDir}[\\/]`), '');
63
+ const parts = rel.split('/').slice(0, -1);
64
+ if (parts.length === 1 && parts[0] === 'index')
65
+ return '/';
66
+ return '/' + parts.join('/');
67
+ }
68
+ function extractLayoutName(pageFile) {
69
+ const jsSource = readFileSync(pageFile, 'utf-8');
70
+ let match = jsSource.match(/static\s+layout\s*=\s*['"]([^'"]+)['"]/);
71
+ if (match?.[1])
72
+ return match[1];
73
+ match = jsSource.match(/@layout\s*\(\s*['"]([^'"]+)['"]\s*\)/);
74
+ if (match?.[1])
75
+ return match[1];
76
+ match = jsSource.match(/@defineLayout\s*\(\s*['"]([^'"]+)['"]/);
77
+ if (match?.[1])
78
+ return match[1];
79
+ return 'default';
80
+ }
81
+ function xmlToStaticHtml(xml, pageContent = '', options = {}) {
82
+ const { templateCache } = options;
83
+ let html = xml;
84
+ html = html.replace(/<templates>/g, '').replace(/<\/templates>/g, '');
85
+ html = html.replace(/^\s*<t[^>]*>/, '').replace(/<\/t>\s*$/, '');
86
+ html = html.replace(/\s+t-name="[^"]*"/g, '');
87
+ html = html.replace(/\s+t-[\w-]+(="[^"]*")?/g, '');
88
+ html = html.replace(/<t\s*\/>/g, '');
89
+ html = html.replace(/<t(?:\s[^>]*)?>([\s\S]*?)<\/t>/g, (_match, inner) => inner);
90
+ if (pageContent) {
91
+ html = html.replace(/<t\s+t-slot="default"\s*\/?>/g, pageContent);
92
+ html = html.replace(/<t\s+t-slot="default"[^>]*>([\s\S]*?)<\/t>/g, pageContent);
93
+ }
94
+ if (templateCache) {
95
+ let previousHtml;
96
+ do {
97
+ previousHtml = html;
98
+ html = html.replace(/<([A-Z][A-Za-z0-9]*)\s*\/>/g, (match, componentName) => {
99
+ const templateNames = [
100
+ componentName,
101
+ componentName.charAt(0).toLowerCase() + componentName.slice(1),
102
+ componentName + 'Component'
103
+ ];
104
+ for (const name of templateNames) {
105
+ if (templateCache.has(name)) {
106
+ return templateCache.get(name);
107
+ }
108
+ }
109
+ return match;
110
+ });
111
+ html = html.replace(/<([A-Z][A-Za-z0-9]*)(?:\s[^>]*)?>([\s\S]*?)<\/\1>/g, (match, componentName) => {
112
+ const templateNames = [
113
+ componentName,
114
+ componentName.charAt(0).toLowerCase() + componentName.slice(1),
115
+ componentName + 'Component'
116
+ ];
117
+ for (const name of templateNames) {
118
+ if (templateCache.has(name)) {
119
+ return templateCache.get(name);
120
+ }
121
+ }
122
+ return match;
123
+ });
124
+ } while (html !== previousHtml);
125
+ }
126
+ else {
127
+ html = html.replace(/<([A-Z][A-Za-z0-9]*)\s*\/>/g, '<!-- $1 -->');
128
+ html = html.replace(/<([A-Z][A-Za-z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, '<!-- $1 -->');
129
+ }
130
+ return html.trim();
131
+ }
132
+ function buildShell(baseHtml, pageFile) {
133
+ let html = baseHtml;
134
+ const jsSource = readFileSync(resolve(cwd, pageFile), 'utf-8');
135
+ const meta = extractMetaFromJs(jsSource);
136
+ html = injectMeta(html, meta);
137
+ const layoutName = extractLayoutName(resolve(cwd, pageFile));
138
+ const layoutsDir = metaowlConfig.layoutsDir ?? 'src/layouts';
139
+ const componentsDir = metaowlConfig.componentsDir ?? 'src/components';
140
+ const templateCache = new Map();
141
+ const componentXmlFiles = globSync(`${componentsDir}/**/*.xml`, { cwd });
142
+ for (const componentXmlFile of componentXmlFiles) {
143
+ const content = readFileSync(resolve(cwd, componentXmlFile), 'utf-8');
144
+ const tNameMatches = content.matchAll(/<t\s+t-name="([^"]+)"[^>]*>([\s\S]*?)<\/t>/g);
145
+ for (const match of tNameMatches) {
146
+ if (match[1] && match[2]) {
147
+ templateCache.set(match[1], match[2]);
148
+ }
149
+ }
150
+ const rootMatch = content.match(/<templates>\s*<t[^>]*>([\s\S]*?)<\/t>\s*<\/templates>/);
151
+ if (rootMatch?.[1]) {
152
+ const fileName = componentXmlFile.replace(/\.xml$/, '').split('/').pop();
153
+ if (fileName) {
154
+ templateCache.set(fileName, rootMatch[1]);
155
+ }
156
+ }
157
+ }
158
+ const layoutXmlFiles = globSync(`${layoutsDir}/**/*.xml`, { cwd });
159
+ for (const layoutXmlFile of layoutXmlFiles) {
160
+ const content = readFileSync(resolve(cwd, layoutXmlFile), 'utf-8');
161
+ const tNameMatches = content.matchAll(/<t\s+t-name="([^"]+)"[^>]*>([\s\S]*?)<\/t>/g);
162
+ for (const match of tNameMatches) {
163
+ if (match[1] && match[2]) {
164
+ templateCache.set(match[1], match[2]);
165
+ }
166
+ }
167
+ }
168
+ const pageXmlFiles = globSync(`${pagesDir}/**/*.xml`, { cwd });
169
+ for (const pageXmlFile of pageXmlFiles) {
170
+ const content = readFileSync(resolve(cwd, pageXmlFile), 'utf-8');
171
+ const tNameMatches = content.matchAll(/<t\s+t-name="([^"]+)"[^>]*>([\s\S]*?)<\/t>/g);
172
+ for (const match of tNameMatches) {
173
+ if (match[1] && match[2]) {
174
+ templateCache.set(match[1], match[2]);
175
+ }
176
+ }
177
+ const rootMatch = content.match(/<templates>\s*<t[^>]*>([\s\S]*?)<\/t>\s*<\/templates>/);
178
+ if (rootMatch?.[1]) {
179
+ const fileName = pageXmlFile.replace(/\.xml$/, '').split('/').pop();
180
+ if (fileName) {
181
+ templateCache.set(fileName, rootMatch[1]);
182
+ }
183
+ }
184
+ }
185
+ let finalContent = '';
186
+ const layoutXmlFile = resolve(cwd, layoutsDir, layoutName, `${layoutName.charAt(0).toUpperCase() + layoutName.slice(1)}Layout.xml`);
187
+ const layoutXmlExists = existsSync(layoutXmlFile);
188
+ const pageXmlFile = resolve(cwd, pageFile.replace(/\.js$/, '.xml'));
189
+ const pageXmlExists = existsSync(pageXmlFile);
190
+ if (layoutXmlExists && pageXmlExists) {
191
+ const layoutXmlContent = readFileSync(layoutXmlFile, 'utf-8');
192
+ const pageXmlContent = readFileSync(pageXmlFile, 'utf-8');
193
+ const pageStaticHtml = xmlToStaticHtml(pageXmlContent, '', { templateCache });
194
+ finalContent = xmlToStaticHtml(layoutXmlContent, pageStaticHtml, { templateCache });
195
+ }
196
+ else if (pageXmlExists) {
197
+ const pageXmlContent = readFileSync(pageXmlFile, 'utf-8');
198
+ finalContent = xmlToStaticHtml(pageXmlContent, '', { templateCache });
199
+ }
200
+ if (finalContent) {
201
+ html = html.replace(/(<div\s+id="metaowl"[^>]*>)(<\/div>)/, `$1${finalContent}$2`);
202
+ }
203
+ return html;
204
+ }
205
+ run('Linting', `node "${resolveOwnRuntimeBin('metaowl-lint')}"`);
206
+ run('Building', `"${resolveBin('vite')}" build`);
207
+ step('Generating static pages...');
208
+ console.log();
209
+ const baseHtml = readFileSync(resolve(cwd, outDir, 'index.html'), 'utf-8');
210
+ const pageFiles = globSync(`${pagesDir}/**/*.js`, { cwd });
211
+ const seen = new Set();
212
+ for (const pageFile of pageFiles) {
213
+ const route = deriveRoute(pageFile);
214
+ if (seen.has(route))
215
+ continue;
216
+ seen.add(route);
217
+ const shell = buildShell(baseHtml, pageFile);
218
+ if (route === '/') {
219
+ writeFileSync(resolve(cwd, outDir, 'index.html'), shell);
220
+ console.log(' /index.html');
221
+ }
222
+ else {
223
+ const destDir = resolve(cwd, outDir, route.slice(1));
224
+ mkdirSync(destDir, { recursive: true });
225
+ writeFileSync(resolve(destDir, 'index.html'), shell);
226
+ console.log(` ${route}/index.html`);
227
+ }
228
+ }
229
+ console.log();
230
+ success(`${seen.size} route(s) generated`);
231
+ console.log();
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * metaowl lint — format with Prettier then lint with ESLint.
4
+ */
5
+ import { execSync } from 'node:child_process';
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
8
+ import { globSync } from 'glob';
9
+ import { banner, cwd, failure, resolveBin, step, success } from './utils.js';
10
+ banner('lint');
11
+ let lintTargets = null;
12
+ try {
13
+ const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf8'));
14
+ lintTargets = pkg.metaowl?.lint ?? null;
15
+ }
16
+ catch {
17
+ // No package.json or no metaowl config.
18
+ }
19
+ const defaults = [
20
+ 'src/metaowl.js',
21
+ 'src/css.js',
22
+ 'src/pages/**',
23
+ 'src/components/**'
24
+ ];
25
+ const candidates = lintTargets ?? defaults;
26
+ const existing = candidates.filter((pattern) => {
27
+ if (existsSync(resolve(cwd, pattern)))
28
+ return true;
29
+ return globSync(pattern, { cwd }).length > 0;
30
+ });
31
+ if (existing.length === 0) {
32
+ success('No lint targets found — skipping');
33
+ console.log();
34
+ process.exit(0);
35
+ }
36
+ const targets = existing.map((target) => `"${target}"`).join(' ');
37
+ step('Formatting with Prettier...');
38
+ console.log();
39
+ try {
40
+ execSync(`"${resolveBin('prettier')}" src --single-quote --no-semi --write`, { stdio: 'inherit', cwd });
41
+ }
42
+ catch {
43
+ failure('Prettier failed');
44
+ process.exit(1);
45
+ }
46
+ console.log();
47
+ step('Linting with ESLint...');
48
+ console.log();
49
+ try {
50
+ execSync(`"${resolveBin('eslint')}" ${targets} --fix`, { stdio: 'inherit', cwd });
51
+ }
52
+ catch {
53
+ failure('ESLint failed');
54
+ process.exit(1);
55
+ }
56
+ console.log();
57
+ success('Lint complete');
58
+ console.log();
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Shared CLI utilities for metaowl bin scripts.
3
+ */
4
+ import { execSync } from 'node:child_process';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { dirname, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ function resolvePackageRoot() {
9
+ let currentDir = dirname(fileURLToPath(import.meta.url));
10
+ while (true) {
11
+ if (existsSync(resolve(currentDir, 'package.json'))) {
12
+ return currentDir;
13
+ }
14
+ const parentDir = resolve(currentDir, '..');
15
+ if (parentDir === currentDir) {
16
+ throw new Error('[metaowl] Could not resolve package root for CLI runtime');
17
+ }
18
+ currentDir = parentDir;
19
+ }
20
+ }
21
+ export const metaowlRoot = resolvePackageRoot();
22
+ export const bin = resolve(metaowlRoot, 'node_modules/.bin');
23
+ export const cwd = process.cwd();
24
+ const cwdBin = resolve(cwd, 'node_modules/.bin');
25
+ const packageJson = JSON.parse(readFileSync(resolve(metaowlRoot, 'package.json'), 'utf-8'));
26
+ export const version = packageJson.version;
27
+ export function resolveOwnRuntimeBin(name) {
28
+ const fileName = name.endsWith('.js') ? name : `${name}.js`;
29
+ return resolve(metaowlRoot, 'build/runtime/bin', fileName);
30
+ }
31
+ export function resolveBin(name) {
32
+ const local = resolve(bin, name);
33
+ if (existsSync(local))
34
+ return local;
35
+ const project = resolve(cwdBin, name);
36
+ if (existsSync(project))
37
+ return project;
38
+ return name;
39
+ }
40
+ const tty = Boolean(process.stdout.isTTY);
41
+ const a = (text, code) => tty ? `\x1b[${code}m${text}\x1b[0m` : text;
42
+ export function banner(command) {
43
+ console.log();
44
+ console.log(` ${a('metaowl', '1;36')} ${a(command, '1')} ${a(`v${version}`, '2')}`);
45
+ console.log();
46
+ }
47
+ export function step(message) {
48
+ console.log(` ${a('›', '36')} ${message}`);
49
+ }
50
+ export function success(message) {
51
+ console.log(` ${a('✓', '32')} ${a(message, '2')}`);
52
+ }
53
+ export function failure(message) {
54
+ console.error(` ${a('✗', '31')} ${message}`);
55
+ }
56
+ export function run(label, cmd, opts = {}) {
57
+ step(label);
58
+ console.log();
59
+ try {
60
+ execSync(cmd, { stdio: 'inherit', cwd, ...opts });
61
+ }
62
+ catch {
63
+ console.log();
64
+ failure(`${label} failed`);
65
+ process.exit(1);
66
+ }
67
+ console.log();
68
+ }
@@ -0,0 +1,141 @@
1
+ import { mountApp, configureOwl } from './modules/app-mounter.js';
2
+ import { buildLayouts, clearLayouts, createLayoutWrapper, defineLayout, discoverLayouts, getCurrentLayout, getDefaultLayout, getLayout, getLayoutNames, getRouteLayout, hasLayout, layout, mountWithLayout, registerLayout, resolveLayout, setDefaultLayout, setRouteLayout, subscribeToLayouts, unregisterLayout } from './modules/layouts.js';
3
+ import { afterEach, _setSpaNavigationCallback, back, beforeEach, cancelNavigation, forward, getCurrentRoute, getPreviousRoute, go, isNavigating, isSpaMode, navigate, navigateTo, processRoutes, push, replace, router, setSpaMode } from './modules/router.js';
4
+ import { Link, registerLinkTemplate } from './modules/link.js';
5
+ import { buildRoutes, createCatchAllRoute, createRedirectRoute, defineRoute, findRoute, generateUrl, isDynamicRoute, matchRoute, route, validateRouteParams } from './modules/file-router.js';
6
+ import { captureError, clearErrorContext, errorBoundary, getErrorContext, initGlobalErrorHandling, onError, setErrorContext } from './modules/error-boundary.js';
7
+ import { configureI18n, createNamespacedT, formatCurrency, formatDate, formatNumber, formatRelativeTime, getLocale, i18n, loadLocaleMessages, setLocale, t } from './modules/i18n.js';
8
+ import { createSchema, fieldProps, useForm, validators } from './modules/forms.js';
9
+ import { authenticate, call, configure, create, getSession, isAuthenticated, listDatabases, logout, OdooService, onAuthChange, read, searchCount, searchRead, unlink, versionInfo, write } from './modules/odoo-rpc.js';
10
+ import { Composables, useAsyncState, useAuth, useCache, useDebounce, useFetch, useLocalStorage, useOnlineStatus, useThrottle, useWindowSize } from './modules/composables.js';
11
+ import { createMockStore, dom, flushPromises, mockRouter, mountComponent, nextTick, TestUtils, userEvent, wait } from './modules/test-utils.js';
12
+ import { createCanonicalUrl, generateOpenGraph, generateRobotsTxt, generateSitemap, generateSitemapIndex, generateTwitterCard, getPriorityByDepth, jsonLd, SEO, validateSitemap } from './modules/seo.js';
13
+ import { cache, checkCapabilities, generateManifest, getStorageInfo, isOnline, isStandalone, PWA, registerServiceWorker, requestPersistentStorage, showNotification, subscribeToConnectivity, subscribeToPush, sync, unregisterServiceWorker, unsubscribeFromPush } from './modules/pwa.js';
14
+ import Cache from './modules/cache.js';
15
+ import Fetch from './modules/fetch.js';
16
+ import * as Meta from './modules/meta.js';
17
+ import { Store, createPersistencePlugin, createStore } from './modules/store.js';
18
+ let appRoutes = null;
19
+ let navSeq = 0;
20
+ let mountingPromise = null;
21
+ function handle404() {
22
+ const el = document.getElementById('metaowl');
23
+ if (el) {
24
+ el.innerHTML = [
25
+ '<div style="font-family:sans-serif;padding:3rem;text-align:center">',
26
+ '<h1 style="font-size:4rem;font-weight:700;margin:0;color:#6b7280">404</h1>',
27
+ '<p style="font-size:1.25rem;color:#9ca3af;margin-top:0.5rem">Page not found</p>',
28
+ '<p style="margin-top:2rem"><a href="/" style="color:#3b82f6;text-decoration:none">← Go home</a></p>',
29
+ '</div>'
30
+ ].join('');
31
+ }
32
+ }
33
+ function isNoRouteFoundError(error) {
34
+ return error instanceof Error && error.message.startsWith('No route found');
35
+ }
36
+ function isBootOptions(value) {
37
+ return !!value && typeof value === 'object' && !Array.isArray(value) && 'spa' in value;
38
+ }
39
+ async function spaNavigate(path) {
40
+ if (!appRoutes) {
41
+ console.error('[metaowl] Routes not available for SPA navigation');
42
+ return;
43
+ }
44
+ const seq = ++navSeq;
45
+ let resolvedRoute;
46
+ try {
47
+ resolvedRoute = await processRoutes(appRoutes, path);
48
+ }
49
+ catch (error) {
50
+ if (seq !== navSeq)
51
+ return;
52
+ if (isNoRouteFoundError(error)) {
53
+ console.warn('[metaowl]', error.message);
54
+ handle404();
55
+ }
56
+ else {
57
+ throw error;
58
+ }
59
+ return;
60
+ }
61
+ if (seq !== navSeq || !resolvedRoute)
62
+ return;
63
+ if (mountingPromise) {
64
+ await mountingPromise.catch(() => { });
65
+ if (seq !== navSeq)
66
+ return;
67
+ }
68
+ mountingPromise = mountApp(resolvedRoute);
69
+ try {
70
+ await mountingPromise;
71
+ }
72
+ finally {
73
+ mountingPromise = null;
74
+ }
75
+ }
76
+ export async function boot(routesOrModules = {}, layoutsOrModules = null, options = {}) {
77
+ const effectiveOptions = { ...options };
78
+ let layoutModules = null;
79
+ if (isBootOptions(layoutsOrModules)) {
80
+ Object.assign(effectiveOptions, layoutsOrModules);
81
+ }
82
+ else {
83
+ layoutModules = layoutsOrModules;
84
+ }
85
+ const { spa = true } = effectiveOptions;
86
+ try {
87
+ if (layoutModules && typeof layoutModules === 'object' && !Array.isArray(layoutModules)) {
88
+ buildLayouts(layoutModules);
89
+ setDefaultLayout('default');
90
+ }
91
+ else {
92
+ await discoverLayouts();
93
+ }
94
+ }
95
+ catch (error) {
96
+ if (error instanceof Error) {
97
+ console.warn('[metaowl] Could not auto-discover layouts:', error.message);
98
+ }
99
+ }
100
+ const routes = Array.isArray(routesOrModules)
101
+ ? routesOrModules
102
+ : buildRoutes(routesOrModules);
103
+ appRoutes = routes;
104
+ if (spa) {
105
+ setSpaMode(true);
106
+ _setSpaNavigationCallback(spaNavigate);
107
+ window.__metaowlNavigate = spaNavigate;
108
+ window.addEventListener('popstate', () => {
109
+ const path = document.location.pathname;
110
+ void spaNavigate(path);
111
+ });
112
+ }
113
+ let resolvedRoute;
114
+ try {
115
+ resolvedRoute = await processRoutes(routes);
116
+ }
117
+ catch (error) {
118
+ if (isNoRouteFoundError(error)) {
119
+ console.warn('[metaowl]', error.message);
120
+ handle404();
121
+ return;
122
+ }
123
+ throw error;
124
+ }
125
+ if (!resolvedRoute)
126
+ return;
127
+ await mountApp(resolvedRoute);
128
+ }
129
+ export { Fetch, Cache, configureOwl, Meta, buildRoutes, Store, createPersistencePlugin, createStore };
130
+ export { registerLayout, unregisterLayout, getLayout, hasLayout, getLayoutNames, setDefaultLayout, getDefaultLayout, resolveLayout, setRouteLayout, getRouteLayout, createLayoutWrapper, mountWithLayout, getCurrentLayout, subscribeToLayouts, clearLayouts, layout, defineLayout, buildLayouts, discoverLayouts };
131
+ export { processRoutes, beforeEach, afterEach, getCurrentRoute, getPreviousRoute, isNavigating, cancelNavigation, navigate, navigateTo, push, replace, back, forward, go, router, setSpaMode, isSpaMode };
132
+ export { Link, registerLinkTemplate };
133
+ export { matchRoute, isDynamicRoute, findRoute, generateUrl, validateRouteParams, createCatchAllRoute, createRedirectRoute, defineRoute, route };
134
+ export { onError, setErrorContext, getErrorContext, clearErrorContext, captureError, initGlobalErrorHandling, errorBoundary };
135
+ export { configureI18n, t, getLocale, setLocale, i18n, loadLocaleMessages, formatDate, formatNumber, formatCurrency, formatRelativeTime, createNamespacedT };
136
+ export { useForm, validators, createSchema, fieldProps };
137
+ export { OdooService, configure, authenticate, logout, searchRead, call, read, create, write, unlink, searchCount, listDatabases, versionInfo, isAuthenticated, getSession, onAuthChange };
138
+ export { useAuth, useLocalStorage, useFetch, useDebounce, useThrottle, useWindowSize, useOnlineStatus, useAsyncState, useCache, Composables };
139
+ export { createMockStore, mockRouter, mountComponent, wait, nextTick, flushPromises, userEvent, dom, TestUtils };
140
+ export { generateSitemap, generateRobotsTxt, jsonLd, createCanonicalUrl, generateOpenGraph, generateTwitterCard, validateSitemap, getPriorityByDepth, generateSitemapIndex, SEO };
141
+ export { generateManifest, registerServiceWorker, unregisterServiceWorker, isStandalone, isOnline, subscribeToConnectivity, requestPersistentStorage, getStorageInfo, sync, subscribeToPush, unsubscribeFromPush, showNotification, cache, checkCapabilities, PWA };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @module AppMounter
3
+ *
4
+ * OWL application mounting with template merging.
5
+ */
6
+ import { mount } from '@odoo/owl';
7
+ import { Link } from './link.js';
8
+ import { getLayout, mountWithLayout, resolveLayout } from './layouts.js';
9
+ import { mergeTemplates } from './templates-manager.js';
10
+ const defaults = {
11
+ warnIfNoStaticProps: true,
12
+ willStartTimeout: 10000,
13
+ translatableAttributes: ['title', 'placeholder', 'label', 'alt']
14
+ };
15
+ let config = { ...defaults };
16
+ let currentApp = null;
17
+ let cachedTemplates = null;
18
+ export function configureOwl(nextConfig) {
19
+ config = { ...defaults, ...nextConfig };
20
+ }
21
+ export async function mountApp(route) {
22
+ const components = typeof COMPONENTS !== 'undefined' ? COMPONENTS : [];
23
+ if (!cachedTemplates) {
24
+ cachedTemplates = await mergeTemplates(components);
25
+ }
26
+ const templates = cachedTemplates;
27
+ const mountElement = document.getElementById('metaowl');
28
+ if (currentApp) {
29
+ try {
30
+ currentApp.destroy();
31
+ }
32
+ catch {
33
+ // Ignore destroy errors from stale app instances.
34
+ }
35
+ currentApp = null;
36
+ }
37
+ if (!mountElement) {
38
+ throw new Error('[metaowl] Mount element "#metaowl" not found');
39
+ }
40
+ mountElement.innerHTML = '';
41
+ const pageComponent = route[0]?.component;
42
+ const pagePath = document.location.pathname;
43
+ if (!pageComponent) {
44
+ throw new Error('[metaowl] Invalid route passed to mountApp()');
45
+ }
46
+ const pageComponentClass = pageComponent;
47
+ const layoutName = resolveLayout(pageComponentClass, pagePath);
48
+ const layoutClass = getLayout(layoutName);
49
+ const baseConfig = {
50
+ ...config,
51
+ templates,
52
+ components: {
53
+ Link,
54
+ 't-link': Link
55
+ }
56
+ };
57
+ let instance;
58
+ if (layoutClass) {
59
+ instance = await mountWithLayout(pageComponentClass, mountElement, { routePath: pagePath, templates }, baseConfig);
60
+ }
61
+ else {
62
+ instance = await mount(pageComponentClass, mountElement, baseConfig);
63
+ }
64
+ currentApp = instance?.__owl__?.app ?? null;
65
+ }