sv 0.17.0 → 1.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +2673 -13
- package/dist/{engine-5Sg3lJiQ.d.mts → engine-BrvAeCUj.d.mts} +22 -15
- package/dist/{engine-B7chsCPG.mjs → engine-DlJ4-99M.mjs} +1294 -2950
- package/dist/shared.json +6 -6
- package/dist/src/index.d.mts +5 -6
- package/dist/src/index.mjs +1 -12
- package/dist/src/testing.d.mts +3 -3
- package/dist/src/testing.mjs +1 -1
- package/dist/templates/addon/assets/src/index.js +1 -1
- package/dist/templates/addon/package.json +3 -3
- package/dist/templates/demo/files.types=checkjs.json +4 -4
- package/dist/templates/demo/files.types=none.json +4 -4
- package/dist/templates/demo/files.types=typescript.json +4 -4
- package/dist/templates/demo/package.json +8 -4
- package/dist/templates/library/package.json +9 -5
- package/dist/templates/minimal/files.types=checkjs.json +2 -2
- package/dist/templates/minimal/files.types=none.json +2 -2
- package/dist/templates/minimal/files.types=typescript.json +2 -2
- package/dist/templates/minimal/package.json +8 -4
- package/package.json +4 -5
package/dist/bin.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { color, resolveCommandArray } from "@sveltejs/sv-utils";
|
|
2
|
+
import { A as Command, B as name, C as pipe, D as union, E as transform, F as forwardExitCode, G as ge, H as Ce, I as helpConfig, J as we, K as le, L as normalizePosix, O as from, P as errorAndExit, R as runAndValidateVerifications, S as picklist, T as string, U as Ee, V as version, W as R$1, Y as R, _ as array, a as verifyCleanWorkingDirectory, b as optional, c as AGENT_NAMES, d as installDependencies, f as installOption, i as create, k as program, n as prepareSvApi, o as formatFiles, p as packageManagerPrompt, q as vt, r as add, s as createWorkspace, u as detectPackageManager, v as boolean, w as strictObject, x as parse$1, y as literal, z as runCommand } from "./engine-DlJ4-99M.mjs";
|
|
3
|
+
import { Walker, coerceVersion, color, isVersionUnsupportedBelow, js, loadPackageJson, parse, resolveCommandArray, svelteConfig, transforms } from "@sveltejs/sv-utils";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
4
6
|
import V from "node:process";
|
|
5
7
|
import { execSync } from "node:child_process";
|
|
6
8
|
//#region src/cli/check.ts
|
|
@@ -31,22 +33,2680 @@ async function runCheck(cwd, args) {
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
//#endregion
|
|
34
|
-
//#region src/
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
//#region src/migrate/index.ts
|
|
37
|
+
function defineMigration(migration) {
|
|
38
|
+
return migration;
|
|
39
|
+
}
|
|
40
|
+
function defineMigrationTask(task) {
|
|
41
|
+
return task;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/migrate/migration-task.ts
|
|
45
|
+
/** Marker prefixing every manual follow-up a migration leaves in the user's code. */
|
|
46
|
+
const MIGRATION_TASK_MARKER = "@migration-task";
|
|
47
|
+
let migrationTaskCount = 0;
|
|
48
|
+
/** Running total of `@migration-task` comments left in the user's code during this migration run. */
|
|
49
|
+
function getMigrationTaskCount() {
|
|
50
|
+
return migrationTaskCount;
|
|
51
|
+
}
|
|
52
|
+
/** Resets the running total; call before starting a migration run. */
|
|
53
|
+
function resetMigrationTaskCount() {
|
|
54
|
+
migrationTaskCount = 0;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Leaves a standardized `@migration-task` comment for the user to resolve manually, and bumps the
|
|
58
|
+
* running total so the post-migration summary can report it without re-reading files.
|
|
59
|
+
*
|
|
60
|
+
* The placement depends on the `target` you pass:
|
|
61
|
+
* - `{ comments, node }` for a JS/TS AST (`transforms.script`)
|
|
62
|
+
* - `{ fragment }` for a `.svelte` component (`transforms.svelte` / `svelteScript`)
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* addMigrationTask('rewrite this manually', { comments, node });
|
|
67
|
+
* addMigrationTask('rewrite this manually', { fragment: ast.fragment, anchor });
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
function addMigrationTask(message, target) {
|
|
71
|
+
if ("fragment" in target) addSvelteComment(target.fragment, message, target.anchor);
|
|
72
|
+
else target.comments.add(target.node, {
|
|
73
|
+
type: "Line",
|
|
74
|
+
value: ` ${MIGRATION_TASK_MARKER} ${message}`
|
|
75
|
+
}, target.position ? { position: target.position } : void 0);
|
|
76
|
+
migrationTaskCount++;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns a standalone `// @migration-task` line comment for files that are generated from strings
|
|
80
|
+
* (rather than through an AST), and bumps the running total so the post-migration summary can
|
|
81
|
+
* report it. Prefer `addMigrationTask` whenever an AST is available.
|
|
82
|
+
*/
|
|
83
|
+
function createMigrationTaskComment(message) {
|
|
84
|
+
migrationTaskCount++;
|
|
85
|
+
return `// ${MIGRATION_TASK_MARKER} ${message}`;
|
|
86
|
+
}
|
|
87
|
+
function addSvelteComment(fragment, message, anchor) {
|
|
88
|
+
const comment = {
|
|
89
|
+
type: "Comment",
|
|
90
|
+
data: ` ${MIGRATION_TASK_MARKER} ${message} `,
|
|
91
|
+
start: 0,
|
|
92
|
+
end: 0
|
|
93
|
+
};
|
|
94
|
+
const newline = {
|
|
95
|
+
type: "Text",
|
|
96
|
+
data: "\n",
|
|
97
|
+
raw: "\n",
|
|
98
|
+
start: 0,
|
|
99
|
+
end: 0
|
|
100
|
+
};
|
|
101
|
+
const index = anchor ? fragment.nodes.indexOf(anchor) : -1;
|
|
102
|
+
if (index === -1) fragment.nodes.unshift(comment, newline);
|
|
103
|
+
else fragment.nodes.splice(index, 0, comment, newline);
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/migrate/migrations/app-state/tasks/app-state/migrate.ts
|
|
107
|
+
const OLD_SOURCE$1 = "$app/stores";
|
|
108
|
+
const NEW_SOURCE$1 = "$app/state";
|
|
109
|
+
const NAVIGATING_REVIEW_MESSAGE = "`navigating` is always defined now - review this usage; use `navigating.to` for the pending navigation";
|
|
110
|
+
/** Stores exported from `$app/stores` that we know how to migrate. */
|
|
111
|
+
const KNOWN_STORES = /* @__PURE__ */ new Set([
|
|
112
|
+
"page",
|
|
113
|
+
"navigating",
|
|
114
|
+
"updated",
|
|
115
|
+
"getStores"
|
|
116
|
+
]);
|
|
117
|
+
/**
|
|
118
|
+
* Migrate `$app/stores` usage to `$app/state` in one Svelte component.
|
|
119
|
+
* Returns whether anything changed.
|
|
120
|
+
*/
|
|
121
|
+
function migrateAppState(ast, fragment) {
|
|
122
|
+
const imports = js.imports.findAll(ast, { from: OLD_SOURCE$1 }).filter((found) => found.kind === "static");
|
|
123
|
+
if (imports.length === 0) return false;
|
|
124
|
+
const locals = /* @__PURE__ */ new Map();
|
|
125
|
+
for (const binding of js.imports.bindings(ast, { from: OLD_SOURCE$1 })) {
|
|
126
|
+
if (!KNOWN_STORES.has(binding.imported)) continue;
|
|
127
|
+
locals.set(`$${binding.local}`, {
|
|
128
|
+
name: binding.local,
|
|
129
|
+
store: binding.imported
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (locals.size === 0) return false;
|
|
133
|
+
const bailReason = findBailReason(ast, locals);
|
|
134
|
+
if (bailReason) {
|
|
135
|
+
addMigrationTask(`could not auto-migrate \`${OLD_SOURCE$1}\` to \`${NEW_SOURCE$1}\`: ${bailReason}; migrate manually`, { fragment });
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
for (const found of imports) js.imports.setSource(found, NEW_SOURCE$1);
|
|
139
|
+
const state = { needsNavigatingReview: false };
|
|
140
|
+
derefStores(ast, locals, state);
|
|
141
|
+
derefStores(fragment, locals, state);
|
|
142
|
+
if (state.needsNavigatingReview) addMigrationTask(NAVIGATING_REVIEW_MESSAGE, { fragment });
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Migrate `$app/stores` usage in a `.svelte.ts` / `.svelte.js` module.
|
|
147
|
+
*
|
|
148
|
+
* Modules never used the `$store` auto-subscription syntax, so the only mechanical change is
|
|
149
|
+
* renaming the import. `.subscribe(...)` / `get(store)` reactivity can't be auto-ported to runes,
|
|
150
|
+
* so those are flagged for manual follow-up. Returns whether anything changed.
|
|
151
|
+
*/
|
|
152
|
+
function migrateAppStateModule(ast, comments) {
|
|
153
|
+
const imports = js.imports.findAll(ast, { from: OLD_SOURCE$1 }).filter((found) => found.kind === "static");
|
|
154
|
+
if (imports.length === 0) return false;
|
|
155
|
+
const storeNames = /* @__PURE__ */ new Set();
|
|
156
|
+
let getStoresImport;
|
|
157
|
+
for (const binding of js.imports.bindings(ast, { from: OLD_SOURCE$1 })) {
|
|
158
|
+
if (!KNOWN_STORES.has(binding.imported)) continue;
|
|
159
|
+
if (binding.imported === "getStores") getStoresImport = binding.declaration;
|
|
160
|
+
else storeNames.add(binding.local);
|
|
161
|
+
}
|
|
162
|
+
if (storeNames.size === 0 && !getStoresImport) return false;
|
|
163
|
+
for (const found of imports) js.imports.setSource(found, NEW_SOURCE$1);
|
|
164
|
+
if (getStoresImport) addMigrationTask("`getStores()` is not available; migrate manually", {
|
|
165
|
+
comments,
|
|
166
|
+
node: getStoresImport
|
|
167
|
+
});
|
|
168
|
+
Walker.walk(ast, null, {
|
|
169
|
+
MemberExpression(node, ctx) {
|
|
170
|
+
if (node.object.type === "Identifier" && storeNames.has(node.object.name) && node.property.type === "Identifier" && node.property.name === "subscribe") addMigrationTask("convert `" + node.object.name + ".subscribe(...)` to a rune (`$derived`/`$effect`)", {
|
|
171
|
+
comments,
|
|
172
|
+
node: enclosingStatement(ctx.path) ?? node
|
|
173
|
+
});
|
|
174
|
+
ctx.next();
|
|
175
|
+
},
|
|
176
|
+
CallExpression(node, ctx) {
|
|
177
|
+
if (node.callee.type === "Identifier" && node.callee.name === "get" && node.arguments[0]?.type === "Identifier" && storeNames.has(node.arguments[0].name)) addMigrationTask("replace `get(" + node.arguments[0].name + ")` - read the value directly (no longer a store)", {
|
|
178
|
+
comments,
|
|
179
|
+
node: enclosingStatement(ctx.path) ?? node
|
|
180
|
+
});
|
|
181
|
+
ctx.next();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
function enclosingStatement(path) {
|
|
187
|
+
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
188
|
+
const node = path[i];
|
|
189
|
+
if (node.type === "VariableDeclaration" || node.type === "ExpressionStatement" || node.type === "ReturnStatement") return node;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Detect usage that can't be mechanically migrated. Returns a human-readable reason, or
|
|
194
|
+
* `undefined` when the file is safe to migrate.
|
|
195
|
+
*/
|
|
196
|
+
function findBailReason(ast, locals) {
|
|
197
|
+
for (const local of locals.values()) if (local.store === "getStores") return "`getStores()` is not available";
|
|
198
|
+
const storeNames = new Set([...locals.values()].map((local) => local.name));
|
|
199
|
+
let reason;
|
|
200
|
+
Walker.walk(ast, null, {
|
|
201
|
+
LabeledStatement(node, ctx) {
|
|
202
|
+
if (node.label.name === "$") reason ??= "legacy reactive statement (`$:`)";
|
|
203
|
+
ctx.next();
|
|
204
|
+
},
|
|
205
|
+
ExportNamedDeclaration(node, ctx) {
|
|
206
|
+
if (node.declaration?.type === "VariableDeclaration" && node.declaration.kind === "let") reason ??= "legacy `export let` props";
|
|
207
|
+
ctx.next();
|
|
208
|
+
},
|
|
209
|
+
CallExpression(node, ctx) {
|
|
210
|
+
if (node.callee.type === "Identifier" && node.callee.name === "derived" && node.arguments[0]?.type === "Identifier" && storeNames.has(node.arguments[0].name)) reason ??= "`derived(...)` store";
|
|
211
|
+
ctx.next();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return reason;
|
|
215
|
+
}
|
|
216
|
+
/** Rewrite `$store` auto-subscription identifiers to their `$app/state` equivalent. */
|
|
217
|
+
function derefStores(node, locals, state) {
|
|
218
|
+
Walker.walk(node, null, { Identifier(node, ctx) {
|
|
219
|
+
const local = locals.get(node.name);
|
|
220
|
+
if (local === void 0) return;
|
|
221
|
+
const parent = ctx.path[ctx.path.length - 1];
|
|
222
|
+
if (!js.identifiers.isReference(node, parent)) return;
|
|
223
|
+
if (parent.type === "Property" && parent.shorthand && parent.key === node) {
|
|
224
|
+
parent.key = { ...node };
|
|
225
|
+
parent.shorthand = false;
|
|
226
|
+
}
|
|
227
|
+
if (local.store === "updated") {
|
|
228
|
+
js.common.replaceChild(parent, node, memberExpression(local.name, "current"));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (local.store === "navigating") {
|
|
232
|
+
if (isBooleanPosition(node, parent)) {
|
|
233
|
+
js.common.replaceChild(parent, node, memberExpression(local.name, "to"));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (!isMemberAccessObject(node, parent)) state.needsNavigatingReview = true;
|
|
237
|
+
}
|
|
238
|
+
node.name = local.name;
|
|
239
|
+
} });
|
|
240
|
+
}
|
|
241
|
+
/** Whether `node` is the object being accessed in `parent` (e.g. `$navigating` in `$navigating.to`). */
|
|
242
|
+
function isMemberAccessObject(node, parent) {
|
|
243
|
+
return parent.type === "MemberExpression" && parent.object === node;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Whether `node` sits in a boolean (truthiness-tested) position, where a bare
|
|
247
|
+
* `navigating` should become `navigating.to`.
|
|
248
|
+
*/
|
|
249
|
+
function isBooleanPosition(node, parent) {
|
|
250
|
+
switch (parent.type) {
|
|
251
|
+
case "IfBlock":
|
|
252
|
+
case "IfStatement":
|
|
253
|
+
case "WhileStatement":
|
|
254
|
+
case "DoWhileStatement":
|
|
255
|
+
case "ConditionalExpression": return parent.test === node;
|
|
256
|
+
case "UnaryExpression": return parent.operator === "!" && parent.argument === node;
|
|
257
|
+
case "LogicalExpression": return (parent.operator === "&&" || parent.operator === "||") && parent.left === node;
|
|
258
|
+
default: return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function memberExpression(object, property) {
|
|
262
|
+
return {
|
|
263
|
+
type: "MemberExpression",
|
|
264
|
+
object: {
|
|
265
|
+
type: "Identifier",
|
|
266
|
+
name: object
|
|
267
|
+
},
|
|
268
|
+
property: {
|
|
269
|
+
type: "Identifier",
|
|
270
|
+
name: property
|
|
271
|
+
},
|
|
272
|
+
computed: false,
|
|
273
|
+
optional: false
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/migrate/migrations/app-state/tasks/app-state.ts
|
|
278
|
+
var app_state_default$1 = defineMigrationTask({
|
|
279
|
+
id: "app-state",
|
|
280
|
+
description: "Migrate from \"$app/stores\" to \"$app/state\"",
|
|
281
|
+
run: ({ sv, language }) => {
|
|
282
|
+
sv.files({
|
|
283
|
+
include: "**/*.{svelte,svelte.ts,svelte.js}",
|
|
284
|
+
where: (content) => content.includes("$app/stores")
|
|
285
|
+
}, (content, path) => {
|
|
286
|
+
if (path.endsWith(".svelte")) return transforms.svelteScript({ language }, ({ ast }) => {
|
|
287
|
+
if (!migrateAppState(ast.instance.content, ast.fragment)) return false;
|
|
288
|
+
})(content);
|
|
289
|
+
return transforms.script(({ ast, comments }) => {
|
|
290
|
+
if (!migrateAppStateModule(ast, comments)) return false;
|
|
291
|
+
})(content);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
37
294
|
});
|
|
38
|
-
|
|
39
|
-
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/migrate/migrations/app-state/index.ts
|
|
297
|
+
var app_state_default = defineMigration({
|
|
298
|
+
id: "app-state",
|
|
299
|
+
description: "Migrate from \"$app/stores\" to \"$app/state\"",
|
|
300
|
+
setup: ({ pkg }) => {
|
|
301
|
+
const kitPackageName = "@sveltejs/kit";
|
|
302
|
+
if (!pkg.devDependencies?.[kitPackageName]) throw new Error(`${color.command(kitPackageName)} is not a devDependency in package.json - this doesn't look like a SvelteKit project.\nPoint to one with ${color.command("--cwd <path>")}, or see ${color.command("sv migrate --help")}.`);
|
|
303
|
+
},
|
|
304
|
+
collect: ({ tasks }) => {
|
|
305
|
+
tasks.add(app_state_default$1, { prerequisite: true });
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/migrate/migrations/legacy-migrations/index.ts
|
|
310
|
+
function legacyTask(name, description) {
|
|
311
|
+
return defineMigrationTask({
|
|
312
|
+
id: name,
|
|
313
|
+
description,
|
|
314
|
+
run: async ({ sv }) => {
|
|
315
|
+
await sv.execute(["svelte-migrate@1", name], "inherit");
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function legacyMigration(name, description) {
|
|
320
|
+
return defineMigration({
|
|
321
|
+
id: name,
|
|
322
|
+
description,
|
|
323
|
+
legacy: true,
|
|
324
|
+
setup: () => {},
|
|
325
|
+
collect: ({ tasks }) => {
|
|
326
|
+
tasks.add(legacyTask(name, description), { prerequisite: true });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
const legacyMigrations = [
|
|
331
|
+
legacyMigration("svelte-5", "Migrate from Svelte 4 to Svelte 5"),
|
|
332
|
+
legacyMigration("self-closing-tags", "Migrate to the new self-closing tag syntax"),
|
|
333
|
+
legacyMigration("svelte-4", "Migrate from Svelte 3 to Svelte 4"),
|
|
334
|
+
legacyMigration("sveltekit-2", "Migrate from SvelteKit 1.0 to SvelteKit 2.0"),
|
|
335
|
+
legacyMigration("package", "Migrate from @sveltejs/package@1 to @sveltejs/package@2"),
|
|
336
|
+
legacyMigration("routes", "Migrate from SvelteKit pre-1.0 routing to SvelteKit 1.0 routing")
|
|
337
|
+
];
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/collect-migration-instructions.ts
|
|
340
|
+
const REPORT_PATH = "MIGRATION_TASKS.md";
|
|
341
|
+
const GUIDE_URL = "https://next.svelte.dev/docs/kit/migrating-to-sveltekit-3";
|
|
342
|
+
const CODE_FILES = "**/*.{js,ts,svelte,mjs,mts,cjs,cts}";
|
|
343
|
+
const GENERATED_MARKER = "<!-- Generated by sv migrate sveltekit-3:collect-migration-instructions -->";
|
|
344
|
+
function guideLink(anchor) {
|
|
345
|
+
return [{
|
|
346
|
+
label: "Migrating to SvelteKit v3",
|
|
347
|
+
url: `${GUIDE_URL}#${anchor}`
|
|
348
|
+
}];
|
|
349
|
+
}
|
|
350
|
+
const migrationTasks = [
|
|
351
|
+
{
|
|
352
|
+
title: "Migrate remaining `base`, `assets`, and `resolveRoute` from `$app/paths` usages",
|
|
353
|
+
checks: [{
|
|
354
|
+
include: CODE_FILES,
|
|
355
|
+
patterns: [/^(?=[\s\S]*['"]\$app\/paths['"])(?=[\s\S]*(?:\bbase\b|\bassets\b|\bresolveRoute\b))/]
|
|
356
|
+
}],
|
|
357
|
+
summary: "The automatic migration can leave `base`, `assets`, and `resolveRoute` usages from `$app/paths`.",
|
|
358
|
+
instructions: "The deprecated base, assets, and resolveRoute exports have been removed from $app/paths. Use asset and resolve instead:```diff// instead of this...\nconst pathname = base + resolveRoute('/blog/[slug]', { slug });\nconst file = assets + '/foo.png';\n\n// ...do this:\nconst pathname = resolve('/blog/[slug]', { slug });\nconst file = asset('foo.png');\n```\nThe `Pathname` and `Asset` types have also been renamed to `Path` and `AssetPath`, and the leading `/` has been removed from those types — so `asset('/foo.png')` should now be `asset('foo.png')`, and pathnames passed to `resolve` no longer start with `/` (e.g. `resolve('blog/hello-world')`). Only route IDs start with `/` now.",
|
|
359
|
+
links: guideLink("$app-paths-base-assets-and-resolveRoute-removed")
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
title: "Migrate remaining `$app/stores` usages",
|
|
363
|
+
checks: [{
|
|
364
|
+
include: CODE_FILES,
|
|
365
|
+
patterns: ["$app/stores"]
|
|
366
|
+
}],
|
|
367
|
+
summary: "The automatic migration can leave `$app/stores` references when their store semantics require application context.",
|
|
368
|
+
instructions: "Replace each remaining `$app/stores` reference with `$app/state`. Remove auto-subscription prefixes, read `page` and `navigating` directly, and use `updated.current`. Rewrite `getStores()`, `get(store)`, subscriptions, re-exports, and dynamic imports according to how the value is used rather than only changing the module name.",
|
|
369
|
+
links: guideLink("$app-stores-(removed)")
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
title: "Migrate the removed `$service-worker` module",
|
|
373
|
+
checks: [{
|
|
374
|
+
include: CODE_FILES,
|
|
375
|
+
patterns: ["$service-worker"]
|
|
376
|
+
}],
|
|
377
|
+
summary: "SvelteKit 3 removes `$service-worker` and replaces its exports with several new modules.",
|
|
378
|
+
instructions: "Import `version` from `$app/env`, application asset metadata from `$app/manifest`, and path helpers from `$app/paths`. Account for the new manifest value shapes rather than blindly renaming `build` or `files` imports.",
|
|
379
|
+
links: guideLink("$service-worker-(removed)")
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
title: "Create a TypeScript project for the service worker",
|
|
383
|
+
checks: [{
|
|
384
|
+
include: ["src/service-worker.ts", "src/service-worker/**/*.ts"],
|
|
385
|
+
patterns: [/\S/]
|
|
386
|
+
}],
|
|
387
|
+
summary: "TypeScript service workers now need a separate project with service-worker-specific types.",
|
|
388
|
+
instructions: "Exclude the service worker from the root tsconfig and ensure `src/service-worker/tsconfig.json` extends `$app/tsconfig/service-worker`. Move a flat `src/service-worker.ts` entry to `src/service-worker/index.ts` if necessary, preserving any tooling references.",
|
|
389
|
+
links: guideLink("$app-tsconfig-service-worker")
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
title: "Make the service worker module-compatible",
|
|
393
|
+
checks: [{
|
|
394
|
+
include: ["src/service-worker.{js,ts}", "src/service-worker/**/*.{js,ts}"],
|
|
395
|
+
patterns: [/\bimportScripts\s*\(/]
|
|
396
|
+
}],
|
|
397
|
+
summary: "SvelteKit 3 registers service workers as modules, where classic worker APIs may not work.",
|
|
398
|
+
instructions: "Replace `importScripts(...)` and any other classic-worker-only assumptions with module imports or module-compatible equivalents.",
|
|
399
|
+
links: guideLink("Miscellaneous-Service-worker-registrations-use-type:-'module'")
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
title: "Copy `page.url` before mutating it",
|
|
403
|
+
checks: [{
|
|
404
|
+
include: CODE_FILES,
|
|
405
|
+
patterns: [
|
|
406
|
+
/\bpage\s*\.\s*url\s*\.\s*searchParams\s*\.\s*(?:append|delete|set|sort)\s*\(/,
|
|
407
|
+
/\bpage\s*\.\s*url\s*\.\s*(?:hash|host|hostname|href|password|pathname|port|protocol|search|username)\s*(?:=|\+\+|--)/,
|
|
408
|
+
"new URL(page.url)"
|
|
409
|
+
]
|
|
410
|
+
}],
|
|
411
|
+
summary: "`page.url` and its search parameters are readonly in SvelteKit 3.",
|
|
412
|
+
instructions: "Create a mutable copy with `new URL(page.url.href)`, mutate that copy, and use it for navigation or serialization. Leave readonly accesses unchanged.",
|
|
413
|
+
links: guideLink("$app-state-page.url-is-now-readonly")
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
title: "Review `goto` destinations",
|
|
417
|
+
checks: [{
|
|
418
|
+
include: CODE_FILES,
|
|
419
|
+
patterns: [/^(?=[\s\S]*['"]\$app\/navigation['"])(?=[\s\S]*\bgoto\s*\()/]
|
|
420
|
+
}],
|
|
421
|
+
summary: "`goto(...)` now rejects destinations that do not resolve to an application route.",
|
|
422
|
+
instructions: "Confirm each destination is an internal route. Use `window.location.href` for external browser navigation, and handle rejected dynamic destinations where they can fail to resolve.",
|
|
423
|
+
links: guideLink("$app-navigation-goto-rejects-for-URLs-that-don't-resolve-to-a-route")
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
title: "Adjust noScroll/keepFocus usage",
|
|
427
|
+
checks: [{
|
|
428
|
+
include: CODE_FILES,
|
|
429
|
+
patterns: [
|
|
430
|
+
/^(?=[\s\S]*['"]\$app\/navigation['"])(?=[\s\S]*\bgoto\s*\([\s\S]*(noScroll|keepFocus))/,
|
|
431
|
+
"data-sveltekit-noscroll",
|
|
432
|
+
"data-sveltekit-keepfocus"
|
|
433
|
+
]
|
|
434
|
+
}],
|
|
435
|
+
summary: "The options `noScroll` and `keepFocus` of `goto(...)` are merged into a single `reset` option.",
|
|
436
|
+
instructions: "replace the `noScroll` and `keepFocus` options of `goto` with a single `reset` option, and the `data-sveltekit-noscroll` and `data-sveltekit-keepfocus` attributes with `data-sveltekit-reset`. `noScroll: false / keepFocus: false` becomes `reset: true` and `noScroll: true / keepFocus: true` becomes `reset: false`.",
|
|
437
|
+
links: guideLink("$app-navigation-goto-options-are-updated")
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
title: "Adjust error(...) usage",
|
|
441
|
+
checks: [{
|
|
442
|
+
include: CODE_FILES,
|
|
443
|
+
patterns: [
|
|
444
|
+
/^(?=[\s\S]*['"]@sveltejs\/kit['"])(?=[\s\S]*\berror\s*\([0-9]+,\s*{)/,
|
|
445
|
+
"data-sveltekit-noscroll",
|
|
446
|
+
"data-sveltekit-keepfocus"
|
|
447
|
+
]
|
|
448
|
+
}],
|
|
449
|
+
summary: "Signature of `error(...)` from `@sveltejs/kit` has changed.",
|
|
450
|
+
instructions: "Replace `error(status, { message, ... })` with `error(status, message, { ... })`.",
|
|
451
|
+
links: guideLink("Error-handling-error()-arguments-changed")
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
title: "Replace `invalidateAll` with `refreshAll`",
|
|
455
|
+
checks: [{
|
|
456
|
+
include: CODE_FILES,
|
|
457
|
+
patterns: [/^(?=[\s\S]*['"]\$app\/navigation['"])(?=[\s\S]*\bgoto\s*\()/]
|
|
458
|
+
}],
|
|
459
|
+
summary: "`invalidateAll` is deprecated in favor of `refreshAll`",
|
|
460
|
+
instructions: "Replace `invalidateAll()` with `refreshAll()`. Note that `refreshAll()` will keep `page.state` intact. Check if that has unwanted impact on the application; if yes point this out in a comment with steps to take instead of replacing that instance of `invalidateAll()`. Similarly, replace goto(..., { invalidateAll: true }) with goto(..., { refreshAll: true }).",
|
|
461
|
+
links: guideLink("$app-navigation-invalidateAll-is-deprecated-in-favour-of-refreshAll")
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
title: "`data-sveltekit-*` uses `false` instead of `\"off\"`",
|
|
465
|
+
checks: [{
|
|
466
|
+
include: "**/*.{svelte,html}",
|
|
467
|
+
patterns: [/data-sveltekit-[\w-]+=["']off["']/]
|
|
468
|
+
}],
|
|
469
|
+
summary: "The `\"off\"` value for `data-sveltekit-*` link attributes has been removed in favour of `false`.",
|
|
470
|
+
instructions: "Replace `data-sveltekit-...=\"off\"` with `data-sveltekit-...=\"false\"`.",
|
|
471
|
+
links: guideLink("Miscellaneous-data-sveltekit-uses-false-instead-of-'off'")
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
title: "Guard navigation `delta` access",
|
|
475
|
+
checks: [{
|
|
476
|
+
include: CODE_FILES,
|
|
477
|
+
patterns: [/^(?=[\s\S]*['"]\$app\/navigation['"])(?=[\s\S]*(?:\.\s*delta\b|\{[^}]*\bdelta\b))/]
|
|
478
|
+
}],
|
|
479
|
+
summary: "Navigation `delta` is now only defined for `popstate` navigations.",
|
|
480
|
+
instructions: "Check the navigation type or handle `undefined` before using `delta`. Do not treat an absent delta as zero unless that has the intended application meaning.",
|
|
481
|
+
links: guideLink("$app-navigation-delta-only-exists-for-popstate-navigations")
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
title: "Handle `preloadData` errors",
|
|
485
|
+
checks: [{
|
|
486
|
+
include: CODE_FILES,
|
|
487
|
+
patterns: [/^(?=[\s\S]*['"]\$app\/navigation['"])(?=[\s\S]*\bpreloadData\s*\()/]
|
|
488
|
+
}],
|
|
489
|
+
summary: "`preloadData(...)` can now return an error result, and redirects report their real status.",
|
|
490
|
+
instructions: "Inspect the complete result flow and add handling for `{ type: \"error\", status, error }` where absent. Do not assume every non-redirect result is loaded or has status 200.",
|
|
491
|
+
links: guideLink("$app-navigation-preloadData-can-return-an-error-result")
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
title: "Review enhanced forms with explicit actions",
|
|
495
|
+
checks: [{
|
|
496
|
+
include: "**/*.svelte",
|
|
497
|
+
patterns: [/^(?=[\s\S]*\buse:enhance\b)(?=[\s\S]*<form\b[^>]*\baction\s*=)/]
|
|
498
|
+
}],
|
|
499
|
+
summary: "Enhanced forms that submit to another page now navigate to that page.",
|
|
500
|
+
instructions: "Determine whether each action resolves to another page and confirm navigation is intended. If the form must remain on the current page, implement that behavior explicitly in the enhancement callback.",
|
|
501
|
+
links: guideLink("$app-forms")
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
title: "Migrate `handleValidationError`",
|
|
505
|
+
checks: [{
|
|
506
|
+
include: "**/hooks*.{js,ts,mjs,mts}",
|
|
507
|
+
patterns: ["handleValidationError"]
|
|
508
|
+
}],
|
|
509
|
+
summary: "`handleValidationError` is removed in SvelteKit 3.",
|
|
510
|
+
instructions: "Move validation-error handling into `handleError` and branch on `kind === \"validation\"`. Merge it carefully with any existing `handleError` logic.",
|
|
511
|
+
links: guideLink("Error-handling-handleValidationError-is-removed")
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
title: "Review `handleError` behavior",
|
|
515
|
+
checks: [{
|
|
516
|
+
include: "**/hooks*.{js,ts,mjs,mts}",
|
|
517
|
+
patterns: ["handleError"]
|
|
518
|
+
}],
|
|
519
|
+
summary: "SvelteKit 3 sends expected, validation, and rendering errors through `handleError`.",
|
|
520
|
+
instructions: "Review filtering, logging, reporting, returned error properties, and status handling for the broader set of errors. If `hooks.client` has an async hook, enable `compilerOptions.experimental.async` in `sveltekit(...)`.",
|
|
521
|
+
links: guideLink("Error-handling-handleError-receives-all-errors")
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
title: "CORS for static assets in development is handled by Vite",
|
|
525
|
+
summary: "SvelteKit no longer sets `access-control-allow-origin: *` on every static asset request in development. CORS is now delegated to Vite's built-in middleware.",
|
|
526
|
+
instructions: "If you rely on cross-origin access to static assets in dev, configure it in your Vite config by adding server.cors.origin = \"*\" to your config.",
|
|
527
|
+
links: guideLink("Security-CORS-for-static-assets-in-development-is-handled-by-Vite")
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
title: "`json` and `text` are deprecated",
|
|
531
|
+
checks: [{
|
|
532
|
+
include: CODE_FILES,
|
|
533
|
+
patterns: [/^(?=[\s\S]*['"]@sveltejs\/kit['"])(?=[\s\S]*\b(json|text)\s*\()/]
|
|
534
|
+
}],
|
|
535
|
+
summary: "The `json(...)` and `text(...)` helpers from `@sveltejs/kit` for generating responses are deprecated.",
|
|
536
|
+
instructions: "Use `Response.json(...)` and `new Response(text)` instead.",
|
|
537
|
+
links: guideLink("sveltejs-kit-json-and-text-are-deprecated")
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
title: "Remove the Node polyfills import",
|
|
541
|
+
checks: [{
|
|
542
|
+
include: CODE_FILES,
|
|
543
|
+
patterns: ["@sveltejs/kit/node/polyfills"]
|
|
544
|
+
}],
|
|
545
|
+
summary: "`@sveltejs/kit/node/polyfills` is removed because SvelteKit 3 requires modern Node.",
|
|
546
|
+
instructions: "Remove the obsolete side-effect import and verify that the custom server or adapter runs on Node 22.17 or newer.",
|
|
547
|
+
links: guideLink("sveltejs-kit-node-polyfills-(removed)")
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
title: "Remove `await` from synchronous Node helpers",
|
|
551
|
+
checks: [{
|
|
552
|
+
include: CODE_FILES,
|
|
553
|
+
patterns: [/^(?=[\s\S]*['"]@sveltejs\/kit\/node['"])(?=[\s\S]*\bawait\s+(?:\w+\.)?(?:getRequest|setResponse)\s*\()/]
|
|
554
|
+
}],
|
|
555
|
+
summary: "`getRequest` and `setResponse` from `@sveltejs/kit/node` are now synchronous.",
|
|
556
|
+
instructions: "Remove `await` only from calls bound to those SvelteKit helpers, then review surrounding promise composition and return types.",
|
|
557
|
+
links: guideLink("sveltejs-kit-node")
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
title: "Rename cookie option types",
|
|
561
|
+
checks: [{
|
|
562
|
+
include: CODE_FILES,
|
|
563
|
+
patterns: ["CookieSerializeOptions", "CookieParseOptions"]
|
|
564
|
+
}],
|
|
565
|
+
summary: "Cookie v2 renames its serialization and parsing option types.",
|
|
566
|
+
instructions: "Rename `CookieSerializeOptions` to `SerializeOptions` and `CookieParseOptions` to `ParseOptions`, changing only types imported from the `cookie` package.",
|
|
567
|
+
links: guideLink("Cookies-Updated-to-cookie-v2")
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
title: "Replace non-ASCII cookie names",
|
|
571
|
+
checks: [{
|
|
572
|
+
include: CODE_FILES,
|
|
573
|
+
patterns: [/\bcookies\s*\.\s*(?:get|getAll|set|delete|serialize)\s*\(\s*['"][^'"]*[^\x00-\x7f][^'"]*['"]/]
|
|
574
|
+
}],
|
|
575
|
+
summary: "Cookie v2 rejects cookie names containing non-ASCII characters.",
|
|
576
|
+
instructions: "Choose a stable ASCII replacement and consider compatibility or cleanup for cookies already issued under the old name.",
|
|
577
|
+
links: guideLink("Cookies-Updated-to-cookie-v2")
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
title: "Rename Cloudflare `platform.context`",
|
|
581
|
+
checks: [{
|
|
582
|
+
include: CODE_FILES,
|
|
583
|
+
patterns: [/\bplatform\s*(?:\?\.|\.)\s*context\b/]
|
|
584
|
+
}],
|
|
585
|
+
summary: "The Cloudflare adapter replaces `platform.context` with `platform.ctx`.",
|
|
586
|
+
instructions: "Confirm the value is the Cloudflare platform execution context, then rename it to `platform.ctx` and update related platform typings.",
|
|
587
|
+
links: guideLink("Adapters-adapter-cloudflare")
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
title: "Move adapter-node `ORIGIN` to `paths.origin`",
|
|
591
|
+
checks: [{
|
|
592
|
+
include: [
|
|
593
|
+
"Dockerfile*",
|
|
594
|
+
"**/Dockerfile*",
|
|
595
|
+
"**/*.{js,ts,mjs,mts,json,jsonc,yml,yaml,toml}"
|
|
596
|
+
],
|
|
597
|
+
patterns: [
|
|
598
|
+
/\bprocess\.env\.ORIGIN\b/,
|
|
599
|
+
/\benv\s*\[\s*['"]ORIGIN['"]\s*\]/,
|
|
600
|
+
/^\s*(?:ENV|ARG)\s+ORIGIN\b/m,
|
|
601
|
+
/^\s*ORIGIN\s*[:=]/m
|
|
602
|
+
]
|
|
603
|
+
}],
|
|
604
|
+
summary: "The adapter-node `ORIGIN` environment variable is removed.",
|
|
605
|
+
instructions: "If this value configures adapter-node, move the public-facing origin to `paths.origin` in `sveltekit(...)`. Remove the environment variable only after confirming it has no unrelated use.",
|
|
606
|
+
links: guideLink("Adapters-adapter-node")
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
title: "Replace the Vercel edge runtime",
|
|
610
|
+
checks: [{
|
|
611
|
+
include: "**/*.{js,ts,mjs,mts,json,jsonc}",
|
|
612
|
+
patterns: [/\bruntime\s*:\s*['"]edge['"]/]
|
|
613
|
+
}],
|
|
614
|
+
summary: "The Vercel adapter no longer supports the edge runtime.",
|
|
615
|
+
instructions: "Select a supported Vercel runtime and review runtime-specific APIs, dependencies, and deployment behavior before changing the configuration.",
|
|
616
|
+
links: guideLink("Adapters-adapter-vercel")
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
title: "Replace `builder.createEntries` in custom adapters",
|
|
620
|
+
checks: [{
|
|
621
|
+
include: CODE_FILES,
|
|
622
|
+
patterns: [/\bbuilder\s*\.\s*createEntries\s*\(/]
|
|
623
|
+
}],
|
|
624
|
+
summary: "The adapter API removes `builder.createEntries`.",
|
|
625
|
+
instructions: "Replace it with the appropriate `builder.writeClient`, `builder.writeServer`, and `builder.writePrerendered` flow for the adapter output model.",
|
|
626
|
+
links: guideLink("Adapters-Adapter-API-changes")
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
title: "Remove request route data from remote queries",
|
|
630
|
+
checks: [{
|
|
631
|
+
include: [
|
|
632
|
+
"**/*.remote.{js,ts}",
|
|
633
|
+
"**/*.remote.*.{js,ts}",
|
|
634
|
+
"**/remote.{js,ts}"
|
|
635
|
+
],
|
|
636
|
+
patterns: [/\bevent\s*\.\s*(?:url|params|route)\b/]
|
|
637
|
+
}],
|
|
638
|
+
summary: "Remote queries can no longer access `event.url`, `event.params`, or `event.route`.",
|
|
639
|
+
instructions: "Pass the required values explicitly as validated query arguments and remove the request-specific event access.",
|
|
640
|
+
links: guideLink("Remote-functions-event.url-event.params-and-event.route-cannot-be-accessed-inside-queries")
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
title: "Use remote form field attributes",
|
|
644
|
+
checks: [{
|
|
645
|
+
include: "**/*.svelte",
|
|
646
|
+
patterns: [/^(?=[\s\S]*(?:from\s*['"][^'"]+\.remote(?:\.[^'"]+)?['"]|import\(\s*['"][^'"]+\.remote))(?=[\s\S]*\bname\s*=)/]
|
|
647
|
+
}],
|
|
648
|
+
summary: "Remote form controls must use attributes from the current form field.",
|
|
649
|
+
instructions: "Replace manually specified control names with attributes from the corresponding field `.as(...)` method, verifying that the field belongs to the submitted form (e.g. `<input name=\"foo\">` -> `<input {...myForm.fields.foo.as(\"text\")}>`).",
|
|
650
|
+
links: guideLink("Remote-functions-Form-submissions-require-field.as()")
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
title: "Review consumers of 204 responses",
|
|
654
|
+
checks: [{
|
|
655
|
+
include: ["src/routes/**/+server.{js,ts}", "**/*.{test,spec}.{js,ts,mjs,mts}"],
|
|
656
|
+
patterns: [/\bstatus\s*:\s*204\b/]
|
|
657
|
+
}],
|
|
658
|
+
summary: "SvelteKit 3 returns no body for 204 and other empty 2xx responses.",
|
|
659
|
+
instructions: "Find callers and tests for the matched endpoint and ensure they do not parse JSON or text from its empty response. Keep the endpoint response unchanged if it is already spec-compliant.",
|
|
660
|
+
links: guideLink("Responses-204-responses-return-no-content")
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
title: "Replace remaining `csrf.checkOrigin` configuration",
|
|
664
|
+
checks: [{
|
|
665
|
+
include: "**/{svelte,vite}.config.{js,ts,mjs,mts,cjs,cts}",
|
|
666
|
+
patterns: ["checkOrigin"]
|
|
667
|
+
}],
|
|
668
|
+
summary: "`csrf.checkOrigin` is removed and CSRF protection is always enabled.",
|
|
669
|
+
instructions: "Remove `checkOrigin`. If cross-origin submissions are intentional, configure the narrowest possible `csrf.trustedOrigins` allowlist instead of disabling protection.",
|
|
670
|
+
links: guideLink("Security-csrf.checkOrigin-replaced-by-csrf.trustedOrigins")
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
title: "Review cross-origin form submissions",
|
|
674
|
+
checks: [{
|
|
675
|
+
include: "**/*.svelte",
|
|
676
|
+
patterns: [/<form\b[^>]*\baction\s*=\s*['"](?:https?:)?\/\//]
|
|
677
|
+
}],
|
|
678
|
+
summary: "Cross-origin mutative requests without a `Content-Type` header are rejected as CSRF.",
|
|
679
|
+
instructions: "Ensure the submission sends an appropriate `Content-Type` header or add the destination to the narrowest possible `csrf.trustedOrigins` allowlist.",
|
|
680
|
+
links: guideLink("Security-Cross-origin-form-submissions-require-a-Content-Type-header")
|
|
681
|
+
}
|
|
682
|
+
];
|
|
683
|
+
var collect_migration_instructions_default = defineMigrationTask({
|
|
684
|
+
id: "collect-migration-instructions",
|
|
685
|
+
description: `Collect instructions for non-automated migration tasks; creates ${REPORT_PATH} whose instructions you should follow to complete the migration`,
|
|
686
|
+
run: ({ sv }) => {
|
|
687
|
+
const findings = [];
|
|
688
|
+
for (const task of migrationTasks) {
|
|
689
|
+
const files = /* @__PURE__ */ new Set();
|
|
690
|
+
if (!task.checks) findings.push({
|
|
691
|
+
task,
|
|
692
|
+
files: []
|
|
693
|
+
});
|
|
694
|
+
else {
|
|
695
|
+
for (const check of task.checks) sv.files({
|
|
696
|
+
include: check.include,
|
|
697
|
+
exclude: [REPORT_PATH],
|
|
698
|
+
where: (content) => check.patterns.some((pattern) => matches(content, pattern))
|
|
699
|
+
}, (_content, file) => {
|
|
700
|
+
files.add(file);
|
|
701
|
+
return false;
|
|
702
|
+
});
|
|
703
|
+
if (files.size > 0) findings.push({
|
|
704
|
+
task,
|
|
705
|
+
files: [...files].sort()
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (findings.length === 0) return;
|
|
710
|
+
sv.file(REPORT_PATH, (content) => {
|
|
711
|
+
if (content && !content.includes("<!-- Generated by sv migrate sveltekit-3:collect-migration-instructions -->")) throw new Error(`'${REPORT_PATH}' already exists and was not generated by this migration. Move or rename it before rerunning the task.`);
|
|
712
|
+
return renderReport(findings);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
function matches(content, pattern) {
|
|
717
|
+
return typeof pattern === "string" ? content.includes(pattern) : pattern.test(content);
|
|
718
|
+
}
|
|
719
|
+
function renderReport(findings) {
|
|
720
|
+
return `${GENERATED_MARKER}
|
|
721
|
+
# SvelteKit 3 migration tasks
|
|
722
|
+
|
|
723
|
+
Some migrations are uncommon, context-dependent, or disproportionately difficult to automate safely. This document lists them.
|
|
724
|
+
|
|
725
|
+
The findings are intentionally broad and may include files that do not require changes.
|
|
726
|
+
|
|
727
|
+
AI agents can work through the migration tasks below one at a time:
|
|
728
|
+
|
|
729
|
+
1. Read the linked documentation or pull request before changing code.
|
|
730
|
+
2. Inspect every listed file and its surrounding context.
|
|
731
|
+
3. Ignore a finding when the file is not actually affected.
|
|
732
|
+
4. Make the smallest necessary change. Do not perform unrelated refactors or introduce unnecessary abstractions.
|
|
733
|
+
5. Ask the user when application intent cannot be inferred.
|
|
734
|
+
6. Run the project's appropriate checks after editing.
|
|
735
|
+
|
|
736
|
+
Also search the project for \`@migration-task\` comments. They may describe additional manual migration work that must be resolved.
|
|
737
|
+
|
|
738
|
+
## Migration tasks
|
|
739
|
+
|
|
740
|
+
${findings.map(({ task, files }) => {
|
|
741
|
+
const links = task.links.map(({ label, url }) => `- [${label}](${url})`).join("\n");
|
|
742
|
+
const locations = files.map((file) => `- [ ] \`${normalizePosix(file)}\``).join("\n");
|
|
743
|
+
let output = `### ${task.title}
|
|
744
|
+
|
|
745
|
+
${task.summary}
|
|
746
|
+
|
|
747
|
+
#### What to do
|
|
748
|
+
|
|
749
|
+
${task.instructions}
|
|
750
|
+
|
|
751
|
+
#### References
|
|
752
|
+
|
|
753
|
+
${links}`;
|
|
754
|
+
if (locations.length) output += `
|
|
755
|
+
|
|
756
|
+
#### Files to review
|
|
757
|
+
|
|
758
|
+
${locations}`;
|
|
759
|
+
return output;
|
|
760
|
+
}).join("\n\n")}
|
|
761
|
+
|
|
762
|
+
## Final verification
|
|
763
|
+
|
|
764
|
+
- [ ] Review every migration task and ignore any irrelevant findings.
|
|
765
|
+
- [ ] Resolve remaining \`@migration-task\` comments.
|
|
766
|
+
- [ ] Run the project's type checker and tests.
|
|
767
|
+
- [ ] Build the project successfully.
|
|
768
|
+
- [ ] Delete this file when all migration work is complete.
|
|
769
|
+
`;
|
|
770
|
+
}
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/environment/env-namespace.ts
|
|
773
|
+
const OLD_SOURCE = "$app/environment";
|
|
774
|
+
const NEW_SOURCE = "$app/env";
|
|
775
|
+
/** Rename `$app/environment` imports to `$app/env`. Returns whether anything changed. */
|
|
776
|
+
function renameEnvNamespace(ast) {
|
|
777
|
+
return js.imports.renameSource(ast, {
|
|
778
|
+
from: OLD_SOURCE,
|
|
779
|
+
to: NEW_SOURCE
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
//#endregion
|
|
783
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/environment/explicit-env-vars.ts
|
|
784
|
+
const ENV_MODULE = /^\$env\/(dynamic|static)\/(public|private)$/;
|
|
785
|
+
/** Migrate `$env/*` imports/usages to `$app/env/*` for one program. Returns whether it mutated `ast`. */
|
|
786
|
+
function migrateExplicitEnvVars(ast, envVars, template, comments) {
|
|
787
|
+
let mutated = false;
|
|
788
|
+
const envImports = collectEnvImports(ast, template, comments);
|
|
789
|
+
if (envImports === "migration-task") mutated = true;
|
|
790
|
+
else if (envImports.length > 0) {
|
|
791
|
+
changeEnvImports(ast, envImports);
|
|
792
|
+
replaceEnvUsages(envImports);
|
|
793
|
+
collectEnvVars(envImports, envVars);
|
|
794
|
+
mutated = true;
|
|
795
|
+
}
|
|
796
|
+
if (migrateDynamicEnvImports(ast, envVars, comments, template)) mutated = true;
|
|
797
|
+
if (template && migrateDynamicEnvImports(template, envVars, comments, template)) mutated = true;
|
|
798
|
+
return mutated;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Rewrite dynamic `import('$env/*')` expressions to `import('$app/env/*')`.
|
|
802
|
+
* Names destructured from the result are collected for the `src/env.ts` declaration;
|
|
803
|
+
* other shapes (namespace binding, rest element, computed keys) are flagged for manual work.
|
|
804
|
+
*/
|
|
805
|
+
function migrateDynamicEnvImports(node, envVars, comments, svelteFragment) {
|
|
806
|
+
let mutated = false;
|
|
807
|
+
for (const found of js.imports.findAll(node, { from: ENV_MODULE })) {
|
|
808
|
+
if (found.kind !== "dynamic") continue;
|
|
809
|
+
const [, type, scope] = found.source.match(ENV_MODULE);
|
|
810
|
+
js.imports.setSource(found, `$app/env/${scope}`);
|
|
811
|
+
mutated = true;
|
|
812
|
+
const names = getDestructuredEnvNames(found.path);
|
|
813
|
+
if (names) for (const name of names) envVars.set(name, {
|
|
814
|
+
type,
|
|
815
|
+
scope,
|
|
816
|
+
name
|
|
817
|
+
});
|
|
818
|
+
else addUnsupportedDynamicImportComment(comments, findCommentTarget(found.path) ?? found.node, svelteFragment);
|
|
819
|
+
}
|
|
820
|
+
return mutated;
|
|
821
|
+
}
|
|
822
|
+
function getDestructuredEnvNames(path) {
|
|
823
|
+
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
824
|
+
const node = path[i];
|
|
825
|
+
if (node.type !== "VariableDeclarator") continue;
|
|
826
|
+
if (node.id.type !== "ObjectPattern") return;
|
|
827
|
+
const names = [];
|
|
828
|
+
for (const property of node.id.properties) {
|
|
829
|
+
if (property.type !== "Property") return;
|
|
830
|
+
if (property.key.type !== "Identifier" || property.computed) return;
|
|
831
|
+
names.push(property.key.name);
|
|
832
|
+
}
|
|
833
|
+
return names;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function addUnsupportedDynamicImportComment(comments, node, svelteFragment) {
|
|
837
|
+
const message = "Declare the imported env variables in src/env.ts manually.";
|
|
838
|
+
if (svelteFragment) {
|
|
839
|
+
addMigrationTask(message, {
|
|
840
|
+
fragment: svelteFragment,
|
|
841
|
+
anchor: node
|
|
842
|
+
});
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (!comments) return;
|
|
846
|
+
addMigrationTask(message, {
|
|
847
|
+
comments,
|
|
848
|
+
node
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
function collectEnvImports(ast, template, comments) {
|
|
852
|
+
const envImports = [];
|
|
853
|
+
let hasMigrationTask = false;
|
|
854
|
+
const relevantImports = js.imports.findAll(ast, { from: /^\$env\// }).filter((found) => found.kind === "static");
|
|
855
|
+
if (relevantImports.length === 0) return envImports;
|
|
856
|
+
for (const found of relevantImports) {
|
|
857
|
+
const match = found.source.match(ENV_MODULE);
|
|
858
|
+
if (!match) continue;
|
|
859
|
+
const type = match[1];
|
|
860
|
+
const scope = match[2];
|
|
861
|
+
const envImport = type === "dynamic" ? collectDynamicEnvImport(ast, found, scope, template, comments) : collectStaticEnvImport(ast, found, scope);
|
|
862
|
+
if (envImport === "migration-task") {
|
|
863
|
+
hasMigrationTask = true;
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (envImport) envImports.push(envImport);
|
|
867
|
+
}
|
|
868
|
+
if (envImports.length === 0 && hasMigrationTask) return "migration-task";
|
|
869
|
+
return envImports;
|
|
870
|
+
}
|
|
871
|
+
function collectDynamicEnvImport(ast, found, scope, template, comments) {
|
|
872
|
+
const importNames = new Set(js.imports.bindings(ast, {
|
|
873
|
+
from: found.source,
|
|
874
|
+
name: "env"
|
|
875
|
+
}).filter((binding) => binding.declaration === found.node).map((binding) => binding.local));
|
|
876
|
+
if (importNames.size === 0) return;
|
|
877
|
+
const usages = getDynamicEnvUsages(ast, importNames, comments, template);
|
|
878
|
+
if (!usages) return "migration-task";
|
|
879
|
+
if (template) {
|
|
880
|
+
const templateUsages = getDynamicEnvUsages(template, importNames, comments, template);
|
|
881
|
+
if (!templateUsages) return "migration-task";
|
|
882
|
+
usages.push(...templateUsages);
|
|
883
|
+
}
|
|
884
|
+
if (usages.length === 0) return;
|
|
885
|
+
return {
|
|
886
|
+
type: "dynamic",
|
|
887
|
+
scope,
|
|
888
|
+
found,
|
|
889
|
+
usages
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function collectStaticEnvImport(ast, found, scope) {
|
|
893
|
+
const importNames = js.imports.bindings(ast, { from: found.source }).filter((binding) => binding.kind === "named" && binding.declaration === found.node).map((binding) => binding.imported);
|
|
894
|
+
if (importNames.length === 0) return;
|
|
895
|
+
return {
|
|
896
|
+
type: "static",
|
|
897
|
+
scope,
|
|
898
|
+
found,
|
|
899
|
+
importNames
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
function getDynamicEnvUsages(node, importNames, comments, svelteFragment) {
|
|
903
|
+
const usages = [];
|
|
904
|
+
let hasUnsupportedUsage = false;
|
|
905
|
+
Walker.walk(node, null, { MemberExpression(node, walkContext) {
|
|
906
|
+
if (node.object.type === "Identifier" && importNames.has(node.object.name)) {
|
|
907
|
+
const name = getDynamicEnvUsageName(node);
|
|
908
|
+
if (!name) {
|
|
909
|
+
hasUnsupportedUsage = true;
|
|
910
|
+
addUnsupportedDynamicEnvComment(comments, findCommentTarget(walkContext.path) ?? node, svelteFragment);
|
|
911
|
+
walkContext.next();
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
usages.push({
|
|
915
|
+
node,
|
|
916
|
+
parent: walkContext.path[walkContext.path.length - 1],
|
|
917
|
+
name
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
walkContext.next();
|
|
921
|
+
} });
|
|
922
|
+
if (hasUnsupportedUsage) return;
|
|
923
|
+
return usages;
|
|
924
|
+
}
|
|
925
|
+
function addUnsupportedDynamicEnvComment(comments, node, svelteFragment) {
|
|
926
|
+
const message = "Rewrite dynamic env lookup manually.";
|
|
927
|
+
if (svelteFragment) {
|
|
928
|
+
addMigrationTask(message, {
|
|
929
|
+
fragment: svelteFragment,
|
|
930
|
+
anchor: node
|
|
931
|
+
});
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (!comments) return;
|
|
935
|
+
addMigrationTask(message, {
|
|
936
|
+
comments,
|
|
937
|
+
node
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
function findCommentTarget(path) {
|
|
941
|
+
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
942
|
+
const node = path[i];
|
|
943
|
+
if (node.type === "VariableDeclaration" || node.type === "ExpressionStatement" || node.type === "ReturnStatement") return node;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function getDynamicEnvUsageName(node) {
|
|
947
|
+
if (!node.computed && node.property.type === "Identifier") return node.property.name;
|
|
948
|
+
if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string" && isValidIdentifierName(node.property.value)) return node.property.value;
|
|
949
|
+
}
|
|
950
|
+
function isValidIdentifierName(name) {
|
|
951
|
+
return /^[$A-Z_a-z][$\w]*$/.test(name);
|
|
952
|
+
}
|
|
953
|
+
function changeEnvImports(ast, envImports) {
|
|
954
|
+
const staticImports = envImports.filter((x) => x.type === "static");
|
|
955
|
+
for (const { scope, found } of staticImports) js.imports.setSource(found, `$app/env/${scope}`);
|
|
956
|
+
const dynamicImports = envImports.filter((x) => x.type === "dynamic");
|
|
957
|
+
for (const { scope, found, usages } of dynamicImports) {
|
|
958
|
+
js.imports.setSource(found, `$app/env/${scope}`);
|
|
959
|
+
found.node.specifiers = [];
|
|
960
|
+
const uniqueUsages = new Set(usages.map((usage) => usage.name));
|
|
961
|
+
for (const name of uniqueUsages) found.node.specifiers.push({
|
|
962
|
+
type: "ImportSpecifier",
|
|
963
|
+
imported: {
|
|
964
|
+
type: "Identifier",
|
|
965
|
+
name
|
|
966
|
+
},
|
|
967
|
+
local: {
|
|
968
|
+
type: "Identifier",
|
|
969
|
+
name
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
mergeEnvImports(ast);
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* If we initially have two imports from $env/static/public and $env/dynamic/public
|
|
977
|
+
* they will produce two separate imports which is not totally clean.
|
|
978
|
+
* This function merges multiple imports from the same new source into a single import.
|
|
979
|
+
*
|
|
980
|
+
* Specifiers of duplicated imports are assumed to be unique and are merged together.
|
|
981
|
+
*/
|
|
982
|
+
function mergeEnvImports(ast) {
|
|
983
|
+
const seenImports = /* @__PURE__ */ new Map();
|
|
984
|
+
for (let i = 0; i < ast.body.length; i += 1) {
|
|
985
|
+
const node = ast.body[i];
|
|
986
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
987
|
+
const source = node.source.value;
|
|
988
|
+
if (typeof source !== "string" || !source.startsWith("$app/env/")) continue;
|
|
989
|
+
const existingImport = seenImports.get(source);
|
|
990
|
+
if (!existingImport) {
|
|
991
|
+
seenImports.set(source, node);
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
existingImport.specifiers.push(...node.specifiers);
|
|
995
|
+
ast.body.splice(i, 1);
|
|
996
|
+
i -= 1;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function replaceEnvUsages(envImports) {
|
|
1000
|
+
const dynamicImports = envImports.filter((x) => x.type === "dynamic");
|
|
1001
|
+
for (const { usages } of dynamicImports) for (const usage of usages) {
|
|
1002
|
+
if (!usage.parent) continue;
|
|
1003
|
+
js.common.replaceChild(usage.parent, usage.node, {
|
|
1004
|
+
type: "Identifier",
|
|
1005
|
+
name: usage.name
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
function collectEnvVars(envImports, envVars) {
|
|
1010
|
+
for (const envImport of envImports) if (envImport.type === "static") for (const name of envImport.importNames) envVars.set(name, {
|
|
1011
|
+
type: envImport.type,
|
|
1012
|
+
scope: envImport.scope,
|
|
1013
|
+
name
|
|
1014
|
+
});
|
|
1015
|
+
else for (const usage of envImport.usages) envVars.set(usage.name, {
|
|
1016
|
+
type: envImport.type,
|
|
1017
|
+
scope: envImport.scope,
|
|
1018
|
+
name: usage.name
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
function addEnvDeclarationFile(ast, comments, envVars) {
|
|
1022
|
+
if (envVars.size === 0) return false;
|
|
1023
|
+
js.imports.addNamed(ast, {
|
|
1024
|
+
from: "@sveltejs/kit/env",
|
|
1025
|
+
imports: ["defineEnvVars"]
|
|
1026
|
+
});
|
|
1027
|
+
const defineCall = js.functions.createCall({
|
|
1028
|
+
name: "defineEnvVars",
|
|
1029
|
+
args: []
|
|
1030
|
+
});
|
|
1031
|
+
const variablesObject = js.functions.getArgument(defineCall, {
|
|
1032
|
+
index: 0,
|
|
1033
|
+
fallback: js.object.create({})
|
|
1034
|
+
});
|
|
1035
|
+
const variablesIdentifier = js.variables.declaration(ast, {
|
|
1036
|
+
kind: "const",
|
|
1037
|
+
name: "variables",
|
|
1038
|
+
value: defineCall
|
|
1039
|
+
});
|
|
1040
|
+
const exportDeclaration = js.exports.createNamed(ast, {
|
|
1041
|
+
name: "variables",
|
|
1042
|
+
fallback: variablesIdentifier
|
|
1043
|
+
});
|
|
1044
|
+
let has_dynamic = false;
|
|
1045
|
+
for (const envVar of envVars.values()) {
|
|
1046
|
+
const value = js.object.property(variablesObject, {
|
|
1047
|
+
name: envVar.name,
|
|
1048
|
+
fallback: js.object.create({})
|
|
1049
|
+
});
|
|
1050
|
+
has_dynamic ||= envVar.type === "dynamic";
|
|
1051
|
+
js.object.overrideProperties(value, {
|
|
1052
|
+
public: envVar.scope === "public" ? true : void 0,
|
|
1053
|
+
static: envVar.type === "static" ? true : void 0,
|
|
1054
|
+
schema: envVar.type === "dynamic" ? js.functions.createArrow({
|
|
1055
|
+
params: ["input"],
|
|
1056
|
+
body: {
|
|
1057
|
+
type: "LogicalExpression",
|
|
1058
|
+
operator: "??",
|
|
1059
|
+
left: {
|
|
1060
|
+
type: "Identifier",
|
|
1061
|
+
name: "input"
|
|
1062
|
+
},
|
|
1063
|
+
right: {
|
|
1064
|
+
type: "Literal",
|
|
1065
|
+
value: "",
|
|
1066
|
+
raw: "''"
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
async: false
|
|
1070
|
+
}) : void 0
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
if (has_dynamic) addMigrationTask("Review usage of dynamic environment variables. They fall back to the empty string if not present, which may not be what you want.", {
|
|
1074
|
+
comments,
|
|
1075
|
+
node: exportDeclaration
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
//#endregion
|
|
1079
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/environment.ts
|
|
1080
|
+
var environment_default = defineMigrationTask({
|
|
1081
|
+
id: "environment",
|
|
1082
|
+
description: "Migrate to explicit environment variables ($app/env)",
|
|
1083
|
+
run: ({ sv, language }) => {
|
|
1084
|
+
const envVars = /* @__PURE__ */ new Map();
|
|
1085
|
+
sv.files({
|
|
1086
|
+
include: "**/*.{ts,js,svelte}",
|
|
1087
|
+
where: (content) => content.includes("$env/") || content.includes("$app/environment")
|
|
1088
|
+
}, (content, path) => {
|
|
1089
|
+
if (path.endsWith(".svelte")) return transforms.svelteScript({ language }, ({ ast }) => {
|
|
1090
|
+
const renamed = renameEnvNamespace(ast.instance.content);
|
|
1091
|
+
const migrated = migrateExplicitEnvVars(ast.instance.content, envVars, ast.fragment);
|
|
1092
|
+
if (!renamed && !migrated) return false;
|
|
1093
|
+
})(content);
|
|
1094
|
+
return transforms.script(({ ast, comments }) => {
|
|
1095
|
+
const renamed = renameEnvNamespace(ast);
|
|
1096
|
+
const migrated = migrateExplicitEnvVars(ast, envVars, void 0, comments);
|
|
1097
|
+
if (!renamed && !migrated) return false;
|
|
1098
|
+
})(content);
|
|
1099
|
+
});
|
|
1100
|
+
if (envVars.size > 0) sv.file(`src/env.${language}`, transforms.script(({ ast, comments }) => addEnvDeclarationFile(ast, comments, envVars)));
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
//#endregion
|
|
1104
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/external-redirects.ts
|
|
1105
|
+
const KIT_MODULE$1 = "@sveltejs/kit";
|
|
1106
|
+
const REDIRECT = "redirect";
|
|
1107
|
+
const BASES = ["https://sveltekit-a.invalid", "https://sveltekit-b.invalid"];
|
|
1108
|
+
/** Stands in for the dynamic remainder of a partially static destination (e.g. `https://${domain}`). */
|
|
1109
|
+
const PLACEHOLDER = "x";
|
|
1110
|
+
var external_redirects_default = defineMigrationTask({
|
|
1111
|
+
id: "external-redirects",
|
|
1112
|
+
description: "Opt external redirects into the new redirect behavior",
|
|
1113
|
+
run: ({ sv }) => {
|
|
1114
|
+
sv.files({
|
|
1115
|
+
include: "**/*.{js,ts,svelte}",
|
|
1116
|
+
where: (content) => content.includes(KIT_MODULE$1) && content.includes(REDIRECT)
|
|
1117
|
+
}, (content, file) => {
|
|
1118
|
+
if (file.endsWith(".svelte")) return transforms.svelte(({ ast }) => {
|
|
1119
|
+
let changed = false;
|
|
1120
|
+
if (ast.module && migrateExternalRedirects(ast.module.content)) changed = true;
|
|
1121
|
+
if (ast.instance && migrateExternalRedirects(ast.instance.content)) changed = true;
|
|
1122
|
+
if (!changed) return false;
|
|
1123
|
+
})(content);
|
|
1124
|
+
return transforms.script(({ ast }) => {
|
|
1125
|
+
if (!migrateExternalRedirects(ast)) return false;
|
|
1126
|
+
})(content);
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
function migrateExternalRedirects(ast) {
|
|
1131
|
+
const { locals, namespaces } = collectRedirectImports(ast);
|
|
1132
|
+
if (locals.size === 0 && namespaces.size === 0) return false;
|
|
1133
|
+
let changed = false;
|
|
1134
|
+
Walker.walk(ast, null, { CallExpression(node, { next }) {
|
|
1135
|
+
const destination = node.arguments[1];
|
|
1136
|
+
if (isRedirectCall(node, locals, namespaces) && node.arguments.length === 2 && destination?.type !== "SpreadElement") {
|
|
1137
|
+
const external = externalValue(destination);
|
|
1138
|
+
if (external !== void 0) {
|
|
1139
|
+
node.arguments.push(js.object.create({ external: js.common.createLiteral(external) }));
|
|
1140
|
+
changed = true;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
next();
|
|
1144
|
+
} });
|
|
1145
|
+
return changed;
|
|
1146
|
+
}
|
|
1147
|
+
function collectRedirectImports(ast) {
|
|
1148
|
+
const bindings = js.imports.bindings(ast, { from: KIT_MODULE$1 });
|
|
1149
|
+
return {
|
|
1150
|
+
locals: new Set(bindings.filter((binding) => binding.kind === "named" && binding.imported === REDIRECT && !binding.isType).map((binding) => binding.local)),
|
|
1151
|
+
namespaces: new Set(bindings.filter((binding) => binding.kind === "namespace" && !binding.isType).map((binding) => binding.local))
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function isRedirectCall(node, locals, namespaces) {
|
|
1155
|
+
const callee = node.callee;
|
|
1156
|
+
if (callee.type === "Identifier") return locals.has(callee.name);
|
|
1157
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Identifier") {
|
|
1158
|
+
if (!namespaces.has(callee.object.name)) return false;
|
|
1159
|
+
if (callee.computed) return callee.property.type === "Literal" && callee.property.value === REDIRECT;
|
|
1160
|
+
return callee.property.type === "Identifier" && callee.property.name === REDIRECT;
|
|
1161
|
+
}
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
function externalValue(destination) {
|
|
1165
|
+
const prefix = staticPrefix(destination);
|
|
1166
|
+
if (prefix === void 0) return void 0;
|
|
1167
|
+
let result = classify(prefix.value);
|
|
1168
|
+
if (result === void 0 && !prefix.complete) result = classify(prefix.value + PLACEHOLDER);
|
|
1169
|
+
return result === false ? void 0 : result;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Classifies a destination string: `true` for external, `'javascript:'` for javascript URLs,
|
|
1173
|
+
* `false` for same-origin, and `undefined` when it cannot be parsed as a URL.
|
|
1174
|
+
*/
|
|
1175
|
+
function classify(value) {
|
|
40
1176
|
try {
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
1177
|
+
const urls = BASES.map((base) => new URL(value, base));
|
|
1178
|
+
if (urls.some((url) => url.protocol === "javascript:")) return "javascript:";
|
|
1179
|
+
return urls.some((url, index) => url.origin !== BASES[index]) || false;
|
|
1180
|
+
} catch {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function staticPrefix(expression) {
|
|
1185
|
+
if (expression.type === "Literal") return typeof expression.value === "string" ? {
|
|
1186
|
+
value: expression.value,
|
|
1187
|
+
complete: true
|
|
1188
|
+
} : void 0;
|
|
1189
|
+
if (expression.type === "TemplateLiteral") {
|
|
1190
|
+
const value = expression.quasis[0]?.value.cooked ?? expression.quasis[0]?.value.raw;
|
|
1191
|
+
if (value === void 0) return void 0;
|
|
1192
|
+
return {
|
|
1193
|
+
value,
|
|
1194
|
+
complete: expression.expressions.length === 0
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
if (expression.type === "BinaryExpression" && expression.operator === "+") {
|
|
1198
|
+
if (expression.left.type === "PrivateIdentifier") return void 0;
|
|
1199
|
+
const left = staticPrefix(expression.left);
|
|
1200
|
+
return left && {
|
|
1201
|
+
value: left.value,
|
|
1202
|
+
complete: false
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
//#endregion
|
|
1207
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/imports.ts
|
|
1208
|
+
const KIT_MODULE = "@sveltejs/kit";
|
|
1209
|
+
const MOVED_EXPORTS = /* @__PURE__ */ new Map([
|
|
1210
|
+
["ActionResult", "$app/forms"],
|
|
1211
|
+
["AfterNavigate", "$app/navigation"],
|
|
1212
|
+
["BeforeNavigate", "$app/navigation"],
|
|
1213
|
+
["CaughtError", "@sveltejs/kit/hooks"],
|
|
1214
|
+
["ClientCaughtError", "@sveltejs/kit/hooks"],
|
|
1215
|
+
["ClientInit", "@sveltejs/kit/hooks"],
|
|
1216
|
+
["DefinedEnvVars", "@sveltejs/kit/env"],
|
|
1217
|
+
["DefinedParams", "@sveltejs/kit/params"],
|
|
1218
|
+
["EnvVarConfig", "@sveltejs/kit/env"],
|
|
1219
|
+
["GotoOptions", "$app/navigation"],
|
|
1220
|
+
["Handle", "@sveltejs/kit/hooks"],
|
|
1221
|
+
["HandleClientError", "@sveltejs/kit/hooks"],
|
|
1222
|
+
["HandleFetch", "@sveltejs/kit/hooks"],
|
|
1223
|
+
["HandleServerError", "@sveltejs/kit/hooks"],
|
|
1224
|
+
["InvalidField", "$app/server"],
|
|
1225
|
+
["LiveQueryRequestedResult", "$app/server"],
|
|
1226
|
+
["LiveRequestedEntry", "$app/server"],
|
|
1227
|
+
["MatcherParam", "@sveltejs/kit/params"],
|
|
1228
|
+
["Navigation", "$app/navigation"],
|
|
1229
|
+
["NavigationBase", "$app/navigation"],
|
|
1230
|
+
["NavigationEnter", "$app/navigation"],
|
|
1231
|
+
["NavigationExternal", "$app/navigation"],
|
|
1232
|
+
["NavigationFormSubmit", "$app/navigation"],
|
|
1233
|
+
["NavigationGoto", "$app/navigation"],
|
|
1234
|
+
["NavigationLeave", "$app/navigation"],
|
|
1235
|
+
["NavigationLink", "$app/navigation"],
|
|
1236
|
+
["NavigationPopState", "$app/navigation"],
|
|
1237
|
+
["NavigationTarget", "$app/navigation"],
|
|
1238
|
+
["NavigationType", "$app/navigation"],
|
|
1239
|
+
["OnNavigate", "$app/navigation"],
|
|
1240
|
+
["Page", "$app/state"],
|
|
1241
|
+
["ParamDefinition", "@sveltejs/kit/params"],
|
|
1242
|
+
["ParamMatcher", "@sveltejs/kit/params"],
|
|
1243
|
+
["ParamValue", "@sveltejs/kit/params"],
|
|
1244
|
+
["QueryRequestedResult", "$app/server"],
|
|
1245
|
+
["ReadonlyURL", "$app/state"],
|
|
1246
|
+
["ReadonlyURLSearchParams", "$app/state"],
|
|
1247
|
+
["RemoteCommand", "$app/server"],
|
|
1248
|
+
["RemoteForm", "$app/server"],
|
|
1249
|
+
["RemoteFormEnhanceCallback", "$app/server"],
|
|
1250
|
+
["RemoteFormEnhanceInstance", "$app/server"],
|
|
1251
|
+
["RemoteFormField", "$app/server"],
|
|
1252
|
+
["RemoteFormFields", "$app/server"],
|
|
1253
|
+
["RemoteFormFieldType", "$app/server"],
|
|
1254
|
+
["RemoteFormFieldValue", "$app/server"],
|
|
1255
|
+
["RemoteFormInput", "$app/server"],
|
|
1256
|
+
["RemoteFormIssue", "$app/server"],
|
|
1257
|
+
["RemoteLiveQuery", "$app/server"],
|
|
1258
|
+
["RemoteLiveQueryFunction", "$app/server"],
|
|
1259
|
+
["RemotePrerenderFunction", "$app/server"],
|
|
1260
|
+
["RemoteQuery", "$app/server"],
|
|
1261
|
+
["RemoteQueryFunction", "$app/server"],
|
|
1262
|
+
["RemoteQueryOverride", "$app/server"],
|
|
1263
|
+
["RemoteQueryUpdate", "$app/server"],
|
|
1264
|
+
["RemoteResource", "$app/server"],
|
|
1265
|
+
["RequestedEntry", "$app/server"],
|
|
1266
|
+
["RequestedResult", "$app/server"],
|
|
1267
|
+
["Reroute", "@sveltejs/kit/hooks"],
|
|
1268
|
+
["ResolveOptions", "@sveltejs/kit/hooks"],
|
|
1269
|
+
["ServerInit", "@sveltejs/kit/hooks"],
|
|
1270
|
+
["SubmitFunction", "$app/forms"],
|
|
1271
|
+
["Transport", "@sveltejs/kit/hooks"],
|
|
1272
|
+
["Transporter", "@sveltejs/kit/hooks"],
|
|
1273
|
+
["ValidationError", "$app/server"],
|
|
1274
|
+
["defineParams", "@sveltejs/kit/params"]
|
|
1275
|
+
]);
|
|
1276
|
+
var imports_default = defineMigrationTask({
|
|
1277
|
+
id: "imports",
|
|
1278
|
+
description: "Move SvelteKit APIs to their new modules",
|
|
1279
|
+
run: ({ sv }) => {
|
|
1280
|
+
sv.files({
|
|
1281
|
+
include: "**/*.{js,ts,mjs,mts,cjs,cts,svelte}",
|
|
1282
|
+
where: (content) => content.includes(KIT_MODULE)
|
|
1283
|
+
}, (content, file) => {
|
|
1284
|
+
if (file.endsWith(".svelte")) return transforms.svelte(({ ast }) => {
|
|
1285
|
+
let changed = false;
|
|
1286
|
+
if (ast.module && migrateProgram(ast.module.content)) changed = true;
|
|
1287
|
+
if (ast.instance && migrateProgram(ast.instance.content)) changed = true;
|
|
1288
|
+
if (migrateComments(ast.comments)) changed = true;
|
|
1289
|
+
if (!changed) return false;
|
|
1290
|
+
})(content);
|
|
1291
|
+
return transforms.script(({ ast, comments }) => {
|
|
1292
|
+
const programChanged = migrateProgram(ast);
|
|
1293
|
+
const commentsChanged = migrateComments(comments.list());
|
|
1294
|
+
if (!programChanged && !commentsChanged) return false;
|
|
1295
|
+
})(content);
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
function migrateProgram(ast) {
|
|
1300
|
+
let changed = migrateNamedImports(ast);
|
|
1301
|
+
const body = [];
|
|
1302
|
+
for (const statement of ast.body) {
|
|
1303
|
+
if (statement.type === "ExportNamedDeclaration" && statement.source?.value === KIT_MODULE) {
|
|
1304
|
+
const replacements = splitExportSpecifiers(statement);
|
|
1305
|
+
if (replacements) {
|
|
1306
|
+
body.push(...replacements);
|
|
1307
|
+
changed = true;
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
body.push(statement);
|
|
1312
|
+
}
|
|
1313
|
+
ast.body = body;
|
|
1314
|
+
return migrateImportTypes(ast) || changed;
|
|
1315
|
+
}
|
|
1316
|
+
function migrateNamedImports(ast) {
|
|
1317
|
+
const bindings = js.imports.bindings(ast, { from: KIT_MODULE }).filter((binding) => binding.kind === "named");
|
|
1318
|
+
if (!bindings.some((binding) => MOVED_EXPORTS.has(binding.imported))) return false;
|
|
1319
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1320
|
+
for (const binding of bindings) {
|
|
1321
|
+
const source = MOVED_EXPORTS.get(binding.imported) ?? KIT_MODULE;
|
|
1322
|
+
const group = bySource.get(source) ?? [];
|
|
1323
|
+
group.push(binding);
|
|
1324
|
+
bySource.set(source, group);
|
|
1325
|
+
}
|
|
1326
|
+
for (const imported of new Set(bindings.map((binding) => binding.imported))) js.imports.remove(ast, {
|
|
1327
|
+
from: KIT_MODULE,
|
|
1328
|
+
name: imported
|
|
1329
|
+
});
|
|
1330
|
+
for (const [source, group] of [...bySource].reverse()) for (const binding of group) js.imports.addNamed(ast, {
|
|
1331
|
+
from: source,
|
|
1332
|
+
imports: { [binding.imported]: binding.local },
|
|
1333
|
+
isType: binding.isType
|
|
1334
|
+
});
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
function splitExportSpecifiers(declaration) {
|
|
1338
|
+
const moved = /* @__PURE__ */ new Map();
|
|
1339
|
+
const remaining = [];
|
|
1340
|
+
for (const specifier of declaration.specifiers) {
|
|
1341
|
+
const name = specifier.local.type === "Identifier" ? specifier.local.name : String(specifier.local.value);
|
|
1342
|
+
const destination = MOVED_EXPORTS.get(name);
|
|
1343
|
+
if (!destination) {
|
|
1344
|
+
remaining.push(specifier);
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
const specifiers = moved.get(destination) ?? [];
|
|
1348
|
+
specifiers.push(specifier);
|
|
1349
|
+
moved.set(destination, specifiers);
|
|
1350
|
+
}
|
|
1351
|
+
if (moved.size === 0) return;
|
|
1352
|
+
const declarations = [];
|
|
1353
|
+
if (remaining.length > 0) declarations.push({
|
|
1354
|
+
...declaration,
|
|
1355
|
+
specifiers: remaining
|
|
1356
|
+
});
|
|
1357
|
+
for (const [source, specifiers] of moved) declarations.push({
|
|
1358
|
+
...declaration,
|
|
1359
|
+
specifiers,
|
|
1360
|
+
source: {
|
|
1361
|
+
type: "Literal",
|
|
1362
|
+
value: source
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
return declarations;
|
|
1366
|
+
}
|
|
1367
|
+
function migrateImportTypes(node) {
|
|
1368
|
+
if (!node || typeof node !== "object") return false;
|
|
1369
|
+
if (Array.isArray(node)) {
|
|
1370
|
+
let changed = false;
|
|
1371
|
+
for (const child of node) if (migrateImportTypes(child)) changed = true;
|
|
1372
|
+
return changed;
|
|
1373
|
+
}
|
|
1374
|
+
const record = node;
|
|
1375
|
+
let changed = false;
|
|
1376
|
+
if (record.type === "TSImportType") {
|
|
1377
|
+
const argument = record.argument;
|
|
1378
|
+
const qualifier = record.qualifier;
|
|
1379
|
+
if (argument?.value === KIT_MODULE && qualifier?.type === "Identifier" && typeof qualifier.name === "string") {
|
|
1380
|
+
const destination = MOVED_EXPORTS.get(qualifier.name);
|
|
1381
|
+
if (destination) {
|
|
1382
|
+
argument.value = destination;
|
|
1383
|
+
argument.raw = void 0;
|
|
1384
|
+
changed = true;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
for (const child of Object.values(record)) if (migrateImportTypes(child)) changed = true;
|
|
1389
|
+
return changed;
|
|
1390
|
+
}
|
|
1391
|
+
function migrateComments(comments) {
|
|
1392
|
+
let changed = false;
|
|
1393
|
+
for (const comment of comments) {
|
|
1394
|
+
let value = comment.value.replace(/import(\s*\(\s*)(['"])@sveltejs\/kit\2(\s*\)\s*\.\s*)([A-Za-z_$][\w$]*)/g, (match, open, quote, close, name) => {
|
|
1395
|
+
const destination = MOVED_EXPORTS.get(name);
|
|
1396
|
+
if (!destination) return match;
|
|
1397
|
+
changed = true;
|
|
1398
|
+
return `import${open}${quote}${destination}${quote}${close}${name}`;
|
|
1399
|
+
});
|
|
1400
|
+
value = value.replace(/@import\s*\{([^}]+)\}\s*from\s*(['"])@sveltejs\/kit\2(;?)/g, (match, list, quote, semicolon) => {
|
|
1401
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1402
|
+
for (const specifier of list.split(",")) {
|
|
1403
|
+
const imported = /^(?:type\s+)?([A-Za-z_$][\w$]*)/.exec(specifier.trim())?.[1];
|
|
1404
|
+
const source = imported && MOVED_EXPORTS.get(imported);
|
|
1405
|
+
const group = groups.get(source ?? KIT_MODULE) ?? [];
|
|
1406
|
+
group.push(specifier.trim());
|
|
1407
|
+
groups.set(source ?? KIT_MODULE, group);
|
|
1408
|
+
}
|
|
1409
|
+
if (groups.size === 1 && groups.has(KIT_MODULE)) return match;
|
|
1410
|
+
changed = true;
|
|
1411
|
+
return [...groups].map(([source, specifiers]) => `@import { ${specifiers.join(", ")} } from ${quote}${source}${quote}${semicolon}`).join("\n * ");
|
|
1412
|
+
});
|
|
1413
|
+
comment.value = value;
|
|
1414
|
+
}
|
|
1415
|
+
return changed;
|
|
1416
|
+
}
|
|
1417
|
+
//#endregion
|
|
1418
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/lib-alias.ts
|
|
1419
|
+
const LIB_ALIAS = /(?<=['"`])\$lib(\/(.+)['"`]|['"`])/g;
|
|
1420
|
+
function libSubpathImports(libDir) {
|
|
1421
|
+
return {
|
|
1422
|
+
"#lib": `./${libDir}/index.js`,
|
|
1423
|
+
"#lib/*": `./${libDir}/*`
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
var lib_alias_default = defineMigrationTask({
|
|
1427
|
+
id: "lib-alias",
|
|
1428
|
+
description: "Replace the $lib alias with #lib subpath imports",
|
|
1429
|
+
run: ({ sv, cwd, directory }) => {
|
|
1430
|
+
sv.file("package.json", transforms.json(({ data }) => {
|
|
1431
|
+
data.imports = {
|
|
1432
|
+
...libSubpathImports(directory.lib),
|
|
1433
|
+
...data.imports
|
|
1434
|
+
};
|
|
1435
|
+
}));
|
|
1436
|
+
sv.files({
|
|
1437
|
+
include: `${directory.src}/**/*.{svelte,svelte.ts,svelte.js,ts,js,svx,md}`,
|
|
1438
|
+
where: (content) => content.includes("$lib")
|
|
1439
|
+
}, (content) => {
|
|
1440
|
+
const rewritten = content.replace(LIB_ALIAS, (match, _, import_path) => {
|
|
1441
|
+
match = match.replace("$lib", "#lib");
|
|
1442
|
+
if (import_path && !import_path.endsWith(".js") && !import_path.endsWith(".ts")) {
|
|
1443
|
+
for (const ending of [
|
|
1444
|
+
".js",
|
|
1445
|
+
".ts",
|
|
1446
|
+
"/index.js",
|
|
1447
|
+
"/index.ts"
|
|
1448
|
+
]) if (fs.existsSync(`${cwd}/${directory.lib}/${import_path}${ending}`)) return match.slice(0, -1) + ending.replace(".ts", ".js") + match.slice(-1);
|
|
1449
|
+
}
|
|
1450
|
+
return match;
|
|
1451
|
+
});
|
|
1452
|
+
return rewritten === content ? false : rewritten;
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
//#endregion
|
|
1457
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/package-json.ts
|
|
1458
|
+
const KIT3_PEERS = {
|
|
1459
|
+
vite: "^8.0.12",
|
|
1460
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
1461
|
+
svelte: "^5.56.4",
|
|
1462
|
+
"svelte-check": "^4.7.5",
|
|
1463
|
+
typescript: "^6.0.0"
|
|
1464
|
+
};
|
|
1465
|
+
var package_json_default = defineMigrationTask({
|
|
1466
|
+
id: "package-json",
|
|
1467
|
+
description: "Update package.json to be compatible with SvelteKit 3.0",
|
|
1468
|
+
run: ({ sv, cwd, language, dependencyVersion }) => {
|
|
1469
|
+
sv.devDependency("@sveltejs/kit", "next");
|
|
1470
|
+
for (const [pkg, range] of Object.entries(KIT3_PEERS)) {
|
|
1471
|
+
if (pkg === "typescript" && language !== "ts") continue;
|
|
1472
|
+
const current = dependencyVersion(pkg);
|
|
1473
|
+
if (current && isVersionUnsupportedBelow(current, range)) sv.devDependency(pkg, range);
|
|
1474
|
+
}
|
|
1475
|
+
const { data: pkg } = loadPackageJson(cwd);
|
|
1476
|
+
const dependencies = {
|
|
1477
|
+
...pkg.devDependencies,
|
|
1478
|
+
...pkg.dependencies
|
|
1479
|
+
};
|
|
1480
|
+
for (const name of Object.keys(dependencies)) if (name.startsWith("@sveltejs/adapter-")) sv.devDependency(name, "next");
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
//#endregion
|
|
1484
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/params.ts
|
|
1485
|
+
const MATCHER_FILE = /^\w+\.[jt]s$/;
|
|
1486
|
+
const TEST_HARNESS_FILE = /\.template\.[jt]s$/;
|
|
1487
|
+
var params_default = defineMigrationTask({
|
|
1488
|
+
id: "params",
|
|
1489
|
+
description: "Consolidate parameter matchers into src/params.js or src/params.ts",
|
|
1490
|
+
run: ({ sv, cwd, directory, language }) => {
|
|
1491
|
+
const paramsDirectory = path.posix.join(directory.src, "params");
|
|
1492
|
+
const absoluteParamsDirectory = path.join(cwd, paramsDirectory);
|
|
1493
|
+
if (!fs.existsSync(absoluteParamsDirectory)) return;
|
|
1494
|
+
const entries = fs.readdirSync(absoluteParamsDirectory, { withFileTypes: true });
|
|
1495
|
+
const isMatcher = (entry) => entry.isFile() && MATCHER_FILE.test(entry.name);
|
|
1496
|
+
const foreignEntries = entries.filter((entry) => !isMatcher(entry) && !TEST_HARNESS_FILE.test(entry.name));
|
|
1497
|
+
const matcherFiles = entries.filter(isMatcher).map((entry) => path.posix.join(paramsDirectory, entry.name)).sort();
|
|
1498
|
+
if (matcherFiles.length === 0) return;
|
|
1499
|
+
const target = `${paramsDirectory}.${language}`;
|
|
1500
|
+
if (fs.existsSync(path.join(cwd, target))) throw new Error(`Cannot migrate parameter matchers because '${target}' already exists`);
|
|
1501
|
+
const context = {
|
|
1502
|
+
usedNames: /* @__PURE__ */ new Set(["defineParams", "params"]),
|
|
1503
|
+
imports: /* @__PURE__ */ new Map(),
|
|
1504
|
+
sideEffectImports: /* @__PURE__ */ new Set()
|
|
1505
|
+
};
|
|
1506
|
+
context.imports.set("named:defineParams:@sveltejs/kit/params", {
|
|
1507
|
+
source: "@sveltejs/kit/params",
|
|
1508
|
+
kind: "named",
|
|
1509
|
+
imported: "defineParams",
|
|
1510
|
+
local: "defineParams",
|
|
1511
|
+
typeOnly: false
|
|
1512
|
+
});
|
|
1513
|
+
const matchers = matcherFiles.map((file) => migrateMatcher(cwd, file, target, context));
|
|
1514
|
+
const parts = [];
|
|
1515
|
+
if (foreignEntries.length > 0) {
|
|
1516
|
+
const names = foreignEntries.map((entry) => entry.name).sort().join(", ");
|
|
1517
|
+
parts.push(createMigrationTaskComment(`'${paramsDirectory}' contains entries that are not parameter matchers (${names}) and was left in place - migrate them and delete the directory manually`));
|
|
1518
|
+
}
|
|
1519
|
+
parts.push(...generateImportCode(context), "");
|
|
1520
|
+
for (const matcher of matchers) if (matcher.hoisted) parts.push(matcher.hoisted, "");
|
|
1521
|
+
parts.push("export const params = defineParams({", matchers.map((matcher) => matcher.entry).join(",\n"), "});", "");
|
|
1522
|
+
const content = parts.join("\n");
|
|
1523
|
+
sv.file(target, () => content);
|
|
1524
|
+
if (foreignEntries.length > 0) return;
|
|
1525
|
+
for (const file of matcherFiles) fs.unlinkSync(path.join(cwd, file));
|
|
1526
|
+
if (fs.readdirSync(absoluteParamsDirectory).length === 0) fs.rmdirSync(absoluteParamsDirectory);
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
function migrateMatcher(cwd, file, target, context) {
|
|
1530
|
+
const name = path.posix.basename(file).slice(0, -3);
|
|
1531
|
+
const content = stripParamMatcherComments(fs.readFileSync(path.join(cwd, file), "utf8"));
|
|
1532
|
+
const parsed = parse.script(content);
|
|
1533
|
+
const importDeclarations = [];
|
|
1534
|
+
const body = [];
|
|
1535
|
+
const paramMatcherTypes = /* @__PURE__ */ new Set(["ParamMatcher"]);
|
|
1536
|
+
let declaresMatch = false;
|
|
1537
|
+
let exportsMatch = false;
|
|
1538
|
+
for (const statement of parsed.ast.body) {
|
|
1539
|
+
if (statement.type === "ImportDeclaration") {
|
|
1540
|
+
if (statement.source.value === "@sveltejs/kit" || statement.source.value === "@sveltejs/kit/params") {
|
|
1541
|
+
for (const specifier of statement.specifiers) if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "ParamMatcher") paramMatcherTypes.add(specifier.local.name);
|
|
1542
|
+
statement.specifiers = statement.specifiers.filter((specifier) => specifier.type !== "ImportSpecifier" || specifier.imported.type !== "Identifier" || specifier.imported.name !== "ParamMatcher");
|
|
1543
|
+
if (statement.specifiers.length === 0) continue;
|
|
1544
|
+
}
|
|
1545
|
+
let source = String(statement.source.value);
|
|
1546
|
+
if (source.startsWith(".")) source = relocateImport(source, file, target);
|
|
1547
|
+
if (statement.specifiers.length === 0) context.sideEffectImports.add(source);
|
|
1548
|
+
else importDeclarations.push({
|
|
1549
|
+
declaration: statement,
|
|
1550
|
+
source
|
|
1551
|
+
});
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
if (statement.type === "ExportDefaultDeclaration") throw new Error(`Cannot migrate '${file}' because it contains a default export`);
|
|
1555
|
+
if (statement.type === "ExportAllDeclaration") throw new Error(`Cannot migrate '${file}' because it re-exports another module`);
|
|
1556
|
+
if (statement.type === "ExportNamedDeclaration") {
|
|
1557
|
+
if (statement.declaration) {
|
|
1558
|
+
stripParamMatcherType(statement.declaration, paramMatcherTypes);
|
|
1559
|
+
if (declares(statement.declaration, "match")) {
|
|
1560
|
+
declaresMatch = true;
|
|
1561
|
+
exportsMatch = true;
|
|
1562
|
+
}
|
|
1563
|
+
body.push(statement.declaration);
|
|
1564
|
+
} else exportsMatch ||= statement.specifiers.some((specifier) => specifier.exported.type === "Identifier" && specifier.exported.name === "match");
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
stripParamMatcherType(statement, paramMatcherTypes);
|
|
1568
|
+
declaresMatch ||= declares(statement, "match");
|
|
1569
|
+
body.push(statement);
|
|
1570
|
+
}
|
|
1571
|
+
if (!declaresMatch || !exportsMatch) throw new Error(`Cannot migrate '${file}' because it does not export a local 'match' declaration`);
|
|
1572
|
+
const renames = /* @__PURE__ */ new Map();
|
|
1573
|
+
for (const { declaration, source } of importDeclarations) {
|
|
1574
|
+
const typeOnly = declaration.importKind === "type";
|
|
1575
|
+
for (const specifier of declaration.specifiers) {
|
|
1576
|
+
const local = specifier.local.name;
|
|
1577
|
+
const finalLocal = registerImport(context, {
|
|
1578
|
+
source,
|
|
1579
|
+
kind: specifier.type === "ImportDefaultSpecifier" ? "default" : specifier.type === "ImportNamespaceSpecifier" ? "namespace" : "named",
|
|
1580
|
+
imported: specifier.type === "ImportDefaultSpecifier" ? "default" : specifier.type === "ImportNamespaceSpecifier" ? "*" : specifier.imported.type === "Identifier" ? specifier.imported.name : String(specifier.imported.value),
|
|
1581
|
+
local,
|
|
1582
|
+
typeOnly
|
|
1583
|
+
});
|
|
1584
|
+
if (finalLocal !== local) renames.set(local, finalLocal);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const nestedBindings = js.scope.nestedBindingNames(body);
|
|
1588
|
+
for (const local of renames.keys()) if (nestedBindings.has(local)) throw new Error(`Cannot migrate '${file}' because the imported name '${local}' conflicts with another matcher and cannot be renamed safely`);
|
|
1589
|
+
const identifier = name.replace(/\W/g, "_");
|
|
1590
|
+
const preferredMatchName = `match${identifier.charAt(0).toUpperCase()}${identifier.slice(1)}`;
|
|
1591
|
+
parsed.ast.body = body;
|
|
1592
|
+
const topLevelBindings = js.scope.topLevelBindings([parsed.ast]);
|
|
1593
|
+
const tentativeNames = new Set(context.usedNames);
|
|
1594
|
+
const bodyRenames = /* @__PURE__ */ new Map();
|
|
1595
|
+
for (const binding of topLevelBindings) {
|
|
1596
|
+
const preferred = binding === "match" ? preferredMatchName : binding;
|
|
1597
|
+
const finalName = js.identifiers.uniqueName(preferred, tentativeNames);
|
|
1598
|
+
if (finalName !== binding) bodyRenames.set(binding, finalName);
|
|
1599
|
+
}
|
|
1600
|
+
const hoistable = [...bodyRenames.keys()].every((binding) => !nestedBindings.has(binding));
|
|
1601
|
+
if (hoistable) {
|
|
1602
|
+
for (const binding of topLevelBindings) context.usedNames.add(bodyRenames.get(binding) ?? binding);
|
|
1603
|
+
for (const [from, to] of bodyRenames) renames.set(from, to);
|
|
1604
|
+
}
|
|
1605
|
+
js.identifiers.renameReferences([parsed.ast], renames);
|
|
1606
|
+
const bodyCode = parsed.generateCode().trim();
|
|
1607
|
+
if (hoistable) {
|
|
1608
|
+
const matchName = bodyRenames.get("match");
|
|
1609
|
+
return {
|
|
1610
|
+
entry: `\t${propertyName(name)}: (param) => (${matchName}(param) ? param : undefined)`,
|
|
1611
|
+
hoisted: bodyCode
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
return { entry: `\t${propertyName(name)}: (() => {\n${indent(bodyCode, 2)}\n\n\t\treturn (param) => (match(param) ? param : undefined);\n\t})()` };
|
|
1615
|
+
}
|
|
1616
|
+
/** Registers an import binding, deduplicating it and returning the module-level local name. */
|
|
1617
|
+
function registerImport(context, binding) {
|
|
1618
|
+
const key = `${binding.kind}:${binding.imported}:${binding.source}`;
|
|
1619
|
+
const existing = context.imports.get(key);
|
|
1620
|
+
if (existing) {
|
|
1621
|
+
existing.typeOnly &&= binding.typeOnly;
|
|
1622
|
+
return existing.local;
|
|
1623
|
+
}
|
|
1624
|
+
const local = js.identifiers.uniqueName(binding.local, context.usedNames);
|
|
1625
|
+
context.imports.set(key, {
|
|
1626
|
+
...binding,
|
|
1627
|
+
local
|
|
1628
|
+
});
|
|
1629
|
+
return local;
|
|
1630
|
+
}
|
|
1631
|
+
function generateImportCode(context) {
|
|
1632
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1633
|
+
for (const binding of context.imports.values()) {
|
|
1634
|
+
const group = bySource.get(binding.source) ?? [];
|
|
1635
|
+
group.push(binding);
|
|
1636
|
+
bySource.set(binding.source, group);
|
|
1637
|
+
}
|
|
1638
|
+
const lines = [];
|
|
1639
|
+
for (const [source, bindings] of bySource) {
|
|
1640
|
+
const from = `from ${quoteString(source)};`;
|
|
1641
|
+
const defaultBinding = bindings.find((binding) => binding.kind === "default");
|
|
1642
|
+
const namespaceBinding = bindings.find((binding) => binding.kind === "namespace");
|
|
1643
|
+
const namedBindings = bindings.filter((binding) => binding.kind === "named");
|
|
1644
|
+
let defaultEmitted = false;
|
|
1645
|
+
if (namespaceBinding) {
|
|
1646
|
+
const prefix = defaultBinding && namedBindings.length === 0 ? `${defaultBinding.local}, ` : "";
|
|
1647
|
+
defaultEmitted = prefix !== "";
|
|
1648
|
+
lines.push(`import ${prefix}* as ${namespaceBinding.local} ${from}`);
|
|
1649
|
+
}
|
|
1650
|
+
if (namedBindings.length > 0 || defaultBinding && !defaultEmitted) {
|
|
1651
|
+
const typeOnly = namedBindings.length > 0 && namedBindings.every((binding) => binding.typeOnly) && (!defaultBinding || defaultEmitted);
|
|
1652
|
+
const specifiers = namedBindings.map((binding) => {
|
|
1653
|
+
const imported = /^[$A-Z_a-z][$\w]*$/.test(binding.imported) ? binding.imported : quoteString(binding.imported);
|
|
1654
|
+
const specifier = imported === binding.local ? binding.local : `${imported} as ${binding.local}`;
|
|
1655
|
+
return !typeOnly && binding.typeOnly ? `type ${specifier}` : specifier;
|
|
1656
|
+
}).join(", ");
|
|
1657
|
+
const clauses = [];
|
|
1658
|
+
if (defaultBinding && !defaultEmitted) clauses.push(defaultBinding.local);
|
|
1659
|
+
if (namedBindings.length > 0) clauses.push(`{ ${specifiers} }`);
|
|
1660
|
+
lines.push(`import ${typeOnly ? "type " : ""}${clauses.join(", ")} ${from}`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
for (const source of context.sideEffectImports) lines.push(`import ${quoteString(source)};`);
|
|
1664
|
+
return lines;
|
|
1665
|
+
}
|
|
1666
|
+
function stripParamMatcherType(node, paramMatcherTypes) {
|
|
1667
|
+
if (node.type !== "VariableDeclaration") return;
|
|
1668
|
+
const declaration = node.declarations.find((declaration) => declaration.id.type === "Identifier" && declaration.id.name === "match");
|
|
1669
|
+
if (!declaration || declaration.id.type !== "Identifier") return;
|
|
1670
|
+
const annotation = declaration.id.typeAnnotation?.typeAnnotation;
|
|
1671
|
+
if (annotation?.type === "TSTypeReference" && annotation.typeName.type === "Identifier" && paramMatcherTypes.has(annotation.typeName.name)) delete declaration.id.typeAnnotation;
|
|
1672
|
+
if ((declaration.init?.type === "TSSatisfiesExpression" || declaration.init?.type === "TSAsExpression") && declaration.init.typeAnnotation.type === "TSTypeReference" && declaration.init.typeAnnotation.typeName.type === "Identifier" && paramMatcherTypes.has(declaration.init.typeAnnotation.typeName.name)) declaration.init = declaration.init.expression;
|
|
1673
|
+
}
|
|
1674
|
+
function stripParamMatcherComments(content) {
|
|
1675
|
+
return content.replace(/\/\*\*[\s\S]*?\*\//g, (comment) => {
|
|
1676
|
+
if (!comment.includes("ParamMatcher")) return comment;
|
|
1677
|
+
const lines = comment.split("\n").filter((line) => !line.includes("ParamMatcher"));
|
|
1678
|
+
return lines.length > 2 ? lines.join("\n") : "";
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
function declares(node, name) {
|
|
1682
|
+
if ((node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") && node.id?.name === name) return true;
|
|
1683
|
+
return node.type === "VariableDeclaration" && node.declarations.some((declaration) => declaration.id.type === "Identifier" && js.scope.collectPatternNames(declaration.id).includes(name));
|
|
1684
|
+
}
|
|
1685
|
+
function relocateImport(source, from, to) {
|
|
1686
|
+
const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(from), source));
|
|
1687
|
+
let relative = path.posix.relative(path.posix.dirname(to), resolved);
|
|
1688
|
+
if (!relative.startsWith(".")) relative = `./${relative}`;
|
|
1689
|
+
return relative;
|
|
1690
|
+
}
|
|
1691
|
+
function propertyName(name) {
|
|
1692
|
+
return /^[$A-Z_a-z][$\w]*$/.test(name) ? name : quoteString(name);
|
|
1693
|
+
}
|
|
1694
|
+
function quoteString(value) {
|
|
1695
|
+
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
1696
|
+
}
|
|
1697
|
+
function indent(content, level) {
|
|
1698
|
+
const prefix = " ".repeat(level);
|
|
1699
|
+
return content.split("\n").map((line) => line ? `${prefix}${line}` : line).join("\n");
|
|
1700
|
+
}
|
|
1701
|
+
//#endregion
|
|
1702
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/paths.ts
|
|
1703
|
+
const PATHS_MODULE = "$app/paths";
|
|
1704
|
+
const TYPES_MODULE = "$app/types";
|
|
1705
|
+
var paths_default = defineMigrationTask({
|
|
1706
|
+
id: "paths",
|
|
1707
|
+
description: "Migrate deprecated $app/paths APIs and path types",
|
|
1708
|
+
run: ({ sv }) => {
|
|
1709
|
+
sv.files({
|
|
1710
|
+
include: "**/*.{js,ts,svelte}",
|
|
1711
|
+
where: (content) => content.includes(PATHS_MODULE) || content.includes(TYPES_MODULE)
|
|
1712
|
+
}, (content, file) => {
|
|
1713
|
+
if (file.endsWith(".svelte")) return transforms.svelte(({ ast }) => {
|
|
1714
|
+
const programs = [];
|
|
1715
|
+
if (ast.module) programs.push(ast.module.content);
|
|
1716
|
+
if (ast.instance) programs.push(ast.instance.content);
|
|
1717
|
+
if (!migratePaths(programs, ast.fragment)) return false;
|
|
1718
|
+
})(content);
|
|
1719
|
+
return transforms.script(({ ast }) => {
|
|
1720
|
+
if (!migratePaths([ast])) return false;
|
|
1721
|
+
})(content);
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
function migratePaths(programs, fragment) {
|
|
1726
|
+
const roots = fragment ? [...programs, fragment] : programs;
|
|
1727
|
+
const pathBindings = programs.flatMap((program) => js.imports.bindings(program, { from: PATHS_MODULE }));
|
|
1728
|
+
const typeBindings = programs.flatMap((program) => js.imports.bindings(program, { from: TYPES_MODULE }));
|
|
1729
|
+
if (pathBindings.length === 0 && typeBindings.length === 0) return false;
|
|
1730
|
+
const baseLocals = importedLocals(pathBindings, "base");
|
|
1731
|
+
const assetsLocals = importedLocals(pathBindings, "assets");
|
|
1732
|
+
const resolveRouteLocals = importedLocals(pathBindings, "resolveRoute");
|
|
1733
|
+
const existingResolve = importAlias(pathBindings, "resolve");
|
|
1734
|
+
const existingAsset = importAlias(pathBindings, "asset");
|
|
1735
|
+
const needsResolve = baseLocals.length > 0 || resolveRouteLocals.length > 0;
|
|
1736
|
+
const needsAsset = assetsLocals.length > 0;
|
|
1737
|
+
const resolveLocal = existingResolve ?? preferredReplacementLocal(resolveRouteLocals, "resolve") ?? availableImportLocal(programs, "resolve", "resolvePath", [...baseLocals, ...resolveRouteLocals]);
|
|
1738
|
+
const assetLocal = existingAsset ?? availableImportLocal(programs, "asset", "assetPath", assetsLocals);
|
|
1739
|
+
const shadowedIdentifiers = js.scope.findShadowedIdentifiers(roots, [
|
|
1740
|
+
...baseLocals,
|
|
1741
|
+
...assetsLocals,
|
|
1742
|
+
...resolveRouteLocals,
|
|
1743
|
+
resolveLocal,
|
|
1744
|
+
assetLocal
|
|
1745
|
+
]);
|
|
1746
|
+
const resolveRouteCalls = /* @__PURE__ */ new Set();
|
|
1747
|
+
for (const root of roots) Walker.walk(root, null, { CallExpression(node, ctx) {
|
|
1748
|
+
if (isNamedIdentifier(node.callee, resolveRouteLocals) && !shadowedIdentifiers.has(node.callee)) resolveRouteCalls.add(node);
|
|
1749
|
+
ctx.next();
|
|
1750
|
+
} });
|
|
1751
|
+
let changed = false;
|
|
1752
|
+
if (baseLocals.length > 0 || assetsLocals.length > 0) changed = collapsePathPrefixes(roots, baseLocals, assetsLocals, resolveRouteLocals, resolveLocal, assetLocal, shadowedIdentifiers);
|
|
1753
|
+
if (js.identifiers.replaceReferences(roots, baseLocals, () => js.functions.createCall({
|
|
1754
|
+
name: resolveLocal,
|
|
1755
|
+
args: [""]
|
|
1756
|
+
}))) changed = true;
|
|
1757
|
+
if (js.identifiers.replaceReferences(roots, assetsLocals, () => js.functions.createCall({
|
|
1758
|
+
name: assetLocal,
|
|
1759
|
+
args: [""]
|
|
1760
|
+
}))) changed = true;
|
|
1761
|
+
if (js.imports.renameBinding(roots, {
|
|
1762
|
+
from: PATHS_MODULE,
|
|
1763
|
+
name: "assets",
|
|
1764
|
+
to: "asset",
|
|
1765
|
+
local: assetLocal
|
|
1766
|
+
})) changed = true;
|
|
1767
|
+
if (js.imports.renameBinding(roots, {
|
|
1768
|
+
from: PATHS_MODULE,
|
|
1769
|
+
name: "base",
|
|
1770
|
+
to: "resolve",
|
|
1771
|
+
local: resolveLocal
|
|
1772
|
+
})) changed = true;
|
|
1773
|
+
if (js.imports.renameBinding(roots, {
|
|
1774
|
+
from: PATHS_MODULE,
|
|
1775
|
+
name: "resolveRoute",
|
|
1776
|
+
to: "resolve",
|
|
1777
|
+
local: resolveLocal
|
|
1778
|
+
})) changed = true;
|
|
1779
|
+
const renamedAsset = renameImportedType(programs, roots, typeBindings, "Asset", "AssetPath");
|
|
1780
|
+
if (renameImportedType(programs, roots, typeBindings, "Pathname", "Path") || renamedAsset) changed = true;
|
|
1781
|
+
if (normalizePathCalls(roots, existingResolve !== void 0 || needsResolve ? resolveLocal : void 0, existingAsset !== void 0 || needsAsset ? assetLocal : void 0, resolveRouteCalls, shadowedIdentifiers)) changed = true;
|
|
1782
|
+
return changed;
|
|
1783
|
+
}
|
|
1784
|
+
function importAlias(bindings, name) {
|
|
1785
|
+
return bindings.find((binding) => binding.imported === name)?.local;
|
|
1786
|
+
}
|
|
1787
|
+
function importedLocals(bindings, name) {
|
|
1788
|
+
return bindings.filter((binding) => binding.kind === "named" && binding.imported === name).map((binding) => binding.local);
|
|
1789
|
+
}
|
|
1790
|
+
function preferredReplacementLocal(locals, replacement) {
|
|
1791
|
+
const local = locals[0];
|
|
1792
|
+
if (!local) return void 0;
|
|
1793
|
+
return local === "resolveRoute" ? replacement : local;
|
|
1794
|
+
}
|
|
1795
|
+
function collapsePathPrefixes(roots, baseLocals, assetsLocals, resolveRouteLocals, resolveLocal, assetLocal, shadowedIdentifiers) {
|
|
1796
|
+
let changed = false;
|
|
1797
|
+
const visitors = {
|
|
1798
|
+
BinaryExpression(node, ctx) {
|
|
1799
|
+
if (node.operator !== "+" || node.left.type !== "Identifier" || shadowedIdentifiers.has(node.left)) {
|
|
1800
|
+
ctx.next();
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
let replacement;
|
|
1804
|
+
if (baseLocals.includes(node.left.name)) replacement = isResolveRouteCall(node.right, resolveRouteLocals, shadowedIdentifiers) ? node.right : js.functions.createCall({
|
|
1805
|
+
name: resolveLocal,
|
|
1806
|
+
args: [withoutLeadingSlash(node.right)]
|
|
1807
|
+
});
|
|
1808
|
+
else if (assetsLocals.includes(node.left.name)) replacement = js.functions.createCall({
|
|
1809
|
+
name: assetLocal,
|
|
1810
|
+
args: [withoutLeadingSlash(node.right)]
|
|
1811
|
+
});
|
|
1812
|
+
if (!replacement) {
|
|
1813
|
+
ctx.next();
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
js.common.replaceChild(ctx.path[ctx.path.length - 1], node, replacement);
|
|
1817
|
+
changed = true;
|
|
1818
|
+
},
|
|
1819
|
+
TemplateLiteral(node, ctx) {
|
|
1820
|
+
const first = node.expressions[0];
|
|
1821
|
+
const parent = ctx.path[ctx.path.length - 1];
|
|
1822
|
+
if (first?.type !== "Identifier" || shadowedIdentifiers.has(first) || node.quasis[0]?.value.raw !== "" || parent.type === "TaggedTemplateExpression" && parent.quasi === node) {
|
|
1823
|
+
ctx.next();
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
let callee;
|
|
1827
|
+
if (baseLocals.includes(first.name)) callee = resolveLocal;
|
|
1828
|
+
if (assetsLocals.includes(first.name)) callee = assetLocal;
|
|
1829
|
+
if (!callee) {
|
|
1830
|
+
ctx.next();
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
const rest = {
|
|
1834
|
+
type: "TemplateLiteral",
|
|
1835
|
+
expressions: node.expressions.slice(1),
|
|
1836
|
+
quasis: node.quasis.slice(1)
|
|
1837
|
+
};
|
|
1838
|
+
if (baseLocals.includes(first.name) && rest.quasis[0]?.value.raw === "" && rest.expressions[0] && isResolveRouteCall(rest.expressions[0], resolveRouteLocals, shadowedIdentifiers)) {
|
|
1839
|
+
const onlyCall = rest.expressions.length === 1 && rest.quasis.every((quasi) => quasi.value.raw === "");
|
|
1840
|
+
js.common.replaceChild(parent, node, onlyCall ? rest.expressions[0] : rest);
|
|
1841
|
+
changed = true;
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
js.common.replaceChild(parent, node, js.functions.createCall({
|
|
1845
|
+
name: callee,
|
|
1846
|
+
args: [withoutLeadingSlash(rest)]
|
|
1847
|
+
}));
|
|
1848
|
+
changed = true;
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
for (const root of roots) Walker.walk(root, null, visitors);
|
|
1852
|
+
return changed;
|
|
1853
|
+
}
|
|
1854
|
+
function renameImportedType(programs, roots, bindings, oldName, newName) {
|
|
1855
|
+
const oldLocals = importedLocals(bindings, oldName);
|
|
1856
|
+
if (oldLocals.length === 0) return false;
|
|
1857
|
+
const targetLocal = importAlias(bindings, newName) ?? (oldLocals[0] === oldName ? availableImportLocal(programs, newName, `App${newName}`, oldLocals) : oldLocals[0]);
|
|
1858
|
+
return js.imports.renameBinding(roots, {
|
|
1859
|
+
from: TYPES_MODULE,
|
|
1860
|
+
name: oldName,
|
|
1861
|
+
to: newName,
|
|
1862
|
+
local: targetLocal
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
function availableImportLocal(programs, preferred, fallback, replacedLocals) {
|
|
1866
|
+
const bindings = js.scope.topLevelBindings(programs);
|
|
1867
|
+
for (const local of replacedLocals) bindings.delete(local);
|
|
1868
|
+
if (!bindings.has(preferred) || replacedLocals.includes(preferred)) return preferred;
|
|
1869
|
+
return js.identifiers.uniqueName(fallback, bindings);
|
|
1870
|
+
}
|
|
1871
|
+
function normalizePathCalls(roots, resolveLocal, assetLocal, resolveRouteCalls, shadowedIdentifiers) {
|
|
1872
|
+
let changed = false;
|
|
1873
|
+
const visitors = { CallExpression(node, ctx) {
|
|
1874
|
+
const argument = node.arguments[0];
|
|
1875
|
+
if (!argument || argument.type === "SpreadElement") {
|
|
1876
|
+
ctx.next();
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
const isAsset = assetLocal !== void 0 && node.callee.type === "Identifier" && node.callee.name === assetLocal;
|
|
1880
|
+
const isPathname = resolveLocal !== void 0 && node.callee.type === "Identifier" && node.callee.name === resolveLocal && node.arguments.length === 1 && !resolveRouteCalls.has(node) && !looksLikeRouteId(argument);
|
|
1881
|
+
if ((isAsset || isPathname) && node.callee.type === "Identifier" && !shadowedIdentifiers.has(node.callee) && removeStaticLeadingSlash(argument)) changed = true;
|
|
1882
|
+
ctx.next();
|
|
1883
|
+
} };
|
|
1884
|
+
for (const root of roots) Walker.walk(root, null, visitors);
|
|
1885
|
+
return changed;
|
|
1886
|
+
}
|
|
1887
|
+
function looksLikeRouteId(expression) {
|
|
1888
|
+
if (expression.type === "Literal" && typeof expression.value === "string") return /[[(]/.test(expression.value);
|
|
1889
|
+
if (expression.type === "TemplateLiteral") return expression.quasis.some((quasi) => /[[(]/.test(quasi.value.raw));
|
|
1890
|
+
return false;
|
|
1891
|
+
}
|
|
1892
|
+
function withoutLeadingSlash(expression) {
|
|
1893
|
+
if (removeStaticLeadingSlash(expression)) return expression;
|
|
1894
|
+
return {
|
|
1895
|
+
type: "CallExpression",
|
|
1896
|
+
callee: {
|
|
1897
|
+
type: "MemberExpression",
|
|
1898
|
+
object: expression,
|
|
1899
|
+
property: {
|
|
1900
|
+
type: "Identifier",
|
|
1901
|
+
name: "slice"
|
|
1902
|
+
},
|
|
1903
|
+
computed: false,
|
|
1904
|
+
optional: false
|
|
1905
|
+
},
|
|
1906
|
+
arguments: [js.common.createLiteral(1)],
|
|
1907
|
+
optional: false
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
function removeStaticLeadingSlash(expression) {
|
|
1911
|
+
if (expression.type === "Literal" && typeof expression.value === "string") {
|
|
1912
|
+
if (!expression.value.startsWith("/")) return false;
|
|
1913
|
+
expression.value = expression.value.slice(1);
|
|
1914
|
+
expression.raw = void 0;
|
|
1915
|
+
return true;
|
|
1916
|
+
}
|
|
1917
|
+
if (expression.type === "TemplateLiteral") {
|
|
1918
|
+
const quasi = expression.quasis[0];
|
|
1919
|
+
if (!quasi?.value.raw.startsWith("/")) return false;
|
|
1920
|
+
quasi.value.raw = quasi.value.raw.slice(1);
|
|
1921
|
+
if (quasi.value.cooked !== void 0 && quasi.value.cooked !== null) quasi.value.cooked = quasi.value.cooked.slice(1);
|
|
1922
|
+
return true;
|
|
1923
|
+
}
|
|
1924
|
+
return false;
|
|
1925
|
+
}
|
|
1926
|
+
function isResolveRouteCall(expression, resolveRouteLocals, shadowedIdentifiers) {
|
|
1927
|
+
return expression.type === "CallExpression" && isNamedIdentifier(expression.callee, resolveRouteLocals) && !shadowedIdentifiers.has(expression.callee);
|
|
1928
|
+
}
|
|
1929
|
+
function isNamedIdentifier(node, names) {
|
|
1930
|
+
return node.type === "Identifier" && names.includes(node.name);
|
|
1931
|
+
}
|
|
1932
|
+
//#endregion
|
|
1933
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/shallow-routing.ts
|
|
1934
|
+
const NAVIGATION_MODULE = "$app/navigation";
|
|
1935
|
+
const SHALLOW_METHODS = /* @__PURE__ */ new Map([["pushState", false], ["replaceState", true]]);
|
|
1936
|
+
const NAVIGATION_HOOKS = /* @__PURE__ */ new Set([
|
|
1937
|
+
"beforeNavigate",
|
|
1938
|
+
"afterNavigate",
|
|
1939
|
+
"onNavigate"
|
|
1940
|
+
]);
|
|
1941
|
+
var shallow_routing_default = defineMigrationTask({
|
|
1942
|
+
id: "shallow-routing",
|
|
1943
|
+
description: "Migrate shallow routing to goto()",
|
|
1944
|
+
run: ({ sv, language }) => {
|
|
1945
|
+
sv.files({
|
|
1946
|
+
include: "**/*.{js,ts,svelte}",
|
|
1947
|
+
where: (content) => content.includes(NAVIGATION_MODULE)
|
|
1948
|
+
}, (content, file) => {
|
|
1949
|
+
if (file.endsWith(".svelte")) return transforms.svelteScript({ language }, ({ ast }) => {
|
|
1950
|
+
if (!migrateShallowRouting(ast.instance.content)) return false;
|
|
1951
|
+
})(content);
|
|
1952
|
+
return transforms.script(({ ast }) => {
|
|
1953
|
+
if (!migrateShallowRouting(ast)) return false;
|
|
1954
|
+
})(content);
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
function migrateShallowRouting(ast) {
|
|
1959
|
+
const methods = /* @__PURE__ */ new Map();
|
|
1960
|
+
const hooks = /* @__PURE__ */ new Set();
|
|
1961
|
+
const oldSpecifiers = [];
|
|
1962
|
+
const navigationImports = [];
|
|
1963
|
+
let gotoLocal;
|
|
1964
|
+
for (const binding of js.imports.bindings(ast, { from: NAVIGATION_MODULE })) {
|
|
1965
|
+
if (binding.imported === "goto") gotoLocal = binding.local;
|
|
1966
|
+
if (NAVIGATION_HOOKS.has(binding.imported)) hooks.add(binding.local);
|
|
1967
|
+
const replace = SHALLOW_METHODS.get(binding.imported);
|
|
1968
|
+
if (replace !== void 0) methods.set(binding.local, replace);
|
|
1969
|
+
if (replace !== void 0 && binding.kind === "named") oldSpecifiers.push(binding.specifier);
|
|
1970
|
+
}
|
|
1971
|
+
for (const statement of ast.body) {
|
|
1972
|
+
if (statement.type !== "ImportDeclaration" || statement.source.value !== NAVIGATION_MODULE) continue;
|
|
1973
|
+
navigationImports.push(statement);
|
|
1974
|
+
}
|
|
1975
|
+
if (methods.size === 0 && hooks.size === 0) return false;
|
|
1976
|
+
const { unmigratable, migratable } = scanMethodCalls(ast, methods);
|
|
1977
|
+
const removable = oldSpecifiers.filter((specifier) => !unmigratable.has(specifier.local.name));
|
|
1978
|
+
let changed = false;
|
|
1979
|
+
if (!gotoLocal && migratable) {
|
|
1980
|
+
const specifier = removable.shift();
|
|
1981
|
+
if (specifier) {
|
|
1982
|
+
const wasUnaliased = specifier.local.name === specifier.imported.name;
|
|
1983
|
+
gotoLocal = specifier.local.name;
|
|
1984
|
+
if (wasUnaliased && js.identifiers.freeName([ast], "goto") === "goto") {
|
|
1985
|
+
specifier.imported.name = "goto";
|
|
1986
|
+
specifier.local.name = "goto";
|
|
1987
|
+
gotoLocal = "goto";
|
|
1988
|
+
} else specifier.imported = js.variables.createIdentifier("goto");
|
|
1989
|
+
} else {
|
|
1990
|
+
gotoLocal = js.identifiers.freeName([ast], "goto");
|
|
1991
|
+
navigationImports.find((navigationImport) => navigationImport.specifiers.some((s) => oldSpecifiers.includes(s))).specifiers.push({
|
|
1992
|
+
type: "ImportSpecifier",
|
|
1993
|
+
imported: js.variables.createIdentifier("goto"),
|
|
1994
|
+
local: js.variables.createIdentifier(gotoLocal)
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
changed = true;
|
|
1998
|
+
}
|
|
1999
|
+
if (removable.length > 0) {
|
|
2000
|
+
const emptiedImports = /* @__PURE__ */ new Set();
|
|
2001
|
+
for (const declaration of navigationImports) {
|
|
2002
|
+
if (declaration.specifiers.length === 0) continue;
|
|
2003
|
+
declaration.specifiers = declaration.specifiers.filter((specifier) => !removable.includes(specifier));
|
|
2004
|
+
if (declaration.specifiers.length === 0) emptiedImports.add(declaration);
|
|
2005
|
+
}
|
|
2006
|
+
ast.body = ast.body.filter((statement) => statement.type !== "ImportDeclaration" || !emptiedImports.has(statement));
|
|
2007
|
+
changed = true;
|
|
2008
|
+
}
|
|
2009
|
+
Walker.walk(ast, null, { CallExpression(node, { next }) {
|
|
2010
|
+
if (node.callee.type !== "Identifier") {
|
|
2011
|
+
next();
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
const replace = methods.get(node.callee.name);
|
|
2015
|
+
if (replace !== void 0 && gotoLocal && !node.arguments.some((argument) => argument.type === "SpreadElement")) {
|
|
2016
|
+
const state = node.arguments[1];
|
|
2017
|
+
node.callee.name = gotoLocal;
|
|
2018
|
+
node.arguments = [...node.arguments[0] ? [node.arguments[0]] : [], gotoOptions(replace, state)];
|
|
2019
|
+
changed = true;
|
|
2020
|
+
}
|
|
2021
|
+
if (hooks.has(node.callee.name) && migrateNavigationHook(node)) changed = true;
|
|
2022
|
+
next();
|
|
2023
|
+
} });
|
|
2024
|
+
return changed;
|
|
2025
|
+
}
|
|
2026
|
+
function scanMethodCalls(ast, methods) {
|
|
2027
|
+
const unmigratable = /* @__PURE__ */ new Set();
|
|
2028
|
+
let migratable = false;
|
|
2029
|
+
Walker.walk(ast, null, { CallExpression(node, { next }) {
|
|
2030
|
+
if (node.callee.type === "Identifier" && methods.has(node.callee.name)) if (node.arguments.some((argument) => argument.type === "SpreadElement")) unmigratable.add(node.callee.name);
|
|
2031
|
+
else migratable = true;
|
|
2032
|
+
next();
|
|
2033
|
+
} });
|
|
2034
|
+
return {
|
|
2035
|
+
unmigratable,
|
|
2036
|
+
migratable
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
function gotoOptions(replace, state) {
|
|
2040
|
+
return js.object.create({
|
|
2041
|
+
shallow: js.common.createLiteral(true),
|
|
2042
|
+
replace: replace ? js.common.createLiteral(true) : void 0,
|
|
2043
|
+
state
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
function migrateNavigationHook(call) {
|
|
2047
|
+
const callback = call.arguments[0];
|
|
2048
|
+
if (callback?.type !== "ArrowFunctionExpression" && callback?.type !== "FunctionExpression") return false;
|
|
2049
|
+
const shallow = shallowTest(callback);
|
|
2050
|
+
if (!shallow) return false;
|
|
2051
|
+
const guard = {
|
|
2052
|
+
type: "IfStatement",
|
|
2053
|
+
test: shallow,
|
|
2054
|
+
consequent: {
|
|
2055
|
+
type: "ReturnStatement",
|
|
2056
|
+
argument: null
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
if (callback.body.type === "BlockStatement") {
|
|
2060
|
+
if (startsWithShallowGuard(callback.body, shallow)) return false;
|
|
2061
|
+
callback.body.body.unshift(guard);
|
|
2062
|
+
} else callback.body = {
|
|
2063
|
+
type: "BlockStatement",
|
|
2064
|
+
body: [guard, {
|
|
2065
|
+
type: "ReturnStatement",
|
|
2066
|
+
argument: callback.body
|
|
2067
|
+
}]
|
|
2068
|
+
};
|
|
2069
|
+
return true;
|
|
2070
|
+
}
|
|
2071
|
+
function shallowTest(callback) {
|
|
2072
|
+
const parameter = callback.params[0];
|
|
2073
|
+
if (!parameter) {
|
|
2074
|
+
callback.params.push({
|
|
2075
|
+
type: "ObjectPattern",
|
|
2076
|
+
properties: [patternProperty("shallow")]
|
|
2077
|
+
});
|
|
2078
|
+
return js.variables.createIdentifier("shallow");
|
|
2079
|
+
}
|
|
2080
|
+
if (parameter.type === "Identifier") return member(parameter.name, "shallow");
|
|
2081
|
+
if (parameter.type === "AssignmentPattern" && parameter.left.type === "Identifier") return member(parameter.left.name, "shallow");
|
|
2082
|
+
if (parameter.type !== "ObjectPattern") return void 0;
|
|
2083
|
+
for (const entry of parameter.properties) {
|
|
2084
|
+
if (entry.type !== "Property") continue;
|
|
2085
|
+
const key = entry.key;
|
|
2086
|
+
if ((key.type !== "Identifier" || key.name !== "shallow") && (key.type !== "Literal" || key.value !== "shallow")) continue;
|
|
2087
|
+
if (entry.value.type === "Identifier") return js.variables.createIdentifier(entry.value.name);
|
|
2088
|
+
if (entry.value.type === "AssignmentPattern" && entry.value.left.type === "Identifier") return js.variables.createIdentifier(entry.value.left.name);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const restIndex = parameter.properties.findIndex((entry) => entry.type === "RestElement");
|
|
2092
|
+
const shallowProperty = patternProperty("shallow");
|
|
2093
|
+
if (restIndex === -1) parameter.properties.push(shallowProperty);
|
|
2094
|
+
else parameter.properties.splice(restIndex, 0, shallowProperty);
|
|
2095
|
+
return js.variables.createIdentifier("shallow");
|
|
2096
|
+
}
|
|
2097
|
+
function startsWithShallowGuard(body, shallow) {
|
|
2098
|
+
const first = body.body[0];
|
|
2099
|
+
return first?.type === "IfStatement" && first.consequent.type === "ReturnStatement" && first.consequent.argument === null && sameExpression(first.test, shallow);
|
|
2100
|
+
}
|
|
2101
|
+
function sameExpression(left, right) {
|
|
2102
|
+
if (left.type === "Identifier" && right.type === "Identifier") return left.name === right.name;
|
|
2103
|
+
return left.type === "MemberExpression" && right.type === "MemberExpression" && left.object.type === "Identifier" && right.object.type === "Identifier" && left.object.name === right.object.name && left.property.type === "Identifier" && right.property.type === "Identifier" && left.property.name === right.property.name;
|
|
2104
|
+
}
|
|
2105
|
+
function patternProperty(name) {
|
|
2106
|
+
const value = js.variables.createIdentifier(name);
|
|
2107
|
+
return {
|
|
2108
|
+
type: "Property",
|
|
2109
|
+
key: js.variables.createIdentifier(name),
|
|
2110
|
+
value,
|
|
2111
|
+
kind: "init",
|
|
2112
|
+
method: false,
|
|
2113
|
+
shorthand: true,
|
|
2114
|
+
computed: false
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
function member(object, property) {
|
|
2118
|
+
return {
|
|
2119
|
+
type: "MemberExpression",
|
|
2120
|
+
object: js.variables.createIdentifier(object),
|
|
2121
|
+
property: js.variables.createIdentifier(property),
|
|
2122
|
+
computed: false,
|
|
2123
|
+
optional: false
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
//#endregion
|
|
2127
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/svelte-config.ts
|
|
2128
|
+
const SVELTE_CONFIG_IMPORT = /(^|\/)svelte\.config(\.[mc]?[jt]s)?$/;
|
|
2129
|
+
var svelte_config_default = defineMigrationTask({
|
|
2130
|
+
id: "svelte-config",
|
|
2131
|
+
description: "Migrate svelte.config.* to vite.config.*",
|
|
2132
|
+
run: ({ sv, cwd }) => {
|
|
2133
|
+
const configSource = svelteConfig.find(cwd);
|
|
2134
|
+
if (!configSource) return;
|
|
2135
|
+
if (configSource.kind === "vite") return;
|
|
2136
|
+
const originalConfigObject = svelteConfig.read(cwd);
|
|
2137
|
+
if (!originalConfigObject) return;
|
|
2138
|
+
fs.unlinkSync(path.join(cwd, configSource.path));
|
|
2139
|
+
svelteConfig.edit({
|
|
2140
|
+
sv,
|
|
45
2141
|
cwd
|
|
2142
|
+
}, ({ ast, override, comments }) => {
|
|
2143
|
+
const configDeclaration = originalConfigObject.ast.body.find((node) => node.type === "VariableDeclaration" && node.declarations.some((d) => d.init === originalConfigObject.config));
|
|
2144
|
+
const originalImports = [];
|
|
2145
|
+
const originalStatements = [];
|
|
2146
|
+
for (const node of originalConfigObject.ast.body) if (node.type === "ImportDeclaration") originalImports.push(node);
|
|
2147
|
+
else if (node.type !== "ExportDefaultDeclaration" && node !== configDeclaration) originalStatements.push(node);
|
|
2148
|
+
ast.body.unshift(...originalImports);
|
|
2149
|
+
const exportIndex = ast.body.findIndex((node) => node.type === "ExportDefaultDeclaration");
|
|
2150
|
+
ast.body.splice(exportIndex, 0, ...originalStatements);
|
|
2151
|
+
const attachments = collectComments(originalConfigObject.ast, originalConfigObject.comments);
|
|
2152
|
+
const propComments = /* @__PURE__ */ new Map();
|
|
2153
|
+
const newConfigProperties = [...originalConfigObject.config.properties, ...originalConfigObject.kit.properties];
|
|
2154
|
+
const keyedConfig = {};
|
|
2155
|
+
let trustsAllOrigins = false;
|
|
2156
|
+
for (const prop of newConfigProperties) {
|
|
2157
|
+
if (prop.type !== "Property") continue;
|
|
2158
|
+
if (prop.key.type !== "Identifier") continue;
|
|
2159
|
+
if (prop.key.name === "kit") continue;
|
|
2160
|
+
const propAttachment = attachments.get(prop);
|
|
2161
|
+
keyedConfig[prop.key.name] = prop.value;
|
|
2162
|
+
if (propAttachment) propComments.set(prop.key.name, propAttachment);
|
|
2163
|
+
}
|
|
2164
|
+
for (const [key, value] of Object.entries(keyedConfig)) {
|
|
2165
|
+
if (key === "experimental" && value.type === "ObjectExpression") {
|
|
2166
|
+
removeProperty(value, "handleRenderingErrors");
|
|
2167
|
+
removeProperty(value, "instrumentation");
|
|
2168
|
+
const index = value.properties.findIndex((prop) => prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "tracing");
|
|
2169
|
+
if (index !== -1) {
|
|
2170
|
+
keyedConfig.tracing = value.properties[index].value;
|
|
2171
|
+
value.properties.splice(index, 1);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
if (key === "prerender" && value.type === "ObjectExpression") {
|
|
2175
|
+
const originProp = value.properties.find((prop) => prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "origin");
|
|
2176
|
+
if (originProp) {
|
|
2177
|
+
keyedConfig.paths ??= {
|
|
2178
|
+
type: "ObjectExpression",
|
|
2179
|
+
properties: []
|
|
2180
|
+
};
|
|
2181
|
+
keyedConfig.paths.properties.push({
|
|
2182
|
+
type: "Property",
|
|
2183
|
+
key: {
|
|
2184
|
+
type: "Identifier",
|
|
2185
|
+
name: "origin"
|
|
2186
|
+
},
|
|
2187
|
+
value: originProp.value,
|
|
2188
|
+
kind: "init",
|
|
2189
|
+
method: false,
|
|
2190
|
+
shorthand: false,
|
|
2191
|
+
computed: false
|
|
2192
|
+
});
|
|
2193
|
+
removeProperty(value, "origin");
|
|
2194
|
+
if (value.properties.length === 0) delete keyedConfig.prerender;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
if (key === "csrf" && value.type === "ObjectExpression") {
|
|
2198
|
+
const checkOrigin = findDisabledCheckOrigin(value);
|
|
2199
|
+
if (checkOrigin) {
|
|
2200
|
+
checkOrigin.key.name = "trustedOrigins";
|
|
2201
|
+
checkOrigin.value = {
|
|
2202
|
+
type: "ArrayExpression",
|
|
2203
|
+
elements: [{
|
|
2204
|
+
type: "Literal",
|
|
2205
|
+
value: "*"
|
|
2206
|
+
}]
|
|
2207
|
+
};
|
|
2208
|
+
trustsAllOrigins = true;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
delete keyedConfig.preloadStrategy;
|
|
2213
|
+
override(keyedConfig);
|
|
2214
|
+
if (trustsAllOrigins) Walker.walk(ast, null, { Property(node, { next }) {
|
|
2215
|
+
if (node.key.type === "Identifier" && node.key.name === "trustedOrigins") addMigrationTask("trusting all origins with '*' is generally not recommended, see https://svelte.dev/docs/kit/configuration#csrf", {
|
|
2216
|
+
comments,
|
|
2217
|
+
node
|
|
2218
|
+
});
|
|
2219
|
+
next();
|
|
2220
|
+
} });
|
|
2221
|
+
const apply = (node, record) => {
|
|
2222
|
+
for (const comment of record?.leading ?? []) comments.add(node, comment);
|
|
2223
|
+
for (const comment of record?.trailing ?? []) comments.add(node, comment, { position: "trailing" });
|
|
2224
|
+
};
|
|
2225
|
+
let rootConfig;
|
|
2226
|
+
forEachNode(ast, (node) => {
|
|
2227
|
+
apply(node, attachments.get(node));
|
|
2228
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "sveltekit" && node.arguments[0]?.type === "ObjectExpression") rootConfig = node.arguments[0];
|
|
2229
|
+
});
|
|
2230
|
+
for (const prop of rootConfig?.properties ?? []) if (prop.type === "Property" && prop.key.type === "Identifier") apply(prop, propComments.get(prop.key.name));
|
|
46
2231
|
});
|
|
47
|
-
|
|
48
|
-
|
|
2232
|
+
let addedConfigLoader = false;
|
|
2233
|
+
sv.files({
|
|
2234
|
+
include: "**/*.{js,ts,mjs,mts,cjs,cts}",
|
|
2235
|
+
where: (content) => content.includes("svelte.config")
|
|
2236
|
+
}, (content, filePath) => {
|
|
2237
|
+
if (/(^|\/)eslint\.config\.[mc]?[jt]s$/.test(filePath)) {
|
|
2238
|
+
const { ast } = parse.script(content);
|
|
2239
|
+
const configImport = js.imports.findAll(ast, { from: SVELTE_CONFIG_IMPORT }).find((item) => item.kind === "static");
|
|
2240
|
+
if (configImport?.node.loc) {
|
|
2241
|
+
const start = offsetAt(content, configImport.node.loc.start);
|
|
2242
|
+
const end = offsetAt(content, configImport.node.loc.end);
|
|
2243
|
+
const replacement = "import { loadConfig } from '@sveltejs/load-config';\n\nconst svelteConfig = (await loadConfig('./', { traverse: false }))?.config;";
|
|
2244
|
+
addedConfigLoader = true;
|
|
2245
|
+
return content.slice(0, start) + replacement + content.slice(end);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
return transforms.script(({ ast, comments, js }) => {
|
|
2249
|
+
const found = js.imports.findAll(ast, { from: SVELTE_CONFIG_IMPORT });
|
|
2250
|
+
if (found.length === 0) return false;
|
|
2251
|
+
for (const imp of found) addMigrationTask("svelte.config was removed; switch to `import { loadConfig } from '@sveltejs/load-config'` to read your config", {
|
|
2252
|
+
comments,
|
|
2253
|
+
node: imp.node
|
|
2254
|
+
});
|
|
2255
|
+
})(content);
|
|
2256
|
+
});
|
|
2257
|
+
if (addedConfigLoader) sv.devDependency("@sveltejs/load-config", "^0.2.2");
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
const posKey = (p) => p.line * 1e6 + p.column;
|
|
2261
|
+
function offsetAt(content, pos) {
|
|
2262
|
+
let offset = 0;
|
|
2263
|
+
for (let line = 1; line < pos.line; line++) offset = content.indexOf("\n", offset) + 1;
|
|
2264
|
+
return offset + pos.column;
|
|
2265
|
+
}
|
|
2266
|
+
/** Visits every AST node (objects with a `type`) reachable from `root`, including `root` itself. */
|
|
2267
|
+
function forEachNode(root, visit) {
|
|
2268
|
+
if (!root || typeof root !== "object") return;
|
|
2269
|
+
if (Array.isArray(root)) {
|
|
2270
|
+
for (const item of root) forEachNode(item, visit);
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
const node = root;
|
|
2274
|
+
if (typeof node.type === "string") visit(node);
|
|
2275
|
+
for (const key in node) {
|
|
2276
|
+
if (key === "loc" || key === "range" || key === "parent") continue;
|
|
2277
|
+
forEachNode(node[key], visit);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
/** Removes a statically named property from an object literal. */
|
|
2281
|
+
function removeProperty(value, name) {
|
|
2282
|
+
const index = value.properties.findIndex((prop) => prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === name);
|
|
2283
|
+
if (index !== -1) value.properties.splice(index, 1);
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Maps each source comment to the node it documents, returned keyed by node identity. A comment is
|
|
2287
|
+
* the trailing comment of the node it sits behind on the same line, otherwise the leading comment of
|
|
2288
|
+
* the next node that starts after it. Nodes are sorted start-ascending then end-descending so an
|
|
2289
|
+
* outer node (e.g. a `Property`) wins over a node it encloses that starts at the same spot (its key).
|
|
2290
|
+
*/
|
|
2291
|
+
function collectComments(ast, source) {
|
|
2292
|
+
const nodes = [];
|
|
2293
|
+
forEachNode(ast, (node) => {
|
|
2294
|
+
if (node.loc) nodes.push({
|
|
2295
|
+
node,
|
|
2296
|
+
start: posKey(node.loc.start),
|
|
2297
|
+
end: posKey(node.loc.end)
|
|
2298
|
+
});
|
|
2299
|
+
});
|
|
2300
|
+
nodes.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
2301
|
+
const topLevel = new Set(ast.body);
|
|
2302
|
+
const map = /* @__PURE__ */ new Map();
|
|
2303
|
+
const recordFor = (node) => {
|
|
2304
|
+
let record = map.get(node);
|
|
2305
|
+
if (!record) map.set(node, record = {
|
|
2306
|
+
leading: [],
|
|
2307
|
+
trailing: []
|
|
2308
|
+
});
|
|
2309
|
+
return record;
|
|
2310
|
+
};
|
|
2311
|
+
for (const comment of source.list()) {
|
|
2312
|
+
const start = posKey(comment.loc.start);
|
|
2313
|
+
const end = posKey(comment.loc.end);
|
|
2314
|
+
let prev;
|
|
2315
|
+
let next;
|
|
2316
|
+
for (const item of nodes) {
|
|
2317
|
+
if (item.end <= start && (!prev || item.end > prev.end)) prev = item;
|
|
2318
|
+
if (item.start >= end) {
|
|
2319
|
+
next = item;
|
|
2320
|
+
break;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
if (prev && comment.loc.start.line === prev.node.loc.end.line) {
|
|
2324
|
+
const safeInline = comment.type === "Block" || topLevel.has(prev.node);
|
|
2325
|
+
const value = safeInline ? comment.value : ` ${comment.value.trim()} `;
|
|
2326
|
+
recordFor(prev.node).trailing.push({
|
|
2327
|
+
type: safeInline ? comment.type : "Block",
|
|
2328
|
+
value
|
|
2329
|
+
});
|
|
2330
|
+
} else if (next) recordFor(next.node).leading.push({
|
|
2331
|
+
type: comment.type,
|
|
2332
|
+
value: comment.value
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
return map;
|
|
2336
|
+
}
|
|
2337
|
+
/** Returns the `checkOrigin: false` property of a `csrf` object, if present. */
|
|
2338
|
+
function findDisabledCheckOrigin(value) {
|
|
2339
|
+
return value.properties.find((p) => p.type === "Property" && p.key.type === "Identifier" && p.key.name === "checkOrigin" && p.value.type === "Literal" && p.value.value === false);
|
|
2340
|
+
}
|
|
2341
|
+
//#endregion
|
|
2342
|
+
//#region src/migrate/migrations/sveltekit-3/tasks/tsconfig.ts
|
|
2343
|
+
const GENERATED_CONFIG = /(^|\/)\.svelte-kit\/tsconfig\.json$/;
|
|
2344
|
+
const LEADING_PARENT = /^\.\.\//;
|
|
2345
|
+
/** The config SvelteKit now generates into `node_modules`, replacing `.svelte-kit/tsconfig.json`. */
|
|
2346
|
+
const PARENT_CONFIG = "$app/tsconfig";
|
|
2347
|
+
/**
|
|
2348
|
+
* Options the generated config already sets. A local copy of the same value is noise, but a
|
|
2349
|
+
* different value is a deliberate override and stays.
|
|
2350
|
+
*/
|
|
2351
|
+
const INHERITED_OPTIONS = {
|
|
2352
|
+
allowImportingTsExtensions: true,
|
|
2353
|
+
allowJs: true,
|
|
2354
|
+
checkJs: true,
|
|
2355
|
+
esModuleInterop: true,
|
|
2356
|
+
forceConsistentCasingInFileNames: true,
|
|
2357
|
+
isolatedModules: true,
|
|
2358
|
+
module: "esnext",
|
|
2359
|
+
moduleDetection: "force",
|
|
2360
|
+
moduleResolution: "bundler",
|
|
2361
|
+
noEmit: true,
|
|
2362
|
+
resolveJsonModule: true,
|
|
2363
|
+
skipLibCheck: true,
|
|
2364
|
+
target: "esnext",
|
|
2365
|
+
verbatimModuleSyntax: true
|
|
2366
|
+
};
|
|
2367
|
+
var tsconfig_default = defineMigrationTask({
|
|
2368
|
+
id: "tsconfig",
|
|
2369
|
+
description: "Extend the generated $app/tsconfig instead of .svelte-kit/tsconfig.json",
|
|
2370
|
+
run: ({ sv, cwd, file }) => {
|
|
2371
|
+
if (!file.typeConfig) return;
|
|
2372
|
+
const moved = dropTypescriptOption(sv, cwd);
|
|
2373
|
+
sv.file(path.relative(cwd, file.typeConfig), transforms.json(({ data }) => {
|
|
2374
|
+
const extended = Array.isArray(data.extends) ? [...data.extends] : [data.extends];
|
|
2375
|
+
const index = extended.findIndex((entry) => entry && GENERATED_CONFIG.test(entry));
|
|
2376
|
+
if (index === -1 && !extended.includes(PARENT_CONFIG)) return false;
|
|
2377
|
+
if (index !== -1) {
|
|
2378
|
+
extended[index] = PARENT_CONFIG;
|
|
2379
|
+
data.extends = Array.isArray(data.extends) ? extended : PARENT_CONFIG;
|
|
2380
|
+
}
|
|
2381
|
+
const include = data.include ??= ["src"];
|
|
2382
|
+
include.push(...moved.filter((entry) => !include.includes(entry)));
|
|
2383
|
+
if (data.compilerOptions?.types) {
|
|
2384
|
+
const types = data.compilerOptions.types;
|
|
2385
|
+
if (!types.includes("$app/types")) types.push("$app/types");
|
|
2386
|
+
}
|
|
2387
|
+
for (const [key, value] of Object.entries(data.compilerOptions ?? {})) if (INHERITED_OPTIONS[key] === value) delete data.compilerOptions[key];
|
|
2388
|
+
}));
|
|
2389
|
+
}
|
|
2390
|
+
});
|
|
2391
|
+
/**
|
|
2392
|
+
* Removes the deprecated `typescript.config` option, returning the paths its hook pushed onto
|
|
2393
|
+
* `include` so they can move to the project's own config. Hooks that do anything else are left in
|
|
2394
|
+
* place and flagged, since their intent can't be expressed as `include` entries.
|
|
2395
|
+
*/
|
|
2396
|
+
function dropTypescriptOption(sv, cwd) {
|
|
2397
|
+
if (!svelteConfig.find(cwd)) return [];
|
|
2398
|
+
const moved = [];
|
|
2399
|
+
svelteConfig.edit({
|
|
2400
|
+
sv,
|
|
2401
|
+
cwd
|
|
2402
|
+
}, ({ ast, comments }) => {
|
|
2403
|
+
const found = findProperty(ast, "typescript");
|
|
2404
|
+
if (!found) return false;
|
|
2405
|
+
const pushed = includePushes(found.property.value);
|
|
2406
|
+
if (!pushed) {
|
|
2407
|
+
addMigrationTask("`typescript.config` is deprecated; configure TypeScript in tsconfig.json directly", {
|
|
2408
|
+
comments,
|
|
2409
|
+
node: found.property
|
|
2410
|
+
});
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
moved.push(...pushed.map((entry) => entry.replace(LEADING_PARENT, "")));
|
|
2414
|
+
found.remove();
|
|
2415
|
+
});
|
|
2416
|
+
return moved;
|
|
2417
|
+
}
|
|
2418
|
+
/** Finds a property by name anywhere in the config, wherever the config keeps it. */
|
|
2419
|
+
function findProperty(ast, name) {
|
|
2420
|
+
let found;
|
|
2421
|
+
Walker.walk(ast, null, { ObjectExpression(node, { next }) {
|
|
2422
|
+
const property = node.properties.find((p) => p.type === "Property" && p.key.type === "Identifier" && p.key.name === name);
|
|
2423
|
+
if (property) found ??= {
|
|
2424
|
+
property,
|
|
2425
|
+
remove: () => node.properties.splice(node.properties.indexOf(property), 1)
|
|
2426
|
+
};
|
|
2427
|
+
next();
|
|
2428
|
+
} });
|
|
2429
|
+
return found;
|
|
2430
|
+
}
|
|
2431
|
+
/** The literal paths a `typescript.config` hook pushes onto `include`, if that's all it does. */
|
|
2432
|
+
function includePushes(option) {
|
|
2433
|
+
if (option.type !== "ObjectExpression" || option.properties.length !== 1) return void 0;
|
|
2434
|
+
const [config] = option.properties;
|
|
2435
|
+
if (config.type !== "Property" || config.key.type !== "Identifier") return void 0;
|
|
2436
|
+
if (config.key.name !== "config") return void 0;
|
|
2437
|
+
const fn = config.value;
|
|
2438
|
+
if (fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression") return void 0;
|
|
2439
|
+
if (fn.body.type !== "BlockStatement") return void 0;
|
|
2440
|
+
const paths = [];
|
|
2441
|
+
for (const statement of fn.body.body) {
|
|
2442
|
+
if (statement.type !== "ExpressionStatement") return void 0;
|
|
2443
|
+
const call = statement.expression;
|
|
2444
|
+
if (call.type !== "CallExpression" || !isIncludePush(call.callee)) return void 0;
|
|
2445
|
+
for (const argument of call.arguments) {
|
|
2446
|
+
if (argument.type !== "Literal" || typeof argument.value !== "string") return void 0;
|
|
2447
|
+
paths.push(argument.value);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
return paths;
|
|
2451
|
+
}
|
|
2452
|
+
const memberName = (node) => node.property.type === "Identifier" ? node.property.name : void 0;
|
|
2453
|
+
/** Matches the callee of `config.include.push(...)`. */
|
|
2454
|
+
function isIncludePush(callee) {
|
|
2455
|
+
return callee.type === "MemberExpression" && memberName(callee) === "push" && callee.object.type === "MemberExpression" && memberName(callee.object) === "include";
|
|
2456
|
+
}
|
|
2457
|
+
//#endregion
|
|
2458
|
+
//#region src/cli/migrate.ts
|
|
2459
|
+
const migrations = [
|
|
2460
|
+
defineMigration({
|
|
2461
|
+
id: "sveltekit-3",
|
|
2462
|
+
changelog: "https://next.svelte.dev/docs/kit/migrating-to-sveltekit-3",
|
|
2463
|
+
description: "A set of migrations for SvelteKit 3.0",
|
|
2464
|
+
setup: ({ pkg, requires }) => {
|
|
2465
|
+
const kitPackageName = "@sveltejs/kit";
|
|
2466
|
+
if (!pkg.devDependencies?.[kitPackageName]) throw new Error(`${color.command(kitPackageName)} is not a devDependency in package.json - this doesn't look like a SvelteKit project.\nPoint to one with ${color.command("--cwd <path>")}, or see ${color.command("sv migrate --help")}.`);
|
|
2467
|
+
const kitVersion = coerceVersion(pkg.devDependencies[kitPackageName]);
|
|
2468
|
+
if (kitVersion.major && kitVersion.major < 2) requires("sveltekit-2");
|
|
2469
|
+
},
|
|
2470
|
+
collect: ({ tasks }) => {
|
|
2471
|
+
tasks.add(package_json_default, { prerequisite: true });
|
|
2472
|
+
tasks.add(tsconfig_default, { prerequisite: true });
|
|
2473
|
+
tasks.add(svelte_config_default, { prerequisite: false });
|
|
2474
|
+
tasks.add(environment_default, { prerequisite: false });
|
|
2475
|
+
tasks.add(paths_default, { prerequisite: false });
|
|
2476
|
+
tasks.add(external_redirects_default, { prerequisite: false });
|
|
2477
|
+
tasks.add(shallow_routing_default, { prerequisite: false });
|
|
2478
|
+
tasks.add(params_default, { prerequisite: false });
|
|
2479
|
+
tasks.add(imports_default, { prerequisite: false });
|
|
2480
|
+
tasks.add(lib_alias_default, { prerequisite: false });
|
|
2481
|
+
tasks.add(app_state_default$1, { prerequisite: false });
|
|
2482
|
+
tasks.add(collect_migration_instructions_default, { prerequisite: false });
|
|
2483
|
+
}
|
|
2484
|
+
}),
|
|
2485
|
+
app_state_default,
|
|
2486
|
+
...legacyMigrations
|
|
2487
|
+
];
|
|
2488
|
+
const MigrationScheme = optional(picklist(migrations.map((m) => m.id)));
|
|
2489
|
+
function normalizeTasksOption(tasks) {
|
|
2490
|
+
return tasks === true ? [] : tasks;
|
|
2491
|
+
}
|
|
2492
|
+
const OptionsSchema = strictObject({
|
|
2493
|
+
cwd: optional(string(), "./"),
|
|
2494
|
+
files: optional(string()),
|
|
2495
|
+
gitCheck: boolean(),
|
|
2496
|
+
tasks: pipe(optional(union([array(string()), literal(true)])), transform(normalizeTasksOption)),
|
|
2497
|
+
confirm: optional(boolean(), false),
|
|
2498
|
+
install: optional(union([boolean(), picklist(AGENT_NAMES)]), true)
|
|
2499
|
+
});
|
|
2500
|
+
const migrate = new Command("migrate").description("a CLI for migrating Svelte(Kit) codebases").argument("[migration]", `migration to run`).option("--cwd <path>", "working directory to run the migration in").option("--files <glob>", "only run the migration on a subset of files matching the provided glob pattern").option("--no-git-check", "even if some files are dirty, no prompt will be shown").option("--tasks [task...]", "migration tasks to run. Omit list of tasks to show available tasks. Use `--task all` to run all migration tasks.").option("--confirm", "skip the final confirmation prompt").option("--no-install", "skip installing dependencies").addOption(installOption).action((migrationName, options) => {
|
|
2501
|
+
if (hasInstallConflict(V.argv)) {
|
|
2502
|
+
errorAndExit(`The ${color.command("--install")} and ${color.command("--no-install")} options cannot be used together.`);
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
let verifiedMigrationName = parse$1(MigrationScheme, migrationName);
|
|
2506
|
+
const verifiedOptions = parse$1(OptionsSchema, options);
|
|
2507
|
+
runCommand(async () => {
|
|
2508
|
+
const pkg = ensureValidWorkspace(verifiedOptions.cwd);
|
|
2509
|
+
if (!pkg) return;
|
|
2510
|
+
if (verifiedOptions.tasks?.length !== 0) await runAndValidateVerifications([...verifyCleanWorkingDirectory(options.cwd, options.gitCheck)]);
|
|
2511
|
+
if (!verifiedMigrationName) verifiedMigrationName = await Ee({
|
|
2512
|
+
message: "Select a migration to run",
|
|
2513
|
+
options: migrations.map((m) => ({
|
|
2514
|
+
value: m.id,
|
|
2515
|
+
label: m.id,
|
|
2516
|
+
hint: m.description
|
|
2517
|
+
}))
|
|
2518
|
+
});
|
|
2519
|
+
if (R(verifiedMigrationName)) {
|
|
2520
|
+
ge("Operation cancelled.");
|
|
2521
|
+
V.exit(1);
|
|
2522
|
+
}
|
|
2523
|
+
const migration = migrations.find((m) => m.id === verifiedMigrationName);
|
|
2524
|
+
if (!migration) {
|
|
2525
|
+
errorAndExit(`Migration ${verifiedMigrationName} not found`);
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
const legacyMigration = migration.legacy ?? false;
|
|
2529
|
+
if (legacyMigration && verifiedOptions.tasks !== void 0) {
|
|
2530
|
+
errorAndExit(`The migration ${migration.id} does not support task selection.`);
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
if (verifiedOptions.tasks?.length === 0) {
|
|
2534
|
+
const allTasks = await collectMigrationTasks(migration, verifiedOptions.cwd);
|
|
2535
|
+
if (allTasks.length === 0) {
|
|
2536
|
+
errorAndExit(`Migration "${migration.id}" did not return any tasks.`);
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
Ce(formatAvailableTasks(allTasks), `Available tasks for ${migration.id}`, { format: (line) => line });
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
const tasks = await determineTasks(migration, verifiedOptions, pkg);
|
|
2543
|
+
if (!tasks) return;
|
|
2544
|
+
resetMigrationTaskCount();
|
|
2545
|
+
const modifiedFiles = await applyTasks(verifiedOptions, tasks, legacyMigration);
|
|
2546
|
+
if (legacyMigration) return;
|
|
2547
|
+
const workspace = await createWorkspace({ cwd: verifiedOptions.cwd });
|
|
2548
|
+
await formatFiles({
|
|
2549
|
+
cwd: workspace.cwd,
|
|
2550
|
+
packageManager: workspace.packageManager,
|
|
2551
|
+
filesToFormat: modifiedFiles.values().toArray(),
|
|
2552
|
+
strategy: "project-script-then-files-only"
|
|
2553
|
+
});
|
|
2554
|
+
const packageManager = verifiedOptions.install === false ? null : verifiedOptions.install === true ? await packageManagerPrompt(workspace.cwd) : verifiedOptions.install;
|
|
2555
|
+
if (packageManager) await installDependencies(packageManager, workspace.cwd);
|
|
2556
|
+
reportNextSteps();
|
|
2557
|
+
});
|
|
2558
|
+
});
|
|
2559
|
+
function ensureValidWorkspace(cwd) {
|
|
2560
|
+
try {
|
|
2561
|
+
return loadPackageJson(cwd).data;
|
|
2562
|
+
} catch {
|
|
2563
|
+
errorAndExit(`No package.json found in ${path.resolve(cwd)}.\nPoint to a project with ${color.command("--cwd <path>")}, or see ${color.command("sv migrate --help")}.`);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
async function determineTasks(migration, options, pkg) {
|
|
2567
|
+
const requiredMigrations = [];
|
|
2568
|
+
const setupOptions = {
|
|
2569
|
+
pkg,
|
|
2570
|
+
cwd: options.cwd,
|
|
2571
|
+
requires: (migrationId) => {
|
|
2572
|
+
requiredMigrations.push(migrationId);
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
try {
|
|
2576
|
+
await migration.setup(setupOptions);
|
|
2577
|
+
} catch (err) {
|
|
2578
|
+
errorAndExit(err instanceof Error ? err.message : String(err));
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
if (requiredMigrations.length > 0) {
|
|
2582
|
+
errorAndExit(`The migration ${migration.id} requires the following migrations to be run first: ${requiredMigrations.join(", ")}`);
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
const allTasks = await collectMigrationTasks(migration, options.cwd);
|
|
2586
|
+
if (allTasks.length === 0) {
|
|
2587
|
+
errorAndExit(`Migration "${migration.id}" did not return any tasks to run.`);
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
const prerequisiteTasks = allTasks.filter((t) => t.prerequisite);
|
|
2591
|
+
const selectableTasks = allTasks.filter((t) => !t.prerequisite);
|
|
2592
|
+
if (!options.tasks?.length && selectableTasks.length > 0) {
|
|
2593
|
+
const workflow = [];
|
|
2594
|
+
if (migration.changelog) workflow.push("Make sure to read the changelog for this migration before running it:", color.website(migration.changelog), "");
|
|
2595
|
+
workflow.push(`To make migration changes easier to review, run ${color.command("one selectable task at a time")} and ${color.command("commit after each run")}.`, "Tasks are intended to create focused commits, not independent migration paths.", `Most projects need ${color.warning("all applicable tasks")} before they work correctly.`);
|
|
2596
|
+
Ce(workflow.join("\n"), "Recommended workflow", { format: (line) => line });
|
|
2597
|
+
}
|
|
2598
|
+
const tasksToRun = [...prerequisiteTasks];
|
|
2599
|
+
if (options.tasks) tasksToRun.push(...selectTasksFromArgs(options.tasks, selectableTasks));
|
|
2600
|
+
else if (selectableTasks.length > 0) {
|
|
2601
|
+
const prerequisiteIds = prerequisiteTasks.map((t) => t.id).join(", ");
|
|
2602
|
+
const selectedTaskIds = await we({
|
|
2603
|
+
message: prerequisiteTasks.length ? `Select tasks to run; prerequisites always included: ${color.command(prerequisiteIds)}` : "Select the tasks to run",
|
|
2604
|
+
options: selectableTasks.map((t) => ({
|
|
2605
|
+
value: t.id,
|
|
2606
|
+
label: t.id,
|
|
2607
|
+
hint: t.description
|
|
2608
|
+
})),
|
|
2609
|
+
required: false
|
|
2610
|
+
});
|
|
2611
|
+
if (R(selectedTaskIds)) {
|
|
2612
|
+
ge("Operation cancelled.");
|
|
2613
|
+
V.exit(1);
|
|
2614
|
+
}
|
|
2615
|
+
const selectedTasks = selectableTasks.filter((t) => selectedTaskIds.includes(t.id));
|
|
2616
|
+
tasksToRun.push(...selectedTasks);
|
|
2617
|
+
}
|
|
2618
|
+
if (tasksToRun.length === 0) {
|
|
2619
|
+
errorAndExit("No tasks selected to run.");
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
Ce(`- ${tasksToRun.map(({ id, description }) => {
|
|
2623
|
+
return `${id} ${color.optional(`(${description})`)}`;
|
|
2624
|
+
}).join("\n- ")}`, "Migration steps", { format: (line) => line });
|
|
2625
|
+
if (!options.confirm) {
|
|
2626
|
+
const proceed = await le({
|
|
2627
|
+
message: "Do you want to proceed?",
|
|
2628
|
+
initialValue: false
|
|
2629
|
+
});
|
|
2630
|
+
if (R(proceed)) {
|
|
2631
|
+
ge("Operation cancelled.");
|
|
2632
|
+
V.exit(1);
|
|
2633
|
+
}
|
|
2634
|
+
if (!proceed) {
|
|
2635
|
+
errorAndExit("Migration cancelled by the user.");
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
return tasksToRun;
|
|
2640
|
+
}
|
|
2641
|
+
async function collectMigrationTasks(migration, cwd) {
|
|
2642
|
+
const allTasks = [];
|
|
2643
|
+
const collectOptions = {
|
|
2644
|
+
cwd,
|
|
2645
|
+
tasks: { add: (task, options) => {
|
|
2646
|
+
allTasks.push({
|
|
2647
|
+
...task,
|
|
2648
|
+
...options
|
|
2649
|
+
});
|
|
2650
|
+
} }
|
|
2651
|
+
};
|
|
2652
|
+
await migration.collect(collectOptions);
|
|
2653
|
+
return allTasks;
|
|
2654
|
+
}
|
|
2655
|
+
function formatAvailableTasks(tasks) {
|
|
2656
|
+
return tasks.map(({ id, description, prerequisite }) => `- ${id}${prerequisite ? color.warning(" (prerequisite)") : ""}: ${color.optional(description)}`).join("\n");
|
|
2657
|
+
}
|
|
2658
|
+
function selectTasksFromArgs(selectedTaskIds, selectableTasks) {
|
|
2659
|
+
if (selectedTaskIds.length > 1 && (selectedTaskIds.includes("all") || selectedTaskIds.includes("prerequisite"))) errorAndExit(`The ${color.command("--tasks")} values ${color.command("all")} and ${color.command("prerequisite")} cannot be combined with other tasks.`);
|
|
2660
|
+
if (selectedTaskIds[0] === "prerequisite") return [];
|
|
2661
|
+
if (selectedTaskIds[0] === "all") return selectableTasks;
|
|
2662
|
+
const invalidTasks = selectedTaskIds.filter((id) => !selectableTasks.some((t) => t.id === id));
|
|
2663
|
+
if (invalidTasks.length > 0) errorAndExit(`Unknown migration task${invalidTasks.length === 1 ? "" : "s"}: ${invalidTasks.map((id) => color.command(id)).join(", ")}\nAvailable tasks: ${selectableTasks.map((t) => color.command(t.id)).join(", ")}`);
|
|
2664
|
+
return selectableTasks.filter((task) => selectedTaskIds.includes(task.id));
|
|
2665
|
+
}
|
|
2666
|
+
/** Prints a summary of the `@migration-task` comments left for the user to resolve, if any. */
|
|
2667
|
+
function reportNextSteps() {
|
|
2668
|
+
const total = getMigrationTaskCount();
|
|
2669
|
+
if (total === 0) return;
|
|
2670
|
+
Ce([`${total} ${total === 1 ? "comment" : "comments"} to review.`, `Search for ${color.command(MIGRATION_TASK_MARKER)} to resolve ${total === 1 ? "it" : "them"}.`].join("\n"), "Next steps", { format: (line) => line });
|
|
2671
|
+
}
|
|
2672
|
+
function hasInstallConflict(argv) {
|
|
2673
|
+
return argv.some((arg) => arg === "--install" || arg.startsWith("--install=")) && argv.includes("--no-install");
|
|
2674
|
+
}
|
|
2675
|
+
async function applyTasks(options, tasks, legacyMigration) {
|
|
2676
|
+
let allModifiedFiles = /* @__PURE__ */ new Set();
|
|
2677
|
+
let allUnmodifiedFiles = /* @__PURE__ */ new Set();
|
|
2678
|
+
const { start, stop, message, error } = vt();
|
|
2679
|
+
if (!legacyMigration) start("Applying migration tasks...");
|
|
2680
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
2681
|
+
const task = tasks[i];
|
|
2682
|
+
if (!legacyMigration) message(`${i + 1}/${tasks.length}: ${task.id}`);
|
|
2683
|
+
try {
|
|
2684
|
+
const workspace = await createWorkspace({ cwd: options.cwd });
|
|
2685
|
+
const { sv, finalize } = prepareSvApi(workspace, {
|
|
2686
|
+
executeOutputPrefix: `${task.id}:`,
|
|
2687
|
+
filesFilter: options.files
|
|
2688
|
+
});
|
|
2689
|
+
await task.run({
|
|
2690
|
+
sv,
|
|
2691
|
+
...workspace
|
|
2692
|
+
});
|
|
2693
|
+
if (!legacyMigration) {
|
|
2694
|
+
const { modifiedFiles, unmodifiedFiles } = finalize();
|
|
2695
|
+
allModifiedFiles = allModifiedFiles.union(modifiedFiles);
|
|
2696
|
+
allUnmodifiedFiles = allUnmodifiedFiles.union(unmodifiedFiles);
|
|
2697
|
+
}
|
|
2698
|
+
} catch (err) {
|
|
2699
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2700
|
+
const message = `Task '${task.id}' failed: ${errorMessage}`;
|
|
2701
|
+
if (!legacyMigration) error(message);
|
|
2702
|
+
R$1.message();
|
|
2703
|
+
ge("Migration failed.");
|
|
2704
|
+
V.exit(1);
|
|
2705
|
+
}
|
|
49
2706
|
}
|
|
2707
|
+
if (!legacyMigration) stop("All tasks applied successfully!");
|
|
2708
|
+
if (allUnmodifiedFiles.size > 0) Ce(`The following files were modified by the migration,\nbut their content was not saved:\n- ${Array.from(allUnmodifiedFiles).join("\n- ")}`, "Unmodified files", { format: (line) => line });
|
|
2709
|
+
return allModifiedFiles;
|
|
50
2710
|
}
|
|
51
2711
|
//#endregion
|
|
52
2712
|
//#region bin.ts
|