Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/@apphosting/adapter-nextjs/e2e/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ describe("app", () => {
assert.notEqual(initialUUID, newUUID, "UUID should change after revalidation");
});

it("serves public assets", async () => {
for (const asset of ["between-links.svg", "between-cards.svg"]) {
const response = await fetch(posix.join(host, asset));
assert.ok(response.ok, `${asset} should be served from public/`);
assert.ok((await response.text()).includes("<svg"), `${asset} should be an SVG`);
}
});

it(`404`, async () => {
const response = await fetch(posix.join(host, Math.random().toString()));
assert.equal(response.status, 404);
Expand Down
31 changes: 31 additions & 0 deletions packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,37 @@ outputFiles:
};
validateTestFiles(tmpDir, expectedFiles);
});

it("merges the public directory when the standalone output is partially populated", async () => {
const { generateBuildOutput, validateOutputDirectory } = await importUtils;
const files = {
".next/standalone/.next/package.json": "",
".next/standalone/server.js": "",
".next/static/staticfile": "",
"public/a.svg": "a",
"public/b.svg": "b",
// Next already copied a.svg here while tracing a server component's file reads,
// so public/ exists in the output but is incomplete.
".next/standalone/public/a.svg": "a",
};
generateTestFiles(tmpDir, files);
await generateBuildOutput(
tmpDir,
tmpDir,
outputBundleOptions,
path.join(tmpDir, ".next"),
defaultNextVersion,
adapterMetadata,
);
await validateOutputDirectory(outputBundleOptions, path.join(tmpDir, ".next"));

const expectedFiles = {
".next/standalone/public/a.svg": "a",
".next/standalone/public/b.svg": "b",
};
validateTestFiles(tmpDir, expectedFiles);
});

it("test populate output bundle options", async () => {
const { populateOutputBundleOptions } = await importUtils;
const expectedOutputBundleOptions = {
Expand Down
15 changes: 14 additions & 1 deletion packages/@apphosting/adapter-nextjs/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export async function generateBuildOutput(
const staticDirectory = join(nextBuildDirectory, "static");
await Promise.all([
copy(staticDirectory, opts.outputStaticDirectoryPath, { overwrite: true }),
copyPublicDirectory(appDir, opts.outputPublicDirectoryPath),
copyResources(appDir, opts.outputDirectoryAppPath, opts.bundleYamlPath),
generateBundleYaml(opts, rootDir, nextVersion, adapterMetadata),
]);
Expand All @@ -166,6 +167,16 @@ export async function generateBuildOutput(
return;
}

// Copy the app's public directory into the standalone output. Next.js' standalone output does not
// reliably include it: the webpack builder omits it entirely, and file tracing can pre-create it
// holding only the assets a server component reads. Merging per file completes either case.
// https://nextjs.org/docs/app/api-reference/config/next-config-js/output#automatically-copying-traced-files
async function copyPublicDirectory(appDir: string, outputPublicDir: string): Promise<void> {
const publicDirectory = join(appDir, "public");
if (!(await exists(publicDirectory))) return;
await copy(publicDirectory, outputPublicDir, { overwrite: true });
}

// Copy all files and directories to apphosting output directory.
// Files are skipped if there is already a file with the same name in the output directory
async function copyResources(
Expand All @@ -181,7 +192,9 @@ async function copyResources(
const existsInOutputBundle = await exists(join(outputBundleAppDir, path));
// Keep apphosting.yaml files in the root directory still, as later steps expect them to be there
const isApphostingYaml = path === "apphosting_preprocessed" || path === "apphosting.yaml";
if (!isbundleYamlDir && !existsInOutputBundle && !isApphostingYaml) {
// The public directory is handled by copyPublicDirectory.
const isPublicDir = path === "public";
if (!isbundleYamlDir && !existsInOutputBundle && !isApphostingYaml && !isPublicDir) {
await copy(join(appDir, path), join(outputBundleAppDir, path));
}
}
Expand Down
Loading