Skip to content

Commit

Permalink
feat: add self-closing-tags migration
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dummdidumm committed Apr 15, 2024
1 parent f71f381 commit 212e084
Show file tree
Hide file tree
Showing 16 changed files with 271 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/sixty-walls-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte-migrate": minor
---

feat: add self-closing-tags migration
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
stageHeight: window.innerHeight,
colors: ['#ff3e00', '#40b3ff', '#676778']
}}
/>
></div>
{/if}

<style>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
<p id="go-to-element">The browser scrolls to me</p>
</div>
<p id="abcde" style="height: 180vh; background-color: hotpink;">I take precedence</p>
<div />
<div></div>
<a href="/anchor-with-manual-scroll/anchor-afternavigate?x=y#go-to-element">reload me</a>
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
<p id="go-to-element">The browser scrolls to me</p>
</div>
<p id="abcde" style="height: 180vh; background-color: hotpink;">I take precedence</p>
<div />
<div></div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div style="height: 2000px; background: palegoldenrod" />
<div style="height: 2000px; background: palegoldenrod"></div>

<a id="one" href="/data-sveltekit/noscroll/target" data-sveltekit-noscroll>one</a>

Expand Down
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>
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>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="container">
<span> ^this is not the top of the screen</span>
<div class="spacer" />
<div class="spacer"></div>
</div>

<style>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@

<a href="/routing/b" data-sveltekit-reload>b</a>

<div class="hydrate-test" />
<div class="hydrate-test"></div>
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>
38 changes: 38 additions & 0 deletions packages/migrate/migrations/self-closing-tags/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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';

export async function migrate() {
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(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');
}
184 changes: 184 additions & 0 deletions packages/migrate/migrations/self-closing-tags/migrate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import MagicString from 'magic-string';
import { parse, preprocess, walk } from 'svelte/compiler';

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 {string} source */
export async function remove_self_closing_tags(source) {
const preprocessed = await preprocess(source, {
script: ({ content }) => ({
code: content
.split('\n')
.map((line) => ' '.repeat(line.length))
.join('\n')
}),
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(/** @type {any} */ (ast.html), {
/** @param {Record<string, any>} node */
enter(node) {
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;
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-zA-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}>`);
}
});
}
}
});

if (is_foreign) {
return source;
}

updates.forEach((update) => update());
return ms.toString();
}
30 changes: 30 additions & 0 deletions packages/migrate/migrations/self-closing-tags/migrate.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { assert, test } from 'vitest';
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(input), output);
});
}
1 change: 1 addition & 0 deletions packages/migrate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"magic-string": "^0.30.5",
"prompts": "^2.4.2",
"semver": "^7.5.4",
"svelte": "^4.0.0",
"tiny-glob": "^0.2.9",
"ts-morph": "^22.0.0",
"typescript": "^5.3.3"
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sites/kit.svelte.dev/src/routes/home/Video.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
</video>

{#if d}
<div class="progress-bar" style={`width: ${(t / d) * 100}%`} />
<div class="progress-bar" style={`width: ${(t / d) * 100}%`}></div>
{/if}

<div class="top-controls">
Expand Down

0 comments on commit 212e084

Please sign in to comment.