function createContext({ code, out, parse, generatorOpts, filename }) {
	let isInitialParse = true;
	let comments = [];

	const ctx = {
		paths: new WeakMap(),
		/** @type {Set<Path>} */
		queue: new Set(),
		code,
		out,
		parse(code, opts) {
			if (isInitialParse) {
				opts = Object.assign({}, opts || {});
				let onComment = opts && opts.onComment;
				if (!Array.isArray(onComment)) {
					opts.onComment = [];
				}
				opts.onComment.push = function (comment) {
					comments.push(comment);
					if (typeof onComment === 'function') {
						onComment(
							comment.type === 'Block',
							comment.value,
							comment.start,
							comment.end,
							comment.loc && comment.loc.start,
							comment.loc && comment.loc.end
						);
					}
					return Array.prototype.push.call(this, comment);
				};
				opts.allowAwaitOutsideFunction = true;
				isInitialParse = false;
			}
			return parse(code, opts);
		},
		generatorOpts,
		types,
		visit,
		/** @type {ReturnType<typeof createTemplate> | null} */
		template: createTemplate(parse),
		Path,
		hub: {
			file: {
				ast: {
					// TODO: use acorn's `onComment()` hook
					comments
				},
				opts: {
					filename
				}
			}
		}
	};

	const bound = { ctx };

	ctx.visit = ctx.visit.bind(bound);

	// @ts-ignore
	ctx.Path = function (node, ancestors) {
		return new Path(node, ancestors, ctx);
	};

	return ctx;
}
@param {object} options
@param {string} options.code
@param {MagicString} options.out
@param {string} [options.filename]
@param {typeof DEFAULTS['parse']} options.parse
@param {{ compact?: boolean, filename?: string }} options.generatorOpts