@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.
@@ -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
+ }