@hashrock/ono 0.1.1 → 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 CHANGED
@@ -1,6 +1,17 @@
1
1
  # Ono
2
2
 
3
- A lightweight JSX library for static site generation with UnoCSS support.
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
- Initialize a new project:
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
- ### CLI Commands
52
-
53
- **Init** - Initialize a new Ono project:
48
+ Or create a simple JSX file and build it:
54
49
 
55
50
  ```bash
56
- ono init
57
- ono init my-project
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
- **Build** - Build a JSX file to HTML:
64
+ # Build it
65
+ npx @hashrock/ono build index.jsx
61
66
 
62
- ```bash
63
- ono build example/index.jsx
67
+ # Or start a dev server
68
+ npx @hashrock/ono dev index.jsx
64
69
  ```
65
70
 
66
- **Build Pages** - Build all pages in a directory:
71
+ ## Usage
67
72
 
68
- ```bash
69
- ono build pages
70
- ```
73
+ ### CLI Commands
71
74
 
72
- **Build with Watch** - Watch for changes and rebuild automatically:
75
+ **Build** - Build JSX files to static HTML:
73
76
 
74
77
  ```bash
75
- ono build pages --watch
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 files to HTML
80
- - Watch for changes in `.jsx` files
81
- - Automatically rebuild when files change
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.1",
4
- "description": "A lightweight JSX library for static site generation with UnoCSS support",
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,9 +51,10 @@
38
51
  "engines": {
39
52
  "node": ">=18.0.0"
40
53
  },
41
- "devDependencies": {},
42
54
  "dependencies": {
43
55
  "@unocss/preset-uno": "^66.5.4",
56
+ "h3": "^2.0.1-rc.5",
57
+ "marked": "^16.4.1",
44
58
  "typescript": "^5.9.3",
45
59
  "unocss": "^66.5.4",
46
60
  "ws": "^8.18.3"
@@ -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
+ }