@netlify/plugin-nextjs 4.36.1 → 4.37.1
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/helpers/config.js +19 -4
- package/lib/helpers/files.js +5 -1
- package/lib/helpers/flags.js +22 -0
- package/lib/helpers/functions.js +134 -14
- package/lib/helpers/pack.js +41 -0
- package/lib/helpers/redirects.js +2 -2
- package/lib/helpers/utils.js +4 -4
- package/lib/index.js +14 -5
- package/lib/templates/getApiHandler.js +18 -14
- package/lib/templates/getHandler.js +1 -1
- package/lib/templates/server.js +9 -4
- package/package.json +3 -3
- package/src/templates/edge/next-dev.js +12 -2
- package/src/templates/edge-shared/utils.ts +2 -0
package/lib/helpers/config.js
CHANGED
|
@@ -9,6 +9,7 @@ const fs_extra_1 = require("fs-extra");
|
|
|
9
9
|
const pathe_1 = require("pathe");
|
|
10
10
|
const slash_1 = __importDefault(require("slash"));
|
|
11
11
|
const constants_1 = require("../constants");
|
|
12
|
+
const flags_1 = require("./flags");
|
|
12
13
|
const utils_1 = require("./utils");
|
|
13
14
|
const ROUTES_MANIFEST_FILE = 'routes-manifest.json';
|
|
14
15
|
const defaultFailBuild = (message, { error }) => {
|
|
@@ -64,8 +65,8 @@ const hasManuallyAddedModule = ({ netlifyConfig, moduleName, }) =>
|
|
|
64
65
|
Object.values(netlifyConfig.functions).some(({ included_files = [] }) => included_files.some((inc) => inc.includes(`node_modules/${moduleName}`)));
|
|
65
66
|
exports.hasManuallyAddedModule = hasManuallyAddedModule;
|
|
66
67
|
/* eslint-enable camelcase */
|
|
67
|
-
const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore = [], }) => {
|
|
68
|
-
var _a;
|
|
68
|
+
const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore = [], apiLambdas, featureFlags, }) => {
|
|
69
|
+
var _a, _b, _c;
|
|
69
70
|
const config = await (0, exports.getRequiredServerFiles)(publish);
|
|
70
71
|
const files = config.files || [];
|
|
71
72
|
const cssFilesToInclude = files.filter((f) => f.startsWith(`${publish}/static/css/`));
|
|
@@ -75,7 +76,7 @@ const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore = [],
|
|
|
75
76
|
}
|
|
76
77
|
// If the user has manually added the module to included_files, then don't exclude it
|
|
77
78
|
const excludedModules = DEFAULT_EXCLUDED_MODULES.filter((moduleName) => !(0, exports.hasManuallyAddedModule)({ netlifyConfig, moduleName }));
|
|
78
|
-
|
|
79
|
+
const configureFunction = (functionName) => {
|
|
79
80
|
var _a, _b;
|
|
80
81
|
(_a = netlifyConfig.functions)[functionName] || (_a[functionName] = { included_files: [], external_node_modules: [] });
|
|
81
82
|
netlifyConfig.functions[functionName].node_bundler = 'nft';
|
|
@@ -91,7 +92,21 @@ const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore = [],
|
|
|
91
92
|
netlifyConfig.functions[functionName].included_files.push(`!${moduleRoot}/**/*`);
|
|
92
93
|
}
|
|
93
94
|
});
|
|
94
|
-
}
|
|
95
|
+
};
|
|
96
|
+
configureFunction(constants_1.HANDLER_FUNCTION_NAME);
|
|
97
|
+
configureFunction(constants_1.ODB_FUNCTION_NAME);
|
|
98
|
+
if ((0, flags_1.splitApiRoutes)(featureFlags)) {
|
|
99
|
+
for (const apiLambda of apiLambdas) {
|
|
100
|
+
const { functionName, includedFiles } = apiLambda;
|
|
101
|
+
(_b = netlifyConfig.functions)[functionName] || (_b[functionName] = { included_files: [] });
|
|
102
|
+
netlifyConfig.functions[functionName].node_bundler = 'none';
|
|
103
|
+
(_c = netlifyConfig.functions[functionName]).included_files || (_c.included_files = []);
|
|
104
|
+
netlifyConfig.functions[functionName].included_files.push(...includedFiles);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
configureFunction('_api_*');
|
|
109
|
+
}
|
|
95
110
|
};
|
|
96
111
|
exports.configureHandlerFunctions = configureHandlerFunctions;
|
|
97
112
|
const buildHeader = (buildHeaderParams) => {
|
package/lib/helpers/files.js
CHANGED
|
@@ -291,6 +291,10 @@ const getSourceFileForPage = (page, roots, pageExtensions = SOURCE_FILE_EXTENSIO
|
|
|
291
291
|
if ((0, fs_extra_1.existsSync)(file)) {
|
|
292
292
|
return file;
|
|
293
293
|
}
|
|
294
|
+
const fileAtFolderIndex = (0, pathe_1.join)(root, page, `index.${extension}`);
|
|
295
|
+
if ((0, fs_extra_1.existsSync)(fileAtFolderIndex)) {
|
|
296
|
+
return fileAtFolderIndex;
|
|
297
|
+
}
|
|
294
298
|
}
|
|
295
299
|
}
|
|
296
300
|
console.log('Could not find source file for page', page);
|
|
@@ -304,7 +308,7 @@ const getDependenciesOfFile = async (file) => {
|
|
|
304
308
|
if (!(0, fs_extra_1.existsSync)(nft)) {
|
|
305
309
|
return [];
|
|
306
310
|
}
|
|
307
|
-
const dependencies = await (0, fs_extra_1.readJson)(nft, 'utf8');
|
|
311
|
+
const dependencies = (await (0, fs_extra_1.readJson)(nft, 'utf8'));
|
|
308
312
|
return dependencies.files.map((dep) => (0, pathe_1.resolve)((0, pathe_1.dirname)(file), dep));
|
|
309
313
|
};
|
|
310
314
|
exports.getDependenciesOfFile = getDependenciesOfFile;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.splitApiRoutes = void 0;
|
|
7
|
+
const destr_1 = __importDefault(require("destr"));
|
|
8
|
+
/**
|
|
9
|
+
* If this flag is enabled, we generate individual Lambda functions for API Routes.
|
|
10
|
+
* They're packed together in 50mb chunks to avoid hitting the Lambda size limit.
|
|
11
|
+
*
|
|
12
|
+
* To prevent bundling times from rising,
|
|
13
|
+
* we use the "none" bundling strategy where we fully rely on Next.js' `.nft.json` files.
|
|
14
|
+
* This should to a significant speedup, but is still experimental.
|
|
15
|
+
*
|
|
16
|
+
* If disabled, we bundle all API Routes into a single function.
|
|
17
|
+
* This is can lead to large bundle sizes.
|
|
18
|
+
*
|
|
19
|
+
* Disabled by default. Can be overriden using the NEXT_SPLIT_API_ROUTES env var.
|
|
20
|
+
*/
|
|
21
|
+
const splitApiRoutes = (featureFlags) => { var _a, _b; return (_b = (_a = (0, destr_1.default)(process.env.NEXT_SPLIT_API_ROUTES)) !== null && _a !== void 0 ? _a : featureFlags.next_split_api_routes) !== null && _b !== void 0 ? _b : false; };
|
|
22
|
+
exports.splitApiRoutes = splitApiRoutes;
|
package/lib/helpers/functions.js
CHANGED
|
@@ -3,22 +3,25 @@ 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.warnOnApiRoutes = exports.getExtendedApiRouteConfigs = exports.getApiRouteConfigs = exports.setupImageFunction = exports.generatePagesResolver = exports.generateFunctions = void 0;
|
|
6
|
+
exports.warnOnApiRoutes = exports.packSingleFunction = exports.getExtendedApiRouteConfigs = exports.getApiRouteConfigs = exports.getAPILambdas = exports.getAPIPRouteCommonDependencies = exports.traceNPMPackage = exports.setupImageFunction = exports.generatePagesResolver = exports.generateFunctions = void 0;
|
|
7
7
|
const node_bridge_1 = __importDefault(require("@vercel/node-bridge"));
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
9
|
const destr_1 = __importDefault(require("destr"));
|
|
10
10
|
const fs_extra_1 = require("fs-extra");
|
|
11
11
|
const outdent_1 = require("outdent");
|
|
12
12
|
const pathe_1 = require("pathe");
|
|
13
|
+
const tiny_glob_1 = __importDefault(require("tiny-glob"));
|
|
13
14
|
const constants_1 = require("../constants");
|
|
14
15
|
const getApiHandler_1 = require("../templates/getApiHandler");
|
|
15
16
|
const getHandler_1 = require("../templates/getHandler");
|
|
16
17
|
const getPageResolver_1 = require("../templates/getPageResolver");
|
|
17
18
|
const analysis_1 = require("./analysis");
|
|
19
|
+
const config_1 = require("./config");
|
|
18
20
|
const files_1 = require("./files");
|
|
19
21
|
const functionsMetaData_1 = require("./functionsMetaData");
|
|
22
|
+
const pack_1 = require("./pack");
|
|
20
23
|
const utils_1 = require("./utils");
|
|
21
|
-
const generateFunctions = async ({ FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, PUBLISH_DIR }, appDir,
|
|
24
|
+
const generateFunctions = async ({ FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, PUBLISH_DIR }, appDir, apiLambdas) => {
|
|
22
25
|
const publish = (0, pathe_1.resolve)(PUBLISH_DIR);
|
|
23
26
|
const functionsDir = (0, pathe_1.resolve)(INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC);
|
|
24
27
|
const functionDir = (0, pathe_1.join)(functionsDir, constants_1.HANDLER_FUNCTION_NAME);
|
|
@@ -27,19 +30,16 @@ const generateFunctions = async ({ FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS
|
|
|
27
30
|
const nextServerModuleRelativeLocation = nextServerModuleAbsoluteLocation
|
|
28
31
|
? (0, pathe_1.relative)(functionDir, nextServerModuleAbsoluteLocation)
|
|
29
32
|
: undefined;
|
|
30
|
-
for (const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
page: route,
|
|
37
|
-
config,
|
|
33
|
+
for (const apiLambda of apiLambdas) {
|
|
34
|
+
const { functionName, routes, type, includedFiles } = apiLambda;
|
|
35
|
+
const apiHandlerSource = (0, getApiHandler_1.getApiHandler)({
|
|
36
|
+
// most api lambdas serve multiple routes, but scheduled functions need to be in separate lambdas.
|
|
37
|
+
// so routes[0] is safe to access.
|
|
38
|
+
schedule: type === "experimental-scheduled" /* ApiRouteType.SCHEDULED */ ? routes[0].config.schedule : undefined,
|
|
38
39
|
publishDir,
|
|
39
40
|
appDir: (0, pathe_1.relative)(functionDir, appDir),
|
|
40
41
|
nextServerModuleRelativeLocation,
|
|
41
42
|
});
|
|
42
|
-
const functionName = (0, utils_1.getFunctionNameForPage)(route, config.type === "experimental-background" /* ApiRouteType.BACKGROUND */);
|
|
43
43
|
await (0, fs_extra_1.ensureDir)((0, pathe_1.join)(functionsDir, functionName));
|
|
44
44
|
// write main API handler file
|
|
45
45
|
await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsDir, functionName, `${functionName}.js`), apiHandlerSource);
|
|
@@ -48,15 +48,23 @@ const generateFunctions = async ({ FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS
|
|
|
48
48
|
await (0, fs_extra_1.copyFile)((0, pathe_1.join)(__dirname, '..', '..', 'lib', 'templates', 'server.js'), (0, pathe_1.join)(functionsDir, functionName, 'server.js'));
|
|
49
49
|
await (0, fs_extra_1.copyFile)((0, pathe_1.join)(__dirname, '..', '..', 'lib', 'templates', 'handlerUtils.js'), (0, pathe_1.join)(functionsDir, functionName, 'handlerUtils.js'));
|
|
50
50
|
const resolveSourceFile = (file) => (0, pathe_1.join)(publish, 'server', file);
|
|
51
|
+
// TODO: this should be unneeded once we use the `none` bundler everywhere
|
|
51
52
|
const resolverSource = await (0, getPageResolver_1.getResolverForSourceFiles)({
|
|
52
53
|
functionsDir,
|
|
53
54
|
// These extra pages are always included by Next.js
|
|
54
|
-
sourceFiles: [
|
|
55
|
+
sourceFiles: [
|
|
56
|
+
...routes.map((route) => route.compiled),
|
|
57
|
+
'pages/_app.js',
|
|
58
|
+
'pages/_document.js',
|
|
59
|
+
'pages/_error.js',
|
|
60
|
+
].map(resolveSourceFile),
|
|
55
61
|
});
|
|
56
62
|
await (0, fs_extra_1.writeFile)((0, pathe_1.join)(functionsDir, functionName, 'pages.js'), resolverSource);
|
|
63
|
+
const nfInternalFiles = await (0, tiny_glob_1.default)((0, pathe_1.join)(functionsDir, functionName, '**'));
|
|
64
|
+
includedFiles.push(...nfInternalFiles);
|
|
57
65
|
}
|
|
58
66
|
const writeHandler = async (functionName, functionTitle, isODB) => {
|
|
59
|
-
const handlerSource =
|
|
67
|
+
const handlerSource = (0, getHandler_1.getHandler)({
|
|
60
68
|
isODB,
|
|
61
69
|
publishDir,
|
|
62
70
|
appDir: (0, pathe_1.relative)(functionDir, appDir),
|
|
@@ -143,6 +151,99 @@ const setupImageFunction = async ({ constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIO
|
|
|
143
151
|
}
|
|
144
152
|
};
|
|
145
153
|
exports.setupImageFunction = setupImageFunction;
|
|
154
|
+
const traceRequiredServerFiles = async (publish) => {
|
|
155
|
+
const { files, relativeAppDir, config: { experimental: { outputFileTracingRoot }, }, } = await (0, config_1.getRequiredServerFiles)(publish);
|
|
156
|
+
const appDirRoot = (0, pathe_1.join)(outputFileTracingRoot, relativeAppDir);
|
|
157
|
+
const absoluteFiles = files.map((file) => (0, pathe_1.join)(appDirRoot, file));
|
|
158
|
+
absoluteFiles.push((0, pathe_1.join)(publish, 'required-server-files.json'));
|
|
159
|
+
return absoluteFiles;
|
|
160
|
+
};
|
|
161
|
+
const traceNextServer = async (publish) => {
|
|
162
|
+
const nextServerDeps = await (0, files_1.getDependenciesOfFile)((0, pathe_1.join)(publish, 'next-server.js'));
|
|
163
|
+
// during testing, i've seen `next-server` contain only one line.
|
|
164
|
+
// this is a sanity check to make sure we're getting all the deps.
|
|
165
|
+
if (nextServerDeps.length < 10) {
|
|
166
|
+
console.error(nextServerDeps);
|
|
167
|
+
throw new Error("next-server.js.nft.json didn't contain all dependencies.");
|
|
168
|
+
}
|
|
169
|
+
const filtered = nextServerDeps.filter((f) => {
|
|
170
|
+
// NFT detects a bunch of large development files that we don't need.
|
|
171
|
+
if (f.endsWith('.development.js'))
|
|
172
|
+
return false;
|
|
173
|
+
// not needed for API Routes!
|
|
174
|
+
if (f.endsWith('node_modules/sass/sass.dart.js'))
|
|
175
|
+
return false;
|
|
176
|
+
return true;
|
|
177
|
+
});
|
|
178
|
+
return filtered;
|
|
179
|
+
};
|
|
180
|
+
const traceNPMPackage = async (packageName, publish) => {
|
|
181
|
+
try {
|
|
182
|
+
return await (0, tiny_glob_1.default)((0, pathe_1.join)((0, pathe_1.dirname)(require.resolve(packageName, { paths: [publish] })), '**', '*'), {
|
|
183
|
+
absolute: true,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (process.env.NODE_ENV === 'test') {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
exports.traceNPMPackage = traceNPMPackage;
|
|
194
|
+
const getAPIPRouteCommonDependencies = async (publish) => {
|
|
195
|
+
const deps = await Promise.all([
|
|
196
|
+
traceRequiredServerFiles(publish),
|
|
197
|
+
traceNextServer(publish),
|
|
198
|
+
// used by our own bridge.js
|
|
199
|
+
(0, exports.traceNPMPackage)('follow-redirects', publish),
|
|
200
|
+
]);
|
|
201
|
+
return deps.flat(1);
|
|
202
|
+
};
|
|
203
|
+
exports.getAPIPRouteCommonDependencies = getAPIPRouteCommonDependencies;
|
|
204
|
+
const sum = (arr) => arr.reduce((v, current) => v + current, 0);
|
|
205
|
+
// TODO: cache results
|
|
206
|
+
const getBundleWeight = async (patterns) => {
|
|
207
|
+
const sizes = await Promise.all(patterns.flatMap(async (pattern) => {
|
|
208
|
+
const files = await (0, tiny_glob_1.default)(pattern);
|
|
209
|
+
return Promise.all(files.map(async (file) => {
|
|
210
|
+
const fStat = await (0, fs_extra_1.stat)(file);
|
|
211
|
+
if (fStat.isFile()) {
|
|
212
|
+
return fStat.size;
|
|
213
|
+
}
|
|
214
|
+
return 0;
|
|
215
|
+
}));
|
|
216
|
+
}));
|
|
217
|
+
return sum(sizes.flat(1));
|
|
218
|
+
};
|
|
219
|
+
const MB = 1024 * 1024;
|
|
220
|
+
const getAPILambdas = async (publish, baseDir, pageExtensions) => {
|
|
221
|
+
const commonDependencies = await (0, exports.getAPIPRouteCommonDependencies)(publish);
|
|
222
|
+
const threshold = 50 * MB - (await getBundleWeight(commonDependencies));
|
|
223
|
+
const apiRoutes = await (0, exports.getApiRouteConfigs)(publish, baseDir, pageExtensions);
|
|
224
|
+
const packFunctions = async (routes, type) => {
|
|
225
|
+
const weighedRoutes = await Promise.all(routes.map(async (route) => ({ value: route, weight: await getBundleWeight(route.includedFiles) })));
|
|
226
|
+
const bins = (0, pack_1.pack)(weighedRoutes, threshold);
|
|
227
|
+
return bins.map((bin, index) => ({
|
|
228
|
+
functionName: bin.length === 1 ? bin[0].functionName : `api-${index}`,
|
|
229
|
+
routes: bin,
|
|
230
|
+
includedFiles: [...commonDependencies, ...routes.flatMap((route) => route.includedFiles)],
|
|
231
|
+
type,
|
|
232
|
+
}));
|
|
233
|
+
};
|
|
234
|
+
const standardFunctions = apiRoutes.filter((route) => !(0, analysis_1.isEdgeConfig)(route.config.runtime) &&
|
|
235
|
+
route.config.type !== "experimental-background" /* ApiRouteType.BACKGROUND */ &&
|
|
236
|
+
route.config.type !== "experimental-scheduled" /* ApiRouteType.SCHEDULED */);
|
|
237
|
+
const scheduledFunctions = apiRoutes.filter((route) => route.config.type === "experimental-scheduled" /* ApiRouteType.SCHEDULED */);
|
|
238
|
+
const backgroundFunctions = apiRoutes.filter((route) => route.config.type === "experimental-background" /* ApiRouteType.BACKGROUND */);
|
|
239
|
+
const scheduledLambdas = scheduledFunctions.map(exports.packSingleFunction);
|
|
240
|
+
const [standardLambdas, backgroundLambdas] = await Promise.all([
|
|
241
|
+
packFunctions(standardFunctions),
|
|
242
|
+
packFunctions(backgroundFunctions, "experimental-background" /* ApiRouteType.BACKGROUND */),
|
|
243
|
+
]);
|
|
244
|
+
return [...standardLambdas, ...backgroundLambdas, ...scheduledLambdas];
|
|
245
|
+
};
|
|
246
|
+
exports.getAPILambdas = getAPILambdas;
|
|
146
247
|
/**
|
|
147
248
|
* Look for API routes, and extract the config from the source file.
|
|
148
249
|
*/
|
|
@@ -155,7 +256,19 @@ const getApiRouteConfigs = async (publish, appDir, pageExtensions) => {
|
|
|
155
256
|
const srcPagesDir = (0, pathe_1.join)(appDir, 'src', 'pages');
|
|
156
257
|
return await Promise.all(apiRoutes.map(async (apiRoute) => {
|
|
157
258
|
const filePath = (0, files_1.getSourceFileForPage)(apiRoute, [pagesDir, srcPagesDir], pageExtensions);
|
|
158
|
-
|
|
259
|
+
const config = await (0, analysis_1.extractConfigFromFile)(filePath, appDir);
|
|
260
|
+
const functionName = (0, utils_1.getFunctionNameForPage)(apiRoute, config.type === "experimental-background" /* ApiRouteType.BACKGROUND */);
|
|
261
|
+
const compiled = pages[apiRoute];
|
|
262
|
+
const compiledPath = (0, pathe_1.join)(publish, 'server', compiled);
|
|
263
|
+
const routeDependencies = await (0, files_1.getDependenciesOfFile)(compiledPath);
|
|
264
|
+
const includedFiles = [compiledPath, ...routeDependencies];
|
|
265
|
+
return {
|
|
266
|
+
functionName,
|
|
267
|
+
route: apiRoute,
|
|
268
|
+
config,
|
|
269
|
+
compiled,
|
|
270
|
+
includedFiles,
|
|
271
|
+
};
|
|
159
272
|
}));
|
|
160
273
|
};
|
|
161
274
|
exports.getApiRouteConfigs = getApiRouteConfigs;
|
|
@@ -168,6 +281,13 @@ const getExtendedApiRouteConfigs = async (publish, appDir, pageExtensions) => {
|
|
|
168
281
|
return settledApiRoutes.filter((apiRoute) => apiRoute.config.type !== undefined);
|
|
169
282
|
};
|
|
170
283
|
exports.getExtendedApiRouteConfigs = getExtendedApiRouteConfigs;
|
|
284
|
+
const packSingleFunction = (func) => ({
|
|
285
|
+
functionName: func.functionName,
|
|
286
|
+
includedFiles: func.includedFiles,
|
|
287
|
+
routes: [func],
|
|
288
|
+
type: func.config.type,
|
|
289
|
+
});
|
|
290
|
+
exports.packSingleFunction = packSingleFunction;
|
|
171
291
|
/**
|
|
172
292
|
* Warn the user of the caveats if they're using background or scheduled API routes
|
|
173
293
|
*/
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pack = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Naïve linear packing algorithm.
|
|
6
|
+
* Takes items with weights, and packs them into boxes of a given threshold.
|
|
7
|
+
* If an item weight exceeds the threshold, it is put into a box of its own.
|
|
8
|
+
*
|
|
9
|
+
* We're using this to combine many API Routes into fewer Lambda functions.
|
|
10
|
+
*
|
|
11
|
+
* This does not compute an optimal solution.
|
|
12
|
+
* For that, we'd take the full dependency graph into account
|
|
13
|
+
* and try to pack routes with intersecting dependencies together.
|
|
14
|
+
* But since most of the lambda bundle consists of node_modules,
|
|
15
|
+
* that probably won't help much.
|
|
16
|
+
* In the future, we might think about using some graph-based analysis here!
|
|
17
|
+
* Too complicated for now.
|
|
18
|
+
*/
|
|
19
|
+
const pack = (items, threshold) => {
|
|
20
|
+
const result = [];
|
|
21
|
+
let currentBox = [];
|
|
22
|
+
let currentWeight = 0;
|
|
23
|
+
const sortedDescending = items.sort((a, b) => b.weight - a.weight);
|
|
24
|
+
for (const item of sortedDescending) {
|
|
25
|
+
const fitsInCurrentBox = currentWeight + item.weight <= threshold;
|
|
26
|
+
if (fitsInCurrentBox) {
|
|
27
|
+
currentBox.push(item.value);
|
|
28
|
+
currentWeight += item.weight;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
if (currentBox.length !== 0)
|
|
32
|
+
result.push(currentBox);
|
|
33
|
+
currentBox = [item.value];
|
|
34
|
+
currentWeight = item.weight;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (currentBox.length !== 0)
|
|
38
|
+
result.push(currentBox);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
exports.pack = pack;
|
package/lib/helpers/redirects.js
CHANGED
|
@@ -184,7 +184,7 @@ const generateDynamicRewrites = ({ dynamicRoutes, prerenderedDynamicRoutes, midd
|
|
|
184
184
|
};
|
|
185
185
|
};
|
|
186
186
|
exports.generateDynamicRewrites = generateDynamicRewrites;
|
|
187
|
-
const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath, trailingSlash, appDir }, buildId,
|
|
187
|
+
const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath, trailingSlash, appDir }, buildId, apiLambdas, }) => {
|
|
188
188
|
const { dynamicRoutes: prerenderedDynamicRoutes, routes: prerenderedStaticRoutes } = await (0, fs_extra_1.readJSON)((0, pathe_1.join)(netlifyConfig.build.publish, 'prerender-manifest.json'));
|
|
189
189
|
const { dynamicRoutes, staticRoutes } = await (0, fs_extra_1.readJSON)((0, pathe_1.join)(netlifyConfig.build.publish, 'routes-manifest.json'));
|
|
190
190
|
netlifyConfig.redirects.push(...generateHiddenPathRedirects({ basePath }));
|
|
@@ -194,7 +194,7 @@ const generateRedirects = async ({ netlifyConfig, nextConfig: { i18n, basePath,
|
|
|
194
194
|
// This is only used in prod, so dev uses `next dev` directly
|
|
195
195
|
netlifyConfig.redirects.push(
|
|
196
196
|
// API routes always need to be served from the regular function
|
|
197
|
-
...(0, utils_1.getApiRewrites)(basePath,
|
|
197
|
+
...(0, utils_1.getApiRewrites)(basePath, apiLambdas),
|
|
198
198
|
// Preview mode gets forced to the function, to bypass pre-rendered pages, but static files need to be skipped
|
|
199
199
|
...(await (0, utils_1.getPreviewRewrites)({ basePath, appDir })));
|
|
200
200
|
const middleware = await (0, files_1.getMiddleware)(netlifyConfig.build.publish);
|
package/lib/helpers/utils.js
CHANGED
|
@@ -105,8 +105,8 @@ const redirectsForNextRouteWithData = ({ route, dataRoute, basePath, to, status
|
|
|
105
105
|
force,
|
|
106
106
|
}));
|
|
107
107
|
exports.redirectsForNextRouteWithData = redirectsForNextRouteWithData;
|
|
108
|
-
const getApiRewrites = (basePath,
|
|
109
|
-
const apiRewrites =
|
|
108
|
+
const getApiRewrites = (basePath, apiLambdas) => {
|
|
109
|
+
const apiRewrites = apiLambdas.flatMap((lambda) => lambda.routes.map((apiRoute) => {
|
|
110
110
|
const [from] = (0, exports.toNetlifyRoute)(`${basePath}${apiRoute.route}`);
|
|
111
111
|
// Scheduled functions can't be invoked directly, so we 404 them.
|
|
112
112
|
if (apiRoute.config.type === "experimental-scheduled" /* ApiRouteType.SCHEDULED */) {
|
|
@@ -114,10 +114,10 @@ const getApiRewrites = (basePath, apiRoutes) => {
|
|
|
114
114
|
}
|
|
115
115
|
return {
|
|
116
116
|
from,
|
|
117
|
-
to: `/.netlify/functions/${
|
|
117
|
+
to: `/.netlify/functions/${lambda.functionName}`,
|
|
118
118
|
status: 200,
|
|
119
119
|
};
|
|
120
|
-
});
|
|
120
|
+
}));
|
|
121
121
|
return [
|
|
122
122
|
...apiRewrites,
|
|
123
123
|
{
|
package/lib/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const config_1 = require("./helpers/config");
|
|
|
14
14
|
const dev_1 = require("./helpers/dev");
|
|
15
15
|
const edge_1 = require("./helpers/edge");
|
|
16
16
|
const files_1 = require("./helpers/files");
|
|
17
|
+
const flags_1 = require("./helpers/flags");
|
|
17
18
|
const functions_1 = require("./helpers/functions");
|
|
18
19
|
const redirects_1 = require("./helpers/redirects");
|
|
19
20
|
const utils_1 = require("./helpers/utils");
|
|
@@ -38,7 +39,7 @@ const plugin = {
|
|
|
38
39
|
netlifyConfig.build.environment.NEXT_PRIVATE_TARGET = 'server';
|
|
39
40
|
},
|
|
40
41
|
// eslint-disable-next-line max-lines-per-function
|
|
41
|
-
async onBuild({ constants, netlifyConfig, utils: { build: { failBuild }, }, }) {
|
|
42
|
+
async onBuild({ constants, netlifyConfig, utils: { build: { failBuild }, }, featureFlags = {}, }) {
|
|
42
43
|
if ((0, utils_1.shouldSkip)()) {
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
@@ -91,10 +92,18 @@ const plugin = {
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
const buildId = (0, fs_extra_1.readFileSync)((0, path_1.join)(publish, 'BUILD_ID'), 'utf8').trim();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
const apiLambdas = (0, flags_1.splitApiRoutes)(featureFlags)
|
|
96
|
+
? await (0, functions_1.getAPILambdas)(publish, appDir, pageExtensions)
|
|
97
|
+
: await (0, functions_1.getExtendedApiRouteConfigs)(publish, appDir, pageExtensions).then((extendedRoutes) => extendedRoutes.map(functions_1.packSingleFunction));
|
|
98
|
+
await (0, functions_1.generateFunctions)(constants, appDir, apiLambdas);
|
|
97
99
|
await (0, functions_1.generatePagesResolver)(constants);
|
|
100
|
+
await (0, config_1.configureHandlerFunctions)({
|
|
101
|
+
netlifyConfig,
|
|
102
|
+
ignore,
|
|
103
|
+
publish: (0, path_1.relative)(process.cwd(), publish),
|
|
104
|
+
apiLambdas,
|
|
105
|
+
featureFlags,
|
|
106
|
+
});
|
|
98
107
|
await (0, files_1.movePublicFiles)({ appDir, outdir, publish, basePath });
|
|
99
108
|
await (0, files_1.patchNextFiles)(appDir);
|
|
100
109
|
if (!(0, destr_1.default)(process.env.SERVE_STATIC_FILES_FROM_ORIGIN)) {
|
|
@@ -116,7 +125,7 @@ const plugin = {
|
|
|
116
125
|
netlifyConfig,
|
|
117
126
|
nextConfig: { basePath, i18n, trailingSlash, appDir },
|
|
118
127
|
buildId,
|
|
119
|
-
|
|
128
|
+
apiLambdas,
|
|
120
129
|
});
|
|
121
130
|
await (0, edge_1.writeEdgeFunctions)({ netlifyConfig, routesManifest });
|
|
122
131
|
},
|
|
@@ -10,8 +10,9 @@ const path = require('path');
|
|
|
10
10
|
const { URLSearchParams, URL } = require('url');
|
|
11
11
|
const { Bridge } = require('@vercel/node-bridge/bridge');
|
|
12
12
|
const { getMultiValueHeaders } = require('./handlerUtils');
|
|
13
|
+
const { getNetlifyNextServer } = require('./server');
|
|
13
14
|
// We return a function and then call `toString()` on it to serialise it as the launcher function
|
|
14
|
-
const makeApiHandler = ({ conf, app, pageRoot,
|
|
15
|
+
const makeApiHandler = ({ conf, app, pageRoot, NextServer }) => {
|
|
15
16
|
var _a;
|
|
16
17
|
// Change working directory into the site root, unless using Nx, which moves the
|
|
17
18
|
// dist directory and handles this itself
|
|
@@ -25,8 +26,7 @@ const makeApiHandler = ({ conf, app, pageRoot, page, NextServer }) => {
|
|
|
25
26
|
require.resolve('./pages.js');
|
|
26
27
|
}
|
|
27
28
|
catch { }
|
|
28
|
-
|
|
29
|
-
;
|
|
29
|
+
const NetlifyNextServer = getNetlifyNextServer(NextServer);
|
|
30
30
|
(_a = process.env).NODE_ENV || (_a.NODE_ENV = 'production');
|
|
31
31
|
// We don't want to write ISR files to disk in the lambda environment
|
|
32
32
|
conf.experimental.isrFlushToDisk = false;
|
|
@@ -39,19 +39,23 @@ const makeApiHandler = ({ conf, app, pageRoot, page, NextServer }) => {
|
|
|
39
39
|
// We memoize this because it can be shared between requests, but don't instantiate it until
|
|
40
40
|
// the first request because we need the host and port.
|
|
41
41
|
let bridge;
|
|
42
|
-
const getBridge = (event) => {
|
|
42
|
+
const getBridge = (event, context) => {
|
|
43
|
+
var _a;
|
|
43
44
|
if (bridge) {
|
|
44
45
|
return bridge;
|
|
45
46
|
}
|
|
47
|
+
const customContext = (_a = context.clientContext) === null || _a === void 0 ? void 0 : _a.custom;
|
|
46
48
|
// Scheduled functions don't have a URL, but we need to give one so Next knows the route to serve
|
|
47
49
|
const url = event.rawUrl ? new URL(event.rawUrl) : new URL(path, process.env.URL || 'http://n');
|
|
48
50
|
const port = Number.parseInt(url.port) || 80;
|
|
49
|
-
const nextServer = new
|
|
51
|
+
const nextServer = new NetlifyNextServer({
|
|
50
52
|
conf,
|
|
51
53
|
dir,
|
|
52
54
|
customServer: false,
|
|
53
55
|
hostname: url.hostname,
|
|
54
56
|
port,
|
|
57
|
+
}, {
|
|
58
|
+
revalidateToken: customContext === null || customContext === void 0 ? void 0 : customContext.odb_refresh_hooks,
|
|
55
59
|
});
|
|
56
60
|
const requestHandler = nextServer.getRequestHandler();
|
|
57
61
|
const server = new Server(async (req, res) => {
|
|
@@ -69,13 +73,11 @@ const makeApiHandler = ({ conf, app, pageRoot, page, NextServer }) => {
|
|
|
69
73
|
};
|
|
70
74
|
return async function handler(event, context) {
|
|
71
75
|
// Ensure that paths are encoded - but don't double-encode them
|
|
72
|
-
event.path =
|
|
76
|
+
event.path = new URL(event.rawUrl).pathname;
|
|
73
77
|
// Next expects to be able to parse the query from the URL
|
|
74
78
|
const query = new URLSearchParams(event.queryStringParameters).toString();
|
|
75
79
|
event.path = query ? `${event.path}?${query}` : event.path;
|
|
76
|
-
|
|
77
|
-
event.headers['x-matched-path'] = page;
|
|
78
|
-
const { headers, ...result } = await getBridge(event).launcher(event, context);
|
|
80
|
+
const { headers, ...result } = await getBridge(event, context).launcher(event, context);
|
|
79
81
|
// Convert all headers to multiValueHeaders
|
|
80
82
|
const multiValueHeaders = getMultiValueHeaders(headers);
|
|
81
83
|
multiValueHeaders['cache-control'] = ['public, max-age=0, must-revalidate'];
|
|
@@ -90,9 +92,10 @@ const makeApiHandler = ({ conf, app, pageRoot, page, NextServer }) => {
|
|
|
90
92
|
/**
|
|
91
93
|
* Handlers for API routes are simpler than page routes, but they each have a separate one
|
|
92
94
|
*/
|
|
93
|
-
const getApiHandler = ({
|
|
94
|
-
// This is a string, but if you have the right editor plugin it should format as js
|
|
95
|
+
const getApiHandler = ({ schedule, publishDir = '../../../.next', appDir = '../../..', nextServerModuleRelativeLocation, }) =>
|
|
96
|
+
// This is a string, but if you have the right editor plugin it should format as js (e.g. bierner.comment-tagged-templates in VS Code)
|
|
95
97
|
(0, outdent_1.outdent /* javascript */) `
|
|
98
|
+
process.env.NODE_ENV = 'production';
|
|
96
99
|
if (!${JSON.stringify(nextServerModuleRelativeLocation)}) {
|
|
97
100
|
throw new Error('Could not find Next.js server')
|
|
98
101
|
}
|
|
@@ -101,16 +104,17 @@ const getApiHandler = ({ page, config, publishDir = '../../../.next', appDir = '
|
|
|
101
104
|
// We copy the file here rather than requiring from the node module
|
|
102
105
|
const { Bridge } = require("./bridge");
|
|
103
106
|
const { getMultiValueHeaders } = require('./handlerUtils')
|
|
107
|
+
const { getNetlifyNextServer } = require('./server')
|
|
104
108
|
const NextServer = require(${JSON.stringify(nextServerModuleRelativeLocation)}).default
|
|
105
109
|
|
|
106
|
-
${
|
|
110
|
+
${schedule ? `const { schedule } = require("@netlify/functions")` : ''}
|
|
107
111
|
|
|
108
112
|
|
|
109
113
|
const { config } = require("${publishDir}/required-server-files.json")
|
|
110
114
|
let staticManifest
|
|
111
115
|
const path = require("path");
|
|
112
116
|
const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", "server"));
|
|
113
|
-
const handler = (${makeApiHandler.toString()})({ conf: config, app: "${appDir}", pageRoot,
|
|
114
|
-
exports.handler = ${
|
|
117
|
+
const handler = (${makeApiHandler.toString()})({ conf: config, app: "${appDir}", pageRoot, NextServer })
|
|
118
|
+
exports.handler = ${schedule ? `schedule(${JSON.stringify(schedule)}, handler);` : 'handler'}
|
|
115
119
|
`;
|
|
116
120
|
exports.getApiHandler = getApiHandler;
|
|
@@ -132,7 +132,7 @@ const makeHandler = ({ conf, app, pageRoot, NextServer, staticManifest = [], mod
|
|
|
132
132
|
};
|
|
133
133
|
};
|
|
134
134
|
const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '../../..', nextServerModuleRelativeLocation, }) =>
|
|
135
|
-
// This is a string, but if you have the right editor plugin it should format as js
|
|
135
|
+
// This is a string, but if you have the right editor plugin it should format as js (e.g. bierner.comment-tagged-templates in VS Code)
|
|
136
136
|
(0, outdent_1.outdent /* javascript */) `
|
|
137
137
|
if (!${JSON.stringify(nextServerModuleRelativeLocation)}) {
|
|
138
138
|
throw new Error('Could not find Next.js server')
|
package/lib/templates/server.js
CHANGED
|
@@ -20,11 +20,16 @@ const getNetlifyNextServer = (NextServer) => {
|
|
|
20
20
|
return async (req, res, parsedUrl) => {
|
|
21
21
|
// preserve the URL before Next.js mutates it for i18n
|
|
22
22
|
const { url, headers } = req;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// handle on-demand revalidation by purging the ODB cache
|
|
26
|
-
if (res.statusCode === 200 && headers['x-prerender-revalidate'] && this.netlifyConfig.revalidateToken) {
|
|
23
|
+
if (headers['x-prerender-revalidate'] && this.netlifyConfig.revalidateToken) {
|
|
24
|
+
// handle on-demand revalidation by purging the ODB cache
|
|
27
25
|
await this.netlifyRevalidate(url);
|
|
26
|
+
res = res;
|
|
27
|
+
res.statusCode = 200;
|
|
28
|
+
res.setHeader('x-nextjs-cache', 'REVALIDATED');
|
|
29
|
+
res.send();
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
return handler(req, res, parsedUrl);
|
|
28
33
|
}
|
|
29
34
|
};
|
|
30
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/plugin-nextjs",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.37.1",
|
|
4
4
|
"description": "Run Next.js seamlessly on Netlify",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
],
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@netlify/esbuild": "0.14.39",
|
|
15
|
-
"@netlify/functions": "^1.
|
|
15
|
+
"@netlify/functions": "^1.6.0",
|
|
16
16
|
"@netlify/ipx": "^1.4.0",
|
|
17
17
|
"@vercel/node-bridge": "^2.1.0",
|
|
18
18
|
"chalk": "^4.1.2",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@delucis/if-env": "^1.1.2",
|
|
40
|
-
"@netlify/build": "^29.11.
|
|
40
|
+
"@netlify/build": "^29.11.5",
|
|
41
41
|
"@types/fs-extra": "^9.0.13",
|
|
42
42
|
"@types/jest": "^27.4.1",
|
|
43
43
|
"@types/merge-stream": "^1.1.2",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest } from 'https://esm.sh/v91/next@12.2.5/deno/dist/server/web/spec-extension/request.js'
|
|
2
2
|
import { NextResponse } from 'https://esm.sh/v91/next@12.2.5/deno/dist/server/web/spec-extension/response.js'
|
|
3
3
|
import { fromFileUrl } from 'https://deno.land/std@0.151.0/path/mod.ts'
|
|
4
|
-
import { buildResponse } from '../edge-shared/utils.ts'
|
|
4
|
+
import { buildResponse, isFunction } from '../edge-shared/utils.ts'
|
|
5
5
|
|
|
6
6
|
globalThis.NFRequestContextMap ||= new Map()
|
|
7
7
|
globalThis.__dirname = fromFileUrl(new URL('./', import.meta.url)).slice(0, -1)
|
|
@@ -36,7 +36,17 @@ const handler = async (req, context) => {
|
|
|
36
36
|
// We need to cache-bust the import because otherwise it will claim it
|
|
37
37
|
// doesn't exist if the user creates it after the server starts
|
|
38
38
|
const nextMiddleware = await import(`../../middleware.js#${++idx}`)
|
|
39
|
-
|
|
39
|
+
|
|
40
|
+
// The middleware file can export a named `middleware` export or a `default` export
|
|
41
|
+
middleware = isFunction(nextMiddleware.middleware)
|
|
42
|
+
? nextMiddleware.middleware
|
|
43
|
+
: isFunction(nextMiddleware.default)
|
|
44
|
+
? nextMiddleware.default
|
|
45
|
+
: undefined
|
|
46
|
+
|
|
47
|
+
if (!middleware) {
|
|
48
|
+
throw new Error('The middleware must export a `middleware` or a `default` function')
|
|
49
|
+
}
|
|
40
50
|
} catch (importError) {
|
|
41
51
|
if (importError.code === 'ERR_MODULE_NOT_FOUND' && importError.message.includes(`middleware.js`)) {
|
|
42
52
|
// No middleware, so we silently return
|