metaowl 0.1.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/CONTRIBUTING.md +49 -0
- package/LICENSE +147 -0
- package/README.md +543 -0
- package/bin/metaowl-build.js +12 -0
- package/bin/metaowl-create.js +270 -0
- package/bin/metaowl-dev.js +12 -0
- package/bin/metaowl-generate.js +176 -0
- package/bin/metaowl-lint.js +71 -0
- package/bin/utils.js +61 -0
- package/config/jsconfig.base.json +3 -0
- package/config/tsconfig.base.json +18 -0
- package/eslint.js +49 -0
- package/index.js +32 -0
- package/modules/app-mounter.js +40 -0
- package/modules/cache.js +57 -0
- package/modules/fetch.js +44 -0
- package/modules/file-router.js +60 -0
- package/modules/meta.js +119 -0
- package/modules/router.js +62 -0
- package/modules/templates-manager.js +20 -0
- package/package.json +68 -0
- package/postcss.cjs +43 -0
- package/test/cache.test.js +55 -0
- package/test/fetch.test.js +100 -0
- package/test/file-router.test.js +55 -0
- package/test/meta.test.js +146 -0
- package/test/router.test.js +77 -0
- package/test/templates-manager.test.js +62 -0
- package/vite/plugin.js +216 -0
- package/vitest.config.js +8 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
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
|
+
*/
|
|
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
|
+
|
|
20
|
+
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()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
28
|
+
failure('Invalid project name. Use only letters, numbers, hyphens, or underscores.')
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const dest = resolve(process.cwd(), name)
|
|
33
|
+
|
|
34
|
+
if (existsSync(dest)) {
|
|
35
|
+
failure(`Directory "${name}" already exists.`)
|
|
36
|
+
process.exit(1)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
step(`Scaffolding project "${name}"...`)
|
|
40
|
+
console.log()
|
|
41
|
+
|
|
42
|
+
// --- File writer helper ---
|
|
43
|
+
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}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- package.json ---
|
|
51
|
+
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'
|
|
69
|
+
|
|
70
|
+
export default metaowlConfig({
|
|
71
|
+
componentsDir: 'src/components',
|
|
72
|
+
pagesDir: 'src/pages'
|
|
73
|
+
})
|
|
74
|
+
`)
|
|
75
|
+
|
|
76
|
+
// --- eslint.config.js ---
|
|
77
|
+
write('eslint.config.js',
|
|
78
|
+
`import { eslintConfig } from 'metaowl/eslint'
|
|
79
|
+
|
|
80
|
+
export default eslintConfig
|
|
81
|
+
`)
|
|
82
|
+
|
|
83
|
+
// --- postcss.config.cjs ---
|
|
84
|
+
write('postcss.config.cjs',
|
|
85
|
+
`const { createPostcssConfig } = require('metaowl/postcss')
|
|
86
|
+
|
|
87
|
+
module.exports = createPostcssConfig()
|
|
88
|
+
`)
|
|
89
|
+
|
|
90
|
+
// --- jsconfig.json ---
|
|
91
|
+
write('jsconfig.json', JSON.stringify({
|
|
92
|
+
extends: './node_modules/metaowl/config/jsconfig.base.json',
|
|
93
|
+
compilerOptions: {
|
|
94
|
+
baseUrl: 'src',
|
|
95
|
+
paths: {
|
|
96
|
+
'@pages/*': ['pages/*'],
|
|
97
|
+
'@components/*': ['components/*']
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
include: ['src']
|
|
101
|
+
}, null, 2) + '\n')
|
|
102
|
+
|
|
103
|
+
// --- .gitignore ---
|
|
104
|
+
write('.gitignore',
|
|
105
|
+
`node_modules/
|
|
106
|
+
dist/
|
|
107
|
+
.env
|
|
108
|
+
`)
|
|
109
|
+
|
|
110
|
+
// --- src/index.html ---
|
|
111
|
+
write('src/index.html',
|
|
112
|
+
`<!doctype html>
|
|
113
|
+
<html lang="en">
|
|
114
|
+
<head>
|
|
115
|
+
<meta charset="UTF-8" />
|
|
116
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
117
|
+
<title>${name}</title>
|
|
118
|
+
</head>
|
|
119
|
+
<body>
|
|
120
|
+
<div id="metaowl"></div>
|
|
121
|
+
<script type="module" src="/metaowl.js"></script>
|
|
122
|
+
</body>
|
|
123
|
+
</html>
|
|
124
|
+
`)
|
|
125
|
+
|
|
126
|
+
// --- src/metaowl.js ---
|
|
127
|
+
write('src/metaowl.js',
|
|
128
|
+
`import { boot, Fetch } from 'metaowl'
|
|
129
|
+
|
|
130
|
+
Fetch.configure({
|
|
131
|
+
baseUrl: import.meta.env.VITE_API_URL ?? ''
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
boot()
|
|
135
|
+
`)
|
|
136
|
+
|
|
137
|
+
// --- src/css.js ---
|
|
138
|
+
write('src/css.js',
|
|
139
|
+
`// Global styles — import shared CSS here.
|
|
140
|
+
// Component and page CSS files are auto-imported by the metaowl Vite plugin.
|
|
141
|
+
`)
|
|
142
|
+
|
|
143
|
+
// --- src/pages/index/Index.js ---
|
|
144
|
+
write('src/pages/index/Index.js',
|
|
145
|
+
`import { Component } from '@odoo/owl'
|
|
146
|
+
import { Meta } from 'metaowl'
|
|
147
|
+
import AppHeader from '@components/AppHeader/AppHeader'
|
|
148
|
+
import AppFooter from '@components/AppFooter/AppFooter'
|
|
149
|
+
|
|
150
|
+
export default class Index extends Component {
|
|
151
|
+
static template = 'Index'
|
|
152
|
+
static components = { AppHeader, AppFooter }
|
|
153
|
+
|
|
154
|
+
setup() {
|
|
155
|
+
Meta.title('Home — ${name}')
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
`)
|
|
159
|
+
|
|
160
|
+
// --- src/pages/index/Index.xml ---
|
|
161
|
+
write('src/pages/index/Index.xml',
|
|
162
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
163
|
+
<templates>
|
|
164
|
+
<t t-name="Index">
|
|
165
|
+
<div class="layout">
|
|
166
|
+
<AppHeader />
|
|
167
|
+
<main class="page page-index">
|
|
168
|
+
<h1>Welcome to ${name}</h1>
|
|
169
|
+
</main>
|
|
170
|
+
<AppFooter />
|
|
171
|
+
</div>
|
|
172
|
+
</t>
|
|
173
|
+
</templates>
|
|
174
|
+
`)
|
|
175
|
+
|
|
176
|
+
// --- src/pages/index/index.css ---
|
|
177
|
+
write('src/pages/index/index.css',
|
|
178
|
+
`.layout {
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
min-height: 100vh;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.page-index {
|
|
185
|
+
flex: 1;
|
|
186
|
+
padding: 2rem;
|
|
187
|
+
}
|
|
188
|
+
`)
|
|
189
|
+
|
|
190
|
+
// --- src/components/AppHeader/AppHeader.js ---
|
|
191
|
+
write('src/components/AppHeader/AppHeader.js',
|
|
192
|
+
`import { Component } from '@odoo/owl'
|
|
193
|
+
|
|
194
|
+
export default class AppHeader extends Component {
|
|
195
|
+
static template = 'AppHeader'
|
|
196
|
+
}
|
|
197
|
+
`)
|
|
198
|
+
|
|
199
|
+
// --- src/components/AppHeader/AppHeader.xml ---
|
|
200
|
+
write('src/components/AppHeader/AppHeader.xml',
|
|
201
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
202
|
+
<templates>
|
|
203
|
+
<t t-name="AppHeader">
|
|
204
|
+
<header class="app-header">
|
|
205
|
+
<span class="app-header__logo">${name}</span>
|
|
206
|
+
</header>
|
|
207
|
+
</t>
|
|
208
|
+
</templates>
|
|
209
|
+
`)
|
|
210
|
+
|
|
211
|
+
// --- src/components/AppHeader/AppHeader.css ---
|
|
212
|
+
write('src/components/AppHeader/AppHeader.css',
|
|
213
|
+
`.app-header {
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
padding: 0 1.5rem;
|
|
217
|
+
height: 56px;
|
|
218
|
+
border-bottom: 1px solid #e5e7eb;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.app-header__logo {
|
|
222
|
+
font-weight: 600;
|
|
223
|
+
font-size: 1.1rem;
|
|
224
|
+
}
|
|
225
|
+
`)
|
|
226
|
+
|
|
227
|
+
// --- src/components/AppFooter/AppFooter.js ---
|
|
228
|
+
write('src/components/AppFooter/AppFooter.js',
|
|
229
|
+
`import { Component } from '@odoo/owl'
|
|
230
|
+
|
|
231
|
+
export default class AppFooter extends Component {
|
|
232
|
+
static template = 'AppFooter'
|
|
233
|
+
}
|
|
234
|
+
`)
|
|
235
|
+
|
|
236
|
+
// --- src/components/AppFooter/AppFooter.xml ---
|
|
237
|
+
write('src/components/AppFooter/AppFooter.xml',
|
|
238
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
239
|
+
<templates>
|
|
240
|
+
<t t-name="AppFooter">
|
|
241
|
+
<footer class="app-footer">
|
|
242
|
+
<span>Built with metaowl</span>
|
|
243
|
+
</footer>
|
|
244
|
+
</t>
|
|
245
|
+
</templates>
|
|
246
|
+
`)
|
|
247
|
+
|
|
248
|
+
// --- src/components/AppFooter/AppFooter.css ---
|
|
249
|
+
write('src/components/AppFooter/AppFooter.css',
|
|
250
|
+
`.app-footer {
|
|
251
|
+
display: flex;
|
|
252
|
+
align-items: center;
|
|
253
|
+
justify-content: center;
|
|
254
|
+
padding: 1rem;
|
|
255
|
+
font-size: 0.85rem;
|
|
256
|
+
color: #6b7280;
|
|
257
|
+
border-top: 1px solid #e5e7eb;
|
|
258
|
+
}
|
|
259
|
+
`)
|
|
260
|
+
|
|
261
|
+
// --- Done ---
|
|
262
|
+
console.log()
|
|
263
|
+
success(`Project "${name}" ready`)
|
|
264
|
+
console.log()
|
|
265
|
+
console.log(' Next steps:')
|
|
266
|
+
console.log()
|
|
267
|
+
console.log(` cd ${name}`)
|
|
268
|
+
console.log(` npm install`)
|
|
269
|
+
console.log(` npm run dev`)
|
|
270
|
+
console.log()
|
|
@@ -0,0 +1,12 @@
|
|
|
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, bin, cwd, step } from './utils.js'
|
|
7
|
+
|
|
8
|
+
banner('dev')
|
|
9
|
+
step('Starting development server...')
|
|
10
|
+
console.log()
|
|
11
|
+
|
|
12
|
+
execSync(`"${bin}/vite"`, { stdio: 'inherit', cwd })
|
|
@@ -0,0 +1,176 @@
|
|
|
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, bin, cwd, metaowlRoot, 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
|
+
* Converts an OWL XML template to best-effort static HTML.
|
|
97
|
+
* - Strips t-name on the root element
|
|
98
|
+
* - Strips all t-* attributes
|
|
99
|
+
* - Unwraps bare <t> elements (replaces with their inner content)
|
|
100
|
+
* - Replaces PascalCase component tags with an HTML comment placeholder
|
|
101
|
+
*/
|
|
102
|
+
function xmlToStaticHtml(xml) {
|
|
103
|
+
let html = xml
|
|
104
|
+
// Remove t-name attribute from root
|
|
105
|
+
html = html.replace(/\s+t-name="[^"]*"/g, '')
|
|
106
|
+
// Remove all t-* attributes (handles both t-if="..." and bare t-else/t-else)
|
|
107
|
+
html = html.replace(/\s+t-[\w-]+(="[^"]*")?/g, '')
|
|
108
|
+
// Unwrap bare <t> wrapper elements (self-closing)
|
|
109
|
+
html = html.replace(/<t\s*\/>/g, '')
|
|
110
|
+
// Unwrap <t ...> ... </t> blocks — replace opening/closing tags with content
|
|
111
|
+
html = html.replace(/<t(?:\s[^>]*)?>([\s\S]*?)<\/t>/g, (_, inner) => inner)
|
|
112
|
+
// Replace PascalCase component tags with a comment stub
|
|
113
|
+
// Matches <ComponentName ... /> and <ComponentName ...></ComponentName>
|
|
114
|
+
html = html.replace(/<([A-Z][A-Za-z0-9]*)\s*\/>/g, '<!-- $1 -->')
|
|
115
|
+
html = html.replace(/<([A-Z][A-Za-z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, '<!-- $1 -->')
|
|
116
|
+
return html.trim()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Build final HTML shell for a page
|
|
120
|
+
function buildShell(baseHtml, pageFile) {
|
|
121
|
+
let html = baseHtml
|
|
122
|
+
|
|
123
|
+
// Extract meta tags from JS source (Meta.title(...), Meta.description(...) etc.)
|
|
124
|
+
const jsSource = readFileSync(resolve(cwd, pageFile), 'utf-8')
|
|
125
|
+
const meta = extractMetaFromJs(jsSource)
|
|
126
|
+
html = injectMeta(html, meta)
|
|
127
|
+
|
|
128
|
+
// Inject static HTML from OWL XML template (auto-extracted, best-effort)
|
|
129
|
+
const xmlFile = resolve(cwd, pageFile.replace(/\.js$/, '.xml'))
|
|
130
|
+
if (existsSync(xmlFile)) {
|
|
131
|
+
const xmlContent = readFileSync(xmlFile, 'utf-8')
|
|
132
|
+
const staticHtml = xmlToStaticHtml(xmlContent)
|
|
133
|
+
if (staticHtml) {
|
|
134
|
+
html = html.replace(/(<div\s+id="metaowl"[^>]*>)(<\/div>)/, `$1${staticHtml}$2`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return html
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 1. Lint
|
|
142
|
+
run('Linting', `node "${metaowlRoot}/bin/metaowl-lint.js"`)
|
|
143
|
+
|
|
144
|
+
// 2. Vite build
|
|
145
|
+
run('Building', `"${bin}/vite" build`)
|
|
146
|
+
|
|
147
|
+
// 3. SSG post-processing
|
|
148
|
+
step('Generating static pages...')
|
|
149
|
+
console.log()
|
|
150
|
+
const baseHtml = readFileSync(resolve(cwd, outDir, 'index.html'), 'utf-8')
|
|
151
|
+
|
|
152
|
+
const pageFiles = globSync(`${pagesDir}/**/*.js`, { cwd })
|
|
153
|
+
|
|
154
|
+
const seen = new Set()
|
|
155
|
+
|
|
156
|
+
for (const pageFile of pageFiles) {
|
|
157
|
+
const route = deriveRoute(pageFile)
|
|
158
|
+
if (seen.has(route)) continue
|
|
159
|
+
seen.add(route)
|
|
160
|
+
|
|
161
|
+
const shell = buildShell(baseHtml, pageFile)
|
|
162
|
+
|
|
163
|
+
if (route === '/') {
|
|
164
|
+
writeFileSync(resolve(cwd, outDir, 'index.html'), shell)
|
|
165
|
+
console.log(` /index.html`)
|
|
166
|
+
} else {
|
|
167
|
+
const destDir = resolve(cwd, outDir, route.slice(1))
|
|
168
|
+
mkdirSync(destDir, { recursive: true })
|
|
169
|
+
writeFileSync(resolve(destDir, 'index.html'), shell)
|
|
170
|
+
console.log(` ${route}/index.html`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log()
|
|
175
|
+
success(`${seen.size} route(s) generated`)
|
|
176
|
+
console.log()
|
|
@@ -0,0 +1,71 @@
|
|
|
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, bin, cwd, 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/owl/pages/**',
|
|
31
|
+
'src/owl/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(`"${bin}/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(`"${bin}/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
|
+
|
package/bin/utils.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI utilities for metaowl bin scripts.
|
|
3
|
+
* Uses ANSI escape codes only when stdout is a TTY (no color when piped).
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'node:fs'
|
|
6
|
+
import { resolve, dirname } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { execSync } from 'node:child_process'
|
|
9
|
+
|
|
10
|
+
export const metaowlRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
11
|
+
export const bin = resolve(metaowlRoot, 'node_modules/.bin')
|
|
12
|
+
export const cwd = process.cwd()
|
|
13
|
+
|
|
14
|
+
const { version } = JSON.parse(readFileSync(resolve(metaowlRoot, 'package.json'), 'utf-8'))
|
|
15
|
+
export { version }
|
|
16
|
+
|
|
17
|
+
const TTY = Boolean(process.stdout.isTTY)
|
|
18
|
+
const a = (str, code) => TTY ? `\x1b[${code}m${str}\x1b[0m` : str
|
|
19
|
+
|
|
20
|
+
/** Print a styled header for the current command. */
|
|
21
|
+
export function banner(command) {
|
|
22
|
+
console.log()
|
|
23
|
+
console.log(` ${a('metaowl', '1;36')} ${a(command, '1')} ${a(`v${version}`, '2')}`)
|
|
24
|
+
console.log()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Print a step indicator: " › message" */
|
|
28
|
+
export function step(msg) {
|
|
29
|
+
console.log(` ${a('›', '36')} ${msg}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Print a success line: " ✓ message" */
|
|
33
|
+
export function success(msg) {
|
|
34
|
+
console.log(` ${a('✓', '32')} ${a(msg, '2')}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Print an error line: " ✗ message" */
|
|
38
|
+
export function failure(msg) {
|
|
39
|
+
console.error(` ${a('✗', '31')} ${msg}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run a shell command, printing a step label before and a blank line after.
|
|
44
|
+
* Exits the process with code 1 on failure.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} label - Human-readable step description.
|
|
47
|
+
* @param {string} cmd - Shell command to execute.
|
|
48
|
+
* @param {object} [opts] - Additional options forwarded to execSync.
|
|
49
|
+
*/
|
|
50
|
+
export function run(label, cmd, opts = {}) {
|
|
51
|
+
step(label)
|
|
52
|
+
console.log()
|
|
53
|
+
try {
|
|
54
|
+
execSync(cmd, { stdio: 'inherit', cwd, ...opts })
|
|
55
|
+
} catch {
|
|
56
|
+
console.log()
|
|
57
|
+
failure(`${label} failed`)
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
console.log()
|
|
61
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
|
6
|
+
"allowJs": false,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"esModuleInterop": false,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"module": "ESNext",
|
|
13
|
+
"moduleResolution": "Node",
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"noEmit": true
|
|
17
|
+
}
|
|
18
|
+
}
|
package/eslint.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default metaowl ESLint configuration.
|
|
6
|
+
*
|
|
7
|
+
* Usage in your project's eslint.config.js:
|
|
8
|
+
*
|
|
9
|
+
* import { eslintConfig } from 'metaowl/eslint'
|
|
10
|
+
* export default eslintConfig
|
|
11
|
+
*
|
|
12
|
+
* To extend/override:
|
|
13
|
+
*
|
|
14
|
+
* import { eslintConfig } from 'metaowl/eslint'
|
|
15
|
+
* export default [
|
|
16
|
+
* ...eslintConfig,
|
|
17
|
+
* { rules: { 'no-console': 'warn' } }
|
|
18
|
+
* ]
|
|
19
|
+
*/
|
|
20
|
+
export const eslintConfig = [
|
|
21
|
+
js.configs.recommended,
|
|
22
|
+
{
|
|
23
|
+
languageOptions: {
|
|
24
|
+
ecmaVersion: 2022,
|
|
25
|
+
sourceType: 'module',
|
|
26
|
+
globals: {
|
|
27
|
+
...globals.browser,
|
|
28
|
+
...globals.node,
|
|
29
|
+
COMPONENTS: 'readonly'
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
files: ['**/*.js', '**/*.jsx'],
|
|
33
|
+
rules: {
|
|
34
|
+
'no-unused-vars': ['error', {
|
|
35
|
+
argsIgnorePattern: '^_',
|
|
36
|
+
varsIgnorePattern: '^_'
|
|
37
|
+
}],
|
|
38
|
+
'semi': ['error', 'never'],
|
|
39
|
+
'quotes': ['error', 'single'],
|
|
40
|
+
'comma-dangle': ['error', 'never'],
|
|
41
|
+
'no-undef': 'off'
|
|
42
|
+
},
|
|
43
|
+
ignores: [
|
|
44
|
+
'node_modules/**',
|
|
45
|
+
'dist/**',
|
|
46
|
+
'build/**'
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
]
|
package/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { processRoutes } from './modules/router.js'
|
|
2
|
+
import { mountApp } from './modules/app-mounter.js'
|
|
3
|
+
import { buildRoutes } from './modules/file-router.js'
|
|
4
|
+
|
|
5
|
+
export { default as Fetch } from './modules/fetch.js'
|
|
6
|
+
export { default as Cache } from './modules/cache.js'
|
|
7
|
+
export { configureOwl } from './modules/app-mounter.js'
|
|
8
|
+
export * as Meta from './modules/meta.js'
|
|
9
|
+
export { buildRoutes }
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Boots the metaowl application.
|
|
13
|
+
*
|
|
14
|
+
* When called without arguments inside a Vite project, the `metaowl:app`
|
|
15
|
+
* plugin transform automatically rewrites `boot()` to
|
|
16
|
+
* `boot(import.meta.glob('./pages/**\/*.js', { eager: true }))` at build time.
|
|
17
|
+
*
|
|
18
|
+
* Can also be called explicitly with:
|
|
19
|
+
* - An import.meta.glob result (file-based routing):
|
|
20
|
+
* boot(import.meta.glob('./pages/**\/*.js', { eager: true }))
|
|
21
|
+
* - A manual route array:
|
|
22
|
+
* boot([{ name: 'index', path: ['/'], component: IndexPage }])
|
|
23
|
+
*
|
|
24
|
+
* @param {Record<string, object>|object[]} [routesOrModules]
|
|
25
|
+
*/
|
|
26
|
+
export async function boot(routesOrModules = {}) {
|
|
27
|
+
const routes = Array.isArray(routesOrModules)
|
|
28
|
+
? routesOrModules
|
|
29
|
+
: buildRoutes(routesOrModules)
|
|
30
|
+
const route = await processRoutes(routes)
|
|
31
|
+
await mountApp(route)
|
|
32
|
+
}
|