@kubuild/core 0.0.1
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/binding-resolver.d.ts +16 -0
- package/dist/binding-resolver.d.ts.map +1 -0
- package/dist/command-tree-utils.d.ts +60 -0
- package/dist/command-tree-utils.d.ts.map +1 -0
- package/dist/commands.d.ts +99 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/document-utils.d.ts +51 -0
- package/dist/document-utils.d.ts.map +1 -0
- package/dist/exporter.d.ts +89 -0
- package/dist/exporter.d.ts.map +1 -0
- package/dist/history.d.ts +107 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/importer.d.ts +243 -0
- package/dist/importer.d.ts.map +1 -0
- package/dist/index.cjs +3686 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3614 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces.d.ts +71 -0
- package/dist/interfaces.d.ts.map +1 -0
- package/dist/migration.d.ts +86 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/round-trip-comparator.d.ts +38 -0
- package/dist/round-trip-comparator.d.ts.map +1 -0
- package/dist/security.d.ts +99 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/template-utils.d.ts +124 -0
- package/dist/template-utils.d.ts.map +1 -0
- package/dist/validator.d.ts +43 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/variable-catalog.d.ts +9 -0
- package/dist/variable-catalog.d.ts.map +1 -0
- package/package.json +58 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3686 @@
|
|
|
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
|
+
CORE_BUILTIN_COMPONENTS: () => CORE_BUILTIN_COMPONENTS,
|
|
24
|
+
DEFAULT_DOCUMENT_SECURITY_LIMITS: () => DEFAULT_DOCUMENT_SECURITY_LIMITS,
|
|
25
|
+
DEFAULT_MAX_HISTORY: () => DEFAULT_MAX_HISTORY,
|
|
26
|
+
DEFAULT_SECURITY_LIMITS: () => DEFAULT_SECURITY_LIMITS,
|
|
27
|
+
DocumentHistoryManager: () => DocumentHistoryManager,
|
|
28
|
+
HistoryEngine: () => HistoryEngine,
|
|
29
|
+
MigrationRegistry: () => MigrationRegistry,
|
|
30
|
+
areNodeIdsCompletelyDistinct: () => areNodeIdsCompletelyDistinct,
|
|
31
|
+
buildSampleVariablesFromCatalog: () => buildSampleVariablesFromCatalog,
|
|
32
|
+
calculateChecksum: () => calculateChecksum,
|
|
33
|
+
canMigrate: () => canMigrate,
|
|
34
|
+
checkZipBomb: () => checkZipBomb,
|
|
35
|
+
cloneTemplateAsPage: () => cloneTemplateAsPage,
|
|
36
|
+
cloneTreeWithNewIds: () => cloneTreeWithNewIds,
|
|
37
|
+
collectAssetReferences: () => collectAssetReferences,
|
|
38
|
+
collectNodeIdSet: () => collectNodeIdSet,
|
|
39
|
+
compareDocumentsSemantically: () => compareDocumentsSemantically,
|
|
40
|
+
compareManifestsSemantically: () => compareManifestsSemantically,
|
|
41
|
+
containsProhibitedKeys: () => containsProhibitedKeys,
|
|
42
|
+
createBlankDocument: () => createBlankDocument,
|
|
43
|
+
createTemplateRecord: () => createTemplateRecord,
|
|
44
|
+
deepClone: () => deepClone,
|
|
45
|
+
defaultIdGenerator: () => defaultIdGenerator,
|
|
46
|
+
defaultMigrationRegistry: () => defaultMigrationRegistry,
|
|
47
|
+
defaultRenameAssetStrategy: () => defaultRenameAssetStrategy,
|
|
48
|
+
duplicateNode: () => duplicateNode,
|
|
49
|
+
exportPackage: () => exportPackage,
|
|
50
|
+
exportStoraPackage: () => exportStoraPackage,
|
|
51
|
+
extractRequirementsFromTree: () => extractRequirementsFromTree,
|
|
52
|
+
extractTemplateRequirements: () => extractTemplateRequirements,
|
|
53
|
+
findMissingComponentNodes: () => findMissingComponentNodes,
|
|
54
|
+
findNodeById: () => findNodeById,
|
|
55
|
+
findNodeLocation: () => findNodeLocation,
|
|
56
|
+
getAncestorChain: () => getAncestorChain,
|
|
57
|
+
getMigrationPath: () => getMigrationPath,
|
|
58
|
+
getNavigationTarget: () => getNavigationTarget,
|
|
59
|
+
getNodeAncestors: () => getNodeAncestors,
|
|
60
|
+
getParentNodeId: () => getParentNodeId,
|
|
61
|
+
importPackage: () => importPackage,
|
|
62
|
+
importStoraPackage: () => importStoraPackage,
|
|
63
|
+
insertNode: () => insertNode,
|
|
64
|
+
inspectPackage: () => inspectPackage,
|
|
65
|
+
isDangerousAssetFilename: () => isDangerousAssetFilename,
|
|
66
|
+
isDangerousPath: () => isDangerousPath,
|
|
67
|
+
isDescendantOf: () => isDescendantOf,
|
|
68
|
+
isSafeUrl: () => isSafeUrl,
|
|
69
|
+
migrateDocument: () => migrateDocument,
|
|
70
|
+
moveNode: () => moveNode,
|
|
71
|
+
preflightPackage: () => preflightPackage,
|
|
72
|
+
previewImportPackage: () => previewImportPackage,
|
|
73
|
+
remapAssetReferences: () => remapAssetReferences,
|
|
74
|
+
remapDocumentAssetReferences: () => remapDocumentAssetReferences,
|
|
75
|
+
removeNode: () => removeNode,
|
|
76
|
+
resolveBinding: () => resolveBinding,
|
|
77
|
+
resolveBindingValue: () => resolveBindingValue,
|
|
78
|
+
sanitizeFilename: () => sanitizeFilename,
|
|
79
|
+
sanitizeHtml: () => sanitizeHtml,
|
|
80
|
+
sanitizeUrl: () => sanitizeUrl,
|
|
81
|
+
saveDraftAsTemplate: () => saveDraftAsTemplate,
|
|
82
|
+
sha256Sync: () => sha256Sync,
|
|
83
|
+
updateProps: () => updateProps,
|
|
84
|
+
updateStyle: () => updateStyle,
|
|
85
|
+
validateDocument: () => validateDocument,
|
|
86
|
+
validateDocumentSecurity: () => validateDocumentSecurity,
|
|
87
|
+
validateTemplate: () => validateTemplate
|
|
88
|
+
});
|
|
89
|
+
module.exports = __toCommonJS(index_exports);
|
|
90
|
+
|
|
91
|
+
// src/binding-resolver.ts
|
|
92
|
+
var FORBIDDEN_KEY_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
93
|
+
function resolveBinding(binding, context) {
|
|
94
|
+
const segments = binding.key.split(".").filter(Boolean);
|
|
95
|
+
let current = context?.variables;
|
|
96
|
+
for (const segment of segments) {
|
|
97
|
+
if (FORBIDDEN_KEY_SEGMENTS.has(segment)) {
|
|
98
|
+
return applyMissingPolicy(binding);
|
|
99
|
+
}
|
|
100
|
+
if (current === null || typeof current !== "object") {
|
|
101
|
+
return applyMissingPolicy(binding);
|
|
102
|
+
}
|
|
103
|
+
if (!Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
104
|
+
return applyMissingPolicy(binding);
|
|
105
|
+
}
|
|
106
|
+
current = current[segment];
|
|
107
|
+
}
|
|
108
|
+
if (current === void 0 || typeof current === "function") {
|
|
109
|
+
return applyMissingPolicy(binding);
|
|
110
|
+
}
|
|
111
|
+
return { status: "resolved", value: current };
|
|
112
|
+
}
|
|
113
|
+
function applyMissingPolicy(binding) {
|
|
114
|
+
if (binding.fallback !== void 0) {
|
|
115
|
+
return { status: "fallback", value: binding.fallback };
|
|
116
|
+
}
|
|
117
|
+
return { status: "empty", value: "" };
|
|
118
|
+
}
|
|
119
|
+
function resolveBindingValue(binding, context) {
|
|
120
|
+
return resolveBinding(binding, context).value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/variable-catalog.ts
|
|
124
|
+
var FORBIDDEN_KEY_SEGMENTS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
125
|
+
function buildSampleVariablesFromCatalog(catalog) {
|
|
126
|
+
const result = {};
|
|
127
|
+
if (!catalog) {
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
for (const entry of catalog) {
|
|
131
|
+
const segments = entry.key.split(".").filter(Boolean);
|
|
132
|
+
if (segments.length === 0 || segments.some((segment) => FORBIDDEN_KEY_SEGMENTS2.has(segment))) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
let cursor = result;
|
|
136
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
137
|
+
const segment = segments[i];
|
|
138
|
+
const existing = cursor[segment];
|
|
139
|
+
if (existing === null || typeof existing !== "object" || Array.isArray(existing)) {
|
|
140
|
+
cursor[segment] = {};
|
|
141
|
+
}
|
|
142
|
+
cursor = cursor[segment];
|
|
143
|
+
}
|
|
144
|
+
cursor[segments[segments.length - 1]] = entry.sampleValue;
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/validator.ts
|
|
150
|
+
var import_schema = require("@kubuild/schema");
|
|
151
|
+
|
|
152
|
+
// src/security.ts
|
|
153
|
+
var DEFAULT_DOCUMENT_SECURITY_LIMITS = {
|
|
154
|
+
maxNodeCount: 5e3,
|
|
155
|
+
maxTreeDepth: 32,
|
|
156
|
+
maxPropsCount: 200,
|
|
157
|
+
maxStringLength: 1e5,
|
|
158
|
+
maxChildrenPerNode: 1e3,
|
|
159
|
+
maxDocumentSizeBytes: 10 * 1024 * 1024,
|
|
160
|
+
// 10 MB
|
|
161
|
+
allowedUrlSchemes: ["http:", "https:", "mailto:", "tel:", "#", "relative", "data:image/"],
|
|
162
|
+
prohibitedPropertyKeys: [
|
|
163
|
+
"__proto__",
|
|
164
|
+
"constructor",
|
|
165
|
+
"prototype",
|
|
166
|
+
"__defineGetter__",
|
|
167
|
+
"__defineSetter__",
|
|
168
|
+
"__lookupGetter__",
|
|
169
|
+
"__lookupSetter__"
|
|
170
|
+
],
|
|
171
|
+
prohibitedAssetExtensions: [
|
|
172
|
+
".exe",
|
|
173
|
+
".dll",
|
|
174
|
+
".so",
|
|
175
|
+
".sh",
|
|
176
|
+
".bat",
|
|
177
|
+
".cmd",
|
|
178
|
+
".js",
|
|
179
|
+
".mjs",
|
|
180
|
+
".cjs",
|
|
181
|
+
".php",
|
|
182
|
+
".py",
|
|
183
|
+
".vbs",
|
|
184
|
+
".wasm",
|
|
185
|
+
".bin",
|
|
186
|
+
".com",
|
|
187
|
+
".scr",
|
|
188
|
+
".pif",
|
|
189
|
+
".hta",
|
|
190
|
+
".jar"
|
|
191
|
+
],
|
|
192
|
+
maxZipExpansionRatio: 100
|
|
193
|
+
};
|
|
194
|
+
function isSafeUrl(url, options) {
|
|
195
|
+
if (typeof url !== "string") return false;
|
|
196
|
+
const trimmed = url.trim();
|
|
197
|
+
if (trimmed.length === 0) return true;
|
|
198
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("?")) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (/[\u0000-\u001F\u007F-\u009F]/.test(trimmed)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
const colonIndex = trimmed.indexOf(":");
|
|
205
|
+
if (colonIndex === -1) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
const scheme = trimmed.substring(0, colonIndex + 1).toLowerCase();
|
|
209
|
+
const dangerousSchemes = ["javascript:", "vbscript:", "file:", "data:"];
|
|
210
|
+
if (scheme !== "data:" && dangerousSchemes.includes(scheme)) {
|
|
211
|
+
if (scheme === "blob:" && options?.allowBlobMedia) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (scheme === "data:") {
|
|
217
|
+
const allowDataImage = options?.allowDataImage ?? true;
|
|
218
|
+
if (!allowDataImage) return false;
|
|
219
|
+
const dataPrefix = trimmed.substring(0, 30).toLowerCase();
|
|
220
|
+
const safeDataImagePrefixes = [
|
|
221
|
+
"data:image/png",
|
|
222
|
+
"data:image/jpeg",
|
|
223
|
+
"data:image/jpg",
|
|
224
|
+
"data:image/webp",
|
|
225
|
+
"data:image/gif",
|
|
226
|
+
"data:image/avif",
|
|
227
|
+
"data:image/svg+xml"
|
|
228
|
+
];
|
|
229
|
+
return safeDataImagePrefixes.some((p) => dataPrefix.startsWith(p));
|
|
230
|
+
}
|
|
231
|
+
const allowed = options?.allowedSchemes || DEFAULT_DOCUMENT_SECURITY_LIMITS.allowedUrlSchemes;
|
|
232
|
+
return allowed.some((s) => s.toLowerCase() === scheme);
|
|
233
|
+
}
|
|
234
|
+
function sanitizeUrl(url, fallback = "", options) {
|
|
235
|
+
if (typeof url !== "string") return fallback;
|
|
236
|
+
return isSafeUrl(url, options) ? url : fallback;
|
|
237
|
+
}
|
|
238
|
+
function containsProhibitedKeys(obj, prohibitedKeys = DEFAULT_DOCUMENT_SECURITY_LIMITS.prohibitedPropertyKeys) {
|
|
239
|
+
const prohibitedSet = new Set(prohibitedKeys.map((k) => k.toLowerCase()));
|
|
240
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
241
|
+
function check(value, currentPath) {
|
|
242
|
+
if (!value || typeof value !== "object") {
|
|
243
|
+
return { found: false, path: "", key: "" };
|
|
244
|
+
}
|
|
245
|
+
if (visited.has(value)) {
|
|
246
|
+
return { found: false, path: "", key: "" };
|
|
247
|
+
}
|
|
248
|
+
visited.add(value);
|
|
249
|
+
if (Array.isArray(value)) {
|
|
250
|
+
for (let i = 0; i < value.length; i++) {
|
|
251
|
+
const res = check(value[i], `${currentPath}[${i}]`);
|
|
252
|
+
if (res.found) return res;
|
|
253
|
+
}
|
|
254
|
+
return { found: false, path: "", key: "" };
|
|
255
|
+
}
|
|
256
|
+
const record = value;
|
|
257
|
+
const keysToCheck = Array.from(/* @__PURE__ */ new Set([...Object.keys(record), ...Object.getOwnPropertyNames(record)]));
|
|
258
|
+
for (const key of keysToCheck) {
|
|
259
|
+
if (prohibitedSet.has(key.toLowerCase())) {
|
|
260
|
+
return { found: true, path: currentPath ? `${currentPath}.${key}` : key, key };
|
|
261
|
+
}
|
|
262
|
+
const res = check(record[key], currentPath ? `${currentPath}.${key}` : key);
|
|
263
|
+
if (res.found) return res;
|
|
264
|
+
}
|
|
265
|
+
if (Object.prototype.hasOwnProperty.call(record, "__proto__")) {
|
|
266
|
+
return { found: true, path: currentPath ? `${currentPath}.__proto__` : "__proto__", key: "__proto__" };
|
|
267
|
+
}
|
|
268
|
+
return { found: false, path: "", key: "" };
|
|
269
|
+
}
|
|
270
|
+
return check(obj, "");
|
|
271
|
+
}
|
|
272
|
+
function validateDocumentSecurity(doc, customLimits) {
|
|
273
|
+
const limits = { ...DEFAULT_DOCUMENT_SECURITY_LIMITS, ...customLimits };
|
|
274
|
+
const errors = [];
|
|
275
|
+
if (!doc || typeof doc !== "object") {
|
|
276
|
+
return { safe: true, errors: [] };
|
|
277
|
+
}
|
|
278
|
+
const protoCheck = containsProhibitedKeys(doc, limits.prohibitedPropertyKeys);
|
|
279
|
+
if (protoCheck.found) {
|
|
280
|
+
errors.push({
|
|
281
|
+
code: "PROTOTYPE_POLLUTION_DETECTED",
|
|
282
|
+
message: `Prohibited property key "${protoCheck.key}" detected at path "${protoCheck.path}" (Prototype Pollution Defense).`,
|
|
283
|
+
path: protoCheck.path,
|
|
284
|
+
details: { key: protoCheck.key }
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const jsonStr = JSON.stringify(doc);
|
|
289
|
+
if (jsonStr.length > limits.maxDocumentSizeBytes) {
|
|
290
|
+
errors.push({
|
|
291
|
+
code: "MAX_DOCUMENT_SIZE_EXCEEDED",
|
|
292
|
+
message: `Document size (${jsonStr.length} bytes) exceeds limit of ${limits.maxDocumentSizeBytes} bytes.`,
|
|
293
|
+
details: { size: jsonStr.length, limit: limits.maxDocumentSizeBytes }
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
const docObj = doc;
|
|
299
|
+
const rootNode = docObj.document;
|
|
300
|
+
if (rootNode && typeof rootNode === "object") {
|
|
301
|
+
let traverse2 = function(node, depth, currentPath) {
|
|
302
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
|
303
|
+
totalNodes++;
|
|
304
|
+
if (totalNodes > limits.maxNodeCount) {
|
|
305
|
+
errors.push({
|
|
306
|
+
code: "MAX_NODE_COUNT_EXCEEDED",
|
|
307
|
+
message: `Total document nodes (${totalNodes}) exceeded security limit of ${limits.maxNodeCount}.`,
|
|
308
|
+
path: currentPath,
|
|
309
|
+
details: { nodeCount: totalNodes, limit: limits.maxNodeCount }
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (depth > limits.maxTreeDepth) {
|
|
314
|
+
errors.push({
|
|
315
|
+
code: "MAX_TREE_DEPTH_EXCEEDED",
|
|
316
|
+
message: `Document nesting depth (${depth}) exceeded security limit of ${limits.maxTreeDepth}.`,
|
|
317
|
+
path: currentPath,
|
|
318
|
+
details: { depth, limit: limits.maxTreeDepth }
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const nodeRecord = node;
|
|
323
|
+
if (nodeRecord.props && typeof nodeRecord.props === "object" && !Array.isArray(nodeRecord.props)) {
|
|
324
|
+
const propsRecord = nodeRecord.props;
|
|
325
|
+
const propKeys = Object.keys(propsRecord);
|
|
326
|
+
if (propKeys.length > limits.maxPropsCount) {
|
|
327
|
+
errors.push({
|
|
328
|
+
code: "MAX_PROPS_COUNT_EXCEEDED",
|
|
329
|
+
message: `Node has ${propKeys.length} props, exceeding limit of ${limits.maxPropsCount}.`,
|
|
330
|
+
path: `${currentPath}/props`,
|
|
331
|
+
details: { count: propKeys.length, limit: limits.maxPropsCount }
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
for (const [k, val] of Object.entries(propsRecord)) {
|
|
335
|
+
if (typeof val === "string" && val.length > limits.maxStringLength) {
|
|
336
|
+
errors.push({
|
|
337
|
+
code: "MAX_STRING_LENGTH_EXCEEDED",
|
|
338
|
+
message: `Prop "${k}" string length (${val.length}) exceeds limit of ${limits.maxStringLength}.`,
|
|
339
|
+
path: `${currentPath}/props/${k}`,
|
|
340
|
+
details: { length: val.length, limit: limits.maxStringLength }
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (nodeRecord.children && Array.isArray(nodeRecord.children)) {
|
|
346
|
+
if (nodeRecord.children.length > limits.maxChildrenPerNode) {
|
|
347
|
+
errors.push({
|
|
348
|
+
code: "MAX_CHILDREN_COUNT_EXCEEDED",
|
|
349
|
+
message: `Node has ${nodeRecord.children.length} direct children, exceeding limit of ${limits.maxChildrenPerNode}.`,
|
|
350
|
+
path: `${currentPath}/children`,
|
|
351
|
+
details: { count: nodeRecord.children.length, limit: limits.maxChildrenPerNode }
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
for (let i = 0; i < nodeRecord.children.length; i++) {
|
|
355
|
+
traverse2(nodeRecord.children[i], depth + 1, `${currentPath}/children/${i}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var traverse = traverse2;
|
|
360
|
+
let totalNodes = 0;
|
|
361
|
+
traverse2(rootNode, 1, "/document");
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
safe: errors.length === 0,
|
|
365
|
+
errors
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function isDangerousAssetFilename(filename, prohibitedExtensions = DEFAULT_DOCUMENT_SECURITY_LIMITS.prohibitedAssetExtensions) {
|
|
369
|
+
if (!filename || typeof filename !== "string") return true;
|
|
370
|
+
const lower = filename.toLowerCase().trim();
|
|
371
|
+
return prohibitedExtensions.some((ext) => {
|
|
372
|
+
const extLower = ext.toLowerCase();
|
|
373
|
+
return lower.endsWith(extLower) || lower.includes(`${extLower}.`);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
function checkZipBomb(compressedSize, uncompressedSize, maxRatio = DEFAULT_DOCUMENT_SECURITY_LIMITS.maxZipExpansionRatio) {
|
|
377
|
+
if (compressedSize <= 0) return false;
|
|
378
|
+
if (uncompressedSize > 10 * 1024 * 1024) {
|
|
379
|
+
const ratio = uncompressedSize / compressedSize;
|
|
380
|
+
return ratio > maxRatio;
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
function sanitizeHtml(rawHtml) {
|
|
385
|
+
if (typeof rawHtml !== "string") return "";
|
|
386
|
+
let html = rawHtml;
|
|
387
|
+
html = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "");
|
|
388
|
+
html = html.replace(/<\/?(object|embed|applet|base|link)\b[^>]*>/gi, "");
|
|
389
|
+
html = html.replace(/\s+on[a-z0-9_-]+\s*=\s*(?:'[^']*'|"[^"]*"|[^\s>]+)/gi, "");
|
|
390
|
+
html = html.replace(
|
|
391
|
+
/\b(href|src|data|action)\s*=\s*(['"])\s*(?:javascript:|vbscript:|data:text\/html)[^'"]*\2/gi,
|
|
392
|
+
'$1="#"'
|
|
393
|
+
);
|
|
394
|
+
html = html.replace(
|
|
395
|
+
/\b(href|src|data|action)\s*=\s*(?:javascript:|vbscript:|data:text\/html)[^\s>]+/gi,
|
|
396
|
+
'$1="#"'
|
|
397
|
+
);
|
|
398
|
+
return html;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/validator.ts
|
|
402
|
+
function validateDocument(input, options = {}) {
|
|
403
|
+
const errors = [];
|
|
404
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
405
|
+
return {
|
|
406
|
+
valid: false,
|
|
407
|
+
success: false,
|
|
408
|
+
errors: [
|
|
409
|
+
{
|
|
410
|
+
code: "GLOBAL_SCHEMA_INVALID",
|
|
411
|
+
message: "Document must be a valid non-null object",
|
|
412
|
+
path: ""
|
|
413
|
+
}
|
|
414
|
+
]
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const doc = input;
|
|
418
|
+
const securityCheck = validateDocumentSecurity(doc, options.securityLimits);
|
|
419
|
+
if (!securityCheck.safe) {
|
|
420
|
+
for (const secErr of securityCheck.errors) {
|
|
421
|
+
errors.push({
|
|
422
|
+
code: secErr.code === "PROTOTYPE_POLLUTION_DETECTED" ? "PROTOTYPE_POLLUTION_DETECTED" : "SECURITY_LIMIT_EXCEEDED",
|
|
423
|
+
message: secErr.message,
|
|
424
|
+
path: secErr.path || "",
|
|
425
|
+
details: secErr.details
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (securityCheck.errors.some((e) => e.code === "PROTOTYPE_POLLUTION_DETECTED")) {
|
|
429
|
+
return {
|
|
430
|
+
valid: false,
|
|
431
|
+
success: false,
|
|
432
|
+
errors
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (doc.schema !== import_schema.SCHEMA_NAME) {
|
|
437
|
+
errors.push({
|
|
438
|
+
code: "GLOBAL_SCHEMA_INVALID",
|
|
439
|
+
message: `Invalid schema identifier. Expected "${import_schema.SCHEMA_NAME}", received "${String(doc.schema)}"`,
|
|
440
|
+
path: "/schema"
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
if (typeof doc.version !== "string" || doc.version.trim().length === 0) {
|
|
444
|
+
errors.push({
|
|
445
|
+
code: "GLOBAL_SCHEMA_INVALID",
|
|
446
|
+
message: "Schema version must be a non-empty string",
|
|
447
|
+
path: "/version"
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
if (!doc.document || typeof doc.document !== "object" || Array.isArray(doc.document)) {
|
|
451
|
+
errors.push({
|
|
452
|
+
code: "GLOBAL_SCHEMA_INVALID",
|
|
453
|
+
message: "Document must contain a root document node object",
|
|
454
|
+
path: "/document"
|
|
455
|
+
});
|
|
456
|
+
return {
|
|
457
|
+
valid: false,
|
|
458
|
+
success: false,
|
|
459
|
+
errors
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const rootNode = doc.document;
|
|
463
|
+
if (rootNode.type !== "page") {
|
|
464
|
+
errors.push({
|
|
465
|
+
code: "ROOT_NODE_INVALID",
|
|
466
|
+
message: `Root node type must be "page", received "${String(rootNode.type)}"`,
|
|
467
|
+
path: "/document/type",
|
|
468
|
+
nodeId: typeof rootNode.id === "string" ? rootNode.id : void 0
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (doc.metadata !== void 0) {
|
|
472
|
+
if (typeof doc.metadata !== "object" || doc.metadata === null || Array.isArray(doc.metadata)) {
|
|
473
|
+
errors.push({
|
|
474
|
+
code: "INVALID_METADATA",
|
|
475
|
+
message: "Document metadata must be an object",
|
|
476
|
+
path: "/metadata"
|
|
477
|
+
});
|
|
478
|
+
} else {
|
|
479
|
+
const meta = doc.metadata;
|
|
480
|
+
if (meta.title !== void 0 && (typeof meta.title !== "string" || meta.title.trim().length === 0)) {
|
|
481
|
+
errors.push({
|
|
482
|
+
code: "INVALID_METADATA",
|
|
483
|
+
message: "Metadata title must be a non-empty string when provided",
|
|
484
|
+
path: "/metadata/title"
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
490
|
+
const visitedObjects = /* @__PURE__ */ new Set();
|
|
491
|
+
validateNodeRecursive(
|
|
492
|
+
rootNode,
|
|
493
|
+
"/document",
|
|
494
|
+
void 0,
|
|
495
|
+
seenIds,
|
|
496
|
+
visitedObjects,
|
|
497
|
+
options,
|
|
498
|
+
errors
|
|
499
|
+
);
|
|
500
|
+
if (errors.length === 0) {
|
|
501
|
+
const zodResult = import_schema.PageDocumentSchema.safeParse(input);
|
|
502
|
+
if (!zodResult.success) {
|
|
503
|
+
for (const issue of zodResult.error.issues) {
|
|
504
|
+
const jsonPath = "/" + issue.path.join("/");
|
|
505
|
+
errors.push({
|
|
506
|
+
code: "GLOBAL_SCHEMA_INVALID",
|
|
507
|
+
message: issue.message,
|
|
508
|
+
path: jsonPath === "/" ? "" : jsonPath
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
return {
|
|
513
|
+
valid: true,
|
|
514
|
+
success: true,
|
|
515
|
+
errors: [],
|
|
516
|
+
data: zodResult.data
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
valid: errors.length === 0,
|
|
522
|
+
success: errors.length === 0,
|
|
523
|
+
errors
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function validateNodeRecursive(nodeObj, currentPath, parentNode, seenIds, visitedObjects, options, errors) {
|
|
527
|
+
if (visitedObjects.has(nodeObj)) {
|
|
528
|
+
errors.push({
|
|
529
|
+
code: "TREE_CYCLE_DETECTED",
|
|
530
|
+
message: `Cyclic reference detected at node path ${currentPath}`,
|
|
531
|
+
path: currentPath,
|
|
532
|
+
nodeId: typeof nodeObj.id === "string" ? nodeObj.id : void 0
|
|
533
|
+
});
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
visitedObjects.add(nodeObj);
|
|
537
|
+
const nodeId = nodeObj.id;
|
|
538
|
+
if (typeof nodeId !== "string" || nodeId.trim().length === 0) {
|
|
539
|
+
errors.push({
|
|
540
|
+
code: "NODE_SHAPE_INVALID",
|
|
541
|
+
message: "Node id must be a non-empty string",
|
|
542
|
+
path: `${currentPath}/id`
|
|
543
|
+
});
|
|
544
|
+
} else {
|
|
545
|
+
if (seenIds.has(nodeId)) {
|
|
546
|
+
errors.push({
|
|
547
|
+
code: "DUPLICATE_NODE_ID",
|
|
548
|
+
message: `Duplicate node ID "${nodeId}" found at ${currentPath} (first seen at ${seenIds.get(nodeId)})`,
|
|
549
|
+
path: `${currentPath}/id`,
|
|
550
|
+
nodeId,
|
|
551
|
+
details: { firstSeenAt: seenIds.get(nodeId) }
|
|
552
|
+
});
|
|
553
|
+
} else {
|
|
554
|
+
seenIds.set(nodeId, currentPath);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const nodeType = nodeObj.type;
|
|
558
|
+
if (typeof nodeType !== "string" || nodeType.trim().length === 0) {
|
|
559
|
+
errors.push({
|
|
560
|
+
code: "NODE_SHAPE_INVALID",
|
|
561
|
+
message: "Node type must be a non-empty string",
|
|
562
|
+
path: `${currentPath}/type`,
|
|
563
|
+
nodeId: typeof nodeId === "string" ? nodeId : void 0
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const effectiveNodeId = typeof nodeId === "string" ? nodeId : void 0;
|
|
567
|
+
const effectiveNodeType = typeof nodeType === "string" ? nodeType : "";
|
|
568
|
+
let componentDef;
|
|
569
|
+
if (effectiveNodeType) {
|
|
570
|
+
if (options.componentRegistry) {
|
|
571
|
+
componentDef = options.componentRegistry.get(effectiveNodeType);
|
|
572
|
+
if (!componentDef && (options.strictComponentTypes || !options.componentRegistry.has(effectiveNodeType))) {
|
|
573
|
+
errors.push({
|
|
574
|
+
code: "UNKNOWN_COMPONENT_TYPE",
|
|
575
|
+
message: `Unknown component type: "${effectiveNodeType}"`,
|
|
576
|
+
path: `${currentPath}/type`,
|
|
577
|
+
nodeId: effectiveNodeId
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
} else if (options.knownComponentTypes) {
|
|
581
|
+
const isKnown = Array.isArray(options.knownComponentTypes) ? options.knownComponentTypes.includes(effectiveNodeType) : options.knownComponentTypes.has(effectiveNodeType);
|
|
582
|
+
if (!isKnown && options.strictComponentTypes) {
|
|
583
|
+
errors.push({
|
|
584
|
+
code: "UNKNOWN_COMPONENT_TYPE",
|
|
585
|
+
message: `Unknown component type: "${effectiveNodeType}"`,
|
|
586
|
+
path: `${currentPath}/type`,
|
|
587
|
+
nodeId: effectiveNodeId
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const children = nodeObj.children;
|
|
593
|
+
if (children !== void 0) {
|
|
594
|
+
if (!Array.isArray(children)) {
|
|
595
|
+
errors.push({
|
|
596
|
+
code: "NODE_SHAPE_INVALID",
|
|
597
|
+
message: "Node children must be an array",
|
|
598
|
+
path: `${currentPath}/children`,
|
|
599
|
+
nodeId: effectiveNodeId
|
|
600
|
+
});
|
|
601
|
+
} else {
|
|
602
|
+
if (componentDef && componentDef.acceptsChildren === false && children.length > 0) {
|
|
603
|
+
errors.push({
|
|
604
|
+
code: "CHILD_POLICY_VIOLATION",
|
|
605
|
+
message: `Component "${effectiveNodeType}" does not accept children, but found ${children.length} child node(s)`,
|
|
606
|
+
path: `${currentPath}/children`,
|
|
607
|
+
nodeId: effectiveNodeId
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
if (parentNode && componentDef?.disallowedParents?.includes(parentNode.type)) {
|
|
611
|
+
errors.push({
|
|
612
|
+
code: "CHILD_POLICY_VIOLATION",
|
|
613
|
+
message: `Component "${effectiveNodeType}" cannot be a child of "${parentNode.type}"`,
|
|
614
|
+
path: currentPath,
|
|
615
|
+
nodeId: effectiveNodeId
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
for (let i = 0; i < children.length; i++) {
|
|
619
|
+
const child = children[i];
|
|
620
|
+
const childPath = `${currentPath}/children/${i}`;
|
|
621
|
+
if (!child || typeof child !== "object" || Array.isArray(child)) {
|
|
622
|
+
errors.push({
|
|
623
|
+
code: "NODE_SHAPE_INVALID",
|
|
624
|
+
message: `Child at index ${i} must be a valid node object`,
|
|
625
|
+
path: childPath,
|
|
626
|
+
nodeId: effectiveNodeId
|
|
627
|
+
});
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const childRecord = child;
|
|
631
|
+
const childType = typeof childRecord.type === "string" ? childRecord.type : "";
|
|
632
|
+
const childIsKnown = !options.componentRegistry || options.componentRegistry.has(childType);
|
|
633
|
+
const childCategory = options.componentRegistry?.get(childType)?.category;
|
|
634
|
+
if (componentDef?.allowedChildren && childType && childIsKnown && !componentDef.allowedChildren.includes(childType) && !componentDef.allowedChildren.includes("*") && !(childCategory && componentDef.allowedChildren.includes(childCategory))) {
|
|
635
|
+
errors.push({
|
|
636
|
+
code: "CHILD_POLICY_VIOLATION",
|
|
637
|
+
message: `Component type "${childType}" is not allowed as a child of "${effectiveNodeType}". Allowed types: ${componentDef.allowedChildren.join(", ")}`,
|
|
638
|
+
path: `${childPath}/type`,
|
|
639
|
+
nodeId: typeof childRecord.id === "string" ? childRecord.id : void 0
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
validateNodeRecursive(
|
|
643
|
+
childRecord,
|
|
644
|
+
childPath,
|
|
645
|
+
{ id: effectiveNodeId || "", type: effectiveNodeType },
|
|
646
|
+
seenIds,
|
|
647
|
+
visitedObjects,
|
|
648
|
+
options,
|
|
649
|
+
errors
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (nodeObj.props !== void 0) {
|
|
655
|
+
if (typeof nodeObj.props !== "object" || nodeObj.props === null || Array.isArray(nodeObj.props)) {
|
|
656
|
+
errors.push({
|
|
657
|
+
code: "NODE_SHAPE_INVALID",
|
|
658
|
+
message: "Node props must be an object",
|
|
659
|
+
path: `${currentPath}/props`,
|
|
660
|
+
nodeId: effectiveNodeId
|
|
661
|
+
});
|
|
662
|
+
} else {
|
|
663
|
+
validatePropsBindings(
|
|
664
|
+
nodeObj.props,
|
|
665
|
+
`${currentPath}/props`,
|
|
666
|
+
effectiveNodeId,
|
|
667
|
+
options,
|
|
668
|
+
errors
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (nodeObj.styles !== void 0) {
|
|
673
|
+
if (typeof nodeObj.styles !== "object" || nodeObj.styles === null || Array.isArray(nodeObj.styles)) {
|
|
674
|
+
errors.push({
|
|
675
|
+
code: "NODE_SHAPE_INVALID",
|
|
676
|
+
message: "Node styles must be an object",
|
|
677
|
+
path: `${currentPath}/styles`,
|
|
678
|
+
nodeId: effectiveNodeId
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function validatePropsBindings(propsObj, propsPath, nodeId, options, errors) {
|
|
684
|
+
for (const [key, value] of Object.entries(propsObj)) {
|
|
685
|
+
const currentPath = `${propsPath}/${key}`;
|
|
686
|
+
if (!value || typeof value !== "object") {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (Array.isArray(value)) {
|
|
690
|
+
value.forEach((item, index) => {
|
|
691
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
692
|
+
validatePropsBindings(
|
|
693
|
+
item,
|
|
694
|
+
`${currentPath}/${index}`,
|
|
695
|
+
nodeId,
|
|
696
|
+
options,
|
|
697
|
+
errors
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
const record = value;
|
|
704
|
+
if (record.type === "asset") {
|
|
705
|
+
if (typeof record.assetId !== "string" || record.assetId.trim().length === 0) {
|
|
706
|
+
errors.push({
|
|
707
|
+
code: "INVALID_ASSET_REFERENCE",
|
|
708
|
+
message: 'Asset reference must have a non-empty "assetId"',
|
|
709
|
+
path: `${currentPath}/assetId`,
|
|
710
|
+
nodeId
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
if (record.fallbackUrl !== void 0 && typeof record.fallbackUrl !== "string") {
|
|
714
|
+
errors.push({
|
|
715
|
+
code: "INVALID_ASSET_REFERENCE",
|
|
716
|
+
message: "Asset fallbackUrl must be a string if provided",
|
|
717
|
+
path: `${currentPath}/fallbackUrl`,
|
|
718
|
+
nodeId
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
} else if (record.type === "variable") {
|
|
722
|
+
if (typeof record.key !== "string" || record.key.trim().length === 0) {
|
|
723
|
+
errors.push({
|
|
724
|
+
code: "INVALID_VARIABLE_BINDING",
|
|
725
|
+
message: 'Variable binding must have a non-empty "key"',
|
|
726
|
+
path: `${currentPath}/key`,
|
|
727
|
+
nodeId
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
} else if (key === "action" || typeof record.type === "string" && record.payload !== void 0) {
|
|
731
|
+
if (typeof record.type !== "string" || record.type.trim().length === 0) {
|
|
732
|
+
errors.push({
|
|
733
|
+
code: "INVALID_ACTION_BINDING",
|
|
734
|
+
message: 'Action binding must have a non-empty "type"',
|
|
735
|
+
path: `${currentPath}/type`,
|
|
736
|
+
nodeId
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
validatePropsBindings(record, currentPath, nodeId, options, errors);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/document-utils.ts
|
|
746
|
+
var import_schema2 = require("@kubuild/schema");
|
|
747
|
+
function createBlankDocument(title = "Untitled Page") {
|
|
748
|
+
return {
|
|
749
|
+
schema: import_schema2.SCHEMA_NAME,
|
|
750
|
+
version: "1.0.0",
|
|
751
|
+
metadata: {
|
|
752
|
+
title,
|
|
753
|
+
description: "",
|
|
754
|
+
author: "",
|
|
755
|
+
tags: [],
|
|
756
|
+
category: "general",
|
|
757
|
+
version: "1.0.0",
|
|
758
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
759
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
760
|
+
},
|
|
761
|
+
document: {
|
|
762
|
+
id: "root-page",
|
|
763
|
+
type: "page",
|
|
764
|
+
props: { title },
|
|
765
|
+
styles: { base: { minHeight: "100vh", backgroundColor: "#ffffff" } },
|
|
766
|
+
children: []
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function findNodeById(rootNode, targetId) {
|
|
771
|
+
if (rootNode.id === targetId) return rootNode;
|
|
772
|
+
if (!rootNode.children) return null;
|
|
773
|
+
for (const child of rootNode.children) {
|
|
774
|
+
const found = findNodeById(child, targetId);
|
|
775
|
+
if (found) return found;
|
|
776
|
+
}
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
function collectAssetReferences(rootNode) {
|
|
780
|
+
const results = [];
|
|
781
|
+
function traverseProps(propsObj, currentPath, nodeId) {
|
|
782
|
+
if (!propsObj || typeof propsObj !== "object") return;
|
|
783
|
+
if (Array.isArray(propsObj)) {
|
|
784
|
+
propsObj.forEach((item, index) => {
|
|
785
|
+
traverseProps(item, `${currentPath}/${index}`, nodeId);
|
|
786
|
+
});
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const record = propsObj;
|
|
790
|
+
if (record.type === "asset" && typeof record.assetId === "string" && record.assetId.trim().length > 0) {
|
|
791
|
+
results.push({
|
|
792
|
+
assetId: record.assetId,
|
|
793
|
+
reference: {
|
|
794
|
+
type: "asset",
|
|
795
|
+
assetId: record.assetId,
|
|
796
|
+
filename: typeof record.filename === "string" ? record.filename : void 0,
|
|
797
|
+
mimeType: typeof record.mimeType === "string" ? record.mimeType : void 0,
|
|
798
|
+
fallbackUrl: typeof record.fallbackUrl === "string" ? record.fallbackUrl : void 0
|
|
799
|
+
},
|
|
800
|
+
nodeId,
|
|
801
|
+
propPath: currentPath
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
for (const [key, val] of Object.entries(record)) {
|
|
805
|
+
if (val && typeof val === "object") {
|
|
806
|
+
traverseProps(val, `${currentPath}/${key}`, nodeId);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function traverseNode(node) {
|
|
811
|
+
if (node.props) {
|
|
812
|
+
traverseProps(node.props, `/document/${node.id}/props`, node.id);
|
|
813
|
+
}
|
|
814
|
+
if (node.children && Array.isArray(node.children)) {
|
|
815
|
+
for (const child of node.children) {
|
|
816
|
+
traverseNode(child);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
traverseNode(rootNode);
|
|
821
|
+
return results;
|
|
822
|
+
}
|
|
823
|
+
function extractRequirementsFromTree(rootNode, registry) {
|
|
824
|
+
const requiredComponents = /* @__PURE__ */ new Set();
|
|
825
|
+
const requiredCapabilities = /* @__PURE__ */ new Set();
|
|
826
|
+
function traverse(node) {
|
|
827
|
+
if (registry) {
|
|
828
|
+
const def = registry.get(node.type);
|
|
829
|
+
if (def?.category === "custom") {
|
|
830
|
+
requiredComponents.add(node.type);
|
|
831
|
+
}
|
|
832
|
+
if (def?.capabilities && Array.isArray(def.capabilities)) {
|
|
833
|
+
for (const cap of def.capabilities) {
|
|
834
|
+
requiredCapabilities.add(cap);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (node.children && Array.isArray(node.children)) {
|
|
839
|
+
for (const child of node.children) {
|
|
840
|
+
traverse(child);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
traverse(rootNode);
|
|
845
|
+
return {
|
|
846
|
+
requiredComponents: Array.from(requiredComponents).sort(),
|
|
847
|
+
requiredCapabilities: Array.from(requiredCapabilities).sort()
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function remapAssetReferences(rootNode, idMap) {
|
|
851
|
+
let remappedCount = 0;
|
|
852
|
+
const getMappedId = (id) => {
|
|
853
|
+
if (idMap instanceof Map) return idMap.get(id);
|
|
854
|
+
return idMap[id];
|
|
855
|
+
};
|
|
856
|
+
function traverseProps(propsObj) {
|
|
857
|
+
if (!propsObj || typeof propsObj !== "object") return;
|
|
858
|
+
if (Array.isArray(propsObj)) {
|
|
859
|
+
for (const item of propsObj) {
|
|
860
|
+
traverseProps(item);
|
|
861
|
+
}
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const record = propsObj;
|
|
865
|
+
if (record.type === "asset" && typeof record.assetId === "string") {
|
|
866
|
+
const newId = getMappedId(record.assetId);
|
|
867
|
+
if (newId !== void 0 && newId !== record.assetId) {
|
|
868
|
+
record.assetId = newId;
|
|
869
|
+
remappedCount++;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
for (const val of Object.values(record)) {
|
|
873
|
+
if (val && typeof val === "object") {
|
|
874
|
+
traverseProps(val);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function traverseNode(node) {
|
|
879
|
+
if (node.props) {
|
|
880
|
+
traverseProps(node.props);
|
|
881
|
+
}
|
|
882
|
+
if (node.children && Array.isArray(node.children)) {
|
|
883
|
+
for (const child of node.children) {
|
|
884
|
+
traverseNode(child);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
traverseNode(rootNode);
|
|
889
|
+
return remappedCount;
|
|
890
|
+
}
|
|
891
|
+
function remapDocumentAssetReferences(doc, idMap) {
|
|
892
|
+
if (doc && doc.document) {
|
|
893
|
+
remapAssetReferences(doc.document, idMap);
|
|
894
|
+
}
|
|
895
|
+
return doc;
|
|
896
|
+
}
|
|
897
|
+
function findMissingComponentNodes(rootNode, registry, knownTypes) {
|
|
898
|
+
const missing = [];
|
|
899
|
+
const knownSet = /* @__PURE__ */ new Set();
|
|
900
|
+
if (knownTypes) {
|
|
901
|
+
for (const t of knownTypes) knownSet.add(t);
|
|
902
|
+
}
|
|
903
|
+
const standardBuiltIns = [
|
|
904
|
+
"page",
|
|
905
|
+
"section",
|
|
906
|
+
"container",
|
|
907
|
+
"columns",
|
|
908
|
+
"heading",
|
|
909
|
+
"text",
|
|
910
|
+
"image",
|
|
911
|
+
"button",
|
|
912
|
+
"collection"
|
|
913
|
+
];
|
|
914
|
+
for (const b of standardBuiltIns) knownSet.add(b);
|
|
915
|
+
function checkNode(node) {
|
|
916
|
+
let isSupported = knownSet.has(node.type);
|
|
917
|
+
if (!isSupported && registry) {
|
|
918
|
+
isSupported = registry.has(node.type);
|
|
919
|
+
}
|
|
920
|
+
if (!isSupported) {
|
|
921
|
+
missing.push({
|
|
922
|
+
nodeId: node.id,
|
|
923
|
+
componentType: node.type,
|
|
924
|
+
props: node.props ? { ...node.props } : {},
|
|
925
|
+
node
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
if (node.children && Array.isArray(node.children)) {
|
|
929
|
+
for (const child of node.children) {
|
|
930
|
+
checkNode(child);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
checkNode(rootNode);
|
|
935
|
+
return missing;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/commands.ts
|
|
939
|
+
var import_schema4 = require("@kubuild/schema");
|
|
940
|
+
|
|
941
|
+
// src/command-tree-utils.ts
|
|
942
|
+
var import_schema3 = require("@kubuild/schema");
|
|
943
|
+
function deepClone(val) {
|
|
944
|
+
return JSON.parse(JSON.stringify(val));
|
|
945
|
+
}
|
|
946
|
+
function findNodeLocation(root, targetId) {
|
|
947
|
+
if (root.id === targetId) {
|
|
948
|
+
return {
|
|
949
|
+
node: root,
|
|
950
|
+
parent: null,
|
|
951
|
+
index: -1
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
return searchNodeLocationRecursive(root, targetId);
|
|
955
|
+
}
|
|
956
|
+
function searchNodeLocationRecursive(currentParent, targetId) {
|
|
957
|
+
if (!currentParent.children || currentParent.children.length === 0) {
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
for (let i = 0; i < currentParent.children.length; i++) {
|
|
961
|
+
const child = currentParent.children[i];
|
|
962
|
+
if (child.id === targetId) {
|
|
963
|
+
return {
|
|
964
|
+
node: child,
|
|
965
|
+
parent: currentParent,
|
|
966
|
+
index: i
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
const foundInSubtree = searchNodeLocationRecursive(child, targetId);
|
|
970
|
+
if (foundInSubtree) {
|
|
971
|
+
return foundInSubtree;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
function getNavigationTarget(root, nodeId, direction) {
|
|
977
|
+
const location = findNodeLocation(root, nodeId);
|
|
978
|
+
if (!location) return null;
|
|
979
|
+
switch (direction) {
|
|
980
|
+
case "parent":
|
|
981
|
+
return location.parent?.id ?? null;
|
|
982
|
+
case "child":
|
|
983
|
+
return location.node.children?.[0]?.id ?? null;
|
|
984
|
+
case "previous-sibling": {
|
|
985
|
+
if (!location.parent) return null;
|
|
986
|
+
return (location.parent.children ?? [])[location.index - 1]?.id ?? null;
|
|
987
|
+
}
|
|
988
|
+
case "next-sibling": {
|
|
989
|
+
if (!location.parent) return null;
|
|
990
|
+
return (location.parent.children ?? [])[location.index + 1]?.id ?? null;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
function getAncestorChain(root, targetId) {
|
|
995
|
+
if (root.id === targetId) return [];
|
|
996
|
+
function walk(node, acc) {
|
|
997
|
+
if (!node.children) return null;
|
|
998
|
+
for (const child of node.children) {
|
|
999
|
+
if (child.id === targetId) return acc;
|
|
1000
|
+
const found = walk(child, [...acc, child.id]);
|
|
1001
|
+
if (found) return found;
|
|
1002
|
+
}
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
return walk(root, [root.id]) ?? [];
|
|
1006
|
+
}
|
|
1007
|
+
function getParentNodeId(root, targetId) {
|
|
1008
|
+
if (root.id === targetId) return null;
|
|
1009
|
+
const stack = [root];
|
|
1010
|
+
while (stack.length > 0) {
|
|
1011
|
+
const current = stack.pop();
|
|
1012
|
+
const children = current.children;
|
|
1013
|
+
if (!children) continue;
|
|
1014
|
+
for (const child of children) {
|
|
1015
|
+
if (child.id === targetId) return current.id;
|
|
1016
|
+
stack.push(child);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
function getNodeAncestors(root, targetId) {
|
|
1022
|
+
const path = [];
|
|
1023
|
+
function walk(node) {
|
|
1024
|
+
path.push(node);
|
|
1025
|
+
if (node.id === targetId) return true;
|
|
1026
|
+
if (node.children) {
|
|
1027
|
+
for (const child of node.children) {
|
|
1028
|
+
if (walk(child)) return true;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
path.pop();
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
return walk(root) ? path : [];
|
|
1035
|
+
}
|
|
1036
|
+
function isDescendantOf(ancestorNode, targetId) {
|
|
1037
|
+
if (ancestorNode.id === targetId) {
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1040
|
+
if (!ancestorNode.children || ancestorNode.children.length === 0) {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
for (const child of ancestorNode.children) {
|
|
1044
|
+
if (isDescendantOf(child, targetId)) {
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
function collectNodeIdSet(root) {
|
|
1051
|
+
return new Set((0, import_schema3.collectNodeIds)(root));
|
|
1052
|
+
}
|
|
1053
|
+
function defaultIdGenerator(oldId, existingIds) {
|
|
1054
|
+
const baseName = oldId.replace(/_copy(_\d+)?$/, "");
|
|
1055
|
+
let candidate = `${baseName}_copy`;
|
|
1056
|
+
let counter = 1;
|
|
1057
|
+
if (existingIds) {
|
|
1058
|
+
while (existingIds.has(candidate)) {
|
|
1059
|
+
candidate = `${baseName}_copy_${counter}`;
|
|
1060
|
+
counter++;
|
|
1061
|
+
}
|
|
1062
|
+
existingIds.add(candidate);
|
|
1063
|
+
}
|
|
1064
|
+
return candidate;
|
|
1065
|
+
}
|
|
1066
|
+
function cloneTreeWithNewIds(root, idGenerator, existingIds) {
|
|
1067
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1068
|
+
const idGen = idGenerator || ((oldId) => defaultIdGenerator(oldId, existingIds));
|
|
1069
|
+
function cloneRecursive(node) {
|
|
1070
|
+
const newId = idGen(node.id);
|
|
1071
|
+
idMap.set(node.id, newId);
|
|
1072
|
+
const clonedProps = node.props ? deepClone(node.props) : void 0;
|
|
1073
|
+
const clonedStyles = node.styles ? deepClone(node.styles) : void 0;
|
|
1074
|
+
const clonedChildren = node.children ? node.children.map((child) => cloneRecursive(child)) : [];
|
|
1075
|
+
return {
|
|
1076
|
+
id: newId,
|
|
1077
|
+
type: node.type,
|
|
1078
|
+
...clonedProps ? { props: clonedProps } : {},
|
|
1079
|
+
...clonedStyles ? { styles: clonedStyles } : {},
|
|
1080
|
+
children: clonedChildren
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
const clonedNode = cloneRecursive(root);
|
|
1084
|
+
return { clonedNode, idMap };
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/commands.ts
|
|
1088
|
+
function insertNode(document, params) {
|
|
1089
|
+
const { parentId, node, index } = params;
|
|
1090
|
+
if (!node || typeof node !== "object" || !node.id || !node.type) {
|
|
1091
|
+
throw new Error('Invalid node: Node must be an object with non-empty "id" and "type"');
|
|
1092
|
+
}
|
|
1093
|
+
const newDoc = deepClone(document);
|
|
1094
|
+
const existingIds = collectNodeIdSet(newDoc.document);
|
|
1095
|
+
const incomingIds = collectNodeIdSet(node);
|
|
1096
|
+
for (const incId of incomingIds) {
|
|
1097
|
+
if (existingIds.has(incId)) {
|
|
1098
|
+
throw new Error(`Cannot insert node: Duplicate node ID "${incId}" already exists in document.`);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const parentLoc = findNodeLocation(newDoc.document, parentId);
|
|
1102
|
+
if (!parentLoc) {
|
|
1103
|
+
throw new Error(`Cannot insert node: Target parent node with ID "${parentId}" not found.`);
|
|
1104
|
+
}
|
|
1105
|
+
const parentNode = parentLoc.node;
|
|
1106
|
+
if (!parentNode.children) {
|
|
1107
|
+
parentNode.children = [];
|
|
1108
|
+
}
|
|
1109
|
+
const insertIndex = typeof index === "number" && index >= 0 && index <= parentNode.children.length ? index : parentNode.children.length;
|
|
1110
|
+
parentNode.children.splice(insertIndex, 0, deepClone(node));
|
|
1111
|
+
return {
|
|
1112
|
+
document: newDoc,
|
|
1113
|
+
event: {
|
|
1114
|
+
type: "NODE_INSERTED",
|
|
1115
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1116
|
+
nodeId: node.id,
|
|
1117
|
+
parentId,
|
|
1118
|
+
index: insertIndex,
|
|
1119
|
+
payload: {
|
|
1120
|
+
nodeType: node.type
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function moveNode(document, params) {
|
|
1126
|
+
const { nodeId, targetParentId, index } = params;
|
|
1127
|
+
if (nodeId === document.document.id) {
|
|
1128
|
+
throw new Error("Cannot move the root page node.");
|
|
1129
|
+
}
|
|
1130
|
+
const newDoc = deepClone(document);
|
|
1131
|
+
const sourceLoc = findNodeLocation(newDoc.document, nodeId);
|
|
1132
|
+
if (!sourceLoc || !sourceLoc.parent) {
|
|
1133
|
+
throw new Error(`Cannot move node: Node with ID "${nodeId}" not found in document.`);
|
|
1134
|
+
}
|
|
1135
|
+
const targetLoc = findNodeLocation(newDoc.document, targetParentId);
|
|
1136
|
+
if (!targetLoc) {
|
|
1137
|
+
throw new Error(`Cannot move node: Target parent with ID "${targetParentId}" not found in document.`);
|
|
1138
|
+
}
|
|
1139
|
+
if (isDescendantOf(sourceLoc.node, targetParentId)) {
|
|
1140
|
+
throw new Error(
|
|
1141
|
+
`Cannot move node: Target parent "${targetParentId}" is the node itself or a descendant of node "${nodeId}".`
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
const previousParentId = sourceLoc.parent.id;
|
|
1145
|
+
const previousIndex = sourceLoc.index;
|
|
1146
|
+
const [removedNode] = sourceLoc.parent.children.splice(previousIndex, 1);
|
|
1147
|
+
const targetParent = targetLoc.node;
|
|
1148
|
+
if (!targetParent.children) {
|
|
1149
|
+
targetParent.children = [];
|
|
1150
|
+
}
|
|
1151
|
+
let targetIndex = typeof index === "number" && index >= 0 && index <= targetParent.children.length ? index : targetParent.children.length;
|
|
1152
|
+
targetParent.children.splice(targetIndex, 0, removedNode);
|
|
1153
|
+
return {
|
|
1154
|
+
document: newDoc,
|
|
1155
|
+
event: {
|
|
1156
|
+
type: "NODE_MOVED",
|
|
1157
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1158
|
+
nodeId,
|
|
1159
|
+
parentId: targetParentId,
|
|
1160
|
+
previousParentId,
|
|
1161
|
+
index: targetIndex,
|
|
1162
|
+
previousIndex
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function updateProps(document, params) {
|
|
1167
|
+
const { nodeId, props, merge = true } = params;
|
|
1168
|
+
if (!props || typeof props !== "object" || Array.isArray(props)) {
|
|
1169
|
+
throw new Error("Props must be an object.");
|
|
1170
|
+
}
|
|
1171
|
+
const newDoc = deepClone(document);
|
|
1172
|
+
const loc = findNodeLocation(newDoc.document, nodeId);
|
|
1173
|
+
if (!loc) {
|
|
1174
|
+
throw new Error(`Cannot update props: Node with ID "${nodeId}" not found in document.`);
|
|
1175
|
+
}
|
|
1176
|
+
const targetNode = loc.node;
|
|
1177
|
+
const previousProps = targetNode.props ? deepClone(targetNode.props) : {};
|
|
1178
|
+
if (merge) {
|
|
1179
|
+
targetNode.props = {
|
|
1180
|
+
...previousProps,
|
|
1181
|
+
...deepClone(props)
|
|
1182
|
+
};
|
|
1183
|
+
} else {
|
|
1184
|
+
targetNode.props = deepClone(props);
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
document: newDoc,
|
|
1188
|
+
event: {
|
|
1189
|
+
type: "PROPS_UPDATED",
|
|
1190
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1191
|
+
nodeId,
|
|
1192
|
+
parentId: loc.parent ? loc.parent.id : void 0,
|
|
1193
|
+
payload: {
|
|
1194
|
+
props: targetNode.props,
|
|
1195
|
+
previousProps
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
function updateStyle(document, params) {
|
|
1201
|
+
const { nodeId, styles, breakpoint, state, merge = true } = params;
|
|
1202
|
+
if (!styles || typeof styles !== "object" || Array.isArray(styles)) {
|
|
1203
|
+
throw new Error("Styles must be an object.");
|
|
1204
|
+
}
|
|
1205
|
+
const newDoc = deepClone(document);
|
|
1206
|
+
const loc = findNodeLocation(newDoc.document, nodeId);
|
|
1207
|
+
if (!loc) {
|
|
1208
|
+
throw new Error(`Cannot update style: Node with ID "${nodeId}" not found in document.`);
|
|
1209
|
+
}
|
|
1210
|
+
const targetNode = loc.node;
|
|
1211
|
+
const currentStyles = targetNode.styles ? deepClone(targetNode.styles) : {};
|
|
1212
|
+
const previousStyles = deepClone(currentStyles);
|
|
1213
|
+
if (state) {
|
|
1214
|
+
const newBreakpointStyles = import_schema4.StyleDefinitionSchema.parse(styles);
|
|
1215
|
+
const states = { ...currentStyles.states ?? {} };
|
|
1216
|
+
const currentStateStyles = states[state] ?? {};
|
|
1217
|
+
if (merge) {
|
|
1218
|
+
states[state] = { ...currentStateStyles, ...deepClone(newBreakpointStyles) };
|
|
1219
|
+
} else {
|
|
1220
|
+
states[state] = deepClone(newBreakpointStyles);
|
|
1221
|
+
}
|
|
1222
|
+
targetNode.styles = { ...currentStyles, states };
|
|
1223
|
+
return {
|
|
1224
|
+
document: newDoc,
|
|
1225
|
+
event: {
|
|
1226
|
+
type: "STYLE_UPDATED",
|
|
1227
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1228
|
+
nodeId,
|
|
1229
|
+
parentId: loc.parent ? loc.parent.id : void 0,
|
|
1230
|
+
payload: {
|
|
1231
|
+
styles: targetNode.styles,
|
|
1232
|
+
previousStyles,
|
|
1233
|
+
state
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
if (breakpoint) {
|
|
1239
|
+
const currentBreakpointStyles = currentStyles[breakpoint] || {};
|
|
1240
|
+
const newBreakpointStyles = import_schema4.StyleDefinitionSchema.parse(styles);
|
|
1241
|
+
if (merge) {
|
|
1242
|
+
currentStyles[breakpoint] = {
|
|
1243
|
+
...currentBreakpointStyles,
|
|
1244
|
+
...deepClone(newBreakpointStyles)
|
|
1245
|
+
};
|
|
1246
|
+
} else {
|
|
1247
|
+
currentStyles[breakpoint] = deepClone(newBreakpointStyles);
|
|
1248
|
+
}
|
|
1249
|
+
} else {
|
|
1250
|
+
const responsiveStylesInput = import_schema4.ResponsiveStylesSchema.parse(styles);
|
|
1251
|
+
if (merge) {
|
|
1252
|
+
for (const bp of ["base", "desktop", "tablet", "mobile"]) {
|
|
1253
|
+
if (responsiveStylesInput[bp] !== void 0) {
|
|
1254
|
+
const currentBp = currentStyles[bp] || {};
|
|
1255
|
+
const incomingBp = responsiveStylesInput[bp] || {};
|
|
1256
|
+
currentStyles[bp] = {
|
|
1257
|
+
...currentBp,
|
|
1258
|
+
...deepClone(incomingBp)
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
} else {
|
|
1263
|
+
targetNode.styles = deepClone(responsiveStylesInput);
|
|
1264
|
+
return {
|
|
1265
|
+
document: newDoc,
|
|
1266
|
+
event: {
|
|
1267
|
+
type: "STYLE_UPDATED",
|
|
1268
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1269
|
+
nodeId,
|
|
1270
|
+
parentId: loc.parent ? loc.parent.id : void 0,
|
|
1271
|
+
payload: {
|
|
1272
|
+
styles: targetNode.styles,
|
|
1273
|
+
previousStyles
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
targetNode.styles = currentStyles;
|
|
1280
|
+
return {
|
|
1281
|
+
document: newDoc,
|
|
1282
|
+
event: {
|
|
1283
|
+
type: "STYLE_UPDATED",
|
|
1284
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1285
|
+
nodeId,
|
|
1286
|
+
parentId: loc.parent ? loc.parent.id : void 0,
|
|
1287
|
+
payload: {
|
|
1288
|
+
styles: currentStyles,
|
|
1289
|
+
previousStyles,
|
|
1290
|
+
breakpoint
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function removeNode(document, params) {
|
|
1296
|
+
const { nodeId } = params;
|
|
1297
|
+
if (nodeId === document.document.id) {
|
|
1298
|
+
throw new Error("Cannot remove the root page node.");
|
|
1299
|
+
}
|
|
1300
|
+
const newDoc = deepClone(document);
|
|
1301
|
+
const loc = findNodeLocation(newDoc.document, nodeId);
|
|
1302
|
+
if (!loc || !loc.parent) {
|
|
1303
|
+
throw new Error(`Cannot remove node: Node with ID "${nodeId}" not found in document.`);
|
|
1304
|
+
}
|
|
1305
|
+
const parentId = loc.parent.id;
|
|
1306
|
+
const index = loc.index;
|
|
1307
|
+
const [removedNode] = loc.parent.children.splice(index, 1);
|
|
1308
|
+
return {
|
|
1309
|
+
document: newDoc,
|
|
1310
|
+
event: {
|
|
1311
|
+
type: "NODE_REMOVED",
|
|
1312
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1313
|
+
nodeId,
|
|
1314
|
+
parentId,
|
|
1315
|
+
index,
|
|
1316
|
+
payload: {
|
|
1317
|
+
node: removedNode
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
function duplicateNode(document, params) {
|
|
1323
|
+
const { nodeId, targetParentId, index, idGenerator } = params;
|
|
1324
|
+
if (nodeId === document.document.id) {
|
|
1325
|
+
throw new Error("Cannot duplicate the root page node.");
|
|
1326
|
+
}
|
|
1327
|
+
const newDoc = deepClone(document);
|
|
1328
|
+
const loc = findNodeLocation(newDoc.document, nodeId);
|
|
1329
|
+
if (!loc || !loc.parent) {
|
|
1330
|
+
throw new Error(`Cannot duplicate node: Node with ID "${nodeId}" not found in document.`);
|
|
1331
|
+
}
|
|
1332
|
+
const existingIds = collectNodeIdSet(newDoc.document);
|
|
1333
|
+
const { clonedNode, idMap } = cloneTreeWithNewIds(loc.node, idGenerator, existingIds);
|
|
1334
|
+
const destinationParentId = targetParentId || loc.parent.id;
|
|
1335
|
+
const destParentLoc = findNodeLocation(newDoc.document, destinationParentId);
|
|
1336
|
+
if (!destParentLoc) {
|
|
1337
|
+
throw new Error(`Cannot duplicate node: Destination parent "${destinationParentId}" not found.`);
|
|
1338
|
+
}
|
|
1339
|
+
const destParent = destParentLoc.node;
|
|
1340
|
+
if (!destParent.children) {
|
|
1341
|
+
destParent.children = [];
|
|
1342
|
+
}
|
|
1343
|
+
let insertIndex;
|
|
1344
|
+
if (typeof index === "number" && index >= 0 && index <= destParent.children.length) {
|
|
1345
|
+
insertIndex = index;
|
|
1346
|
+
} else if (destinationParentId === loc.parent.id) {
|
|
1347
|
+
insertIndex = loc.index + 1;
|
|
1348
|
+
} else {
|
|
1349
|
+
insertIndex = destParent.children.length;
|
|
1350
|
+
}
|
|
1351
|
+
destParent.children.splice(insertIndex, 0, clonedNode);
|
|
1352
|
+
return {
|
|
1353
|
+
document: newDoc,
|
|
1354
|
+
event: {
|
|
1355
|
+
type: "NODE_DUPLICATED",
|
|
1356
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1357
|
+
nodeId: clonedNode.id,
|
|
1358
|
+
parentId: destinationParentId,
|
|
1359
|
+
index: insertIndex,
|
|
1360
|
+
payload: {
|
|
1361
|
+
originalNodeId: nodeId,
|
|
1362
|
+
idMap: Object.fromEntries(idMap.entries())
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/history.ts
|
|
1369
|
+
var DEFAULT_MAX_HISTORY = 50;
|
|
1370
|
+
var HistoryEngine = class {
|
|
1371
|
+
past = [];
|
|
1372
|
+
current;
|
|
1373
|
+
future = [];
|
|
1374
|
+
maxHistory;
|
|
1375
|
+
constructor(initialState, options = {}) {
|
|
1376
|
+
if (initialState === void 0 || initialState === null) {
|
|
1377
|
+
throw new Error("Initial state must be defined.");
|
|
1378
|
+
}
|
|
1379
|
+
const limit = options.maxHistory ?? DEFAULT_MAX_HISTORY;
|
|
1380
|
+
if (typeof limit !== "number" || limit <= 0 || !Number.isFinite(limit)) {
|
|
1381
|
+
throw new Error("maxHistory option must be a positive integer.");
|
|
1382
|
+
}
|
|
1383
|
+
this.maxHistory = Math.floor(limit);
|
|
1384
|
+
this.current = {
|
|
1385
|
+
state: deepClone(initialState),
|
|
1386
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Get the current active state (returns an immutable clone).
|
|
1391
|
+
*/
|
|
1392
|
+
get present() {
|
|
1393
|
+
return deepClone(this.current.state);
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Check if undo is available.
|
|
1397
|
+
*/
|
|
1398
|
+
canUndo() {
|
|
1399
|
+
return this.past.length > 0;
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Check if redo is available.
|
|
1403
|
+
*/
|
|
1404
|
+
canRedo() {
|
|
1405
|
+
return this.future.length > 0;
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Get total number of states in undo stack.
|
|
1409
|
+
*/
|
|
1410
|
+
get undoCount() {
|
|
1411
|
+
return this.past.length;
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Get total number of states in redo stack.
|
|
1415
|
+
*/
|
|
1416
|
+
get redoCount() {
|
|
1417
|
+
return this.future.length;
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Get max history limit.
|
|
1421
|
+
*/
|
|
1422
|
+
get limit() {
|
|
1423
|
+
return this.maxHistory;
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Push a new state.
|
|
1427
|
+
* Clears the redo stack and shifts oldest past entries if maxHistory is exceeded.
|
|
1428
|
+
*/
|
|
1429
|
+
push(nextState, event) {
|
|
1430
|
+
if (nextState === void 0 || nextState === null) {
|
|
1431
|
+
throw new Error("Cannot push undefined or null state into history.");
|
|
1432
|
+
}
|
|
1433
|
+
this.past.push(this.current);
|
|
1434
|
+
while (this.past.length > this.maxHistory) {
|
|
1435
|
+
this.past.shift();
|
|
1436
|
+
}
|
|
1437
|
+
this.current = {
|
|
1438
|
+
state: deepClone(nextState),
|
|
1439
|
+
event,
|
|
1440
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1441
|
+
};
|
|
1442
|
+
this.future = [];
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Undo to the previous state.
|
|
1446
|
+
* Returns the restored state, or undefined if undo is not possible.
|
|
1447
|
+
*/
|
|
1448
|
+
undo() {
|
|
1449
|
+
if (!this.canUndo()) {
|
|
1450
|
+
return void 0;
|
|
1451
|
+
}
|
|
1452
|
+
const previousEntry = this.past.pop();
|
|
1453
|
+
this.future.unshift(this.current);
|
|
1454
|
+
this.current = previousEntry;
|
|
1455
|
+
return this.present;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Redo to the next state in future stack.
|
|
1459
|
+
* Returns the restored state, or undefined if redo is not possible.
|
|
1460
|
+
*/
|
|
1461
|
+
redo() {
|
|
1462
|
+
if (!this.canRedo()) {
|
|
1463
|
+
return void 0;
|
|
1464
|
+
}
|
|
1465
|
+
const nextEntry = this.future.shift();
|
|
1466
|
+
this.past.push(this.current);
|
|
1467
|
+
this.current = nextEntry;
|
|
1468
|
+
return this.present;
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Reset the history engine with a new initial state.
|
|
1472
|
+
* Clears both undo and redo stacks (e.g. when loading a new document).
|
|
1473
|
+
*/
|
|
1474
|
+
reset(newState) {
|
|
1475
|
+
if (newState === void 0 || newState === null) {
|
|
1476
|
+
throw new Error("New state must be defined when resetting history.");
|
|
1477
|
+
}
|
|
1478
|
+
this.past = [];
|
|
1479
|
+
this.future = [];
|
|
1480
|
+
this.current = {
|
|
1481
|
+
state: deepClone(newState),
|
|
1482
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Clear both past and future stacks without changing the current state.
|
|
1487
|
+
*/
|
|
1488
|
+
clear() {
|
|
1489
|
+
this.past = [];
|
|
1490
|
+
this.future = [];
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Get a snapshot of current history status.
|
|
1494
|
+
*/
|
|
1495
|
+
getState() {
|
|
1496
|
+
return {
|
|
1497
|
+
canUndo: this.canUndo(),
|
|
1498
|
+
canRedo: this.canRedo(),
|
|
1499
|
+
undoCount: this.undoCount,
|
|
1500
|
+
redoCount: this.redoCount,
|
|
1501
|
+
present: this.present
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
var DocumentHistoryManager = class {
|
|
1506
|
+
history;
|
|
1507
|
+
constructor(initialDocument, options) {
|
|
1508
|
+
this.history = new HistoryEngine(initialDocument, options);
|
|
1509
|
+
}
|
|
1510
|
+
get document() {
|
|
1511
|
+
return this.history.present;
|
|
1512
|
+
}
|
|
1513
|
+
get canUndo() {
|
|
1514
|
+
return this.history.canUndo();
|
|
1515
|
+
}
|
|
1516
|
+
get canRedo() {
|
|
1517
|
+
return this.history.canRedo();
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Execute a mutation command immutably and record the resulting state and change event in history.
|
|
1521
|
+
*/
|
|
1522
|
+
execute(executor) {
|
|
1523
|
+
const currentDoc = this.history.present;
|
|
1524
|
+
const result = executor(currentDoc);
|
|
1525
|
+
this.history.push(result.document, result.event);
|
|
1526
|
+
return result;
|
|
1527
|
+
}
|
|
1528
|
+
undo() {
|
|
1529
|
+
return this.history.undo();
|
|
1530
|
+
}
|
|
1531
|
+
redo() {
|
|
1532
|
+
return this.history.redo();
|
|
1533
|
+
}
|
|
1534
|
+
reset(newDocument) {
|
|
1535
|
+
this.history.reset(newDocument);
|
|
1536
|
+
}
|
|
1537
|
+
clear() {
|
|
1538
|
+
this.history.clear();
|
|
1539
|
+
}
|
|
1540
|
+
getState() {
|
|
1541
|
+
return this.history.getState();
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1545
|
+
// src/migration.ts
|
|
1546
|
+
var import_schema5 = require("@kubuild/schema");
|
|
1547
|
+
var MigrationRegistry = class {
|
|
1548
|
+
steps = /* @__PURE__ */ new Map();
|
|
1549
|
+
/**
|
|
1550
|
+
* Register a migration step from one version to another.
|
|
1551
|
+
*/
|
|
1552
|
+
register(step) {
|
|
1553
|
+
const key = this.getKey(step.fromVersion, step.toVersion);
|
|
1554
|
+
this.steps.set(key, step);
|
|
1555
|
+
}
|
|
1556
|
+
getKey(fromVersion, toVersion) {
|
|
1557
|
+
return `${fromVersion}->${toVersion}`;
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Find shortest path of versions to migrate from source to target using BFS.
|
|
1561
|
+
*/
|
|
1562
|
+
findPath(sourceVersion, targetVersion) {
|
|
1563
|
+
if (sourceVersion === targetVersion) {
|
|
1564
|
+
return [sourceVersion];
|
|
1565
|
+
}
|
|
1566
|
+
const queue = [
|
|
1567
|
+
{ version: sourceVersion, path: [sourceVersion] }
|
|
1568
|
+
];
|
|
1569
|
+
const visited = /* @__PURE__ */ new Set([sourceVersion]);
|
|
1570
|
+
while (queue.length > 0) {
|
|
1571
|
+
const { version: currentVersion, path } = queue.shift();
|
|
1572
|
+
for (const step of this.steps.values()) {
|
|
1573
|
+
if (step.fromVersion === currentVersion) {
|
|
1574
|
+
const nextVersion = step.toVersion;
|
|
1575
|
+
if (nextVersion === targetVersion) {
|
|
1576
|
+
return [...path, nextVersion];
|
|
1577
|
+
}
|
|
1578
|
+
if (!visited.has(nextVersion)) {
|
|
1579
|
+
visited.add(nextVersion);
|
|
1580
|
+
queue.push({ version: nextVersion, path: [...path, nextVersion] });
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Check if a valid migration path exists between source and target versions.
|
|
1589
|
+
*/
|
|
1590
|
+
hasPath(sourceVersion, targetVersion) {
|
|
1591
|
+
return this.findPath(sourceVersion, targetVersion) !== null;
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Get the migration step handler for a direct version step.
|
|
1595
|
+
*/
|
|
1596
|
+
getStep(fromVersion, toVersion) {
|
|
1597
|
+
return this.steps.get(this.getKey(fromVersion, toVersion));
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
function convertNodeStylesToResponsive(node) {
|
|
1601
|
+
if (node.styles && typeof node.styles === "object" && !Array.isArray(node.styles)) {
|
|
1602
|
+
const styles = node.styles;
|
|
1603
|
+
const hasBreakpointKeys = "base" in styles || "desktop" in styles || "tablet" in styles || "mobile" in styles;
|
|
1604
|
+
if (!hasBreakpointKeys) {
|
|
1605
|
+
node.styles = {
|
|
1606
|
+
base: { ...styles }
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if (Array.isArray(node.children)) {
|
|
1611
|
+
for (const child of node.children) {
|
|
1612
|
+
if (child && typeof child === "object") {
|
|
1613
|
+
convertNodeStylesToResponsive(child);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
var defaultMigrationRegistry = new MigrationRegistry();
|
|
1619
|
+
defaultMigrationRegistry.register({
|
|
1620
|
+
fromVersion: "0.1.0",
|
|
1621
|
+
toVersion: "1.0.0",
|
|
1622
|
+
description: "Migrate alpha 0.1.0 schema (root node key, flat styles) to v1.0.0",
|
|
1623
|
+
migrate: (rawDoc) => {
|
|
1624
|
+
const migrated = deepClone(rawDoc);
|
|
1625
|
+
migrated.schema = import_schema5.SCHEMA_NAME;
|
|
1626
|
+
migrated.version = "1.0.0";
|
|
1627
|
+
if (migrated.root && !migrated.document) {
|
|
1628
|
+
migrated.document = migrated.root;
|
|
1629
|
+
delete migrated.root;
|
|
1630
|
+
}
|
|
1631
|
+
if (!migrated.metadata || typeof migrated.metadata !== "object") {
|
|
1632
|
+
migrated.metadata = {
|
|
1633
|
+
title: "Migrated Page",
|
|
1634
|
+
description: "",
|
|
1635
|
+
author: "",
|
|
1636
|
+
tags: [],
|
|
1637
|
+
category: "general",
|
|
1638
|
+
version: "1.0.0",
|
|
1639
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1640
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1641
|
+
};
|
|
1642
|
+
} else {
|
|
1643
|
+
const meta = migrated.metadata;
|
|
1644
|
+
meta.title = meta.title || "Migrated Page";
|
|
1645
|
+
meta.tags = Array.isArray(meta.tags) ? meta.tags : [];
|
|
1646
|
+
meta.version = "1.0.0";
|
|
1647
|
+
}
|
|
1648
|
+
if (migrated.document && typeof migrated.document === "object") {
|
|
1649
|
+
convertNodeStylesToResponsive(migrated.document);
|
|
1650
|
+
}
|
|
1651
|
+
return migrated;
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
defaultMigrationRegistry.register({
|
|
1655
|
+
fromVersion: "0.2.0",
|
|
1656
|
+
toVersion: "1.0.0",
|
|
1657
|
+
description: "Migrate 0.2.0 schema to v1.0.0",
|
|
1658
|
+
migrate: (rawDoc) => {
|
|
1659
|
+
const migrated = deepClone(rawDoc);
|
|
1660
|
+
migrated.schema = import_schema5.SCHEMA_NAME;
|
|
1661
|
+
migrated.version = "1.0.0";
|
|
1662
|
+
if (migrated.document && typeof migrated.document === "object") {
|
|
1663
|
+
convertNodeStylesToResponsive(migrated.document);
|
|
1664
|
+
}
|
|
1665
|
+
return migrated;
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
defaultMigrationRegistry.register({
|
|
1669
|
+
fromVersion: "0.9.0",
|
|
1670
|
+
toVersion: "1.0.0",
|
|
1671
|
+
description: "Migrate 0.9.0 beta schema to v1.0.0",
|
|
1672
|
+
migrate: (rawDoc) => {
|
|
1673
|
+
const migrated = deepClone(rawDoc);
|
|
1674
|
+
migrated.schema = import_schema5.SCHEMA_NAME;
|
|
1675
|
+
migrated.version = "1.0.0";
|
|
1676
|
+
if (!migrated.metadata || typeof migrated.metadata !== "object") {
|
|
1677
|
+
migrated.metadata = {
|
|
1678
|
+
title: "Migrated Page",
|
|
1679
|
+
description: "",
|
|
1680
|
+
author: "",
|
|
1681
|
+
tags: [],
|
|
1682
|
+
category: "general",
|
|
1683
|
+
version: "1.0.0"
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
return migrated;
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
defaultMigrationRegistry.register({
|
|
1690
|
+
fromVersion: "1.0",
|
|
1691
|
+
toVersion: "1.0.0",
|
|
1692
|
+
description: "Normalize 1.0 version string to 1.0.0",
|
|
1693
|
+
migrate: (rawDoc) => {
|
|
1694
|
+
const migrated = deepClone(rawDoc);
|
|
1695
|
+
migrated.version = "1.0.0";
|
|
1696
|
+
return migrated;
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
function canMigrate(sourceVersion, targetVersion = import_schema5.CURRENT_SCHEMA_VERSION, registry = defaultMigrationRegistry) {
|
|
1700
|
+
if (!sourceVersion || !targetVersion) return false;
|
|
1701
|
+
if (sourceVersion === targetVersion) return true;
|
|
1702
|
+
return registry.hasPath(sourceVersion, targetVersion);
|
|
1703
|
+
}
|
|
1704
|
+
function getMigrationPath(sourceVersion, targetVersion = import_schema5.CURRENT_SCHEMA_VERSION, registry = defaultMigrationRegistry) {
|
|
1705
|
+
if (!sourceVersion || !targetVersion) return null;
|
|
1706
|
+
return registry.findPath(sourceVersion, targetVersion);
|
|
1707
|
+
}
|
|
1708
|
+
function migrateDocument(rawDocument, options = {}) {
|
|
1709
|
+
const targetVersion = options.targetVersion ?? import_schema5.CURRENT_SCHEMA_VERSION;
|
|
1710
|
+
const dryRun = options.dryRun ?? false;
|
|
1711
|
+
const registry = options.registry ?? defaultMigrationRegistry;
|
|
1712
|
+
const validate = options.validate ?? true;
|
|
1713
|
+
if (!rawDocument || typeof rawDocument !== "object" || Array.isArray(rawDocument)) {
|
|
1714
|
+
const error = {
|
|
1715
|
+
code: "INVALID_SOURCE_DOCUMENT",
|
|
1716
|
+
message: "Source document must be a non-null object.",
|
|
1717
|
+
sourceVersion: "unknown",
|
|
1718
|
+
targetVersion
|
|
1719
|
+
};
|
|
1720
|
+
return {
|
|
1721
|
+
success: false,
|
|
1722
|
+
diagnostic: {
|
|
1723
|
+
success: false,
|
|
1724
|
+
sourceVersion: "unknown",
|
|
1725
|
+
targetVersion,
|
|
1726
|
+
migrationPath: [],
|
|
1727
|
+
stepsApplied: 0,
|
|
1728
|
+
dryRun,
|
|
1729
|
+
errors: [error]
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
const doc = rawDocument;
|
|
1734
|
+
const sourceVersion = typeof doc.version === "string" ? doc.version.trim() : "unknown";
|
|
1735
|
+
if (sourceVersion === targetVersion) {
|
|
1736
|
+
const cloned = deepClone(doc);
|
|
1737
|
+
if (validate) {
|
|
1738
|
+
const validation = validateDocument(cloned);
|
|
1739
|
+
if (!validation.valid) {
|
|
1740
|
+
return {
|
|
1741
|
+
success: false,
|
|
1742
|
+
diagnostic: {
|
|
1743
|
+
success: false,
|
|
1744
|
+
sourceVersion,
|
|
1745
|
+
targetVersion,
|
|
1746
|
+
migrationPath: [sourceVersion],
|
|
1747
|
+
stepsApplied: 0,
|
|
1748
|
+
dryRun,
|
|
1749
|
+
errors: validation.errors.map((e) => ({
|
|
1750
|
+
code: "INVALID_SOURCE_DOCUMENT",
|
|
1751
|
+
message: `Validation error: ${e.message} at ${e.path}`,
|
|
1752
|
+
sourceVersion,
|
|
1753
|
+
targetVersion
|
|
1754
|
+
}))
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return {
|
|
1760
|
+
success: true,
|
|
1761
|
+
document: cloned,
|
|
1762
|
+
diagnostic: {
|
|
1763
|
+
success: true,
|
|
1764
|
+
sourceVersion,
|
|
1765
|
+
targetVersion,
|
|
1766
|
+
migrationPath: [sourceVersion],
|
|
1767
|
+
stepsApplied: 0,
|
|
1768
|
+
dryRun
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
const path = registry.findPath(sourceVersion, targetVersion);
|
|
1773
|
+
if (!path || path.length <= 1) {
|
|
1774
|
+
const error = {
|
|
1775
|
+
code: "NO_MIGRATION_PATH",
|
|
1776
|
+
message: `No migration path found from schema version "${sourceVersion}" to "${targetVersion}".`,
|
|
1777
|
+
sourceVersion,
|
|
1778
|
+
targetVersion
|
|
1779
|
+
};
|
|
1780
|
+
return {
|
|
1781
|
+
success: false,
|
|
1782
|
+
diagnostic: {
|
|
1783
|
+
success: false,
|
|
1784
|
+
sourceVersion,
|
|
1785
|
+
targetVersion,
|
|
1786
|
+
migrationPath: [],
|
|
1787
|
+
stepsApplied: 0,
|
|
1788
|
+
dryRun,
|
|
1789
|
+
errors: [error]
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
if (dryRun) {
|
|
1794
|
+
return {
|
|
1795
|
+
success: true,
|
|
1796
|
+
diagnostic: {
|
|
1797
|
+
success: true,
|
|
1798
|
+
sourceVersion,
|
|
1799
|
+
targetVersion,
|
|
1800
|
+
migrationPath: path,
|
|
1801
|
+
stepsApplied: path.length - 1,
|
|
1802
|
+
dryRun: true
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
let currentDoc = deepClone(doc);
|
|
1807
|
+
let stepsApplied = 0;
|
|
1808
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1809
|
+
const fromVer = path[i];
|
|
1810
|
+
const toVer = path[i + 1];
|
|
1811
|
+
const step = registry.getStep(fromVer, toVer);
|
|
1812
|
+
if (!step) {
|
|
1813
|
+
const error = {
|
|
1814
|
+
code: "MIGRATION_EXECUTION_FAILED",
|
|
1815
|
+
message: `Registered migration step for "${fromVer}" -> "${toVer}" is missing executor.`,
|
|
1816
|
+
sourceVersion,
|
|
1817
|
+
targetVersion
|
|
1818
|
+
};
|
|
1819
|
+
return {
|
|
1820
|
+
success: false,
|
|
1821
|
+
diagnostic: {
|
|
1822
|
+
success: false,
|
|
1823
|
+
sourceVersion,
|
|
1824
|
+
targetVersion,
|
|
1825
|
+
migrationPath: path,
|
|
1826
|
+
stepsApplied,
|
|
1827
|
+
dryRun,
|
|
1828
|
+
errors: [error]
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
try {
|
|
1833
|
+
currentDoc = step.migrate(currentDoc);
|
|
1834
|
+
stepsApplied++;
|
|
1835
|
+
} catch (err) {
|
|
1836
|
+
const error = {
|
|
1837
|
+
code: "MIGRATION_EXECUTION_FAILED",
|
|
1838
|
+
message: `Failed executing migration step from "${fromVer}" to "${toVer}": ${err instanceof Error ? err.message : String(err)}`,
|
|
1839
|
+
sourceVersion,
|
|
1840
|
+
targetVersion,
|
|
1841
|
+
details: { stepFrom: fromVer, stepTo: toVer }
|
|
1842
|
+
};
|
|
1843
|
+
return {
|
|
1844
|
+
success: false,
|
|
1845
|
+
diagnostic: {
|
|
1846
|
+
success: false,
|
|
1847
|
+
sourceVersion,
|
|
1848
|
+
targetVersion,
|
|
1849
|
+
migrationPath: path,
|
|
1850
|
+
stepsApplied,
|
|
1851
|
+
dryRun,
|
|
1852
|
+
errors: [error]
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
const finalDoc = currentDoc;
|
|
1858
|
+
if (validate) {
|
|
1859
|
+
const validation = validateDocument(finalDoc);
|
|
1860
|
+
if (!validation.valid) {
|
|
1861
|
+
return {
|
|
1862
|
+
success: false,
|
|
1863
|
+
diagnostic: {
|
|
1864
|
+
success: false,
|
|
1865
|
+
sourceVersion,
|
|
1866
|
+
targetVersion,
|
|
1867
|
+
migrationPath: path,
|
|
1868
|
+
stepsApplied,
|
|
1869
|
+
dryRun,
|
|
1870
|
+
errors: validation.errors.map((e) => ({
|
|
1871
|
+
code: "MIGRATION_EXECUTION_FAILED",
|
|
1872
|
+
message: `Post-migration validation error: ${e.message} at ${e.path}`,
|
|
1873
|
+
sourceVersion,
|
|
1874
|
+
targetVersion
|
|
1875
|
+
}))
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return {
|
|
1881
|
+
success: true,
|
|
1882
|
+
document: finalDoc,
|
|
1883
|
+
diagnostic: {
|
|
1884
|
+
success: true,
|
|
1885
|
+
sourceVersion,
|
|
1886
|
+
targetVersion,
|
|
1887
|
+
migrationPath: path,
|
|
1888
|
+
stepsApplied,
|
|
1889
|
+
dryRun: false
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// src/exporter.ts
|
|
1895
|
+
var import_fflate = require("fflate");
|
|
1896
|
+
var import_schema6 = require("@kubuild/schema");
|
|
1897
|
+
function sha256Sync(data) {
|
|
1898
|
+
const K = [
|
|
1899
|
+
1116352408,
|
|
1900
|
+
1899447441,
|
|
1901
|
+
3049323471,
|
|
1902
|
+
3921009573,
|
|
1903
|
+
961987163,
|
|
1904
|
+
1508970993,
|
|
1905
|
+
2453635748,
|
|
1906
|
+
2870763221,
|
|
1907
|
+
3624381080,
|
|
1908
|
+
310598401,
|
|
1909
|
+
607225278,
|
|
1910
|
+
1426881987,
|
|
1911
|
+
1925078388,
|
|
1912
|
+
2162078206,
|
|
1913
|
+
2614888103,
|
|
1914
|
+
3248222580,
|
|
1915
|
+
3835390401,
|
|
1916
|
+
4022224774,
|
|
1917
|
+
264347078,
|
|
1918
|
+
604807628,
|
|
1919
|
+
770255983,
|
|
1920
|
+
1249150122,
|
|
1921
|
+
1555081692,
|
|
1922
|
+
1996064986,
|
|
1923
|
+
2554220882,
|
|
1924
|
+
2821834349,
|
|
1925
|
+
2952996808,
|
|
1926
|
+
3210313671,
|
|
1927
|
+
3336571891,
|
|
1928
|
+
3584528711,
|
|
1929
|
+
113926993,
|
|
1930
|
+
338241895,
|
|
1931
|
+
666307205,
|
|
1932
|
+
773529912,
|
|
1933
|
+
1294757372,
|
|
1934
|
+
1396182291,
|
|
1935
|
+
1695183700,
|
|
1936
|
+
1986661051,
|
|
1937
|
+
2177026350,
|
|
1938
|
+
2456956037,
|
|
1939
|
+
2730485921,
|
|
1940
|
+
2820302411,
|
|
1941
|
+
3259730800,
|
|
1942
|
+
3345764771,
|
|
1943
|
+
3516065817,
|
|
1944
|
+
3600352804,
|
|
1945
|
+
4094571909,
|
|
1946
|
+
275423344,
|
|
1947
|
+
430227734,
|
|
1948
|
+
506948616,
|
|
1949
|
+
659060556,
|
|
1950
|
+
883997877,
|
|
1951
|
+
958139571,
|
|
1952
|
+
1322822218,
|
|
1953
|
+
1537002063,
|
|
1954
|
+
1747873779,
|
|
1955
|
+
1955562222,
|
|
1956
|
+
2024104815,
|
|
1957
|
+
2227730452,
|
|
1958
|
+
2361852424,
|
|
1959
|
+
2428436474,
|
|
1960
|
+
2756734187,
|
|
1961
|
+
3204031479,
|
|
1962
|
+
3329325298
|
|
1963
|
+
];
|
|
1964
|
+
let h0 = 1779033703;
|
|
1965
|
+
let h1 = 3144134277;
|
|
1966
|
+
let h2 = 1013904242;
|
|
1967
|
+
let h3 = 2773480762;
|
|
1968
|
+
let h4 = 1359893119;
|
|
1969
|
+
let h5 = 2600822924;
|
|
1970
|
+
let h6 = 528734635;
|
|
1971
|
+
let h7 = 1541459225;
|
|
1972
|
+
const length = data.length;
|
|
1973
|
+
const bitLength = length * 8;
|
|
1974
|
+
const newLength = length + 9 + 63 >>> 6 << 6;
|
|
1975
|
+
const padded = new Uint8Array(newLength);
|
|
1976
|
+
padded.set(data);
|
|
1977
|
+
padded[length] = 128;
|
|
1978
|
+
const view = new DataView(padded.buffer);
|
|
1979
|
+
view.setUint32(newLength - 4, bitLength >>> 0, false);
|
|
1980
|
+
view.setUint32(newLength - 8, Math.floor(bitLength / 4294967296), false);
|
|
1981
|
+
const w = new Uint32Array(64);
|
|
1982
|
+
for (let chunk = 0; chunk < newLength; chunk += 64) {
|
|
1983
|
+
for (let i = 0; i < 16; i++) {
|
|
1984
|
+
w[i] = view.getUint32(chunk + i * 4, false);
|
|
1985
|
+
}
|
|
1986
|
+
for (let i = 16; i < 64; i++) {
|
|
1987
|
+
const s0 = (w[i - 15] >>> 7 | w[i - 15] << 25) ^ (w[i - 15] >>> 18 | w[i - 15] << 14) ^ w[i - 15] >>> 3;
|
|
1988
|
+
const s1 = (w[i - 2] >>> 17 | w[i - 2] << 15) ^ (w[i - 2] >>> 19 | w[i - 2] << 13) ^ w[i - 2] >>> 10;
|
|
1989
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
1990
|
+
}
|
|
1991
|
+
let a = h0;
|
|
1992
|
+
let b = h1;
|
|
1993
|
+
let c = h2;
|
|
1994
|
+
let d = h3;
|
|
1995
|
+
let e = h4;
|
|
1996
|
+
let f = h5;
|
|
1997
|
+
let g = h6;
|
|
1998
|
+
let h = h7;
|
|
1999
|
+
for (let i = 0; i < 64; i++) {
|
|
2000
|
+
const S1 = (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7);
|
|
2001
|
+
const ch = e & f ^ ~e & g;
|
|
2002
|
+
const temp1 = h + S1 + ch + K[i] + w[i] >>> 0;
|
|
2003
|
+
const S0 = (a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10);
|
|
2004
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
2005
|
+
const temp2 = S0 + maj >>> 0;
|
|
2006
|
+
h = g;
|
|
2007
|
+
g = f;
|
|
2008
|
+
f = e;
|
|
2009
|
+
e = d + temp1 >>> 0;
|
|
2010
|
+
d = c;
|
|
2011
|
+
c = b;
|
|
2012
|
+
b = a;
|
|
2013
|
+
a = temp1 + temp2 >>> 0;
|
|
2014
|
+
}
|
|
2015
|
+
h0 = h0 + a >>> 0;
|
|
2016
|
+
h1 = h1 + b >>> 0;
|
|
2017
|
+
h2 = h2 + c >>> 0;
|
|
2018
|
+
h3 = h3 + d >>> 0;
|
|
2019
|
+
h4 = h4 + e >>> 0;
|
|
2020
|
+
h5 = h5 + f >>> 0;
|
|
2021
|
+
h6 = h6 + g >>> 0;
|
|
2022
|
+
h7 = h7 + h >>> 0;
|
|
2023
|
+
}
|
|
2024
|
+
const result = [h0, h1, h2, h3, h4, h5, h6, h7].map((val) => val.toString(16).padStart(8, "0")).join("");
|
|
2025
|
+
return result;
|
|
2026
|
+
}
|
|
2027
|
+
async function calculateChecksum(data) {
|
|
2028
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto?.subtle?.digest) {
|
|
2029
|
+
try {
|
|
2030
|
+
const buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
2031
|
+
const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", buffer);
|
|
2032
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
2033
|
+
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2034
|
+
return `sha256:${hashHex}`;
|
|
2035
|
+
} catch {
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
return `sha256:${sha256Sync(data)}`;
|
|
2039
|
+
}
|
|
2040
|
+
function sanitizeFilename(filename, assetId, mimeType) {
|
|
2041
|
+
let name = (filename || "").trim();
|
|
2042
|
+
name = name.replace(/^[./\\]+/, "").replace(/[/\\]+/g, "_").replace(/\0/g, "");
|
|
2043
|
+
name = name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2044
|
+
if (!name || name === "." || name === "..") {
|
|
2045
|
+
const ext = getExtensionFromMime(mimeType) || "bin";
|
|
2046
|
+
const cleanId = assetId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2047
|
+
name = `${cleanId}.${ext}`;
|
|
2048
|
+
}
|
|
2049
|
+
return name;
|
|
2050
|
+
}
|
|
2051
|
+
function getExtensionFromMime(mimeType) {
|
|
2052
|
+
if (!mimeType) return null;
|
|
2053
|
+
const m = mimeType.toLowerCase();
|
|
2054
|
+
if (m.includes("png")) return "png";
|
|
2055
|
+
if (m.includes("jpeg") || m.includes("jpg")) return "jpg";
|
|
2056
|
+
if (m.includes("webp")) return "webp";
|
|
2057
|
+
if (m.includes("svg")) return "svg";
|
|
2058
|
+
if (m.includes("gif")) return "gif";
|
|
2059
|
+
if (m.includes("json")) return "json";
|
|
2060
|
+
if (m.includes("pdf")) return "pdf";
|
|
2061
|
+
if (m.includes("mp4")) return "mp4";
|
|
2062
|
+
if (m.includes("mp3")) return "mp3";
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
async function convertToUint8Array(input) {
|
|
2066
|
+
if (!input) return null;
|
|
2067
|
+
if (input instanceof Uint8Array) {
|
|
2068
|
+
return input;
|
|
2069
|
+
}
|
|
2070
|
+
if (input instanceof ArrayBuffer) {
|
|
2071
|
+
return new Uint8Array(input);
|
|
2072
|
+
}
|
|
2073
|
+
if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
2074
|
+
const arrayBuffer = await input.arrayBuffer();
|
|
2075
|
+
return new Uint8Array(arrayBuffer);
|
|
2076
|
+
}
|
|
2077
|
+
if (typeof input === "string") {
|
|
2078
|
+
if (input.startsWith("data:")) {
|
|
2079
|
+
const commaIdx = input.indexOf(",");
|
|
2080
|
+
if (commaIdx !== -1) {
|
|
2081
|
+
const meta = input.substring(0, commaIdx);
|
|
2082
|
+
const rawData = input.substring(commaIdx + 1);
|
|
2083
|
+
if (meta.includes(";base64")) {
|
|
2084
|
+
if (typeof atob === "function") {
|
|
2085
|
+
const binary = atob(rawData);
|
|
2086
|
+
const bytes = new Uint8Array(binary.length);
|
|
2087
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2088
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2089
|
+
}
|
|
2090
|
+
return bytes;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
return new TextEncoder().encode(input);
|
|
2096
|
+
}
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
async function exportPackage(document, options = {}) {
|
|
2100
|
+
const validation = validateDocument(document, {
|
|
2101
|
+
componentRegistry: options.componentRegistry,
|
|
2102
|
+
knownComponentTypes: options.knownComponentTypes,
|
|
2103
|
+
...options.validationOptions
|
|
2104
|
+
});
|
|
2105
|
+
if (!validation.valid || !validation.data) {
|
|
2106
|
+
const diagnosticMessage = `Document validation failed with ${validation.errors.length} error(s):
|
|
2107
|
+
` + validation.errors.map((e) => `[${e.code}] ${e.path}: ${e.message}`).join("\n");
|
|
2108
|
+
return {
|
|
2109
|
+
success: false,
|
|
2110
|
+
errors: validation.errors,
|
|
2111
|
+
diagnosticMessage
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
const pageDoc = JSON.parse(JSON.stringify(validation.data));
|
|
2115
|
+
if (options.metadata) {
|
|
2116
|
+
pageDoc.metadata = {
|
|
2117
|
+
...pageDoc.metadata || {
|
|
2118
|
+
title: "Untitled Page",
|
|
2119
|
+
description: "",
|
|
2120
|
+
author: "",
|
|
2121
|
+
tags: [],
|
|
2122
|
+
category: "general",
|
|
2123
|
+
version: "1.0.0"
|
|
2124
|
+
},
|
|
2125
|
+
...options.metadata,
|
|
2126
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
const collectedAssets = collectAssetReferences(pageDoc.document);
|
|
2130
|
+
const assetRefsById = /* @__PURE__ */ new Map();
|
|
2131
|
+
for (const item of collectedAssets) {
|
|
2132
|
+
if (!assetRefsById.has(item.assetId)) {
|
|
2133
|
+
assetRefsById.set(item.assetId, item);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
const requirements = extractRequirementsFromTree(pageDoc.document, options.componentRegistry);
|
|
2137
|
+
const manifestAssets = [];
|
|
2138
|
+
const archiveFiles = {};
|
|
2139
|
+
const assetCollectionErrors = [];
|
|
2140
|
+
const usedArchivePaths = /* @__PURE__ */ new Set();
|
|
2141
|
+
for (const [assetId, assetRef] of assetRefsById.entries()) {
|
|
2142
|
+
let rawAsset;
|
|
2143
|
+
let customMime = assetRef.reference.mimeType;
|
|
2144
|
+
let customFilename = assetRef.reference.filename;
|
|
2145
|
+
if (options.assets) {
|
|
2146
|
+
if (options.assets instanceof Map) {
|
|
2147
|
+
rawAsset = options.assets.get(assetId);
|
|
2148
|
+
} else if (typeof options.assets === "object" && assetId in options.assets) {
|
|
2149
|
+
rawAsset = options.assets[assetId];
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
if (!rawAsset && options.getAssetBytes) {
|
|
2153
|
+
try {
|
|
2154
|
+
rawAsset = await options.getAssetBytes(assetId, assetRef);
|
|
2155
|
+
} catch (err) {
|
|
2156
|
+
assetCollectionErrors.push({
|
|
2157
|
+
code: "INVALID_ASSET_REFERENCE",
|
|
2158
|
+
message: `Failed to retrieve asset bytes for "${assetId}": ${err instanceof Error ? err.message : String(err)}`,
|
|
2159
|
+
path: assetRef.propPath,
|
|
2160
|
+
nodeId: assetRef.nodeId
|
|
2161
|
+
});
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
if (!rawAsset && options.assetProvider?.resolve) {
|
|
2166
|
+
try {
|
|
2167
|
+
const resolved = await options.assetProvider.resolve(assetId);
|
|
2168
|
+
if (resolved) {
|
|
2169
|
+
rawAsset = resolved;
|
|
2170
|
+
}
|
|
2171
|
+
} catch {
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
let dataToConvert = rawAsset;
|
|
2175
|
+
if (rawAsset && typeof rawAsset === "object" && "data" in rawAsset) {
|
|
2176
|
+
const assetObj = rawAsset;
|
|
2177
|
+
dataToConvert = assetObj.data;
|
|
2178
|
+
if (assetObj.mimeType) customMime = assetObj.mimeType;
|
|
2179
|
+
if (assetObj.filename) customFilename = assetObj.filename;
|
|
2180
|
+
}
|
|
2181
|
+
const assetBytes = await convertToUint8Array(dataToConvert);
|
|
2182
|
+
if (assetBytes) {
|
|
2183
|
+
const sanitizedName = sanitizeFilename(customFilename, assetId, customMime);
|
|
2184
|
+
let archivePath = `assets/${sanitizedName}`;
|
|
2185
|
+
let counter = 1;
|
|
2186
|
+
while (usedArchivePaths.has(archivePath)) {
|
|
2187
|
+
const extDot = sanitizedName.lastIndexOf(".");
|
|
2188
|
+
if (extDot !== -1) {
|
|
2189
|
+
archivePath = `assets/${sanitizedName.substring(0, extDot)}_${counter}${sanitizedName.substring(extDot)}`;
|
|
2190
|
+
} else {
|
|
2191
|
+
archivePath = `assets/${sanitizedName}_${counter}`;
|
|
2192
|
+
}
|
|
2193
|
+
counter++;
|
|
2194
|
+
}
|
|
2195
|
+
usedArchivePaths.add(archivePath);
|
|
2196
|
+
const mimeType = customMime || "application/octet-stream";
|
|
2197
|
+
const checksum = await calculateChecksum(assetBytes);
|
|
2198
|
+
manifestAssets.push({
|
|
2199
|
+
id: assetId,
|
|
2200
|
+
path: archivePath,
|
|
2201
|
+
mimeType,
|
|
2202
|
+
size: assetBytes.byteLength,
|
|
2203
|
+
checksum
|
|
2204
|
+
});
|
|
2205
|
+
archiveFiles[archivePath] = assetBytes;
|
|
2206
|
+
} else {
|
|
2207
|
+
const isExternal = Boolean(assetRef.reference.fallbackUrl || options.allowExternalFallback);
|
|
2208
|
+
if (!isExternal) {
|
|
2209
|
+
assetCollectionErrors.push({
|
|
2210
|
+
code: "INVALID_ASSET_REFERENCE",
|
|
2211
|
+
message: `Required asset "${assetId}" could not be collected for export and has no external fallback`,
|
|
2212
|
+
path: assetRef.propPath,
|
|
2213
|
+
nodeId: assetRef.nodeId
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
if (assetCollectionErrors.length > 0) {
|
|
2219
|
+
const diagnosticMessage = `Asset collection failed with ${assetCollectionErrors.length} error(s):
|
|
2220
|
+
` + assetCollectionErrors.map((e) => `[${e.code}] ${e.path}: ${e.message}`).join("\n");
|
|
2221
|
+
return {
|
|
2222
|
+
success: false,
|
|
2223
|
+
errors: assetCollectionErrors,
|
|
2224
|
+
diagnosticMessage
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
const metadata = pageDoc.metadata || {
|
|
2228
|
+
title: "Untitled Page",
|
|
2229
|
+
description: "",
|
|
2230
|
+
author: "",
|
|
2231
|
+
tags: [],
|
|
2232
|
+
category: "general",
|
|
2233
|
+
version: "1.0.0",
|
|
2234
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2235
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2236
|
+
};
|
|
2237
|
+
const manifest = {
|
|
2238
|
+
schema: import_schema6.SCHEMA_NAME,
|
|
2239
|
+
schemaVersion: pageDoc.version || import_schema6.CURRENT_SCHEMA_VERSION,
|
|
2240
|
+
packageVersion: options.packageVersion || "1.0.0",
|
|
2241
|
+
builderCompatibility: options.builderCompatibility || ">=0.1.0",
|
|
2242
|
+
requiredComponents: requirements.requiredComponents,
|
|
2243
|
+
requiredCapabilities: requirements.requiredCapabilities,
|
|
2244
|
+
assets: manifestAssets,
|
|
2245
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2246
|
+
};
|
|
2247
|
+
archiveFiles["manifest.json"] = (0, import_fflate.strToU8)(JSON.stringify(manifest, null, 2));
|
|
2248
|
+
archiveFiles["page.json"] = (0, import_fflate.strToU8)(JSON.stringify(pageDoc, null, 2));
|
|
2249
|
+
archiveFiles["metadata.json"] = (0, import_fflate.strToU8)(JSON.stringify(metadata, null, 2));
|
|
2250
|
+
const archive = (0, import_fflate.zipSync)(archiveFiles, { level: 6 });
|
|
2251
|
+
return {
|
|
2252
|
+
success: true,
|
|
2253
|
+
archive,
|
|
2254
|
+
manifest,
|
|
2255
|
+
page: pageDoc,
|
|
2256
|
+
metadata,
|
|
2257
|
+
assetCount: manifestAssets.length
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
var exportStoraPackage = exportPackage;
|
|
2261
|
+
|
|
2262
|
+
// src/importer.ts
|
|
2263
|
+
var import_fflate2 = require("fflate");
|
|
2264
|
+
var import_schema7 = require("@kubuild/schema");
|
|
2265
|
+
var DEFAULT_SECURITY_LIMITS = {
|
|
2266
|
+
maxArchiveSize: 50 * 1024 * 1024,
|
|
2267
|
+
// 50 MB
|
|
2268
|
+
maxUncompressedSize: 100 * 1024 * 1024,
|
|
2269
|
+
// 100 MB
|
|
2270
|
+
maxFileCount: 1e3,
|
|
2271
|
+
maxAssetSize: 25 * 1024 * 1024
|
|
2272
|
+
// 25 MB
|
|
2273
|
+
};
|
|
2274
|
+
function isDangerousPath(filepath) {
|
|
2275
|
+
if (!filepath || typeof filepath !== "string") return true;
|
|
2276
|
+
if (filepath.includes("\0")) return true;
|
|
2277
|
+
if (filepath.startsWith("/") || filepath.startsWith("\\") || /^[a-zA-Z]:/.test(filepath)) {
|
|
2278
|
+
return true;
|
|
2279
|
+
}
|
|
2280
|
+
const normalized = filepath.replace(/\\/g, "/");
|
|
2281
|
+
const segments = normalized.split("/");
|
|
2282
|
+
for (const seg of segments) {
|
|
2283
|
+
if (seg === ".." || seg.trim() === "..") {
|
|
2284
|
+
return true;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
return false;
|
|
2288
|
+
}
|
|
2289
|
+
async function preflightPackage(archiveData, options = {}) {
|
|
2290
|
+
const limits = {
|
|
2291
|
+
...DEFAULT_SECURITY_LIMITS,
|
|
2292
|
+
...options.securityLimits
|
|
2293
|
+
};
|
|
2294
|
+
const targetVersion = options.targetSchemaVersion || import_schema7.CURRENT_SCHEMA_VERSION;
|
|
2295
|
+
const migrationRegistry = options.migrationRegistry || defaultMigrationRegistry;
|
|
2296
|
+
const diagnostics = [];
|
|
2297
|
+
let dependencyPolicy = options.dependencyPolicy || "cancel";
|
|
2298
|
+
if (!options.dependencyPolicy) {
|
|
2299
|
+
if (options.allowMissingComponents && options.allowMissingCapabilities) {
|
|
2300
|
+
dependencyPolicy = "import-with-placeholder";
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
const assetCollisionStrategy = options.assetCollisionStrategy || "reject";
|
|
2304
|
+
const archiveBytes = archiveData instanceof Uint8Array ? archiveData : new Uint8Array(archiveData);
|
|
2305
|
+
if (!archiveBytes || archiveBytes.byteLength === 0) {
|
|
2306
|
+
diagnostics.push({
|
|
2307
|
+
code: "ARCHIVE_EMPTY",
|
|
2308
|
+
severity: "error",
|
|
2309
|
+
message: "Archive is empty or 0 bytes."
|
|
2310
|
+
});
|
|
2311
|
+
return buildReport(false, false, { targetVersion, dependencyPolicy, assetCollisionStrategy, diagnostics });
|
|
2312
|
+
}
|
|
2313
|
+
if (archiveBytes.byteLength > limits.maxArchiveSize) {
|
|
2314
|
+
diagnostics.push({
|
|
2315
|
+
code: "SIZE_LIMIT_EXCEEDED",
|
|
2316
|
+
severity: "error",
|
|
2317
|
+
message: `Archive size (${archiveBytes.byteLength} bytes) exceeds maximum limit of ${limits.maxArchiveSize} bytes.`,
|
|
2318
|
+
details: { actualBytes: archiveBytes.byteLength, limitBytes: limits.maxArchiveSize }
|
|
2319
|
+
});
|
|
2320
|
+
return buildReport(false, false, { targetVersion, dependencyPolicy, assetCollisionStrategy, diagnostics });
|
|
2321
|
+
}
|
|
2322
|
+
let unzipped;
|
|
2323
|
+
try {
|
|
2324
|
+
unzipped = (0, import_fflate2.unzipSync)(archiveBytes);
|
|
2325
|
+
} catch (err) {
|
|
2326
|
+
diagnostics.push({
|
|
2327
|
+
code: "ARCHIVE_CORRUPT",
|
|
2328
|
+
severity: "error",
|
|
2329
|
+
message: `Failed to unzip archive: ${err instanceof Error ? err.message : String(err)}`
|
|
2330
|
+
});
|
|
2331
|
+
return buildReport(false, false, { targetVersion, dependencyPolicy, assetCollisionStrategy, diagnostics });
|
|
2332
|
+
}
|
|
2333
|
+
const entries = Object.keys(unzipped);
|
|
2334
|
+
if (entries.length > limits.maxFileCount) {
|
|
2335
|
+
diagnostics.push({
|
|
2336
|
+
code: "FILE_COUNT_EXCEEDED",
|
|
2337
|
+
severity: "error",
|
|
2338
|
+
message: `Archive file count (${entries.length}) exceeds maximum limit of ${limits.maxFileCount}.`,
|
|
2339
|
+
details: { fileCount: entries.length, limit: limits.maxFileCount }
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
let totalUncompressedBytes = 0;
|
|
2343
|
+
for (const entryPath of entries) {
|
|
2344
|
+
if (isDangerousPath(entryPath)) {
|
|
2345
|
+
diagnostics.push({
|
|
2346
|
+
code: "ZIP_SLIP_DETECTED",
|
|
2347
|
+
severity: "error",
|
|
2348
|
+
message: `Security violation: Path traversal or Zip-Slip attempt detected in archive entry "${entryPath}".`,
|
|
2349
|
+
path: entryPath
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
if (entryPath.startsWith("assets/") && isDangerousAssetFilename(entryPath)) {
|
|
2353
|
+
diagnostics.push({
|
|
2354
|
+
code: "EXECUTABLE_ASSET_DETECTED",
|
|
2355
|
+
severity: "error",
|
|
2356
|
+
message: `Security violation: Executable or dangerous asset file extension detected in archive entry "${entryPath}".`,
|
|
2357
|
+
path: entryPath
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
const fileBytes = unzipped[entryPath];
|
|
2361
|
+
if (fileBytes) {
|
|
2362
|
+
totalUncompressedBytes += fileBytes.byteLength;
|
|
2363
|
+
if (entryPath.startsWith("assets/") && fileBytes.byteLength > limits.maxAssetSize) {
|
|
2364
|
+
diagnostics.push({
|
|
2365
|
+
code: "ASSET_LIMIT_EXCEEDED",
|
|
2366
|
+
severity: "error",
|
|
2367
|
+
message: `Asset "${entryPath}" size (${fileBytes.byteLength} bytes) exceeds maximum limit of ${limits.maxAssetSize} bytes.`,
|
|
2368
|
+
path: entryPath,
|
|
2369
|
+
details: { actualBytes: fileBytes.byteLength, limitBytes: limits.maxAssetSize }
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
if (totalUncompressedBytes > limits.maxUncompressedSize) {
|
|
2375
|
+
diagnostics.push({
|
|
2376
|
+
code: "SIZE_LIMIT_EXCEEDED",
|
|
2377
|
+
severity: "error",
|
|
2378
|
+
message: `Total uncompressed archive size (${totalUncompressedBytes} bytes) exceeds maximum limit of ${limits.maxUncompressedSize} bytes.`,
|
|
2379
|
+
details: { actualBytes: totalUncompressedBytes, limitBytes: limits.maxUncompressedSize }
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
if (checkZipBomb(archiveBytes.byteLength, totalUncompressedBytes)) {
|
|
2383
|
+
diagnostics.push({
|
|
2384
|
+
code: "SIZE_LIMIT_EXCEEDED",
|
|
2385
|
+
severity: "error",
|
|
2386
|
+
message: `Security violation: Potential Zip Bomb attack detected (excessive compression ratio).`,
|
|
2387
|
+
details: { compressedBytes: archiveBytes.byteLength, uncompressedBytes: totalUncompressedBytes }
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
const manifestEntry = unzipped["manifest.json"];
|
|
2391
|
+
if (!manifestEntry) {
|
|
2392
|
+
diagnostics.push({
|
|
2393
|
+
code: "MISSING_MANIFEST",
|
|
2394
|
+
severity: "error",
|
|
2395
|
+
message: 'Archive is missing mandatory "manifest.json" file.'
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
const pageEntry = unzipped["page.json"];
|
|
2399
|
+
if (!pageEntry) {
|
|
2400
|
+
diagnostics.push({
|
|
2401
|
+
code: "MISSING_PAGE_DOCUMENT",
|
|
2402
|
+
severity: "error",
|
|
2403
|
+
message: 'Archive is missing mandatory "page.json" file.'
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
if (!manifestEntry || !pageEntry || diagnostics.some((d) => d.code === "ZIP_SLIP_DETECTED")) {
|
|
2407
|
+
return buildReport(false, false, {
|
|
2408
|
+
targetVersion,
|
|
2409
|
+
dependencyPolicy,
|
|
2410
|
+
assetCollisionStrategy,
|
|
2411
|
+
totalUncompressedBytes,
|
|
2412
|
+
diagnostics
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
let parsedManifest;
|
|
2416
|
+
try {
|
|
2417
|
+
const manifestJson = JSON.parse((0, import_fflate2.strFromU8)(manifestEntry));
|
|
2418
|
+
const manifestProtoCheck = containsProhibitedKeys(manifestJson);
|
|
2419
|
+
if (manifestProtoCheck.found) {
|
|
2420
|
+
diagnostics.push({
|
|
2421
|
+
code: "PROTOTYPE_POLLUTION_DETECTED",
|
|
2422
|
+
severity: "error",
|
|
2423
|
+
message: `Security violation: Prohibited key "${manifestProtoCheck.key}" in manifest.json.`,
|
|
2424
|
+
path: manifestProtoCheck.path
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
const parseResult = import_schema7.ManifestSchema.safeParse(manifestJson);
|
|
2428
|
+
if (!parseResult.success) {
|
|
2429
|
+
const issues = parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
|
|
2430
|
+
diagnostics.push({
|
|
2431
|
+
code: "INVALID_MANIFEST",
|
|
2432
|
+
severity: "error",
|
|
2433
|
+
message: `Manifest validation failed: ${issues}`,
|
|
2434
|
+
details: { issues: parseResult.error.issues }
|
|
2435
|
+
});
|
|
2436
|
+
} else {
|
|
2437
|
+
parsedManifest = parseResult.data;
|
|
2438
|
+
}
|
|
2439
|
+
} catch (err) {
|
|
2440
|
+
diagnostics.push({
|
|
2441
|
+
code: "INVALID_MANIFEST",
|
|
2442
|
+
severity: "error",
|
|
2443
|
+
message: `Failed to parse "manifest.json" as valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
let rawMetadata;
|
|
2447
|
+
const metadataEntry = unzipped["metadata.json"];
|
|
2448
|
+
if (metadataEntry) {
|
|
2449
|
+
try {
|
|
2450
|
+
rawMetadata = JSON.parse((0, import_fflate2.strFromU8)(metadataEntry));
|
|
2451
|
+
} catch {
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
let rawPageDoc;
|
|
2455
|
+
try {
|
|
2456
|
+
rawPageDoc = JSON.parse((0, import_fflate2.strFromU8)(pageEntry));
|
|
2457
|
+
const pageProtoCheck = containsProhibitedKeys(rawPageDoc);
|
|
2458
|
+
if (pageProtoCheck.found) {
|
|
2459
|
+
diagnostics.push({
|
|
2460
|
+
code: "PROTOTYPE_POLLUTION_DETECTED",
|
|
2461
|
+
severity: "error",
|
|
2462
|
+
message: `Security violation: Prohibited key "${pageProtoCheck.key}" in page.json.`,
|
|
2463
|
+
path: pageProtoCheck.path
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
} catch (err) {
|
|
2467
|
+
diagnostics.push({
|
|
2468
|
+
code: "INVALID_PAGE_DOCUMENT",
|
|
2469
|
+
severity: "error",
|
|
2470
|
+
message: `Failed to parse "page.json" as valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
const docVersion = typeof rawPageDoc?.version === "string" ? rawPageDoc.version : parsedManifest?.schemaVersion || "unknown";
|
|
2474
|
+
let requiresMigration = false;
|
|
2475
|
+
let migrationPath;
|
|
2476
|
+
if (rawPageDoc) {
|
|
2477
|
+
if (docVersion !== targetVersion) {
|
|
2478
|
+
if (canMigrate(docVersion, targetVersion, migrationRegistry)) {
|
|
2479
|
+
requiresMigration = true;
|
|
2480
|
+
migrationPath = getMigrationPath(docVersion, targetVersion, migrationRegistry) || void 0;
|
|
2481
|
+
diagnostics.push({
|
|
2482
|
+
code: "UNSUPPORTED_SCHEMA_VERSION",
|
|
2483
|
+
severity: "info",
|
|
2484
|
+
message: `Document schema version "${docVersion}" requires migration to current version "${targetVersion}".`,
|
|
2485
|
+
details: { sourceVersion: docVersion, targetVersion, migrationPath }
|
|
2486
|
+
});
|
|
2487
|
+
} else {
|
|
2488
|
+
diagnostics.push({
|
|
2489
|
+
code: "NO_MIGRATION_PATH",
|
|
2490
|
+
severity: "error",
|
|
2491
|
+
message: `Document schema version "${docVersion}" is not compatible and no migration path exists to "${targetVersion}".`,
|
|
2492
|
+
details: { sourceVersion: docVersion, targetVersion }
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
} else {
|
|
2496
|
+
const docValidation = validateDocument(rawPageDoc, {
|
|
2497
|
+
componentRegistry: options.componentRegistry,
|
|
2498
|
+
knownComponentTypes: options.knownComponentTypes
|
|
2499
|
+
});
|
|
2500
|
+
const criticalDocErrors = docValidation.errors.filter((e) => {
|
|
2501
|
+
if (e.code === "UNKNOWN_COMPONENT_TYPE" && dependencyPolicy !== "cancel") {
|
|
2502
|
+
return false;
|
|
2503
|
+
}
|
|
2504
|
+
return true;
|
|
2505
|
+
});
|
|
2506
|
+
if (criticalDocErrors.length > 0) {
|
|
2507
|
+
diagnostics.push({
|
|
2508
|
+
code: "INVALID_PAGE_DOCUMENT",
|
|
2509
|
+
severity: "error",
|
|
2510
|
+
message: `Document validation failed with ${criticalDocErrors.length} error(s):
|
|
2511
|
+
` + criticalDocErrors.map((e) => `[${e.code}] ${e.path}: ${e.message}`).join("\n")
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
const missingComponents = [];
|
|
2517
|
+
const missingCapabilities = [];
|
|
2518
|
+
const assetConflicts = [];
|
|
2519
|
+
const existingAssetIdsSet = /* @__PURE__ */ new Set();
|
|
2520
|
+
if (options.existingAssetIds) {
|
|
2521
|
+
if (Array.isArray(options.existingAssetIds)) {
|
|
2522
|
+
for (const id of options.existingAssetIds) existingAssetIdsSet.add(id);
|
|
2523
|
+
} else if (options.existingAssetIds instanceof Set) {
|
|
2524
|
+
for (const id of options.existingAssetIds) existingAssetIdsSet.add(id);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
if (options.assetProvider?.list && !options.existingAssetIds) {
|
|
2528
|
+
try {
|
|
2529
|
+
const list = await options.assetProvider.list();
|
|
2530
|
+
if (Array.isArray(list)) {
|
|
2531
|
+
for (const item of list) {
|
|
2532
|
+
if (item?.id) existingAssetIdsSet.add(item.id);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
} catch {
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
if (parsedManifest) {
|
|
2539
|
+
for (const assetItem of parsedManifest.assets) {
|
|
2540
|
+
const assetFile = unzipped[assetItem.path];
|
|
2541
|
+
if (!assetFile) {
|
|
2542
|
+
diagnostics.push({
|
|
2543
|
+
code: "ASSET_FILE_MISSING",
|
|
2544
|
+
severity: "error",
|
|
2545
|
+
message: `Declared asset "${assetItem.id}" is missing in archive at path "${assetItem.path}".`,
|
|
2546
|
+
path: assetItem.path,
|
|
2547
|
+
details: { assetId: assetItem.id }
|
|
2548
|
+
});
|
|
2549
|
+
continue;
|
|
2550
|
+
}
|
|
2551
|
+
if (assetFile.byteLength !== assetItem.size) {
|
|
2552
|
+
diagnostics.push({
|
|
2553
|
+
code: "ASSET_SIZE_MISMATCH",
|
|
2554
|
+
severity: "error",
|
|
2555
|
+
message: `Asset "${assetItem.id}" size mismatch: expected ${assetItem.size} bytes, found ${assetFile.byteLength} bytes.`,
|
|
2556
|
+
path: assetItem.path,
|
|
2557
|
+
details: { assetId: assetItem.id, expected: assetItem.size, actual: assetFile.byteLength }
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
if (assetItem.checksum) {
|
|
2561
|
+
const actualChecksum = await calculateChecksum(assetFile);
|
|
2562
|
+
if (actualChecksum !== assetItem.checksum) {
|
|
2563
|
+
diagnostics.push({
|
|
2564
|
+
code: "ASSET_CHECKSUM_MISMATCH",
|
|
2565
|
+
severity: "error",
|
|
2566
|
+
message: `Asset "${assetItem.id}" checksum mismatch: expected ${assetItem.checksum}, calculated ${actualChecksum}.`,
|
|
2567
|
+
path: assetItem.path,
|
|
2568
|
+
details: { assetId: assetItem.id, expected: assetItem.checksum, actual: actualChecksum }
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
let isConflict = false;
|
|
2573
|
+
if (typeof options.existingAssetIds === "function") {
|
|
2574
|
+
const fnRes = await options.existingAssetIds(assetItem.id);
|
|
2575
|
+
if (fnRes) isConflict = true;
|
|
2576
|
+
} else if (existingAssetIdsSet.has(assetItem.id)) {
|
|
2577
|
+
isConflict = true;
|
|
2578
|
+
}
|
|
2579
|
+
if (isConflict) {
|
|
2580
|
+
assetConflicts.push({
|
|
2581
|
+
assetId: assetItem.id,
|
|
2582
|
+
path: assetItem.path,
|
|
2583
|
+
mimeType: assetItem.mimeType,
|
|
2584
|
+
size: assetItem.size
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
if (assetConflicts.length > 0) {
|
|
2589
|
+
const conflictIds = assetConflicts.map((c) => c.assetId).join(", ");
|
|
2590
|
+
if (assetCollisionStrategy === "reject" && !options.onAssetConflict) {
|
|
2591
|
+
diagnostics.push({
|
|
2592
|
+
code: "ASSET_CONFLICT",
|
|
2593
|
+
severity: "error",
|
|
2594
|
+
message: `Asset collision detected for asset(s): ${conflictIds}. Host assets will not be overwritten without an explicit assetCollisionStrategy.`,
|
|
2595
|
+
details: { assetConflicts, strategy: assetCollisionStrategy }
|
|
2596
|
+
});
|
|
2597
|
+
} else {
|
|
2598
|
+
diagnostics.push({
|
|
2599
|
+
code: "ASSET_CONFLICT",
|
|
2600
|
+
severity: "info",
|
|
2601
|
+
message: `Asset collision detected for asset(s): ${conflictIds}. Strategy "${assetCollisionStrategy}" will be applied on import.`,
|
|
2602
|
+
details: { assetConflicts, strategy: assetCollisionStrategy }
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
const knownSet = /* @__PURE__ */ new Set();
|
|
2607
|
+
if (options.knownComponentTypes) {
|
|
2608
|
+
for (const t of options.knownComponentTypes) knownSet.add(t);
|
|
2609
|
+
}
|
|
2610
|
+
const builtIns = ["page", "section", "container", "columns", "heading", "text", "image", "button", "collection"];
|
|
2611
|
+
for (const b of builtIns) knownSet.add(b);
|
|
2612
|
+
for (const compType of parsedManifest.requiredComponents) {
|
|
2613
|
+
let isSupported = knownSet.has(compType);
|
|
2614
|
+
if (!isSupported && options.componentRegistry) {
|
|
2615
|
+
isSupported = options.componentRegistry.has(compType);
|
|
2616
|
+
}
|
|
2617
|
+
if (!isSupported) {
|
|
2618
|
+
missingComponents.push(compType);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
if (missingComponents.length > 0) {
|
|
2622
|
+
if (dependencyPolicy === "cancel" && !options.allowMissingComponents) {
|
|
2623
|
+
diagnostics.push({
|
|
2624
|
+
code: "MISSING_COMPONENTS",
|
|
2625
|
+
severity: "error",
|
|
2626
|
+
message: `Package requires custom component(s) not registered in host: ${missingComponents.join(", ")}.`,
|
|
2627
|
+
details: { missingComponents, policy: dependencyPolicy }
|
|
2628
|
+
});
|
|
2629
|
+
} else if (dependencyPolicy === "import-with-placeholder" || options.allowMissingComponents) {
|
|
2630
|
+
diagnostics.push({
|
|
2631
|
+
code: "MISSING_COMPONENTS",
|
|
2632
|
+
severity: "warning",
|
|
2633
|
+
message: `Package requires custom component(s) [${missingComponents.join(", ")}] not registered in host. Nodes and props are preserved intact for placeholder rendering.`,
|
|
2634
|
+
details: { missingComponents, policy: dependencyPolicy }
|
|
2635
|
+
});
|
|
2636
|
+
} else if (dependencyPolicy === "install-or-register-before-import") {
|
|
2637
|
+
diagnostics.push({
|
|
2638
|
+
code: "MISSING_COMPONENTS",
|
|
2639
|
+
severity: "info",
|
|
2640
|
+
message: `Package requires custom component(s) [${missingComponents.join(", ")}]. Installation/registration required before import.`,
|
|
2641
|
+
details: { missingComponents, policy: dependencyPolicy }
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
const supportedCaps = /* @__PURE__ */ new Set();
|
|
2646
|
+
if (options.supportedCapabilities) {
|
|
2647
|
+
for (const c of options.supportedCapabilities) supportedCaps.add(c);
|
|
2648
|
+
}
|
|
2649
|
+
for (const cap of parsedManifest.requiredCapabilities) {
|
|
2650
|
+
if (!supportedCaps.has(cap)) {
|
|
2651
|
+
missingCapabilities.push(cap);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
if (missingCapabilities.length > 0) {
|
|
2655
|
+
if (dependencyPolicy === "cancel" && !options.allowMissingCapabilities) {
|
|
2656
|
+
diagnostics.push({
|
|
2657
|
+
code: "MISSING_CAPABILITIES",
|
|
2658
|
+
severity: "error",
|
|
2659
|
+
message: `Package requires capability/capabilities not supported by host: ${missingCapabilities.join(", ")}.`,
|
|
2660
|
+
details: { missingCapabilities, policy: dependencyPolicy }
|
|
2661
|
+
});
|
|
2662
|
+
} else if (dependencyPolicy === "import-with-placeholder" || options.allowMissingCapabilities) {
|
|
2663
|
+
diagnostics.push({
|
|
2664
|
+
code: "MISSING_CAPABILITIES",
|
|
2665
|
+
severity: "warning",
|
|
2666
|
+
message: `Package requires capability/capabilities [${missingCapabilities.join(", ")}] not supported by host. Placeholder rendering enabled.`,
|
|
2667
|
+
details: { missingCapabilities, policy: dependencyPolicy }
|
|
2668
|
+
});
|
|
2669
|
+
} else if (dependencyPolicy === "install-or-register-before-import") {
|
|
2670
|
+
diagnostics.push({
|
|
2671
|
+
code: "MISSING_CAPABILITIES",
|
|
2672
|
+
severity: "info",
|
|
2673
|
+
message: `Package requires capability/capabilities [${missingCapabilities.join(", ")}]. Registration required before import.`,
|
|
2674
|
+
details: { missingCapabilities, policy: dependencyPolicy }
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
const hasErrors = diagnostics.some((d) => d.severity === "error");
|
|
2680
|
+
const valid = !hasErrors;
|
|
2681
|
+
const canImport = valid;
|
|
2682
|
+
return buildReport(valid, canImport, {
|
|
2683
|
+
manifest: parsedManifest,
|
|
2684
|
+
rawMetadata,
|
|
2685
|
+
sourceVersion: docVersion,
|
|
2686
|
+
targetVersion,
|
|
2687
|
+
requiresMigration,
|
|
2688
|
+
migrationPath,
|
|
2689
|
+
missingComponents,
|
|
2690
|
+
missingCapabilities,
|
|
2691
|
+
dependencyPolicy,
|
|
2692
|
+
assetCount: parsedManifest?.assets.length || 0,
|
|
2693
|
+
assetConflicts,
|
|
2694
|
+
assetCollisionStrategy,
|
|
2695
|
+
totalUncompressedBytes,
|
|
2696
|
+
diagnostics
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
function buildReport(valid, canImport, fields) {
|
|
2700
|
+
return {
|
|
2701
|
+
valid,
|
|
2702
|
+
canImport,
|
|
2703
|
+
targetVersion: fields.targetVersion || import_schema7.CURRENT_SCHEMA_VERSION,
|
|
2704
|
+
requiresMigration: fields.requiresMigration || false,
|
|
2705
|
+
missingComponents: fields.missingComponents || [],
|
|
2706
|
+
missingCapabilities: fields.missingCapabilities || [],
|
|
2707
|
+
dependencyPolicy: fields.dependencyPolicy || "cancel",
|
|
2708
|
+
assetCount: fields.assetCount || 0,
|
|
2709
|
+
assetConflicts: fields.assetConflicts || [],
|
|
2710
|
+
assetCollisionStrategy: fields.assetCollisionStrategy || "reject",
|
|
2711
|
+
totalUncompressedBytes: fields.totalUncompressedBytes || 0,
|
|
2712
|
+
diagnostics: fields.diagnostics || [],
|
|
2713
|
+
...fields
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
var previewImportPackage = preflightPackage;
|
|
2717
|
+
var inspectPackage = preflightPackage;
|
|
2718
|
+
function defaultRenameAssetStrategy(incoming, existingIds) {
|
|
2719
|
+
let candidate = `${incoming.id}_imported`;
|
|
2720
|
+
let counter = 1;
|
|
2721
|
+
while (existingIds.has(candidate)) {
|
|
2722
|
+
candidate = `${incoming.id}_imported_${counter}`;
|
|
2723
|
+
counter++;
|
|
2724
|
+
}
|
|
2725
|
+
return candidate;
|
|
2726
|
+
}
|
|
2727
|
+
async function importPackage(archiveData, options = {}) {
|
|
2728
|
+
let preflight = await preflightPackage(archiveData, options);
|
|
2729
|
+
const dependencyPolicy = options.dependencyPolicy || preflight.dependencyPolicy;
|
|
2730
|
+
if (dependencyPolicy === "install-or-register-before-import" && (preflight.missingComponents.length > 0 || preflight.missingCapabilities.length > 0) && options.onMissingDependency) {
|
|
2731
|
+
try {
|
|
2732
|
+
await options.onMissingDependency(
|
|
2733
|
+
{
|
|
2734
|
+
components: [...preflight.missingComponents],
|
|
2735
|
+
capabilities: [...preflight.missingCapabilities]
|
|
2736
|
+
},
|
|
2737
|
+
preflight
|
|
2738
|
+
);
|
|
2739
|
+
preflight = await preflightPackage(archiveData, {
|
|
2740
|
+
...options,
|
|
2741
|
+
dependencyPolicy: "cancel"
|
|
2742
|
+
// Strict check after install attempt
|
|
2743
|
+
});
|
|
2744
|
+
} catch (err) {
|
|
2745
|
+
const diag = {
|
|
2746
|
+
code: "HOST_ADAPTER_ERROR",
|
|
2747
|
+
severity: "error",
|
|
2748
|
+
message: `Dynamic dependency registration failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2749
|
+
};
|
|
2750
|
+
return {
|
|
2751
|
+
success: false,
|
|
2752
|
+
errors: [diag],
|
|
2753
|
+
diagnosticMessage: diag.message,
|
|
2754
|
+
preflight
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
if (!preflight.canImport || !preflight.manifest) {
|
|
2759
|
+
const errorDiagnostics = preflight.diagnostics.filter((d) => d.severity === "error");
|
|
2760
|
+
const diagnosticMessage = `Package import rejected with ${errorDiagnostics.length} error(s):
|
|
2761
|
+
` + errorDiagnostics.map((d) => `[${d.code}] ${d.path ? d.path + ": " : ""}${d.message}`).join("\n");
|
|
2762
|
+
return {
|
|
2763
|
+
success: false,
|
|
2764
|
+
errors: errorDiagnostics,
|
|
2765
|
+
diagnosticMessage,
|
|
2766
|
+
preflight
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
const archiveBytes = archiveData instanceof Uint8Array ? archiveData : new Uint8Array(archiveData);
|
|
2770
|
+
const unzipped = (0, import_fflate2.unzipSync)(archiveBytes);
|
|
2771
|
+
const pageRaw = JSON.parse((0, import_fflate2.strFromU8)(unzipped["page.json"]));
|
|
2772
|
+
let finalDocument;
|
|
2773
|
+
if (preflight.requiresMigration) {
|
|
2774
|
+
const migrationRegistry = options.migrationRegistry || defaultMigrationRegistry;
|
|
2775
|
+
const migrationRes = migrateDocument(pageRaw, {
|
|
2776
|
+
targetVersion: options.targetSchemaVersion || import_schema7.CURRENT_SCHEMA_VERSION,
|
|
2777
|
+
registry: migrationRegistry,
|
|
2778
|
+
validate: true
|
|
2779
|
+
});
|
|
2780
|
+
if (!migrationRes.success || !migrationRes.document) {
|
|
2781
|
+
const err = {
|
|
2782
|
+
code: "MIGRATION_FAILED",
|
|
2783
|
+
severity: "error",
|
|
2784
|
+
message: `Failed to migrate document from "${preflight.sourceVersion}" to "${preflight.targetVersion}".`,
|
|
2785
|
+
details: { diagnostic: migrationRes.diagnostic }
|
|
2786
|
+
};
|
|
2787
|
+
return {
|
|
2788
|
+
success: false,
|
|
2789
|
+
errors: [err],
|
|
2790
|
+
diagnosticMessage: err.message,
|
|
2791
|
+
preflight
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
finalDocument = migrationRes.document;
|
|
2795
|
+
} else {
|
|
2796
|
+
finalDocument = pageRaw;
|
|
2797
|
+
}
|
|
2798
|
+
let metadata = finalDocument.metadata || {
|
|
2799
|
+
title: "Imported Page",
|
|
2800
|
+
description: "",
|
|
2801
|
+
author: "",
|
|
2802
|
+
tags: [],
|
|
2803
|
+
category: "general",
|
|
2804
|
+
version: finalDocument.version || import_schema7.CURRENT_SCHEMA_VERSION
|
|
2805
|
+
};
|
|
2806
|
+
if (unzipped["metadata.json"]) {
|
|
2807
|
+
try {
|
|
2808
|
+
const metaFromFile = JSON.parse((0, import_fflate2.strFromU8)(unzipped["metadata.json"]));
|
|
2809
|
+
metadata = {
|
|
2810
|
+
...metadata,
|
|
2811
|
+
...metaFromFile
|
|
2812
|
+
};
|
|
2813
|
+
} catch {
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
finalDocument.metadata = metadata;
|
|
2817
|
+
const existingAssetIdsSet = /* @__PURE__ */ new Set();
|
|
2818
|
+
if (options.existingAssetIds) {
|
|
2819
|
+
if (Array.isArray(options.existingAssetIds)) {
|
|
2820
|
+
for (const id of options.existingAssetIds) existingAssetIdsSet.add(id);
|
|
2821
|
+
} else if (options.existingAssetIds instanceof Set) {
|
|
2822
|
+
for (const id of options.existingAssetIds) existingAssetIdsSet.add(id);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
if (options.assetProvider?.list && !options.existingAssetIds) {
|
|
2826
|
+
try {
|
|
2827
|
+
const list = await options.assetProvider.list();
|
|
2828
|
+
if (Array.isArray(list)) {
|
|
2829
|
+
for (const item of list) {
|
|
2830
|
+
if (item?.id) existingAssetIdsSet.add(item.id);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
} catch {
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
const renameAssetFn = options.renameAssetStrategy || defaultRenameAssetStrategy;
|
|
2837
|
+
const renameMap = {};
|
|
2838
|
+
const extractedAssets = /* @__PURE__ */ new Map();
|
|
2839
|
+
for (const assetItem of preflight.manifest.assets) {
|
|
2840
|
+
const assetBytes = unzipped[assetItem.path];
|
|
2841
|
+
if (assetBytes) {
|
|
2842
|
+
let targetAssetId = assetItem.id;
|
|
2843
|
+
let effectiveStrategy = options.assetCollisionStrategy || "reject";
|
|
2844
|
+
const conflictInfo = preflight.assetConflicts.find((c) => c.assetId === assetItem.id);
|
|
2845
|
+
if (conflictInfo) {
|
|
2846
|
+
if (options.onAssetConflict) {
|
|
2847
|
+
try {
|
|
2848
|
+
const conflictRes = await options.onAssetConflict(conflictInfo);
|
|
2849
|
+
if (typeof conflictRes === "string") {
|
|
2850
|
+
effectiveStrategy = conflictRes;
|
|
2851
|
+
} else {
|
|
2852
|
+
effectiveStrategy = conflictRes.action;
|
|
2853
|
+
if (conflictRes.newAssetId) {
|
|
2854
|
+
targetAssetId = conflictRes.newAssetId;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
} catch (err) {
|
|
2858
|
+
const diag = {
|
|
2859
|
+
code: "HOST_ADAPTER_ERROR",
|
|
2860
|
+
severity: "error",
|
|
2861
|
+
message: `Asset conflict resolution handler failed for "${assetItem.id}": ${err instanceof Error ? err.message : String(err)}`,
|
|
2862
|
+
path: assetItem.path
|
|
2863
|
+
};
|
|
2864
|
+
return {
|
|
2865
|
+
success: false,
|
|
2866
|
+
errors: [diag],
|
|
2867
|
+
diagnosticMessage: diag.message,
|
|
2868
|
+
preflight
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
if (effectiveStrategy === "reject") {
|
|
2873
|
+
const diag = {
|
|
2874
|
+
code: "ASSET_CONFLICT",
|
|
2875
|
+
severity: "error",
|
|
2876
|
+
message: `Import aborted: Asset "${assetItem.id}" collides with host asset. No overwrite strategy specified.`,
|
|
2877
|
+
path: assetItem.path
|
|
2878
|
+
};
|
|
2879
|
+
return {
|
|
2880
|
+
success: false,
|
|
2881
|
+
errors: [diag],
|
|
2882
|
+
diagnosticMessage: diag.message,
|
|
2883
|
+
preflight
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
if (effectiveStrategy === "rename") {
|
|
2887
|
+
if (targetAssetId === assetItem.id) {
|
|
2888
|
+
targetAssetId = renameAssetFn(assetItem, existingAssetIdsSet);
|
|
2889
|
+
}
|
|
2890
|
+
renameMap[assetItem.id] = targetAssetId;
|
|
2891
|
+
existingAssetIdsSet.add(targetAssetId);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
let hostInfo;
|
|
2895
|
+
if (effectiveStrategy === "reuse-existing" && conflictInfo) {
|
|
2896
|
+
if (options.assetProvider?.resolve) {
|
|
2897
|
+
hostInfo = await options.assetProvider.resolve(targetAssetId);
|
|
2898
|
+
}
|
|
2899
|
+
} else {
|
|
2900
|
+
const effectiveMeta = {
|
|
2901
|
+
...assetItem,
|
|
2902
|
+
id: targetAssetId
|
|
2903
|
+
};
|
|
2904
|
+
if (options.onAssetImport) {
|
|
2905
|
+
try {
|
|
2906
|
+
const result = await options.onAssetImport(targetAssetId, assetBytes, effectiveMeta);
|
|
2907
|
+
if (result) hostInfo = result;
|
|
2908
|
+
} catch (err) {
|
|
2909
|
+
const diag = {
|
|
2910
|
+
code: "HOST_ADAPTER_ERROR",
|
|
2911
|
+
severity: "error",
|
|
2912
|
+
message: `Host asset import hook failed for "${targetAssetId}": ${err instanceof Error ? err.message : String(err)}`,
|
|
2913
|
+
path: assetItem.path
|
|
2914
|
+
};
|
|
2915
|
+
return {
|
|
2916
|
+
success: false,
|
|
2917
|
+
errors: [diag],
|
|
2918
|
+
diagnosticMessage: diag.message,
|
|
2919
|
+
preflight
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
} else if (options.assetProvider?.upload) {
|
|
2923
|
+
try {
|
|
2924
|
+
const blob = new Blob([assetBytes], { type: assetItem.mimeType });
|
|
2925
|
+
const filename = assetItem.path.replace(/^assets\//, "");
|
|
2926
|
+
const file = new File([blob], filename, { type: assetItem.mimeType });
|
|
2927
|
+
const info = await options.assetProvider.upload(file, {
|
|
2928
|
+
assetId: targetAssetId,
|
|
2929
|
+
originalAssetId: assetItem.id
|
|
2930
|
+
});
|
|
2931
|
+
if (info) hostInfo = info;
|
|
2932
|
+
} catch {
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
extractedAssets.set(targetAssetId, {
|
|
2937
|
+
bytes: assetBytes,
|
|
2938
|
+
meta: {
|
|
2939
|
+
...assetItem,
|
|
2940
|
+
id: targetAssetId
|
|
2941
|
+
},
|
|
2942
|
+
hostInfo
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
if (Object.keys(renameMap).length > 0) {
|
|
2947
|
+
remapDocumentAssetReferences(finalDocument, renameMap);
|
|
2948
|
+
}
|
|
2949
|
+
return {
|
|
2950
|
+
success: true,
|
|
2951
|
+
document: finalDocument,
|
|
2952
|
+
manifest: preflight.manifest,
|
|
2953
|
+
metadata,
|
|
2954
|
+
extractedAssets,
|
|
2955
|
+
renamedAssets: Object.keys(renameMap).length > 0 ? renameMap : void 0,
|
|
2956
|
+
preflight
|
|
2957
|
+
};
|
|
2958
|
+
}
|
|
2959
|
+
var importStoraPackage = importPackage;
|
|
2960
|
+
|
|
2961
|
+
// src/round-trip-comparator.ts
|
|
2962
|
+
function isAssetReferenceLike(val) {
|
|
2963
|
+
if (!val || typeof val !== "object") return false;
|
|
2964
|
+
const obj = val;
|
|
2965
|
+
return obj.type === "asset" && typeof obj.assetId === "string" && obj.assetId.trim().length > 0;
|
|
2966
|
+
}
|
|
2967
|
+
function isVariableBindingLike(val) {
|
|
2968
|
+
if (!val || typeof val !== "object") return false;
|
|
2969
|
+
const obj = val;
|
|
2970
|
+
return obj.type === "variable" && typeof obj.key === "string" && obj.key.trim().length > 0;
|
|
2971
|
+
}
|
|
2972
|
+
function isActionBindingLike(val) {
|
|
2973
|
+
if (!val || typeof val !== "object") return false;
|
|
2974
|
+
const obj = val;
|
|
2975
|
+
return obj.type !== void 0 && typeof obj.type === "string" && obj.type.trim().length > 0 && ("payload" in obj || Object.keys(obj).length <= 2);
|
|
2976
|
+
}
|
|
2977
|
+
function normalizeAssetId(id, assetIdMap) {
|
|
2978
|
+
if (!assetIdMap) return id;
|
|
2979
|
+
if (assetIdMap instanceof Map) {
|
|
2980
|
+
return assetIdMap.get(id) ?? id;
|
|
2981
|
+
}
|
|
2982
|
+
return assetIdMap[id] ?? id;
|
|
2983
|
+
}
|
|
2984
|
+
function deepEqual(a, b) {
|
|
2985
|
+
if (a === b) return true;
|
|
2986
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
|
|
2987
|
+
return false;
|
|
2988
|
+
}
|
|
2989
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
2990
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
2991
|
+
if (a.length !== b.length) return false;
|
|
2992
|
+
for (let i = 0; i < a.length; i++) {
|
|
2993
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
2994
|
+
}
|
|
2995
|
+
return true;
|
|
2996
|
+
}
|
|
2997
|
+
const objA = a;
|
|
2998
|
+
const objB = b;
|
|
2999
|
+
const keysA = Object.keys(objA);
|
|
3000
|
+
const keysB = Object.keys(objB);
|
|
3001
|
+
if (keysA.length !== keysB.length) return false;
|
|
3002
|
+
for (const k of keysA) {
|
|
3003
|
+
if (!Object.prototype.hasOwnProperty.call(objB, k)) return false;
|
|
3004
|
+
if (!deepEqual(objA[k], objB[k])) return false;
|
|
3005
|
+
}
|
|
3006
|
+
return true;
|
|
3007
|
+
}
|
|
3008
|
+
function comparePropValues(path, expected, actual, differences, options) {
|
|
3009
|
+
if (isVariableBindingLike(expected)) {
|
|
3010
|
+
if (!isVariableBindingLike(actual)) {
|
|
3011
|
+
differences.push({
|
|
3012
|
+
path,
|
|
3013
|
+
kind: "BINDING_MISMATCH",
|
|
3014
|
+
message: `Expected variable binding at "${path}", but got non-binding value.`,
|
|
3015
|
+
expected,
|
|
3016
|
+
actual
|
|
3017
|
+
});
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
const expBinding = expected;
|
|
3021
|
+
const actBinding = actual;
|
|
3022
|
+
if (expBinding.key !== actBinding.key) {
|
|
3023
|
+
differences.push({
|
|
3024
|
+
path: `${path}.key`,
|
|
3025
|
+
kind: "BINDING_MISMATCH",
|
|
3026
|
+
message: `Variable binding key mismatch at "${path}": expected "${expBinding.key}", got "${actBinding.key}".`,
|
|
3027
|
+
expected: expBinding.key,
|
|
3028
|
+
actual: actBinding.key
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
if (!deepEqual(expBinding.fallback, actBinding.fallback)) {
|
|
3032
|
+
differences.push({
|
|
3033
|
+
path: `${path}.fallback`,
|
|
3034
|
+
kind: "BINDING_MISMATCH",
|
|
3035
|
+
message: `Variable binding fallback mismatch at "${path}".`,
|
|
3036
|
+
expected: expBinding.fallback,
|
|
3037
|
+
actual: actBinding.fallback
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
if (isActionBindingLike(expected) && !isAssetReferenceLike(expected) && !isVariableBindingLike(expected)) {
|
|
3043
|
+
if (!isActionBindingLike(actual)) {
|
|
3044
|
+
differences.push({
|
|
3045
|
+
path,
|
|
3046
|
+
kind: "ACTION_MISMATCH",
|
|
3047
|
+
message: `Expected action binding at "${path}", but got non-action value.`,
|
|
3048
|
+
expected,
|
|
3049
|
+
actual
|
|
3050
|
+
});
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
const expAction = expected;
|
|
3054
|
+
const actAction = actual;
|
|
3055
|
+
if (expAction.type !== actAction.type) {
|
|
3056
|
+
differences.push({
|
|
3057
|
+
path: `${path}.type`,
|
|
3058
|
+
kind: "ACTION_MISMATCH",
|
|
3059
|
+
message: `Action binding type mismatch at "${path}": expected "${expAction.type}", got "${actAction.type}".`,
|
|
3060
|
+
expected: expAction.type,
|
|
3061
|
+
actual: actAction.type
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
if (!deepEqual(expAction.payload ?? {}, actAction.payload ?? {})) {
|
|
3065
|
+
differences.push({
|
|
3066
|
+
path: `${path}.payload`,
|
|
3067
|
+
kind: "ACTION_MISMATCH",
|
|
3068
|
+
message: `Action binding payload mismatch at "${path}".`,
|
|
3069
|
+
expected: expAction.payload,
|
|
3070
|
+
actual: actAction.payload
|
|
3071
|
+
});
|
|
3072
|
+
}
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
if (isAssetReferenceLike(expected)) {
|
|
3076
|
+
if (!isAssetReferenceLike(actual)) {
|
|
3077
|
+
differences.push({
|
|
3078
|
+
path,
|
|
3079
|
+
kind: "ASSET_MISMATCH",
|
|
3080
|
+
message: `Expected asset reference at "${path}", but got non-asset value.`,
|
|
3081
|
+
expected,
|
|
3082
|
+
actual
|
|
3083
|
+
});
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
const expAsset = expected;
|
|
3087
|
+
const actAsset = actual;
|
|
3088
|
+
const expectedMappedId = normalizeAssetId(expAsset.assetId, options?.assetIdMap);
|
|
3089
|
+
if (actAsset.assetId !== expectedMappedId) {
|
|
3090
|
+
differences.push({
|
|
3091
|
+
path: `${path}.assetId`,
|
|
3092
|
+
kind: "ASSET_MISMATCH",
|
|
3093
|
+
message: `Asset reference ID mismatch at "${path}": expected "${expectedMappedId}" (original: "${expAsset.assetId}"), got "${actAsset.assetId}".`,
|
|
3094
|
+
expected: expectedMappedId,
|
|
3095
|
+
actual: actAsset.assetId
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
if (expAsset.filename !== actAsset.filename) {
|
|
3099
|
+
differences.push({
|
|
3100
|
+
path: `${path}.filename`,
|
|
3101
|
+
kind: "ASSET_MISMATCH",
|
|
3102
|
+
message: `Asset reference filename mismatch at "${path}": expected "${expAsset.filename}", got "${actAsset.filename}".`,
|
|
3103
|
+
expected: expAsset.filename,
|
|
3104
|
+
actual: actAsset.filename
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
if (expAsset.mimeType !== actAsset.mimeType) {
|
|
3108
|
+
differences.push({
|
|
3109
|
+
path: `${path}.mimeType`,
|
|
3110
|
+
kind: "ASSET_MISMATCH",
|
|
3111
|
+
message: `Asset reference mimeType mismatch at "${path}": expected "${expAsset.mimeType}", got "${actAsset.mimeType}".`,
|
|
3112
|
+
expected: expAsset.mimeType,
|
|
3113
|
+
actual: actAsset.mimeType
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
if (expected !== null && actual !== null && typeof expected === "object" && typeof actual === "object") {
|
|
3119
|
+
if (Array.isArray(expected) !== Array.isArray(actual)) {
|
|
3120
|
+
differences.push({
|
|
3121
|
+
path,
|
|
3122
|
+
kind: "PROP_MISMATCH",
|
|
3123
|
+
message: `Type mismatch at "${path}": one is array, other is object.`,
|
|
3124
|
+
expected,
|
|
3125
|
+
actual
|
|
3126
|
+
});
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
if (Array.isArray(expected) && Array.isArray(actual)) {
|
|
3130
|
+
if (expected.length !== actual.length) {
|
|
3131
|
+
differences.push({
|
|
3132
|
+
path,
|
|
3133
|
+
kind: "PROP_MISMATCH",
|
|
3134
|
+
message: `Array length mismatch at "${path}": expected length ${expected.length}, got ${actual.length}.`,
|
|
3135
|
+
expected,
|
|
3136
|
+
actual
|
|
3137
|
+
});
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
for (let i = 0; i < expected.length; i++) {
|
|
3141
|
+
comparePropValues(`${path}[${i}]`, expected[i], actual[i], differences, options);
|
|
3142
|
+
}
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
const expObj = expected;
|
|
3146
|
+
const actObj = actual;
|
|
3147
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(expObj), ...Object.keys(actObj)]);
|
|
3148
|
+
for (const key of allKeys) {
|
|
3149
|
+
if (!(key in expObj)) {
|
|
3150
|
+
differences.push({
|
|
3151
|
+
path: `${path}.${key}`,
|
|
3152
|
+
kind: "PROP_MISMATCH",
|
|
3153
|
+
message: `Unexpected extra prop key "${key}" at "${path}".`,
|
|
3154
|
+
actual: actObj[key]
|
|
3155
|
+
});
|
|
3156
|
+
} else if (!(key in actObj)) {
|
|
3157
|
+
differences.push({
|
|
3158
|
+
path: `${path}.${key}`,
|
|
3159
|
+
kind: "PROP_MISMATCH",
|
|
3160
|
+
message: `Missing expected prop key "${key}" at "${path}".`,
|
|
3161
|
+
expected: expObj[key]
|
|
3162
|
+
});
|
|
3163
|
+
} else {
|
|
3164
|
+
comparePropValues(`${path}.${key}`, expObj[key], actObj[key], differences, options);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
if (expected !== actual) {
|
|
3170
|
+
differences.push({
|
|
3171
|
+
path,
|
|
3172
|
+
kind: "PROP_MISMATCH",
|
|
3173
|
+
message: `Value mismatch at "${path}": expected "${String(expected)}", got "${String(actual)}".`,
|
|
3174
|
+
expected,
|
|
3175
|
+
actual
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
function compareStyles(nodePath, expected, actual, differences = []) {
|
|
3180
|
+
const breakpoints = ["base", "desktop", "tablet", "mobile"];
|
|
3181
|
+
const expStyles = expected || {};
|
|
3182
|
+
const actStyles = actual || {};
|
|
3183
|
+
for (const bp of breakpoints) {
|
|
3184
|
+
const expBp = expStyles[bp] || {};
|
|
3185
|
+
const actBp = actStyles[bp] || {};
|
|
3186
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(expBp), ...Object.keys(actBp)]);
|
|
3187
|
+
for (const key of allKeys) {
|
|
3188
|
+
const expVal = expBp[key];
|
|
3189
|
+
const actVal = actBp[key];
|
|
3190
|
+
const expNorm = expVal === void 0 || expVal === "" ? void 0 : expVal;
|
|
3191
|
+
const actNorm = actVal === void 0 || actVal === "" ? void 0 : actVal;
|
|
3192
|
+
if (expNorm !== actNorm) {
|
|
3193
|
+
differences.push({
|
|
3194
|
+
path: `${nodePath}.styles.${bp}.${key}`,
|
|
3195
|
+
kind: "STYLE_MISMATCH",
|
|
3196
|
+
message: `Style mismatch at "${nodePath}.styles.${bp}.${key}": expected "${String(expVal)}", got "${String(actVal)}".`,
|
|
3197
|
+
expected: expVal,
|
|
3198
|
+
actual: actVal
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
function compareNodes(expected, actual, nodePath, differences, options) {
|
|
3205
|
+
if (expected.id !== actual.id) {
|
|
3206
|
+
differences.push({
|
|
3207
|
+
path: `${nodePath}.id`,
|
|
3208
|
+
kind: "NODE_MISSING",
|
|
3209
|
+
message: `Node ID mismatch at "${nodePath}": expected "${expected.id}", got "${actual.id}".`,
|
|
3210
|
+
expected: expected.id,
|
|
3211
|
+
actual: actual.id
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
if (expected.type !== actual.type) {
|
|
3215
|
+
differences.push({
|
|
3216
|
+
path: `${nodePath}.type`,
|
|
3217
|
+
kind: "NODE_TYPE",
|
|
3218
|
+
message: `Node type mismatch for "${expected.id}": expected "${expected.type}", got "${actual.type}".`,
|
|
3219
|
+
expected: expected.type,
|
|
3220
|
+
actual: actual.type
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
comparePropValues(`${nodePath}.props`, expected.props || {}, actual.props || {}, differences, options);
|
|
3224
|
+
compareStyles(nodePath, expected.styles, actual.styles, differences);
|
|
3225
|
+
const expChildren = expected.children || [];
|
|
3226
|
+
const actChildren = actual.children || [];
|
|
3227
|
+
if (expChildren.length !== actChildren.length) {
|
|
3228
|
+
differences.push({
|
|
3229
|
+
path: `${nodePath}.children`,
|
|
3230
|
+
kind: "NODE_ORDER",
|
|
3231
|
+
message: `Children count mismatch for node "${expected.id}": expected ${expChildren.length}, got ${actChildren.length}.`,
|
|
3232
|
+
expected: expChildren.map((c) => c.id),
|
|
3233
|
+
actual: actChildren.map((c) => c.id)
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
const maxLen = Math.max(expChildren.length, actChildren.length);
|
|
3237
|
+
for (let i = 0; i < maxLen; i++) {
|
|
3238
|
+
const expChild = expChildren[i];
|
|
3239
|
+
const actChild = actChildren[i];
|
|
3240
|
+
if (!expChild && actChild) {
|
|
3241
|
+
differences.push({
|
|
3242
|
+
path: `${nodePath}.children[${i}]`,
|
|
3243
|
+
kind: "NODE_EXTRA",
|
|
3244
|
+
message: `Unexpected extra child node "${actChild.id}" at index ${i}.`,
|
|
3245
|
+
actual: actChild.id
|
|
3246
|
+
});
|
|
3247
|
+
} else if (expChild && !actChild) {
|
|
3248
|
+
differences.push({
|
|
3249
|
+
path: `${nodePath}.children[${i}]`,
|
|
3250
|
+
kind: "NODE_MISSING",
|
|
3251
|
+
message: `Missing child node "${expChild.id}" at index ${i}.`,
|
|
3252
|
+
expected: expChild.id
|
|
3253
|
+
});
|
|
3254
|
+
} else if (expChild && actChild) {
|
|
3255
|
+
if (expChild.id !== actChild.id) {
|
|
3256
|
+
differences.push({
|
|
3257
|
+
path: `${nodePath}.children[${i}]`,
|
|
3258
|
+
kind: "NODE_ORDER",
|
|
3259
|
+
message: `Node order mismatch at index ${i}: expected node "${expChild.id}", got "${actChild.id}".`,
|
|
3260
|
+
expected: expChild.id,
|
|
3261
|
+
actual: actChild.id
|
|
3262
|
+
});
|
|
3263
|
+
}
|
|
3264
|
+
compareNodes(expChild, actChild, `${nodePath}.children[${i}](${expChild.id})`, differences, options);
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
function compareDocumentsSemantically(expected, actual, options = {}) {
|
|
3269
|
+
const differences = [];
|
|
3270
|
+
const { ignoreTimestamps = true } = options;
|
|
3271
|
+
if (expected.schema !== actual.schema) {
|
|
3272
|
+
differences.push({
|
|
3273
|
+
path: "schema",
|
|
3274
|
+
kind: "METADATA_MISMATCH",
|
|
3275
|
+
message: `Schema mismatch: expected "${expected.schema}", got "${actual.schema}".`,
|
|
3276
|
+
expected: expected.schema,
|
|
3277
|
+
actual: actual.schema
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
3280
|
+
if (expected.version !== actual.version) {
|
|
3281
|
+
differences.push({
|
|
3282
|
+
path: "version",
|
|
3283
|
+
kind: "METADATA_MISMATCH",
|
|
3284
|
+
message: `Schema version mismatch: expected "${expected.version}", got "${actual.version}".`,
|
|
3285
|
+
expected: expected.version,
|
|
3286
|
+
actual: actual.version
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
const expMeta = expected.metadata || {};
|
|
3290
|
+
const actMeta = actual.metadata || {};
|
|
3291
|
+
const metaKeys = /* @__PURE__ */ new Set([...Object.keys(expMeta), ...Object.keys(actMeta)]);
|
|
3292
|
+
for (const k of metaKeys) {
|
|
3293
|
+
if (ignoreTimestamps && (k === "updatedAt" || k === "createdAt")) {
|
|
3294
|
+
continue;
|
|
3295
|
+
}
|
|
3296
|
+
const expVal = expMeta[k];
|
|
3297
|
+
const actVal = actMeta[k];
|
|
3298
|
+
if (!deepEqual(expVal, actVal)) {
|
|
3299
|
+
differences.push({
|
|
3300
|
+
path: `metadata.${k}`,
|
|
3301
|
+
kind: "METADATA_MISMATCH",
|
|
3302
|
+
message: `Metadata mismatch at "metadata.${k}": expected "${String(expVal)}", got "${String(actVal)}".`,
|
|
3303
|
+
expected: expVal,
|
|
3304
|
+
actual: actVal
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
compareNodes(
|
|
3309
|
+
expected.document,
|
|
3310
|
+
actual.document,
|
|
3311
|
+
`root(${expected.document.id})`,
|
|
3312
|
+
differences,
|
|
3313
|
+
options
|
|
3314
|
+
);
|
|
3315
|
+
return {
|
|
3316
|
+
equivalent: differences.length === 0,
|
|
3317
|
+
differences
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
function compareManifestsSemantically(expected, actual, options = {}) {
|
|
3321
|
+
const differences = [];
|
|
3322
|
+
const { ignoreTimestamps = true } = options;
|
|
3323
|
+
if (expected.schema !== actual.schema) {
|
|
3324
|
+
differences.push({
|
|
3325
|
+
path: "manifest.schema",
|
|
3326
|
+
kind: "METADATA_MISMATCH",
|
|
3327
|
+
message: `Manifest schema mismatch: expected "${expected.schema}", got "${actual.schema}".`,
|
|
3328
|
+
expected: expected.schema,
|
|
3329
|
+
actual: actual.schema
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
if (expected.schemaVersion !== actual.schemaVersion) {
|
|
3333
|
+
differences.push({
|
|
3334
|
+
path: "manifest.schemaVersion",
|
|
3335
|
+
kind: "METADATA_MISMATCH",
|
|
3336
|
+
message: `Manifest schemaVersion mismatch: expected "${expected.schemaVersion}", got "${actual.schemaVersion}".`,
|
|
3337
|
+
expected: expected.schemaVersion,
|
|
3338
|
+
actual: actual.schemaVersion
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
const expComponents = new Set(expected.requiredComponents || []);
|
|
3342
|
+
const actComponents = new Set(actual.requiredComponents || []);
|
|
3343
|
+
for (const c of expComponents) {
|
|
3344
|
+
if (!actComponents.has(c)) {
|
|
3345
|
+
differences.push({
|
|
3346
|
+
path: "manifest.requiredComponents",
|
|
3347
|
+
kind: "DEPENDENCY_MISMATCH",
|
|
3348
|
+
message: `Missing required component in manifest: "${c}".`,
|
|
3349
|
+
expected: c
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
for (const c of actComponents) {
|
|
3354
|
+
if (!expComponents.has(c)) {
|
|
3355
|
+
differences.push({
|
|
3356
|
+
path: "manifest.requiredComponents",
|
|
3357
|
+
kind: "DEPENDENCY_MISMATCH",
|
|
3358
|
+
message: `Unexpected extra required component in manifest: "${c}".`,
|
|
3359
|
+
actual: c
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
const expCaps = new Set(expected.requiredCapabilities || []);
|
|
3364
|
+
const actCaps = new Set(actual.requiredCapabilities || []);
|
|
3365
|
+
for (const c of expCaps) {
|
|
3366
|
+
if (!actCaps.has(c)) {
|
|
3367
|
+
differences.push({
|
|
3368
|
+
path: "manifest.requiredCapabilities",
|
|
3369
|
+
kind: "DEPENDENCY_MISMATCH",
|
|
3370
|
+
message: `Missing required capability in manifest: "${c}".`,
|
|
3371
|
+
expected: c
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
for (const c of actCaps) {
|
|
3376
|
+
if (!expCaps.has(c)) {
|
|
3377
|
+
differences.push({
|
|
3378
|
+
path: "manifest.requiredCapabilities",
|
|
3379
|
+
kind: "DEPENDENCY_MISMATCH",
|
|
3380
|
+
message: `Unexpected extra required capability in manifest: "${c}".`,
|
|
3381
|
+
actual: c
|
|
3382
|
+
});
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
if (expected.assets.length !== actual.assets.length) {
|
|
3386
|
+
differences.push({
|
|
3387
|
+
path: "manifest.assets",
|
|
3388
|
+
kind: "ASSET_MISMATCH",
|
|
3389
|
+
message: `Manifest asset count mismatch: expected ${expected.assets.length}, got ${actual.assets.length}.`,
|
|
3390
|
+
expected: expected.assets.length,
|
|
3391
|
+
actual: actual.assets.length
|
|
3392
|
+
});
|
|
3393
|
+
}
|
|
3394
|
+
return {
|
|
3395
|
+
equivalent: differences.length === 0,
|
|
3396
|
+
differences
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
// src/template-utils.ts
|
|
3401
|
+
var import_schema8 = require("@kubuild/schema");
|
|
3402
|
+
var CORE_BUILTIN_COMPONENTS = /* @__PURE__ */ new Set([
|
|
3403
|
+
"page",
|
|
3404
|
+
"section",
|
|
3405
|
+
"container",
|
|
3406
|
+
"columns",
|
|
3407
|
+
"column",
|
|
3408
|
+
"heading",
|
|
3409
|
+
"text",
|
|
3410
|
+
"image",
|
|
3411
|
+
"button",
|
|
3412
|
+
"collection"
|
|
3413
|
+
]);
|
|
3414
|
+
function validateTemplate(value) {
|
|
3415
|
+
const parseResult = import_schema8.TemplateRecordSchema.safeParse(value);
|
|
3416
|
+
if (parseResult.success) {
|
|
3417
|
+
return {
|
|
3418
|
+
valid: true,
|
|
3419
|
+
data: parseResult.data
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
const errors = parseResult.error.issues.map((issue) => ({
|
|
3423
|
+
code: issue.code,
|
|
3424
|
+
message: issue.message,
|
|
3425
|
+
path: `/${issue.path.join("/")}`
|
|
3426
|
+
}));
|
|
3427
|
+
return {
|
|
3428
|
+
valid: false,
|
|
3429
|
+
errors
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
function collectComponentTypes(node, types) {
|
|
3433
|
+
if (node.type) {
|
|
3434
|
+
types.add(node.type);
|
|
3435
|
+
}
|
|
3436
|
+
if (node.children && Array.isArray(node.children)) {
|
|
3437
|
+
for (const child of node.children) {
|
|
3438
|
+
collectComponentTypes(child, types);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
function extractTemplateRequirements(doc, builtinComponents = CORE_BUILTIN_COMPONENTS) {
|
|
3443
|
+
const componentTypes = /* @__PURE__ */ new Set();
|
|
3444
|
+
if (doc?.document) {
|
|
3445
|
+
collectComponentTypes(doc.document, componentTypes);
|
|
3446
|
+
}
|
|
3447
|
+
const customComponents = [];
|
|
3448
|
+
for (const type of componentTypes) {
|
|
3449
|
+
if (!builtinComponents.has(type)) {
|
|
3450
|
+
customComponents.push(type);
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
return {
|
|
3454
|
+
requiredComponents: customComponents.sort(),
|
|
3455
|
+
requiredCapabilities: []
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
function createTemplateRecord(params) {
|
|
3459
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3460
|
+
let requirements = params.requirements;
|
|
3461
|
+
if (!requirements && params.document) {
|
|
3462
|
+
requirements = extractTemplateRequirements(params.document);
|
|
3463
|
+
}
|
|
3464
|
+
const candidate = {
|
|
3465
|
+
id: params.id,
|
|
3466
|
+
name: params.name,
|
|
3467
|
+
description: params.description ?? "",
|
|
3468
|
+
category: params.category ?? "general",
|
|
3469
|
+
tags: params.tags ?? [],
|
|
3470
|
+
thumbnail: params.thumbnail,
|
|
3471
|
+
author: params.author ?? "",
|
|
3472
|
+
version: params.version ?? "1.0.0",
|
|
3473
|
+
document: params.document,
|
|
3474
|
+
packageReference: params.packageReference,
|
|
3475
|
+
requirements: requirements ?? { requiredComponents: [], requiredCapabilities: [] },
|
|
3476
|
+
createdAt: params.createdAt ?? now,
|
|
3477
|
+
updatedAt: params.updatedAt ?? now,
|
|
3478
|
+
custom: params.custom
|
|
3479
|
+
};
|
|
3480
|
+
const validation = validateTemplate(candidate);
|
|
3481
|
+
if (!validation.valid || !validation.data) {
|
|
3482
|
+
const errorDetails = validation.errors?.map((e) => `${e.path}: ${e.message}`).join(", ");
|
|
3483
|
+
throw new Error(`Invalid template record: ${errorDetails}`);
|
|
3484
|
+
}
|
|
3485
|
+
return validation.data;
|
|
3486
|
+
}
|
|
3487
|
+
function saveDraftAsTemplate(draft, metadata, options = {}) {
|
|
3488
|
+
if (!metadata || typeof metadata !== "object") {
|
|
3489
|
+
throw new Error("Save as template requires metadata with id and name.");
|
|
3490
|
+
}
|
|
3491
|
+
const cleanId = metadata.id?.trim();
|
|
3492
|
+
if (!cleanId) {
|
|
3493
|
+
throw new Error("Template ID is required and cannot be empty.");
|
|
3494
|
+
}
|
|
3495
|
+
const cleanName = metadata.name?.trim();
|
|
3496
|
+
if (!cleanName) {
|
|
3497
|
+
throw new Error("Template name is required and cannot be empty.");
|
|
3498
|
+
}
|
|
3499
|
+
if (!draft || !draft.document) {
|
|
3500
|
+
throw new Error("Invalid draft document: document tree is missing.");
|
|
3501
|
+
}
|
|
3502
|
+
if (options.validateDraft !== false) {
|
|
3503
|
+
const validation = validateDocument(draft);
|
|
3504
|
+
if (!validation.valid) {
|
|
3505
|
+
const errorMsg = validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ");
|
|
3506
|
+
throw new Error(`Cannot save invalid draft as template: ${errorMsg}`);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
const draftSnapshot = deepClone(draft);
|
|
3510
|
+
return createTemplateRecord({
|
|
3511
|
+
id: cleanId,
|
|
3512
|
+
name: cleanName,
|
|
3513
|
+
description: metadata.description?.trim() ?? "",
|
|
3514
|
+
category: metadata.category?.trim() ?? "general",
|
|
3515
|
+
tags: metadata.tags ? [...metadata.tags] : [],
|
|
3516
|
+
thumbnail: metadata.thumbnail,
|
|
3517
|
+
author: metadata.author?.trim() ?? "",
|
|
3518
|
+
version: metadata.version?.trim() ?? "1.0.0",
|
|
3519
|
+
document: draftSnapshot,
|
|
3520
|
+
requirements: options.requirements,
|
|
3521
|
+
custom: metadata.custom ? deepClone(metadata.custom) : void 0
|
|
3522
|
+
});
|
|
3523
|
+
}
|
|
3524
|
+
function cloneTreeWithFreshIds(root, idGen) {
|
|
3525
|
+
function cloneRec(node) {
|
|
3526
|
+
const newId = idGen(node.id, node);
|
|
3527
|
+
const clonedProps = node.props ? deepClone(node.props) : void 0;
|
|
3528
|
+
const clonedStyles = node.styles ? deepClone(node.styles) : void 0;
|
|
3529
|
+
const clonedChildren = node.children ? node.children.map((child) => cloneRec(child)) : [];
|
|
3530
|
+
return {
|
|
3531
|
+
id: newId,
|
|
3532
|
+
type: node.type,
|
|
3533
|
+
...clonedProps ? { props: clonedProps } : {},
|
|
3534
|
+
...clonedStyles ? { styles: clonedStyles } : {},
|
|
3535
|
+
children: clonedChildren
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
return cloneRec(root);
|
|
3539
|
+
}
|
|
3540
|
+
function cloneTemplateAsPage(templateOrDoc, options = {}) {
|
|
3541
|
+
let sourceDoc;
|
|
3542
|
+
let templateOrigin = null;
|
|
3543
|
+
if ((0, import_schema8.isTemplateRecord)(templateOrDoc)) {
|
|
3544
|
+
if (!templateOrDoc.document) {
|
|
3545
|
+
throw new Error(
|
|
3546
|
+
`Template "${templateOrDoc.name}" (${templateOrDoc.id}) does not contain an inline document snapshot to clone from.`
|
|
3547
|
+
);
|
|
3548
|
+
}
|
|
3549
|
+
sourceDoc = templateOrDoc.document;
|
|
3550
|
+
templateOrigin = {
|
|
3551
|
+
id: templateOrDoc.id,
|
|
3552
|
+
name: templateOrDoc.name,
|
|
3553
|
+
version: templateOrDoc.version ?? "1.0.0"
|
|
3554
|
+
};
|
|
3555
|
+
} else if ("schema" in templateOrDoc && "document" in templateOrDoc) {
|
|
3556
|
+
sourceDoc = templateOrDoc;
|
|
3557
|
+
} else {
|
|
3558
|
+
throw new Error("Invalid input: expected a TemplateRecord or PageDocument.");
|
|
3559
|
+
}
|
|
3560
|
+
const existingIdsInSource = new Set((0, import_schema8.collectNodeIds)(sourceDoc.document));
|
|
3561
|
+
const assignedNewIds = /* @__PURE__ */ new Set();
|
|
3562
|
+
const prefix = options.idPrefix || `page_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 6)}`;
|
|
3563
|
+
let counter = 1;
|
|
3564
|
+
const defaultIdGen = (oldId, node) => {
|
|
3565
|
+
let candidate = `${node.type}_${prefix}_${counter}`;
|
|
3566
|
+
while (existingIdsInSource.has(candidate) || assignedNewIds.has(candidate)) {
|
|
3567
|
+
counter++;
|
|
3568
|
+
candidate = `${node.type}_${prefix}_${counter}`;
|
|
3569
|
+
}
|
|
3570
|
+
counter++;
|
|
3571
|
+
assignedNewIds.add(candidate);
|
|
3572
|
+
return candidate;
|
|
3573
|
+
};
|
|
3574
|
+
const idGen = options.idGenerator || defaultIdGen;
|
|
3575
|
+
const clonedRootNode = cloneTreeWithFreshIds(sourceDoc.document, idGen);
|
|
3576
|
+
const rootPageNode = {
|
|
3577
|
+
...clonedRootNode,
|
|
3578
|
+
type: "page"
|
|
3579
|
+
};
|
|
3580
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3581
|
+
const pageTitle = options.title ?? (templateOrigin ? `${templateOrigin.name}` : sourceDoc.metadata?.title ?? "Untitled Page");
|
|
3582
|
+
const customMetadata = {
|
|
3583
|
+
...sourceDoc.metadata?.custom ? deepClone(sourceDoc.metadata.custom) : {},
|
|
3584
|
+
...options.customMetadata ? deepClone(options.customMetadata) : {}
|
|
3585
|
+
};
|
|
3586
|
+
if (options.recordOrigin !== false && templateOrigin) {
|
|
3587
|
+
customMetadata.originTemplate = templateOrigin;
|
|
3588
|
+
}
|
|
3589
|
+
const newDoc = {
|
|
3590
|
+
schema: sourceDoc.schema,
|
|
3591
|
+
version: sourceDoc.version,
|
|
3592
|
+
metadata: {
|
|
3593
|
+
title: pageTitle,
|
|
3594
|
+
description: sourceDoc.metadata?.description ?? "",
|
|
3595
|
+
author: options.author ?? sourceDoc.metadata?.author ?? "",
|
|
3596
|
+
tags: sourceDoc.metadata?.tags ? [...sourceDoc.metadata.tags] : [],
|
|
3597
|
+
category: sourceDoc.metadata?.category ?? "general",
|
|
3598
|
+
version: "1.0.0",
|
|
3599
|
+
createdAt: now,
|
|
3600
|
+
updatedAt: now,
|
|
3601
|
+
custom: customMetadata
|
|
3602
|
+
},
|
|
3603
|
+
document: rootPageNode
|
|
3604
|
+
};
|
|
3605
|
+
return newDoc;
|
|
3606
|
+
}
|
|
3607
|
+
function areNodeIdsCompletelyDistinct(docA, docB) {
|
|
3608
|
+
if (!docA?.document || !docB?.document) return true;
|
|
3609
|
+
const idsA = new Set((0, import_schema8.collectNodeIds)(docA.document));
|
|
3610
|
+
const idsB = (0, import_schema8.collectNodeIds)(docB.document);
|
|
3611
|
+
for (const id of idsB) {
|
|
3612
|
+
if (idsA.has(id)) {
|
|
3613
|
+
return false;
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
return true;
|
|
3617
|
+
}
|
|
3618
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3619
|
+
0 && (module.exports = {
|
|
3620
|
+
CORE_BUILTIN_COMPONENTS,
|
|
3621
|
+
DEFAULT_DOCUMENT_SECURITY_LIMITS,
|
|
3622
|
+
DEFAULT_MAX_HISTORY,
|
|
3623
|
+
DEFAULT_SECURITY_LIMITS,
|
|
3624
|
+
DocumentHistoryManager,
|
|
3625
|
+
HistoryEngine,
|
|
3626
|
+
MigrationRegistry,
|
|
3627
|
+
areNodeIdsCompletelyDistinct,
|
|
3628
|
+
buildSampleVariablesFromCatalog,
|
|
3629
|
+
calculateChecksum,
|
|
3630
|
+
canMigrate,
|
|
3631
|
+
checkZipBomb,
|
|
3632
|
+
cloneTemplateAsPage,
|
|
3633
|
+
cloneTreeWithNewIds,
|
|
3634
|
+
collectAssetReferences,
|
|
3635
|
+
collectNodeIdSet,
|
|
3636
|
+
compareDocumentsSemantically,
|
|
3637
|
+
compareManifestsSemantically,
|
|
3638
|
+
containsProhibitedKeys,
|
|
3639
|
+
createBlankDocument,
|
|
3640
|
+
createTemplateRecord,
|
|
3641
|
+
deepClone,
|
|
3642
|
+
defaultIdGenerator,
|
|
3643
|
+
defaultMigrationRegistry,
|
|
3644
|
+
defaultRenameAssetStrategy,
|
|
3645
|
+
duplicateNode,
|
|
3646
|
+
exportPackage,
|
|
3647
|
+
exportStoraPackage,
|
|
3648
|
+
extractRequirementsFromTree,
|
|
3649
|
+
extractTemplateRequirements,
|
|
3650
|
+
findMissingComponentNodes,
|
|
3651
|
+
findNodeById,
|
|
3652
|
+
findNodeLocation,
|
|
3653
|
+
getAncestorChain,
|
|
3654
|
+
getMigrationPath,
|
|
3655
|
+
getNavigationTarget,
|
|
3656
|
+
getNodeAncestors,
|
|
3657
|
+
getParentNodeId,
|
|
3658
|
+
importPackage,
|
|
3659
|
+
importStoraPackage,
|
|
3660
|
+
insertNode,
|
|
3661
|
+
inspectPackage,
|
|
3662
|
+
isDangerousAssetFilename,
|
|
3663
|
+
isDangerousPath,
|
|
3664
|
+
isDescendantOf,
|
|
3665
|
+
isSafeUrl,
|
|
3666
|
+
migrateDocument,
|
|
3667
|
+
moveNode,
|
|
3668
|
+
preflightPackage,
|
|
3669
|
+
previewImportPackage,
|
|
3670
|
+
remapAssetReferences,
|
|
3671
|
+
remapDocumentAssetReferences,
|
|
3672
|
+
removeNode,
|
|
3673
|
+
resolveBinding,
|
|
3674
|
+
resolveBindingValue,
|
|
3675
|
+
sanitizeFilename,
|
|
3676
|
+
sanitizeHtml,
|
|
3677
|
+
sanitizeUrl,
|
|
3678
|
+
saveDraftAsTemplate,
|
|
3679
|
+
sha256Sync,
|
|
3680
|
+
updateProps,
|
|
3681
|
+
updateStyle,
|
|
3682
|
+
validateDocument,
|
|
3683
|
+
validateDocumentSecurity,
|
|
3684
|
+
validateTemplate
|
|
3685
|
+
});
|
|
3686
|
+
//# sourceMappingURL=index.cjs.map
|