Skip to content

Commit d085e8a

Browse files
authored
Merge pull request #131 from FirebaseExtended/nextjsadaptor-refactor-tests
Add unit tests and refactor nextjs build adaptor to make testing easier
2 parents b9a17ba + 49aa860 commit d085e8a

File tree

4 files changed

+263
-69
lines changed

4 files changed

+263
-69
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
const importUtils = import("@apphosting/adapter-nextjs/dist/utils.js");
2+
import assert from "assert";
3+
import fs from "fs";
4+
import path from "path";
5+
import os from "os";
6+
import { OutputBundleOptions } from "../interfaces.js";
7+
8+
describe("build commands", () => {
9+
let tmpDir: string;
10+
let outputBundleOptions: OutputBundleOptions;
11+
beforeEach(() => {
12+
tmpDir = generateTmpDir();
13+
outputBundleOptions = {
14+
bundleYamlPath: path.join(tmpDir, ".apphosting/bundle.yaml"),
15+
outputDirectory: path.join(tmpDir, ".apphosting"),
16+
outputPublicDirectory: path.join(tmpDir, ".apphosting/public"),
17+
outputStaticDirectory: path.join(tmpDir, ".apphosting/.next/static"),
18+
serverFilePath: path.join(tmpDir, ".apphosting/server.js"),
19+
};
20+
});
21+
22+
it("expects all output bundle files to be generated", async () => {
23+
const { generateOutputDirectory } = await importUtils;
24+
const files = {
25+
".next/standalone/standalonefile": "",
26+
".next/static/staticfile": "",
27+
".next/routes-manifest.json": `{
28+
"headers":[],
29+
"rewrites":[],
30+
"redirects":[]
31+
}`,
32+
};
33+
generateTestFiles(tmpDir, files);
34+
await generateOutputDirectory(tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
35+
36+
const expectedFiles = {
37+
".apphosting/.next/static/staticfile": "",
38+
".apphosting/standalonefile": "",
39+
".apphosting/bundle.yaml": `headers: []
40+
redirects: []
41+
rewrites: []
42+
runCommand: node .apphosting/server.js
43+
neededDirs:
44+
- .apphosting
45+
staticAssets:
46+
- .apphosting/public
47+
`,
48+
};
49+
validateTestFiles(tmpDir, expectedFiles);
50+
});
51+
52+
it("expects public directory to be copied over", async () => {
53+
const { generateOutputDirectory } = await importUtils;
54+
const files = {
55+
".next/standalone/standalonefile": "",
56+
".next/static/staticfile": "",
57+
"public/publicfile": "",
58+
".next/routes-manifest.json": `{
59+
"headers":[],
60+
"rewrites":[],
61+
"redirects":[]
62+
}`,
63+
};
64+
generateTestFiles(tmpDir, files);
65+
await generateOutputDirectory(tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
66+
67+
const expectedFiles = {
68+
".apphosting/.next/static/staticfile": "",
69+
".apphosting/standalonefile": "",
70+
".apphosting/public/publicfile": "",
71+
".apphosting/bundle.yaml": `headers: []
72+
redirects: []
73+
rewrites: []
74+
runCommand: node .apphosting/server.js
75+
neededDirs:
76+
- .apphosting
77+
staticAssets:
78+
- .apphosting/public
79+
`,
80+
};
81+
validateTestFiles(tmpDir, expectedFiles);
82+
});
83+
84+
it("expects bundle.yaml headers/rewrites/redirects to be generated", async () => {
85+
const { generateOutputDirectory } = await importUtils;
86+
const files = {
87+
".next/standalone/standalonefile": "",
88+
".next/static/staticfile": "",
89+
".next/routes-manifest.json": `{
90+
"headers":[{"source":"source", "headers":["header1"]}],
91+
"rewrites":[{"source":"source", "destination":"destination"}],
92+
"redirects":[{"source":"source", "destination":"destination"}]
93+
}`,
94+
};
95+
generateTestFiles(tmpDir, files);
96+
await generateOutputDirectory(tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
97+
98+
const expectedFiles = {
99+
".apphosting/.next/static/staticfile": "",
100+
".apphosting/standalonefile": "",
101+
".apphosting/bundle.yaml": `headers:
102+
- source: source
103+
headers:
104+
- header1
105+
redirects:
106+
- source: source
107+
destination: destination
108+
rewrites:
109+
- source: source
110+
destination: destination
111+
runCommand: node .apphosting/server.js
112+
neededDirs:
113+
- .apphosting
114+
staticAssets:
115+
- .apphosting/public
116+
`,
117+
};
118+
validateTestFiles(tmpDir, expectedFiles);
119+
});
120+
121+
it("test populate output bundle options", async () => {
122+
const { populateOutputBundleOptions } = await importUtils;
123+
const expectedOutputBundleOptions = {
124+
bundleYamlPath: "test/.apphosting/bundle.yaml",
125+
outputDirectory: "test/.apphosting",
126+
outputPublicDirectory: "test/.apphosting/public",
127+
outputStaticDirectory: "test/.apphosting/.next/static",
128+
serverFilePath: "test/.apphosting/server.js",
129+
};
130+
assert.deepEqual(populateOutputBundleOptions("test"), expectedOutputBundleOptions);
131+
});
132+
afterEach(() => {
133+
fs.rmSync(tmpDir, { recursive: true, force: true });
134+
});
135+
});
136+
function generateTmpDir(): string {
137+
return fs.mkdtempSync(path.join(os.tmpdir(), "test-files"));
138+
}
139+
140+
function generateTestFiles(baseDir: string, filesToGenerate: Object): void {
141+
Object.entries(filesToGenerate).forEach((file) => {
142+
const fileName = file[0];
143+
const contents = file[1];
144+
const fileToGenerate = path.join(baseDir, fileName);
145+
fs.mkdirSync(path.dirname(fileToGenerate), { recursive: true });
146+
fs.writeFileSync(fileToGenerate, contents);
147+
});
148+
}
149+
150+
function validateTestFiles(baseDir: string, expectedFiles: Object): void {
151+
Object.entries(expectedFiles).forEach((file) => {
152+
const fileName = file[0];
153+
const expectedContents = file[1];
154+
const fileToRead = path.join(baseDir, fileName);
155+
const contents = fs.readFileSync(fileToRead).toString();
156+
assert.deepEqual(contents, expectedContents);
157+
});
158+
}
Lines changed: 10 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,19 @@
11
#! /usr/bin/env node
2-
import { spawnSync } from "child_process";
3-
import { loadConfig, readRoutesManifest } from "../utils.js";
2+
import {
3+
loadConfig,
4+
build,
5+
populateOutputBundleOptions,
6+
generateOutputDirectory,
7+
} from "../utils.js";
48

5-
import { join, relative, normalize } from "path";
6-
import fsExtra from "fs-extra";
7-
import { stringify as yamlStringify } from "yaml";
9+
import { join } from "path";
810

9-
// unable to use shorthand imports on fsExtra since fsExtra is CJS
10-
const { move, exists, writeFile } = fsExtra;
1111
const cwd = process.cwd();
1212

13-
// Set standalone mode
14-
process.env.NEXT_PRIVATE_STANDALONE = "true";
15-
// Opt-out sending telemetry to Vercel
16-
process.env.NEXT_TELEMETRY_DISABLED = "1";
17-
1813
build(cwd);
1914

15+
const outputBundleOptions = populateOutputBundleOptions(cwd);
2016
const { distDir } = await loadConfig(cwd);
21-
const manifest = await readRoutesManifest(join(cwd, distDir));
22-
23-
const appHostingOutputDirectory = join(cwd, ".apphosting");
24-
const appHostingStaticDirectory = join(appHostingOutputDirectory, ".next", "static");
25-
const appHostingPublicDirectory = join(appHostingOutputDirectory, "public");
26-
const outputBundlePath = join(appHostingOutputDirectory, "bundle.yaml");
27-
const serverFilePath = join(appHostingOutputDirectory, "server.js");
28-
29-
const standaloneDirectory = join(cwd, distDir, "standalone");
30-
const staticDirectory = join(cwd, distDir, "static");
31-
const publicDirectory = join(cwd, "public");
32-
33-
// Run build command
34-
function build(cwd: string) {
35-
spawnSync("npm", ["run", "build"], { cwd, shell: true, stdio: "inherit" });
36-
}
37-
38-
// move public directory to apphosting output public directory
39-
const movePublicDirectory = async () => {
40-
const publicDirectoryExists = await exists(publicDirectory);
41-
if (!publicDirectoryExists) return;
42-
await move(publicDirectory, appHostingPublicDirectory, { overwrite: true });
43-
};
44-
45-
// generate bundle.yaml
46-
const generateBundleYaml = async () => {
47-
const headers = manifest.headers.map((it) => ({ ...it, regex: undefined }));
48-
const redirects = manifest.redirects
49-
.filter((it) => !it.internal)
50-
.map((it) => ({ ...it, regex: undefined }));
51-
const beforeFileRewrites = Array.isArray(manifest.rewrites)
52-
? manifest.rewrites
53-
: manifest.rewrites?.beforeFiles || [];
54-
const rewrites = beforeFileRewrites.map((it) => ({ ...it, regex: undefined }));
55-
await writeFile(
56-
outputBundlePath,
57-
yamlStringify({
58-
headers,
59-
redirects,
60-
rewrites,
61-
runCommand: `node ${normalize(relative(cwd, serverFilePath))}`,
62-
neededDirs: [normalize(relative(cwd, appHostingOutputDirectory))],
63-
staticAssets: [normalize(relative(cwd, appHostingPublicDirectory))],
64-
}),
65-
);
66-
};
17+
const nextBuildDirectory = join(cwd, distDir);
6718

68-
// move the standalone directory, the static directory and the public directory to apphosting output directory
69-
// as well as generating bundle.yaml
70-
await move(standaloneDirectory, appHostingOutputDirectory, { overwrite: true });
71-
await Promise.all([
72-
move(staticDirectory, appHostingStaticDirectory, { overwrite: true }),
73-
movePublicDirectory(),
74-
generateBundleYaml(),
75-
]);
19+
await generateOutputDirectory(cwd, outputBundleOptions, nextBuildDirectory);

packages/@apphosting/adapter-nextjs/src/interfaces.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,12 @@ export interface RoutesManifest {
7171
localeDetection?: false;
7272
};
7373
}
74+
75+
// The output bundle options are specified here
76+
export interface OutputBundleOptions {
77+
bundleYamlPath: string;
78+
outputDirectory: string;
79+
serverFilePath: string;
80+
outputPublicDirectory: string;
81+
outputStaticDirectory: string;
82+
}

packages/@apphosting/adapter-nextjs/src/utils.ts

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import { join } from "path";
21
import fsExtra from "fs-extra";
32
import { PHASE_PRODUCTION_BUILD } from "./constants.js";
43
import { ROUTES_MANIFEST } from "./constants.js";
54
import { fileURLToPath } from "url";
5+
import { OutputBundleOptions } from "./interfaces.js";
6+
import { stringify as yamlStringify } from "yaml";
7+
import { spawnSync } from "child_process";
68

7-
import type { RoutesManifest } from "./interfaces.js";
9+
import { join, relative, normalize } from "path";
810

11+
import type { RoutesManifest } from "./interfaces.js";
912
// fs-extra is CJS, readJson can't be imported using shorthand
10-
export const { readJson } = fsExtra;
13+
export const { move, exists, writeFile, readJson } = fsExtra;
1114

1215
export async function loadConfig(cwd: string) {
1316
// dynamically load NextJS so this can be used in an NPX context
@@ -26,3 +29,83 @@ export const isMain = (meta: ImportMeta) => {
2629
if (!process.argv[1]) return false;
2730
return process.argv[1] === fileURLToPath(meta.url);
2831
};
32+
33+
export function populateOutputBundleOptions(cwd: string): OutputBundleOptions {
34+
const outputBundleDir = join(cwd, ".apphosting");
35+
return {
36+
bundleYamlPath: join(outputBundleDir, "bundle.yaml"),
37+
outputDirectory: outputBundleDir,
38+
serverFilePath: join(outputBundleDir, "server.js"),
39+
outputPublicDirectory: join(outputBundleDir, "public"),
40+
outputStaticDirectory: join(outputBundleDir, ".next", "static"),
41+
};
42+
}
43+
44+
// Run build command
45+
export function build(cwd: string): void {
46+
// Set standalone mode
47+
process.env.NEXT_PRIVATE_STANDALONE = "true";
48+
// Opt-out sending telemetry to Vercel
49+
process.env.NEXT_TELEMETRY_DISABLED = "1";
50+
spawnSync("npm", ["run", "build"], { cwd, shell: true, stdio: "inherit" });
51+
}
52+
53+
// move the standalone directory, the static directory and the public directory to apphosting output directory
54+
// as well as generating bundle.yaml
55+
export async function generateOutputDirectory(
56+
cwd: string,
57+
outputBundleOptions: OutputBundleOptions,
58+
nextBuildDirectory: string,
59+
): Promise<void> {
60+
const standaloneDirectory = join(nextBuildDirectory, "standalone");
61+
await move(standaloneDirectory, outputBundleOptions.outputDirectory, { overwrite: true });
62+
63+
const staticDirectory = join(nextBuildDirectory, "static");
64+
const publicDirectory = join(cwd, "public");
65+
await Promise.all([
66+
move(staticDirectory, outputBundleOptions.outputStaticDirectory, { overwrite: true }),
67+
movePublicDirectory(publicDirectory, outputBundleOptions.outputPublicDirectory),
68+
generateBundleYaml(outputBundleOptions, nextBuildDirectory, cwd),
69+
]);
70+
return;
71+
}
72+
73+
// move public directory to apphosting output public directory
74+
async function movePublicDirectory(
75+
publicDirectory: string,
76+
appHostingPublicDirectory: string,
77+
): Promise<void> {
78+
const publicDirectoryExists = await exists(publicDirectory);
79+
if (!publicDirectoryExists) return;
80+
await move(publicDirectory, appHostingPublicDirectory, { overwrite: true });
81+
return;
82+
}
83+
84+
// generate bundle.yaml
85+
async function generateBundleYaml(
86+
outputBundleOptions: OutputBundleOptions,
87+
nextBuildDirectory: string,
88+
cwd: string,
89+
): Promise<void> {
90+
const manifest = await readRoutesManifest(nextBuildDirectory);
91+
const headers = manifest.headers.map((it) => ({ ...it, regex: undefined }));
92+
const redirects = manifest.redirects
93+
.filter((it) => !it.internal)
94+
.map((it) => ({ ...it, regex: undefined }));
95+
const beforeFileRewrites = Array.isArray(manifest.rewrites)
96+
? manifest.rewrites
97+
: manifest.rewrites?.beforeFiles || [];
98+
const rewrites = beforeFileRewrites.map((it) => ({ ...it, regex: undefined }));
99+
await writeFile(
100+
outputBundleOptions.bundleYamlPath,
101+
yamlStringify({
102+
headers,
103+
redirects,
104+
rewrites,
105+
runCommand: `node ${normalize(relative(cwd, outputBundleOptions.serverFilePath))}`,
106+
neededDirs: [normalize(relative(cwd, outputBundleOptions.outputDirectory))],
107+
staticAssets: [normalize(relative(cwd, outputBundleOptions.outputPublicDirectory))],
108+
}),
109+
);
110+
return;
111+
}

0 commit comments

Comments
 (0)