SvelteKit Filesystem Read Feature
| Published: January 20, 2024
| Updated: January 20, 2024
Rich Harris just announced a new feature to SvelteKit which will provide an improved approach to returning assets from your filesystem. The API is dead simple and it also means handling dynamic assets can now be included in the build process.
In the official docs, the explanation of the new read
function is limited to one line:
Read the contents of an imported asset from the filesystem
import { read } from '$app/server';
function read(asset: string): Response;
At a glance, it might not mean a whole lot but this article will outline why this is a brilliant improvement, and give examples where you can include it in your project.
The example shown in this article is based on the Rich Harris fowl-play showcase project. He also walks through some of the steps in this video:
Before the update
Dealing with the node filesystem (fs
) has always been a bane for many node.js developers. The unnecessary complexity and unexpected behaviors have lead Rich and the Svelte team to work on a better way to return files in SvelteKit.
Before the new read
feature in SvelteKit, you would typically use fs.readFileSync()
to return files. But the problem with that is you’re buffering the file to memory before you can do anything with it. And when you deal with large files, this becomes a problem.
Alternatively, you could stream the file using fs.createReadStream()
like this:
reader = fs.createReadStream('input.txt');
reader.on('data', function (chunk) {
console.log(chunk.toString());
});
But this has hairs on it and it’s more complex than necessary.
If you wanted to have dynamic assets in your app, it was usually easier to set up a storage bucket, store the URL’s and just present those to the user. But if this wasn’t an option, you would have to find a fairly verbose solution that would work with the node fs
system.
In order to simply simulate this dynamic file problem, we’ll use an example from the fowl-play project by Rich Harris, the creator of this feature.
In this showcase project, the app is a single page with a single img
tag:
<img src="/duckduckgoose.jpg" alt="Either a duck, a duck or a goose" />
The img
references the directory: src/routes/duckduckgoose.jpg
(which is a folder - despite the .jpg
extension), which contains 2 images (duck.jpg
, and goose.jpg
), and a +server.js
file.
The +server.js
file runs a simple randomness function, and either returns the duck.jpg
file, or the goose.jpg
file from the filesystem using fs.readFileSync()
.
import fs from 'node:fs';
export function GET() {
const bird = Math.random() < 2 / 3 ? 'duck' : 'goose';
const data = fs.readFileSync(`src/routes/duckduckgoose.jpg/${bird}.jpg`);
return new Response(data, {
headers: {
'Content-Type': 'image/jpeg',
'Content-Length': data.length.toString()
}
});
}
When the img
tag on the page references src="/duckduckgoose.jpg"
, it’s not exactly referencing any specific image - but rather this GET
endpoint in the +server.js
file above. So when the application goes through the build process, neither the duck.jpg
file nor the goose.jpg
file will be included.
So this is to say, unless something is directly referencing a file in the application, it won’t be included in the build stage, and therefore your deployed app won’t be able to read the files you think are there. You’ll get an error because the img
tag doesn’t point to anything - the directory doesn’t exist.
Note: Due to the lack of optimizations in the dev environment, this will work on your localhost
. It just doesn’t work in build
versions, i.e. deployed versions.
With the new update
The SvelteKit team have managed to fix this issue, and the new read
function makes it :
import duck from './duck.jpg';
import goose from './goose.jpg';
import { read } from '$app/server';
export function GET() {
const bird = Math.random() < 2 / 3 ? duck : goose;
return read(bird);
}
And now, using the same randomness function, the app now works because the two duck.jpg
and goose.jpg
now form part of the application, in the build process.
This will work in all node environments - both developer
and build
.
Why this is great
TL;DR
- You won’t have to buffer the file to memory before sending in to the client,
- You can access dynamic imports,
- It’s simple, and you don’t have to deal with the seemingly infinite amount of
fs
read alternatives (each with its own shortcoming).
There’s a few reasons I ❤️ this implementation.
Firstly, dealing with the filesystem is a pain. There always seems to be quirks that take unnecessary amounts of time to fix.
In the first example where we used fs.readFileSync()
, you’re buffering the image data into memory before the response. If you’re dealing with large files, this starts to become a problem, and your computation costs are going to take a hit.
Secondly, With the read
API, you’re dealing with a gorgeous interface any lay can grasp.
return read(file);
I’m stoked for us SvelteKit developers that don’t have to go hell and back trying to deal with the filesystem anymore. 🎉
But what about those explicit imports?
Say you have a system which is dynamic and you can’t import the files explicitly like the above example.
We can remove those imports entirely, and use a blanket import to get every .jpg
in the parent directory:
import { read } from '$app/server';
const fowl = import.meta.glob('./*.jpg', {
as: 'url',
eager: true
}); // Import all files in this folder ending in .jpg
const assets = Object.values(fowl); // Array of image URLs
export function GET() {
const asset = assets[Math.floor(Math.random() * assets.length)]; // Pick a random image from the array
return read(asset); // SEND IT 😎
}
With SvelteKit/Vite, this still works. If the file is in the directory, then it gets included in your build process!
Conclusion
The overall takeaway is that handling filesystem assets in SvelteKit just got a whole lot simpler. Maybe you don’t have to offload your asset system in your app to a cloud storage bucket.
Perhaps you’ve been dealing with a long-standing bug and you couldn’t figure out how to best dynamically import files into your deployed build.
With the new read
feature, you get:
- Dynamic imports included in your build
- Cleaner code,
- Cost savings on not having to buffer files to array before sending them.
- No footguns
As I mentioned in the SvelteKit V2 article, the core Svelte team are constantly push new features that make developing with it so much better. This is just one of many awesome things to come.