diff --git a/.gitignore b/.gitignore index 312dd73e52..92973ce2bf 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ staticwebapp.config.json .eslintcache playground/firebase.json .zeabur +.apphosting test/fixture/functions .data diff --git a/package.json b/package.json index 1be45a81f9..6748b3dafe 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "semver": "^7.6.3", "serve-placeholder": "^2.0.2", "serve-static": "^1.16.2", - "std-env": "^3.7.0", + "std-env": "^3.8.0", "ufo": "^1.5.4", "uncrypto": "^0.1.3", "unctx": "^2.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6333edf52..1db25efbf3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,8 +190,8 @@ importers: specifier: ^1.16.2 version: 1.16.2 std-env: - specifier: ^3.7.0 - version: 3.7.0 + specifier: ^3.8.0 + version: 3.8.0 ufo: specifier: ^1.5.4 version: 1.5.4 @@ -5243,8 +5243,8 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} - std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} stoppable@1.1.0: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} @@ -7754,7 +7754,7 @@ snapshots: istanbul-reports: 3.1.7 magic-string: 0.30.12 magicast: 0.3.5 - std-env: 3.7.0 + std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 1.2.0 vitest: 2.1.4(@edge-runtime/vm@4.0.3)(@types/node@22.8.6)(terser@5.36.0) @@ -8281,7 +8281,7 @@ snapshots: pkg-types: 1.2.1 scule: 1.3.0 semver: 7.6.3 - std-env: 3.7.0 + std-env: 3.8.0 yaml: 2.6.0 transitivePeerDependencies: - magicast @@ -10090,7 +10090,7 @@ snapshots: mlly: 1.7.2 node-forge: 1.3.1 pathe: 1.1.2 - std-env: 3.7.0 + std-env: 3.8.0 ufo: 1.5.4 untun: 0.1.3 uqr: 0.1.2 @@ -11684,7 +11684,7 @@ snapshots: statuses@2.0.1: {} - std-env@3.7.0: {} + std-env@3.8.0: {} stoppable@1.1.0: {} @@ -12299,7 +12299,7 @@ snapshots: expect-type: 1.1.0 magic-string: 0.30.12 pathe: 1.1.2 - std-env: 3.7.0 + std-env: 3.8.0 tinybench: 2.9.0 tinyexec: 0.3.1 tinypool: 1.0.1 diff --git a/src/presets/_types.gen.ts b/src/presets/_types.gen.ts index 48a4a0ca68..a59068ffc2 100644 --- a/src/presets/_types.gen.ts +++ b/src/presets/_types.gen.ts @@ -20,6 +20,6 @@ export interface PresetOptions { export const presetsWithConfig = ["awsAmplify","awsLambda","azure","cloudflare","firebase","netlify","vercel"] as const; -export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure" | "azure-functions" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-module-legacy" | "cloudflare-pages" | "cloudflare-pages-static" | "cloudflare-worker" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "edgio" | "firebase" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis" | "iis-handler" | "iis-node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlify-edge" | "netlify-legacy" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-listener" | "node-server" | "platform-sh" | "render-com" | "service-worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static"; +export type PresetName = "alwaysdata" | "aws-amplify" | "aws-lambda" | "azure" | "azure-functions" | "azure-swa" | "base-worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-durable" | "cloudflare-module" | "cloudflare-module-legacy" | "cloudflare-pages" | "cloudflare-pages-static" | "cloudflare-worker" | "deno" | "deno-deploy" | "deno-server" | "digital-ocean" | "edgio" | "firebase" | "firebase-app-hosting" | "flight-control" | "genezio" | "github-pages" | "gitlab-pages" | "heroku" | "iis" | "iis-handler" | "iis-node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlify-edge" | "netlify-legacy" | "netlify-static" | "nitro-dev" | "nitro-prerender" | "node" | "node-cluster" | "node-listener" | "node-server" | "platform-sh" | "render-com" | "service-worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercel-static" | "winterjs" | "zeabur" | "zeabur-static" | "zerops" | "zerops-static"; -export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "azure" | "azure-functions" | "azureFunctions" | "azure_functions" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-module-legacy" | "cloudflareModuleLegacy" | "cloudflare_module_legacy" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "cloudflare-worker" | "cloudflareWorker" | "cloudflare_worker" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "edgio" | "firebase" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlifyBuilder" | "netlify_builder" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-legacy" | "netlifyLegacy" | "netlify_legacy" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-listener" | "nodeListener" | "node_listener" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "service-worker" | "serviceWorker" | "service_worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercelEdge" | "vercel_edge" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {}); +export type PresetNameInput = "alwaysdata" | "aws-amplify" | "awsAmplify" | "aws_amplify" | "aws-lambda" | "awsLambda" | "aws_lambda" | "azure" | "azure-functions" | "azureFunctions" | "azure_functions" | "azure-swa" | "azureSwa" | "azure_swa" | "base-worker" | "baseWorker" | "base_worker" | "bun" | "cleavr" | "cli" | "cloudflare" | "cloudflare-durable" | "cloudflareDurable" | "cloudflare_durable" | "cloudflare-module" | "cloudflareModule" | "cloudflare_module" | "cloudflare-module-legacy" | "cloudflareModuleLegacy" | "cloudflare_module_legacy" | "cloudflare-pages" | "cloudflarePages" | "cloudflare_pages" | "cloudflare-pages-static" | "cloudflarePagesStatic" | "cloudflare_pages_static" | "cloudflare-worker" | "cloudflareWorker" | "cloudflare_worker" | "deno" | "deno-deploy" | "denoDeploy" | "deno_deploy" | "deno-server" | "denoServer" | "deno_server" | "digital-ocean" | "digitalOcean" | "digital_ocean" | "edgio" | "firebase" | "firebase-app-hosting" | "firebaseAppHosting" | "firebase_app_hosting" | "flight-control" | "flightControl" | "flight_control" | "genezio" | "github-pages" | "githubPages" | "github_pages" | "gitlab-pages" | "gitlabPages" | "gitlab_pages" | "heroku" | "iis" | "iis-handler" | "iisHandler" | "iis_handler" | "iis-node" | "iisNode" | "iis_node" | "koyeb" | "layer0" | "netlify" | "netlify-builder" | "netlifyBuilder" | "netlify_builder" | "netlify-edge" | "netlifyEdge" | "netlify_edge" | "netlify-legacy" | "netlifyLegacy" | "netlify_legacy" | "netlify-static" | "netlifyStatic" | "netlify_static" | "nitro-dev" | "nitroDev" | "nitro_dev" | "nitro-prerender" | "nitroPrerender" | "nitro_prerender" | "node" | "node-cluster" | "nodeCluster" | "node_cluster" | "node-listener" | "nodeListener" | "node_listener" | "node-server" | "nodeServer" | "node_server" | "platform-sh" | "platformSh" | "platform_sh" | "render-com" | "renderCom" | "render_com" | "service-worker" | "serviceWorker" | "service_worker" | "static" | "stormkit" | "vercel" | "vercel-edge" | "vercelEdge" | "vercel_edge" | "vercel-static" | "vercelStatic" | "vercel_static" | "winterjs" | "zeabur" | "zeabur-static" | "zeaburStatic" | "zeabur_static" | "zerops" | "zerops-static" | "zeropsStatic" | "zerops_static" | (string & {}); diff --git a/src/presets/firebase/preset.ts b/src/presets/firebase/preset.ts index dbaf703804..d13c16e370 100644 --- a/src/presets/firebase/preset.ts +++ b/src/presets/firebase/preset.ts @@ -1,8 +1,11 @@ -import { defineNitroPreset } from "nitropack/kit"; -import { basename } from "pathe"; +import { defineNitroPreset, writeFile } from "nitropack/kit"; +import { version as nitroVersion } from "nitropack/meta"; +import { basename, join } from "pathe"; import type { Plugin } from "rollup"; import { genSafeVariableName } from "knitwork"; +import { stringifyYAML } from "confbox"; import { updatePackageJSON, writeFirebaseConfig } from "./utils"; +import type { AppHostingOptions, AppHostingOutputBundleConfig } from "./types"; export type { FirebaseOptions as PresetOptions } from "./types"; @@ -68,4 +71,37 @@ const firebase = defineNitroPreset( } ); -export default [firebase] as const; +const firebaseAppHosting = defineNitroPreset( + { + extends: "node-server", + serveStatic: true, + hooks: { + async compiled(nitro) { + await writeFile( + join(nitro.options.rootDir, ".apphosting/bundle.yaml"), + stringifyYAML({ + version: "v1", + runConfig: { + runCommand: "node .output/server/index.mjs", + ...(nitro.options.firebase as AppHostingOptions)?.appHosting, + }, + metadata: { + framework: nitro.options.framework.name || "nitropack", + frameworkVersion: nitro.options.framework.version || "2.x", + adapterPackageName: "nitropack", + adapterVersion: nitroVersion, + }, + } satisfies AppHostingOutputBundleConfig), + true + ); + }, + }, + }, + { + name: "firebase-app-hosting" as const, + stdName: "firebase_app_hosting", + url: import.meta.url, + } +); + +export default [firebase, firebaseAppHosting] as const; diff --git a/src/presets/firebase/types.ts b/src/presets/firebase/types.ts index fdce06a754..07938b9224 100644 --- a/src/presets/firebase/types.ts +++ b/src/presets/firebase/types.ts @@ -1,7 +1,12 @@ import type { RuntimeOptions, region } from "firebase-functions"; import type { HttpsOptions } from "firebase-functions/v2/https"; -export type FirebaseOptions = FirebaseOptionsGen1 | FirebaseOptionsGen2; +export type FirebaseOptions = + | FirebaseOptionsGen1 + | FirebaseOptionsGen2 + | AppHostingOptions; + +// ---- Firebase Functions ---- export interface FirebaseOptionsBase { gen: 1 | 2; @@ -40,3 +45,52 @@ export interface FirebaseOptionsGen2 extends FirebaseOptionsBase { */ httpsOptions?: HttpsOptions; } + +// ---- Firebase App Hosting ---- + +export interface AppHostingOptions { + appHosting: Partial; +} + +// Source: https://github.com/FirebaseExtended/firebase-framework-tools/blob/main/packages/%40apphosting/common/src/index.ts +export interface AppHostingOutputBundleConfig { + version: "v1"; + + // Fields needed to configure the App Hosting server + runConfig: { + /** Command to start the server (e.g. "node dist/index.js"). Assume this command is run from the root dir of the workspace. */ + runCommand: string; + /** Environment variables set when the app is run. */ + environmentVariables?: Array<{ + /** Name of the variable. */ + variable: string; + /** Value associated with the variable. */ + value: string; + /** Where the variable will be available, for now only RUNTIME is supported. */ + availability: "RUNTIME"[]; + }>; + // See https://firebase.google.com/docs/reference/apphosting/rest/v1beta/projects.locations.backends.builds#runconfig for documentation on the next fields + /** The maximum number of concurrent requests that each server instance can receive. */ + concurrency?: number; + /** The number of CPUs used in a single server instance. */ + cpu?: number; + /** The amount of memory available for a server instance. */ + memoryMiB?: number; + /** The limit on the minimum number of function instances that may coexist at a given time. */ + minInstances?: number; + /** The limit on the maximum number of function instances that may coexist at a given time. */ + maxInstances?: number; + }; + + // Additional fields needed for identifying the framework and adapter being used + metadata: { + // Name of the adapter (this should be the official package name) e.g. "@apphosting/adapter-nextjs" + adapterPackageName: string; + // Version of the adapter, e.g. "18.0.1" + adapterVersion: string; + // Name of the framework that is being supported, e.g. "angular" + framework: string; + // Version of the framework that is being supported, e.g. "18.0.1" + frameworkVersion?: string; + }; +}