From 6181a848e7c14afd59139dcf58f9282d64347890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Thu, 10 Apr 2025 10:12:40 +0100 Subject: [PATCH] feat: add API cache + 404 page --- package.json | 4 +- scripts/postbuild.mjs | 10 +++ src/commands/base-command.ts | 13 +++- src/lib/templates/404.html | 103 ++++++++++++++++++++++++++++ src/utils/detect-server-settings.ts | 22 +++++- src/utils/static-server.ts | 20 +++++- 6 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 scripts/postbuild.mjs create mode 100644 src/lib/templates/404.html diff --git a/package.json b/package.json index fcb5bfdf2a8..bb33e97350c 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "scripts": { "prepare": "is-ci || husky install node_modules/@netlify/eslint-config-node/.husky/", "start": "node ./bin/run.js", - "build": "tsc", + "build": "run-s build:ts build:post", + "build:ts": "tsc", + "build:post": "node scripts/postbuild.mjs", "dev": "tsc --watch", "test": "run-s format test:dev", "format": "run-s format:check-fix:*", diff --git a/scripts/postbuild.mjs b/scripts/postbuild.mjs new file mode 100644 index 00000000000..bb475584b42 --- /dev/null +++ b/scripts/postbuild.mjs @@ -0,0 +1,10 @@ +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const cwd = path.dirname(fileURLToPath(import.meta.url)) + +const srcPath = path.resolve(cwd, '../src/lib/templates') +const destPath = path.resolve(cwd, '../dist/lib/templates') + +fs.cpSync(srcPath, destPath, { recursive: true }) diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 592f464b755..aac1e0be9b7 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -16,9 +16,10 @@ import inquirer from 'inquirer' // @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'inqu... Remove this comment to see the full error message import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt' import merge from 'lodash/merge.js' -import { NetlifyAPI } from 'netlify' +import { NetlifyAPI, APICache } from 'netlify' import { getAgent } from '../lib/http-agent.js' +import { getPathInHome } from '../lib/settings.js' import { NETLIFY_CYAN, USER_AGENT, @@ -77,6 +78,13 @@ const COMMANDS_WITHOUT_WORKSPACE_OPTIONS = new Set(['api', 'recipes', 'completio */ const COMMANDS_WITH_FEATURE_FLAGS = new Set(['build', 'dev', 'deploy']) +const apiCache = new APICache({ + fsPath: getPathInHome(['api-cache']), + ttl: 30_000, + // swr: 3_600_000, + swr: Infinity, +}) + /** Formats a help list correctly with the correct indent */ const formatHelpList = (textArray: string[]) => textArray.join('\n').replace(/^/gm, ' '.repeat(HELP_INDENT_WIDTH)) @@ -566,7 +574,7 @@ export default class BaseCommand extends Command { httpProxy: flags.httpProxy, certificateFile: flags.httpProxyCertificateFilename, }) - const apiOpts = { ...apiUrlOpts, agent } + const apiOpts = { ...apiUrlOpts, agent, cache: apiCache } // TODO: remove typecast once we have proper types for the API const api = new NetlifyAPI(token || '', apiOpts) as NetlifyOptions['api'] @@ -697,6 +705,7 @@ export default class BaseCommand extends Command { try { return await resolveConfig({ accountId: this.accountId, + apiCache, config: config.configFilePath, packagePath: config.packagePath, repositoryRoot: config.repositoryRoot, diff --git a/src/lib/templates/404.html b/src/lib/templates/404.html new file mode 100644 index 00000000000..6051fee9d07 --- /dev/null +++ b/src/lib/templates/404.html @@ -0,0 +1,103 @@ + + + + + + Page Not Found + + + +
+
+
+

Page Not Found

+
+
+

Looks like you've followed a broken link or entered a URL that doesn't exist on this site.

+ +

If this is a brand new site, you can add files to the netlify/publish directory or configure a custom publish directory. +

+
+
+
+ + + + \ No newline at end of file diff --git a/src/utils/detect-server-settings.ts b/src/utils/detect-server-settings.ts index 4de315cced1..bb24bc5c8ee 100644 --- a/src/utils/detect-server-settings.ts +++ b/src/utils/detect-server-settings.ts @@ -1,3 +1,4 @@ +import { statSync } from 'fs' import { readFile } from 'fs/promises' import { EOL } from 'os' import { dirname, relative, resolve } from 'path' @@ -92,9 +93,26 @@ const DEFAULT_STATIC_PORT = 3999 * Logs a message that it was unable to determine the dist directory and falls back to the workingDir */ const getDefaultDist = (workingDir: string) => { + const netlifyPublishPath = resolve(workingDir, 'netlify', 'publish') + + try { + const stat = statSync(netlifyPublishPath) + + if (stat.isDirectory()) { + log(`${NETLIFYDEVWARN} Unable to determine public folder to serve files from. Using current working directory`) + log(` Setup a netlify.toml file with a [dev] section to specify your dev server settings.`) + log(` https://docs.netlify.com/cli/local-development/#project-detection`) + + return netlifyPublishPath + } + } catch { + // no-op + } + log(`${NETLIFYDEVWARN} Unable to determine public folder to serve files from. Using current working directory`) - log(`${NETLIFYDEVWARN} Setup a netlify.toml file with a [dev] section to specify your dev server settings.`) - log(`${NETLIFYDEVWARN} See docs at: https://docs.netlify.com/cli/local-development/#project-detection`) + log(` Setup a netlify.toml file with a [dev] section to specify your dev server settings.`) + log(` https://docs.netlify.com/cli/local-development/#project-detection`) + return workingDir } diff --git a/src/utils/static-server.ts b/src/utils/static-server.ts index b9f4a348255..9b26f5d4e68 100644 --- a/src/utils/static-server.ts +++ b/src/utils/static-server.ts @@ -1,10 +1,15 @@ +import { statSync } from 'fs' import path from 'path' +import { fileURLToPath } from 'url' import fastifyStatic from '@fastify/static' import Fastify from 'fastify' import { log, NETLIFYDEVLOG } from './command-helpers.js' +const cwd = path.dirname(fileURLToPath(import.meta.url)) +const default404Template = path.resolve(cwd, '../lib/templates/404.html') + /** * @param {object} config * @param {import('./types.js').ServerSettings} config.settings @@ -21,7 +26,20 @@ export const startStaticServer = async ({ settings }) => { }) server.setNotFoundHandler((_req, res) => { - res.code(404).sendFile('404.html', rootPath) + let pagePath = default404Template + + try { + const custom404Path = path.join(settings.dist, '404.html') + const stats = statSync(custom404Path) + + if (stats.isFile()) { + pagePath = custom404Path + } + } catch { + // no-op + } + + res.code(404).sendFile(path.basename(pagePath), path.dirname(pagePath)) }) server.addHook('onRequest', (req, reply, done) => {