Skip to content

Commit 7363f29

Browse files
committed
Quick .apphosting_build PoC
1 parent 0bd8d12 commit 7363f29

File tree

10 files changed

+187
-33
lines changed

10 files changed

+187
-33
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ node_modules
99
.vscode
1010
*.log
1111
.DS_Store
12+
/apphosting-*.tgz
13+
/firebase-frameworks-*.tgz
1214

1315
# firebase
1416
.firebase

package-lock.json

Lines changed: 37 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"eslint-plugin-jsdoc": "^46.8.2",
2020
"eslint-plugin-prettier": "^5.0.1",
2121
"lerna": "^7.4.0",
22-
"prettier": "^3.0.3"
22+
"prettier": "^3.0.3",
23+
"typescript": "^5.2.0"
2324
}
2425
}

packages/@apphosting/adapter-nextjs/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
],
3636
"license": "Apache-2.0",
3737
"dependencies": {
38+
"fs-extra": "^11.1.1",
39+
"yaml": "^2.3.4"
3840
},
3941
"peerDependencies": {
4042
"next": "~14.0.0"
@@ -45,6 +47,7 @@
4547
}
4648
},
4749
"devDependencies": {
50+
"@types/fs-extra": "^11.0.4",
4851
"@types/tmp": "^0.2.6",
4952
"next": "~14.0.0",
5053
"semver": "^7.5.4",
Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,60 @@
11
#! /usr/bin/env node
22
import { spawnSync } from "child_process";
3+
import { join } from "path";
4+
import fsExtra from "fs-extra";
5+
import { stringify as yamlStringify } from "yaml";
6+
7+
const { copy, mkdirp, exists, readJson, writeFile } = fsExtra;
8+
9+
import type { RoutesManifest } from "../interfaces.js";
310

411
const cwd = process.cwd();
512

6-
spawnSync("next", ["build"], { cwd, stdio: "inherit" });
13+
// TODO don't await these here, clean up these imports
14+
// for the constants we should probably keep them the same way we do in firebase-tools
15+
const { default: nextServerConfig }: { default: typeof import("next/dist/server/config.js") } = await import(`${cwd}/node_modules/next/dist/server/config.js`);
16+
const { PHASE_PRODUCTION_BUILD, ROUTES_MANIFEST }: typeof import("next/constants.js") = await import(`${cwd}/node_modules/next/constants.js`);
17+
18+
const loadConfig = nextServerConfig.default;
19+
20+
// Since this tool can be executed with NPX maybe we should just call `npm run build`?
21+
spawnSync("next", ["build"], { cwd, stdio: "inherit", env: { ...process.env, NEXT_PRIVATE_STANDALONE: 'true' } });
22+
23+
const { distDir, basePath } = await loadConfig(PHASE_PRODUCTION_BUILD, cwd);
24+
25+
const destination = join(cwd, '.apphosting_build');
26+
27+
const staticDestination = join(destination, "static", basePath);
28+
const nextStaticDestination = join(staticDestination, "_next", "static");
29+
const publicDestination = join(destination, "public");
30+
31+
const standaloneDir = join(cwd, distDir, "standalone");
32+
const publicDir = join(cwd, "public");
33+
const nextStaticDir = join(cwd, distDir, "static");
34+
35+
await mkdirp(nextStaticDestination);
36+
37+
const copyPublicDir = async () => {
38+
const publicDirExists = await exists(publicDir);
39+
if (!publicDirExists) return;
40+
await Promise.all([
41+
copy(publicDir, publicDestination),
42+
copy(publicDir, staticDestination)
43+
]);
44+
};
45+
46+
const writeBundleYaml = async () => {
47+
const manifest: RoutesManifest = await readJson(join(cwd, distDir, ROUTES_MANIFEST));
48+
const headers = manifest.headers.map(it => ({ ...it, regex: undefined }));
49+
const redirects = manifest.redirects.filter(it => !it.internal).map(it => ({ ...it, regex: undefined }));
50+
const beforeFileRewrites = Array.isArray(manifest.rewrites) ? manifest.rewrites : manifest.rewrites?.beforeFiles || [];
51+
const rewrites = beforeFileRewrites.map(it => ({ ...it, regex: undefined }));
52+
await writeFile(join(destination, "bundle.yaml"), yamlStringify({ headers, redirects, rewrites }));
53+
}
54+
55+
await Promise.all([
56+
copy(standaloneDir, destination),
57+
copy(nextStaticDir, nextStaticDestination),
58+
copyPublicDir(),
59+
writeBundleYaml(),
60+
]);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { RouteHas } from "next/dist/lib/load-custom-routes.js";
2+
3+
export interface RoutesManifestRewriteObject {
4+
beforeFiles?: RoutesManifestRewrite[];
5+
afterFiles?: RoutesManifestRewrite[];
6+
fallback?: RoutesManifestRewrite[];
7+
}
8+
9+
export interface RoutesManifestRedirect {
10+
source: string;
11+
destination: string;
12+
locale?: false;
13+
internal?: boolean;
14+
statusCode: number;
15+
regex: string;
16+
has?: RouteHas[];
17+
missing?: RouteHas[];
18+
}
19+
20+
export interface RoutesManifestRewrite {
21+
source: string;
22+
destination: string;
23+
has?: RouteHas[];
24+
missing?: RouteHas[];
25+
regex: string;
26+
}
27+
28+
export interface RoutesManifestHeader {
29+
source: string;
30+
headers: { key: string; value: string }[];
31+
has?: RouteHas[];
32+
missing?: RouteHas[];
33+
regex: string;
34+
}
35+
36+
// Next.js's exposed interface is incomplete here
37+
export interface RoutesManifest {
38+
version: number;
39+
pages404: boolean;
40+
basePath: string;
41+
redirects: Array<RoutesManifestRedirect>;
42+
rewrites?: Array<RoutesManifestRewrite> | RoutesManifestRewriteObject;
43+
headers: Array<RoutesManifestHeader>;
44+
staticRoutes: Array<{
45+
page: string;
46+
regex: string;
47+
namedRegex?: string;
48+
routeKeys?: { [key: string]: string };
49+
}>;
50+
dynamicRoutes: Array<{
51+
page: string;
52+
regex: string;
53+
namedRegex?: string;
54+
routeKeys?: { [key: string]: string };
55+
}>;
56+
dataRoutes: Array<{
57+
page: string;
58+
routeKeys?: { [key: string]: string };
59+
dataRouteRegex: string;
60+
namedDataRouteRegex?: string;
61+
}>;
62+
i18n?: {
63+
domains?: Array<{
64+
http?: true;
65+
domain: string;
66+
locales?: string[];
67+
defaultLocale: string;
68+
}>;
69+
locales: string[];
70+
defaultLocale: string;
71+
localeDetection?: false;
72+
};
73+
}

packages/@apphosting/build/src/bin/build.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@ program
3131
`^${version.major}.${version.minor + 1}.0`, // newer production match
3232
].join(" || ");
3333
const adapterName = `@apphosting/adapter-${framework}`;
34+
// TODO add types here
3435
let npmInfo = JSON.parse(spawnSync("npm", ["info", `${adapterName}@"${versionStackRank}"`, "--json"]).stdout.toString());
3536
if (npmInfo.error) npmInfo = JSON.parse(spawnSync("npm", ["info", adapterName, "--json"]).stdout.toString());
3637
if (npmInfo.error) {
3738
throw new Error(npmInfo.error.detail)
3839
}
40+
npmInfo = [].concat(npmInfo);
41+
npmInfo = npmInfo.filter((it:any) => !it.version.includes('-canary.')).sort((a:any,b:any) => new Date(b.time[b.version]).getTime() - new Date(a.time[a.version]).getTime())[0];
3942
const adapterVersion = semverParse(npmInfo.version);
4043
if (!adapterVersion) throw new Error(`Unable to parse ${adapterVersion}`);
4144
// TODO actually write a reasonable error message here & use a generator function

packages/firebase-frameworks/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,8 @@
7373
"firebase": "^10.0.0",
7474
"firebase-admin": "^11.0.1",
7575
"firebase-functions": "^4.3.1",
76-
"next": "^14.0.1",
76+
"next": "~14.0.0",
7777
"sharp": "^0.32.1",
78-
"ts-node": "^10.9.1",
79-
"typescript": "^4.7.4"
78+
"ts-node": "^10.9.1"
8079
}
8180
}

packages/firebase-frameworks/src/next.js/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { parse } from "url";
2-
import { default as next } from "next";
2+
import next from "next";
3+
import LRU from "lru-cache";
4+
35
import type { Request } from "firebase-functions/v2/https";
46
import type { Response } from "express";
5-
import LRU from "lru-cache";
6-
import { NextServer } from "next/dist/server/next.js";
7+
import type { NextServer } from "next/dist/server/next.js";
8+
9+
const createServer = next.default;
710

811
const nextAppsLRU = new LRU<string, NextServer>({
912
// TODO tune this
@@ -24,8 +27,7 @@ export const handle = async (req: Request, res: Response) => {
2427
// dynamic for middleware.
2528
let nextApp = nextAppsLRU.get(key);
2629
if (!nextApp) {
27-
// @ts-expect-error
28-
nextApp = next({
30+
nextApp = createServer({
2931
dev: false,
3032
dir: process.cwd(),
3133
hostname: "0.0.0.0",

tsconfig.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
{
22
"compilerOptions": {
33
"noEmit": true,
4-
"target": "es2020",
4+
"target": "ES2020",
55
"module": "NodeNext",
6+
"moduleResolution": "NodeNext",
67
"types": ["node"],
78
"esModuleInterop": true,
89
"forceConsistentCasingInFileNames": true,
910
"strict": true,
1011
"declaration": true,
12+
"skipLibCheck": true,
1113
"paths": {
1214
"firebase-frameworks": ["./packages/firebase-frameworks"],
1315
"@apphosting/adapter-nextjs": ["./packages/adapter-nextjs"]

0 commit comments

Comments
 (0)