@hashrock/ono 0.1.2 → 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 CHANGED
@@ -1,341 +1,56 @@
1
- # Ono
1
+ # @hashrock/ono
2
2
 
3
- Minimalist SSG framework with JSX, powered by TypeScript's JSX transformer
3
+ ミニマリストなSSGフレームワーク。JSXTypeScriptJSXトランスフォーマーを活用。
4
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.
15
-
16
- ## Features
17
-
18
- - Minimal JSX runtime for building static HTML sites
19
- - Built-in bundler for JSX files
20
- - CLI tool for building JSX to HTML
21
- - Development server with live reload
22
- - File watching for automatic rebuilds
23
- - Support for components and props
24
- - Integrated UnoCSS for atomic CSS generation
25
- - Pages directory support for multi-page sites
26
- - Content collections with Markdown support
27
- - Dynamic routes with `[slug].jsx` pattern
28
- - Uses TypeScript's JSX transform
29
-
30
- ## Installation
31
-
32
- ### Global Installation
33
-
34
- ```bash
35
- npm install -g @hashrock/ono
36
- ```
37
-
38
- ### Local Installation
5
+ ## インストール
39
6
 
40
7
  ```bash
41
8
  npm install @hashrock/ono
42
9
  ```
43
10
 
44
- ## Quick Start
45
-
46
- **Try it online**: [Ono REPL](https://hashrock.github.io/ono/) - Worker-powered JSX playground!
47
-
48
- Or create a simple JSX file and build it:
49
-
50
- ```bash
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
63
-
64
- # Build it
65
- npx @hashrock/ono build index.jsx
66
-
67
- # Or start a dev server
68
- npx @hashrock/ono dev index.jsx
69
- ```
70
-
71
- ## Usage
72
-
73
- ### CLI Commands
74
-
75
- **Build** - Build JSX files to static HTML:
76
-
77
- ```bash
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
82
- ```
83
-
84
- This will:
85
- - Build your JSX file(s) to HTML
86
- - Copy files from `public/` directory
87
- - Generate UnoCSS automatically
88
-
89
- **Dev Server** - Start a development server with live reload:
90
-
91
- ```bash
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
97
- ```
98
-
99
- This will:
100
- - Build your JSX file(s) to HTML
101
- - Start an HTTP server (default: http://localhost:3000)
102
- - Watch for changes and rebuild automatically
103
- - Live reload the browser when files change
104
- - Generate UnoCSS automatically
105
-
106
- **Help** - Show usage information:
107
-
108
- ```bash
109
- ono --help
110
- ```
111
-
112
- ### JSX Example
113
-
114
- Create a JSX file with components:
11
+ ## 使い方
115
12
 
116
13
  ```jsx
117
- // example/index.jsx
118
14
  export default function App() {
119
- return (
120
- <html>
121
- <head>
122
- <title>My Site</title>
123
- </head>
124
- <body>
125
- <Header title="Welcome" />
126
- <main>
127
- <p>Hello, Ono!</p>
128
- </main>
129
- </body>
130
- </html>
131
- );
132
- }
133
-
134
- function Header({ title }) {
135
- return (
136
- <header>
137
- <h1>{title}</h1>
138
- </header>
139
- );
15
+ return <h1>Hello, Ono!</h1>;
140
16
  }
141
17
  ```
142
18
 
143
- Build it:
144
-
145
19
  ```bash
146
- ono build example/index.jsx
147
- ```
148
-
149
- This generates `dist/index.html`:
150
-
151
- ```html
152
- <!DOCTYPE html>
153
- <html>
154
- <head>
155
- <title>My Site</title>
156
- </head>
157
- <body>
158
- <header>
159
- <h1>Welcome</h1>
160
- </header>
161
- <main>
162
- <p>Hello, Ono!</p>
163
- </main>
164
- </body>
165
- </html>
166
- ```
167
-
168
- ### UnoCSS Integration
169
-
170
- Ono automatically integrates with UnoCSS. Simply use utility classes in your JSX:
171
-
172
- ```jsx
173
- export default function App() {
174
- return (
175
- <div class="max-w-800px mx-auto p-8">
176
- <h1 class="text-3xl font-bold text-blue-600">
177
- Welcome to Ono
178
- </h1>
179
- <p class="mt-4 text-gray-700">
180
- UnoCSS utilities are automatically generated!
181
- </p>
182
- </div>
183
- );
184
- }
20
+ npx ono build index.jsx
21
+ npx ono dev index.jsx
185
22
  ```
186
23
 
187
- The CSS will be automatically generated to `dist/uno.css` and included in your HTML.
188
-
189
- ### Custom UnoCSS Configuration
24
+ ## 機能
190
25
 
191
- Create a `uno.config.js` file in your project root:
26
+ - JSXから静的HTMLへの変換
27
+ - ライブリロード付き開発サーバー
28
+ - UnoCSS統合
29
+ - コンテンツコレクション(Markdown)
30
+ - 動的ルート(`[slug].jsx`)
192
31
 
193
- ```javascript
194
- import { presetUno } from "unocss";
195
-
196
- export default {
197
- presets: [presetUno()],
198
- theme: {
199
- colors: {
200
- primary: "#0070f3",
201
- },
202
- },
203
- shortcuts: {
204
- "btn": "px-4 py-2 rounded bg-primary text-white",
205
- },
206
- };
207
- ```
32
+ ## 制限事項
208
33
 
209
- ## Configuration
210
-
211
- ### TypeScript/JSX Setup
212
-
213
- Add to your `tsconfig.json`:
214
-
215
- ```json
216
- {
217
- "compilerOptions": {
218
- "jsx": "react-jsx",
219
- "jsxImportSource": "@hashrock/ono"
220
- }
221
- }
222
- ```
223
-
224
- Or use JSDoc comments in your `.jsx` files:
34
+ - **React Fragmentは非対応**: `<>...</>` や `<React.Fragment>` はサポートされていません。代わりに配列や親要素でラップしてください。
225
35
 
226
36
  ```jsx
227
- /** @jsxImportSource @hashrock/ono */
228
- ```
229
-
230
- ## Development
231
-
232
- ### Install Dependencies
233
-
234
- ```bash
235
- npm install
236
- ```
237
-
238
- ### Run Tests
239
-
240
- Run all unit tests:
241
-
242
- ```bash
243
- npm test
244
- ```
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
-
264
- ### Watch Mode
265
-
266
- ```bash
267
- npm run test:watch
268
- ```
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
-
291
- ### Link for Local Development
292
-
293
- ```bash
294
- npm link
295
- ```
296
-
297
- This allows you to use the `ono` command globally while developing.
298
-
299
- ## API
300
-
301
- ### JSX Runtime
302
-
303
- ```javascript
304
- import { createElement } from '@hashrock/ono/jsx-runtime';
305
- ```
306
-
307
- The JSX runtime automatically handles JSX transformation when using TypeScript or Babel.
308
-
309
- ### Renderer
310
-
311
- ```javascript
312
- import { renderToString } from '@hashrock/ono';
313
-
314
- const html = renderToString(<div>Hello</div>);
315
- ```
316
-
317
- ### Bundler
318
-
319
- ```javascript
320
- import { bundle } from '@hashrock/ono';
321
-
322
- const code = await bundle('./path/to/file.jsx');
323
- ```
324
-
325
- ## Pages Mode
326
-
327
- Ono supports building multi-page sites by placing JSX files in a `pages/` directory:
328
-
329
- ```
330
- pages/
331
- ├── index.jsx → dist/index.html
332
- ├── about.jsx → dist/about.html
333
- └── blog/
334
- └── first-post.jsx → dist/blog/first-post.html
335
- ```
336
-
337
- Each file should export a default function that returns a complete HTML document. Use the Layout component pattern for shared structure across pages.
338
-
339
- ## License
340
-
341
- MIT
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.2",
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
+ }