Repository: rollup/rollup
File: src/watch/watch-proxy.ts (TypeScript)

Code before:
} from '../rollup/types';
import { ensureArray } from '../utils/ensureArray';
import { error, logInvalidOption } from '../utils/logs';
import { mergeOptions } from '../utils/options/mergeOptions';
import { URL_WATCH } from '../utils/urls';
import { WatchEmitter } from './WatchEmitter';
import { loadFsEvents } from './fsevents-importer';

export default function watch(configs: RollupOptions[] | RollupOptions): RollupWatcher {
	const emitter = new WatchEmitter() as RollupWatcher;

	watchInternal(configs, emitter).catch(error => {
		handleError(error);
	});

	return emitter;
}

function checkWatchConfig(config: MergedRollupOptions[]): boolean {
	for (const item of config) {
		if (item.input && item.output) {
			const input = typeof item.input === 'string' ? ensureArray(item.input) : item.input;
			const output = ensureArray(item.output);
			for (const index in input) {
				const inputPath = input[index as keyof typeof input] as string;
				const subPath = output.some(o => o.dir && inputPath.startsWith?.(o.dir));
				if (subPath) {
					return false;
				}
			}
		}
	}
	return true;
}

async function watchInternal(configs: MaybeArray<RollupOptions>, emitter: RollupWatcher) {
	const optionsList = await Promise.all(
		ensureArray(configs).map(config => mergeOptions(config, true))
	);
	const watchOptionsList = optionsList.filter(config => config.watch !== false);
	if (watchOptionsList.length === 0) {
		return error(
			logInvalidOption(
				'watch',
				URL_WATCH,
				'there must be at least one config where "watch" is not set to "false"'
			)
		);
	}
	if (!checkWatchConfig(watchOptionsList)) {
		return error(

Review comment (bug):
While only an edge case, "startsWith" can have false positives, e.g. if the output dir is `foo/website/` and the input is `foo/website.js`, which feels slightly plausible to me. AI suggest something like

```js
function isSubPath(parentDir, filePath) {
  const relative = path.relative(parentDir, filePath);
  return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
}
```

which does not feel too bad to me, though of course it is more involved. Alternatively, one could probably extend "startsWith" with a check that the remaining part of inputPath starts with a path separator.

Code after:
} from '../rollup/types';
import { ensureArray } from '../utils/ensureArray';
import { error, logInvalidOption } from '../utils/logs';
import { mergeOptions } from '../utils/options/mergeOptions';
import { URL_WATCH } from '../utils/urls';
import { WatchEmitter } from './WatchEmitter';
import { loadFsEvents } from './fsevents-importer';

export default function watch(configs: RollupOptions[] | RollupOptions): RollupWatcher {
	const emitter = new WatchEmitter() as RollupWatcher;

	watchInternal(configs, emitter).catch(error => {
		handleError(error);
	});

	return emitter;
}

function withTrailingSlash(path: string): string {
	if (path[path.length - 1] !== '/') {
		return `${path}/`;
	}
	return path;
}

function checkWatchConfig(config: MergedRollupOptions[]): void {
	for (const item of config) {
		if (item.input && item.output) {
			const input = typeof item.input === 'string' ? ensureArray(item.input) : item.input;
			const output = ensureArray(item.output);
			for (const index in input) {
				const inputPath = input[index as keyof typeof input] as string;
				const subPath = output.find(o => {
					if (!o.dir || typeof inputPath !== 'string') {
						return false;
					}
					const _outPath = withTrailingSlash(o.dir);
					const _inputPath = withTrailingSlash(inputPath);
					return _inputPath.startsWith(_outPath);
				});
				if (subPath) {
					error(
						logInvalidOption(
							'watch',
							URL_WATCH,
							`the input "${inputPath}" is a subpath of the output "${subPath.dir}"`
						)
					);
				}
			}
		}