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.
- package/CHANGELOG.md +52 -0
- package/README.md +13 -15
- package/build/runtime/bin/metaowl-build.js +10 -0
- package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
- package/build/runtime/bin/metaowl-dev.js +10 -0
- package/build/runtime/bin/metaowl-generate.js +231 -0
- package/build/runtime/bin/metaowl-lint.js +58 -0
- package/build/runtime/bin/utils.js +68 -0
- package/build/runtime/index.js +141 -0
- package/build/runtime/modules/app-mounter.js +65 -0
- package/build/runtime/modules/auto-import.js +140 -0
- package/build/runtime/modules/cache.js +49 -0
- package/build/runtime/modules/composables.js +353 -0
- package/build/runtime/modules/error-boundary.js +116 -0
- package/build/runtime/modules/fetch.js +31 -0
- package/build/runtime/modules/file-router.js +205 -0
- package/build/runtime/modules/forms.js +193 -0
- package/build/runtime/modules/i18n.js +167 -0
- package/build/runtime/modules/layouts.js +163 -0
- package/build/runtime/modules/link.js +141 -0
- package/build/runtime/modules/meta.js +117 -0
- package/build/runtime/modules/odoo-rpc.js +264 -0
- package/build/runtime/modules/pwa.js +262 -0
- package/build/runtime/modules/router.js +389 -0
- package/build/runtime/modules/seo.js +186 -0
- package/build/runtime/modules/store.js +196 -0
- package/build/runtime/modules/templates-manager.js +52 -0
- package/build/runtime/modules/test-utils.js +238 -0
- package/build/runtime/vite/plugin.js +183 -0
- package/eslint.js +29 -0
- package/package.json +29 -11
- package/CONTRIBUTING.md +0 -49
- package/bin/metaowl-build.js +0 -12
- package/bin/metaowl-dev.js +0 -12
- package/bin/metaowl-generate.js +0 -339
- package/bin/metaowl-lint.js +0 -71
- package/bin/utils.js +0 -82
- package/index.js +0 -328
- package/modules/app-mounter.js +0 -104
- package/modules/auto-import.js +0 -225
- package/modules/cache.js +0 -59
- package/modules/composables.js +0 -600
- package/modules/error-boundary.js +0 -228
- package/modules/fetch.js +0 -51
- package/modules/file-router.js +0 -478
- package/modules/forms.js +0 -353
- package/modules/i18n.js +0 -333
- package/modules/layouts.js +0 -431
- package/modules/link.js +0 -255
- package/modules/meta.js +0 -119
- package/modules/odoo-rpc.js +0 -511
- package/modules/pwa.js +0 -515
- package/modules/router.js +0 -769
- package/modules/seo.js +0 -501
- package/modules/store.js +0 -409
- package/modules/templates-manager.js +0 -89
- package/modules/test-utils.js +0 -532
- package/test/auto-import.test.js +0 -110
- package/test/cache.test.js +0 -55
- package/test/composables.test.js +0 -103
- package/test/dynamic-routes.test.js +0 -469
- package/test/error-boundary.test.js +0 -126
- package/test/fetch.test.js +0 -100
- package/test/file-router.test.js +0 -55
- package/test/forms.test.js +0 -203
- package/test/i18n.test.js +0 -188
- package/test/layouts.test.js +0 -395
- package/test/link.test.js +0 -189
- package/test/meta.test.js +0 -146
- package/test/odoo-rpc.test.js +0 -547
- package/test/pwa.test.js +0 -154
- package/test/router-guards.test.js +0 -229
- package/test/router.test.js +0 -77
- package/test/seo.test.js +0 -353
- package/test/store.test.js +0 -476
- package/test/templates-manager.test.js +0 -83
- package/test/test-utils.test.js +0 -314
- package/vite/plugin.js +0 -277
- 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
|
+
}
|