Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions src/commands/pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -142,10 +142,11 @@ async function getPackageContents(
fileFn?: (file: FileInfo, stream?: ReadableStream) => Promise<void> | void,
): Promise<PackageInfo> {
// Get the manifest, and generate the base contents.
const manifest = await readJsonFile<Manifest>(join(path, "manifest.json"));
const manifestPath = join(path, "manifest.json");
const manifest = await readJsonFile<Manifest>(manifestPath);
const contents: PackageInfo = {
files: [],
manifest,
manifest: manifest.value,
size: 0,
sizePad: 0,
};
Expand All @@ -158,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 = JSON.stringify(manifest, undefined, "".repeat(4));
delete manifest.value.Nodejs?.Debug;
const sanitizedManifest = manifest.stringify();

const stream = new Readable();
stream.push(sanitizedManifest, "utf-8");
Expand Down Expand Up @@ -189,13 +190,13 @@ async function version(path: string, version: string | null): Promise<VersionRev
let original: string | undefined;

if (existsSync(manifestPath)) {
original = await readFile(manifestPath, { encoding: "utf-8" });
const manifest = JSON.parse(original) as Partial<Manifest>;
const manifest = await readJsonFile<Manifest>(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))}`;
write(JSON.stringify(manifest, undefined, "\t"));
version ??= manifest.value.Version?.toString() || "";
manifest.value.Version = `${version}${".0".repeat(Math.max(0, 4 - version.split(".").length))}`;

write(manifest.stringify());
}

return {
Expand Down
40 changes: 38 additions & 2 deletions src/system/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,19 +165,55 @@ export async function mkdirIfNotExists(path: string): Promise<void> {
* @param path Path to the JSON file.
* @returns Contents parsed as JSON.
*/
export async function readJsonFile<T>(path: string): Promise<T> {
export async function readJsonFile<T>(path: string): Promise<JsonFile<T>> {
if (!existsSync(path)) {
throw new Error(`JSON file not found, ${path}`);
}

try {
const contents = await readFile(path, { encoding: "utf-8" });
return JSON.parse(contents);
const value = JSON.parse(contents) as T;

return {
value,
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(/(?<!\r)\n/g, "\r\n");
}

return stringified;
},
};
} catch (cause) {
throw new Error(`Failed to pase JSON file, ${path}`, { cause });
}
}

/**
* A JSON file with its parsed value and a method to stringify it while preserving formatting.
*/
export type JsonFile<T> = {
/**
* The parsed value of the JSON file.
*/
readonly value: T;
/**
* Stringifies the JSON value while preserving original formatting.
* @returns The stringified JSON.
*/
stringify(): string;
};

/**
* Defines how a path will be relocated.
*/
Expand Down