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
@@ -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
+ }