From 528da1c1b4c0f4571e256daaaeab3110eae1300b Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Sat, 21 Sep 2024 19:13:55 +0200 Subject: [PATCH] fix: migrate events to be more inline with svelte 4 --- packages/svelte/src/compiler/migrate/index.js | 164 +++--------------- packages/svelte/src/legacy/legacy-client.js | 31 ++++ .../samples/event-handlers/output.svelte | 73 +++----- .../migrate/samples/state-ts/output.svelte | 4 +- 4 files changed, 77 insertions(+), 195 deletions(-) diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 89f03cc3e3..f38fe91454 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -54,7 +54,9 @@ export function migrate(source) { rest_props_name: analysis.root.unique('rest').name, end: source.length, run_name: analysis.root.unique('run').name, - needs_run: false + listener_name: analysis.root.unique('listener').name, + needs_run: false, + needs_listener: false }; if (parsed.module) { @@ -71,7 +73,7 @@ export function migrate(source) { state = { ...state, scope: analysis.template.scope }; walk(parsed.fragment, state, template); - const run_import = `import { run${state.run_name === 'run' ? '' : ` as ${state.run_name}`} } from 'svelte/legacy';`; + const legacy_import = `import { ${state.needs_run ? `run${state.run_name === 'run' ? '' : ` as ${state.run_name}`}` : ''}${state.needs_listener ? `${state.needs_run ? ', ' : ''}listener${state.listener_name === 'listener' ? '' : ` as ${state.listener_name}`}` : ''} } from 'svelte/legacy';`; let added_legacy_import = false; if (state.props.length > 0 || analysis.uses_rest_props || analysis.uses_props) { @@ -145,7 +147,8 @@ export function migrate(source) { props_declaration = `\n${indent}${props_declaration}`; str.appendRight(state.props_insertion_point, props_declaration); } else { - const imports = state.needs_run ? `${indent}${run_import}\n` : ''; + const imports = + state.needs_run || state.needs_listener ? `${indent}${legacy_import}\n` : ''; str.prepend(`\n\n`); added_legacy_import = true; } @@ -193,14 +196,14 @@ export function migrate(source) { } } - if (state.needs_run && !added_legacy_import) { + if ((state.needs_run || state.needs_listener) && !added_legacy_import) { if (parsed.instance) { str.appendRight( /** @type {number} */ (parsed.instance.content.start), - `\n${indent}${run_import}\n` + `\n${indent}${legacy_import}\n` ); } else { - str.prepend(`\n\n`); + str.prepend(`\n\n`); } } @@ -225,7 +228,9 @@ export function migrate(source) { * rest_props_name: string; * end: number; * run_name: string; + * listener_name: string; * needs_run: boolean; + * needs_listener: boolean; * }} State */ @@ -702,38 +707,19 @@ function handle_events(element, state) { last.expression.params[0]?.type === 'Identifier' ? last.expression.params[0].name : generate_event_name(last, state); - let prepend = ''; - for (let i = 0; i < nodes.length - 1; i += 1) { + for (let i = 0; i < nodes.length; i += 1) { + let prepend = ''; const node = nodes[i]; const indent = get_indent(state, node, element); + const args = `,"${node.name}"${node.modifiers.length > 0 ? `,${JSON.stringify(node.modifiers)}` : ''}`; if (node.expression) { - let body = ''; - if (node.expression.type === 'ArrowFunctionExpression') { - body = state.str.original.substring( - /** @type {number} */ (node.expression.body.start), - /** @type {number} */ (node.expression.body.end) - ); - } else { - body = `${state.str.original.substring( - /** @type {number} */ (node.expression.start), - /** @type {number} */ (node.expression.end) - )}();`; - } - - for (const modifier of node.modifiers) { - if (modifier === 'stopPropagation') { - body = `\n${indent}${payload_name}.stopPropagation();\n${body}`; - } else if (modifier === 'preventDefault') { - body = `\n${indent}${payload_name}.preventDefault();\n${body}`; - } else if (modifier === 'stopImmediatePropagation') { - body = `\n${indent}${payload_name}.stopImmediatePropagation();\n${body}`; - } else { - body = `\n${indent}// @migration-task: incorporate ${modifier} modifier\n${body}`; - } - } - - prepend += `\n${indent}${body}\n`; + let body = state.str.original.substring( + /** @type {number} */ (node.expression.start), + /** @type {number} */ (node.expression.end) + ); + prepend += `use:${state.listener_name}={[${body}${args}]}`; + state.needs_listener = true; } else { if (!local) { local = state.scope.generate(`on${node.name}`); @@ -746,115 +732,11 @@ function handle_events(element, state) { type: '(event: any) => void' }); } - prepend += `\n${indent}${local}?.(${payload_name});\n`; - } - - state.str.remove(node.start, node.end); - } - - if (last.expression) { - // remove : from on:click - state.str.remove(last.start + 2, last.start + 3); - // remove modifiers - if (last.modifiers.length > 0) { - state.str.remove( - last.start + last.name.length + 3, - state.str.original.indexOf('=', last.start) - ); - } - if (last.modifiers.includes('capture')) { - state.str.appendRight(last.start + last.name.length + 3, 'capture'); - } - - const indent = get_indent(state, last, element); - - for (const modifier of last.modifiers) { - if (modifier === 'stopPropagation') { - prepend += `\n${indent}${payload_name}.stopPropagation();\n`; - } else if (modifier === 'preventDefault') { - prepend += `\n${indent}${payload_name}.preventDefault();\n`; - } else if (modifier === 'stopImmediatePropagation') { - prepend += `\n${indent}${payload_name}.stopImmediatePropagation();\n`; - } else if (modifier !== 'capture') { - prepend += `\n${indent}// @migration-task: incorporate ${modifier} modifier\n`; - } - } - - if (prepend) { - let pos = last.expression.start; - if (last.expression.type === 'ArrowFunctionExpression') { - pos = last.expression.body.start; - if ( - last.expression.params.length > 0 && - last.expression.params[0].type !== 'Identifier' - ) { - const start = /** @type {number} */ (last.expression.params[0].start); - const end = /** @type {number} */ (last.expression.params[0].end); - // replace event payload with generated one that others use, - // then destructure generated payload param into what the user wrote - state.str.overwrite(start, end, payload_name); - prepend = `let ${state.str.original.substring( - start, - end - )} = ${payload_name};\n${prepend}`; - } else if (last.expression.params.length === 0) { - // add generated payload param to arrow function - const pos = state.str.original.lastIndexOf(')', last.expression.body.start); - state.str.prependLeft(pos, payload_name); - } - - const needs_curlies = last.expression.body.type !== 'BlockStatement'; - const end = /** @type {number} */ (last.expression.body.end) - (needs_curlies ? 0 : 1); - pos = /** @type {number} */ (pos) + (needs_curlies ? 0 : 1); - if (needs_curlies && state.str.original[pos - 1] === '(') { - // Prettier does something like on:click={() => (foo = true)}, we need to remove the braces in this case - state.str.update(pos - 1, pos, `{${prepend}${indent}`); - state.str.update(end, end + 1, `\n${indent.slice(state.indent.length)}}`); - } else { - state.str.prependRight(pos, `${needs_curlies ? '{' : ''}${prepend}${indent}`); - state.str.appendRight( - end, - `\n${indent.slice(state.indent.length)}${needs_curlies ? '}' : ''}` - ); - } - } else { - state.str.update( - /** @type {number} */ (last.expression.start), - /** @type {number} */ (last.expression.end), - `(${payload_name}) => {${prepend}\n${indent}${state.str.original.substring( - /** @type {number} */ (last.expression.start), - /** @type {number} */ (last.expression.end) - )}?.(${payload_name});\n}` - ); - } - } - } else { - // turn on:click into a prop - // Check if prop already set, could happen when on:click on different elements - if (!local) { - local = state.scope.generate(`on${last.name}`); - state.props.push({ - local, - exported, - init: '', - bindable: false, - optional: true, - type: '(event: any) => void' - }); - } - - let replacement = ''; - if (!prepend) { - if (exported === local) { - replacement = `{${name}}`; - } else { - replacement = `${name}={${local}}`; - } - } else { - replacement = `${name}={(${payload_name}) => {${prepend}\n${state.indent}${local}?.(${payload_name});\n}}`; + prepend += `use:${state.listener_name}={[(${payload_name})=>{${local}?.(${payload_name});}${args}]}`; + state.needs_listener = true; } - state.str.update(last.start, last.end, replacement); + state.str.overwrite(node.start, node.end, prepend); } } } diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 23f00bc30a..57650a29a5 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -170,3 +170,34 @@ class Svelte4Component { export function run(fn) { user_pre_effect(fn); } + +/** + * @param {EventTarget} node + * @param {[EventListener, string, string[]]} options + */ +export function listener(node, [handler, type, modifiers = []]) { + const aborter = new AbortController(); + node.addEventListener( + type, + (e) => { + for (let modifier of modifiers) { + // @ts-expect-error modifier is just a string + if (modifier in e && typeof e[modifier] === 'function') { + // @ts-expect-error modifier is just a string + e[modifier](); + } + } + handler?.(e); + }, + { + signal: aborter.signal, + passive: modifiers.includes('passive'), + capture: modifiers.includes('capture') + } + ); + return { + destroy() { + aborter.abort(); + } + }; +} diff --git a/packages/svelte/tests/migrate/samples/event-handlers/output.svelte b/packages/svelte/tests/migrate/samples/event-handlers/output.svelte index a424a686bc..a31e3e9d54 100644 --- a/packages/svelte/tests/migrate/samples/event-handlers/output.svelte +++ b/packages/svelte/tests/migrate/samples/event-handlers/output.svelte @@ -1,73 +1,40 @@ - - + - - + + - - - - + + + + - - - - - - + + + + + +
click me click me - +
\ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/state-ts/output.svelte b/packages/svelte/tests/migrate/samples/state-ts/output.svelte index c6f672c535..e62447a2e2 100644 --- a/packages/svelte/tests/migrate/samples/state-ts/output.svelte +++ b/packages/svelte/tests/migrate/samples/state-ts/output.svelte @@ -1,8 +1,10 @@
- \ No newline at end of file + \ No newline at end of file