Skip to content

Commit

Permalink
feat: add self-closing-tags migration (#12128)
Browse files Browse the repository at this point in the history
* 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
dummdidumm authored Apr 16, 2024
1 parent f71f381 commit bd54aa1
Show file tree
Hide file tree
Showing 16 changed files with 310 additions and 11 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>
56 changes: 56 additions & 0 deletions packages/migrate/migrations/self-closing-tags/index.js
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 packages/migrate/migrations/self-closing-tags/migrate.js
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 packages/migrate/migrations/self-closing-tags/migrate.spec.js
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);
});
}
5 changes: 4 additions & 1 deletion packages/migrate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,22 @@
"!migrations/**/samples.md"
],
"dependencies": {
"import-meta-resolve": "^4.0.0",
"kleur": "^4.1.5",
"magic-string": "^0.30.5",
"prompts": "^2.4.2",
"semver": "^7.5.4",
"tiny-glob": "^0.2.9",
"ts-morph": "^22.0.0",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"zimmerframe": "^1.1.2"
},
"devDependencies": {
"@types/node": "^18.19.3",
"@types/prompts": "^2.4.9",
"@types/semver": "^7.5.6",
"prettier": "^3.1.1",
"svelte": "^4.2.10",
"vitest": "^1.5.0"
},
"scripts": {
Expand Down
Loading

0 comments on commit bd54aa1

Please sign in to comment.