@hashrock/ono 0.1.3 → 0.2.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/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @hashrock/ono
2
+
3
+ ミニマリストなSSGフレームワーク。JSXとTypeScriptのJSXトランスフォーマーを活用。
4
+
5
+ ## インストール
6
+
7
+ ```bash
8
+ npm install @hashrock/ono
9
+ ```
10
+
11
+ ## 使い方
12
+
13
+ ```jsx
14
+ export default function App() {
15
+ return <h1>Hello, Ono!</h1>;
16
+ }
17
+ ```
18
+
19
+ ```bash
20
+ npx ono build index.jsx
21
+ npx ono dev index.jsx
22
+ ```
23
+
24
+ ## 機能
25
+
26
+ - JSXから静的HTMLへの変換
27
+ - ライブリロード付き開発サーバー
28
+ - UnoCSS統合
29
+ - コンテンツコレクション(Markdown)
30
+ - 動的ルート(`[slug].jsx`)
31
+
32
+ ## 制限事項
33
+
34
+ - **React Fragmentは非対応**: `<>...</>` や `<React.Fragment>` はサポートされていません。代わりに配列や親要素でラップしてください。
35
+
36
+ ```jsx
37
+ // NG: React Fragmentは使用不可
38
+ <>
39
+ <div>Item 1</div>
40
+ <div>Item 2</div>
41
+ </>
42
+
43
+ // OK: 配列を使用
44
+ [
45
+ <div>Item 1</div>,
46
+ <div>Item 2</div>
47
+ ]
48
+
49
+ // OK: 親要素でラップ
50
+ <div>
51
+ <div>Item 1</div>
52
+ <div>Item 2</div>
53
+ </div>
54
+ ```
55
+
56
+ 詳細なドキュメントは[ルートのREADME](../../README.md)を参照してください。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hashrock/ono",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Minimalist SSG framework with JSX, powered by TypeScript's JSX transformer",
5
5
  "type": "module",
6
6
  "main": "src/jsx-runtime.js",
@@ -9,8 +9,9 @@
9
9
  "./jsx-runtime": "./src/jsx-runtime.js",
10
10
  "./renderer": "./src/renderer.js",
11
11
  "./transformer": "./src/transformer.js",
12
+ "./parser": "./src/parser.js",
12
13
  "./bundler": "./src/bundler.js",
13
- "./content": "./src/content.js",
14
+ "./barrels": "./src/barrels.js",
14
15
  "./browser/compiler": "./src/browser/compiler.js",
15
16
  "./browser/unocss": "./src/browser/unocss.js"
16
17
  },
@@ -23,11 +24,10 @@
23
24
  "LICENSE"
24
25
  ],
25
26
  "scripts": {
26
- "test": "node --test test/**/*.test.js",
27
- "test:watch": "node --test --watch test/**/*.test.js",
28
- "test:snapshot": "node --test test/**/*.snapshot.test.js",
29
- "test:snapshot:update": "UPDATE_SNAPSHOTS=true node --test test/**/*.snapshot.test.js",
30
- "test:all": "npm run test && npm run test:snapshot"
27
+ "typecheck": "tsc --noEmit",
28
+ "test": "node --test test/*.test.js",
29
+ "test:watch": "node --test --watch test/*.test.js",
30
+ "test:update": "node --test --test-update-snapshots test/*.test.js"
31
31
  },
32
32
  "keywords": [
33
33
  "jsx",
@@ -49,14 +49,15 @@
49
49
  },
50
50
  "homepage": "https://github.com/hashrock/ono#readme",
51
51
  "engines": {
52
- "node": ">=18.0.0"
52
+ "node": ">=22.3.0"
53
53
  },
54
54
  "dependencies": {
55
+ "@unocss/core": "^66.5.4",
55
56
  "@unocss/preset-uno": "^66.5.4",
56
- "h3": "^2.0.1-rc.5",
57
- "marked": "^16.4.1",
58
- "typescript": "^5.9.3",
59
- "unocss": "^66.5.4",
60
- "ws": "^8.18.3"
57
+ "@unocss/reset": "^66.5.4",
58
+ "typescript": "^5.9.3"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "^26.1.1"
61
62
  }
62
63
  }
package/src/barrels.js ADDED
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Barrels - Auto-generated barrel files with type inference
3
+ */
4
+ import { readdir, writeFile } from "node:fs/promises";
5
+ import { join, basename, relative } from "node:path";
6
+ import { importJSXModule } from "./builder.js";
7
+ import { isJSXFile } from "./utils.js";
8
+
9
+ /**
10
+ * Convert kebab-case or snake_case to camelCase
11
+ * @param {string} str - String to convert
12
+ * @returns {string} camelCase string
13
+ */
14
+ function toCamelCase(str) {
15
+ return str.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
16
+ }
17
+
18
+ /**
19
+ * Infer TypeScript type from a JavaScript value
20
+ * @param {unknown} value
21
+ * @returns {string}
22
+ */
23
+ function inferType(value) {
24
+ if (value === null) return "null";
25
+ if (value === undefined) return "undefined";
26
+ if (Array.isArray(value)) {
27
+ if (value.length === 0) return "unknown[]";
28
+ // Infer from first element
29
+ return `${inferType(value[0])}[]`;
30
+ }
31
+ if (value instanceof Date) return "Date";
32
+ if (typeof value === "object") {
33
+ // For nested objects, use Record type
34
+ return "Record<string, unknown>";
35
+ }
36
+ return typeof value; // 'string' | 'number' | 'boolean'
37
+ }
38
+
39
+ /**
40
+ * Generate Meta type definition from collected metas
41
+ * @param {Array<Record<string, unknown> | null>} metas
42
+ */
43
+ function generateMetaType(metas) {
44
+ if (metas.length === 0) return "export type Meta = Record<string, never>;";
45
+
46
+ // Collect all keys and their occurrence count
47
+ /** @type {Record<string, number>} */
48
+ const keyCounts = {};
49
+ /** @type {Record<string, string>} */
50
+ const keyTypes = {};
51
+
52
+ for (const meta of metas) {
53
+ if (!meta) continue;
54
+ for (const [key, value] of Object.entries(meta)) {
55
+ keyCounts[key] = (keyCounts[key] || 0) + 1;
56
+ // Store the first non-undefined value's type
57
+ if (!keyTypes[key] && value !== undefined) {
58
+ keyTypes[key] = inferType(value);
59
+ }
60
+ }
61
+ }
62
+
63
+ // Generate type fields
64
+ const fields = Object.entries(keyCounts)
65
+ .map(([key, count]) => {
66
+ const type = keyTypes[key] || "unknown";
67
+ const optional = count < metas.length;
68
+ return ` ${key}${optional ? "?" : ""}: ${type};`;
69
+ })
70
+ .join("\n");
71
+
72
+ return `export type Meta = {\n${fields}\n};`;
73
+ }
74
+
75
+ /**
76
+ * Get all entry files from a barrel directory
77
+ * @param {string} barrelDir - Directory to scan for barrel entries
78
+ * @returns {Promise<Array<{id: string, file: string, path: string}>>}
79
+ */
80
+ async function getBarrelEntries(barrelDir) {
81
+ const entries = [];
82
+
83
+ try {
84
+ const files = await readdir(barrelDir, { withFileTypes: true });
85
+
86
+ for (const file of files) {
87
+ if (file.isFile() && isJSXFile(file.name)) {
88
+ const ext = file.name.endsWith(".tsx") ? ".tsx" : ".jsx";
89
+ const id = basename(file.name, ext);
90
+ entries.push({
91
+ id,
92
+ file: file.name,
93
+ path: join(barrelDir, file.name),
94
+ });
95
+ }
96
+ }
97
+ } catch (error) {
98
+ if (error.code !== "ENOENT") throw error;
99
+ }
100
+
101
+ return entries.sort((a, b) => a.id.localeCompare(b.id));
102
+ }
103
+
104
+ /**
105
+ * Load meta from an entry file by compiling and evaluating it
106
+ * @param {string} entryPath - Path to the entry file
107
+ * @returns {Promise<Record<string, unknown> | null>} Meta object or null
108
+ */
109
+ async function loadMeta(entryPath) {
110
+ try {
111
+ const module = await importJSXModule(entryPath);
112
+ return module.meta || null;
113
+ } catch (error) {
114
+ console.warn(`Warning: Could not load meta from ${entryPath}:`, error.message);
115
+ return null;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Generate a barrel file for a directory
121
+ * @param {string} barrelDir
122
+ * @param {{ silent?: boolean }} [options]
123
+ */
124
+ export async function generateBarrel(barrelDir, options = {}) {
125
+ const { silent = false } = options;
126
+
127
+ const entries = await getBarrelEntries(barrelDir);
128
+
129
+ if (entries.length === 0) {
130
+ return null;
131
+ }
132
+
133
+ // Load all metas
134
+ const metas = await Promise.all(
135
+ entries.map((entry) => loadMeta(entry.path))
136
+ );
137
+
138
+ // Generate type definition
139
+ const metaType = generateMetaType(metas);
140
+
141
+ // Generate imports with camelCase identifiers.
142
+ // Import first, then export the local bindings — a plain
143
+ // `export ... from` would not bring the names into scope for `posts`.
144
+ const imports = entries
145
+ .map((entry, idx) => {
146
+ const camelId = toCamelCase(entry.id);
147
+ const from = `'./${basename(barrelDir)}/${entry.file}'`;
148
+ if (metas[idx] !== null) {
149
+ return `import ${camelId}, { meta as ${camelId}Meta } from ${from};\nexport { ${camelId}, ${camelId}Meta };`;
150
+ }
151
+ return `import ${camelId} from ${from};\nexport { ${camelId} };`;
152
+ })
153
+ .join("\n");
154
+
155
+ // Generate entries array (keep original IDs for URL paths)
156
+ const entriesArray = `export const entries = [${entries.map((e) => `'${e.id}'`).join(", ")}] as const;`;
157
+
158
+ // Generate ID to component mapping
159
+ const mapping = entries
160
+ .map((entry, idx) => {
161
+ const camelId = toCamelCase(entry.id);
162
+ const hasExportedMeta = metas[idx] !== null;
163
+ if (hasExportedMeta) {
164
+ return ` '${entry.id}': { component: ${camelId}, meta: ${camelId}Meta }`;
165
+ }
166
+ return ` '${entry.id}': { component: ${camelId}, meta: null }`;
167
+ })
168
+ .join(",\n");
169
+ const mappingExport = `export const posts = {\n${mapping}\n};`;
170
+ const entryIdType = "export type EntryId = typeof entries[number];";
171
+
172
+ // Generate barrel content
173
+ const barrelContent = `// Auto-generated barrel file - DO NOT EDIT
174
+ // Generated by Ono SSG
175
+
176
+ ${metaType}
177
+
178
+ ${imports}
179
+
180
+ ${entriesArray}
181
+ ${entryIdType}
182
+
183
+ ${mappingExport}
184
+ `;
185
+
186
+ // Write barrel file
187
+ const barrelPath = `${barrelDir}.ts`;
188
+ await writeFile(barrelPath, barrelContent);
189
+
190
+ if (!silent) {
191
+ console.log(` Generated: ${relative(process.cwd(), barrelPath)}`);
192
+ }
193
+
194
+ return barrelPath;
195
+ }
196
+
197
+ /**
198
+ * Generate all barrel files in a directory
199
+ * @param {string} barrelsRoot
200
+ * @param {{ silent?: boolean }} [options]
201
+ */
202
+ export async function generateBarrels(barrelsRoot, options = {}) {
203
+ const { silent = false } = options;
204
+
205
+ try {
206
+ const entries = await readdir(barrelsRoot, { withFileTypes: true });
207
+ const results = [];
208
+
209
+ for (const entry of entries) {
210
+ if (entry.isDirectory()) {
211
+ const barrelDir = join(barrelsRoot, entry.name);
212
+ const result = await generateBarrel(barrelDir, { silent: true });
213
+ if (result) {
214
+ results.push(result);
215
+ }
216
+ }
217
+ }
218
+
219
+ if (!silent && results.length > 0) {
220
+ console.log(`Generated ${results.length} barrel file(s)`);
221
+ }
222
+
223
+ return results;
224
+ } catch (error) {
225
+ if (error.code === "ENOENT") {
226
+ return [];
227
+ }
228
+ throw error;
229
+ }
230
+ }
@@ -1,188 +1,83 @@
1
1
  /**
2
- * Browser compiler utilities shared with the REPL worker.
2
+ * Browser compiler - shared with the REPL worker.
3
+ *
4
+ * Uses the same mini bundler as the Node build (bundler.js/parser.js).
5
+ * Package imports are rejected — the browser has no module resolution —
6
+ * and the bundle is evaluated with new Function, with the JSX runtime
7
+ * passed in as parameters.
3
8
  */
4
9
 
5
10
  import { transformJSX } from '../transformer.js';
6
11
  import { renderToString } from '../renderer.js';
7
- import { h } from '../jsx-runtime.js';
12
+ import { h, Fragment } from '../jsx-runtime.js';
13
+ import { bundle } from '../bundler.js';
8
14
  import { getUnoGenerator } from './unocss.js';
9
15
 
10
- function resolveImport(from, to) {
11
- if (to.startsWith('./') || to.startsWith('../')) {
12
- const fromParts = from.split('/').slice(0, -1);
13
- const targetParts = to.split('/');
14
- const resolved = [...fromParts];
15
-
16
- for (const part of targetParts) {
17
- if (!part || part === '.') continue;
18
- if (part === '..') {
19
- resolved.pop();
20
- } else {
21
- resolved.push(part);
22
- }
16
+ /**
17
+ * Join a relative specifier against the importing file's virtual path
18
+ * @param {string} fromId
19
+ * @param {string} specifier
20
+ */
21
+ function resolveImport(fromId, specifier) {
22
+ const fromParts = fromId.split('/').slice(0, -1);
23
+ const targetParts = specifier.split('/');
24
+ const resolved = [...fromParts];
25
+
26
+ for (const part of targetParts) {
27
+ if (!part || part === '.') continue;
28
+ if (part === '..') {
29
+ resolved.pop();
30
+ } else {
31
+ resolved.push(part);
23
32
  }
24
-
25
- return resolved.join('/');
26
33
  }
27
34
 
28
- return to;
35
+ return resolved.join('/');
29
36
  }
30
37
 
31
- function topoSortModules(files, entryPoint) {
32
- const filenames = Object.keys(files);
33
- const dependencyMap = new Map();
34
- const importPattern = /import\s+(?:[\s\S]+?)?from\s+['"]([^'"]+)['"]/g;
35
-
36
- for (const filename of filenames) {
37
- const source = files[filename] || '';
38
- const deps = [];
39
- source.replace(importPattern, (match, specifier) => {
40
- const resolved = resolveImport(filename, specifier);
41
- if (files[resolved]) {
42
- deps.push(resolved);
43
- }
44
- return match;
45
- });
46
- dependencyMap.set(filename, deps);
47
- }
48
-
49
- const visited = new Set();
50
- const order = [];
51
-
52
- const visit = (file) => {
53
- if (!files[file] || visited.has(file)) return;
54
- visited.add(file);
55
- const deps = dependencyMap.get(file) || [];
56
- for (const dep of deps) {
57
- visit(dep);
58
- }
59
- order.push(file);
60
- };
61
-
62
- if (entryPoint) {
63
- visit(entryPoint);
64
- }
65
-
66
- for (const filename of filenames) {
67
- if (!visited.has(filename)) {
68
- visit(filename);
69
- }
38
+ /**
39
+ * @param {Record<string, string>} files
40
+ * @param {string} entryPoint
41
+ */
42
+ async function bundleProject(files, entryPoint) {
43
+ if (!(entryPoint in files)) {
44
+ throw new Error(`Entry point '${entryPoint}' not found`);
70
45
  }
71
46
 
72
- return order;
73
- }
74
-
75
- function bundleModules(files, entryPoint) {
76
- const order = topoSortModules(files, entryPoint);
77
- let bundledCode = 'const __modules = {};\n';
78
-
79
- for (const filename of order) {
80
- const transformedCode = transformJSX(files[filename], filename);
81
- let code = transformedCode;
82
- const exportMappings = new Map();
83
-
84
- const addExport = (exportName, localName = exportName) => {
85
- if (!exportName || exportMappings.has(exportName)) return;
86
- exportMappings.set(exportName, localName);
87
- };
88
-
89
- code = code.replace(/import\s+{([^}]+)}\s+from\s+['"]([^'"]+)['"]/g, (match, imports, path) => {
90
- const resolvedPath = resolveImport(filename, path);
91
- const importNames = imports.split(',').map(part => part.trim()).filter(Boolean);
92
-
93
- return importNames
94
- .map(name => {
95
- const [local, alias] = name.split(/\s+as\s+/).map(token => token && token.trim());
96
- const localName = alias || local;
97
- const exportName = local;
98
- return `const ${localName} = __modules['${resolvedPath}']['${exportName}'];`;
99
- })
100
- .join('\n');
101
- });
102
-
103
- code = code.replace(/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g, (match, localName, path) => {
104
- const resolvedPath = resolveImport(filename, path);
105
- return `const ${localName} = __modules['${resolvedPath}']['default'];`;
106
- });
107
-
108
- code = code.replace(/import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g, (match, localName, path) => {
109
- const resolvedPath = resolveImport(filename, path);
110
- return `const ${localName} = __modules['${resolvedPath}'];`;
111
- });
112
-
113
- code = code.replace(/export\s+default\s+function\s+([A-Za-z_$][\w$]*)/g, (match, name) => {
114
- addExport('default', name);
115
- return `function ${name}`;
116
- });
117
-
118
- code = code.replace(/export\s+default\s+([A-Za-z_$][\w$]*)/g, (match, name) => {
119
- addExport('default', name);
120
- return `${name}`;
121
- });
122
-
123
- code = code.replace(/export\s+(const|let|var)\s+([A-Za-z_$][\w$]*)/g, (match, kind, name) => {
124
- addExport(name);
125
- return `${kind} ${name}`;
126
- });
127
-
128
- code = code.replace(/export\s+function\s+([A-Za-z_$][\w$]*)/g, (match, name) => {
129
- addExport(name);
130
- return `function ${name}`;
131
- });
132
-
133
- code = code.replace(/export\s+class\s+([A-Za-z_$][\w$]*)/g, (match, name) => {
134
- addExport(name);
135
- return `class ${name}`;
136
- });
137
-
138
- code = code.replace(/export\s*{\s*([^}]+)\s*};?/g, (match, names) => {
139
- names
140
- .split(',')
141
- .map(part => part.trim())
142
- .filter(Boolean)
143
- .forEach(part => {
144
- const [local, alias] = part.split(/\s+as\s+/).map(token => token && token.trim());
145
- addExport(alias || local, local);
146
- });
147
- return '';
148
- });
149
-
150
- code = code.replace(/export\s*{\s*};?/g, '');
151
-
152
- if (filename === entryPoint) {
153
- const functionMatches = code.matchAll(/function\s+([A-Za-z_$][\w$]*)/g);
154
- for (const match of functionMatches) {
155
- const name = match[1];
156
- addExport(name);
157
- }
158
- }
159
-
160
- const exportEntries = Array.from(exportMappings.entries()).map(([exportName, localName]) => {
161
- if (exportName === 'default') {
162
- return `'default': ${localName}`;
163
- }
164
- if (exportName === localName) {
165
- return exportName;
47
+ const { code } = await bundle({
48
+ entry: entryPoint,
49
+ resolve: (specifier, fromId) => {
50
+ const resolved = resolveImport(fromId, specifier);
51
+ if (!(resolved in files)) {
52
+ throw new Error(`Cannot find module '${specifier}' imported from '${fromId}'`);
166
53
  }
167
- return `'${exportName}': ${localName}`;
168
- });
169
-
170
- const exportsObject = exportEntries.length > 0 ? `{ ${exportEntries.join(', ')} }` : '{}';
171
-
172
- bundledCode += `
173
- __modules['${filename}'] = (() => {
174
- ${code}
175
- return ${exportsObject};
176
- })();
177
- `;
178
- }
54
+ return resolved;
55
+ },
56
+ load: (id) => transformJSX(files[id], id),
57
+ onExternal: 'error',
58
+ exposeEntryFunctions: true,
59
+ });
60
+
61
+ return code;
62
+ }
179
63
 
180
- bundledCode += `const __entry = __modules['${entryPoint}'];\n`;
64
+ /** @param {string} bundledCode */
65
+ function evaluateEntryModule(bundledCode) {
66
+ const getEntryModule = new Function('h', 'Fragment', `
67
+ ${bundledCode}
68
+ return __ono_entry;
69
+ `);
181
70
 
182
- return bundledCode;
71
+ return getEntryModule(h, Fragment);
183
72
  }
184
73
 
185
- function extractRenderCandidate(entryModule, entryCode) {
74
+ /**
75
+ * Pick what to render from the entry module: the default export if there
76
+ * is one, otherwise the last exported function (REPL snippets usually
77
+ * define components and finish with an App function), otherwise any value.
78
+ * @param {any} entryModule
79
+ */
80
+ function extractRenderCandidate(entryModule) {
186
81
  if (!entryModule || typeof entryModule !== 'object') {
187
82
  return null;
188
83
  }
@@ -195,21 +90,9 @@ function extractRenderCandidate(entryModule, entryCode) {
195
90
  return () => defaultExport;
196
91
  }
197
92
 
198
- const lastCallMatch = entryCode.match(/([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*$/m);
199
- if (lastCallMatch) {
200
- const lastCallName = lastCallMatch[1];
201
- const candidate = entryModule[lastCallName];
202
- if (typeof candidate === 'function') {
203
- return candidate;
204
- }
205
- if (candidate !== undefined) {
206
- return () => candidate;
207
- }
208
- }
209
-
210
- const fallbackFunction = Object.values(entryModule).find(value => typeof value === 'function');
211
- if (fallbackFunction) {
212
- return fallbackFunction;
93
+ const functions = Object.values(entryModule).filter((value) => typeof value === 'function');
94
+ if (functions.length > 0) {
95
+ return functions[functions.length - 1];
213
96
  }
214
97
 
215
98
  const firstValue = Object.values(entryModule)[0];
@@ -220,20 +103,15 @@ function extractRenderCandidate(entryModule, entryCode) {
220
103
  return null;
221
104
  }
222
105
 
223
- function evaluateEntryModule(bundledCode) {
224
- const getEntryModule = new Function('h', `
225
- ${bundledCode}
226
- return __entry;
227
- `);
228
-
229
- return getEntryModule(h);
230
- }
231
-
106
+ /**
107
+ * @param {Record<string, string>} files
108
+ * @param {string} entryPoint
109
+ * @param {{ enableUno?: boolean, unoConfig?: any }} [options]
110
+ */
232
111
  export async function compileProject(files, entryPoint = 'index.jsx', options = {}) {
233
- const bundled = bundleModules(files, entryPoint);
112
+ const bundled = await bundleProject(files, entryPoint);
234
113
  const entryModule = evaluateEntryModule(bundled);
235
- const entryCode = files[entryPoint] || '';
236
- const renderCandidate = extractRenderCandidate(entryModule, entryCode);
114
+ const renderCandidate = extractRenderCandidate(entryModule);
237
115
 
238
116
  if (!renderCandidate) {
239
117
  throw new Error('Unable to determine a render function in the entry module.');
@@ -5,9 +5,12 @@
5
5
  import { createGenerator } from '@unocss/core';
6
6
  import { presetUno } from '@unocss/preset-uno';
7
7
 
8
+ /** @type {string | null} */
8
9
  let cachedGeneratorKey = null;
10
+ /** @type {Promise<any> | null} */
9
11
  let cachedGeneratorPromise = null;
10
12
 
13
+ /** @param {object} [config] */
11
14
  function serializeConfig(config = {}) {
12
15
  try {
13
16
  return JSON.stringify(config);
@@ -16,12 +19,13 @@ function serializeConfig(config = {}) {
16
19
  }
17
20
  }
18
21
 
22
+ /** @param {object} [config] */
19
23
  export async function getUnoGenerator(config = {}) {
20
24
  const key = serializeConfig(config);
21
25
  if (!cachedGeneratorPromise || cachedGeneratorKey !== key) {
22
26
  cachedGeneratorKey = key;
23
27
  cachedGeneratorPromise = createGenerator({
24
- presets: [presetUno()],
28
+ presets: [/** @type {any} */ (presetUno())],
25
29
  ...config,
26
30
  });
27
31
  }