Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add vercel-static and netlify-static presets #1073

Merged
merged 13 commits into from
Apr 15, 2023
3 changes: 2 additions & 1 deletion src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,10 @@ async function _build(nitro: Nitro, rollupConfig: RollupConfig) {
await generateFSTree(nitro.options.output.serverDir)
);
}
await nitro.hooks.callHook("compiled", nitro);
}

await nitro.hooks.callHook("compiled", nitro);

// Show deploy and preview hints
const rOutput = relative(process.cwd(), nitro.options.output.dir);
const rewriteRelativePaths = (input: string) => {
Expand Down
5 changes: 2 additions & 3 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ export async function loadOptions(
// Preset
let presetOverride =
(configOverrides.preset as string) || process.env.NITRO_PRESET;
const defaultPreset = detectTarget() || "node-server";
if (configOverrides.dev) {
presetOverride = "nitro-dev";
}
Expand All @@ -126,7 +125,7 @@ export async function loadOptions(
preset: presetOverride,
},
defaultConfig: {
preset: defaultPreset,
preset: detectTarget() || "node-server",
},
defaults: NitroDefaults,
resolve(id: string) {
Expand All @@ -149,7 +148,7 @@ export async function loadOptions(
options.preset =
presetOverride ||
(layers.find((l) => l.config.preset)?.config.preset as string) ||
defaultPreset;
(detectTarget({ static: !options.build }) ?? "node-server");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking since we didn't release nitropack yet, would it made sense that we introduced static: boolean flag instead of top level build flag? It is mainly for sake of static support we introduced flag...

Copy link
Member Author

@danielroe danielroe Apr 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that works πŸ‘ only downside is that it's less accurate - build: false is precisely what the flag does. The rest is enabled only by static presets.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you still prefer top-level static? very happy to change it if, on reflection, you feel that's the best.


options.rootDir = resolve(options.rootDir || ".");
options.workspaceDir = await findWorkspaceDir(options.rootDir).catch(
Expand Down
45 changes: 35 additions & 10 deletions src/presets/netlify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,48 @@ export const netlifyEdge = defineNitroPreset({
},
});

export const netlifyStatic = defineNitroPreset({
extends: "static",
output: {
publicDir: "{{ rootDir }}/dist",
},
commands: {
preview: "npx serve ./static",
},
hooks: {
async compiled(nitro: Nitro) {
await writeHeaders(nitro);
await writeRedirects(nitro);
},
},
});

async function writeRedirects(nitro: Nitro) {
const redirectsPath = join(nitro.options.output.publicDir, "_redirects");
let contents = "/* /.netlify/functions/server 200";
const staticFallback = existsSync(
join(nitro.options.output.publicDir, "404.html")
)
? "/* /404.html 404"
: "";
let contents = nitro.options.build
? "/* /.netlify/functions/server 200"
: staticFallback;

const rules = Object.entries(nitro.options.routeRules).sort(
(a, b) => a[0].split(/\/(?!\*)/).length - b[0].split(/\/(?!\*)/).length
);

// Rewrite static ISR paths to builder functions
for (const [key, value] of rules.filter(
([_, value]) => value.isr !== undefined
)) {
contents = value.isr
? `${key.replace("/**", "/*")}\t/.netlify/builders/server 200\n` +
contents
: `${key.replace("/**", "/*")}\t/.netlify/functions/server 200\n` +
contents;
if (nitro.options.build) {
// Rewrite static ISR paths to builder functions
for (const [key, value] of rules.filter(
([_, value]) => value.isr !== undefined
)) {
contents = value.isr
? `${key.replace("/**", "/*")}\t/.netlify/builders/server 200\n` +
contents
: `${key.replace("/**", "/*")}\t/.netlify/functions/server 200\n` +
contents;
}
}

for (const [key, routeRules] of rules.filter(
Expand Down
112 changes: 70 additions & 42 deletions src/presets/vercel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,30 @@ export const vercelEdge = defineNitroPreset({
},
});

export const vercelStatic = defineNitroPreset({
extends: "static",
output: {
dir: "{{ rootDir }}/.vercel/output",
publicDir: "{{ output.dir }}/static",
},
commands: {
preview: "npx serve ./static",
},
hooks: {
async compiled(nitro: Nitro) {
const buildConfigPath = resolve(nitro.options.output.dir, "config.json");
const buildConfig = generateBuildConfig(nitro);
await writeFile(buildConfigPath, JSON.stringify(buildConfig, null, 2));
},
},
});

function generateBuildConfig(nitro: Nitro) {
const rules = Object.entries(nitro.options.routeRules).sort(
(a, b) => b[0].split(/\/(?!\*)/).length - a[0].split(/\/(?!\*)/).length
);

return defu(nitro.options.vercel?.config, <VercelBuildConfigV3>{
const config = defu(nitro.options.vercel?.config, <VercelBuildConfigV3>{
version: 3,
overrides: {
// Nitro static prerendered route overrides
Expand Down Expand Up @@ -163,50 +181,60 @@ function generateBuildConfig(nitro: Nitro) {
continue: true,
})),
{ handle: "filesystem" },
// ISR rules
...rules
.filter(
([key, value]) =>
// value.isr === false || (value.isr && key.includes("/**"))
value.isr !== undefined
)
.map(([key, value]) => {
const src = key.replace(/^(.*)\/\*\*/, "(?<url>$1/.*)");
if (value.isr === false) {
// we need to write a rule to avoid route being shadowed by another cache rule elsewhere
return {
src,
dest: "/__nitro",
};
}
],
});

// Early return if we are not building a serverless function
if (!nitro.options.build) {
return config;
}

config.routes.push(
// ISR rules
...rules
.filter(
([key, value]) =>
// value.isr === false || (value.isr && key.includes("/**"))
value.isr !== undefined
)
.map(([key, value]) => {
const src = key.replace(/^(.*)\/\*\*/, "(?<url>$1/.*)");
if (value.isr === false) {
// we need to write a rule to avoid route being shadowed by another cache rule elsewhere
return {
src,
dest:
nitro.options.preset === "vercel-edge"
? "/__nitro?url=$url"
: generateEndpoint(key) + "?url=$url",
dest: "/__nitro",
};
}),
// If we are using an ISR function for /, then we need to write this explicitly
...(nitro.options.routeRules["/"]?.isr
? [
{
src: "(?<url>/)",
dest: "/__nitro-index",
},
]
: []),
// If we are using an ISR function as a fallback, then we do not need to output the below fallback route as well
...(!nitro.options.routeRules["/**"]?.isr
? [
{
src: "/(.*)",
dest: "/__nitro",
},
]
: []),
],
});
}
return {
src,
dest:
nitro.options.preset === "vercel-edge"
? "/__nitro?url=$url"
: generateEndpoint(key) + "?url=$url",
};
}),
// If we are using an ISR function for /, then we need to write this explicitly
...(nitro.options.routeRules["/"]?.isr
? [
{
src: "(?<url>/)",
dest: "/__nitro-index",
},
]
: []),
// If we are using an ISR function as a fallback, then we do not need to output the below fallback route as well
...(!nitro.options.routeRules["/**"]?.isr
? [
{
src: "/(.*)",
dest: "/__nitro",
},
]
: [])
);

return config;
}

function generateEndpoint(url: string) {
Expand Down
13 changes: 11 additions & 2 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,17 @@ const autodetectableProviders: Partial<
cleavr: "cleavr",
};

export function detectTarget() {
return autodetectableProviders[provider];
const autodetectableStaticProviders: Partial<
Record<ProviderName, KebabCase<keyof typeof _PRESETS>>
> = {
netlify: "netlify-static",
vercel: "vercel-static",
};

export function detectTarget(options: { static?: boolean } = {}) {
return options?.static
? autodetectableStaticProviders[provider]
: autodetectableProviders[provider];
}

export async function isDirectory(path: string) {
Expand Down