Skip to content

Commit 777f465

Browse files
authored
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.
1 parent 2d74e5b commit 777f465

File tree

7 files changed

+239
-11
lines changed

7 files changed

+239
-11
lines changed

.changeset/tame-parrots-tie.md

Lines changed: 6 additions & 0 deletions
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

Lines changed: 5 additions & 2 deletions
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

Lines changed: 153 additions & 0 deletions
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

Lines changed: 41 additions & 8 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 23 additions & 0 deletions
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

Lines changed: 9 additions & 1 deletion
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy