762 lines
22 KiB
JavaScript
762 lines
22 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import process from 'node:process';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { nodeFileTrace } from '@vercel/nft';
|
|
import esbuild from 'esbuild';
|
|
import { get_pathname, pattern_to_src } from './utils.js';
|
|
import { VERSION } from '@sveltejs/kit';
|
|
|
|
const name = '@sveltejs/adapter-vercel';
|
|
const DEFAULT_FUNCTION_NAME = 'fn';
|
|
|
|
const get_default_runtime = () => {
|
|
const major = Number(process.version.slice(1).split('.')[0]);
|
|
|
|
// If we're building on Vercel, we know that the version will be fine because Vercel
|
|
// provides Node (and Vercel won't provide something it doesn't support).
|
|
// Also means we're not on the hook for updating the adapter every time a new Node
|
|
// version is added to Vercel.
|
|
if (!process.env.VERCEL) {
|
|
if (major < 18 || major > 22) {
|
|
throw new Error(
|
|
`Building locally with unsupported Node.js version: ${process.version}. Please use Node 18, 20 or 22 to build your project, or explicitly specify a runtime in your adapter configuration.`
|
|
);
|
|
}
|
|
|
|
if (major % 2 !== 0) {
|
|
throw new Error(
|
|
`Unsupported Node.js version: ${process.version}. Please use an even-numbered Node version to build your project, or explicitly specify a runtime in your adapter configuration.`
|
|
);
|
|
}
|
|
}
|
|
|
|
return `nodejs${major}.x`;
|
|
};
|
|
|
|
// https://vercel.com/docs/functions/edge-functions/edge-runtime#compatible-node.js-modules
|
|
const compatible_node_modules = ['async_hooks', 'events', 'buffer', 'assert', 'util'];
|
|
|
|
/** @type {import('./index.js').default} **/
|
|
const plugin = function (defaults = {}) {
|
|
if ('edge' in defaults) {
|
|
throw new Error("{ edge: true } has been removed in favour of { runtime: 'edge' }");
|
|
}
|
|
|
|
return {
|
|
name,
|
|
|
|
async adapt(builder) {
|
|
if (!builder.routes) {
|
|
throw new Error(
|
|
'@sveltejs/adapter-vercel >=2.x (possibly installed through @sveltejs/adapter-auto) requires @sveltejs/kit version 1.5 or higher. ' +
|
|
'Either downgrade the adapter or upgrade @sveltejs/kit'
|
|
);
|
|
}
|
|
|
|
const dir = '.vercel/output';
|
|
const tmp = builder.getBuildDirectory('vercel-tmp');
|
|
|
|
builder.rimraf(dir);
|
|
builder.rimraf(tmp);
|
|
|
|
if (fs.existsSync('vercel.json')) {
|
|
const vercel_file = fs.readFileSync('vercel.json', 'utf-8');
|
|
const vercel_config = JSON.parse(vercel_file);
|
|
validate_vercel_json(builder, vercel_config);
|
|
}
|
|
|
|
const files = fileURLToPath(new URL('./files', import.meta.url).href);
|
|
|
|
const dirs = {
|
|
static: `${dir}/static${builder.config.kit.paths.base}`,
|
|
functions: `${dir}/functions`
|
|
};
|
|
|
|
builder.log.minor('Copying assets...');
|
|
|
|
builder.writeClient(dirs.static);
|
|
builder.writePrerendered(dirs.static);
|
|
|
|
const static_config = static_vercel_config(builder, defaults, dirs.static);
|
|
|
|
builder.log.minor('Generating serverless function...');
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {import('./index.js').ServerlessConfig} config
|
|
* @param {import('@sveltejs/kit').RouteDefinition<import('./index.js').Config>[]} routes
|
|
*/
|
|
async function generate_serverless_function(name, config, routes) {
|
|
const dir = `${dirs.functions}/${name}.func`;
|
|
|
|
const relativePath = path.posix.relative(tmp, builder.getServerDirectory());
|
|
|
|
builder.copy(`${files}/serverless.js`, `${tmp}/index.js`, {
|
|
replace: {
|
|
SERVER: `${relativePath}/index.js`,
|
|
MANIFEST: './manifest.js'
|
|
}
|
|
});
|
|
|
|
write(
|
|
`${tmp}/manifest.js`,
|
|
`export const manifest = ${builder.generateManifest({ relativePath, routes })};\n`
|
|
);
|
|
|
|
await create_function_bundle(builder, `${tmp}/index.js`, dir, config);
|
|
|
|
for (const asset of builder.findServerAssets(routes)) {
|
|
// TODO use symlinks, once Build Output API supports doing so
|
|
builder.copy(`${builder.getServerDirectory()}/${asset}`, `${dir}/${asset}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {import('./index.js').EdgeConfig} config
|
|
* @param {import('@sveltejs/kit').RouteDefinition<import('./index.js').EdgeConfig>[]} routes
|
|
*/
|
|
async function generate_edge_function(name, config, routes) {
|
|
const tmp = builder.getBuildDirectory(`vercel-tmp/${name}`);
|
|
const relativePath = path.posix.relative(tmp, builder.getServerDirectory());
|
|
|
|
builder.copy(`${files}/edge.js`, `${tmp}/edge.js`, {
|
|
replace: {
|
|
SERVER: `${relativePath}/index.js`,
|
|
MANIFEST: './manifest.js'
|
|
}
|
|
});
|
|
|
|
write(
|
|
`${tmp}/manifest.js`,
|
|
`export const manifest = ${builder.generateManifest({ relativePath, routes })};\n`
|
|
);
|
|
|
|
try {
|
|
const result = await esbuild.build({
|
|
entryPoints: [`${tmp}/edge.js`],
|
|
outfile: `${dirs.functions}/${name}.func/index.js`,
|
|
target: 'es2020', // TODO verify what the edge runtime supports
|
|
bundle: true,
|
|
platform: 'browser',
|
|
format: 'esm',
|
|
external: [
|
|
...compatible_node_modules,
|
|
...compatible_node_modules.map((id) => `node:${id}`),
|
|
...(config.external || [])
|
|
],
|
|
sourcemap: 'linked',
|
|
banner: { js: 'globalThis.global = globalThis;' },
|
|
loader: {
|
|
'.wasm': 'copy',
|
|
'.woff': 'copy',
|
|
'.woff2': 'copy',
|
|
'.ttf': 'copy',
|
|
'.eot': 'copy',
|
|
'.otf': 'copy'
|
|
}
|
|
});
|
|
|
|
if (result.warnings.length > 0) {
|
|
const formatted = await esbuild.formatMessages(result.warnings, {
|
|
kind: 'warning',
|
|
color: true
|
|
});
|
|
|
|
console.error(formatted.join('\n'));
|
|
}
|
|
} catch (err) {
|
|
const error = /** @type {import('esbuild').BuildFailure} */ (err);
|
|
for (const e of error.errors) {
|
|
for (const node of e.notes) {
|
|
const match =
|
|
/The package "(.+)" wasn't found on the file system but is built into node/.exec(
|
|
node.text
|
|
);
|
|
|
|
if (match) {
|
|
node.text = `Cannot use "${match[1]}" when deploying to Vercel Edge Functions.`;
|
|
}
|
|
}
|
|
}
|
|
|
|
const formatted = await esbuild.formatMessages(error.errors, {
|
|
kind: 'error',
|
|
color: true
|
|
});
|
|
|
|
console.error(formatted.join('\n'));
|
|
|
|
throw new Error(
|
|
`Bundling with esbuild failed with ${error.errors.length} ${
|
|
error.errors.length === 1 ? 'error' : 'errors'
|
|
}`
|
|
);
|
|
}
|
|
|
|
write(
|
|
`${dirs.functions}/${name}.func/.vc-config.json`,
|
|
JSON.stringify(
|
|
{
|
|
runtime: config.runtime,
|
|
regions: config.regions,
|
|
entrypoint: 'index.js',
|
|
framework: {
|
|
slug: 'sveltekit',
|
|
version: VERSION
|
|
}
|
|
},
|
|
null,
|
|
'\t'
|
|
)
|
|
);
|
|
}
|
|
|
|
/** @type {Map<string, { i: number, config: import('./index.js').Config, routes: import('@sveltejs/kit').RouteDefinition<import('./index.js').Config>[] }>} */
|
|
const groups = new Map();
|
|
|
|
/** @type {Map<string, { hash: string, route_id: string }>} */
|
|
const conflicts = new Map();
|
|
|
|
/** @type {Map<string, string>} */
|
|
const functions = new Map();
|
|
|
|
/** @type {Map<import('@sveltejs/kit').RouteDefinition<import('./index.js').Config>, { expiration: number | false, bypassToken: string | undefined, allowQuery: string[], group: number, passQuery: true }>} */
|
|
const isr_config = new Map();
|
|
|
|
/** @type {Set<string>} */
|
|
const ignored_isr = new Set();
|
|
|
|
// group routes by config
|
|
for (const route of builder.routes) {
|
|
const runtime = route.config?.runtime ?? defaults?.runtime ?? get_default_runtime();
|
|
const config = { runtime, ...defaults, ...route.config };
|
|
|
|
if (is_prerendered(route)) {
|
|
if (config.isr) {
|
|
ignored_isr.add(route.id);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const node_runtime = /nodejs([0-9]+)\.x/.exec(runtime);
|
|
if (runtime !== 'edge' && (!node_runtime || parseInt(node_runtime[1]) < 18)) {
|
|
throw new Error(
|
|
`Invalid runtime '${runtime}' for route ${route.id}. Valid runtimes are 'edge' and 'nodejs18.x' or higher ` +
|
|
'(see the Node.js Version section in your Vercel project settings for info on the currently supported versions).'
|
|
);
|
|
}
|
|
|
|
if (config.isr) {
|
|
const directory = path.relative('.', builder.config.kit.files.routes + route.id);
|
|
|
|
if (!runtime.startsWith('nodejs')) {
|
|
throw new Error(
|
|
`${directory}: Routes using \`isr\` must use a Node.js runtime (for example 'nodejs20.x')`
|
|
);
|
|
}
|
|
|
|
if (config.isr.allowQuery?.includes('__pathname')) {
|
|
throw new Error(
|
|
`${directory}: \`__pathname\` is a reserved query parameter for \`isr.allowQuery\``
|
|
);
|
|
}
|
|
|
|
isr_config.set(route, {
|
|
expiration: config.isr.expiration,
|
|
bypassToken: config.isr.bypassToken,
|
|
allowQuery: ['__pathname', ...(config.isr.allowQuery ?? [])],
|
|
group: isr_config.size + 1,
|
|
passQuery: true
|
|
});
|
|
}
|
|
|
|
const hash = hash_config(config);
|
|
|
|
// first, check there are no routes with incompatible configs that will be merged
|
|
const pattern = route.pattern.toString();
|
|
const existing = conflicts.get(pattern);
|
|
if (existing) {
|
|
if (existing.hash !== hash) {
|
|
throw new Error(
|
|
`The ${route.id} and ${existing.route_id} routes must be merged into a single function that matches the ${route.pattern} regex, but they have incompatible configs. You must either rename one of the routes, or make their configs match.`
|
|
);
|
|
}
|
|
} else {
|
|
conflicts.set(pattern, { hash, route_id: route.id });
|
|
}
|
|
|
|
// then, create a group for each config
|
|
const id = config.split ? `${hash}-${groups.size}` : hash;
|
|
let group = groups.get(id);
|
|
if (!group) {
|
|
group = { i: groups.size, config, routes: [] };
|
|
groups.set(id, group);
|
|
}
|
|
|
|
group.routes.push(route);
|
|
}
|
|
|
|
if (ignored_isr.size) {
|
|
builder.log.warn(
|
|
'\nWarning: The following routes have an ISR config which is ignored because the route is prerendered:'
|
|
);
|
|
|
|
for (const ignored of ignored_isr) {
|
|
console.log(` - ${ignored}`);
|
|
}
|
|
|
|
console.log(
|
|
'Either remove the "prerender" option from these routes to use ISR, or remove the ISR config.\n'
|
|
);
|
|
}
|
|
|
|
const singular = groups.size === 1;
|
|
|
|
for (const group of groups.values()) {
|
|
const generate_function =
|
|
group.config.runtime === 'edge' ? generate_edge_function : generate_serverless_function;
|
|
|
|
// generate one function for the group
|
|
const name = singular ? DEFAULT_FUNCTION_NAME : `fn-${group.i}`;
|
|
|
|
await generate_function(
|
|
name,
|
|
/** @type {any} */ (group.config),
|
|
/** @type {import('@sveltejs/kit').RouteDefinition<any>[]} */ (group.routes)
|
|
);
|
|
|
|
for (const route of group.routes) {
|
|
functions.set(route.pattern.toString(), name);
|
|
}
|
|
}
|
|
|
|
for (const route of builder.routes) {
|
|
if (is_prerendered(route)) continue;
|
|
|
|
const pattern = route.pattern.toString();
|
|
const src = pattern_to_src(pattern);
|
|
const name = functions.get(pattern) ?? 'fn-0';
|
|
|
|
const isr = isr_config.get(route);
|
|
if (isr) {
|
|
const isr_name = route.id.slice(1) || '__root__'; // should we check that __root__ isn't a route?
|
|
const base = `${dirs.functions}/${isr_name}`;
|
|
builder.mkdirp(base);
|
|
|
|
const target = `${dirs.functions}/${name}.func`;
|
|
const relative = path.relative(path.dirname(base), target);
|
|
|
|
// create a symlink to the actual function, but use the
|
|
// route name so that we can derive the correct URL
|
|
fs.symlinkSync(relative, `${base}.func`);
|
|
fs.symlinkSync(`../${relative}`, `${base}/__data.json.func`);
|
|
|
|
const pathname = get_pathname(route);
|
|
const json = JSON.stringify(isr, null, '\t');
|
|
|
|
write(`${base}.prerender-config.json`, json);
|
|
write(`${base}/__data.json.prerender-config.json`, json);
|
|
|
|
const q = `?__pathname=/${pathname}`;
|
|
|
|
static_config.routes.push({
|
|
src: src + '$',
|
|
dest: `/${isr_name}${q}`
|
|
});
|
|
|
|
static_config.routes.push({
|
|
src: src + '/__data.json$',
|
|
dest: `/${isr_name}/__data.json${q}`
|
|
});
|
|
} else if (!singular) {
|
|
static_config.routes.push({ src: src + '(?:/__data.json)?$', dest: `/${name}` });
|
|
}
|
|
}
|
|
|
|
if (!singular) {
|
|
// we need to create a catch-all route so that 404s are handled
|
|
// by SvelteKit rather than Vercel
|
|
|
|
const runtime = defaults.runtime ?? get_default_runtime();
|
|
const generate_function =
|
|
runtime === 'edge' ? generate_edge_function : generate_serverless_function;
|
|
|
|
await generate_function(
|
|
DEFAULT_FUNCTION_NAME,
|
|
/** @type {any} */ ({ runtime, ...defaults }),
|
|
[]
|
|
);
|
|
}
|
|
|
|
// optional chaining to support older versions that don't have this setting yet
|
|
if (builder.config.kit.router?.resolution === 'server') {
|
|
// Create a separate edge function just for server-side route resolution.
|
|
// By omitting all routes we're ensuring it's small (the routes will still be available
|
|
// to the route resolution, because it does not rely on the server routing manifest)
|
|
await generate_edge_function(
|
|
`${builder.config.kit.appDir}/route`,
|
|
{
|
|
external: 'external' in defaults ? defaults.external : undefined,
|
|
runtime: 'edge'
|
|
},
|
|
[]
|
|
);
|
|
|
|
static_config.routes.push({
|
|
src: `${builder.config.kit.paths.base}/(|.+/)__route\\.js`,
|
|
dest: `${builder.config.kit.paths.base}/${builder.config.kit.appDir}/route`
|
|
});
|
|
}
|
|
|
|
// Catch-all route must come at the end, otherwise it will swallow all other routes,
|
|
// including ISR aliases if there is only one function
|
|
static_config.routes.push({ src: '/.*', dest: `/${DEFAULT_FUNCTION_NAME}` });
|
|
|
|
builder.log.minor('Writing routes...');
|
|
|
|
write(`${dir}/config.json`, JSON.stringify(static_config, null, '\t'));
|
|
},
|
|
|
|
supports: {
|
|
// reading from the filesystem only works in serverless functions
|
|
read: ({ config, route }) => {
|
|
const runtime = config.runtime ?? defaults.runtime;
|
|
|
|
if (runtime === 'edge') {
|
|
throw new Error(
|
|
`${name}: Cannot use \`read\` from \`$app/server\` in route \`${route.id}\` configured with \`runtime: 'edge'\``
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
/** @param {import('./index.js').EdgeConfig & import('./index.js').ServerlessConfig} config */
|
|
function hash_config(config) {
|
|
return [
|
|
config.runtime ?? '',
|
|
config.external ?? '',
|
|
config.regions ?? '',
|
|
config.memory ?? '',
|
|
config.maxDuration ?? '',
|
|
!!config.isr // need to distinguish ISR from non-ISR functions, because ISR functions can't use streaming mode
|
|
].join('/');
|
|
}
|
|
|
|
/**
|
|
* @param {string} file
|
|
* @param {string} data
|
|
*/
|
|
function write(file, data) {
|
|
try {
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
} catch {
|
|
// do nothing
|
|
}
|
|
|
|
fs.writeFileSync(file, data);
|
|
}
|
|
|
|
// This function is duplicated in adapter-static
|
|
/**
|
|
* @param {import('@sveltejs/kit').Builder} builder
|
|
* @param {import('./index.js').Config} config
|
|
* @param {string} dir
|
|
*/
|
|
function static_vercel_config(builder, config, dir) {
|
|
/** @type {any[]} */
|
|
const prerendered_redirects = [];
|
|
|
|
/** @type {Record<string, { path: string }>} */
|
|
const overrides = {};
|
|
|
|
/** @type {import('./index.js').ImagesConfig | undefined} */
|
|
const images = config.images;
|
|
|
|
for (let [src, redirect] of builder.prerendered.redirects) {
|
|
if (src.replace(/\/$/, '') === redirect.location.replace(/\/$/, '')) {
|
|
// ignore the extreme edge case of a `/foo` -> `/foo/` redirect,
|
|
// which would only arise if the response was generated by a
|
|
// `handle` hook or outside the app altogether (since you
|
|
// can't declaratively create both routes)
|
|
} else {
|
|
// redirect both `/foo` and `/foo/` to `redirect.location`
|
|
src = src.replace(/\/?$/, '/?');
|
|
}
|
|
|
|
prerendered_redirects.push({
|
|
src,
|
|
headers: {
|
|
Location: redirect.location
|
|
},
|
|
status: redirect.status
|
|
});
|
|
}
|
|
|
|
for (const [path, page] of builder.prerendered.pages) {
|
|
let overrides_path = path.slice(1);
|
|
|
|
if (path !== '/') {
|
|
/** @type {string | undefined} */
|
|
let counterpart_route = path + '/';
|
|
|
|
if (path.endsWith('/')) {
|
|
counterpart_route = path.slice(0, -1);
|
|
overrides_path = path.slice(1, -1);
|
|
}
|
|
|
|
prerendered_redirects.push(
|
|
{ src: path, dest: counterpart_route },
|
|
{ src: counterpart_route, status: 308, headers: { Location: path } }
|
|
);
|
|
}
|
|
|
|
overrides[page.file] = { path: overrides_path };
|
|
}
|
|
|
|
const routes = [
|
|
...prerendered_redirects,
|
|
{
|
|
src: `/${builder.getAppPath()}/immutable/.+`,
|
|
headers: {
|
|
'cache-control': 'public, immutable, max-age=31536000'
|
|
}
|
|
}
|
|
];
|
|
|
|
// https://vercel.com/docs/deployments/skew-protection
|
|
if (process.env.VERCEL_SKEW_PROTECTION_ENABLED) {
|
|
routes.push({
|
|
src: '/.*',
|
|
has: [
|
|
{
|
|
type: 'header',
|
|
key: 'Sec-Fetch-Dest',
|
|
value: 'document'
|
|
}
|
|
],
|
|
headers: {
|
|
'Set-Cookie': `__vdpl=${process.env.VERCEL_DEPLOYMENT_ID}; Path=${builder.config.kit.paths.base}/; SameSite=Strict; Secure; HttpOnly`
|
|
},
|
|
continue: true
|
|
});
|
|
|
|
// this is a dreadful hack that is necessary until the Vercel Build Output API
|
|
// allows you to set multiple cookies for a single route. essentially, since we
|
|
// know that the entry file will be requested immediately, we can set the second
|
|
// cookie in _that_ response rather than the document response
|
|
const base = `${dir}/${builder.config.kit.appDir}/immutable/entry`;
|
|
const entry = fs.readdirSync(base).find((file) => file.startsWith('start.'));
|
|
|
|
if (!entry) {
|
|
throw new Error('Could not find entry point');
|
|
}
|
|
|
|
routes.splice(-2, 0, {
|
|
src: `/${builder.getAppPath()}/immutable/entry/${entry}`,
|
|
headers: {
|
|
'Set-Cookie': `__vdpl=; Path=/${builder.getAppPath()}/version.json; SameSite=Strict; Secure; HttpOnly`
|
|
},
|
|
continue: true
|
|
});
|
|
}
|
|
|
|
routes.push({
|
|
handle: 'filesystem'
|
|
});
|
|
|
|
return {
|
|
version: 3,
|
|
routes,
|
|
overrides,
|
|
images
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {import('@sveltejs/kit').Builder} builder
|
|
* @param {string} entry
|
|
* @param {string} dir
|
|
* @param {import('./index.js').ServerlessConfig} config
|
|
*/
|
|
async function create_function_bundle(builder, entry, dir, config) {
|
|
fs.rmSync(dir, { force: true, recursive: true });
|
|
|
|
let base = entry;
|
|
while (base !== (base = path.dirname(base)));
|
|
|
|
const traced = await nodeFileTrace([entry], { base });
|
|
|
|
/** @type {Map<string, string[]>} */
|
|
const resolution_failures = new Map();
|
|
|
|
traced.warnings.forEach((error) => {
|
|
// pending https://github.com/vercel/nft/issues/284
|
|
if (error.message.startsWith('Failed to resolve dependency node:')) return;
|
|
|
|
// parse errors are likely not js and can safely be ignored,
|
|
// such as this html file in "main" meant for nw instead of node:
|
|
// https://github.com/vercel/nft/issues/311
|
|
if (error.message.startsWith('Failed to parse')) return;
|
|
|
|
if (error.message.startsWith('Failed to resolve dependency')) {
|
|
const match = /Cannot find module '(.+?)' loaded from (.+)/;
|
|
const [, module, importer] = match.exec(error.message) ?? [, error.message, '(unknown)'];
|
|
|
|
if (!resolution_failures.has(importer)) {
|
|
resolution_failures.set(importer, []);
|
|
}
|
|
|
|
/** @type {string[]} */ (resolution_failures.get(importer)).push(module);
|
|
} else {
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
if (resolution_failures.size > 0) {
|
|
const cwd = process.cwd();
|
|
builder.log.warn(
|
|
'Warning: The following modules failed to locate dependencies that may (or may not) be required for your app to work:'
|
|
);
|
|
|
|
for (const [importer, modules] of resolution_failures) {
|
|
console.error(` ${path.relative(cwd, importer)}`);
|
|
for (const module of modules) {
|
|
console.error(` - \u001B[1m\u001B[36m${module}\u001B[39m\u001B[22m`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const files = Array.from(traced.fileList);
|
|
|
|
// find common ancestor directory
|
|
/** @type {string[]} */
|
|
let common_parts = files[0]?.split(path.sep) ?? [];
|
|
|
|
for (let i = 1; i < files.length; i += 1) {
|
|
const file = files[i];
|
|
const parts = file.split(path.sep);
|
|
|
|
for (let j = 0; j < common_parts.length; j += 1) {
|
|
if (parts[j] !== common_parts[j]) {
|
|
common_parts = common_parts.slice(0, j);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const ancestor = base + common_parts.join(path.sep);
|
|
|
|
for (const file of traced.fileList) {
|
|
const source = base + file;
|
|
const dest = path.join(dir, path.relative(ancestor, source));
|
|
|
|
const stats = fs.statSync(source);
|
|
const is_dir = stats.isDirectory();
|
|
|
|
const realpath = fs.realpathSync(source);
|
|
|
|
try {
|
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
} catch {
|
|
// do nothing
|
|
}
|
|
|
|
if (source !== realpath) {
|
|
const realdest = path.join(dir, path.relative(ancestor, realpath));
|
|
fs.symlinkSync(path.relative(path.dirname(dest), realdest), dest, is_dir ? 'dir' : 'file');
|
|
} else if (!is_dir) {
|
|
fs.copyFileSync(source, dest);
|
|
}
|
|
}
|
|
|
|
write(
|
|
`${dir}/.vc-config.json`,
|
|
JSON.stringify(
|
|
{
|
|
runtime: config.runtime,
|
|
regions: config.regions,
|
|
memory: config.memory,
|
|
maxDuration: config.maxDuration,
|
|
handler: path.relative(base + ancestor, entry),
|
|
launcherType: 'Nodejs',
|
|
experimentalResponseStreaming: !config.isr,
|
|
framework: {
|
|
slug: 'sveltekit',
|
|
version: VERSION
|
|
}
|
|
},
|
|
null,
|
|
'\t'
|
|
)
|
|
);
|
|
|
|
write(`${dir}/package.json`, JSON.stringify({ type: 'module' }));
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {import('@sveltejs/kit').Builder} builder
|
|
* @param {any} vercel_config
|
|
*/
|
|
function validate_vercel_json(builder, vercel_config) {
|
|
if (builder.routes.length > 0 && !builder.routes[0].api) {
|
|
// bail — we're on an older SvelteKit version that doesn't
|
|
// populate `route.api.methods`, so we can't check
|
|
// to see if cron paths are valid
|
|
return;
|
|
}
|
|
|
|
const crons = /** @type {Array<unknown>} */ (
|
|
Array.isArray(vercel_config?.crons) ? vercel_config.crons : []
|
|
);
|
|
|
|
/** For a route to be considered 'valid', it must be an API route with a GET handler */
|
|
const valid_routes = builder.routes.filter((route) => route.api.methods.includes('GET'));
|
|
|
|
/** @type {Array<string>} */
|
|
const unmatched_paths = [];
|
|
|
|
for (const cron of crons) {
|
|
if (typeof cron !== 'object' || cron === null || !('path' in cron)) {
|
|
continue;
|
|
}
|
|
|
|
const { path } = cron;
|
|
if (typeof path !== 'string') {
|
|
continue;
|
|
}
|
|
|
|
if (!valid_routes.some((route) => route.pattern.test(path))) {
|
|
unmatched_paths.push(path);
|
|
}
|
|
}
|
|
|
|
if (unmatched_paths.length) {
|
|
builder.log.warn(
|
|
'\nWarning: vercel.json defines cron tasks that use paths that do not correspond to an API route with a GET handler (ignore this if the request is handled in your `handle` hook):'
|
|
);
|
|
|
|
for (const path of unmatched_paths) {
|
|
console.log(` - ${path}`);
|
|
}
|
|
|
|
console.log('');
|
|
}
|
|
}
|
|
|
|
/** @param {import('@sveltejs/kit').RouteDefinition} route */
|
|
function is_prerendered(route) {
|
|
return (
|
|
route.prerender === true ||
|
|
(route.prerender === 'auto' && route.segments.every((segment) => !segment.dynamic))
|
|
);
|
|
}
|
|
|
|
export default plugin;
|