Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const tracePkgs = [
"defu", // used by open-api runtime
"destr", // used by node-server and deno-server
"get-port-please", // used by dev server
"mime", // used by dev public assets runtime
"rendu", // used by HTML renderer template
"scule", // used by runtime config
"source-map", // used by dev error runtime
Expand Down
67 changes: 67 additions & 0 deletions src/build/virtual/public-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,73 @@ export default function publicAssets(nitro: Nitro) {
])
);

if (nitro.options.dev) {
const publicAssetDirs = nitro.options.publicAssets.map((dir) => ({
baseURL: withTrailingSlash(joinURL(nitro.options.baseURL, dir.baseURL || "/")),
dir: dir.dir,
}));

return /* js */ `
import { statSync, promises as fsp } from 'node:fs'
import { resolve, relative, isAbsolute, sep } from 'node:path'
import mime from 'mime'

const publicAssetDirs = ${JSON.stringify(publicAssetDirs)}
export const publicAssetBases = ${JSON.stringify(publicAssetBases)}

export function isPublicAssetURL(id = '') {
if (getAsset(id)) {
return true
}
for (const base in publicAssetBases) {
if (id.startsWith(base)) { return true }
}
return false
}

export function getPublicAssetMeta(id = '') {
for (const base in publicAssetBases) {
if (id.startsWith(base)) { return publicAssetBases[base] }
}
return {}
}

export function getAsset (id) {
for (const { baseURL, dir } of publicAssetDirs) {
if (!id.startsWith(baseURL)) { continue }
const fullPath = resolve(dir, id.slice(baseURL.length))
const relativePath = relative(dir, fullPath)
if (relativePath.split(sep)[0] === '..' || isAbsolute(relativePath)) { continue }
let stat
try {
stat = statSync(fullPath)
} catch {
continue
}
if (!stat.isFile()) { continue }
let type = mime.getType(id.replace(/\\.(gz|br|zst)$/, '')) || 'text/plain'
if (type.startsWith('text')) { type += '; charset=utf-8' }
let encoding
if (id.endsWith('.gz')) { encoding = 'gzip' }
else if (id.endsWith('.br')) { encoding = 'br' }
else if (id.endsWith('.zst')) { encoding = 'zstd' }
return {
type,
encoding,
mtime: stat.mtime.toJSON(),
size: stat.size,
path: fullPath,
}
}
}

export function readAsset (id) {
const asset = getAsset(id)
return asset ? fsp.readFile(asset.path) : Promise.resolve(null)
}
`;
}

// prettier-ignore
type _serveStaticAsKey = Exclude<typeof nitro.options.serveStatic, boolean> | "true" | "false";
// prettier-ignore
Expand Down
1 change: 1 addition & 0 deletions src/runtime/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const runtimeDependencies: string[] = [
"h3", // dep
"rou3", // sub-dep of h3
"hookable", // traced
"mime", // traced
"ocache", // dep
"ohash", // traced
"rendu", // traced
Expand Down
9 changes: 9 additions & 0 deletions test/fixture/server/routes/fetch-public-asset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { serverFetch } from "nitro";

export default async () => {
const res = await serverFetch("/build/test.txt");
return {
status: res.status,
body: await res.text(),
};
};
14 changes: 14 additions & 0 deletions test/presets/nitro-dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ import type { OpenAPI3 } from "../../src/types/openapi-ts.ts";
import { describe, expect, it } from "vitest";
import { setupTest, testNitro } from "../tests.ts";

describe("nitro:preset:nitro-dev (serve static)", async () => {
const ctx = await setupTest("nitro-dev", {
config: { serveStatic: true },
outDirSuffix: "-serve-static",
});

it("serves public assets via internal fetch", async () => {
const res = await ctx.fetch("/fetch-public-asset");
const data = await res.json();
expect(data.status).toBe(200);
expect(data.body).toBe("Works!\n");
});
});

describe("nitro:preset:nitro-dev", async () => {
const ctx = await setupTest("nitro-dev");
testNitro(
Expand Down
5 changes: 5 additions & 0 deletions test/presets/vercel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,10 @@ describe("nitro:preset:vercel:web", async () => {
"dest": "/file",
"src": "/file",
},
{
"dest": "/fetch-public-asset",
"src": "/fetch-public-asset",
},
{
"dest": "/fetch",
"src": "/fetch",
Expand Down Expand Up @@ -524,6 +528,7 @@ describe("nitro:preset:vercel:web", async () => {
"functions/errors/captured.func (symlink)",
"functions/errors/stack.func (symlink)",
"functions/errors/throw.func (symlink)",
"functions/fetch-public-asset.func (symlink)",
"functions/fetch.func (symlink)",
"functions/file.func (symlink)",
"functions/icon.png.func (symlink)",
Expand Down
8 changes: 7 additions & 1 deletion test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export async function setupTest(
if (ctx.isDev) {
// Setup development server
const devServer = createDevServer(ctx.nitro);
const server = await devServer.listen({});
const server = await devServer.listen({ port: 0 });
ctx.server = {
url: server.url!,
close: () => server.close(),
Expand Down Expand Up @@ -481,6 +481,12 @@ export function testNitro(
expect(headers["content-type"]).toBe("text/plain; charset=utf-8");
});

it("serve static asset via internal fetch", async () => {
const { data } = await callHandler({ url: "/fetch-public-asset" });
expect(data.status).toBe(200);
expect(data.body).toBe("Works!\n");
});

it("stores content-type for prerendered routes", async () => {
const { data, headers } = await callHandler({
url: "/api/param/prerender4",
Expand Down