From 77199047fc52765530d38c291d76e8ec3e37d9a1 Mon Sep 17 00:00:00 2001 From: Zack Hoherchak Date: Fri, 9 Jan 2026 17:03:43 -0500 Subject: [PATCH 1/7] preserve manifest line endings --- src/commands/pack.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/commands/pack.ts b/src/commands/pack.ts index 66fb92a..62bf120 100644 --- a/src/commands/pack.ts +++ b/src/commands/pack.ts @@ -192,10 +192,21 @@ async function version(path: string, version: string | null): Promise; + // Detect the original line ending style (CRLF or LF) + const lineEnding = original.includes("\r\n") ? "\r\n" : "\n"; + // Ensure the version in the manifest has the correct number of segments, [{major}.{minor}.{patch}.{build}] version ??= manifest.Version?.toString() || ""; manifest.Version = `${version}${".0".repeat(Math.max(0, 4 - version.split(".").length))}`; - write(JSON.stringify(manifest, undefined, "\t")); + + let stringified = JSON.stringify(manifest, undefined, "\t"); + + // Preserve original line endings + if (lineEnding === "\r\n") { + stringified = stringified.replaceAll("\n", "\r\n"); + } + + write(stringified); } return { From 59c1820b46b88b92d09281c8f0dc2a92700a3283 Mon Sep 17 00:00:00 2001 From: Zack Hoherchak Date: Fri, 9 Jan 2026 17:31:28 -0500 Subject: [PATCH 2/7] preserve indentation style for manifest --- src/commands/pack.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/pack.ts b/src/commands/pack.ts index 62bf120..6df1f61 100644 --- a/src/commands/pack.ts +++ b/src/commands/pack.ts @@ -195,11 +195,15 @@ async function version(path: string, version: string | null): Promise Date: Tue, 13 Jan 2026 10:58:14 -0500 Subject: [PATCH 3/7] address pr comments --- src/commands/pack.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commands/pack.ts b/src/commands/pack.ts index 6df1f61..68230d1 100644 --- a/src/commands/pack.ts +++ b/src/commands/pack.ts @@ -197,8 +197,7 @@ async function version(path: string, version: string | null): Promise Date: Wed, 14 Jan 2026 15:25:24 -0500 Subject: [PATCH 4/7] retain manifest formatting on debug removal --- src/commands/pack.ts | 47 ++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/commands/pack.ts b/src/commands/pack.ts index 68230d1..7304dfb 100644 --- a/src/commands/pack.ts +++ b/src/commands/pack.ts @@ -10,7 +10,7 @@ import type { ReadableStream } from "node:stream/web"; import { command } from "../common/command"; import { StdoutError } from "../common/stdout"; import { getPluginId } from "../stream-deck"; -import { type FileInfo, getFiles, mkdirIfNotExists, readJsonFile, sizeAsString } from "../system/fs"; +import { type FileInfo, getFiles, mkdirIfNotExists, sizeAsString } from "../system/fs"; import { defaultOptions, validate, type ValidateOptions } from "./validate"; /** @@ -142,7 +142,9 @@ async function getPackageContents( fileFn?: (file: FileInfo, stream?: ReadableStream) => Promise | void, ): Promise { // Get the manifest, and generate the base contents. - const manifest = await readJsonFile(join(path, "manifest.json")); + const manifestPath = join(path, "manifest.json"); + const originalManifestContent = await readFile(manifestPath, { encoding: "utf-8" }); + const manifest = JSON.parse(originalManifestContent) as Manifest; const contents: PackageInfo = { files: [], manifest, @@ -159,7 +161,7 @@ async function getPackageContents( // When the entry is the manifest, remove the `Nodejs.Debug` flag. if (file.path.relative === "manifest.json") { delete manifest.Nodejs?.Debug; - const sanitizedManifest = JSON.stringify(manifest, undefined, "".repeat(4)); + const sanitizedManifest = stringifyManifest(manifest, originalManifestContent); const stream = new Readable(); stream.push(sanitizedManifest, "utf-8"); @@ -177,6 +179,30 @@ async function getPackageContents( return contents; } +/** + * Stringifies a manifest object while preserving the original formatting (line endings and indentation). + * @param manifest The manifest object to stringify. + * @param originalContent The original file content to detect formatting from. + * @returns The stringified manifest with preserved formatting. + */ +function stringifyManifest(manifest: Partial, originalContent: string): string { + // Detect the original line ending style (CRLF or LF) + const lineEnding = originalContent.includes("\r\n") ? "\r\n" : "\n"; + + // Detect the original indentation style (tabs or spaces) + const indentMatch = originalContent.match(/^[\t ]+/m); + const indent = indentMatch?.[0] ?? "\t"; + + let stringified = JSON.stringify(manifest, undefined, indent); + + // Preserve original line endings + if (lineEnding === "\r\n") { + stringified = stringified.replace(/(?; - // Detect the original line ending style (CRLF or LF) - const lineEnding = original.includes("\r\n") ? "\r\n" : "\n"; - - // Detect the original indentation style (tabs or spaces) - const indentMatch = original.match(/^[\t ]+/m); - const indent = indentMatch?.[0] ?? "\t"; // Ensure the version in the manifest has the correct number of segments, [{major}.{minor}.{patch}.{build}] version ??= manifest.Version?.toString() || ""; manifest.Version = `${version}${".0".repeat(Math.max(0, 4 - version.split(".").length))}`; - let stringified = JSON.stringify(manifest, undefined, indent); - - // Preserve original line endings - if (lineEnding === "\r\n") { - stringified = stringified.replace(/(? Date: Thu, 15 Jan 2026 19:26:54 -0500 Subject: [PATCH 5/7] add smart JsonFile --- src/commands/pack.ts | 46 ++++++++++---------------------------------- src/system/fs.ts | 38 ++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/src/commands/pack.ts b/src/commands/pack.ts index 7304dfb..10961dc 100644 --- a/src/commands/pack.ts +++ b/src/commands/pack.ts @@ -2,7 +2,7 @@ import { Manifest } from "@elgato/schemas/streamdeck/plugins"; import { ZipWriter } from "@zip.js/zip.js"; import chalk from "chalk"; import { createReadStream, createWriteStream, existsSync, writeFileSync } from "node:fs"; -import { readFile, rm } from "node:fs/promises"; +import { rm } from "node:fs/promises"; import { basename, dirname, join, resolve } from "node:path"; import { Readable, Writable } from "node:stream"; import type { ReadableStream } from "node:stream/web"; @@ -10,7 +10,7 @@ import type { ReadableStream } from "node:stream/web"; import { command } from "../common/command"; import { StdoutError } from "../common/stdout"; import { getPluginId } from "../stream-deck"; -import { type FileInfo, getFiles, mkdirIfNotExists, sizeAsString } from "../system/fs"; +import { type FileInfo, getFiles, mkdirIfNotExists, readJsonFile, sizeAsString } from "../system/fs"; import { defaultOptions, validate, type ValidateOptions } from "./validate"; /** @@ -143,11 +143,10 @@ async function getPackageContents( ): Promise { // Get the manifest, and generate the base contents. const manifestPath = join(path, "manifest.json"); - const originalManifestContent = await readFile(manifestPath, { encoding: "utf-8" }); - const manifest = JSON.parse(originalManifestContent) as Manifest; + const manifest = await readJsonFile(manifestPath); const contents: PackageInfo = { files: [], - manifest, + manifest: manifest.value, size: 0, sizePad: 0, }; @@ -160,8 +159,8 @@ async function getPackageContents( if (fileFn) { // When the entry is the manifest, remove the `Nodejs.Debug` flag. if (file.path.relative === "manifest.json") { - delete manifest.Nodejs?.Debug; - const sanitizedManifest = stringifyManifest(manifest, originalManifestContent); + delete manifest.value.Nodejs?.Debug; + const sanitizedManifest = manifest.stringify(); const stream = new Readable(); stream.push(sanitizedManifest, "utf-8"); @@ -179,30 +178,6 @@ async function getPackageContents( return contents; } -/** - * Stringifies a manifest object while preserving the original formatting (line endings and indentation). - * @param manifest The manifest object to stringify. - * @param originalContent The original file content to detect formatting from. - * @returns The stringified manifest with preserved formatting. - */ -function stringifyManifest(manifest: Partial, originalContent: string): string { - // Detect the original line ending style (CRLF or LF) - const lineEnding = originalContent.includes("\r\n") ? "\r\n" : "\n"; - - // Detect the original indentation style (tabs or spaces) - const indentMatch = originalContent.match(/^[\t ]+/m); - const indent = indentMatch?.[0] ?? "\t"; - - let stringified = JSON.stringify(manifest, undefined, indent); - - // Preserve original line endings - if (lineEnding === "\r\n") { - stringified = stringified.replace(/(?; + const manifest = await readJsonFile(manifestPath); // Ensure the version in the manifest has the correct number of segments, [{major}.{minor}.{patch}.{build}] - version ??= manifest.Version?.toString() || ""; - manifest.Version = `${version}${".0".repeat(Math.max(0, 4 - version.split(".").length))}`; + version ??= manifest.value.Version?.toString() || ""; + manifest.value.Version = `${version}${".0".repeat(Math.max(0, 4 - version.split(".").length))}`; - write(stringifyManifest(manifest, original)); + write(manifest.stringify()); } return { diff --git a/src/system/fs.ts b/src/system/fs.ts index c6a6162..7c09319 100644 --- a/src/system/fs.ts +++ b/src/system/fs.ts @@ -165,19 +165,53 @@ export async function mkdirIfNotExists(path: string): Promise { * @param path Path to the JSON file. * @returns Contents parsed as JSON. */ -export async function readJsonFile(path: string): Promise { +export async function readJsonFile(path: string): Promise> { if (!existsSync(path)) { throw new Error(`JSON file not found, ${path}`); } try { const contents = await readFile(path, { encoding: "utf-8" }); - return JSON.parse(contents); + let _value = JSON.parse(contents) as T; + + return { + get value(): T { + return _value; + }, + set value(v: T) { + _value = v; + }, + stringify(): string { + // Detect the original line ending style (CRLF or LF) + const lineEnding = contents.includes("\r\n") ? "\r\n" : "\n"; + + // Detect the original indentation style (tabs or spaces) + const indentMatch = contents.match(/^[\t ]+/m); + const indent = indentMatch?.[0] ?? "\t"; + + let stringified = JSON.stringify(_value, undefined, indent); + + // Preserve original line endings + if (lineEnding === "\r\n") { + stringified = stringified.replace(/(? = { + value: T; + stringify(): string; +}; + /** * Defines how a path will be relocated. */ From adff7e38d4c30157c0b074eb9f5c3c6ea5ed4ada Mon Sep 17 00:00:00 2001 From: Zack Hoherchak Date: Thu, 15 Jan 2026 19:30:53 -0500 Subject: [PATCH 6/7] add jsdocs --- src/system/fs.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/system/fs.ts b/src/system/fs.ts index 7c09319..d062c94 100644 --- a/src/system/fs.ts +++ b/src/system/fs.ts @@ -208,7 +208,14 @@ export async function readJsonFile(path: string): Promise> { * A JSON file with its parsed value and a method to stringify it while preserving formatting. */ export type JsonFile = { + /** + * The parsed value of the JSON file. + */ value: T; + /** + * Stringifies the JSON value while preserving original formatting. + * @returns The stringified JSON. + */ stringify(): string; }; From b2c1d3bbd5e6b9b839c9b1a7dada5b69bccc1e0e Mon Sep 17 00:00:00 2001 From: Zack Hoherchak Date: Fri, 16 Jan 2026 10:16:26 -0500 Subject: [PATCH 7/7] JsonFile value readonly --- src/system/fs.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/system/fs.ts b/src/system/fs.ts index d062c94..c631f24 100644 --- a/src/system/fs.ts +++ b/src/system/fs.ts @@ -172,15 +172,10 @@ export async function readJsonFile(path: string): Promise> { try { const contents = await readFile(path, { encoding: "utf-8" }); - let _value = JSON.parse(contents) as T; + const value = JSON.parse(contents) as T; return { - get value(): T { - return _value; - }, - set value(v: T) { - _value = v; - }, + value, stringify(): string { // Detect the original line ending style (CRLF or LF) const lineEnding = contents.includes("\r\n") ? "\r\n" : "\n"; @@ -189,7 +184,7 @@ export async function readJsonFile(path: string): Promise> { const indentMatch = contents.match(/^[\t ]+/m); const indent = indentMatch?.[0] ?? "\t"; - let stringified = JSON.stringify(_value, undefined, indent); + let stringified = JSON.stringify(value, undefined, indent); // Preserve original line endings if (lineEnding === "\r\n") { @@ -211,7 +206,7 @@ export type JsonFile = { /** * The parsed value of the JSON file. */ - value: T; + readonly value: T; /** * Stringifies the JSON value while preserving original formatting. * @returns The stringified JSON.