Skip to content

Commit 4f32fb0

Browse files
feat: adapter-vercel o11y (#13679)
* Implement better build output using symlinks * snake_case * prettier * explanatory comment * generate symlinks when app uses function splitting * improve internal function naming and eliminate conflicts * changeset --------- Co-authored-by: Tobias Lins <[email protected]>
1 parent 8100635 commit 4f32fb0

File tree

2 files changed

+58
-19
lines changed

2 files changed

+58
-19
lines changed

.changeset/sour-moles-heal.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/adapter-vercel': minor
3+
---
4+
5+
feat: create symlink functions for each route, for better observability

packages/adapter-vercel/index.js

+53-19
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { get_pathname, pattern_to_src } from './utils.js';
88
import { VERSION } from '@sveltejs/kit';
99

1010
const name = '@sveltejs/adapter-vercel';
11-
const DEFAULT_FUNCTION_NAME = 'fn';
11+
const INTERNAL = '![-]'; // this name is guaranteed not to conflict with user routes
1212

1313
const get_default_runtime = () => {
1414
const major = Number(process.version.slice(1).split('.')[0]);
@@ -319,7 +319,7 @@ const plugin = function (defaults = {}) {
319319
group.config.runtime === 'edge' ? generate_edge_function : generate_serverless_function;
320320

321321
// generate one function for the group
322-
const name = singular ? DEFAULT_FUNCTION_NAME : `fn-${group.i}`;
322+
const name = singular ? `${INTERNAL}/catchall` : `${INTERNAL}/${group.i}`;
323323

324324
await generate_function(
325325
name,
@@ -332,12 +332,27 @@ const plugin = function (defaults = {}) {
332332
}
333333
}
334334

335+
if (!singular) {
336+
// we need to create a catch-all route so that 404s are handled
337+
// by SvelteKit rather than Vercel
338+
339+
const runtime = defaults.runtime ?? get_default_runtime();
340+
const generate_function =
341+
runtime === 'edge' ? generate_edge_function : generate_serverless_function;
342+
343+
await generate_function(
344+
`${INTERNAL}/catchall`,
345+
/** @type {any} */ ({ runtime, ...defaults }),
346+
[]
347+
);
348+
}
349+
335350
for (const route of builder.routes) {
336351
if (is_prerendered(route)) continue;
337352

338353
const pattern = route.pattern.toString();
339354
const src = pattern_to_src(pattern);
340-
const name = functions.get(pattern) ?? 'fn-0';
355+
const name = functions.get(pattern);
341356

342357
const isr = isr_config.get(route);
343358
if (isr) {
@@ -370,24 +385,43 @@ const plugin = function (defaults = {}) {
370385
src: src + '/__data.json$',
371386
dest: `/${isr_name}/__data.json${q}`
372387
});
373-
} else if (!singular) {
374-
static_config.routes.push({ src: src + '(?:/__data.json)?$', dest: `/${name}` });
375-
}
376-
}
388+
} else {
389+
// Create a symlink for each route to the main function for better observability
390+
// (without this, every request appears to go through `/![-]`)
377391

378-
if (!singular) {
379-
// we need to create a catch-all route so that 404s are handled
380-
// by SvelteKit rather than Vercel
392+
// Use 'index' for the root route's filesystem representation
393+
// Use an empty string ('') for the root route's destination name part in Vercel config
394+
const is_root = route.id === '/';
395+
const route_fs_name = is_root ? 'index' : route.id.slice(1);
396+
const route_dest_name = is_root ? '' : route.id.slice(1);
381397

382-
const runtime = defaults.runtime ?? get_default_runtime();
383-
const generate_function =
384-
runtime === 'edge' ? generate_edge_function : generate_serverless_function;
398+
// Define paths using path.join for safety
399+
const base_dir = path.join(dirs.functions, route_fs_name); // e.g., .vercel/output/functions/index
400+
// The main symlink should be named based on the route, adjacent to its potential directory
401+
const main_symlink_path = `${base_dir}.func`; // e.g., .vercel/output/functions/index.func
402+
// The data symlink goes inside the directory
403+
const data_symlink_path = path.join(base_dir, '__data.json.func'); // e.g., .vercel/output/functions/index/__data.json.func
385404

386-
await generate_function(
387-
DEFAULT_FUNCTION_NAME,
388-
/** @type {any} */ ({ runtime, ...defaults }),
389-
[]
390-
);
405+
const target = path.join(dirs.functions, `${name}.func`); // The actual function directory e.g., .vercel/output/functions/![-].func
406+
407+
// Ensure the directory for the data endpoint symlink exists (e.g., functions/index/)
408+
builder.mkdirp(base_dir);
409+
410+
// Calculate relative paths FROM the directory containing the symlink TO the target
411+
const relative_for_main = path.relative(path.dirname(main_symlink_path), target);
412+
const relative_for_data = path.relative(path.dirname(data_symlink_path), target); // This is path.relative(base_dir, target)
413+
414+
// Create symlinks
415+
fs.symlinkSync(relative_for_main, main_symlink_path); // Creates functions/index.func -> ![-].func
416+
fs.symlinkSync(relative_for_data, data_symlink_path); // Creates functions/index/__data.json.func -> ../![-].func
417+
418+
// Add route to the config
419+
static_config.routes.push({
420+
src: src + '(?:/__data.json)?$', // Matches the incoming request path
421+
dest: `/${route_dest_name}` // Maps to the function: '/' for root, '/about' for about, etc.
422+
// Vercel uses this dest to find the corresponding .func dir/symlink
423+
});
424+
}
391425
}
392426

393427
// optional chaining to support older versions that don't have this setting yet
@@ -412,7 +446,7 @@ const plugin = function (defaults = {}) {
412446

413447
// Catch-all route must come at the end, otherwise it will swallow all other routes,
414448
// including ISR aliases if there is only one function
415-
static_config.routes.push({ src: '/.*', dest: `/${DEFAULT_FUNCTION_NAME}` });
449+
static_config.routes.push({ src: '/.*', dest: `/${INTERNAL}/catchall` });
416450

417451
builder.log.minor('Writing routes...');
418452

0 commit comments

Comments
 (0)