@shockpkg/dir-projector
Version:
Package for creating Shockwave Director projectors
829 lines (765 loc) • 19.1 kB
JavaScript
"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