diff --git a/packages/runtime/src/constants.ts b/packages/runtime/src/constants.ts index 7bc87c82df..998f25ea34 100644 --- a/packages/runtime/src/constants.ts +++ b/packages/runtime/src/constants.ts @@ -1,3 +1,5 @@ +import destr from 'destr' + export const HANDLER_FUNCTION_NAME = '___netlify-handler' export const ODB_FUNCTION_NAME = '___netlify-odb-handler' export const IMAGE_FUNCTION_NAME = '_ipx' @@ -7,18 +9,29 @@ export const HANDLER_FUNCTION_TITLE = 'Next.js SSR handler' export const ODB_FUNCTION_TITLE = 'Next.js ISR handler' export const IMAGE_FUNCTION_TITLE = 'next/image handler' // These are paths in .next that shouldn't be publicly accessible -export const HIDDEN_PATHS = [ - '/cache/*', - '/server/*', - '/serverless/*', - '/trace', - '/traces', - '/routes-manifest.json', - '/build-manifest.json', - '/prerender-manifest.json', - '/react-loadable-manifest.json', - '/BUILD_ID', -] +export const HIDDEN_PATHS = destr(process.env.NEXT_KEEP_METADATA_FILES) + ? [] + : [ + '/cache', + '/server', + '/serverless', + '/trace', + '/traces', + '/routes-manifest.json', + '/build-manifest.json', + '/prerender-manifest.json', + '/react-loadable-manifest.json', + '/BUILD_ID', + '/app-build-manifest.json', + '/app-path-routes-manifest.json', + '/export-marker.json', + '/images-manifest.json', + '/next-server.js.nft.json', + '/package.json', + '/prerender-manifest.js', + '/required-server-files.json', + '/static-manifest.json', + ] export const ODB_FUNCTION_PATH = `/.netlify/builders/${ODB_FUNCTION_NAME}` export const HANDLER_FUNCTION_PATH = `/.netlify/functions/${HANDLER_FUNCTION_NAME}` diff --git a/packages/runtime/src/helpers/files.ts b/packages/runtime/src/helpers/files.ts index 28c6e3c84e..93bf733ade 100644 --- a/packages/runtime/src/helpers/files.ts +++ b/packages/runtime/src/helpers/files.ts @@ -2,7 +2,18 @@ import { cpus } from 'os' import type { NetlifyConfig } from '@netlify/build' import { yellowBright } from 'chalk' -import { existsSync, readJson, move, copy, writeJson, readFile, writeFile, ensureDir, readFileSync } from 'fs-extra' +import { + existsSync, + readJson, + move, + copy, + writeJson, + readFile, + writeFile, + ensureDir, + readFileSync, + remove, +} from 'fs-extra' import globby from 'globby' import { PrerenderManifest } from 'next/dist/build' import { outdent } from 'outdent' @@ -10,7 +21,7 @@ import pLimit from 'p-limit' import { join, resolve, dirname } from 'pathe' import slash from 'slash' -import { MINIMUM_REVALIDATE_SECONDS, DIVIDER } from '../constants' +import { MINIMUM_REVALIDATE_SECONDS, DIVIDER, HIDDEN_PATHS } from '../constants' import { NextConfig } from './config' import { loadPrerenderManifest } from './edge' @@ -138,8 +149,8 @@ export const moveStaticPages = async ({ console.warn('Error moving file', source, error) } } - // Move all static files, except error documents and nft manifests - const pages = await globby(['{app,pages}/**/*.{html,json,rsc}', '!**/(500|404|*.js.nft).{html,json}'], { + // Move all static files, except nft manifests + const pages = await globby(['{app,pages}/**/*.{html,json,rsc}', '!**/*.js.nft.{html,json}'], { cwd: outputDir, dot: true, }) @@ -467,3 +478,15 @@ export const movePublicFiles = async ({ await copy(publicDir, `${publish}${basePath}/`) } } + +export const removeMetadataFiles = async (publish: string) => { + // Limit concurrent deletions to number of cpus or 2 if there is only 1 + const limit = pLimit(Math.max(2, cpus().length)) + + const removePromises = HIDDEN_PATHS.map((HIDDEN_PATH) => { + const pathToDelete = join(publish, HIDDEN_PATH) + return limit(() => remove(pathToDelete)) + }) + + await Promise.all(removePromises) +} diff --git a/packages/runtime/src/helpers/redirects.ts b/packages/runtime/src/helpers/redirects.ts index c7194a3b7b..8777951f4c 100644 --- a/packages/runtime/src/helpers/redirects.ts +++ b/packages/runtime/src/helpers/redirects.ts @@ -7,7 +7,7 @@ import type { PrerenderManifest, SsgRoute } from 'next/dist/build' import { outdent } from 'outdent' import { join } from 'pathe' -import { HANDLER_FUNCTION_PATH, HIDDEN_PATHS, ODB_FUNCTION_PATH } from '../constants' +import { HANDLER_FUNCTION_PATH, ODB_FUNCTION_PATH } from '../constants' import { isAppDirRoute, loadAppPathRoutesManifest } from './edge' import { getMiddleware } from './files' @@ -27,14 +27,6 @@ import { const matchesMiddleware = (middleware: Array, route: string): boolean => middleware.some((middlewarePath) => route.startsWith(middlewarePath)) -const generateHiddenPathRedirects = ({ basePath }: Pick): NetlifyConfig['redirects'] => - HIDDEN_PATHS.map((path) => ({ - from: `${basePath}${path}`, - to: '/404.html', - status: 404, - force: true, - })) - const generateLocaleRedirects = ({ i18n, basePath, @@ -281,8 +273,6 @@ export const generateRedirects = async ({ join(netlifyConfig.build.publish, 'routes-manifest.json'), ) - netlifyConfig.redirects.push(...generateHiddenPathRedirects({ basePath })) - if (i18n && i18n.localeDetection !== false) { netlifyConfig.redirects.push(...generateLocaleRedirects({ i18n, basePath, trailingSlash })) } diff --git a/packages/runtime/src/helpers/utils.ts b/packages/runtime/src/helpers/utils.ts index 7bb84478ab..386c6f91ac 100644 --- a/packages/runtime/src/helpers/utils.ts +++ b/packages/runtime/src/helpers/utils.ts @@ -170,7 +170,7 @@ export const redirectsForNext404Route = ({ }): NetlifyConfig['redirects'] => netlifyRoutesForNextRoute({ route, buildId, i18n }).map(({ redirect, locale }) => ({ from: `${basePath}${redirect}`, - to: locale ? `${basePath}/server/pages/${locale}/404.html` : `${basePath}/server/pages/404.html`, + to: locale ? `${basePath}/${locale}/404.html` : `${basePath}/404.html`, status: 404, force, })) diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 07b160e24d..e823eeebbd 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -17,7 +17,7 @@ import { } from './helpers/config' import { onPreDev } from './helpers/dev' import { writeEdgeFunctions, loadMiddlewareManifest, cleanupEdgeFunctions } from './helpers/edge' -import { moveStaticPages, movePublicFiles, patchNextFiles } from './helpers/files' +import { moveStaticPages, movePublicFiles, patchNextFiles, removeMetadataFiles } from './helpers/files' import { splitApiRoutes } from './helpers/flags' import { generateFunctions, @@ -254,6 +254,12 @@ const plugin: NetlifyPlugin = { warnForProblematicUserRewrites({ basePath, redirects }) warnForRootRedirects({ appDir }) await warnOnApiRoutes({ FUNCTIONS_DIST }) + + // we are removing metadata files from Publish directory + // we have to do this after functions were bundled as bundling still + // require those files, but we don't want to publish them + await removeMetadataFiles(publish) + if (experimental?.appDir) { console.log( '🧪 Thank you for testing "appDir" support on Netlify. For known issues and to give feedback, visit https://ntl.fyi/next-13-feedback', diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index d8ec3d361a..a322148a33 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -1087,6 +1087,14 @@ Array [ "app/blog/sarah/second-post.rsc", "blog/sarah/second-post.rsc", ], + Array [ + "pages/en/404.html", + "en/404.html", + ], + Array [ + "pages/en/500.html", + "en/500.html", + ], Array [ "pages/en/broken-image.html", "en/broken-image.html", @@ -1207,6 +1215,14 @@ Array [ "pages/en/static.html", "en/static.html", ], + Array [ + "pages/es/404.html", + "es/404.html", + ], + Array [ + "pages/es/500.html", + "es/500.html", + ], Array [ "pages/es/broken-image.html", "es/broken-image.html", @@ -1263,6 +1279,14 @@ Array [ "pages/es/static.html", "es/static.html", ], + Array [ + "pages/fr/404.html", + "fr/404.html", + ], + Array [ + "pages/fr/500.html", + "fr/500.html", + ], Array [ "pages/fr/broken-image.html", "fr/broken-image.html", @@ -1456,7 +1480,7 @@ Array [ "force": false, "from": "/_next/data/build-id/en/getStaticProps/:id.json", "status": 404, - "to": "/server/pages/en/404.html", + "to": "/en/404.html", }, Object { "force": false, @@ -1504,7 +1528,7 @@ Array [ "force": false, "from": "/_next/data/build-id/en/getStaticProps/withRevalidate/:id.json", "status": 404, - "to": "/server/pages/en/404.html", + "to": "/en/404.html", }, Object { "force": true, @@ -1702,7 +1726,7 @@ Array [ "force": false, "from": "/_next/data/build-id/es/getStaticProps/:id.json", "status": 404, - "to": "/server/pages/es/404.html", + "to": "/es/404.html", }, Object { "force": false, @@ -1750,7 +1774,7 @@ Array [ "force": false, "from": "/_next/data/build-id/es/getStaticProps/withRevalidate/:id.json", "status": 404, - "to": "/server/pages/es/404.html", + "to": "/es/404.html", }, Object { "force": false, @@ -1912,7 +1936,7 @@ Array [ "force": false, "from": "/_next/data/build-id/fr/getStaticProps/:id.json", "status": 404, - "to": "/server/pages/fr/404.html", + "to": "/fr/404.html", }, Object { "force": false, @@ -1960,7 +1984,7 @@ Array [ "force": false, "from": "/_next/data/build-id/fr/getStaticProps/withRevalidate/:id.json", "status": 404, - "to": "/server/pages/fr/404.html", + "to": "/fr/404.html", }, Object { "force": false, @@ -2206,24 +2230,6 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": true, - "from": "/BUILD_ID", - "status": 404, - "to": "/404.html", - }, - Object { - "force": true, - "from": "/build-manifest.json", - "status": 404, - "to": "/404.html", - }, - Object { - "force": true, - "from": "/cache/*", - "status": 404, - "to": "/404.html", - }, Object { "force": false, "from": "/css", @@ -2360,7 +2366,7 @@ Array [ "force": false, "from": "/es/getStaticProps/:id", "status": 404, - "to": "/server/pages/es/404.html", + "to": "/es/404.html", }, Object { "force": false, @@ -2408,7 +2414,7 @@ Array [ "force": false, "from": "/es/getStaticProps/withRevalidate/:id", "status": 404, - "to": "/server/pages/es/404.html", + "to": "/es/404.html", }, Object { "force": false, @@ -2600,7 +2606,7 @@ Array [ "force": false, "from": "/fr/getStaticProps/:id", "status": 404, - "to": "/server/pages/fr/404.html", + "to": "/fr/404.html", }, Object { "force": false, @@ -2648,7 +2654,7 @@ Array [ "force": false, "from": "/fr/getStaticProps/withRevalidate/:id", "status": 404, - "to": "/server/pages/fr/404.html", + "to": "/fr/404.html", }, Object { "force": false, @@ -2756,7 +2762,7 @@ Array [ "force": false, "from": "/getStaticProps/:id", "status": 404, - "to": "/server/pages/en/404.html", + "to": "/en/404.html", }, Object { "force": false, @@ -2804,7 +2810,7 @@ Array [ "force": false, "from": "/getStaticProps/withRevalidate/:id", "status": 404, - "to": "/server/pages/en/404.html", + "to": "/en/404.html", }, Object { "force": true, @@ -2883,54 +2889,24 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": true, - "from": "/prerender-manifest.json", - "status": 404, - "to": "/404.html", - }, Object { "force": false, "from": "/previewTest", "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": true, - "from": "/react-loadable-manifest.json", - "status": 404, - "to": "/404.html", - }, Object { "force": false, "from": "/redirectme", "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": true, - "from": "/routes-manifest.json", - "status": 404, - "to": "/404.html", - }, Object { "force": false, "from": "/script", "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": true, - "from": "/server/*", - "status": 404, - "to": "/404.html", - }, - Object { - "force": true, - "from": "/serverless/*", - "status": 404, - "to": "/404.html", - }, Object { "force": false, "from": "/shows/:id", @@ -2966,17 +2942,5 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": true, - "from": "/trace", - "status": 404, - "to": "/404.html", - }, - Object { - "force": true, - "from": "/traces", - "status": 404, - "to": "/404.html", - }, ] `; diff --git a/test/e2e/next-test-lib/next-modes/next-deploy.ts b/test/e2e/next-test-lib/next-modes/next-deploy.ts index 793f41ea18..1463cbc1af 100644 --- a/test/e2e/next-test-lib/next-modes/next-deploy.ts +++ b/test/e2e/next-test-lib/next-modes/next-deploy.ts @@ -74,6 +74,7 @@ export class NextDeployInstance extends NextInstance { NETLIFY_SITE_ID: this._netlifySiteId, NODE_ENV: 'production', DISABLE_IPX: platform() === 'linux' ? undefined : '1', + NEXT_KEEP_METADATA_FILES: 'true', }, }) diff --git a/test/helpers/utils.spec.ts b/test/helpers/utils.spec.ts index 19d70fcae2..a6c5c9398c 100644 --- a/test/helpers/utils.spec.ts +++ b/test/helpers/utils.spec.ts @@ -149,8 +149,8 @@ describe('redirectsForNext404Route', () => { } expect(redirectsForNext404Route(mockRoute)).toStrictEqual([ - { force: false, from: '/_next/data/test/test.json', status: 404, to: '/server/pages/404.html' }, - { force: false, from: '/test', status: 404, to: '/server/pages/404.html' }, + { force: false, from: '/_next/data/test/test.json', status: 404, to: '/404.html' }, + { force: false, from: '/test', status: 404, to: '/404.html' }, ]) }) @@ -166,12 +166,12 @@ describe('redirectsForNext404Route', () => { } expect(redirectsForNext404Route(mockRoute)).toStrictEqual([ - { force: false, from: '/_next/data/test/en/test.json', status: 404, to: '/server/pages/en/404.html' }, - { force: false, from: '/test', status: 404, to: '/server/pages/en/404.html' }, - { force: false, from: '/_next/data/test/es/test.json', status: 404, to: '/server/pages/es/404.html' }, - { force: false, from: '/es/test', status: 404, to: '/server/pages/es/404.html' }, - { force: false, from: '/_next/data/test/fr/test.json', status: 404, to: '/server/pages/fr/404.html' }, - { force: false, from: '/fr/test', status: 404, to: '/server/pages/fr/404.html' }, + { force: false, from: '/_next/data/test/en/test.json', status: 404, to: '/en/404.html' }, + { force: false, from: '/test', status: 404, to: '/en/404.html' }, + { force: false, from: '/_next/data/test/es/test.json', status: 404, to: '/es/404.html' }, + { force: false, from: '/es/test', status: 404, to: '/es/404.html' }, + { force: false, from: '/_next/data/test/fr/test.json', status: 404, to: '/fr/404.html' }, + { force: false, from: '/fr/test', status: 404, to: '/fr/404.html' }, ]) }) @@ -182,13 +182,13 @@ describe('redirectsForNext404Route', () => { force: false, from: '/_next/data/test/getStaticProps/:id.json', status: 404, - to: '/server/pages/404.html', + to: '/404.html', }, { force: false, from: '/getStaticProps/:id', status: 404, - to: '/server/pages/404.html', + to: '/404.html', }, ], dynamicRoutesThatMatchMiddleware: [], diff --git a/test/index.spec.ts b/test/index.spec.ts index ec81feb12a..d18baa8685 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1031,6 +1031,23 @@ describe('onPostBuild', () => { }, ]) }) + + it(`removes metadata files`, async () => { + await moveNextDist() + + // routes-manifest.json is one of metadata files that seems to be created with default demo site + // there are a lot of other files, but we will test just one + const manifestPath = path.resolve('.next/routes-manifest.json') + + expect(await pathExists(manifestPath)).toBe(true) + + await nextRuntime.onPostBuild({ + ...defaultArgs, + utils: { ...utils, cache: { save: jest.fn() }, functions: { list: jest.fn().mockResolvedValue([]) } }, + }) + + expect(await pathExists(manifestPath)).toBe(false) + }) }) describe('function helpers', () => {