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,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * metaowl lint — format with Prettier then lint with ESLint.
4
+ */
5
+ import { execSync } from 'node:child_process';
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
8
+ import { globSync } from 'glob';
9
+ import { banner, cwd, failure, resolveBin, step, success } from './utils.js';
10
+ banner('lint');
11
+ let lintTargets = null;
12
+ try {
13
+ const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf8'));
14
+ lintTargets = pkg.metaowl?.lint ?? null;
15
+ }
16
+ catch {
17
+ // No package.json or no metaowl config.
18
+ }
19
+ const defaults = [
20
+ 'src/metaowl.js',
21
+ 'src/css.js',
22
+ 'src/pages/**',
23
+ 'src/components/**'
24
+ ];
25
+ const candidates = lintTargets ?? defaults;
26
+ const existing = candidates.filter((pattern) => {
27
+ if (existsSync(resolve(cwd, pattern)))
28
+ return true;
29
+ return globSync(pattern, { cwd }).length > 0;
30
+ });
31
+ if (existing.length === 0) {
32
+ success('No lint targets found — skipping');
33
+ console.log();
34
+ process.exit(0);
35
+ }
36
+ const targets = existing.map((target) => `"${target}"`).join(' ');
37
+ step('Formatting with Prettier...');
38
+ console.log();
39
+ try {
40
+ execSync(`"${resolveBin('prettier')}" src --single-quote --no-semi --write`, { stdio: 'inherit', cwd });
41
+ }
42
+ catch {
43
+ failure('Prettier failed');
44
+ process.exit(1);
45
+ }
46
+ console.log();
47
+ step('Linting with ESLint...');
48
+ console.log();
49
+ try {
50
+ execSync(`"${resolveBin('eslint')}" ${targets} --fix`, { stdio: 'inherit', cwd });
51
+ }
52
+ catch {
53
+ failure('ESLint failed');
54
+ process.exit(1);
55
+ }
56
+ console.log();
57
+ success('Lint complete');
58
+ console.log();
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Shared CLI utilities for metaowl bin scripts.
3
+ */
4
+ import { execSync } from 'node:child_process';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { dirname, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ function resolvePackageRoot() {
9
+ let currentDir = dirname(fileURLToPath(import.meta.url));
10
+ while (true) {
11
+ if (existsSync(resolve(currentDir, 'package.json'))) {
12
+ return currentDir;
13
+ }
14
+ const parentDir = resolve(currentDir, '..');
15
+ if (parentDir === currentDir) {
16
+ throw new Error('[metaowl] Could not resolve package root for CLI runtime');
17
+ }
18
+ currentDir = parentDir;
19
+ }
20
+ }
21
+ export const metaowlRoot = resolvePackageRoot();
22
+ export const bin = resolve(metaowlRoot, 'node_modules/.bin');
23
+ export const cwd = process.cwd();
24
+ const cwdBin = resolve(cwd, 'node_modules/.bin');
25
+ const packageJson = JSON.parse(readFileSync(resolve(metaowlRoot, 'package.json'), 'utf-8'));
26
+ export const version = packageJson.version;
27
+ export function resolveOwnRuntimeBin(name) {
28
+ const fileName = name.endsWith('.js') ? name : `${name}.js`;
29
+ return resolve(metaowlRoot, 'build/runtime/bin', fileName);
30
+ }
31
+ export function resolveBin(name) {
32
+ const local = resolve(bin, name);
33
+ if (existsSync(local))
34
+ return local;
35
+ const project = resolve(cwdBin, name);
36
+ if (existsSync(project))
37
+ return project;
38
+ return name;
39
+ }
40
+ const tty = Boolean(process.stdout.isTTY);
41
+ const a = (text, code) => tty ? `\x1b[${code}m${text}\x1b[0m` : text;
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
+ export function step(message) {
48
+ console.log(` ${a('›', '36')} ${message}`);
49
+ }
50
+ export function success(message) {
51
+ console.log(` ${a('✓', '32')} ${a(message, '2')}`);
52
+ }
53
+ export function failure(message) {
54
+ console.error(` ${a('✗', '31')} ${message}`);
55
+ }
56
+ export function run(label, cmd, opts = {}) {
57
+ step(label);
58
+ console.log();
59
+ try {
60
+ execSync(cmd, { stdio: 'inherit', cwd, ...opts });
61
+ }
62
+ catch {
63
+ console.log();
64
+ failure(`${label} failed`);
65
+ process.exit(1);
66
+ }
67
+ console.log();
68
+ }
@@ -0,0 +1,144 @@
1
+ import { mountApp, configureOwl } from './modules/app-mounter.js';
2
+ import { buildLayouts, clearLayouts, createLayoutWrapper, createNestedLayoutWrapper, defineLayout, defineNestedLayout, discoverLayouts, getCurrentLayout, getDefaultLayout, getLayout, getLayoutChain, getLayoutNames, getParentLayout, getRouteLayout, hasLayout, layout, mountWithLayout, registerLayout, resolveLayout, setDefaultLayout, setParentLayout, setRouteLayout, subscribeToLayouts, unregisterLayout } from './modules/layouts.js';
3
+ import { afterEach, _setSpaNavigationCallback, back, beforeEach, cancelNavigation, forward, getCurrentRoute, getPreviousRoute, go, isNavigating, isSpaMode, navigate, navigateTo, processRoutes, push, replace, router, setSpaMode } from './modules/router.js';
4
+ import { Link, registerLinkTemplate } from './modules/link.js';
5
+ import { buildRoutes, createCatchAllRoute, createRedirectRoute, defineRoute, findRoute, generateUrl, isDynamicRoute, matchRoute, route, validateRouteParams } from './modules/file-router.js';
6
+ import { captureError, clearErrorContext, errorBoundary, getErrorContext, initGlobalErrorHandling, onError, setErrorContext } from './modules/error-boundary.js';
7
+ import { configureI18n, createNamespacedT, formatCurrency, formatDate, formatNumber, formatRelativeTime, getLocale, i18n, loadLocaleMessages, setLocale, t } from './modules/i18n.js';
8
+ import { createSchema, fieldProps, useForm, validators } from './modules/forms.js';
9
+ import { authenticate, call, configure, create, getSession, isAuthenticated, listDatabases, logout, OdooService, onAuthChange, read, searchCount, searchRead, unlink, versionInfo, write } from './modules/odoo-rpc.js';
10
+ import { Composables, useAsyncState, useAuth, useCache, useDebounce, useFetch, useLocalStorage, useOnlineStatus, useThrottle, useWindowSize } from './modules/composables.js';
11
+ import { createMockStore, dom, flushPromises, mockRouter, mountComponent, nextTick, TestUtils, userEvent, wait } from './modules/test-utils.js';
12
+ import { createCanonicalUrl, generateOpenGraph, generateRobotsTxt, generateSitemap, generateSitemapIndex, generateTwitterCard, getPriorityByDepth, jsonLd, SEO, validateSitemap } from './modules/seo.js';
13
+ import { cache, checkCapabilities, generateManifest, getStorageInfo, isOnline, isStandalone, PWA, registerServiceWorker, requestPersistentStorage, showNotification, subscribeToConnectivity, subscribeToPush, sync, unregisterServiceWorker, unsubscribeFromPush } from './modules/pwa.js';
14
+ import Cache from './modules/cache.js';
15
+ import Fetch from './modules/fetch.js';
16
+ import * as Fonts from './modules/fonts.js';
17
+ import { ImageOptimizer } from './modules/image.js';
18
+ import * as Meta from './modules/meta.js';
19
+ import { Store, createPersistencePlugin, createStore } from './modules/store.js';
20
+ let appRoutes = null;
21
+ let navSeq = 0;
22
+ let mountingPromise = null;
23
+ function handle404() {
24
+ const el = document.getElementById('metaowl');
25
+ if (el) {
26
+ el.innerHTML = [
27
+ '<div style="font-family:sans-serif;padding:3rem;text-align:center">',
28
+ '<h1 style="font-size:4rem;font-weight:700;margin:0;color:#6b7280">404</h1>',
29
+ '<p style="font-size:1.25rem;color:#9ca3af;margin-top:0.5rem">Page not found</p>',
30
+ '<p style="margin-top:2rem"><a href="/" style="color:#3b82f6;text-decoration:none">← Go home</a></p>',
31
+ '</div>'
32
+ ].join('');
33
+ }
34
+ }
35
+ function isNoRouteFoundError(error) {
36
+ return error instanceof Error && error.message.startsWith('No route found');
37
+ }
38
+ function isBootOptions(value) {
39
+ return !!value && typeof value === 'object' && !Array.isArray(value) && 'spa' in value;
40
+ }
41
+ async function spaNavigate(path) {
42
+ if (!appRoutes) {
43
+ console.error('[metaowl] Routes not available for SPA navigation');
44
+ return;
45
+ }
46
+ const seq = ++navSeq;
47
+ let resolvedRoute;
48
+ try {
49
+ resolvedRoute = await processRoutes(appRoutes, path);
50
+ }
51
+ catch (error) {
52
+ if (seq !== navSeq)
53
+ return;
54
+ if (isNoRouteFoundError(error)) {
55
+ console.warn('[metaowl]', error.message);
56
+ handle404();
57
+ }
58
+ else {
59
+ throw error;
60
+ }
61
+ return;
62
+ }
63
+ if (seq !== navSeq || !resolvedRoute)
64
+ return;
65
+ if (mountingPromise) {
66
+ await mountingPromise.catch(() => { });
67
+ if (seq !== navSeq)
68
+ return;
69
+ }
70
+ mountingPromise = mountApp(resolvedRoute);
71
+ try {
72
+ await mountingPromise;
73
+ }
74
+ finally {
75
+ mountingPromise = null;
76
+ }
77
+ }
78
+ export async function boot(routesOrModules = {}, layoutsOrModules = null, options = {}) {
79
+ const effectiveOptions = { ...options };
80
+ let layoutModules = null;
81
+ if (isBootOptions(layoutsOrModules)) {
82
+ Object.assign(effectiveOptions, layoutsOrModules);
83
+ }
84
+ else {
85
+ layoutModules = layoutsOrModules;
86
+ }
87
+ const { spa = true } = effectiveOptions;
88
+ try {
89
+ if (layoutModules && typeof layoutModules === 'object' && !Array.isArray(layoutModules)) {
90
+ buildLayouts(layoutModules);
91
+ setDefaultLayout('default');
92
+ }
93
+ else {
94
+ await discoverLayouts();
95
+ }
96
+ }
97
+ catch (error) {
98
+ if (error instanceof Error) {
99
+ console.warn('[metaowl] Could not auto-discover layouts:', error.message);
100
+ }
101
+ }
102
+ const routes = Array.isArray(routesOrModules)
103
+ ? routesOrModules
104
+ : buildRoutes(routesOrModules);
105
+ appRoutes = routes;
106
+ if (spa) {
107
+ setSpaMode(true);
108
+ _setSpaNavigationCallback(spaNavigate);
109
+ window.__metaowlNavigate = spaNavigate;
110
+ window.addEventListener('popstate', () => {
111
+ const path = document.location.pathname;
112
+ void spaNavigate(path);
113
+ });
114
+ }
115
+ let resolvedRoute;
116
+ try {
117
+ resolvedRoute = await processRoutes(routes);
118
+ }
119
+ catch (error) {
120
+ if (isNoRouteFoundError(error)) {
121
+ console.warn('[metaowl]', error.message);
122
+ handle404();
123
+ return;
124
+ }
125
+ throw error;
126
+ }
127
+ if (!resolvedRoute)
128
+ return;
129
+ await mountApp(resolvedRoute);
130
+ }
131
+ export { Fetch, Cache, configureOwl, Meta, buildRoutes, Store, createPersistencePlugin, createStore };
132
+ export { registerLayout, unregisterLayout, getLayout, hasLayout, getLayoutNames, setDefaultLayout, getDefaultLayout, resolveLayout, setRouteLayout, getRouteLayout, createLayoutWrapper, createNestedLayoutWrapper, mountWithLayout, getCurrentLayout, subscribeToLayouts, clearLayouts, layout, defineLayout, defineNestedLayout, buildLayouts, discoverLayouts, setParentLayout, getParentLayout, getLayoutChain };
133
+ export { processRoutes, beforeEach, afterEach, getCurrentRoute, getPreviousRoute, isNavigating, cancelNavigation, navigate, navigateTo, push, replace, back, forward, go, router, setSpaMode, isSpaMode };
134
+ export { Link, registerLinkTemplate };
135
+ export { matchRoute, isDynamicRoute, findRoute, generateUrl, validateRouteParams, createCatchAllRoute, createRedirectRoute, defineRoute, route };
136
+ export { onError, setErrorContext, getErrorContext, clearErrorContext, captureError, initGlobalErrorHandling, errorBoundary };
137
+ export { configureI18n, t, getLocale, setLocale, i18n, loadLocaleMessages, formatDate, formatNumber, formatCurrency, formatRelativeTime, createNamespacedT };
138
+ export { useForm, validators, createSchema, fieldProps };
139
+ export { OdooService, configure, authenticate, logout, searchRead, call, read, create, write, unlink, searchCount, listDatabases, versionInfo, isAuthenticated, getSession, onAuthChange };
140
+ export { useAuth, useLocalStorage, useFetch, useDebounce, useThrottle, useWindowSize, useOnlineStatus, useAsyncState, useCache, Composables };
141
+ export { createMockStore, mockRouter, mountComponent, wait, nextTick, flushPromises, userEvent, dom, TestUtils };
142
+ export { generateSitemap, generateRobotsTxt, jsonLd, createCanonicalUrl, generateOpenGraph, generateTwitterCard, validateSitemap, getPriorityByDepth, generateSitemapIndex, SEO };
143
+ export { generateManifest, registerServiceWorker, unregisterServiceWorker, isStandalone, isOnline, subscribeToConnectivity, requestPersistentStorage, getStorageInfo, sync, subscribeToPush, unsubscribeFromPush, showNotification, cache, checkCapabilities, PWA };
144
+ export { ImageOptimizer as Image, Fonts };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @module AppMounter
3
+ *
4
+ * OWL application mounting with template merging.
5
+ */
6
+ import { mount } from '@odoo/owl';
7
+ import { Link } from './link.js';
8
+ import { getLayout, mountWithLayout, resolveLayout } from './layouts.js';
9
+ import { mergeTemplates } from './templates-manager.js';
10
+ const defaults = {
11
+ warnIfNoStaticProps: true,
12
+ willStartTimeout: 10000,
13
+ translatableAttributes: ['title', 'placeholder', 'label', 'alt']
14
+ };
15
+ let config = { ...defaults };
16
+ let currentApp = null;
17
+ let cachedTemplates = null;
18
+ export function configureOwl(nextConfig) {
19
+ config = {
20
+ ...defaults,
21
+ ...nextConfig,
22
+ translatableAttributes: nextConfig.translatableAttributes
23
+ ? Array.isArray(nextConfig.translatableAttributes)
24
+ ? nextConfig.translatableAttributes
25
+ : [nextConfig.translatableAttributes]
26
+ : defaults.translatableAttributes
27
+ };
28
+ }
29
+ export async function mountApp(route) {
30
+ const components = typeof COMPONENTS !== 'undefined' ? COMPONENTS : [];
31
+ if (!cachedTemplates) {
32
+ cachedTemplates = await mergeTemplates(components);
33
+ }
34
+ const templates = cachedTemplates;
35
+ const mountElement = document.getElementById('metaowl');
36
+ if (currentApp) {
37
+ try {
38
+ currentApp.destroy();
39
+ }
40
+ catch {
41
+ // Ignore destroy errors from stale app instances.
42
+ }
43
+ currentApp = null;
44
+ }
45
+ if (!mountElement) {
46
+ throw new Error('[metaowl] Mount element "#metaowl" not found');
47
+ }
48
+ mountElement.innerHTML = '';
49
+ const pageComponent = route[0]?.component;
50
+ const pagePath = document.location.pathname;
51
+ if (!pageComponent) {
52
+ throw new Error('[metaowl] Invalid route passed to mountApp()');
53
+ }
54
+ const pageComponentClass = pageComponent;
55
+ const layoutName = resolveLayout(pageComponentClass, pagePath);
56
+ const layoutClass = getLayout(layoutName);
57
+ const baseConfig = {
58
+ ...config,
59
+ templates,
60
+ components: {
61
+ Link,
62
+ 't-link': Link
63
+ }
64
+ };
65
+ let instance;
66
+ if (layoutClass) {
67
+ instance = await mountWithLayout(pageComponentClass, mountElement, { routePath: pagePath, templates }, baseConfig);
68
+ }
69
+ else {
70
+ instance = await mount(pageComponentClass, mountElement, baseConfig);
71
+ }
72
+ currentApp = instance?.__owl__?.app ?? null;
73
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @module AutoImport
3
+ *
4
+ * Automatic component importing for MetaOwl applications.
5
+ */
6
+ import { globSync } from 'glob';
7
+ import { basename, dirname, extname, relative, resolve } from 'node:path';
8
+ let importMap = null;
9
+ export function generateComponentMap(componentsDir, pattern = '*.js') {
10
+ const map = new Map();
11
+ const globPattern = pattern.includes('/') ? pattern : `${componentsDir}/${pattern}`;
12
+ const files = globSync(globPattern);
13
+ for (const file of files) {
14
+ if (file.includes('.test.') || file.includes('.spec.'))
15
+ continue;
16
+ const name = getComponentName(file);
17
+ if (!name)
18
+ continue;
19
+ const importPath = `/@components/${relative(componentsDir, file).replace(/\\/g, '/')}`;
20
+ map.set(name, importPath);
21
+ }
22
+ return map;
23
+ }
24
+ function getComponentName(filePath) {
25
+ const ext = extname(filePath);
26
+ const base = basename(filePath, ext);
27
+ if (basename(filePath) === base + ext) {
28
+ return base;
29
+ }
30
+ const dir = relative(process.cwd(), filePath).split('/');
31
+ if (base === 'index' && dir.length > 1) {
32
+ return toPascalCase(dir[dir.length - 2] ?? '');
33
+ }
34
+ return toPascalCase(base);
35
+ }
36
+ function toPascalCase(str) {
37
+ return str
38
+ .replace(/[-_](.)/g, (_, char) => char.toUpperCase())
39
+ .replace(/^[a-z]/, (char) => char.toUpperCase());
40
+ }
41
+ export async function scanComponents(componentsDir, options = {}) {
42
+ const { pattern: _pattern = '*.js' } = options;
43
+ const absoluteDir = resolve(componentsDir);
44
+ const fs = await import('node:fs/promises');
45
+ const { join } = await import('node:path');
46
+ const components = [];
47
+ async function scanDir(dir) {
48
+ try {
49
+ const entries = await fs.readdir(dir, { withFileTypes: true });
50
+ for (const entry of entries) {
51
+ const fullPath = join(dir, entry.name);
52
+ if (entry.isDirectory()) {
53
+ await scanDir(fullPath);
54
+ }
55
+ else if (entry.isFile() && entry.name.endsWith('.js')) {
56
+ if (entry.name.includes('.test.') || entry.name.includes('.spec.'))
57
+ continue;
58
+ const name = getComponentName(fullPath);
59
+ if (name && !components.includes(name)) {
60
+ components.push(name);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ catch {
66
+ // Directory doesn't exist or can't be read
67
+ }
68
+ }
69
+ await scanDir(absoluteDir);
70
+ return components;
71
+ }
72
+ export async function generateComponentDts(components, outputPath) {
73
+ const { mkdirSync, writeFileSync } = await import('node:fs');
74
+ const dir = dirname(outputPath);
75
+ mkdirSync(dir, { recursive: true });
76
+ const declarations = components
77
+ .map((name) => ` ${name}: typeof import('./components/${name}/${name}.js').default`)
78
+ .join('\n');
79
+ const content = `// Auto-generated by metaowl - do not edit\ndeclare module '@metaowl/components' {\n${declarations}\n}\n`;
80
+ writeFileSync(outputPath, content, 'utf-8');
81
+ }
82
+ export function generateImports(componentMap) {
83
+ const lines = [];
84
+ for (const [name, path] of componentMap) {
85
+ lines.push(`import ${name} from '${path}'`);
86
+ }
87
+ return lines.join('\n');
88
+ }
89
+ export function generateComponentsObject(componentMap) {
90
+ const entries = Array.from(componentMap.keys())
91
+ .map((name) => ` ${name}`)
92
+ .join(',\n');
93
+ return `{\n${entries}\n}`;
94
+ }
95
+ export function createAutoImportPlugin(options = {}) {
96
+ const { enabled = false, componentsDir = 'src/components', pattern = '**/*.js' } = options;
97
+ if (!enabled) {
98
+ return null;
99
+ }
100
+ importMap = generateComponentMap(componentsDir, `${componentsDir}/${pattern}`);
101
+ return {
102
+ name: 'metaowl:auto-import',
103
+ enforce: 'pre',
104
+ config(config) {
105
+ config.resolve ||= {};
106
+ config.resolve.alias ||= {};
107
+ config.resolve.alias['/@components'] = resolve(process.cwd(), componentsDir);
108
+ },
109
+ transform(code, id) {
110
+ if (!id.includes('/pages/') || !/\.[jt]s$/.test(id)) {
111
+ return null;
112
+ }
113
+ if (!code.includes('/* auto-import */') && !code.includes('// auto-import')) {
114
+ return null;
115
+ }
116
+ if (!importMap) {
117
+ return null;
118
+ }
119
+ const imports = generateImports(importMap);
120
+ const componentsObj = generateComponentsObject(importMap);
121
+ let transformed = imports + '\n\n' + code;
122
+ if (transformed.includes('extends Component')) {
123
+ transformed = transformed.replace(/(class\s+\w+\s+extends\s+Component\s*\{)/, `$1\n static components = ${componentsObj}\n`);
124
+ }
125
+ return { code: transformed, map: null };
126
+ }
127
+ };
128
+ }
129
+ export function registerAutoImport(name, path) {
130
+ if (!importMap) {
131
+ importMap = new Map();
132
+ }
133
+ importMap.set(name, path);
134
+ }
135
+ export function getAutoImportMap() {
136
+ return importMap ? new Map(importMap) : null;
137
+ }
138
+ export function clearAutoImports() {
139
+ importMap = null;
140
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @module Cache
3
+ *
4
+ * Async-style localStorage wrapper.
5
+ *
6
+ * Values are automatically JSON-serialised on write and deserialised on read.
7
+ * All methods return Promises so they are interchangeable with IndexedDB-based
8
+ * alternatives without changing call-sites.
9
+ */
10
+ export default class Cache {
11
+ /**
12
+ * Retrieve a value by key.
13
+ */
14
+ static async get(key) {
15
+ const rawValue = localStorage.getItem(key);
16
+ return rawValue === null ? null : JSON.parse(rawValue);
17
+ }
18
+ /**
19
+ * Store a value under the given key.
20
+ */
21
+ static async set(key, value) {
22
+ localStorage.setItem(key, JSON.stringify(value));
23
+ }
24
+ /**
25
+ * Remove a single entry.
26
+ */
27
+ static async remove(key) {
28
+ localStorage.removeItem(key);
29
+ }
30
+ /**
31
+ * Remove all entries from localStorage.
32
+ */
33
+ static async clear() {
34
+ localStorage.clear();
35
+ }
36
+ /**
37
+ * Return all keys currently stored in localStorage.
38
+ */
39
+ static async keys() {
40
+ const keys = [];
41
+ for (let index = 0; index < localStorage.length; index++) {
42
+ const key = localStorage.key(index);
43
+ if (key !== null) {
44
+ keys.push(key);
45
+ }
46
+ }
47
+ return keys;
48
+ }
49
+ }