@hashrock/ono 0.1.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/LICENSE +21 -0
- package/README.md +299 -0
- package/package.json +49 -0
- package/src/bundler.js +73 -0
- package/src/cli.js +1014 -0
- package/src/jsx-runtime.js +45 -0
- package/src/renderer.js +128 -0
- package/src/resolver.js +137 -0
- package/src/transformer.js +48 -0
- package/src/unocss.js +75 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSX Runtime - createElement function
|
|
3
|
+
* Creates a VNode (Virtual Node) from JSX
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Flatten array recursively and filter out falsy values
|
|
8
|
+
*/
|
|
9
|
+
function flattenChildren(children) {
|
|
10
|
+
const result = [];
|
|
11
|
+
|
|
12
|
+
for (const child of children) {
|
|
13
|
+
if (child === null || child === undefined || typeof child === 'boolean') {
|
|
14
|
+
// Skip null, undefined, and boolean values
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (Array.isArray(child)) {
|
|
19
|
+
// Recursively flatten arrays
|
|
20
|
+
result.push(...flattenChildren(child));
|
|
21
|
+
} else {
|
|
22
|
+
result.push(child);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a VNode
|
|
31
|
+
* @param {string|Function} tag - HTML tag name or component function
|
|
32
|
+
* @param {Object} props - Element properties/attributes
|
|
33
|
+
* @param {...any} children - Child elements
|
|
34
|
+
* @returns {Object} VNode object
|
|
35
|
+
*/
|
|
36
|
+
export function createElement(tag, props, ...children) {
|
|
37
|
+
return {
|
|
38
|
+
tag,
|
|
39
|
+
props: props || {},
|
|
40
|
+
children: flattenChildren(children)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Alias for compatibility
|
|
45
|
+
export const h = createElement;
|
package/src/renderer.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer - Convert VNodes to HTML strings
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Self-closing HTML tags
|
|
6
|
+
const SELF_CLOSING_TAGS = new Set([
|
|
7
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
8
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Escape HTML special characters to prevent XSS
|
|
13
|
+
*/
|
|
14
|
+
function escapeHtml(text) {
|
|
15
|
+
const map = {
|
|
16
|
+
'&': '&',
|
|
17
|
+
'<': '<',
|
|
18
|
+
'>': '>',
|
|
19
|
+
'"': '"',
|
|
20
|
+
"'": '''
|
|
21
|
+
};
|
|
22
|
+
return String(text).replace(/[&<>"']/g, (char) => map[char]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert camelCase to kebab-case
|
|
27
|
+
*/
|
|
28
|
+
function camelToKebab(str) {
|
|
29
|
+
return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert style object to CSS string
|
|
34
|
+
*/
|
|
35
|
+
function styleToString(style) {
|
|
36
|
+
if (typeof style === 'string') {
|
|
37
|
+
return style;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Object.entries(style)
|
|
41
|
+
.map(([key, value]) => `${camelToKebab(key)}: ${value}`)
|
|
42
|
+
.join('; ');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render attributes to string
|
|
47
|
+
*/
|
|
48
|
+
function renderAttributes(props) {
|
|
49
|
+
const attributes = [];
|
|
50
|
+
|
|
51
|
+
for (const [key, value] of Object.entries(props)) {
|
|
52
|
+
// Skip special props
|
|
53
|
+
if (key === 'children') continue;
|
|
54
|
+
|
|
55
|
+
// Handle className -> class conversion
|
|
56
|
+
if (key === 'className') {
|
|
57
|
+
attributes.push(`class="${escapeHtml(value)}"`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle style object
|
|
62
|
+
if (key === 'style') {
|
|
63
|
+
const styleStr = styleToString(value);
|
|
64
|
+
attributes.push(`style="${escapeHtml(styleStr)}"`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle boolean attributes
|
|
69
|
+
if (typeof value === 'boolean') {
|
|
70
|
+
if (value) {
|
|
71
|
+
attributes.push(key);
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle regular attributes
|
|
77
|
+
if (value != null) {
|
|
78
|
+
attributes.push(`${key}="${escapeHtml(value)}"`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return attributes.length > 0 ? ' ' + attributes.join(' ') : '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Render a VNode to HTML string
|
|
87
|
+
*/
|
|
88
|
+
export function renderToString(vnode) {
|
|
89
|
+
// Handle primitive values
|
|
90
|
+
if (vnode == null || typeof vnode === 'boolean') {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof vnode === 'string' || typeof vnode === 'number') {
|
|
95
|
+
return escapeHtml(vnode);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle VNode object
|
|
99
|
+
const { tag, props, children } = vnode;
|
|
100
|
+
|
|
101
|
+
// Handle component functions
|
|
102
|
+
if (typeof tag === 'function') {
|
|
103
|
+
// Pass props with children
|
|
104
|
+
const componentProps = { ...props };
|
|
105
|
+
if (children && children.length > 0) {
|
|
106
|
+
componentProps.children = children;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Call component function and render result
|
|
110
|
+
const result = tag(componentProps);
|
|
111
|
+
return renderToString(result);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Handle HTML elements
|
|
115
|
+
const attrs = renderAttributes(props);
|
|
116
|
+
const isSelfClosing = SELF_CLOSING_TAGS.has(tag);
|
|
117
|
+
|
|
118
|
+
if (isSelfClosing) {
|
|
119
|
+
return `<${tag}${attrs} />`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Render children
|
|
123
|
+
const childrenHtml = children
|
|
124
|
+
.map(child => renderToString(child))
|
|
125
|
+
.join('');
|
|
126
|
+
|
|
127
|
+
return `<${tag}${attrs}>${childrenHtml}</${tag}>`;
|
|
128
|
+
}
|
package/src/resolver.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Resolver - Parse imports and resolve dependencies
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse import statements from source code
|
|
10
|
+
* @param {string} code - Source code
|
|
11
|
+
* @returns {Array} Array of import objects with specifier
|
|
12
|
+
*/
|
|
13
|
+
export function parseImports(code) {
|
|
14
|
+
const imports = [];
|
|
15
|
+
|
|
16
|
+
// Match various import patterns:
|
|
17
|
+
// import foo from "bar"
|
|
18
|
+
// import { foo } from "bar"
|
|
19
|
+
// import * as foo from "bar"
|
|
20
|
+
// import "bar"
|
|
21
|
+
const importRegex = /import\s+(?:[\w{},\s*]+\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
22
|
+
|
|
23
|
+
let match;
|
|
24
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
25
|
+
imports.push({
|
|
26
|
+
specifier: match[1]
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return imports;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve import path relative to the importing file
|
|
35
|
+
* @param {string} importPath - Import specifier (e.g., "./Button.jsx")
|
|
36
|
+
* @param {string} fromFile - Absolute path of the file doing the import
|
|
37
|
+
* @returns {string} Absolute path to the imported file
|
|
38
|
+
*/
|
|
39
|
+
export function resolveImportPath(importPath, fromFile) {
|
|
40
|
+
// If it's already an absolute path, return as-is
|
|
41
|
+
if (path.isAbsolute(importPath)) {
|
|
42
|
+
return importPath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// For relative imports, resolve relative to the importing file
|
|
46
|
+
const dir = path.dirname(fromFile);
|
|
47
|
+
return path.resolve(dir, importPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Topological sort for dependency graph
|
|
52
|
+
* @param {Map} graph - Dependency graph (file -> [dependencies])
|
|
53
|
+
* @returns {Array} Sorted array of file paths
|
|
54
|
+
*/
|
|
55
|
+
function topologicalSort(graph) {
|
|
56
|
+
const sorted = [];
|
|
57
|
+
const visited = new Set();
|
|
58
|
+
const visiting = new Set();
|
|
59
|
+
|
|
60
|
+
function visit(node) {
|
|
61
|
+
if (visited.has(node)) return;
|
|
62
|
+
if (visiting.has(node)) {
|
|
63
|
+
throw new Error(`Circular dependency detected: ${node}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
visiting.add(node);
|
|
67
|
+
|
|
68
|
+
const deps = graph.get(node) || [];
|
|
69
|
+
for (const dep of deps) {
|
|
70
|
+
visit(dep);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
visiting.delete(node);
|
|
74
|
+
visited.add(node);
|
|
75
|
+
sorted.push(node);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Visit all nodes
|
|
79
|
+
for (const node of graph.keys()) {
|
|
80
|
+
visit(node);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return sorted;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Collect all dependencies recursively
|
|
88
|
+
* @param {string} entryFile - Absolute path to entry file
|
|
89
|
+
* @returns {Object} Object with modules (Set), graph (Map), and order (Array)
|
|
90
|
+
*/
|
|
91
|
+
export async function collectDependencies(entryFile) {
|
|
92
|
+
const modules = new Set();
|
|
93
|
+
const graph = new Map();
|
|
94
|
+
const queue = [entryFile];
|
|
95
|
+
|
|
96
|
+
// BFS to collect all dependencies
|
|
97
|
+
while (queue.length > 0) {
|
|
98
|
+
const currentFile = queue.shift();
|
|
99
|
+
|
|
100
|
+
// Skip if already processed
|
|
101
|
+
if (modules.has(currentFile)) continue;
|
|
102
|
+
|
|
103
|
+
// Read the file
|
|
104
|
+
let source;
|
|
105
|
+
try {
|
|
106
|
+
source = await fs.readFile(currentFile, "utf-8");
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new Error(`Cannot read file: ${currentFile}\n${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Parse imports
|
|
112
|
+
const imports = parseImports(source);
|
|
113
|
+
const dependencies = [];
|
|
114
|
+
|
|
115
|
+
for (const imp of imports) {
|
|
116
|
+
// Only process relative imports (skip node_modules, etc.)
|
|
117
|
+
if (imp.specifier.startsWith(".")) {
|
|
118
|
+
const resolvedPath = resolveImportPath(imp.specifier, currentFile);
|
|
119
|
+
dependencies.push(resolvedPath);
|
|
120
|
+
queue.push(resolvedPath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add to modules and graph
|
|
125
|
+
modules.add(currentFile);
|
|
126
|
+
graph.set(currentFile, dependencies);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Sort dependencies topologically
|
|
130
|
+
const order = topologicalSort(graph);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
modules,
|
|
134
|
+
graph,
|
|
135
|
+
order
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transformer - Convert JSX to JavaScript using TypeScript compiler
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Transform JSX code to JavaScript
|
|
9
|
+
* @param {string} source - JSX source code
|
|
10
|
+
* @param {string} [filename='input.jsx'] - Optional filename for better error messages
|
|
11
|
+
* @returns {string} Transformed JavaScript code
|
|
12
|
+
*/
|
|
13
|
+
export function transformJSX(source, filename = 'input.jsx') {
|
|
14
|
+
// TypeScript compiler options
|
|
15
|
+
const compilerOptions = {
|
|
16
|
+
jsx: ts.JsxEmit.React,
|
|
17
|
+
jsxFactory: 'h',
|
|
18
|
+
module: ts.ModuleKind.ESNext,
|
|
19
|
+
target: ts.ScriptTarget.ESNext,
|
|
20
|
+
esModuleInterop: true,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Transpile the code
|
|
24
|
+
const result = ts.transpileModule(source, {
|
|
25
|
+
compilerOptions,
|
|
26
|
+
fileName: filename,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return result.outputText;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Transform JSX file and add necessary imports if not present
|
|
34
|
+
* @param {string} source - JSX source code
|
|
35
|
+
* @param {string} [filename='input.jsx'] - Optional filename
|
|
36
|
+
* @returns {string} Transformed JavaScript with imports
|
|
37
|
+
*/
|
|
38
|
+
export function transformJSXWithImports(source, filename = 'input.jsx') {
|
|
39
|
+
let transformedCode = transformJSX(source, filename);
|
|
40
|
+
|
|
41
|
+
// Check if the code uses 'h' function (JSX was transformed)
|
|
42
|
+
if (transformedCode.includes('h(') && !source.includes('import') && !source.includes('from')) {
|
|
43
|
+
// Add import statement for h function
|
|
44
|
+
transformedCode = `import { h } from './jsx-runtime.js';\n${transformedCode}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return transformedCode;
|
|
48
|
+
}
|
package/src/unocss.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UnoCSS Integration for Mini JSX
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createGenerator, presetUno } from "unocss";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create UnoCSS generator with default config
|
|
11
|
+
* @param {object} userConfig - User configuration
|
|
12
|
+
* @returns {Promise<object>} UnoCSS generator instance
|
|
13
|
+
*/
|
|
14
|
+
export async function createUnoGenerator(userConfig = {}) {
|
|
15
|
+
return await createGenerator({
|
|
16
|
+
presets: [presetUno()],
|
|
17
|
+
...userConfig,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load UnoCSS config from file
|
|
23
|
+
* @param {string} configPath - Path to config file
|
|
24
|
+
* @returns {Promise<object>} Configuration object
|
|
25
|
+
*/
|
|
26
|
+
export async function loadUnoConfig(configPath) {
|
|
27
|
+
try {
|
|
28
|
+
const configUrl = `file://${path.resolve(configPath)}?t=${Date.now()}`;
|
|
29
|
+
const module = await import(configUrl);
|
|
30
|
+
return module.default || module;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// Config file doesn't exist, return empty config
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate CSS from HTML content
|
|
39
|
+
* @param {string} html - HTML content to scan for classes
|
|
40
|
+
* @param {object} config - UnoCSS configuration
|
|
41
|
+
* @returns {Promise<string>} Generated CSS
|
|
42
|
+
*/
|
|
43
|
+
export async function generateCSS(html, config = {}) {
|
|
44
|
+
const uno = await createUnoGenerator(config);
|
|
45
|
+
const { css } = await uno.generate(html);
|
|
46
|
+
return css;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract and generate UnoCSS for multiple HTML files
|
|
51
|
+
* @param {string[]} htmlFiles - Array of HTML file paths
|
|
52
|
+
* @param {object} config - UnoCSS configuration
|
|
53
|
+
* @returns {Promise<string>} Combined generated CSS
|
|
54
|
+
*/
|
|
55
|
+
export async function generateCSSFromFiles(htmlFiles, config = {}) {
|
|
56
|
+
const uno = await createUnoGenerator(config);
|
|
57
|
+
|
|
58
|
+
// Read all HTML files
|
|
59
|
+
const htmlContents = await Promise.all(
|
|
60
|
+
htmlFiles.map(async (file) => {
|
|
61
|
+
try {
|
|
62
|
+
return await fs.readFile(file, "utf-8");
|
|
63
|
+
} catch {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Combine all HTML content
|
|
70
|
+
const combinedHTML = htmlContents.join("\n");
|
|
71
|
+
|
|
72
|
+
// Generate CSS
|
|
73
|
+
const { css } = await uno.generate(combinedHTML);
|
|
74
|
+
return css;
|
|
75
|
+
}
|