Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Svmigrate deletes styles and replaces with /*$$__STYLE_CONTENT__$$*/ #14712

Closed
justingolden21 opened this issue Dec 14, 2024 · 17 comments
Closed
Labels
awaiting submitter needs a reproduction, or clarification

Comments

@justingolden21
Copy link

Describe the bug

Deleted all my styles and output /*$$__STYLE_CONTENT__$$*/

image

Reproduction

I ran npx sv migrate svelte-5

I had a file with an "error" due to having a <tr> inside a <table>.

I understand and are technically correct, but

  1. This shouldn't break the entire file and migration script.
  2. Nearly all browsers will insert the tbody anyway / figure out how to render the table. I've been developing websites for a decade and it's never shown up as much as an a11y warning on lighthouse, let alone an actual a11y problem or fail for browser engine to render.
  3. This should be a configuration setting.

I understand something more important like not having a button within a button, but for a tr inside a table, this is ridiculous.

I've got a svelte 4 project that is quite large, and I'm not doing anything wacky or hacky or legacy, and it's taking me hours to sift through all this and fix all the the hundreds of lines that svmigrate deleted.

I appreciate the devs hard at work on svmigrate; it's well beyond my paygrade here and (hopefully in the future) saves devs a lot of time and headache.

Logs

No response

System Info

System:
    OS: Windows 10 10.0.19045
    CPU: (16) x64 11th Gen Intel(R) Core(TM) i7-11700K @ 3.60GHz
    Memory: 20.06 GB / 31.87 GB
  Binaries:
    Node: 20.11.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 10.2.4 - C:\Program Files\nodejs\npm.CMD
    pnpm: 8.14.1 - ~\AppData\Roaming\npm\pnpm.CMD
  Browsers:
    Edge: Chromium (127.0.2651.74)
    Internet Explorer: 11.0.19041.4355

Severity

blocking an upgrade

@justingolden21
Copy link
Author

Probably a separate issue, but the migrate script couldn't migrate

const squares = writable<Cubie[]>([]);
...
{#each $squares as { x, y, color }}

And spit out:
<!-- @migration-task Error while migrating Svelte code: The $ prefix is reserved, and cannot be used for variables and imports -->

The migration script can't fix writables?

@paoloricciuti
Copy link
Member

Probably a separate issue, but the migrate script couldn't migrate

const squares = writable<Cubie[]>([]);
...
{#each $squares as { x, y, color }}

And spit out: <!-- @migration-task Error while migrating Svelte code: The $ prefix is reserved, and cannot be used for variables and imports -->

The migration script can't fix writables?

Can you provide a reproduction for this?

@paoloricciuti
Copy link
Member

As per the original issue: which version of svelte are you on? This was fixed before and i can't reproduce it locally. So please provide a reproduction for both issues.

I understand something more important like not having a button within a button, but for a tr inside a table, this is ridiculous.

Also please, we are all here to help you and this tone doesn't adhere to our code of conduct. The reason why svelte errors out in such cases is not only to make your code be more correct but also because the browser adding an extra <tbody> could break the hydration algorithm. I wonder tho if we could detect and migrate such cases.

@paoloricciuti paoloricciuti added the awaiting submitter needs a reproduction, or clarification label Dec 15, 2024
@justingolden21
Copy link
Author

@paoloricciuti

Thanks for the help. Below is the exact file that failed on the cubie issue I mentioned:

File
<script context="module" lang="ts">
	/**
	 * Displays a 2d rubiks scramble color map, scramble string and quick buttons
	 *
	 * Uses the current scramble stored in settings
	 */

	import { get, writable } from 'svelte/store';
	import TailwindColors from 'tailwindcss/colors.js';

	import { getKeys } from '$/types';
	import Cube, { type Face, type Move, type SquareKey } from '$lib/util/rubiksCube';
	import getScramble from '$lib/util/rubiksScrambleGenerator';

	// (x, y) positions on canvas to draw squares
	type Position = [number, number];

	type Cubie = {
		x: number;
		y: number;
		color: string;
	};

	const squares = writable<Cubie[]>([]);

	function addSquare(position: Position, color: string) {
		squares.update((s) => {
			s.push({
				x: position[0],
				y: position[1],
				color: color
			});
			return s;
		});
	}

	// Modified from https://github.com/notchahm/js_speedcube_scrambler
	function drawScramble(cube: Cube) {
		const cornerLetterPositionMap = {
			A: [155, 0],
			B: [255, 0],
			C: [255, 100],
			D: [155, 100],
			E: [0, 155],
			F: [100, 155],
			G: [100, 255],
			H: [0, 255],
			I: [155, 155],
			J: [255, 155],
			K: [255, 255],
			L: [155, 255],
			M: [310, 155],
			N: [410, 155],
			O: [410, 255],
			P: [310, 255],
			Q: [465, 155],
			R: [565, 155],
			S: [565, 255],
			T: [465, 255],
			U: [155, 310],
			V: [255, 310],
			W: [255, 410],
			X: [155, 410]
		} as Record<SquareKey, Position>;
		const edgeLetterPositionMap = {
			A: [205, 0],
			B: [255, 50],
			C: [205, 100],
			D: [155, 50],
			E: [50, 155],
			F: [100, 205],
			G: [50, 255],
			H: [0, 205],
			I: [205, 155],
			J: [255, 205],
			K: [205, 255],
			L: [155, 205],
			M: [360, 155],
			N: [410, 205],
			O: [360, 255],
			P: [310, 205],
			Q: [515, 155],
			R: [565, 205],
			S: [515, 255],
			T: [465, 205],
			U: [205, 310],
			V: [255, 360],
			W: [205, 410],
			X: [155, 360]
		} as Record<SquareKey, Position>;
		const centerPositionMap = {
			W: [205, 50],
			Y: [205, 360],
			O: [50, 205],
			R: [360, 205],
			B: [515, 205],
			G: [205, 205]
		} as Record<Face, Position>;

		// Same as `colorRGB` in `RubiksCube3DModel.svelte`
		const colorMap = {
			W: '#fff',
			Y: TailwindColors.yellow[400], // #facc15
			O: TailwindColors.orange[500], // #f97316
			R: TailwindColors.red[600], // #dc2626
			B: TailwindColors.blue[600], // #2563eb
			G: TailwindColors.green[600] // #16a34a
		};
		const cornerFaces = cube.getCornerFaces();
		const edgeFaces = cube.getEdgeFaces();
		squares.set([]);
		for (const face of getKeys(centerPositionMap)) {
			const position = centerPositionMap[face];
			const color = colorMap[face];
			addSquare(position, color);
		}
		for (const letter of getKeys(cornerFaces)) {
			const position = cornerLetterPositionMap[letter];
			const color = colorMap[cornerFaces[letter]];
			addSquare(position, color);
		}
		for (const letter of getKeys(edgeFaces)) {
			const position = edgeLetterPositionMap[letter];
			const color = colorMap[edgeFaces[letter]];
			addSquare(position, color);
		}
	}

	export function generateScramble(newScramble = true) {
		const $settings = get(settings);

		// create a new scramble
		if (newScramble) {
			// Use the current timestamp as a seed for random scramble
			$settings.rubiks.scramble = getScramble(new Date().toISOString());
		}

		// Create a new cube and apply the scramble to it
		const c = new Cube();
		$settings.rubiks.scramble.split(' ').forEach((m) => c.applyMove(m as Move));

		// Draw the 2d scramble map given the scrambled 3d cube representation
		drawScramble(c);

		settings.set($settings);
	}
</script>

<script lang="ts">
	import { onMount } from 'svelte';

	import { Icon } from '$lib/components/Icon';
	import { dictionary } from '$lib/stores/languageDictionary';
	import { settings } from '$lib/stores/settings';
	import { open } from '$lib/util/modal';

	onMount(() => generateScramble($settings.rubiks.scramble === ''));
</script>

<p class="h3 font-light whitespace-nowrap overflow-x-auto">
	{$settings.rubiks.scramble}
</p>

<svg
	viewBox="-2.5 -2.5 620 465"
	class="my-4 lg:my-6 mx-auto md:max-h-[40vh]"
	xmlns="http://www.w3.org/2000/svg">
	<g class="stroke-base-100 dark:stroke-base-900" stroke-width="2.5">
		{#each $squares as { x, y, color }}
			<rect {x} {y} fill={color} width={50} height={50} class="transition-colors" />
		{/each}
	</g>
</svg>

<div class="flex flex-wrap gap-4 lg:gap-6">
	<button
		class="outline-btn btn-move-right mx-auto w-full sm:w-auto"
		on:click={() => generateScramble()}>
		<Icon name="chevron_right" />
		{$dictionary.rubiksSettings['Generate scramble']}
	</button>

	<button
		class="outline-btn btn-move-up mx-auto w-full sm:w-auto"
		on:click={() => open('rubiks-scramble-guide')}>
		<Icon name="info" />
		{$dictionary.rubiksSettings['Scramble Guide']}
	</button>

	<button
		class="outline-btn btn-move-up mx-auto w-full sm:w-auto"
		on:click={() => open('rubiks-scramble-walkthrough', { scramble: $settings.rubiks.scramble })}>
		<Icon name="info" />
		{$dictionary.rubiksSettings['Scramble Walkthrough']}
	</button>

	<button
		class="outline-btn btn-move-up mx-auto w-full sm:w-auto"
		on:click={() => open('rubiks-interactable-cube')}>
		<Icon name="cube" />
		{$dictionary.rubiksSettings['Interactable Cube']}
	</button>
</div>

And here is the original file that got the styles deleted:

File
<script lang="ts">
	/**
	 * Modal for displaying a single rubiks cube session
	 */

	import Icon from '$lib/components/Icon/Icon.svelte';
	import ThemedVectorArt from '$lib/components/ThemedVectorArt.svelte';
	import { dictionary } from '$lib/stores/languageDictionary';
	import { settings } from '$lib/stores/settings';
	import downloadCSV from '$lib/util/downloadCSV';
	import getFormat from '$lib/util/getFormat';
	import { close, open } from '$lib/util/modal';
	import { getAvg, getHi, getLo } from '$lib/util/rubiksStats';
	import { getDateTime } from '$lib/util/timeText';
	import uid from '$lib/util/uid';

	// get date session started
	const getFormattedDate = (date: Date) => {
		return getDateTime(date, getFormat('clock', 'date'));
	};

	export let data: { sessionID: string };

	$: theSession = $settings.rubiks.sessions[data.sessionID];

	function deleteSession() {
		delete $settings.rubiks.sessions[data.sessionID];
		// Handle if deleted session is the current one
		if ($settings.rubiks.currentSessionID === data.sessionID) {
			if (Object.keys($settings.rubiks.sessions).length === 0) {
				// If no other sessions exist, create one
				const rubiksSessionID = uid();
				$settings.rubiks.sessions = {
					[rubiksSessionID]: {
						name: '',
						timestamp: Date.now(),
						times: []
					}
				};
				$settings.rubiks.currentSessionID = rubiksSessionID;
			} else {
				// Else set the current session to the first one
				$settings.rubiks.currentSessionID = Object.keys($settings.rubiks.sessions)[0];
			}
		}
		close(); // Close the modal
	}

	function downloadSessionCSV() {
		const session = $settings.rubiks.sessions[data.sessionID];
		const csvData = [
			[
				$dictionary.rubiksSettings['Session Name'],
				$dictionary.rubiksSettings['Session Timestamp'],
				$dictionary.rubiksSettings['Session ID']
			],
			[session.name, session.timestamp, data.sessionID],
			[],
			[
				$dictionary.stopwatchSettings['Time'],
				$dictionary.rubiksSettings['Penalty'],
				$dictionary.rubiksSettings['Scramble'],
				$dictionary.rubiksSettings['Timestamp']
			]
		];
		session.times.map((time) => {
			csvData.push([time.time + time.penalty, time.penalty, time.scramble, time.timestamp]);
		});

		downloadCSV(csvData, `${session.name || 'Untitled'}.csv`);
	}

	function deleteTime(timestamp: number) {
		theSession.times = theSession.times.filter((t) => t.timestamp !== timestamp);

		// Update the time list behind the modal
		$settings.rubiks.sessions = $settings.rubiks.sessions;
	}
</script>

<!--
	Wrap modal in `if` check so deleting session doesn't crash.
	Modal will close after session is deleted anyway and we can't open a session that doesn't exist,
	so this `if` check resolves all problems.
-->
{#if theSession}
	<div class="pad">
		<div class="flex flex-wrap flex-col md:flex-row justify-between items-center gap-4 lg:gap-10">
			<div class="text-center">
				<label for="session-name" class="label">
					{$dictionary.pomodoroSettings['Name:']}
				</label>
				<input id="session-name" type="text" maxlength="40" bind:value={theSession.name} />
			</div>

			<p class="p">{getFormattedDate(new Date(theSession.timestamp))}</p>

			<button
				class="my-4 outline-btn inline-flex"
				class:accent-btn={data.sessionID === $settings.rubiks.currentSessionID}
				on:click={() => ($settings.rubiks.currentSessionID = data.sessionID)}>
				<Icon name="play" />
				<p>
					{$dictionary.rubiksSettings[
						data.sessionID === $settings.rubiks.currentSessionID
							? 'Current session'
							: 'Make current'
					]}
				</p>
			</button>

			{#if theSession.times.length > 0}
				<p class="h4">
					{$dictionary.rubiksSettings['Lo:']}
					<span class="font-bold">
						{getLo(theSession.times) / 1000}{$dictionary.rubiksSettings['seconds_short']}
					</span>
					<span class="mx-1">|</span>
					{$dictionary.rubiksSettings['Avg:']}
					<span class="font-bold">
						{getAvg(theSession.times) / 1000}{$dictionary.rubiksSettings['seconds_short']}
					</span>
					<span class="mx-1">|</span>
					{$dictionary.rubiksSettings['Hi:']}
					<span class="font-bold">
						{getHi(theSession.times) / 1000}{$dictionary.rubiksSettings['seconds_short']}
					</span>
				</p>

				<button class="outline-btn btn-move-down" on:click={downloadSessionCSV}>
					<Icon name="download" />
					{$dictionary.labels['Download']}
				</button>
			{/if}

			<button class="outline-btn btn-rotate-clock-sm" on:click={deleteSession}>
				<Icon name="trash" />
				{$dictionary.labels['Delete']}
			</button>
		</div>

		<hr class="my-10 max-w-full" />

		{#if theSession.times.length > 0}
			<div class="w-full overflow-x-auto">
				<table class="p pb-4 whitespace-nowrap max-w-5xl mx-auto">
					<tr>
						<th>{$dictionary.stopwatchSettings['Time']}</th>
						<th>{$dictionary.rubiksSettings['Penalty']}</th>
						<th>{$dictionary.rubiksSettings['Scramble']}</th>
						<th />
					</tr>
					{#each theSession.times as time}
						<tr>
							<td>
								<span>
									<!-- fix for shift if time becomes over 10s due to penalty -->
									<span class="opacity-0">
										{time.time + time.penalty < 10_000 ? '0' : ''}
									</span>
									{(time.time + time.penalty) / 1000}{$dictionary.rubiksSettings['seconds_short']}
								</span>
							</td>
							<td class="flex items-center gap-4">
								<button
									class="btn block"
									disabled={time.penalty === 0}
									on:click={() => (time.penalty -= 2000)}>
									{$dictionary.rubiksSettings['-2s']}
								</button>
								<span class="block text-center">
									<!-- fix for shift if over 10s penalty -->
									<span class="opacity-0">
										{time.penalty < 10_000 ? '0' : ''}
									</span>
									{time.penalty / 1000}s
								</span>
								<button
									class="btn block"
									disabled={time.penalty === 100_000}
									on:click={() => (time.penalty += 2000)}>
									{$dictionary.rubiksSettings['+2s']}
								</button>
							</td>
							<td>
								<button
									class="a p-sm"
									on:click={() => open('rubiks-scramble-walkthrough', { scramble: time.scramble })}>
									{time.scramble}
								</button>
							</td>
							<td>
								<button
									class="icon-btn"
									on:click={() => deleteTime(time.timestamp)}
									title={$dictionary.labels['Delete']}>
									<Icon name="trash" />
								</button>
							</td>
						</tr>
					{/each}
				</table>
			</div>
		{:else}
			<div class="select-none w-80 xl:w-96 mx-auto">
				<ThemedVectorArt name="rubiks" />
			</div>
			<h3 class="h3 sm:mt-6 text-center">{$dictionary.rubiksSettings['No times yet']}</h3>
		{/if}
	</div>
{/if}

<style lang="postcss">
	table td {
		@apply border-l border-base-300 first:border-0 dark:border-base-600;
	}
	.accent-btn {
		@apply border-0 bg-accent-700 text-white hover:bg-accent-700;
	}
</style>

As for the tbody, I understand it's technically more correct, but it doesn't feel like it should break svmigrate. Is there something that changed with hydration between svelte 4 and svelte 5 such that no tbody was valid and now isn't valid for hydration?

Here are the deps before upgrade:

"devDependencies": {
		"@sveltejs/adapter-netlify": "^4.3.3",
		"@sveltejs/adapter-static": "^3.0.1",
		"@sveltejs/kit": "^2.0.0",
		"@sveltejs/vite-plugin-svelte": "^3.0.0",
		"@tauri-apps/api": "^1.6.0",
		"@tauri-apps/cli": "^1.5.13",
		"@types/gtag.js": "^0.0.12",
		"@types/suncalc": "^1.9.2",
		"@typescript-eslint/eslint-plugin": "^6.16.0",
		"@typescript-eslint/parser": "^6.16.0",
		"@vitest/coverage-v8": "^1.1.0",
		"autoprefixer": "^10.4.16",
		"eslint": "^8.56.0",
		"eslint-config-prettier": "^9.1.0",
		"eslint-plugin-import": "^2.29.1",
		"eslint-plugin-prettier": "^5.1.2",
		"eslint-plugin-svelte": "^2.35.1",
		"lighthouse": "^11.4.0",
		"postcss": "^8.4.32",
		"postcss-load-config": "^5.0.2",
		"prettier": "^3.1.1",
		"prettier-plugin-organize-imports": "^3.2.4",
		"prettier-plugin-svelte": "^3.1.2",
		"prettier-plugin-tailwindcss": "^0.5.10",
		"puppeteer": "^21.3.8",
		"pwa-asset-generator": "^6.3.1",
		"svelte": "^4.2.8",
		"svelte-check": "^3.6.2",
		"svelte-preprocess": "^5.1.3",
		"typescript": "^5.3.3",
		"vite": "^5.0.13",
		"vitest": "^1.1.0"
	},
	"dependencies": {
		"dayjs": "^1.11.10",
		"lz-string": "^1.5.0",
		"motion": "^10.16.4",
		"screenfull": "^6.0.2",
		"suncalc": "^1.9.0",
		"tailwindcss": "^3.4.0"
	},

And after:

"devDependencies": {
		"@sveltejs/adapter-netlify": "^4.3.3",
		"@sveltejs/adapter-static": "^3.0.1",
		"@sveltejs/kit": "^2.5.27",
		"@sveltejs/vite-plugin-svelte": "^4.0.0",
		"@tauri-apps/api": "^1.6.0",
		"@tauri-apps/cli": "^1.5.13",
		"@types/gtag.js": "^0.0.12",
		"@types/suncalc": "^1.9.2",
		"@typescript-eslint/eslint-plugin": "^6.16.0",
		"@typescript-eslint/parser": "^6.16.0",
		"@vitest/coverage-v8": "^1.1.0",
		"autoprefixer": "^10.4.16",
		"eslint": "^8.56.0",
		"eslint-config-prettier": "^9.1.0",
		"eslint-plugin-import": "^2.29.1",
		"eslint-plugin-prettier": "^5.1.2",
		"eslint-plugin-svelte": "^2.45.1",
		"lighthouse": "^11.4.0",
		"postcss": "^8.4.32",
		"postcss-load-config": "^5.0.2",
		"prettier": "^3.1.1",
		"prettier-plugin-organize-imports": "^3.2.4",
		"prettier-plugin-svelte": "^3.2.6",
		"prettier-plugin-tailwindcss": "^0.5.10",
		"puppeteer": "^21.3.8",
		"pwa-asset-generator": "^6.3.1",
		"svelte": "^5.0.0",
		"svelte-check": "^4.0.0",
		"svelte-preprocess": "^6.0.0",
		"typescript": "^5.5.0",
		"vite": "^5.4.4",
		"vitest": "^1.1.0"
	},
	"dependencies": {
		"dayjs": "^1.11.10",
		"lz-string": "^1.5.0",
		"motion": "^10.16.4",
		"screenfull": "^6.0.2",
		"suncalc": "^1.9.0",
		"tailwindcss": "^3.4.0"
	},

The only thing I did was run npx sv migrate svelte-5. I've been spending hours since then trying to fix issues, but that's all that I did with regards to this current issue.

Thanks again for the help!

@justingolden21
Copy link
Author

Another file where the migration script ended up copying and pasting my comment over and over again:

Original File
<script lang="ts">
	import type { ChangeEventHandler } from 'svelte/elements';
	import { systemFontFamilies } from '$lib/data/consts';

	type LabelMapper = (value: string | number, idx: number) => string;

	/*
    id: id of select and for label
    selectLabel: label text
    value: current value of select (can be bound)
    disabled: disabled attr of select
    values: list of values to loop through, option values
    labels: array of labels for each option, indexed by values
    onchange: function that runs on:change
    labelMapper: custom function that maps a value to a label string
    dynamicFont: true if each option should have a font family according to its value (for font family pickers)
    */
	export let id: string | undefined = undefined,
		selectLabel: string = '',
		showLabel: boolean = true,
		value: string | number | null,
		disabled = false,
		values: Readonly<(string | number)[]> = [],
		labels: Record<string | number, string> | undefined = undefined,
		onchange: ChangeEventHandler<HTMLSelectElement> | undefined = undefined,
		labelMapper: LabelMapper = (value) => (labels ? labels[value] : String(value)),
		dynamicFont = false;

	// HACK: svelte has no way of knowing other than this that the labelMapper depends on labels
	// Update the values when labels changes, so the dropdown has the correct values
	$: labels, (values = values);
</script>

{#if selectLabel && showLabel}
	<label for={id} class="label">{selectLabel}</label>
{/if}

<!--
	The change listener is basically `on:change={onchange}`
	except we copy the event so the currentTarget doesn't change.
	Otherwise, the component wouldn't work.
	We use `currentTarget` instead of `target` to make TypeScript happy.
 -->
<select
	{id}
	{disabled}
	aria-label={selectLabel}
	bind:value
	on:change={(e) => onchange?.({ ...e, currentTarget: e.currentTarget })}>
	{#each values as val, idx}
		<option
			value={val}
			style={dynamicFont && val === ''
				? `font-family:${systemFontFamilies}`
				: `font-family:${dynamicFont ? val : ''}`}>{labelMapper(val, idx)}</option>
	{/each}
</select>
New File
<script lang="ts">
	import { run } from 'svelte/legacy';

	import type { ChangeEventHandler } from 'svelte/elements';
	import { systemFontFamilies } from '$lib/data/consts';

	type LabelMapper = (value: string | number, idx: number) => string;

	
	interface Props {
		/*
    id: id of select and for label
    selectLabel: label text
    value: current value of select (can be bound)
    disabled: disabled attr of select
    values: list of values to loop through, option values
    labels: array of labels for each option, indexed by values
    onchange: function that runs on:change
    labelMapper: custom function that maps a value to a label string
    dynamicFont: true if each option should have a font family according to its value (for font family pickers)
    */
		id?: string | undefined;
		/*
    id: id of select and for label
    selectLabel: label text
    value: current value of select (can be bound)
    disabled: disabled attr of select
    values: list of values to loop through, option values
    labels: array of labels for each option, indexed by values
    onchange: function that runs on:change
    labelMapper: custom function that maps a value to a label string
    dynamicFont: true if each option should have a font family according to its value (for font family pickers)
    */
		selectLabel?: string;
		/*
    id: id of select and for label
    selectLabel: label text
    value: current value of select (can be bound)
    disabled: disabled attr of select
    values: list of values to loop through, option values
    labels: array of labels for each option, indexed by values
    onchange: function that runs on:change
    labelMapper: custom function that maps a value to a label string
    dynamicFont: true if each option should have a font family according to its value (for font family pickers)
    */
		showLabel?: boolean;
		/*
    id: id of select and for label
    selectLabel: label text
    value: current value of select (can be bound)
    disabled: disabled attr of select
    values: list of values to loop through, option values
    labels: array of labels for each option, indexed by values
    onchange: function that runs on:change
    labelMapper: custom function that maps a value to a label string
    dynamicFont: true if each option should have a font family according to its value (for font family pickers)
    */
		value: string | number | null;
		/*
    id: id of select and for label
    selectLabel: label text
    value: current value of select (can be bound)
    disabled: disabled attr of select
    values: list of values to loop through, option values
    labels: array of labels for each option, indexed by values
    onchange: function that runs on:change
    labelMapper: custom function that maps a value to a label string
    dynamicFont: true if each option should have a font family according to its value (for font family pickers)
    */
		disabled?: boolean;
		/*
    id: id of select and for label
    selectLabel: label text
    value: current value of select (can be bound)
    disabled: disabled attr of select
    values: list of values to loop through, option values
    labels: array of labels for each option, indexed by values
    onchange: function that runs on:change
    labelMapper: custom function that maps a value to a label string
    dynamicFont: true if each option should have a font family according to its value (for font family pickers)
    */
		values?: Readonly<(string | number)[]>;
		/*
    id: id of select and for label
    selectLabel: label text
    value: current value of select (can be bound)
    disabled: disabled attr of select
    values: list of values to loop through, option values
    labels: array of labels for each option, indexed by values
    onchange: function that runs on:change
    labelMapper: custom function that maps a value to a label string
    dynamicFont: true if each option should have a font family according to its value (for font family pickers)
    */
		labels?: Record<string | number, string> | undefined;
		/*
    id: id of select and for label
    selectLabel: label text
    value: current value of select (can be bound)
    disabled: disabled attr of select
    values: list of values to loop through, option values
    labels: array of labels for each option, indexed by values
    onchange: function that runs on:change
    labelMapper: custom function that maps a value to a label string
    dynamicFont: true if each option should have a font family according to its value (for font family pickers)
    */
		onchange?: ChangeEventHandler<HTMLSelectElement> | undefined;
		/*
    id: id of select and for label
    selectLabel: label text
    value: current value of select (can be bound)
    disabled: disabled attr of select
    values: list of values to loop through, option values
    labels: array of labels for each option, indexed by values
    onchange: function that runs on:change
    labelMapper: custom function that maps a value to a label string
    dynamicFont: true if each option should have a font family according to its value (for font family pickers)
    */
		labelMapper?: LabelMapper;
		/*
    id: id of select and for label
    selectLabel: label text
    value: current value of select (can be bound)
    disabled: disabled attr of select
    values: list of values to loop through, option values
    labels: array of labels for each option, indexed by values
    onchange: function that runs on:change
    labelMapper: custom function that maps a value to a label string
    dynamicFont: true if each option should have a font family according to its value (for font family pickers)
    */
		dynamicFont?: boolean;
	}

	let {
		id = undefined,
		selectLabel = '',
		showLabel = true,
		value = $bindable(),
		disabled = false,
		values = $bindable([]),
		labels = undefined,
		onchange = undefined,
		labelMapper = (value) => (labels ? labels[value] : String(value)),
		dynamicFont = false
	}: Props = $props();

	// HACK: svelte has no way of knowing other than this that the labelMapper depends on labels
	// Update the values when labels changes, so the dropdown has the correct values
	run(() => {
		labels, (values = values);
	});
</script>

{#if selectLabel && showLabel}
	<label for={id} class="label">{selectLabel}</label>
{/if}

<!--
	The change listener is basically `on:change={onchange}`
	except we copy the event so the currentTarget doesn't change.
	Otherwise, the component wouldn't work.
	We use `currentTarget` instead of `target` to make TypeScript happy.
 -->
<select
	{id}
	{disabled}
	aria-label={selectLabel}
	bind:value
	onchange={(e) => onchange?.({ ...e, currentTarget: e.currentTarget })}>
	{#each values as val, idx}
		<option
			value={val}
			style={dynamicFont && val === ''
				? `font-family:${systemFontFamilies}`
				: `font-family:${dynamicFont ? val : ''}`}>{labelMapper(val, idx)}</option>
	{/each}
</select>

@justingolden21
Copy link
Author

A question while I'm bothering you about sv migrate:

Is there a reason this:

let importText = '';
$: exportText = encodeBirthdays($settings.birthdays.birthdays);

Was replaced with this:

let importText = $state('');
let exportText;
run(() => {
	exportText = encodeBirthdays($settings.birthdays.birthdays);
});

And not just $derived()?

Is it because the migration script has no way of knowing if encodeBirthdays has any side effects or something?

@paoloricciuti
Copy link
Member

For the first issue i confirm that while it can't migrate it doesn't substitute the styles (you should probably update svelte and try again).

For the The $ prefix is reserved, and cannot be used for variables and imports error it's actually because of generateScramble

export function generateScramble(newScramble = true) {
    const $settings = get(settings);

    // create a new scramble
    if (newScramble) {
        // Use the current timestamp as a seed for random scramble
        $settings.rubiks.scramble = getScramble(new Date().toISOString());
    }

    // Create a new cube and apply the scramble to it
    const c = new Cube();
    $settings.rubiks.scramble.split(' ').forEach((m) => c.applyMove(m as Move));

    // Draw the 2d scramble map given the scrambled 3d cube representation
    drawScramble(c);

    settings.set($settings);
}

it's creating a variable starting with $ and it's now a reserved prefix (in case we want to add other runes in the future we don't want user code to possibly conflict). Changing that will solve the issue.

For the comment issue we can't do much about it...we use the comment above the prop declaration to keep the comments attached to the new prop declaration and we can't possibly know that what you meant with that comment is to provide a comment for different lines of the declaration.

Finally for the run issue there are multiple reasons why we switch to run instead of derived, it could be because you reassign it or (i can't recall) also because we can't determine if it's side effect free.

I'll close this issue since everything is resolved but thanks for reporting!

@paoloricciuti paoloricciuti closed this as not planned Won't fix, can't repro, duplicate, stale Dec 16, 2024
@justingolden21
Copy link
Author

Thanks again for the reply.

I tried multiple times and it replaces the styles.

I understand why the $ is reserved and why that fails. That makes sense to me.

For the comment issue, I agree you can't know where it goes, but surely you can agree that copying and pasting the same comment over and over is a bug and not intentional, right?

Thanks for explaining run. It seems like it's to better handle backwards compatibility, so $derived will be the answer the majority of the time, but to ensure nothing breaks, it uses run and then the dev manually changes that to derived.


I wouldn't say the replacing styles issue is resolved, but I suppose that's all I'll get. Thanks again for all the help and fast replies.

@paoloricciuti
Copy link
Member

I tried multiple times and it replaces the styles.

Did you update svelte to latest before doing it? I literally copy pasted your component in a fresh project and migrated it and it did not delete the styles. If you can provide your code I can take a look.

For the comment issue, I agree you can't know where it goes, but surely you can agree that copying and pasting the same comment over and over is a bug and not intentional, right?

Obviously in an ideal word we wouldn't do that but how can we know if that is meant to be a comment that goes over every prop or one that should just go on top of everything reliably?

@justingolden21
Copy link
Author

I suppose you have no way of knowing where the comment goes. I just think you put it in the first location it could go rather than copying and pasting. But it's not a big deal.

Also wdym update svelte before doing it? Isn't the whole point of sv migrate to update svelte for me, from 4 to 5? Do I update to the latest version of svelte 4 before running sv migrate? Is that mentioned in the docs? Or do I update to svelte 5 and then run sv migrate after that?

Thanks again for the help. Appreciate all the work on svelte.

@justingolden21
Copy link
Author

Do you have any advice for svelte 4 to 5 migration by the way? It's been a few weeks and I'm still nowhere close and running into a bunch of bugs. The diff is also thousands of lines across 120+ files, and 25% increased line count from the diff. I keep finding new bugs and pages that are broken. It's starting to really not feel worth upgrading. I'm not doing anything super complex, it's mostly basic CRUD/MVC stuff, not much worse than a todo app. I just have a lot of components and pages. Thanks in advance.

@paoloricciuti
Copy link
Member

Also wdym update svelte before doing it? Isn't the whole point of sv migrate to update svelte for me, from 4 to 5?

sv migrate does two things: it upgrade the packages and then migrate every component using the migration script. I have the feeling you started the migration when svelte was at 5.x with a decently low x. If you installed when svelte was 5.0.5 for example and you have a lock file you might be using a decently old version of svelte 5 that doesn't have the fix we did to the migration script. So before running the migration again try to do a pnpm I svelte@latest. That should hopefully fix most of the bugs.

This is the number one suggestion: if you then run into problems what I would do is just revert the changes of the problematic components leaving them in legacy mode and then migrate them by hand incrementally

@justingolden21
Copy link
Author

I began migration from my svelte 4 project to svelte 5 by running sv migrate around 2 weeks ago

@justingolden21
Copy link
Author

Also relating to comments getting duplicated, in some cases, the comments were deleted entirely:

image

Not sure if worth looking into but just a friendly heads up.

@paoloricciuti
Copy link
Member

I began migration from my svelte 4 project to svelte 5 by running sv migrate around 2 weeks ago

Regardless...did you tried to delete the lock, install again and run the migration?

@justingolden21
Copy link
Author

Tried again, deleting pnpm-lock.yaml, then running pnpm install then npx sv migrate svelte-5 as suggested in migration guide.

Still had the issue of copy and paste duplicate comments:

image

And still resulted in deleting all my styles when there is "invalid" html

image

Again, using the term "invalid" quite loosely here, since tables have rendered fine for many years on 99.99% of browsers without tbody, browsers have no problems, screen readers have no problems, doesn't cause bugs, hurt a11y, etc. And I don't mind fixing it, but it doesn't feel like something that should break sv migrate imo.

Thanks again for the help.

It's been a few weeks but I'm almost done migrating my project. Not the fault of migrate, but it's been a lot of work, many hours a day for many days. Lots of infinite loops that didn't used to occur, deleted comments, moved code, deleted css, fixing for legacy APIs, etc. The diff is way way longer, many hundreds of lines, but there are parts of it that are cleaner. I do really like runes. I've certainly got my issues with svelte 5 as well, but that's another topic.

@paoloricciuti
Copy link
Member

Then I need you to provide an actual reproduction for this, as I've said I was not able to reproduce with your code in my project so there's definitely something weird with the rest of the setup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting submitter needs a reproduction, or clarification
Projects
None yet
Development

No branches or pull requests

2 participants