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.
Files changed (83) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +267 -2
  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 +144 -0
  10. package/build/runtime/modules/app-mounter.js +73 -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/constants.js +38 -0
  15. package/build/runtime/modules/error-boundary.js +116 -0
  16. package/build/runtime/modules/fetch.js +31 -0
  17. package/build/runtime/modules/file-router.js +207 -0
  18. package/build/runtime/modules/fonts.js +172 -0
  19. package/build/runtime/modules/forms.js +193 -0
  20. package/build/runtime/modules/i18n.js +180 -0
  21. package/build/runtime/modules/image.js +175 -0
  22. package/build/runtime/modules/layouts.js +214 -0
  23. package/build/runtime/modules/link.js +141 -0
  24. package/build/runtime/modules/meta.js +117 -0
  25. package/build/runtime/modules/odoo-rpc.js +265 -0
  26. package/build/runtime/modules/pwa.js +272 -0
  27. package/build/runtime/modules/router.js +384 -0
  28. package/build/runtime/modules/seo.js +186 -0
  29. package/build/runtime/modules/store.js +198 -0
  30. package/build/runtime/modules/templates-manager.js +52 -0
  31. package/build/runtime/modules/test-utils.js +238 -0
  32. package/build/runtime/vite/plugin.js +197 -0
  33. package/eslint.js +29 -0
  34. package/package.json +45 -27
  35. package/CONTRIBUTING.md +0 -49
  36. package/bin/metaowl-build.js +0 -12
  37. package/bin/metaowl-dev.js +0 -12
  38. package/bin/metaowl-generate.js +0 -339
  39. package/bin/metaowl-lint.js +0 -71
  40. package/bin/utils.js +0 -82
  41. package/eslint.config.js +0 -3
  42. package/index.js +0 -328
  43. package/modules/app-mounter.js +0 -104
  44. package/modules/auto-import.js +0 -225
  45. package/modules/cache.js +0 -59
  46. package/modules/composables.js +0 -600
  47. package/modules/error-boundary.js +0 -228
  48. package/modules/fetch.js +0 -51
  49. package/modules/file-router.js +0 -478
  50. package/modules/forms.js +0 -353
  51. package/modules/i18n.js +0 -333
  52. package/modules/layouts.js +0 -431
  53. package/modules/link.js +0 -255
  54. package/modules/meta.js +0 -119
  55. package/modules/odoo-rpc.js +0 -511
  56. package/modules/pwa.js +0 -515
  57. package/modules/router.js +0 -769
  58. package/modules/seo.js +0 -501
  59. package/modules/store.js +0 -409
  60. package/modules/templates-manager.js +0 -89
  61. package/modules/test-utils.js +0 -532
  62. package/test/auto-import.test.js +0 -110
  63. package/test/cache.test.js +0 -55
  64. package/test/composables.test.js +0 -103
  65. package/test/dynamic-routes.test.js +0 -469
  66. package/test/error-boundary.test.js +0 -126
  67. package/test/fetch.test.js +0 -100
  68. package/test/file-router.test.js +0 -55
  69. package/test/forms.test.js +0 -203
  70. package/test/i18n.test.js +0 -188
  71. package/test/layouts.test.js +0 -395
  72. package/test/link.test.js +0 -189
  73. package/test/meta.test.js +0 -146
  74. package/test/odoo-rpc.test.js +0 -547
  75. package/test/pwa.test.js +0 -154
  76. package/test/router-guards.test.js +0 -229
  77. package/test/router.test.js +0 -77
  78. package/test/seo.test.js +0 -353
  79. package/test/store.test.js +0 -476
  80. package/test/templates-manager.test.js +0 -83
  81. package/test/test-utils.test.js +0 -314
  82. package/vite/plugin.js +0 -290
  83. 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 { createInterface } from 'node:readline/promises'
11
- import { mkdirSync, writeFileSync, existsSync } from 'node:fs'
12
- import { resolve, join, dirname } from 'node:path'
13
- import { banner, step, success, failure, version } from './utils.js'
14
-
15
- banner('create')
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
- const rl = createInterface({ input: process.stdin, output: process.stdout })
22
- name = (await rl.question(' Project name: ')).trim()
23
- rl.close()
24
- console.log()
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
- failure('Invalid project name. Use only letters, numbers, hyphens, or underscores.')
29
- process.exit(1)
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
- failure(`Directory "${name}" already exists.`)
36
- process.exit(1)
23
+ failure(`Directory "${name}" already exists.`);
24
+ process.exit(1);
37
25
  }
38
-
39
- step(`Scaffolding project "${name}"...`)
40
- console.log()
41
-
42
- // --- File writer helper ---
26
+ step(`Scaffolding project "${name}"...`);
27
+ console.log();
43
28
  function write(filePath, content) {
44
- const abs = join(dest, filePath)
45
- mkdirSync(dirname(abs), { recursive: true })
46
- writeFileSync(abs, content, 'utf-8')
47
- console.log(` ${filePath}`)
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
- name,
53
- version: '0.1.0',
54
- type: 'module',
55
- scripts: {
56
- dev: 'metaowl-dev',
57
- build: 'metaowl-build',
58
- generate: 'metaowl-generate',
59
- lint: 'metaowl-lint'
60
- },
61
- dependencies: {
62
- metaowl: `^${version}`
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
- extends: './node_modules/metaowl/config/jsconfig.base.json',
95
- compilerOptions: {
96
- baseUrl: '.',
97
- paths: {
98
- '@pages/*': ['src/pages/*'],
99
- '@components/*': ['src/components/*']
100
- }
101
- },
102
- include: ['src']
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 (file-based routing)
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=\"default\"/>\` to render page content:
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
- // --- Done ---
737
- console.log()
738
- success(`Project "${name}" ready`)
739
- console.log()
740
- console.log(' Next steps:')
741
- console.log()
742
- console.log(` cd ${name}`)
743
- console.log(` npm install`)
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, '&amp;').replace(/"/g, '&quot;');
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();