@ethisyscore/vite-plugin 1.0.0-alpha.12 → 1.0.0-alpha.13
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/dist/index.cjs +34 -12
- package/dist/index.js +35 -13
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -31,29 +31,46 @@ function slash(p) {
|
|
|
31
31
|
function escapeHtml(str) {
|
|
32
32
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
33
33
|
}
|
|
34
|
+
function assertSafeRelativePath(value, label) {
|
|
35
|
+
const normalized = (0, import_node_path.normalize)(value);
|
|
36
|
+
if ((0, import_node_path.isAbsolute)(value) || (0, import_node_path.isAbsolute)(normalized)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`[ethisys-manifest] ${label} "${value}" must be relative, not absolute.`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (normalized.includes("..")) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`[ethisys-manifest] ${label} "${value}" must not contain path traversal.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
34
47
|
function ethisysManifestPlugin(options = {}) {
|
|
35
48
|
const manifestRelPath = options.manifestPath ?? "../extension.manifest.json";
|
|
36
49
|
const mountId = options.mountId ?? "root";
|
|
37
50
|
let entries = [];
|
|
38
51
|
let rootDir;
|
|
39
52
|
let manifestAbsPath;
|
|
53
|
+
const htmlCache = /* @__PURE__ */ new Map();
|
|
40
54
|
function generateHtml(entry) {
|
|
41
55
|
if (entry.template) {
|
|
56
|
+
assertSafeRelativePath(entry.template, "Template path");
|
|
42
57
|
const templatePath = (0, import_node_path.resolve)(rootDir, entry.template);
|
|
43
58
|
if ((0, import_node_fs.existsSync)(templatePath)) {
|
|
44
59
|
let html = (0, import_node_fs.readFileSync)(templatePath, "utf-8");
|
|
45
|
-
const
|
|
60
|
+
const safeMountId2 = escapeHtml(mountId);
|
|
61
|
+
const mountPattern = new RegExp(`id=["']${safeMountId2}["']`);
|
|
46
62
|
if (!mountPattern.test(html)) {
|
|
47
63
|
throw new Error(
|
|
48
64
|
`[ethisys-manifest] Custom template "${entry.template}" must contain an element with id="${mountId}".`
|
|
49
65
|
);
|
|
50
66
|
}
|
|
51
|
-
html = html.
|
|
67
|
+
html = html.replaceAll("{{SOURCE}}", escapeHtml(entry.source));
|
|
52
68
|
return html;
|
|
53
69
|
}
|
|
54
70
|
}
|
|
55
71
|
const safeTitle = escapeHtml(entry.title ?? "Plugin");
|
|
56
72
|
const safeSource = escapeHtml(entry.source);
|
|
73
|
+
const safeMountId = escapeHtml(mountId);
|
|
57
74
|
return `<!doctype html>
|
|
58
75
|
<html lang="en">
|
|
59
76
|
<head>
|
|
@@ -62,7 +79,7 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
62
79
|
<title>${safeTitle}</title>
|
|
63
80
|
</head>
|
|
64
81
|
<body>
|
|
65
|
-
<div id="${
|
|
82
|
+
<div id="${safeMountId}"></div>
|
|
66
83
|
<script type="module" src="${safeSource}"></script>
|
|
67
84
|
</body>
|
|
68
85
|
</html>`;
|
|
@@ -97,11 +114,7 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
97
114
|
const entrypointSources = /* @__PURE__ */ new Map();
|
|
98
115
|
for (const entry of entries) {
|
|
99
116
|
const source = entry.source;
|
|
100
|
-
|
|
101
|
-
throw new Error(
|
|
102
|
-
`[ethisys-manifest] Source path "${source}" must be relative with no traversal.`
|
|
103
|
-
);
|
|
104
|
-
}
|
|
117
|
+
assertSafeRelativePath(source, "Source path");
|
|
105
118
|
const sourcePath = (0, import_node_path.resolve)(rootDir, source);
|
|
106
119
|
if (!(0, import_node_fs.existsSync)(sourcePath)) {
|
|
107
120
|
throw new Error(
|
|
@@ -163,8 +176,12 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
163
176
|
const filename = url === "/" || url === "/index.html" ? "index.html" : url.slice(1);
|
|
164
177
|
const entry = entries.find((e) => e.entrypoint === filename);
|
|
165
178
|
if (entry) {
|
|
166
|
-
|
|
167
|
-
|
|
179
|
+
const cacheKey = `entry:${filename}`;
|
|
180
|
+
if (!htmlCache.has(cacheKey)) {
|
|
181
|
+
const rawHtml = generateHtml(entry);
|
|
182
|
+
htmlCache.set(cacheKey, server.transformIndexHtml(url, rawHtml));
|
|
183
|
+
}
|
|
184
|
+
const html = await htmlCache.get(cacheKey);
|
|
168
185
|
res.setHeader("Content-Type", "text/html");
|
|
169
186
|
res.statusCode = 200;
|
|
170
187
|
res.end(html);
|
|
@@ -176,8 +193,12 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
176
193
|
(e) => e.entrypoint === "index.html"
|
|
177
194
|
);
|
|
178
195
|
if (mainEntry) {
|
|
179
|
-
|
|
180
|
-
|
|
196
|
+
const cacheKey = "spa:index.html";
|
|
197
|
+
if (!htmlCache.has(cacheKey)) {
|
|
198
|
+
const rawHtml = generateHtml(mainEntry);
|
|
199
|
+
htmlCache.set(cacheKey, server.transformIndexHtml("/", rawHtml));
|
|
200
|
+
}
|
|
201
|
+
const html = await htmlCache.get(cacheKey);
|
|
181
202
|
res.setHeader("Content-Type", "text/html");
|
|
182
203
|
res.statusCode = 200;
|
|
183
204
|
res.end(html);
|
|
@@ -198,6 +219,7 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
198
219
|
try {
|
|
199
220
|
entries = readManifest();
|
|
200
221
|
validateEntries();
|
|
222
|
+
htmlCache.clear();
|
|
201
223
|
} catch (e) {
|
|
202
224
|
server.config.logger.error(String(e));
|
|
203
225
|
return [];
|
package/dist/index.js
CHANGED
|
@@ -1,35 +1,52 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { readFileSync, existsSync } from "fs";
|
|
3
|
-
import { resolve, sep } from "path";
|
|
3
|
+
import { isAbsolute, normalize, resolve, sep } from "path";
|
|
4
4
|
function slash(p) {
|
|
5
5
|
return sep === "\\" ? p.replace(/\\/g, "/") : p;
|
|
6
6
|
}
|
|
7
7
|
function escapeHtml(str) {
|
|
8
8
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
9
9
|
}
|
|
10
|
+
function assertSafeRelativePath(value, label) {
|
|
11
|
+
const normalized = normalize(value);
|
|
12
|
+
if (isAbsolute(value) || isAbsolute(normalized)) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`[ethisys-manifest] ${label} "${value}" must be relative, not absolute.`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
if (normalized.includes("..")) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`[ethisys-manifest] ${label} "${value}" must not contain path traversal.`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
10
23
|
function ethisysManifestPlugin(options = {}) {
|
|
11
24
|
const manifestRelPath = options.manifestPath ?? "../extension.manifest.json";
|
|
12
25
|
const mountId = options.mountId ?? "root";
|
|
13
26
|
let entries = [];
|
|
14
27
|
let rootDir;
|
|
15
28
|
let manifestAbsPath;
|
|
29
|
+
const htmlCache = /* @__PURE__ */ new Map();
|
|
16
30
|
function generateHtml(entry) {
|
|
17
31
|
if (entry.template) {
|
|
32
|
+
assertSafeRelativePath(entry.template, "Template path");
|
|
18
33
|
const templatePath = resolve(rootDir, entry.template);
|
|
19
34
|
if (existsSync(templatePath)) {
|
|
20
35
|
let html = readFileSync(templatePath, "utf-8");
|
|
21
|
-
const
|
|
36
|
+
const safeMountId2 = escapeHtml(mountId);
|
|
37
|
+
const mountPattern = new RegExp(`id=["']${safeMountId2}["']`);
|
|
22
38
|
if (!mountPattern.test(html)) {
|
|
23
39
|
throw new Error(
|
|
24
40
|
`[ethisys-manifest] Custom template "${entry.template}" must contain an element with id="${mountId}".`
|
|
25
41
|
);
|
|
26
42
|
}
|
|
27
|
-
html = html.
|
|
43
|
+
html = html.replaceAll("{{SOURCE}}", escapeHtml(entry.source));
|
|
28
44
|
return html;
|
|
29
45
|
}
|
|
30
46
|
}
|
|
31
47
|
const safeTitle = escapeHtml(entry.title ?? "Plugin");
|
|
32
48
|
const safeSource = escapeHtml(entry.source);
|
|
49
|
+
const safeMountId = escapeHtml(mountId);
|
|
33
50
|
return `<!doctype html>
|
|
34
51
|
<html lang="en">
|
|
35
52
|
<head>
|
|
@@ -38,7 +55,7 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
38
55
|
<title>${safeTitle}</title>
|
|
39
56
|
</head>
|
|
40
57
|
<body>
|
|
41
|
-
<div id="${
|
|
58
|
+
<div id="${safeMountId}"></div>
|
|
42
59
|
<script type="module" src="${safeSource}"></script>
|
|
43
60
|
</body>
|
|
44
61
|
</html>`;
|
|
@@ -73,11 +90,7 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
73
90
|
const entrypointSources = /* @__PURE__ */ new Map();
|
|
74
91
|
for (const entry of entries) {
|
|
75
92
|
const source = entry.source;
|
|
76
|
-
|
|
77
|
-
throw new Error(
|
|
78
|
-
`[ethisys-manifest] Source path "${source}" must be relative with no traversal.`
|
|
79
|
-
);
|
|
80
|
-
}
|
|
93
|
+
assertSafeRelativePath(source, "Source path");
|
|
81
94
|
const sourcePath = resolve(rootDir, source);
|
|
82
95
|
if (!existsSync(sourcePath)) {
|
|
83
96
|
throw new Error(
|
|
@@ -139,8 +152,12 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
139
152
|
const filename = url === "/" || url === "/index.html" ? "index.html" : url.slice(1);
|
|
140
153
|
const entry = entries.find((e) => e.entrypoint === filename);
|
|
141
154
|
if (entry) {
|
|
142
|
-
|
|
143
|
-
|
|
155
|
+
const cacheKey = `entry:${filename}`;
|
|
156
|
+
if (!htmlCache.has(cacheKey)) {
|
|
157
|
+
const rawHtml = generateHtml(entry);
|
|
158
|
+
htmlCache.set(cacheKey, server.transformIndexHtml(url, rawHtml));
|
|
159
|
+
}
|
|
160
|
+
const html = await htmlCache.get(cacheKey);
|
|
144
161
|
res.setHeader("Content-Type", "text/html");
|
|
145
162
|
res.statusCode = 200;
|
|
146
163
|
res.end(html);
|
|
@@ -152,8 +169,12 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
152
169
|
(e) => e.entrypoint === "index.html"
|
|
153
170
|
);
|
|
154
171
|
if (mainEntry) {
|
|
155
|
-
|
|
156
|
-
|
|
172
|
+
const cacheKey = "spa:index.html";
|
|
173
|
+
if (!htmlCache.has(cacheKey)) {
|
|
174
|
+
const rawHtml = generateHtml(mainEntry);
|
|
175
|
+
htmlCache.set(cacheKey, server.transformIndexHtml("/", rawHtml));
|
|
176
|
+
}
|
|
177
|
+
const html = await htmlCache.get(cacheKey);
|
|
157
178
|
res.setHeader("Content-Type", "text/html");
|
|
158
179
|
res.statusCode = 200;
|
|
159
180
|
res.end(html);
|
|
@@ -174,6 +195,7 @@ function ethisysManifestPlugin(options = {}) {
|
|
|
174
195
|
try {
|
|
175
196
|
entries = readManifest();
|
|
176
197
|
validateEntries();
|
|
198
|
+
htmlCache.clear();
|
|
177
199
|
} catch (e) {
|
|
178
200
|
server.config.logger.error(String(e));
|
|
179
201
|
return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethisyscore/vite-plugin",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.13",
|
|
4
4
|
"description": "Vite plugin for manifest-driven HTML entry points in EthisysCore plugins. Reads feature.manifest.json and generates HTML shells in-memory — zero boilerplate HTML files needed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|