Skip to content

Commit eb8e4aa

Browse files
Merge main into release
2 parents 6670d9c + 777f465 commit eb8e4aa

File tree

7 files changed

+239
-11
lines changed

7 files changed

+239
-11
lines changed

.changeset/tame-parrots-tie.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/util': minor
3+
'firebase': minor
4+
---
5+
6+
Add support for the `FIREBASE_WEBAPP_CONFIG` environment variable at install time.

packages/util/package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"./package.json": "./package.json"
2323
},
2424
"files": [
25-
"dist"
25+
"dist",
26+
"postinstall.js"
2627
],
2728
"scripts": {
2829
"lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'",
@@ -38,13 +39,15 @@
3839
"test:node": "TS_NODE_CACHE=NO TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha test/**/*.test.* --config ../../config/mocharc.node.js",
3940
"trusted-type-check": "tsec -p tsconfig.json --noEmit",
4041
"api-report": "api-extractor run --local --verbose",
41-
"typings:public": "node ../../scripts/build/use_typings.js ./dist/util-public.d.ts"
42+
"typings:public": "node ../../scripts/build/use_typings.js ./dist/util-public.d.ts",
43+
"postinstall": "node ./postinstall.js"
4244
},
4345
"license": "Apache-2.0",
4446
"dependencies": {
4547
"tslib": "^2.1.0"
4648
},
4749
"devDependencies": {
50+
"@rollup/plugin-replace": "6.0.2",
4851
"rollup": "2.79.2",
4952
"rollup-plugin-typescript2": "0.36.0",
5053
"typescript": "5.5.4"

packages/util/postinstall.js

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
const { writeFile, readFile } = require('node:fs/promises');
19+
const { pathToFileURL } = require('node:url');
20+
const { isAbsolute, join } = require('node:path');
21+
22+
const ENV_VARIABLE = 'FIREBASE_WEBAPP_CONFIG';
23+
24+
async function getPartialConfig() {
25+
const envVariable = process.env[ENV_VARIABLE]?.trim();
26+
27+
if (!envVariable) {
28+
return undefined;
29+
}
30+
31+
// Like FIREBASE_CONFIG (admin autoinit) FIREBASE_WEBAPP_CONFIG can be
32+
// either a JSON representation of FirebaseOptions or the path to a filename
33+
if (envVariable.startsWith('{"')) {
34+
try {
35+
return JSON.parse(envVariable);
36+
} catch (e) {
37+
console.warn(
38+
`JSON payload in \$${ENV_VARIABLE} could not be parsed, ignoring.\n`,
39+
e
40+
);
41+
return undefined;
42+
}
43+
}
44+
45+
const fileURL = pathToFileURL(
46+
isAbsolute(envVariable) ? envVariable : join(process.cwd(), envVariable)
47+
);
48+
49+
try {
50+
const fileContents = await readFile(fileURL, 'utf-8');
51+
return JSON.parse(fileContents);
52+
} catch (e) {
53+
console.warn(
54+
`Contents of "${envVariable}" could not be parsed, ignoring \$${ENV_VARIABLE}.\n`,
55+
e
56+
);
57+
return undefined;
58+
}
59+
}
60+
61+
async function getFinalConfig(partialConfig) {
62+
if (!partialConfig) {
63+
return undefined;
64+
}
65+
// In Firebase App Hosting the config provided to the environment variable is up-to-date and
66+
// "complete" we should not reach out to the webConfig endpoint to freshen it
67+
if (process.env.X_GOOGLE_TARGET_PLATFORM === 'fah') {
68+
return partialConfig;
69+
}
70+
const projectId = partialConfig.projectId || '-';
71+
// If the projectId starts with demo- this is an demo project from the firebase emulators
72+
// treat the config as whole
73+
if (projectId.startsWith('demo-')) {
74+
return partialConfig;
75+
}
76+
const appId = partialConfig.appId;
77+
const apiKey = partialConfig.apiKey;
78+
if (!appId || !apiKey) {
79+
console.warn(
80+
`Unable to fetch Firebase config, appId and apiKey are required, ignoring \$${ENV_VARIABLE}.`
81+
);
82+
return undefined;
83+
}
84+
85+
const url = `https://firebase.googleapis.com/v1alpha/projects/${projectId}/apps/${appId}/webConfig`;
86+
87+
try {
88+
const response = await fetch(url, {
89+
headers: { 'x-goog-api-key': apiKey }
90+
});
91+
if (!response.ok) {
92+
console.warn(
93+
`Unable to fetch Firebase config, ignoring \$${ENV_VARIABLE}.`
94+
);
95+
console.warn(
96+
`${url} returned ${response.statusText} (${response.status})`
97+
);
98+
try {
99+
console.warn((await response.json()).error.message);
100+
} catch (e) {}
101+
return undefined;
102+
}
103+
const json = await response.json();
104+
return { ...json, apiKey };
105+
} catch (e) {
106+
console.warn(
107+
`Unable to fetch Firebase config, ignoring \$${ENV_VARIABLE}.\n`,
108+
e
109+
);
110+
return undefined;
111+
}
112+
}
113+
114+
function handleUnexpectedError(e) {
115+
console.warn(
116+
`Unexpected error encountered in @firebase/util postinstall script, ignoring \$${ENV_VARIABLE}.`
117+
);
118+
console.warn(e);
119+
process.exit(0);
120+
}
121+
122+
getPartialConfig()
123+
.catch(handleUnexpectedError)
124+
.then(getFinalConfig)
125+
.catch(handleUnexpectedError)
126+
.then(async finalConfig => {
127+
const defaults = finalConfig && {
128+
config: finalConfig,
129+
emulatorHosts: {
130+
firestore: process.env.FIRESTORE_EMULATOR_HOST,
131+
database: process.env.FIREBASE_DATABASE_EMULATOR_HOST,
132+
storage: process.env.FIREBASE_STORAGE_EMULATOR_HOST,
133+
auth: process.env.FIREBASE_AUTH_EMULATOR_HOST
134+
}
135+
};
136+
137+
await Promise.all([
138+
writeFile(
139+
join(__dirname, 'dist', 'postinstall.js'),
140+
`'use strict';
141+
Object.defineProperty(exports, '__esModule', { value: true });
142+
exports.getDefaultsFromPostinstall = () => (${JSON.stringify(defaults)});`
143+
),
144+
writeFile(
145+
join(__dirname, 'dist', 'postinstall.mjs'),
146+
`const getDefaultsFromPostinstall = () => (${JSON.stringify(defaults)});
147+
export { getDefaultsFromPostinstall };`
148+
)
149+
]);
150+
151+
process.exit(0);
152+
})
153+
.catch(handleUnexpectedError);

packages/util/rollup.config.js

+41-8
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,26 @@
1616
*/
1717

1818
import typescriptPlugin from 'rollup-plugin-typescript2';
19+
import replacePlugin from '@rollup/plugin-replace';
1920
import typescript from 'typescript';
2021
import pkg from './package.json';
2122
import { emitModulePackageFile } from '../../scripts/build/rollup_emit_module_package_file';
2223

23-
const deps = Object.keys(
24-
Object.assign({}, pkg.peerDependencies, pkg.dependencies)
25-
);
24+
const deps = [
25+
...Object.keys(Object.assign({}, pkg.peerDependencies, pkg.dependencies)),
26+
'./postinstall'
27+
];
2628

2729
const buildPlugins = [typescriptPlugin({ typescript })];
2830

31+
function replaceSrcPostinstallWith(path) {
32+
return replacePlugin({
33+
'./src/postinstall': `'${path}'`,
34+
delimiters: ["'", "'"],
35+
preventAssignment: true
36+
});
37+
}
38+
2939
const browserBuilds = [
3040
{
3141
input: 'index.ts',
@@ -34,7 +44,7 @@ const browserBuilds = [
3444
format: 'es',
3545
sourcemap: true
3646
},
37-
plugins: buildPlugins,
47+
plugins: [...buildPlugins, replaceSrcPostinstallWith('./postinstall.mjs')],
3848
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
3949
},
4050
{
@@ -44,7 +54,7 @@ const browserBuilds = [
4454
format: 'cjs',
4555
sourcemap: true
4656
},
47-
plugins: buildPlugins,
57+
plugins: [...buildPlugins, replaceSrcPostinstallWith('./postinstall.js')],
4858
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
4959
}
5060
];
@@ -57,7 +67,7 @@ const nodeBuilds = [
5767
format: 'cjs',
5868
sourcemap: true
5969
},
60-
plugins: buildPlugins,
70+
plugins: [...buildPlugins, replaceSrcPostinstallWith('./postinstall.js')],
6171
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
6272
},
6373
{
@@ -67,9 +77,32 @@ const nodeBuilds = [
6777
format: 'es',
6878
sourcemap: true
6979
},
70-
plugins: [...buildPlugins, emitModulePackageFile()],
80+
plugins: [
81+
...buildPlugins,
82+
emitModulePackageFile(),
83+
replaceSrcPostinstallWith('../postinstall.mjs')
84+
],
7185
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
7286
}
7387
];
7488

75-
export default [...browserBuilds, ...nodeBuilds];
89+
const autoinitBuild = [
90+
{
91+
input: './src/postinstall.ts',
92+
output: {
93+
file: './dist/postinstall.js',
94+
format: 'cjs'
95+
},
96+
plugins: buildPlugins
97+
},
98+
{
99+
input: './src/postinstall.ts',
100+
output: {
101+
file: './dist/postinstall.mjs',
102+
format: 'es'
103+
},
104+
plugins: buildPlugins
105+
}
106+
];
107+
108+
export default [...browserBuilds, ...nodeBuilds, ...autoinitBuild];

packages/util/src/defaults.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import { base64Decode } from './crypt';
1919
import { getGlobal } from './global';
20+
import { getDefaultsFromPostinstall } from './postinstall';
2021

2122
/**
2223
* Keys for experimental properties on the `FirebaseDefaults` object.
@@ -100,6 +101,7 @@ const getDefaultsFromCookie = (): FirebaseDefaults | undefined => {
100101
export const getDefaults = (): FirebaseDefaults | undefined => {
101102
try {
102103
return (
104+
getDefaultsFromPostinstall() ||
103105
getDefaultsFromGlobal() ||
104106
getDefaultsFromEnvVariable() ||
105107
getDefaultsFromCookie()

packages/util/src/postinstall.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import type { FirebaseDefaults } from './defaults';
19+
20+
// This value is retrieved and hardcoded by the NPM postinstall script
21+
export const getDefaultsFromPostinstall: () =>
22+
| FirebaseDefaults
23+
| undefined = () => undefined;

yarn.lock

+9-1
Original file line numberDiff line numberDiff line change
@@ -2633,6 +2633,14 @@
26332633
is-module "^1.0.0"
26342634
resolve "^1.22.1"
26352635

2636+
"@rollup/plugin-replace@6.0.2":
2637+
version "6.0.2"
2638+
resolved "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.2.tgz#2f565d312d681e4570ff376c55c5c08eb6f1908d"
2639+
integrity sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ==
2640+
dependencies:
2641+
"@rollup/pluginutils" "^5.0.1"
2642+
magic-string "^0.30.3"
2643+
26362644
"@rollup/plugin-strip@2.1.0":
26372645
version "2.1.0"
26382646
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:
1123311241
dependencies:
1123411242
sourcemap-codec "^1.4.8"
1123511243

11236-
magic-string@^0.30.2, magic-string@~0.30.0:
11244+
magic-string@^0.30.2, magic-string@^0.30.3, magic-string@~0.30.0:
1123711245
version "0.30.17"
1123811246
resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
1123911247
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==

0 commit comments

Comments
 (0)