Skip to content

Commit

Permalink
feat: v2 migration helpers (#11199)
Browse files Browse the repository at this point in the history
* move reusable stuff into utils

* start of kit v2 migration - remove throw from redirect/error

* docs (WIP)

* migrate tsconfig

* Update documentation/docs/60-appendix/11-v2-migration-guide.md

* add goto to migration guide

* prettier

* mentiond `dangerZone.trackServerFetches`

* update migration docs

* shuffle files around

* oops

* tweak docs

* tweak some wording

* fix semver comparison

* more docs

* tsconfig and package.json changes

* add migration note on cookies

* svelte config migration

* failed attempt to inject cookie comment

* cookie note

* adjust note

* lint
  • Loading branch information
dummdidumm authored Dec 11, 2023
1 parent a93a39f commit 8a80190
Show file tree
Hide file tree
Showing 12 changed files with 778 additions and 117 deletions.
File renamed without changes.
99 changes: 99 additions & 0 deletions documentation/docs/60-appendix/30-migrating-to-sveltekit-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
title: Migrating to SvelteKit v2
---

Upgrading from SvelteKit version 1 to version 2 should be mostly seamless. There are a few breaking changes to note, which are listed here. You can use `npx svelte-migrate sveltekit-2` to migrate some of these changes automatically.

We highly recommend upgrading to the most recent 1.x version before upgrading to 2.0, so that you can take advantage of targeted deprecation warnings. We also recommend [updating to Svelte 4](https://svelte.dev/docs/v4-migration-guide) first: Later versions of SvelteKit 1.x support it, and SvelteKit 2.0 requires it.

## `redirect` and `error` are no longer thrown by you

Previously, you had to `throw` the values returned from `error(...)` and `redirect(...)` yourself. In SvelteKit 2 this is no longer the case — calling the functions is sufficient.

```diff
import { error } from '@sveltejs/kit'

...
- throw error(500, 'something went wrong');
+ error(500, 'something went wrong');
```

`svelte-migrate` will do these changes automatically for you.

If the error or redirect is thrown inside a `try {...}` block (hint: don't do this!), you can distinguish them from unexpected errors using [`isHttpError`](/docs/modules#sveltejs-kit-ishttperror) and [`isRedirect`](/docs/modules#sveltejs-kit-isredirect) imported from `@sveltejs/kit`.

## path is required when setting cookies

When receiving a `Set-Cookie` header that doesn't specify a `path`, browsers will [set the cookie path](https://www.rfc-editor.org/rfc/rfc6265#section-5.1.4) to the parent of the resource in question. This behaviour isn't particularly helpful or intuitive, and frequently results in bugs because the developer expected the cookie to apply to the domain as a whole.

As of SvelteKit 2.0, you need to set a `path` when calling `cookies.set(...)`, `cookies.delete(...)` or `cookies.serialize(...)` so that there's no ambiguity. Most of the time, you probably want to use `path: '/'`, but you can set it to whatever you like, including relative paths — `''` means 'the current path', `'.'` means 'the current directory'.

```diff
export function load({ cookies }) {
- cookies.set(name, value);
+ cookies.set(name, value, { path: '/' });
return { response }
}
```

## Top-level promises are no longer awaited

In SvelteKit version 1, if the top-level properties of the object returned from a `load` function were promises, they were automatically awaited. With the introduction of [streaming](https://svelte.dev/blog/streaming-snapshots-sveltekit) this behavior became a bit awkward as it forces you to nest your streamed data one level deep.

As of version 2, SvelteKit no longer differentiates between top-level and non-top-level promises. To get back the blocking behavior, use `await` (with `Promise.all` to prevent waterfalls, where appropriate):

```diff
// If you have a single promise
export function load({ fetch }) {
- const response = fetch(...).then(r => r.json());
+ const response = await fetch(...).then(r => r.json());
return { response }
}
```

```diff
// If you have multiple promises
export function load({ fetch }) {
- const a = fetch(...).then(r => r.json());
- const b = fetch(...).then(r => r.json());
+ const [a, b] = Promise.all([
+ fetch(...).then(r => r.json()),
+ fetch(...).then(r => r.json()),
+ ]);
return { a, b };
}
```

## `path` is now a required option for cookies

`cookies.set`, `cookies.delete` and `cookies.serialize` all have an options argument through which certain cookie serialization options are configurable. One of the is the `path` setting, which tells browser under which URLs a cookie is applicable. In SvelteKit 1.x, the `path` is optional and defaults to what the browser does, which is removing everything up to and including the last slash in the pathname of the URL. This means that if you're on `/foo/bar`, then the `path` is `/foo`, but if you're on `/foo/bar/`, the `path` is `/foo/bar`. This behavior is somewhat confusing, and most of the time you probably want to have cookies available more broadly (many people set `path` to `/` for that reason) instead of scratching their heads why a cookie they have set doesn't apply elsewhere. For this reason, `path` is a required option in SvelteKit 2.

```diff
// file: foo/bar/+page.svelte
export function load ({ cookies }) {
- cookies.set('key', 'value');
+ cookies.set('key', 'value', { path: '/foo' });
}
```

`svelte-migrate` will add comments highlighting the locations that need to be adjusted.

## goto(...) no longer accepts external URLs

To navigate to an external URL, use `window.location = url`.

## paths are now relative by default

In SvelteKit 1, `%sveltekit.assets%` in your `app.html` was replaced with a relative path by default (i.e. `.` or `..` or `../..` etc, depending on the path being rendered) during server-side rendering unless the [`paths.relative`](/docs/configuration#paths) config option was explicitly set to `false`. The same was true for `base` and `assets` imported from `$app/paths`, but only if the `paths.relative` option was explicitly set to `true`.

This inconsistency is fixed in version 2. Paths are either always relative or always absolute, depending on the value of [`paths.relative`](/docs/configuration#paths). It defaults to `true` as this results in more portable apps: if the `base` is something other than the app expected (as is the case when viewed on the [Internet Archive](https://archive.org/), for example) or unknown at build time (as is the case when deploying to [IPFS](https://ipfs.tech/) and so on), fewer things are likely to break.

## Server fetches are not trackable anymore

Previously it was possible to track URLs from `fetch`es on the server in order to rerun load functions. This poses a possible security risk (private URLs leaking), and as such it was behind the `dangerZone.trackServerFetches` setting, which is now removed.

## Updated dependency requirements

SvelteKit requires Node `18.13` or higher, Vite `^5.0`, vite-plugin-svelte `^3.0`, TypeScript `^5.0` and Svelte version 4 or higher. `svelte-migrate` will do the `package.json` bumps for you.

As part of the TypeScript upgrade, the generated `tsconfig.json` (the one your `tsconfig.json` extends from) now uses `"moduleResolution": "bundler"` (which is recommended by the TypeScript team, as it properly resolves types from packages with an `exports` map in package.json) and `verbatimModuleSyntax` (which replaces the existing `importsNotUsedAsValues ` and `preserveValueImports` flags — if you have those in your `tsconfig.json`, remove them. `svelte-migrate` will do this for you).
10 changes: 6 additions & 4 deletions packages/migrate/migrations/svelte-4/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import colors from 'kleur';
import fs from 'node:fs';
import prompts from 'prompts';
import glob from 'tiny-glob/sync.js';
import { bail, check_git } from '../../utils.js';
import { update_js_file, update_pkg_json, update_svelte_file } from './migrate.js';
import { bail, check_git, update_js_file, update_svelte_file } from '../../utils.js';
import { transform_code, transform_svelte_code, update_pkg_json } from './migrate.js';

export async function migrate() {
if (!fs.existsSync('package.json')) {
Expand Down Expand Up @@ -78,9 +78,11 @@ export async function migrate() {
for (const file of files) {
if (extensions.some((ext) => file.endsWith(ext))) {
if (svelte_extensions.some((ext) => file.endsWith(ext))) {
update_svelte_file(file, migrate_transition.value);
update_svelte_file(file, transform_code, (code) =>
transform_svelte_code(code, migrate_transition.value)
);
} else {
update_js_file(file);
update_js_file(file, transform_code);
}
}
}
Expand Down
139 changes: 26 additions & 113 deletions packages/migrate/migrations/svelte-4/migrate.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from 'node:fs';
import { Project, ts, Node } from 'ts-morph';
import semver from 'semver';
import { log_migration, log_on_ts_modification, update_pkg } from '../../utils.js';

export function update_pkg_json() {
fs.writeFileSync(
Expand All @@ -13,93 +13,31 @@ export function update_pkg_json() {
* @param {string} content
*/
export function update_pkg_json_content(content) {
const indent = content.split('\n')[1].match(/^\s+/)?.[0] || ' ';
const pkg = JSON.parse(content);

/**
* @param {string} name
* @param {string} version
* @param {string} [additional]
*/
function update_pkg(name, version, additional = '') {
if (pkg.dependencies?.[name]) {
const existing_range = pkg.dependencies[name];

if (semver.validRange(existing_range) && !semver.subset(existing_range, version)) {
log_migration(`Updated ${name} to ${version} ${additional}`);
pkg.dependencies[name] = version;
}
}

if (pkg.devDependencies?.[name]) {
const existing_range = pkg.devDependencies[name];

if (semver.validRange(existing_range) && !semver.subset(existing_range, version)) {
log_migration(`Updated ${name} to ${version} ${additional}`);
pkg.devDependencies[name] = version;
}
}
}

update_pkg('svelte', '^4.0.0');
update_pkg('svelte-check', '^3.4.3');
update_pkg('svelte-preprocess', '^5.0.3');
update_pkg('@sveltejs/kit', '^1.20.4');
update_pkg('@sveltejs/vite-plugin-svelte', '^2.4.1');
update_pkg(
'svelte-loader',
'^3.1.8',
' (if you are still on webpack 4, you need to update to webpack 5)'
);
update_pkg('rollup-plugin-svelte', '^7.1.5');
update_pkg('prettier-plugin-svelte', '^2.10.1');
update_pkg('eslint-plugin-svelte', '^2.30.0');
update_pkg(
'eslint-plugin-svelte3',
'^4.0.0',
' (this package is deprecated, use eslint-plugin-svelte instead. More info: https://svelte.dev/docs/v4-migration-guide#new-eslint-package)'
);
update_pkg(
'typescript',
'^5.0.0',
' (this might introduce new type errors due to breaking changes within TypeScript)'
);

return JSON.stringify(pkg, null, indent);
}

/**
* @param {string} file_path
* @param {boolean} migrate_transition
*/
export function update_svelte_file(file_path, migrate_transition) {
try {
const content = fs.readFileSync(file_path, 'utf-8');
const updated = content.replace(
/<script([^]*?)>([^]+?)<\/script>(\n*)/g,
(_match, attrs, contents, whitespace) => {
return `<script${attrs}>${transform_code(
contents,
(attrs.includes('lang=') || attrs.includes('type=')) &&
(attrs.includes('ts') || attrs.includes('typescript'))
)}</script>${whitespace}`;
}
);
fs.writeFileSync(file_path, transform_svelte_code(updated, migrate_transition), 'utf-8');
} catch (e) {
console.error(`Error updating ${file_path}:`, e);
}
}

/** @param {string} file_path */
export function update_js_file(file_path) {
try {
const content = fs.readFileSync(file_path, 'utf-8');
const updated = transform_code(content, file_path.endsWith('.ts'));
fs.writeFileSync(file_path, updated, 'utf-8');
} catch (e) {
console.error(`Error updating ${file_path}:`, e);
}
return update_pkg(content, [
['svelte', '^4.0.0'],
['svelte-check', '^3.4.3'],
['svelte-preprocess', '^5.0.3'],
['@sveltejs/kit', '^1.20.4'],
['@sveltejs/vite-plugin-svelte', '^2.4.1'],
[
'svelte-loader',
'^3.1.8',
' (if you are still on webpack 4, you need to update to webpack 5)'
],
['rollup-plugin-svelte', '^7.1.5'],
['prettier-plugin-svelte', '^2.10.1'],
['eslint-plugin-svelte', '^2.30.0'],
[
'eslint-plugin-svelte3',
'^4.0.0',
' (this package is deprecated, use eslint-plugin-svelte instead. More info: https://svelte.dev/docs/v4-migration-guide#new-eslint-package)'
],
[
'typescript',
'^5.0.0',
' (this might introduce new type errors due to breaking changes within TypeScript)'
]
]);
}

/**
Expand Down Expand Up @@ -401,28 +339,3 @@ function replaceInJsDoc(source, replacer) {
}
});
}

const logged_migrations = new Set();

/**
* @param {import('ts-morph').SourceFile} source
* @param {string} text
*/
function log_on_ts_modification(source, text) {
let logged = false;
const log = () => {
if (!logged) {
logged = true;
log_migration(text);
}
};
source.onModified(log);
return () => source.onModified(log, false);
}

/** @param {string} text */
function log_migration(text) {
if (logged_migrations.has(text)) return;
console.log(text);
logged_migrations.add(text);
}
Loading

0 comments on commit 8a80190

Please sign in to comment.