Skip to content

Commit

Permalink
Introduce a custom build-output-path and app-path argument for more f…
Browse files Browse the repository at this point in the history
…lexible monorepo support. (#214)

* Introduce a custom build-output-path and app-path argument for more flexible monorepo support.
  • Loading branch information
lorenzodejong committed Sep 5, 2023
1 parent 010edc2 commit 420c503
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 30 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,26 @@ await build({
});
```

#### Custom app and build output paths

OpenNext runs the `build` script from your current command folder by default. When running OpenNext from a monorepo with decentralised application and build output paths, you can specify a custom `appPath` and/or `buildOutputPath`. This will allow you to execute your command from the root of the monorepo.

```bash
# CLI
open-next build --build-command "pnpm custom:build" --app-path "./apps/example-app" --build-output-path "./dist/apps/example-app"
```

```ts
// JS
import { build } from "open-next/build.js";

await build({
buildCommand: "pnpm custom:build",
appPath: "./apps/example-app",
buildOutputPath: "./dist/apps/example-app"
});
```

#### Minify server function

Enabling this option will minimize all `.js` and `.json` files in the server function bundle using the [node-minify](https://github.com/srod/node-minify) library. This can reduce the size of the server function bundle by about 40%, depending on the size of your app.
Expand Down
10 changes: 10 additions & 0 deletions examples/app-router/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ export function middleware(request: NextRequest) {
);
const responseHeaders = new Headers();
responseHeaders.set("response-header", "response-header");

// Set the cache control header with custom swr
// For: isr.test.ts
if (path === "/isr") {
responseHeaders.set(
"cache-control",
"max-age=10, stale-while-revalidate=999",
);
}

const r = NextResponse.next({
headers: responseHeaders,
request: {
Expand Down
4 changes: 2 additions & 2 deletions packages/open-next/src/adapters/plugins/routing/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ export function fixCacheHeaderForHtmlPages(

export function fixSWRCacheHeader(headers: Record<string, string | undefined>) {
// WORKAROUND: `NextServer` does not set correct SWR cache headers — https://github.com/serverless-stack/open-next#workaround-nextserver-does-not-set-correct-swr-cache-headers
if (headers["cache-control"]?.includes("stale-while-revalidate")) {
if (headers["cache-control"]) {
headers["cache-control"] = headers["cache-control"].replace(
"stale-while-revalidate",
/\bstale-while-revalidate(?!=)/,
"stale-while-revalidate=2592000", // 30 days
);
}
Expand Down
86 changes: 58 additions & 28 deletions packages/open-next/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ interface BuildOptions {
* ```
*/
buildCommand?: string;
/**
* The path to the target folder of build output from the `buildCommand` option (the path which will contain the `.next` and `.open-next` folders). This path is relative from the current process.cwd().
* @default "."
*/
buildOutputPath?: string;
/**
* The path to the root of the Next.js app's source code. This path is relative from the current process.cwd().
* @default "."
*/
appPath?: string;
}

const require = topLevelCreateRequire(import.meta.url);
Expand All @@ -46,14 +56,17 @@ export type PublicFiles = {
};

export async function build(opts: BuildOptions = {}) {
const { root: monorepoRoot, packager } = findMonorepoRoot(
path.join(process.cwd(), opts.appPath || "."),
);

// Initialize options
options = normalizeOptions(opts);
options = normalizeOptions(opts, monorepoRoot);

// Pre-build validation
checkRunningInsideNextjsApp();
printNextjsVersion();
printOpenNextVersion();
const { root: monorepoRoot, packager } = findMonorepoRoot();

// Build Next.js app
printHeader("Building Next.js app");
Expand All @@ -74,13 +87,17 @@ export async function build(opts: BuildOptions = {}) {
}
}

function normalizeOptions(opts: BuildOptions) {
const appPath = process.cwd();
const outputDir = ".open-next";
function normalizeOptions(opts: BuildOptions, root: string) {
const appPath = path.join(process.cwd(), opts.appPath || ".");
const buildOutputPath = path.join(process.cwd(), opts.buildOutputPath || ".");
const outputDir = path.join(buildOutputPath, ".open-next");
const nextPackageJsonPath = findNextPackageJsonPath(appPath, root);
return {
openNextVersion: getOpenNextVersion(),
nextVersion: getNextVersion(appPath),
nextVersion: getNextVersion(nextPackageJsonPath),
nextPackageJsonPath,
appPath,
appBuildOutputPath: buildOutputPath,
appPublicPath: path.join(appPath, "public"),
outputDir,
tempDir: path.join(outputDir, ".build"),
Expand All @@ -103,8 +120,7 @@ function checkRunningInsideNextjsApp() {
}
}

function findMonorepoRoot() {
const { appPath } = options;
function findMonorepoRoot(appPath: string) {
let currentPath = appPath;
while (currentPath !== "/") {
const found = [
Expand All @@ -128,6 +144,13 @@ function findMonorepoRoot() {
return { root: appPath, packager: "npm" as const };
}

function findNextPackageJsonPath(appPath: string, root: string) {
// This is needed for the case where the app is a single-version monorepo and the package.json is in the root of the monorepo
return fs.existsSync(path.join(appPath, "./package.json"))
? path.join(appPath, "./package.json")
: path.join(root, "./package.json");
}

function setStandaloneBuildMode(monorepoRoot: string) {
// Equivalent to setting `target: "standalone"` in next.config.js
process.env.NEXT_PRIVATE_STANDALONE = "true";
Expand All @@ -136,13 +159,13 @@ function setStandaloneBuildMode(monorepoRoot: string) {
}

function buildNextjsApp(packager: "npm" | "yarn" | "pnpm") {
const { appPath } = options;
const { nextPackageJsonPath } = options;
const command =
options.buildCommand ??
(packager === "npm" ? "npm run build" : `${packager} build`);
cp.execSync(command, {
stdio: "inherit",
cwd: appPath,
cwd: path.dirname(nextPackageJsonPath),
});
}

Expand Down Expand Up @@ -226,7 +249,7 @@ async function minifyServerBundle() {
function createRevalidationBundle() {
console.info(`Bundling revalidation function...`);

const { appPath, outputDir } = options;
const { appBuildOutputPath, outputDir } = options;

// Create output folder
const outputPath = path.join(outputDir, "revalidation-function");
Expand All @@ -241,15 +264,15 @@ function createRevalidationBundle() {

// Copy over .next/prerender-manifest.json file
fs.copyFileSync(
path.join(appPath, ".next", "prerender-manifest.json"),
path.join(appBuildOutputPath, ".next", "prerender-manifest.json"),
path.join(outputPath, "prerender-manifest.json"),
);
}

function createImageOptimizationBundle() {
console.info(`Bundling image optimization function...`);

const { appPath, outputDir } = options;
const { appPath, appBuildOutputPath, outputDir } = options;

// Create output folder
const outputPath = path.join(outputDir, "image-optimization-function");
Expand Down Expand Up @@ -289,7 +312,7 @@ function createImageOptimizationBundle() {
// Copy over .next/required-server-files.json file
fs.mkdirSync(path.join(outputPath, ".next"));
fs.copyFileSync(
path.join(appPath, ".next/required-server-files.json"),
path.join(appBuildOutputPath, ".next/required-server-files.json"),
path.join(outputPath, ".next/required-server-files.json"),
);

Expand All @@ -310,7 +333,7 @@ function createImageOptimizationBundle() {
function createStaticAssets() {
console.info(`Bundling static assets...`);

const { appPath, appPublicPath, outputDir } = options;
const { appBuildOutputPath, appPublicPath, outputDir } = options;

// Create output folder
const outputPath = path.join(outputDir, "assets");
Expand All @@ -322,11 +345,11 @@ function createStaticAssets() {
// - .next/static => _next/static
// - public/* => *
fs.copyFileSync(
path.join(appPath, ".next/BUILD_ID"),
path.join(appBuildOutputPath, ".next/BUILD_ID"),
path.join(outputPath, "BUILD_ID"),
);
fs.cpSync(
path.join(appPath, ".next/static"),
path.join(appBuildOutputPath, ".next/static"),
path.join(outputPath, "_next", "static"),
{ recursive: true },
);
Expand All @@ -338,12 +361,16 @@ function createStaticAssets() {
function createCacheAssets(monorepoRoot: string) {
console.info(`Bundling cache assets...`);

const { appPath, outputDir } = options;
const packagePath = path.relative(monorepoRoot, appPath);
const buildId = getBuildId(appPath);
const { appBuildOutputPath, outputDir } = options;
const packagePath = path.relative(monorepoRoot, appBuildOutputPath);
const buildId = getBuildId(appBuildOutputPath);

// Copy pages to cache folder
const dotNextPath = path.join(appPath, ".next/standalone", packagePath);
const dotNextPath = path.join(
appBuildOutputPath,
".next/standalone",
packagePath,
);
const outputPath = path.join(outputDir, "cache", buildId);
[".next/server/pages", ".next/server/app"]
.map((dir) => path.join(dotNextPath, dir))
Expand All @@ -361,7 +388,10 @@ function createCacheAssets(monorepoRoot: string) {
);

// Copy fetch-cache to cache folder
const fetchCachePath = path.join(appPath, ".next/cache/fetch-cache");
const fetchCachePath = path.join(
appBuildOutputPath,
".next/cache/fetch-cache",
);
if (fs.existsSync(fetchCachePath)) {
const fetchOutputPath = path.join(outputDir, "cache", "__fetch", buildId);
fs.mkdirSync(fetchOutputPath, { recursive: true });
Expand All @@ -376,7 +406,7 @@ function createCacheAssets(monorepoRoot: string) {
async function createServerBundle(monorepoRoot: string) {
console.info(`Bundling server function...`);

const { appPath, outputDir } = options;
const { appPath, appBuildOutputPath, outputDir } = options;

// Create output folder
const outputPath = path.join(outputDir, "server-function");
Expand All @@ -388,12 +418,12 @@ async function createServerBundle(monorepoRoot: string) {
// `.next/standalone/package/path` (ie. `.next`, `server.js`).
// We need to output the handler file inside the package path.
const isMonorepo = monorepoRoot !== appPath;
const packagePath = path.relative(monorepoRoot, appPath);
const packagePath = path.relative(monorepoRoot, appBuildOutputPath);

// Copy over standalone output files
// note: if user uses pnpm as the package manager, node_modules contain
// symlinks. We don't want to resolve the symlinks when copying.
fs.cpSync(path.join(appPath, ".next/standalone"), outputPath, {
fs.cpSync(path.join(appBuildOutputPath, ".next/standalone"), outputPath, {
recursive: true,
verbatimSymlinks: true,
});
Expand Down Expand Up @@ -685,9 +715,9 @@ function getOpenNextVersion() {
return require(path.join(__dirname, "../package.json")).version;
}

function getNextVersion(appPath: string) {
const version = require(path.join(appPath, "./package.json")).dependencies
.next;
function getNextVersion(nextPackageJsonPath: string) {
const version = require(nextPackageJsonPath).dependencies.next;

// Drop the -canary.n suffix
return version.split("-")[0];
}
Expand Down
2 changes: 2 additions & 0 deletions packages/open-next/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ if (Object.keys(args).includes("--help")) printHelp();

build({
buildCommand: args["--build-command"],
buildOutputPath: args["--build-output-path"],
appPath: args["--app-path"],
minify: Object.keys(args).includes("--minify"),
});

Expand Down
22 changes: 22 additions & 0 deletions packages/tests-e2e/tests/appRouter/isr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,25 @@ test("Incremental Static Regeneration", async ({ page }) => {

expect(newTime).not.toEqual(finalTime);
});

test("headers", async ({ page }) => {
let responsePromise = page.waitForResponse((response) => {
return response.status() === 200;
});
await page.goto("/isr");

while (true) {
const response = await responsePromise;
const headers = response.headers();

// this was set in middleware
if (headers["cache-control"] === "max-age=10, stale-while-revalidate=999") {
break;
}
await wait(1000);
responsePromise = page.waitForResponse((response) => {
return response.status() === 200;
});
await page.reload();
}
});

1 comment on commit 420c503

@vercel
Copy link

@vercel vercel bot commented on 420c503 Sep 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

open-next – ./

open-next-git-main-sst-dev.vercel.app
open-next-sst-dev.vercel.app
open-next.vercel.app

Please sign in to comment.