html-component-engine 0.1.2 → 0.1.3

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.
@@ -1,312 +1,317 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import { pathToFileURL } from 'url';
4
- import { parseComponentTag, parseVariants, parseSelfClosingComponentTag } from './utils.js';
5
-
6
- /**
7
- * Compile HTML by resolving all components
8
- * @param {string} html - The HTML content to compile
9
- * @param {string} root - The root directory (pages directory)
10
- * @param {string} projectRoot - The project root directory
11
- * @returns {Promise<string>} - Compiled HTML
12
- */
13
- export async function compileHtml(html, root, projectRoot = null) {
14
- const effectiveProjectRoot = projectRoot || path.dirname(root);
15
- let result = html;
16
-
17
- // First, process components with children: <Component name="...">children</Component>
18
- result = await processComponentsWithChildren(result, root, effectiveProjectRoot);
19
-
20
- // Then, process self-closing components: <Component src="..." />
21
- result = await processSelfClosingComponents(result, root, effectiveProjectRoot);
22
-
23
- return result;
24
- }
25
-
26
- /**
27
- * Process components with children (slot-based)
28
- * Matches: <Component name="ComponentName">...children...</Component>
29
- */
30
- async function processComponentsWithChildren(html, root, projectRoot) {
31
- // Regex to match <Component name="...">...</Component>
32
- const componentRegex = /<Component\s+name="([^"]+)"([^>]*)>([\s\S]*?)<\/Component>/g;
33
-
34
- let result = html;
35
- let matches = [...html.matchAll(componentRegex)];
36
-
37
- for (const match of matches) {
38
- const fullTag = match[0];
39
- const componentName = match[1];
40
- const attrsStr = match[2];
41
- const children = match[3].trim();
42
-
43
- // Parse additional attributes
44
- const attrs = parseAttributes(attrsStr);
45
-
46
- // Load component
47
- let componentContent = await loadComponent(componentName, root, projectRoot, attrs);
48
-
49
- if (componentContent === null) {
50
- console.error(`Component "${componentName}" not found`);
51
- result = result.replace(fullTag, `<!-- Component "${componentName}" not found -->`);
52
- continue;
53
- }
54
-
55
- // Replace {{ children }} placeholder with actual children content
56
- componentContent = componentContent.replace(/\{\{\s*children\s*\}\}/g, children);
57
-
58
- // Replace props with {{key}} placeholders
59
- for (const [key, value] of Object.entries(attrs)) {
60
- componentContent = componentContent.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'), value);
61
- }
62
-
63
- // Recursively compile nested components
64
- const compiledComponent = await compileHtml(componentContent, root, projectRoot);
65
- result = result.replace(fullTag, compiledComponent);
66
- }
67
-
68
- return result;
69
- }
70
-
71
- /**
72
- * Process self-closing components
73
- * Matches: <Component src="..." />
74
- */
75
- async function processSelfClosingComponents(html, root, projectRoot) {
76
- const componentRegex = /<Component[^>]+\/>/g;
77
-
78
- let result = html;
79
- const matches = [...html.matchAll(componentRegex)];
80
-
81
- for (const match of matches) {
82
- const tag = match[0];
83
- const attrs = parseSelfClosingComponentTag(tag);
84
- if (!attrs || !attrs.src) continue;
85
-
86
- const name = attrs.src;
87
- let componentContent = await loadComponent(name, root, projectRoot, attrs);
88
-
89
- if (componentContent === null) {
90
- console.error(`Component "${name}" not found`);
91
- result = result.replace(tag, `<!-- Component "${name}" not found -->`);
92
- continue;
93
- }
94
-
95
- // Parse variants (only for HTML content)
96
- const variants = parseVariants(componentContent);
97
-
98
- // Replace props with {{key}} placeholders
99
- for (const [key, value] of Object.entries(attrs)) {
100
- if (key === 'variant' && variants[value]) {
101
- componentContent = componentContent.replace(/\{\{\s*variantClasses\s*\}\}/g, variants[value]);
102
- } else if (key !== 'src' && key !== 'variant') {
103
- componentContent = componentContent.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'), value);
104
- }
105
- }
106
-
107
- // If no variant specified, replace {{variantClasses}} with empty
108
- componentContent = componentContent.replace(/\{\{\s*variantClasses\s*\}\}/g, '');
109
-
110
- // Recursively compile nested components
111
- const compiledComponent = await compileHtml(componentContent, root, projectRoot);
112
- result = result.replace(tag, compiledComponent);
113
- }
114
-
115
- return result;
116
- }
117
-
118
- /**
119
- * Load a component by name
120
- * @param {string} name - Component name (e.g., "Card" or "main/Button")
121
- * @param {string} root - The pages root directory
122
- * @param {string} projectRoot - The project root directory
123
- * @param {object} attrs - Component attributes/props
124
- * @returns {Promise<string|null>} - Component content or null if not found
125
- */
126
- async function loadComponent(name, root, projectRoot, attrs = {}) {
127
- // Normalize name for path construction (handle both / and \)
128
- const normalizedName = name.replace(/\\/g, '/');
129
-
130
- // root = srcRoot (e.g., example/src)
131
- // projectRoot = project root (e.g., example)
132
- // Components are in srcRoot/components
133
- const possiblePaths = [
134
- path.join(root, 'components', `${normalizedName}.html`), // srcRoot/components/
135
- path.join(projectRoot, 'src', 'components', `${normalizedName}.html`), // projectRoot/src/components/
136
- path.join(projectRoot, 'components', `${normalizedName}.html`), // projectRoot/components/
137
- ];
138
-
139
- for (const componentPath of possiblePaths) {
140
- try {
141
- await fs.access(componentPath); // Check if file exists first
142
- const content = await fs.readFile(componentPath, 'utf8');
143
- return content;
144
- } catch {
145
- // Try .js file at the same location
146
- const jsPath = componentPath.replace('.html', '.js');
147
- try {
148
- const componentModule = await import(pathToFileURL(jsPath));
149
- const componentExport = componentModule.default || componentModule;
150
-
151
- if (typeof componentExport === 'function') {
152
- const props = { ...attrs };
153
- delete props.src;
154
- delete props.name;
155
- return componentExport(props);
156
- } else {
157
- return String(componentExport);
158
- }
159
- } catch {
160
- continue;
161
- }
162
- }
163
- }
164
-
165
- return null;
166
- }
167
-
168
- /**
169
- * Parse attributes from an attribute string
170
- * @param {string} attrsStr - Attribute string like ' class="foo" id="bar"'
171
- * @returns {object} - Object with attribute key-value pairs
172
- */
173
- function parseAttributes(attrsStr) {
174
- const attrs = {};
175
- const attrRegex = /(\w+)="([^"]*)"/g;
176
- let attrMatch;
177
- while ((attrMatch = attrRegex.exec(attrsStr)) !== null) {
178
- attrs[attrMatch[1]] = attrMatch[2];
179
- }
180
- return attrs;
181
- }
182
-
183
- /**
184
- * Inline CSS from <link> tags into <style> tags
185
- * @param {string} html - The HTML content
186
- * @param {string} root - The src root directory
187
- * @param {string} projectRoot - The project root directory
188
- * @returns {Promise<string>} - HTML with inlined CSS
189
- */
190
- export async function inlineCss(html, root, projectRoot) {
191
- const linkRegex = /<link[^>]+rel=["']stylesheet["'][^>]*>/gi;
192
- const matches = [...html.matchAll(linkRegex)];
193
-
194
- let result = html;
195
-
196
- for (const match of matches) {
197
- const linkTag = match[0];
198
- const hrefMatch = linkTag.match(/href=["']([^"']+)["']/);
199
-
200
- if (!hrefMatch) continue;
201
-
202
- let href = hrefMatch[1];
203
-
204
- // Skip external URLs
205
- if (href.startsWith('http://') || href.startsWith('https://')) {
206
- continue;
207
- }
208
-
209
- // Resolve the CSS file path - try multiple locations
210
- let cssPath = null;
211
- let cssContent = null;
212
-
213
- // Possible paths to check
214
- const pathsToTry = [];
215
-
216
- if (href.startsWith('/')) {
217
- const cleanHref = href.slice(1);
218
- // /styles/styles.css -> src/assets/styles/styles.css (publicDir pattern)
219
- pathsToTry.push(path.join(root, 'assets', cleanHref));
220
- // /assets/styles/styles.css -> src/assets/styles/styles.css
221
- pathsToTry.push(path.join(root, cleanHref));
222
- // Try project root
223
- pathsToTry.push(path.join(projectRoot, 'src', 'assets', cleanHref));
224
- } else {
225
- // Relative path
226
- pathsToTry.push(path.join(root, href));
227
- }
228
-
229
- for (const tryPath of pathsToTry) {
230
- if (await fileExists(tryPath)) {
231
- cssPath = tryPath;
232
- break;
233
- }
234
- }
235
-
236
- if (cssPath) {
237
- try {
238
- cssContent = await fs.readFile(cssPath, 'utf8');
239
- const styleTag = `<style>\n${cssContent}\n</style>`;
240
- result = result.replace(linkTag, styleTag);
241
- } catch (error) {
242
- console.warn(`Could not inline CSS from ${href}: ${error.message}`);
243
- }
244
- } else {
245
- console.warn(`Could not find CSS file for ${href}`);
246
- }
247
- }
248
-
249
- return result;
250
- }
251
-
252
- /**
253
- * Inline JS from <script src="..."> tags into inline <script> tags
254
- * @param {string} html - The HTML content
255
- * @param {string} root - The pages root directory
256
- * @param {string} projectRoot - The project root directory
257
- * @returns {Promise<string>} - HTML with inlined JS
258
- */
259
- export async function inlineJs(html, root, projectRoot) {
260
- const scriptRegex = /<script[^>]+src=["']([^"']+)["'][^>]*><\/script>/gi;
261
- const matches = [...html.matchAll(scriptRegex)];
262
-
263
- let result = html;
264
-
265
- for (const match of matches) {
266
- const scriptTag = match[0];
267
- let src = match[1];
268
-
269
- // Skip external URLs and Vite client
270
- if (src.startsWith('http://') || src.startsWith('https://') || src.includes('@vite')) {
271
- continue;
272
- }
273
-
274
- // Resolve the JS file path
275
- let jsPath;
276
- if (src.startsWith('/')) {
277
- jsPath = path.join(projectRoot, 'src', 'assets', src.slice(1));
278
- if (!await fileExists(jsPath)) {
279
- jsPath = path.join(root, 'assets', src.slice(1));
280
- }
281
- if (!await fileExists(jsPath)) {
282
- jsPath = path.join(root, src.slice(1));
283
- }
284
- } else {
285
- jsPath = path.join(root, src);
286
- }
287
-
288
- try {
289
- const jsContent = await fs.readFile(jsPath, 'utf8');
290
- const inlineScriptTag = `<script>\n${jsContent}\n</script>`;
291
- result = result.replace(scriptTag, inlineScriptTag);
292
- } catch (error) {
293
- console.warn(`Could not inline JS from ${src}: ${error.message}`);
294
- }
295
- }
296
-
297
- return result;
298
- }
299
-
300
- /**
301
- * Check if a file exists
302
- * @param {string} filePath - Path to check
303
- * @returns {Promise<boolean>}
304
- */
305
- async function fileExists(filePath) {
306
- try {
307
- await fs.access(filePath);
308
- return true;
309
- } catch {
310
- return false;
311
- }
312
- }
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { pathToFileURL } from 'url';
4
+ import { parseComponentTag, parseVariants, parseSelfClosingComponentTag } from './utils.js';
5
+
6
+ /**
7
+ * Compile HTML by resolving all components
8
+ * @param {string} html - The HTML content to compile
9
+ * @param {string} root - The root directory (pages directory)
10
+ * @param {string} projectRoot - The project root directory
11
+ * @returns {Promise<string>} - Compiled HTML
12
+ */
13
+ export async function compileHtml(html, root, projectRoot = null) {
14
+ const effectiveProjectRoot = projectRoot || path.dirname(root);
15
+ let result = html;
16
+
17
+ // First, process components with children: <Component name="...">children</Component>
18
+ result = await processComponentsWithChildren(result, root, effectiveProjectRoot);
19
+
20
+ // Then, process self-closing components: <Component src="..." />
21
+ result = await processSelfClosingComponents(result, root, effectiveProjectRoot);
22
+
23
+ return result;
24
+ }
25
+
26
+ /**
27
+ * Process components with children (slot-based)
28
+ * Matches: <Component src="...">...children...</Component>
29
+ * Also supports legacy: <Component name="...">...children...</Component>
30
+ */
31
+ async function processComponentsWithChildren(html, root, projectRoot) {
32
+ // Regex to match non-self-closing <Component ...>...</Component>
33
+ const componentRegex = /<Component\b([^>]*)>([\s\S]*?)<\/Component>/g;
34
+
35
+ let result = html;
36
+ let matches = [...html.matchAll(componentRegex)];
37
+
38
+ for (const match of matches) {
39
+ const fullTag = match[0];
40
+ const attrsStr = match[1];
41
+ const children = match[2];
42
+
43
+ // Parse additional attributes
44
+ const attrs = parseAttributes(attrsStr);
45
+ const componentName = attrs.src || attrs.name;
46
+
47
+ if (!componentName) {
48
+ continue;
49
+ }
50
+
51
+ // Load component
52
+ let componentContent = await loadComponent(componentName, root, projectRoot, attrs);
53
+
54
+ if (componentContent === null) {
55
+ console.error(`Component "${componentName}" not found`);
56
+ result = result.replace(fullTag, `<!-- Component "${componentName}" not found -->`);
57
+ continue;
58
+ }
59
+
60
+ // Replace {{ children }} placeholder with actual children content
61
+ componentContent = componentContent.replace(/\{\{\s*children\s*\}\}/g, children);
62
+
63
+ // Replace props with {{key}} placeholders
64
+ for (const [key, value] of Object.entries(attrs)) {
65
+ componentContent = componentContent.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'), value);
66
+ }
67
+
68
+ // Recursively compile nested components
69
+ const compiledComponent = await compileHtml(componentContent, root, projectRoot);
70
+ result = result.replace(fullTag, compiledComponent);
71
+ }
72
+
73
+ return result;
74
+ }
75
+
76
+ /**
77
+ * Process self-closing components
78
+ * Matches: <Component src="..." />
79
+ */
80
+ async function processSelfClosingComponents(html, root, projectRoot) {
81
+ const componentRegex = /<Component[^>]+\/>/g;
82
+
83
+ let result = html;
84
+ const matches = [...html.matchAll(componentRegex)];
85
+
86
+ for (const match of matches) {
87
+ const tag = match[0];
88
+ const attrs = parseSelfClosingComponentTag(tag);
89
+ if (!attrs || !attrs.src) continue;
90
+
91
+ const name = attrs.src;
92
+ let componentContent = await loadComponent(name, root, projectRoot, attrs);
93
+
94
+ if (componentContent === null) {
95
+ console.error(`Component "${name}" not found`);
96
+ result = result.replace(tag, `<!-- Component "${name}" not found -->`);
97
+ continue;
98
+ }
99
+
100
+ // Parse variants (only for HTML content)
101
+ const variants = parseVariants(componentContent);
102
+
103
+ // Replace props with {{key}} placeholders
104
+ for (const [key, value] of Object.entries(attrs)) {
105
+ if (key === 'variant' && variants[value]) {
106
+ componentContent = componentContent.replace(/\{\{\s*variantClasses\s*\}\}/g, variants[value]);
107
+ } else if (key !== 'src' && key !== 'variant') {
108
+ componentContent = componentContent.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'), value);
109
+ }
110
+ }
111
+
112
+ // If no variant specified, replace {{variantClasses}} with empty
113
+ componentContent = componentContent.replace(/\{\{\s*variantClasses\s*\}\}/g, '');
114
+
115
+ // Recursively compile nested components
116
+ const compiledComponent = await compileHtml(componentContent, root, projectRoot);
117
+ result = result.replace(tag, compiledComponent);
118
+ }
119
+
120
+ return result;
121
+ }
122
+
123
+ /**
124
+ * Load a component by name
125
+ * @param {string} name - Component name (e.g., "Card" or "main/Button")
126
+ * @param {string} root - The pages root directory
127
+ * @param {string} projectRoot - The project root directory
128
+ * @param {object} attrs - Component attributes/props
129
+ * @returns {Promise<string|null>} - Component content or null if not found
130
+ */
131
+ async function loadComponent(name, root, projectRoot, attrs = {}) {
132
+ // Normalize name for path construction (handle both / and \)
133
+ const normalizedName = name.replace(/\\/g, '/');
134
+
135
+ // root = srcRoot (e.g., example/src)
136
+ // projectRoot = project root (e.g., example)
137
+ // Components are in srcRoot/components
138
+ const possiblePaths = [
139
+ path.join(root, 'components', `${normalizedName}.html`), // srcRoot/components/
140
+ path.join(projectRoot, 'src', 'components', `${normalizedName}.html`), // projectRoot/src/components/
141
+ path.join(projectRoot, 'components', `${normalizedName}.html`), // projectRoot/components/
142
+ ];
143
+
144
+ for (const componentPath of possiblePaths) {
145
+ try {
146
+ await fs.access(componentPath); // Check if file exists first
147
+ const content = await fs.readFile(componentPath, 'utf8');
148
+ return content;
149
+ } catch {
150
+ // Try .js file at the same location
151
+ const jsPath = componentPath.replace('.html', '.js');
152
+ try {
153
+ const componentModule = await import(pathToFileURL(jsPath));
154
+ const componentExport = componentModule.default || componentModule;
155
+
156
+ if (typeof componentExport === 'function') {
157
+ const props = { ...attrs };
158
+ delete props.src;
159
+ delete props.name;
160
+ return componentExport(props);
161
+ } else {
162
+ return String(componentExport);
163
+ }
164
+ } catch {
165
+ continue;
166
+ }
167
+ }
168
+ }
169
+
170
+ return null;
171
+ }
172
+
173
+ /**
174
+ * Parse attributes from an attribute string
175
+ * @param {string} attrsStr - Attribute string like ' class="foo" id="bar"'
176
+ * @returns {object} - Object with attribute key-value pairs
177
+ */
178
+ function parseAttributes(attrsStr) {
179
+ const attrs = {};
180
+ const attrRegex = /([\w-]+)=["']([^"']*)["']/g;
181
+ let attrMatch;
182
+ while ((attrMatch = attrRegex.exec(attrsStr)) !== null) {
183
+ attrs[attrMatch[1]] = attrMatch[2];
184
+ }
185
+ return attrs;
186
+ }
187
+
188
+ /**
189
+ * Inline CSS from <link> tags into <style> tags
190
+ * @param {string} html - The HTML content
191
+ * @param {string} root - The src root directory
192
+ * @param {string} projectRoot - The project root directory
193
+ * @returns {Promise<string>} - HTML with inlined CSS
194
+ */
195
+ export async function inlineCss(html, root, projectRoot) {
196
+ const linkRegex = /<link[^>]+rel=["']stylesheet["'][^>]*>/gi;
197
+ const matches = [...html.matchAll(linkRegex)];
198
+
199
+ let result = html;
200
+
201
+ for (const match of matches) {
202
+ const linkTag = match[0];
203
+ const hrefMatch = linkTag.match(/href=["']([^"']+)["']/);
204
+
205
+ if (!hrefMatch) continue;
206
+
207
+ let href = hrefMatch[1];
208
+
209
+ // Skip external URLs
210
+ if (href.startsWith('http://') || href.startsWith('https://')) {
211
+ continue;
212
+ }
213
+
214
+ // Resolve the CSS file path - try multiple locations
215
+ let cssPath = null;
216
+ let cssContent = null;
217
+
218
+ // Possible paths to check
219
+ const pathsToTry = [];
220
+
221
+ if (href.startsWith('/')) {
222
+ const cleanHref = href.slice(1);
223
+ // /styles/styles.css -> src/assets/styles/styles.css (publicDir pattern)
224
+ pathsToTry.push(path.join(root, 'assets', cleanHref));
225
+ // /assets/styles/styles.css -> src/assets/styles/styles.css
226
+ pathsToTry.push(path.join(root, cleanHref));
227
+ // Try project root
228
+ pathsToTry.push(path.join(projectRoot, 'src', 'assets', cleanHref));
229
+ } else {
230
+ // Relative path
231
+ pathsToTry.push(path.join(root, href));
232
+ }
233
+
234
+ for (const tryPath of pathsToTry) {
235
+ if (await fileExists(tryPath)) {
236
+ cssPath = tryPath;
237
+ break;
238
+ }
239
+ }
240
+
241
+ if (cssPath) {
242
+ try {
243
+ cssContent = await fs.readFile(cssPath, 'utf8');
244
+ const styleTag = `<style>\n${cssContent}\n</style>`;
245
+ result = result.replace(linkTag, styleTag);
246
+ } catch (error) {
247
+ console.warn(`Could not inline CSS from ${href}: ${error.message}`);
248
+ }
249
+ } else {
250
+ console.warn(`Could not find CSS file for ${href}`);
251
+ }
252
+ }
253
+
254
+ return result;
255
+ }
256
+
257
+ /**
258
+ * Inline JS from <script src="..."> tags into inline <script> tags
259
+ * @param {string} html - The HTML content
260
+ * @param {string} root - The pages root directory
261
+ * @param {string} projectRoot - The project root directory
262
+ * @returns {Promise<string>} - HTML with inlined JS
263
+ */
264
+ export async function inlineJs(html, root, projectRoot) {
265
+ const scriptRegex = /<script[^>]+src=["']([^"']+)["'][^>]*><\/script>/gi;
266
+ const matches = [...html.matchAll(scriptRegex)];
267
+
268
+ let result = html;
269
+
270
+ for (const match of matches) {
271
+ const scriptTag = match[0];
272
+ let src = match[1];
273
+
274
+ // Skip external URLs and Vite client
275
+ if (src.startsWith('http://') || src.startsWith('https://') || src.includes('@vite')) {
276
+ continue;
277
+ }
278
+
279
+ // Resolve the JS file path
280
+ let jsPath;
281
+ if (src.startsWith('/')) {
282
+ jsPath = path.join(projectRoot, 'src', 'assets', src.slice(1));
283
+ if (!await fileExists(jsPath)) {
284
+ jsPath = path.join(root, 'assets', src.slice(1));
285
+ }
286
+ if (!await fileExists(jsPath)) {
287
+ jsPath = path.join(root, src.slice(1));
288
+ }
289
+ } else {
290
+ jsPath = path.join(root, src);
291
+ }
292
+
293
+ try {
294
+ const jsContent = await fs.readFile(jsPath, 'utf8');
295
+ const inlineScriptTag = `<script>\n${jsContent}\n</script>`;
296
+ result = result.replace(scriptTag, inlineScriptTag);
297
+ } catch (error) {
298
+ console.warn(`Could not inline JS from ${src}: ${error.message}`);
299
+ }
300
+ }
301
+
302
+ return result;
303
+ }
304
+
305
+ /**
306
+ * Check if a file exists
307
+ * @param {string} filePath - Path to check
308
+ * @returns {Promise<boolean>}
309
+ */
310
+ async function fileExists(filePath) {
311
+ try {
312
+ await fs.access(filePath);
313
+ return true;
314
+ } catch {
315
+ return false;
316
+ }
317
+ }