@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 CHANGED
@@ -31,29 +31,46 @@ function slash(p) {
31
31
  function escapeHtml(str) {
32
32
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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 mountPattern = new RegExp(`id=["']${mountId}["']`);
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.replace("{{SOURCE}}", entry.source);
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="${mountId}"></div>
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
- if (source.includes("..") || source.startsWith("/")) {
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
- let html = generateHtml(entry);
167
- html = await server.transformIndexHtml(url, html);
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
- let html = generateHtml(mainEntry);
180
- html = await server.transformIndexHtml(url, html);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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 mountPattern = new RegExp(`id=["']${mountId}["']`);
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.replace("{{SOURCE}}", entry.source);
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="${mountId}"></div>
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
- if (source.includes("..") || source.startsWith("/")) {
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
- let html = generateHtml(entry);
143
- html = await server.transformIndexHtml(url, html);
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
- let html = generateHtml(mainEntry);
156
- html = await server.transformIndexHtml(url, html);
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.12",
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",