Skip to content

Commit ac43d41

Browse files
committed
fix(plugin-lighthouse): prevent cleanup permissions error on windows
1 parent b7ce5c6 commit ac43d41

File tree

3 files changed

+97
-2
lines changed

3 files changed

+97
-2
lines changed

packages/plugin-lighthouse/src/lib/runner/runner.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import {
1212
getConfig,
1313
normalizeAuditOutputs,
1414
toAuditOutputs,
15+
withLocalTmpDir,
1516
} from './utils.js';
1617

1718
export function createRunnerFunction(
1819
urls: string[],
1920
flags: LighthouseCliFlags = DEFAULT_CLI_FLAGS,
2021
): RunnerFunction {
21-
return async (): Promise<AuditOutputs> => {
22+
return withLocalTmpDir(async (): Promise<AuditOutputs> => {
2223
const config = await getConfig(flags);
2324
const normalizationFlags = enrichFlags(flags);
2425
const isSingleUrl = !shouldExpandForUrls(urls.length);
@@ -58,7 +59,7 @@ export function createRunnerFunction(
5859
);
5960
}
6061
return normalizeAuditOutputs(allResults, normalizationFlags);
61-
};
62+
});
6263
}
6364

6465
async function runLighthouseForUrl(

packages/plugin-lighthouse/src/lib/runner/utils.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ import experimentalConfig from 'lighthouse/core/config/experimental-config.js';
66
import perfConfig from 'lighthouse/core/config/perf-config.js';
77
import type Details from 'lighthouse/types/lhr/audit-details';
88
import type { Result } from 'lighthouse/types/lhr/audit-result';
9+
import os from 'node:os';
10+
import path from 'node:path';
911
import type { AuditOutput, AuditOutputs } from '@code-pushup/models';
1012
import {
1113
formatReportScore,
1214
importModule,
15+
pluginWorkDir,
1316
readJsonFile,
1417
ui,
1518
} from '@code-pushup/utils';
19+
import { LIGHTHOUSE_PLUGIN_SLUG } from '../constants.js';
1620
import type { LighthouseOptions } from '../types.js';
1721
import { logUnsupportedDetails, toAuditDetails } from './details/details.js';
1822
import type { LighthouseCliFlags } from './types.js';
@@ -167,3 +171,31 @@ export function enrichFlags(
167171
outputPath: urlSpecificOutputPath,
168172
};
169173
}
174+
175+
/**
176+
* Wraps Lighthouse runner with `TEMP` directory override for Windows, to prevent permissions error on cleanup.
177+
*
178+
* `Runtime error encountered: EPERM, Permission denied: \\?\C:\Users\RUNNER~1\AppData\Local\Temp\lighthouse.57724617 '\\?\C:\Users\RUNNER~1\AppData\Local\Temp\lighthouse.57724617'`
179+
*
180+
* @param fn Async function which runs Lighthouse.
181+
* @returns Wrapped function which overrides `TEMP` environment variable, before cleaning up afterwards.
182+
*/
183+
export function withLocalTmpDir<T>(fn: () => Promise<T>): () => Promise<T> {
184+
if (os.platform() !== 'win32') {
185+
return fn;
186+
}
187+
188+
return async () => {
189+
const originalTmpDir = process.env['TEMP'];
190+
process.env['TEMP'] = path.join(
191+
pluginWorkDir(LIGHTHOUSE_PLUGIN_SLUG),
192+
'tmp',
193+
);
194+
195+
try {
196+
return await fn();
197+
} finally {
198+
process.env['TEMP'] = originalTmpDir;
199+
}
200+
};
201+
}

packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import log from 'lighthouse-logger';
44
import type Details from 'lighthouse/types/lhr/audit-details';
55
import type { Result } from 'lighthouse/types/lhr/audit-result';
66
import { vol } from 'memfs';
7+
import os from 'node:os';
78
import path from 'node:path';
89
import { beforeEach, describe, expect, it, vi } from 'vitest';
910
import {
@@ -22,6 +23,7 @@ import {
2223
getConfig,
2324
normalizeAuditOutputs,
2425
toAuditOutputs,
26+
withLocalTmpDir,
2527
} from './utils.js';
2628

2729
// mock bundleRequire inside importEsmModule used for fetching config
@@ -502,3 +504,63 @@ describe('enrichFlags', () => {
502504
});
503505
});
504506
});
507+
508+
describe('withLocalTmpDir', () => {
509+
it('should return unchanged function on Linux', () => {
510+
vi.spyOn(os, 'platform').mockReturnValue('linux');
511+
const runner = vi.fn().mockResolvedValue('result');
512+
513+
expect(withLocalTmpDir(runner)).toBe(runner);
514+
});
515+
516+
it('should return unchanged function on MacOS', () => {
517+
vi.spyOn(os, 'platform').mockReturnValue('darwin');
518+
const runner = vi.fn().mockResolvedValue('result');
519+
520+
expect(withLocalTmpDir(runner)).toBe(runner);
521+
});
522+
523+
it('should wrap function on Windows', async () => {
524+
vi.spyOn(os, 'platform').mockReturnValue('win32');
525+
const runner = vi.fn().mockResolvedValue('result');
526+
527+
const transformed = withLocalTmpDir(runner);
528+
529+
expect(transformed).not.toBe(runner);
530+
await expect(transformed()).resolves.toBe('result');
531+
expect(runner).toHaveBeenCalled();
532+
});
533+
534+
it('should override TEMP environment variable before function call', async () => {
535+
vi.spyOn(os, 'platform').mockReturnValue('win32');
536+
const runner = vi
537+
.fn()
538+
.mockImplementation(
539+
async () => `TEMP directory is ${process.env['TEMP']}`,
540+
);
541+
542+
await expect(withLocalTmpDir(runner)()).resolves.toBe(
543+
`TEMP directory is ${path.join('node_modules', '.code-pushup', 'lighthouse', 'tmp')}`,
544+
);
545+
});
546+
547+
it('should reset TEMP environment variable after function resolves', async () => {
548+
const originalTmpDir = String.raw`\\?\C:\Users\RUNNER~1\AppData\Local\Temp`;
549+
const runner = vi.fn().mockResolvedValue('result');
550+
vi.spyOn(os, 'platform').mockReturnValue('win32');
551+
vi.stubEnv('TEMP', originalTmpDir);
552+
553+
await withLocalTmpDir(runner)();
554+
555+
expect(process.env['TEMP']).toBe(originalTmpDir);
556+
});
557+
558+
it('should reset TEMP environment variable after function rejects', async () => {
559+
const runner = vi.fn().mockRejectedValue('error');
560+
vi.spyOn(os, 'platform').mockReturnValue('win32');
561+
vi.stubEnv('TEMP', '');
562+
563+
await expect(withLocalTmpDir(runner)()).rejects.toBe('error');
564+
expect(process.env['TEMP']).toBe('');
565+
});
566+
});

0 commit comments

Comments
 (0)