Skip to content

Commit 288f731

Browse files
Rich-Harrisdummdidummbenmccann
authored
feat: implement read (#11649)
* feat: implement readAsset * lint etc * maybe make it work on netlify? no idea how to test, manual deploys dont appear to respect .netlify directory * set length/type from manifest * tidy up * regenerate types * lint * missed a spot * more efficient manifest generation * working on vercel * lint/fix * Update packages/adapter-vercel/index.js Co-authored-by: Simon H <[email protected]> * fix * createReadable helper * more future-proof API * account for basepath * lint * rename to just `read` * inline docs * it is already deprecated, we just need to remove it * read_asset -> read_implementation * add test * Apply suggestions from code review Co-authored-by: Ben McCann <[email protected]> * improve searchability * prevent $app/server being imported client-side * regenerate types * add dev time feature tracking mechanism * test feature support at build time * lint * lint * regenerate types * account for hooks.server.js, mostly * regenerate types * fix * bump peerdeps, add changesets * createReadable -> createReadableStream * Apply suggestions from code review Co-authored-by: Ben McCann <[email protected]> * remove unnecessary if * replace docs for find_server_assets * update adapter author docs * regenerate types * explain what __SVELTEKIT_TRACK__ does * mention `$app/server` on server-only modules page * minor details * oh ffs * exclude prerendered routes from feature detection, handle /@fs assets in dev * use read to populate content.json * fix prerendering * simplify * simplify docs logic * fix * style * simplify * lockfile * Apply suggestions from code review Co-authored-by: Ben McCann <[email protected]> * capitalize * Apply suggestions from code review Co-authored-by: Ben McCann <[email protected]> --------- Co-authored-by: Rich Harris <[email protected]> Co-authored-by: Simon H <[email protected]> Co-authored-by: Ben McCann <[email protected]>
1 parent 7d3cccd commit 288f731

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+800
-235
lines changed

.changeset/rude-apples-jam.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@sveltejs/adapter-netlify': major
3+
'@sveltejs/adapter-vercel': major
4+
'@sveltejs/adapter-node': major
5+
---
6+
7+
breaking: update peer dependency on `@sveltejs/kit`

.changeset/tasty-masks-talk.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: add `$app/server` module with `read` function for reading assets from filesystem

.changeset/tiny-maps-chew.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@sveltejs/adapter-netlify': minor
3+
'@sveltejs/adapter-vercel': minor
4+
'@sveltejs/adapter-node': minor
5+
---
6+
7+
feat: support `read` from `$app/server`

documentation/docs/25-build-and-deploy/99-writing-adapters.md

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ If an adapter for your preferred environment doesn't yet exist, you can build yo
77
Adapters packages must implement the following API, which creates an `Adapter`:
88

99
```js
10+
// @errors: 2322
1011
// @filename: ambient.d.ts
1112
type AdapterSpecificOptions = any;
1213

@@ -19,6 +20,13 @@ export default function (options) {
1920
name: 'adapter-package-name',
2021
async adapt(builder) {
2122
// adapter implementation
23+
},
24+
supports: {
25+
read: ({ config, route }) => {
26+
// Return `true` if the route with the given `config` can use `read`
27+
// from `$app/server` in production, return `false` if it can't.
28+
// Or throw a descriptive error describing how to configure the deployment
29+
}
2230
}
2331
};
2432

documentation/docs/30-advanced/50-server-only-modules.md

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Like a good friend, SvelteKit keeps your secrets. When writing your backend and
88

99
The `$env/static/private` and `$env/dynamic/private` modules, which are covered in the [modules](modules) section, can only be imported into modules that only run on the server, such as [`hooks.server.js`](hooks#server-hooks) or [`+page.server.js`](routing#page-page-server-js).
1010

11+
## Server-only utilities
12+
13+
The [`$app/server`](/docs/modules#$app-server) module, which contains a `read` function for reading assets from the filesystem, can likewise only be imported by code that runs on the server.
14+
1115
## Your modules
1216

1317
You can make your own modules server-only in two ways:

packages/adapter-netlify/index.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import toml from '@iarna/toml';
2727
* }} HandlerManifest
2828
*/
2929

30+
const name = '@sveltejs/adapter-netlify';
3031
const files = fileURLToPath(new URL('./files', import.meta.url).href);
3132

3233
const edge_set_in_env_var =
@@ -38,7 +39,7 @@ const FUNCTION_PREFIX = 'sveltekit-';
3839
/** @type {import('./index.js').default} */
3940
export default function ({ split = false, edge = edge_set_in_env_var } = {}) {
4041
return {
41-
name: '@sveltejs/adapter-netlify',
42+
name,
4243

4344
async adapt(builder) {
4445
if (!builder.routes) {
@@ -92,6 +93,19 @@ export default function ({ split = false, edge = edge_set_in_env_var } = {}) {
9293
} else {
9394
await generate_lambda_functions({ builder, split, publish });
9495
}
96+
},
97+
98+
supports: {
99+
// reading from the filesystem only works in serverless functions
100+
read: ({ route }) => {
101+
if (edge) {
102+
throw new Error(
103+
`${name}: Cannot use \`read\` from \`$app/server\` in route \`${route.id}\` when using edge functions`
104+
);
105+
}
106+
107+
return true;
108+
}
95109
}
96110
};
97111
}

packages/adapter-netlify/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,6 @@
5151
"vitest": "^1.2.0"
5252
},
5353
"peerDependencies": {
54-
"@sveltejs/kit": "^2.0.0"
54+
"@sveltejs/kit": "^2.4.0"
5555
}
5656
}

packages/adapter-netlify/src/serverless.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import './shims';
22
import { Server } from '0SERVER';
33
import { split_headers } from './headers.js';
4+
import { createReadableStream } from '@sveltejs/kit/node';
45

56
/**
67
* @param {import('@sveltejs/kit').SSRManifest} manifest
@@ -10,7 +11,8 @@ export function init(manifest) {
1011
const server = new Server(manifest);
1112

1213
let init_promise = server.init({
13-
env: process.env
14+
env: process.env,
15+
read: (file) => createReadableStream(`.netlify/server/${file}`)
1416
});
1517

1618
return async (event, context) => {

packages/adapter-node/ambient.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ declare module 'HANDLER' {
99
declare module 'MANIFEST' {
1010
import { SSRManifest } from '@sveltejs/kit';
1111

12+
export const base: string;
1213
export const manifest: SSRManifest;
1314
export const prerendered: Set<string>;
1415
}

packages/adapter-node/index.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ export default function (opts = {}) {
3939

4040
writeFileSync(
4141
`${tmp}/manifest.js`,
42-
`export const manifest = ${builder.generateManifest({ relativePath: './' })};\n\n` +
43-
`export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n`
42+
[
43+
`export const manifest = ${builder.generateManifest({ relativePath: './' })};`,
44+
`export const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});`,
45+
`export const base = ${JSON.stringify(builder.config.kit.paths.base)};`
46+
].join('\n\n')
4447
);
4548

4649
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
@@ -86,6 +89,10 @@ export default function (opts = {}) {
8689
ENV_PREFIX: JSON.stringify(envPrefix)
8790
}
8891
});
92+
},
93+
94+
supports: {
95+
read: () => true
8996
}
9097
};
9198
}

packages/adapter-node/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,6 @@
5050
"rollup": "^4.9.5"
5151
},
5252
"peerDependencies": {
53-
"@sveltejs/kit": "^2.0.0"
53+
"@sveltejs/kit": "^2.4.0"
5454
}
5555
}

packages/adapter-node/src/handler.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import path from 'node:path';
44
import sirv from 'sirv';
55
import { fileURLToPath } from 'node:url';
66
import { parse as polka_url_parser } from '@polka/url';
7-
import { getRequest, setResponse } from '@sveltejs/kit/node';
7+
import { getRequest, setResponse, createReadableStream } from '@sveltejs/kit/node';
88
import { Server } from 'SERVER';
9-
import { manifest, prerendered } from 'MANIFEST';
9+
import { manifest, prerendered, base } from 'MANIFEST';
1010
import { env } from 'ENV';
1111

1212
/* global ENV_PREFIX */
1313

1414
const server = new Server(manifest);
15-
await server.init({ env: process.env });
15+
1616
const origin = env('ORIGIN', undefined);
1717
const xff_depth = parseInt(env('XFF_DEPTH', '1'));
1818
const address_header = env('ADDRESS_HEADER', '').toLowerCase();
@@ -29,6 +29,13 @@ if (isNaN(body_size_limit)) {
2929

3030
const dir = path.dirname(fileURLToPath(import.meta.url));
3131

32+
const asset_dir = `${dir}/client${base}`;
33+
34+
await server.init({
35+
env: process.env,
36+
read: (file) => createReadableStream(`${asset_dir}/${file}`)
37+
});
38+
3239
/**
3340
* @param {string} path
3441
* @param {boolean} client

packages/adapter-vercel/files/serverless.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { installPolyfills } from '@sveltejs/kit/node/polyfills';
2-
import { getRequest, setResponse } from '@sveltejs/kit/node';
2+
import { getRequest, setResponse, createReadableStream } from '@sveltejs/kit/node';
33
import { Server } from 'SERVER';
44
import { manifest } from 'MANIFEST';
55

@@ -8,7 +8,8 @@ installPolyfills();
88
const server = new Server(manifest);
99

1010
await server.init({
11-
env: /** @type {Record<string, string>} */ (process.env)
11+
env: /** @type {Record<string, string>} */ (process.env),
12+
read: createReadableStream
1213
});
1314

1415
const DATA_SUFFIX = '/__data.json';

packages/adapter-vercel/index.js

+25-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { nodeFileTrace } from '@vercel/nft';
55
import esbuild from 'esbuild';
66
import { get_pathname } from './utils.js';
77

8+
const name = '@sveltejs/adapter-vercel';
89
const DEFAULT_FUNCTION_NAME = 'fn';
910

1011
const get_default_runtime = () => {
@@ -24,7 +25,7 @@ const plugin = function (defaults = {}) {
2425
}
2526

2627
return {
27-
name: '@sveltejs/adapter-vercel',
28+
name,
2829

2930
async adapt(builder) {
3031
if (!builder.routes) {
@@ -63,6 +64,8 @@ const plugin = function (defaults = {}) {
6364
* @param {import('@sveltejs/kit').RouteDefinition<import('.').Config>[]} routes
6465
*/
6566
async function generate_serverless_function(name, config, routes) {
67+
const dir = `${dirs.functions}/${name}.func`;
68+
6669
const relativePath = path.posix.relative(tmp, builder.getServerDirectory());
6770

6871
builder.copy(`${files}/serverless.js`, `${tmp}/index.js`, {
@@ -77,12 +80,12 @@ const plugin = function (defaults = {}) {
7780
`export const manifest = ${builder.generateManifest({ relativePath, routes })};\n`
7881
);
7982

80-
await create_function_bundle(
81-
builder,
82-
`${tmp}/index.js`,
83-
`${dirs.functions}/${name}.func`,
84-
config
85-
);
83+
await create_function_bundle(builder, `${tmp}/index.js`, dir, config);
84+
85+
for (const asset of builder.findServerAssets(routes)) {
86+
// TODO use symlinks, once Build Output API supports doing so
87+
builder.copy(`${builder.getServerDirectory()}/${asset}`, `${dir}/${asset}`);
88+
}
8689
}
8790

8891
/**
@@ -335,6 +338,21 @@ const plugin = function (defaults = {}) {
335338
builder.log.minor('Writing routes...');
336339

337340
write(`${dir}/config.json`, JSON.stringify(static_config, null, '\t'));
341+
},
342+
343+
supports: {
344+
// reading from the filesystem only works in serverless functions
345+
read: ({ config, route }) => {
346+
const runtime = config.runtime ?? defaults.runtime;
347+
348+
if (runtime === 'edge') {
349+
throw new Error(
350+
`${name}: Cannot use \`read\` from \`$app/server\` in route \`${route.id}\` configured with \`runtime: 'edge'\``
351+
);
352+
}
353+
354+
return true;
355+
}
338356
}
339357
};
340358
};

packages/adapter-vercel/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,6 @@
4242
"vitest": "^1.2.0"
4343
},
4444
"peerDependencies": {
45-
"@sveltejs/kit": "^2.0.0"
45+
"@sveltejs/kit": "^2.4.0"
4646
}
4747
}

packages/kit/scripts/generate-dts.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ await createBundle({
1313
'$app/forms': 'src/runtime/app/forms.js',
1414
'$app/navigation': 'src/runtime/app/navigation.js',
1515
'$app/paths': 'src/runtime/app/paths/types.d.ts',
16+
'$app/server': 'src/runtime/app/server/index.js',
1617
'$app/stores': 'src/runtime/app/stores.js'
1718
},
1819
include: ['src']

packages/kit/src/core/adapt/builder.js

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { get_env } from '../../exports/vite/utils.js';
1111
import generate_fallback from '../postbuild/fallback.js';
1212
import { write } from '../sync/utils.js';
1313
import { list_files } from '../utils.js';
14+
import { find_server_assets } from '../generate_manifest/find_server_assets.js';
1415

1516
const pipe = promisify(pipeline);
1617
const extensions = ['.html', '.js', '.mjs', '.json', '.css', '.svg', '.xml', '.wasm'];
@@ -145,6 +146,13 @@ export function create_builder({
145146
}
146147
},
147148

149+
findServerAssets(route_data) {
150+
return find_server_assets(
151+
build_data,
152+
route_data.map((route) => /** @type {import('types').RouteData} */ (lookup.get(route)))
153+
);
154+
},
155+
148156
async generateFallback(dest) {
149157
const manifest_path = `${config.kit.outDir}/output/server/manifest-full.js`;
150158
const env = get_env(config.kit.env, vite_config.mode);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { find_deps } from '../../exports/vite/build/utils.js';
2+
3+
/**
4+
* Finds all the assets that are imported by server files associated with `routes`
5+
* @param {import('types').BuildData} build_data
6+
* @param {import('types').RouteData[]} routes
7+
*/
8+
export function find_server_assets(build_data, routes) {
9+
/**
10+
* All nodes actually used in the routes definition (prerendered routes are omitted).
11+
* Root layout/error is always included as they are needed for 404 and root errors.
12+
* @type {Set<any>}
13+
*/
14+
const used_nodes = new Set([0, 1]);
15+
16+
// TODO add hooks.server.js asset imports
17+
/** @type {Set<string>} */
18+
const server_assets = new Set();
19+
20+
/** @param {string} id */
21+
function add_assets(id) {
22+
if (id in build_data.server_manifest) {
23+
const deps = find_deps(build_data.server_manifest, id, false);
24+
for (const asset of deps.assets) {
25+
server_assets.add(asset);
26+
}
27+
}
28+
}
29+
30+
for (const route of routes) {
31+
if (route.page) {
32+
for (const i of route.page.layouts) used_nodes.add(i);
33+
for (const i of route.page.errors) used_nodes.add(i);
34+
used_nodes.add(route.page.leaf);
35+
}
36+
37+
if (route.endpoint) {
38+
add_assets(route.endpoint.file);
39+
}
40+
}
41+
42+
for (const n of used_nodes) {
43+
const node = build_data.manifest_data.nodes[n];
44+
if (node?.server) add_assets(node.server);
45+
}
46+
47+
if (build_data.manifest_data.hooks.server) {
48+
add_assets(build_data.manifest_data.hooks.server);
49+
}
50+
51+
return Array.from(server_assets);
52+
}

0 commit comments

Comments
 (0)