@ethisyscore/vite-plugin 1.0.0-alpha.12

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 ADDED
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ethisysManifestPlugin: () => ethisysManifestPlugin
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var import_node_fs = require("fs");
27
+ var import_node_path = require("path");
28
+ function slash(p) {
29
+ return import_node_path.sep === "\\" ? p.replace(/\\/g, "/") : p;
30
+ }
31
+ function escapeHtml(str) {
32
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
33
+ }
34
+ function ethisysManifestPlugin(options = {}) {
35
+ const manifestRelPath = options.manifestPath ?? "../extension.manifest.json";
36
+ const mountId = options.mountId ?? "root";
37
+ let entries = [];
38
+ let rootDir;
39
+ let manifestAbsPath;
40
+ function generateHtml(entry) {
41
+ if (entry.template) {
42
+ const templatePath = (0, import_node_path.resolve)(rootDir, entry.template);
43
+ if ((0, import_node_fs.existsSync)(templatePath)) {
44
+ let html = (0, import_node_fs.readFileSync)(templatePath, "utf-8");
45
+ const mountPattern = new RegExp(`id=["']${mountId}["']`);
46
+ if (!mountPattern.test(html)) {
47
+ throw new Error(
48
+ `[ethisys-manifest] Custom template "${entry.template}" must contain an element with id="${mountId}".`
49
+ );
50
+ }
51
+ html = html.replace("{{SOURCE}}", entry.source);
52
+ return html;
53
+ }
54
+ }
55
+ const safeTitle = escapeHtml(entry.title ?? "Plugin");
56
+ const safeSource = escapeHtml(entry.source);
57
+ return `<!doctype html>
58
+ <html lang="en">
59
+ <head>
60
+ <meta charset="UTF-8" />
61
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
62
+ <title>${safeTitle}</title>
63
+ </head>
64
+ <body>
65
+ <div id="${mountId}"></div>
66
+ <script type="module" src="${safeSource}"></script>
67
+ </body>
68
+ </html>`;
69
+ }
70
+ function readManifest() {
71
+ if (!(0, import_node_fs.existsSync)(manifestAbsPath)) {
72
+ return [];
73
+ }
74
+ const raw = (0, import_node_fs.readFileSync)(manifestAbsPath, "utf-8");
75
+ let manifest;
76
+ try {
77
+ manifest = JSON.parse(raw);
78
+ } catch {
79
+ throw new Error(
80
+ `[ethisys-manifest] Failed to parse manifest at "${manifestAbsPath}".`
81
+ );
82
+ }
83
+ const result = [];
84
+ for (const page of manifest.ui?.pages ?? []) {
85
+ if (page.source) {
86
+ result.push(page);
87
+ }
88
+ }
89
+ for (const surface of manifest.ui?.surfaces ?? []) {
90
+ if (surface.source) {
91
+ result.push(surface);
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+ function validateEntries() {
97
+ const entrypointSources = /* @__PURE__ */ new Map();
98
+ for (const entry of entries) {
99
+ 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
+ }
105
+ const sourcePath = (0, import_node_path.resolve)(rootDir, source);
106
+ if (!(0, import_node_fs.existsSync)(sourcePath)) {
107
+ throw new Error(
108
+ `[ethisys-manifest] Source file "${source}" for entry "${entry.entrypoint}" does not exist.`
109
+ );
110
+ }
111
+ const existingSource = entrypointSources.get(entry.entrypoint);
112
+ if (existingSource && existingSource !== source) {
113
+ throw new Error(
114
+ `[ethisys-manifest] Conflicting sources for entrypoint "${entry.entrypoint}": "${existingSource}" and "${source}". Only one source file can be mapped to a single entrypoint.`
115
+ );
116
+ }
117
+ entrypointSources.set(entry.entrypoint, source);
118
+ }
119
+ }
120
+ return {
121
+ name: "ethisys-manifest",
122
+ enforce: "pre",
123
+ config(config, { command }) {
124
+ rootDir = config.root ?? process.cwd();
125
+ manifestAbsPath = (0, import_node_path.resolve)(rootDir, manifestRelPath);
126
+ entries = readManifest();
127
+ if (entries.length === 0) {
128
+ return;
129
+ }
130
+ validateEntries();
131
+ if (command === "build") {
132
+ const input = {};
133
+ const seen = /* @__PURE__ */ new Set();
134
+ for (const entry of entries) {
135
+ if (seen.has(entry.entrypoint)) {
136
+ continue;
137
+ }
138
+ seen.add(entry.entrypoint);
139
+ const name = entry.entrypoint.replace(/\.html$/, "");
140
+ input[name] = slash((0, import_node_path.resolve)(rootDir, entry.entrypoint));
141
+ }
142
+ return {
143
+ build: {
144
+ rollupOptions: {
145
+ input
146
+ }
147
+ }
148
+ };
149
+ }
150
+ },
151
+ // Dev server: intercept HTML requests and serve virtual HTML.
152
+ // Pre-middleware — runs before Vite's built-in handlers.
153
+ // Safe because we only respond to .html requests and SPA navigation;
154
+ // module requests (/@vite/client, /@react-refresh, .ts/.tsx/.css files)
155
+ // don't match our conditions and pass through to Vite's transform middleware.
156
+ configureServer(server) {
157
+ server.watcher.add(manifestAbsPath);
158
+ server.middlewares.use(async (req, res, next) => {
159
+ try {
160
+ const url = req.url?.split("?")[0] ?? "";
161
+ const acceptsHtml = req.headers.accept?.includes("text/html");
162
+ if (url.endsWith(".html") || url === "/" || url === "/index.html") {
163
+ const filename = url === "/" || url === "/index.html" ? "index.html" : url.slice(1);
164
+ const entry = entries.find((e) => e.entrypoint === filename);
165
+ if (entry) {
166
+ let html = generateHtml(entry);
167
+ html = await server.transformIndexHtml(url, html);
168
+ res.setHeader("Content-Type", "text/html");
169
+ res.statusCode = 200;
170
+ res.end(html);
171
+ return;
172
+ }
173
+ }
174
+ if (acceptsHtml && !url.includes(".")) {
175
+ const mainEntry = entries.find(
176
+ (e) => e.entrypoint === "index.html"
177
+ );
178
+ if (mainEntry) {
179
+ let html = generateHtml(mainEntry);
180
+ html = await server.transformIndexHtml(url, html);
181
+ res.setHeader("Content-Type", "text/html");
182
+ res.statusCode = 200;
183
+ res.end(html);
184
+ return;
185
+ }
186
+ }
187
+ next();
188
+ } catch (err) {
189
+ next(err);
190
+ }
191
+ });
192
+ },
193
+ // Watch manifest for changes during dev — reload entries on edit.
194
+ // Entire handler is wrapped in try/catch so JSON syntax errors in the
195
+ // manifest don't crash the dev server (common during active editing).
196
+ handleHotUpdate({ file, server }) {
197
+ if (slash(file) === slash(manifestAbsPath)) {
198
+ try {
199
+ entries = readManifest();
200
+ validateEntries();
201
+ } catch (e) {
202
+ server.config.logger.error(String(e));
203
+ return [];
204
+ }
205
+ server.config.logger.info(
206
+ "[ethisys-manifest] Manifest changed \u2014 reloading entries"
207
+ );
208
+ server.hot.send({ type: "full-reload" });
209
+ return [];
210
+ }
211
+ },
212
+ // Build: resolve virtual HTML module IDs (no physical files exist on disk)
213
+ resolveId(id) {
214
+ const normalizedId = slash(id);
215
+ const match = entries.find(
216
+ (e) => slash((0, import_node_path.resolve)(rootDir, e.entrypoint)) === normalizedId
217
+ );
218
+ if (match) {
219
+ return id;
220
+ }
221
+ },
222
+ // Build: provide virtual HTML content for Rollup
223
+ load(id) {
224
+ const normalizedId = slash(id);
225
+ const match = entries.find(
226
+ (e) => slash((0, import_node_path.resolve)(rootDir, e.entrypoint)) === normalizedId
227
+ );
228
+ if (match) {
229
+ return generateHtml(match);
230
+ }
231
+ }
232
+ };
233
+ }
234
+ // Annotate the CommonJS export names for ESM import in node:
235
+ 0 && (module.exports = {
236
+ ethisysManifestPlugin
237
+ });
@@ -0,0 +1,26 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ interface EthisysPluginOptions {
4
+ /**
5
+ * Path to the manifest file, relative to Vite's root directory.
6
+ * @default "../extension.manifest.json"
7
+ */
8
+ manifestPath?: string;
9
+ /**
10
+ * ID of the mount element in the generated HTML.
11
+ * React plugins use "root", Vue plugins use "app".
12
+ * @default "root"
13
+ */
14
+ mountId?: string;
15
+ }
16
+ /**
17
+ * Vite plugin that reads `extension.manifest.json` (or a custom manifest path)
18
+ * and auto-generates HTML entry points for all pages and surfaces that declare
19
+ * a `source` field. No physical HTML files are required.
20
+ *
21
+ * Pages without a `source` field share the main entry point (SPA mode).
22
+ * Surfaces (dialogs, panels) each get their own HTML shell.
23
+ */
24
+ declare function ethisysManifestPlugin(options?: EthisysPluginOptions): Plugin;
25
+
26
+ export { type EthisysPluginOptions, ethisysManifestPlugin };
@@ -0,0 +1,26 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ interface EthisysPluginOptions {
4
+ /**
5
+ * Path to the manifest file, relative to Vite's root directory.
6
+ * @default "../extension.manifest.json"
7
+ */
8
+ manifestPath?: string;
9
+ /**
10
+ * ID of the mount element in the generated HTML.
11
+ * React plugins use "root", Vue plugins use "app".
12
+ * @default "root"
13
+ */
14
+ mountId?: string;
15
+ }
16
+ /**
17
+ * Vite plugin that reads `extension.manifest.json` (or a custom manifest path)
18
+ * and auto-generates HTML entry points for all pages and surfaces that declare
19
+ * a `source` field. No physical HTML files are required.
20
+ *
21
+ * Pages without a `source` field share the main entry point (SPA mode).
22
+ * Surfaces (dialogs, panels) each get their own HTML shell.
23
+ */
24
+ declare function ethisysManifestPlugin(options?: EthisysPluginOptions): Plugin;
25
+
26
+ export { type EthisysPluginOptions, ethisysManifestPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,212 @@
1
+ // src/index.ts
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { resolve, sep } from "path";
4
+ function slash(p) {
5
+ return sep === "\\" ? p.replace(/\\/g, "/") : p;
6
+ }
7
+ function escapeHtml(str) {
8
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
9
+ }
10
+ function ethisysManifestPlugin(options = {}) {
11
+ const manifestRelPath = options.manifestPath ?? "../extension.manifest.json";
12
+ const mountId = options.mountId ?? "root";
13
+ let entries = [];
14
+ let rootDir;
15
+ let manifestAbsPath;
16
+ function generateHtml(entry) {
17
+ if (entry.template) {
18
+ const templatePath = resolve(rootDir, entry.template);
19
+ if (existsSync(templatePath)) {
20
+ let html = readFileSync(templatePath, "utf-8");
21
+ const mountPattern = new RegExp(`id=["']${mountId}["']`);
22
+ if (!mountPattern.test(html)) {
23
+ throw new Error(
24
+ `[ethisys-manifest] Custom template "${entry.template}" must contain an element with id="${mountId}".`
25
+ );
26
+ }
27
+ html = html.replace("{{SOURCE}}", entry.source);
28
+ return html;
29
+ }
30
+ }
31
+ const safeTitle = escapeHtml(entry.title ?? "Plugin");
32
+ const safeSource = escapeHtml(entry.source);
33
+ return `<!doctype html>
34
+ <html lang="en">
35
+ <head>
36
+ <meta charset="UTF-8" />
37
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
38
+ <title>${safeTitle}</title>
39
+ </head>
40
+ <body>
41
+ <div id="${mountId}"></div>
42
+ <script type="module" src="${safeSource}"></script>
43
+ </body>
44
+ </html>`;
45
+ }
46
+ function readManifest() {
47
+ if (!existsSync(manifestAbsPath)) {
48
+ return [];
49
+ }
50
+ const raw = readFileSync(manifestAbsPath, "utf-8");
51
+ let manifest;
52
+ try {
53
+ manifest = JSON.parse(raw);
54
+ } catch {
55
+ throw new Error(
56
+ `[ethisys-manifest] Failed to parse manifest at "${manifestAbsPath}".`
57
+ );
58
+ }
59
+ const result = [];
60
+ for (const page of manifest.ui?.pages ?? []) {
61
+ if (page.source) {
62
+ result.push(page);
63
+ }
64
+ }
65
+ for (const surface of manifest.ui?.surfaces ?? []) {
66
+ if (surface.source) {
67
+ result.push(surface);
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+ function validateEntries() {
73
+ const entrypointSources = /* @__PURE__ */ new Map();
74
+ for (const entry of entries) {
75
+ 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
+ }
81
+ const sourcePath = resolve(rootDir, source);
82
+ if (!existsSync(sourcePath)) {
83
+ throw new Error(
84
+ `[ethisys-manifest] Source file "${source}" for entry "${entry.entrypoint}" does not exist.`
85
+ );
86
+ }
87
+ const existingSource = entrypointSources.get(entry.entrypoint);
88
+ if (existingSource && existingSource !== source) {
89
+ throw new Error(
90
+ `[ethisys-manifest] Conflicting sources for entrypoint "${entry.entrypoint}": "${existingSource}" and "${source}". Only one source file can be mapped to a single entrypoint.`
91
+ );
92
+ }
93
+ entrypointSources.set(entry.entrypoint, source);
94
+ }
95
+ }
96
+ return {
97
+ name: "ethisys-manifest",
98
+ enforce: "pre",
99
+ config(config, { command }) {
100
+ rootDir = config.root ?? process.cwd();
101
+ manifestAbsPath = resolve(rootDir, manifestRelPath);
102
+ entries = readManifest();
103
+ if (entries.length === 0) {
104
+ return;
105
+ }
106
+ validateEntries();
107
+ if (command === "build") {
108
+ const input = {};
109
+ const seen = /* @__PURE__ */ new Set();
110
+ for (const entry of entries) {
111
+ if (seen.has(entry.entrypoint)) {
112
+ continue;
113
+ }
114
+ seen.add(entry.entrypoint);
115
+ const name = entry.entrypoint.replace(/\.html$/, "");
116
+ input[name] = slash(resolve(rootDir, entry.entrypoint));
117
+ }
118
+ return {
119
+ build: {
120
+ rollupOptions: {
121
+ input
122
+ }
123
+ }
124
+ };
125
+ }
126
+ },
127
+ // Dev server: intercept HTML requests and serve virtual HTML.
128
+ // Pre-middleware — runs before Vite's built-in handlers.
129
+ // Safe because we only respond to .html requests and SPA navigation;
130
+ // module requests (/@vite/client, /@react-refresh, .ts/.tsx/.css files)
131
+ // don't match our conditions and pass through to Vite's transform middleware.
132
+ configureServer(server) {
133
+ server.watcher.add(manifestAbsPath);
134
+ server.middlewares.use(async (req, res, next) => {
135
+ try {
136
+ const url = req.url?.split("?")[0] ?? "";
137
+ const acceptsHtml = req.headers.accept?.includes("text/html");
138
+ if (url.endsWith(".html") || url === "/" || url === "/index.html") {
139
+ const filename = url === "/" || url === "/index.html" ? "index.html" : url.slice(1);
140
+ const entry = entries.find((e) => e.entrypoint === filename);
141
+ if (entry) {
142
+ let html = generateHtml(entry);
143
+ html = await server.transformIndexHtml(url, html);
144
+ res.setHeader("Content-Type", "text/html");
145
+ res.statusCode = 200;
146
+ res.end(html);
147
+ return;
148
+ }
149
+ }
150
+ if (acceptsHtml && !url.includes(".")) {
151
+ const mainEntry = entries.find(
152
+ (e) => e.entrypoint === "index.html"
153
+ );
154
+ if (mainEntry) {
155
+ let html = generateHtml(mainEntry);
156
+ html = await server.transformIndexHtml(url, html);
157
+ res.setHeader("Content-Type", "text/html");
158
+ res.statusCode = 200;
159
+ res.end(html);
160
+ return;
161
+ }
162
+ }
163
+ next();
164
+ } catch (err) {
165
+ next(err);
166
+ }
167
+ });
168
+ },
169
+ // Watch manifest for changes during dev — reload entries on edit.
170
+ // Entire handler is wrapped in try/catch so JSON syntax errors in the
171
+ // manifest don't crash the dev server (common during active editing).
172
+ handleHotUpdate({ file, server }) {
173
+ if (slash(file) === slash(manifestAbsPath)) {
174
+ try {
175
+ entries = readManifest();
176
+ validateEntries();
177
+ } catch (e) {
178
+ server.config.logger.error(String(e));
179
+ return [];
180
+ }
181
+ server.config.logger.info(
182
+ "[ethisys-manifest] Manifest changed \u2014 reloading entries"
183
+ );
184
+ server.hot.send({ type: "full-reload" });
185
+ return [];
186
+ }
187
+ },
188
+ // Build: resolve virtual HTML module IDs (no physical files exist on disk)
189
+ resolveId(id) {
190
+ const normalizedId = slash(id);
191
+ const match = entries.find(
192
+ (e) => slash(resolve(rootDir, e.entrypoint)) === normalizedId
193
+ );
194
+ if (match) {
195
+ return id;
196
+ }
197
+ },
198
+ // Build: provide virtual HTML content for Rollup
199
+ load(id) {
200
+ const normalizedId = slash(id);
201
+ const match = entries.find(
202
+ (e) => slash(resolve(rootDir, e.entrypoint)) === normalizedId
203
+ );
204
+ if (match) {
205
+ return generateHtml(match);
206
+ }
207
+ }
208
+ };
209
+ }
210
+ export {
211
+ ethisysManifestPlugin
212
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@ethisyscore/vite-plugin",
3
+ "version": "1.0.0-alpha.12",
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
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "test": "vitest run"
23
+ },
24
+ "peerDependencies": {
25
+ "vite": ">=5.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^25.5.0",
29
+ "tsup": "^8.5.1",
30
+ "typescript": "^5.9.3",
31
+ "vite": "^8.0.1",
32
+ "vitest": "^4.1.0"
33
+ },
34
+ "keywords": [
35
+ "ethisyscore",
36
+ "vite",
37
+ "vite-plugin",
38
+ "plugin",
39
+ "manifest",
40
+ "mpa",
41
+ "multi-page"
42
+ ],
43
+ "license": "Apache-2.0",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/ethisysltd/ethisyscore-plugin-sdk"
47
+ }
48
+ }