@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 +56 -0
- package/package.json +14 -13
- package/src/barrels.js +230 -0
- package/src/browser/compiler.js +71 -193
- package/src/browser/unocss.js +5 -1
- package/src/builder.js +109 -230
- package/src/bundler.js +215 -64
- package/src/cli.js +22 -191
- package/src/commands/build.js +141 -0
- package/src/commands/dev.js +54 -0
- package/src/constants.js +74 -0
- package/src/jsx-runtime.js +15 -5
- package/src/parser.js +555 -0
- package/src/renderer.js +29 -48
- package/src/server.js +88 -74
- package/src/transformer.js +1 -18
- package/src/unocss.js +20 -24
- package/src/utils.js +55 -0
- package/src/watcher.js +98 -125
- package/src/content.js +0 -272
- package/src/resolver.js +0 -137
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.
|
|
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
|
-
"./
|
|
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
|
-
"
|
|
27
|
-
"test
|
|
28
|
-
"test:
|
|
29
|
-
"test:
|
|
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": ">=
|
|
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
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
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
|
+
}
|
package/src/browser/compiler.js
CHANGED
|
@@ -1,188 +1,83 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser compiler
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
35
|
+
return resolved.join('/');
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
|
71
|
+
return getEntryModule(h, Fragment);
|
|
183
72
|
}
|
|
184
73
|
|
|
185
|
-
|
|
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
|
|
199
|
-
if (
|
|
200
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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 =
|
|
112
|
+
const bundled = await bundleProject(files, entryPoint);
|
|
234
113
|
const entryModule = evaluateEntryModule(bundled);
|
|
235
|
-
const
|
|
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.');
|
package/src/browser/unocss.js
CHANGED
|
@@ -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
|
}
|