-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add self-closing-tags migration (#12128)
* feat: add self-closing-tags migration Companion to sveltejs/svelte#11114. This adds an npx svelte-migrate self-closing-tags migration that replaces all the self-closing non-void elements in your .svelte files. * use local Svelte installation
- Loading branch information
1 parent
f71f381
commit bd54aa1
Showing
16 changed files
with
310 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"svelte-migrate": minor | ||
--- | ||
|
||
feat: add self-closing-tags migration |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
packages/kit/test/apps/basics/src/routes/data-sveltekit/noscroll/+page.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
packages/kit/test/apps/basics/src/routes/data-sveltekit/noscroll/target/+page.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
<div style="height: 2000px; background: palegoldenrod" /> | ||
<div style="height: 2000px; background: palegoldenrod"></div> | ||
|
||
<h1>target</h1> |
2 changes: 1 addition & 1 deletion
2
packages/kit/test/apps/basics/src/routes/iframes/nested/parent/+page.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
<iframe title="Child content" src="./child" /> | ||
<iframe title="Child content" src="./child"></iframe> |
2 changes: 1 addition & 1 deletion
2
packages/kit/test/apps/basics/src/routes/no-ssr/margin/+page.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,4 +12,4 @@ | |
|
||
<a href="/routing/b" data-sveltekit-reload>b</a> | ||
|
||
<div class="hydrate-test" /> | ||
<div class="hydrate-test"></div> |
2 changes: 1 addition & 1 deletion
2
packages/kit/test/apps/basics/src/routes/scroll/cross-document/a/+page.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
<h1>a</h1> | ||
|
||
<div style="height: 200vh; background: teal" /> | ||
<div style="height: 200vh; background: teal"></div> | ||
|
||
<a data-sveltekit-reload href="/scroll/cross-document/b">b</a> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import colors from 'kleur'; | ||
import fs from 'node:fs'; | ||
import prompts from 'prompts'; | ||
import glob from 'tiny-glob/sync.js'; | ||
import { remove_self_closing_tags } from './migrate.js'; | ||
import { pathToFileURL } from 'node:url'; | ||
import { resolve } from 'import-meta-resolve'; | ||
|
||
export async function migrate() { | ||
let compiler; | ||
try { | ||
compiler = await import_from_cwd('svelte/compiler'); | ||
} catch (e) { | ||
console.log(colors.bold().red('❌ Could not find a local Svelte installation.')); | ||
return; | ||
} | ||
|
||
console.log( | ||
colors.bold().yellow('\nThis will update .svelte files inside the current directory\n') | ||
); | ||
|
||
const response = await prompts({ | ||
type: 'confirm', | ||
name: 'value', | ||
message: 'Continue?', | ||
initial: false | ||
}); | ||
|
||
if (!response.value) { | ||
process.exit(1); | ||
} | ||
|
||
const files = glob('**/*.svelte') | ||
.map((file) => file.replace(/\\/g, '/')) | ||
.filter((file) => !file.includes('/node_modules/')); | ||
|
||
for (const file of files) { | ||
try { | ||
const code = await remove_self_closing_tags(compiler, fs.readFileSync(file, 'utf-8')); | ||
fs.writeFileSync(file, code); | ||
} catch (e) { | ||
// continue | ||
} | ||
} | ||
|
||
console.log(colors.bold().green('✔ Your project has been updated')); | ||
console.log(' If using Prettier, please upgrade to the latest prettier-plugin-svelte version'); | ||
} | ||
|
||
/** @param {string} name */ | ||
async function import_from_cwd(name) { | ||
const cwd = pathToFileURL(process.cwd()).href; | ||
const url = await resolve(name, cwd + '/x.js'); | ||
|
||
return import(url); | ||
} |
191 changes: 191 additions & 0 deletions
191
packages/migrate/migrations/self-closing-tags/migrate.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
import MagicString from 'magic-string'; | ||
import { walk } from 'zimmerframe'; | ||
|
||
const VoidElements = [ | ||
'area', | ||
'base', | ||
'br', | ||
'col', | ||
'embed', | ||
'hr', | ||
'img', | ||
'input', | ||
'keygen', | ||
'link', | ||
'menuitem', | ||
'meta', | ||
'param', | ||
'source', | ||
'track', | ||
'wbr' | ||
]; | ||
|
||
const SVGElements = [ | ||
'altGlyph', | ||
'altGlyphDef', | ||
'altGlyphItem', | ||
'animate', | ||
'animateColor', | ||
'animateMotion', | ||
'animateTransform', | ||
'circle', | ||
'clipPath', | ||
'color-profile', | ||
'cursor', | ||
'defs', | ||
'desc', | ||
'discard', | ||
'ellipse', | ||
'feBlend', | ||
'feColorMatrix', | ||
'feComponentTransfer', | ||
'feComposite', | ||
'feConvolveMatrix', | ||
'feDiffuseLighting', | ||
'feDisplacementMap', | ||
'feDistantLight', | ||
'feDropShadow', | ||
'feFlood', | ||
'feFuncA', | ||
'feFuncB', | ||
'feFuncG', | ||
'feFuncR', | ||
'feGaussianBlur', | ||
'feImage', | ||
'feMerge', | ||
'feMergeNode', | ||
'feMorphology', | ||
'feOffset', | ||
'fePointLight', | ||
'feSpecularLighting', | ||
'feSpotLight', | ||
'feTile', | ||
'feTurbulence', | ||
'filter', | ||
'font', | ||
'font-face', | ||
'font-face-format', | ||
'font-face-name', | ||
'font-face-src', | ||
'font-face-uri', | ||
'foreignObject', | ||
'g', | ||
'glyph', | ||
'glyphRef', | ||
'hatch', | ||
'hatchpath', | ||
'hkern', | ||
'image', | ||
'line', | ||
'linearGradient', | ||
'marker', | ||
'mask', | ||
'mesh', | ||
'meshgradient', | ||
'meshpatch', | ||
'meshrow', | ||
'metadata', | ||
'missing-glyph', | ||
'mpath', | ||
'path', | ||
'pattern', | ||
'polygon', | ||
'polyline', | ||
'radialGradient', | ||
'rect', | ||
'set', | ||
'solidcolor', | ||
'stop', | ||
'svg', | ||
'switch', | ||
'symbol', | ||
'text', | ||
'textPath', | ||
'tref', | ||
'tspan', | ||
'unknown', | ||
'use', | ||
'view', | ||
'vkern' | ||
]; | ||
|
||
/** | ||
* @param {{ preprocess: any, parse: any }} svelte_compiler | ||
* @param {string} source | ||
*/ | ||
export async function remove_self_closing_tags({ preprocess, parse }, source) { | ||
const preprocessed = await preprocess(source, { | ||
/** @param {{ content: string }} input */ | ||
script: ({ content }) => ({ | ||
code: content | ||
.split('\n') | ||
.map((line) => ' '.repeat(line.length)) | ||
.join('\n') | ||
}), | ||
/** @param {{ content: string }} input */ | ||
style: ({ content }) => ({ | ||
code: content | ||
.split('\n') | ||
.map((line) => ' '.repeat(line.length)) | ||
.join('\n') | ||
}) | ||
}); | ||
const ast = parse(preprocessed.code); | ||
const ms = new MagicString(source); | ||
/** @type {Array<() => void>} */ | ||
const updates = []; | ||
let is_foreign = false; | ||
let is_custom_element = false; | ||
|
||
walk(ast.html, null, { | ||
_(node, { next, stop }) { | ||
if (node.type === 'Options') { | ||
const namespace = node.attributes.find( | ||
/** @param {any} a */ | ||
(a) => a.type === 'Attribute' && a.name === 'namespace' | ||
); | ||
if (namespace?.value[0].data === 'foreign') { | ||
is_foreign = true; | ||
stop(); | ||
return; | ||
} | ||
|
||
is_custom_element = node.attributes.some( | ||
/** @param {any} a */ | ||
(a) => a.type === 'Attribute' && (a.name === 'customElement' || a.name === 'tag') | ||
); | ||
} | ||
|
||
if (node.type === 'Element' || node.type === 'Slot') { | ||
const is_self_closing = source[node.end - 2] === '/'; | ||
if ( | ||
!is_self_closing || | ||
VoidElements.includes(node.name) || | ||
SVGElements.includes(node.name) || | ||
!/^[a-z0-9_-]+$/.test(node.name) | ||
) { | ||
return; | ||
} | ||
|
||
let start = node.end - 2; | ||
if (source[start - 1] === ' ') { | ||
start--; | ||
} | ||
updates.push(() => { | ||
if (node.type === 'Element' || is_custom_element) { | ||
ms.update(start, node.end, `></${node.name}>`); | ||
} | ||
}); | ||
} | ||
|
||
next(); | ||
} | ||
}); | ||
|
||
if (is_foreign) { | ||
return source; | ||
} | ||
|
||
updates.forEach((update) => update()); | ||
return ms.toString(); | ||
} |
31 changes: 31 additions & 0 deletions
31
packages/migrate/migrations/self-closing-tags/migrate.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { assert, test } from 'vitest'; | ||
import * as compiler from 'svelte/compiler'; | ||
import { remove_self_closing_tags } from './migrate.js'; | ||
|
||
/** @type {Record<string, string>} */ | ||
const tests = { | ||
'<div/>': '<div></div>', | ||
'<div />': '<div></div>', | ||
'<custom-element />': '<custom-element></custom-element>', | ||
'<div class="foo"/>': '<div class="foo"></div>', | ||
'<div class="foo" />': '<div class="foo"></div>', | ||
'\t<div\n\t\tonclick={blah}\n\t/>': '\t<div\n\t\tonclick={blah}\n\t></div>', | ||
'<foo-bar/>': '<foo-bar></foo-bar>', | ||
'<link/>': '<link/>', | ||
'<link />': '<link />', | ||
'<svg><g /></svg>': '<svg><g /></svg>', | ||
'<slot />': '<slot />', | ||
'<svelte:options customElement="my-element" /><slot />': | ||
'<svelte:options customElement="my-element" /><slot></slot>', | ||
'<svelte:options namespace="foreign" /><foo />': '<svelte:options namespace="foreign" /><foo />', | ||
'<script>console.log("<div />")</script>': '<script>console.log("<div />")</script>', | ||
'<script lang="ts">let a: string = ""</script><div />': | ||
'<script lang="ts">let a: string = ""</script><div></div>' | ||
}; | ||
|
||
for (const input in tests) { | ||
test(input, async () => { | ||
const output = tests[input]; | ||
assert.equal(await remove_self_closing_tags(compiler, input), output); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.