-
Notifications
You must be signed in to change notification settings - Fork 110
/
Copy pathoverrides.ts
224 lines (202 loc) · 7.27 KB
/
overrides.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import { AdapterMetadata, MiddlewareManifest } from "./interfaces.js";
import {
loadRouteManifest,
writeRouteManifest,
loadMiddlewareManifest,
exists,
writeFile,
loadConfig,
} from "./utils.js";
import { join, extname } from "path";
import { rename as renamePromise } from "fs/promises";
const DEFAULT_NEXT_CONFIG_FILE = "next.config.js";
/**
* Overrides the user's Next Config file (next.config.[ts|js|mjs]) to add configs
* optimized for Firebase App Hosting.
*/
export async function overrideNextConfig(projectRoot: string, nextConfigFileName: string) {
console.log(`Overriding Next Config to add configs optmized for Firebase App Hosting`);
const userNextConfigExists = await exists(join(projectRoot, nextConfigFileName));
if (!userNextConfigExists) {
console.log(`No Next config file found, creating one with Firebase App Hosting overrides`);
try {
await writeFile(join(projectRoot, DEFAULT_NEXT_CONFIG_FILE), defaultNextConfigForFAH());
console.log(
`Successfully created ${DEFAULT_NEXT_CONFIG_FILE} with Firebase App Hosting overrides`,
);
} catch (error) {
console.error(`Error creating ${DEFAULT_NEXT_CONFIG_FILE}: ${error}`);
throw error;
}
return;
}
// A Next Config already exists in the user's project, so it needs to be overriden
// Determine the file extension
const fileExtension = extname(nextConfigFileName);
const originalConfigName = `next.config.original${fileExtension}`;
// Rename the original config file
try {
const configPath = join(projectRoot, nextConfigFileName);
const originalPath = join(projectRoot, originalConfigName);
await renamePromise(configPath, originalPath);
// Create a new config file with the appropriate import
let importStatement;
switch (fileExtension) {
case ".js":
importStatement = `const originalConfig = require('./${originalConfigName}');`;
break;
case ".mjs":
importStatement = `import originalConfig from './${originalConfigName}';`;
break;
case ".ts":
importStatement = `import originalConfig from './${originalConfigName.replace(
".ts",
"",
)}';`;
break;
default:
throw new Error(
`Unsupported file extension for Next Config: "${fileExtension}", please use ".js", ".mjs", or ".ts"`,
);
}
// Create the new config content with our overrides
const newConfigContent = getCustomNextConfig(importStatement, fileExtension);
// Write the new config file
await writeFile(join(projectRoot, nextConfigFileName), newConfigContent);
console.log(`Successfully created ${nextConfigFileName} with Firebase App Hosting overrides`);
} catch (error) {
console.error(`Error overriding Next.js config: ${error}`);
throw error;
}
}
/**
* Returns a custom Next.js config that optimizes the app for Firebase App Hosting.
*
* Current overrides include:
* - images.unoptimized = true, unless user explicitly sets images.unoptimized to false or
* is using a custom image loader.
* @param importStatement The import statement for the original config.
* @param fileExtension The file extension of the original config. Use ".js", ".mjs", or ".ts"
* @return The custom Next.js config.
*/
function getCustomNextConfig(importStatement: string, fileExtension: string) {
return `
// @ts-nocheck
${importStatement}
// This file was automatically generated by Firebase App Hosting adapter
const fahOptimizedConfig = (config) => ({
...config,
images: {
...(config.images || {}),
...(config.images?.unoptimized === undefined && config.images?.loader === undefined
? { unoptimized: true }
: {}),
},
});
const config = typeof originalConfig === 'function'
? async (...args) => {
const resolvedConfig = await originalConfig(...args);
return fahOptimizedConfig(resolvedConfig);
}
: fahOptimizedConfig(originalConfig);
${fileExtension === ".mjs" ? "export default config;" : "module.exports = config;"}
`;
}
/**
* Returns the default Next Config file that is created in the user's project
* if one does not exist already. This config ensures the Next.Js app is optimized
* for Firebase App Hosting.
*/
function defaultNextConfigForFAH() {
return `
// @ts-nocheck
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
unoptimized: true,
}
}
module.exports = nextConfig
`;
}
/**
* This function is used to validate the state of an app after running
* overrideNextConfig. It validates that:
* 1. if user has a next config it is preserved in a next.config.original.[js|ts|mjs] file
* 2. a next config exists (should be created with FAH overrides
* even if user did not create one)
* 3. next config can be loaded by NextJs without any issues.
*/
export async function validateNextConfigOverride(
root: string,
projectRoot: string,
configFileName: string,
) {
const userNextConfigExists = await exists(join(root, configFileName));
const configFilePath = join(
root,
userNextConfigExists ? configFileName : DEFAULT_NEXT_CONFIG_FILE,
);
if (!(await exists(configFilePath))) {
throw new Error(
`Next Config Override Failed: Next.js config file not found at ${configFilePath}`,
);
}
try {
await loadConfig(root, projectRoot);
} catch (error) {
throw new Error(
`Resulting Next Config is invalid: ${
error instanceof Error ? error.message : "Unknown error"
}`,
);
}
}
/**
* Modifies the app's route manifest (routes-manifest.json) to add Firebase App Hosting
* specific overrides (i.e headers).
*
* This function adds the following headers to all routes:
* - x-fah-adapter: The Firebase App Hosting adapter version used to build the app.
* - x-fah-middleware: When middleware is enabled.
* @param appPath The path to the app directory.
* @param distDir The path to the dist directory.
* @param adapterMetadata The adapter metadata.
*/
export async function addRouteOverrides(
appPath: string,
distDir: string,
adapterMetadata: AdapterMetadata,
) {
const middlewareManifest = loadMiddlewareManifest(appPath, distDir);
const routeManifest = loadRouteManifest(appPath, distDir);
routeManifest.headers.push({
source: "/:path*",
headers: [
{
key: "x-fah-adapter",
value: `nextjs-${adapterMetadata.adapterVersion}`,
},
...(middlewareExists(middlewareManifest)
? [
{
key: "x-fah-middleware",
value: "true",
},
]
: []),
],
/*
NextJs converts the source string to a regex using path-to-regexp (https://github.com/pillarjs/path-to-regexp) at
build time: https://github.com/vercel/next.js/blob/canary/packages/next/src/build/index.ts#L1273.
This regex is then used to match the route against the request path.
This regex was generated by building a sample NextJs app with the source string `/:path*` and then inspecting the
routes-manifest.json file.
*/
regex: "^(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))?(?:/)?$",
});
await writeRouteManifest(appPath, distDir, routeManifest);
}
function middlewareExists(middlewareManifest: MiddlewareManifest) {
return Object.keys(middlewareManifest.middleware).length > 0;
}