@jasonscheirer/electron-forge-maker-msix
Version:
An `electron-forge` maker for MSIX that supports `electron-forge` v6 and can be used as a replacement for `electron-builder`. Supports code-signing.
325 lines • 16.5 kB
JavaScript
import fs from 'fs-extra';
import path from 'node:path';
import { run } from './run';
import { findInWindowsKits } from './walk';
export const makePRI = async (outPath, config) => {
var _a;
const makePRIPath = (_a = config.makePriPath) !== null && _a !== void 0 ? _a : (await findInWindowsKits('makepri.exe'));
const outPriPath = path.join(outPath, 'resources.pri');
const priConfigPath = path.join(outPath, 'priconfig.xml');
await run(makePRIPath, ['createconfig', '/cf', priConfigPath, '/dq', 'en-US', '/pv', '10.0.0']);
await run(makePRIPath, ['new', '/pr', outPath, '/cf', priConfigPath, '/of', outPriPath]);
};
const ENTITY_REPLACEMENTS = [
[/[&]/g, '&'],
[/["]/g, '"'],
[/[<]/g, '<'],
[/[>]/g, '>'],
];
const xmlSafeString = (input) => input &&
ENTITY_REPLACEMENTS.reduce((input, [fromString, toNewString]) => input.replace(fromString, toNewString), input);
const msixSafeVersion = (inVersion) => Array.from(inVersion.match(/\d+/g) || [])
.concat(['0', '0', '0', '0'])
.slice(0, 4)
.join('.');
export const makeContentTypeXML = async (outPath) => {
const fileName = '[Content_Types].xml';
const outFileName = path.join(outPath, fileName);
const co = `
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="mp3" ContentType="audio/mpeg" />
<Default Extension="png" ContentType="image/png" />
<Default Extension="ico" ContentType="image/vnd.microsoft.icon" />
<Default Extension="dll" ContentType="application/x-msdownload" />
<Default Extension="pak" ContentType="application/octet-stream" />
<Default Extension="bin" ContentType="application/octet-stream" />
<Default Extension="dat" ContentType="application/octet-stream" />
<Default Extension="html" ContentType="text/html" />
<Default Extension="json" ContentType="application/json" />
<Default Extension="xml" ContentType="text/xml" />
<Default Extension="asar" ContentType="application/octet-stream" />
<Default Extension="node" ContentType="application/octet-stream" />
<Default Extension="exe" ContentType="application/x-msdownload" />
<Default Extension="pri" ContentType="application/octet-stream" />
<Default Extension="yml" ContentType="application/yaml" />
<Default Extension="yaml" ContentType="application/yaml" />
<Default Extension="appinstaller" ContentType="application/appinstaller" />
<Override PartName="/AppxManifest.xml" ContentType="application/vnd.ms-appx.manifest+xml" />
<Override PartName="/AppxBlockMap.xml" ContentType="application/vnd.ms-appx.blockmap+xml" />
<Override PartName="/AppxSignature.p7x" ContentType="application/vnd.ms-appx.signature" />
<Override PartName="/AppxMetadata/CodeIntegrity.cat" ContentType="application/vnd.ms-pkiseccat" />
</Types>`;
await fs.writeFile(outFileName, co);
};
export const getPublisher = async (executable, config) => {
var _a, _b, _c;
const stdout = await run((_a = config.sigCheckPath) !== null && _a !== void 0 ? _a : 'sigcheck.exe', ['-accepteula', executable], true);
const publisherRE = /\r\n[ \t]+Publisher:[ \t]+(?<publisher>.+?)\r\n/;
const foundPublisher = (_c = (_b = stdout.match(publisherRE)) === null || _b === void 0 ? void 0 : _b.groups) === null || _c === void 0 ? void 0 : _c.publisher;
if (foundPublisher) {
return foundPublisher;
}
else {
if (config.codesign) {
throw new Error(`Could not determine publisher: ${executable} is not signed or sigcheck is not installed.`);
}
else {
return 'CN=My Company LLC, O=My Company LLC, L=San Francisco, S=California, C=US';
}
}
};
const CapabilityMap = {
GraphicsCapture: '<uap6:Capability Name="graphicsCapture" />',
Microphone: '<DeviceCapability Name="microphone" />',
Webcam: '<DeviceCapability Name="webcam"/>',
};
export const makeAppManifestXML = ({ appID, appName, architecture, appDescription, executable, publisher, version, protocols, fileAssociations, appCapabilities, allowExternalContent, copilotKey, baseDownloadURL, appInstallerFilename, makeAppInstaller, runAtStartup, exeAlias, startupParams, appURIHandlers, unvirtualizedResources, }) => {
const fileAssociationsExtension = fileAssociations
? `<uap:Extension Category="windows.fileTypeAssociation">
<uap:FileTypeAssociation Name="batchrenamer">
<uap:DisplayName>${appName} Readable Files</uap:DisplayName>
<uap:Logo>assets\\StoreLogo.png</uap:Logo>
<uap:SupportedFileTypes>
${fileAssociations.map((ext) => `<uap:FileType>${ext}</uap:FileType>`).join('')}
</uap:SupportedFileTypes>
</uap:FileTypeAssociation>
</uap:Extension>
`
: '';
const startupExtension = runAtStartup
? `
<desktop:Extension
Category="windows.startupTask"
Executable="${xmlSafeString(executable)}"
${startupParams ? `uap11:Parameters="${xmlSafeString(startupParams)}"` : ''}
EntryPoint="Windows.FullTrustApplication">
<desktop:StartupTask TaskId="${xmlSafeString(appID)}.Startup" Enabled="true" DisplayName="${xmlSafeString(appName)}" />
</desktop:Extension>
`
: '';
const exeAliasExtension = exeAlias
? `
${startupExtension}
<uap3:Extension
Category="windows.appExecutionAlias"
Executable="${xmlSafeString(executable)}"
EntryPoint="Windows.FullTrustApplication">
<uap3:AppExecutionAlias>
<desktop:ExecutionAlias Alias="${xmlSafeString(executable.split(/[/\\]/).pop())}" />
</uap3:AppExecutionAlias>
</uap3:Extension>
`
: '';
let extensions = `
${fileAssociationsExtension}
${startupExtension}
${exeAliasExtension}
`;
let autoUpdateXML = '';
if (baseDownloadURL && makeAppInstaller) {
autoUpdateXML += `<uap13:AutoUpdate>
<uap13:AppInstaller File="${xmlSafeString(appInstallerFilename)}" />
</uap13:AutoUpdate>
`;
}
let additionalCapabilities = '';
if (appCapabilities) {
additionalCapabilities += [...new Set(appCapabilities)]
.map((cap) => CapabilityMap[cap])
.filter((cv) => !!cv);
}
if (protocols) {
for (const protocolGroup of protocols) {
for (const protocol of protocolGroup.schemes) {
extensions += `<uap3:Extension Category="windows.protocol">
<uap3:Protocol Name="${xmlSafeString(protocol)}" Parameters=""%1"">
<uap:DisplayName>${xmlSafeString(protocol === protocolGroup.name ? protocol : `${protocolGroup.name} (${protocol})`)}</uap:DisplayName>
</uap3:Protocol>
</uap3:Extension>
`;
}
}
}
if (appURIHandlers) {
const hosts = appURIHandlers
.map((h) => ` <uap3:Host Name="${xmlSafeString(h)}" />`)
.join('\n');
extensions += `<uap3:Extension Category="windows.appUriHandler">
<uap3:AppUriHandler>
${hosts}
</uap3:AppUriHandler>
</uap3:Extension>
`;
}
let minVersionTested = '10.0.17763.0';
let maxVersionTested = '10.0.21300.0';
if (copilotKey) {
// Version range to enable this is later
minVersionTested = '10.0.19041.0';
maxVersionTested = '10.0.22621.0';
const tapString = copilotKey.tap
? `<SingleTap ${copilotKey.tap.wparam ? `MessageWParam="${xmlSafeString(copilotKey.tap.wparam.toString())}"` : ''}>${xmlSafeString(copilotKey.tap.url)}</SingleTap>`
: '';
const startString = copilotKey.start
? `<PressAndHoldStart ${copilotKey.start.wparam ? `MessageWParam="${xmlSafeString(copilotKey.start.wparam.toString())}"` : ''}>${xmlSafeString(copilotKey.start.url)}</PressAndHoldStart>`
: '';
const stopString = copilotKey.stop
? `<PressAndHoldStop ${copilotKey.stop.wparam ? `MessageWParam="${xmlSafeString(copilotKey.stop.wparam.toString())}"` : ''}>${xmlSafeString(copilotKey.stop.url)}</PressAndHoldStop>`
: '';
extensions += `
<uap3:Extension Category="windows.appExtension">
<uap3:AppExtension Name="com.microsoft.windows.copilotkeyprovider"
DisplayName="${appName} - Copilot Key"
Id="${appID}"
Description="${appDescription}"
PublicFolder="Public">
<uap3:Properties>
${tapString}
${startString}
${stopString}
</uap3:Properties>
</uap3:AppExtension>
</uap3:Extension>
`;
}
const devirtualize = unvirtualizedResources
? `<desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
<desktop6:FileSystemWriteVirtualization>disabled</desktop6:FileSystemWriteVirtualization>`
: '';
return `
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
xmlns:uap6="http://schemas.microsoft.com/appx/manifest/uap/windows10/6"
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
xmlns:uap11="http://schemas.microsoft.com/appx/manifest/uap/windows10/11"
xmlns:uap13="http://schemas.microsoft.com/appx/manifest/uap/windows10/13"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:desktop2="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2"
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
xmlns:desktop7="http://schemas.microsoft.com/appx/manifest/desktop/windows10/7"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap uap3 uap10 desktop7 rescap">
<Identity Name="${xmlSafeString(appID)}" Publisher="${xmlSafeString(publisher.startsWith('CN=') ? publisher : `CN=${publisher}`)}" Version="${xmlSafeString(msixSafeVersion(version))}" ProcessorArchitecture="${xmlSafeString(architecture)}" />
<Properties>
<DisplayName>${xmlSafeString(appName)}</DisplayName>
<PublisherDisplayName>${xmlSafeString(appName)}</PublisherDisplayName>
<Description>${xmlSafeString(appDescription)}</Description>
<Logo>assets\\StoreLogo.png</Logo>
<uap10:PackageIntegrity>
<uap10:Content Enforcement="off" />
</uap10:PackageIntegrity>
<uap10:AllowExternalContent>${allowExternalContent}</uap10:AllowExternalContent>
${autoUpdateXML}
${devirtualize}
</Properties>
<Resources>
<Resource Language="en-us" />
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="${xmlSafeString(minVersionTested)}" MaxVersionTested="${xmlSafeString(maxVersionTested)}" />
</Dependencies>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<rescap:Capability Name="packageManagement" />
${unvirtualizedResources ? '<rescap:Capability Name="unvirtualizedResources" />' : ''}
<Capability Name="internetClient" />
${additionalCapabilities}
</Capabilities>
<Applications>
<Application Id="${xmlSafeString(appID)}" Executable="${xmlSafeString(executable)}"
EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements BackgroundColor="transparent" DisplayName="${xmlSafeString(appName)}"
Square150x150Logo="assets\\${xmlSafeString(appID)}-150x150Logo.png"
Square44x44Logo="assets\\${xmlSafeString(appID)}-44x44Logo.png" Description="${xmlSafeString(appName)}">
<uap:DefaultTile Wide310x150Logo="assets\\${xmlSafeString(appID)}-310x150Logo.png"
Square310x310Logo="assets\\${xmlSafeString(appID)}-310x310Logo.png"
Square71x71Logo="assets\\${xmlSafeString(appID)}-71x71Logo.png" />
</uap:VisualElements>
<Extensions>
${extensions}
</Extensions>
</Application>
</Applications>
<Extensions>
<desktop2:Extension Category="windows.firewallRules">
<desktop2:FirewallRules Executable="${xmlSafeString(executable)}">
<desktop2:Rule Direction="in" IPProtocol="TCP" Profile="all"/>
<desktop2:Rule Direction="out" IPProtocol="TCP" Profile="all"/>
</desktop2:FirewallRules>
</desktop2:Extension>
</Extensions>
</Package>`.trim();
};
export const makeManifestConfiguration = ({ appID, version, executable, config, options, }) => {
var _a, _b, _c;
return {
appID,
appName: options.appName,
appDescription: (_a = config.appDescription) !== null && _a !== void 0 ? _a : options.appName,
executable,
architecture: options.targetArch,
version,
publisher: config.publisher,
allowExternalContent: !!config.allowExternalContent,
protocols: options.forgeConfig.packagerConfig.protocols,
fileAssociations: config.fileExtensions,
baseDownloadURL: config.baseDownloadURL,
makeAppInstaller: (_b = config.makeAppInstaller) !== null && _b !== void 0 ? _b : true,
msixFilename: `${options.appName}-${options.targetArch}-${version}.msix`,
appInstallerFilename: `${options.appName}-${options.targetArch}.appinstaller`,
appCapabilities: config.appCapabilities,
copilotKey: config.copilotKey,
allowRollbacks: config.allowRollbacks,
runAtStartup: !!config.runAtStartup,
startupParams: config.startupParams,
exeAlias: !!config.exeAlias,
appURIHandlers: config.appURIHandlers,
unvirtualizedResources: Boolean((_c = config.unvirtualizedResources) !== null && _c !== void 0 ? _c : true),
};
};
export const makeAppManifest = async (outPath, manifestConfig) => {
await fs.ensureDir(outPath);
const outFilePath = path.join(outPath, 'AppxManifest.xml');
const manifestXML = makeAppManifestXML(manifestConfig);
fs.writeFile(outFilePath, manifestXML);
};
export const makeAppInstallerXML = ({ appID, publisher, version, baseDownloadURL, msixFilename, appInstallerFilename, allowRollbacks, }) => {
const base = baseDownloadURL === null || baseDownloadURL === void 0 ? void 0 : baseDownloadURL.replace(/\/+$/, '');
const MSIXURL = `${base}/${xmlSafeString(msixFilename)}`;
// Go round trip through URL class to escape spaces etc
const appInstallerURL = new URL(`${base}/${appInstallerFilename}`).toString();
return `
<AppInstaller
xmlns="http://schemas.microsoft.com/appx/appinstaller/2021"
Version="1.0.0.0"
Uri="${xmlSafeString(appInstallerURL)}" >
<MainBundle
Name="${xmlSafeString(appID)}"
Publisher="${xmlSafeString(publisher.startsWith('CN=') ? publisher : `CN=${publisher}`)}"
Version="${xmlSafeString(msixSafeVersion(version))}"
Uri="${xmlSafeString(new URL(MSIXURL).toString())}" />
<UpdateSettings>
<OnLaunch HoursBetweenUpdateChecks="12" />
<ForceUpdateFromAnyVersion>${allowRollbacks !== null && allowRollbacks !== void 0 ? allowRollbacks : true}</ForceUpdateFromAnyVersion>
</UpdateSettings>
<UpdateUris>
<UpdateUri>${xmlSafeString(appInstallerURL)}</UpdateUri>
</UpdateUris>
<RepairUris>
<RepairUri>${xmlSafeString(appInstallerURL)}</RepairUri>
</RepairUris>
</AppInstaller>`;
};
export const makeAppInstaller = async (outPath, inBundlePath, manifestConfig) => {
await fs.ensureDir(outPath);
const outFilePath = path.join(outPath, manifestConfig.appInstallerFilename);
const embedFilePath = path.join(inBundlePath, manifestConfig.appInstallerFilename);
if (manifestConfig.makeAppInstaller && manifestConfig.baseDownloadURL) {
const outXML = makeAppInstallerXML(manifestConfig);
await fs.writeFile(outFilePath, outXML);
await fs.writeFile(embedFilePath, outXML);
return outFilePath;
}
};
//# sourceMappingURL=msixTools.js.map