function client_component(analysis, options) {
	/** @type {ComponentClientTransformState} */
	const state = {
		analysis,
		options,
		scope: analysis.module.scope,
		scopes: analysis.module.scopes,
		is_instance: false,
		hoisted: [b.import_all('$', 'svelte/internal/client')],
		node: /** @type {any} */ (null), // populated by the root node
		legacy_reactive_imports: [],
		legacy_reactive_statements: new Map(),
		metadata: {
			namespace: options.namespace,
			bound_contenteditable: false
		},
		events: new Set(),
		preserve_whitespace: options.preserveWhitespace,
		state_fields: new Map(),
		transform: {},
		in_constructor: false,
		instance_level_snippets: [],
		module_level_snippets: [],

		// these are set inside the `Fragment` visitor, and cannot be used until then
		init: /** @type {any} */ (null),
		update: /** @type {any} */ (null),
		expressions: /** @type {any} */ (null),
		after_update: /** @type {any} */ (null),
		template: /** @type {any} */ (null)
	};

	const module = /** @type {ESTree.Program} */ (
		walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, visitors)
	);

	const instance_state = {
		...state,
		transform: { ...state.transform },
		scope: analysis.instance.scope,
		scopes: analysis.instance.scopes,
		is_instance: true
	};

	const instance = /** @type {ESTree.Program} */ (
		walk(/** @type {AST.SvelteNode} */ (analysis.instance.ast), instance_state, visitors)
	);

	const template = /** @type {ESTree.Program} */ (
		walk(
			/** @type {AST.SvelteNode} */ (analysis.template.ast),
			{
				...state,
				transform: instance_state.transform,
				scope: analysis.instance.scope,
				scopes: analysis.template.scopes
			},
			visitors
		)
	);

	module.body.unshift(...state.legacy_reactive_imports);

	/** @type {ESTree.Statement[]} */
	const store_setup = [];

	/** @type {ESTree.VariableDeclaration[]} */
	const legacy_reactive_declarations = [];

	let needs_store_cleanup = false;

	for (const [name, binding] of analysis.instance.scope.declarations) {
		if (binding.kind === 'legacy_reactive') {
			legacy_reactive_declarations.push(
				b.const(
					name,
					b.call('$.mutable_source', undefined, analysis.immutable ? b.true : undefined)
				)
			);
		}
		if (binding.kind === 'store_sub') {
			if (store_setup.length === 0) {
				needs_store_cleanup = true;
				store_setup.push(
					b.const(b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]), b.call('$.setup_stores'))
				);
			}

			// We're creating an arrow function that gets the store value which minifies better for two or more references
			const store_reference = build_getter(b.id(name.slice(1)), instance_state);
			const store_get = b.call('$.store_get', store_reference, b.literal(name), b.id('$$stores'));
			store_setup.push(
				b.const(
					binding.node,
					dev
						? b.thunk(
								b.sequence([
									b.call('$.validate_store', store_reference, b.literal(name.slice(1))),
									store_get
								])
							)
						: b.thunk(store_get)
				)
			);
		}
	}

	for (const [node] of analysis.reactive_statements) {
		const statement = [...state.legacy_reactive_statements].find(([n]) => n === node);
		if (statement === undefined) {
			throw new Error('Could not find reactive statement');
		}
		instance.body.push(statement[1]);
	}

	if (analysis.reactive_statements.size > 0) {
		instance.body.push(b.stmt(b.call('$.legacy_pre_effect_reset')));
	}

	/**
	 * Used to store the group nodes
	 * @type {ESTree.VariableDeclaration[]}
	 */
	const group_binding_declarations = [];
	for (const group of analysis.binding_groups.values()) {
		group_binding_declarations.push(b.const(group.name, b.array([])));
	}

	/** @type {Array<ESTree.Property | ESTree.SpreadElement>} */
	const component_returned_object = analysis.exports.flatMap(({ name, alias }) => {
		const binding = instance_state.scope.get(name);
		const expression = build_getter(b.id(name), instance_state);
		const getter = b.get(alias ?? name, [b.return(expression)]);

		if (expression.type === 'Identifier') {
			if (binding?.declaration_kind === 'let' || binding?.declaration_kind === 'var') {
				return [
					getter,
					b.set(alias ?? name, [b.stmt(b.assignment('=', expression, b.id('$$value')))])
				];
			} else if (!dev) {
				return b.init(alias ?? name, expression);
			}
		}

		if (binding?.kind === 'prop' || binding?.kind === 'bindable_prop') {
			return [getter, b.set(alias ?? name, [b.stmt(b.call(name, b.id('$$value')))])];
		}

		if (binding?.kind === 'state' || binding?.kind === 'raw_state') {
			const value = binding.kind === 'state' ? b.call('$.proxy', b.id('$$value')) : b.id('$$value');
			return [getter, b.set(alias ?? name, [b.stmt(b.call('$.set', b.id(name), value))])];
		}

		return getter;
	});

	const properties = [...analysis.instance.scope.declarations].filter(
		([name, binding]) =>
			(binding.kind === 'prop' || binding.kind === 'bindable_prop') && !name.startsWith('$$')
	);

	if (analysis.accessors) {
		for (const [name, binding] of properties) {
			const key = binding.prop_alias ?? name;

			const getter = b.get(key, [b.return(b.call(b.id(name)))]);

			const setter = b.set(key, [
				b.stmt(b.call(b.id(name), b.id('$$value'))),
				b.stmt(b.call('$.flush'))
			]);

			if (analysis.runes && binding.initial) {
				// turn `set foo($$value)` into `set foo($$value = expression)`
				setter.value.params[0] = {
					type: 'AssignmentPattern',
					left: b.id('$$value'),
					right: /** @type {ESTree.Expression} */ (binding.initial)
				};
			}

			component_returned_object.push(getter, setter);
		}
	}

	if (options.compatibility.componentApi === 4) {
		component_returned_object.push(
			b.init('$set', b.id('$.update_legacy_props')),
			b.init(
				'$on',
				b.arrow(
					[b.id('$$event_name'), b.id('$$event_cb')],
					b.call(
						'$.add_legacy_event_listener',
						b.id('$$props'),
						b.id('$$event_name'),
						b.id('$$event_cb')
					)
				)
			)
		);
	} else if (dev) {
		component_returned_object.push(b.spread(b.call(b.id('$.legacy_api'))));
	}

	const push_args = [b.id('$$props'), b.literal(analysis.runes)];
	if (dev) push_args.push(b.id(analysis.name));

	const component_block = b.block([
		...store_setup,
		...legacy_reactive_declarations,
		...group_binding_declarations,
		...state.instance_level_snippets,
		.../** @type {ESTree.Statement[]} */ (instance.body),
		analysis.runes || !analysis.needs_context
			? b.empty
			: b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)),
		.../** @type {ESTree.Statement[]} */ (template.body)
	]);

	if (!analysis.runes) {
		// Bind static exports to props so that people can access them with bind:x
		for (const { name, alias } of analysis.exports) {
			component_block.body.push(
				b.stmt(
					b.call(
						'$.bind_prop',
						b.id('$$props'),
						b.literal(alias ?? name),
						build_getter(b.id(name), instance_state)
					)
				)
			);
		}
	}

	if (analysis.css.ast !== null && analysis.inject_styles) {
		const hash = b.literal(analysis.css.hash);
		const code = b.literal(render_stylesheet(analysis.source, analysis, options).code);

		state.hoisted.push(b.const('$$css', b.object([b.init('hash', hash), b.init('code', code)])));

		component_block.body.unshift(
			b.stmt(b.call('$.append_styles', b.id('$$anchor'), b.id('$$css')))
		);
	}

	if (analysis.needs_mutation_validation) {
		component_block.body.unshift(
			b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props')))
		);
	}

	const should_inject_context =
		dev ||
		analysis.needs_context ||
		analysis.reactive_statements.size > 0 ||
		component_returned_object.length > 0;

	// we want the cleanup function for the stores to run as the very last thing
	// so that it can effectively clean up the store subscription even after the user effects runs
	if (should_inject_context) {
		component_block.body.unshift(b.stmt(b.call('$.push', ...push_args)));

		let to_push;

		if (component_returned_object.length > 0) {
			let pop_call = b.call('$.pop', b.object(component_returned_object));
			to_push = needs_store_cleanup ? b.var('$$pop', pop_call) : b.return(pop_call);
		} else {
			to_push = b.stmt(b.call('$.pop'));
		}

		component_block.body.push(to_push);
	}

	if (needs_store_cleanup) {
		component_block.body.push(b.stmt(b.call('$$cleanup')));
		if (component_returned_object.length > 0) {
			component_block.body.push(b.return(b.id('$$pop')));
		}
	}

	if (analysis.uses_rest_props) {
		const named_props = analysis.exports.map(({ name, alias }) => alias ?? name);
		for (const [name, binding] of analysis.instance.scope.declarations) {
			if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name);
		}

		component_block.body.unshift(
			b.const(
				'$$restProps',
				b.call(
					'$.legacy_rest_props',
					b.id('$$sanitized_props'),
					b.array(named_props.map((name) => b.literal(name)))
				)
			)
		);
	}

	if (analysis.uses_props || analysis.uses_rest_props) {
		const to_remove = [
			b.literal('children'),
			b.literal('$$slots'),
			b.literal('$$events'),
			b.literal('$$legacy')
		];
		if (analysis.custom_element) {
			to_remove.push(b.literal('$$host'));
		}

		component_block.body.unshift(
			b.const(
				'$$sanitized_props',
				b.call('$.legacy_rest_props', b.id('$$props'), b.array(to_remove))
			)
		);
	}

	if (analysis.uses_slots) {
		component_block.body.unshift(b.const('$$slots', b.call('$.sanitize_slots', b.id('$$props'))));
	}

	let should_inject_props =
		should_inject_context ||
		analysis.needs_props ||
		analysis.uses_props ||
		analysis.uses_rest_props ||
		analysis.uses_slots ||
		analysis.slot_names.size > 0;

	// Merge hoisted statements into module body.
	// Ensure imports are on top, with the order preserved, then module body, then hoisted statements
	/** @type {ESTree.ImportDeclaration[]} */
	const imports = [];
	/** @type {ESTree.Program['body']} */
	let body = [];

	for (const entry of [...module.body, ...state.hoisted]) {
		if (entry.type === 'ImportDeclaration') {
			imports.push(entry);
		} else {
			body.push(entry);
		}
	}

	body = [...imports, ...state.module_level_snippets, ...body];

	const component = b.function_declaration(
		b.id(analysis.name),
		should_inject_props ? [b.id('$$anchor'), b.id('$$props')] : [b.id('$$anchor')],
		component_block
	);

	if (options.hmr) {
		const id = b.id(analysis.name);
		const HMR = b.id('$.HMR');

		const existing = b.member(id, HMR, true);
		const incoming = b.member(b.id('module.default'), HMR, true);

		const accept_fn_body = [
			b.stmt(b.assignment('=', b.member(incoming, 'source'), b.member(existing, 'source'))),
			b.stmt(b.call('$.set', b.member(existing, 'source'), b.member(incoming, 'original')))
		];

		if (analysis.css.hash) {
			// remove existing `<style>` element, in case CSS changed
			accept_fn_body.unshift(b.stmt(b.call('$.cleanup_styles', b.literal(analysis.css.hash))));
		}

		const hmr = b.block([
			b.stmt(b.assignment('=', id, b.call('$.hmr', id, b.thunk(b.member(existing, 'source'))))),

			b.stmt(b.call('import.meta.hot.accept', b.arrow([b.id('module')], b.block(accept_fn_body))))
		]);

		body.push(component, b.if(b.id('import.meta.hot'), hmr), b.export_default(b.id(analysis.name)));
	} else {
		body.push(b.export_default(component));
	}

	if (dev) {
		// add `App[$.FILENAME] = 'App.svelte'` so that we can print useful messages later
		body.unshift(
			b.stmt(
				b.assignment('=', b.member(b.id(analysis.name), '$.FILENAME', true), b.literal(filename))
			)
		);
	}

	if (!analysis.runes) {
		body.unshift(b.imports([], 'svelte/internal/flags/legacy'));
	}

	if (analysis.tracing) {
		body.unshift(b.imports([], 'svelte/internal/flags/tracing'));
	}

	if (options.discloseVersion) {
		body.unshift(b.imports([], 'svelte/internal/disclose-version'));
	}

	if (options.compatibility.componentApi === 4) {
		body.unshift(b.imports([['createClassComponent', '$$_createClassComponent']], 'svelte/legacy'));
		component_block.body.unshift(
			b.if(
				b.id('new.target'),
				b.return(
					b.call(
						'$$_createClassComponent',
						// When called with new, the first argument is the constructor options
						b.object([b.init('component', b.id(analysis.name)), b.spread(b.id('$$anchor'))])
					)
				)
			)
		);
	} else if (dev) {
		component_block.body.unshift(b.stmt(b.call('$.check_target', b.id('new.target'))));
	}

	if (analysis.props_id) {
		// need to be placed on first line of the component for hydration
		component_block.body.unshift(b.const(analysis.props_id, b.call('$.props_id')));
	}

	if (state.events.size > 0) {
		body.push(
			b.stmt(b.call('$.delegate', b.array(Array.from(state.events).map((name) => b.literal(name)))))
		);
	}

	if (analysis.custom_element) {
		const ce = analysis.custom_element;
		const ce_props = typeof ce === 'boolean' ? {} : ce.props || {};

		/** @type {ESTree.Property[]} */
		const props_str = [];

		for (const [name, prop_def] of Object.entries(ce_props)) {
			const binding = analysis.instance.scope.get(name);
			const key = binding?.prop_alias ?? name;

			if (
				!prop_def.type &&
				binding?.initial?.type === 'Literal' &&
				typeof binding?.initial.value === 'boolean'
			) {
				prop_def.type = 'Boolean';
			}

			const value = b.object(
				/** @type {ESTree.Property[]} */ (
					[
						prop_def.attribute ? b.init('attribute', b.literal(prop_def.attribute)) : undefined,
						prop_def.reflect ? b.init('reflect', b.true) : undefined,
						prop_def.type ? b.init('type', b.literal(prop_def.type)) : undefined
					].filter(Boolean)
				)
			);

			props_str.push(b.init(key, value));
		}

		for (const [name, binding] of properties) {
			const key = binding.prop_alias ?? name;
			if (ce_props[key]) continue;

			props_str.push(b.init(key, b.object([])));
		}

		const slots_str = b.array([...analysis.slot_names.keys()].map((name) => b.literal(name)));
		const accessors_str = b.array(
			analysis.exports.map(({ name, alias }) => b.literal(alias ?? name))
		);
		const use_shadow_dom = typeof ce === 'boolean' || ce.shadow !== 'none' ? true : false;

		const create_ce = b.call(
			'$.create_custom_element',
			b.id(analysis.name),
			b.object(props_str),
			slots_str,
			accessors_str,
			b.literal(use_shadow_dom),
			/** @type {any} */ (typeof ce !== 'boolean' ? ce.extend : undefined)
		);

		// If a tag name is provided, call `customElements.define`, otherwise leave to the user
		if (typeof ce !== 'boolean' && typeof ce.tag === 'string') {
			const define = b.stmt(b.call('customElements.define', b.literal(ce.tag), create_ce));

			if (options.hmr) {
				body.push(
					b.if(b.binary('==', b.call('customElements.get', b.literal(ce.tag)), b.null), define)
				);
			} else {
				body.push(define);
			}
		} else {
			body.push(b.stmt(create_ce));
		}
	}

	return {
		type: 'Program',
		sourceType: 'module',
		body
	};
}
@param {ComponentAnalysis} analysis
@param {ValidatedCompileOptions} options
@returns {ESTree.Program}