Skip to content

Commit

Permalink
perf(netlify-legacy): exclude static paths from SSR function
Browse files Browse the repository at this point in the history
Backporting #2822 to the netlify-legacy preset.

Since it's using "v1" Netlify Functions
(https://docs.netlify.com/functions/lambda-compatibility), it doesn't have access to
`excludedPath` and `preferStatic`, so we implement this with redirects.
  • Loading branch information
serhalp committed Oct 31, 2024
1 parent e3a2d6f commit 9d88e76
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 6 deletions.
4 changes: 2 additions & 2 deletions src/presets/netlify/legacy/preset.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync, promises as fsp } from "node:fs";
import { promises as fsp } from "node:fs";
import { defineNitroPreset } from "nitropack/kit";
import type { Nitro } from "nitropack/types";
import { dirname, join } from "pathe";
Expand All @@ -24,7 +24,7 @@ const netlify = defineNitroPreset(
},
async compiled(nitro: Nitro) {
await writeHeaders(nitro);
await writeRedirects(nitro);
await writeRedirects(nitro, "/.netlify/functions/server");

if (nitro.options.netlify) {
const configPath = join(
Expand Down
35 changes: 31 additions & 4 deletions src/presets/netlify/legacy/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
import { existsSync, promises as fsp } from "node:fs";
import type { Nitro } from "nitropack/types";
import type { Nitro, PublicAssetDir } from "nitropack/types";
import { join } from "pathe";
import { joinURL } from "ufo";

export async function writeRedirects(nitro: Nitro) {
export function generateCatchAllRedirects(
publicAssets: PublicAssetDir[],
catchAllPath?: string
): string {
if (!catchAllPath) return "";

return [
// e.g.: /_nuxt/* /_nuxt/:splat 200
// Because of Netlify CDN shadowing
// (https://docs.netlify.com/routing/redirects/rewrites-proxies/#shadowing),
// this config avoids function invocations for all static paths, even 404s.
...getStaticPaths(publicAssets).map(
(path) => `${path} ${path.replace("/*", "/:splat")} 200`
),
`/* ${catchAllPath} 200`,
].join("\n");
}

export async function writeRedirects(nitro: Nitro, catchAllPath?: string) {
const redirectsPath = join(nitro.options.output.publicDir, "_redirects");
const staticFallback = existsSync(
join(nitro.options.output.publicDir, "404.html")
Expand All @@ -11,7 +30,7 @@ export async function writeRedirects(nitro: Nitro) {
: "";
let contents = nitro.options.static
? staticFallback
: "/* /.netlify/functions/server 200";
: generateCatchAllRedirects(nitro.options.publicAssets, catchAllPath);

const rules = Object.entries(nitro.options.routeRules).sort(
(a, b) => a[0].split(/\/(?!\*)/).length - b[0].split(/\/(?!\*)/).length
Expand Down Expand Up @@ -103,12 +122,20 @@ export async function writeHeaders(nitro: Nitro) {
await fsp.writeFile(headersPath, contents);
}

export function getStaticPaths(publicAssets: PublicAssetDir[]): string[] {
return publicAssets
.filter(
(dir) => dir.fallthrough !== true && dir.baseURL && dir.baseURL !== "/"
)
.map((dir) => joinURL("/", dir.baseURL!, "*"));
}

export function deprecateSWR(nitro: Nitro) {
if (nitro.options.future.nativeSWR) {
return;
}
let hasLegacyOptions = false;
for (const [key, value] of Object.entries(nitro.options.routeRules)) {
for (const [_key, value] of Object.entries(nitro.options.routeRules)) {
if (_hasProp(value, "isr")) {
continue;
}
Expand Down
8 changes: 8 additions & 0 deletions test/presets/netlify-legacy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ describe("nitro:preset:netlify-legacy", async () => {
const ctx = await setupTest("netlify-legacy", {
compatibilityDate: "2024-01-01",
config: {
publicAssets: [
{
dir: "dist/_nuxt",
baseURL: "_nuxt",
},
],
output: {
publicDir: resolve(getPresetTmpDir("netlify-legacy"), "dist"),
},
Expand Down Expand Up @@ -70,6 +76,8 @@ describe("nitro:preset:netlify-legacy", async () => {
/rules/isr-ttl/* /.netlify/builders/server 200
/rules/isr/* /.netlify/builders/server 200
/rules/dynamic /.netlify/functions/server 200
/_nuxt/* /_nuxt/:splat 200
/build/* /build/:splat 200
/* /.netlify/functions/server 200"
`);
});
Expand Down
61 changes: 61 additions & 0 deletions test/unit/netlify-legacy.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest";
import { generateCatchAllRedirects } from "../../src/presets/netlify/legacy/utils";

describe("generateCatchAllRedirects", () => {
it("returns empty string if `catchAllPath` is not defined", () => {
expect(generateCatchAllRedirects([], undefined)).toEqual("");
});

it("includes a redirect from `/*` to `catchAllPath` if defined", () => {
expect(generateCatchAllRedirects([], "/catch-all")).toEqual(
"/* /catch-all 200"
);
});

it("includes a splat redirect for each non-fallthrough non-root public asset path, BEFORE the catch-all", () => {
const publicAssets = [
{
fallthrough: true,
baseURL: "with-fallthrough",
dir: "with-fallthrough-dir",
maxAge: 0,
},
{
fallthrough: true,
dir: "with-fallthrough-no-baseURL-dir",
maxAge: 0,
},
{
fallthrough: false,
dir: "no-fallthrough-no-baseURL-dir",
maxAge: 0,
},
{
fallthrough: false,
dir: "no-fallthrough-root-baseURL-dir",
baseURL: "/",
maxAge: 0,
},
{
baseURL: "with-default-fallthrough",
dir: "with-default-fallthrough-dir",
maxAge: 0,
},
{
fallthrough: false,
baseURL: "nested/no-fallthrough",
dir: "nested/no-fallthrough-dir",
maxAge: 0,
},
];
expect(
generateCatchAllRedirects(publicAssets, "/catch-all")
).toMatchInlineSnapshot(
`
"/with-default-fallthrough/* /with-default-fallthrough/:splat 200
/nested/no-fallthrough/* /nested/no-fallthrough/:splat 200
/* /catch-all 200"
`.trim()
);
});
});

0 comments on commit 9d88e76

Please sign in to comment.