@netlify/plugin-nextjs 4.29.2 → 4.29.3-appdir.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.
@@ -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.writeEdgeFunctions = exports.writeDevEdgeFunction = exports.cleanupEdgeFunctions = exports.loadMiddlewareManifest = void 0;
6
+ exports.writeEdgeFunctions = exports.writeDevEdgeFunction = exports.cleanupEdgeFunctions = exports.loadAppPathRoutesManifest = exports.loadMiddlewareManifest = void 0;
7
7
  /* eslint-disable max-lines */
8
8
  const fs_1 = require("fs");
9
9
  const path_1 = require("path");
@@ -13,14 +13,15 @@ const fs_extra_1 = require("fs-extra");
13
13
  const outdent_1 = require("outdent");
14
14
  const config_1 = require("./config");
15
15
  const matchers_1 = require("./matchers");
16
- const loadMiddlewareManifest = (netlifyConfig) => {
17
- const middlewarePath = (0, path_1.resolve)(netlifyConfig.build.publish, 'server', 'middleware-manifest.json');
18
- if (!(0, fs_1.existsSync)(middlewarePath)) {
19
- return null;
16
+ const maybeLoadJson = (path) => {
17
+ if ((0, fs_1.existsSync)(path)) {
18
+ return (0, fs_extra_1.readJson)(path);
20
19
  }
21
- return (0, fs_extra_1.readJson)(middlewarePath);
22
20
  };
21
+ const loadMiddlewareManifest = (netlifyConfig) => maybeLoadJson((0, path_1.resolve)(netlifyConfig.build.publish, 'server', 'middleware-manifest.json'));
23
22
  exports.loadMiddlewareManifest = loadMiddlewareManifest;
23
+ const loadAppPathRoutesManifest = (netlifyConfig) => maybeLoadJson((0, path_1.resolve)(netlifyConfig.build.publish, 'app-path-routes-manifest.json'));
24
+ exports.loadAppPathRoutesManifest = loadAppPathRoutesManifest;
24
25
  /**
25
26
  * Convert the Next middleware name into a valid Edge Function name
26
27
  */
@@ -35,27 +36,23 @@ import {
35
36
  // Deno defines "window", but naughty libraries think this means it's a browser
36
37
  delete globalThis.window
37
38
  globalThis.process = { env: {...Deno.env.toObject(), NEXT_RUNTIME: 'edge', 'NEXT_PRIVATE_MINIMAL_MODE': '1' } }
39
+ globalThis.EdgeRuntime = "netlify-edge"
38
40
  // Next uses "self" as a function-scoped global-like object
39
41
  const self = {}
40
42
  let _ENTRIES = {}
41
43
 
42
- class Response extends globalThis.Response {
43
- constructor(body, init) {
44
- super(body, init);
45
- // Next.js uses this extension to the Headers API implemented by Cloudflare workerd
46
- this.headers.getAll = (name) => {
47
- name = name.toLowerCase();
48
- if (name !== "set-cookie") {
49
- throw new Error("Headers.getAll is only supported for Set-Cookie");
50
- }
51
- return [...this.headers.entries()]
52
- .filter(([key]) => key === name)
53
- .map(([, value]) => value);
54
- };
55
- }
44
+ // Next.js uses this extension to the Headers API implemented by Cloudflare workerd
45
+ if(!('getAll' in Headers.prototype)) {
46
+ Headers.prototype.getAll = function getAll(name) {
47
+ name = name.toLowerCase();
48
+ if (name !== "set-cookie") {
49
+ throw new Error("Headers.getAll is only supported for Set-Cookie");
50
+ }
51
+ return [...this.entries()]
52
+ .filter(([key]) => key === name)
53
+ .map(([, value]) => value);
54
+ };
56
55
  }
57
-
58
-
59
56
  // Next uses blob: urls to refer to local assets, so we need to intercept these
60
57
  const _fetch = globalThis.fetch
61
58
  const fetch = async (url, init) => {
@@ -112,7 +109,7 @@ const getMiddlewareBundle = async ({ edgeFunctionDefinition, netlifyConfig, }) =
112
109
  };
113
110
  const getEdgeTemplatePath = (file) => (0, path_1.join)(__dirname, '..', '..', 'src', 'templates', 'edge', file);
114
111
  const copyEdgeSourceFile = ({ file, target, edgeFunctionDir, }) => fs_1.promises.copyFile(getEdgeTemplatePath(file), (0, path_1.join)(edgeFunctionDir, target !== null && target !== void 0 ? target : file));
115
- const writeEdgeFunction = async ({ edgeFunctionDefinition, edgeFunctionRoot, netlifyConfig, nextConfig, }) => {
112
+ const writeEdgeFunction = async ({ edgeFunctionDefinition, edgeFunctionRoot, netlifyConfig, pageRegexMap, appPathRoutesManifest = {}, nextConfig, cache, }) => {
116
113
  const name = sanitizeName(edgeFunctionDefinition.name);
117
114
  const edgeFunctionDir = (0, path_1.join)(edgeFunctionRoot, name);
118
115
  const bundle = await getMiddlewareBundle({
@@ -140,11 +137,19 @@ const writeEdgeFunction = async ({ edgeFunctionDefinition, edgeFunctionRoot, net
140
137
  else {
141
138
  matchers.push(...edgeFunctionDefinition.matchers);
142
139
  }
140
+ // If the EF matches a page, it's an app dir page so needs a matcher too
141
+ // The object will be empty if appDir isn't enabled in the Next config
142
+ if (pageRegexMap && edgeFunctionDefinition.page in appPathRoutesManifest) {
143
+ const regexp = pageRegexMap.get(appPathRoutesManifest[edgeFunctionDefinition.page]);
144
+ if (regexp) {
145
+ matchers.push({ regexp });
146
+ }
147
+ }
143
148
  await (0, fs_extra_1.writeJson)((0, path_1.join)(edgeFunctionDir, 'matchers.json'), matchers);
144
149
  // We add a defintion for each matching path
145
150
  return matchers.map((matcher) => {
146
151
  const pattern = (0, matchers_1.stripLookahead)(matcher.regexp);
147
- return { function: name, pattern, name: edgeFunctionDefinition.name };
152
+ return { function: name, pattern, name: edgeFunctionDefinition.name, cache };
148
153
  });
149
154
  };
150
155
  const cleanupEdgeFunctions = ({ INTERNAL_EDGE_FUNCTIONS_SRC = '.netlify/edge-functions', }) => (0, fs_extra_1.emptyDir)(INTERNAL_EDGE_FUNCTIONS_SRC);
@@ -172,7 +177,7 @@ exports.writeDevEdgeFunction = writeDevEdgeFunction;
172
177
  /**
173
178
  * Writes Edge Functions for the Next middleware
174
179
  */
175
- const writeEdgeFunctions = async (netlifyConfig) => {
180
+ const writeEdgeFunctions = async ({ netlifyConfig, routesManifest, }) => {
176
181
  const manifest = {
177
182
  functions: [],
178
183
  version: 1,
@@ -219,13 +224,22 @@ const writeEdgeFunctions = async (netlifyConfig) => {
219
224
  // Older versions of the manifest format don't have the functions field
220
225
  // No, the version field was not incremented
221
226
  if (typeof middlewareManifest.functions === 'object') {
227
+ // When using the app dir, we also need to check if the EF matches a page
228
+ const appPathRoutesManifest = await (0, exports.loadAppPathRoutesManifest)(netlifyConfig);
229
+ const pageRegexMap = new Map([...(routesManifest.dynamicRoutes || []), ...(routesManifest.staticRoutes || [])].map((route) => [
230
+ route.page,
231
+ route.regex,
232
+ ]));
222
233
  for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
223
234
  usesEdge = true;
224
235
  const functionDefinitions = await writeEdgeFunction({
225
236
  edgeFunctionDefinition,
226
237
  edgeFunctionRoot,
227
238
  netlifyConfig,
239
+ pageRegexMap,
240
+ appPathRoutesManifest,
228
241
  nextConfig,
242
+ cache: 'manual',
229
243
  });
230
244
  manifest.functions.push(...functionDefinitions);
231
245
  }
@@ -70,7 +70,6 @@ exports.getMiddleware = getMiddleware;
70
70
  const moveStaticPages = async ({ netlifyConfig, target, i18n, basePath, }) => {
71
71
  console.log('Moving static page files to serve from CDN...');
72
72
  const outputDir = (0, pathe_1.join)(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless');
73
- const root = (0, pathe_1.join)(outputDir, 'pages');
74
73
  const buildId = (0, fs_extra_1.readFileSync)((0, pathe_1.join)(netlifyConfig.build.publish, 'BUILD_ID'), 'utf8').trim();
75
74
  const dataDir = (0, pathe_1.join)('_next', 'data', buildId);
76
75
  await (0, fs_extra_1.ensureDir)((0, pathe_1.join)(netlifyConfig.build.publish, dataDir));
@@ -92,14 +91,17 @@ const moveStaticPages = async ({ netlifyConfig, target, i18n, basePath, }) => {
92
91
  }
93
92
  }
94
93
  });
95
- const files = [];
94
+ let fileCount = 0;
96
95
  const filesManifest = {};
97
96
  const moveFile = async (file) => {
97
+ // Strip the initial 'app' or 'pages' directory from the output path
98
+ const pathname = file.split('/').slice(1).join('/');
99
+ // .rsc data files go next to the html file
98
100
  const isData = file.endsWith('.json');
99
- const source = (0, pathe_1.join)(root, file);
100
- const targetFile = isData ? (0, pathe_1.join)(dataDir, file) : file;
101
+ const source = (0, pathe_1.join)(outputDir, file);
102
+ const targetFile = isData ? (0, pathe_1.join)(dataDir, pathname) : pathname;
101
103
  const targetPath = basePath ? (0, pathe_1.join)(basePath, targetFile) : targetFile;
102
- files.push(file);
104
+ fileCount += 1;
103
105
  filesManifest[file] = targetPath;
104
106
  const dest = (0, pathe_1.join)(netlifyConfig.build.publish, targetPath);
105
107
  try {
@@ -110,8 +112,8 @@ const moveStaticPages = async ({ netlifyConfig, target, i18n, basePath, }) => {
110
112
  }
111
113
  };
112
114
  // Move all static files, except error documents and nft manifests
113
- const pages = await (0, globby_1.default)(['**/*.{html,json}', '!**/(500|404|*.js.nft).{html,json}'], {
114
- cwd: root,
115
+ const pages = await (0, globby_1.default)(['{app,pages}/**/*.{html,json,rsc}', '!**/(500|404|*.js.nft).{html,json}'], {
116
+ cwd: outputDir,
115
117
  dot: true,
116
118
  });
117
119
  const matchingMiddleware = new Set();
@@ -149,7 +151,7 @@ const moveStaticPages = async ({ netlifyConfig, target, i18n, basePath, }) => {
149
151
  return limit(moveFile, filePath);
150
152
  });
151
153
  await Promise.all(promises);
152
- console.log(`Moved ${files.length} files`);
154
+ console.log(`Moved ${fileCount} files`);
153
155
  if (matchedPages.size !== 0) {
154
156
  console.log((0, chalk_1.yellowBright)((0, outdent_1.outdent) `
155
157
  Skipped moving ${matchedPages.size} ${matchedPages.size === 1 ? 'file because it matches' : 'files because they match'} middleware, so cannot be deployed to the CDN and will be served from the origin instead.
@@ -20,10 +20,13 @@ const utils_1 = require("./utils");
20
20
  const generateFunctions = async ({ FUNCTIONS_SRC = constants_1.DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, PUBLISH_DIR }, appDir, apiRoutes) => {
21
21
  const publish = (0, pathe_1.resolve)(PUBLISH_DIR);
22
22
  const functionsDir = (0, pathe_1.resolve)(INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC);
23
- console.log({ functionsDir });
24
23
  const functionDir = (0, pathe_1.join)(functionsDir, constants_1.HANDLER_FUNCTION_NAME);
25
24
  const publishDir = (0, pathe_1.relative)(functionDir, publish);
26
25
  for (const { route, config, compiled } of apiRoutes) {
26
+ // Don't write a lambda if the runtime is edge
27
+ if (config.runtime === 'experimental-edge') {
28
+ continue;
29
+ }
27
30
  const apiHandlerSource = await (0, getApiHandler_1.getApiHandler)({
28
31
  page: route,
29
32
  config,
package/lib/index.js CHANGED
@@ -44,7 +44,7 @@ const plugin = {
44
44
  }
45
45
  const { publish } = netlifyConfig.build;
46
46
  (0, verification_1.checkNextSiteHasBuilt)({ publish, failBuild });
47
- const { appDir, basePath, i18n, images, target, ignore, trailingSlash, outdir, experimental } = await (0, config_1.getNextConfig)({
47
+ const { appDir, basePath, i18n, images, target, ignore, trailingSlash, outdir, experimental, routesManifest } = await (0, config_1.getNextConfig)({
48
48
  publish,
49
49
  failBuild,
50
50
  });
@@ -118,7 +118,7 @@ const plugin = {
118
118
  buildId,
119
119
  apiRoutes,
120
120
  });
121
- await (0, edge_1.writeEdgeFunctions)(netlifyConfig);
121
+ await (0, edge_1.writeEdgeFunctions)({ netlifyConfig, routesManifest });
122
122
  },
123
123
  async onPostBuild({ netlifyConfig: { build: { publish }, redirects, headers, }, utils: { status, cache, functions, build: { failBuild }, }, constants: { FUNCTIONS_DIST }, }) {
124
124
  await (0, cache_1.saveCache)({ cache, publish });
@@ -134,11 +134,14 @@ const plugin = {
134
134
  await (0, verification_1.checkForOldFunctions)({ functions });
135
135
  await (0, verification_1.checkZipSize)((0, path_1.join)(FUNCTIONS_DIST, `${constants_1.ODB_FUNCTION_NAME}.zip`));
136
136
  const nextConfig = await (0, config_1.getNextConfig)({ publish, failBuild });
137
- const { basePath, appDir } = nextConfig;
137
+ const { basePath, appDir, experimental } = nextConfig;
138
138
  (0, config_1.generateCustomHeaders)(nextConfig, headers);
139
139
  (0, verification_1.warnForProblematicUserRewrites)({ basePath, redirects });
140
140
  (0, verification_1.warnForRootRedirects)({ appDir });
141
141
  await (0, functions_1.warnOnApiRoutes)({ FUNCTIONS_DIST });
142
+ if (experimental === null || experimental === void 0 ? void 0 : experimental.appDir) {
143
+ console.log('🧪 Thank you for testing "appDir" support on Netlify. For known issues and to give feedback, visit https://ntl.fyi/next-13-feedback');
144
+ }
142
145
  },
143
146
  };
144
147
  // The types haven't been updated yet
@@ -105,7 +105,7 @@ const getApiHandler = ({ page, config, publishDir = '../../../.next', appDir = '
105
105
  const { config } = require("${publishDir}/required-server-files.json")
106
106
  let staticManifest
107
107
  const path = require("path");
108
- const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", "serverless", "pages"));
108
+ const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", "server"));
109
109
  const handler = (${makeHandler.toString()})(config, "${appDir}", pageRoot, ${JSON.stringify(page)})
110
110
  exports.handler = ${config.type === "experimental-scheduled" /* ApiRouteType.SCHEDULED */ ? `schedule(${JSON.stringify(config.schedule)}, handler);` : 'handler'}
111
111
  `;
@@ -37,9 +37,7 @@ const makeHandler = (conf, app, pageRoot, staticManifest = [], mode = 'ssr') =>
37
37
  for (const [key, value] of Object.entries(conf.env)) {
38
38
  process.env[key] = String(value);
39
39
  }
40
- // Set during the request as it needs the host header. Hoisted so we can define the function once
41
- let base;
42
- augmentFsModule({ promises, staticManifest, pageRoot, getBase: () => base });
40
+ augmentFsModule({ promises, staticManifest, pageRoot });
43
41
  // We memoize this because it can be shared between requests, but don't instantiate it until
44
42
  // the first request because we need the host and port.
45
43
  let bridge;
@@ -49,9 +47,6 @@ const makeHandler = (conf, app, pageRoot, staticManifest = [], mode = 'ssr') =>
49
47
  }
50
48
  const url = new URL(event.rawUrl);
51
49
  const port = Number.parseInt(url.port) || 80;
52
- const { host } = event.headers;
53
- const protocol = event.headers['x-forwarded-proto'] || 'http';
54
- base = `${protocol}://${host}`;
55
50
  const NextServer = getNextServer();
56
51
  const nextServer = new NextServer({
57
52
  conf,
@@ -149,7 +144,7 @@ const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '..
149
144
  staticManifest = require("${publishDir}/static-manifest.json")
150
145
  } catch {}
151
146
  const path = require("path");
152
- const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", config.target === "server" ? "server" : "serverless", "pages"));
147
+ const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", "server"));
153
148
  exports.handler = ${isODB
154
149
  ? `builder((${makeHandler.toString()})(config, "${appDir}", pageRoot, staticManifest, 'odb'));`
155
150
  : `(${makeHandler.toString()})(config, "${appDir}", pageRoot, staticManifest, 'ssr');`}
@@ -75,7 +75,7 @@ exports.getMultiValueHeaders = getMultiValueHeaders;
75
75
  /**
76
76
  * Monkey-patch the fs module to download missing files from the CDN
77
77
  */
78
- const augmentFsModule = ({ promises, staticManifest, pageRoot, getBase, }) => {
78
+ const augmentFsModule = ({ promises, staticManifest, pageRoot, }) => {
79
79
  // Only do this if we have some static files moved to the CDN
80
80
  if (staticManifest.length === 0) {
81
81
  return;
@@ -93,22 +93,23 @@ const augmentFsModule = ({ promises, staticManifest, pageRoot, getBase, }) => {
93
93
  const statsOrig = promises.stat;
94
94
  // ...then money-patch it to see if it's requesting a CDN file
95
95
  promises.readFile = (async (file, options) => {
96
- const base = getBase();
96
+ // In production use the public URL (e.g. https://example.com). Otherwise use the deploy URL, e.g. https://deploy-preview-123--example.netlify.app
97
+ const baseUrl = process.env.CONTEXT === 'production' ? process.env.URL : process.env.DEPLOY_PRIME_URL;
97
98
  // We only care about page files
98
99
  if (file.startsWith(pageRoot)) {
99
- // We only want the part after `pages/`
100
+ // We only want the part after `.next/server/`
100
101
  const filePath = file.slice(pageRoot.length + 1);
101
102
  // Is it in the CDN and not local?
102
103
  if (staticFiles.has(filePath) && !(0, fs_1.existsSync)(file)) {
103
104
  // This name is safe to use, because it's one that was already created by Next
104
105
  const cacheFile = path_1.default.join(cacheDir, filePath);
105
- const url = `${base}/${staticFiles.get(filePath)}`;
106
+ const url = `${baseUrl}/${staticFiles.get(filePath)}`;
106
107
  // If it's already downloading we can wait for it to finish
107
108
  if (downloadPromises.has(url)) {
108
109
  await downloadPromises.get(url);
109
110
  }
110
111
  // Have we already cached it? We download every time if running locally to avoid staleness
111
- if ((!(0, fs_1.existsSync)(cacheFile) || process.env.NETLIFY_DEV) && base) {
112
+ if ((!(0, fs_1.existsSync)(cacheFile) || process.env.NETLIFY_DEV) && baseUrl) {
112
113
  await promises.mkdir(path_1.default.dirname(cacheFile), { recursive: true });
113
114
  try {
114
115
  // Append the path to our host and we can load it like a regular page
@@ -129,7 +130,7 @@ const augmentFsModule = ({ promises, staticManifest, pageRoot, getBase, }) => {
129
130
  promises.stat = ((file, options) => {
130
131
  // We only care about page files
131
132
  if (file.startsWith(pageRoot)) {
132
- // We only want the part after `pages/`
133
+ // We only want the part after `.next/server`
133
134
  const cacheFile = path_1.default.join(cacheDir, file.slice(pageRoot.length + 1));
134
135
  if ((0, fs_1.existsSync)(cacheFile)) {
135
136
  return statsOrig(cacheFile, options);
@@ -10,4 +10,4 @@ exports.handler = (0, ipx_1.createIPXHandler)({
10
10
  remotePatterns: imageconfig_json_1.remotePatterns,
11
11
  responseHeaders: imageconfig_json_1.responseHeaders,
12
12
  });
13
- /* eslint-enable n/no-missing-import, import/no-unresolved, @typescript-eslint/ban-ts-comment */
13
+ /* eslint-enable import/no-unresolved, @typescript-eslint/ban-ts-comment */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "4.29.2",
3
+ "version": "4.29.3-appdir.0",
4
4
  "description": "Run Next.js seamlessly on Netlify",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@delucis/if-env": "^1.1.2",
39
- "@netlify/build": "^28.2.2",
39
+ "@netlify/build": "^28.4.4",
40
40
  "@types/fs-extra": "^9.0.13",
41
41
  "@types/jest": "^27.4.1",
42
42
  "@types/merge-stream": "^1.1.2",
@@ -13,6 +13,12 @@ export interface FetchEventResult {
13
13
  waitUntil: Promise<any>
14
14
  }
15
15
 
16
+ export interface I18NConfig {
17
+ defaultLocale: string
18
+ localeDetection?: false
19
+ locales: string[]
20
+ }
21
+
16
22
  export interface RequestData {
17
23
  geo?: {
18
24
  city?: string
@@ -27,7 +33,7 @@ export interface RequestData {
27
33
  method: string
28
34
  nextConfig?: {
29
35
  basePath?: string
30
- i18n?: Record<string, unknown>
36
+ i18n?: I18NConfig | null
31
37
  trailingSlash?: boolean
32
38
  }
33
39
  page?: {