diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6ea1d4..80cf038 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,4 +24,6 @@ jobs: - run: npm ci + - run: npm run typecheck + - run: npm test diff --git a/bplistParser.d.ts b/bplistParser.d.ts deleted file mode 100644 index 28b845d..0000000 --- a/bplistParser.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -export type CallbackFunction = (error: Error | null, result: [T]) => void; - -/** Maximum size, in bytes, of an object the parser will allocate. Read-only; use {@link setMaxObjectSize}. */ -export declare const maxObjectSize: number; - -/** Maximum number of objects the parser will read from a plist. Read-only; use {@link setMaxObjectCount}. */ -export declare const maxObjectCount: number; - -export declare function setMaxObjectSize(value: number): void; - -export declare function setMaxObjectCount(value: number): void; - -/** Wrapper for a CoreFoundation keyed-archiver UID value. */ -export declare class UID { - constructor(id: number); - UID: number; -} - -export declare function parseFile( - fileNameOrBuffer: string | Buffer, - callback?: CallbackFunction -): Promise<[T]>; - -export declare function parseFileSync(fileNameOrBuffer: string | Buffer): [T]; - -export declare function parseBuffer(buffer: string | Buffer): [T]; diff --git a/bplistParser.js b/bplistParser.ts similarity index 84% rename from bplistParser.js rename to bplistParser.ts index ee92edb..38fcbe8 100644 --- a/bplistParser.js +++ b/bplistParser.ts @@ -1,7 +1,5 @@ /* eslint-disable no-console */ -'use strict'; - // adapted from https://github.com/3breadt/dd-plist import fs from 'node:fs'; @@ -13,11 +11,11 @@ export let maxObjectCount = 32768; // Exported bindings are read-only to consumers (an ESM import binding cannot be // assigned, and the CommonJS build exposes exports as getters), so these knobs // are tuned through setters rather than by assigning to the exports. -export function setMaxObjectSize(value) { +export function setMaxObjectSize(value: number): void { maxObjectSize = value; } -export function setMaxObjectCount(value) { +export function setMaxObjectCount(value: number): void { maxObjectCount = value; } @@ -26,21 +24,30 @@ export function setMaxObjectCount(value) { // So we just hardcode the correct value. const EPOCH = 978307200000; -// UID object definition -export const UID = function(id) { - this.UID = id; -}; +/** Wrapper for a CoreFoundation keyed-archiver UID value. */ +export class UID { + UID: number; + + constructor(id: number) { + this.UID = id; + } +} + +export type CallbackFunction = (error: Error | null, result?: [T]) => void; -export const parseFile = function (fileNameOrBuffer, callback) { - return new Promise(function (resolve, reject) { - function tryParseBuffer(buffer) { - let err = null; - let result; +export function parseFile( + fileNameOrBuffer: string | Buffer, + callback?: CallbackFunction +): Promise<[T]> { + return new Promise<[T]>(function (resolve, reject) { + function tryParseBuffer(buffer: Buffer) { + let err: Error | null = null; + let result: [T] | undefined; try { - result = parseBuffer(buffer); + result = parseBuffer(buffer); resolve(result); } catch (ex) { - err = ex; + err = ex as Error; reject(err); } finally { if (callback) callback(err, result); @@ -59,16 +66,14 @@ export const parseFile = function (fileNameOrBuffer, callback) { tryParseBuffer(data); }); }); -}; +} -export const parseFileSync = function (fileNameOrBuffer) { - if (!Buffer.isBuffer(fileNameOrBuffer)) { - fileNameOrBuffer = fs.readFileSync(fileNameOrBuffer); - } - return parseBuffer(fileNameOrBuffer); -}; +export function parseFileSync(fileNameOrBuffer: string | Buffer): [T] { + const buffer = Buffer.isBuffer(fileNameOrBuffer) ? fileNameOrBuffer : fs.readFileSync(fileNameOrBuffer); + return parseBuffer(buffer); +} -export const parseBuffer = function (buffer) { +export function parseBuffer(buffer: Buffer): [T] { // check header const header = buffer.slice(0, 'bplist00'.length).toString('utf8'); if (header !== 'bplist00') { @@ -107,7 +112,7 @@ export const parseBuffer = function (buffer) { } // Handle offset table - const offsetTable = []; + const offsetTable: number[] = []; for (let i = 0; i < numObjects; i++) { const offsetBytes = buffer.slice(offsetTableOffset + i * offsetSize, offsetTableOffset + (i + 1) * offsetSize); @@ -121,7 +126,7 @@ export const parseBuffer = function (buffer) { // For the format specification check // // Apple's binary property list parser implementation. - function parseObject(tableOffset) { + function parseObject(tableOffset: number): any { const offset = offsetTable[tableOffset]; const type = buffer[offset]; const objType = (type & 0xF0) >> 4; //First 4 bits @@ -151,7 +156,7 @@ export const parseBuffer = function (buffer) { throw new Error("Unhandled type 0x" + objType.toString(16)); } - function parseSimple() { + function parseSimple(): any { //Simple switch (objInfo) { case 0x0: // null @@ -167,7 +172,7 @@ export const parseBuffer = function (buffer) { } } - function parseInteger() { + function parseInteger(): number | bigint { const length = Math.pow(2, objInfo); if (length < maxObjectSize) { const data = buffer.slice(offset + 1, offset + 1 + length); @@ -177,7 +182,7 @@ export const parseBuffer = function (buffer) { } - function parseUID() { + function parseUID(): UID { const length = objInfo + 1; if (length < maxObjectSize) { return new UID(readUInt(buffer.slice(offset + 1, offset + 1 + length))); @@ -185,7 +190,7 @@ export const parseBuffer = function (buffer) { throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); } - function parseReal() { + function parseReal(): number | undefined { const length = Math.pow(2, objInfo); if (length < maxObjectSize) { const realBuffer = buffer.slice(offset + 1, offset + 1 + length); @@ -200,7 +205,7 @@ export const parseBuffer = function (buffer) { } } - function parseDate() { + function parseDate(): Date { if (objInfo != 0x3) { console.error("Unknown date type :" + objInfo + ". Parsing anyway..."); } @@ -208,7 +213,7 @@ export const parseBuffer = function (buffer) { return new Date(EPOCH + (1000 * dateBuffer.readDoubleBE(0))); } - function parseData() { + function parseData(): Buffer { let dataoffset = 1; let length = objInfo; if (objInfo == 0xF) { @@ -232,9 +237,9 @@ export const parseBuffer = function (buffer) { throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); } - function parsePlistString (isUtf16) { - isUtf16 = isUtf16 || 0; - let enc = "utf8"; + function parsePlistString(isUtf16?: boolean): string { + const utf16Flag = isUtf16 || false; + let enc: BufferEncoding = "utf8"; let length = objInfo; let stroffset = 1; if (objInfo == 0xF) { @@ -253,10 +258,10 @@ export const parseBuffer = function (buffer) { } } // length is String length -> to get byte length multiply by 2, as 1 character takes 2 bytes in UTF-16 - length *= (isUtf16 + 1); + length *= (utf16Flag ? 2 : 1); if (length < maxObjectSize) { let plistString = Buffer.from(buffer.slice(offset + stroffset, offset + stroffset + length)); - if (isUtf16) { + if (utf16Flag) { plistString = swapBytes(plistString); enc = "ucs2"; } @@ -265,7 +270,7 @@ export const parseBuffer = function (buffer) { throw new Error("Too little heap space available! Wanted to read " + length + " bytes, but only " + maxObjectSize + " are available."); } - function parseArray() { + function parseArray(): any[] { let length = objInfo; let arrayoffset = 1; if (objInfo == 0xF) { @@ -286,7 +291,7 @@ export const parseBuffer = function (buffer) { if (length * objectRefSize > maxObjectSize) { throw new Error("Too little heap space available!"); } - const array = []; + const array: any[] = []; for (let i = 0; i < length; i++) { const objRef = readUInt(buffer.slice(offset + arrayoffset + i * objectRefSize, offset + arrayoffset + (i + 1) * objectRefSize)); array[i] = parseObject(objRef); @@ -294,7 +299,7 @@ export const parseBuffer = function (buffer) { return array; } - function parseDictionary() { + function parseDictionary(): Record { let length = objInfo; let dictoffset = 1; if (objInfo == 0xF) { @@ -318,7 +323,7 @@ export const parseBuffer = function (buffer) { if (debug) { console.log("Parsing dictionary #" + tableOffset); } - const dict = {}; + const dict: Record = {}; for (let i = 0; i < length; i++) { const keyRef = readUInt(buffer.slice(offset + dictoffset + i * objectRefSize, offset + dictoffset + (i + 1) * objectRefSize)); const valRef = readUInt(buffer.slice(offset + dictoffset + (length * objectRefSize) + i * objectRefSize, offset + dictoffset + (length * objectRefSize) + (i + 1) * objectRefSize)); @@ -334,9 +339,9 @@ export const parseBuffer = function (buffer) { } return [ parseObject(topObject) ]; -}; +} -function readUInt(buffer, start) { +function readUInt(buffer: Buffer, start?: number): number { start = start || 0; let l = 0; @@ -346,7 +351,7 @@ function readUInt(buffer, start) { return l; } -function readBigUInt(buffer) { +function readBigUInt(buffer: Buffer): bigint { let value = 0n; for (const byte of buffer) { value = (value << 8n) | BigInt(byte); @@ -354,14 +359,14 @@ function readBigUInt(buffer) { return value; } -function simplifyInteger(value) { +function simplifyInteger(value: bigint): number | bigint { if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) { return Number(value); } return value; } -function readInteger(buffer) { +function readInteger(buffer: Buffer): number | bigint { let value = readBigUInt(buffer); if (buffer.length === 8 && (buffer[0] & 0x80)) { @@ -372,12 +377,12 @@ function readInteger(buffer) { } // we're just going to toss the high order bits because javascript doesn't have 64-bit ints -function readUInt64BE(buffer, start) { +function readUInt64BE(buffer: Buffer, start: number): number { const data = buffer.slice(start, start + 8); return readUInt(data, 0); } -function swapBytes(buffer) { +function swapBytes(buffer: T): T { const len = buffer.length; for (let i = 0; i < len; i += 2) { const a = buffer[i]; diff --git a/build.js b/build.js index a5ff5e0..075cbb3 100644 --- a/build.js +++ b/build.js @@ -1,7 +1,8 @@ import { build } from 'esbuild'; -import { copyFile, mkdir, readFile, writeFile } from 'fs/promises'; +import { execFileSync } from 'node:child_process'; +import { mkdir, readFile, rm, writeFile } from 'fs/promises'; -const entry = 'bplistParser.js'; +const entry = 'bplistParser.ts'; const outdir = 'dist'; await mkdir(outdir, { recursive: true }); @@ -20,10 +21,15 @@ await Promise.all([ build({ ...common, format: 'cjs', outfile: `${outdir}/index.cjs` }), ]); -// Ship the same declarations under both extensions so `import` and `require` -// consumers each resolve types under their own resolution mode. -const types = await readFile('bplistParser.d.ts', 'utf8'); -await writeFile(`${outdir}/index.d.ts`, types); -await writeFile(`${outdir}/index.d.cts`, types); +// tsc emits the declaration for the entry file into dist/types; ship the same +// declarations under both extensions so `import` and `require` consumers each +// resolve types under their own resolution mode. +execFileSync('npx', ['tsc', '-p', 'tsconfig.build.json'], { stdio: 'inherit' }); +const types = await readFile(`${outdir}/types/bplistParser.d.ts`, 'utf8'); +await Promise.all([ + writeFile(`${outdir}/index.d.ts`, types), + writeFile(`${outdir}/index.d.cts`, types), +]); +await rm(`${outdir}/types`, { recursive: true, force: true }); console.log('built', outdir); diff --git a/eslint.config.js b/eslint.config.js index 54ccc05..4d4acd5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,11 +1,22 @@ import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; import globals from 'globals'; -export default [ +export default tseslint.config( { ignores: ['dist/**', 'node_modules/**'], }, js.configs.recommended, + { + files: ['**/*.ts'], + extends: [...tseslint.configs.recommended], + rules: { + // Parsing/creating plists is inherently dynamic (arbitrary nested + // dicts/arrays of unknown shape); `any` is the honest type here, not a + // shortcut. + '@typescript-eslint/no-explicit-any': 'off', + }, + }, { languageOptions: { ecmaVersion: 2022, @@ -48,4 +59,4 @@ export default [ }, }, }, -]; +); diff --git a/package-lock.json b/package-lock.json index 2ac6572..964a564 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,21 @@ { "name": "bplist-parser", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bplist-parser", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "devDependencies": { "@eslint/js": "10.0.x", + "@types/node": "26.4.x", "esbuild": "0.28.x", "eslint": "10.10.x", "globals": "17.12.x", + "typescript": "6.0.x", + "typescript-eslint": "8.69.x", "vitest": "5.0.x" }, "engines": { @@ -1077,6 +1080,246 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", + "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.69.0.tgz", + "integrity": "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/type-utils": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.69.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.69.0.tgz", + "integrity": "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.69.0.tgz", + "integrity": "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.69.0", + "@typescript-eslint/types": "^8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.69.0.tgz", + "integrity": "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.69.0.tgz", + "integrity": "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.69.0.tgz", + "integrity": "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.69.0.tgz", + "integrity": "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.69.0.tgz", + "integrity": "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.69.0", + "@typescript-eslint/tsconfig-utils": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.69.0.tgz", + "integrity": "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.69.0.tgz", + "integrity": "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vitest/mocker": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz", @@ -2306,6 +2549,19 @@ "@rolldown/binding-win32-x64-msvc": "1.2.7" } }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2398,6 +2654,19 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2411,6 +2680,51 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.69.0.tgz", + "integrity": "sha512-B3MltX0VqjUBNEe3b3sSuiRbfa6XrfHFtBiPamjT5AsW/dfq+y+bc0wyuS9DxAS1LyzCxRp2+rxzpLUvqM2BvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.69.0", + "@typescript-eslint/parser": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index 1ff7e4f..e8fa9ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bplist-parser", - "version": "0.4.0", + "version": "0.5.0", "description": "Binary plist parser.", "type": "module", "main": "./dist/index.cjs", @@ -25,9 +25,10 @@ "scripts": { "build": "node build.js", "lint": "eslint .", + "typecheck": "tsc --noEmit", "test": "vitest run", "smoke": "npm run build && node smoke.cjs", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run typecheck && npm run build" }, "keywords": [ "bplist", @@ -41,9 +42,12 @@ "license": "MIT", "devDependencies": { "@eslint/js": "10.0.x", + "@types/node": "26.4.x", "esbuild": "0.28.x", "eslint": "10.10.x", "globals": "17.12.x", + "typescript": "6.0.x", + "typescript-eslint": "8.69.x", "vitest": "5.0.x" }, "homepage": "https://github.com/nearinfinity/node-bplist-parser", diff --git a/test/parse.test.js b/test/parse.test.ts similarity index 86% rename from test/parse.test.js rename to test/parse.test.ts index c86f312..80e2494 100644 --- a/test/parse.test.js +++ b/test/parse.test.ts @@ -14,7 +14,7 @@ describe('bplist-parser', function () { const [dict] = await bplist.parseFile(file); const endTime = new Date(); - console.log('Parsed "' + file + '" in ' + (endTime - startTime1) + 'ms'); + console.log('Parsed "' + file + '" in ' + (endTime.getTime() - startTime1.getTime()) + 'ms'); assert.equal(dict['Application Version'], "9.0.3"); assert.equal(dict['Library Persistent ID'], "6F81D37F95101437"); assert.deepEqual(dict, bplist.parseFileSync(file)[0]); @@ -26,7 +26,7 @@ describe('bplist-parser', function () { const [dict] = await bplist.parseFile(file); const endTime = new Date(); - console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); + console.log('Parsed "' + file + '" in ' + (endTime.getTime() - startTime.getTime()) + 'ms'); assert.equal(dict['CFBundleIdentifier'], 'com.apple.dictionary.MySample'); assert.deepEqual(dict, bplist.parseFileSync(file)[0]); @@ -38,7 +38,7 @@ describe('bplist-parser', function () { const [dict] = await bplist.parseFile(file); const endTime = new Date(); - console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); + console.log('Parsed "' + file + '" in ' + (endTime.getTime() - startTime.getTime()) + 'ms'); assert.equal(dict['PopupMenu'][2]['Key'], "\n #import \n\n#import \n\nint main(int argc, char *argv[])\n{\n return macruby_main(\"rb_main.rb\", argc, argv);\n}\n"); assert.deepEqual(dict, bplist.parseFileSync(file)[0]); @@ -50,7 +50,7 @@ describe('bplist-parser', function () { const [dict] = await bplist.parseFile(file); const endTime = new Date(); - console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); + console.log('Parsed "' + file + '" in ' + (endTime.getTime() - startTime.getTime()) + 'ms'); assert.equal(dict['duration'], 5555.0495000000001); assert.equal(dict['position'], 4.6269989039999997); @@ -63,7 +63,7 @@ describe('bplist-parser', function () { const [dict] = await bplist.parseFile(file); const endTime = new Date(); - console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); + console.log('Parsed "' + file + '" in ' + (endTime.getTime() - startTime.getTime()) + 'ms'); assert.equal(dict['CFBundleName'], 'sellStuff'); assert.equal(dict['CFBundleShortVersionString'], '2.6.1'); @@ -77,7 +77,7 @@ describe('bplist-parser', function () { const [dict] = await bplist.parseFile(file); const endTime = new Date(); - console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); + console.log('Parsed "' + file + '" in ' + (endTime.getTime() - startTime.getTime()) + 'ms'); assert.equal(dict['CFBundleName'], '天翼阅读'); assert.equal(dict['CFBundleDisplayName'], '天翼阅读'); @@ -90,7 +90,7 @@ describe('bplist-parser', function () { const [dict] = await bplist.parseFile(file); const endTime = new Date(); - console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); + console.log('Parsed "' + file + '" in ' + (endTime.getTime() - startTime.getTime()) + 'ms'); assert.deepEqual(dict['$objects'][1]['NS.keys'], [{UID:2}, {UID:3}, {UID:4}]); assert.deepEqual(dict['$objects'][1]['NS.objects'], [{UID: 5}, {UID:6}, {UID:7}]); @@ -104,7 +104,7 @@ describe('bplist-parser', function () { const [dict] = await bplist.parseFile(file); const endTime = new Date(); - console.log('Parsed "' + file + '" in ' + (endTime - startTime) + 'ms'); + console.log('Parsed "' + file + '" in ' + (endTime.getTime() - startTime.getTime()) + 'ms'); assert.equal(dict['zero'], '0'); assert.equal(dict['int32item'], '1234567890'); @@ -154,7 +154,9 @@ describe('bplist-parser', function () { }); }); -function makeIntegerArray(cases) { +type IntegerCase = { value: bigint; bytes: number; expected: number | bigint }; + +function makeIntegerArray(cases: IntegerCase[]): Buffer { if (cases.length >= 15) { throw new Error('test helper only supports short arrays'); } @@ -165,7 +167,7 @@ function makeIntegerArray(cases) { ...cases.map((_, index) => writeUIntBE(index + 1, objectRefSize)) ]); const objects = [root, ...cases.map(({value, bytes}) => makeIntegerObject(value, bytes))]; - const offsets = []; + const offsets: number[] = []; let offset = 8; for (const object of objects) { @@ -192,8 +194,8 @@ function makeIntegerArray(cases) { ]); } -function makeIntegerObject(value, byteLength) { - const widthInfo = { +function makeIntegerObject(value: bigint, byteLength: number): Buffer { + const widthInfo: Record = { 1: 0, 2: 1, 4: 2, @@ -210,7 +212,7 @@ function makeIntegerObject(value, byteLength) { ]); } -function writeUIntBE(value, byteLength) { +function writeUIntBE(value: bigint | number, byteLength: number): Buffer { let integer = BigInt(value); if (integer < 0n) { @@ -225,7 +227,7 @@ function writeUIntBE(value, byteLength) { return buffer; } -function byteWidth(value) { +function byteWidth(value: number): number { if (value <= 0xff) { return 1; } diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..b2009a7 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "noEmit": false, + "outDir": "dist/types" + }, + "include": ["bplistParser.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d5ba5b5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "skipLibCheck": true, + "types": ["node"], + "noEmit": true + }, + "include": ["bplistParser.ts", "test/**/*.ts"] +}