metaowl 0.4.1 → 0.5.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 +22 -0
- package/README.md +12 -0
- 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 +141 -0
- package/build/runtime/modules/app-mounter.js +65 -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/error-boundary.js +116 -0
- package/build/runtime/modules/fetch.js +31 -0
- package/build/runtime/modules/file-router.js +205 -0
- package/build/runtime/modules/forms.js +193 -0
- package/build/runtime/modules/i18n.js +167 -0
- package/build/runtime/modules/layouts.js +163 -0
- package/build/runtime/modules/link.js +141 -0
- package/build/runtime/modules/meta.js +117 -0
- package/build/runtime/modules/odoo-rpc.js +264 -0
- package/build/runtime/modules/pwa.js +262 -0
- package/build/runtime/modules/router.js +389 -0
- package/build/runtime/modules/seo.js +186 -0
- package/build/runtime/modules/store.js +196 -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 +183 -0
- package/eslint.js +29 -0
- package/package.json +28 -10
- 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/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
|
-
|
package/bin/utils.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
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 { existsSync, 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
|
-
const cwdBin = resolve(cwd, 'node_modules/.bin')
|
|
14
|
-
|
|
15
|
-
const { version } = JSON.parse(readFileSync(resolve(metaowlRoot, 'package.json'), 'utf-8'))
|
|
16
|
-
export { version }
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Resolve an executable path with fallback for hoisted installs.
|
|
20
|
-
* Priority:
|
|
21
|
-
* 1) metaowl-local node_modules/.bin
|
|
22
|
-
* 2) project node_modules/.bin
|
|
23
|
-
* 3) command name (PATH lookup by shell)
|
|
24
|
-
*
|
|
25
|
-
* @param {string} name
|
|
26
|
-
* @returns {string}
|
|
27
|
-
*/
|
|
28
|
-
export function resolveBin(name) {
|
|
29
|
-
const local = resolve(bin, name)
|
|
30
|
-
if (existsSync(local)) return local
|
|
31
|
-
|
|
32
|
-
const project = resolve(cwdBin, name)
|
|
33
|
-
if (existsSync(project)) return project
|
|
34
|
-
|
|
35
|
-
return name
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const TTY = Boolean(process.stdout.isTTY)
|
|
39
|
-
const a = (str, code) => TTY ? `\x1b[${code}m${str}\x1b[0m` : str
|
|
40
|
-
|
|
41
|
-
/** Print a styled header for the current command. */
|
|
42
|
-
export function banner(command) {
|
|
43
|
-
console.log()
|
|
44
|
-
console.log(` ${a('metaowl', '1;36')} ${a(command, '1')} ${a(`v${version}`, '2')}`)
|
|
45
|
-
console.log()
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Print a step indicator: " › message" */
|
|
49
|
-
export function step(msg) {
|
|
50
|
-
console.log(` ${a('›', '36')} ${msg}`)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Print a success line: " ✓ message" */
|
|
54
|
-
export function success(msg) {
|
|
55
|
-
console.log(` ${a('✓', '32')} ${a(msg, '2')}`)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Print an error line: " ✗ message" */
|
|
59
|
-
export function failure(msg) {
|
|
60
|
-
console.error(` ${a('✗', '31')} ${msg}`)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Run a shell command, printing a step label before and a blank line after.
|
|
65
|
-
* Exits the process with code 1 on failure.
|
|
66
|
-
*
|
|
67
|
-
* @param {string} label - Human-readable step description.
|
|
68
|
-
* @param {string} cmd - Shell command to execute.
|
|
69
|
-
* @param {object} [opts] - Additional options forwarded to execSync.
|
|
70
|
-
*/
|
|
71
|
-
export function run(label, cmd, opts = {}) {
|
|
72
|
-
step(label)
|
|
73
|
-
console.log()
|
|
74
|
-
try {
|
|
75
|
-
execSync(cmd, { stdio: 'inherit', cwd, ...opts })
|
|
76
|
-
} catch {
|
|
77
|
-
console.log()
|
|
78
|
-
failure(`${label} failed`)
|
|
79
|
-
process.exit(1)
|
|
80
|
-
}
|
|
81
|
-
console.log()
|
|
82
|
-
}
|
package/eslint.config.js
DELETED