metaowl 0.4.1 → 0.6.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 +50 -0
- package/README.md +267 -2
- 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 +144 -0
- package/build/runtime/modules/app-mounter.js +73 -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/constants.js +38 -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 +207 -0
- package/build/runtime/modules/fonts.js +172 -0
- package/build/runtime/modules/forms.js +193 -0
- package/build/runtime/modules/i18n.js +180 -0
- package/build/runtime/modules/image.js +175 -0
- package/build/runtime/modules/layouts.js +214 -0
- package/build/runtime/modules/link.js +141 -0
- package/build/runtime/modules/meta.js +117 -0
- package/build/runtime/modules/odoo-rpc.js +265 -0
- package/build/runtime/modules/pwa.js +272 -0
- package/build/runtime/modules/router.js +384 -0
- package/build/runtime/modules/seo.js +186 -0
- package/build/runtime/modules/store.js +198 -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 +197 -0
- package/eslint.js +29 -0
- package/package.json +45 -27
- 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/eslint.config.js +0 -3
- 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 -290
- package/vitest.config.js +0 -8
|
@@ -1,71 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* metaowl create — scaffold a new metaowl project.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* metaowl-create [project-name]
|
|
7
|
-
*
|
|
8
|
-
* If no name is given, it will be prompted interactively.
|
|
9
4
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { banner, step, success,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// --- Project name ---
|
|
18
|
-
let name = process.argv[2]?.trim()
|
|
19
|
-
|
|
5
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { createInterface } from 'node:readline/promises';
|
|
8
|
+
import { banner, failure, step, success, version } from './utils.js';
|
|
9
|
+
banner('create');
|
|
10
|
+
let name = process.argv[2]?.trim() ?? '';
|
|
20
11
|
if (!name) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
name = (await rl.question(' Project name: ')).trim();
|
|
14
|
+
rl.close();
|
|
15
|
+
console.log();
|
|
25
16
|
}
|
|
26
|
-
|
|
27
17
|
if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
failure('Invalid project name. Use only letters, numbers, hyphens, or underscores.');
|
|
19
|
+
process.exit(1);
|
|
30
20
|
}
|
|
31
|
-
|
|
32
|
-
const dest = resolve(process.cwd(), name)
|
|
33
|
-
|
|
21
|
+
const dest = resolve(process.cwd(), name);
|
|
34
22
|
if (existsSync(dest)) {
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
failure(`Directory "${name}" already exists.`);
|
|
24
|
+
process.exit(1);
|
|
37
25
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
console.log()
|
|
41
|
-
|
|
42
|
-
// --- File writer helper ---
|
|
26
|
+
step(`Scaffolding project "${name}"...`);
|
|
27
|
+
console.log();
|
|
43
28
|
function write(filePath, content) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
29
|
+
const abs = join(dest, filePath);
|
|
30
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
31
|
+
writeFileSync(abs, content, 'utf-8');
|
|
32
|
+
console.log(` ${filePath}`);
|
|
48
33
|
}
|
|
49
|
-
|
|
50
|
-
// --- package.json ---
|
|
51
34
|
write('package.json', JSON.stringify({
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}, null, 2) + '\n')
|
|
65
|
-
|
|
66
|
-
// --- vite.config.js ---
|
|
67
|
-
write('vite.config.js',
|
|
68
|
-
`import { metaowlConfig } from 'metaowl/vite'
|
|
35
|
+
name,
|
|
36
|
+
version: '0.1.0',
|
|
37
|
+
type: 'module',
|
|
38
|
+
scripts: {
|
|
39
|
+
dev: 'metaowl-dev',
|
|
40
|
+
build: 'metaowl-build',
|
|
41
|
+
generate: 'metaowl-generate',
|
|
42
|
+
lint: 'metaowl-lint'
|
|
43
|
+
},
|
|
44
|
+
dependencies: {
|
|
45
|
+
metaowl: `^${version}`
|
|
46
|
+
}
|
|
47
|
+
}, null, 2) + '\n');
|
|
48
|
+
write('vite.config.js', `import { metaowlConfig } from 'metaowl/vite'
|
|
69
49
|
|
|
70
50
|
export default async () => {
|
|
71
51
|
return metaowlConfig({
|
|
@@ -73,45 +53,31 @@ export default async () => {
|
|
|
73
53
|
pagesDir: 'src/pages'
|
|
74
54
|
})
|
|
75
55
|
}
|
|
76
|
-
`)
|
|
77
|
-
|
|
78
|
-
// --- eslint.config.js ---
|
|
79
|
-
write('eslint.config.js',
|
|
80
|
-
`import { eslintConfig } from 'metaowl/eslint'
|
|
56
|
+
`);
|
|
57
|
+
write('eslint.config.js', `import { eslintConfig } from 'metaowl/eslint'
|
|
81
58
|
|
|
82
59
|
export default eslintConfig
|
|
83
|
-
`)
|
|
84
|
-
|
|
85
|
-
// --- postcss.config.cjs ---
|
|
86
|
-
write('postcss.config.cjs',
|
|
87
|
-
`const { createPostcssConfig } = require('metaowl/postcss')
|
|
60
|
+
`);
|
|
61
|
+
write('postcss.config.cjs', `const { createPostcssConfig } = require('metaowl/postcss')
|
|
88
62
|
|
|
89
63
|
module.exports = createPostcssConfig()
|
|
90
|
-
`)
|
|
91
|
-
|
|
92
|
-
// --- jsconfig.json ---
|
|
64
|
+
`);
|
|
93
65
|
write('jsconfig.json', JSON.stringify({
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}, null, 2) + '\n')
|
|
104
|
-
|
|
105
|
-
// --- .gitignore ---
|
|
106
|
-
write('.gitignore',
|
|
107
|
-
`node_modules/
|
|
66
|
+
extends: './node_modules/metaowl/config/jsconfig.base.json',
|
|
67
|
+
compilerOptions: {
|
|
68
|
+
baseUrl: '.',
|
|
69
|
+
paths: {
|
|
70
|
+
'@pages/*': ['src/pages/*'],
|
|
71
|
+
'@components/*': ['src/components/*']
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
include: ['src']
|
|
75
|
+
}, null, 2) + '\n');
|
|
76
|
+
write('.gitignore', `node_modules/
|
|
108
77
|
dist/
|
|
109
78
|
.env
|
|
110
|
-
`)
|
|
111
|
-
|
|
112
|
-
// --- src/index.html ---
|
|
113
|
-
write('src/index.html',
|
|
114
|
-
`<!doctype html>
|
|
79
|
+
`);
|
|
80
|
+
write('src/index.html', `<!doctype html>
|
|
115
81
|
<html lang="en">
|
|
116
82
|
<head>
|
|
117
83
|
<meta charset="UTF-8" />
|
|
@@ -123,28 +89,19 @@ write('src/index.html',
|
|
|
123
89
|
<script type="module" src="/metaowl.js"></script>
|
|
124
90
|
</body>
|
|
125
91
|
</html>
|
|
126
|
-
`)
|
|
127
|
-
|
|
128
|
-
// --- src/metaowl.js ---
|
|
129
|
-
write('src/metaowl.js',
|
|
130
|
-
`import { boot, Fetch } from 'metaowl'
|
|
92
|
+
`);
|
|
93
|
+
write('src/metaowl.js', `import { boot, Fetch } from 'metaowl'
|
|
131
94
|
|
|
132
95
|
Fetch.configure({
|
|
133
96
|
baseUrl: import.meta.env.VITE_API_URL ?? ''
|
|
134
97
|
})
|
|
135
98
|
|
|
136
99
|
boot()
|
|
137
|
-
`)
|
|
138
|
-
|
|
139
|
-
// --- src/css.js ---
|
|
140
|
-
write('src/css.js',
|
|
141
|
-
`// Global styles — import shared CSS here.
|
|
100
|
+
`);
|
|
101
|
+
write('src/css.js', `// Global styles — import shared CSS here.
|
|
142
102
|
// Component and page CSS files are auto-imported by the metaowl Vite plugin.
|
|
143
|
-
`)
|
|
144
|
-
|
|
145
|
-
// --- src/pages/index/Index.js ---
|
|
146
|
-
write('src/pages/index/Index.js',
|
|
147
|
-
`import { Component } from '@odoo/owl'
|
|
103
|
+
`);
|
|
104
|
+
write('src/pages/index/Index.js', `import { Component } from '@odoo/owl'
|
|
148
105
|
import { Meta } from 'metaowl'
|
|
149
106
|
import AppHeader from '@components/AppHeader/AppHeader'
|
|
150
107
|
import AppFooter from '@components/AppFooter/AppFooter'
|
|
@@ -157,11 +114,8 @@ export default class Index extends Component {
|
|
|
157
114
|
Meta.title('Home — ${name}')
|
|
158
115
|
}
|
|
159
116
|
}
|
|
160
|
-
`)
|
|
161
|
-
|
|
162
|
-
// --- src/pages/index/Index.xml ---
|
|
163
|
-
write('src/pages/index/Index.xml',
|
|
164
|
-
`<templates>
|
|
117
|
+
`);
|
|
118
|
+
write('src/pages/index/Index.xml', `<templates>
|
|
165
119
|
<t t-name="Index">
|
|
166
120
|
<div class="layout">
|
|
167
121
|
<AppHeader />
|
|
@@ -172,11 +126,8 @@ write('src/pages/index/Index.xml',
|
|
|
172
126
|
</div>
|
|
173
127
|
</t>
|
|
174
128
|
</templates>
|
|
175
|
-
`)
|
|
176
|
-
|
|
177
|
-
// --- src/pages/index/index.css ---
|
|
178
|
-
write('src/pages/index/index.css',
|
|
179
|
-
`.layout {
|
|
129
|
+
`);
|
|
130
|
+
write('src/pages/index/index.css', `.layout {
|
|
180
131
|
display: flex;
|
|
181
132
|
flex-direction: column;
|
|
182
133
|
min-height: 100vh;
|
|
@@ -186,31 +137,22 @@ write('src/pages/index/index.css',
|
|
|
186
137
|
flex: 1;
|
|
187
138
|
padding: 2rem;
|
|
188
139
|
}
|
|
189
|
-
`)
|
|
190
|
-
|
|
191
|
-
// --- src/components/AppHeader/AppHeader.js ---
|
|
192
|
-
write('src/components/AppHeader/AppHeader.js',
|
|
193
|
-
`import { Component } from '@odoo/owl'
|
|
140
|
+
`);
|
|
141
|
+
write('src/components/AppHeader/AppHeader.js', `import { Component } from '@odoo/owl'
|
|
194
142
|
|
|
195
143
|
export default class AppHeader extends Component {
|
|
196
144
|
static template = 'AppHeader'
|
|
197
145
|
}
|
|
198
|
-
`)
|
|
199
|
-
|
|
200
|
-
// --- src/components/AppHeader/AppHeader.xml ---
|
|
201
|
-
write('src/components/AppHeader/AppHeader.xml',
|
|
202
|
-
`<templates>
|
|
146
|
+
`);
|
|
147
|
+
write('src/components/AppHeader/AppHeader.xml', `<templates>
|
|
203
148
|
<t t-name="AppHeader">
|
|
204
149
|
<header class="app-header">
|
|
205
150
|
<span class="app-header__logo">${name}</span>
|
|
206
151
|
</header>
|
|
207
152
|
</t>
|
|
208
153
|
</templates>
|
|
209
|
-
`)
|
|
210
|
-
|
|
211
|
-
// --- src/components/AppHeader/AppHeader.css ---
|
|
212
|
-
write('src/components/AppHeader/AppHeader.css',
|
|
213
|
-
`.app-header {
|
|
154
|
+
`);
|
|
155
|
+
write('src/components/AppHeader/AppHeader.css', `.app-header {
|
|
214
156
|
display: flex;
|
|
215
157
|
align-items: center;
|
|
216
158
|
padding: 0 1.5rem;
|
|
@@ -222,31 +164,22 @@ write('src/components/AppHeader/AppHeader.css',
|
|
|
222
164
|
font-weight: 600;
|
|
223
165
|
font-size: 1.1rem;
|
|
224
166
|
}
|
|
225
|
-
`)
|
|
226
|
-
|
|
227
|
-
// --- src/components/AppFooter/AppFooter.js ---
|
|
228
|
-
write('src/components/AppFooter/AppFooter.js',
|
|
229
|
-
`import { Component } from '@odoo/owl'
|
|
167
|
+
`);
|
|
168
|
+
write('src/components/AppFooter/AppFooter.js', `import { Component } from '@odoo/owl'
|
|
230
169
|
|
|
231
170
|
export default class AppFooter extends Component {
|
|
232
171
|
static template = 'AppFooter'
|
|
233
172
|
}
|
|
234
|
-
`)
|
|
235
|
-
|
|
236
|
-
// --- src/components/AppFooter/AppFooter.xml ---
|
|
237
|
-
write('src/components/AppFooter/AppFooter.xml',
|
|
238
|
-
`<templates>
|
|
173
|
+
`);
|
|
174
|
+
write('src/components/AppFooter/AppFooter.xml', `<templates>
|
|
239
175
|
<t t-name="AppFooter">
|
|
240
176
|
<footer class="app-footer">
|
|
241
177
|
<span>Built with metaowl</span>
|
|
242
178
|
</footer>
|
|
243
179
|
</t>
|
|
244
180
|
</templates>
|
|
245
|
-
`)
|
|
246
|
-
|
|
247
|
-
// --- src/components/AppFooter/AppFooter.css ---
|
|
248
|
-
write('src/components/AppFooter/AppFooter.css',
|
|
249
|
-
`.app-footer {
|
|
181
|
+
`);
|
|
182
|
+
write('src/components/AppFooter/AppFooter.css', `.app-footer {
|
|
250
183
|
display: flex;
|
|
251
184
|
align-items: center;
|
|
252
185
|
justify-content: center;
|
|
@@ -255,11 +188,8 @@ write('src/components/AppFooter/AppFooter.css',
|
|
|
255
188
|
color: #6b7280;
|
|
256
189
|
border-top: 1px solid #e5e7eb;
|
|
257
190
|
}
|
|
258
|
-
`)
|
|
259
|
-
|
|
260
|
-
// --- .cursorrules ---
|
|
261
|
-
write('.cursorrules',
|
|
262
|
-
`# MetaOwl Project Rules
|
|
191
|
+
`);
|
|
192
|
+
write('.cursorrules', `# MetaOwl Project Rules
|
|
263
193
|
|
|
264
194
|
## Framework Overview
|
|
265
195
|
This is a MetaOwl application - a lightweight meta-framework for Odoo OWL built on top of Vite.
|
|
@@ -296,7 +226,7 @@ This is a MetaOwl application - a lightweight meta-framework for Odoo OWL built
|
|
|
296
226
|
|
|
297
227
|
## File Structure
|
|
298
228
|
- pages/ - Route components
|
|
299
|
-
- components/ - Reusable components
|
|
229
|
+
- components/ - Reusable components
|
|
300
230
|
- layouts/ - Page layouts (optional)
|
|
301
231
|
- metaowl.js - App entry point
|
|
302
232
|
|
|
@@ -309,11 +239,8 @@ This is a MetaOwl application - a lightweight meta-framework for Odoo OWL built
|
|
|
309
239
|
## Documentation
|
|
310
240
|
- See README.md for full API reference
|
|
311
241
|
- See metaowl module docs for detailed API
|
|
312
|
-
`)
|
|
313
|
-
|
|
314
|
-
// --- CLAUDE.md ---
|
|
315
|
-
write('CLAUDE.md',
|
|
316
|
-
`# ${name} - MetaOwl Project
|
|
242
|
+
`);
|
|
243
|
+
write('CLAUDE.md', `# ${name} - MetaOwl Project
|
|
317
244
|
|
|
318
245
|
## Quick Start for Claude Code
|
|
319
246
|
|
|
@@ -329,7 +256,7 @@ This project uses MetaOwl, a meta-framework for Odoo OWL on Vite.
|
|
|
329
256
|
\`\`\`
|
|
330
257
|
src/
|
|
331
258
|
├── metaowl.js # App bootstrap
|
|
332
|
-
├── pages/ # Route components
|
|
259
|
+
├── pages/ # Route components
|
|
333
260
|
│ └── index/
|
|
334
261
|
│ ├── Index.js # Page component
|
|
335
262
|
│ ├── Index.xml # OWL template
|
|
@@ -420,7 +347,7 @@ src/
|
|
|
420
347
|
│ └── AdminLayout.xml
|
|
421
348
|
\`\`\`
|
|
422
349
|
|
|
423
|
-
Layout Template uses \`<t t-slot
|
|
350
|
+
Layout Template uses \`<t t-slot="default"/>\` to render page content:
|
|
424
351
|
\`\`\`xml
|
|
425
352
|
<t t-name="AdminLayout">
|
|
426
353
|
<div class="layout-admin">
|
|
@@ -461,11 +388,8 @@ Meta.canonical('https://example.com/page')
|
|
|
461
388
|
- File-based routing uses directory name, not file name
|
|
462
389
|
- OWL uses xml templates, not JSX
|
|
463
390
|
- Static properties are required (template, components)
|
|
464
|
-
`)
|
|
465
|
-
|
|
466
|
-
// --- llms.txt ---
|
|
467
|
-
write('llms.txt',
|
|
468
|
-
`# MetaOwl LLM Context
|
|
391
|
+
`);
|
|
392
|
+
write('llms.txt', `# MetaOwl LLM Context
|
|
469
393
|
|
|
470
394
|
## Framework Identity
|
|
471
395
|
MetaOwl is a lightweight meta-framework for Odoo OWL (Odoo Web Library) applications, built on Vite. It provides file-based routing, state management, and app mounting.
|
|
@@ -611,11 +535,8 @@ export default class ComponentName extends Component {
|
|
|
611
535
|
- README.md - Full documentation
|
|
612
536
|
- https://github.com/odoo/owl - OWL framework
|
|
613
537
|
- https://vitejs.dev - Vite documentation
|
|
614
|
-
`)
|
|
615
|
-
|
|
616
|
-
// --- .github/copilot-instructions.md ---
|
|
617
|
-
write('.github/copilot-instructions.md',
|
|
618
|
-
`# GitHub Copilot Instructions for MetaOwl
|
|
538
|
+
`);
|
|
539
|
+
write('.github/copilot-instructions.md', `# GitHub Copilot Instructions for MetaOwl
|
|
619
540
|
|
|
620
541
|
## About This Project
|
|
621
542
|
This is a MetaOwl application - a lightweight meta-framework for Odoo OWL (Odoo Web Library) built on Vite.
|
|
@@ -731,15 +652,13 @@ router.beforeEach((to, from, next) => {
|
|
|
731
652
|
- Scope CSS to components
|
|
732
653
|
- Use Meta helpers for SEO
|
|
733
654
|
- Leverage Store for shared state
|
|
734
|
-
`)
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
console.log()
|
|
738
|
-
|
|
739
|
-
console.log()
|
|
740
|
-
console.log(
|
|
741
|
-
console.log()
|
|
742
|
-
console.log(
|
|
743
|
-
console.log(
|
|
744
|
-
console.log(` npm run dev`)
|
|
745
|
-
console.log()
|
|
655
|
+
`);
|
|
656
|
+
console.log();
|
|
657
|
+
success(`Project "${name}" ready`);
|
|
658
|
+
console.log();
|
|
659
|
+
console.log(' Next steps:');
|
|
660
|
+
console.log();
|
|
661
|
+
console.log(` cd ${name}`);
|
|
662
|
+
console.log(' npm install');
|
|
663
|
+
console.log(' npm run dev');
|
|
664
|
+
console.log();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* metaowl dev — start the Vite development server.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { banner, cwd, resolveBin, step } from './utils.js';
|
|
7
|
+
banner('dev');
|
|
8
|
+
step('Starting development server...');
|
|
9
|
+
console.log();
|
|
10
|
+
execSync(`"${resolveBin('vite')}"`, { stdio: 'inherit', cwd });
|
|
@@ -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();
|