Skip to content

Commit e5981f2

Browse files
Handle NextJS 13.3 app directory (#5691)
* Handle Next 13 app directory—fixes #5661, #5522 * Actually pull static assets from app directory now that it's stabilizing, fixes #5660 * Hack for detecting image optimization in app directory * Add frameworks CTA * Add integration test for frameworks back in * Add timeouts to NPM and bundle commands—fixes #5661, #5622 --------- Co-authored-by: Leonardo Ortiz <[email protected]>
1 parent ada638b commit e5981f2

File tree

32 files changed

+848
-383
lines changed

32 files changed

+848
-383
lines changed

.github/workflows/node-test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ jobs:
9494
- npm run test:storage-emulator-integration
9595
- npm run test:triggers-end-to-end
9696
- npm run test:triggers-end-to-end:inspect
97+
- npm run test:webframeworks-deploy
9798
steps:
9899
- uses: actions/checkout@v3
99100
- uses: actions/setup-node@v3
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export async function GET() {
2+
return new Response(JSON.stringify([1, 2, 3]), {
3+
status: 200,
4+
headers: {
5+
"content-type": "application/json",
6+
"custom-header": "custom-value",
7+
},
8+
});
9+
}

scripts/webframeworks-deploy-tests/run.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ set -e # Immediately exit on failure
44
# Globally link the CLI for the testing framework
55
./scripts/clean-install.sh
66

7+
source scripts/set-default-credentials.sh
8+
79
(cd scripts/webframeworks-deploy-tests/hosting; npm i; npm run build)
810

911
mocha scripts/webframeworks-deploy-tests/tests.ts
Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { expect } from "chai";
2+
import * as glob from "glob";
23

34
import * as cli from "./cli";
45
import { requireAuth } from "../../src/requireAuth";
6+
import { getBuildId } from "../../src/frameworks/next/utils";
7+
import { relative } from "path";
58

6-
const FIREBASE_PROJECT = process.env.GCLOUD_PROJECT || "";
9+
const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || "";
710
const FIREBASE_DEBUG = process.env.FIREBASE_DEBUG || "";
811

912
function genRandomId(n = 10): string {
@@ -15,37 +18,109 @@ function genRandomId(n = 10): string {
1518
return id;
1619
}
1720

18-
describe("webframeworks deploy", function (this) {
21+
async function getFilesListFromDir(dir: string): Promise<string[]> {
22+
const files = await new Promise<string[]>((resolve, reject) => {
23+
glob(`${dir}/**/*`, (err, matches) => {
24+
if (err) reject(err);
25+
resolve(matches);
26+
});
27+
});
28+
return files.map((path) => relative(dir, path));
29+
}
30+
31+
describe("webframeworks deploy build", function (this) {
1932
this.timeout(1000_000);
2033

34+
let result: cli.Result;
35+
2136
const RUN_ID = genRandomId();
2237
console.log(`TEST RUN: ${RUN_ID}`);
2338

24-
async function setOptsAndDeploy(): Promise<cli.Result> {
25-
const args = [];
39+
async function setOptsAndBuild(): Promise<cli.Result> {
40+
const args = ["exit 0"];
2641
if (FIREBASE_DEBUG) {
2742
args.push("--debug");
2843
}
29-
return await cli.exec("deploy", FIREBASE_PROJECT, args, __dirname, false);
44+
45+
return await cli.exec("emulators:exec", FIREBASE_PROJECT, args, __dirname, false);
3046
}
3147

3248
before(async () => {
3349
expect(FIREBASE_PROJECT).to.not.be.empty;
3450

3551
await requireAuth({});
52+
process.env.FIREBASE_CLI_EXPERIMENTS = "webframeworks";
53+
result = await setOptsAndBuild();
3654
});
3755

3856
after(() => {
3957
// This is not an empty block.
4058
});
4159

42-
it("deploys functions with runtime options", async () => {
60+
it("should log reasons for backend", () => {
4361
process.env.FIREBASE_CLI_EXPERIMENTS = "webframeworks";
4462

45-
const result = await setOptsAndDeploy();
63+
expect(result.stdout, "build result").to.match(
64+
/Building a Cloud Function to run this application. This is needed due to:/
65+
);
66+
expect(result.stdout, "build result").to.match(/middleware/);
67+
expect(result.stdout, "build result").to.match(/Image Optimization/);
68+
expect(result.stdout, "build result").to.match(/use of revalidate \/bar/);
69+
expect(result.stdout, "build result").to.match(/non-static route \/api\/hello/);
70+
});
71+
72+
it("should have the expected static files to be deployed", async () => {
73+
const buildId = await getBuildId(`${__dirname}/hosting/.next`);
74+
75+
const DOT_FIREBASE_FOLDER_PATH = `${__dirname}/.firebase/${FIREBASE_PROJECT}`;
76+
77+
const EXPECTED_FILES = [
78+
`_next`,
79+
`_next/static`,
80+
`_next/static/chunks`,
81+
`_next/static/chunks/app`,
82+
`_next/static/chunks/app/bar`,
83+
`_next/static/chunks/app/foo`,
84+
`_next/static/chunks/pages`,
85+
`_next/static/chunks/pages/about`,
86+
`_next/static/css`,
87+
`_next/static/${buildId}`,
88+
`_next/static/${buildId}/_buildManifest.js`,
89+
`_next/static/${buildId}/_ssgManifest.js`,
90+
`404.html`,
91+
`500.html`,
92+
`foo.html`,
93+
`index.html`,
94+
];
95+
96+
const EXPECTED_PATTERNS = [
97+
`_next\/static\/chunks\/[0-9]+-[^\.]+\.js`,
98+
`_next\/static\/chunks\/app-internals-[^\.]+\.js`,
99+
`_next\/static\/chunks\/app\/bar\/page-[^\.]+\.js`,
100+
`_next\/static\/chunks\/app\/foo\/page-[^\.]+\.js`,
101+
`_next\/static\/chunks\/app\/layout-[^\.]+\.js`,
102+
`_next\/static\/chunks\/main-[^\.]+\.js`,
103+
`_next\/static\/chunks\/main-app-[^\.]+\.js`,
104+
`_next\/static\/chunks\/pages\/_app-[^\.]+\.js`,
105+
`_next\/static\/chunks\/pages\/_error-[^\.]+\.js`,
106+
`_next\/static\/chunks\/pages\/about\/me-[^\.]+\.js`,
107+
`_next\/static\/chunks\/pages\/index-[^\.]+\.js`,
108+
`_next\/static\/chunks\/polyfills-[^\.]+\.js`,
109+
`_next\/static\/chunks\/webpack-[^\.]+\.js`,
110+
`_next\/static\/css\/[^\.]+\.css`,
111+
].map((it) => new RegExp(it));
112+
113+
const files = await getFilesListFromDir(`${DOT_FIREBASE_FOLDER_PATH}/hosting`);
114+
const unmatchedFiles = files.filter(
115+
(it) =>
116+
!(EXPECTED_FILES.includes(it) || EXPECTED_PATTERNS.some((pattern) => !!it.match(pattern)))
117+
);
118+
const unmatchedExpectations = [
119+
...EXPECTED_FILES.filter((it) => !files.includes(it)),
120+
...EXPECTED_PATTERNS.filter((it) => !files.some((file) => !!file.match(it))),
121+
];
46122

47-
expect(result.stdout, "deploy result").to.match(/file upload complete/);
48-
expect(result.stdout, "deploy result").to.match(/found 20 files/);
49-
expect(result.stdout, "deploy result").to.match(/Deploy complete!/);
123+
expect(unmatchedFiles).to.be.empty;
124+
expect(unmatchedExpectations).to.be.empty;
50125
});
51126
});

src/frameworks/angular/index.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,20 @@ import { spawn } from "cross-spawn";
55
import { copy, pathExists } from "fs-extra";
66
import { mkdir } from "fs/promises";
77

8+
import { BuildResult, Discovery, FrameworkType, SupportLevel } from "../interfaces";
9+
import { promptOnce } from "../../prompt";
810
import {
9-
BuildResult,
10-
Discovery,
11-
findDependency,
12-
FrameworkType,
11+
simpleProxy,
12+
warnIfCustomBuildScript,
1313
getNodeModuleBin,
1414
relativeRequire,
15-
SupportLevel,
16-
} from "..";
17-
import { promptOnce } from "../../prompt";
18-
import { simpleProxy, warnIfCustomBuildScript } from "../utils";
15+
findDependency,
16+
} from "../utils";
1917

2018
export const name = "Angular";
21-
export const support = SupportLevel.Experimental;
19+
export const support = SupportLevel.Preview;
2220
export const type = FrameworkType.Framework;
21+
export const docsUrl = "https://firebase.google.com/docs/hosting/frameworks/angular";
2322

2423
const DEFAULT_BUILD_SCRIPT = ["ng build"];
2524

src/frameworks/astro/index.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import { sync as spawnSync, spawn } from "cross-spawn";
22
import { copy, existsSync } from "fs-extra";
33
import { join } from "path";
4+
import { BuildResult, Discovery, FrameworkType, SupportLevel } from "../interfaces";
5+
import { FirebaseError } from "../../error";
46
import {
5-
BuildResult,
6-
Discovery,
7-
FrameworkType,
8-
SupportLevel,
7+
readJSON,
8+
simpleProxy,
9+
warnIfCustomBuildScript,
910
findDependency,
1011
getNodeModuleBin,
11-
} from "..";
12-
import { FirebaseError } from "../../error";
13-
import { readJSON, simpleProxy, warnIfCustomBuildScript } from "../utils";
12+
} from "../utils";
1413
import { getBootstrapScript, getConfig } from "./utils";
1514

1615
export const name = "Astro";

src/frameworks/constants.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { readdirSync, statSync } from "fs";
2+
import { join } from "path";
3+
import { Framework, SupportLevel } from "./interfaces";
4+
import * as clc from "colorette";
5+
6+
export const NPM_COMMAND_TIMEOUT_MILLIES = 10_000;
7+
8+
export const SupportLevelWarnings = {
9+
[SupportLevel.Experimental]: (framework: string) => `Thank you for trying our ${clc.italic(
10+
"experimental"
11+
)} support for ${framework} on Firebase Hosting.
12+
${clc.yellow(`While this integration is maintained by Googlers it is not a supported Firebase product.
13+
Issues filed on GitHub will be addressed on a best-effort basis by maintainers and other community members.`)}`,
14+
[SupportLevel.Preview]: (framework: string) => `Thank you for trying our ${clc.italic(
15+
"early preview"
16+
)} of ${framework} support on Firebase Hosting.
17+
${clc.yellow(
18+
"During the preview, support is best-effort and breaking changes can be expected. Proceed with caution."
19+
)}`,
20+
};
21+
22+
export const DEFAULT_DOCS_URL =
23+
"https://firebase.google.com/docs/hosting/frameworks/frameworks-overview";
24+
export const FILE_BUG_URL =
25+
"https://github.com/firebase/firebase-tools/issues/new?template=bug_report.md";
26+
export const FEATURE_REQUEST_URL =
27+
"https://github.com/firebase/firebase-tools/issues/new?template=feature_request.md";
28+
export const MAILING_LIST_URL = "https://goo.gle/41enW5X";
29+
30+
export const FIREBASE_FRAMEWORKS_VERSION = "^0.9.0";
31+
export const FIREBASE_FUNCTIONS_VERSION = "^4.3.0";
32+
export const FIREBASE_ADMIN_VERSION = "^11.0.1";
33+
export const NODE_VERSION = parseInt(process.versions.node, 10);
34+
export const VALID_ENGINES = { node: [16, 18] };
35+
36+
export const DEFAULT_REGION = "us-central1";
37+
export const ALLOWED_SSR_REGIONS = [
38+
{ name: "us-central1 (Iowa)", value: "us-central1" },
39+
{ name: "us-west1 (Oregon)", value: "us-west1" },
40+
{ name: "us-east1 (South Carolina)", value: "us-east1" },
41+
{ name: "europe-west1 (Belgium)", value: "europe-west1" },
42+
{ name: "asia-east1 (Taiwan)", value: "asia-east1" },
43+
];
44+
45+
export const WebFrameworks: Record<string, Framework> = Object.fromEntries(
46+
readdirSync(__dirname)
47+
.filter((path) => statSync(join(__dirname, path)).isDirectory())
48+
.map((path) => {
49+
// If not called by the CLI, (e.g., by the VS Code Extension)
50+
// __dirname won't refer to this folder and these files won't be available.
51+
// Instead it may find sibling folders that aren't modules, and this
52+
// require will throw.
53+
// Long term fix may be to bundle this instead of reading files at runtime
54+
// but for now, this prevents crashing.
55+
try {
56+
return [path, require(join(__dirname, path))];
57+
} catch (e) {
58+
return [];
59+
}
60+
})
61+
.filter(
62+
([, obj]) =>
63+
obj && obj.name && obj.discover && obj.build && obj.type !== undefined && obj.support
64+
)
65+
);

src/frameworks/express/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import { execSync } from "child_process";
22
import { copy, pathExists } from "fs-extra";
33
import { mkdir, readFile } from "fs/promises";
44
import { join } from "path";
5-
import { BuildResult, FrameworkType, SupportLevel } from "..";
5+
import { BuildResult, FrameworkType, SupportLevel } from "../interfaces";
66

77
// Use "true &&"" to keep typescript from compiling this file and rewriting
88
// the import statement into a require
99
const { dynamicImport } = require(true && "../../dynamicImport");
1010

1111
export const name = "Express.js";
12-
export const support = SupportLevel.Experimental;
12+
export const support = SupportLevel.Preview;
1313
export const type = FrameworkType.Custom;
14+
export const docsUrl = "https://firebase.google.com/docs/hosting/frameworks/express";
1415

1516
async function getConfig(root: string) {
1617
const packageJsonBuffer = await readFile(join(root, "package.json"));

src/frameworks/flutter/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { join } from "path";
44
import { load as loadYaml } from "js-yaml";
55
import { readFile } from "fs/promises";
66

7-
import { BuildResult, Discovery, FrameworkType, SupportLevel } from "..";
7+
import { BuildResult, Discovery, FrameworkType, SupportLevel } from "../interfaces";
88
import { FirebaseError } from "../../error";
99
import { assertFlutterCliExists } from "./utils";
1010

0 commit comments

Comments
 (0)