@hashrock/ono 0.1.0 → 0.1.2
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 +88 -46
- package/package.json +19 -6
- package/src/browser/compiler.js +253 -0
- package/src/browser/unocss.js +29 -0
- package/src/builder.js +317 -0
- package/src/bundler.js +12 -4
- package/src/cli.js +178 -945
- package/src/content.js +272 -0
- package/src/jsx-runtime.js +19 -0
- package/src/renderer.js +11 -4
- package/src/server.js +99 -0
- package/src/unocss.js +28 -2
- package/src/watcher.js +167 -0
package/README.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# Ono
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Minimalist SSG framework with JSX, powered by TypeScript's JSX transformer
|
|
4
|
+
|
|
5
|
+
## Why Ono?
|
|
6
|
+
|
|
7
|
+
Ono is designed to be a minimal alternative to Astro, leveraging TypeScript's built-in JSX transformation capabilities. The core philosophy is:
|
|
8
|
+
|
|
9
|
+
- **Minimal Dependencies**: Uses TypeScript's standard `tsx` transform feature, avoiding complex build toolchains
|
|
10
|
+
- **High Portability**: Designed to be lightweight enough to run in Web Workers or even entirely in the browser
|
|
11
|
+
- **Simple Architecture**: A minimal subset of static site generation features, focusing on what matters most
|
|
12
|
+
- **Future Vision**: Enable browser-based REPL experiences and client-side static site generation
|
|
13
|
+
|
|
14
|
+
Unlike heavyweight frameworks, Ono embraces simplicity. It's perfect for developers who want the power of JSX and component-based development without the complexity of a full framework.
|
|
4
15
|
|
|
5
16
|
## Features
|
|
6
17
|
|
|
@@ -11,8 +22,9 @@ A lightweight JSX library for static site generation with UnoCSS support.
|
|
|
11
22
|
- File watching for automatic rebuilds
|
|
12
23
|
- Support for components and props
|
|
13
24
|
- Integrated UnoCSS for atomic CSS generation
|
|
14
|
-
- Quick project initialization with templates
|
|
15
25
|
- Pages directory support for multi-page sites
|
|
26
|
+
- Content collections with Markdown support
|
|
27
|
+
- Dynamic routes with `[slug].jsx` pattern
|
|
16
28
|
- Uses TypeScript's JSX transform
|
|
17
29
|
|
|
18
30
|
## Installation
|
|
@@ -31,62 +43,57 @@ npm install @hashrock/ono
|
|
|
31
43
|
|
|
32
44
|
## Quick Start
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
ono init my-project
|
|
38
|
-
cd my-project
|
|
39
|
-
npm run dev
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
This creates a complete project structure with:
|
|
43
|
-
- `pages/` directory with example page
|
|
44
|
-
- `components/` directory with Layout and Hello components
|
|
45
|
-
- `public/` directory for static assets
|
|
46
|
-
- UnoCSS integration pre-configured
|
|
47
|
-
- Development and build scripts
|
|
48
|
-
|
|
49
|
-
## Usage
|
|
46
|
+
**Try it online**: [Ono REPL](https://hashrock.github.io/ono/) - Worker-powered JSX playground!
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
**Init** - Initialize a new Ono project:
|
|
48
|
+
Or create a simple JSX file and build it:
|
|
54
49
|
|
|
55
50
|
```bash
|
|
56
|
-
|
|
57
|
-
ono
|
|
58
|
-
|
|
51
|
+
# Create a JSX file
|
|
52
|
+
echo '/** @jsxImportSource @hashrock/ono */
|
|
53
|
+
export default function App() {
|
|
54
|
+
return (
|
|
55
|
+
<html>
|
|
56
|
+
<head><title>Hello Ono</title></head>
|
|
57
|
+
<body>
|
|
58
|
+
<h1>Hello, Ono!</h1>
|
|
59
|
+
</body>
|
|
60
|
+
</html>
|
|
61
|
+
);
|
|
62
|
+
}' > index.jsx
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
# Build it
|
|
65
|
+
npx @hashrock/ono build index.jsx
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
ono
|
|
67
|
+
# Or start a dev server
|
|
68
|
+
npx @hashrock/ono dev index.jsx
|
|
64
69
|
```
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
## Usage
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
ono build pages
|
|
70
|
-
```
|
|
73
|
+
### CLI Commands
|
|
71
74
|
|
|
72
|
-
**Build
|
|
75
|
+
**Build** - Build JSX files to static HTML:
|
|
73
76
|
|
|
74
77
|
```bash
|
|
75
|
-
ono build pages
|
|
78
|
+
ono build # Build all pages in pages/ directory
|
|
79
|
+
ono build pages # Build all pages in pages/ directory
|
|
80
|
+
ono build example/index.jsx # Build a single file
|
|
81
|
+
ono build --output dist # Specify output directory
|
|
76
82
|
```
|
|
77
83
|
|
|
78
84
|
This will:
|
|
79
|
-
- Build your JSX
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
- No live reload (simple file watching)
|
|
85
|
+
- Build your JSX file(s) to HTML
|
|
86
|
+
- Copy files from `public/` directory
|
|
87
|
+
- Generate UnoCSS automatically
|
|
83
88
|
|
|
84
89
|
**Dev Server** - Start a development server with live reload:
|
|
85
90
|
|
|
86
91
|
```bash
|
|
87
|
-
ono dev
|
|
88
|
-
ono dev pages
|
|
89
|
-
ono dev example/index.jsx
|
|
92
|
+
ono dev # Start dev server for pages/ directory
|
|
93
|
+
ono dev pages # Start dev server for pages/ directory
|
|
94
|
+
ono dev example/index.jsx # Start dev server for a single file
|
|
95
|
+
ono dev --port 8080 # Start dev server on custom port
|
|
96
|
+
ono dev --output build # Use custom output directory
|
|
90
97
|
```
|
|
91
98
|
|
|
92
99
|
This will:
|
|
@@ -96,12 +103,6 @@ This will:
|
|
|
96
103
|
- Live reload the browser when files change
|
|
97
104
|
- Generate UnoCSS automatically
|
|
98
105
|
|
|
99
|
-
You can specify custom options:
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
ono dev --port 8080 --output build
|
|
103
|
-
```
|
|
104
|
-
|
|
105
106
|
**Help** - Show usage information:
|
|
106
107
|
|
|
107
108
|
```bash
|
|
@@ -236,16 +237,57 @@ npm install
|
|
|
236
237
|
|
|
237
238
|
### Run Tests
|
|
238
239
|
|
|
240
|
+
Run all unit tests:
|
|
241
|
+
|
|
239
242
|
```bash
|
|
240
243
|
npm test
|
|
241
244
|
```
|
|
242
245
|
|
|
246
|
+
Run snapshot tests:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
npm run test:snapshot
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Run all tests (unit + snapshot):
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
npm run test:all
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Update snapshots after intentional changes:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
npm run test:snapshot:update
|
|
262
|
+
```
|
|
263
|
+
|
|
243
264
|
### Watch Mode
|
|
244
265
|
|
|
245
266
|
```bash
|
|
246
267
|
npm run test:watch
|
|
247
268
|
```
|
|
248
269
|
|
|
270
|
+
### Testing Strategy
|
|
271
|
+
|
|
272
|
+
This project uses a comprehensive testing approach:
|
|
273
|
+
|
|
274
|
+
- **Unit Tests**: Traditional assertion-based tests for core functionality
|
|
275
|
+
- **Snapshot Tests**: Capture and verify output consistency for HTML rendering, JSX transformation, and CLI commands
|
|
276
|
+
- **Integration Tests**: End-to-end testing of build processes and content collections
|
|
277
|
+
|
|
278
|
+
For more details about snapshot testing, see [SNAPSHOT_TESTING.md](./SNAPSHOT_TESTING.md).
|
|
279
|
+
|
|
280
|
+
### Continuous Integration
|
|
281
|
+
|
|
282
|
+
This project uses GitHub Actions for automated testing. The CI pipeline:
|
|
283
|
+
|
|
284
|
+
- Tests on Node.js 22.x with Ubuntu
|
|
285
|
+
- Runs comprehensive unit and snapshot tests
|
|
286
|
+
- Validates CLI commands and build processes
|
|
287
|
+
- Ensures output consistency across environments
|
|
288
|
+
|
|
289
|
+
See `.github/workflows/ci.yml` for the full configuration
|
|
290
|
+
|
|
249
291
|
### Link for Local Development
|
|
250
292
|
|
|
251
293
|
```bash
|
package/package.json
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hashrock/ono",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Minimalist SSG framework with JSX, powered by TypeScript's JSX transformer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/jsx-runtime.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/jsx-runtime.js",
|
|
9
|
+
"./jsx-runtime": "./src/jsx-runtime.js",
|
|
10
|
+
"./renderer": "./src/renderer.js",
|
|
11
|
+
"./transformer": "./src/transformer.js",
|
|
12
|
+
"./bundler": "./src/bundler.js",
|
|
13
|
+
"./content": "./src/content.js",
|
|
14
|
+
"./browser/compiler": "./src/browser/compiler.js",
|
|
15
|
+
"./browser/unocss": "./src/browser/unocss.js"
|
|
16
|
+
},
|
|
7
17
|
"bin": {
|
|
8
18
|
"ono": "src/cli.js"
|
|
9
19
|
},
|
|
@@ -14,7 +24,10 @@
|
|
|
14
24
|
],
|
|
15
25
|
"scripts": {
|
|
16
26
|
"test": "node --test test/**/*.test.js",
|
|
17
|
-
"test:watch": "node --test --watch 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"
|
|
18
31
|
},
|
|
19
32
|
"keywords": [
|
|
20
33
|
"jsx",
|
|
@@ -38,11 +51,11 @@
|
|
|
38
51
|
"engines": {
|
|
39
52
|
"node": ">=18.0.0"
|
|
40
53
|
},
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"typescript": "^5.9.3"
|
|
43
|
-
},
|
|
44
54
|
"dependencies": {
|
|
45
55
|
"@unocss/preset-uno": "^66.5.4",
|
|
56
|
+
"h3": "^2.0.1-rc.5",
|
|
57
|
+
"marked": "^16.4.1",
|
|
58
|
+
"typescript": "^5.9.3",
|
|
46
59
|
"unocss": "^66.5.4",
|
|
47
60
|
"ws": "^8.18.3"
|
|
48
61
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser compiler utilities shared with the REPL worker.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { transformJSX } from '../transformer.js';
|
|
6
|
+
import { renderToString } from '../renderer.js';
|
|
7
|
+
import { h } from '../jsx-runtime.js';
|
|
8
|
+
import { getUnoGenerator } from './unocss.js';
|
|
9
|
+
|
|
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
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return resolved.join('/');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return to;
|
|
29
|
+
}
|
|
30
|
+
|
|
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
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
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;
|
|
166
|
+
}
|
|
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
|
+
}
|
|
179
|
+
|
|
180
|
+
bundledCode += `const __entry = __modules['${entryPoint}'];\n`;
|
|
181
|
+
|
|
182
|
+
return bundledCode;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function extractRenderCandidate(entryModule, entryCode) {
|
|
186
|
+
if (!entryModule || typeof entryModule !== 'object') {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const defaultExport = entryModule.default;
|
|
191
|
+
if (typeof defaultExport === 'function') {
|
|
192
|
+
return defaultExport;
|
|
193
|
+
}
|
|
194
|
+
if (defaultExport !== undefined) {
|
|
195
|
+
return () => defaultExport;
|
|
196
|
+
}
|
|
197
|
+
|
|
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;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const firstValue = Object.values(entryModule)[0];
|
|
216
|
+
if (firstValue !== undefined) {
|
|
217
|
+
return () => firstValue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function evaluateEntryModule(bundledCode) {
|
|
224
|
+
const getEntryModule = new Function('h', `
|
|
225
|
+
${bundledCode}
|
|
226
|
+
return __entry;
|
|
227
|
+
`);
|
|
228
|
+
|
|
229
|
+
return getEntryModule(h);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function compileProject(files, entryPoint = 'index.jsx', options = {}) {
|
|
233
|
+
const bundled = bundleModules(files, entryPoint);
|
|
234
|
+
const entryModule = evaluateEntryModule(bundled);
|
|
235
|
+
const entryCode = files[entryPoint] || '';
|
|
236
|
+
const renderCandidate = extractRenderCandidate(entryModule, entryCode);
|
|
237
|
+
|
|
238
|
+
if (!renderCandidate) {
|
|
239
|
+
throw new Error('Unable to determine a render function in the entry module.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const vnode = renderCandidate();
|
|
243
|
+
const html = renderToString(vnode);
|
|
244
|
+
|
|
245
|
+
let css = '';
|
|
246
|
+
if (options.enableUno !== false) {
|
|
247
|
+
const uno = await getUnoGenerator(options.unoConfig);
|
|
248
|
+
const result = await uno.generate(html, { preflights: true });
|
|
249
|
+
css = result.css;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { html, css };
|
|
253
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight UnoCSS helpers for browser environments.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createGenerator } from '@unocss/core';
|
|
6
|
+
import { presetUno } from '@unocss/preset-uno';
|
|
7
|
+
|
|
8
|
+
let cachedGeneratorKey = null;
|
|
9
|
+
let cachedGeneratorPromise = null;
|
|
10
|
+
|
|
11
|
+
function serializeConfig(config = {}) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.stringify(config);
|
|
14
|
+
} catch {
|
|
15
|
+
return '__dynamic__';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getUnoGenerator(config = {}) {
|
|
20
|
+
const key = serializeConfig(config);
|
|
21
|
+
if (!cachedGeneratorPromise || cachedGeneratorKey !== key) {
|
|
22
|
+
cachedGeneratorKey = key;
|
|
23
|
+
cachedGeneratorPromise = createGenerator({
|
|
24
|
+
presets: [presetUno()],
|
|
25
|
+
...config,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return cachedGeneratorPromise;
|
|
29
|
+
}
|