diff --git a/spec/Bundle.ts b/spec/Bundle.ts new file mode 100644 index 0000000..f77d645 --- /dev/null +++ b/spec/Bundle.ts @@ -0,0 +1,58 @@ +import { Stream } from "stream"; +import { default as Bundle, filesSymbol } from "../lib/Bundle"; + +describe("Bundle", () => { + let bundle: InstanceType; + + beforeEach(() => { + bundle = new Bundle("application/vnd.apple.pkpass"); + }); + + it("should throw an error if no mime-type is specified in the constructor", () => { + // @ts-expect-error + expect(() => new Bundle()).toThrowError( + Error, + "Cannot build Bundle. MimeType is missing", + ); + }); + + it("should expose the mime-type as public property", () => { + expect(bundle.mimeType).toBe("application/vnd.apple.pkpass"); + }); + + it("should allow to add buffers", () => { + const buffer = Buffer.alloc(0); + bundle.addBuffer("pass.json", buffer); + + expect(bundle[filesSymbol]).toEqual({ "pass.json": buffer }); + }); + + it("should throw error if freezed", async () => { + addEmptyFilesToBundle(bundle); + + await bundle.getAsBuffer(); + + expect(() => + bundle.addBuffer("icon.png", Buffer.alloc(0)), + ).toThrowError(Error, "Cannot add file. Bundle is closed."); + }); + + it("should return a stream with 'getAsStream'", () => { + addEmptyFilesToBundle(bundle); + + expect(bundle.getAsStream()).toBeInstanceOf(Stream); + }); + + it("should return a buffer with 'getAsBuffer'", async () => { + addEmptyFilesToBundle(bundle); + + expect(await bundle.getAsBuffer()).toBeInstanceOf(Buffer); + }); +}); + +function addEmptyFilesToBundle(bundle: Bundle) { + const buffer = Buffer.alloc(0); + bundle.addBuffer("pass.json", buffer); + bundle.addBuffer("icon@2x.png", buffer); + bundle.addBuffer("icon@3x.png", buffer); +} diff --git a/src/Bundle.ts b/src/Bundle.ts new file mode 100644 index 0000000..966fcf8 --- /dev/null +++ b/src/Bundle.ts @@ -0,0 +1,99 @@ +import { Stream } from "stream"; +import { ZipFile } from "yazl"; + +export const filesSymbol = Symbol("bundleFiles"); +const bundleStateSymbol = Symbol("state"); +const archiveSymbol = Symbol("zip"); + +enum BundleState { + CLOSED = 0, + OPEN = 1, +} + +namespace Mime { + export type type = string; + export type subtype = string; +} + +/** + * Defines a container ready to be distributed. + * If no mimeType is passed to the constructor, + * it will throw an error. + */ + +export default class Bundle { + private [bundleStateSymbol]: BundleState = BundleState.OPEN; + private [filesSymbol]: { [key: string]: Buffer } = {}; + private [archiveSymbol] = new ZipFile(); + + constructor(public mimeType: `${Mime.type}/${Mime.subtype}`) { + if (!mimeType) { + throw new Error("Cannot build Bundle. MimeType is missing"); + } + } + + /** + * Freezes / Closes the bundle so no more files + * can be added any further. + */ + + private freeze() { + if (this[bundleStateSymbol] === BundleState.CLOSED) { + return; + } + + this[bundleStateSymbol] = BundleState.CLOSED; + this[archiveSymbol].end(); + } + + /** + * Allows files to be added to the bundle. + * If the bundle is closed, it will throw an error. + * + * @param fileName + * @param buffer + */ + + public addBuffer(fileName: string, buffer: Buffer) { + if (this[bundleStateSymbol] === BundleState.CLOSED) { + throw new Error("Cannot add file. Bundle is closed."); + } + + this[filesSymbol][fileName] = buffer; + this[archiveSymbol].addBuffer(buffer, fileName); + } + + /** + * Closes the bundle and returns it as a Buffer. + * Once closed, the bundle does not allow files + * to be added any further. + * + * @returns Promise + */ + + public getAsBuffer(): Promise { + const stream = this.getAsStream(); + const chunks = []; + + return new Promise((resolve) => { + stream.on("data", (data: Buffer) => { + chunks.push(data); + }); + + stream.on("end", () => resolve(Buffer.from(chunks))); + }); + } + + /** + * Closes the bundle and returns it as a stream. + * Once closed, the bundle does not allow files + * to be added any further. + * + * @returns + */ + + public getAsStream(): Stream { + this.freeze(); + return this[archiveSymbol].outputStream; + } +} diff --git a/src/index.ts b/src/index.ts index 55dfbb3..7d46253 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,5 @@ export type { AbstractModel } from "./abstract"; export { createPass } from "./factory"; export { createAbstractModel } from "./abstract"; + +export { default as Bundle } from "./Bundle";