@catfyrr/vite-plugin-ssi 0.0.1 → 0.0.2
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 +283 -9
- package/dist/index.js +341 -11
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# Vite SSI Plugin
|
|
2
2
|
|
|
3
|
-
A fully Apache/Nginx compatible Server-Side Includes (SSI) plugin for Vite.
|
|
3
|
+
A fully Apache/Nginx compatible Server-Side Includes (SSI) plugin for Vite. This plugin processes SSI directives in your HTML files during development, build, and preview modes, enabling server-side includes without needing a traditional web server.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- 🌐
|
|
8
|
-
- 🚀 Vite
|
|
9
|
-
- 🔧
|
|
7
|
+
- 🌐 **SSI Directive Support** - Process Server-Side Includes in your Vite projects
|
|
8
|
+
- 🚀 **Full Vite Integration** - Works seamlessly in dev, build, and preview modes
|
|
9
|
+
- 🔧 **Configurable File Types** - Intelligently process different file types with SSI
|
|
10
|
+
- 🔄 **Hot Module Replacement (HMR)** - Automatic reloading when SSI dependencies change
|
|
11
|
+
- 📦 **Zero Configuration** - Works out of the box with sensible defaults
|
|
12
|
+
- 🛡️ **Safe by Default** - Circular dependency detection and depth limiting
|
|
10
13
|
|
|
11
14
|
## Installation
|
|
12
15
|
|
|
@@ -18,7 +21,9 @@ npm install @catfyrr/vite-plugin-ssi
|
|
|
18
21
|
npx jsr install @catfyrr/vite-plugin-ssi
|
|
19
22
|
```
|
|
20
23
|
|
|
21
|
-
##
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
Add the plugin to your `vite.config.ts`:
|
|
22
27
|
|
|
23
28
|
```typescript
|
|
24
29
|
import { defineConfig } from 'vite';
|
|
@@ -26,13 +31,282 @@ import vitePluginSsi from '@catfyrr/vite-plugin-ssi';
|
|
|
26
31
|
|
|
27
32
|
export default defineConfig({
|
|
28
33
|
plugins: [
|
|
29
|
-
vitePluginSsi(
|
|
30
|
-
|
|
31
|
-
})
|
|
32
|
-
]
|
|
34
|
+
vitePluginSsi(),
|
|
35
|
+
],
|
|
33
36
|
});
|
|
34
37
|
```
|
|
35
38
|
|
|
39
|
+
Now you can use SSI directives in your HTML files:
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<!DOCTYPE html>
|
|
43
|
+
<html>
|
|
44
|
+
<head>
|
|
45
|
+
<!--#include virtual="/common/head.html" -->
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
<!--#include virtual="/components/header.html" -->
|
|
49
|
+
<main>
|
|
50
|
+
<h1>Welcome</h1>
|
|
51
|
+
</main>
|
|
52
|
+
<!--#include virtual="/components/footer.html" -->
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Configuration Options
|
|
58
|
+
|
|
59
|
+
### `maxDepth`
|
|
60
|
+
|
|
61
|
+
Maximum depth for recursive includes.
|
|
62
|
+
|
|
63
|
+
- **Type:** `number`
|
|
64
|
+
- **Default:** `10`
|
|
65
|
+
- **Description:** Prevents infinite recursion and circular dependencies
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
vitePluginSsi({
|
|
69
|
+
maxDepth: 15, // Allow deeper nesting
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### `enforce`
|
|
74
|
+
|
|
75
|
+
When to run this plugin in the Vite pipeline.
|
|
76
|
+
|
|
77
|
+
- **Type:** `'pre' | 'post'`
|
|
78
|
+
- **Default:** `'pre'`
|
|
79
|
+
- **Description:** Controls the execution order relative to other plugins
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
vitePluginSsi({
|
|
83
|
+
enforce: 'pre', // Run before other plugins
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `apply`
|
|
88
|
+
|
|
89
|
+
Apply plugin only in specific environments.
|
|
90
|
+
|
|
91
|
+
- **Type:** `'serve' | 'build' | 'preview' | { serve?: boolean; build?: boolean; preview?: boolean }`
|
|
92
|
+
- **Default:** `undefined` (applies to all environments)
|
|
93
|
+
- **Description:** Control which Vite commands should process SSI
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Apply only in dev server
|
|
97
|
+
vitePluginSsi({
|
|
98
|
+
apply: 'serve',
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Apply only in build
|
|
102
|
+
vitePluginSsi({
|
|
103
|
+
apply: 'build',
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// Apply to specific environments using object form
|
|
107
|
+
vitePluginSsi({
|
|
108
|
+
apply: {
|
|
109
|
+
serve: true,
|
|
110
|
+
build: true,
|
|
111
|
+
preview: false,
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `includeFileTypes`
|
|
117
|
+
|
|
118
|
+
File types to apply SSI processing to for included files (at any depth).
|
|
119
|
+
|
|
120
|
+
- **Type:** `string[]`
|
|
121
|
+
- **Default:** `[]` (only process top-level HTML files)
|
|
122
|
+
- **Description:** When files are included via SSI, they will also be processed if they match these types. SSI always applies to top-level HTML files regardless of this setting.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Process JS/TS files when included
|
|
126
|
+
vitePluginSsi({
|
|
127
|
+
includeFileTypes: ['js', 'html'], // Processes .js, .mjs, .ts, .tsx, .jsx, etc.
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The plugin intelligently maps file types to extensions:
|
|
132
|
+
- `'js'` → `.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.jsx`, `.mts`, `.cts`
|
|
133
|
+
- `'html'` → `.html`, `.htm`, `.shtml`
|
|
134
|
+
- `'css'` → `.css`, `.scss`, `.sass`, `.less`, `.styl`
|
|
135
|
+
- `'json'` → `.json`, `.jsonc`
|
|
136
|
+
- `'xml'` → `.xml`, `.xhtml`
|
|
137
|
+
- `'text'` → `.txt`, `.md`, `.markdown`
|
|
138
|
+
|
|
139
|
+
### `fileTypeMap`
|
|
140
|
+
|
|
141
|
+
Custom file type to extension mappings.
|
|
142
|
+
|
|
143
|
+
- **Type:** `FileTypeMap`
|
|
144
|
+
- **Default:** See [file-types.ts](./src/file-types.ts) for defaults
|
|
145
|
+
- **Description:** Override or extend the default file type mappings
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import type { FileTypeMap } from '@catfyrr/vite-plugin-ssi';
|
|
149
|
+
|
|
150
|
+
const customFileTypes: FileTypeMap = {
|
|
151
|
+
html: ['.html', '.htm', '.shtml'],
|
|
152
|
+
js: ['.js', '.mjs', '.ts', '.jsx', '.tsx'],
|
|
153
|
+
// Add custom types
|
|
154
|
+
vue: ['.vue'],
|
|
155
|
+
svelte: ['.svelte'],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
vitePluginSsi({
|
|
159
|
+
includeFileTypes: ['js', 'vue'],
|
|
160
|
+
fileTypeMap: customFileTypes,
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Usage Examples
|
|
165
|
+
|
|
166
|
+
### Basic Include
|
|
167
|
+
|
|
168
|
+
```html
|
|
169
|
+
<!-- index.html -->
|
|
170
|
+
<!DOCTYPE html>
|
|
171
|
+
<html>
|
|
172
|
+
<head>
|
|
173
|
+
<title>My Site</title>
|
|
174
|
+
</head>
|
|
175
|
+
<body>
|
|
176
|
+
<!--#include virtual="/header.html" -->
|
|
177
|
+
<main>Content</main>
|
|
178
|
+
<!--#include virtual="/footer.html" -->
|
|
179
|
+
</body>
|
|
180
|
+
</html>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Absolute and Relative Paths
|
|
184
|
+
|
|
185
|
+
```html
|
|
186
|
+
<!-- Absolute paths from project root -->
|
|
187
|
+
<!--#include virtual="/components/nav.html" -->
|
|
188
|
+
|
|
189
|
+
<!-- Relative paths from current file -->
|
|
190
|
+
<!--#include virtual="../common/sidebar.html" -->
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Processing Non-HTML Files
|
|
194
|
+
|
|
195
|
+
When you want SSI to process included JavaScript/TypeScript files:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
vitePluginSsi({
|
|
199
|
+
includeFileTypes: ['js', 'html'],
|
|
200
|
+
})
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```html
|
|
204
|
+
<!-- index.html -->
|
|
205
|
+
<!--#include virtual="/scripts/utils.ts" -->
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The included `.ts` file will also be processed for SSI directives if it contains them.
|
|
209
|
+
|
|
210
|
+
### Environment-Specific Configuration
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { defineConfig } from 'vite';
|
|
214
|
+
import vitePluginSsi from '@catfyrr/vite-plugin-ssi';
|
|
215
|
+
|
|
216
|
+
export default defineConfig({
|
|
217
|
+
plugins: [
|
|
218
|
+
vitePluginSsi({
|
|
219
|
+
// Only process SSI during build (for static site generation)
|
|
220
|
+
apply: 'build',
|
|
221
|
+
includeFileTypes: ['html'],
|
|
222
|
+
maxDepth: 5,
|
|
223
|
+
}),
|
|
224
|
+
],
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## How It Works
|
|
229
|
+
|
|
230
|
+
1. **Development Mode (`vite dev`)**: SSI directives are processed on-the-fly when HTML files are served. Changes to included files trigger HMR automatically.
|
|
231
|
+
|
|
232
|
+
2. **Build Mode (`vite build`)**: SSI directives are processed during the build phase, and the final HTML output contains the resolved includes.
|
|
233
|
+
|
|
234
|
+
3. **Preview Mode (`vite preview`)**: SSI directives are processed when serving the built files, allowing you to test the built output with SSI processing.
|
|
235
|
+
|
|
236
|
+
## Error Handling
|
|
237
|
+
|
|
238
|
+
The plugin provides clear error messages for common issues:
|
|
239
|
+
|
|
240
|
+
- **Circular Dependencies**: Detected and reported with the dependency chain
|
|
241
|
+
- **Missing Files**: Included files that don't exist show an error comment
|
|
242
|
+
- **Max Depth Exceeded**: Recursion limits are enforced and reported
|
|
243
|
+
|
|
244
|
+
```html
|
|
245
|
+
<!-- If header.html doesn't exist -->
|
|
246
|
+
<!-- SSI Error: File not found: /path/to/header.html -->
|
|
247
|
+
|
|
248
|
+
<!-- If circular dependency detected -->
|
|
249
|
+
<!-- SSI Error: Circular include detected: file-a.html -> file-b.html -> file-a.html -->
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Compatibility
|
|
253
|
+
|
|
254
|
+
This plugin currently supports:
|
|
255
|
+
|
|
256
|
+
✅ **Implemented:**
|
|
257
|
+
- `<!--#include virtual="..." -->` - File inclusion with absolute and relative paths
|
|
258
|
+
- Recursive includes with depth limiting
|
|
259
|
+
- Circular dependency detection
|
|
260
|
+
- HMR for included file changes
|
|
261
|
+
- Configurable file type processing
|
|
262
|
+
|
|
263
|
+
📋 **Roadmap:** See [COMPATIBILITY.md](./COMPATIBILITY.md) for full compatibility tracking with Apache and Nginx SSI modules.
|
|
264
|
+
|
|
265
|
+
## TypeScript Support
|
|
266
|
+
|
|
267
|
+
The plugin includes full TypeScript definitions:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import vitePluginSsi, { type VitePluginSsiOptions, type FileTypeMap } from '@catfyrr/vite-plugin-ssi';
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Troubleshooting
|
|
274
|
+
|
|
275
|
+
### SSI directives not being processed
|
|
276
|
+
|
|
277
|
+
1. Ensure your HTML file has the correct extension (`.html`, `.htm`, `.shtml`)
|
|
278
|
+
2. Check that the plugin is added to your `vite.config.ts`
|
|
279
|
+
3. Verify file paths are correct (use absolute paths from project root or relative paths)
|
|
280
|
+
|
|
281
|
+
### HMR not working for included files
|
|
282
|
+
|
|
283
|
+
- Make sure you're using the dev server (`vite dev`)
|
|
284
|
+
- Check that the included file is being tracked as a dependency
|
|
285
|
+
- Verify the included file path matches exactly (case-sensitive on some systems)
|
|
286
|
+
|
|
287
|
+
### Circular dependency errors
|
|
288
|
+
|
|
289
|
+
- Review your include structure to identify the cycle
|
|
290
|
+
- Consider using a shared partial file instead of circular includes
|
|
291
|
+
- Adjust `maxDepth` if needed, but beware of infinite loops
|
|
292
|
+
|
|
293
|
+
## Security Considerations
|
|
294
|
+
|
|
295
|
+
- The plugin only processes SSI directives, it does not execute shell commands or arbitrary code
|
|
296
|
+
- File access is limited to the project directory
|
|
297
|
+
- Circular dependencies are detected and prevented
|
|
298
|
+
- Maximum include depth limits prevent excessive recursion
|
|
299
|
+
|
|
300
|
+
## Contributing
|
|
301
|
+
|
|
302
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
|
|
303
|
+
|
|
36
304
|
## License
|
|
37
305
|
|
|
38
306
|
MIT
|
|
307
|
+
|
|
308
|
+
## References
|
|
309
|
+
|
|
310
|
+
- [Apache SSI Documentation](https://httpd.apache.org/docs/current/howto/ssi.html)
|
|
311
|
+
- [Nginx SSI Module Documentation](https://nginx.org/en/docs/http/ngx_http_ssi_module.html)
|
|
312
|
+
- [Apache mod_include Documentation](https://httpd.apache.org/docs/current/mod/mod_include.html)
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
// @bun @bun-cjs
|
|
2
|
-
(function(exports, require, module, __filename, __dirname) {var
|
|
2
|
+
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
3
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
6
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
+
for (let key of __getOwnPropNames(mod))
|
|
12
|
+
if (!__hasOwnProp.call(to, key))
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: () => mod[key],
|
|
15
|
+
enumerable: true
|
|
16
|
+
});
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
6
19
|
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
7
20
|
var __toCommonJS = (from) => {
|
|
8
21
|
var entry = __moduleCache.get(from), desc;
|
|
@@ -33,21 +46,338 @@ __export(exports_src, {
|
|
|
33
46
|
default: () => vitePluginSsi
|
|
34
47
|
});
|
|
35
48
|
module.exports = __toCommonJS(exports_src);
|
|
49
|
+
var path3 = __toESM(require("path"));
|
|
50
|
+
|
|
51
|
+
// src/file-types.ts
|
|
52
|
+
var DEFAULT_FILE_TYPE_MAP = {
|
|
53
|
+
html: [".html", ".htm", ".shtml"],
|
|
54
|
+
js: [".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".mts", ".cts"],
|
|
55
|
+
css: [".css", ".scss", ".sass", ".less", ".styl"],
|
|
56
|
+
json: [".json", ".jsonc"],
|
|
57
|
+
xml: [".xml", ".xhtml"],
|
|
58
|
+
text: [".txt", ".md", ".markdown"]
|
|
59
|
+
};
|
|
60
|
+
function matchesFileType(filePath, fileTypes, fileTypeMap) {
|
|
61
|
+
const ext = getFileExtension(filePath);
|
|
62
|
+
if (!ext)
|
|
63
|
+
return false;
|
|
64
|
+
for (const fileType of fileTypes) {
|
|
65
|
+
const extensions = fileTypeMap[fileType.toLowerCase()] || [];
|
|
66
|
+
if (extensions.includes(ext)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
function getFileExtension(filePath) {
|
|
73
|
+
const lastDot = filePath.lastIndexOf(".");
|
|
74
|
+
if (lastDot === -1)
|
|
75
|
+
return null;
|
|
76
|
+
const ext = filePath.substring(lastDot);
|
|
77
|
+
return ext && ext.length > 1 ? ext.toLowerCase() : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/ssi.ts
|
|
81
|
+
var import_fs = require("fs");
|
|
82
|
+
var path = __toESM(require("path"));
|
|
83
|
+
function resolveIncludePath(virtualPath, includingFile, root) {
|
|
84
|
+
if (virtualPath.startsWith("/")) {
|
|
85
|
+
return path.join(root, virtualPath.slice(1));
|
|
86
|
+
}
|
|
87
|
+
const includingDir = path.dirname(includingFile);
|
|
88
|
+
return path.resolve(includingDir, virtualPath);
|
|
89
|
+
}
|
|
90
|
+
function normalizePath(filePath) {
|
|
91
|
+
return path.resolve(filePath).replace(/\\/g, "/");
|
|
92
|
+
}
|
|
93
|
+
async function processSsi(filePath, content, options) {
|
|
94
|
+
const { root, maxDepth, includeFileTypes = [], fileTypeMap = DEFAULT_FILE_TYPE_MAP } = options;
|
|
95
|
+
return processSsiRecursive(filePath, content, root, new Set, 0, maxDepth, includeFileTypes, fileTypeMap);
|
|
96
|
+
}
|
|
97
|
+
async function processSsiRecursive(filePath, content, root, seen, depth, maxDepth, includeFileTypes, fileTypeMap) {
|
|
98
|
+
const normalizedPath = normalizePath(filePath);
|
|
99
|
+
const deps = new Set;
|
|
100
|
+
if (seen.has(normalizedPath)) {
|
|
101
|
+
const seenArray = Array.from(seen);
|
|
102
|
+
const cycleStart = seenArray.indexOf(normalizedPath);
|
|
103
|
+
const cycle = seenArray.slice(cycleStart).concat(normalizedPath).join(" -> ");
|
|
104
|
+
return {
|
|
105
|
+
code: `<!-- SSI Error: Circular include detected: ${cycle} -->`,
|
|
106
|
+
deps
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (depth >= maxDepth) {
|
|
110
|
+
return {
|
|
111
|
+
code: `<!-- SSI Error: Maximum include depth (${maxDepth}) exceeded -->`,
|
|
112
|
+
deps
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
seen.add(normalizedPath);
|
|
116
|
+
const includeRegex = /<!--#include\s+virtual\s*=\s*"([^"]+)"\s*-->/g;
|
|
117
|
+
let match;
|
|
118
|
+
let result = content;
|
|
119
|
+
const replacements = [];
|
|
120
|
+
const matches = [];
|
|
121
|
+
while ((match = includeRegex.exec(content)) !== null) {
|
|
122
|
+
matches.push({
|
|
123
|
+
index: match.index,
|
|
124
|
+
length: match[0].length,
|
|
125
|
+
virtualPath: match[1]
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
for (let i = matches.length - 1;i >= 0; i--) {
|
|
129
|
+
const { index, length, virtualPath } = matches[i];
|
|
130
|
+
const matchEnd = index + length;
|
|
131
|
+
const resolvedPath = resolveIncludePath(virtualPath, filePath, root);
|
|
132
|
+
const normalizedResolvedPath = normalizePath(resolvedPath);
|
|
133
|
+
deps.add(normalizedResolvedPath);
|
|
134
|
+
try {
|
|
135
|
+
await import_fs.promises.access(resolvedPath);
|
|
136
|
+
const includedContent = await import_fs.promises.readFile(resolvedPath, "utf-8");
|
|
137
|
+
const shouldProcessSsi = includeFileTypes.length === 0 ? false : matchesFileType(resolvedPath, includeFileTypes, fileTypeMap);
|
|
138
|
+
let processed;
|
|
139
|
+
if (shouldProcessSsi) {
|
|
140
|
+
processed = await processSsiRecursive(resolvedPath, includedContent, root, new Set(seen), depth + 1, maxDepth, includeFileTypes, fileTypeMap);
|
|
141
|
+
} else {
|
|
142
|
+
processed = {
|
|
143
|
+
code: includedContent,
|
|
144
|
+
deps: new Set
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
processed.deps.forEach((dep) => deps.add(dep));
|
|
148
|
+
replacements.push({
|
|
149
|
+
start: index,
|
|
150
|
+
end: matchEnd,
|
|
151
|
+
replacement: processed.code
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
replacements.push({
|
|
155
|
+
start: index,
|
|
156
|
+
end: matchEnd,
|
|
157
|
+
replacement: `<!-- SSI Error: File not found: ${normalizedResolvedPath} -->`
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const { start, end, replacement } of replacements) {
|
|
162
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
163
|
+
}
|
|
164
|
+
return { code: result, deps };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/dev-server.ts
|
|
168
|
+
var import_fs2 = require("fs");
|
|
169
|
+
var path2 = __toESM(require("path"));
|
|
170
|
+
function setupPreviewServer(previewServer, options) {
|
|
171
|
+
const outDir = previewServer.config.build.outDir || "dist";
|
|
172
|
+
const projectRoot = previewServer.config.root;
|
|
173
|
+
const distRoot = path2.resolve(projectRoot, outDir);
|
|
174
|
+
const processOptions = {
|
|
175
|
+
root: projectRoot,
|
|
176
|
+
maxDepth: options.maxDepth,
|
|
177
|
+
includeFileTypes: options.includeFileTypes,
|
|
178
|
+
fileTypeMap: options.fileTypeMap
|
|
179
|
+
};
|
|
180
|
+
previewServer.middlewares.use(async (req, res, next) => {
|
|
181
|
+
try {
|
|
182
|
+
if (!req.url || req.method !== "GET")
|
|
183
|
+
return next();
|
|
184
|
+
let reqPath = req.url.split("?")[0];
|
|
185
|
+
if (reqPath === "/" || reqPath === "") {
|
|
186
|
+
reqPath = "/index.html";
|
|
187
|
+
}
|
|
188
|
+
if (!reqPath.endsWith(".html"))
|
|
189
|
+
return next();
|
|
190
|
+
const filePath = path2.join(distRoot, reqPath.startsWith("/") ? reqPath.slice(1) : reqPath);
|
|
191
|
+
const exists = await import_fs2.promises.access(filePath).then(() => true).catch(() => false);
|
|
192
|
+
if (!exists)
|
|
193
|
+
return next();
|
|
194
|
+
const raw = await import_fs2.promises.readFile(filePath, "utf-8");
|
|
195
|
+
const relativePath = path2.relative(distRoot, filePath);
|
|
196
|
+
const sourceFilePath = path2.join(projectRoot, relativePath);
|
|
197
|
+
const result = await processSsi(sourceFilePath, raw, processOptions);
|
|
198
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
199
|
+
res.end(result.code);
|
|
200
|
+
return;
|
|
201
|
+
} catch (err) {
|
|
202
|
+
return next();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function handleHotUpdate(ctx, server, reverseDependencyMap) {
|
|
207
|
+
const changedFile = normalizePath(ctx.file);
|
|
208
|
+
const affectedHtmlFiles = reverseDependencyMap.get(changedFile);
|
|
209
|
+
if (!affectedHtmlFiles || affectedHtmlFiles.size === 0) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const affectedModules = [];
|
|
213
|
+
for (const htmlFile of affectedHtmlFiles) {
|
|
214
|
+
const modulesByFile = server.moduleGraph.getModulesByFile(htmlFile);
|
|
215
|
+
if (modulesByFile && modulesByFile.size > 0) {
|
|
216
|
+
modulesByFile.forEach((module2) => {
|
|
217
|
+
affectedModules.push(module2);
|
|
218
|
+
});
|
|
219
|
+
} else {
|
|
220
|
+
try {
|
|
221
|
+
const relativePath = path2.relative(server.config.root, htmlFile);
|
|
222
|
+
const url = `/${relativePath.replace(/\\/g, "/")}`;
|
|
223
|
+
const urlModule = server.moduleGraph.urlToModuleMap.get(url);
|
|
224
|
+
if (urlModule) {
|
|
225
|
+
affectedModules.push(urlModule);
|
|
226
|
+
}
|
|
227
|
+
} catch {}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (affectedModules.length > 0) {
|
|
231
|
+
affectedModules.forEach((module2) => {
|
|
232
|
+
try {
|
|
233
|
+
server.moduleGraph.invalidateModule(module2);
|
|
234
|
+
if ("reloadModule" in server && typeof server.reloadModule === "function") {
|
|
235
|
+
server.reloadModule(module2);
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
server.ws.send({ type: "full-reload" });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
return affectedModules;
|
|
242
|
+
}
|
|
243
|
+
if (affectedHtmlFiles.size > 0) {
|
|
244
|
+
server.ws.send({ type: "full-reload" });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function transformIndexHtml(html, ctx, options) {
|
|
248
|
+
const root = ctx.server?.config.root || options.root || process.cwd();
|
|
249
|
+
const filename = ctx.filename || "index.html";
|
|
250
|
+
const filePath = path2.isAbsolute(filename) ? filename : path2.resolve(root, filename);
|
|
251
|
+
try {
|
|
252
|
+
const processOptions = {
|
|
253
|
+
root,
|
|
254
|
+
maxDepth: options.maxDepth,
|
|
255
|
+
includeFileTypes: options.includeFileTypes,
|
|
256
|
+
fileTypeMap: options.fileTypeMap
|
|
257
|
+
};
|
|
258
|
+
const result = await processSsi(filePath, html, processOptions);
|
|
259
|
+
return result.code;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return `<!-- SSI Error: ${error instanceof Error ? error.message : String(error)} -->
|
|
262
|
+
${html}`;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/index.ts
|
|
267
|
+
function normalizeApplyOption(apply) {
|
|
268
|
+
if (!apply) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (typeof apply === "string") {
|
|
272
|
+
if (apply === "preview") {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
return apply;
|
|
276
|
+
}
|
|
277
|
+
if (apply.serve === true && !apply.build) {
|
|
278
|
+
return "serve";
|
|
279
|
+
}
|
|
280
|
+
if (apply.build === true && !apply.serve) {
|
|
281
|
+
return "build";
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
function shouldApplyInEnvironment(apply, command) {
|
|
286
|
+
if (!apply) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
if (typeof apply === "string") {
|
|
290
|
+
return apply === command;
|
|
291
|
+
}
|
|
292
|
+
if (command === "serve") {
|
|
293
|
+
return apply.serve !== false;
|
|
294
|
+
}
|
|
295
|
+
if (command === "build") {
|
|
296
|
+
return apply.build !== false;
|
|
297
|
+
}
|
|
298
|
+
if (command === "preview") {
|
|
299
|
+
return apply.preview !== false;
|
|
300
|
+
}
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
36
303
|
function vitePluginSsi(options = {}) {
|
|
37
|
-
const {
|
|
304
|
+
const {
|
|
305
|
+
maxDepth = 10,
|
|
306
|
+
enforce = "pre",
|
|
307
|
+
apply: applyOption,
|
|
308
|
+
includeFileTypes = [],
|
|
309
|
+
fileTypeMap
|
|
310
|
+
} = options;
|
|
311
|
+
const mergedFileTypeMap = {
|
|
312
|
+
...DEFAULT_FILE_TYPE_MAP,
|
|
313
|
+
...fileTypeMap
|
|
314
|
+
};
|
|
315
|
+
const dependencyGraph = new Map;
|
|
316
|
+
const reverseDependencyMap = new Map;
|
|
317
|
+
let server;
|
|
318
|
+
let command = "serve";
|
|
319
|
+
let resolvedRoot;
|
|
38
320
|
return {
|
|
39
321
|
name: "vite-plugin-ssi",
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
322
|
+
enforce,
|
|
323
|
+
apply: normalizeApplyOption(applyOption),
|
|
324
|
+
configResolved(config) {
|
|
325
|
+
command = config.command || "serve";
|
|
326
|
+
resolvedRoot = config.root;
|
|
327
|
+
},
|
|
328
|
+
configureServer(_server) {
|
|
329
|
+
server = _server;
|
|
330
|
+
},
|
|
331
|
+
configurePreviewServer(previewServer) {
|
|
332
|
+
if (!shouldApplyInEnvironment(applyOption, "preview")) {
|
|
333
|
+
return () => {};
|
|
334
|
+
}
|
|
335
|
+
setupPreviewServer(previewServer, {
|
|
336
|
+
maxDepth,
|
|
337
|
+
includeFileTypes,
|
|
338
|
+
fileTypeMap: mergedFileTypeMap
|
|
339
|
+
});
|
|
340
|
+
},
|
|
341
|
+
async transformIndexHtml(html, ctx) {
|
|
342
|
+
const currentCommand = ctx.server ? ctx.server.config.command || "serve" : command;
|
|
343
|
+
if (!shouldApplyInEnvironment(applyOption, currentCommand)) {
|
|
344
|
+
return html;
|
|
43
345
|
}
|
|
44
|
-
|
|
346
|
+
const root = ctx.server?.config.root || resolvedRoot || process.cwd();
|
|
347
|
+
const result = await transformIndexHtml(html, ctx, {
|
|
348
|
+
root,
|
|
349
|
+
maxDepth,
|
|
350
|
+
includeFileTypes,
|
|
351
|
+
fileTypeMap: mergedFileTypeMap
|
|
352
|
+
});
|
|
353
|
+
const filename = ctx.filename || "index.html";
|
|
354
|
+
const filePath = path3.isAbsolute(filename) ? filename : path3.resolve(root, filename);
|
|
355
|
+
const normalizedFilePath = normalizePath(filePath);
|
|
356
|
+
const processOptions = {
|
|
357
|
+
root,
|
|
358
|
+
maxDepth,
|
|
359
|
+
includeFileTypes,
|
|
360
|
+
fileTypeMap: mergedFileTypeMap
|
|
361
|
+
};
|
|
362
|
+
const depsResult = await processSsi(filePath, html, processOptions);
|
|
363
|
+
dependencyGraph.set(normalizedFilePath, depsResult.deps);
|
|
364
|
+
depsResult.deps.forEach((dep) => {
|
|
365
|
+
if (!reverseDependencyMap.has(dep)) {
|
|
366
|
+
reverseDependencyMap.set(dep, new Set);
|
|
367
|
+
}
|
|
368
|
+
reverseDependencyMap.get(dep).add(normalizedFilePath);
|
|
369
|
+
});
|
|
370
|
+
return result;
|
|
371
|
+
},
|
|
372
|
+
handleHotUpdate(ctx) {
|
|
373
|
+
if (!shouldApplyInEnvironment(applyOption, command)) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (!server) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
return handleHotUpdate(ctx, server, reverseDependencyMap);
|
|
45
380
|
}
|
|
46
381
|
};
|
|
47
382
|
}
|
|
48
|
-
function defaultProcessIncludes(content) {
|
|
49
|
-
return content.replace(/<!--\s*#include\s+(virtual|file)="([^"]+)"\s*-->/g, (match, type, path) => {
|
|
50
|
-
return `<!-- Unimplemented SSI include: ${type} - ${path} -->`;
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
383
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catfyrr/vite-plugin-ssi",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Fully Apache/Nginx compatible Server-Side Includes (SSI) Vite plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
"test:watch": "bun test --watch",
|
|
26
26
|
"lint": "eslint src tests",
|
|
27
27
|
"lint:fix": "eslint src tests --fix",
|
|
28
|
-
"prepublishOnly": "bun run build"
|
|
29
|
-
"prepare": "husky install"
|
|
28
|
+
"prepublishOnly": "bun run build"
|
|
30
29
|
},
|
|
31
30
|
"keywords": [
|
|
32
31
|
"vite",
|