@povio/ui 5.0.0-rc.12 → 5.0.0-rc.14
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 +9 -1
- package/dist/{AppCanvasPage-DCjiFfQW.js → AppCanvasPage-vta1j5YQ.js} +583 -847
- package/dist/{CanvasComponentsFrame-CZXY0Dxi.js → CanvasComponentsFrame-D6Mh_G3q.js} +1 -1
- package/dist/{ComponentGallery-D4U5hXZL.js → ComponentGallery-CzKU899_.js} +2 -2
- package/dist/{Form-DmcByQts.d.ts → Form-DAtj2szV.d.ts} +1 -1
- package/dist/{Inputs-C0sUmuPv.d.ts → Inputs-C_6dVwC7.d.ts} +1 -1
- package/dist/{Table-BN2_J_Ne.js → Table-piX1SWvN.js} +54 -1
- package/dist/{UIConfigurator-CBU0G6rb.js → UIConfigurator-CjAW1SJY.js} +1521 -318
- package/dist/config/app-canvas-tanstack.js +1 -1
- package/dist/config/app-canvas.d.ts +1 -1
- package/dist/config/app-canvas.js +1 -1
- package/dist/config/ui-config.d.ts +1 -1
- package/dist/config/ui-configurator-vite.d.ts +9 -0
- package/dist/config/ui-configurator-vite.js +560 -184
- package/dist/config/ui-configurator.d.ts +14 -2
- package/dist/config/ui-configurator.js +2 -2
- package/dist/{configuration.context-iuZu-ie0.d.ts → configuration.context-3I7-wBII.d.ts} +3 -2
- package/dist/{dynamicInputs-DdcsSoYu.d.ts → dynamicInputs-R5i7AT_C.d.ts} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/rhf/inputs.d.ts +1 -1
- package/dist/rhf.d.ts +2 -2
- package/dist/rhf.js +3 -3
- package/dist/styles/app-canvas.css +15 -42
- package/dist/styles/configurator.css +453 -92
- package/dist/{table.cva-D5oQ-QvA.js → table.cva-DlMT-zLD.js} +1 -54
- package/dist/tanstack/form.d.ts +1 -1
- package/dist/tanstack/inputs.d.ts +2 -2
- package/dist/tanstack/table.js +2 -2
- package/dist/tanstack.d.ts +4 -4
- package/dist/tanstack.js +3 -3
- package/dist/{uiConfig.context-BqKWd_MO.d.ts → uiConfig.context-BksnNQyw.d.ts} +1 -1
- package/dist/{useTableColumnConfig-6zn3zKl3.js → useTableColumnConfig-CNJ_ZvIN.js} +1 -1
- package/package.json +1 -1
- package/src/configurator/README.md +18 -1
|
@@ -1,154 +1,26 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { lstat, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
positions: {}
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
function createCanvasManifest() {
|
|
14
|
-
return {
|
|
15
|
-
version: 2,
|
|
16
|
-
workspaces: [createCanvasWorkspace("All pages", "default")],
|
|
17
|
-
activeWorkspaceId: "default",
|
|
18
|
-
examples: {},
|
|
19
|
-
breakpoint: "desktop"
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function isRecord(value) {
|
|
23
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
24
|
-
}
|
|
25
|
-
function isJSON(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
|
|
26
|
-
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
27
|
-
if (typeof value === "number") return Number.isFinite(value);
|
|
28
|
-
if (typeof value !== "object" || depth > 30 || ancestors.has(value)) return false;
|
|
29
|
-
ancestors.add(value);
|
|
30
|
-
const valid = Object.values(value).every((child) => isJSON(child, ancestors, depth + 1));
|
|
31
|
-
ancestors.delete(value);
|
|
32
|
-
return valid;
|
|
33
|
-
}
|
|
34
|
-
function safeJSON(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
|
|
35
|
-
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
36
|
-
if (typeof value === "number") return Number.isFinite(value);
|
|
37
|
-
if (typeof value !== "object" || depth > 30 || ancestors.has(value)) return false;
|
|
38
|
-
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
|
|
39
|
-
ancestors.add(value);
|
|
40
|
-
const valid = Object.entries(value).every(([key, child]) => ![
|
|
41
|
-
"__proto__",
|
|
42
|
-
"prototype",
|
|
43
|
-
"constructor"
|
|
44
|
-
].includes(key) && safeJSON(child, ancestors, depth + 1));
|
|
45
|
-
ancestors.delete(value);
|
|
46
|
-
return valid;
|
|
47
|
-
}
|
|
48
|
-
const stringMap = (value) => isRecord(value) && Object.values(value).every((item) => typeof item === "string");
|
|
49
|
-
const variantValue = (value) => value === null || typeof value === "string" || typeof value === "boolean";
|
|
50
|
-
function validCvaEdits(value) {
|
|
51
|
-
return isRecord(value) && Object.entries(value).every(([slot, definition]) => {
|
|
52
|
-
if (!/^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*$/.test(slot) || !isRecord(definition) || !Array.isArray(definition.base) || !definition.base.every((item) => typeof item === "string") || !isRecord(definition.config)) return false;
|
|
53
|
-
const config = definition.config;
|
|
54
|
-
return isRecord(config.variants) && Object.values(config.variants).every(stringMap) && isRecord(config.defaultVariants) && Object.values(config.defaultVariants).every(variantValue) && Array.isArray(config.compoundVariants) && config.compoundVariants.every((compound) => isRecord(compound) && Object.values(compound).every((item) => variantValue(item) || Array.isArray(item) && (item.every((part) => typeof part === "string") || item.every((part) => typeof part === "boolean"))));
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
/** Returns an independent JSON configuration, or undefined for invalid/unset overrides. */
|
|
58
|
-
function parseCanvasConfiguration(value) {
|
|
59
|
-
if (!isRecord(value) || !safeJSON(value) || !isRecord(value.theme) || ![
|
|
60
|
-
"light",
|
|
61
|
-
"dark",
|
|
62
|
-
"theme"
|
|
63
|
-
].every((key) => stringMap(value.theme && value.theme[key])) || !isRecord(value.config) || value.overrides !== void 0 && !validCvaEdits(value.overrides)) return void 0;
|
|
64
|
-
return JSON.parse(JSON.stringify(value));
|
|
65
|
-
}
|
|
66
|
-
function parsePositions(input) {
|
|
67
|
-
return Object.fromEntries(Object.entries(isRecord(input) ? input : {}).flatMap(([breakpoint, entries]) => {
|
|
68
|
-
if (!isRecord(entries)) return [];
|
|
69
|
-
return [[breakpoint, Object.fromEntries(Object.entries(entries).flatMap(([id, point]) => isRecord(point) && typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y) ? [[id, {
|
|
70
|
-
x: point.x,
|
|
71
|
-
y: point.y
|
|
72
|
-
}]] : []))]];
|
|
73
|
-
}));
|
|
74
|
-
}
|
|
75
|
-
/** Sanitize either stored JSON text or an imported value, migrating v1 positions into the default workspace. */
|
|
76
|
-
function parseCanvasManifest(input) {
|
|
77
|
-
const fallback = createCanvasManifest();
|
|
78
|
-
let value = input;
|
|
79
|
-
if (typeof value === "string") try {
|
|
80
|
-
value = JSON.parse(value);
|
|
81
|
-
} catch {
|
|
82
|
-
return fallback;
|
|
83
|
-
}
|
|
84
|
-
if (!isRecord(value) || value.version !== 1 && value.version !== 2) return fallback;
|
|
85
|
-
const examples = Object.fromEntries(Object.entries(isRecord(value.examples) ? value.examples : {}).flatMap(([id, example]) => {
|
|
86
|
-
if (!isRecord(example)) return [];
|
|
87
|
-
const clean = {};
|
|
88
|
-
if (typeof example.title === "string") clean.title = example.title;
|
|
89
|
-
if (isRecord(example.params)) clean.params = Object.fromEntries(Object.entries(example.params).filter((entry) => typeof entry[1] === "string"));
|
|
90
|
-
if (isRecord(example.search)) clean.search = Object.fromEntries(Object.entries(example.search).filter(([, item]) => isJSON(item)));
|
|
91
|
-
return [[id, clean]];
|
|
92
|
-
}));
|
|
93
|
-
const workspaces = [];
|
|
94
|
-
const ids = /* @__PURE__ */ new Set();
|
|
95
|
-
if (value.version === 1) workspaces.push({
|
|
96
|
-
...createCanvasWorkspace("All pages", "default"),
|
|
97
|
-
positions: parsePositions(value.positions)
|
|
98
|
-
});
|
|
99
|
-
else if (Array.isArray(value.workspaces)) for (const entry of value.workspaces) {
|
|
100
|
-
if (!isRecord(entry) || typeof entry.id !== "string" || !entry.id.trim() || ids.has(entry.id)) continue;
|
|
101
|
-
ids.add(entry.id);
|
|
102
|
-
const configuration = parseCanvasConfiguration(entry.configuration);
|
|
103
|
-
workspaces.push({
|
|
104
|
-
id: entry.id,
|
|
105
|
-
name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : "Workspace",
|
|
106
|
-
enabledPages: Array.isArray(entry.enabledPages) ? [...new Set(entry.enabledPages.filter((id) => typeof id === "string" && id.length > 0))] : null,
|
|
107
|
-
positions: parsePositions(entry.positions),
|
|
108
|
-
...configuration ? { configuration } : {}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
if (!workspaces.length) workspaces.push(createCanvasWorkspace("All pages", "default"));
|
|
112
|
-
return {
|
|
113
|
-
version: 2,
|
|
114
|
-
workspaces,
|
|
115
|
-
activeWorkspaceId: typeof value.activeWorkspaceId === "string" && workspaces.some((workspace) => workspace.id === value.activeWorkspaceId) ? value.activeWorkspaceId : workspaces[0].id,
|
|
116
|
-
examples,
|
|
117
|
-
breakpoint: typeof value.breakpoint === "string" && value.breakpoint.length > 0 ? value.breakpoint : fallback.breakpoint
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
//#endregion
|
|
121
|
-
//#region src/app-canvas/storage-protocol.ts
|
|
122
|
-
const canvasStorageMetaName = "povio-ui-canvas-storage";
|
|
3
|
+
import { access, lstat, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
//#region src/configurator/repository-storage-protocol.ts
|
|
8
|
+
const componentConfigurationMetaName = "povio-ui-component-configuration";
|
|
9
|
+
/** Dev-server event sent when a repository storage file changes, so other tabs reload it. */
|
|
10
|
+
const repositoryStorageChangedEvent = "povio-ui:repository-storage-changed";
|
|
123
11
|
//#endregion
|
|
124
|
-
//#region src/
|
|
12
|
+
//#region src/configurator/repository-storage-vite.ts
|
|
125
13
|
var StorageError = class extends Error {
|
|
126
14
|
constructor(message, status) {
|
|
127
15
|
super(message);
|
|
128
16
|
this.status = status;
|
|
129
17
|
}
|
|
130
18
|
};
|
|
131
|
-
const
|
|
132
|
-
const inside = (root, path) => {
|
|
19
|
+
const inside$1 = (root, path) => {
|
|
133
20
|
const rel = relative(root, path);
|
|
134
21
|
return !!rel && !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
135
22
|
};
|
|
136
|
-
const record = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
137
|
-
function validPositions(value) {
|
|
138
|
-
return record(value) && Object.values(value).every((entries) => record(entries) && Object.values(entries).every((position) => record(position) && typeof position.x === "number" && Number.isFinite(position.x) && typeof position.y === "number" && Number.isFinite(position.y)));
|
|
139
|
-
}
|
|
140
|
-
function decodeManifest(value) {
|
|
141
|
-
if (!record(value) || ![1, 2].includes(Number(value.version)) || typeof value.breakpoint !== "string" || !record(value.examples)) throw new StorageError("Invalid canvas manifest.", 400);
|
|
142
|
-
if (value.version === 1) {
|
|
143
|
-
if (!validPositions(value.positions)) throw new StorageError("Invalid canvas positions.", 400);
|
|
144
|
-
} else if (value.version !== 2 || !Array.isArray(value.workspaces) || !value.workspaces.length || typeof value.activeWorkspaceId !== "string" || !value.workspaces.every((workspace) => record(workspace) && typeof workspace.id === "string" && workspace.id.length > 0 && typeof workspace.name === "string" && (workspace.enabledPages === null || Array.isArray(workspace.enabledPages) && workspace.enabledPages.every((id) => typeof id === "string")) && validPositions(workspace.positions) && (workspace.configuration === void 0 || parseCanvasConfiguration(workspace.configuration) !== void 0))) throw new StorageError("Invalid canvas workspaces.", 400);
|
|
145
|
-
if (!Object.values(value.examples).every((example) => record(example) && (example.title === void 0 || typeof example.title === "string") && (example.params === void 0 || record(example.params) && Object.values(example.params).every((param) => typeof param === "string")) && (example.search === void 0 || record(example.search)))) throw new StorageError("Invalid canvas examples.", 400);
|
|
146
|
-
if (value.version === 2) {
|
|
147
|
-
const workspaces = value.workspaces;
|
|
148
|
-
if (new Set(workspaces.map((workspace) => workspace.id)).size !== workspaces.length || !workspaces.some((workspace) => workspace.id === value.activeWorkspaceId)) throw new StorageError("Invalid canvas workspace identity.", 400);
|
|
149
|
-
}
|
|
150
|
-
return parseCanvasManifest(value);
|
|
151
|
-
}
|
|
23
|
+
const record$2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
152
24
|
const revisionOf = (text) => createHash("sha256").update(text).digest("hex");
|
|
153
25
|
const respond$1 = (res, status, body) => {
|
|
154
26
|
res.writeHead(status, {
|
|
@@ -157,32 +29,33 @@ const respond$1 = (res, status, body) => {
|
|
|
157
29
|
});
|
|
158
30
|
res.end(JSON.stringify(body));
|
|
159
31
|
};
|
|
160
|
-
async function readBody(req) {
|
|
32
|
+
async function readBody(req, label, maxBytes) {
|
|
161
33
|
let size = 0;
|
|
162
34
|
const chunks = [];
|
|
163
35
|
for await (const chunk of req) {
|
|
164
36
|
size += Buffer.byteLength(chunk);
|
|
165
|
-
if (size > maxBytes) throw new StorageError(
|
|
37
|
+
if (size > maxBytes) throw new StorageError(`${label} file exceeds ${maxBytes / 1024} KB.`, 413);
|
|
166
38
|
chunks.push(Buffer.from(chunk));
|
|
167
39
|
}
|
|
168
40
|
try {
|
|
169
41
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
170
42
|
} catch {
|
|
171
|
-
throw new StorageError(
|
|
43
|
+
throw new StorageError(`Expected JSON ${label.toLowerCase()} data.`, 400);
|
|
172
44
|
}
|
|
173
45
|
}
|
|
174
46
|
/** Fixed-path development persistence only. Never commits or pushes repository changes. */
|
|
175
|
-
async function
|
|
47
|
+
async function configureRepositoryStorage(server, { filename, route, field, label, decode, maxBytes = 262144 }) {
|
|
48
|
+
const noun = label.toLowerCase();
|
|
176
49
|
const root = await realpath(server.config.root);
|
|
177
50
|
const filePath = resolve(root, filename);
|
|
178
51
|
const parent = dirname(filePath);
|
|
179
|
-
if (!inside(root, filePath) || !filePath.endsWith(".json")) throw new Error(
|
|
52
|
+
if (!inside$1(root, filePath) || !filePath.endsWith(".json")) throw new Error(`Povio UI ${noun} file must be a JSON file inside this app's Vite root.`);
|
|
180
53
|
const guard = async () => {
|
|
181
|
-
if (await realpath(parent) !== parent) throw new StorageError(
|
|
54
|
+
if (await realpath(parent) !== parent) throw new StorageError(`${label} file parent cannot be a symbolic link.`, 409);
|
|
182
55
|
try {
|
|
183
56
|
const stat = await lstat(filePath);
|
|
184
|
-
if (stat.isSymbolicLink() || !stat.isFile()) throw new StorageError(
|
|
185
|
-
if (stat.size > maxBytes) throw new StorageError(
|
|
57
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new StorageError(`${label} file must be a regular file, not a symbolic link.`, 409);
|
|
58
|
+
if (stat.size > maxBytes) throw new StorageError(`${label} file exceeds ${maxBytes / 1024} KB.`, 413);
|
|
186
59
|
} catch (error) {
|
|
187
60
|
if (error.code !== "ENOENT") throw error;
|
|
188
61
|
}
|
|
@@ -190,7 +63,7 @@ async function configureCanvasStorage(server, filename = "povio.canvas.json") {
|
|
|
190
63
|
await guard();
|
|
191
64
|
const file = relative(root, filePath).split(sep).join("/");
|
|
192
65
|
const connection = {
|
|
193
|
-
endpoint: `${new URL(server.config.base, "http://localhost").pathname.replace(/\/?$/, "/")}__povio_ui
|
|
66
|
+
endpoint: `${new URL(server.config.base, "http://localhost").pathname.replace(/\/?$/, "/")}__povio_ui/${route}`,
|
|
194
67
|
token: randomUUID(),
|
|
195
68
|
file
|
|
196
69
|
};
|
|
@@ -212,7 +85,7 @@ async function configureCanvasStorage(server, filename = "povio.canvas.json") {
|
|
|
212
85
|
}
|
|
213
86
|
};
|
|
214
87
|
const handle = async (req, res) => {
|
|
215
|
-
if (req.headers["x-povio-
|
|
88
|
+
if (req.headers["x-povio-ui-storage-token"] !== connection.token) throw new StorageError(`Invalid ${noun} storage token.`, 403);
|
|
216
89
|
const origin = req.headers.origin;
|
|
217
90
|
let validOrigin = true;
|
|
218
91
|
if (origin) try {
|
|
@@ -220,18 +93,18 @@ async function configureCanvasStorage(server, filename = "povio.canvas.json") {
|
|
|
220
93
|
} catch {
|
|
221
94
|
validOrigin = false;
|
|
222
95
|
}
|
|
223
|
-
if (!validOrigin || req.headers["sec-fetch-site"] === "cross-site") throw new StorageError(
|
|
96
|
+
if (!validOrigin || req.headers["sec-fetch-site"] === "cross-site") throw new StorageError(`${label} storage requires the app's origin.`, 403);
|
|
224
97
|
if (req.method === "GET") {
|
|
225
98
|
await pending;
|
|
226
99
|
const current = await readCurrent();
|
|
227
|
-
let
|
|
100
|
+
let value = null;
|
|
228
101
|
if (current.text !== null) try {
|
|
229
|
-
|
|
102
|
+
value = decode(JSON.parse(current.text));
|
|
230
103
|
} catch {
|
|
231
|
-
throw new StorageError(
|
|
104
|
+
throw new StorageError(`Repository ${noun} file is invalid. Correct it before saving.`, 409);
|
|
232
105
|
}
|
|
233
106
|
return respond$1(res, 200, {
|
|
234
|
-
|
|
107
|
+
[field]: value,
|
|
235
108
|
revision: current.revision,
|
|
236
109
|
file
|
|
237
110
|
});
|
|
@@ -241,13 +114,13 @@ async function configureCanvasStorage(server, filename = "povio.canvas.json") {
|
|
|
241
114
|
throw new StorageError("Use GET or POST.", 405);
|
|
242
115
|
}
|
|
243
116
|
if (!req.headers["content-type"]?.startsWith("application/json")) throw new StorageError("Expected application/json.", 415);
|
|
244
|
-
const body = await readBody(req);
|
|
245
|
-
if (!record(body) || !(body.revision === null || typeof body.revision === "string")) throw new StorageError(
|
|
246
|
-
const
|
|
247
|
-
const text = `${JSON.stringify(
|
|
248
|
-
if (Buffer.byteLength(text) > maxBytes) throw new StorageError(
|
|
117
|
+
const body = await readBody(req, label, maxBytes);
|
|
118
|
+
if (!record$2(body) || !(body.revision === null || typeof body.revision === "string")) throw new StorageError(`Expected ${field} and revision.`, 400);
|
|
119
|
+
const value = decode(body[field]);
|
|
120
|
+
const text = `${JSON.stringify(value, null, 2)}\n`;
|
|
121
|
+
if (Buffer.byteLength(text) > maxBytes) throw new StorageError(`${label} file exceeds ${maxBytes / 1024} KB.`, 413);
|
|
249
122
|
const write = pending.then(async () => {
|
|
250
|
-
if ((await readCurrent()).revision !== body.revision) throw new StorageError(
|
|
123
|
+
if ((await readCurrent()).revision !== body.revision) throw new StorageError(`${label} file changed. Reload it before saving again.`, 409);
|
|
251
124
|
const temporary = `${filePath}.povio-${randomUUID()}.tmp`;
|
|
252
125
|
try {
|
|
253
126
|
await writeFile(temporary, text, {
|
|
@@ -270,11 +143,479 @@ async function configureCanvasStorage(server, filename = "povio.canvas.json") {
|
|
|
270
143
|
};
|
|
271
144
|
server.middlewares.use((req, res, next) => {
|
|
272
145
|
if (req.url?.split("?")[0] !== connection.endpoint) return next();
|
|
273
|
-
handle(req, res).catch((error) => respond$1(res, error instanceof StorageError ? error.status : 500, { error: error instanceof Error ? error.message :
|
|
146
|
+
handle(req, res).catch((error) => respond$1(res, error instanceof StorageError ? error.status : 500, { error: error instanceof Error ? error.message : `Could not access ${noun} file.` }));
|
|
274
147
|
});
|
|
148
|
+
const isFile = (path) => resolve(path) === filePath;
|
|
275
149
|
return {
|
|
276
150
|
connection,
|
|
277
|
-
|
|
151
|
+
isFile,
|
|
152
|
+
isStorageFile: (path) => isFile(path) || resolve(path).startsWith(`${filePath}.povio-`),
|
|
153
|
+
revisionOf
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/app-canvas/manifest.ts
|
|
158
|
+
function createCanvasWorkspace(name, id) {
|
|
159
|
+
return {
|
|
160
|
+
id,
|
|
161
|
+
name: name.trim() || "Workspace",
|
|
162
|
+
enabledPages: null,
|
|
163
|
+
positions: {}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function createCanvasManifest() {
|
|
167
|
+
return {
|
|
168
|
+
version: 2,
|
|
169
|
+
workspaces: [createCanvasWorkspace("All pages", "default")],
|
|
170
|
+
activeWorkspaceId: "default",
|
|
171
|
+
examples: {},
|
|
172
|
+
breakpoint: "desktop"
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function isRecord$1(value) {
|
|
176
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
177
|
+
}
|
|
178
|
+
function isJSON(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
|
|
179
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
180
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
181
|
+
if (typeof value !== "object" || depth > 30 || ancestors.has(value)) return false;
|
|
182
|
+
ancestors.add(value);
|
|
183
|
+
const valid = Object.values(value).every((child) => isJSON(child, ancestors, depth + 1));
|
|
184
|
+
ancestors.delete(value);
|
|
185
|
+
return valid;
|
|
186
|
+
}
|
|
187
|
+
function parsePositions(input) {
|
|
188
|
+
return Object.fromEntries(Object.entries(isRecord$1(input) ? input : {}).flatMap(([breakpoint, entries]) => {
|
|
189
|
+
if (!isRecord$1(entries)) return [];
|
|
190
|
+
return [[breakpoint, Object.fromEntries(Object.entries(entries).flatMap(([id, point]) => isRecord$1(point) && typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y) ? [[id, {
|
|
191
|
+
x: point.x,
|
|
192
|
+
y: point.y
|
|
193
|
+
}]] : []))]];
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
/** Sanitize either stored JSON text or an imported value, migrating v1 positions into the default workspace. */
|
|
197
|
+
function parseCanvasManifest(input) {
|
|
198
|
+
const fallback = createCanvasManifest();
|
|
199
|
+
let value = input;
|
|
200
|
+
if (typeof value === "string") try {
|
|
201
|
+
value = JSON.parse(value);
|
|
202
|
+
} catch {
|
|
203
|
+
return fallback;
|
|
204
|
+
}
|
|
205
|
+
if (!isRecord$1(value) || value.version !== 1 && value.version !== 2) return fallback;
|
|
206
|
+
const examples = Object.fromEntries(Object.entries(isRecord$1(value.examples) ? value.examples : {}).flatMap(([id, example]) => {
|
|
207
|
+
if (!isRecord$1(example)) return [];
|
|
208
|
+
const clean = {};
|
|
209
|
+
if (typeof example.title === "string") clean.title = example.title;
|
|
210
|
+
if (isRecord$1(example.params)) clean.params = Object.fromEntries(Object.entries(example.params).filter((entry) => typeof entry[1] === "string"));
|
|
211
|
+
if (isRecord$1(example.search)) clean.search = Object.fromEntries(Object.entries(example.search).filter(([, item]) => isJSON(item)));
|
|
212
|
+
return [[id, clean]];
|
|
213
|
+
}));
|
|
214
|
+
const workspaces = [];
|
|
215
|
+
const ids = /* @__PURE__ */ new Set();
|
|
216
|
+
if (value.version === 1) workspaces.push({
|
|
217
|
+
...createCanvasWorkspace("All pages", "default"),
|
|
218
|
+
positions: parsePositions(value.positions)
|
|
219
|
+
});
|
|
220
|
+
else if (Array.isArray(value.workspaces)) for (const entry of value.workspaces) {
|
|
221
|
+
if (!isRecord$1(entry) || typeof entry.id !== "string" || !entry.id.trim() || ids.has(entry.id)) continue;
|
|
222
|
+
ids.add(entry.id);
|
|
223
|
+
workspaces.push({
|
|
224
|
+
id: entry.id,
|
|
225
|
+
name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : "Workspace",
|
|
226
|
+
enabledPages: Array.isArray(entry.enabledPages) ? [...new Set(entry.enabledPages.filter((id) => typeof id === "string" && id.length > 0))] : null,
|
|
227
|
+
positions: parsePositions(entry.positions)
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (!workspaces.length) workspaces.push(createCanvasWorkspace("All pages", "default"));
|
|
231
|
+
return {
|
|
232
|
+
version: 2,
|
|
233
|
+
workspaces,
|
|
234
|
+
activeWorkspaceId: typeof value.activeWorkspaceId === "string" && workspaces.some((workspace) => workspace.id === value.activeWorkspaceId) ? value.activeWorkspaceId : workspaces[0].id,
|
|
235
|
+
examples,
|
|
236
|
+
breakpoint: typeof value.breakpoint === "string" && value.breakpoint.length > 0 ? value.breakpoint : fallback.breakpoint
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/app-canvas/storage-vite.ts
|
|
241
|
+
const record$1 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
242
|
+
function validPositions(value) {
|
|
243
|
+
return record$1(value) && Object.values(value).every((entries) => record$1(entries) && Object.values(entries).every((position) => record$1(position) && typeof position.x === "number" && Number.isFinite(position.x) && typeof position.y === "number" && Number.isFinite(position.y)));
|
|
244
|
+
}
|
|
245
|
+
function decodeManifest(value) {
|
|
246
|
+
if (!record$1(value) || ![1, 2].includes(Number(value.version)) || typeof value.breakpoint !== "string" || !record$1(value.examples)) throw new StorageError("Invalid canvas manifest.", 400);
|
|
247
|
+
if (value.version === 1) {
|
|
248
|
+
if (!validPositions(value.positions)) throw new StorageError("Invalid canvas positions.", 400);
|
|
249
|
+
} else if (value.version !== 2 || !Array.isArray(value.workspaces) || !value.workspaces.length || typeof value.activeWorkspaceId !== "string" || !value.workspaces.every((workspace) => record$1(workspace) && typeof workspace.id === "string" && workspace.id.length > 0 && typeof workspace.name === "string" && (workspace.enabledPages === null || Array.isArray(workspace.enabledPages) && workspace.enabledPages.every((id) => typeof id === "string")) && validPositions(workspace.positions))) throw new StorageError("Invalid canvas workspaces.", 400);
|
|
250
|
+
if (!Object.values(value.examples).every((example) => record$1(example) && (example.title === void 0 || typeof example.title === "string") && (example.params === void 0 || record$1(example.params) && Object.values(example.params).every((param) => typeof param === "string")) && (example.search === void 0 || record$1(example.search)))) throw new StorageError("Invalid canvas examples.", 400);
|
|
251
|
+
if (value.version === 2) {
|
|
252
|
+
const workspaces = value.workspaces;
|
|
253
|
+
if (new Set(workspaces.map((workspace) => workspace.id)).size !== workspaces.length || !workspaces.some((workspace) => workspace.id === value.activeWorkspaceId)) throw new StorageError("Invalid canvas workspace identity.", 400);
|
|
254
|
+
}
|
|
255
|
+
return parseCanvasManifest(value);
|
|
256
|
+
}
|
|
257
|
+
/** Fixed-path development persistence for canvas workspaces. Never commits or pushes repository changes. */
|
|
258
|
+
function configureCanvasStorage(server, filename = "povio.canvas.json") {
|
|
259
|
+
return configureRepositoryStorage(server, {
|
|
260
|
+
filename,
|
|
261
|
+
route: "canvas",
|
|
262
|
+
field: "manifest",
|
|
263
|
+
label: "Canvas",
|
|
264
|
+
decode: decodeManifest
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/app-canvas/storage-protocol.ts
|
|
269
|
+
const canvasStorageMetaName = "povio-ui-canvas-storage";
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/configurator/theme-versions.ts
|
|
272
|
+
const themeVersionsMetaName = "povio-ui-theme-versions";
|
|
273
|
+
function createThemeVersion(name, id, configuration) {
|
|
274
|
+
return {
|
|
275
|
+
id,
|
|
276
|
+
name: name.trim() || "Theme",
|
|
277
|
+
...configuration ? { configuration: structuredClone(configuration) } : {}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function createThemeVersions() {
|
|
281
|
+
return {
|
|
282
|
+
version: 1,
|
|
283
|
+
versions: [createThemeVersion("Default", "default")],
|
|
284
|
+
activeVersionId: "default"
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function isRecord(value) {
|
|
288
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
289
|
+
}
|
|
290
|
+
function safeJSON(value, ancestors = /* @__PURE__ */ new Set(), depth = 0) {
|
|
291
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
292
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
293
|
+
if (typeof value !== "object" || depth > 30 || ancestors.has(value)) return false;
|
|
294
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
|
|
295
|
+
ancestors.add(value);
|
|
296
|
+
const valid = Object.entries(value).every(([key, child]) => ![
|
|
297
|
+
"__proto__",
|
|
298
|
+
"prototype",
|
|
299
|
+
"constructor"
|
|
300
|
+
].includes(key) && safeJSON(child, ancestors, depth + 1));
|
|
301
|
+
ancestors.delete(value);
|
|
302
|
+
return valid;
|
|
303
|
+
}
|
|
304
|
+
const stringMap = (value) => isRecord(value) && Object.values(value).every((item) => typeof item === "string");
|
|
305
|
+
const variantValue = (value) => value === null || typeof value === "string" || typeof value === "boolean";
|
|
306
|
+
function validCvaEdits(value) {
|
|
307
|
+
return isRecord(value) && Object.entries(value).every(([slot, definition]) => {
|
|
308
|
+
if (!/^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*$/.test(slot) || !isRecord(definition) || !Array.isArray(definition.base) || !definition.base.every((item) => typeof item === "string") || !isRecord(definition.config)) return false;
|
|
309
|
+
const config = definition.config;
|
|
310
|
+
return isRecord(config.variants) && Object.values(config.variants).every(stringMap) && isRecord(config.defaultVariants) && Object.values(config.defaultVariants).every(variantValue) && Array.isArray(config.compoundVariants) && config.compoundVariants.every((compound) => isRecord(compound) && Object.values(compound).every((item) => variantValue(item) || Array.isArray(item) && (item.every((part) => typeof part === "string") || item.every((part) => typeof part === "boolean"))));
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/** Returns an independent JSON configuration, or undefined for invalid/unset overrides. */
|
|
314
|
+
function parseUIConfiguration(value) {
|
|
315
|
+
if (!isRecord(value) || !safeJSON(value) || !isRecord(value.theme) || ![
|
|
316
|
+
"light",
|
|
317
|
+
"dark",
|
|
318
|
+
"theme"
|
|
319
|
+
].every((key) => stringMap(value.theme && value.theme[key])) || !isRecord(value.config) || value.overrides !== void 0 && !validCvaEdits(value.overrides)) return void 0;
|
|
320
|
+
return JSON.parse(JSON.stringify(value));
|
|
321
|
+
}
|
|
322
|
+
/** Returns an independent component configuration, or undefined when invalid. */
|
|
323
|
+
function parseComponentConfiguration(value) {
|
|
324
|
+
if (!isRecord(value) || !safeJSON(value) || !isRecord(value.config) || value.overrides !== void 0 && !validCvaEdits(value.overrides)) return void 0;
|
|
325
|
+
const copy = JSON.parse(JSON.stringify(value));
|
|
326
|
+
return {
|
|
327
|
+
config: copy.config ?? {},
|
|
328
|
+
overrides: copy.overrides ?? {}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** Sanitize stored JSON text or a repository value. */
|
|
332
|
+
function parseThemeVersions(input) {
|
|
333
|
+
const fallback = createThemeVersions();
|
|
334
|
+
let value = input;
|
|
335
|
+
if (typeof value === "string") try {
|
|
336
|
+
value = JSON.parse(value);
|
|
337
|
+
} catch {
|
|
338
|
+
return fallback;
|
|
339
|
+
}
|
|
340
|
+
if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.versions)) return fallback;
|
|
341
|
+
const versions = [];
|
|
342
|
+
const ids = /* @__PURE__ */ new Set();
|
|
343
|
+
for (const entry of value.versions) {
|
|
344
|
+
if (!isRecord(entry) || typeof entry.id !== "string" || !entry.id.trim() || ids.has(entry.id)) continue;
|
|
345
|
+
ids.add(entry.id);
|
|
346
|
+
const configuration = parseUIConfiguration(entry.configuration);
|
|
347
|
+
versions.push({
|
|
348
|
+
id: entry.id,
|
|
349
|
+
name: typeof entry.name === "string" && entry.name.trim() ? entry.name.trim() : "Theme",
|
|
350
|
+
...configuration ? { configuration } : {}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (!versions.length) return fallback;
|
|
354
|
+
return {
|
|
355
|
+
version: 1,
|
|
356
|
+
versions,
|
|
357
|
+
activeVersionId: typeof value.activeVersionId === "string" && versions.some((version) => version.id === value.activeVersionId) ? value.activeVersionId : versions[0].id
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
//#endregion
|
|
361
|
+
//#region src/configurator/theme-versions-vite.ts
|
|
362
|
+
const record = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
363
|
+
function decodeThemeVersions(value) {
|
|
364
|
+
if (!record(value) || value.version !== 1 || !Array.isArray(value.versions) || !value.versions.length || typeof value.activeVersionId !== "string" || !value.versions.every((version) => record(version) && typeof version.id === "string" && version.id.length > 0 && typeof version.name === "string" && (version.configuration === void 0 || parseUIConfiguration(version.configuration) !== void 0))) throw new StorageError("Invalid theme versions.", 400);
|
|
365
|
+
const ids = value.versions.map((version) => version.id);
|
|
366
|
+
if (new Set(ids).size !== ids.length || !ids.includes(value.activeVersionId)) throw new StorageError("Invalid theme version identity.", 400);
|
|
367
|
+
return parseThemeVersions(value);
|
|
368
|
+
}
|
|
369
|
+
/** Theme versions shared through the repository, separate from canvas layout. Never commits or pushes. */
|
|
370
|
+
function configureThemeVersionsStorage(server, filename = "povio.themes.json") {
|
|
371
|
+
return configureRepositoryStorage(server, {
|
|
372
|
+
filename,
|
|
373
|
+
route: "themes",
|
|
374
|
+
field: "themes",
|
|
375
|
+
label: "Theme versions",
|
|
376
|
+
decode: decodeThemeVersions,
|
|
377
|
+
maxBytes: 4194304
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region src/configurator/component-configuration-vite.ts
|
|
382
|
+
/** The component settings Apply theme writes beside the theme CSS. Hot updates reach the app like the CSS. */
|
|
383
|
+
function configureComponentConfigurationStorage(server, filename) {
|
|
384
|
+
return configureRepositoryStorage(server, {
|
|
385
|
+
filename,
|
|
386
|
+
route: "configuration",
|
|
387
|
+
field: "configuration",
|
|
388
|
+
label: "UI configuration",
|
|
389
|
+
decode: (value) => {
|
|
390
|
+
const configuration = parseComponentConfiguration(value);
|
|
391
|
+
if (!configuration) throw new StorageError("Invalid UI configuration.", 400);
|
|
392
|
+
return configuration;
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/configurator/theme-writer-protocol.ts
|
|
398
|
+
/** Shared by the browser configurator and the isolated Node Vite entry. */
|
|
399
|
+
const themeWriterMetaName = "povio-ui-theme-writer";
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/configurator/fontsource-vite.ts
|
|
402
|
+
const execute = promisify(execFile);
|
|
403
|
+
const fontId = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
404
|
+
const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
405
|
+
var FontError = class extends Error {
|
|
406
|
+
constructor(message, status = 400) {
|
|
407
|
+
super(message);
|
|
408
|
+
this.status = status;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
const dependencies = {
|
|
412
|
+
fetch: (...args) => globalThis.fetch(...args),
|
|
413
|
+
install: async (command, args, cwd) => {
|
|
414
|
+
await execute(command, args, {
|
|
415
|
+
cwd,
|
|
416
|
+
shell: false,
|
|
417
|
+
timeout: 18e4,
|
|
418
|
+
maxBuffer: 1048576,
|
|
419
|
+
env: {
|
|
420
|
+
...process.env,
|
|
421
|
+
npm_config_ignore_scripts: "true",
|
|
422
|
+
YARN_ENABLE_SCRIPTS: "false"
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
const exists = async (file) => {
|
|
428
|
+
try {
|
|
429
|
+
await access(file);
|
|
430
|
+
return true;
|
|
431
|
+
} catch {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const inside = (root, file) => {
|
|
436
|
+
const path = relative(root, file);
|
|
437
|
+
return !!path && !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
|
|
438
|
+
};
|
|
439
|
+
async function manager(root) {
|
|
440
|
+
let directory = root;
|
|
441
|
+
while (true) {
|
|
442
|
+
let declared = "";
|
|
443
|
+
try {
|
|
444
|
+
declared = JSON.parse(await readFile(resolve(directory, "package.json"), "utf8")).packageManager ?? "";
|
|
445
|
+
} catch {}
|
|
446
|
+
if (/^(bun|npm|pnpm|yarn)@/.test(declared)) return {
|
|
447
|
+
name: declared.split("@")[0],
|
|
448
|
+
modernYarn: /^yarn@[2-9]/.test(declared)
|
|
449
|
+
};
|
|
450
|
+
for (const [file, name] of [
|
|
451
|
+
["bun.lock", "bun"],
|
|
452
|
+
["bun.lockb", "bun"],
|
|
453
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
454
|
+
["package-lock.json", "npm"],
|
|
455
|
+
["yarn.lock", "yarn"]
|
|
456
|
+
]) if (await exists(resolve(directory, file))) return {
|
|
457
|
+
name,
|
|
458
|
+
modernYarn: name === "yarn" && (await readFile(resolve(directory, file), "utf8")).includes("__metadata:")
|
|
459
|
+
};
|
|
460
|
+
const parent = dirname(directory);
|
|
461
|
+
if (parent === directory) break;
|
|
462
|
+
directory = parent;
|
|
463
|
+
}
|
|
464
|
+
throw new FontError("No existing package manager was detected. Add a lockfile or packageManager field first.", 409);
|
|
465
|
+
}
|
|
466
|
+
async function metadata(fetcher, url) {
|
|
467
|
+
const response = await fetcher(url, {
|
|
468
|
+
redirect: "error",
|
|
469
|
+
signal: AbortSignal.timeout(2e4)
|
|
470
|
+
});
|
|
471
|
+
if (!response.ok) throw new FontError(response.status === 404 ? "Fontsource font/package was not found." : "Fontsource metadata could not be loaded.", response.status === 404 ? 400 : 502);
|
|
472
|
+
return response.json();
|
|
473
|
+
}
|
|
474
|
+
/** Internal service factory: injection is for server tests, never a plugin/client execution option. */
|
|
475
|
+
async function createFontsourceInstaller(rootInput, cssName, deps = dependencies) {
|
|
476
|
+
const root = await realpath(rootInput), css = resolve(root, cssName), packageFile = resolve(root, "package.json");
|
|
477
|
+
if (!inside(root, css) || !css.endsWith(".css")) throw new FontError("fontCssFile must be a CSS file inside the app root.");
|
|
478
|
+
if (!await exists(css) || !await exists(packageFile)) return void 0;
|
|
479
|
+
const guard = async () => {
|
|
480
|
+
if (await realpath(css) !== css || await realpath(packageFile) !== packageFile) throw new FontError("Font CSS and package.json must not use symbolic links.", 409);
|
|
481
|
+
};
|
|
482
|
+
await guard();
|
|
483
|
+
let pending = Promise.resolve();
|
|
484
|
+
const install = (id) => {
|
|
485
|
+
const task = pending.then(async () => {
|
|
486
|
+
if (typeof id !== "string" || id.length > 100 || !fontId.test(id)) throw new FontError("Expected a valid Fontsource font ID.");
|
|
487
|
+
await guard();
|
|
488
|
+
const font = await metadata(deps.fetch, `https://api.fontsource.org/v1/fonts/${id}`);
|
|
489
|
+
if (font.id !== id || typeof font.family !== "string" || !font.family.trim() || !Array.isArray(font.weights) || !Array.isArray(font.styles)) throw new FontError("Invalid Fontsource font metadata.", 502);
|
|
490
|
+
let variable = false;
|
|
491
|
+
if (font.variable) variable = !!(await metadata(deps.fetch, `https://api.fontsource.org/v1/variable/${id}`)).axes?.wght;
|
|
492
|
+
const packageName = `${variable ? "@fontsource-variable" : "@fontsource"}/${id}`;
|
|
493
|
+
const registry = await metadata(deps.fetch, `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`);
|
|
494
|
+
if (registry.name !== packageName || typeof registry.version !== "string" || !versionPattern.test(registry.version)) throw new FontError("Invalid npm package version metadata.", 502);
|
|
495
|
+
const weights = font.weights.filter((weight) => typeof weight === "number" && Number.isInteger(weight) && weight >= 1 && weight <= 1e3);
|
|
496
|
+
const styles = ["normal", "italic"].filter((style) => font.styles.includes(style));
|
|
497
|
+
if (!weights.length || !styles.length) throw new FontError("This font has no supported styles.");
|
|
498
|
+
const imports = variable ? styles.map((style) => `${packageName}/wght${style === "italic" ? "-italic" : ""}.css`) : weights.flatMap((weight) => styles.map((style) => `${packageName}/${weight}${style === "italic" ? "-italic" : ""}.css`));
|
|
499
|
+
const require = createRequire(packageFile);
|
|
500
|
+
const installed = async (exact = false) => {
|
|
501
|
+
try {
|
|
502
|
+
const declared = JSON.parse(await readFile(packageFile, "utf8"));
|
|
503
|
+
if (!declared.dependencies?.[packageName] && !declared.devDependencies?.[packageName]) return false;
|
|
504
|
+
const pkg = JSON.parse(await readFile(require.resolve(`${packageName}/package.json`), "utf8"));
|
|
505
|
+
if (exact && pkg.version !== registry.version) return false;
|
|
506
|
+
if (!versionPattern.test(pkg.version) || pkg.version.split(".")[0] !== registry.version.split(".")[0]) return false;
|
|
507
|
+
await Promise.all(imports.map((item) => access(require.resolve(item))));
|
|
508
|
+
return true;
|
|
509
|
+
} catch {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
const alreadyInstalled = await installed();
|
|
514
|
+
if (!alreadyInstalled) {
|
|
515
|
+
const pm = await manager(root);
|
|
516
|
+
const spec = `${packageName}@${registry.version}`;
|
|
517
|
+
const args = pm.name === "npm" ? [
|
|
518
|
+
"install",
|
|
519
|
+
"--save-exact",
|
|
520
|
+
"--ignore-scripts",
|
|
521
|
+
spec
|
|
522
|
+
] : pm.name === "yarn" ? [
|
|
523
|
+
"add",
|
|
524
|
+
"--exact",
|
|
525
|
+
...pm.modernYarn ? [] : ["--ignore-scripts"],
|
|
526
|
+
spec
|
|
527
|
+
] : [
|
|
528
|
+
"add",
|
|
529
|
+
"--exact",
|
|
530
|
+
"--ignore-scripts",
|
|
531
|
+
spec
|
|
532
|
+
];
|
|
533
|
+
if (pm.name === "pnpm") args[1] = "--save-exact";
|
|
534
|
+
try {
|
|
535
|
+
await deps.install(pm.name, args, root);
|
|
536
|
+
} catch {
|
|
537
|
+
throw new FontError("Font installation failed. Existing CSS imports were left unchanged.", 502);
|
|
538
|
+
}
|
|
539
|
+
if (!await installed(true)) throw new FontError("Installed font files could not be verified. CSS imports were left unchanged.", 502);
|
|
540
|
+
}
|
|
541
|
+
await guard();
|
|
542
|
+
const source = await readFile(css, "utf8");
|
|
543
|
+
const existing = new Set([...source.matchAll(/@import\s+(?:url\(\s*)?["']([^"']+)["']/g)].map((match) => match[1]));
|
|
544
|
+
const missing = imports.filter((item) => !existing.has(item));
|
|
545
|
+
if (missing.length) {
|
|
546
|
+
const additions = missing.map((item) => `@import "${item}";`).join("\n");
|
|
547
|
+
const charset = source.match(/^(?:\uFEFF)?@charset\s+["'][^"']+["'];\s*/i)?.[0] ?? "";
|
|
548
|
+
const next = `${charset}${additions}\n${source.slice(charset.length)}`;
|
|
549
|
+
const temporary = `${css}.fontsource-${randomUUID()}.tmp`;
|
|
550
|
+
try {
|
|
551
|
+
await writeFile(temporary, next, { flag: "wx" });
|
|
552
|
+
await guard();
|
|
553
|
+
await rename(temporary, css);
|
|
554
|
+
} finally {
|
|
555
|
+
await unlink(temporary).catch(() => {});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
family: `${font.family}${variable ? " Variable" : ""}`,
|
|
560
|
+
packageName,
|
|
561
|
+
imports,
|
|
562
|
+
alreadyInstalled
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
pending = task.then(() => {}, () => {});
|
|
566
|
+
return task;
|
|
567
|
+
};
|
|
568
|
+
return {
|
|
569
|
+
cssFile: relative(root, css).split(sep).join("/"),
|
|
570
|
+
install
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async function configureFontsource(server, cssName, token, base) {
|
|
574
|
+
const installer = await createFontsourceInstaller(server.config.root, cssName);
|
|
575
|
+
if (!installer) return void 0;
|
|
576
|
+
const endpoint = `${base}__povio_ui/fontsource`;
|
|
577
|
+
server.middlewares.use((req, res, next) => {
|
|
578
|
+
if (req.url?.split("?")[0] !== endpoint) return next();
|
|
579
|
+
handle(req, res).catch((error) => respond(res, error instanceof FontError ? error.status : 500, { error: error instanceof Error ? error.message : "Could not install font." }));
|
|
580
|
+
});
|
|
581
|
+
function respond(res, status, body) {
|
|
582
|
+
res.writeHead(status, {
|
|
583
|
+
"Content-Type": "application/json",
|
|
584
|
+
"Cache-Control": "no-store"
|
|
585
|
+
});
|
|
586
|
+
res.end(JSON.stringify(body));
|
|
587
|
+
}
|
|
588
|
+
async function handle(req, res) {
|
|
589
|
+
if (req.method !== "POST") throw new FontError("Use POST to install a font.", 405);
|
|
590
|
+
if (req.headers["x-povio-ui-theme-token"] !== token) throw new FontError("Invalid theme token.", 403);
|
|
591
|
+
if (req.headers["sec-fetch-site"] === "cross-site") throw new FontError("Font installation requires the app origin.", 403);
|
|
592
|
+
if (req.headers.origin) {
|
|
593
|
+
let sameOrigin = false;
|
|
594
|
+
try {
|
|
595
|
+
sameOrigin = new URL(req.headers.origin).origin === `${server.config.server.https ? "https" : "http"}://${req.headers.host}`;
|
|
596
|
+
} catch {}
|
|
597
|
+
if (!sameOrigin) throw new FontError("Font installation requires the app origin.", 403);
|
|
598
|
+
}
|
|
599
|
+
if (!req.headers["content-type"]?.startsWith("application/json")) throw new FontError("Expected application/json.", 415);
|
|
600
|
+
let size = 0;
|
|
601
|
+
const chunks = [];
|
|
602
|
+
for await (const chunk of req) {
|
|
603
|
+
size += Buffer.byteLength(chunk);
|
|
604
|
+
if (size > 1024) throw new FontError("Font request is too large.", 413);
|
|
605
|
+
chunks.push(Buffer.from(chunk));
|
|
606
|
+
}
|
|
607
|
+
let data;
|
|
608
|
+
try {
|
|
609
|
+
data = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
610
|
+
} catch {
|
|
611
|
+
throw new FontError("Expected JSON containing a font ID.");
|
|
612
|
+
}
|
|
613
|
+
const id = data && typeof data === "object" && !Array.isArray(data) ? data.id : void 0;
|
|
614
|
+
respond(res, 200, await installer.install(id));
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
endpoint,
|
|
618
|
+
cssFile: installer.cssFile
|
|
278
619
|
};
|
|
279
620
|
}
|
|
280
621
|
//#endregion
|
|
@@ -347,10 +688,6 @@ function updateThemeSource(source, exportedCss) {
|
|
|
347
688
|
return source;
|
|
348
689
|
}
|
|
349
690
|
//#endregion
|
|
350
|
-
//#region src/configurator/theme-writer-protocol.ts
|
|
351
|
-
/** Shared by the browser configurator and the isolated Node Vite entry. */
|
|
352
|
-
const themeWriterMetaName = "povio-ui-theme-writer";
|
|
353
|
-
//#endregion
|
|
354
691
|
//#region src/configurator/vite.ts
|
|
355
692
|
var ThemeWriteError = class extends Error {
|
|
356
693
|
constructor(message, status) {
|
|
@@ -384,24 +721,30 @@ function uiConfigurator(options = {}) {
|
|
|
384
721
|
let connection;
|
|
385
722
|
let pendingWrite = Promise.resolve();
|
|
386
723
|
let canvasStorage;
|
|
724
|
+
let themeVersionsStorage;
|
|
725
|
+
let componentStorage;
|
|
387
726
|
return {
|
|
388
727
|
name: "vite-plugin-povio-ui-configurator",
|
|
389
728
|
apply: (_config, environment) => environment.command === "serve" && !environment.isPreview,
|
|
390
729
|
async configureServer(server) {
|
|
391
730
|
if (options.canvasFile !== false) canvasStorage = await configureCanvasStorage(server, options.canvasFile);
|
|
731
|
+
if (options.themeVersionsFile !== false) themeVersionsStorage = await configureThemeVersionsStorage(server, options.themeVersionsFile);
|
|
392
732
|
const root = await realpath(server.config.root);
|
|
393
733
|
const configuredFile = resolve(server.config.root, options.themeFile ?? "src/styles/theme.css");
|
|
394
734
|
const themeFile = await realpath(configuredFile);
|
|
395
735
|
const file = relative(root, themeFile);
|
|
396
736
|
if (!file || isAbsolute(file) || file === ".." || file.startsWith(`..${sep}`)) throw new Error("Povio UI themeFile must be inside this app's Vite root.");
|
|
397
737
|
if (!themeFile.endsWith(".css")) throw new Error("Povio UI themeFile must be a CSS file.");
|
|
398
|
-
|
|
738
|
+
if (options.configurationFile !== false) componentStorage = await configureComponentConfigurationStorage(server, options.configurationFile ?? join(dirname(file), "ui-configuration.json"));
|
|
739
|
+
const base = new URL(server.config.base, "http://localhost").pathname.replace(/\/?$/, "/");
|
|
740
|
+
const endpoint = `${base}__povio_ui/theme`;
|
|
399
741
|
connection = {
|
|
400
742
|
endpoint,
|
|
401
743
|
token: randomUUID(),
|
|
402
744
|
file: file.split(sep).join("/")
|
|
403
745
|
};
|
|
404
746
|
const token = connection.token;
|
|
747
|
+
if (options.fontCssFile !== false) connection.fontsource = await configureFontsource(server, options.fontCssFile ?? "src/styles/globals.css", token, base);
|
|
405
748
|
const save = async (request, response) => {
|
|
406
749
|
if (request.method !== "POST") {
|
|
407
750
|
response.setHeader("Allow", "POST");
|
|
@@ -436,26 +779,59 @@ function uiConfigurator(options = {}) {
|
|
|
436
779
|
});
|
|
437
780
|
});
|
|
438
781
|
},
|
|
439
|
-
handleHotUpdate(context) {
|
|
440
|
-
|
|
782
|
+
async handleHotUpdate(context) {
|
|
783
|
+
const storage = [canvasStorage, themeVersionsStorage].find((item) => item?.isStorageFile(context.file));
|
|
784
|
+
if (!storage) return;
|
|
785
|
+
if (storage.isFile(context.file)) {
|
|
786
|
+
const text = await Promise.resolve(context.read()).catch(() => null);
|
|
787
|
+
const change = {
|
|
788
|
+
endpoint: storage.connection.endpoint,
|
|
789
|
+
revision: text ? storage.revisionOf(text) : null
|
|
790
|
+
};
|
|
791
|
+
context.server.ws.send({
|
|
792
|
+
type: "custom",
|
|
793
|
+
event: repositoryStorageChangedEvent,
|
|
794
|
+
data: change
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
return [];
|
|
441
798
|
},
|
|
442
799
|
transformIndexHtml() {
|
|
443
800
|
if (!connection) return;
|
|
444
|
-
return [
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
tag: "meta",
|
|
453
|
-
attrs: {
|
|
454
|
-
name: canvasStorageMetaName,
|
|
455
|
-
content: JSON.stringify(canvasStorage.connection)
|
|
801
|
+
return [
|
|
802
|
+
{
|
|
803
|
+
tag: "meta",
|
|
804
|
+
attrs: {
|
|
805
|
+
name: themeWriterMetaName,
|
|
806
|
+
content: JSON.stringify(connection)
|
|
807
|
+
},
|
|
808
|
+
injectTo: "head"
|
|
456
809
|
},
|
|
457
|
-
|
|
458
|
-
|
|
810
|
+
...canvasStorage ? [{
|
|
811
|
+
tag: "meta",
|
|
812
|
+
attrs: {
|
|
813
|
+
name: canvasStorageMetaName,
|
|
814
|
+
content: JSON.stringify(canvasStorage.connection)
|
|
815
|
+
},
|
|
816
|
+
injectTo: "head"
|
|
817
|
+
}] : [],
|
|
818
|
+
...componentStorage ? [{
|
|
819
|
+
tag: "meta",
|
|
820
|
+
attrs: {
|
|
821
|
+
name: componentConfigurationMetaName,
|
|
822
|
+
content: JSON.stringify(componentStorage.connection)
|
|
823
|
+
},
|
|
824
|
+
injectTo: "head"
|
|
825
|
+
}] : [],
|
|
826
|
+
...themeVersionsStorage ? [{
|
|
827
|
+
tag: "meta",
|
|
828
|
+
attrs: {
|
|
829
|
+
name: themeVersionsMetaName,
|
|
830
|
+
content: JSON.stringify(themeVersionsStorage.connection)
|
|
831
|
+
},
|
|
832
|
+
injectTo: "head"
|
|
833
|
+
}] : []
|
|
834
|
+
];
|
|
459
835
|
}
|
|
460
836
|
};
|
|
461
837
|
}
|