@catfyrr/vite-plugin-ssi 0.0.3 → 0.0.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.
@@ -0,0 +1,25 @@
1
+ import type { ViteDevServer, PreviewServer } from 'vite';
2
+ export interface DevServerOptions {
3
+ maxDepth: number;
4
+ includeFileTypes?: string[];
5
+ fileTypeMap?: import('./file-types').FileTypeMap;
6
+ }
7
+ /**
8
+ * Sets up preview server middleware to process SSI on-the-fly
9
+ */
10
+ export declare function setupPreviewServer(previewServer: PreviewServer, options: DevServerOptions): void;
11
+ /**
12
+ * Handles HMR updates when SSI dependency files change
13
+ */
14
+ export declare function handleHotUpdate(ctx: {
15
+ file: string;
16
+ }, server: ViteDevServer, reverseDependencyMap: Map<string, Set<string>>): Array<import('vite').ModuleNode> | void;
17
+ /**
18
+ * Transforms index HTML with SSI processing
19
+ */
20
+ export declare function transformIndexHtml(html: string, ctx: {
21
+ filename?: string;
22
+ server?: ViteDevServer;
23
+ }, options: DevServerOptions & {
24
+ root?: string;
25
+ }): Promise<string>;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * File type to extension mappings for intelligent SSI processing
3
+ */
4
+ export interface FileTypeMap {
5
+ [key: string]: string[];
6
+ }
7
+ /**
8
+ * Default file type mappings
9
+ * Maps file type names to their associated extensions
10
+ */
11
+ export declare const DEFAULT_FILE_TYPE_MAP: FileTypeMap;
12
+ /**
13
+ * Checks if a file path matches any of the specified file types
14
+ */
15
+ export declare function matchesFileType(filePath: string, fileTypes: string[], fileTypeMap: FileTypeMap): boolean;
16
+ /**
17
+ * Checks if a file path matches HTML file extensions (for top-level processing)
18
+ */
19
+ export declare function isHtmlFile(filePath: string): boolean;
@@ -1,5 +1,4 @@
1
- // @bun @bun-cjs
2
- (function(exports, require, module, __filename, __dirname) {var __create = Object.create;
1
+ var __create = Object.create;
3
2
  var __getProtoOf = Object.getPrototypeOf;
4
3
  var __defProp = Object.defineProperty;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -380,4 +379,3 @@ function vitePluginSsi(options = {}) {
380
379
  }
381
380
  };
382
381
  }
383
- })
@@ -0,0 +1,38 @@
1
+ import type { Plugin } from 'vite';
2
+ import { type FileTypeMap } from './file-types';
3
+ export type { FileTypeMap } from './file-types';
4
+ export interface VitePluginSsiOptions {
5
+ /**
6
+ * Maximum depth for recursive includes
7
+ * @default 10
8
+ */
9
+ maxDepth?: number;
10
+ /**
11
+ * When to run this plugin in the pipeline
12
+ * @default 'pre'
13
+ */
14
+ enforce?: 'pre' | 'post';
15
+ /**
16
+ * Apply plugin only in specific environments
17
+ * @default undefined (applies to all environments)
18
+ */
19
+ apply?: 'serve' | 'build' | 'preview' | {
20
+ serve?: boolean;
21
+ build?: boolean;
22
+ preview?: boolean;
23
+ };
24
+ /**
25
+ * File types to apply SSI processing to for included files (at any depth)
26
+ * SSI always applies to top-level HTML files (.html, .htm, .shtml)
27
+ * When files are included via SSI, they will also be processed if they match these types
28
+ * Examples: ['js', 'html', 'css'] - will process .js/.ts/.mjs/etc., .html/.htm/.shtml, .css/.scss/etc.
29
+ * @default [] (only process top-level HTML files)
30
+ */
31
+ includeFileTypes?: string[];
32
+ /**
33
+ * Custom file type to extension mappings
34
+ * Allows overriding or extending the default mappings
35
+ */
36
+ fileTypeMap?: FileTypeMap;
37
+ }
38
+ export default function vitePluginSsi(options?: VitePluginSsiOptions): Plugin;
package/dist/index.mjs ADDED
@@ -0,0 +1,338 @@
1
+ // src/index.ts
2
+ import * as path3 from "path";
3
+
4
+ // src/file-types.ts
5
+ var DEFAULT_FILE_TYPE_MAP = {
6
+ html: [".html", ".htm", ".shtml"],
7
+ js: [".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".mts", ".cts"],
8
+ css: [".css", ".scss", ".sass", ".less", ".styl"],
9
+ json: [".json", ".jsonc"],
10
+ xml: [".xml", ".xhtml"],
11
+ text: [".txt", ".md", ".markdown"]
12
+ };
13
+ function matchesFileType(filePath, fileTypes, fileTypeMap) {
14
+ const ext = getFileExtension(filePath);
15
+ if (!ext)
16
+ return false;
17
+ for (const fileType of fileTypes) {
18
+ const extensions = fileTypeMap[fileType.toLowerCase()] || [];
19
+ if (extensions.includes(ext)) {
20
+ return true;
21
+ }
22
+ }
23
+ return false;
24
+ }
25
+ function getFileExtension(filePath) {
26
+ const lastDot = filePath.lastIndexOf(".");
27
+ if (lastDot === -1)
28
+ return null;
29
+ const ext = filePath.substring(lastDot);
30
+ return ext && ext.length > 1 ? ext.toLowerCase() : null;
31
+ }
32
+
33
+ // src/ssi.ts
34
+ import { promises as fs } from "fs";
35
+ import * as path from "path";
36
+ function resolveIncludePath(virtualPath, includingFile, root) {
37
+ if (virtualPath.startsWith("/")) {
38
+ return path.join(root, virtualPath.slice(1));
39
+ }
40
+ const includingDir = path.dirname(includingFile);
41
+ return path.resolve(includingDir, virtualPath);
42
+ }
43
+ function normalizePath(filePath) {
44
+ return path.resolve(filePath).replace(/\\/g, "/");
45
+ }
46
+ async function processSsi(filePath, content, options) {
47
+ const { root, maxDepth, includeFileTypes = [], fileTypeMap = DEFAULT_FILE_TYPE_MAP } = options;
48
+ return processSsiRecursive(filePath, content, root, new Set, 0, maxDepth, includeFileTypes, fileTypeMap);
49
+ }
50
+ async function processSsiRecursive(filePath, content, root, seen, depth, maxDepth, includeFileTypes, fileTypeMap) {
51
+ const normalizedPath = normalizePath(filePath);
52
+ const deps = new Set;
53
+ if (seen.has(normalizedPath)) {
54
+ const seenArray = Array.from(seen);
55
+ const cycleStart = seenArray.indexOf(normalizedPath);
56
+ const cycle = seenArray.slice(cycleStart).concat(normalizedPath).join(" -> ");
57
+ return {
58
+ code: `<!-- SSI Error: Circular include detected: ${cycle} -->`,
59
+ deps
60
+ };
61
+ }
62
+ if (depth >= maxDepth) {
63
+ return {
64
+ code: `<!-- SSI Error: Maximum include depth (${maxDepth}) exceeded -->`,
65
+ deps
66
+ };
67
+ }
68
+ seen.add(normalizedPath);
69
+ const includeRegex = /<!--#include\s+virtual\s*=\s*"([^"]+)"\s*-->/g;
70
+ let match;
71
+ let result = content;
72
+ const replacements = [];
73
+ const matches = [];
74
+ while ((match = includeRegex.exec(content)) !== null) {
75
+ matches.push({
76
+ index: match.index,
77
+ length: match[0].length,
78
+ virtualPath: match[1]
79
+ });
80
+ }
81
+ for (let i = matches.length - 1;i >= 0; i--) {
82
+ const { index, length, virtualPath } = matches[i];
83
+ const matchEnd = index + length;
84
+ const resolvedPath = resolveIncludePath(virtualPath, filePath, root);
85
+ const normalizedResolvedPath = normalizePath(resolvedPath);
86
+ deps.add(normalizedResolvedPath);
87
+ try {
88
+ await fs.access(resolvedPath);
89
+ const includedContent = await fs.readFile(resolvedPath, "utf-8");
90
+ const shouldProcessSsi = includeFileTypes.length === 0 ? false : matchesFileType(resolvedPath, includeFileTypes, fileTypeMap);
91
+ let processed;
92
+ if (shouldProcessSsi) {
93
+ processed = await processSsiRecursive(resolvedPath, includedContent, root, new Set(seen), depth + 1, maxDepth, includeFileTypes, fileTypeMap);
94
+ } else {
95
+ processed = {
96
+ code: includedContent,
97
+ deps: new Set
98
+ };
99
+ }
100
+ processed.deps.forEach((dep) => deps.add(dep));
101
+ replacements.push({
102
+ start: index,
103
+ end: matchEnd,
104
+ replacement: processed.code
105
+ });
106
+ } catch (error) {
107
+ replacements.push({
108
+ start: index,
109
+ end: matchEnd,
110
+ replacement: `<!-- SSI Error: File not found: ${normalizedResolvedPath} -->`
111
+ });
112
+ }
113
+ }
114
+ for (const { start, end, replacement } of replacements) {
115
+ result = result.slice(0, start) + replacement + result.slice(end);
116
+ }
117
+ return { code: result, deps };
118
+ }
119
+
120
+ // src/dev-server.ts
121
+ import { promises as fs2 } from "fs";
122
+ import * as path2 from "path";
123
+ function setupPreviewServer(previewServer, options) {
124
+ const outDir = previewServer.config.build.outDir || "dist";
125
+ const projectRoot = previewServer.config.root;
126
+ const distRoot = path2.resolve(projectRoot, outDir);
127
+ const processOptions = {
128
+ root: projectRoot,
129
+ maxDepth: options.maxDepth,
130
+ includeFileTypes: options.includeFileTypes,
131
+ fileTypeMap: options.fileTypeMap
132
+ };
133
+ previewServer.middlewares.use(async (req, res, next) => {
134
+ try {
135
+ if (!req.url || req.method !== "GET")
136
+ return next();
137
+ let reqPath = req.url.split("?")[0];
138
+ if (reqPath === "/" || reqPath === "") {
139
+ reqPath = "/index.html";
140
+ }
141
+ if (!reqPath.endsWith(".html"))
142
+ return next();
143
+ const filePath = path2.join(distRoot, reqPath.startsWith("/") ? reqPath.slice(1) : reqPath);
144
+ const exists = await fs2.access(filePath).then(() => true).catch(() => false);
145
+ if (!exists)
146
+ return next();
147
+ const raw = await fs2.readFile(filePath, "utf-8");
148
+ const relativePath = path2.relative(distRoot, filePath);
149
+ const sourceFilePath = path2.join(projectRoot, relativePath);
150
+ const result = await processSsi(sourceFilePath, raw, processOptions);
151
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
152
+ res.end(result.code);
153
+ return;
154
+ } catch (err) {
155
+ return next();
156
+ }
157
+ });
158
+ }
159
+ function handleHotUpdate(ctx, server, reverseDependencyMap) {
160
+ const changedFile = normalizePath(ctx.file);
161
+ const affectedHtmlFiles = reverseDependencyMap.get(changedFile);
162
+ if (!affectedHtmlFiles || affectedHtmlFiles.size === 0) {
163
+ return;
164
+ }
165
+ const affectedModules = [];
166
+ for (const htmlFile of affectedHtmlFiles) {
167
+ const modulesByFile = server.moduleGraph.getModulesByFile(htmlFile);
168
+ if (modulesByFile && modulesByFile.size > 0) {
169
+ modulesByFile.forEach((module) => {
170
+ affectedModules.push(module);
171
+ });
172
+ } else {
173
+ try {
174
+ const relativePath = path2.relative(server.config.root, htmlFile);
175
+ const url = `/${relativePath.replace(/\\/g, "/")}`;
176
+ const urlModule = server.moduleGraph.urlToModuleMap.get(url);
177
+ if (urlModule) {
178
+ affectedModules.push(urlModule);
179
+ }
180
+ } catch {}
181
+ }
182
+ }
183
+ if (affectedModules.length > 0) {
184
+ affectedModules.forEach((module) => {
185
+ try {
186
+ server.moduleGraph.invalidateModule(module);
187
+ if ("reloadModule" in server && typeof server.reloadModule === "function") {
188
+ server.reloadModule(module);
189
+ }
190
+ } catch {
191
+ server.ws.send({ type: "full-reload" });
192
+ }
193
+ });
194
+ return affectedModules;
195
+ }
196
+ if (affectedHtmlFiles.size > 0) {
197
+ server.ws.send({ type: "full-reload" });
198
+ }
199
+ }
200
+ async function transformIndexHtml(html, ctx, options) {
201
+ const root = ctx.server?.config.root || options.root || process.cwd();
202
+ const filename = ctx.filename || "index.html";
203
+ const filePath = path2.isAbsolute(filename) ? filename : path2.resolve(root, filename);
204
+ try {
205
+ const processOptions = {
206
+ root,
207
+ maxDepth: options.maxDepth,
208
+ includeFileTypes: options.includeFileTypes,
209
+ fileTypeMap: options.fileTypeMap
210
+ };
211
+ const result = await processSsi(filePath, html, processOptions);
212
+ return result.code;
213
+ } catch (error) {
214
+ return `<!-- SSI Error: ${error instanceof Error ? error.message : String(error)} -->
215
+ ${html}`;
216
+ }
217
+ }
218
+
219
+ // src/index.ts
220
+ function normalizeApplyOption(apply) {
221
+ if (!apply) {
222
+ return;
223
+ }
224
+ if (typeof apply === "string") {
225
+ if (apply === "preview") {
226
+ return;
227
+ }
228
+ return apply;
229
+ }
230
+ if (apply.serve === true && !apply.build) {
231
+ return "serve";
232
+ }
233
+ if (apply.build === true && !apply.serve) {
234
+ return "build";
235
+ }
236
+ return;
237
+ }
238
+ function shouldApplyInEnvironment(apply, command) {
239
+ if (!apply) {
240
+ return true;
241
+ }
242
+ if (typeof apply === "string") {
243
+ return apply === command;
244
+ }
245
+ if (command === "serve") {
246
+ return apply.serve !== false;
247
+ }
248
+ if (command === "build") {
249
+ return apply.build !== false;
250
+ }
251
+ if (command === "preview") {
252
+ return apply.preview !== false;
253
+ }
254
+ return true;
255
+ }
256
+ function vitePluginSsi(options = {}) {
257
+ const {
258
+ maxDepth = 10,
259
+ enforce = "pre",
260
+ apply: applyOption,
261
+ includeFileTypes = [],
262
+ fileTypeMap
263
+ } = options;
264
+ const mergedFileTypeMap = {
265
+ ...DEFAULT_FILE_TYPE_MAP,
266
+ ...fileTypeMap
267
+ };
268
+ const dependencyGraph = new Map;
269
+ const reverseDependencyMap = new Map;
270
+ let server;
271
+ let command = "serve";
272
+ let resolvedRoot;
273
+ return {
274
+ name: "vite-plugin-ssi",
275
+ enforce,
276
+ apply: normalizeApplyOption(applyOption),
277
+ configResolved(config) {
278
+ command = config.command || "serve";
279
+ resolvedRoot = config.root;
280
+ },
281
+ configureServer(_server) {
282
+ server = _server;
283
+ },
284
+ configurePreviewServer(previewServer) {
285
+ if (!shouldApplyInEnvironment(applyOption, "preview")) {
286
+ return () => {};
287
+ }
288
+ setupPreviewServer(previewServer, {
289
+ maxDepth,
290
+ includeFileTypes,
291
+ fileTypeMap: mergedFileTypeMap
292
+ });
293
+ },
294
+ async transformIndexHtml(html, ctx) {
295
+ const currentCommand = ctx.server ? ctx.server.config.command || "serve" : command;
296
+ if (!shouldApplyInEnvironment(applyOption, currentCommand)) {
297
+ return html;
298
+ }
299
+ const root = ctx.server?.config.root || resolvedRoot || process.cwd();
300
+ const result = await transformIndexHtml(html, ctx, {
301
+ root,
302
+ maxDepth,
303
+ includeFileTypes,
304
+ fileTypeMap: mergedFileTypeMap
305
+ });
306
+ const filename = ctx.filename || "index.html";
307
+ const filePath = path3.isAbsolute(filename) ? filename : path3.resolve(root, filename);
308
+ const normalizedFilePath = normalizePath(filePath);
309
+ const processOptions = {
310
+ root,
311
+ maxDepth,
312
+ includeFileTypes,
313
+ fileTypeMap: mergedFileTypeMap
314
+ };
315
+ const depsResult = await processSsi(filePath, html, processOptions);
316
+ dependencyGraph.set(normalizedFilePath, depsResult.deps);
317
+ depsResult.deps.forEach((dep) => {
318
+ if (!reverseDependencyMap.has(dep)) {
319
+ reverseDependencyMap.set(dep, new Set);
320
+ }
321
+ reverseDependencyMap.get(dep).add(normalizedFilePath);
322
+ });
323
+ return result;
324
+ },
325
+ handleHotUpdate(ctx) {
326
+ if (!shouldApplyInEnvironment(applyOption, command)) {
327
+ return;
328
+ }
329
+ if (!server) {
330
+ return;
331
+ }
332
+ return handleHotUpdate(ctx, server, reverseDependencyMap);
333
+ }
334
+ };
335
+ }
336
+ export {
337
+ vitePluginSsi as default
338
+ };
package/dist/ssi.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { type FileTypeMap } from './file-types';
2
+ export interface ProcessResult {
3
+ code: string;
4
+ deps: Set<string>;
5
+ }
6
+ export interface ProcessSsiOptions {
7
+ root: string;
8
+ maxDepth: number;
9
+ includeFileTypes?: string[];
10
+ fileTypeMap?: FileTypeMap;
11
+ }
12
+ /**
13
+ * Normalizes a file path to absolute path for consistent comparison
14
+ */
15
+ export declare function normalizePath(filePath: string): string;
16
+ /**
17
+ * Processes SSI includes recursively
18
+ */
19
+ export declare function processSsi(filePath: string, content: string, options: ProcessSsiOptions): Promise<ProcessResult>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catfyrr/vite-plugin-ssi",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Fully Apache/Nginx compatible Server-Side Includes (SSI) Vite plugin",
5
5
  "type": "module",
6
6
  "exports": {
@@ -18,9 +18,10 @@
18
18
  "node": ">=18.0.0"
19
19
  },
20
20
  "scripts": {
21
- "build": "bun run build:esm && bun run build:cjs",
22
- "build:esm": "bun build ./src/index.ts --outdir dist --target bun --format esm",
23
- "build:cjs": "bun build ./src/index.ts --outdir dist --target bun --format cjs",
21
+ "build": "bun run build:esm && bun run build:cjs && bun run build:types",
22
+ "build:esm": "bun build ./src/index.ts --outfile dist/index.mjs --target node --format esm",
23
+ "build:cjs": "bun build ./src/index.ts --outfile dist/index.cjs --target node --format cjs",
24
+ "build:types": "tsc --emitDeclarationOnly --declaration --outDir dist",
24
25
  "test": "bun test",
25
26
  "test:watch": "bun test --watch",
26
27
  "lint": "eslint src tests",
@@ -38,7 +39,7 @@
38
39
  "license": "MIT",
39
40
  "repository": {
40
41
  "type": "git",
41
- "url": "https://github.com/catFurr/vite-plugin-ssi.git"
42
+ "url": "git+https://github.com/catFurr/vite-plugin-ssi.git"
42
43
  },
43
44
  "bugs": {
44
45
  "url": "https://github.com/catFurr/vite-plugin-ssi/issues"