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.
Files changed (3) hide show
  1. package/README.md +621 -86
  2. package/package.json +12 -7
  3. 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
- function fileIncludePlugin(options = {}) {
5
- const {
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
- return {
25
- name: "vite-plugin-file-include",
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
- transformIndexHtml(html) {
28
- return processIncludes(
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
- transform(code, id) {
42
- if (id.endsWith(".html")) {
43
- return {
44
- code: processIncludes(
45
- code,
46
- baseDir,
47
- includePattern,
48
- loopPattern,
49
- ifPattern,
50
- context,
51
- customFunctions,
52
- new Set(),
53
- readFileCached
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
- return { code };
58
- },
72
+ } while (newContent !== lastContent);
59
73
 
60
- handleHotUpdate({ file, server, modules }) {
61
- if (file.endsWith(".html")) {
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
- if (mod) {
65
- server.moduleGraph.invalidateModule(mod);
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
- return [];
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
- /* ---------------- Core Processing ---------------- */
92
-
93
- function processIncludes(
94
- content,
95
- dir,
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
- let data = {};
170
- if (jsonData) {
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
- try {
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
- function processLoops(
204
- content,
205
- dir,
206
- loopPattern,
207
- context,
208
- customFunctions,
209
- includePattern,
210
- ifPattern,
211
- visited,
212
- readFileCached
213
- ) {
214
- const regex = new RegExp(
215
- `${loopPattern}\\(\\s*['"]([^'"]+)['"]\\s*,\\s*(\\[[\\s\\S]*?\\]|['"][^'"]+['"])\\s*\\)\\s*;?`,
216
- "g"
217
- );
218
-
219
- return content.replace(regex, (match, filePath, jsonArrayOrFilePath) => {
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
- if (jsonArrayOrFilePath.trim().startsWith("[")) {
225
- dataArray = JSON.parse(jsonArrayOrFilePath);
226
- } else {
227
- const jsonFilePath = path.resolve(
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 JSON: ${jsonArrayOrFilePath}`);
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
- let loopTemplate = readFileCached(loopPath);
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 include loop file: ${loopPath}`);
264
- return "";
214
+ console.error(`Failed to read file: ${filePath}`, error.message);
215
+ return null;
265
216
  }
266
- });
267
- }
217
+ }
268
218
 
269
- function processConditionals(
270
- content,
271
- dir,
272
- ifPattern,
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
- const result = evaluateCondition(condition, context, customFunctions);
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 evaluate condition: ${condition}`);
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 result = evaluateExpression(expression, data, customFunctions);
338
- return result !== undefined ? result : match;
339
- } catch {
340
- return match;
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
- function evaluateExpression(expression, data, customFunctions) {
346
- const context = { ...data, ...customFunctions };
347
- return new Function("context", `with (context) { return ${expression}; }`)(
348
- context
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
- function evaluateCondition(condition, context, customFunctions) {
353
- const ctx = { ...context, ...customFunctions };
354
- return new Function("context", `with (context) { return ${condition}; }`)(ctx);
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 };