Skip to content

Commit

Permalink
feat: include server assets for Vercel serverless functions (#10979)
Browse files Browse the repository at this point in the history
* add ability to copy server assets per route

* changeset

* fix test

* fix doc

* fix asset copying

* change process dir when using vite preview

* remove process.chdir from vite preview

* include assets used by default error pages

* fix doc link

* better doc example

* fix doc type

* ok for real

* add tests for vercel adapter

* prettier

* fix tests

* fix types

* more type fixes

* try this

* documentation

* skip adapter-vercel tests if on node v20

* oopsie wrong PR

* move server asset metadata resolving to function

* update changesets

* revert builder test fixes

* fix lockfile

* prettier

* lint

* fix vercel tsc errors

* prepublish only

* fix: avoid running load functions when prerendering if no server load function exists and SSR is off (#11405)

* chore: upgrade eslint-plugin-unicorn (#11432)

* Version Packages (#11420)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* chore(deps): update dependency worktop to v0.8.0-next.16 (#11437)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* i don't think we need this any more (#11439)

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* fix: only disallow dynamic env access when prerendering (#11436)

* only disallow dynamic env access when prerendering

* changeset

* add test

* sigh

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* docs: fix links to sveltesociety.dev (#11441)

* Version Packages (#11442)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix: set ESLint config type to `Config` instead of `FlatConfig` (#11453)

* Version Packages (#11457)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix: improve warning when encountering import.meta.env (#11440)

* empty commit (#11469)

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* fix form actions docs (#11470)

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* Version Packages (#11468)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* chore(deps): update pnpm to v8.13.1 (#11471)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* docs: add performance page (#11424)

* docs: add performance page

* address minor feedback issues

* lazy loading and waterfalls

* prefetching

* Apply suggestions from code review

* Apply suggestions from code review

* various

* tweak

* MDN page says nothing about lazy-loading videos

* fix link

* Update documentation/docs/40-best-practices/05-performance.md

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>

* move images page

* Update documentation/docs/40-best-practices/05-performance.md

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>

* update font section (no point mentioning font-display without a recommended value)

* various tweaks

---------

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>

* chore(deps): update pnpm to v8.14.0 (#11504)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* feat: use latest Azure adapter in adapter-auto (#11496)

* fix: update @vercel/nft to 0.26.1 (#11508)

* Version Packages (#11507)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* exclude universal nodes

* include assets imported by server hooks

* update adapter-vercel tests

* fix tests

* fix and add test for server hooks assets

* update adapter-vercel test app packages

* im gonna lose my mind

* ignore hashes for filenames in tests

* oops

* fix tests?

* revert adapter-vercel types fix

* improve readability of route asset for loop

* simplify file reading in serverless functions example

---------

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Geoff Rich <4992896+geoffrich@users.noreply.github.com>
  • Loading branch information
8 people authored Jan 15, 2024
1 parent 22ed677 commit fe1a11e
Show file tree
Hide file tree
Showing 52 changed files with 1,278 additions and 617 deletions.
5 changes: 5 additions & 0 deletions .changeset/loud-needles-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/adapter-vercel': minor
---

feat: bundle server assets with serverless functions
5 changes: 5 additions & 0 deletions .changeset/selfish-bobcats-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: adapter method to get server assets used by each route
27 changes: 24 additions & 3 deletions documentation/docs/25-build-and-deploy/80-adapter-netlify.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,29 @@ Additionally, you can add your own Netlify functions by creating a directory for
directory = "functions"
```

## Troubleshooting

### Accessing the file system

You can't access the file system through methods like `fs.readFileSync` in Serverless/Edge environments. If you need to access files that way, do that during building the app through [prerendering](https://kit.svelte.dev/docs/page-options#prerender). If you have a blog for example and don't want to manage your content through a CMS, then you need to prerender the content (or prerender the endpoint from which you get it) and redeploy your blog everytime you add new content.
You can [use files in Netlify Serverless Functions](https://www.netlify.com/blog/2021/08/12/how-to-include-files-in-netlify-serverless-functions/).

```js
// @errors: 2307 7031
/// file: +server.js
import fs from "node:fs";
import path from "node:path";
import { dev } from '$app/environment';

// importing a static asset will return the resolved path in the production build
import PalatinoBoldFont from "$lib/fonts/PalatinoBold.ttf";

const cwd = process.cwd();

// server assets live in `.netlify/server` when deployed to Netlify
const dir = dev ? cwd : path.join(cwd, '.netlify/server');

const pathToFile = path.join(dir, PalatinoBoldFont);

export async function GET() {
const file = fs.readFileSync(pathToFile);
// ...
}
```
23 changes: 20 additions & 3 deletions documentation/docs/25-build-and-deploy/90-adapter-vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,25 @@ If you have Vercel functions contained in the `api` directory at the project's r

Projects created before a certain date may default to using an older Node version than what SvelteKit currently requires. You can [change the Node version in your project settings](https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/node-js#node.js-version).

## Troubleshooting

### Accessing the file system

You can't access the file system through methods like `fs.readFileSync` in Serverless/Edge environments. If you need to access files that way, do that during building the app through [prerendering](https://kit.svelte.dev/docs/page-options#prerender). If you have a blog for example and don't want to manage your content through a CMS, then you need to prerender the content (or prerender the endpoint from which you get it) and redeploy your blog everytime you add new content.
You can [use files in Serverless Functions on Vercel](https://vercel.com/guides/how-can-i-use-files-in-serverless-functions).

```js
// @errors: 2307 7031
/// file: api/pdf/+server.js
import fs from "node:fs";
import path from "node:path";

// importing a static asset will return the resolved path in the production build
import PalatinoBoldFont from "$lib/fonts/PalatinoBold.ttf";

const pathToFile = path.join(process.cwd(), PalatinoBoldFont);

export async function GET() {
const file = fs.readFileSync(pathToFile);
// ...
}
```

> Only assets that are imported in `+page.server`, `+layout.server` and `+server` files are included in the Serverless Function bundle.
1 change: 1 addition & 0 deletions packages/adapter-vercel/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.DS_Store
node_modules
.vercel
49 changes: 36 additions & 13 deletions packages/adapter-vercel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ const plugin = function (defaults = {}) {

/**
* @param {string} name
* @param {import('.').ServerlessConfig} config
* @param {import('@sveltejs/kit').RouteDefinition<import('.').Config>[]} routes
* @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 relativePath = path.posix.relative(tmp, builder.getServerDirectory());
Expand All @@ -81,14 +81,15 @@ const plugin = function (defaults = {}) {
builder,
`${tmp}/index.js`,
`${dirs.functions}/${name}.func`,
config
config,
routes
);
}

/**
* @param {string} name
* @param {import('.').EdgeConfig} config
* @param {import('@sveltejs/kit').RouteDefinition<import('.').EdgeConfig>[]} routes
* @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}`);
Expand Down Expand Up @@ -135,7 +136,7 @@ const plugin = function (defaults = {}) {
);
}

/** @type {Map<string, { i: number, config: import('.').Config, routes: import('@sveltejs/kit').RouteDefinition<import('.').Config>[] }>} */
/** @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 }>} */
Expand All @@ -144,7 +145,7 @@ const plugin = function (defaults = {}) {
/** @type {Map<string, string>} */
const functions = new Map();

/** @type {Map<import('@sveltejs/kit').RouteDefinition<import('.').Config>, { expiration: number | false, bypassToken: string | undefined, allowQuery: string[], group: number, passQuery: true }>} */
/** @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>} */
Expand All @@ -163,7 +164,7 @@ const plugin = function (defaults = {}) {
}

const node_runtime = /nodejs([0-9]+)\.x/.exec(runtime);
if (runtime !== 'edge' && (!node_runtime || node_runtime[1] < 18)) {
if (runtime !== 'edge' && (!node_runtime || +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).'
Expand Down Expand Up @@ -368,7 +369,7 @@ function write(file, data) {
// This function is duplicated in adapter-static
/**
* @param {import('@sveltejs/kit').Builder} builder
* @param {import('.').Config} config
* @param {import('index.js').Config} config
*/
function static_vercel_config(builder, config) {
/** @type {any[]} */
Expand All @@ -377,8 +378,11 @@ function static_vercel_config(builder, config) {
/** @type {Record<string, { path: string }>} */
const overrides = {};

/** @type {import('./index').ImagesConfig} */
const images = config.images;
/** @type {import('./index.js').ImagesConfig | undefined} */
let images;
if (config.runtime !== 'edge') {
images = /** @type {import('./index.js').ServerlessConfig} */ (config).images;
}

for (const [src, redirect] of builder.prerendered.redirects) {
prerendered_redirects.push({
Expand Down Expand Up @@ -434,9 +438,10 @@ function static_vercel_config(builder, config) {
* @param {import('@sveltejs/kit').Builder} builder
* @param {string} entry
* @param {string} dir
* @param {import('.').ServerlessConfig} config
* @param {import('./index.js').ServerlessConfig} config
* @param {import('@sveltejs/kit').RouteDefinition[]} routes
*/
async function create_function_bundle(builder, entry, dir, config) {
async function create_function_bundle(builder, entry, dir, config, routes) {
fs.rmSync(dir, { force: true, recursive: true });

let base = entry;
Expand Down Expand Up @@ -544,6 +549,24 @@ async function create_function_bundle(builder, entry, dir, config) {
)
);

const server_assets = builder.getServerAssets();
let routes_assets = new Set(server_assets.rootErrorPage);

for (const route of routes) {
const assets = server_assets.routes.get(route.id);
if (assets) {
routes_assets = new Set([...routes_assets, ...assets]);
}
}

if (server_assets.hooks) {
routes_assets = new Set([...routes_assets, ...server_assets.hooks]);
}

for (const asset of routes_assets) {
builder.copy(path.join(builder.getServerDirectory(), asset), path.join(dir, asset));
}

write(`${dir}/package.json`, JSON.stringify({ type: 'module' }));
}

Expand Down
3 changes: 2 additions & 1 deletion packages/adapter-vercel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@
"lint": "prettier --check .",
"format": "pnpm lint --write",
"check": "tsc",
"test": "vitest run"
"test": "vitest run & pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test"
},
"dependencies": {
"@vercel/nft": "^0.26.1",
"esbuild": "^0.19.9"
},
"devDependencies": {
"@playwright/test": "1.30.0",
"@sveltejs/kit": "workspace:^",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@types/node": "^18.19.3",
Expand Down
10 changes: 10 additions & 0 deletions packages/adapter-vercel/test/apps/split/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
1 change: 1 addition & 0 deletions packages/adapter-vercel/test/apps/split/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
18 changes: 18 additions & 0 deletions packages/adapter-vercel/test/apps/split/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "~TODO~",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"test": "playwright test"
},
"devDependencies": {
"@sveltejs/kit": "workspace:^",
"svelte": "^4.2.8",
"typescript": "^5.3.3",
"vite": "^5.0.8"
},
"type": "module"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { config as default } from '../../utils.js';
12 changes: 12 additions & 0 deletions packages/adapter-vercel/test/apps/split/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}

export {};
12 changes: 12 additions & 0 deletions packages/adapter-vercel/test/apps/split/src/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
6 changes: 6 additions & 0 deletions packages/adapter-vercel/test/apps/split/src/hooks.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import text from '$lib/hooks.server.js.txt';

export async function handle({ event, resolve }) {
event.setHeaders({ 'x-server-asset': text });
return resolve(event);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
+error.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
+layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
+layout.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
+page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
+page.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
+server.js
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hooks.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root_error
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root_layout
3 changes: 3 additions & 0 deletions packages/adapter-vercel/test/apps/split/src/lib/transitive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import text from './transitive.txt';

export { text };
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
transitive
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script>
import asset from '$lib/root_error.txt';
/** @type {import('./$types').PageData}*/
export let data;
</script>

<h1>{data}</h1>
<p>{asset}</p>
5 changes: 5 additions & 0 deletions packages/adapter-vercel/test/apps/split/src/routes/+layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import layout from '$lib/+layout.js.txt';

export function load({ data }) {
return { ...data, layout };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import layout_server from '$lib/+layout.server.js.txt';

export function load() {
return { layout_server };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { text } from '$lib/transitive';

export function load() {
return { text };
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import server_asset from '$lib/+server.js.txt';

export function GET() {
return new Response(server_asset);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
import asset from '$lib/+error.svelte.txt';
</script>

<p>{asset}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import layout from '$lib/+layout.js.txt';

export function load() {
return { layout };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
import asset from '$lib/+layout.svelte.txt';
</script>

<p>{asset}</p>

<slot />
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import page_data from '$lib/+page.js.txt';

export async function load({ parent, data }) {
return { ...data, ...(await parent()), page_data };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import page_server_text from '$lib/+page.server.js.txt';

export async function load({ parent }) {
return { ...(await parent()), page_server_text };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
import asset from '$lib/+page.svelte.txt';
</script>

<p>{asset}</p>
Loading

0 comments on commit fe1a11e

Please sign in to comment.