html-component-engine 0.1.2 → 0.1.4
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 +36 -36
- package/bin/cli.js +529 -529
- package/package.json +37 -37
- package/src/engine/compiler.js +403 -312
- package/src/engine/utils.js +76 -74
package/src/engine/compiler.js
CHANGED
|
@@ -1,312 +1,403 @@
|
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* @param {string}
|
|
256
|
-
* @
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 ONLY non-self-closing <Component ...>...</Component>
|
|
33
|
+
// Prevents accidentally matching <Component ... /> as an opening tag.
|
|
34
|
+
const componentRegex = /<Component\b(?![^>]*\/\s*>)([^>]*)>([\s\S]*?)<\/Component>/g;
|
|
35
|
+
|
|
36
|
+
let result = html;
|
|
37
|
+
let matches = [...html.matchAll(componentRegex)];
|
|
38
|
+
|
|
39
|
+
for (const match of matches) {
|
|
40
|
+
const fullTag = match[0];
|
|
41
|
+
const attrsStr = match[1];
|
|
42
|
+
const children = match[2];
|
|
43
|
+
|
|
44
|
+
// Parse additional attributes
|
|
45
|
+
const attrs = parseAttributes(attrsStr);
|
|
46
|
+
const componentName = attrs.src || attrs.name;
|
|
47
|
+
|
|
48
|
+
if (!componentName) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Load component
|
|
53
|
+
let componentContent = await loadComponent(componentName, root, projectRoot, attrs);
|
|
54
|
+
|
|
55
|
+
if (componentContent === null) {
|
|
56
|
+
console.error(`Component "${componentName}" not found`);
|
|
57
|
+
result = result.replace(fullTag, `<!-- Component "${componentName}" not found -->`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Replace children and named slots
|
|
62
|
+
const { defaultChildren, namedSlots } = extractSlots(children);
|
|
63
|
+
componentContent = componentContent.replace(/\{\{\s*children\s*\}\}/g, defaultChildren);
|
|
64
|
+
for (const [slotName, slotContent] of Object.entries(namedSlots)) {
|
|
65
|
+
componentContent = componentContent.replace(
|
|
66
|
+
new RegExp(`\\{\\{\\s*slot:${slotName}\\s*\\}\\}`, 'g'),
|
|
67
|
+
slotContent,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Replace props with {{key}} placeholders
|
|
72
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
73
|
+
componentContent = componentContent.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'), value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Recursively compile nested components
|
|
77
|
+
const compiledComponent = await compileHtml(componentContent, root, projectRoot);
|
|
78
|
+
result = result.replace(fullTag, compiledComponent);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Process self-closing components
|
|
86
|
+
* Matches: <Component src="..." />
|
|
87
|
+
*/
|
|
88
|
+
async function processSelfClosingComponents(html, root, projectRoot) {
|
|
89
|
+
const componentRegex = /<Component[^>]+\/>/g;
|
|
90
|
+
|
|
91
|
+
let result = html;
|
|
92
|
+
const matches = [...html.matchAll(componentRegex)];
|
|
93
|
+
|
|
94
|
+
for (const match of matches) {
|
|
95
|
+
const tag = match[0];
|
|
96
|
+
const attrs = parseSelfClosingComponentTag(tag);
|
|
97
|
+
if (!attrs) continue;
|
|
98
|
+
|
|
99
|
+
const name = attrs.src || attrs.name;
|
|
100
|
+
if (!name) continue;
|
|
101
|
+
let componentContent = await loadComponent(name, root, projectRoot, attrs);
|
|
102
|
+
|
|
103
|
+
if (componentContent === null) {
|
|
104
|
+
console.error(`Component "${name}" not found`);
|
|
105
|
+
result = result.replace(tag, `<!-- Component "${name}" not found -->`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Parse variants (only for HTML content)
|
|
110
|
+
const variants = parseVariants(componentContent);
|
|
111
|
+
|
|
112
|
+
// Replace props with {{key}} placeholders
|
|
113
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
114
|
+
if (key === 'variant' && variants[value]) {
|
|
115
|
+
componentContent = componentContent.replace(/\{\{\s*variantClasses\s*\}\}/g, variants[value]);
|
|
116
|
+
} else if (key !== 'src' && key !== 'variant') {
|
|
117
|
+
componentContent = componentContent.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'), value);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// If no variant specified, replace {{variantClasses}} with empty
|
|
122
|
+
componentContent = componentContent.replace(/\{\{\s*variantClasses\s*\}\}/g, '');
|
|
123
|
+
|
|
124
|
+
// Recursively compile nested components
|
|
125
|
+
const compiledComponent = await compileHtml(componentContent, root, projectRoot);
|
|
126
|
+
result = result.replace(tag, compiledComponent);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Load a component by name
|
|
134
|
+
* @param {string} name - Component name (e.g., "Card" or "main/Button")
|
|
135
|
+
* @param {string} root - The pages root directory
|
|
136
|
+
* @param {string} projectRoot - The project root directory
|
|
137
|
+
* @param {object} attrs - Component attributes/props
|
|
138
|
+
* @returns {Promise<string|null>} - Component content or null if not found
|
|
139
|
+
*/
|
|
140
|
+
async function loadComponent(name, root, projectRoot, attrs = {}) {
|
|
141
|
+
// Normalize name for path construction (handle both / and \)
|
|
142
|
+
const normalizedName = name.replace(/\\/g, '/').replace(/\.html$/i, '');
|
|
143
|
+
|
|
144
|
+
// root = srcRoot (e.g., example/src)
|
|
145
|
+
// projectRoot = project root (e.g., example)
|
|
146
|
+
// Components are in srcRoot/components
|
|
147
|
+
const possiblePaths = [
|
|
148
|
+
path.join(root, 'components', `${normalizedName}.html`), // srcRoot/components/
|
|
149
|
+
path.join(projectRoot, 'src', 'components', `${normalizedName}.html`), // projectRoot/src/components/
|
|
150
|
+
path.join(projectRoot, 'components', `${normalizedName}.html`), // projectRoot/components/
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
for (const componentPath of possiblePaths) {
|
|
154
|
+
try {
|
|
155
|
+
await fs.access(componentPath); // Check if file exists first
|
|
156
|
+
const content = await fs.readFile(componentPath, 'utf8');
|
|
157
|
+
return content;
|
|
158
|
+
} catch {
|
|
159
|
+
// Try .js file at the same location
|
|
160
|
+
const jsPath = componentPath.replace('.html', '.js');
|
|
161
|
+
try {
|
|
162
|
+
const componentModule = await import(pathToFileURL(jsPath));
|
|
163
|
+
const componentExport = componentModule.default || componentModule;
|
|
164
|
+
|
|
165
|
+
if (typeof componentExport === 'function') {
|
|
166
|
+
const props = { ...attrs };
|
|
167
|
+
delete props.src;
|
|
168
|
+
delete props.name;
|
|
169
|
+
return componentExport(props);
|
|
170
|
+
} else {
|
|
171
|
+
return String(componentExport);
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Try case-insensitive HTML lookup to support mixed-case component tags
|
|
179
|
+
try {
|
|
180
|
+
const caseInsensitivePath = await findCaseInsensitivePath(componentPath);
|
|
181
|
+
if (caseInsensitivePath) {
|
|
182
|
+
const content = await fs.readFile(caseInsensitivePath, 'utf8');
|
|
183
|
+
return content;
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
// ignore
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Try case-insensitive JS component lookup
|
|
190
|
+
try {
|
|
191
|
+
const jsPath = componentPath.replace('.html', '.js');
|
|
192
|
+
const caseInsensitiveJsPath = await findCaseInsensitivePath(jsPath);
|
|
193
|
+
if (caseInsensitiveJsPath) {
|
|
194
|
+
const componentModule = await import(pathToFileURL(caseInsensitiveJsPath));
|
|
195
|
+
const componentExport = componentModule.default || componentModule;
|
|
196
|
+
|
|
197
|
+
if (typeof componentExport === 'function') {
|
|
198
|
+
const props = { ...attrs };
|
|
199
|
+
delete props.src;
|
|
200
|
+
delete props.name;
|
|
201
|
+
return componentExport(props);
|
|
202
|
+
} else {
|
|
203
|
+
return String(componentExport);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// ignore
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Find a file path in a case-insensitive way
|
|
216
|
+
* @param {string} targetPath
|
|
217
|
+
* @returns {Promise<string|null>}
|
|
218
|
+
*/
|
|
219
|
+
async function findCaseInsensitivePath(targetPath) {
|
|
220
|
+
const dir = path.dirname(targetPath);
|
|
221
|
+
const base = path.basename(targetPath).toLowerCase();
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const entries = await fs.readdir(dir);
|
|
225
|
+
const matched = entries.find((entry) => entry.toLowerCase() === base);
|
|
226
|
+
if (!matched) return null;
|
|
227
|
+
return path.join(dir, matched);
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Parse attributes from an attribute string
|
|
235
|
+
* @param {string} attrsStr - Attribute string like ' class="foo" id="bar"'
|
|
236
|
+
* @returns {object} - Object with attribute key-value pairs
|
|
237
|
+
*/
|
|
238
|
+
function parseAttributes(attrsStr) {
|
|
239
|
+
const attrs = {};
|
|
240
|
+
const attrRegex = /([\w-]+)(?:=(?:"([^"]*)"|'([^']*)'))?/g;
|
|
241
|
+
let attrMatch;
|
|
242
|
+
while ((attrMatch = attrRegex.exec(attrsStr)) !== null) {
|
|
243
|
+
const key = attrMatch[1];
|
|
244
|
+
const value = attrMatch[2] ?? attrMatch[3] ?? 'true';
|
|
245
|
+
attrs[key] = value;
|
|
246
|
+
}
|
|
247
|
+
return attrs;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Extract named slots from component children content
|
|
252
|
+
* Supported syntax:
|
|
253
|
+
* <template slot="actions">...</template>
|
|
254
|
+
* {{ slot:actions }}
|
|
255
|
+
* @param {string} children
|
|
256
|
+
* @returns {{defaultChildren: string, namedSlots: Record<string, string>}}
|
|
257
|
+
*/
|
|
258
|
+
function extractSlots(children) {
|
|
259
|
+
const namedSlots = {};
|
|
260
|
+
const slotRegex = /<template\s+slot=["']([\w-]+)["']\s*>([\s\S]*?)<\/template>/g;
|
|
261
|
+
|
|
262
|
+
let defaultChildren = children;
|
|
263
|
+
defaultChildren = defaultChildren.replace(slotRegex, (_, slotName, slotContent) => {
|
|
264
|
+
namedSlots[slotName] = slotContent;
|
|
265
|
+
return '';
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
defaultChildren,
|
|
270
|
+
namedSlots,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Inline CSS from <link> tags into <style> tags
|
|
276
|
+
* @param {string} html - The HTML content
|
|
277
|
+
* @param {string} root - The src root directory
|
|
278
|
+
* @param {string} projectRoot - The project root directory
|
|
279
|
+
* @returns {Promise<string>} - HTML with inlined CSS
|
|
280
|
+
*/
|
|
281
|
+
export async function inlineCss(html, root, projectRoot) {
|
|
282
|
+
const linkRegex = /<link[^>]+rel=["']stylesheet["'][^>]*>/gi;
|
|
283
|
+
const matches = [...html.matchAll(linkRegex)];
|
|
284
|
+
|
|
285
|
+
let result = html;
|
|
286
|
+
|
|
287
|
+
for (const match of matches) {
|
|
288
|
+
const linkTag = match[0];
|
|
289
|
+
const hrefMatch = linkTag.match(/href=["']([^"']+)["']/);
|
|
290
|
+
|
|
291
|
+
if (!hrefMatch) continue;
|
|
292
|
+
|
|
293
|
+
let href = hrefMatch[1];
|
|
294
|
+
|
|
295
|
+
// Skip external URLs
|
|
296
|
+
if (href.startsWith('http://') || href.startsWith('https://')) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Resolve the CSS file path - try multiple locations
|
|
301
|
+
let cssPath = null;
|
|
302
|
+
let cssContent = null;
|
|
303
|
+
|
|
304
|
+
// Possible paths to check
|
|
305
|
+
const pathsToTry = [];
|
|
306
|
+
|
|
307
|
+
if (href.startsWith('/')) {
|
|
308
|
+
const cleanHref = href.slice(1);
|
|
309
|
+
// /styles/styles.css -> src/assets/styles/styles.css (publicDir pattern)
|
|
310
|
+
pathsToTry.push(path.join(root, 'assets', cleanHref));
|
|
311
|
+
// /assets/styles/styles.css -> src/assets/styles/styles.css
|
|
312
|
+
pathsToTry.push(path.join(root, cleanHref));
|
|
313
|
+
// Try project root
|
|
314
|
+
pathsToTry.push(path.join(projectRoot, 'src', 'assets', cleanHref));
|
|
315
|
+
} else {
|
|
316
|
+
// Relative path
|
|
317
|
+
pathsToTry.push(path.join(root, href));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for (const tryPath of pathsToTry) {
|
|
321
|
+
if (await fileExists(tryPath)) {
|
|
322
|
+
cssPath = tryPath;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (cssPath) {
|
|
328
|
+
try {
|
|
329
|
+
cssContent = await fs.readFile(cssPath, 'utf8');
|
|
330
|
+
const styleTag = `<style>\n${cssContent}\n</style>`;
|
|
331
|
+
result = result.replace(linkTag, styleTag);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.warn(`Could not inline CSS from ${href}: ${error.message}`);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
console.warn(`Could not find CSS file for ${href}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Inline JS from <script src="..."> tags into inline <script> tags
|
|
345
|
+
* @param {string} html - The HTML content
|
|
346
|
+
* @param {string} root - The pages root directory
|
|
347
|
+
* @param {string} projectRoot - The project root directory
|
|
348
|
+
* @returns {Promise<string>} - HTML with inlined JS
|
|
349
|
+
*/
|
|
350
|
+
export async function inlineJs(html, root, projectRoot) {
|
|
351
|
+
const scriptRegex = /<script[^>]+src=["']([^"']+)["'][^>]*><\/script>/gi;
|
|
352
|
+
const matches = [...html.matchAll(scriptRegex)];
|
|
353
|
+
|
|
354
|
+
let result = html;
|
|
355
|
+
|
|
356
|
+
for (const match of matches) {
|
|
357
|
+
const scriptTag = match[0];
|
|
358
|
+
let src = match[1];
|
|
359
|
+
|
|
360
|
+
// Skip external URLs and Vite client
|
|
361
|
+
if (src.startsWith('http://') || src.startsWith('https://') || src.includes('@vite')) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Resolve the JS file path
|
|
366
|
+
let jsPath;
|
|
367
|
+
if (src.startsWith('/')) {
|
|
368
|
+
jsPath = path.join(projectRoot, 'src', 'assets', src.slice(1));
|
|
369
|
+
if (!await fileExists(jsPath)) {
|
|
370
|
+
jsPath = path.join(root, 'assets', src.slice(1));
|
|
371
|
+
}
|
|
372
|
+
if (!await fileExists(jsPath)) {
|
|
373
|
+
jsPath = path.join(root, src.slice(1));
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
jsPath = path.join(root, src);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const jsContent = await fs.readFile(jsPath, 'utf8');
|
|
381
|
+
const inlineScriptTag = `<script>\n${jsContent}\n</script>`;
|
|
382
|
+
result = result.replace(scriptTag, inlineScriptTag);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
console.warn(`Could not inline JS from ${src}: ${error.message}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Check if a file exists
|
|
393
|
+
* @param {string} filePath - Path to check
|
|
394
|
+
* @returns {Promise<boolean>}
|
|
395
|
+
*/
|
|
396
|
+
async function fileExists(filePath) {
|
|
397
|
+
try {
|
|
398
|
+
await fs.access(filePath);
|
|
399
|
+
return true;
|
|
400
|
+
} catch {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
}
|