Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- add support for `next.config.ts` and `next.config.mts` in Next.js deployments (#9871)
7 changes: 6 additions & 1 deletion src/frameworks/next/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ export const APP_PATHS_MANIFEST: typeof APP_PATHS_MANIFEST_TYPE = "app-paths-man
export const SERVER_REFERENCE_MANIFEST: `${typeof SERVER_REFERENCE_MANIFEST_TYPE}.json` =
"server-reference-manifest.json";

export const CONFIG_FILES = ["next.config.js", "next.config.mjs"] as const;
export const CONFIG_FILES = [
"next.config.js",
"next.config.mjs",
"next.config.ts",
"next.config.mts",
] as const;

export const ESBUILD_VERSION = "^0.19.2";

Expand Down
7 changes: 5 additions & 2 deletions src/frameworks/next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@
const DEFAULT_NUMBER_OF_REASONS_TO_LIST = 5;

function getReactVersion(cwd: string): string | undefined {
return findDependency("react-dom", { cwd, omitDev: false })?.version;

Check warning on line 103 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .version on an `any` value

Check warning on line 103 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe return of an `any` typed value
}

/**
* Returns whether this codebase is a Next.js backend.
*/
export async function discover(dir: string) {

Check warning on line 109 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
if (!(await pathExists(join(dir, "package.json")))) return;
const version = getNextVersion(dir);
if (!(await whichNextConfigFile(dir)) && !version) return;
Expand Down Expand Up @@ -163,10 +163,10 @@

const nextBuild = new Promise((resolve, reject) => {
const buildProcess = spawn(cli, ["build"], { cwd: dir, env });
buildProcess.stdout?.on("data", (data) => logger.info(data.toString()));

Check warning on line 166 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value

Check warning on line 166 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .toString on an `any` value

Check warning on line 166 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Error`
buildProcess.stderr?.on("data", (data) => logger.info(data.toString()));

Check warning on line 167 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value

Check warning on line 167 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .toString on an `any` value

Check warning on line 167 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Error`
buildProcess.on("error", (err) => {
reject(new FirebaseError(`Unable to build your Next.js app: ${err}`));

Check warning on line 169 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "Error" of template literal expression
});
buildProcess.on("exit", (code) => {
resolve(code);
Expand Down Expand Up @@ -684,9 +684,12 @@
logLevel: "error",
external: productionDeps,
};
if (configFile === "next.config.mjs") {
// ensure generated file is .mjs if the config is .mjs
if (configFile === "next.config.mjs" || configFile === "next.config.mts") {
// ensure generated file is .mjs if the config is .mjs or .mts
esbuildArgs.format = "esm";
esbuildArgs.outfile = join(destDir, "next.config.mjs");
} else {
esbuildArgs.outfile = join(destDir, "next.config.js");
}

const bundle = await esbuild.build(esbuildArgs);
Expand Down
57 changes: 57 additions & 0 deletions src/frameworks/next/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
findEsbuildPath,
installEsbuild,
isNextJsVersionVulnerable,
whichNextConfigFile,
} from "./utils";

import * as frameworksUtils from "../utils";
Expand Down Expand Up @@ -76,6 +77,62 @@ import {
import { pathsWithCustomRoutesInternalPrefix } from "./testing/i18n";

describe("Next.js utils", () => {
describe("whichNextConfigFile", () => {
let sandbox: sinon.SinonSandbox;
let pathExistsStub: sinon.SinonStub;

beforeEach(() => {
sandbox = sinon.createSandbox();
pathExistsStub = sandbox.stub(fsExtra, "pathExists");
});

afterEach(() => {
sandbox.restore();
});

it("should return next.config.js if it exists", async () => {
pathExistsStub.withArgs("next.config.js").resolves(true);
pathExistsStub.resolves(false);

expect(await whichNextConfigFile("")).to.equal("next.config.js");
});

it("should return next.config.mjs if it exists", async () => {
pathExistsStub.withArgs("next.config.mjs").resolves(true);
pathExistsStub.resolves(false);

expect(await whichNextConfigFile("")).to.equal("next.config.mjs");
});

it("should return next.config.ts if it exists", async () => {
pathExistsStub.withArgs("next.config.ts").resolves(true);
pathExistsStub.resolves(false);

expect(await whichNextConfigFile("")).to.equal("next.config.ts");
});

it("should return next.config.mts if it exists", async () => {
pathExistsStub.withArgs("next.config.mts").resolves(true);
pathExistsStub.resolves(false);

expect(await whichNextConfigFile("")).to.equal("next.config.mts");
});

it("should return null if no config file exists", async () => {
pathExistsStub.resolves(false);

expect(await whichNextConfigFile("")).to.be.null;
});

it("should prioritize next.config.js over others", async () => {
pathExistsStub.withArgs("next.config.js").resolves(true);
pathExistsStub.withArgs("next.config.mjs").resolves(true);
pathExistsStub.resolves(false);

expect(await whichNextConfigFile("")).to.equal("next.config.js");
});
});

describe("cleanEscapedChars", () => {
it("should clean escaped chars", () => {
// path containing all escaped chars
Expand Down
Loading