@deckjsx/node 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +45 -0
- package/dist/index.mjs +698 -0
- package/package.json +36 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { AssetLoader, DeckPlugin, RenderPatchPlanPart } from "deckjsx/integration";
|
|
2
|
+
import { RenderResult } from "deckjsx";
|
|
3
|
+
|
|
4
|
+
//#region src/package-patch.d.ts
|
|
5
|
+
type WriteDiagnostic = {
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly message: string;
|
|
8
|
+
readonly path?: string;
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/index.d.ts
|
|
12
|
+
type WriteStrategy = "atomic-replace" | "in-place" | "write-file";
|
|
13
|
+
type WriteResult = {
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly status: "created" | "failed" | "patched" | "replaced";
|
|
16
|
+
readonly strategy: WriteStrategy;
|
|
17
|
+
readonly bytesWritten: number;
|
|
18
|
+
readonly patchedParts: readonly string[];
|
|
19
|
+
readonly diagnostics: readonly WriteDiagnostic[];
|
|
20
|
+
};
|
|
21
|
+
type PatchablePptxPartInspectionStatus = "missing" | "stale" | "unsupported" | "verified";
|
|
22
|
+
type PatchablePptxPartInspection = Omit<RenderPatchPlanPart, "buildReason" | "buildStatus"> & {
|
|
23
|
+
readonly status: PatchablePptxPartInspectionStatus;
|
|
24
|
+
readonly currentFingerprint?: string;
|
|
25
|
+
readonly zipMethod?: number;
|
|
26
|
+
};
|
|
27
|
+
type PatchablePptxInspectionResult = {
|
|
28
|
+
readonly path: string;
|
|
29
|
+
readonly ok: boolean;
|
|
30
|
+
readonly patchable: boolean;
|
|
31
|
+
readonly manifestPath: string;
|
|
32
|
+
readonly partCount: number;
|
|
33
|
+
readonly parts: readonly PatchablePptxPartInspection[];
|
|
34
|
+
readonly diagnostics: readonly WriteDiagnostic[];
|
|
35
|
+
};
|
|
36
|
+
type NodeFileAssetLoaderOptions = {
|
|
37
|
+
readonly root?: string;
|
|
38
|
+
readonly resolverIdentity?: string;
|
|
39
|
+
};
|
|
40
|
+
declare function createNodeFileAssetLoader(options?: NodeFileAssetLoaderOptions): AssetLoader;
|
|
41
|
+
declare function nodeAssets(options?: NodeFileAssetLoaderOptions): DeckPlugin;
|
|
42
|
+
declare function inspectPatchablePptx(outputPath: string): Promise<PatchablePptxInspectionResult>;
|
|
43
|
+
declare function write(render: RenderResult, outputPath: string): Promise<WriteResult>;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { NodeFileAssetLoaderOptions, PatchablePptxInspectionResult, PatchablePptxPartInspection, PatchablePptxPartInspectionStatus, type WriteDiagnostic, WriteResult, WriteStrategy, createNodeFileAssetLoader, inspectPatchablePptx, nodeAssets, write };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, open, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { PATCH_MANIFEST_KIND, PATCH_MANIFEST_PATH, PATCH_MANIFEST_VERSION, integrationContextId, patchManifestFromParts } from "deckjsx/integration";
|
|
5
|
+
const PATCH_RESERVE_MARKER = "deckjsx-patch-reserve:";
|
|
6
|
+
const textDecoder = new TextDecoder();
|
|
7
|
+
const textEncoder$1 = new TextEncoder();
|
|
8
|
+
const CRC32_TABLE = createCrc32Table();
|
|
9
|
+
function tryCreateInPlacePatch(currentBytes, nextBytes, patchPlan) {
|
|
10
|
+
const currentArchive = parseZipArchive(currentBytes);
|
|
11
|
+
const nextArchive = parseZipArchive(nextBytes);
|
|
12
|
+
if (!currentArchive || !nextArchive) return inPlacePatchFailure("deckjsx.node.write.unreadableZip", "Existing or rendered PPTX bytes could not be read as a ZIP archive.");
|
|
13
|
+
const manifestEntry = currentArchive.entries.get(PATCH_MANIFEST_PATH);
|
|
14
|
+
const currentManifest = manifestEntry ? parsePatchManifest(manifestEntry.bytes) : void 0;
|
|
15
|
+
if (!manifestEntry || !currentManifest) return inPlacePatchFailure("deckjsx.node.write.missingPatchManifest", "Existing PPTX does not contain a deckjsx patch manifest.", PATCH_MANIFEST_PATH);
|
|
16
|
+
const currentParts = new Map(currentManifest.parts.map((part) => [part.packagePartId, part]));
|
|
17
|
+
const nextParts = new Map(patchPlan.parts.filter((part) => part.patchableKind !== "manifest").map((part) => [part.packagePartId, part]));
|
|
18
|
+
for (const currentPart of currentParts.values()) if (!nextParts.has(currentPart.packagePartId)) return inPlacePatchFailure("deckjsx.node.write.patchManifestRemovedPart", "Existing PPTX patch manifest contains a package part that is not present in the current render patch plan.", currentPart.path);
|
|
19
|
+
const segments = [];
|
|
20
|
+
const patchedParts = [];
|
|
21
|
+
for (const nextPart of patchPlan.parts) {
|
|
22
|
+
if (nextPart.patchableKind === "manifest") continue;
|
|
23
|
+
const currentPart = currentParts.get(nextPart.packagePartId);
|
|
24
|
+
if (!currentPart || currentPart.path !== nextPart.path) return inPlacePatchFailure("deckjsx.node.write.patchManifestMismatch", "Existing PPTX patch manifest does not match the current render patch plan.", nextPart.path);
|
|
25
|
+
const currentEntry = currentArchive.entries.get(currentPart.path);
|
|
26
|
+
const nextEntry = nextArchive.entries.get(nextPart.path);
|
|
27
|
+
if (!currentEntry || !nextEntry || currentEntry.method !== 0 || nextEntry.method !== 0) return inPlacePatchFailure("deckjsx.node.write.unsupportedZipEntry", "Changed package part cannot be updated in place because its ZIP entry is missing or unsupported.", nextPart.path);
|
|
28
|
+
if (fingerprintPatchableEntry(currentPart, currentEntry.bytes) !== currentPart.fingerprint) return inPlacePatchFailure("deckjsx.node.write.patchManifestStale", "Existing PPTX package part bytes do not match the deckjsx patch manifest.", currentPart.path);
|
|
29
|
+
if (currentPart.fingerprint === nextPart.fingerprint) continue;
|
|
30
|
+
let patchedBytes;
|
|
31
|
+
if (nextPart.patchableKind === "xml") patchedBytes = paddedXmlBytes(nextEntry.bytes, currentEntry.compressedSize);
|
|
32
|
+
else if (nextPart.patchableKind === "media") patchedBytes = nextEntry.bytes.byteLength === currentEntry.compressedSize ? nextEntry.bytes : void 0;
|
|
33
|
+
else return inPlacePatchFailure("deckjsx.node.write.unsupportedPatchableKind", "Changed package part cannot be updated in place by the current node writer.", nextPart.path);
|
|
34
|
+
if (!patchedBytes) return inPlacePatchFailure("deckjsx.node.write.inPlacePatchExceededCapacity", "Changed package part exceeded its reserved in-place patch capacity.", nextPart.path);
|
|
35
|
+
segments.push(...zipEntryPatchSegments(currentEntry, patchedBytes));
|
|
36
|
+
patchedParts.push(nextPart.path);
|
|
37
|
+
}
|
|
38
|
+
const manifestBytes = paddedManifestBytes(patchManifestLogicalBytes(patchPlan), manifestEntry.compressedSize);
|
|
39
|
+
if (!manifestBytes) return inPlacePatchFailure("deckjsx.node.write.inPlacePatchExceededCapacity", "Patch manifest exceeded its reserved in-place patch capacity.", PATCH_MANIFEST_PATH);
|
|
40
|
+
segments.push(...zipEntryPatchSegments(manifestEntry, manifestBytes));
|
|
41
|
+
patchedParts.push(PATCH_MANIFEST_PATH);
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
segments,
|
|
45
|
+
patchedParts
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function fingerprintPatchableEntry(part, bytes) {
|
|
49
|
+
if (part.patchableKind === "xml") return fingerprintBytes(stripXmlReserve(bytes));
|
|
50
|
+
return fingerprintBytes(bytes);
|
|
51
|
+
}
|
|
52
|
+
function parsePatchManifest(bytes) {
|
|
53
|
+
try {
|
|
54
|
+
const value = JSON.parse(textDecoder.decode(bytes).trim());
|
|
55
|
+
return isPatchManifest(value) ? value : void 0;
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isPatchManifest(value) {
|
|
61
|
+
if (!isRecord(value) || value.kind !== PATCH_MANIFEST_KIND || value.version !== PATCH_MANIFEST_VERSION) return false;
|
|
62
|
+
return Array.isArray(value.parts) && value.parts.every(isPatchManifestPart);
|
|
63
|
+
}
|
|
64
|
+
function isPatchManifestPart(value) {
|
|
65
|
+
if (!isRecord(value)) return false;
|
|
66
|
+
return typeof value.packagePartId === "string" && typeof value.path === "string" && isPatchableKind(value.patchableKind) && typeof value.reservedCapacity === "number" && Number.isSafeInteger(value.reservedCapacity) && value.reservedCapacity >= 0 && typeof value.logicalByteLength === "number" && Number.isSafeInteger(value.logicalByteLength) && value.logicalByteLength >= 0 && typeof value.storedByteLength === "number" && Number.isSafeInteger(value.storedByteLength) && value.storedByteLength >= 0 && typeof value.fingerprint === "string";
|
|
67
|
+
}
|
|
68
|
+
function isPatchableKind(value) {
|
|
69
|
+
return value === "manifest" || value === "media" || value === "xml";
|
|
70
|
+
}
|
|
71
|
+
function isRecord(value) {
|
|
72
|
+
return typeof value === "object" && value !== null;
|
|
73
|
+
}
|
|
74
|
+
function parseZipArchive(bytes) {
|
|
75
|
+
const eocdOffset = findEndOfCentralDirectory(bytes);
|
|
76
|
+
if (eocdOffset < 0) return;
|
|
77
|
+
const entryCount = readUint16(bytes, eocdOffset + 10);
|
|
78
|
+
const centralDirectorySize = readUint32(bytes, eocdOffset + 12);
|
|
79
|
+
const centralDirectoryOffset = readUint32(bytes, eocdOffset + 16);
|
|
80
|
+
if (entryCount === void 0 || centralDirectorySize === void 0 || centralDirectoryOffset === void 0) return;
|
|
81
|
+
if (centralDirectoryOffset + centralDirectorySize > bytes.byteLength) return;
|
|
82
|
+
const entries = /* @__PURE__ */ new Map();
|
|
83
|
+
let cursor = centralDirectoryOffset;
|
|
84
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
85
|
+
const centralSignature = readUint32(bytes, cursor);
|
|
86
|
+
const method = readUint16(bytes, cursor + 10);
|
|
87
|
+
const compressedSize = readUint32(bytes, cursor + 20);
|
|
88
|
+
const uncompressedSize = readUint32(bytes, cursor + 24);
|
|
89
|
+
const pathByteLength = readUint16(bytes, cursor + 28);
|
|
90
|
+
const extraByteLength = readUint16(bytes, cursor + 30);
|
|
91
|
+
const commentByteLength = readUint16(bytes, cursor + 32);
|
|
92
|
+
const localHeaderOffset = readUint32(bytes, cursor + 42);
|
|
93
|
+
if (centralSignature !== 33639248 || method === void 0 || compressedSize === void 0 || uncompressedSize === void 0 || pathByteLength === void 0 || extraByteLength === void 0 || commentByteLength === void 0 || localHeaderOffset === void 0) return;
|
|
94
|
+
const pathStart = cursor + 46;
|
|
95
|
+
const pathEnd = pathStart + pathByteLength;
|
|
96
|
+
if (pathEnd > bytes.byteLength) return;
|
|
97
|
+
const entryPath = textDecoder.decode(bytes.subarray(pathStart, pathEnd));
|
|
98
|
+
const localEntry = localZipEntry(bytes, {
|
|
99
|
+
compressedSize,
|
|
100
|
+
localHeaderOffset,
|
|
101
|
+
path: entryPath
|
|
102
|
+
});
|
|
103
|
+
if (!localEntry) return;
|
|
104
|
+
entries.set(entryPath, {
|
|
105
|
+
path: entryPath,
|
|
106
|
+
method,
|
|
107
|
+
compressedSize,
|
|
108
|
+
uncompressedSize,
|
|
109
|
+
localHeaderOffset,
|
|
110
|
+
centralHeaderOffset: cursor,
|
|
111
|
+
dataOffset: localEntry.dataOffset,
|
|
112
|
+
bytes: bytes.subarray(localEntry.dataOffset, localEntry.dataOffset + compressedSize)
|
|
113
|
+
});
|
|
114
|
+
cursor = pathEnd + extraByteLength + commentByteLength;
|
|
115
|
+
}
|
|
116
|
+
return { entries };
|
|
117
|
+
}
|
|
118
|
+
function fingerprintBytes(bytes) {
|
|
119
|
+
let hash = 2166136261;
|
|
120
|
+
for (const byte of bytes) {
|
|
121
|
+
hash ^= byte;
|
|
122
|
+
hash = Math.imul(hash, 16777619);
|
|
123
|
+
}
|
|
124
|
+
return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
125
|
+
}
|
|
126
|
+
function inPlacePatchFailure(code, message, path) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
diagnostics: [{
|
|
130
|
+
code,
|
|
131
|
+
message,
|
|
132
|
+
...path ? { path } : {}
|
|
133
|
+
}]
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function patchManifestLogicalBytes(patchPlan) {
|
|
137
|
+
const manifest = patchManifestFromParts(patchPlan.parts);
|
|
138
|
+
return textEncoder$1.encode(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
139
|
+
}
|
|
140
|
+
function paddedManifestBytes(logical, targetSize) {
|
|
141
|
+
if (logical.byteLength > targetSize) return;
|
|
142
|
+
const bytes = new Uint8Array(targetSize);
|
|
143
|
+
bytes.set(logical, 0);
|
|
144
|
+
bytes.fill(32, logical.byteLength);
|
|
145
|
+
return bytes;
|
|
146
|
+
}
|
|
147
|
+
function paddedXmlBytes(bytes, targetSize) {
|
|
148
|
+
const logical = stripXmlReserve(bytes);
|
|
149
|
+
if (logical.byteLength === targetSize) return logical;
|
|
150
|
+
const prefix = textEncoder$1.encode(`\n<!--${PATCH_RESERVE_MARKER}`);
|
|
151
|
+
const suffix = textEncoder$1.encode("-->");
|
|
152
|
+
if (targetSize - logical.byteLength - prefix.byteLength - suffix.byteLength < 0) return;
|
|
153
|
+
const result = new Uint8Array(targetSize);
|
|
154
|
+
result.set(logical, 0);
|
|
155
|
+
result.set(prefix, logical.byteLength);
|
|
156
|
+
result.fill(46, logical.byteLength + prefix.byteLength, targetSize - suffix.byteLength);
|
|
157
|
+
result.set(suffix, targetSize - suffix.byteLength);
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
function stripXmlReserve(bytes) {
|
|
161
|
+
const text = textDecoder.decode(bytes);
|
|
162
|
+
const markerIndex = text.lastIndexOf(`<!--${PATCH_RESERVE_MARKER}`);
|
|
163
|
+
if (markerIndex < 0) return bytes;
|
|
164
|
+
return textEncoder$1.encode(text.slice(0, markerIndex));
|
|
165
|
+
}
|
|
166
|
+
function zipEntryPatchSegments(entry, bytes) {
|
|
167
|
+
const crc = uint32Bytes(crc32(bytes));
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
position: entry.localHeaderOffset + 14,
|
|
171
|
+
bytes: crc
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
position: entry.centralHeaderOffset + 16,
|
|
175
|
+
bytes: crc
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
position: entry.dataOffset,
|
|
179
|
+
bytes
|
|
180
|
+
}
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
function localZipEntry(bytes, input) {
|
|
184
|
+
if (readUint32(bytes, input.localHeaderOffset) !== 67324752) return;
|
|
185
|
+
const pathByteLength = readUint16(bytes, input.localHeaderOffset + 26);
|
|
186
|
+
const extraByteLength = readUint16(bytes, input.localHeaderOffset + 28);
|
|
187
|
+
if (pathByteLength === void 0 || extraByteLength === void 0) return;
|
|
188
|
+
const dataOffset = input.localHeaderOffset + 30 + pathByteLength + extraByteLength;
|
|
189
|
+
return dataOffset + input.compressedSize <= bytes.byteLength ? { dataOffset } : void 0;
|
|
190
|
+
}
|
|
191
|
+
function findEndOfCentralDirectory(bytes) {
|
|
192
|
+
const minimumOffset = Math.max(0, bytes.byteLength - 65535 - 22);
|
|
193
|
+
for (let offset = bytes.byteLength - 22; offset >= minimumOffset; offset -= 1) if (readUint32(bytes, offset) === 101010256) return offset;
|
|
194
|
+
return -1;
|
|
195
|
+
}
|
|
196
|
+
function readUint16(bytes, offset) {
|
|
197
|
+
if (offset < 0 || offset + 2 > bytes.byteLength) return;
|
|
198
|
+
return bytes[offset] | bytes[offset + 1] << 8;
|
|
199
|
+
}
|
|
200
|
+
function readUint32(bytes, offset) {
|
|
201
|
+
if (offset < 0 || offset + 4 > bytes.byteLength) return;
|
|
202
|
+
return (bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24) >>> 0;
|
|
203
|
+
}
|
|
204
|
+
function uint32Bytes(value) {
|
|
205
|
+
return new Uint8Array([
|
|
206
|
+
value & 255,
|
|
207
|
+
value >>> 8 & 255,
|
|
208
|
+
value >>> 16 & 255,
|
|
209
|
+
value >>> 24 & 255
|
|
210
|
+
]);
|
|
211
|
+
}
|
|
212
|
+
function createCrc32Table() {
|
|
213
|
+
const table = new Uint32Array(256);
|
|
214
|
+
for (let index = 0; index < table.length; index += 1) {
|
|
215
|
+
let value = index;
|
|
216
|
+
for (let bit = 0; bit < 8; bit += 1) value = value & 1 ? 3988292384 ^ value >>> 1 : value >>> 1;
|
|
217
|
+
table[index] = value >>> 0;
|
|
218
|
+
}
|
|
219
|
+
return table;
|
|
220
|
+
}
|
|
221
|
+
function crc32(bytes) {
|
|
222
|
+
let crc = 4294967295;
|
|
223
|
+
for (const byte of bytes) crc = CRC32_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
|
|
224
|
+
return (crc ^ 4294967295) >>> 0;
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/index.ts
|
|
228
|
+
const textEncoder = new TextEncoder();
|
|
229
|
+
function createNodeFileAssetLoader(options = {}) {
|
|
230
|
+
const normalizedOptions = {
|
|
231
|
+
...options,
|
|
232
|
+
root: options.root ? path.resolve(options.root) : void 0
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
resolverIdentity: options.resolverIdentity ?? nodeFileAssetResolverIdentity(normalizedOptions),
|
|
236
|
+
async probe(context) {
|
|
237
|
+
const filePath = resolveNodeFileAssetPath({
|
|
238
|
+
options: normalizedOptions,
|
|
239
|
+
source: context.source,
|
|
240
|
+
importer: context.origin?.importer
|
|
241
|
+
});
|
|
242
|
+
if (!filePath.ok) return nodeFileAssetOriginMissing({
|
|
243
|
+
phase: "probe",
|
|
244
|
+
source: context.source
|
|
245
|
+
});
|
|
246
|
+
if (!filePath.path) return;
|
|
247
|
+
try {
|
|
248
|
+
return {
|
|
249
|
+
ok: true,
|
|
250
|
+
value: await probeFileAsset(filePath.path)
|
|
251
|
+
};
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return nodeFileAssetReadFailure({
|
|
254
|
+
phase: "probe",
|
|
255
|
+
filePath: filePath.path,
|
|
256
|
+
source: context.source,
|
|
257
|
+
error
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
async load(context) {
|
|
262
|
+
const filePath = resolveNodeFileAssetPath({
|
|
263
|
+
options: normalizedOptions,
|
|
264
|
+
source: context.source,
|
|
265
|
+
importer: context.origin?.importer
|
|
266
|
+
});
|
|
267
|
+
if (!filePath.ok) return nodeFileAssetOriginMissing({
|
|
268
|
+
phase: "load",
|
|
269
|
+
source: context.source
|
|
270
|
+
});
|
|
271
|
+
if (!filePath.path) return;
|
|
272
|
+
try {
|
|
273
|
+
return {
|
|
274
|
+
ok: true,
|
|
275
|
+
value: await loadFileAsset(filePath.path)
|
|
276
|
+
};
|
|
277
|
+
} catch (error) {
|
|
278
|
+
return nodeFileAssetReadFailure({
|
|
279
|
+
phase: "load",
|
|
280
|
+
filePath: filePath.path,
|
|
281
|
+
source: context.source,
|
|
282
|
+
error
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function nodeAssets(options = {}) {
|
|
289
|
+
return {
|
|
290
|
+
kind: "deckjsx.plugin",
|
|
291
|
+
id: "@deckjsx/node/assets",
|
|
292
|
+
name: "@deckjsx/node/assets",
|
|
293
|
+
integration: {
|
|
294
|
+
id: integrationContextId("@deckjsx/node/assets"),
|
|
295
|
+
assetLoaders: [createNodeFileAssetLoader(options)]
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async function inspectPatchablePptx(outputPath) {
|
|
300
|
+
try {
|
|
301
|
+
return inspectPatchablePptxBytes(new Uint8Array(await readFile(outputPath)), outputPath);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
return {
|
|
304
|
+
path: outputPath,
|
|
305
|
+
ok: false,
|
|
306
|
+
patchable: false,
|
|
307
|
+
manifestPath: PATCH_MANIFEST_PATH,
|
|
308
|
+
partCount: 0,
|
|
309
|
+
parts: [],
|
|
310
|
+
diagnostics: [{
|
|
311
|
+
code: "deckjsx.node.inspect.readFailed",
|
|
312
|
+
message: error instanceof Error ? error.message : String(error),
|
|
313
|
+
path: outputPath
|
|
314
|
+
}]
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async function write(render, outputPath) {
|
|
319
|
+
if (!render.ok) {
|
|
320
|
+
const renderDiagnosticCodes = render.diagnostics.items.map((item) => item.code);
|
|
321
|
+
return {
|
|
322
|
+
path: outputPath,
|
|
323
|
+
status: "failed",
|
|
324
|
+
strategy: "write-file",
|
|
325
|
+
bytesWritten: 0,
|
|
326
|
+
patchedParts: [],
|
|
327
|
+
diagnostics: [{
|
|
328
|
+
code: "deckjsx.node.write.renderFailed",
|
|
329
|
+
message: renderDiagnosticCodes.length ? `@deckjsx/node write() requires a successful render result. Render diagnostics: ${renderDiagnosticCodes.join(", ")}.` : "@deckjsx/node write() requires a successful render result.",
|
|
330
|
+
path: outputPath
|
|
331
|
+
}]
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const artifact = render.artifact;
|
|
335
|
+
if (!artifact) return {
|
|
336
|
+
path: outputPath,
|
|
337
|
+
status: "failed",
|
|
338
|
+
strategy: "write-file",
|
|
339
|
+
bytesWritten: 0,
|
|
340
|
+
patchedParts: [],
|
|
341
|
+
diagnostics: [{
|
|
342
|
+
code: "deckjsx.node.write.missingArtifact",
|
|
343
|
+
message: "@deckjsx/node write() requires a render result with an artifact.",
|
|
344
|
+
path: outputPath
|
|
345
|
+
}]
|
|
346
|
+
};
|
|
347
|
+
if (artifact.format !== "pptx") return {
|
|
348
|
+
path: outputPath,
|
|
349
|
+
status: "failed",
|
|
350
|
+
strategy: "write-file",
|
|
351
|
+
bytesWritten: 0,
|
|
352
|
+
patchedParts: [],
|
|
353
|
+
diagnostics: [{
|
|
354
|
+
code: "deckjsx.node.write.unsupportedFormat",
|
|
355
|
+
message: `@deckjsx/node write() can only write pptx artifacts, got ${artifact.format}.`,
|
|
356
|
+
path: outputPath
|
|
357
|
+
}]
|
|
358
|
+
};
|
|
359
|
+
const lock = await acquireWriteLock(outputPath);
|
|
360
|
+
if (!lock.ok) return {
|
|
361
|
+
path: outputPath,
|
|
362
|
+
status: "failed",
|
|
363
|
+
strategy: "write-file",
|
|
364
|
+
bytesWritten: 0,
|
|
365
|
+
patchedParts: [],
|
|
366
|
+
diagnostics: lock.diagnostics
|
|
367
|
+
};
|
|
368
|
+
try {
|
|
369
|
+
if (!await pathExists(outputPath)) {
|
|
370
|
+
await writeFile(outputPath, artifact.bytes);
|
|
371
|
+
return {
|
|
372
|
+
path: outputPath,
|
|
373
|
+
status: "created",
|
|
374
|
+
strategy: "write-file",
|
|
375
|
+
bytesWritten: artifact.bytes.byteLength,
|
|
376
|
+
patchedParts: [],
|
|
377
|
+
diagnostics: []
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const patch = render.patchPlan ? tryCreateInPlacePatch(await readFile(outputPath), artifact.bytes, render.patchPlan) : void 0;
|
|
381
|
+
if (patch?.ok) {
|
|
382
|
+
await writePatchSegments(outputPath, patch.segments);
|
|
383
|
+
return {
|
|
384
|
+
path: outputPath,
|
|
385
|
+
status: "patched",
|
|
386
|
+
strategy: "in-place",
|
|
387
|
+
bytesWritten: patch.segments.reduce((total, segment) => total + segment.bytes.byteLength, 0),
|
|
388
|
+
patchedParts: patch.patchedParts,
|
|
389
|
+
diagnostics: []
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
await replaceFile(outputPath, artifact.bytes);
|
|
393
|
+
return {
|
|
394
|
+
path: outputPath,
|
|
395
|
+
status: "replaced",
|
|
396
|
+
strategy: "atomic-replace",
|
|
397
|
+
bytesWritten: artifact.bytes.byteLength,
|
|
398
|
+
patchedParts: [],
|
|
399
|
+
diagnostics: patch?.diagnostics ?? []
|
|
400
|
+
};
|
|
401
|
+
} catch (error) {
|
|
402
|
+
return {
|
|
403
|
+
path: outputPath,
|
|
404
|
+
status: "failed",
|
|
405
|
+
strategy: "write-file",
|
|
406
|
+
bytesWritten: 0,
|
|
407
|
+
patchedParts: [],
|
|
408
|
+
diagnostics: [{
|
|
409
|
+
code: "deckjsx.node.write.failed",
|
|
410
|
+
message: error instanceof Error ? error.message : String(error),
|
|
411
|
+
path: outputPath
|
|
412
|
+
}]
|
|
413
|
+
};
|
|
414
|
+
} finally {
|
|
415
|
+
await releaseWriteLock(lock.lock);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function inspectPatchablePptxBytes(bytes, outputPath) {
|
|
419
|
+
const archive = parseZipArchive(bytes);
|
|
420
|
+
if (!archive) return {
|
|
421
|
+
path: outputPath,
|
|
422
|
+
ok: false,
|
|
423
|
+
patchable: false,
|
|
424
|
+
manifestPath: PATCH_MANIFEST_PATH,
|
|
425
|
+
partCount: 0,
|
|
426
|
+
parts: [],
|
|
427
|
+
diagnostics: [{
|
|
428
|
+
code: "deckjsx.node.inspect.unreadableZip",
|
|
429
|
+
message: "Existing PPTX bytes could not be read as a ZIP archive.",
|
|
430
|
+
path: outputPath
|
|
431
|
+
}]
|
|
432
|
+
};
|
|
433
|
+
const manifestEntry = archive.entries.get(PATCH_MANIFEST_PATH);
|
|
434
|
+
if (!manifestEntry) return {
|
|
435
|
+
path: outputPath,
|
|
436
|
+
ok: false,
|
|
437
|
+
patchable: false,
|
|
438
|
+
manifestPath: PATCH_MANIFEST_PATH,
|
|
439
|
+
partCount: 0,
|
|
440
|
+
parts: [],
|
|
441
|
+
diagnostics: [{
|
|
442
|
+
code: "deckjsx.node.inspect.missingPatchManifest",
|
|
443
|
+
message: "Existing PPTX does not contain a deckjsx patch manifest.",
|
|
444
|
+
path: PATCH_MANIFEST_PATH
|
|
445
|
+
}]
|
|
446
|
+
};
|
|
447
|
+
const manifest = parsePatchManifest(manifestEntry.bytes);
|
|
448
|
+
if (!manifest) return {
|
|
449
|
+
path: outputPath,
|
|
450
|
+
ok: false,
|
|
451
|
+
patchable: false,
|
|
452
|
+
manifestPath: PATCH_MANIFEST_PATH,
|
|
453
|
+
partCount: 0,
|
|
454
|
+
parts: [],
|
|
455
|
+
diagnostics: [{
|
|
456
|
+
code: "deckjsx.node.inspect.invalidPatchManifest",
|
|
457
|
+
message: "Existing PPTX deckjsx patch manifest could not be parsed.",
|
|
458
|
+
path: PATCH_MANIFEST_PATH
|
|
459
|
+
}]
|
|
460
|
+
};
|
|
461
|
+
const diagnostics = [];
|
|
462
|
+
const parts = manifest.parts.map((part) => {
|
|
463
|
+
const entry = archive.entries.get(part.path);
|
|
464
|
+
if (!entry) {
|
|
465
|
+
diagnostics.push({
|
|
466
|
+
code: "deckjsx.node.inspect.missingPatchPart",
|
|
467
|
+
message: "Patch manifest references a package part that is missing from the PPTX archive.",
|
|
468
|
+
path: part.path
|
|
469
|
+
});
|
|
470
|
+
return {
|
|
471
|
+
...part,
|
|
472
|
+
status: "missing"
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (entry.method !== 0) {
|
|
476
|
+
diagnostics.push({
|
|
477
|
+
code: "deckjsx.node.inspect.unsupportedZipEntry",
|
|
478
|
+
message: "Patch manifest references a ZIP entry that cannot be updated in place.",
|
|
479
|
+
path: part.path
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
...part,
|
|
483
|
+
status: "unsupported",
|
|
484
|
+
zipMethod: entry.method
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const currentFingerprint = fingerprintPatchableEntry(part, entry.bytes);
|
|
488
|
+
if (currentFingerprint !== part.fingerprint) {
|
|
489
|
+
diagnostics.push({
|
|
490
|
+
code: "deckjsx.node.inspect.patchManifestStale",
|
|
491
|
+
message: "Existing PPTX package part bytes do not match the deckjsx patch manifest.",
|
|
492
|
+
path: part.path
|
|
493
|
+
});
|
|
494
|
+
return {
|
|
495
|
+
...part,
|
|
496
|
+
status: "stale",
|
|
497
|
+
currentFingerprint,
|
|
498
|
+
zipMethod: entry.method
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
...part,
|
|
503
|
+
status: "verified",
|
|
504
|
+
currentFingerprint,
|
|
505
|
+
zipMethod: entry.method
|
|
506
|
+
};
|
|
507
|
+
});
|
|
508
|
+
return {
|
|
509
|
+
path: outputPath,
|
|
510
|
+
ok: diagnostics.length === 0,
|
|
511
|
+
patchable: diagnostics.length === 0,
|
|
512
|
+
manifestPath: PATCH_MANIFEST_PATH,
|
|
513
|
+
partCount: parts.length,
|
|
514
|
+
parts,
|
|
515
|
+
diagnostics
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function lockPathFor(outputPath) {
|
|
519
|
+
return path.join(path.dirname(outputPath), `.${path.basename(outputPath)}.deckjsx-lock`);
|
|
520
|
+
}
|
|
521
|
+
async function acquireWriteLock(outputPath) {
|
|
522
|
+
const lockPath = lockPathFor(outputPath);
|
|
523
|
+
let handle;
|
|
524
|
+
try {
|
|
525
|
+
handle = await open(lockPath, "wx");
|
|
526
|
+
} catch (error) {
|
|
527
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : "";
|
|
528
|
+
return {
|
|
529
|
+
ok: false,
|
|
530
|
+
diagnostics: [{
|
|
531
|
+
code: code === "EEXIST" ? "deckjsx.node.write.lockUnavailable" : "deckjsx.node.write.lockFailed",
|
|
532
|
+
message: code === "EEXIST" ? "Another deckjsx write appears to hold the output lock." : error instanceof Error ? error.message : String(error),
|
|
533
|
+
path: lockPath
|
|
534
|
+
}]
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
await handle.writeFile(`${process.pid}\n`);
|
|
539
|
+
} catch {
|
|
540
|
+
await handle.close();
|
|
541
|
+
await unlink(lockPath).catch(() => void 0);
|
|
542
|
+
return {
|
|
543
|
+
ok: false,
|
|
544
|
+
diagnostics: [{
|
|
545
|
+
code: "deckjsx.node.write.lockFailed",
|
|
546
|
+
message: "Deckjsx could not initialize the output lock file.",
|
|
547
|
+
path: lockPath
|
|
548
|
+
}]
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
ok: true,
|
|
553
|
+
lock: {
|
|
554
|
+
path: lockPath,
|
|
555
|
+
handle
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
async function releaseWriteLock(lock) {
|
|
560
|
+
await lock.handle.close().catch(() => void 0);
|
|
561
|
+
await unlink(lock.path).catch(() => void 0);
|
|
562
|
+
}
|
|
563
|
+
async function pathExists(outputPath) {
|
|
564
|
+
try {
|
|
565
|
+
await access(outputPath, constants.F_OK);
|
|
566
|
+
return true;
|
|
567
|
+
} catch {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async function replaceFile(outputPath, bytes) {
|
|
572
|
+
const temporaryPath = path.join(path.dirname(outputPath), `.${path.basename(outputPath)}.${process.pid}.${Date.now()}.deckjsx-tmp`);
|
|
573
|
+
await writeFile(temporaryPath, bytes);
|
|
574
|
+
await rename(temporaryPath, outputPath);
|
|
575
|
+
}
|
|
576
|
+
function extensionFromPath(filePath) {
|
|
577
|
+
return path.extname(filePath).slice(1).toLowerCase() || void 0;
|
|
578
|
+
}
|
|
579
|
+
function mediaTypeFromExtension(extension) {
|
|
580
|
+
switch (extension) {
|
|
581
|
+
case "gif": return "image/gif";
|
|
582
|
+
case "jpg":
|
|
583
|
+
case "jpeg": return "image/jpeg";
|
|
584
|
+
case "png": return "image/png";
|
|
585
|
+
case "svg": return "image/svg+xml";
|
|
586
|
+
case "webp": return "image/webp";
|
|
587
|
+
default: return;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function pngDimensions(bytes) {
|
|
591
|
+
if (bytes.byteLength < 24 || bytes[0] !== 137 || bytes[1] !== 80 || bytes[2] !== 78 || bytes[3] !== 71) return {};
|
|
592
|
+
return {
|
|
593
|
+
width: (bytes[16] << 24 | bytes[17] << 16 | bytes[18] << 8 | bytes[19]) >>> 0,
|
|
594
|
+
height: (bytes[20] << 24 | bytes[21] << 16 | bytes[22] << 8 | bytes[23]) >>> 0
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function nodeFileAssetResolverIdentity(options) {
|
|
598
|
+
const config = JSON.stringify({
|
|
599
|
+
package: "@deckjsx/node",
|
|
600
|
+
root: options.root ?? process.cwd()
|
|
601
|
+
});
|
|
602
|
+
return `@deckjsx/node:file:${fingerprintBytes(textEncoder.encode(config))}`;
|
|
603
|
+
}
|
|
604
|
+
function resolveNodeFileAssetPath(input) {
|
|
605
|
+
if (input.source.kind !== "path") return { ok: true };
|
|
606
|
+
if (path.isAbsolute(input.source.path)) return {
|
|
607
|
+
ok: true,
|
|
608
|
+
path: input.source.path
|
|
609
|
+
};
|
|
610
|
+
if (input.importer) return {
|
|
611
|
+
ok: true,
|
|
612
|
+
path: path.resolve(path.dirname(input.importer), input.source.path)
|
|
613
|
+
};
|
|
614
|
+
if (input.options.root) return {
|
|
615
|
+
ok: true,
|
|
616
|
+
path: path.resolve(input.options.root, input.source.path)
|
|
617
|
+
};
|
|
618
|
+
return { ok: false };
|
|
619
|
+
}
|
|
620
|
+
async function probeFileAsset(filePath) {
|
|
621
|
+
const [metadata, bytes] = await Promise.all([stat(filePath), readFile(filePath)]);
|
|
622
|
+
const extension = extensionFromPath(filePath);
|
|
623
|
+
const mediaType = mediaTypeFromExtension(extension);
|
|
624
|
+
const dimensions = mediaType === "image/png" ? pngDimensions(bytes) : {};
|
|
625
|
+
return {
|
|
626
|
+
...mediaType ? { mediaType } : {},
|
|
627
|
+
...extension ? { extension } : {},
|
|
628
|
+
...dimensions.width ? { width: dimensions.width } : {},
|
|
629
|
+
...dimensions.height ? { height: dimensions.height } : {},
|
|
630
|
+
byteLength: metadata.size,
|
|
631
|
+
hash: fingerprintBytes(bytes),
|
|
632
|
+
provenance: {
|
|
633
|
+
kind: "file",
|
|
634
|
+
resolvedId: fingerprintBytes(bytes),
|
|
635
|
+
hashSource: "bytes"
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
async function loadFileAsset(filePath) {
|
|
640
|
+
const buffer = await readFile(filePath);
|
|
641
|
+
const bytes = new Uint8Array(buffer);
|
|
642
|
+
return {
|
|
643
|
+
...await probeFileAsset(filePath),
|
|
644
|
+
bytes
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function errorMessage(error) {
|
|
648
|
+
return error instanceof Error ? error.message : String(error);
|
|
649
|
+
}
|
|
650
|
+
function authoredAssetSource(source) {
|
|
651
|
+
return source.kind === "path" ? source.path : JSON.stringify(source);
|
|
652
|
+
}
|
|
653
|
+
function nodeFileAssetReadFailure(input) {
|
|
654
|
+
return {
|
|
655
|
+
ok: false,
|
|
656
|
+
diagnostics: [{
|
|
657
|
+
severity: "error",
|
|
658
|
+
code: "E_NODE_FILE_ASSET_READ_FAILED",
|
|
659
|
+
title: "node file asset could not be read",
|
|
660
|
+
message: "@deckjsx/node resolved this media source to a local file, but could not read its bytes.",
|
|
661
|
+
labels: [{
|
|
662
|
+
path: input.filePath,
|
|
663
|
+
message: errorMessage(input.error)
|
|
664
|
+
}],
|
|
665
|
+
notes: [`phase=${input.phase}`, `source=${authoredAssetSource(input.source)}`],
|
|
666
|
+
help: ["Check that the media path exists relative to the importing slide/component module or configured node asset root."]
|
|
667
|
+
}]
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function nodeFileAssetOriginMissing(input) {
|
|
671
|
+
const source = authoredAssetSource(input.source);
|
|
672
|
+
return {
|
|
673
|
+
ok: false,
|
|
674
|
+
diagnostics: [{
|
|
675
|
+
severity: "error",
|
|
676
|
+
code: "E_NODE_FILE_ASSET_ORIGIN_MISSING",
|
|
677
|
+
title: "node file asset importer origin is missing",
|
|
678
|
+
message: "@deckjsx/node received a relative media path but no importing module path or asset root was available.",
|
|
679
|
+
labels: [{
|
|
680
|
+
path: source,
|
|
681
|
+
message: "relative node asset paths require an importer origin or createNodeFileAssetLoader root"
|
|
682
|
+
}],
|
|
683
|
+
notes: [`phase=${input.phase}`, `source=${source}`],
|
|
684
|
+
help: ["Pass mediaSourceOrigins metadata from JSX transforms or configure createNodeFileAssetLoader({ root })."]
|
|
685
|
+
}]
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
async function writePatchSegments(outputPath, segments) {
|
|
689
|
+
const file = await open(outputPath, "r+");
|
|
690
|
+
try {
|
|
691
|
+
for (const segment of segments) await file.write(segment.bytes, 0, segment.bytes.byteLength, segment.position);
|
|
692
|
+
await file.sync();
|
|
693
|
+
} finally {
|
|
694
|
+
await file.close();
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
//#endregion
|
|
698
|
+
export { createNodeFileAssetLoader, inspectPatchablePptx, nodeAssets, write };
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deckjsx/node",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node.js runtime helpers for deckjsx render artifacts.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"types": "./dist/index.d.mts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
|
+
"import": "./dist/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "vp pack",
|
|
23
|
+
"dev": "vp pack --watch",
|
|
24
|
+
"test": "vp test",
|
|
25
|
+
"check": "vp check",
|
|
26
|
+
"prepublishOnly": "vp run build"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^25.9.3",
|
|
30
|
+
"vite-plus": "^0.1.24"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"deckjsx": "^0.9.0"
|
|
34
|
+
},
|
|
35
|
+
"packageManager": "bun@1.3.14"
|
|
36
|
+
}
|