chore: streamline config loader (#29627)

This commit is contained in:
Pavel Feldman 2024-02-22 15:14:13 -08:00 committed by GitHub
parent adccd39b01
commit ee93136132
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 99 additions and 142 deletions

View File

@ -17,7 +17,7 @@
import fs from 'fs';
import path from 'path';
import { Watcher } from 'playwright/lib/fsWatcher';
import { loadConfigFromFile } from 'playwright/lib/common/configLoader';
import { loadConfigFromFileRestartIfNeeded } from 'playwright/lib/common/configLoader';
import { Runner } from 'playwright/lib/runner/runner';
import type { PluginContext } from 'rollup';
import { source as injectedSource } from './generated/indexSource';
@ -25,7 +25,7 @@ import { createConfig, populateComponentsFromTests, resolveDirs, transformIndexF
import type { ComponentRegistry } from './viteUtils';
export async function runDevServer(configFile: string) {
const config = await loadConfigFromFile(configFile);
const config = await loadConfigFromFileRestartIfNeeded(configFile);
if (!config)
return;

View File

@ -26,6 +26,11 @@ import type { ConfigCLIOverrides } from './ipc';
import type { FullConfig, FullProject } from '../../types/test';
import { setTransformConfig } from '../transform/transform';
export type ConfigLocation = {
resolvedConfigFile?: string;
configDir: string;
};
export type FixturesWithLocation = {
fixtures: Fixtures;
location: Location;
@ -58,53 +63,54 @@ export class FullConfigInternal {
return (config as any)[configInternalSymbol];
}
constructor(configDir: string, configFile: string | undefined, config: Config, configCLIOverrides: ConfigCLIOverrides) {
if (configCLIOverrides.projects && config.projects)
constructor(location: ConfigLocation, userConfig: Config, configCLIOverrides: ConfigCLIOverrides) {
if (configCLIOverrides.projects && userConfig.projects)
throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`);
const { resolvedConfigFile, configDir } = location;
const packageJsonPath = getPackageJsonPath(configDir);
const packageJsonDir = packageJsonPath ? path.dirname(packageJsonPath) : undefined;
const throwawayArtifactsPath = packageJsonDir || process.cwd();
this.configDir = configDir;
this.configCLIOverrides = configCLIOverrides;
this.globalOutputDir = takeFirst(configCLIOverrides.outputDir, pathResolve(configDir, config.outputDir), throwawayArtifactsPath, path.resolve(process.cwd()));
this.globalOutputDir = takeFirst(configCLIOverrides.outputDir, pathResolve(configDir, userConfig.outputDir), throwawayArtifactsPath, path.resolve(process.cwd()));
this.preserveOutputDir = configCLIOverrides.preserveOutputDir || false;
this.ignoreSnapshots = takeFirst(configCLIOverrides.ignoreSnapshots, config.ignoreSnapshots, false);
const privateConfiguration = (config as any)['@playwright/test'];
this.ignoreSnapshots = takeFirst(configCLIOverrides.ignoreSnapshots, userConfig.ignoreSnapshots, false);
const privateConfiguration = (userConfig as any)['@playwright/test'];
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
this.config = {
configFile,
rootDir: pathResolve(configDir, config.testDir) || configDir,
forbidOnly: takeFirst(configCLIOverrides.forbidOnly, config.forbidOnly, false),
fullyParallel: takeFirst(configCLIOverrides.fullyParallel, config.fullyParallel, false),
globalSetup: takeFirst(resolveScript(config.globalSetup, configDir), null),
globalTeardown: takeFirst(resolveScript(config.globalTeardown, configDir), null),
globalTimeout: takeFirst(configCLIOverrides.globalTimeout, config.globalTimeout, 0),
grep: takeFirst(config.grep, defaultGrep),
grepInvert: takeFirst(config.grepInvert, null),
maxFailures: takeFirst(configCLIOverrides.maxFailures, config.maxFailures, 0),
metadata: takeFirst(config.metadata, {}),
preserveOutput: takeFirst(config.preserveOutput, 'always'),
reporter: takeFirst(configCLIOverrides.reporter, resolveReporters(config.reporter, configDir), [[defaultReporter]]),
reportSlowTests: takeFirst(config.reportSlowTests, { max: 5, threshold: 15000 }),
quiet: takeFirst(configCLIOverrides.quiet, config.quiet, false),
configFile: resolvedConfigFile,
rootDir: pathResolve(configDir, userConfig.testDir) || configDir,
forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false),
fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false),
globalSetup: takeFirst(resolveScript(userConfig.globalSetup, configDir), null),
globalTeardown: takeFirst(resolveScript(userConfig.globalTeardown, configDir), null),
globalTimeout: takeFirst(configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0),
grep: takeFirst(userConfig.grep, defaultGrep),
grepInvert: takeFirst(userConfig.grepInvert, null),
maxFailures: takeFirst(configCLIOverrides.maxFailures, userConfig.maxFailures, 0),
metadata: takeFirst(userConfig.metadata, {}),
preserveOutput: takeFirst(userConfig.preserveOutput, 'always'),
reporter: takeFirst(configCLIOverrides.reporter, resolveReporters(userConfig.reporter, configDir), [[defaultReporter]]),
reportSlowTests: takeFirst(userConfig.reportSlowTests, { max: 5, threshold: 15000 }),
quiet: takeFirst(configCLIOverrides.quiet, userConfig.quiet, false),
projects: [],
shard: takeFirst(configCLIOverrides.shard, config.shard, null),
updateSnapshots: takeFirst(configCLIOverrides.updateSnapshots, config.updateSnapshots, 'missing'),
shard: takeFirst(configCLIOverrides.shard, userConfig.shard, null),
updateSnapshots: takeFirst(configCLIOverrides.updateSnapshots, userConfig.updateSnapshots, 'missing'),
version: require('../../package.json').version,
workers: 0,
webServer: null,
};
for (const key in config) {
for (const key in userConfig) {
if (key.startsWith('@'))
(this.config as any)[key] = (config as any)[key];
(this.config as any)[key] = (userConfig as any)[key];
}
(this.config as any)[configInternalSymbol] = this;
const workers = takeFirst(configCLIOverrides.workers, config.workers, '50%');
const workers = takeFirst(configCLIOverrides.workers, userConfig.workers, '50%');
if (typeof workers === 'string') {
if (workers.endsWith('%')) {
const cpus = os.cpus().length;
@ -116,7 +122,7 @@ export class FullConfigInternal {
this.config.workers = workers;
}
const webServers = takeFirst(config.webServer, null);
const webServers = takeFirst(userConfig.webServer, null);
if (Array.isArray(webServers)) { // multiple web server mode
// Due to previous choices, this value shows up to the user in globalSetup as part of FullConfig. Arrays are not supported by the old type.
this.config.webServer = null;
@ -128,13 +134,13 @@ export class FullConfigInternal {
this.webServers = [];
}
const projectConfigs = configCLIOverrides.projects || config.projects || [config];
this.projects = projectConfigs.map(p => new FullProjectInternal(configDir, config, this, p, this.configCLIOverrides, throwawayArtifactsPath));
const projectConfigs = configCLIOverrides.projects || userConfig.projects || [userConfig];
this.projects = projectConfigs.map(p => new FullProjectInternal(configDir, userConfig, this, p, this.configCLIOverrides, throwawayArtifactsPath));
resolveProjectDependencies(this.projects);
this._assignUniqueProjectIds(this.projects);
setTransformConfig({
babelPlugins: privateConfiguration?.babelPlugins || [],
external: config.build?.external || [],
external: userConfig.build?.external || [],
});
this.config.projects = this.projects.map(p => p.project);
}

View File

@ -21,7 +21,7 @@ import type { ConfigCLIOverrides, SerializedConfig } from './ipc';
import { requireOrImport } from '../transform/transform';
import type { Config, Project } from '../../types/test';
import { errorWithFile, fileIsModule } from '../util';
import { setCurrentConfig } from './globals';
import type { ConfigLocation } from './config';
import { FullConfigInternal } from './config';
import { addToCompilationCache } from '../transform/compilationCache';
import { initializeEsmLoader, registerESMLoader } from './esmLoaderHost';
@ -84,60 +84,34 @@ export const defineConfig = (...configs: any[]) => {
return result;
};
export class ConfigLoader {
private _configCLIOverrides: ConfigCLIOverrides;
private _fullConfig: FullConfigInternal | undefined;
export async function deserializeConfig(data: SerializedConfig): Promise<FullConfigInternal> {
if (data.compilationCache)
addToCompilationCache(data.compilationCache);
constructor(configCLIOverrides?: ConfigCLIOverrides) {
this._configCLIOverrides = configCLIOverrides || {};
}
static async deserialize(data: SerializedConfig): Promise<FullConfigInternal> {
if (data.compilationCache)
addToCompilationCache(data.compilationCache);
const loader = new ConfigLoader(data.configCLIOverrides);
const config = data.configFile ? await loader.loadConfigFile(data.configFile) : await loader.loadEmptyConfig(data.configDir);
await initializeEsmLoader();
return config;
}
async loadConfigFile(file: string, ignoreProjectDependencies = false): Promise<FullConfigInternal> {
if (this._fullConfig)
throw new Error('Cannot load two config files');
const config = await requireOrImportDefaultObject(file) as Config;
const fullConfig = await this._loadConfig(config, path.dirname(file), file);
setCurrentConfig(fullConfig);
if (ignoreProjectDependencies) {
for (const project of fullConfig.projects) {
project.deps = [];
project.teardown = undefined;
}
}
this._fullConfig = fullConfig;
return fullConfig;
}
async loadEmptyConfig(configDir: string): Promise<FullConfigInternal> {
const fullConfig = await this._loadConfig({}, configDir);
setCurrentConfig(fullConfig);
return fullConfig;
}
private async _loadConfig(config: Config, configDir: string, configFile?: string): Promise<FullConfigInternal> {
// 1. Validate data provided in the config file.
validateConfig(configFile || '<default config>', config);
const fullConfig = new FullConfigInternal(configDir, configFile, config, this._configCLIOverrides);
fullConfig.defineConfigWasUsed = !!(config as any)[kDefineConfigWasUsed];
return fullConfig;
}
const config = await loadConfig(data.location, data.configCLIOverrides);
await initializeEsmLoader();
return config;
}
async function requireOrImportDefaultObject(file: string) {
let object = await requireOrImport(file);
export async function loadUserConfig(location: ConfigLocation): Promise<Config> {
let object = location.resolvedConfigFile ? await requireOrImport(location.resolvedConfigFile) : {};
if (object && typeof object === 'object' && ('default' in object))
object = object['default'];
return object;
return object as Config;
}
export async function loadConfig(location: ConfigLocation, overrides?: ConfigCLIOverrides, ignoreProjectDependencies = false): Promise<FullConfigInternal> {
const userConfig = await loadUserConfig(location);
validateConfig(location.resolvedConfigFile || '<default config>', userConfig);
const fullConfig = new FullConfigInternal(location, userConfig, overrides || {});
fullConfig.defineConfigWasUsed = !!(userConfig as any)[kDefineConfigWasUsed];
if (ignoreProjectDependencies) {
for (const project of fullConfig.projects) {
project.deps = [];
project.teardown = undefined;
}
}
return fullConfig;
}
function validateConfig(file: string, config: Config) {
@ -312,7 +286,7 @@ function validateProject(file: string, project: Project, title: string) {
}
}
export function resolveConfigFile(configFileOrDirectory: string): string | null {
export function resolveConfigFile(configFileOrDirectory: string): string | undefined {
const resolveConfig = (configFile: string) => {
if (fs.existsSync(configFile))
return configFile;
@ -334,7 +308,7 @@ export function resolveConfigFile(configFileOrDirectory: string): string | null
if (configFile)
return configFile;
// If there is no config, assume this as a root testing directory.
return null;
return undefined;
} else {
// When passed a file, it must be a config file.
const configFile = resolveConfig(configFileOrDirectory);
@ -342,21 +316,24 @@ export function resolveConfigFile(configFileOrDirectory: string): string | null
}
}
export async function loadConfigFromFile(configFile: string | undefined, overrides?: ConfigCLIOverrides, ignoreDeps?: boolean): Promise<FullConfigInternal | null> {
export async function loadConfigFromFileRestartIfNeeded(configFile: string | undefined, overrides?: ConfigCLIOverrides, ignoreDeps?: boolean): Promise<FullConfigInternal | null> {
const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd();
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory);
if (restartWithExperimentalTsEsm(resolvedConfigFile))
return null;
const configLoader = new ConfigLoader(overrides);
let config: FullConfigInternal;
if (resolvedConfigFile)
config = await configLoader.loadConfigFile(resolvedConfigFile, ignoreDeps);
else
config = await configLoader.loadEmptyConfig(configFileOrDirectory);
return config;
const location: ConfigLocation = {
configDir: resolvedConfigFile ? path.dirname(resolvedConfigFile) : configFileOrDirectory,
resolvedConfigFile,
};
return await loadConfig(location, overrides, ignoreDeps);
}
export function restartWithExperimentalTsEsm(configFile: string | null): boolean {
export async function loadEmptyConfigForMergeReports() {
// Merge reports is "different" for no good reason. It should not pick up local config from the cwd.
return await loadConfig({ configDir: process.cwd() });
}
export function restartWithExperimentalTsEsm(configFile: string | undefined): boolean {
const nodeVersion = +process.versions.node.split('.')[0];
// New experimental loader is only supported on Node 16+.
if (nodeVersion < 16)

View File

@ -16,7 +16,6 @@
import type { TestInfoImpl } from '../worker/testInfo';
import type { Suite } from './test';
import type { FullConfigInternal } from './config';
let currentTestInfoValue: TestInfoImpl | null = null;
export function setCurrentTestInfo(testInfo: TestInfoImpl | null) {
@ -61,11 +60,3 @@ export function setIsWorkerProcess() {
export function isWorkerProcess() {
return _isWorkerProcess;
}
let currentConfigValue: FullConfigInternal | null = null;
export function setCurrentConfig(config: FullConfigInternal | null) {
currentConfigValue = config;
}
export function currentConfig(): FullConfigInternal | null {
return currentConfigValue;
}

View File

@ -16,7 +16,7 @@
import util from 'util';
import { serializeCompilationCache } from '../transform/compilationCache';
import type { FullConfigInternal } from './config';
import type { ConfigLocation, FullConfigInternal } from './config';
import type { ReporterDescription, TestInfoError, TestStatus } from '../../types/test';
export type ConfigCLIOverrides = {
@ -41,8 +41,7 @@ export type ConfigCLIOverrides = {
};
export type SerializedConfig = {
configFile: string | undefined;
configDir: string;
location: ConfigLocation;
configCLIOverrides: ConfigCLIOverrides;
compilationCache: any;
};
@ -138,8 +137,7 @@ export type EnvProducedPayload = [string, string | null][];
export function serializeConfig(config: FullConfigInternal, passCompilationCache: boolean): SerializedConfig {
const result: SerializedConfig = {
configFile: config.config.configFile,
configDir: config.configDir,
location: { configDir: config.configDir, resolvedConfigFile: config.config.configFile },
configCLIOverrides: config.configCLIOverrides,
compilationCache: passCompilationCache ? serializeCompilationCache() : undefined,
};

View File

@ -15,7 +15,7 @@
*/
import type { SerializedConfig } from '../common/ipc';
import { ConfigLoader } from '../common/configLoader';
import { deserializeConfig } from '../common/configLoader';
import { ProcessRunner } from '../common/process';
import type { FullConfigInternal } from '../common/config';
import { loadTestFile } from '../common/testLoader';
@ -36,7 +36,7 @@ export class LoaderMain extends ProcessRunner {
private _config(): Promise<FullConfigInternal> {
if (!this._configPromise)
this._configPromise = ConfigLoader.deserialize(this._serializedConfig);
this._configPromise = deserializeConfig(this._serializedConfig);
return this._configPromise;
}

View File

@ -24,12 +24,11 @@ import { stopProfiling, startProfiling, gracefullyProcessExitDoNotHang } from 'p
import { serializeError } from './util';
import { showHTMLReport } from './reporters/html';
import { createMergedReport } from './reporters/merge';
import { ConfigLoader, loadConfigFromFile } from './common/configLoader';
import { loadConfigFromFileRestartIfNeeded, loadEmptyConfigForMergeReports } from './common/configLoader';
import type { ConfigCLIOverrides } from './common/ipc';
import type { FullResult, TestError } from '../types/testReporter';
import type { FullConfig, TraceMode } from '../types/test';
import { builtInReporters, defaultReporter, defaultTimeout } from './common/config';
import type { FullConfigInternal } from './common/config';
import { program } from 'playwright-core/lib/cli/program';
export { program } from 'playwright-core/lib/cli/program';
import type { ReporterDescription } from '../types/test';
@ -74,7 +73,7 @@ function addClearCacheCommand(program: Command) {
command.description('clears build and test caches');
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
command.action(async opts => {
const configInternal = await loadConfigFromFile(opts.config);
const configInternal = await loadConfigFromFileRestartIfNeeded(opts.config);
if (!configInternal)
return;
const { config, configDir } = configInternal;
@ -154,7 +153,7 @@ Examples:
async function runTests(args: string[], opts: { [key: string]: any }) {
await startProfiling();
const config = await loadConfigFromFile(opts.config, overridesFromOptions(opts), opts.deps === false);
const config = await loadConfigFromFileRestartIfNeeded(opts.config, overridesFromOptions(opts), opts.deps === false);
if (!config)
return;
@ -183,7 +182,7 @@ export async function withRunnerAndMutedWrite(configFile: string | undefined, ca
const stdoutWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = ((a: any, b: any, c: any) => process.stderr.write(a, b, c)) as any;
try {
const config = await loadConfigFromFile(configFile);
const config = await loadConfigFromFileRestartIfNeeded(configFile);
if (!config)
return;
const runner = new Runner(config);
@ -209,13 +208,7 @@ async function listTestFiles(opts: { [key: string]: any }) {
async function mergeReports(reportDir: string | undefined, opts: { [key: string]: any }) {
const configFile = opts.config;
let config: FullConfigInternal | null;
if (configFile) {
config = await loadConfigFromFile(configFile);
} else {
const configLoader = new ConfigLoader();
config = await configLoader.loadEmptyConfig(process.cwd());
}
const config = configFile ? await loadConfigFromFileRestartIfNeeded(configFile) : await loadEmptyConfigForMergeReports();
if (!config)
return;

View File

@ -16,12 +16,11 @@
import type http from 'http';
import path from 'path';
import { ManualPromise, createGuid } from 'playwright-core/lib/utils';
import { ManualPromise, createGuid, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils';
import { WSServer } from 'playwright-core/lib/utils';
import type { WebSocket } from 'playwright-core/lib/utilsBundle';
import type { FullResult } from 'playwright/types/testReporter';
import type { FullConfigInternal } from '../common/config';
import { ConfigLoader, resolveConfigFile, restartWithExperimentalTsEsm } from '../common/configLoader';
import { loadConfig, resolveConfigFile, restartWithExperimentalTsEsm } from '../common/configLoader';
import { InternalReporter } from '../reporters/internalReporter';
import { Multiplexer } from '../reporters/multiplexer';
import { createReporters } from './reporters';
@ -29,6 +28,7 @@ import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer } from
import type { ConfigCLIOverrides } from '../common/ipc';
import { Runner } from './runner';
import type { FindRelatedTestFilesReport } from './runner';
import type { ConfigLocation } from '../common/config';
type PlaywrightTestOptions = {
headed?: boolean,
@ -40,11 +40,6 @@ type PlaywrightTestOptions = {
connectWsEndpoint?: string;
};
type ConfigPaths = {
configFile: string | null;
configDir: string;
};
export async function runTestServer(configFile: string | undefined) {
process.env.PW_TEST_HTML_REPORT_OPEN = 'never';
@ -52,8 +47,8 @@ export async function runTestServer(configFile: string | undefined) {
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory);
if (restartWithExperimentalTsEsm(resolvedConfigFile))
return null;
const configPaths: ConfigPaths = {
configFile: resolvedConfigFile,
const configPaths: ConfigLocation = {
resolvedConfigFile,
configDir: resolvedConfigFile ? path.dirname(resolvedConfigFile) : configFileOrDirectory
};
@ -77,16 +72,19 @@ export async function runTestServer(configFile: string | undefined) {
});
const url = await wss.listen(0, 'localhost', '/' + createGuid());
// eslint-disable-next-line no-console
process.on('exit', () => wss.close().catch(console.error));
// eslint-disable-next-line no-console
console.log(`Listening on ${url}`);
process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0));
}
class Dispatcher {
private _testRun: { run: Promise<FullResult['status']>, stop: ManualPromise<void> } | undefined;
private _ws: WebSocket;
private _configPaths: ConfigPaths;
private _configLocation: ConfigLocation;
constructor(configPaths: ConfigPaths, ws: WebSocket) {
this._configPaths = configPaths;
constructor(configLocation: ConfigLocation, ws: WebSocket) {
this._configLocation = configLocation;
this._ws = ws;
process.stdout.write = ((chunk: string | Buffer, cb?: Buffer | Function, cb2?: Function) => {
@ -190,13 +188,7 @@ class Dispatcher {
}
private async _loadConfig(overrides: ConfigCLIOverrides) {
const configLoader = new ConfigLoader(overrides);
let config: FullConfigInternal;
if (this._configPaths.configFile)
config = await configLoader.loadConfigFile(this._configPaths.configFile, false);
else
config = await configLoader.loadEmptyConfig(this._configPaths.configDir);
return config;
return await loadConfig(this._configLocation, overrides);
}
private _dispatchEvent(method: string, params: any) {

View File

@ -18,7 +18,7 @@ import { colors } from 'playwright-core/lib/utilsBundle';
import { debugTest, formatLocation, relativeFilePath, serializeError } from '../util';
import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc';
import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals';
import { ConfigLoader } from '../common/configLoader';
import { deserializeConfig } from '../common/configLoader';
import type { Suite, TestCase } from '../common/test';
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
import { FixtureRunner } from './fixtureRunner';
@ -205,7 +205,7 @@ export class WorkerMain extends ProcessRunner {
if (this._config)
return;
this._config = await ConfigLoader.deserialize(this._params.config);
this._config = await deserializeConfig(this._params.config);
this._project = this._config.projects.find(p => p.id === this._params.projectId)!;
this._poolBuilder = PoolBuilder.createForWorker(this._project);
}