@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.
@@ -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;
@@ -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
+ '<': '&lt;',
18
+ '>': '&gt;',
19
+ '"': '&quot;',
20
+ "'": '&#039;'
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
+ }
@@ -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
+ }