UNPKG

@shockpkg/dir-projector

Version:

Package for creating Shockwave Director projectors

829 lines (765 loc) 19.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProjectorOttoMac = void 0; var _promises = require("node:fs/promises"); var _nodePath = require("node:path"); var _archiveFiles = require("@shockpkg/archive-files"); var _plistDom = require("@shockpkg/plist-dom"); var _util = require("../../util.js"); var _otto = require("../otto.js"); /** * ProjectorOttoMac object. */ class ProjectorOttoMac extends _otto.ProjectorOtto { /** * Binary name. */ binaryName = null; /** * Intel binary package, not universal binary. */ intel = false; /** * Icon data. */ iconData = null; /** * Icon file. */ iconFile = null; /** * Info.plist data. * Currently only supports XML plist. */ infoPlistData = null; /** * Info.plist file. * Currently only supports XML plist. */ infoPlistFile = null; /** * PkgInfo data. */ pkgInfoData = null; /** * PkgInfo file. */ pkgInfoFile = null; /** * Update the bundle name in Info.plist. * Possible values: * - false: Leave untouched. * - true: Output name. * - null: Remove value. * - string: Custom value. */ bundleName = false; /** * Nest Xtras at *.app/Contents/xtras. */ nestXtrasContents = false; /** * ProjectorOttoMac constructor. * * @param path Output path. */ constructor(path) { super(path); } /** * @inheritdoc */ get extension() { return '.app'; } /** * @inheritdoc */ get configNewline() { return '\n'; } /** * @inheritdoc */ get lingoNewline() { return '\n'; } /** * @inheritdoc */ get splashImageExtension() { return '.pict'; } /** * If icon is specified. * * @returns Has icon. */ get hasIcon() { return !!(this.iconData || this.iconFile); } /** * If Info.plist is specified. * * @returns Has Info.plist. */ get hasInfoPlist() { return !!(this.infoPlistData || this.infoPlistFile); } /** * If PkgInfo is specified. * * @returns Has PkgInfo. */ get hasPkgInfo() { return !!(this.pkgInfoData || this.pkgInfoFile); } /** * Get the Projector Resources directory name. * * @returns Directory name. */ get projectorResourcesDirectoryName() { return this.intel ? 'Projector Intel Resources' : 'Projector Resources'; } /** * Get app binary name, default. * * @returns File name. */ get appBinaryNameDefault() { return 'Projector'; } /** * Get app binary name, custom. * * @returns File name. */ get appBinaryNameCustom() { return this.binaryName; } /** * Get app binary name. * * @returns File name. */ get appBinaryName() { return this.appBinaryNameCustom || this.appBinaryNameDefault; } /** * Get app icon name, default. * * @returns File name. */ get appIconNameDefault() { return 'projector.icns'; } /** * Get app icon name, custom. * * @returns File name. */ get appIconNameCustom() { const n = this.binaryName; return n ? `${n}.icns` : null; } /** * Get app icon name. * * @returns File name. */ get appIconName() { return this.appIconNameCustom || this.appIconNameDefault; } /** * Get app rsrc name, default. * * @returns File name. */ get appRsrcNameDefault() { return 'Projector.rsrc'; } /** * Get app rsrc name, custom. * * @returns File name. */ get appRsrcNameCustom() { const n = this.binaryName; return n ? `${n}.rsrc` : null; } /** * Get app rsrc name. * * @returns File name. */ get appRsrcName() { return this.appRsrcNameCustom || this.appRsrcNameDefault; } /** * Get app Info.plist path. * * @returns File path. */ get appPathInfoPlist() { return 'Contents/Info.plist'; } /** * Get app PkgInfo path. * * @returns File path. */ get appPathPkgInfo() { return 'Contents/PkgInfo'; } /** * Get app Frameworks path. * * @returns File path. */ get appPathFrameworks() { return 'Contents/Frameworks'; } /** * Get app Xtras path. * * @returns Directory path. */ get appPathXtras() { return `Contents/${this.xtrasName}`; } /** * Get app binary path, default. * * @returns File path. */ get appPathBinaryDefault() { return `Contents/MacOS/${this.appBinaryNameDefault}`; } /** * Get app binary path, custom. * * @returns File path. */ get appPathBinaryCustom() { const n = this.appBinaryNameCustom; return n ? `Contents/MacOS/${n}` : null; } /** * Get app binary path. * * @returns File path. */ get appPathBinary() { return this.appPathBinaryCustom || this.appPathBinaryDefault; } /** * Get app icon path, default. * * @returns File path. */ get appPathIconDefault() { return `Contents/Resources/${this.appIconNameDefault}`; } /** * Get app icon path, custom. * * @returns File path. */ get appPathIconCustom() { const n = this.appIconNameCustom; return n ? `Contents/Resources/${n}` : null; } /** * Get app icon path. * * @returns File path. */ get appPathIcon() { return this.appPathIconCustom || this.appPathIconDefault; } /** * Get app rsrc path, default. * * @returns File path. */ get appPathRsrcDefault() { return `Contents/Resources/${this.appRsrcNameDefault}`; } /** * Get app rsrc path, custom. * * @returns File path. */ get appPathRsrcCustom() { const n = this.appRsrcNameCustom; return n ? `Contents/Resources/${n}` : null; } /** * Get app rsrc path. * * @returns File path. */ get appPathRsrc() { return this.appPathRsrcCustom || this.appPathRsrcDefault; } /** * Get the icon path. * * @returns Icon path. */ get iconPath() { return (0, _nodePath.join)(this.path, this.appPathIcon); } /** * Get the Info.plist path. * * @returns Info.plist path. */ get infoPlistPath() { return (0, _nodePath.join)(this.path, this.appPathInfoPlist); } /** * Get the PkgInfo path. * * @returns PkgInfo path. */ get pkgInfoPath() { return (0, _nodePath.join)(this.path, this.appPathPkgInfo); } /** * Get the binary path. * * @returns Binary path. */ get binaryPath() { return (0, _nodePath.join)(this.path, this.appPathBinary); } /** * Get outout Xtras path. * * @returns Output path. */ get xtrasPath() { if (this.nestXtrasContents) { return `${this.path}/${this.appPathXtras}`; } return super.xtrasPath; } /** * Get icon data if any specified, from data or file. * * @returns Icon data or null. */ async getIconData() { const { iconData, iconFile } = this; if (iconData) { return typeof iconData === 'function' ? iconData() : iconData; } if (iconFile) { const d = await (0, _promises.readFile)(iconFile); return new Uint8Array(d.buffer, d.byteOffset, d.byteLength); } return null; } /** * Get Info.plist data if any specified, from data or file. * * @returns Info.plist data or null. */ async getInfoPlistData() { const { infoPlistData, infoPlistFile } = this; if (infoPlistData) { switch (typeof infoPlistData) { case 'function': { const d = await infoPlistData(); return typeof d === 'string' ? d : new TextDecoder().decode(d); } case 'string': { return infoPlistData; } default: { // Fall through. } } return new TextDecoder().decode(infoPlistData); } if (infoPlistFile) { return (0, _promises.readFile)(infoPlistFile, 'utf8'); } return null; } /** * Get PkgInfo data if any specified, from data or file. * * @returns PkgInfo data or null. */ async getPkgInfoData() { const { pkgInfoData, pkgInfoFile } = this; if (pkgInfoData) { switch (typeof pkgInfoData) { case 'function': { return pkgInfoData(); } case 'string': { return new TextEncoder().encode(pkgInfoData); } default: { // Fall through. } } return pkgInfoData; } if (pkgInfoFile) { const d = await (0, _promises.readFile)(pkgInfoFile); return new Uint8Array(d.buffer, d.byteOffset, d.byteLength); } return null; } /** * Get configured bundle name, or null to remove. * * @returns New name or null. */ getBundleName() { const { bundleName } = this; return bundleName === true ? (0, _util.trimExtension)((0, _nodePath.basename)(this.path), this.extension, true) : bundleName; } /** * @inheritdoc */ async _writeSkeleton(skeleton) { const { path, shockwave, appPathFrameworks, appPathBinaryDefault, appPathBinaryCustom, appPathIconDefault, appPathIconCustom, appPathRsrcDefault, appPathRsrcCustom, xtrasName, xtrasPath, projectorResourcesDirectoryName } = this; const xtrasMappings = this.getIncludeXtrasMappings(); let foundProjectorResourcesDirectory = false; let foundFrameworks = false; let foundBinary = false; let foundIcon = false; let foundRsrc = false; let foundXtras = false; const patches = await this._getPatches(); /** * Extract entry, and also apply patches if any. * * @param entry Archive entry. * @param dest Output path. */ const extract = async (entry, dest) => { if (entry.type === _archiveFiles.PathType.FILE) { let data = null; for (const patch of patches) { // eslint-disable-next-line unicorn/prefer-regexp-test if (patch.match(entry.volumePath)) { if (!data) { // eslint-disable-next-line no-await-in-loop const d = await entry.read(); if (!d) { throw new Error(`Failed to read: ${entry.volumePath}`); } data = new Uint8Array(d.buffer, d.byteOffset, d.byteLength); } // eslint-disable-next-line no-await-in-loop data = await patch.modify(data); } } if (data) { await (0, _promises.mkdir)((0, _nodePath.dirname)(dest), { recursive: true }); await (0, _promises.writeFile)(dest, data); await entry.setAttributes(dest, null, { ignoreTimes: true }); return; } } await entry.extract(dest); }; /** * Xtras handler. * * @param entry Archive entry. * @returns Boolean. */ const xtrasHandler = async entry => { // Check if Xtras path. const xtrasRel = (0, _util.pathRelativeBase)(entry.volumePath, xtrasName, true); if (xtrasRel === null) { return false; } foundXtras = true; // Find output path if being included, else skip. const dest = this.includeXtrasMappingsDest(xtrasMappings, xtrasRel); if (!dest) { return true; } await extract(entry, (0, _nodePath.join)(xtrasPath, dest)); return true; }; /** * Resources handler. * * @param entry Archive entry. * @returns Boolean. */ const projectorResourcesHandler = async entry => { // Check if projector path. const projectorRel = (0, _util.pathRelativeBase)(entry.volumePath, projectorResourcesDirectoryName, true); if (projectorRel === null) { return false; } foundProjectorResourcesDirectory = true; if ((0, _util.pathRelativeBaseMatch)(projectorRel, appPathFrameworks, true)) { foundFrameworks = true; // Exclude Frameworks directory for Shockwave projectors. if (shockwave) { return true; } } let dest = projectorRel; // Possibly rename the binary. if ((0, _util.pathRelativeBaseMatch)(projectorRel, appPathBinaryDefault, true)) { foundBinary = true; if (appPathBinaryCustom) { dest = appPathBinaryCustom; } } // Special case for icon. if ((0, _util.pathRelativeBaseMatch)(projectorRel, appPathIconDefault, true)) { foundIcon = true; // Possible rename the icon. if (appPathIconCustom) { dest = appPathIconCustom; } } // Special case for rsrc. if ((0, _util.pathRelativeBaseMatch)(projectorRel, appPathRsrcDefault, true)) { foundRsrc = true; if (appPathRsrcCustom) { dest = appPathRsrcCustom; } } await extract(entry, (0, _nodePath.join)(path, dest)); return true; }; const archive = await (0, _archiveFiles.createArchiveByFileStatOrThrow)(skeleton, { nobrowse: this.nobrowse }); await archive.read(async entry => { if (entry.type === _archiveFiles.PathType.RESOURCE_FORK) { return true; } if (await xtrasHandler(entry)) { return true; } if (await projectorResourcesHandler(entry)) { return true; } return true; }); if (!foundProjectorResourcesDirectory) { throw new Error(`Failed to locate: ${projectorResourcesDirectoryName}`); } if (!foundFrameworks) { const d = projectorResourcesDirectoryName; throw new Error(`Failed to locate: ${d}/${appPathFrameworks}`); } if (!foundBinary) { const d = projectorResourcesDirectoryName; throw new Error(`Failed to locate: ${d}/${appPathBinaryDefault}`); } if (!foundIcon) { const d = projectorResourcesDirectoryName; throw new Error(`Failed to locate: ${d}/${appPathIconDefault}`); } if (!foundRsrc) { const d = projectorResourcesDirectoryName; throw new Error(`Failed to locate: ${d}/${appPathRsrcDefault}`); } if (!foundXtras) { throw new Error(`Failed to locate: ${xtrasName}`); } await Promise.all(patches.map(async p => p.after())); } /** * Get patches to apply. * * @returns Patches list. */ async _getPatches() { return (await Promise.all([this._getPatchIcon(), this._getPatchPkgInfo(), this._getPatchInfoPlist()])).filter(Boolean); } /** * Get patch for icon. * * @returns Patch spec. */ async _getPatchIcon() { const iconData = await this.getIconData(); if (!iconData) { return null; } const { projectorResourcesDirectoryName, appPathIconDefault } = this; let count = 0; const patch = { // eslint-disable-next-line jsdoc/require-jsdoc match: file => { const projectorRel = (0, _util.pathRelativeBase)(file, projectorResourcesDirectoryName, true); return projectorRel !== null && (0, _util.pathRelativeBaseMatch)(projectorRel, appPathIconDefault, true); }, // eslint-disable-next-line jsdoc/require-jsdoc modify: data => { count++; return iconData; }, // eslint-disable-next-line jsdoc/require-jsdoc after: () => { if (!count) { const d = projectorResourcesDirectoryName; const f = appPathIconDefault; throw new Error(`Failed to locate for replace: ${d}/${f}`); } } }; return patch; } /** * Get patch for PkgInfo. * * @returns Patch spec. */ async _getPatchPkgInfo() { const infoData = await this.getPkgInfoData(); if (!infoData) { return null; } const { projectorResourcesDirectoryName, appPathPkgInfo } = this; let count = 0; const patch = { // eslint-disable-next-line jsdoc/require-jsdoc match: file => { const projectorRel = (0, _util.pathRelativeBase)(file, projectorResourcesDirectoryName, true); return projectorRel !== null && (0, _util.pathRelativeBaseMatch)(projectorRel, appPathPkgInfo, true); }, // eslint-disable-next-line jsdoc/require-jsdoc modify: data => { count++; return infoData; }, // eslint-disable-next-line jsdoc/require-jsdoc after: async () => { // Some skeletons lack this file, just write in that case. if (!count) { const { pkgInfoPath } = this; await (0, _promises.mkdir)((0, _nodePath.dirname)(pkgInfoPath), { recursive: true }); await (0, _promises.writeFile)(pkgInfoPath, infoData); } } }; return patch; } /** * Get patch for Info.plist. * * @returns Patch spec. */ async _getPatchInfoPlist() { const customPlist = await this.getInfoPlistData(); const bundleName = this.getBundleName(); const { appBinaryNameCustom, appIconNameCustom, projectorResourcesDirectoryName, appPathInfoPlist } = this; if (!(customPlist !== null || appIconNameCustom || appBinaryNameCustom || bundleName !== false)) { return null; } let count = 0; const patch = { // eslint-disable-next-line jsdoc/require-jsdoc match: file => { const projectorRel = (0, _util.pathRelativeBase)(file, projectorResourcesDirectoryName, true); return projectorRel !== null && (0, _util.pathRelativeBaseMatch)(projectorRel, appPathInfoPlist, true); }, // eslint-disable-next-line jsdoc/require-jsdoc modify: data => { // Use a custom plist or the existing one. const xml = customPlist ?? new TextDecoder().decode(data); const plist = new _plistDom.Plist(); plist.fromXml(xml); const dict = plist.getValue().castAs(_plistDom.ValueDict); if (appIconNameCustom) { dict.set('CFBundleIconFile', new _plistDom.ValueString(appIconNameCustom)); } if (appBinaryNameCustom) { dict.set('CFBundleExecutable', new _plistDom.ValueString(appBinaryNameCustom)); } if (bundleName !== false) { const key = 'CFBundleName'; if (bundleName === null) { dict.delete(key); } else { dict.set(key, new _plistDom.ValueString(bundleName)); } } const plistData = new TextEncoder().encode(plist.toXml()); count++; return plistData; }, // eslint-disable-next-line jsdoc/require-jsdoc after: () => { if (!count) { const d = projectorResourcesDirectoryName; const f = appPathInfoPlist; throw new Error(`Failed to locate for update: ${d}/${f}`); } } }; return patch; } } exports.ProjectorOttoMac = ProjectorOttoMac; //# sourceMappingURL=mac.js.map