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
package/eslint.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import js from '@eslint/js'
2
+ import tsParser from '@typescript-eslint/parser'
3
+ import tsPlugin from '@typescript-eslint/eslint-plugin'
2
4
  import globals from 'globals'
3
5
 
4
6
  /**
@@ -41,6 +43,33 @@ export const eslintConfig = [
41
43
  'no-undef': 'off'
42
44
  }
43
45
  },
46
+ {
47
+ files: ['**/*.ts', '**/*.tsx'],
48
+ languageOptions: {
49
+ ecmaVersion: 2022,
50
+ sourceType: 'module',
51
+ parser: tsParser,
52
+ globals: {
53
+ ...globals.browser,
54
+ ...globals.node,
55
+ COMPONENTS: 'readonly'
56
+ }
57
+ },
58
+ plugins: {
59
+ '@typescript-eslint': tsPlugin
60
+ },
61
+ rules: {
62
+ 'no-unused-vars': 'off',
63
+ '@typescript-eslint/no-unused-vars': ['error', {
64
+ argsIgnorePattern: '^_',
65
+ varsIgnorePattern: '^_'
66
+ }],
67
+ 'semi': ['error', 'never'],
68
+ 'quotes': ['error', 'single'],
69
+ 'comma-dangle': ['error', 'never'],
70
+ 'no-undef': 'off'
71
+ }
72
+ },
44
73
  {
45
74
  ignores: [
46
75
  'node_modules/**',
package/package.json CHANGED
@@ -1,22 +1,56 @@
1
1
  {
2
2
  "name": "metaowl",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Lightweight meta-framework for Odoo OWL — file-based routing, app mounting, Fetch helper, Cache, Meta tags, SSG generator, and a Vite plugin.",
5
+ "author": "Dennis Schott",
6
+ "license": "LGPL-3.0-only",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dennisschott/metaowl"
10
+ },
11
+ "homepage": "https://metaowl.org",
12
+ "bugs": {
13
+ "url": "https://github.com/dennisschott/metaowl/issues"
14
+ },
5
15
  "type": "module",
6
- "main": "index.js",
16
+ "main": "./build/runtime/index.js",
7
17
  "exports": {
8
- ".": "./index.js",
9
- "./vite": "./vite/plugin.js",
18
+ ".": "./build/runtime/index.js",
19
+ "./vite": "./build/runtime/vite/plugin.js",
10
20
  "./eslint": "./eslint.js",
11
21
  "./postcss": "./postcss.cjs"
12
22
  },
13
23
  "bin": {
14
- "metaowl-create": "./bin/metaowl-create.js",
15
- "metaowl-dev": "./bin/metaowl-dev.js",
16
- "metaowl-build": "./bin/metaowl-build.js",
17
- "metaowl-generate": "./bin/metaowl-generate.js",
18
- "metaowl-lint": "./bin/metaowl-lint.js"
24
+ "metaowl-create": "./build/runtime/bin/metaowl-create.js",
25
+ "metaowl-dev": "./build/runtime/bin/metaowl-dev.js",
26
+ "metaowl-build": "./build/runtime/bin/metaowl-build.js",
27
+ "metaowl-generate": "./build/runtime/bin/metaowl-generate.js",
28
+ "metaowl-lint": "./build/runtime/bin/metaowl-lint.js"
29
+ },
30
+ "scripts": {
31
+ "clean:runtime": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('build/runtime', { recursive: true, force: true })\"",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "lint": "eslint . --ext .js,.ts",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit",
36
+ "release:check": "npm run typecheck && npm test",
37
+ "build:runtime": "npm run clean:runtime && tsc -p tsconfig.build.json && node --input-type=module -e \"import { chmodSync, readdirSync } from 'node:fs'; for (const entry of readdirSync('build/runtime/bin')) { if (entry.endsWith('.js')) chmodSync('build/runtime/bin/' + entry, 0o755); }\"",
38
+ "release:build": "npm run release:check && npm run build:runtime",
39
+ "release:pack": "npm run release:check && npm pack --dry-run",
40
+ "prepack": "npm run build:runtime"
19
41
  },
42
+ "engines": {
43
+ "node": ">=20.0.0"
44
+ },
45
+ "files": [
46
+ "build/runtime",
47
+ "config",
48
+ "eslint.js",
49
+ "postcss.cjs",
50
+ "README.md",
51
+ "LICENSE",
52
+ "CHANGELOG.md"
53
+ ],
20
54
  "keywords": [
21
55
  "owl",
22
56
  "odoo",
@@ -24,17 +58,7 @@
24
58
  "vite",
25
59
  "meta-framework"
26
60
  ],
27
- "author": "Dennis Schott",
28
- "license": "LGPL-3.0-only",
29
61
  "sideEffects": false,
30
- "repository": {
31
- "type": "git",
32
- "url": "https://github.com/dennisschott/metaowl"
33
- },
34
- "homepage": "https://metaowl.org",
35
- "bugs": {
36
- "url": "https://github.com/dennisschott/metaowl/issues"
37
- },
38
62
  "dependencies": {
39
63
  "@eslint/js": "^9.20.1",
40
64
  "@odoo/owl": "^2.8.2",
@@ -51,15 +75,9 @@
51
75
  "vite": "^8.0.0",
52
76
  "vite-tsconfig-paths": "^6.1.1"
53
77
  },
54
- "engines": {
55
- "node": ">=20.0.0"
56
- },
57
- "scripts": {
58
- "test": "vitest run",
59
- "test:watch": "vitest",
60
- "lint": "eslint . --ext .js,.ts"
61
- },
62
78
  "devDependencies": {
79
+ "@types/jsdom": "^28.0.1",
80
+ "@types/node": "^25.6.0",
63
81
  "jsdom": "^28.1.0",
64
82
  "vitest": "^4.1.0"
65
83
  }
package/CONTRIBUTING.md DELETED
@@ -1,49 +0,0 @@
1
- # Contributing to metaowl
2
-
3
- Thank you for considering a contribution! Here is how to get started.
4
-
5
- ## Reporting Issues
6
-
7
- Before opening a new issue, please [search existing issues](https://github.com/dennisschott/metaowl/issues) to avoid duplicates. When filing a bug report, include:
8
-
9
- - metaowl version (`npm ls metaowl`)
10
- - Node.js version (`node --version`)
11
- - Minimal reproduction steps or a repository link
12
- - Expected vs. actual behaviour
13
-
14
- ## Development Setup
15
-
16
- ```bash
17
- git clone https://github.com/dennisschott/metaowl.git
18
- cd metaowl
19
- npm install
20
- ```
21
-
22
- ## Submitting a Pull Request
23
-
24
- 1. **Open an issue first** to discuss significant changes before investing time in an implementation.
25
- 2. Fork the repository and create a feature branch from `main`:
26
- ```bash
27
- git checkout -b feat/my-feature
28
- ```
29
- 3. Make your changes, keeping scope minimal and focused.
30
- 4. Ensure the project is lint-clean:
31
- ```bash
32
- node bin/metaowl-lint.js
33
- ```
34
- 5. Commit using [Conventional Commits](https://www.conventionalcommits.org):
35
- - `feat:` — new features
36
- - `fix:` — bug fixes
37
- - `docs:` — documentation only
38
- - `chore:` — tooling, dependencies
39
- 6. Push your branch and open a pull request against `main`.
40
-
41
- ## Code Style
42
-
43
- - ES modules (`import`/`export`) throughout.
44
- - No semicolons; single quotes; no trailing commas (enforced by ESLint).
45
- - Keep public API surface minimal — prefer extending existing modules over adding new entry points.
46
-
47
- ## License
48
-
49
- By contributing you agree that your contributions will be licensed under the [MIT License](LICENSE).
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * metaowl build — lint then production build.
4
- */
5
- import { banner, metaowlRoot, resolveBin, run, success } from './utils.js'
6
-
7
- banner('build')
8
- run('Linting', `node "${metaowlRoot}/bin/metaowl-lint.js"`)
9
- run('Building', `"${resolveBin('vite')}" build`)
10
- success('Build complete')
11
- console.log()
12
-
@@ -1,12 +0,0 @@
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
-
8
- banner('dev')
9
- step('Starting development server...')
10
- console.log()
11
-
12
- execSync(`"${resolveBin('vite')}"`, { stdio: 'inherit', cwd })
@@ -1,339 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * metaowl generate — SSG production build.
4
- *
5
- * Runs lint + vite build, then generates a static HTML file for every page:
6
- * - <title> and meta tags extracted statically from Meta.*() calls in the page JS
7
- * - Static HTML content auto-extracted from the page's OWL XML template,
8
- * with OWL-specific syntax stripped (best-effort, no JS evaluated)
9
- *
10
- * pagesDir / outDir can be overridden in package.json under "metaowl":
11
- * { "metaowl": { "pagesDir": "src/pages", "outDir": "dist" } }
12
- */
13
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
14
- import { resolve } from 'node:path'
15
- import { globSync } from 'glob'
16
- import { banner, cwd, metaowlRoot, resolveBin, run, step, success, failure } from './utils.js'
17
-
18
- banner('generate')
19
-
20
- function escapeAttr(str) {
21
- return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;')
22
- }
23
-
24
- /**
25
- * Statically extract Meta.*() call arguments from JS source.
26
- * Only works for string literals — dynamic values are skipped.
27
- *
28
- * Returns an object with the keys:
29
- * title, description, keywords, author, canonical,
30
- * ogTitle, ogDescription, ogImage, ogUrl, ogType, ogSiteName
31
- */
32
- function extractMetaFromJs(src) {
33
- const meta = {}
34
- const fns = [
35
- 'title', 'description', 'keywords', 'author', 'canonical',
36
- 'ogTitle', 'ogDescription', 'ogImage', 'ogUrl', 'ogType', 'ogSiteName',
37
- ]
38
- for (const fn of fns) {
39
- const m = src.match(new RegExp(`Meta\\.${fn}\\s*\\(\\s*(['"\`])([^'"\`]+)\\1\\s*\\)`))
40
- if (m) meta[fn] = m[2]
41
- }
42
- return meta
43
- }
44
-
45
- /**
46
- * Inject extracted meta values into the HTML head.
47
- * Returns the modified HTML string.
48
- */
49
- function injectMeta(html, meta) {
50
- if (meta.title) {
51
- html = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeAttr(meta.title)}</title>`)
52
- }
53
- /** @param {string} selector @param {string} tag */
54
- const injectTag = (selector, tag) => {
55
- html = html.replace(new RegExp(`\\s*${selector}[^>]*>\\s*`, 'gi'), '')
56
- html = html.replace('</head>', ` ${tag}\n </head>`)
57
- }
58
- if (meta.description)
59
- injectTag('<meta\\s+name="description"', `<meta name="description" content="${escapeAttr(meta.description)}">`)
60
- if (meta.keywords)
61
- injectTag('<meta\\s+name="keywords"', `<meta name="keywords" content="${escapeAttr(meta.keywords)}">`)
62
- if (meta.author)
63
- injectTag('<meta\\s+name="author"', `<meta name="author" content="${escapeAttr(meta.author)}">`)
64
- if (meta.canonical)
65
- injectTag('<link\\s+rel="canonical"', `<link rel="canonical" href="${escapeAttr(meta.canonical)}">`)
66
- if (meta.ogTitle)
67
- injectTag('<meta\\s+property="og:title"', `<meta property="og:title" content="${escapeAttr(meta.ogTitle)}">`)
68
- if (meta.ogDescription)
69
- injectTag('<meta\\s+property="og:description"', `<meta property="og:description" content="${escapeAttr(meta.ogDescription)}">`)
70
- if (meta.ogImage)
71
- injectTag('<meta\\s+property="og:image"', `<meta property="og:image" content="${escapeAttr(meta.ogImage)}">`)
72
- if (meta.ogUrl)
73
- injectTag('<meta\\s+property="og:url"', `<meta property="og:url" content="${escapeAttr(meta.ogUrl)}">`)
74
- if (meta.ogType)
75
- injectTag('<meta\\s+property="og:type"', `<meta property="og:type" content="${escapeAttr(meta.ogType)}">`)
76
- if (meta.ogSiteName)
77
- injectTag('<meta\\s+property="og:site_name"', `<meta property="og:site_name" content="${escapeAttr(meta.ogSiteName)}">`)
78
- return html
79
- }
80
-
81
- // Read project config
82
- const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf-8'))
83
- const metaowlConfig = pkg.metaowl ?? {}
84
- const pagesDir = metaowlConfig.pagesDir ?? 'src/pages'
85
- const outDir = metaowlConfig.outDir ?? 'dist'
86
-
87
- // Derive URL route from a page file path
88
- function deriveRoute(pageFile) {
89
- const rel = pageFile.replace(new RegExp(`^${pagesDir}[\\/]`), '')
90
- const parts = rel.split('/').slice(0, -1)
91
- if (parts.length === 1 && parts[0] === 'index') return '/'
92
- return '/' + parts.join('/')
93
- }
94
-
95
- /**
96
- * Extracts the layout name from a page component JS file.
97
- * Looks for `static layout = 'layoutName'` or `@layout('layoutName')`.
98
- *
99
- * @param {string} pageFile - Absolute path to the page JS file
100
- * @returns {string|null} Layout name or null if not found
101
- */
102
- function extractLayoutName(pageFile) {
103
- const jsSource = readFileSync(pageFile, 'utf-8')
104
-
105
- // Match: static layout = 'layoutName' or static layout = "layoutName"
106
- let match = jsSource.match(/static\s+layout\s*=\s*['"]([^'"]+)['"]/)
107
- if (match) return match[1]
108
-
109
- // Match: @layout('layoutName') or @layout("layoutName")
110
- match = jsSource.match(/@layout\s*\(\s*['"]([^'"]+)['"]\s*\)/)
111
- if (match) return match[1]
112
-
113
- // Match: @defineLayout('layoutName', ...)
114
- match = jsSource.match(/@defineLayout\s*\(\s*['"]([^'"]+)['"]/)
115
- if (match) return match[1]
116
-
117
- return 'default'
118
- }
119
-
120
- /**
121
- * Converts an OWL XML template to best-effort static HTML.
122
- * - Strips t-name on the root element
123
- * - Strips all t-* attributes
124
- * - Unwraps bare <t> elements (replaces with their inner content)
125
- * - Replaces PascalCase component tags with their template content
126
- * - Replaces t-slot="default" with provided page content
127
- * - Removes <templates> wrapper and root <t> elements
128
- *
129
- * @param {string} xml - OWL XML template
130
- * @param {string} [pageContent] - Optional page content to inject into t-slot="default"
131
- * @param {object} [options] - Options object
132
- * @param {Map<string, string>} [options.templateCache] - Cache of template name -> content
133
- */
134
- function xmlToStaticHtml(xml, pageContent = '', options = {}) {
135
- const { templateCache } = options
136
- let html = xml
137
-
138
- // Remove <templates> wrapper
139
- html = html.replace(/<templates>/g, '').replace(/<\/templates>/g, '')
140
-
141
- // Remove root <t> elements (self-closing or with content)
142
- // Match <t ...>...</t> at the start and end
143
- html = html.replace(/^\s*<t[^>]*>/, '').replace(/<\/t>\s*$/, '')
144
-
145
- // Remove t-name attribute from root
146
- html = html.replace(/\s+t-name="[^"]*"/g, '')
147
- // Remove all t-* attributes (handles both t-if="..." and bare t-else/t-else)
148
- html = html.replace(/\s+t-[\w-]+(="[^"]*")?/g, '')
149
- // Unwrap bare <t> wrapper elements (self-closing)
150
- html = html.replace(/<t\s*\/>/g, '')
151
- // Unwrap <t ...> ... </t> blocks — replace opening/closing tags with content
152
- html = html.replace(/<t(?:\s[^>]*)?>([\s\S]*?)<\/t>/g, (_, inner) => inner)
153
- // Replace t-slot="default" with page content (if provided)
154
- if (pageContent) {
155
- html = html.replace(/<t\s+t-slot="default"\s*\/?>/g, pageContent)
156
- html = html.replace(/<t\s+t-slot="default"[^>]*>([\s\S]*?)<\/t>/g, pageContent)
157
- }
158
-
159
- // Replace PascalCase component tags with their template content (if available)
160
- if (templateCache) {
161
- // Keep replacing until no more changes (for nested components)
162
- let previousHtml
163
- do {
164
- previousHtml = html
165
- html = html.replace(/<([A-Z][A-Za-z0-9]*)\s*\/>/g, (match, componentName) => {
166
- // Try different naming conventions
167
- const templateNames = [
168
- componentName,
169
- componentName.charAt(0).toLowerCase() + componentName.slice(1),
170
- componentName + 'Component',
171
- ]
172
- for (const name of templateNames) {
173
- if (templateCache.has(name)) {
174
- return templateCache.get(name)
175
- }
176
- }
177
- return match // Keep original if not found
178
- })
179
- html = html.replace(/<([A-Z][A-Za-z0-9]*)(?:\s[^>]*)?>([\s\S]*?)<\/\1>/g, (match, componentName, innerContent) => {
180
- const templateNames = [
181
- componentName,
182
- componentName.charAt(0).toLowerCase() + componentName.slice(1),
183
- componentName + 'Component',
184
- ]
185
- for (const name of templateNames) {
186
- if (templateCache.has(name)) {
187
- return templateCache.get(name)
188
- }
189
- }
190
- return match
191
- })
192
- } while (html !== previousHtml)
193
- } else {
194
- // No cache available, use comment stubs as fallback
195
- html = html.replace(/<([A-Z][A-Za-z0-9]*)\s*\/>/g, '<!-- $1 -->')
196
- html = html.replace(/<([A-Z][A-Za-z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, '<!-- $1 -->')
197
- }
198
-
199
- return html.trim()
200
- }
201
-
202
- // Build final HTML shell for a page
203
- function buildShell(baseHtml, pageFile) {
204
- let html = baseHtml
205
-
206
- // Extract meta tags from JS source (Meta.title(...), Meta.description(...) etc.)
207
- const jsSource = readFileSync(resolve(cwd, pageFile), 'utf-8')
208
- const meta = extractMetaFromJs(jsSource)
209
- html = injectMeta(html, meta)
210
-
211
- // Get the page's layout name
212
- const layoutName = extractLayoutName(resolve(cwd, pageFile))
213
- const layoutsDir = metaowlConfig.layoutsDir ?? 'src/layouts'
214
- const componentsDir = metaowlConfig.componentsDir ?? 'src/components'
215
-
216
- // Build template cache from all XML files (components, pages, layouts)
217
- const templateCache = new Map()
218
-
219
- // Scan component XML files
220
- const componentXmlFiles = globSync(`${componentsDir}/**/*.xml`, { cwd })
221
- for (const componentXmlFile of componentXmlFiles) {
222
- const content = readFileSync(resolve(cwd, componentXmlFile), 'utf-8')
223
- // Extract t-name values and their content: <t t-name="ComponentName">...content...</t>
224
- const tNameMatches = content.matchAll(/<t\s+t-name="([^"]+)"[^>]*>([\s\S]*?)<\/t>/g)
225
- for (const match of tNameMatches) {
226
- const templateName = match[1]
227
- const templateContent = match[2]
228
- templateCache.set(templateName, templateContent)
229
- }
230
- // Also try to get the root element as the template (without t-name)
231
- const rootMatch = content.match(/<templates>\s*<t[^>]*>([\s\S]*?)<\/t>\s*<\/templates>/)
232
- if (rootMatch) {
233
- // Try to derive component name from file path
234
- const fileName = componentXmlFile.replace(/\.xml$/, '').split('/').pop()
235
- if (fileName) {
236
- templateCache.set(fileName, rootMatch[1])
237
- }
238
- }
239
- }
240
-
241
- // Also scan layout XML files for templates
242
- const layoutXmlFiles = globSync(`${layoutsDir}/**/*.xml`, { cwd })
243
- for (const layoutXmlFile of layoutXmlFiles) {
244
- const content = readFileSync(resolve(cwd, layoutXmlFile), 'utf-8')
245
- const tNameMatches = content.matchAll(/<t\s+t-name="([^"]+)"[^>]*>([\s\S]*?)<\/t>/g)
246
- for (const match of tNameMatches) {
247
- templateCache.set(match[1], match[2])
248
- }
249
- }
250
-
251
- // Also scan page XML files for templates
252
- const pageXmlFiles = globSync(`${pagesDir}/**/*.xml`, { cwd })
253
- for (const pageXmlFile of pageXmlFiles) {
254
- const content = readFileSync(resolve(cwd, pageXmlFile), 'utf-8')
255
- const tNameMatches = content.matchAll(/<t\s+t-name="([^"]+)"[^>]*>([\s\S]*?)<\/t>/g)
256
- for (const match of tNameMatches) {
257
- templateCache.set(match[1], match[2])
258
- }
259
- // Try to get the root element as the template (without t-name)
260
- const rootMatch = content.match(/<templates>\s*<t[^>]*>([\s\S]*?)<\/t>\s*<\/templates>/)
261
- if (rootMatch) {
262
- const fileName = pageXmlFile.replace(/\.xml$/, '').split('/').pop()
263
- if (fileName) {
264
- templateCache.set(fileName, rootMatch[1])
265
- }
266
- }
267
- }
268
-
269
- // Try to find and read the layout XML template
270
- let finalContent = ''
271
-
272
- // First, try to read the layout XML
273
- const layoutXmlFile = resolve(cwd, layoutsDir, layoutName, `${layoutName.charAt(0).toUpperCase() + layoutName.slice(1)}Layout.xml`)
274
- const layoutXmlExists = existsSync(layoutXmlFile)
275
-
276
- // Read the page XML template
277
- const pageXmlFile = resolve(cwd, pageFile.replace(/\.js$/, '.xml'))
278
- const pageXmlExists = existsSync(pageXmlFile)
279
-
280
- if (layoutXmlExists && pageXmlExists) {
281
- // Read both layout and page templates
282
- const layoutXmlContent = readFileSync(layoutXmlFile, 'utf-8')
283
- const pageXmlContent = readFileSync(pageXmlFile, 'utf-8')
284
-
285
- // Convert page XML to static HTML first
286
- const pageStaticHtml = xmlToStaticHtml(pageXmlContent, '', { templateCache })
287
-
288
- // Then inject into layout's t-slot="default"
289
- finalContent = xmlToStaticHtml(layoutXmlContent, pageStaticHtml, { templateCache })
290
- } else if (pageXmlExists) {
291
- // No layout found, just use page content
292
- const pageXmlContent = readFileSync(pageXmlFile, 'utf-8')
293
- finalContent = xmlToStaticHtml(pageXmlContent, '', { templateCache })
294
- }
295
-
296
- // Inject the combined layout + page HTML
297
- if (finalContent) {
298
- html = html.replace(/(<div\s+id="metaowl"[^>]*>)(<\/div>)/, `$1${finalContent}$2`)
299
- }
300
-
301
- return html
302
- }
303
-
304
- // 1. Lint
305
- run('Linting', `node "${metaowlRoot}/bin/metaowl-lint.js"`)
306
-
307
- // 2. Vite build
308
- run('Building', `"${resolveBin('vite')}" build`)
309
-
310
- // 3. SSG post-processing
311
- step('Generating static pages...')
312
- console.log()
313
- const baseHtml = readFileSync(resolve(cwd, outDir, 'index.html'), 'utf-8')
314
-
315
- const pageFiles = globSync(`${pagesDir}/**/*.js`, { cwd })
316
-
317
- const seen = new Set()
318
-
319
- for (const pageFile of pageFiles) {
320
- const route = deriveRoute(pageFile)
321
- if (seen.has(route)) continue
322
- seen.add(route)
323
-
324
- const shell = buildShell(baseHtml, pageFile)
325
-
326
- if (route === '/') {
327
- writeFileSync(resolve(cwd, outDir, 'index.html'), shell)
328
- console.log(` /index.html`)
329
- } else {
330
- const destDir = resolve(cwd, outDir, route.slice(1))
331
- mkdirSync(destDir, { recursive: true })
332
- writeFileSync(resolve(destDir, 'index.html'), shell)
333
- console.log(` ${route}/index.html`)
334
- }
335
- }
336
-
337
- console.log()
338
- success(`${seen.size} route(s) generated`)
339
- console.log()
@@ -1,71 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * metaowl lint — format with Prettier then lint with ESLint.
4
- *
5
- * Lint targets can be configured in package.json under `metaowl.lint`:
6
- *
7
- * "metaowl": {
8
- * "lint": ["src/app.js", "src/pages/**", "src/components/**"]
9
- * }
10
- */
11
- import { execSync } from 'node:child_process'
12
- import { existsSync, readFileSync } from 'node:fs'
13
- import { resolve } from 'node:path'
14
- import { globSync } from 'glob'
15
- import { banner, cwd, resolveBin, step, success, failure } from './utils.js'
16
-
17
- banner('lint')
18
-
19
- let lintTargets = null
20
- try {
21
- const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf8'))
22
- lintTargets = pkg?.metaowl?.lint ?? null
23
- } catch {
24
- // no package.json or no metaowl config
25
- }
26
-
27
- const defaults = [
28
- 'src/metaowl.js',
29
- 'src/css.js',
30
- 'src/pages/**',
31
- 'src/components/**'
32
- ]
33
-
34
- const candidates = lintTargets ?? defaults
35
-
36
- const existing = candidates.filter(pattern => {
37
- if (existsSync(resolve(cwd, pattern))) return true
38
- return globSync(pattern, { cwd }).length > 0
39
- })
40
-
41
- if (existing.length === 0) {
42
- success('No lint targets found — skipping')
43
- console.log()
44
- process.exit(0)
45
- }
46
-
47
- const targets = existing.map(t => `"${t}"`).join(' ')
48
-
49
- step('Formatting with Prettier...')
50
- console.log()
51
- try {
52
- execSync(`"${resolveBin('prettier')}" src --single-quote --no-semi --write`, { stdio: 'inherit', cwd })
53
- } catch {
54
- failure('Prettier failed')
55
- process.exit(1)
56
- }
57
- console.log()
58
-
59
- step('Linting with ESLint...')
60
- console.log()
61
- try {
62
- execSync(`"${resolveBin('eslint')}" ${targets} --fix`, { stdio: 'inherit', cwd })
63
- } catch {
64
- failure('ESLint failed')
65
- process.exit(1)
66
- }
67
- console.log()
68
-
69
- success('Lint complete')
70
- console.log()
71
-