Skip to content

Commit de3b711

Browse files
committed
feat: write appVersion into the packaged app's package.json
The resolved appVersion is now written to the version field of the copied app's package.json (in the staging directory, never the user's source directory) before asar packing. This makes app.getVersion() return the correct version on platforms without executable version metadata (i.e. Linux), consistent with the existing metadata stamping on Windows and macOS. Closes #1331
1 parent 2a9c0c6 commit de3b711

4 files changed

Lines changed: 179 additions & 0 deletions

File tree

src/platform.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,36 @@ export class App {
177177
async buildApp() {
178178
await this.copyTemplate();
179179
await validateElectronApp(this.opts.dir, this.originalResourcesAppDir);
180+
await this.writeAppVersion();
180181
await this.asarApp();
181182
}
182183

184+
/**
185+
* Writes the resolved {@link Options.appVersion | appVersion} to the `version` field of the
186+
* copied app's `package.json`, so that `app.getVersion()` returns it at runtime even on
187+
* platforms that don't store the version in executable metadata (i.e. Linux).
188+
*
189+
* Only the copy of the app in the staging directory is modified, never the user's source
190+
* directory.
191+
*/
192+
async writeAppVersion() {
193+
if (typeof this.opts.appVersion !== 'string') {
194+
return;
195+
}
196+
197+
const packageJSONPath = path.join(this.originalResourcesAppDir, 'package.json');
198+
const packageJSON = JSON.parse(
199+
(await fs.promises.readFile(packageJSONPath, 'utf8')).replace(/^\uFEFF/, ''),
200+
);
201+
if (packageJSON.version === this.opts.appVersion) {
202+
return;
203+
}
204+
205+
debug(`Setting version in ${packageJSONPath} to ${this.opts.appVersion}`);
206+
packageJSON.version = this.opts.appVersion;
207+
await fs.promises.writeFile(packageJSONPath, JSON.stringify(packageJSON, null, 2));
208+
}
209+
183210
async copyTemplate() {
184211
await runHooks(this.opts.beforeCopy, this.hookArgsWithOriginalResourcesAppDir);
185212

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ export interface Options {
255255
* with this argument. If neither are provided, the version of Electron will be used. Maps
256256
* to the `ProductVersion` metadata property on Windows, and `CFBundleShortVersionString`
257257
* on macOS.
258+
*
259+
* The resolved value is also written to the `version` field of the packaged app's
260+
* `package.json`, which is how the version reaches `app.getVersion()` at runtime on
261+
* platforms without executable version metadata, such as Linux.
258262
*/
259263
appVersion?: string;
260264
/**

test/packager.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
parseInfoPlist,
1616
} from './utils.js';
1717
import { OfficialArch, OfficialPlatform, Options } from '../src/types.js';
18+
import { extractFile } from '@electron/asar';
1819
import { createDownloadOpts, downloadElectronZip } from '../src/download.js';
1920
import plist, { PlistObject } from 'plist';
2021
import { filterCFBundleIdentifier } from '../src/mac.js';
@@ -477,6 +478,58 @@ describe('packager', () => {
477478
});
478479
});
479480

481+
describe('appVersion', () => {
482+
it('writes appVersion into the packaged package.json', async ({ baseOpts }) => {
483+
const opts = {
484+
...baseOpts,
485+
appVersion: '1.2.3',
486+
platform: 'linux',
487+
arch: 'x64',
488+
} as const;
489+
490+
const paths = await packager(opts);
491+
expect(paths).toHaveLength(1);
492+
493+
const asarPath = path.join(
494+
paths[0],
495+
generateResourcesPath({ name: opts.name, platform: opts.platform }),
496+
'app.asar',
497+
);
498+
const packageJSON = JSON.parse(extractFile(asarPath, 'package.json').toString('utf8'));
499+
expect(packageJSON.version).toEqual('1.2.3');
500+
501+
// The user's source package.json must never be modified
502+
const sourcePackageJSON = JSON.parse(
503+
fs.readFileSync(path.join(opts.dir, 'package.json'), 'utf8'),
504+
);
505+
expect(sourcePackageJSON.version).toEqual('4.99.101');
506+
});
507+
508+
it('writes appVersion into the packaged package.json when asar is disabled', async ({
509+
baseOpts,
510+
}) => {
511+
const opts = {
512+
...baseOpts,
513+
appVersion: '5.6.7',
514+
asar: false,
515+
platform: 'linux',
516+
arch: 'x64',
517+
} as const;
518+
519+
const paths = await packager(opts);
520+
expect(paths).toHaveLength(1);
521+
522+
const packageJSONPath = path.join(
523+
paths[0],
524+
generateResourcesPath({ name: opts.name, platform: opts.platform }),
525+
'app',
526+
'package.json',
527+
);
528+
const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, 'utf8'));
529+
expect(packageJSON.version).toEqual('5.6.7');
530+
});
531+
});
532+
480533
it('should ignore previously-packaged out dir', async ({ baseOpts }) => {
481534
const fixture = path.join(__dirname, 'fixtures', 'basic');
482535
const opts = {

test/platform.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
import { afterEach, describe, expect, it } from 'vitest';
5+
import { App } from '../src/platform.js';
6+
import type { ProcessedOptionsWithSinglePlatformArch } from '../src/types.js';
7+
8+
describe('App.writeAppVersion', () => {
9+
let out: string;
10+
11+
afterEach(async () => {
12+
await fs.promises.rm(out, { recursive: true, force: true });
13+
});
14+
15+
async function createApp(appVersion: string | undefined, packageJSON: Record<string, unknown>) {
16+
out = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'electron-packager-platform-test-'));
17+
const opts = {
18+
name: 'writeAppVersionTest',
19+
dir: path.join(__dirname, 'fixtures', 'basic'),
20+
out,
21+
tmpdir: false,
22+
platform: 'linux',
23+
arch: 'x64',
24+
electronVersion: '27.0.0',
25+
appVersion,
26+
ignore: [],
27+
} as unknown as ProcessedOptionsWithSinglePlatformArch;
28+
29+
const app = new App(opts, path.join(out, 'template'));
30+
await fs.promises.mkdir(app.originalResourcesAppDir, { recursive: true });
31+
const packageJSONPath = path.join(app.originalResourcesAppDir, 'package.json');
32+
await fs.promises.writeFile(packageJSONPath, JSON.stringify(packageJSON, null, 2));
33+
34+
return { app, packageJSONPath };
35+
}
36+
37+
it('writes the resolved appVersion to the copied package.json', async () => {
38+
const { app, packageJSONPath } = await createApp('1.2.3', {
39+
name: 'test-app',
40+
main: 'main.js',
41+
version: '0.0.1',
42+
});
43+
44+
await app.writeAppVersion();
45+
46+
const packageJSON = JSON.parse(await fs.promises.readFile(packageJSONPath, 'utf8'));
47+
expect(packageJSON.version).toEqual('1.2.3');
48+
expect(packageJSON.name).toEqual('test-app');
49+
expect(packageJSON.main).toEqual('main.js');
50+
});
51+
52+
it('adds a version field if the copied package.json does not have one', async () => {
53+
const { app, packageJSONPath } = await createApp('4.5.6', {
54+
name: 'test-app',
55+
main: 'main.js',
56+
});
57+
58+
await app.writeAppVersion();
59+
60+
const packageJSON = JSON.parse(await fs.promises.readFile(packageJSONPath, 'utf8'));
61+
expect(packageJSON.version).toEqual('4.5.6');
62+
});
63+
64+
it('does not modify the package.json if appVersion is not resolved', async () => {
65+
const { app, packageJSONPath } = await createApp(undefined, {
66+
name: 'test-app',
67+
main: 'main.js',
68+
});
69+
const originalContents = await fs.promises.readFile(packageJSONPath, 'utf8');
70+
71+
await app.writeAppVersion();
72+
73+
expect(await fs.promises.readFile(packageJSONPath, 'utf8')).toEqual(originalContents);
74+
expect(JSON.parse(originalContents).version).toBeUndefined();
75+
});
76+
77+
it('does not rewrite the package.json if the version already matches', async () => {
78+
const { app, packageJSONPath } = await createApp('1.2.3', {
79+
name: 'test-app',
80+
main: 'main.js',
81+
version: '1.2.3',
82+
});
83+
// Formatting that a rewrite would not preserve
84+
await fs.promises.writeFile(
85+
packageJSONPath,
86+
'{"name":"test-app","main":"main.js","version":"1.2.3"}',
87+
);
88+
89+
await app.writeAppVersion();
90+
91+
expect(await fs.promises.readFile(packageJSONPath, 'utf8')).toEqual(
92+
'{"name":"test-app","main":"main.js","version":"1.2.3"}',
93+
);
94+
});
95+
});

0 commit comments

Comments
 (0)