@netlify/plugin-nextjs 4.26.0 → 4.27.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.
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.extractConfigFromFile = exports.validateConfigValue = void 0;
27
+ const fs_1 = __importStar(require("fs"));
28
+ const extract_const_value_1 = require("next/dist/build/analysis/extract-const-value");
29
+ const parse_module_1 = require("next/dist/build/analysis/parse-module");
30
+ const pathe_1 = require("pathe");
31
+ const validateConfigValue = (config, apiFilePath) => {
32
+ if (config.type === "experimental-scheduled" /* ApiRouteType.SCHEDULED */) {
33
+ if (!config.schedule) {
34
+ console.error(`Invalid config value in ${(0, pathe_1.relative)(process.cwd(), apiFilePath)}: schedule is required when type is "${"experimental-scheduled" /* ApiRouteType.SCHEDULED */}"`);
35
+ return false;
36
+ }
37
+ if (config.runtime === 'experimental-edge') {
38
+ console.error(`Invalid config value in ${(0, pathe_1.relative)(process.cwd(), apiFilePath)}: edge runtime is not supported for scheduled functions`);
39
+ return false;
40
+ }
41
+ return true;
42
+ }
43
+ if (!config.type || config.type === "experimental-background" /* ApiRouteType.BACKGROUND */) {
44
+ if (config.schedule) {
45
+ console.error(`Invalid config value in ${(0, pathe_1.relative)(process.cwd(), apiFilePath)}: schedule is not allowed unless type is "${"experimental-scheduled" /* ApiRouteType.SCHEDULED */}"`);
46
+ return false;
47
+ }
48
+ if (config.type && config.runtime === 'experimental-edge') {
49
+ console.error(`Invalid config value in ${(0, pathe_1.relative)(process.cwd(), apiFilePath)}: edge runtime is not supported for background functions`);
50
+ return false;
51
+ }
52
+ return true;
53
+ }
54
+ console.error(`Invalid config value in ${(0, pathe_1.relative)(process.cwd(), apiFilePath)}: type ${config.type} is not supported`);
55
+ return false;
56
+ };
57
+ exports.validateConfigValue = validateConfigValue;
58
+ /**
59
+ * Uses Next's swc static analysis to extract the config values from a file.
60
+ */
61
+ const extractConfigFromFile = async (apiFilePath) => {
62
+ if (!apiFilePath || !(0, fs_1.existsSync)(apiFilePath)) {
63
+ return {};
64
+ }
65
+ const fileContent = await fs_1.default.promises.readFile(apiFilePath, 'utf8');
66
+ // No need to parse if there's no "config"
67
+ if (!fileContent.includes('config')) {
68
+ return {};
69
+ }
70
+ const ast = await (0, parse_module_1.parseModule)(apiFilePath, fileContent);
71
+ let config;
72
+ try {
73
+ config = (0, extract_const_value_1.extractExportedConstValue)(ast, 'config');
74
+ }
75
+ catch (error) {
76
+ if (error instanceof extract_const_value_1.UnsupportedValueError) {
77
+ console.warn(`Unsupported config value in ${(0, pathe_1.relative)(process.cwd(), apiFilePath)}`);
78
+ }
79
+ return {};
80
+ }
81
+ if ((0, exports.validateConfigValue)(config, apiFilePath)) {
82
+ return config;
83
+ }
84
+ throw new Error(`Unsupported config value in ${(0, pathe_1.relative)(process.cwd(), apiFilePath)}`);
85
+ };
86
+ exports.extractConfigFromFile = extractConfigFromFile;
@@ -69,7 +69,7 @@ const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore = [] }
69
69
  }
70
70
  /* eslint-enable no-underscore-dangle */
71
71
  ;
72
- [constants_1.HANDLER_FUNCTION_NAME, constants_1.ODB_FUNCTION_NAME].forEach((functionName) => {
72
+ [constants_1.HANDLER_FUNCTION_NAME, constants_1.ODB_FUNCTION_NAME, '_api_*'].forEach((functionName) => {
73
73
  var _a, _b;
74
74
  (_a = netlifyConfig.functions)[functionName] || (_a[functionName] = { included_files: [], external_node_modules: [] });
75
75
  netlifyConfig.functions[functionName].node_bundler = 'nft';
@@ -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.enableEdgeInNextConfig = exports.writeEdgeFunctions = exports.writeDevEdgeFunction = exports.cleanupEdgeFunctions = exports.loadMiddlewareManifest = void 0;
6
+ exports.writeEdgeFunctions = exports.writeDevEdgeFunction = exports.cleanupEdgeFunctions = exports.loadMiddlewareManifest = void 0;
7
7
  /* eslint-disable max-lines */
8
8
  const fs_1 = require("fs");
9
9
  const path_1 = require("path");
@@ -181,10 +181,4 @@ const writeEdgeFunctions = async (netlifyConfig) => {
181
181
  await (0, fs_extra_1.writeJson)((0, path_1.join)(edgeFunctionRoot, 'manifest.json'), manifest);
182
182
  };
183
183
  exports.writeEdgeFunctions = writeEdgeFunctions;
184
- const enableEdgeInNextConfig = async (publish) => {
185
- const configFile = (0, path_1.join)(publish, 'required-server-files.json');
186
- const config = await (0, fs_extra_1.readJSON)(configFile);
187
- await (0, fs_extra_1.writeJSON)(configFile, config);
188
- };
189
- exports.enableEdgeInNextConfig = enableEdgeInNextConfig;
190
184
  /* eslint-enable max-lines */
@@ -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.movePublicFiles = exports.unpatchNextFiles = exports.unpatchFile = exports.patchNextFiles = exports.moveStaticPages = exports.getMiddleware = exports.matchesRewrite = exports.matchesRedirect = exports.matchMiddleware = exports.stripLocale = exports.isDynamicRoute = void 0;
6
+ exports.movePublicFiles = exports.unpatchNextFiles = exports.unpatchFile = exports.patchNextFiles = exports.getDependenciesOfFile = exports.getSourceFileForPage = exports.moveStaticPages = exports.getMiddleware = exports.matchesRewrite = exports.matchesRedirect = exports.matchMiddleware = exports.stripLocale = exports.isDynamicRoute = void 0;
7
7
  /* eslint-disable max-lines */
8
8
  const os_1 = require("os");
9
9
  const chalk_1 = require("chalk");
@@ -16,6 +16,7 @@ const slash_1 = __importDefault(require("slash"));
16
16
  const constants_1 = require("../constants");
17
17
  const utils_1 = require("./utils");
18
18
  const TEST_ROUTE = /(|\/)\[[^/]+?](\/|\.html|$)/;
19
+ const SOURCE_FILE_EXTENSIONS = ['js', 'jsx', 'ts', 'tsx'];
19
20
  const isDynamicRoute = (route) => TEST_ROUTE.test(route);
20
21
  exports.isDynamicRoute = isDynamicRoute;
21
22
  const stripLocale = (rawPath, locales = []) => {
@@ -270,6 +271,31 @@ const getServerFile = (root, includeBase = true) => {
270
271
  }
271
272
  return (0, utils_1.findModuleFromBase)({ candidates, paths: [root] });
272
273
  };
274
+ /**
275
+ * Find the source file for a given page route
276
+ */
277
+ const getSourceFileForPage = (page, root) => {
278
+ for (const extension of SOURCE_FILE_EXTENSIONS) {
279
+ const file = (0, pathe_1.join)(root, `${page}.${extension}`);
280
+ if ((0, fs_extra_1.existsSync)(file)) {
281
+ return file;
282
+ }
283
+ }
284
+ console.log('Could not find source file for page', page);
285
+ };
286
+ exports.getSourceFileForPage = getSourceFileForPage;
287
+ /**
288
+ * Reads the node file trace file for a given file, and resolves the dependencies
289
+ */
290
+ const getDependenciesOfFile = async (file) => {
291
+ const nft = `${file}.nft.json`;
292
+ if (!(0, fs_extra_1.existsSync)(nft)) {
293
+ return [];
294
+ }
295
+ const dependencies = await (0, fs_extra_1.readJson)(nft, 'utf8');
296
+ return dependencies.files.map((dep) => (0, pathe_1.resolve)(file, dep));
297
+ };
298
+ exports.getDependenciesOfFile = getDependenciesOfFile;
273
299
  const baseServerReplacements = [
274
300
  // force manual revalidate during cache fetches
275
301
  [
@@ -3,24 +3,48 @@ 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.setupImageFunction = exports.generatePagesResolver = exports.generateFunctions = void 0;
6
+ exports.warnOnApiRoutes = exports.getApiRouteConfigs = exports.setupImageFunction = exports.generatePagesResolver = exports.generateFunctions = void 0;
7
7
  const node_bridge_1 = __importDefault(require("@vercel/node-bridge"));
8
+ const chalk_1 = __importDefault(require("chalk"));
8
9
  const destr_1 = __importDefault(require("destr"));
9
10
  const fs_extra_1 = require("fs-extra");
11
+ const outdent_1 = require("outdent");
10
12
  const pathe_1 = require("pathe");
11
13
  const constants_1 = require("../constants");
14
+ const getApiHandler_1 = require("../templates/getApiHandler");
12
15
  const getHandler_1 = require("../templates/getHandler");
13
16
  const getPageResolver_1 = require("../templates/getPageResolver");
14
- const generateFunctions = async ({ FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, PUBLISH_DIR }, appDir) => {
15
- const functionsDir = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC;
16
- const functionDir = (0, pathe_1.join)(process.cwd(), functionsDir, constants_1.HANDLER_FUNCTION_NAME);
17
- const publishDir = (0, pathe_1.relative)(functionDir, (0, pathe_1.resolve)(PUBLISH_DIR));
18
- const writeHandler = async (func, isODB) => {
17
+ const analysis_1 = require("./analysis");
18
+ const files_1 = require("./files");
19
+ const utils_1 = require("./utils");
20
+ const generateFunctions = async ({ FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, PUBLISH_DIR }, appDir, apiRoutes) => {
21
+ const publish = (0, pathe_1.resolve)(PUBLISH_DIR);
22
+ const functionsDir = (0, pathe_1.resolve)(INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC);
23
+ console.log({ functionsDir });
24
+ const functionDir = (0, pathe_1.join)(functionsDir, constants_1.HANDLER_FUNCTION_NAME);
25
+ const publishDir = (0, pathe_1.relative)(functionDir, publish);
26
+ for (const { route, config, compiled } of apiRoutes) {
27
+ const apiHandlerSource = await (0, getApiHandler_1.getApiHandler)({
28
+ page: route,
29
+ config,
30
+ });
31
+ const functionName = (0, utils_1.getFunctionNameForPage)(route, config.type === "experimental-background" /* ApiRouteType.BACKGROUND */);
32
+ await (0, fs_extra_1.ensureDir)((0, pathe_1.join)(functionsDir, functionName));
33
+ await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsDir, functionName, `${functionName}.js`), apiHandlerSource);
34
+ await (0, fs_extra_1.copyFile)(node_bridge_1.default, (0, pathe_1.join)(functionsDir, functionName, 'bridge.js'));
35
+ await (0, fs_extra_1.copyFile)((0, pathe_1.join)(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'), (0, pathe_1.join)(functionsDir, functionName, 'handlerUtils.js'));
36
+ const resolverSource = await (0, getPageResolver_1.getSinglePageResolver)({
37
+ functionsDir,
38
+ sourceFile: (0, pathe_1.join)(publish, 'server', compiled),
39
+ });
40
+ await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsDir, functionName, 'pages.js'), resolverSource);
41
+ }
42
+ const writeHandler = async (functionName, isODB) => {
19
43
  const handlerSource = await (0, getHandler_1.getHandler)({ isODB, publishDir, appDir: (0, pathe_1.relative)(functionDir, appDir) });
20
- await (0, fs_extra_1.ensureDir)((0, pathe_1.join)(functionsDir, func));
21
- await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsDir, func, `${func}.js`), handlerSource);
22
- await (0, fs_extra_1.copyFile)(node_bridge_1.default, (0, pathe_1.join)(functionsDir, func, 'bridge.js'));
23
- await (0, fs_extra_1.copyFile)((0, pathe_1.join)(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'), (0, pathe_1.join)(functionsDir, func, 'handlerUtils.js'));
44
+ await (0, fs_extra_1.ensureDir)((0, pathe_1.join)(functionsDir, functionName));
45
+ await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsDir, functionName, `${functionName}.js`), handlerSource);
46
+ await (0, fs_extra_1.copyFile)(node_bridge_1.default, (0, pathe_1.join)(functionsDir, functionName, 'bridge.js'));
47
+ await (0, fs_extra_1.copyFile)((0, pathe_1.join)(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'), (0, pathe_1.join)(functionsDir, functionName, 'handlerUtils.js'));
24
48
  };
25
49
  await writeHandler(constants_1.HANDLER_FUNCTION_NAME, false);
26
50
  await writeHandler(constants_1.ODB_FUNCTION_NAME, true);
@@ -92,3 +116,42 @@ const setupImageFunction = async ({ constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIO
92
116
  }
93
117
  };
94
118
  exports.setupImageFunction = setupImageFunction;
119
+ /**
120
+ * Look for API routes, and extract the config from the source file.
121
+ */
122
+ const getApiRouteConfigs = async (publish, baseDir) => {
123
+ const pages = await (0, fs_extra_1.readJSON)((0, pathe_1.join)(publish, 'server', 'pages-manifest.json'));
124
+ const apiRoutes = Object.keys(pages).filter((page) => page.startsWith('/api/'));
125
+ const pagesDir = (0, pathe_1.join)(baseDir, 'pages');
126
+ return Promise.all(apiRoutes.map(async (apiRoute) => {
127
+ const filePath = (0, files_1.getSourceFileForPage)(apiRoute, pagesDir);
128
+ return { route: apiRoute, config: await (0, analysis_1.extractConfigFromFile)(filePath), compiled: pages[apiRoute] };
129
+ }));
130
+ };
131
+ exports.getApiRouteConfigs = getApiRouteConfigs;
132
+ /**
133
+ * Warn the user of the caveats if they're using background or scheduled API routes
134
+ */
135
+ const warnOnApiRoutes = async ({ FUNCTIONS_DIST, }) => {
136
+ const functionsManifestPath = (0, pathe_1.join)(FUNCTIONS_DIST, 'manifest.json');
137
+ if (!(0, fs_extra_1.existsSync)(functionsManifestPath)) {
138
+ return;
139
+ }
140
+ const { functions } = await (0, fs_extra_1.readJSON)(functionsManifestPath);
141
+ if (functions.some((func) => func.name.endsWith('-background'))) {
142
+ console.warn((0, outdent_1.outdent) `
143
+ ${chalk_1.default.yellowBright `Using background API routes`}
144
+ If your account type does not support background functions, the deploy will fail.
145
+ During local development, background API routes will run as regular API routes, but in production they will immediately return an empty "202 Accepted" response.
146
+ `);
147
+ }
148
+ if (functions.some((func) => func.schedule)) {
149
+ console.warn((0, outdent_1.outdent) `
150
+ ${chalk_1.default.yellowBright `Using scheduled API routes`}
151
+ These are run on a schedule when deployed to production.
152
+ You can test them locally by loading them in your browser but this will not be available when deployed, and any returned value is ignored.
153
+ `);
154
+ }
155
+ };
156
+ exports.warnOnApiRoutes = warnOnApiRoutes;
157
+ /* eslint-enable max-lines */
@@ -152,7 +152,7 @@ const generateDynamicRewrites = ({ dynamicRoutes, prerenderedDynamicRoutes, midd
152
152
  dynamicRewrites,
153
153
  };
154
154
  };
155
- const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath, trailingSlash, appDir }, buildId, }) => {
155
+ const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath, trailingSlash, appDir }, buildId, apiRoutes, }) => {
156
156
  const { dynamicRoutes: prerenderedDynamicRoutes, routes: prerenderedStaticRoutes } = await (0, fs_extra_1.readJSON)((0, pathe_1.join)(netlifyConfig.build.publish, 'prerender-manifest.json'));
157
157
  const { dynamicRoutes, staticRoutes } = await (0, fs_extra_1.readJSON)((0, pathe_1.join)(netlifyConfig.build.publish, 'routes-manifest.json'));
158
158
  netlifyConfig.redirects.push(...generateHiddenPathRedirects({ basePath }));
@@ -162,7 +162,7 @@ const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath,
162
162
  // This is only used in prod, so dev uses `next dev` directly
163
163
  netlifyConfig.redirects.push(
164
164
  // API routes always need to be served from the regular function
165
- ...(0, utils_1.getApiRewrites)(basePath),
165
+ ...(0, utils_1.getApiRewrites)(basePath, apiRoutes),
166
166
  // Preview mode gets forced to the function, to bypass pre-rendered pages, but static files need to be skipped
167
167
  ...(await (0, utils_1.getPreviewRewrites)({ basePath, appDir })));
168
168
  const middleware = await (0, files_1.getMiddleware)(netlifyConfig.build.publish);
@@ -3,10 +3,22 @@ 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.getRemotePatterns = exports.isBundleSizeCheckDisabled = exports.getCustomImageResponseHeaders = exports.isNextAuthInstalled = exports.findModuleFromBase = exports.shouldSkip = exports.getPreviewRewrites = exports.getApiRewrites = exports.redirectsForNextRouteWithData = exports.redirectsForNextRoute = exports.is404Route = exports.isApiRoute = exports.routeToDataRoute = exports.netlifyRoutesForNextRouteWithData = exports.toNetlifyRoute = void 0;
6
+ exports.getRemotePatterns = exports.isBundleSizeCheckDisabled = exports.getCustomImageResponseHeaders = exports.isNextAuthInstalled = exports.findModuleFromBase = exports.shouldSkip = exports.getPreviewRewrites = exports.getApiRewrites = exports.redirectsForNextRouteWithData = exports.redirectsForNextRoute = exports.is404Route = exports.isApiRoute = exports.routeToDataRoute = exports.netlifyRoutesForNextRouteWithData = exports.toNetlifyRoute = exports.getFunctionNameForPage = void 0;
7
7
  const globby_1 = __importDefault(require("globby"));
8
8
  const pathe_1 = require("pathe");
9
9
  const constants_1 = require("../constants");
10
+ const RESERVED_FILENAME = /[^\w_-]/g;
11
+ /**
12
+ * Given a Next route, generates a valid Netlify function name.
13
+ * If "background" is true then the function name will have `-background`
14
+ * appended to it, meaning that it is executed as a background function.
15
+ */
16
+ const getFunctionNameForPage = (page, background = false) => `${page
17
+ .replace(constants_1.CATCH_ALL_REGEX, '_$1-SPLAT')
18
+ .replace(constants_1.OPTIONAL_CATCH_ALL_REGEX, '-SPLAT')
19
+ .replace(constants_1.DYNAMIC_PARAMETER_REGEX, '_$1-PARAM')
20
+ .replace(RESERVED_FILENAME, '_')}-${background ? 'background' : 'handler'}`;
21
+ exports.getFunctionNameForPage = getFunctionNameForPage;
10
22
  const toNetlifyRoute = (nextRoute) => {
11
23
  const netlifyRoutes = [nextRoute];
12
24
  // If the route is an optional catch-all route, we need to add a second
@@ -79,18 +91,28 @@ const redirectsForNextRouteWithData = ({ route, dataRoute, basePath, to, status
79
91
  force,
80
92
  }));
81
93
  exports.redirectsForNextRouteWithData = redirectsForNextRouteWithData;
82
- const getApiRewrites = (basePath) => [
83
- {
84
- from: `${basePath}/api`,
85
- to: constants_1.HANDLER_FUNCTION_PATH,
86
- status: 200,
87
- },
88
- {
89
- from: `${basePath}/api/*`,
90
- to: constants_1.HANDLER_FUNCTION_PATH,
91
- status: 200,
92
- },
93
- ];
94
+ const getApiRewrites = (basePath, apiRoutes) => {
95
+ const apiRewrites = apiRoutes.map((apiRoute) => {
96
+ const [from] = (0, exports.toNetlifyRoute)(`${basePath}${apiRoute.route}`);
97
+ // Scheduled functions can't be invoked directly, so we 404 them.
98
+ if (apiRoute.config.type === "experimental-scheduled" /* ApiRouteType.SCHEDULED */) {
99
+ return { from, to: '/404.html', status: 404 };
100
+ }
101
+ return {
102
+ from,
103
+ to: `/.netlify/functions/${(0, exports.getFunctionNameForPage)(apiRoute.route, apiRoute.config.type === "experimental-background" /* ApiRouteType.BACKGROUND */)}`,
104
+ status: 200,
105
+ };
106
+ });
107
+ return [
108
+ ...apiRewrites,
109
+ {
110
+ from: `${basePath}/api/*`,
111
+ to: constants_1.HANDLER_FUNCTION_PATH,
112
+ status: 200,
113
+ },
114
+ ];
115
+ };
94
116
  exports.getApiRewrites = getApiRewrites;
95
117
  const getPreviewRewrites = async ({ basePath, appDir }) => {
96
118
  const publicFiles = await (0, globby_1.default)('**/*', { cwd: (0, pathe_1.join)(appDir, 'public') });
package/lib/index.js CHANGED
@@ -90,10 +90,11 @@ const plugin = {
90
90
  }
91
91
  const buildId = (0, fs_extra_1.readFileSync)((0, path_1.join)(publish, 'BUILD_ID'), 'utf8').trim();
92
92
  await (0, config_1.configureHandlerFunctions)({ netlifyConfig, ignore, publish: (0, path_1.relative)(process.cwd(), publish) });
93
- await (0, functions_1.generateFunctions)(constants, appDir);
93
+ const apiRoutes = await (0, functions_1.getApiRouteConfigs)(publish, appDir);
94
+ await (0, functions_1.generateFunctions)(constants, appDir, apiRoutes);
94
95
  await (0, functions_1.generatePagesResolver)({ target, constants });
95
96
  await (0, files_1.movePublicFiles)({ appDir, outdir, publish });
96
- await (0, files_1.patchNextFiles)(basePath);
97
+ await (0, files_1.patchNextFiles)(appDir);
97
98
  if (!(0, destr_1.default)(process.env.SERVE_STATIC_FILES_FROM_ORIGIN)) {
98
99
  await (0, files_1.moveStaticPages)({ target, netlifyConfig, i18n, basePath });
99
100
  }
@@ -113,6 +114,7 @@ const plugin = {
113
114
  netlifyConfig,
114
115
  nextConfig: { basePath, i18n, trailingSlash, appDir },
115
116
  buildId,
117
+ apiRoutes,
116
118
  });
117
119
  await (0, edge_1.writeEdgeFunctions)(netlifyConfig);
118
120
  },
@@ -134,6 +136,7 @@ const plugin = {
134
136
  (0, config_1.generateCustomHeaders)(nextConfig, headers);
135
137
  (0, verification_1.warnForProblematicUserRewrites)({ basePath, redirects });
136
138
  (0, verification_1.warnForRootRedirects)({ appDir });
139
+ await (0, functions_1.warnOnApiRoutes)({ FUNCTIONS_DIST });
137
140
  },
138
141
  };
139
142
  // The types haven't been updated yet
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getApiHandler = void 0;
4
+ // Aliasing like this means the editor may be able to syntax-highlight the string
5
+ const outdent_1 = require("outdent");
6
+ /* eslint-disable @typescript-eslint/no-var-requires */
7
+ const { Server } = require('http');
8
+ const path = require('path');
9
+ // eslint-disable-next-line n/prefer-global/url, n/prefer-global/url-search-params
10
+ const { URLSearchParams, URL } = require('url');
11
+ const { Bridge } = require('@vercel/node-bridge/bridge');
12
+ const { getMultiValueHeaders, getNextServer } = require('./handlerUtils');
13
+ // We return a function and then call `toString()` on it to serialise it as the launcher function
14
+ const makeHandler = (conf, app, pageRoot, page) => {
15
+ var _a;
16
+ // Change working directory into the site root, unless using Nx, which moves the
17
+ // dist directory and handles this itself
18
+ const dir = path.resolve(__dirname, app);
19
+ if (pageRoot.startsWith(dir)) {
20
+ process.chdir(dir);
21
+ }
22
+ // This is just so nft knows about the page entrypoints. It's not actually used
23
+ try {
24
+ // eslint-disable-next-line n/no-missing-require
25
+ require.resolve('./pages.js');
26
+ }
27
+ catch { }
28
+ // React assumes you want development mode if NODE_ENV is unset.
29
+ ;
30
+ (_a = process.env).NODE_ENV || (_a.NODE_ENV = 'production');
31
+ // We don't want to write ISR files to disk in the lambda environment
32
+ conf.experimental.isrFlushToDisk = false;
33
+ // This is our flag that we use when patching the source
34
+ // eslint-disable-next-line no-underscore-dangle
35
+ process.env._BYPASS_SSG = 'true';
36
+ for (const [key, value] of Object.entries(conf.env)) {
37
+ process.env[key] = String(value);
38
+ }
39
+ // We memoize this because it can be shared between requests, but don't instantiate it until
40
+ // the first request because we need the host and port.
41
+ let bridge;
42
+ const getBridge = (event) => {
43
+ if (bridge) {
44
+ return bridge;
45
+ }
46
+ // Scheduled functions don't have a URL, but we need to give one so Next knows the route to serve
47
+ const url = event.rawUrl ? new URL(event.rawUrl) : new URL(path, process.env.URL || 'http://n');
48
+ const port = Number.parseInt(url.port) || 80;
49
+ const NextServer = getNextServer();
50
+ const nextServer = new NextServer({
51
+ // We know we're just an API route, so can enable minimal mode
52
+ minimalMode: true,
53
+ conf,
54
+ dir,
55
+ customServer: false,
56
+ hostname: url.hostname,
57
+ port,
58
+ });
59
+ const requestHandler = nextServer.getRequestHandler();
60
+ const server = new Server(async (req, res) => {
61
+ try {
62
+ await requestHandler(req, res);
63
+ }
64
+ catch (error) {
65
+ console.error(error);
66
+ throw new Error('Error handling request. See function logs for details.');
67
+ }
68
+ });
69
+ bridge = new Bridge(server);
70
+ bridge.listen();
71
+ return bridge;
72
+ };
73
+ return async function handler(event, context) {
74
+ // Ensure that paths are encoded - but don't double-encode them
75
+ event.path = event.rawUrl ? new URL(event.rawUrl).pathname : page;
76
+ // Next expects to be able to parse the query from the URL
77
+ const query = new URLSearchParams(event.queryStringParameters).toString();
78
+ event.path = query ? `${event.path}?${query}` : event.path;
79
+ // We know the page
80
+ event.headers['x-matched-path'] = page;
81
+ const { headers, ...result } = await getBridge(event).launcher(event, context);
82
+ // Convert all headers to multiValueHeaders
83
+ const multiValueHeaders = getMultiValueHeaders(headers);
84
+ multiValueHeaders['cache-control'] = ['public, max-age=0, must-revalidate'];
85
+ console.log(`[${event.httpMethod}] ${event.path} (API)`);
86
+ return {
87
+ ...result,
88
+ multiValueHeaders,
89
+ isBase64Encoded: result.encoding === 'base64',
90
+ };
91
+ };
92
+ };
93
+ /**
94
+ * Handlers for API routes are simpler than page routes, but they each have a separate one
95
+ */
96
+ const getApiHandler = ({ page, config, publishDir = '../../../.next', appDir = '../../..', }) =>
97
+ // This is a string, but if you have the right editor plugin it should format as js
98
+ (0, outdent_1.outdent /* javascript */) `
99
+ const { Server } = require("http");
100
+ // We copy the file here rather than requiring from the node module
101
+ const { Bridge } = require("./bridge");
102
+ const { getMaxAge, getMultiValueHeaders, getNextServer } = require('./handlerUtils')
103
+
104
+ ${config.type === "experimental-scheduled" /* ApiRouteType.SCHEDULED */ ? `const { schedule } = require("@netlify/functions")` : ''}
105
+
106
+
107
+ const { config } = require("${publishDir}/required-server-files.json")
108
+ let staticManifest
109
+ const path = require("path");
110
+ const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", "serverless", "pages"));
111
+ const handler = (${makeHandler.toString()})(config, "${appDir}", pageRoot, ${JSON.stringify(page)})
112
+ exports.handler = ${config.type === "experimental-scheduled" /* ApiRouteType.SCHEDULED */ ? `schedule(${JSON.stringify(config.schedule)}, handler);` : 'handler'}
113
+ `;
114
+ exports.getApiHandler = getApiHandler;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getHandler = void 0;
4
+ // Aliasing like this means the editor may be able to syntax-highlight the string
4
5
  const outdent_1 = require("outdent");
5
6
  /* eslint-disable @typescript-eslint/no-var-requires */
6
7
  const { promises } = require('fs');
@@ -127,14 +128,14 @@ const makeHandler = (conf, app, pageRoot, staticManifest = [], mode = 'ssr') =>
127
128
  };
128
129
  const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '../../..' }) =>
129
130
  // This is a string, but if you have the right editor plugin it should format as js
130
- (0, outdent_1.outdent) `
131
+ (0, outdent_1.outdent /* javascript */) `
131
132
  const { Server } = require("http");
132
133
  const { promises } = require("fs");
133
134
  // We copy the file here rather than requiring from the node module
134
135
  const { Bridge } = require("./bridge");
135
136
  const { augmentFsModule, getMaxAge, getMultiValueHeaders, getNextServer } = require('./handlerUtils')
136
137
 
137
- const { builder } = require("@netlify/functions");
138
+ ${isODB ? `const { builder } = require("@netlify/functions")` : ''}
138
139
  const { config } = require("${publishDir}/required-server-files.json")
139
140
  let staticManifest
140
141
  try {
@@ -3,12 +3,14 @@ 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.getPageResolver = void 0;
6
+ exports.getSinglePageResolver = exports.getPageResolver = void 0;
7
7
  const path_1 = require("path");
8
8
  const outdent_1 = require("outdent");
9
+ const pathe_1 = require("pathe");
9
10
  const slash_1 = __importDefault(require("slash"));
10
11
  const tiny_glob_1 = __importDefault(require("tiny-glob"));
11
12
  const constants_1 = require("../constants");
13
+ const files_1 = require("../helpers/files");
12
14
  // Generate a file full of require.resolve() calls for all the pages in the
13
15
  // build. This is used by the nft bundler to find all the pages.
14
16
  const getPageResolver = async ({ publish, target }) => {
@@ -31,3 +33,22 @@ const getPageResolver = async ({ publish, target }) => {
31
33
  `;
32
34
  };
33
35
  exports.getPageResolver = getPageResolver;
36
+ /**
37
+ * API routes only need the dependencies for a single entrypoint, so we use the
38
+ * NFT trace file to get the dependencies.
39
+ */
40
+ const getSinglePageResolver = async ({ functionsDir, sourceFile, }) => {
41
+ const dependencies = await (0, files_1.getDependenciesOfFile)(sourceFile);
42
+ // We don't need the actual name, just the relative path.
43
+ const functionDir = (0, pathe_1.resolve)(functionsDir, 'functionName');
44
+ const pageFiles = [sourceFile, ...dependencies]
45
+ .map((file) => `require.resolve('${(0, pathe_1.relative)(functionDir, file)}')`)
46
+ .sort();
47
+ return (0, outdent_1.outdent /* javascript */) `
48
+ // This file is purely to allow nft to know about these pages.
49
+ try {
50
+ ${pageFiles.join('\n ')}
51
+ } catch {}
52
+ `;
53
+ };
54
+ exports.getSinglePageResolver = getSinglePageResolver;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "4.26.0",
3
+ "version": "4.27.0",
4
4
  "description": "Run Next.js seamlessly on Netlify",
5
5
  "main": "index.js",
6
6
  "files": [