metaowl 0.4.0 → 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.
Files changed (79) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +13 -15
  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 +141 -0
  10. package/build/runtime/modules/app-mounter.js +65 -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/error-boundary.js +116 -0
  15. package/build/runtime/modules/fetch.js +31 -0
  16. package/build/runtime/modules/file-router.js +205 -0
  17. package/build/runtime/modules/forms.js +193 -0
  18. package/build/runtime/modules/i18n.js +167 -0
  19. package/build/runtime/modules/layouts.js +163 -0
  20. package/build/runtime/modules/link.js +141 -0
  21. package/build/runtime/modules/meta.js +117 -0
  22. package/build/runtime/modules/odoo-rpc.js +264 -0
  23. package/build/runtime/modules/pwa.js +262 -0
  24. package/build/runtime/modules/router.js +389 -0
  25. package/build/runtime/modules/seo.js +186 -0
  26. package/build/runtime/modules/store.js +196 -0
  27. package/build/runtime/modules/templates-manager.js +52 -0
  28. package/build/runtime/modules/test-utils.js +238 -0
  29. package/build/runtime/vite/plugin.js +183 -0
  30. package/eslint.js +29 -0
  31. package/package.json +29 -11
  32. package/CONTRIBUTING.md +0 -49
  33. package/bin/metaowl-build.js +0 -12
  34. package/bin/metaowl-dev.js +0 -12
  35. package/bin/metaowl-generate.js +0 -339
  36. package/bin/metaowl-lint.js +0 -71
  37. package/bin/utils.js +0 -82
  38. package/index.js +0 -328
  39. package/modules/app-mounter.js +0 -104
  40. package/modules/auto-import.js +0 -225
  41. package/modules/cache.js +0 -59
  42. package/modules/composables.js +0 -600
  43. package/modules/error-boundary.js +0 -228
  44. package/modules/fetch.js +0 -51
  45. package/modules/file-router.js +0 -478
  46. package/modules/forms.js +0 -353
  47. package/modules/i18n.js +0 -333
  48. package/modules/layouts.js +0 -431
  49. package/modules/link.js +0 -255
  50. package/modules/meta.js +0 -119
  51. package/modules/odoo-rpc.js +0 -511
  52. package/modules/pwa.js +0 -515
  53. package/modules/router.js +0 -769
  54. package/modules/seo.js +0 -501
  55. package/modules/store.js +0 -409
  56. package/modules/templates-manager.js +0 -89
  57. package/modules/test-utils.js +0 -532
  58. package/test/auto-import.test.js +0 -110
  59. package/test/cache.test.js +0 -55
  60. package/test/composables.test.js +0 -103
  61. package/test/dynamic-routes.test.js +0 -469
  62. package/test/error-boundary.test.js +0 -126
  63. package/test/fetch.test.js +0 -100
  64. package/test/file-router.test.js +0 -55
  65. package/test/forms.test.js +0 -203
  66. package/test/i18n.test.js +0 -188
  67. package/test/layouts.test.js +0 -395
  68. package/test/link.test.js +0 -189
  69. package/test/meta.test.js +0 -146
  70. package/test/odoo-rpc.test.js +0 -547
  71. package/test/pwa.test.js +0 -154
  72. package/test/router-guards.test.js +0 -229
  73. package/test/router.test.js +0 -77
  74. package/test/seo.test.js +0 -353
  75. package/test/store.test.js +0 -476
  76. package/test/templates-manager.test.js +0 -83
  77. package/test/test-utils.test.js +0 -314
  78. package/vite/plugin.js +0 -277
  79. package/vitest.config.js +0 -8
@@ -0,0 +1,183 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { config as dotenvConfig } from 'dotenv';
7
+ import { globSync } from 'glob';
8
+ import tsconfigPaths from 'vite-tsconfig-paths';
9
+ const require = createRequire(import.meta.url);
10
+ function resolveOwlPath() {
11
+ return require.resolve('@odoo/owl/dist/owl.es.js', {
12
+ paths: [process.cwd(), dirname(fileURLToPath(import.meta.url))]
13
+ });
14
+ }
15
+ function collectXml(globPattern) {
16
+ return globSync(globPattern).map((filePath) => filePath.replace(/^src[\\/]/, '/'));
17
+ }
18
+ function mergeXmlFiles(xmlPaths) {
19
+ const templates = xmlPaths.map((filePath) => {
20
+ try {
21
+ let content = readFileSync(filePath, 'utf-8');
22
+ content = content.replace(/<templates>/g, '').replace(/<\/templates>/g, '');
23
+ return content;
24
+ }
25
+ catch (error) {
26
+ console.error(`[metaowl] Failed to read XML file: ${filePath}`, error);
27
+ return '';
28
+ }
29
+ }).join('');
30
+ return '<templates>' + templates + '</templates>';
31
+ }
32
+ export async function metaowlPlugin(options = {}) {
33
+ const { root = 'src', outDir = '../dist', publicDir = '../public', componentsDir = 'src/components', pagesDir = 'src/pages', layoutsDir = 'src/layouts', frameworkEntry = './node_modules/metaowl/index.js', vendorPackages = ['@odoo/owl'], autoImport = {}, envPrefix } = options;
34
+ const componentXml = collectXml(`${componentsDir}/**/*.xml`);
35
+ const pageXml = collectXml(`${pagesDir}/**/*.xml`);
36
+ const layoutXml = collectXml(`${layoutsDir}/**/*.xml`);
37
+ const allComponents = [...layoutXml, ...pageXml, ...componentXml];
38
+ let outDirResolved = null;
39
+ const autoImportDtsPath = resolve(process.cwd(), '.metaowl', 'components.d.ts');
40
+ let autoImportPlugin = null;
41
+ if (autoImport.enabled) {
42
+ const { generateComponentDts, scanComponents } = await import('../modules/auto-import.js');
43
+ const components = await scanComponents(componentsDir, { pattern: autoImport.pattern || '*.js' });
44
+ const metaowlDir = dirname(autoImportDtsPath);
45
+ if (!existsSync(metaowlDir)) {
46
+ mkdirSync(metaowlDir, { recursive: true });
47
+ }
48
+ await generateComponentDts(components, autoImportDtsPath);
49
+ autoImportPlugin = {
50
+ name: 'metaowl:auto-import',
51
+ enforce: 'pre',
52
+ configResolved() {
53
+ // Components are scanned at startup.
54
+ },
55
+ handleHotUpdate({ file }) {
56
+ if (file.startsWith(resolve(componentsDir)) && file.endsWith('.js')) {
57
+ void scanComponents(componentsDir, { pattern: autoImport.pattern || '*.js' }).then((nextComponents) => {
58
+ return generateComponentDts(nextComponents, autoImportDtsPath);
59
+ });
60
+ }
61
+ }
62
+ };
63
+ }
64
+ const plugins = [
65
+ ...(autoImportPlugin ? [autoImportPlugin] : []),
66
+ tsconfigPaths({ root: process.cwd() }),
67
+ {
68
+ name: 'metaowl:define',
69
+ config(cfg, { mode }) {
70
+ dotenvConfig();
71
+ const isDev = mode === 'development';
72
+ const safeEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => key === 'NODE_ENV' || (envPrefix && key.startsWith(envPrefix))));
73
+ cfg.define = {
74
+ ...(cfg.define ?? {}),
75
+ DEV_MODE: isDev,
76
+ COMPONENTS: JSON.stringify(isDev ? allComponents : ['/templates.xml']),
77
+ 'process.env': safeEnv
78
+ };
79
+ cfg.root = cfg.root ?? root;
80
+ cfg.publicDir = cfg.publicDir ?? publicDir;
81
+ cfg.appType = cfg.appType ?? 'spa';
82
+ const owlPath = resolveOwlPath();
83
+ cfg.resolve = {
84
+ ...(cfg.resolve ?? {}),
85
+ alias: {
86
+ ...(cfg.resolve?.alias ?? {}),
87
+ '@odoo/owl': owlPath
88
+ }
89
+ };
90
+ cfg.build = {
91
+ outDir,
92
+ emptyOutDir: true,
93
+ sourcemap: isDev,
94
+ chunkSizeWarningLimit: 1024,
95
+ target: 'esnext',
96
+ rollupOptions: {
97
+ input: resolve(root, 'index.html'),
98
+ output: {
99
+ manualChunks: {
100
+ vendor: vendorPackages,
101
+ framework: [frameworkEntry]
102
+ }
103
+ }
104
+ },
105
+ ...(cfg.build ?? {})
106
+ };
107
+ cfg.optimizeDeps = {
108
+ include: ['@odoo/owl'],
109
+ ...(cfg.optimizeDeps ?? {})
110
+ };
111
+ },
112
+ configResolved(resolvedConfig) {
113
+ outDirResolved = resolve(resolvedConfig.root, resolvedConfig.build.outDir);
114
+ }
115
+ },
116
+ {
117
+ name: 'metaowl:app',
118
+ transform(code, id) {
119
+ if (!id.endsWith('/metaowl.js'))
120
+ return null;
121
+ const pagesRel = pagesDir.replace(new RegExp(`^${root}[\\/]`), '');
122
+ const layoutsRel = layoutsDir.replace(new RegExp(`^${root}[\\/]`), '');
123
+ return {
124
+ code: code.replace(/boot\(\s*\)/, `boot(import.meta.glob('./${pagesRel}/**/*.js', { eager: true }), import.meta.glob('./${layoutsRel}/**/*.js', { eager: true }))`),
125
+ map: null
126
+ };
127
+ }
128
+ },
129
+ {
130
+ name: 'metaowl:styles',
131
+ transform(code, id) {
132
+ if (!id.endsWith('/css.js'))
133
+ return null;
134
+ const compRel = componentsDir.replace(new RegExp(`^${root}[\\/]`), '');
135
+ const pagesRel = pagesDir.replace(new RegExp(`^${root}[\\/]`), '');
136
+ const layoutsRel = layoutsDir.replace(new RegExp(`^${root}[\\/]`), '');
137
+ return {
138
+ code: code + '\n' +
139
+ `import.meta.glob('/${compRel}/**/*.{css,scss}', { eager: true })\n` +
140
+ `import.meta.glob('/${pagesRel}/**/*.{css,scss}', { eager: true })\n` +
141
+ `import.meta.glob('/${layoutsRel}/**/*.{css,scss}', { eager: true })\n`,
142
+ map: null
143
+ };
144
+ }
145
+ },
146
+ {
147
+ name: 'metaowl:copy-assets',
148
+ apply: 'build',
149
+ closeBundle() {
150
+ if (!outDirResolved)
151
+ return;
152
+ const projectRoot = process.cwd();
153
+ const xmlFiles = globSync([`${componentsDir}/**/*.xml`, `${pagesDir}/**/*.xml`, `${layoutsDir}/**/*.xml`]);
154
+ const mergedXml = mergeXmlFiles(xmlFiles);
155
+ const hash = createHash('sha256').update(mergedXml).digest('hex').slice(0, 8);
156
+ const hashedFilename = `templates.${hash}.xml`;
157
+ writeFileSync(resolve(outDirResolved, hashedFilename), mergedXml, 'utf-8');
158
+ const outputFiles = globSync(['**/*.html', '**/*.js'], { cwd: outDirResolved, absolute: true });
159
+ for (const file of outputFiles) {
160
+ const content = readFileSync(file, 'utf-8');
161
+ if (content.includes('/templates.xml')) {
162
+ writeFileSync(file, content.replace(/\/templates\.xml/g, `/${hashedFilename}`), 'utf-8');
163
+ }
164
+ }
165
+ const srcImages = resolve(projectRoot, root, 'assets', 'images');
166
+ if (existsSync(srcImages)) {
167
+ cpSync(srcImages, resolve(outDirResolved, 'assets', 'images'), { recursive: true });
168
+ }
169
+ }
170
+ }
171
+ ];
172
+ return plugins;
173
+ }
174
+ export async function metaowlConfig(options = {}) {
175
+ const { server, preview, build, ...metaowlOptions } = options;
176
+ const plugins = await metaowlPlugin(metaowlOptions);
177
+ return {
178
+ server: { port: 3000, strictPort: true, host: true, ...server },
179
+ preview: { port: 4173, strictPort: true, ...preview },
180
+ ...(build ? { build } : {}),
181
+ plugins
182
+ };
183
+ }
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,31 @@
1
1
  {
2
2
  "name": "metaowl",
3
- "version": "0.4.0",
3
+ "version": "0.5.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
5
  "type": "module",
6
- "main": "index.js",
6
+ "main": "./build/runtime/index.js",
7
7
  "exports": {
8
- ".": "./index.js",
9
- "./vite": "./vite/plugin.js",
8
+ ".": "./build/runtime/index.js",
9
+ "./vite": "./build/runtime/vite/plugin.js",
10
10
  "./eslint": "./eslint.js",
11
11
  "./postcss": "./postcss.cjs"
12
12
  },
13
13
  "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"
14
+ "metaowl-create": "./build/runtime/bin/metaowl-create.js",
15
+ "metaowl-dev": "./build/runtime/bin/metaowl-dev.js",
16
+ "metaowl-build": "./build/runtime/bin/metaowl-build.js",
17
+ "metaowl-generate": "./build/runtime/bin/metaowl-generate.js",
18
+ "metaowl-lint": "./build/runtime/bin/metaowl-lint.js"
19
19
  },
20
+ "files": [
21
+ "build/runtime",
22
+ "config",
23
+ "eslint.js",
24
+ "postcss.cjs",
25
+ "README.md",
26
+ "LICENSE",
27
+ "CHANGELOG.md"
28
+ ],
20
29
  "keywords": [
21
30
  "owl",
22
31
  "odoo",
@@ -49,17 +58,26 @@
49
58
  "globals": "^13.24.0",
50
59
  "prettier": "3.5.1",
51
60
  "vite": "^8.0.0",
52
- "vite-plugin-handlebars": "^2.0.0",
53
61
  "vite-tsconfig-paths": "^6.1.1"
54
62
  },
55
63
  "engines": {
56
64
  "node": ">=20.0.0"
57
65
  },
58
66
  "scripts": {
67
+ "clean:runtime": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('build/runtime', { recursive: true, force: true })\"",
59
68
  "test": "vitest run",
60
- "test:watch": "vitest"
69
+ "test:watch": "vitest",
70
+ "lint": "eslint . --ext .js,.ts",
71
+ "typecheck": "tsc -p tsconfig.json --noEmit",
72
+ "release:check": "npm run typecheck && npm test",
73
+ "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); }\"",
74
+ "release:build": "npm run release:check && npm run build:runtime",
75
+ "release:pack": "npm run release:check && npm pack --dry-run",
76
+ "prepack": "npm run build:runtime"
61
77
  },
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 })