function wmrMiddleware(options) {
	let { cwd, root, out, distDir = 'dist', onError, onChange = NOOP, alias, sourcemap } = options;

	distDir = resolve(dirname(out), distDir);

	const NonRollup = createPluginContainer(getPlugins(options), {
		cwd: root,
		sourcemap,
		writeFile: (filename, source) => {
			// Remove .cache folder from filename if present. The cache
			// works with relative keys only.
			if (isAbsolute(filename)) {
				const relativeFile = relative(out, filename);
				if (!relativeFile.startsWith('..')) {
					filename = relativeFile;
				}
			}
			writeCacheFile(filename, source);
		},
		output: {
			// assetFileNames: '@asset/[name][extname]',
			// chunkFileNames: '[name][extname]',
			// Use a hash to prevent collisions between assets with the
			// same basename.
			assetFileNames: '[name][extname]?asset',
			dir: out,
			format: 'esm'
		}
	});

	NonRollup.buildStart();
	NonRollup.outputOptions();

	// Make watcher aware of aliased directories
	const pathAliases = Object.keys(alias)
		.filter(key => key.endsWith('/*'))
		.map(key => alias[key]);
	const watchDirs = [root, resolve(cwd, 'package.json'), ...pathAliases];
	logWatcher(`watching:\n${watchDirs.map(d => kl.dim('- ' + d)).join('\n')}`);

	const watcher = watch(watchDirs, {
		cwd,
		disableGlobbing: true,
		ignoreInitial: true,
		ignored: [/(^|[/\\])(node_modules|\.git|\.DS_Store)([/\\]|$)/, resolve(cwd, out), resolve(cwd, distDir)]
	});
	const pendingChanges = new Set();

	let timeout = null;
	function flushChanges() {
		timeout = null;
		onChange({ changes: Array.from(pendingChanges), duration: 0 });
		pendingChanges.clear();
	}

	function bubbleUpdates(filename, visited = new Set()) {
		if (visited.has(filename)) return true;
		visited.add(filename);
		// Delete file from the in-memory cache:
		WRITE_CACHE.delete(filename);

		const mod = moduleGraph.get(filename);
		if (!mod) return false;

		if (mod.hasErrored) {
			mod.hasErrored = false;
			return false;
		}

		if (mod.acceptingUpdates) {
			mod.stale = true;
			pendingChanges.add(filename);
			return true;
		} else if (mod.dependents.size) {
			let accepts = true;
			[...mod.dependents].forEach(value => {
				if (!bubbleUpdates(value, visited)) accepts = false;
			});

			if (accepts) mod.stale = true;

			return accepts;
		}

		// We need a full-reload signal
		return false;
	}

	/**
	 * @param {string} absoluteId
	 * @param {'create' | 'update' | 'delete'} changeType
	 */
	function applyWatchChanges(absoluteId, changeType) {
		const event = { event: changeType };

		const seen = new Set();
		const items = [absoluteId];
		/** @type {string | undefined} */
		let item;
		while ((item = items.pop()) !== undefined) {
			if (seen.has(item)) continue;
			const res = NonRollup.watchChange(item, event);
			if (Array.isArray(res)) items.push(...res);
			seen.add(item);
		}

		const changed = Array.from(seen);
		for (let file of changed) {
			const originalFile = file;
			file = normalize(file);

			// Resolve potentially aliased paths and normalize to 'nix:
			const aliased = matchAlias(alias, file.split(sep).join(posix.sep));
			if (aliased) {
				// Trim leading slash, we need a relative path for the cache
				file = aliased.slice(1);
			} else {
				if (isAbsolute(file)) {
					const relativeFile = relative(root, file);
					if (!relativeFile.startsWith('..')) {
						file = relativeFile;
					}
				}

				file = file.split(sep).join(posix.sep);
			}

			logWatcher(`${kl.cyan(originalFile)} -> ${kl.dim(file)} [${changeType}]`);

			logCache(`delete: ${kl.cyan(file)}`);
			WRITE_CACHE.delete(file);

			// Delete source file if there is any
			if (options.sourcemap) {
				const sourceKey = file + '.map';

				if (WRITE_CACHE.has(sourceKey)) {
					logCache(`delete: ${kl.cyan(sourceKey)}`);
					WRITE_CACHE.delete(sourceKey);
				}
			}

			// We could be dealing with an asset
			if (WRITE_CACHE.has(file + '?asset')) {
				const cacheKey = file + '?asset';
				logCache(`delete: ${kl.cyan(cacheKey)}`);
				WRITE_CACHE.delete(cacheKey);
			}

			// ...or a proxy module
			if (WRITE_CACHE.has(file + '?module')) {
				const cacheKey = file + '?module';
				logCache(`delete: ${kl.cyan(cacheKey)}`);
				WRITE_CACHE.delete(cacheKey);
			}

			if (!pendingChanges.size) timeout = setTimeout(flushChanges, 60);

			if (moduleGraph.has(file + '?module')) {
				pendingChanges.add('/' + file + '?module');

				// Update files relying on the proxy module so that they
				// re-evaluate CSS module class names.
				bubbleUpdates(file + '?module');
			}

			if (STYLE_REG.test(file)) {
				pendingChanges.add('/' + file);
			} else if (/\.(mjs|[tj]sx?)$/.test(file)) {
				if (!moduleGraph.has(file)) {
					onChange({ reload: true });
					clearTimeout(timeout);
					return;
				}

				if (!bubbleUpdates(file)) {
					pendingChanges.clear();
					clearTimeout(timeout);
					onChange({ reload: true });
				}
			} else {
				pendingChanges.clear();
				clearTimeout(timeout);
				onChange({ reload: true });
			}
		}
	}

	watcher.on('add', filename => {
		const absolute = resolve(cwd, filename);
		applyWatchChanges(absolute, 'create');
	});

	watcher.on('unlink', filename => {
		const absolute = resolve(cwd, filename);
		applyWatchChanges(absolute, 'delete');
	});

	watcher.on('change', filename => {
		const absolute = resolve(cwd, filename);
		applyWatchChanges(absolute, 'update');
	});

	return async (req, res, next) => {
		// @ts-ignore
		let path = posix.normalize(req.path);

		const queryParams = new URL(req.url, 'file://').searchParams;

		if (path.startsWith('/@npm/')) {
			return next();
		}

		let prefix = '';

		// Workaround for transform forcing extensionless ids to be
		// non-js
		let hasIdPrefix = false;

		let file = '';
		let id = path;

		// Cache key for write cache. Preserves `@id` and `@alias` prefixes
		let cacheKey = id.slice(1); // Strip "/" at the beginning

		// Path for virtual modules that refer to an unprefixed id.
		if (path.startsWith('/@id/')) {
			// Virtual paths have no exact file match, so we don't set `file`
			hasIdPrefix = true;
			id = path.slice('/@id/'.length);

			// Add back leading slash if it was part of the virtual id.
			// Example: `/@windicss/windi.css`
			if (req.path.startsWith('/@id//')) {
				id = '/' + id;
			}
		} else if (path.startsWith('/@alias/')) {
			id = posix.normalize(path.slice('/@alias/'.length));

			// Resolve to a file here for non-js Transforms
			file = resolveAlias(options.alias, id);
		} else {
			const prefixMatches = path.match(/^\/?@([a-z-]+)(\/.+)$/);
			if (prefixMatches) {
				prefix = '\0' + prefixMatches[1] + ':';
				path = prefixMatches[2];
			}

			if (path.startsWith('/@id/')) {
				// Virtual paths have no exact file match, so we don't set `file`
				hasIdPrefix = true;
				path = path.slice('/@id'.length);
			}

			// convert to OS path:
			const osPath = path.slice(1).split(posix.sep).join(sep);

			file = resolve(root, osPath);

			// Rollup-style CWD-relative Unix-normalized path "id":
			id = relative(root, file).replace(/^\.\//, '').replace(/^[\0]/, '').split(sep).join(posix.sep);

			// TODO: Vefify prefix mappings in write cache
			cacheKey = prefix + id;
			// Normalize the cacheKey so it matches what will be in the WRITE_CACHE, where we store in native paths
			cacheKey = cacheKey.split(posix.sep).join(sep);

			if (!hasIdPrefix) {
				id = `./${id}`;
			}

			// add back any prefix if there was one:
			file = prefix + file;
			id = prefix + id;
		}

		// Force serving as a js module for proxy modules. Main use
		// case is CSS-Modules.
		const isModule = queryParams.has('module');
		const isAsset = queryParams.has('asset');

		let type;
		if (isModule) type = 'application/javascript;charset=utf-8';
		else if (isAsset && /\.(?:s[ac]ss|less)$/.test(file)) type = 'text/css';
		else type = getMimeType(file);

		if (type) {
			res.setHeader('Content-Type', type);
		}

		log(`${kl.cyan(formatPath(path))} -> ${kl.dim(id)} file: ${kl.dim(file)}`);

		try {
			/** @type {(ctx: Context) => Result | Promise<Result>} */
			let transform;
			if (path === '/_wmr.js') {
				transform = getWmrClient.bind(null);
			} else if (/\.map$/.test(path)) {
				transform = TRANSFORMS.asset;
			} else if (queryParams.has('asset')) {
				cacheKey += '?asset';
				transform = TRANSFORMS.asset;
			} else if (prefix || hasIdPrefix || isModule || /\.([mc]js|[tj]sx?)$/.test(file) || STYLE_REG.test(file)) {
				transform = TRANSFORMS.js;
			} else if (file.startsWith(root + sep) && (await isFile(file))) {
				// Ignore dotfiles
				if (posix.basename(file).startsWith('.')) {
					throw {
						message: `File not found`,
						code: 404
					};
				}
				transform = TRANSFORMS.asset;
			} else {
				transform = TRANSFORMS.generic;
			}

			const start = Date.now();
			let result = await transform({
				req,
				res,
				id,
				file,
				path,
				prefix,
				root,
				out,
				NonRollup,
				alias: options.alias,
				cacheKey
			});

			// return false to skip handling:
			if (result === false) return next();

			// return a value to use it as the response:
			if (result != null) {
				// Grab the asset id out of the compiled js
				// TODO: Wire this up into Style-Plugin by passing the
				// import type through resolution somehow
				if (!isModule && STYLE_REG.test(file) && typeof result === 'string') {
					const match = result.match(/style\(["']\/([^"']+?)["'].*?\);/m);

					if (match) {
						if (WRITE_CACHE.has(match[1])) {
							result = /** @type {string} */ WRITE_CACHE.get(match[1]);
							res.setHeader('Content-Type', 'text/css;charset=utf-8');
						}
					}
				}

				log(`<-- ${kl.cyan(formatPath(id))} as ${kl.dim('' + res.getHeader('Content-Type'))}`);
				const time = Date.now() - start;
				res.writeHead(200, {
					'Content-Length': Buffer.byteLength(result, 'utf-8'),
					'Server-Timing': `${transform.name};dur=${time}`
				});
				res.end(result);
			}
		} catch (e) {
			// `throw null` also skips handling
			if (e == null) return next();

			if (e.code === 'ENOENT') {
				// e.message = `not found (.${sep}${relative(root, e.path)})`;
				e.message = `File not found`;
				e.code = 404;
			}

			onError(e);
			next(e);
		}
	};
}
@param {import('wmr').BuildOptions & { distDir: string }} options
@returns {import('polka').Middleware}