You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte/packages/svelte/src/compiler/migrate/index.js

969 lines
29 KiB

/** @import { VariableDeclarator, Node, Identifier } from 'estree' */
/** @import { Visitors } from 'zimmerframe' */
/** @import { ComponentAnalysis } from '../phases/types.js' */
/** @import { Scope } from '../phases/scope.js' */
/** @import { AST, Binding, SvelteNode, ValidatedCompileOptions } from '#compiler' */
import MagicString from 'magic-string';
import { walk } from 'zimmerframe';
import { parse } from '../phases/1-parse/index.js';
import { analyze_component } from '../phases/2-analyze/index.js';
import { get_rune } from '../phases/scope.js';
import { reset, reset_warning_filter } from '../state.js';
import { extract_identifiers } from '../utils/ast.js';
import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js';
import { validate_component_options } from '../validate-options.js';
const regex_style_tags = /(<style[^>]+>)([\S\s]*?)(<\/style>)/g;
const style_placeholder = '/*$$__STYLE_CONTENT__$$*/';
/**
* Does a best-effort migration of Svelte code towards using runes, event attributes and render tags.
* May throw an error if the code is too complex to migrate automatically.
*
* @param {string} source
* @returns {{ code: string; }}
*/
export function migrate(source) {
try {
// Blank CSS, could contain SCSS or similar that needs a preprocessor.
// Since we don't care about CSS in this migration, we'll just ignore it.
/** @type {Array<[number, string]>} */
const style_contents = [];
source = source.replace(regex_style_tags, (_, start, content, end, idx) => {
style_contents.push([idx + start.length, content]);
return start + style_placeholder + end;
});
reset_warning_filter(() => false);
reset(source, { filename: 'migrate.svelte' });
let parsed = parse(source);
const { customElement: customElementOptions, ...parsed_options } = parsed.options || {};
/** @type {ValidatedCompileOptions} */
const combined_options = {
...validate_component_options({}, ''),
...parsed_options,
customElementOptions
};
const str = new MagicString(source);
const analysis = analyze_component(parsed, source, combined_options);
const indent = guess_indent(source);
for (const content of style_contents) {
str.overwrite(content[0], content[0] + style_placeholder.length, content[1]);
}
/** @type {State} */
let state = {
scope: analysis.instance.scope,
analysis,
str,
indent,
props: [],
props_insertion_point: parsed.instance?.content.start ?? 0,
has_props_rune: false,
end: source.length,
names: {
props: analysis.root.unique('props').name,
rest: analysis.root.unique('rest').name,
// event stuff
run: analysis.root.unique('run').name,
handlers: analysis.root.unique('handlers').name,
stopImmediatePropagation: analysis.root.unique('stopImmediatePropagation').name,
preventDefault: analysis.root.unique('preventDefault').name,
stopPropagation: analysis.root.unique('stopPropagation').name,
once: analysis.root.unique('once').name,
self: analysis.root.unique('self').name,
trusted: analysis.root.unique('trusted').name,
createBubbler: analysis.root.unique('createBubbler').name,
bubble: analysis.root.unique('bubble').name,
passive: analysis.root.unique('passive').name,
nonpassive: analysis.root.unique('nonpassive').name
},
legacy_imports: new Set(),
script_insertions: new Set()
};
if (parsed.module) {
const context = parsed.module.attributes.find((attr) => attr.name === 'context');
if (context) {
state.str.update(context.start, context.end, 'module');
}
}
if (parsed.instance) {
walk(parsed.instance.content, state, instance_script);
}
state = { ...state, scope: analysis.template.scope };
walk(parsed.fragment, state, template);
const specifiers = [...state.legacy_imports].map((imported) => {
const local = state.names[imported];
return imported === local ? imported : `${imported} as ${local}`;
});
const legacy_import = `import { ${specifiers.join(', ')} } from 'svelte/legacy';`;
let added_legacy_import = false;
if (state.props.length > 0 || analysis.uses_rest_props || analysis.uses_props) {
const has_many_props = state.props.length > 3;
const newline_separator = `\n${indent}${indent}`;
const props_separator = has_many_props ? newline_separator : ' ';
let props = '';
if (analysis.uses_props) {
props = `...${state.names.props}`;
} else {
props = state.props
.map((prop) => {
let prop_str =
prop.local === prop.exported ? prop.local : `${prop.exported}: ${prop.local}`;
if (prop.bindable) {
prop_str += ` = $bindable(${prop.init})`;
} else if (prop.init) {
prop_str += ` = ${prop.init}`;
}
return prop_str;
})
.join(`,${props_separator}`);
if (analysis.uses_rest_props) {
props += `${state.props.length > 0 ? `,${props_separator}` : ''}...${state.names.rest}`;
}
}
if (state.has_props_rune) {
// some render tags or forwarded event attributes to add
str.appendRight(state.props_insertion_point, ` ${props},`);
} else {
const uses_ts = parsed.instance?.attributes.some(
(attr) => attr.name === 'lang' && /** @type {any} */ (attr).value[0].data === 'ts'
);
const type_name = state.scope.root.unique('Props').name;
let type = '';
if (uses_ts) {
if (analysis.uses_props || analysis.uses_rest_props) {
type = `interface ${type_name} { [key: string]: any }`;
} else {
type = `interface ${type_name} {${newline_separator}${state.props
.map((prop) => {
const comment = prop.comment ? `${prop.comment}${newline_separator}` : '';
return `${comment}${prop.exported}${prop.optional ? '?' : ''}: ${prop.type};`;
})
.join(newline_separator)}\n${indent}}`;
}
} else {
if (analysis.uses_props || analysis.uses_rest_props) {
type = `{Record<string, any>}`;
} else {
type = `{${state.props
.map((prop) => {
return `${prop.exported}${prop.optional ? '?' : ''}: ${prop.type}`;
})
.join(`, `)}}`;
}
}
let props_declaration = `let {${props_separator}${props}${has_many_props ? `\n${indent}` : ' '}}`;
if (uses_ts) {
props_declaration = `${type}\n\n${indent}${props_declaration}`;
props_declaration = `${props_declaration}${type ? `: ${type_name}` : ''} = $props();`;
} else {
props_declaration = `/** @type {${type}} */\n${indent}${props_declaration}`;
props_declaration = `${props_declaration} = $props();`;
}
if (parsed.instance) {
props_declaration = `\n${indent}${props_declaration}`;
str.appendRight(state.props_insertion_point, props_declaration);
} else {
const imports = state.legacy_imports.size > 0 ? `${indent}${legacy_import}\n` : '';
const script_insertions =
state.script_insertions.size > 0
? `\n${indent}${[...state.script_insertions].join(`\n${indent}`)}`
: '';
str.prepend(
`<script>\n${imports}${indent}${props_declaration}${script_insertions}\n</script>\n\n`
);
added_legacy_import = true;
}
}
}
/**
* If true, then we need to move all reactive statements to the end of the script block,
* in their correct order. Svelte 4 reordered reactive statements, $derived/$effect.pre
* don't have this behavior.
*/
let needs_reordering = false;
for (const [node, { dependencies }] of state.analysis.reactive_statements) {
/** @type {Binding[]} */
let ids = [];
if (
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
ids = extract_identifiers(node.body.expression.left)
.map((id) => state.scope.get(id.name))
.filter((id) => !!id);
}
if (
dependencies.some(
(dep) =>
!ids.includes(dep) &&
/** @type {number} */ (dep.node.start) > /** @type {number} */ (node.start)
)
) {
needs_reordering = true;
break;
}
}
if (needs_reordering) {
const nodes = Array.from(state.analysis.reactive_statements.keys());
for (const node of nodes) {
const { start, end } = get_node_range(source, node);
str.appendLeft(end, '\n');
str.move(start, end, /** @type {number} */ (parsed.instance?.content.end));
str.remove(start - (source[start - 2] === '\r' ? 2 : 1), start);
}
}
if (state.legacy_imports.size > 0 && !added_legacy_import) {
const script_insertions =
state.script_insertions.size > 0
? `\n${indent}${[...state.script_insertions].join(`\n${indent}`)}`
: '';
if (parsed.instance) {
str.appendRight(
/** @type {number} */ (parsed.instance.content.start),
`\n${indent}${legacy_import}${script_insertions}\n`
);
} else {
str.prepend(
`<script>\n${indent}${legacy_import}\n${indent}${script_insertions}\n</script>\n\n`
);
}
}
return { code: str.toString() };
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error while migrating Svelte code');
throw e;
}
}
/**
* @typedef {{
* scope: Scope;
* str: MagicString;
* analysis: ComponentAnalysis;
* indent: string;
* props: Array<{ local: string; exported: string; init: string; bindable: boolean; slot_name?: string; optional: boolean; type: string; comment?: string }>;
* props_insertion_point: number;
* has_props_rune: boolean;
* end: number;
* names: Record<string, string>;
* legacy_imports: Set<string>;
* script_insertions: Set<string>
* }} State
*/
/** @type {Visitors<SvelteNode, State>} */
const instance_script = {
_(node, { state, next }) {
// @ts-expect-error
const comments = node.leadingComments;
if (comments) {
for (const comment of comments) {
if (comment.type === 'Line') {
const migrated = migrate_svelte_ignore(comment.value);
if (migrated !== comment.value) {
state.str.overwrite(comment.start + '//'.length, comment.end, migrated);
}
}
}
}
next();
},
Identifier(node, { state, path }) {
handle_identifier(node, state, path);
},
ImportDeclaration(node, { state }) {
state.props_insertion_point = node.end ?? state.props_insertion_point;
},
ExportNamedDeclaration(node, { state, next }) {
if (node.declaration) {
next();
return;
}
let count_removed = 0;
for (const specifier of node.specifiers) {
const binding = state.scope.get(specifier.local.name);
if (binding?.kind === 'bindable_prop') {
state.str.remove(
/** @type {number} */ (specifier.start),
/** @type {number} */ (specifier.end)
);
count_removed++;
}
}
if (count_removed === node.specifiers.length) {
state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end));
}
},
VariableDeclaration(node, { state, path }) {
if (state.scope !== state.analysis.instance.scope) {
return;
}
let nr_of_props = 0;
for (const declarator of node.declarations) {
if (state.analysis.runes) {
if (get_rune(declarator.init, state.scope) === '$props') {
state.props_insertion_point = /** @type {number} */ (declarator.id.start) + 1;
state.has_props_rune = true;
}
continue;
}
let bindings;
try {
bindings = state.scope.get_bindings(declarator);
} catch (e) {
// no bindings, so we can skip this
continue;
}
const has_state = bindings.some((binding) => binding.kind === 'state');
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (!has_state && !has_props) {
continue;
}
if (has_props) {
nr_of_props++;
if (declarator.id.type !== 'Identifier') {
// TODO invest time in this?
throw new Error(
'Encountered an export declaration pattern that is not supported for automigration.'
);
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
// const tmp = state.scope.generate('tmp');
// const paths = extract_paths(declarator.id);
// state.props_pre.push(
// b.declaration('const', b.id(tmp), visit(declarator.init!) as Expression)
// );
// for (const path of paths) {
// const name = (path.node as Identifier).name;
// const binding = state.scope.get(name)!;
// const value = path.expression!(b.id(tmp));
// if (binding.kind === 'bindable_prop' || binding.kind === 'rest_prop') {
// state.props.push({
// local: name,
// exported: binding.prop_alias ? binding.prop_alias : name,
// init: value
// });
// state.props_insertion_point = /** @type {number} */(declarator.end);
// } else {
// declarations.push(b.declarator(path.node, value));
// }
// }
}
const name = declarator.id.name;
const binding = /** @type {Binding} */ (state.scope.get(name));
if (state.analysis.uses_props && (declarator.init || binding.updated)) {
throw new Error(
'$$props is used together with named props in a way that cannot be automatically migrated.'
);
}
const prop = state.props.find((prop) => prop.exported === (binding.prop_alias || name));
if (prop) {
// $$Props type was used
prop.init = declarator.init
? state.str.original.substring(
/** @type {number} */ (declarator.init.start),
/** @type {number} */ (declarator.init.end)
)
: '';
prop.bindable = binding.updated;
prop.exported = binding.prop_alias || name;
} else {
state.props.push({
local: name,
exported: binding.prop_alias ? binding.prop_alias : name,
init: declarator.init
? state.str.original.substring(
/** @type {number} */ (declarator.init.start),
/** @type {number} */ (declarator.init.end)
)
: '',
optional: !!declarator.init,
bindable: binding.updated,
...extract_type_and_comment(declarator, state.str, path)
});
}
state.props_insertion_point = /** @type {number} */ (declarator.end);
state.str.update(
/** @type {number} */ (declarator.start),
/** @type {number} */ (declarator.end),
''
);
continue;
}
// state
if (declarator.init) {
let { start, end } = /** @type {{ start: number, end: number }} */ (declarator.init);
if (declarator.init.type === 'SequenceExpression') {
while (state.str.original[start] !== '(') start -= 1;
while (state.str.original[end - 1] !== ')') end += 1;
}
state.str.prependLeft(start, '$state(');
state.str.appendRight(end, ')');
} else {
state.str.prependLeft(
/** @type {number} */ (declarator.id.typeAnnotation?.end ?? declarator.id.end),
' = $state()'
);
}
}
if (nr_of_props === node.declarations.length) {
let start = /** @type {number} */ (node.start);
let end = /** @type {number} */ (node.end);
const parent = path.at(-1);
if (parent?.type === 'ExportNamedDeclaration') {
start = /** @type {number} */ (parent.start);
end = /** @type {number} */ (parent.end);
}
while (state.str.original[start] !== '\n') start--;
while (state.str.original[end] !== '\n') end++;
state.str.update(start, end, '');
}
},
BreakStatement(node, { state, path }) {
if (path[1].type !== 'LabeledStatement') return;
if (node.label?.name !== '$') return;
state.str.update(
/** @type {number} */ (node.start),
/** @type {number} */ (node.end),
'return;'
);
},
LabeledStatement(node, { path, state, next }) {
if (state.analysis.runes) return;
if (path.length > 1) return;
if (node.label.name !== '$') return;
next();
if (
node.body.type === 'ExpressionStatement' &&
node.body.expression.type === 'AssignmentExpression'
) {
const ids = extract_identifiers(node.body.expression.left);
const bindings = ids.map((id) => state.scope.get(id.name));
const reassigned_bindings = bindings.filter((b) => b?.reassigned);
if (reassigned_bindings.length === 0 && !bindings.some((b) => b?.kind === 'store_sub')) {
let { start, end } = /** @type {{ start: number, end: number }} */ (
node.body.expression.right
);
// $derived
state.str.update(
/** @type {number} */ (node.start),
/** @type {number} */ (node.body.expression.start),
'let '
);
if (node.body.expression.right.type === 'SequenceExpression') {
while (state.str.original[start] !== '(') start -= 1;
while (state.str.original[end - 1] !== ')') end += 1;
}
state.str.prependRight(start, `$derived(`);
// in a case like `$: ({ a } = b())`, there's already a trailing parenthesis.
// otherwise, we need to add one
if (state.str.original[/** @type {number} */ (node.body.start)] !== '(') {
state.str.appendLeft(end, `)`);
}
return;
} else {
for (const binding of reassigned_bindings) {
if (binding && ids.includes(binding.node)) {
// implicitly-declared variable which we need to make explicit
state.str.prependRight(
/** @type {number} */ (node.start),
`let ${binding.node.name}${binding.kind === 'state' ? ' = $state()' : ''};\n${state.indent}`
);
}
}
}
}
state.legacy_imports.add('run');
const is_block_stmt = node.body.type === 'BlockStatement';
const start_end = /** @type {number} */ (node.body.start);
// TODO try to find out if we can use $derived.by instead?
if (is_block_stmt) {
state.str.update(
/** @type {number} */ (node.start),
start_end + 1,
`${state.names.run}(() => {`
);
const end = /** @type {number} */ (node.body.end);
state.str.update(end - 1, end, '});');
} else {
state.str.update(
/** @type {number} */ (node.start),
start_end,
`${state.names.run}(() => {\n${state.indent}`
);
state.str.indent(state.indent, {
exclude: [
[0, /** @type {number} */ (node.body.start)],
[/** @type {number} */ (node.body.end), state.end]
]
});
state.str.appendLeft(/** @type {number} */ (node.end), `\n${state.indent}});`);
}
}
};
/** @type {Visitors<SvelteNode, State>} */
const template = {
Identifier(node, { state, path }) {
handle_identifier(node, state, path);
},
RegularElement(node, { state, next }) {
handle_events(node, state);
next();
},
SvelteElement(node, { state, next }) {
if (node.tag.type === 'Literal') {
let is_static = true;
let a = /** @type {number} */ (node.tag.start);
let b = /** @type {number} */ (node.tag.end);
let quote_mark = state.str.original[a - 1];
while (state.str.original[--a] !== '=') {
if (state.str.original[a] === '{') {
is_static = false;
break;
}
}
if (is_static && state.str.original[b] === quote_mark) {
state.str.prependLeft(a + 1, '{');
state.str.appendRight(/** @type {number} */ (node.tag.end) + 1, '}');
}
}
handle_events(node, state);
next();
},
SvelteWindow(node, { state, next }) {
handle_events(node, state);
next();
},
SvelteBody(node, { state, next }) {
handle_events(node, state);
next();
},
SvelteDocument(node, { state, next }) {
handle_events(node, state);
next();
},
SlotElement(node, { state, next }) {
let name = 'children';
let slot_name = 'default';
let slot_props = '{ ';
for (const attr of node.attributes) {
if (attr.type === 'SpreadAttribute') {
slot_props += `...${state.str.original.substring(/** @type {number} */ (attr.expression.start), attr.expression.end)}, `;
} else if (attr.type === 'Attribute') {
if (attr.name === 'name') {
slot_name = /** @type {any} */ (attr.value)[0].data;
} else {
const attr_value =
attr.value === true || Array.isArray(attr.value) ? attr.value : [attr.value];
const value =
attr_value !== true
? state.str.original.substring(
attr_value[0].start,
attr_value[attr_value.length - 1].end
)
: 'true';
slot_props += value === attr.name ? `${value}, ` : `${attr.name}: ${value}, `;
}
}
}
slot_props += '}';
if (slot_props === '{ }') {
slot_props = '';
}
const existing_prop = state.props.find((prop) => prop.slot_name === slot_name);
if (existing_prop) {
name = existing_prop.local;
} else if (slot_name !== 'default') {
name = state.scope.generate(slot_name);
}
if (!existing_prop) {
state.props.push({
local: name,
exported: name,
init: '',
bindable: false,
optional: true,
slot_name,
type: `import('svelte').${slot_props ? 'Snippet<[any]>' : 'Snippet'}`
});
}
if (node.fragment.nodes.length > 0) {
next();
state.str.update(
node.start,
node.fragment.nodes[0].start,
`{#if ${name}}{@render ${name}(${slot_props})}{:else}`
);
state.str.update(node.fragment.nodes[node.fragment.nodes.length - 1].end, node.end, '{/if}');
} else {
state.str.update(node.start, node.end, `{@render ${name}?.(${slot_props})}`);
}
},
Comment(node, { state }) {
const migrated = migrate_svelte_ignore(node.data);
if (migrated !== node.data) {
state.str.overwrite(node.start + '<!--'.length, node.end - '-->'.length, migrated);
}
}
};
/**
* @param {VariableDeclarator} declarator
* @param {MagicString} str
* @param {SvelteNode[]} path
*/
function extract_type_and_comment(declarator, str, path) {
const parent = path.at(-1);
// Try to find jsdoc above the declaration
let comment_node = /** @type {Node} */ (parent)?.leadingComments?.at(-1);
if (comment_node?.type !== 'Block') comment_node = undefined;
const comment_start = /** @type {any} */ (comment_node)?.start;
const comment_end = /** @type {any} */ (comment_node)?.end;
const comment = comment_node && str.original.substring(comment_start, comment_end);
if (comment_node) {
str.update(comment_start, comment_end, '');
}
if (declarator.id.typeAnnotation) {
let start = declarator.id.typeAnnotation.start + 1; // skip the colon
while (str.original[start] === ' ') {
start++;
}
return { type: str.original.substring(start, declarator.id.typeAnnotation.end), comment };
}
// try to find a comment with a type annotation, hinting at jsdoc
if (parent?.type === 'ExportNamedDeclaration' && comment_node) {
const match = /@type {(.+)}/.exec(comment_node.value);
if (match) {
return { type: match[1] };
}
}
// try to infer it from the init
if (declarator.init?.type === 'Literal') {
const type = typeof declarator.init.value;
if (type === 'string' || type === 'number' || type === 'boolean') {
return { type, comment };
}
}
return { type: 'any', comment };
}
// Ensure modifiers are applied in the same order as Svelte 4
const modifier_order = [
'preventDefault',
'stopPropagation',
'stopImmediatePropagation',
'self',
'trusted',
'once'
];
/**
* @param {AST.RegularElement | AST.SvelteElement | AST.SvelteWindow | AST.SvelteDocument | AST.SvelteBody} element
* @param {State} state
*/
function handle_events(element, state) {
/** @type {Map<string, AST.OnDirective[]>} */
const handlers = new Map();
for (const attribute of element.attributes) {
if (attribute.type !== 'OnDirective') continue;
let name = `on${attribute.name}`;
if (attribute.modifiers.includes('capture')) {
name += 'capture';
}
const nodes = handlers.get(name) || [];
nodes.push(attribute);
handlers.set(name, nodes);
}
for (const [name, nodes] of handlers) {
const handlers = [];
let first = null;
for (const node of nodes) {
/** @type {string} */
let body;
if (node.expression) {
body = state.str.original.substring(
/** @type {number} */ (node.expression.start),
/** @type {number} */ (node.expression.end)
);
} else {
body = `${state.names.bubble}('${node.name}')`;
state.legacy_imports.add('createBubbler');
state.script_insertions.add(
`const ${state.names.bubble} = ${state.names.createBubbler}();`
);
}
const has_passive = node.modifiers.includes('passive');
const has_nonpassive = node.modifiers.includes('nonpassive');
const modifiers = modifier_order.filter((modifier) => node.modifiers.includes(modifier));
for (const modifier of modifiers) {
state.legacy_imports.add(modifier);
body = `${state.names[modifier]}(${body})`;
}
if (has_passive || has_nonpassive) {
const action = has_passive ? 'passive' : 'nonpassive';
state.legacy_imports.add(action);
state.str.overwrite(
node.start,
node.end,
`use:${state.names[action]}={['${node.name}', () => ${body}]}`
);
} else {
if (first) {
let start = node.start;
let end = node.end;
while (/[\s\n]/.test(state.str.original[start - 1])) start -= 1;
state.str.remove(start, end);
} else {
first = node;
}
handlers.push(body);
}
}
if (first) {
/** @type {string} */
let replacement;
if (handlers.length > 1) {
state.legacy_imports.add('handlers');
replacement = `${name}={${state.names.handlers}(${handlers.join(', ')})}`;
} else {
const handler = handlers[0];
replacement = handler === name ? `{${handler}}` : `${name}={${handler}}`;
}
state.str.overwrite(first.start, first.end, replacement);
}
}
}
/**
* Returns start and end of the node. If the start is preceeded with white-space-only before a line break,
* the start will be the start of the line.
* @param {string} source
* @param {Node} node
*/
function get_node_range(source, node) {
let start = /** @type {number} */ (node.start);
let end = /** @type {number} */ (node.end);
let idx = start;
while (source[idx - 1] !== '\n' && source[idx - 1] !== '\r') {
idx--;
if (source[idx] !== ' ' && source[idx] !== '\t') {
idx = start;
break;
}
}
start = idx;
return { start, end };
}
/**
* @param {AST.OnDirective} last
* @param {State} state
*/
function generate_event_name(last, state) {
const scope =
(last.expression && state.analysis.template.scopes.get(last.expression)) || state.scope;
let name = 'event';
if (!scope.get(name)) return name;
let i = 1;
while (scope.get(`${name}${i}`)) i += 1;
return `${name}${i}`;
}
/**
* @param {Identifier} node
* @param {State} state
* @param {any[]} path
*/
function handle_identifier(node, state, path) {
const parent = path.at(-1);
if (parent?.type === 'MemberExpression' && parent.property === node) return;
if (state.analysis.uses_props) {
if (node.name === '$$props' || node.name === '$$restProps') {
// not 100% correct for $$restProps but it'll do
state.str.update(
/** @type {number} */ (node.start),
/** @type {number} */ (node.end),
state.names.props
);
} else {
const binding = state.scope.get(node.name);
if (binding?.kind === 'bindable_prop') {
state.str.prependLeft(/** @type {number} */ (node.start), `${state.names.props}.`);
}
}
} else if (node.name === '$$restProps' && state.analysis.uses_rest_props) {
state.str.update(
/** @type {number} */ (node.start),
/** @type {number} */ (node.end),
state.names.rest
);
} else if (node.name === '$$slots' && state.analysis.uses_slots) {
if (parent?.type === 'MemberExpression') {
state.str.update(/** @type {number} */ (node.start), parent.property.start, '');
if (parent.property.name === 'default') {
state.str.update(parent.property.start, parent.property.end, 'children');
}
}
// else passed as identifier, we don't know what to do here, so let it error
} else if (
parent?.type === 'TSInterfaceDeclaration' ||
parent?.type === 'TSTypeAliasDeclaration'
) {
const members =
parent.type === 'TSInterfaceDeclaration' ? parent.body.body : parent.typeAnnotation?.members;
if (Array.isArray(members)) {
if (node.name === '$$Props') {
for (const member of members) {
const prop = state.props.find((prop) => prop.exported === member.key.name);
const type = state.str.original.substring(
member.typeAnnotation.typeAnnotation.start,
member.typeAnnotation.typeAnnotation.end
);
let comment;
const comment_node = member.leadingComments?.at(-1);
if (comment_node?.type === 'Block') {
comment = state.str.original.substring(comment_node.start, comment_node.end);
}
if (prop) {
prop.type = type;
prop.optional = member.optional;
prop.comment = comment ?? prop.comment;
} else {
state.props.push({
local: member.key.name,
exported: member.key.name,
init: '',
bindable: false,
optional: member.optional,
type,
comment
});
}
}
state.str.remove(parent.start, parent.end);
}
}
}
}
/** @param {string} content */
function guess_indent(content) {
const lines = content.split('\n');
const tabbed = lines.filter((line) => /^\t+/.test(line));
const spaced = lines.filter((line) => /^ {2,}/.test(line));
if (tabbed.length === 0 && spaced.length === 0) {
return '\t';
}
// More lines tabbed than spaced? Assume tabs, and
// default to tabs in the case of a tie (or nothing
// to go on)
if (tabbed.length >= spaced.length) {
return '\t';
}
// Otherwise, we need to guess the multiple
const min = spaced.reduce((previous, current) => {
const count = /^ +/.exec(current)?.[0].length ?? 0;
return Math.min(count, previous);
}, Infinity);
return ' '.repeat(min);
}