Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/plugin-lighthouse/src/lib/runner/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import {
getConfig,
normalizeAuditOutputs,
toAuditOutputs,
withLocalTmpDir,
} from './utils.js';

export function createRunnerFunction(
urls: string[],
flags: LighthouseCliFlags = DEFAULT_CLI_FLAGS,
): RunnerFunction {
return async (): Promise<AuditOutputs> => {
return withLocalTmpDir(async (): Promise<AuditOutputs> => {
const config = await getConfig(flags);
const normalizationFlags = enrichFlags(flags);
const isSingleUrl = !shouldExpandForUrls(urls.length);
Expand Down Expand Up @@ -58,7 +59,7 @@ export function createRunnerFunction(
);
}
return normalizeAuditOutputs(allResults, normalizationFlags);
};
});
}

async function runLighthouseForUrl(
Expand Down
32 changes: 32 additions & 0 deletions packages/plugin-lighthouse/src/lib/runner/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import experimentalConfig from 'lighthouse/core/config/experimental-config.js';
import perfConfig from 'lighthouse/core/config/perf-config.js';
import type Details from 'lighthouse/types/lhr/audit-details';
import type { Result } from 'lighthouse/types/lhr/audit-result';
import os from 'node:os';
import path from 'node:path';
import type { AuditOutput, AuditOutputs } from '@code-pushup/models';
import {
formatReportScore,
importModule,
pluginWorkDir,
readJsonFile,
ui,
} from '@code-pushup/utils';
import { LIGHTHOUSE_PLUGIN_SLUG } from '../constants.js';
import type { LighthouseOptions } from '../types.js';
import { logUnsupportedDetails, toAuditDetails } from './details/details.js';
import type { LighthouseCliFlags } from './types.js';
Expand Down Expand Up @@ -167,3 +171,31 @@ export function enrichFlags(
outputPath: urlSpecificOutputPath,
};
}

/**
* Wraps Lighthouse runner with `TEMP` directory override for Windows, to prevent permissions error on cleanup.
*
* `Runtime error encountered: EPERM, Permission denied: \\?\C:\Users\RUNNER~1\AppData\Local\Temp\lighthouse.57724617 '\\?\C:\Users\RUNNER~1\AppData\Local\Temp\lighthouse.57724617'`
*
* @param fn Async function which runs Lighthouse.
* @returns Wrapped function which overrides `TEMP` environment variable, before cleaning up afterwards.
*/
export function withLocalTmpDir<T>(fn: () => Promise<T>): () => Promise<T> {
if (os.platform() !== 'win32') {
return fn;
}

return async () => {
const originalTmpDir = process.env['TEMP'];
process.env['TEMP'] = path.join(
pluginWorkDir(LIGHTHOUSE_PLUGIN_SLUG),
'tmp',
);

try {
return await fn();
} finally {
process.env['TEMP'] = originalTmpDir;
}
};
}
62 changes: 62 additions & 0 deletions packages/plugin-lighthouse/src/lib/runner/utils.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import log from 'lighthouse-logger';
import type Details from 'lighthouse/types/lhr/audit-details';
import type { Result } from 'lighthouse/types/lhr/audit-result';
import { vol } from 'memfs';
import os from 'node:os';
import path from 'node:path';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
Expand All @@ -22,6 +23,7 @@ import {
getConfig,
normalizeAuditOutputs,
toAuditOutputs,
withLocalTmpDir,
} from './utils.js';

// mock bundleRequire inside importEsmModule used for fetching config
Expand Down Expand Up @@ -502,3 +504,63 @@ describe('enrichFlags', () => {
});
});
});

describe('withLocalTmpDir', () => {
it('should return unchanged function on Linux', () => {
vi.spyOn(os, 'platform').mockReturnValue('linux');
const runner = vi.fn().mockResolvedValue('result');

expect(withLocalTmpDir(runner)).toBe(runner);
});

it('should return unchanged function on MacOS', () => {
vi.spyOn(os, 'platform').mockReturnValue('darwin');
const runner = vi.fn().mockResolvedValue('result');

expect(withLocalTmpDir(runner)).toBe(runner);
});

it('should wrap function on Windows', async () => {
vi.spyOn(os, 'platform').mockReturnValue('win32');
const runner = vi.fn().mockResolvedValue('result');

const transformed = withLocalTmpDir(runner);

expect(transformed).not.toBe(runner);
await expect(transformed()).resolves.toBe('result');
expect(runner).toHaveBeenCalled();
});

it('should override TEMP environment variable before function call', async () => {
vi.spyOn(os, 'platform').mockReturnValue('win32');
const runner = vi
.fn()
.mockImplementation(
async () => `TEMP directory is ${process.env['TEMP']}`,
);

await expect(withLocalTmpDir(runner)()).resolves.toBe(
`TEMP directory is ${path.join('node_modules', '.code-pushup', 'lighthouse', 'tmp')}`,
);
});

it('should reset TEMP environment variable after function resolves', async () => {
const originalTmpDir = String.raw`\\?\C:\Users\RUNNER~1\AppData\Local\Temp`;
const runner = vi.fn().mockResolvedValue('result');
vi.spyOn(os, 'platform').mockReturnValue('win32');
vi.stubEnv('TEMP', originalTmpDir);

await withLocalTmpDir(runner)();

expect(process.env['TEMP']).toBe(originalTmpDir);
});

it('should reset TEMP environment variable after function rejects', async () => {
const runner = vi.fn().mockRejectedValue('error');
vi.spyOn(os, 'platform').mockReturnValue('win32');
vi.stubEnv('TEMP', '');

await expect(withLocalTmpDir(runner)()).rejects.toBe('error');
expect(process.env['TEMP']).toBe('');
});
});
Loading