manan-data 0.0.0

inventyv tikv datalayer
Documentation
import {mkdir} from 'node:fs/promises';
import {createRequire} from 'node:module';
import {join as joinPath, resolve as resolvePath} from 'node:path';
import process from 'node:process';
import {pathToFileURL} from 'node:url';
import {workerData} from 'node:worker_threads';

import setUpCurrentlyUnhandled from 'currently-unhandled';
import writeFileAtomic from 'write-file-atomic';

import {set as setChalk} from '../chalk.js';
import nowAndTimers from '../now-and-timers.cjs';
import providerManager from '../provider-manager.js';
import Runner from '../runner.js';
import serializeError from '../serialize-error.js';

import channel from './channel.cjs';
import {runCompletionHandlers} from './completion-handlers.js';
import lineNumberSelection from './line-numbers.js';
import {set as setOptions} from './options.cjs';
import {flags, refs, sharedWorkerTeardowns} from './state.cjs';
import {isRunningInThread, isRunningInChildProcess} from './utils.cjs';

const currentlyUnhandled = setUpCurrentlyUnhandled();
let runner;

let expectingExit = false;

const forceExit = () => {
	expectingExit = true;
	process.exit(1);
};

const avaIsDone = () => {
	expectingExit = true;
	runCompletionHandlers();
};

// Override process.exit with an undetectable replacement
// to report when it is called from a test (which it should never be).
const handleProcessExit = (target, thisArg, args) => {
	if (!expectingExit) {
		const error = new Error('Unexpected process.exit()');
		Error.captureStackTrace(error, handleProcessExit);
		channel.send({type: 'process-exit', stack: error.stack});
	}

	target.apply(thisArg, args);
};

process.exit = new Proxy(process.exit, {
	apply: handleProcessExit,
});

const run = async options => {
	setOptions(options);
	setChalk(options.chalkOptions);

	if (options.chalkOptions.level > 0) {
		const {stdout, stderr} = process;
		global.console = Object.assign(global.console, new console.Console({stdout, stderr, colorMode: true}));
	}

	let checkSelectedByLineNumbers;
	try {
		checkSelectedByLineNumbers = lineNumberSelection({
			file: options.file,
			lineNumbers: options.lineNumbers,
		});
	} catch (error) {
		channel.send({type: 'line-number-selection-error', err: serializeError(error)});
		checkSelectedByLineNumbers = () => false;
	}

	runner = new Runner({
		checkSelectedByLineNumbers,
		experiments: options.experiments,
		failFast: options.failFast,
		failWithoutAssertions: options.failWithoutAssertions,
		file: options.file,
		match: options.match,
		projectDir: options.projectDir,
		recordNewSnapshots: options.recordNewSnapshots,
		runOnlyExclusive: options.runOnlyExclusive,
		serial: options.serial,
		snapshotDir: options.snapshotDir,
		updateSnapshots: options.updateSnapshots,
	});

	refs.runnerChain = runner.chain;

	channel.peerFailed.then(() => {
		runner.interrupt();
	});

	runner.on('accessed-snapshots', filename => channel.send({type: 'accessed-snapshots', filename}));
	runner.on('stateChange', state => channel.send(state));

	runner.on('error', error => {
		channel.send({type: 'internal-error', err: serializeError(error)});
		forceExit();
	});

	runner.on('finish', async () => {
		try {
			const {touchedFiles} = await runner.saveSnapshotState();
			if (touchedFiles) {
				channel.send({type: 'touched-files', files: touchedFiles});
			}
		} catch (error) {
			channel.send({type: 'internal-error', err: serializeError(error)});
			forceExit();
			return;
		}

		try {
			await Promise.all(sharedWorkerTeardowns.map(fn => fn()));
		} catch (error) {
			channel.send({type: 'uncaught-exception', err: serializeError(error)});
			forceExit();
			return;
		}

		nowAndTimers.setImmediate(() => {
			const unhandled = currentlyUnhandled();
			if (unhandled.length === 0) {
				return avaIsDone();
			}

			for (const rejection of unhandled) {
				channel.send({type: 'unhandled-rejection', err: serializeError(rejection.reason, {testFile: options.file})});
			}

			forceExit();
		});
	});

	process.on('uncaughtException', error => {
		channel.send({type: 'uncaught-exception', err: serializeError(error, {testFile: options.file})});
		forceExit();
	});

	// Store value to prevent required modules from modifying it.
	const testPath = options.file;

	const extensionsToLoadAsModules = Object.entries(options.moduleTypes)
		.filter(([, type]) => type === 'module')
		.map(([extension]) => extension);

	// Install before processing options.require, so if helpers are added to the
	// require configuration the *compiled* helper will be loaded.
	const {projectDir, providerStates = []} = options;
	const providers = [];
	await Promise.all(providerStates.map(async ({type, state, protocol}) => {
		if (type === 'typescript') {
			const provider = await providerManager.typescript(projectDir, {protocol});
			providers.push(provider.worker({extensionsToLoadAsModules, state}));
		}
	}));

	const require = createRequire(import.meta.url);
	const load = async ref => {
		for (const provider of providers) {
			if (provider.canLoad(ref)) {
				return provider.load(ref, {requireFn: require});
			}
		}

		for (const extension of extensionsToLoadAsModules) {
			if (ref.endsWith(`.${extension}`)) {
				return import(pathToFileURL(ref));
			}
		}

		// We still support require() since it's more easily monkey-patched.
		return require(ref);
	};

	const loadRequiredModule = async ref => {
		// If the provider can load the module, assume it's a local file and not a
		// dependency.
		for (const provider of providers) {
			if (provider.canLoad(ref)) {
				return provider.load(ref, {requireFn: require});
			}
		}

		// Try to load the module as a file, relative to the project directory.
		// Match load() behavior.
		const fullPath = resolvePath(projectDir, ref);
		try {
			for (const extension of extensionsToLoadAsModules) {
				if (fullPath.endsWith(`.${extension}`)) {
					return await import(pathToFileURL(fullPath)); // eslint-disable-line no-await-in-loop
				}
			}

			return require(fullPath);
		} catch (error) {
			// If the module could not be found, assume it's not a file but a dependency.
			if (error.code === 'ERR_MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_FOUND') {
				return importFromProject(ref);
			}

			throw error;
		}
	};

	let importFromProject = async ref => {
		// Do not use the cacheDir since it's not guaranteed to be inside node_modules.
		const avaCacheDir = joinPath(projectDir, 'node_modules', '.cache', 'ava');
		await mkdir(avaCacheDir, {recursive: true});
		const stubPath = joinPath(avaCacheDir, 'import-from-project.mjs');
		await writeFileAtomic(stubPath, 'export const importFromProject = ref => import(ref);\n');
		({importFromProject} = await import(pathToFileURL(stubPath)));
		return importFromProject(ref);
	};

	try {
		for await (const [ref, ...args] of (options.require ?? [])) {
			const loadedModule = await loadRequiredModule(ref);

			if (typeof loadedModule === 'function') { // CJS module
				await loadedModule(...args);
			} else if (typeof loadedModule.default === 'function') { // ES module, or exports.default from CJS
				const {default: fn} = loadedModule;
				await fn(...args);
			}
		}

		if (options.debug?.port !== undefined && options.debug?.host !== undefined) {
			// If an inspector was active when the main process started, and is
			// already active for the worker process, do not open a new one.
			const {default: inspector} = await import('node:inspector');
			if (!options.debug.active || inspector.url() === undefined) {
				inspector.open(options.debug.port, options.debug.host, true);
			}

			if (options.debug.break) {
				debugger; // eslint-disable-line no-debugger
			}
		}

		await load(testPath);

		if (flags.loadedMain) {
			// Unreference the channel if the test file required AVA. This stops it
			// from keeping the event loop busy, which means the `beforeExit` event can be
			// used to detect when tests stall.
			channel.unref();
		} else {
			channel.send({type: 'missing-ava-import'});
			forceExit();
		}
	} catch (error) {
		channel.send({type: 'uncaught-exception', err: serializeError(error, {testFile: options.file})});
		forceExit();
	}
};

const onError = error => {
	// There shouldn't be any errors, but if there are we may not have managed
	// to bootstrap enough code to serialize them. Re-throw and let the process
	// crash.
	setImmediate(() => {
		throw error;
	});
};

let options;
if (isRunningInThread) {
	channel.send({type: 'starting'}); // AVA won't terminate the worker thread until it's seen this message.
	({options} = workerData);
	delete workerData.options; // Don't allow user code access.
} else if (isRunningInChildProcess) {
	channel.send({type: 'ready-for-options'});
	options = await channel.options;
}

try {
	await run(options);
} catch (error) {
	onError(error);
}