metaowl 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +267 -2
  3. package/build/runtime/bin/metaowl-build.js +10 -0
  4. package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
  5. package/build/runtime/bin/metaowl-dev.js +10 -0
  6. package/build/runtime/bin/metaowl-generate.js +231 -0
  7. package/build/runtime/bin/metaowl-lint.js +58 -0
  8. package/build/runtime/bin/utils.js +68 -0
  9. package/build/runtime/index.js +144 -0
  10. package/build/runtime/modules/app-mounter.js +73 -0
  11. package/build/runtime/modules/auto-import.js +140 -0
  12. package/build/runtime/modules/cache.js +49 -0
  13. package/build/runtime/modules/composables.js +353 -0
  14. package/build/runtime/modules/constants.js +38 -0
  15. package/build/runtime/modules/error-boundary.js +116 -0
  16. package/build/runtime/modules/fetch.js +31 -0
  17. package/build/runtime/modules/file-router.js +207 -0
  18. package/build/runtime/modules/fonts.js +172 -0
  19. package/build/runtime/modules/forms.js +193 -0
  20. package/build/runtime/modules/i18n.js +180 -0
  21. package/build/runtime/modules/image.js +175 -0
  22. package/build/runtime/modules/layouts.js +214 -0
  23. package/build/runtime/modules/link.js +141 -0
  24. package/build/runtime/modules/meta.js +117 -0
  25. package/build/runtime/modules/odoo-rpc.js +265 -0
  26. package/build/runtime/modules/pwa.js +272 -0
  27. package/build/runtime/modules/router.js +384 -0
  28. package/build/runtime/modules/seo.js +186 -0
  29. package/build/runtime/modules/store.js +198 -0
  30. package/build/runtime/modules/templates-manager.js +52 -0
  31. package/build/runtime/modules/test-utils.js +238 -0
  32. package/build/runtime/vite/plugin.js +197 -0
  33. package/eslint.js +29 -0
  34. package/package.json +45 -27
  35. package/CONTRIBUTING.md +0 -49
  36. package/bin/metaowl-build.js +0 -12
  37. package/bin/metaowl-dev.js +0 -12
  38. package/bin/metaowl-generate.js +0 -339
  39. package/bin/metaowl-lint.js +0 -71
  40. package/bin/utils.js +0 -82
  41. package/eslint.config.js +0 -3
  42. package/index.js +0 -328
  43. package/modules/app-mounter.js +0 -104
  44. package/modules/auto-import.js +0 -225
  45. package/modules/cache.js +0 -59
  46. package/modules/composables.js +0 -600
  47. package/modules/error-boundary.js +0 -228
  48. package/modules/fetch.js +0 -51
  49. package/modules/file-router.js +0 -478
  50. package/modules/forms.js +0 -353
  51. package/modules/i18n.js +0 -333
  52. package/modules/layouts.js +0 -431
  53. package/modules/link.js +0 -255
  54. package/modules/meta.js +0 -119
  55. package/modules/odoo-rpc.js +0 -511
  56. package/modules/pwa.js +0 -515
  57. package/modules/router.js +0 -769
  58. package/modules/seo.js +0 -501
  59. package/modules/store.js +0 -409
  60. package/modules/templates-manager.js +0 -89
  61. package/modules/test-utils.js +0 -532
  62. package/test/auto-import.test.js +0 -110
  63. package/test/cache.test.js +0 -55
  64. package/test/composables.test.js +0 -103
  65. package/test/dynamic-routes.test.js +0 -469
  66. package/test/error-boundary.test.js +0 -126
  67. package/test/fetch.test.js +0 -100
  68. package/test/file-router.test.js +0 -55
  69. package/test/forms.test.js +0 -203
  70. package/test/i18n.test.js +0 -188
  71. package/test/layouts.test.js +0 -395
  72. package/test/link.test.js +0 -189
  73. package/test/meta.test.js +0 -146
  74. package/test/odoo-rpc.test.js +0 -547
  75. package/test/pwa.test.js +0 -154
  76. package/test/router-guards.test.js +0 -229
  77. package/test/router.test.js +0 -77
  78. package/test/seo.test.js +0 -353
  79. package/test/store.test.js +0 -476
  80. package/test/templates-manager.test.js +0 -83
  81. package/test/test-utils.test.js +0 -314
  82. package/vite/plugin.js +0 -290
  83. package/vitest.config.js +0 -8
@@ -0,0 +1,198 @@
1
+ /**
2
+ * @module Store
3
+ *
4
+ * Lightweight state management for OWL applications, inspired by Pinia/Vuex.
5
+ */
6
+ import { reactive } from '@odoo/owl';
7
+ const stores = new Map();
8
+ const plugins = [];
9
+ export class Store {
10
+ _id;
11
+ _config;
12
+ _state;
13
+ _getters;
14
+ _mutations;
15
+ _actions;
16
+ _subscribers;
17
+ _actionSubscribers;
18
+ constructor(id, config) {
19
+ this._id = id;
20
+ this._config = config;
21
+ this._state = reactive(config.state ? config.state() : {});
22
+ this._getters = {};
23
+ this._mutations = config.mutations || {};
24
+ this._actions = config.actions || {};
25
+ this._subscribers = [];
26
+ this._actionSubscribers = [];
27
+ if (config.getters) {
28
+ for (const [name, fn] of Object.entries(config.getters)) {
29
+ Object.defineProperty(this._getters, name, {
30
+ get: () => fn(this._state, this._getters),
31
+ enumerable: true,
32
+ configurable: true
33
+ });
34
+ }
35
+ }
36
+ for (const plugin of plugins) {
37
+ plugin(this);
38
+ }
39
+ }
40
+ get id() {
41
+ return this._id;
42
+ }
43
+ get state() {
44
+ return this._state;
45
+ }
46
+ get getters() {
47
+ return this._getters;
48
+ }
49
+ commit(type, payload) {
50
+ const mutation = this._mutations[type];
51
+ if (!mutation) {
52
+ throw new Error(`[metaowl] Mutation "${type}" not found in store "${this._id}"`);
53
+ }
54
+ const prevState = JSON.parse(JSON.stringify(this._state));
55
+ const result = mutation(this._state, payload);
56
+ for (const subscriber of this._subscribers) {
57
+ subscriber({ type, payload }, this._state, prevState);
58
+ }
59
+ return result;
60
+ }
61
+ async dispatch(type, payload) {
62
+ const action = this._actions[type];
63
+ if (!action) {
64
+ throw new Error(`[metaowl] Action "${type}" not found in store "${this._id}"`);
65
+ }
66
+ const context = {
67
+ state: this._state,
68
+ getters: this._getters,
69
+ commit: this.commit.bind(this),
70
+ dispatch: this.dispatch.bind(this)
71
+ };
72
+ for (const subscriber of this._actionSubscribers) {
73
+ subscriber({ type, payload }, 'before');
74
+ }
75
+ try {
76
+ const result = await action(context, payload);
77
+ for (const subscriber of this._actionSubscribers) {
78
+ subscriber({ type, payload }, 'after', result);
79
+ }
80
+ return result;
81
+ }
82
+ catch (error) {
83
+ for (const subscriber of this._actionSubscribers) {
84
+ subscriber({ type, payload }, 'error', error);
85
+ }
86
+ throw error;
87
+ }
88
+ }
89
+ subscribe(callback) {
90
+ this._subscribers.push(callback);
91
+ return () => {
92
+ const index = this._subscribers.indexOf(callback);
93
+ if (index > -1)
94
+ this._subscribers.splice(index, 1);
95
+ };
96
+ }
97
+ subscribeAction(callback) {
98
+ this._actionSubscribers.push(callback);
99
+ return () => {
100
+ const index = this._actionSubscribers.indexOf(callback);
101
+ if (index > -1)
102
+ this._actionSubscribers.splice(index, 1);
103
+ };
104
+ }
105
+ reset() {
106
+ if (this._config.state) {
107
+ const initialState = this._config.state();
108
+ const keys = Object.keys(this._state);
109
+ for (const key of keys) {
110
+ ;
111
+ this._state[key] = undefined;
112
+ }
113
+ Object.assign(this._state, initialState);
114
+ }
115
+ }
116
+ static define(id, config) {
117
+ return function useStore() {
118
+ if (!stores.has(id)) {
119
+ stores.set(id, new Store(id, config));
120
+ }
121
+ return stores.get(id);
122
+ };
123
+ }
124
+ static get(id) {
125
+ return stores.get(id);
126
+ }
127
+ static has(id) {
128
+ return stores.has(id);
129
+ }
130
+ static remove(id) {
131
+ const store = stores.get(id);
132
+ if (store) {
133
+ store.reset();
134
+ return stores.delete(id);
135
+ }
136
+ return false;
137
+ }
138
+ static clear() {
139
+ stores.clear();
140
+ }
141
+ static storeIds() {
142
+ return Array.from(stores.keys());
143
+ }
144
+ static use(plugin) {
145
+ plugins.push(plugin);
146
+ }
147
+ }
148
+ export function createPersistencePlugin(options = {}) {
149
+ const { storage = localStorage, key, paths } = options;
150
+ return function persistencePlugin(store) {
151
+ const storageKey = key || `metaowl:store:${store.id}`;
152
+ try {
153
+ const saved = storage.getItem(storageKey);
154
+ if (saved) {
155
+ const persisted = JSON.parse(saved);
156
+ if (paths) {
157
+ for (const path of paths) {
158
+ if (path in persisted && path in store.state) {
159
+ store.state[path] = persisted[path];
160
+ }
161
+ }
162
+ }
163
+ else {
164
+ Object.assign(store.state, persisted);
165
+ }
166
+ }
167
+ }
168
+ catch (error) {
169
+ console.warn('[metaowl] Failed to restore store from storage:', error);
170
+ }
171
+ store.subscribe((_mutation, state) => {
172
+ try {
173
+ const toPersist = paths
174
+ ? Object.fromEntries(paths.map((path) => [path, state[path]]))
175
+ : state;
176
+ storage.setItem(storageKey, JSON.stringify(toPersist));
177
+ }
178
+ catch (error) {
179
+ console.warn('[metaowl] Failed to persist store:', error);
180
+ }
181
+ });
182
+ };
183
+ }
184
+ export function createStore(initialState = {}) {
185
+ const state = reactive({ ...initialState });
186
+ state.$patch = (partialState) => {
187
+ Object.assign(state, partialState);
188
+ };
189
+ state.$reset = () => {
190
+ Object.keys(state).forEach((key) => {
191
+ if (!key.startsWith('$')) {
192
+ delete state[key];
193
+ }
194
+ });
195
+ Object.assign(state, initialState);
196
+ };
197
+ return state;
198
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @module TemplatesManager
3
+ *
4
+ * Template loading and merging utilities for OWL applications.
5
+ */
6
+ import { loadFile } from '@odoo/owl';
7
+ const LINK_COMPONENT_TEMPLATE = /* xml */ `
8
+ <t t-name="Link">
9
+ <a
10
+ t-att="forwardedAttrs"
11
+ t-att-href="props.to"
12
+ t-att-class="linkClasses"
13
+ t-att-target="props.target"
14
+ t-att-rel="linkRel"
15
+ t-att-title="props.title"
16
+ t-att-download="props.download"
17
+ t-on-click="onClick"
18
+ >
19
+ <t t-slot="default"/>
20
+ </a>
21
+ </t>
22
+ `;
23
+ const INTERNAL_TEMPLATES = [LINK_COMPONENT_TEMPLATE];
24
+ export async function mergeTemplates(files) {
25
+ const fileArray = Array.isArray(files) ? files : [files];
26
+ if (fileArray.length === 1) {
27
+ try {
28
+ const content = await loadFile(fileArray[0]);
29
+ if (content.trim().startsWith('<templates>')) {
30
+ return content.replace('</templates>', INTERNAL_TEMPLATES.join('') + '</templates>');
31
+ }
32
+ return '<templates>' + content + INTERNAL_TEMPLATES.join('') + '</templates>';
33
+ }
34
+ catch (error) {
35
+ console.error(`[metaowl] Failed to load template: ${fileArray[0]}`, error);
36
+ return '<templates>' + INTERNAL_TEMPLATES.join('') + '</templates>';
37
+ }
38
+ }
39
+ const results = await Promise.all(fileArray.map(async (file) => {
40
+ try {
41
+ return await loadFile(file);
42
+ }
43
+ catch (error) {
44
+ console.error(`[metaowl] Failed to load template: ${file}`, error);
45
+ return '';
46
+ }
47
+ }));
48
+ return '<templates>' + results.join('') + INTERNAL_TEMPLATES.join('') + '</templates>';
49
+ }
50
+ export function getInternalTemplates() {
51
+ return [...INTERNAL_TEMPLATES];
52
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @module TestUtils
3
+ *
4
+ * Testing utilities for MetaOwl OWL applications.
5
+ */
6
+ import { mount, reactive } from '@odoo/owl';
7
+ export function createMockStore(config = {}) {
8
+ const { state: initialState = {}, getters: getterDefs = {}, mutations: mutationDefs = {}, actions: actionDefs = {} } = config;
9
+ const state = reactive({ ...initialState });
10
+ const getters = {};
11
+ for (const [name, fn] of Object.entries(getterDefs)) {
12
+ Object.defineProperty(getters, name, {
13
+ get: () => fn(state),
14
+ enumerable: true
15
+ });
16
+ }
17
+ const mutations = {};
18
+ for (const [name, fn] of Object.entries(mutationDefs)) {
19
+ mutations[name] = (payload) => {
20
+ fn(state, payload);
21
+ };
22
+ }
23
+ const actions = {};
24
+ for (const [name, fn] of Object.entries(actionDefs)) {
25
+ actions[name] = async (payload) => {
26
+ const context = {
27
+ state,
28
+ getters,
29
+ commit: (mutation, nextPayload) => mutations[mutation]?.(nextPayload),
30
+ dispatch: (action, nextPayload) => actions[action]?.(nextPayload)
31
+ };
32
+ return await fn(context, payload);
33
+ };
34
+ }
35
+ return {
36
+ state,
37
+ getters,
38
+ mutations,
39
+ actions,
40
+ commit(name, payload) {
41
+ if (mutations[name]) {
42
+ mutations[name](payload);
43
+ }
44
+ else {
45
+ console.warn(`[TestUtils] Mutation '${name}' not found`);
46
+ }
47
+ },
48
+ async dispatch(name, payload) {
49
+ if (actions[name]) {
50
+ return await actions[name](payload);
51
+ }
52
+ console.warn(`[TestUtils] Action '${name}' not found`);
53
+ return undefined;
54
+ },
55
+ reset() {
56
+ Object.keys(state).forEach((key) => delete state[key]);
57
+ Object.assign(state, initialState);
58
+ },
59
+ setState(newState) {
60
+ Object.assign(state, newState);
61
+ }
62
+ };
63
+ }
64
+ export function mockRouter(config = {}) {
65
+ const { initialRoute = '/', routes = [] } = config;
66
+ const currentRoute = reactive({
67
+ path: initialRoute,
68
+ name: null,
69
+ params: {},
70
+ query: {},
71
+ hash: ''
72
+ });
73
+ const beforeEachGuards = [];
74
+ const afterEachHooks = [];
75
+ function parseUrl(url) {
76
+ const [pathAndQuery, hash = ''] = url.split('#');
77
+ const [path = '', queryString = ''] = pathAndQuery.split('?');
78
+ const query = {};
79
+ if (queryString) {
80
+ queryString.split('&').forEach((param) => {
81
+ const [key, value] = param.split('=');
82
+ query[decodeURIComponent(key)] = decodeURIComponent(value || '');
83
+ });
84
+ }
85
+ let matchedRoute = null;
86
+ let params = {};
87
+ for (const route of routes) {
88
+ const match = matchPath(path, route.path);
89
+ if (match) {
90
+ matchedRoute = route;
91
+ params = match.params;
92
+ break;
93
+ }
94
+ }
95
+ return {
96
+ path,
97
+ name: matchedRoute?.name || null,
98
+ params,
99
+ query,
100
+ hash
101
+ };
102
+ }
103
+ function matchPath(path, pattern) {
104
+ const paramNames = [];
105
+ const regexPattern = pattern
106
+ .replace(/\*/g, '.*')
107
+ .replace(/:([^/]+)/g, (_match, name) => {
108
+ paramNames.push(name);
109
+ return '([^/]+)';
110
+ });
111
+ const regex = new RegExp(`^${regexPattern}$`);
112
+ const match = path.match(regex);
113
+ if (!match)
114
+ return null;
115
+ const params = {};
116
+ paramNames.forEach((name, index) => {
117
+ params[name] = match[index + 1];
118
+ });
119
+ return { params };
120
+ }
121
+ Object.assign(currentRoute, parseUrl(initialRoute));
122
+ return {
123
+ currentRoute,
124
+ async push(path) {
125
+ const to = parseUrl(path);
126
+ const from = { ...currentRoute };
127
+ for (const guard of beforeEachGuards) {
128
+ const result = await guard(to, from, () => { });
129
+ if (result === false)
130
+ return;
131
+ }
132
+ Object.assign(currentRoute, to);
133
+ for (const hook of afterEachHooks) {
134
+ await hook(to, from);
135
+ }
136
+ },
137
+ async replace(path) {
138
+ await this.push(path);
139
+ },
140
+ back() {
141
+ // Mock - does nothing in test environment
142
+ },
143
+ beforeEach(guard) {
144
+ beforeEachGuards.push(guard);
145
+ return () => {
146
+ const index = beforeEachGuards.indexOf(guard);
147
+ if (index > -1)
148
+ beforeEachGuards.splice(index, 1);
149
+ };
150
+ },
151
+ afterEach(hook) {
152
+ afterEachHooks.push(hook);
153
+ return () => {
154
+ const index = afterEachHooks.indexOf(hook);
155
+ if (index > -1)
156
+ afterEachHooks.splice(index, 1);
157
+ };
158
+ },
159
+ resolve(name, params = {}) {
160
+ const route = routes.find((candidate) => candidate.name === name);
161
+ if (!route)
162
+ return '/';
163
+ let path = route.path;
164
+ for (const [key, value] of Object.entries(params)) {
165
+ path = path.replace(`:${key}`, String(value));
166
+ }
167
+ return path;
168
+ }
169
+ };
170
+ }
171
+ export async function mountComponent(ComponentClass, options = {}) {
172
+ const { props = {}, store = null, router = null, target = document.createElement('div') } = options;
173
+ const env = {};
174
+ if (store)
175
+ env.store = store;
176
+ if (router)
177
+ env.router = router;
178
+ return await mount(ComponentClass, target, {
179
+ props,
180
+ env
181
+ });
182
+ }
183
+ export function wait(ms) {
184
+ return new Promise((resolve) => setTimeout(resolve, ms));
185
+ }
186
+ export function nextTick() {
187
+ return new Promise((resolve) => {
188
+ requestAnimationFrame(() => resolve());
189
+ });
190
+ }
191
+ export async function flushPromises() {
192
+ await new Promise((resolve) => setTimeout(resolve, 0));
193
+ }
194
+ export const userEvent = {
195
+ async click(element) {
196
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
197
+ await flushPromises();
198
+ },
199
+ async type(input, text) {
200
+ input.value = text;
201
+ input.dispatchEvent(new Event('input', { bubbles: true }));
202
+ await flushPromises();
203
+ },
204
+ async submit(form) {
205
+ form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
206
+ await flushPromises();
207
+ },
208
+ async select(select, value) {
209
+ select.value = value;
210
+ select.dispatchEvent(new Event('change', { bubbles: true }));
211
+ await flushPromises();
212
+ }
213
+ };
214
+ export const dom = {
215
+ query(selector, container = document) {
216
+ return container.querySelector(selector);
217
+ },
218
+ queryAll(selector, container = document) {
219
+ return container.querySelectorAll(selector);
220
+ },
221
+ hasClass(element, className) {
222
+ return element?.classList?.contains(className) || false;
223
+ },
224
+ text(element) {
225
+ return element?.textContent?.trim() || '';
226
+ }
227
+ };
228
+ export const TestUtils = {
229
+ createMockStore,
230
+ mockRouter,
231
+ mountComponent,
232
+ wait,
233
+ nextTick,
234
+ flushPromises,
235
+ userEvent,
236
+ dom
237
+ };
238
+ export default TestUtils;
@@ -0,0 +1,197 @@
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 isPathSafe(path, root) {
11
+ const resolved = resolve(root, path);
12
+ return resolved.startsWith(resolve(root));
13
+ }
14
+ function sanitizeDirOption(path, root, fallback) {
15
+ if (!path)
16
+ return fallback;
17
+ if (isPathSafe(path, root))
18
+ return path;
19
+ console.warn(`[metaowl] Path "${path}" escapes root, using default "${fallback}"`);
20
+ return fallback;
21
+ }
22
+ function resolveOwlPath() {
23
+ return require.resolve('@odoo/owl/dist/owl.es.js', {
24
+ paths: [process.cwd(), dirname(fileURLToPath(import.meta.url))]
25
+ });
26
+ }
27
+ function collectXml(globPattern) {
28
+ return globSync(globPattern).map((filePath) => filePath.replace(/^src[\\/]/, '/'));
29
+ }
30
+ function mergeXmlFiles(xmlPaths) {
31
+ const templates = xmlPaths.map((filePath) => {
32
+ try {
33
+ let content = readFileSync(filePath, 'utf-8');
34
+ content = content.replace(/<templates>/g, '').replace(/<\/templates>/g, '');
35
+ return content;
36
+ }
37
+ catch (error) {
38
+ console.error(`[metaowl] Failed to read XML file: ${filePath}`, error);
39
+ return '';
40
+ }
41
+ }).join('');
42
+ return '<templates>' + templates + '</templates>';
43
+ }
44
+ export async function metaowlPlugin(options = {}) {
45
+ const rootDir = options.root ?? 'src';
46
+ const resolvedRoot = resolve(process.cwd(), rootDir);
47
+ const { outDir = '../dist', publicDir = '../public', componentsDir = sanitizeDirOption(options.componentsDir ?? 'src/components', resolvedRoot, 'src/components'), pagesDir = sanitizeDirOption(options.pagesDir ?? 'src/pages', resolvedRoot, 'src/pages'), layoutsDir = sanitizeDirOption(options.layoutsDir ?? 'src/layouts', resolvedRoot, 'src/layouts'), frameworkEntry = './node_modules/metaowl/index.js', vendorPackages = ['@odoo/owl'], autoImport = {}, envPrefix } = options;
48
+ const componentXml = collectXml(`${componentsDir}/**/*.xml`);
49
+ const pageXml = collectXml(`${pagesDir}/**/*.xml`);
50
+ const layoutXml = collectXml(`${layoutsDir}/**/*.xml`);
51
+ const allComponents = [...layoutXml, ...pageXml, ...componentXml];
52
+ let outDirResolved = null;
53
+ const autoImportDtsPath = resolve(process.cwd(), '.metaowl', 'components.d.ts');
54
+ let autoImportPlugin = null;
55
+ if (autoImport.enabled) {
56
+ const { generateComponentDts, scanComponents } = await import('../modules/auto-import.js');
57
+ const components = await scanComponents(componentsDir, { pattern: autoImport.pattern || '*.js' });
58
+ const metaowlDir = dirname(autoImportDtsPath);
59
+ if (!existsSync(metaowlDir)) {
60
+ mkdirSync(metaowlDir, { recursive: true });
61
+ }
62
+ await generateComponentDts(components, autoImportDtsPath);
63
+ autoImportPlugin = {
64
+ name: 'metaowl:auto-import',
65
+ enforce: 'pre',
66
+ configResolved() {
67
+ // Components are scanned at startup.
68
+ },
69
+ handleHotUpdate({ file }) {
70
+ if (file.startsWith(resolve(componentsDir)) && file.endsWith('.js')) {
71
+ void scanComponents(componentsDir, { pattern: autoImport.pattern || '*.js' }).then((nextComponents) => {
72
+ return generateComponentDts(nextComponents, autoImportDtsPath);
73
+ });
74
+ }
75
+ }
76
+ };
77
+ }
78
+ const plugins = [
79
+ ...(autoImportPlugin ? [autoImportPlugin] : []),
80
+ tsconfigPaths({ root: process.cwd() }),
81
+ {
82
+ name: 'metaowl:define',
83
+ config(cfg, { mode }) {
84
+ dotenvConfig();
85
+ const isDev = mode === 'development';
86
+ const safeEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => key === 'NODE_ENV' || (envPrefix && key.startsWith(envPrefix))));
87
+ cfg.define = {
88
+ ...(cfg.define ?? {}),
89
+ DEV_MODE: isDev,
90
+ COMPONENTS: JSON.stringify(isDev ? allComponents : ['/templates.xml']),
91
+ 'process.env': safeEnv
92
+ };
93
+ cfg.root = cfg.root ?? rootDir;
94
+ cfg.publicDir = cfg.publicDir ?? publicDir;
95
+ cfg.appType = cfg.appType ?? 'spa';
96
+ const owlPath = resolveOwlPath();
97
+ cfg.resolve = {
98
+ ...(cfg.resolve ?? {}),
99
+ alias: {
100
+ ...(cfg.resolve?.alias ?? {}),
101
+ '@odoo/owl': owlPath
102
+ }
103
+ };
104
+ cfg.build = {
105
+ outDir,
106
+ emptyOutDir: true,
107
+ sourcemap: isDev,
108
+ chunkSizeWarningLimit: 1024,
109
+ target: 'esnext',
110
+ rollupOptions: {
111
+ input: resolve(rootDir, 'index.html'),
112
+ output: {
113
+ manualChunks: {
114
+ vendor: vendorPackages,
115
+ framework: [frameworkEntry]
116
+ }
117
+ }
118
+ },
119
+ ...(cfg.build ?? {})
120
+ };
121
+ cfg.optimizeDeps = {
122
+ include: ['@odoo/owl'],
123
+ ...(cfg.optimizeDeps ?? {})
124
+ };
125
+ },
126
+ configResolved(resolvedConfig) {
127
+ outDirResolved = resolve(resolvedConfig.root, resolvedConfig.build.outDir);
128
+ }
129
+ },
130
+ {
131
+ name: 'metaowl:app',
132
+ transform(code, id) {
133
+ if (!id.endsWith('/metaowl.js'))
134
+ return null;
135
+ const pagesRel = pagesDir.replace(new RegExp(`^${rootDir}[\\/]`), '');
136
+ const layoutsRel = layoutsDir.replace(new RegExp(`^${rootDir}[\\/]`), '');
137
+ return {
138
+ code: code.replace(/boot\(\s*\)/, `boot(import.meta.glob('./${pagesRel}/**/*.js', { eager: true }), import.meta.glob('./${layoutsRel}/**/*.js', { eager: true }))`),
139
+ map: null
140
+ };
141
+ }
142
+ },
143
+ {
144
+ name: 'metaowl:styles',
145
+ transform(code, id) {
146
+ if (!id.endsWith('/css.js'))
147
+ return null;
148
+ const compRel = componentsDir.replace(new RegExp(`^${rootDir}[\\/]`), '');
149
+ const pagesRel = pagesDir.replace(new RegExp(`^${rootDir}[\\/]`), '');
150
+ const layoutsRel = layoutsDir.replace(new RegExp(`^${rootDir}[\\/]`), '');
151
+ return {
152
+ code: code + '\n' +
153
+ `import.meta.glob('/${compRel}/**/*.{css,scss}', { eager: true })\n` +
154
+ `import.meta.glob('/${pagesRel}/**/*.{css,scss}', { eager: true })\n` +
155
+ `import.meta.glob('/${layoutsRel}/**/*.{css,scss}', { eager: true })\n`,
156
+ map: null
157
+ };
158
+ }
159
+ },
160
+ {
161
+ name: 'metaowl:copy-assets',
162
+ apply: 'build',
163
+ closeBundle() {
164
+ if (!outDirResolved)
165
+ return;
166
+ const projectRoot = process.cwd();
167
+ const xmlFiles = globSync([`${componentsDir}/**/*.xml`, `${pagesDir}/**/*.xml`, `${layoutsDir}/**/*.xml`]);
168
+ const mergedXml = mergeXmlFiles(xmlFiles);
169
+ const hash = createHash('sha256').update(mergedXml).digest('hex').slice(0, 8);
170
+ const hashedFilename = `templates.${hash}.xml`;
171
+ writeFileSync(resolve(outDirResolved, hashedFilename), mergedXml, 'utf-8');
172
+ const outputFiles = globSync(['**/*.html', '**/*.js'], { cwd: outDirResolved, absolute: true });
173
+ for (const file of outputFiles) {
174
+ const content = readFileSync(file, 'utf-8');
175
+ if (content.includes('/templates.xml')) {
176
+ writeFileSync(file, content.replace(/\/templates\.xml/g, `/${hashedFilename}`), 'utf-8');
177
+ }
178
+ }
179
+ const srcImages = resolve(projectRoot, rootDir, 'assets', 'images');
180
+ if (existsSync(srcImages)) {
181
+ cpSync(srcImages, resolve(outDirResolved, 'assets', 'images'), { recursive: true });
182
+ }
183
+ }
184
+ }
185
+ ];
186
+ return plugins;
187
+ }
188
+ export async function metaowlConfig(options = {}) {
189
+ const { server, preview, build, ...metaowlOptions } = options;
190
+ const plugins = await metaowlPlugin(metaowlOptions);
191
+ return {
192
+ server: { port: 3000, strictPort: true, host: true, ...server },
193
+ preview: { port: 4173, strictPort: true, ...preview },
194
+ ...(build ? { build } : {}),
195
+ plugins
196
+ };
197
+ }