Skip to content

Commit

Permalink
refactor(configuration): migrate algorithm to Chain of Responsibility…
Browse files Browse the repository at this point in the history
… pattern for improved extensibility and separation of concerns
  • Loading branch information
KernelPanic92 committed Jan 17, 2025
1 parent bedd61d commit b0c5ab2
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 55 deletions.
85 changes: 85 additions & 0 deletions lib/src/runner/configuration-loaders/configuration-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as fs from 'fs';
import * as path from 'path';

import { CypressRunnerConfig } from '../cypress-runner-config';

/**
* Object delegated to load CypressRunnerConfig.
* Each loader in the chain is responsible for attempting to load a configuration file
* based on its specific file extension. If a loader cannot find or parse the file,
* it delegates the responsibility to the next loader in the chain.
*/
export abstract class ConfigurationLoader {
private static readonly CYPRESS_RUNNER_CONFIG_FILENAME = '.cypressrunnerrc';

private _next: ConfigurationLoader | undefined;
private _parent: ConfigurationLoader | undefined;
private get supportedExtensions(): string[] {
const parentFormats = this._parent?.supportedExtensions ?? [];

return [...parentFormats, this.extension];
}

/**
* The file extension supported by this loader.
*/
protected abstract readonly extension: string;

/**
* Parse the configuration file at the given path.
*/
protected abstract parse(path: string): CypressRunnerConfig;

/**
* Attempts to load the configuration file. If the file is found and valid,
* it is parsed and returned. Otherwise, the responsibility is delegated
* to the next loader in the chain.
*/
public load(): CypressRunnerConfig {
const jsonPath = this.resolveConfigurationPath();
if (fs.existsSync(jsonPath)) {
return this.parse(jsonPath);
}

return this.next();
}

/**
* Sets the next loader in the chain and establishes a parent-child relationship.
*/
public setNext(loader: ConfigurationLoader): ConfigurationLoader {
this._next = loader;
loader._parent = this;

return loader;
}

/**
* Delegates the loading process to the next loader in the chain.
* Throws an error if no configuration file is found by any loader in the chain.
*/
protected next(): CypressRunnerConfig {
const { CYPRESS_RUNNER_CONFIG_FILENAME } = ConfigurationLoader;
if (!this._next) {
const supportedExtensions = this.supportedExtensions
.filter((extension) => extension.trim().length > 0)
.join('|');

throw new Error(
`No configuration file found. Please create a ${CYPRESS_RUNNER_CONFIG_FILENAME} file with the appropriate format: ${CYPRESS_RUNNER_CONFIG_FILENAME}{${supportedExtensions}}?.`,
);
}

return this._next.load();
}

/**
* Resolves the path to the configuration file based on the current working directory
* and the file extension supported by this loader.
*/
protected resolveConfigurationPath(): string {
const { CYPRESS_RUNNER_CONFIG_FILENAME } = ConfigurationLoader;

return path.resolve(process.cwd(), CYPRESS_RUNNER_CONFIG_FILENAME + this.extension);
}
}
12 changes: 12 additions & 0 deletions lib/src/runner/configuration-loaders/default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as fs from 'fs';

import { CypressRunnerConfig } from '../cypress-runner-config';
import { JsonConfigurationLoader } from './json';

export class DefaultConfigurationLoader extends JsonConfigurationLoader {
protected extension = '';

protected parse(path: string): CypressRunnerConfig {
return JSON.parse(fs.readFileSync(path, 'utf-8'));
}
}
18 changes: 18 additions & 0 deletions lib/src/runner/configuration-loaders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { DefaultConfigurationLoader } from './default';
import { JavaScriptConfigurationLoader } from './javascript';
import { JsonConfigurationLoader } from './json';
import { TypeScriptConfigurationLoader } from './typescript';

export * from './configuration-loader';
export * from './default';
export * from './javascript';
export * from './json';
export * from './typescript';

const defaultLoader = new DefaultConfigurationLoader();
defaultLoader
.setNext(new JsonConfigurationLoader())
.setNext(new JavaScriptConfigurationLoader())
.setNext(new TypeScriptConfigurationLoader());

export const loader = defaultLoader;
10 changes: 10 additions & 0 deletions lib/src/runner/configuration-loaders/javascript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CypressRunnerConfig } from '../cypress-runner-config';
import { ConfigurationLoader } from './configuration-loader';

export class JavaScriptConfigurationLoader extends ConfigurationLoader {
protected extension = '.js';

protected parse(path: string): CypressRunnerConfig {
return require(path);
}
}
12 changes: 12 additions & 0 deletions lib/src/runner/configuration-loaders/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as fs from 'fs';

import { CypressRunnerConfig } from '../cypress-runner-config';
import { ConfigurationLoader } from './configuration-loader';

export class JsonConfigurationLoader extends ConfigurationLoader {
protected extension = '.json';

protected parse(path: string): CypressRunnerConfig {
return JSON.parse(fs.readFileSync(path, 'utf-8'));
}
}
24 changes: 24 additions & 0 deletions lib/src/runner/configuration-loaders/typescript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { register } from 'ts-node';

import { CypressRunnerConfig } from '../cypress-runner-config';
import { ConfigurationLoader } from './configuration-loader';

export class TypeScriptConfigurationLoader extends ConfigurationLoader {
protected extension = '.ts';

protected parse(path: string): CypressRunnerConfig {
register({
transpileOnly: true,
compilerOptions: {
module: 'commonjs',
},
});

const config = require(path).default;
if (!config) {
throw new Error('Missing default export configuration');
}

return config;
}
}
1 change: 0 additions & 1 deletion lib/src/runner/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export * from './banner';
export * from './cypress-runner-config';
export * from './kill-concurrently-result';
export * from './load-configuration';
export * from './runner';
export * from './start-web-server-commands';
export * from './wait-web-services';
52 changes: 0 additions & 52 deletions lib/src/runner/load-configuration.ts

This file was deleted.

4 changes: 2 additions & 2 deletions lib/src/runner/runner.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { execSync } from 'child_process';

import { printBanner } from './banner';
import { loader } from './configuration-loaders';
import { CypressRunnerConfig } from './cypress-runner-config';
import { loadConfiguration } from './load-configuration';
import { startWebServerCommands } from './start-web-server-commands';
import { waitWebServices } from './wait-web-services';

Expand All @@ -15,7 +15,7 @@ export const cypressRunner = async (): Promise<void> => {
const processArguments = [...process.argv];
processArguments.splice(0, SCRIPT_ARG_START_INDEX);

const configuration: CypressRunnerConfig = loadConfiguration();
const configuration: CypressRunnerConfig = loader.load();
const isDebug = configuration.debug;

if (isDebug) {
Expand Down

0 comments on commit b0c5ab2

Please sign in to comment.