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
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.
|
|
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).
|
package/bin/metaowl-build.js
DELETED
|
@@ -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
|
-
|
package/bin/metaowl-dev.js
DELETED
|
@@ -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 })
|
package/bin/metaowl-generate.js
DELETED
|
@@ -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, '&').replace(/"/g, '"')
|
|
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()
|
package/bin/metaowl-lint.js
DELETED
|
@@ -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
|
-
|