diff --git a/CHANGELOG.md b/CHANGELOG.md index c76768396c9..68edae115db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,3 @@ - Fixed an issue preventing Angular apps using ng-deploy from being emulated or deployed. (#6584) +- Warn if a Web Framework is outside a well known version range on deploy/emulate. (#6562) +- Use Web Framework's well known version range in `firebase init hosting`. (#6562) diff --git a/src/frameworks/angular/index.ts b/src/frameworks/angular/index.ts index 8afaea089bd..69158462478 100644 --- a/src/frameworks/angular/index.ts +++ b/src/frameworks/angular/index.ts @@ -20,6 +20,7 @@ import { } from "../utils"; import { getAllTargets, + getAngularVersion, getBrowserConfig, getBuildConfig, getContext, @@ -35,15 +36,18 @@ export const docsUrl = "https://firebase.google.com/docs/hosting/frameworks/angu const DEFAULT_BUILD_SCRIPT = ["ng build"]; +export const supportedRange = "14 - 17"; + export async function discover(dir: string): Promise { if (!(await pathExists(join(dir, "package.json")))) return; if (!(await pathExists(join(dir, "angular.json")))) return; - return { mayWantBackend: true, publicDirectory: join(dir, "src", "assets") }; + const version = getAngularVersion(dir); + return { mayWantBackend: true, publicDirectory: join(dir, "src", "assets"), version }; } export function init(setup: any, config: any) { execSync( - `npx --yes -p @angular/cli@latest ng new ${setup.projectId} --directory ${setup.hosting.source} --skip-git`, + `npx --yes -p @angular/cli@"${supportedRange}" ng new ${setup.projectId} --directory ${setup.hosting.source} --skip-git`, { stdio: "inherit", cwd: config.projectDir, diff --git a/src/frameworks/angular/utils.ts b/src/frameworks/angular/utils.ts index 42b8639e6b4..f4a3dce0df4 100644 --- a/src/frameworks/angular/utils.ts +++ b/src/frameworks/angular/utils.ts @@ -3,12 +3,13 @@ import type { ProjectDefinition } from "@angular-devkit/core/src/workspace"; import type { WorkspaceNodeModulesArchitectHost } from "@angular-devkit/architect/node"; import { AngularI18nConfig } from "./interfaces"; -import { relativeRequire, validateLocales } from "../utils"; +import { findDependency, relativeRequire, validateLocales } from "../utils"; import { FirebaseError } from "../../error"; import { join, posix, sep } from "path"; import { BUILD_TARGET_PURPOSE } from "../interfaces"; import { AssertionError } from "assert"; import { assertIsString } from "../../utils"; +import { coerce } from "semver"; async function localesForTarget( dir: string, @@ -539,3 +540,17 @@ export async function getBuildConfig(sourceDir: string, configuration: string) { ssr, }; } + +/** + * Get Angular version in the following format: `major.minor.patch`, ignoring + * canary versions as it causes issues with semver comparisons. + */ +export function getAngularVersion(cwd: string): string | undefined { + const dependency = findDependency("@angular/core", { cwd, depth: 0, omitDev: false }); + if (!dependency) return undefined; + + const angularVersionSemver = coerce(dependency.version); + if (!angularVersionSemver) return dependency.version; + + return angularVersionSemver.toString(); +} diff --git a/src/frameworks/astro/index.ts b/src/frameworks/astro/index.ts index 665504183a3..3f0bd4923b5 100644 --- a/src/frameworks/astro/index.ts +++ b/src/frameworks/astro/index.ts @@ -9,14 +9,17 @@ import { getAstroVersion, getBootstrapScript, getConfig } from "./utils"; export const name = "Astro"; export const support = SupportLevel.Experimental; export const type = FrameworkType.MetaFramework; +export const supportedRange = "2 - 3"; export async function discover(dir: string): Promise { if (!existsSync(join(dir, "package.json"))) return; - if (!getAstroVersion(dir)) return; + const version = getAstroVersion(dir); + if (!version) return; const { output, publicDir: publicDirectory } = await getConfig(dir); return { mayWantBackend: output !== "static", publicDirectory, + version, }; } diff --git a/src/frameworks/constants.ts b/src/frameworks/constants.ts index 4dac085d56f..daac71889a3 100644 --- a/src/frameworks/constants.ts +++ b/src/frameworks/constants.ts @@ -8,12 +8,12 @@ export const SupportLevelWarnings = { [SupportLevel.Experimental]: (framework: string) => `Thank you for trying our ${clc.italic( "experimental" )} support for ${framework} on Firebase Hosting. - ${clc.yellow(`While this integration is maintained by Googlers it is not a supported Firebase product. + ${clc.red(`While this integration is maintained by Googlers it is not a supported Firebase product. Issues filed on GitHub will be addressed on a best-effort basis by maintainers and other community members.`)}`, [SupportLevel.Preview]: (framework: string) => `Thank you for trying our ${clc.italic( "early preview" )} of ${framework} support on Firebase Hosting. - ${clc.yellow( + ${clc.red( "During the preview, support is best-effort and breaking changes can be expected. Proceed with caution." )}`, }; diff --git a/src/frameworks/docs/nextjs.md b/src/frameworks/docs/nextjs.md index 9d75c21c930..611bf5ae818 100644 --- a/src/frameworks/docs/nextjs.md +++ b/src/frameworks/docs/nextjs.md @@ -14,7 +14,7 @@ Using the {{firebase_cli}}, you can deploy your Next.js Web apps to Firebase and serve them with {{firebase_hosting}}. The {{cli}} respects your Next.js settings and translates them to Firebase settings with zero or minimal extra configuration on your part. If your app includes dynamic server-side logic, the {{cli}} deploys that -logic to {{cloud_functions_full}}. The latest supported Next.js version is 13.4.7. +logic to {{cloud_functions_full}}. <<_includes/_preview-disclaimer.md>> diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts index b25bc4f49b3..01b8d21f665 100644 --- a/src/frameworks/index.ts +++ b/src/frameworks/index.ts @@ -261,11 +261,21 @@ export async function prepareFrameworks( name, support, docsUrl, + supportedRange, getValidBuildTargets = GET_DEFAULT_BUILD_TARGETS, shouldUseDevModeHandle = DEFAULT_SHOULD_USE_DEV_MODE_HANDLE, } = WebFrameworks[framework]; + logger.info( - `\n${frameworksCallToAction(SupportLevelWarnings[support](name), docsUrl, " ")}\n` + `\n${frameworksCallToAction( + SupportLevelWarnings[support](name), + docsUrl, + " ", + name, + results.version, + supportedRange, + results.vite + )}\n` ); const hostingEmulatorInfo = emulators.find((e) => e.name === Emulators.HOSTING); diff --git a/src/frameworks/interfaces.ts b/src/frameworks/interfaces.ts index 06cf7062aaf..f3825c0660a 100644 --- a/src/frameworks/interfaces.ts +++ b/src/frameworks/interfaces.ts @@ -23,6 +23,8 @@ export const enum SupportLevel { export interface Discovery { mayWantBackend: boolean; publicDirectory: string; + version?: string; + vite?: boolean; } export interface BuildResult { @@ -53,6 +55,7 @@ export type FrameworkContext = { }; export interface Framework { + supportedRange?: string; discover: (dir: string) => Promise; type: FrameworkType; name: string; diff --git a/src/frameworks/next/index.ts b/src/frameworks/next/index.ts index ccdcd36eced..0368e6b28b6 100644 --- a/src/frameworks/next/index.ts +++ b/src/frameworks/next/index.ts @@ -73,6 +73,8 @@ import { logger } from "../../logger"; const DEFAULT_BUILD_SCRIPT = ["next build"]; const PUBLIC_DIR = "public"; +export const supportedRange = "12 - 14.0"; + export const name = "Next.js"; export const support = SupportLevel.Preview; export const type = FrameworkType.MetaFramework; @@ -90,9 +92,10 @@ function getReactVersion(cwd: string): string | undefined { */ export async function discover(dir: string) { if (!(await pathExists(join(dir, "package.json")))) return; - if (!(await pathExists("next.config.js")) && !getNextVersion(dir)) return; + const version = getNextVersion(dir); + if (!(await pathExists("next.config.js")) && !version) return; - return { mayWantBackend: true, publicDirectory: join(dir, PUBLIC_DIR) }; + return { mayWantBackend: true, publicDirectory: join(dir, PUBLIC_DIR), version }; } /** @@ -317,9 +320,9 @@ export async function init(setup: any, config: any) { choices: ["JavaScript", "TypeScript"], }); execSync( - `npx --yes create-next-app@latest -e hello-world ${setup.hosting.source} --use-npm ${ - language === "TypeScript" ? "--ts" : "--js" - }`, + `npx --yes create-next-app@"${supportedRange}" -e hello-world ${ + setup.hosting.source + } --use-npm ${language === "TypeScript" ? "--ts" : "--js"}`, { stdio: "inherit", cwd: config.projectDir } ); } diff --git a/src/frameworks/nuxt/index.ts b/src/frameworks/nuxt/index.ts index 8b79667f9c1..3a029c5f34e 100644 --- a/src/frameworks/nuxt/index.ts +++ b/src/frameworks/nuxt/index.ts @@ -10,6 +10,7 @@ import { getNuxtVersion } from "./utils"; export const name = "Nuxt"; export const support = SupportLevel.Experimental; export const type = FrameworkType.Toolchain; +export const supportedRange = "3"; import { nuxtConfigFilesExist } from "./utils"; import type { NuxtOptions } from "./interfaces"; @@ -27,16 +28,16 @@ export async function discover(dir: string) { const anyConfigFileExists = await nuxtConfigFilesExist(dir); - const nuxtVersion = getNuxtVersion(dir); - if (!anyConfigFileExists && !nuxtVersion) return; - if (nuxtVersion && lt(nuxtVersion, "3.0.0-0")) return; + const version = getNuxtVersion(dir); + if (!anyConfigFileExists && !version) return; + if (version && lt(version, "3.0.0-0")) return; const { dir: { public: publicDirectory }, ssr: mayWantBackend, } = await getConfig(dir); - return { publicDirectory, mayWantBackend }; + return { publicDirectory, mayWantBackend, version }; } export async function build(cwd: string) { diff --git a/src/frameworks/nuxt2/index.ts b/src/frameworks/nuxt2/index.ts index f31a99ba6d7..381ed59a59b 100644 --- a/src/frameworks/nuxt2/index.ts +++ b/src/frameworks/nuxt2/index.ts @@ -12,6 +12,7 @@ import { spawn } from "cross-spawn"; export const name = "Nuxt"; export const support = SupportLevel.Experimental; export const type = FrameworkType.MetaFramework; +export const supportedRange = "2"; async function getAndLoadNuxt(options: { rootDir: string; for: string }) { const nuxt = await relativeRequire(options.rootDir, "nuxt/dist/nuxt.js"); @@ -27,10 +28,10 @@ async function getAndLoadNuxt(options: { rootDir: string; for: string }) { */ export async function discover(rootDir: string) { if (!(await pathExists(join(rootDir, "package.json")))) return; - const nuxtVersion = getNuxtVersion(rootDir); - if (!nuxtVersion || (nuxtVersion && gte(nuxtVersion, "3.0.0-0"))) return; + const version = getNuxtVersion(rootDir); + if (!version || (version && gte(version, "3.0.0-0"))) return; const { app } = await getAndLoadNuxt({ rootDir, for: "build" }); - return { mayWantBackend: true, publicDirectory: app.options.dir.static }; + return { mayWantBackend: true, publicDirectory: app.options.dir.static, version }; } /** diff --git a/src/frameworks/sveltekit/index.ts b/src/frameworks/sveltekit/index.ts index 962a7842795..44c8af1c18a 100644 --- a/src/frameworks/sveltekit/index.ts +++ b/src/frameworks/sveltekit/index.ts @@ -11,7 +11,8 @@ export const name = "SvelteKit"; export const support = SupportLevel.Experimental; export const type = FrameworkType.MetaFramework; export const discover = viteDiscoverWithNpmDependency("@sveltejs/kit"); -export { getDevModeHandle } from "../vite"; + +export { getDevModeHandle, supportedRange } from "../vite"; export async function build(root: string) { const config = await getConfig(root); diff --git a/src/frameworks/utils.ts b/src/frameworks/utils.ts index 0aafbc845fa..a0ac6ff8dea 100644 --- a/src/frameworks/utils.ts +++ b/src/frameworks/utils.ts @@ -5,6 +5,7 @@ import { readFile } from "fs/promises"; import { IncomingMessage, request as httpRequest, ServerResponse, Agent } from "http"; import { sync as spawnSync } from "cross-spawn"; import * as clc from "colorette"; +import { satisfies as semverSatisfied } from "semver"; import { logger } from "../logger"; import { FirebaseError } from "../error"; @@ -360,21 +361,36 @@ export function relativeRequire(dir: string, mod: string) { } } -export function conjoinOptions( - opts: any[], - conjunction: string = "and", - separator: string = "," -): string | undefined { - if (!opts.length) return; - if (opts.length === 1) return opts[0].toString(); - if (opts.length === 2) return `${opts[0].toString()} ${conjunction} ${opts[1].toString()}`; - const lastElement = opts.slice(-1)[0].toString(); - const allButLast = opts.slice(0, -1).map((it) => it.toString()); +export function conjoinOptions(_opts: any[], conjunction = "and", separator = ","): string { + if (!_opts.length) return ""; + const opts: string[] = _opts.map((it) => it.toString().trim()); + if (opts.length === 1) return opts[0]; + if (opts.length === 2) return `${opts[0]} ${conjunction} ${opts[1]}`; + const lastElement = opts.slice(-1)[0]; + const allButLast = opts.slice(0, -1); return `${allButLast.join(`${separator} `)}${separator} ${conjunction} ${lastElement}`; } -export function frameworksCallToAction(message: string, docsUrl = DEFAULT_DOCS_URL, prefix = "") { - return `${prefix}${message} +export function frameworksCallToAction( + message: string, + docsUrl = DEFAULT_DOCS_URL, + prefix = "", + framework?: string, + version?: string, + supportedRange?: string, + vite = false +): string { + return `${prefix}${message}${ + framework && supportedRange && (!version || !semverSatisfied(version, supportedRange)) + ? clc.yellow( + `\n${prefix}The integration is known to work with ${ + vite ? "Vite" : framework + } version ${clc.italic( + conjoinOptions(supportedRange.split("||")) + )}. You may encounter errors.` + ) + : `` + } ${prefix}${clc.bold("Documentation:")} ${docsUrl} ${prefix}${clc.bold("File a bug:")} ${FILE_BUG_URL} diff --git a/src/frameworks/vite/index.ts b/src/frameworks/vite/index.ts index 3a16558c069..78075bf9a0f 100644 --- a/src/frameworks/vite/index.ts +++ b/src/frameworks/vite/index.ts @@ -16,6 +16,7 @@ import { export const name = "Vite"; export const support = SupportLevel.Experimental; export const type = FrameworkType.Toolchain; +export const supportedRange = "3 - 5"; export const DEFAULT_BUILD_SCRIPT = ["vite build", "tsc && vite build"]; @@ -32,10 +33,13 @@ export async function init(setup: any, config: any, baseTemplate: string = "vani { name: "TypeScript", value: `${baseTemplate}-ts` }, ], }); - execSync(`npm create vite@latest ${setup.hosting.source} --yes -- --template ${template}`, { - stdio: "inherit", - cwd: config.projectDir, - }); + execSync( + `npm create vite@"${supportedRange}" ${setup.hosting.source} --yes -- --template ${template}`, + { + stdio: "inherit", + cwd: config.projectDir, + } + ); execSync(`npm install`, { stdio: "inherit", cwd: join(config.projectDir, setup.hosting.source) }); } @@ -56,11 +60,21 @@ export async function discover(dir: string, plugin?: string, npmDependency?: str pathExists(join(dir, "vite.config.ts")), ]); const anyConfigFileExists = configFilesExist.some((it) => it); - if (!anyConfigFileExists && !findDependency("vite", { cwd: dir, depth, omitDev: false })) return; + const version: string | undefined = findDependency("vite", { + cwd: dir, + depth, + omitDev: false, + })?.version; + if (!anyConfigFileExists && !version) return; if (npmDependency && !additionalDep) return; const { appType, publicDir: publicDirectory, plugins } = await getConfig(dir); if (plugin && !plugins.find(({ name }) => name === plugin)) return; - return { mayWantBackend: appType !== "spa", publicDirectory }; + return { + mayWantBackend: appType !== "spa", + publicDirectory, + version, + vite: true, + }; } export async function build(root: string) { diff --git a/src/test/frameworks/angular/index.spec.ts b/src/test/frameworks/angular/index.spec.ts index 2a29539ea23..20721ce619a 100644 --- a/src/test/frameworks/angular/index.spec.ts +++ b/src/test/frameworks/angular/index.spec.ts @@ -23,6 +23,7 @@ describe("Angular", () => { expect(await discover(cwd)).to.deep.equal({ mayWantBackend: true, publicDirectory: join(cwd, "src", "assets"), + version: undefined, }); }); }); diff --git a/src/test/frameworks/astro/index.spec.ts b/src/test/frameworks/astro/index.spec.ts index 7682b4b9404..6f1d42a7d19 100644 --- a/src/test/frameworks/astro/index.spec.ts +++ b/src/test/frameworks/astro/index.spec.ts @@ -54,6 +54,7 @@ describe("Astro", () => { expect(await discover(cwd)).to.deep.equal({ mayWantBackend: false, publicDirectory: publicDir, + version: "2.2.2", }); }); @@ -84,6 +85,7 @@ describe("Astro", () => { expect(await discover(cwd)).to.deep.equal({ mayWantBackend: true, publicDirectory: publicDir, + version: "2.2.2", }); }); }); diff --git a/src/test/frameworks/nuxt/index.spec.ts b/src/test/frameworks/nuxt/index.spec.ts index d2a5e9f70b7..2ffabcf6a17 100644 --- a/src/test/frameworks/nuxt/index.spec.ts +++ b/src/test/frameworks/nuxt/index.spec.ts @@ -45,6 +45,7 @@ describe("Nuxt 2 utils", () => { expect(await discoverNuxt2(discoverNuxtDir)).to.deep.equal({ mayWantBackend: true, publicDirectory: "static", + version: "2.15.8", }); }); @@ -75,6 +76,7 @@ describe("Nuxt 2 utils", () => { expect(await discoverNuxt3(discoverNuxtDir)).to.deep.equal({ mayWantBackend: true, publicDirectory: "public", + version: "3.0.0", }); }); }); diff --git a/src/test/frameworks/utils.spec.ts b/src/test/frameworks/utils.spec.ts index 55c99df27c5..28882c44e01 100644 --- a/src/test/frameworks/utils.spec.ts +++ b/src/test/frameworks/utils.spec.ts @@ -102,8 +102,8 @@ describe("Frameworks utils", () => { const defaultSeparator = ","; const defaultConjunction = "and"; - it("should return undefined if there's no options", () => { - expect(conjoinOptions([])).to.be.undefined; + it("should return empty string if there's no options", () => { + expect(conjoinOptions([])).to.be.eql(""); }); it("should return option if there's only one", () => {