Skip to content

Commit 514c023

Browse files
committed
test: cover sanitizePackageJson default, override, chaining, and hook ordering
1 parent 834b9c9 commit 514c023

1 file changed

Lines changed: 263 additions & 0 deletions

File tree

test/sanitize-package-json.spec.ts

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5+
6+
import { App } from '../src/platform.js';
7+
import {
8+
defaultSanitizePackageJson,
9+
sanitizeAppPackageJson,
10+
} from '../src/sanitize-package-json.js';
11+
import type { ProcessedOptionsWithSinglePlatformArch } from '../src/types.js';
12+
13+
const STRIPPED_FIELDS = {
14+
devDependencies: { typescript: '^5.0.0' },
15+
scripts: { start: 'electron .' },
16+
workspaces: ['packages/*'],
17+
packageManager: 'yarn@4.10.2',
18+
resolutions: { lodash: '4.17.21' },
19+
overrides: { lodash: '4.17.21' },
20+
pnpm: { overrides: {} },
21+
private: true,
22+
publishConfig: { provenance: true },
23+
devEngines: { runtime: { name: 'node' } },
24+
jest: { testEnvironment: 'node' },
25+
eslintConfig: { root: true },
26+
prettier: { semi: false },
27+
browserslist: ['last 2 versions'],
28+
'lint-staged': { '*.js': 'eslint' },
29+
'nano-staged': { '*.js': 'eslint' },
30+
husky: { hooks: {} },
31+
commitlint: { extends: [] },
32+
mocha: { spec: 'test' },
33+
ava: { files: [] },
34+
nyc: { all: true },
35+
c8: { all: true },
36+
tap: { coverage: true },
37+
xo: { semicolon: false },
38+
standard: { env: [] },
39+
};
40+
41+
const KEPT_FIELDS = {
42+
main: 'main.js',
43+
name: 'sanitize-test',
44+
productName: 'Sanitize Test',
45+
version: '1.2.3',
46+
type: 'module',
47+
desktopName: 'sanitize-test.desktop',
48+
imports: { '#internal': './src/internal.js' },
49+
exports: { '.': './main.js' },
50+
dependencies: { debug: '^4.4.1' },
51+
optionalDependencies: { fsevents: '^2.3.3' },
52+
peerDependencies: { electron: '>=30.0.0' },
53+
};
54+
55+
function fullPackageJson(): Record<string, unknown> {
56+
return structuredClone({ ...KEPT_FIELDS, ...STRIPPED_FIELDS });
57+
}
58+
59+
describe('defaultSanitizePackageJson', () => {
60+
it('strips development-only fields', () => {
61+
const sanitized = defaultSanitizePackageJson(fullPackageJson());
62+
for (const field of Object.keys(STRIPPED_FIELDS)) {
63+
expect(sanitized, `expected ${field} to be stripped`).not.toHaveProperty([field]);
64+
}
65+
});
66+
67+
it('keeps runtime-relevant fields', () => {
68+
const sanitized = defaultSanitizePackageJson(fullPackageJson());
69+
expect(sanitized).toEqual(KEPT_FIELDS);
70+
});
71+
});
72+
73+
describe('sanitizeAppPackageJson', () => {
74+
let bundledAppDir: string;
75+
76+
beforeEach(async () => {
77+
bundledAppDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'packager-sanitize-'));
78+
await fs.promises.writeFile(
79+
path.join(bundledAppDir, 'package.json'),
80+
JSON.stringify(fullPackageJson()),
81+
);
82+
});
83+
84+
afterEach(async () => {
85+
await fs.promises.rm(bundledAppDir, { recursive: true, force: true });
86+
});
87+
88+
async function readBundledPackageJson(): Promise<{
89+
raw: string;
90+
parsed: Record<string, unknown>;
91+
}> {
92+
const raw = await fs.promises.readFile(path.join(bundledAppDir, 'package.json'), 'utf8');
93+
return { raw, parsed: JSON.parse(raw) };
94+
}
95+
96+
it('writes the default-sanitized package.json to disk with 2-space indentation', async () => {
97+
await sanitizeAppPackageJson({} as ProcessedOptionsWithSinglePlatformArch, bundledAppDir);
98+
99+
const { raw, parsed } = await readBundledPackageJson();
100+
expect(parsed).toEqual(KEPT_FIELDS);
101+
expect(raw).toBe(`${JSON.stringify(KEPT_FIELDS, null, 2)}\n`);
102+
});
103+
104+
it('applies the default sanitizer when an empty array is provided', async () => {
105+
const opts = {
106+
sanitizePackageJson: [],
107+
} as unknown as ProcessedOptionsWithSinglePlatformArch;
108+
await sanitizeAppPackageJson(opts, bundledAppDir);
109+
110+
const { parsed } = await readBundledPackageJson();
111+
expect(parsed).toEqual(KEPT_FIELDS);
112+
});
113+
114+
it('replaces the default sanitizer with user-provided functions', async () => {
115+
const opts = {
116+
sanitizePackageJson: [
117+
async (packageJson: Record<string, unknown>) => {
118+
delete packageJson.scripts;
119+
packageJson.userField = 'set by user';
120+
return packageJson;
121+
},
122+
],
123+
} as unknown as ProcessedOptionsWithSinglePlatformArch;
124+
await sanitizeAppPackageJson(opts, bundledAppDir);
125+
126+
const { parsed } = await readBundledPackageJson();
127+
expect(parsed).not.toHaveProperty('scripts');
128+
expect(parsed).toHaveProperty('userField', 'set by user');
129+
// the default sanitizer must NOT also run
130+
expect(parsed).toHaveProperty('devDependencies');
131+
expect(parsed).toHaveProperty(['lint-staged']);
132+
});
133+
134+
it('writes the object returned by the user-provided sanitizer', async () => {
135+
const opts = {
136+
sanitizePackageJson: [() => ({ name: 'replaced', main: 'main.js' })],
137+
} as unknown as ProcessedOptionsWithSinglePlatformArch;
138+
await sanitizeAppPackageJson(opts, bundledAppDir);
139+
140+
const { parsed } = await readBundledPackageJson();
141+
expect(parsed).toEqual({ name: 'replaced', main: 'main.js' });
142+
});
143+
144+
it('runs multiple sanitizers serially, each receiving the previous result', async () => {
145+
const callOrder: string[] = [];
146+
const opts = {
147+
electronVersion: '30.0.0',
148+
platform: 'darwin',
149+
arch: 'arm64',
150+
sanitizePackageJson: [
151+
(
152+
packageJson: Record<string, unknown>,
153+
electronVersion: string,
154+
platform: string,
155+
arch: string,
156+
) => {
157+
callOrder.push(`first:${electronVersion}:${platform}:${arch}`);
158+
return { fromFirst: Object.keys(packageJson).length };
159+
},
160+
async (
161+
packageJson: Record<string, unknown>,
162+
electronVersion: string,
163+
platform: string,
164+
arch: string,
165+
) => {
166+
callOrder.push(`second:${electronVersion}:${platform}:${arch}`);
167+
return { ...packageJson, fromSecond: true };
168+
},
169+
],
170+
} as unknown as ProcessedOptionsWithSinglePlatformArch;
171+
await sanitizeAppPackageJson(opts, bundledAppDir);
172+
173+
expect(callOrder).toEqual(['first:30.0.0:darwin:arm64', 'second:30.0.0:darwin:arm64']);
174+
const { parsed } = await readBundledPackageJson();
175+
expect(parsed).toEqual({
176+
fromFirst: Object.keys(fullPackageJson()).length,
177+
fromSecond: true,
178+
});
179+
});
180+
181+
it('does nothing when no package.json exists', async () => {
182+
await fs.promises.rm(path.join(bundledAppDir, 'package.json'));
183+
await expect(
184+
sanitizeAppPackageJson({} as ProcessedOptionsWithSinglePlatformArch, bundledAppDir),
185+
).resolves.toBeUndefined();
186+
expect(fs.existsSync(path.join(bundledAppDir, 'package.json'))).toBe(false);
187+
});
188+
});
189+
190+
describe('copyTemplate', () => {
191+
let appDir: string;
192+
let tmpBase: string;
193+
194+
beforeEach(async () => {
195+
appDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'packager-sanitize-app-'));
196+
tmpBase = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'packager-sanitize-tmp-'));
197+
await fs.promises.writeFile(
198+
path.join(appDir, 'package.json'),
199+
JSON.stringify(fullPackageJson()),
200+
);
201+
await fs.promises.writeFile(path.join(appDir, 'main.js'), '');
202+
});
203+
204+
afterEach(async () => {
205+
for (const dir of [appDir, tmpBase]) {
206+
await fs.promises.rm(dir, { recursive: true, force: true });
207+
}
208+
});
209+
210+
function baseOpts(): ProcessedOptionsWithSinglePlatformArch {
211+
return {
212+
dir: appDir,
213+
name: 'sanitizeTest',
214+
appVersion: '1.2.3',
215+
electronVersion: '30.0.0',
216+
platform: 'linux',
217+
arch: 'x64',
218+
ignore: [],
219+
tmpdir: tmpBase,
220+
quiet: true,
221+
};
222+
}
223+
224+
it('sanitizes after the afterCopy hooks and before the afterPrune hooks', async () => {
225+
const devDependenciesSeen: Record<string, boolean> = {};
226+
const readDevDependencies = (hookName: string) => {
227+
return async ({ buildPath }: { buildPath: string }) => {
228+
const packageJson = JSON.parse(
229+
await fs.promises.readFile(path.join(buildPath, 'package.json'), 'utf8'),
230+
);
231+
devDependenciesSeen[hookName] = 'devDependencies' in packageJson;
232+
};
233+
};
234+
const opts = {
235+
...baseOpts(),
236+
prune: true,
237+
afterCopy: [readDevDependencies('afterCopy')],
238+
afterPrune: [readDevDependencies('afterPrune')],
239+
};
240+
241+
const app = new App(opts, path.join(tmpBase, 'unused-template'));
242+
await app.copyTemplate();
243+
244+
expect(devDependenciesSeen).toEqual({ afterCopy: true, afterPrune: false });
245+
const bundledPackageJson = JSON.parse(
246+
await fs.promises.readFile(path.join(app.originalResourcesAppDir, 'package.json'), 'utf8'),
247+
);
248+
expect(bundledPackageJson).toEqual(KEPT_FIELDS);
249+
});
250+
251+
it('is incompatible with prebuiltAsar', async () => {
252+
const opts = {
253+
...baseOpts(),
254+
prebuiltAsar: path.join(appDir, 'app.asar'),
255+
sanitizePackageJson: [(packageJson: Record<string, unknown>) => packageJson],
256+
};
257+
258+
const app = new App(opts, path.join(tmpBase, 'unused-template'));
259+
await expect(app.copyPrebuiltAsar()).rejects.toThrow(
260+
'sanitizePackageJson is incompatible with prebuiltAsar',
261+
);
262+
});
263+
});

0 commit comments

Comments
 (0)