@netlify/plugin-nextjs 4.32.1 → 4.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/constants.js CHANGED
@@ -1,9 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DIVIDER = exports.LAMBDA_MAX_SIZE = exports.MINIMUM_REVALIDATE_SECONDS = exports.DYNAMIC_PARAMETER_REGEX = exports.OPTIONAL_CATCH_ALL_REGEX = exports.CATCH_ALL_REGEX = exports.DEFAULT_FUNCTIONS_SRC = exports.HANDLER_FUNCTION_PATH = exports.ODB_FUNCTION_PATH = exports.HIDDEN_PATHS = exports.IMAGE_FUNCTION_NAME = exports.ODB_FUNCTION_NAME = exports.HANDLER_FUNCTION_NAME = void 0;
3
+ exports.DIVIDER = exports.LAMBDA_MAX_SIZE = exports.MINIMUM_REVALIDATE_SECONDS = exports.DYNAMIC_PARAMETER_REGEX = exports.OPTIONAL_CATCH_ALL_REGEX = exports.CATCH_ALL_REGEX = exports.DEFAULT_FUNCTIONS_SRC = exports.HANDLER_FUNCTION_PATH = exports.ODB_FUNCTION_PATH = exports.HIDDEN_PATHS = exports.IMAGE_FUNCTION_TITLE = exports.ODB_FUNCTION_TITLE = exports.HANDLER_FUNCTION_TITLE = exports.NEXT_PLUGIN = exports.NEXT_PLUGIN_NAME = exports.IMAGE_FUNCTION_NAME = exports.ODB_FUNCTION_NAME = exports.HANDLER_FUNCTION_NAME = void 0;
4
4
  exports.HANDLER_FUNCTION_NAME = '___netlify-handler';
5
5
  exports.ODB_FUNCTION_NAME = '___netlify-odb-handler';
6
6
  exports.IMAGE_FUNCTION_NAME = '_ipx';
7
+ exports.NEXT_PLUGIN_NAME = '@netlify/next-runtime';
8
+ exports.NEXT_PLUGIN = '@netlify/plugin-nextjs';
9
+ exports.HANDLER_FUNCTION_TITLE = 'Next.js SSR handler';
10
+ exports.ODB_FUNCTION_TITLE = 'Next.js ISR handler';
11
+ exports.IMAGE_FUNCTION_TITLE = 'next/image handler';
7
12
  // These are paths in .next that shouldn't be publicly accessible
8
13
  exports.HIDDEN_PATHS = [
9
14
  '/cache/*',
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.watchForMiddlewareChanges = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const esbuild_1 = require("@netlify/esbuild");
7
+ const chokidar_1 = require("chokidar");
8
+ // For more information on Next.js middleware, see https://nextjs.org/docs/advanced-features/middleware
9
+ // These are the locations that a middleware file can exist in a Next.js application
10
+ // If other possible locations are added by Next.js, they should be added here.
11
+ const MIDDLEWARE_FILE_LOCATIONS = [
12
+ 'middleware.js',
13
+ 'middleware.ts',
14
+ 'src/middleware.js',
15
+ 'src/middleware.ts',
16
+ ];
17
+ const toFileList = (watched) => Object.entries(watched).flatMap(([dir, files]) => files.map((file) => (0, path_1.join)(dir, file)));
18
+ /**
19
+ * Compile the middleware file using esbuild
20
+ */
21
+ const buildMiddlewareFile = async (entryPoints, base) => {
22
+ try {
23
+ await (0, esbuild_1.build)({
24
+ entryPoints,
25
+ outfile: (0, path_1.join)(base, '.netlify', 'middleware.js'),
26
+ bundle: true,
27
+ format: 'esm',
28
+ target: 'esnext',
29
+ absWorkingDir: base,
30
+ });
31
+ }
32
+ catch (error) {
33
+ console.error(error.toString());
34
+ }
35
+ };
36
+ /**
37
+ * We only compile middleware if there's exactly one file. If there's more than one, we log a warning and don't compile.
38
+ */
39
+ const shouldFilesBeCompiled = (watchedFiles, isFirstRun) => {
40
+ if (watchedFiles.length === 0) {
41
+ if (!isFirstRun) {
42
+ // Only log on subsequent builds, because having it on first build makes it seem like a warning, when it's a normal state
43
+ console.log('No middleware found');
44
+ }
45
+ return false;
46
+ }
47
+ if (watchedFiles.length > 1) {
48
+ console.log('Multiple middleware files found:');
49
+ console.log(watchedFiles.join('\n'));
50
+ console.log('This is not supported.');
51
+ return false;
52
+ }
53
+ return true;
54
+ };
55
+ const updateWatchedFiles = async (watcher, base, isFirstRun = false) => {
56
+ try {
57
+ // Start by deleting the old file. If we error out, we don't want to leave the old file around
58
+ await fs_1.promises.unlink((0, path_1.join)(base, '.netlify', 'middleware.js'));
59
+ }
60
+ catch {
61
+ // Ignore, because it's fine if it didn't exist
62
+ }
63
+ // The list of watched files is an object with the directory as the key and an array of files as the value.
64
+ // We need to flatten this into a list of files
65
+ const watchedFiles = toFileList(watcher.getWatched());
66
+ if (!shouldFilesBeCompiled(watchedFiles, isFirstRun)) {
67
+ watcher.emit('build');
68
+ return;
69
+ }
70
+ console.log(`${isFirstRun ? 'Building' : 'Rebuilding'} middleware ${watchedFiles[0]}...`);
71
+ await buildMiddlewareFile(watchedFiles, base);
72
+ console.log('...done');
73
+ watcher.emit('build');
74
+ };
75
+ /**
76
+ * Watch for changes to the middleware file location. When a change is detected, recompile the middleware file.
77
+ *
78
+ * @param base The base directory to watch
79
+ * @returns a file watcher and a promise that resolves when the initial scan is complete.
80
+ */
81
+ const watchForMiddlewareChanges = (base) => {
82
+ const watcher = (0, chokidar_1.watch)(MIDDLEWARE_FILE_LOCATIONS, {
83
+ // Try and ensure renames just emit one event
84
+ atomic: true,
85
+ // Don't emit for every watched file, just once after the scan is done
86
+ ignoreInitial: true,
87
+ cwd: base,
88
+ });
89
+ watcher
90
+ .on('change', (path) => {
91
+ console.log(`File ${path} has been changed`);
92
+ updateWatchedFiles(watcher, base);
93
+ })
94
+ .on('add', (path) => {
95
+ console.log(`File ${path} has been added`);
96
+ updateWatchedFiles(watcher, base);
97
+ })
98
+ .on('unlink', (path) => {
99
+ console.log(`File ${path} has been removed`);
100
+ updateWatchedFiles(watcher, base);
101
+ });
102
+ return {
103
+ watcher,
104
+ isReady: new Promise((resolve) => {
105
+ watcher.on('ready', async () => {
106
+ console.log('Initial scan for middleware file complete. Ready for changes.');
107
+ // This only happens on the first scan
108
+ await updateWatchedFiles(watcher, base, true);
109
+ console.log('Ready');
110
+ resolve();
111
+ });
112
+ }),
113
+ nextBuild: () => new Promise((resolve) => {
114
+ watcher.once('build', resolve);
115
+ }),
116
+ };
117
+ };
118
+ exports.watchForMiddlewareChanges = watchForMiddlewareChanges;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.generateCustomHeaders = exports.configureHandlerFunctions = exports.hasManuallyAddedModule = exports.updateRequiredServerFiles = exports.getRequiredServerFiles = exports.getNextConfig = void 0;
6
+ exports.generateCustomHeaders = exports.configureHandlerFunctions = exports.hasManuallyAddedModule = exports.resolveModuleRoot = exports.updateRequiredServerFiles = exports.getRequiredServerFiles = exports.getNextConfig = void 0;
7
7
  const destr_1 = __importDefault(require("destr"));
8
8
  const fs_extra_1 = require("fs-extra");
9
9
  const pathe_1 = require("pathe");
@@ -57,6 +57,7 @@ const resolveModuleRoot = (moduleName) => {
57
57
  return null;
58
58
  }
59
59
  };
60
+ exports.resolveModuleRoot = resolveModuleRoot;
60
61
  const DEFAULT_EXCLUDED_MODULES = ['sharp', 'electron'];
61
62
  const hasManuallyAddedModule = ({ netlifyConfig, moduleName, }) =>
62
63
  /* eslint-disable camelcase */
@@ -68,10 +69,9 @@ const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore = [],
68
69
  const config = await (0, exports.getRequiredServerFiles)(publish);
69
70
  const files = config.files || [];
70
71
  const cssFilesToInclude = files.filter((f) => f.startsWith(`${publish}/static/css/`));
71
- /* eslint-disable no-underscore-dangle */
72
72
  if (!(0, destr_1.default)(process.env.DISABLE_IPX)) {
73
- (_a = netlifyConfig.functions)._ipx || (_a._ipx = {});
74
- netlifyConfig.functions._ipx.node_bundler = 'nft';
73
+ (_a = netlifyConfig.functions)[constants_1.IMAGE_FUNCTION_NAME] || (_a[constants_1.IMAGE_FUNCTION_NAME] = {});
74
+ netlifyConfig.functions[constants_1.IMAGE_FUNCTION_NAME].node_bundler = 'nft';
75
75
  }
76
76
  // If the user has manually added the module to included_files, then don't exclude it
77
77
  const excludedModules = DEFAULT_EXCLUDED_MODULES.filter((moduleName) => !(0, exports.hasManuallyAddedModule)({ netlifyConfig, moduleName }));
@@ -81,12 +81,12 @@ const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore = [],
81
81
  netlifyConfig.functions[functionName].node_bundler = 'nft';
82
82
  (_b = netlifyConfig.functions[functionName]).included_files || (_b.included_files = []);
83
83
  netlifyConfig.functions[functionName].included_files.push('.env', '.env.local', '.env.production', '.env.production.local', './public/locales/**', './next-i18next.config.js', `${publish}/server/**`, `${publish}/serverless/**`, `${publish}/*.json`, `${publish}/BUILD_ID`, `${publish}/static/chunks/webpack-middleware*.js`, `!${publish}/server/**/*.js.nft.json`, `!${publish}/server/**/*.map`, '!**/node_modules/@next/swc*/**/*', ...cssFilesToInclude, ...ignore.map((path) => `!${(0, slash_1.default)(path)}`));
84
- const nextRoot = resolveModuleRoot('next');
84
+ const nextRoot = (0, exports.resolveModuleRoot)('next');
85
85
  if (nextRoot) {
86
86
  netlifyConfig.functions[functionName].included_files.push(`!${nextRoot}/dist/server/lib/squoosh/**/*.wasm`, `!${nextRoot}/dist/next-server/server/lib/squoosh/**/*.wasm`, `!${nextRoot}/dist/compiled/webpack/bundle4.js`, `!${nextRoot}/dist/compiled/webpack/bundle5.js`);
87
87
  }
88
88
  excludedModules.forEach((moduleName) => {
89
- const moduleRoot = resolveModuleRoot(moduleName);
89
+ const moduleRoot = (0, exports.resolveModuleRoot)(moduleName);
90
90
  if (moduleRoot) {
91
91
  netlifyConfig.functions[functionName].included_files.push(`!${moduleRoot}/**/*`);
92
92
  }
@@ -5,10 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.onPreDev = void 0;
7
7
  const path_1 = require("path");
8
- const stream_1 = require("stream");
9
8
  const execa_1 = __importDefault(require("execa"));
10
- const fs_extra_1 = require("fs-extra");
11
- const merge_stream_1 = __importDefault(require("merge-stream"));
12
9
  const edge_1 = require("./edge");
13
10
  const files_1 = require("./files");
14
11
  // The types haven't been updated yet
@@ -17,32 +14,10 @@ const onPreDev = async ({ constants, netlifyConfig }) => {
17
14
  const base = (_a = netlifyConfig.build.base) !== null && _a !== void 0 ? _a : process.cwd();
18
15
  // Need to patch the files, because build might not have been run
19
16
  await (0, files_1.patchNextFiles)(base);
20
- // Clean up old functions
21
- await (0, fs_extra_1.unlink)((0, path_1.resolve)('.netlify', 'middleware.js')).catch(() => {
22
- // Ignore if it doesn't exist
23
- });
24
17
  await (0, edge_1.writeDevEdgeFunction)(constants);
25
- // Eventually we might want to do this via esbuild's API, but for now the CLI works fine
26
- const common = [`--bundle`, `--outdir=${(0, path_1.resolve)('.netlify')}`, `--format=esm`, `--target=esnext`, '--watch'];
27
- const opts = {
28
- all: true,
29
- env: { ...process.env, FORCE_COLOR: '1' },
30
- };
31
- // TypeScript
32
- const tsout = (0, execa_1.default)(`esbuild`, [...common, (0, path_1.resolve)(base, 'middleware.ts')], opts).all;
33
- // JavaScript
34
- const jsout = (0, execa_1.default)(`esbuild`, [...common, (0, path_1.resolve)(base, 'middleware.js')], opts).all;
35
- const filter = new stream_1.Transform({
36
- transform(chunk, encoding, callback) {
37
- const str = chunk.toString(encoding);
38
- // Skip if message includes this, because we run even when the files are missing
39
- if (!str.includes('[ERROR] Could not resolve')) {
40
- this.push(chunk);
41
- }
42
- callback();
43
- },
18
+ // Don't await this or it will never finish
19
+ execa_1.default.node((0, path_1.resolve)(__dirname, '..', '..', 'lib', 'helpers', 'middlewareWatcher.js'), [base, process.env.NODE_ENV === 'test' ? '--once' : ''], {
20
+ stdio: 'inherit',
44
21
  });
45
- (0, merge_stream_1.default)(tsout, jsout).pipe(filter).pipe(process.stdout);
46
- // Don't return the promise because we don't want to wait for the child process to finish
47
22
  };
48
23
  exports.onPreDev = onPreDev;
@@ -10,6 +10,7 @@ const chalk_1 = require("chalk");
10
10
  const destr_1 = __importDefault(require("destr"));
11
11
  const fs_extra_1 = require("fs-extra");
12
12
  const outdent_1 = require("outdent");
13
+ const constants_1 = require("../constants");
13
14
  const config_1 = require("./config");
14
15
  const matchers_1 = require("./matchers");
15
16
  const maybeLoadJson = (path) => {
@@ -286,7 +287,7 @@ const writeEdgeFunctions = async ({ netlifyConfig, routesManifest, }) => {
286
287
  const edgeFunctionDir = (0, path_1.join)(edgeFunctionRoot, 'ipx');
287
288
  await (0, fs_extra_1.ensureDir)(edgeFunctionDir);
288
289
  await copyEdgeSourceFile({ edgeFunctionDir, file: 'ipx.ts', target: 'index.ts' });
289
- await (0, fs_extra_1.copyFile)((0, path_1.join)('.netlify', 'functions-internal', '_ipx', 'imageconfig.json'), (0, path_1.join)(edgeFunctionDir, 'imageconfig.json'));
290
+ await (0, fs_extra_1.copyFile)((0, path_1.join)('.netlify', 'functions-internal', constants_1.IMAGE_FUNCTION_NAME, 'imageconfig.json'), (0, path_1.join)(edgeFunctionDir, 'imageconfig.json'));
290
291
  manifest.functions.push({
291
292
  function: 'ipx',
292
293
  name: 'next/image handler',
@@ -16,6 +16,7 @@ const getHandler_1 = require("../templates/getHandler");
16
16
  const getPageResolver_1 = require("../templates/getPageResolver");
17
17
  const analysis_1 = require("./analysis");
18
18
  const files_1 = require("./files");
19
+ const functionsMetaData_1 = require("./functionsMetaData");
19
20
  const utils_1 = require("./utils");
20
21
  const generateFunctions = async ({ FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, PUBLISH_DIR }, appDir, apiRoutes) => {
21
22
  const publish = (0, pathe_1.resolve)(PUBLISH_DIR);
@@ -35,8 +36,11 @@ const generateFunctions = async ({ FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS
35
36
  });
36
37
  const functionName = (0, utils_1.getFunctionNameForPage)(route, config.type === "experimental-background" /* ApiRouteType.BACKGROUND */);
37
38
  await (0, fs_extra_1.ensureDir)((0, pathe_1.join)(functionsDir, functionName));
39
+ // write main API handler file
38
40
  await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsDir, functionName, `${functionName}.js`), apiHandlerSource);
41
+ // copy handler dependencies (VercelNodeBridge, NetlifyNextServer, etc.)
39
42
  await (0, fs_extra_1.copyFile)(node_bridge_1.default, (0, pathe_1.join)(functionsDir, functionName, 'bridge.js'));
43
+ await (0, fs_extra_1.copyFile)((0, pathe_1.join)(__dirname, '..', '..', 'lib', 'templates', 'server.js'), (0, pathe_1.join)(functionsDir, functionName, 'server.js'));
40
44
  await (0, fs_extra_1.copyFile)((0, pathe_1.join)(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'), (0, pathe_1.join)(functionsDir, functionName, 'handlerUtils.js'));
41
45
  const resolveSourceFile = (file) => (0, pathe_1.join)(publish, 'server', file);
42
46
  const resolverSource = await (0, getPageResolver_1.getResolverForSourceFiles)({
@@ -46,15 +50,19 @@ const generateFunctions = async ({ FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS
46
50
  });
47
51
  await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsDir, functionName, 'pages.js'), resolverSource);
48
52
  }
49
- const writeHandler = async (functionName, isODB) => {
53
+ const writeHandler = async (functionName, functionTitle, isODB) => {
50
54
  const handlerSource = await (0, getHandler_1.getHandler)({ isODB, publishDir, appDir: (0, pathe_1.relative)(functionDir, appDir) });
51
55
  await (0, fs_extra_1.ensureDir)((0, pathe_1.join)(functionsDir, functionName));
56
+ // write main handler file (standard or ODB)
52
57
  await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsDir, functionName, `${functionName}.js`), handlerSource);
58
+ // copy handler dependencies (VercelNodeBridge, NetlifyNextServer, etc.)
53
59
  await (0, fs_extra_1.copyFile)(node_bridge_1.default, (0, pathe_1.join)(functionsDir, functionName, 'bridge.js'));
60
+ await (0, fs_extra_1.copyFile)((0, pathe_1.join)(__dirname, '..', '..', 'lib', 'templates', 'server.js'), (0, pathe_1.join)(functionsDir, functionName, 'server.js'));
54
61
  await (0, fs_extra_1.copyFile)((0, pathe_1.join)(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'), (0, pathe_1.join)(functionsDir, functionName, 'handlerUtils.js'));
62
+ (0, functionsMetaData_1.writeFunctionConfiguration)({ functionName, functionTitle, functionsDir });
55
63
  };
56
- await writeHandler(constants_1.HANDLER_FUNCTION_NAME, false);
57
- await writeHandler(constants_1.ODB_FUNCTION_NAME, true);
64
+ await writeHandler(constants_1.HANDLER_FUNCTION_NAME, constants_1.HANDLER_FUNCTION_TITLE, false);
65
+ await writeHandler(constants_1.ODB_FUNCTION_NAME, constants_1.ODB_FUNCTION_TITLE, true);
58
66
  };
59
67
  exports.generateFunctions = generateFunctions;
60
68
  /**
@@ -96,6 +104,11 @@ const setupImageFunction = async ({ constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIO
96
104
  responseHeaders,
97
105
  });
98
106
  await (0, fs_extra_1.copyFile)((0, pathe_1.join)(__dirname, '..', '..', 'lib', 'templates', 'ipx.js'), (0, pathe_1.join)(functionDirectory, functionName));
107
+ (0, functionsMetaData_1.writeFunctionConfiguration)({
108
+ functionName: constants_1.IMAGE_FUNCTION_NAME,
109
+ functionTitle: constants_1.IMAGE_FUNCTION_TITLE,
110
+ functionsDir: functionsPath,
111
+ });
99
112
  // If we have edge functions then the request will have already been rewritten
100
113
  // so this won't match. This is matched if edge is disabled or unavailable.
101
114
  netlifyConfig.redirects.push({
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writeFunctionConfiguration = void 0;
4
+ const fs_extra_1 = require("fs-extra");
5
+ const pathe_1 = require("pathe");
6
+ const constants_1 = require("../constants");
7
+ const config_1 = require("./config");
8
+ const getNextRuntimeVersion = async (packageJsonPath, useNodeModulesPath) => {
9
+ if (!(0, fs_extra_1.existsSync)(packageJsonPath)) {
10
+ return;
11
+ }
12
+ const packagePlugin = await (0, fs_extra_1.readJSON)(packageJsonPath);
13
+ return useNodeModulesPath ? packagePlugin.version : packagePlugin.dependencies[constants_1.NEXT_PLUGIN];
14
+ };
15
+ /**
16
+ * Creates a function configuration file for the given function.
17
+ *
18
+ * @param functionInfo The information needed to create a function configuration file
19
+ */
20
+ const writeFunctionConfiguration = async (functionInfo) => {
21
+ const { functionName, functionTitle, functionsDir } = functionInfo;
22
+ const pluginPackagePath = '.netlify/plugins/package.json';
23
+ const moduleRoot = (0, config_1.resolveModuleRoot)(constants_1.NEXT_PLUGIN);
24
+ const nodeModulesPath = moduleRoot ? (0, pathe_1.join)(moduleRoot, 'package.json') : null;
25
+ const nextPluginVersion = (await getNextRuntimeVersion(nodeModulesPath, true)) ||
26
+ (await getNextRuntimeVersion(pluginPackagePath, false)) ||
27
+ // The runtime version should always be available, but if it's not, return 'unknown'
28
+ 'unknown';
29
+ const metadata = {
30
+ config: {
31
+ name: functionTitle,
32
+ generator: `${constants_1.NEXT_PLUGIN_NAME}@${nextPluginVersion}`,
33
+ },
34
+ version: 1,
35
+ };
36
+ await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsDir, functionName, `${functionName}.json`), JSON.stringify(metadata));
37
+ };
38
+ exports.writeFunctionConfiguration = writeFunctionConfiguration;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const path_1 = require("path");
4
+ const compiler_1 = require("./compiler");
5
+ const run = async () => {
6
+ const { isReady, watcher } = (0, compiler_1.watchForMiddlewareChanges)((0, path_1.resolve)(process.argv[2]));
7
+ await isReady;
8
+ if (process.argv[3] === '--once') {
9
+ watcher.close();
10
+ }
11
+ };
12
+ run();
@@ -49,7 +49,7 @@ const toNetlifyRoute = (nextRoute) => {
49
49
  exports.toNetlifyRoute = toNetlifyRoute;
50
50
  const generateNetlifyRoutes = ({ route, dataRoute, withData = true, }) => [...(withData ? (0, exports.toNetlifyRoute)(dataRoute) : []), ...(0, exports.toNetlifyRoute)(route)];
51
51
  exports.generateNetlifyRoutes = generateNetlifyRoutes;
52
- const routeToDataRoute = (route, buildId, locale) => `/_next/data/${buildId}${locale ? `/${locale}` : ''}${route === '/' ? '/index' : route}.json`;
52
+ const routeToDataRoute = (route, buildId, locale) => `/_next/data/${buildId}${locale ? `/${locale}` : ''}${route === '/' ? (locale ? '' : '/index') : route}.json`;
53
53
  exports.routeToDataRoute = routeToDataRoute;
54
54
  // Default locale is served from root, not localized
55
55
  const localizeRoute = (route, locale, defaultLocale) => locale === defaultLocale ? route : `/${locale}${route}`;
@@ -10,9 +10,10 @@ const path = require('path');
10
10
  // eslint-disable-next-line n/prefer-global/url, n/prefer-global/url-search-params
11
11
  const { URLSearchParams, URL } = require('url');
12
12
  const { Bridge } = require('@vercel/node-bridge/bridge');
13
- const { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, getNextServer, normalizePath, } = require('./handlerUtils');
13
+ const { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, normalizePath, } = require('./handlerUtils');
14
+ const { NetlifyNextServer } = require('./server');
14
15
  // We return a function and then call `toString()` on it to serialise it as the launcher function
15
- // eslint-disable-next-line max-params
16
+ // eslint-disable-next-line max-params, max-lines-per-function
16
17
  const makeHandler = (conf, app, pageRoot, staticManifest = [], mode = 'ssr') => {
17
18
  var _a;
18
19
  // Change working directory into the site root, unless using Nx, which moves the
@@ -43,20 +44,22 @@ const makeHandler = (conf, app, pageRoot, staticManifest = [], mode = 'ssr') =>
43
44
  // We memoize this because it can be shared between requests, but don't instantiate it until
44
45
  // the first request because we need the host and port.
45
46
  let bridge;
46
- const getBridge = (event) => {
47
+ const getBridge = (event, context) => {
48
+ const { clientContext: { custom: customContext }, } = context;
47
49
  if (bridge) {
48
50
  return bridge;
49
51
  }
50
52
  const url = new URL(event.rawUrl);
51
53
  const port = Number.parseInt(url.port) || 80;
52
54
  base = url.origin;
53
- const NextServer = getNextServer();
54
- const nextServer = new NextServer({
55
+ const nextServer = new NetlifyNextServer({
55
56
  conf,
56
57
  dir,
57
58
  customServer: false,
58
59
  hostname: url.hostname,
59
60
  port,
61
+ }, {
62
+ revalidateToken: customContext.odb_refresh_hooks,
60
63
  });
61
64
  const requestHandler = nextServer.getRequestHandler();
62
65
  const server = new Server(async (req, res) => {
@@ -90,7 +93,7 @@ const makeHandler = (conf, app, pageRoot, staticManifest = [], mode = 'ssr') =>
90
93
  // eslint-disable-next-line no-underscore-dangle
91
94
  process.env._NETLIFY_GRAPH_TOKEN = graphToken;
92
95
  }
93
- const { headers, ...result } = await getBridge(event).launcher(event, context);
96
+ const { headers, ...result } = await getBridge(event, context).launcher(event, context);
94
97
  // Convert all headers to multiValueHeaders
95
98
  const multiValueHeaders = getMultiValueHeaders(headers);
96
99
  if (event.headers['x-next-debug-logging']) {
@@ -142,6 +145,7 @@ const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '..
142
145
  // We copy the file here rather than requiring from the node module
143
146
  const { Bridge } = require("./bridge");
144
147
  const { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, getNextServer, normalizePath } = require('./handlerUtils')
148
+ const { NetlifyNextServer } = require('./server')
145
149
 
146
150
  ${isODB ? `const { builder } = require("@netlify/functions")` : ''}
147
151
  const { config } = require("${publishDir}/required-server-files.json")
@@ -30,7 +30,7 @@ exports.getAllPageDependencies = getAllPageDependencies;
30
30
  const getResolverForDependencies = ({ dependencies, functionDir, }) => {
31
31
  const pageFiles = dependencies.map((file) => `require.resolve('${(0, pathe_1.relative)(functionDir, file)}')`);
32
32
  return (0, outdent_1.outdent /* javascript */) `
33
- // This file is purely to allow nft to know about these pages.
33
+ // This file is purely to allow nft to know about these pages.
34
34
  exports.resolvePages = () => {
35
35
  try {
36
36
  ${pageFiles.join('\n ')}
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.normalizePath = exports.getPrefetchResponse = exports.getNextServer = exports.augmentFsModule = exports.getMultiValueHeaders = exports.getMaxAge = exports.downloadFile = void 0;
6
+ exports.localizeDataRoute = exports.localizeRoute = exports.unlocalizeRoute = exports.normalizeRoute = exports.netlifyApiFetch = exports.normalizePath = exports.getPrefetchResponse = exports.getNextServer = exports.augmentFsModule = exports.getMultiValueHeaders = exports.getMaxAge = exports.downloadFile = void 0;
7
7
  const fs_1 = require("fs");
8
8
  const os_1 = require("os");
9
9
  const path_1 = __importDefault(require("path"));
@@ -206,3 +206,51 @@ const normalizePath = (event) => {
206
206
  return new URL(event.rawUrl).pathname;
207
207
  };
208
208
  exports.normalizePath = normalizePath;
209
+ // Simple Netlify API client
210
+ const netlifyApiFetch = ({ endpoint, payload, token, method = 'GET', }) => new Promise((resolve, reject) => {
211
+ const body = JSON.stringify(payload);
212
+ const req = follow_redirects_1.https.request({
213
+ hostname: 'api.netlify.com',
214
+ port: 443,
215
+ path: `/api/v1/${endpoint}`,
216
+ method,
217
+ headers: {
218
+ 'Content-Type': 'application/json',
219
+ 'Content-Length': body.length,
220
+ Authorization: `Bearer ${token}`,
221
+ },
222
+ }, (res) => {
223
+ let data = '';
224
+ res.on('data', (chunk) => {
225
+ data += chunk;
226
+ });
227
+ res.on('end', () => {
228
+ resolve(JSON.parse(data));
229
+ });
230
+ });
231
+ req.on('error', reject);
232
+ req.write(body);
233
+ req.end();
234
+ });
235
+ exports.netlifyApiFetch = netlifyApiFetch;
236
+ // Remove trailing slash from a route (except for the root route)
237
+ const normalizeRoute = (route) => (route.endsWith('/') ? route.slice(0, -1) || '/' : route);
238
+ exports.normalizeRoute = normalizeRoute;
239
+ // Check if a route has a locale prefix (including the root route)
240
+ const isLocalized = (route, i18n) => i18n.locales.some((locale) => route === `/${locale}` || route.startsWith(`/${locale}/`));
241
+ // Remove the locale prefix from a route (if any)
242
+ const unlocalizeRoute = (route, i18n) => isLocalized(route, i18n) ? `/${route.split('/').slice(2).join('/')}` : route;
243
+ exports.unlocalizeRoute = unlocalizeRoute;
244
+ // Add the default locale prefix to a route (if necessary)
245
+ const localizeRoute = (route, i18n) => isLocalized(route, i18n) ? route : (0, exports.normalizeRoute)(`/${i18n.defaultLocale}${route}`);
246
+ exports.localizeRoute = localizeRoute;
247
+ // Normalize a data route to include the locale prefix and remove the index suffix
248
+ const localizeDataRoute = (dataRoute, localizedRoute) => {
249
+ if (dataRoute.endsWith('.rsc'))
250
+ return dataRoute;
251
+ const locale = localizedRoute.split('/').find(Boolean);
252
+ return dataRoute
253
+ .replace(new RegExp(`/_next/data/(.+?)/(${locale}/)?`), `/_next/data/$1/${locale}/`)
254
+ .replace(/\/index\.json$/, '.json');
255
+ };
256
+ exports.localizeDataRoute = localizeDataRoute;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NetlifyNextServer = void 0;
4
+ const handlerUtils_1 = require("./handlerUtils");
5
+ const NextServer = (0, handlerUtils_1.getNextServer)();
6
+ class NetlifyNextServer extends NextServer {
7
+ constructor(options, netlifyConfig) {
8
+ super(options);
9
+ this.netlifyConfig = netlifyConfig;
10
+ // copy the prerender manifest so it doesn't get mutated by Next.js
11
+ const manifest = this.getPrerenderManifest();
12
+ this.netlifyPrerenderManifest = {
13
+ ...manifest,
14
+ routes: { ...manifest.routes },
15
+ dynamicRoutes: { ...manifest.dynamicRoutes },
16
+ };
17
+ }
18
+ getRequestHandler() {
19
+ const handler = super.getRequestHandler();
20
+ return async (req, res, parsedUrl) => {
21
+ // preserve the URL before Next.js mutates it for i18n
22
+ const { url, headers } = req;
23
+ // handle the original res.revalidate() request
24
+ await handler(req, res, parsedUrl);
25
+ // handle on-demand revalidation by purging the ODB cache
26
+ if (res.statusCode === 200 && headers['x-prerender-revalidate'] && this.netlifyConfig.revalidateToken) {
27
+ await this.netlifyRevalidate(url);
28
+ }
29
+ };
30
+ }
31
+ async netlifyRevalidate(route) {
32
+ try {
33
+ // call netlify API to revalidate the path
34
+ const result = await (0, handlerUtils_1.netlifyApiFetch)({
35
+ endpoint: `sites/${process.env.SITE_ID}/refresh_on_demand_builders`,
36
+ payload: {
37
+ paths: this.getNetlifyPathsForRoute(route),
38
+ domain: this.hostname,
39
+ },
40
+ token: this.netlifyConfig.revalidateToken,
41
+ method: 'POST',
42
+ });
43
+ if (!result.ok) {
44
+ throw new Error(result.message);
45
+ }
46
+ }
47
+ catch (error) {
48
+ console.log(`Error revalidating ${route}:`, error.message);
49
+ throw error;
50
+ }
51
+ }
52
+ getNetlifyPathsForRoute(route) {
53
+ const { i18n } = this.nextConfig;
54
+ const { routes, dynamicRoutes } = this.netlifyPrerenderManifest;
55
+ // matches static routes
56
+ const normalizedRoute = (0, handlerUtils_1.normalizeRoute)(i18n ? (0, handlerUtils_1.localizeRoute)(route, i18n) : route);
57
+ if (normalizedRoute in routes) {
58
+ const { dataRoute } = routes[normalizedRoute];
59
+ const normalizedDataRoute = i18n ? (0, handlerUtils_1.localizeDataRoute)(dataRoute, normalizedRoute) : dataRoute;
60
+ return [route, normalizedDataRoute];
61
+ }
62
+ // matches dynamic routes
63
+ const unlocalizedRoute = i18n ? (0, handlerUtils_1.unlocalizeRoute)(normalizedRoute, i18n) : normalizedRoute;
64
+ for (const dynamicRoute in dynamicRoutes) {
65
+ const { dataRoute, routeRegex } = dynamicRoutes[dynamicRoute];
66
+ const matches = unlocalizedRoute.match(routeRegex);
67
+ if (matches && matches.length !== 0) {
68
+ // remove the first match, which is the full route
69
+ matches.shift();
70
+ // replace the dynamic segments with the actual values
71
+ const interpolatedDataRoute = dataRoute.replace(/\[(.*?)]/g, () => matches.shift());
72
+ const normalizedDataRoute = i18n
73
+ ? (0, handlerUtils_1.localizeDataRoute)(interpolatedDataRoute, normalizedRoute)
74
+ : interpolatedDataRoute;
75
+ return [route, normalizedDataRoute];
76
+ }
77
+ }
78
+ throw new Error(`not an ISR route`);
79
+ }
80
+ }
81
+ exports.NetlifyNextServer = NetlifyNextServer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "4.32.1",
3
+ "version": "4.33.0",
4
4
  "description": "Run Next.js seamlessly on Netlify",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -16,6 +16,7 @@
16
16
  "@netlify/ipx": "^1.3.3",
17
17
  "@vercel/node-bridge": "^2.1.0",
18
18
  "chalk": "^4.1.2",
19
+ "chokidar": "^3.5.3",
19
20
  "destr": "^1.1.1",
20
21
  "execa": "^5.1.1",
21
22
  "follow-redirects": "^1.15.2",
@@ -36,7 +37,7 @@
36
37
  },
37
38
  "devDependencies": {
38
39
  "@delucis/if-env": "^1.1.2",
39
- "@netlify/build": "^29.6.8",
40
+ "@netlify/build": "^29.7.1",
40
41
  "@types/fs-extra": "^9.0.13",
41
42
  "@types/jest": "^27.4.1",
42
43
  "@types/merge-stream": "^1.1.2",
@@ -38,8 +38,7 @@ const handler = async (req, context) => {
38
38
  const nextMiddleware = await import(`../../middleware.js#${++idx}`)
39
39
  middleware = nextMiddleware.middleware
40
40
  } catch (importError) {
41
- // Error message is `Module not found "file://<path>/middleware.js#123456".` in Deno
42
- if (importError.code === 'ERR_MODULE_NOT_FOUND' && importError.message.includes(`middleware.js#${idx}`)) {
41
+ if (importError.code === 'ERR_MODULE_NOT_FOUND' && importError.message.includes(`middleware.js`)) {
43
42
  // No middleware, so we silently return
44
43
  return
45
44
  }
@@ -8,6 +8,22 @@ export interface FetchEventResult {
8
8
 
9
9
  type NextDataTransform = <T>(data: T) => T
10
10
 
11
+ function normalizeDataUrl(redirect: string) {
12
+ // If the redirect is a data URL, we need to normalize it.
13
+ // next.js code reference: https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-next-pathname-info.ts#L46
14
+ if (redirect.startsWith('/_next/data/') && redirect.includes('.json')) {
15
+ const paths = redirect
16
+ .replace(/^\/_next\/data\//, '')
17
+ .replace(/\.json/, '')
18
+ .split('/')
19
+
20
+ const buildId = paths[0]
21
+ redirect = paths[1] !== 'index' ? `/${paths.slice(1).join('/')}` : '/'
22
+ }
23
+
24
+ return redirect
25
+ }
26
+
11
27
  /**
12
28
  * This is how Next handles rewritten URLs.
13
29
  */
@@ -196,7 +212,9 @@ export const buildResponse = async ({
196
212
  // Apply all of the transforms to the props
197
213
  const props = response.dataTransforms.reduce((prev, transform) => transform(prev), data.props)
198
214
  // Replace the data with the transformed props
199
- textChunk.replace(JSON.stringify({ ...data, props }))
215
+ // With `html: true` the input is treated as raw HTML
216
+ // @see https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/#global-types
217
+ textChunk.replace(JSON.stringify({ ...data, props }), { html: true })
200
218
  } catch (err) {
201
219
  console.log('Could not parse', err)
202
220
  }
@@ -249,6 +267,12 @@ export const buildResponse = async ({
249
267
  res.headers.set('x-nextjs-redirect', relativizeURL(redirect, request.url))
250
268
  }
251
269
 
270
+ const nextRedirect = res.headers.get('x-nextjs-redirect')
271
+
272
+ if (nextRedirect && isDataReq) {
273
+ res.headers.set('x-nextjs-redirect', normalizeDataUrl(nextRedirect))
274
+ }
275
+
252
276
  if (res.headers.get('x-middleware-next') === '1') {
253
277
  return addMiddlewareHeaders(context.next(), res)
254
278
  }