vite-file-include 1.1.0 → 1.2.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/README.md +621 -86
- package/package.json +12 -7
- package/src/index.js +394 -311
package/src/index.js
CHANGED
|
@@ -1,357 +1,440 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
includePattern = "@@include"
|
|
7
|
-
loopPattern = "@@loop"
|
|
8
|
-
ifPattern = "@@if"
|
|
9
|
-
baseDir = process.cwd()
|
|
10
|
-
context = {}
|
|
11
|
-
customFunctions = {}
|
|
12
|
-
} = options;
|
|
13
|
-
|
|
14
|
-
// Cache for file reads
|
|
15
|
-
const fileCache = new Map();
|
|
16
|
-
|
|
17
|
-
const readFileCached = (filePath) => {
|
|
18
|
-
if (!fileCache.has(filePath)) {
|
|
19
|
-
fileCache.set(filePath, fs.readFileSync(filePath, "utf-8"));
|
|
20
|
-
}
|
|
21
|
-
return fileCache.get(filePath);
|
|
22
|
-
};
|
|
4
|
+
class FileIncludeProcessor {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.includePattern = options.includePattern || "@@include";
|
|
7
|
+
this.loopPattern = options.loopPattern || "@@loop";
|
|
8
|
+
this.ifPattern = options.ifPattern || "@@if";
|
|
9
|
+
this.baseDir = options.baseDir || process.cwd();
|
|
10
|
+
this.context = options.context || {};
|
|
11
|
+
this.customFunctions = options.customFunctions || {};
|
|
23
12
|
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
// Unified regex patterns
|
|
14
|
+
this.patterns = {
|
|
15
|
+
include: null,
|
|
16
|
+
loop: null,
|
|
17
|
+
conditional: null,
|
|
18
|
+
variable: /\{\{\s*(.*?)\s*\}\}/g
|
|
19
|
+
};
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
html,
|
|
30
|
-
baseDir,
|
|
31
|
-
includePattern,
|
|
32
|
-
loopPattern,
|
|
33
|
-
ifPattern,
|
|
34
|
-
context,
|
|
35
|
-
customFunctions,
|
|
36
|
-
new Set(),
|
|
37
|
-
readFileCached
|
|
38
|
-
);
|
|
39
|
-
},
|
|
21
|
+
this.initializePatterns();
|
|
22
|
+
}
|
|
40
23
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Initialize unified regex patterns
|
|
26
|
+
*/
|
|
27
|
+
initializePatterns() {
|
|
28
|
+
// Unified pattern for directives: @@directive('arg1', arg2)
|
|
29
|
+
this.patterns.include = new RegExp(
|
|
30
|
+
`${this.escapeRegex(this.includePattern)}\\(\\s*['"]([^'"]+)['"]\\s*(?:,\\s*({[\\s\\S]*?}))?\\s*\\)\\s*;?`,
|
|
31
|
+
"g"
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
this.patterns.loop = new RegExp(
|
|
35
|
+
`${this.escapeRegex(this.loopPattern)}\\(\\s*['"]([^'"]+)['"]\\s*,\\s*(\\[[\\s\\S]*?\\]|['"][^'"]+['"])\\s*\\)\\s*;?`,
|
|
36
|
+
"g"
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
this.patterns.conditional = new RegExp(
|
|
40
|
+
`${this.escapeRegex(this.ifPattern)}\\s*\\(([^)]+)\\)\\s*{([\\s\\S]*?)};?`,
|
|
41
|
+
"g"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Escape special regex characters
|
|
47
|
+
*/
|
|
48
|
+
escapeRegex(string) {
|
|
49
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Main processing entry point
|
|
54
|
+
*/
|
|
55
|
+
process(content, dir, visited = new Set(), localContext = this.context) {
|
|
56
|
+
let newContent = content;
|
|
57
|
+
let lastContent;
|
|
58
|
+
let iterations = 0;
|
|
59
|
+
const maxIterations = 100;
|
|
60
|
+
|
|
61
|
+
do {
|
|
62
|
+
lastContent = newContent;
|
|
63
|
+
newContent = this.processDirective('include', newContent, dir, visited, localContext);
|
|
64
|
+
newContent = this.processDirective('loop', newContent, dir, visited, localContext);
|
|
65
|
+
newContent = this.processDirective('conditional', newContent, dir, visited, localContext);
|
|
66
|
+
|
|
67
|
+
iterations++;
|
|
68
|
+
if (iterations >= maxIterations) {
|
|
69
|
+
console.warn(`⚠️ Maximum iterations (${maxIterations}) reached. Possible infinite loop.`);
|
|
70
|
+
break;
|
|
56
71
|
}
|
|
57
|
-
|
|
58
|
-
},
|
|
72
|
+
} while (newContent !== lastContent);
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const mod = modules.find((m) => m.file && m.file.endsWith(".html"));
|
|
74
|
+
// Final pass to replace any remaining variables
|
|
75
|
+
newContent = this.injectData(newContent, localContext);
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
server.ws.send({
|
|
67
|
-
type: "update",
|
|
68
|
-
updates: [
|
|
69
|
-
{
|
|
70
|
-
type: "js-update",
|
|
71
|
-
path: mod.url,
|
|
72
|
-
acceptedPath: mod.url,
|
|
73
|
-
timestamp: Date.now(),
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
});
|
|
77
|
-
} else {
|
|
78
|
-
server.ws.send({
|
|
79
|
-
type: "custom",
|
|
80
|
-
event: "vite-file-include:update",
|
|
81
|
-
data: { file },
|
|
82
|
-
});
|
|
83
|
-
}
|
|
77
|
+
return newContent;
|
|
78
|
+
}
|
|
84
79
|
|
|
85
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Unified directive processor
|
|
82
|
+
*/
|
|
83
|
+
processDirective(type, content, dir, visited, localContext) {
|
|
84
|
+
const pattern = this.patterns[type];
|
|
85
|
+
if (!pattern) {
|
|
86
|
+
console.error(`Unknown directive type: ${type}`);
|
|
87
|
+
return content;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return content.replace(pattern, (...args) => {
|
|
91
|
+
try {
|
|
92
|
+
switch (type) {
|
|
93
|
+
case 'include':
|
|
94
|
+
return this.handleInclude(args, dir, visited, localContext);
|
|
95
|
+
case 'loop':
|
|
96
|
+
return this.handleLoop(args, dir, visited, localContext);
|
|
97
|
+
case 'conditional':
|
|
98
|
+
return this.handleConditional(args, dir, visited, localContext);
|
|
99
|
+
default:
|
|
100
|
+
return args[0];
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(`Error processing ${type}:`, error.message);
|
|
104
|
+
return ""; // Return empty string on error to avoid outputting broken directives
|
|
86
105
|
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
includePattern,
|
|
97
|
-
loopPattern,
|
|
98
|
-
ifPattern,
|
|
99
|
-
context,
|
|
100
|
-
customFunctions,
|
|
101
|
-
visited,
|
|
102
|
-
readFileCached
|
|
103
|
-
) {
|
|
104
|
-
let lastContent;
|
|
105
|
-
do {
|
|
106
|
-
lastContent = content;
|
|
107
|
-
content = processIncludesWithPattern(
|
|
108
|
-
content,
|
|
109
|
-
dir,
|
|
110
|
-
includePattern,
|
|
111
|
-
loopPattern,
|
|
112
|
-
ifPattern,
|
|
113
|
-
context,
|
|
114
|
-
customFunctions,
|
|
115
|
-
visited,
|
|
116
|
-
readFileCached
|
|
117
|
-
);
|
|
118
|
-
content = processLoops(
|
|
119
|
-
content,
|
|
120
|
-
dir,
|
|
121
|
-
loopPattern,
|
|
122
|
-
context,
|
|
123
|
-
customFunctions,
|
|
124
|
-
includePattern,
|
|
125
|
-
ifPattern,
|
|
126
|
-
visited,
|
|
127
|
-
readFileCached
|
|
128
|
-
);
|
|
129
|
-
content = processConditionals(
|
|
130
|
-
content,
|
|
131
|
-
dir,
|
|
132
|
-
ifPattern,
|
|
133
|
-
includePattern,
|
|
134
|
-
loopPattern,
|
|
135
|
-
context,
|
|
136
|
-
customFunctions,
|
|
137
|
-
visited,
|
|
138
|
-
readFileCached
|
|
139
|
-
);
|
|
140
|
-
} while (content !== lastContent);
|
|
109
|
+
/**
|
|
110
|
+
* Handle @@include directive
|
|
111
|
+
*/
|
|
112
|
+
handleInclude(args, dir, visited, localContext) {
|
|
113
|
+
const [match, filePath, jsonData] = args;
|
|
141
114
|
|
|
142
|
-
return content;
|
|
143
|
-
}
|
|
144
115
|
|
|
145
|
-
function processIncludesWithPattern(
|
|
146
|
-
content,
|
|
147
|
-
dir,
|
|
148
|
-
includePattern,
|
|
149
|
-
loopPattern,
|
|
150
|
-
ifPattern,
|
|
151
|
-
context,
|
|
152
|
-
customFunctions,
|
|
153
|
-
visited,
|
|
154
|
-
readFileCached
|
|
155
|
-
) {
|
|
156
|
-
const regex = new RegExp(
|
|
157
|
-
`${includePattern}\\(\\s*['"]([^'"]+)['"]\\s*(?:,\\s*({[\\s\\S]*?}))?\\s*\\)\\s*;?`,
|
|
158
|
-
"g"
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
return content.replace(regex, (match, filePath, jsonData) => {
|
|
162
116
|
const includePath = path.resolve(dir, filePath);
|
|
117
|
+
|
|
163
118
|
if (visited.has(includePath)) {
|
|
164
119
|
console.warn(`⚠️ Circular include detected: ${includePath}`);
|
|
165
120
|
return "";
|
|
166
121
|
}
|
|
122
|
+
|
|
167
123
|
visited.add(includePath);
|
|
168
124
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
data = JSON.parse(jsonData);
|
|
173
|
-
} catch {
|
|
174
|
-
console.error(`Failed to parse JSON data: ${jsonData}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
125
|
+
const data = jsonData ? this.parseJSON(jsonData, 'include data') : {};
|
|
126
|
+
const fileContent = this.readFile(includePath);
|
|
177
127
|
|
|
178
|
-
|
|
179
|
-
let includedContent = readFileCached(includePath);
|
|
180
|
-
includedContent = injectData(
|
|
181
|
-
includedContent,
|
|
182
|
-
{ ...context, ...data },
|
|
183
|
-
customFunctions
|
|
184
|
-
);
|
|
185
|
-
return processIncludes(
|
|
186
|
-
includedContent,
|
|
187
|
-
path.dirname(includePath),
|
|
188
|
-
includePattern,
|
|
189
|
-
loopPattern,
|
|
190
|
-
ifPattern,
|
|
191
|
-
{ ...context, ...data },
|
|
192
|
-
customFunctions,
|
|
193
|
-
visited,
|
|
194
|
-
readFileCached
|
|
195
|
-
);
|
|
196
|
-
} catch (err) {
|
|
197
|
-
console.error(`Failed to include file: ${includePath}`);
|
|
198
|
-
return "";
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
}
|
|
128
|
+
if (fileContent === null) return "";
|
|
202
129
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
130
|
+
const newContext = { ...localContext, ...data };
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
const processedContent = this.injectData(fileContent, newContext);
|
|
134
|
+
return this.process(
|
|
135
|
+
processedContent,
|
|
136
|
+
path.dirname(includePath),
|
|
137
|
+
visited,
|
|
138
|
+
newContext
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Handle @@loop directive
|
|
144
|
+
*/
|
|
145
|
+
handleLoop(args, dir, visited, localContext) {
|
|
146
|
+
const [match, filePath, jsonArrayOrFilePath] = args;
|
|
220
147
|
const loopPath = path.resolve(dir, filePath);
|
|
221
|
-
let dataArray = [];
|
|
222
148
|
|
|
149
|
+
const dataArray = this.parseLoopData(jsonArrayOrFilePath, dir);
|
|
150
|
+
if (!dataArray) return "";
|
|
151
|
+
|
|
152
|
+
const loopTemplate = this.readFile(loopPath);
|
|
153
|
+
if (loopTemplate === null) return "";
|
|
154
|
+
|
|
155
|
+
return dataArray
|
|
156
|
+
.map((data, index) => {
|
|
157
|
+
const loopContext = {
|
|
158
|
+
...localContext,
|
|
159
|
+
...data,
|
|
160
|
+
_index: index,
|
|
161
|
+
_total: dataArray.length
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const loopContent = this.injectData(loopTemplate, loopContext);
|
|
165
|
+
return this.process(loopContent, dir, visited, loopContext);
|
|
166
|
+
})
|
|
167
|
+
.join("");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handle @@if directive
|
|
172
|
+
*/
|
|
173
|
+
handleConditional(args, dir, visited, localContext) {
|
|
174
|
+
const [match, condition, body] = args;
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
const result = this.evaluateCondition(condition, localContext);
|
|
178
|
+
|
|
179
|
+
if (!result) return "";
|
|
180
|
+
|
|
181
|
+
return this.process(body.trim(), dir, visited, localContext);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Parse loop data from inline JSON or file path
|
|
186
|
+
*/
|
|
187
|
+
parseLoopData(jsonArrayOrFilePath, dir) {
|
|
223
188
|
try {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
dir,
|
|
229
|
-
jsonArrayOrFilePath.replace(/['"]/g, "")
|
|
230
|
-
);
|
|
231
|
-
const jsonData = readFileCached(jsonFilePath);
|
|
232
|
-
dataArray = JSON.parse(jsonData);
|
|
189
|
+
const trimmed = jsonArrayOrFilePath.trim();
|
|
190
|
+
|
|
191
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
192
|
+
return JSON.parse(trimmed);
|
|
233
193
|
}
|
|
194
|
+
|
|
195
|
+
const jsonFilePath = path.resolve(dir, trimmed.replace(/['"]/g, ""));
|
|
196
|
+
const jsonContent = this.readFile(jsonFilePath);
|
|
197
|
+
|
|
198
|
+
if (jsonContent === null) return null;
|
|
199
|
+
|
|
200
|
+
return JSON.parse(jsonContent);
|
|
234
201
|
} catch (error) {
|
|
235
|
-
console.error(`Failed to parse loop
|
|
236
|
-
return
|
|
202
|
+
console.error(`Failed to parse loop data: ${jsonArrayOrFilePath}`, error.message);
|
|
203
|
+
return null;
|
|
237
204
|
}
|
|
205
|
+
}
|
|
238
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Read file with error handling
|
|
209
|
+
*/
|
|
210
|
+
readFile(filePath) {
|
|
239
211
|
try {
|
|
240
|
-
|
|
241
|
-
return dataArray
|
|
242
|
-
.map((data) => {
|
|
243
|
-
const mergedContext = { ...context, ...data };
|
|
244
|
-
const loopContent = injectData(
|
|
245
|
-
loopTemplate,
|
|
246
|
-
mergedContext,
|
|
247
|
-
customFunctions
|
|
248
|
-
);
|
|
249
|
-
return processIncludes(
|
|
250
|
-
loopContent,
|
|
251
|
-
dir,
|
|
252
|
-
includePattern,
|
|
253
|
-
loopPattern,
|
|
254
|
-
ifPattern,
|
|
255
|
-
mergedContext,
|
|
256
|
-
customFunctions,
|
|
257
|
-
visited,
|
|
258
|
-
readFileCached
|
|
259
|
-
);
|
|
260
|
-
})
|
|
261
|
-
.join("");
|
|
212
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
262
213
|
} catch (error) {
|
|
263
|
-
console.error(`Failed to
|
|
264
|
-
return
|
|
214
|
+
console.error(`Failed to read file: ${filePath}`, error.message);
|
|
215
|
+
return null;
|
|
265
216
|
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
217
|
+
}
|
|
268
218
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
includePattern,
|
|
274
|
-
loopPattern,
|
|
275
|
-
context,
|
|
276
|
-
customFunctions,
|
|
277
|
-
visited,
|
|
278
|
-
readFileCached
|
|
279
|
-
) {
|
|
280
|
-
const regex = new RegExp(
|
|
281
|
-
`${ifPattern}\\s*\\(([^)]+)\\)\\s*{([\\s\\S]*?)};?`,
|
|
282
|
-
"g"
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
return content.replace(regex, (match, condition, body) => {
|
|
219
|
+
/**
|
|
220
|
+
* Parse JSON with error handling
|
|
221
|
+
*/
|
|
222
|
+
parseJSON(jsonString, context = 'data') {
|
|
286
223
|
try {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (result) {
|
|
290
|
-
let processed = processIncludesWithPattern(
|
|
291
|
-
body.trim(),
|
|
292
|
-
dir,
|
|
293
|
-
includePattern,
|
|
294
|
-
loopPattern,
|
|
295
|
-
ifPattern,
|
|
296
|
-
context,
|
|
297
|
-
customFunctions,
|
|
298
|
-
visited,
|
|
299
|
-
readFileCached
|
|
300
|
-
);
|
|
301
|
-
processed = processLoops(
|
|
302
|
-
processed,
|
|
303
|
-
dir,
|
|
304
|
-
loopPattern,
|
|
305
|
-
context,
|
|
306
|
-
customFunctions,
|
|
307
|
-
includePattern,
|
|
308
|
-
ifPattern,
|
|
309
|
-
visited,
|
|
310
|
-
readFileCached
|
|
311
|
-
);
|
|
312
|
-
processed = processConditionals(
|
|
313
|
-
processed,
|
|
314
|
-
dir,
|
|
315
|
-
ifPattern,
|
|
316
|
-
includePattern,
|
|
317
|
-
loopPattern,
|
|
318
|
-
context,
|
|
319
|
-
customFunctions,
|
|
320
|
-
visited,
|
|
321
|
-
readFileCached
|
|
322
|
-
);
|
|
323
|
-
return processed;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return "";
|
|
224
|
+
return JSON.parse(jsonString);
|
|
327
225
|
} catch (error) {
|
|
328
|
-
console.error(`Failed to
|
|
329
|
-
return
|
|
226
|
+
console.error(`Failed to parse JSON ${context}: ${jsonString}`, error.message);
|
|
227
|
+
return {};
|
|
330
228
|
}
|
|
331
|
-
}
|
|
332
|
-
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Inject data into {{ }} expressions
|
|
233
|
+
*/
|
|
234
|
+
injectData(content, data) {
|
|
235
|
+
return content.replace(this.patterns.variable, (match, expression) => {
|
|
236
|
+
try {
|
|
237
|
+
const result = this.evaluateExpression(expression, data);
|
|
238
|
+
return result !== undefined && result !== null ? result : match;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(`Failed to evaluate expression: ${expression}`, error.message);
|
|
241
|
+
return match;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Evaluate JavaScript expression with context
|
|
248
|
+
*/
|
|
249
|
+
evaluateExpression(expression, data) {
|
|
250
|
+
const context = { ...data, ...this.customFunctions };
|
|
251
|
+
return new Function("context", `with (context) { return ${expression}; }`)(context);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Evaluate conditional expression
|
|
256
|
+
*/
|
|
257
|
+
evaluateCondition(condition, localContext) {
|
|
258
|
+
|
|
333
259
|
|
|
334
|
-
function injectData(content, data, customFunctions = {}) {
|
|
335
|
-
return content.replace(/\{\{\s*(.*?)\s*\}\}/g, (match, expression) => {
|
|
336
260
|
try {
|
|
337
|
-
const
|
|
338
|
-
return
|
|
339
|
-
} catch {
|
|
340
|
-
|
|
261
|
+
const context = { ...this.context, ...localContext, ...this.customFunctions };
|
|
262
|
+
return new Function("context", `with (context) { return ${condition}; }`)(context);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error(`Error evaluating condition: "${condition}"`, error.message);
|
|
265
|
+
return false;
|
|
341
266
|
}
|
|
342
|
-
}
|
|
343
|
-
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Add custom function at runtime
|
|
271
|
+
*/
|
|
272
|
+
addFunction(name, fn) {
|
|
273
|
+
this.customFunctions[name] = fn;
|
|
274
|
+
}
|
|
344
275
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Update context at runtime
|
|
278
|
+
*/
|
|
279
|
+
updateContext(newContext) {
|
|
280
|
+
this.context = { ...this.context, ...newContext };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Reset processor state
|
|
285
|
+
*/
|
|
286
|
+
reset() {
|
|
287
|
+
this.context = {};
|
|
288
|
+
this.customFunctions = {};
|
|
289
|
+
}
|
|
350
290
|
}
|
|
351
291
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Vite Plugin Factory
|
|
294
|
+
*/
|
|
295
|
+
function fileIncludePlugin(options = {}) {
|
|
296
|
+
const processor = new FileIncludeProcessor(options);
|
|
297
|
+
const dependencyGraph = new Map(); // Track file dependencies
|
|
298
|
+
let server;
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
name: "vite-plugin-file-include",
|
|
302
|
+
|
|
303
|
+
configureServer(viteServer) {
|
|
304
|
+
server = viteServer;
|
|
305
|
+
|
|
306
|
+
// Add HMR client code injection
|
|
307
|
+
viteServer.middlewares.use((req, res, next) => {
|
|
308
|
+
next();
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
transformIndexHtml: {
|
|
313
|
+
order: 'pre',
|
|
314
|
+
handler(html, ctx) {
|
|
315
|
+
const processed = processor.process(html, processor.baseDir, new Set(), processor.context);
|
|
316
|
+
|
|
317
|
+
// Inject HMR client script
|
|
318
|
+
const hmrScript = `
|
|
319
|
+
<script type="module">
|
|
320
|
+
if (import.meta.hot) {
|
|
321
|
+
import.meta.hot.on('vite-file-include:update', async (data) => {
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
// Fetch the updated page
|
|
326
|
+
const response = await fetch(window.location.pathname);
|
|
327
|
+
const html = await response.text();
|
|
328
|
+
|
|
329
|
+
// Parse the new HTML
|
|
330
|
+
const parser = new DOMParser();
|
|
331
|
+
const newDoc = parser.parseFromString(html, 'text/html');
|
|
332
|
+
|
|
333
|
+
// Update body content without full reload
|
|
334
|
+
const currentBody = document.body;
|
|
335
|
+
const newBody = newDoc.body;
|
|
336
|
+
|
|
337
|
+
// Preserve scroll position
|
|
338
|
+
const scrollPos = window.scrollY;
|
|
339
|
+
|
|
340
|
+
// Replace body content
|
|
341
|
+
currentBody.innerHTML = newBody.innerHTML;
|
|
342
|
+
|
|
343
|
+
// Copy body attributes
|
|
344
|
+
Array.from(newBody.attributes).forEach(attr => {
|
|
345
|
+
currentBody.setAttribute(attr.name, attr.value);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Restore scroll position
|
|
349
|
+
window.scrollTo(0, scrollPos);
|
|
350
|
+
|
|
351
|
+
// Re-execute scripts if needed
|
|
352
|
+
const scripts = currentBody.querySelectorAll('script:not([type="module"])');
|
|
353
|
+
scripts.forEach(script => {
|
|
354
|
+
if (script.src) {
|
|
355
|
+
const newScript = document.createElement('script');
|
|
356
|
+
Array.from(script.attributes).forEach(attr => {
|
|
357
|
+
newScript.setAttribute(attr.name, attr.value);
|
|
358
|
+
});
|
|
359
|
+
script.parentNode.replaceChild(newScript, script);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error('[HMR] Update failed, reloading page:', error);
|
|
366
|
+
window.location.reload();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
</script>
|
|
371
|
+
`;
|
|
372
|
+
|
|
373
|
+
// Inject before closing body tag
|
|
374
|
+
if (processed.includes('</body>')) {
|
|
375
|
+
return processed.replace('</body>', `${hmrScript}</body>`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return processed;
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
transform(code, id) {
|
|
383
|
+
if (id.endsWith(".html")) {
|
|
384
|
+
// Track this file
|
|
385
|
+
if (!dependencyGraph.has(id)) {
|
|
386
|
+
dependencyGraph.set(id, new Set());
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
code: processor.process(code, processor.baseDir),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
handleHotUpdate({ file, server, modules }) {
|
|
397
|
+
if (file.endsWith(".html")) {
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
// Find all modules that might be affected
|
|
401
|
+
const affectedModules = [];
|
|
402
|
+
|
|
403
|
+
modules.forEach(mod => {
|
|
404
|
+
if (mod.file && mod.file.endsWith('.html')) {
|
|
405
|
+
affectedModules.push(mod);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Invalidate modules
|
|
410
|
+
affectedModules.forEach(mod => {
|
|
411
|
+
server.moduleGraph.invalidateModule(mod);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Send custom HMR update event
|
|
415
|
+
server.ws.send({
|
|
416
|
+
type: 'custom',
|
|
417
|
+
event: 'vite-file-include:update',
|
|
418
|
+
data: {
|
|
419
|
+
file: path.basename(file),
|
|
420
|
+
path: file,
|
|
421
|
+
timestamp: Date.now()
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Return empty array to prevent default full reload
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
// Expose processor for advanced usage
|
|
431
|
+
api: {
|
|
432
|
+
processor,
|
|
433
|
+
addFunction: (name, fn) => processor.addFunction(name, fn),
|
|
434
|
+
updateContext: (ctx) => processor.updateContext(ctx),
|
|
435
|
+
},
|
|
436
|
+
};
|
|
355
437
|
}
|
|
356
438
|
|
|
357
439
|
export default fileIncludePlugin;
|
|
440
|
+
export { FileIncludeProcessor };
|