App Hosting JS SDK autoinit (#8483)

Adds a `postinstall` step to `@firebase/util` which hardcodes autoinit defaults into `@firebase/util/dist/postinstall.(m)js` for both CJS and ESM variants [go/firestack-autoinit](http://goto.google.com/firestack-autoinit). 

Hardcoding is important since environment variable substitution doesn't dive into a developer's `node_modules` folder. This gives us a framework agnostic way to inject configuration and is a more robust solution than the _experimental_ autoinit methods provided by Web Frameworks [go/firebase-api-client-autoinit](https://goto.google.com/firebase-api-client-autoinit). Once this lands, we'll backport to Hosting and Functions and aim to deprecate the other autoinit methods.
This commit is contained in:
James Daniels
2025-02-26 10:51:42 -05:00
committed by GitHub
parent 07d62febde
commit d0ef8d6e71
7 changed files with 239 additions and 11 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@firebase/util': minor
'firebase': minor
---
Add support for the `FIREBASE_WEBAPP_CONFIG` environment variable at install time.
+5 -2
View File
@@ -22,7 +22,8 @@
"./package.json": "./package.json"
},
"files": [
"dist"
"dist",
"postinstall.js"
],
"scripts": {
"lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'",
@@ -38,13 +39,15 @@
"test:node": "TS_NODE_CACHE=NO TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha test/**/*.test.* --config ../../config/mocharc.node.js",
"trusted-type-check": "tsec -p tsconfig.json --noEmit",
"api-report": "api-extractor run --local --verbose",
"typings:public": "node ../../scripts/build/use_typings.js ./dist/util-public.d.ts"
"typings:public": "node ../../scripts/build/use_typings.js ./dist/util-public.d.ts",
"postinstall": "node ./postinstall.js"
},
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
},
"devDependencies": {
"@rollup/plugin-replace": "6.0.2",
"rollup": "2.79.2",
"rollup-plugin-typescript2": "0.36.0",
"typescript": "5.5.4"
+153
View File
@@ -0,0 +1,153 @@
/**
* @license
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { writeFile, readFile } = require('node:fs/promises');
const { pathToFileURL } = require('node:url');
const { isAbsolute, join } = require('node:path');
const ENV_VARIABLE = 'FIREBASE_WEBAPP_CONFIG';
async function getPartialConfig() {
const envVariable = process.env[ENV_VARIABLE]?.trim();
if (!envVariable) {
return undefined;
}
// Like FIREBASE_CONFIG (admin autoinit) FIREBASE_WEBAPP_CONFIG can be
// either a JSON representation of FirebaseOptions or the path to a filename
if (envVariable.startsWith('{"')) {
try {
return JSON.parse(envVariable);
} catch (e) {
console.warn(
`JSON payload in \$${ENV_VARIABLE} could not be parsed, ignoring.\n`,
e
);
return undefined;
}
}
const fileURL = pathToFileURL(
isAbsolute(envVariable) ? envVariable : join(process.cwd(), envVariable)
);
try {
const fileContents = await readFile(fileURL, 'utf-8');
return JSON.parse(fileContents);
} catch (e) {
console.warn(
`Contents of "${envVariable}" could not be parsed, ignoring \$${ENV_VARIABLE}.\n`,
e
);
return undefined;
}
}
async function getFinalConfig(partialConfig) {
if (!partialConfig) {
return undefined;
}
// In Firebase App Hosting the config provided to the environment variable is up-to-date and
// "complete" we should not reach out to the webConfig endpoint to freshen it
if (process.env.X_GOOGLE_TARGET_PLATFORM === 'fah') {
return partialConfig;
}
const projectId = partialConfig.projectId || '-';
// If the projectId starts with demo- this is an demo project from the firebase emulators
// treat the config as whole
if (projectId.startsWith('demo-')) {
return partialConfig;
}
const appId = partialConfig.appId;
const apiKey = partialConfig.apiKey;
if (!appId || !apiKey) {
console.warn(
`Unable to fetch Firebase config, appId and apiKey are required, ignoring \$${ENV_VARIABLE}.`
);
return undefined;
}
const url = `https://firebase.googleapis.com/v1alpha/projects/${projectId}/apps/${appId}/webConfig`;
try {
const response = await fetch(url, {
headers: { 'x-goog-api-key': apiKey }
});
if (!response.ok) {
console.warn(
`Unable to fetch Firebase config, ignoring \$${ENV_VARIABLE}.`
);
console.warn(
`${url} returned ${response.statusText} (${response.status})`
);
try {
console.warn((await response.json()).error.message);
} catch (e) {}
return undefined;
}
const json = await response.json();
return { ...json, apiKey };
} catch (e) {
console.warn(
`Unable to fetch Firebase config, ignoring \$${ENV_VARIABLE}.\n`,
e
);
return undefined;
}
}
function handleUnexpectedError(e) {
console.warn(
`Unexpected error encountered in @firebase/util postinstall script, ignoring \$${ENV_VARIABLE}.`
);
console.warn(e);
process.exit(0);
}
getPartialConfig()
.catch(handleUnexpectedError)
.then(getFinalConfig)
.catch(handleUnexpectedError)
.then(async finalConfig => {
const defaults = finalConfig && {
config: finalConfig,
emulatorHosts: {
firestore: process.env.FIRESTORE_EMULATOR_HOST,
database: process.env.FIREBASE_DATABASE_EMULATOR_HOST,
storage: process.env.FIREBASE_STORAGE_EMULATOR_HOST,
auth: process.env.FIREBASE_AUTH_EMULATOR_HOST
}
};
await Promise.all([
writeFile(
join(__dirname, 'dist', 'postinstall.js'),
`'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.getDefaultsFromPostinstall = () => (${JSON.stringify(defaults)});`
),
writeFile(
join(__dirname, 'dist', 'postinstall.mjs'),
`const getDefaultsFromPostinstall = () => (${JSON.stringify(defaults)});
export { getDefaultsFromPostinstall };`
)
]);
process.exit(0);
})
.catch(handleUnexpectedError);
+41 -8
View File
@@ -16,16 +16,26 @@
*/
import typescriptPlugin from 'rollup-plugin-typescript2';
import replacePlugin from '@rollup/plugin-replace';
import typescript from 'typescript';
import pkg from './package.json';
import { emitModulePackageFile } from '../../scripts/build/rollup_emit_module_package_file';
const deps = Object.keys(
Object.assign({}, pkg.peerDependencies, pkg.dependencies)
);
const deps = [
...Object.keys(Object.assign({}, pkg.peerDependencies, pkg.dependencies)),
'./postinstall'
];
const buildPlugins = [typescriptPlugin({ typescript })];
function replaceSrcPostinstallWith(path) {
return replacePlugin({
'./src/postinstall': `'${path}'`,
delimiters: ["'", "'"],
preventAssignment: true
});
}
const browserBuilds = [
{
input: 'index.ts',
@@ -34,7 +44,7 @@ const browserBuilds = [
format: 'es',
sourcemap: true
},
plugins: buildPlugins,
plugins: [...buildPlugins, replaceSrcPostinstallWith('./postinstall.mjs')],
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
},
{
@@ -44,7 +54,7 @@ const browserBuilds = [
format: 'cjs',
sourcemap: true
},
plugins: buildPlugins,
plugins: [...buildPlugins, replaceSrcPostinstallWith('./postinstall.js')],
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
}
];
@@ -57,7 +67,7 @@ const nodeBuilds = [
format: 'cjs',
sourcemap: true
},
plugins: buildPlugins,
plugins: [...buildPlugins, replaceSrcPostinstallWith('./postinstall.js')],
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
},
{
@@ -67,9 +77,32 @@ const nodeBuilds = [
format: 'es',
sourcemap: true
},
plugins: [...buildPlugins, emitModulePackageFile()],
plugins: [
...buildPlugins,
emitModulePackageFile(),
replaceSrcPostinstallWith('../postinstall.mjs')
],
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
}
];
export default [...browserBuilds, ...nodeBuilds];
const autoinitBuild = [
{
input: './src/postinstall.ts',
output: {
file: './dist/postinstall.js',
format: 'cjs'
},
plugins: buildPlugins
},
{
input: './src/postinstall.ts',
output: {
file: './dist/postinstall.mjs',
format: 'es'
},
plugins: buildPlugins
}
];
export default [...browserBuilds, ...nodeBuilds, ...autoinitBuild];
+2
View File
@@ -17,6 +17,7 @@
import { base64Decode } from './crypt';
import { getGlobal } from './global';
import { getDefaultsFromPostinstall } from './postinstall';
/**
* Keys for experimental properties on the `FirebaseDefaults` object.
@@ -100,6 +101,7 @@ const getDefaultsFromCookie = (): FirebaseDefaults | undefined => {
export const getDefaults = (): FirebaseDefaults | undefined => {
try {
return (
getDefaultsFromPostinstall() ||
getDefaultsFromGlobal() ||
getDefaultsFromEnvVariable() ||
getDefaultsFromCookie()
+23
View File
@@ -0,0 +1,23 @@
/**
* @license
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { FirebaseDefaults } from './defaults';
// This value is retrieved and hardcoded by the NPM postinstall script
export const getDefaultsFromPostinstall: () =>
| FirebaseDefaults
| undefined = () => undefined;
+9 -1
View File
@@ -2633,6 +2633,14 @@
is-module "^1.0.0"
resolve "^1.22.1"
"@rollup/[email protected]":
version "6.0.2"
resolved "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.2.tgz#2f565d312d681e4570ff376c55c5c08eb6f1908d"
integrity sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ==
dependencies:
"@rollup/pluginutils" "^5.0.1"
magic-string "^0.30.3"
"@rollup/[email protected]":
version "2.1.0"
resolved "https://registry.npmjs.org/@rollup/plugin-strip/-/plugin-strip-2.1.0.tgz#04c2d2ccfb2c6b192bb70447fbf26e336379a333"
@@ -11233,7 +11241,7 @@ magic-string@^0.25.2, magic-string@^0.25.7:
dependencies:
sourcemap-codec "^1.4.8"
magic-string@^0.30.2, magic-string@~0.30.0:
magic-string@^0.30.2, magic-string@^0.30.3, magic-string@~0.30.0:
version "0.30.17"
resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==