Skip to content
Open
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
169 changes: 169 additions & 0 deletions src/init/features/hosting/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as config from "../../../config";
import * as getDefaultHostingSiteMod from "../../../getDefaultHostingSite";
import * as hostingInteractive from "../../../hosting/interactive";
import * as hostingApi from "../../../hosting/api";
import * as frameworks from "../../../frameworks";
import { Client } from "../../../apiv2";
import { askQuestions, actuate } from "./index";
import { Setup } from "../..";
Expand All @@ -31,6 +32,7 @@ describe("hosting feature init", () => {
};
const cfg = new config.Config({}, { projectDir: "/", cwd: "/" });

sandbox.stub(frameworks, "discover").resolves(undefined);
// Mock existing site check
sandbox.stub(getDefaultHostingSiteMod, "getDefaultHostingSite").resolves("test-site");

Expand Down Expand Up @@ -72,6 +74,7 @@ describe("hosting feature init", () => {
};
const cfg = new config.Config({}, { projectDir: "/", cwd: "/" });

sandbox.stub(frameworks, "discover").resolves(undefined);
sandbox
.stub(getDefaultHostingSiteMod, "getDefaultHostingSite")
.rejects(getDefaultHostingSiteMod.errNoDefaultSite);
Expand All @@ -93,9 +96,175 @@ describe("hosting feature init", () => {
expect(pickSiteStub.called).to.be.true;
expect(setup.featureInfo?.hosting?.newSiteId).to.equal("new-site-id");
});

it("should redirect to apphosting init when a Node.js framework is detected", async () => {
const setup: Setup = {
config: {},
rcfile: { projects: {}, targets: {}, etags: {} },
projectId: "test-project",
features: [],
instructions: [],
};
const cfg = new config.Config({}, { projectDir: "/", cwd: "/" });

sandbox.stub(frameworks, "discover").resolves({ framework: "next", mayWantBackend: true });
const confirmStub = sandbox.stub(prompt, "confirm");
const inputStub = sandbox.stub(prompt, "input");
sandbox.stub(github, "initGitHub").resolves();

await askQuestions(setup, cfg, {
cwd: "/",
configPath: "",
only: "",
except: "",
nonInteractive: false,
} as any);

expect(setup.features).to.deep.equal(["apphosting"]);
expect(setup.featureInfo?.hosting).to.deep.equal({ redirectToAppHosting: true });
expect(confirmStub.called).to.be.false;
expect(inputStub.called).to.be.false;
});

it("should not terminate when no framework is detected", async () => {
const setup: Setup = {
config: {},
rcfile: { projects: {}, targets: {}, etags: {} },
projectId: "test-project",
instructions: [],
};
const cfg = new config.Config({}, { projectDir: "/", cwd: "/" });

sandbox.stub(frameworks, "discover").resolves(undefined);
sandbox.stub(getDefaultHostingSiteMod, "getDefaultHostingSite").resolves("test-site");

sandbox.stub(prompt, "confirm").resolves(false);
const inputStub = sandbox.stub(prompt, "input").resolves("public");
sandbox.stub(github, "initGitHub").resolves();

await askQuestions(setup, cfg, {
cwd: "/",
configPath: "",
only: "",
except: "",
nonInteractive: false,
} as any);

expect(
inputStub.calledWith(
sinon.match({ message: "What do you want to use as your public directory?" }),
),
).to.be.true;
});

it("should not terminate for static frameworks like Flutter", async () => {
const setup: Setup = {
config: {},
rcfile: { projects: {}, targets: {}, etags: {} },
projectId: "test-project",
instructions: [],
};
const cfg = new config.Config({}, { projectDir: "/", cwd: "/" });

sandbox
.stub(frameworks, "discover")
.resolves({ framework: "flutter", mayWantBackend: false });
sandbox.stub(getDefaultHostingSiteMod, "getDefaultHostingSite").resolves("test-site");

sandbox.stub(prompt, "confirm").resolves(false);
const inputStub = sandbox.stub(prompt, "input").resolves("public");
sandbox.stub(github, "initGitHub").resolves();

await askQuestions(setup, cfg, {
cwd: "/",
configPath: "",
only: "",
except: "",
nonInteractive: false,
} as any);

expect(
inputStub.calledWith(
sinon.match({ message: "What do you want to use as your public directory?" }),
),
).to.be.true;
});

it("should not terminate when a framework without backend is detected", async () => {
const setup: Setup = {
config: {},
rcfile: { projects: {}, targets: {}, etags: {} },
projectId: "test-project",
instructions: [],
};
const cfg = new config.Config({}, { projectDir: "/", cwd: "/" });

sandbox.stub(frameworks, "discover").resolves({ framework: "nextjs", mayWantBackend: false });
sandbox.stub(getDefaultHostingSiteMod, "getDefaultHostingSite").resolves("test-site");

sandbox.stub(prompt, "confirm").resolves(false);
const inputStub = sandbox.stub(prompt, "input").resolves("public");
sandbox.stub(github, "initGitHub").resolves();

await askQuestions(setup, cfg, {
cwd: "/",
configPath: "",
only: "",
except: "",
nonInteractive: false,
} as any);

expect(
inputStub.calledWith(
sinon.match({ message: "What do you want to use as your public directory?" }),
),
).to.be.true;
});
});

describe("actuate", () => {
it("should throw when hosting info is missing", async () => {
const setup: Setup = {
config: {},
rcfile: { projects: {}, targets: {}, etags: {} },
projectId: "test-project",
instructions: [],
};
const cfg = new config.Config({}, { projectDir: "/", cwd: "/" });

await expect(
actuate(setup, cfg, {
cwd: "/",
configPath: "",
only: "",
except: "",
nonInteractive: false,
} as any),
).to.be.rejectedWith(/Could not find hosting info/);
});

it("should be a no-op when hosting was redirected to apphosting", async () => {
const setup: Setup = {
config: {},
rcfile: { projects: {}, targets: {}, etags: {} },
projectId: "test-project",
featureInfo: { hosting: { redirectToAppHosting: true } },
instructions: [],
};
const cfg = new config.Config({}, { projectDir: "/", cwd: "/" });
const askWriteStub = sandbox.stub(cfg, "askWriteProjectFile").resolves();

await actuate(setup, cfg, {
cwd: "/",
configPath: "",
only: "",
except: "",
nonInteractive: false,
} as any);

expect(askWriteStub.called).to.be.false;
});

it("should write 404.html and index.html for non-SPA", async () => {
const setup: Setup = {
config: {},
Expand Down
19 changes: 19 additions & 0 deletions src/init/features/hosting/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as clc from "colorette";
import { join } from "path";
import { Client } from "../../../apiv2";
import { discover } from "../../../frameworks";
import * as github from "./github";
import { confirm, input } from "../../../prompt";
import { logger } from "../../../logger";
Expand All @@ -19,6 +20,7 @@ const MISSING_TEMPLATE = readTemplateSync("init/hosting/404.html");
const DEFAULT_IGNORES = ["firebase.json", "**/.*", "**/node_modules/**"];

export interface RequiredInfo {
redirectToAppHosting?: boolean;
newSiteId?: string;
public?: string;
spa?: boolean;
Expand All @@ -27,6 +29,19 @@ export interface RequiredInfo {
// TODO: come up with a better way to type this
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function askQuestions(setup: Setup, config: Config, options: Options): Promise<void> {
const discoveredFramework = await discover(config.projectDir, false);
if (discoveredFramework && discoveredFramework.mayWantBackend) {
const frameworkName = discoveredFramework.framework;
logger.info();
logger.info(
`Detected a ${frameworkName} codebase. Setting up ${clc.bold("App Hosting")} instead.`,
);
setup.featureInfo ||= {};
setup.featureInfo.hosting = { redirectToAppHosting: true };
Copy link
Contributor

Choose a reason for hiding this comment

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

app hosting does not support flutter and i believe one of the web frameworks detected is flutter

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you for bringing this up. Flutter won't actually trigger this because its mayWantBackend is always false, i.e. it's always static.

However, frameworks other than Angular and Next.js that (I think) are not supported by App Hosting will have this issue:

  • Express.js: always
  • Nuxt 2: always
  • Nuxt 3: when SSR is enabled
  • Astro: when output is not "static"
  • Vite: when app type is not "spa"

WDYT about:

  1. only run init apphosting automatically for Next.js and Angular projects
  2. error out for non supported frameworks with a backend?

setup.features?.unshift("apphosting");
return;
}

setup.featureInfo = setup.featureInfo || {};
setup.featureInfo.hosting = {};

Expand Down Expand Up @@ -93,6 +108,10 @@ export async function actuate(setup: Setup, config: Config, options: Options): P
);
}

if (hostingInfo.redirectToAppHosting) {
return;
}

if (hostingInfo.newSiteId && setup.projectId) {
await createSite(setup.projectId, hostingInfo.newSiteId);
logger.info();
Expand Down
Loading