firebase-tools 15.15.0 → 15.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agentSkills.js +3 -2
- package/lib/apphosting/backend.js +7 -19
- package/lib/apphosting/localbuilds.js +32 -4
- package/lib/apphosting/prompts.js +32 -11
- package/lib/apphosting/secrets/index.js +43 -0
- package/lib/apphosting/yaml.js +2 -4
- package/lib/bin/firebase.js +10 -0
- package/lib/bin/mcp.js +12 -1
- package/lib/commands/apphosting-backends-create.js +4 -10
- package/lib/commands/dataconnect-sdk-generate.js +2 -2
- package/lib/commands/deploy.js +6 -1
- package/lib/config.js +1 -0
- package/lib/dataconnect/build.js +6 -0
- package/lib/dataconnect/names.js +1 -1
- package/lib/deploy/apphosting/prepare.js +5 -2
- package/lib/deploy/firestore/prepare.js +1 -1
- package/lib/deploy/functions/backend.js +22 -14
- package/lib/deploy/functions/prepare.js +8 -2
- package/lib/deploy/functions/release/fabricator.js +26 -14
- package/lib/deploy/functions/runtimes/python/index.js +3 -0
- package/lib/deploy/functions/runtimes/supported/types.js +6 -0
- package/lib/deploy/hosting/prepare.js +1 -0
- package/lib/emulator/adminSdkConfig.js +2 -1
- package/lib/emulator/apphosting/serve.js +2 -39
- package/lib/emulator/commandUtils.js +4 -1
- package/lib/emulator/controller.js +4 -3
- package/lib/emulator/dataconnectEmulator.js +7 -4
- package/lib/emulator/downloadableEmulatorInfo.json +30 -30
- package/lib/emulator/functionsEmulator.js +12 -6
- package/lib/emulator/functionsEmulatorRuntime.js +12 -5
- package/lib/emulator/functionsRuntimeWorker.js +6 -3
- package/lib/experiments.js +6 -1
- package/lib/frameworks/angular/index.js +6 -1
- package/lib/gcp/rules.js +8 -4
- package/lib/init/features/agentSkills.js +2 -2
- package/lib/init/features/apphosting.js +2 -8
- package/lib/init/features/dataconnect/index.js +26 -15
- package/lib/init/features/dataconnect/sdk.js +8 -3
- package/lib/init/features/firestore/rules.js +2 -1
- package/lib/init/features/functions/dart.js +2 -0
- package/lib/init/features/storage/rules.js +2 -1
- package/lib/mcp/apps/update_environment/mcp-app.js +138 -0
- package/lib/mcp/index.js +57 -2
- package/lib/mcp/resources/index.js +2 -0
- package/lib/mcp/resources/update_environment_ui.js +32 -0
- package/lib/mcp/tools/core/get_security_rules.js +2 -1
- package/lib/mcp/util.js +19 -0
- package/lib/rulesDeploy.js +35 -16
- package/lib/tsconfig.compile.tsbuildinfo +1 -1
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/schema/firebase-config.json +4 -2
- package/templates/init/functions/dart/analysis_options.yaml +30 -0
- package/templates/init/functions/dart/pubspec.yaml +1 -0
package/lib/agentSkills.js
CHANGED
|
@@ -6,10 +6,11 @@ const child_process_1 = require("child_process");
|
|
|
6
6
|
const utils = require("./utils");
|
|
7
7
|
const prompt = require("./prompt");
|
|
8
8
|
const error_1 = require("./error");
|
|
9
|
-
async function promptForAgentSkills() {
|
|
9
|
+
async function promptForAgentSkills(options) {
|
|
10
10
|
return prompt.confirm({
|
|
11
11
|
message: "Would you like to install agent skills for Firebase?",
|
|
12
|
-
default:
|
|
12
|
+
default: !options?.nonInteractive,
|
|
13
|
+
nonInteractive: options?.nonInteractive,
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
16
|
async function installAgentSkills(options) {
|
|
@@ -66,7 +66,7 @@ async function awaitTlsReady(url) {
|
|
|
66
66
|
}
|
|
67
67
|
} while (!ready);
|
|
68
68
|
}
|
|
69
|
-
async function doSetup(projectId, nonInteractive, webAppName, backendId, serviceAccount, primaryRegion, rootDir, runtime
|
|
69
|
+
async function doSetup(projectId, nonInteractive, webAppName, backendId, serviceAccount, primaryRegion, rootDir, runtime) {
|
|
70
70
|
await ensureRequiredApisEnabled(projectId);
|
|
71
71
|
await ensureAppHostingComputeServiceAccount(projectId, serviceAccount ? serviceAccount : null);
|
|
72
72
|
let location = primaryRegion;
|
|
@@ -99,25 +99,13 @@ async function doSetup(projectId, nonInteractive, webAppName, backendId, service
|
|
|
99
99
|
if (!location || !backendId) {
|
|
100
100
|
throw new error_1.FirebaseError("Internal error: location or backendId is not defined.");
|
|
101
101
|
}
|
|
102
|
-
|
|
103
|
-
if (nonInteractive) {
|
|
104
|
-
runtime = prompts_1.DEFAULT_RUNTIME;
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
runtime = await (0, prompts_1.promptRuntime)(projectId, location);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (automaticBaseImageUpdatesDisabled === undefined && (0, experiments_1.isEnabled)("abiu")) {
|
|
111
|
-
if (!nonInteractive) {
|
|
112
|
-
automaticBaseImageUpdatesDisabled = !(await (0, prompts_1.promptAutomaticBaseImageUpdates)());
|
|
113
|
-
}
|
|
114
|
-
}
|
|
102
|
+
runtime = await (0, prompts_1.resolveRuntime)(projectId, location, nonInteractive, runtime);
|
|
115
103
|
const webApp = await app_1.webApps.getOrCreateWebApp(projectId, webAppName ? webAppName : null, backendId);
|
|
116
104
|
if (!webApp) {
|
|
117
105
|
(0, utils_1.logWarning)(`Firebase web app not set`);
|
|
118
106
|
}
|
|
119
107
|
const createBackendSpinner = ora("Creating your new backend...").start();
|
|
120
|
-
const backend = await createBackend(projectId, location, backendId, serviceAccount ? serviceAccount : null, gitRepositoryLink, webApp?.id, rootDir, runtime
|
|
108
|
+
const backend = await createBackend(projectId, location, backendId, serviceAccount ? serviceAccount : null, gitRepositoryLink, webApp?.id, rootDir, runtime);
|
|
121
109
|
createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`);
|
|
122
110
|
if (nonInteractive) {
|
|
123
111
|
return;
|
|
@@ -158,7 +146,7 @@ async function doSetup(projectId, nonInteractive, webAppName, backendId, service
|
|
|
158
146
|
}
|
|
159
147
|
(0, utils_1.logSuccess)(`Your backend is now deployed at:\n\thttps://${backend.uri}`);
|
|
160
148
|
}
|
|
161
|
-
async function doSetupSourceDeploy(projectId, backendId) {
|
|
149
|
+
async function doSetupSourceDeploy(projectId, backendId, nonInteractive, rootDir = "/") {
|
|
162
150
|
const location = await promptLocation(projectId, "Select a primary region to host your backend:\n");
|
|
163
151
|
const webAppSpinner = ora("Creating a new web app...\n").start();
|
|
164
152
|
const webApp = await app_1.webApps.getOrCreateWebApp(projectId, null, backendId);
|
|
@@ -166,8 +154,9 @@ async function doSetupSourceDeploy(projectId, backendId) {
|
|
|
166
154
|
(0, utils_1.logWarning)(`Firebase web app not set`);
|
|
167
155
|
}
|
|
168
156
|
webAppSpinner.stop();
|
|
157
|
+
const runtime = await (0, prompts_1.resolveRuntime)(projectId, location, nonInteractive);
|
|
169
158
|
const createBackendSpinner = ora("Creating your new backend...").start();
|
|
170
|
-
const backend = await createBackend(projectId, location, backendId, null, undefined, webApp?.id);
|
|
159
|
+
const backend = await createBackend(projectId, location, backendId, null, undefined, webApp?.id, rootDir, runtime);
|
|
171
160
|
createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`);
|
|
172
161
|
return {
|
|
173
162
|
backend,
|
|
@@ -242,7 +231,7 @@ async function promptNewBackendId(projectId, location) {
|
|
|
242
231
|
function defaultComputeServiceAccountEmail(projectId) {
|
|
243
232
|
return `${DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`;
|
|
244
233
|
}
|
|
245
|
-
async function createBackend(projectId, location, backendId, serviceAccount, repository, webAppId, rootDir = "/", runtime
|
|
234
|
+
async function createBackend(projectId, location, backendId, serviceAccount, repository, webAppId, rootDir = "/", runtime) {
|
|
246
235
|
const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId);
|
|
247
236
|
const backendReqBody = {
|
|
248
237
|
servingLocality: "GLOBAL_ACCESS",
|
|
@@ -258,7 +247,6 @@ async function createBackend(projectId, location, backendId, serviceAccount, rep
|
|
|
258
247
|
};
|
|
259
248
|
if ((0, experiments_1.isEnabled)("abiu")) {
|
|
260
249
|
backendReqBody.runtime = { value: runtime ?? "" };
|
|
261
|
-
backendReqBody.automaticBaseImageUpdatesDisabled = automaticBaseImageUpdatesDisabled;
|
|
262
250
|
}
|
|
263
251
|
async function createBackendAndPoll() {
|
|
264
252
|
const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId);
|
|
@@ -2,9 +2,24 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.localBuild = localBuild;
|
|
4
4
|
const build_1 = require("@apphosting/build");
|
|
5
|
-
|
|
5
|
+
const secrets_1 = require("./secrets");
|
|
6
|
+
const prompt_1 = require("../prompt");
|
|
7
|
+
const error_1 = require("../error");
|
|
8
|
+
async function localBuild(projectId, projectRoot, framework, env = {}, options) {
|
|
9
|
+
const hasBuildAvailableSecrets = Object.values(env).some((v) => v.secret && (!v.availability || v.availability.includes("BUILD")));
|
|
10
|
+
if (hasBuildAvailableSecrets && !options?.allowLocalBuildSecrets) {
|
|
11
|
+
if (options?.nonInteractive) {
|
|
12
|
+
throw new error_1.FirebaseError("Using build-available secrets during a local build in non-interactive mode requires the --allow-local-build-secrets flag.");
|
|
13
|
+
}
|
|
14
|
+
if (!(await (0, prompt_1.confirm)({
|
|
15
|
+
message: "Your build includes secrets that are available to the build environment. Using secrets in local builds may leave sensitive values in local artifacts/temporary files. Do you want to continue?",
|
|
16
|
+
default: false,
|
|
17
|
+
}))) {
|
|
18
|
+
throw new error_1.FirebaseError("Cancelled local build due to BUILD-available secrets.");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
6
21
|
const originalEnv = { ...process.env };
|
|
7
|
-
const addedEnv = toProcessEnv(env);
|
|
22
|
+
const addedEnv = await toProcessEnv(projectId, env);
|
|
8
23
|
for (const [key, value] of Object.entries(addedEnv)) {
|
|
9
24
|
process.env[key] = value;
|
|
10
25
|
}
|
|
@@ -37,6 +52,19 @@ async function localBuild(projectRoot, framework, env = {}) {
|
|
|
37
52
|
},
|
|
38
53
|
};
|
|
39
54
|
}
|
|
40
|
-
function toProcessEnv(env) {
|
|
41
|
-
|
|
55
|
+
async function toProcessEnv(projectId, env) {
|
|
56
|
+
const entries = await Promise.all(Object.entries(env).map(async ([key, value]) => {
|
|
57
|
+
if (value.availability && !value.availability.includes("BUILD")) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (value.secret) {
|
|
61
|
+
const resolvedValue = await (0, secrets_1.loadSecret)(projectId, value.secret);
|
|
62
|
+
return [key, resolvedValue];
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
return [key, value.value || ""];
|
|
66
|
+
}
|
|
67
|
+
}));
|
|
68
|
+
const filteredEntries = entries.filter((entry) => entry !== null);
|
|
69
|
+
return Object.fromEntries(filteredEntries);
|
|
42
70
|
}
|
|
@@ -2,34 +2,55 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_RUNTIME = void 0;
|
|
4
4
|
exports.promptRuntime = promptRuntime;
|
|
5
|
-
exports.
|
|
5
|
+
exports.resolveRuntime = resolveRuntime;
|
|
6
6
|
const prompt_1 = require("../prompt");
|
|
7
7
|
const utils_1 = require("../utils");
|
|
8
8
|
const apphosting = require("../gcp/apphosting");
|
|
9
|
+
const experiments_1 = require("../experiments");
|
|
9
10
|
exports.DEFAULT_RUNTIME = "nodejs";
|
|
10
11
|
async function promptRuntime(projectId, location) {
|
|
11
|
-
const choices = [
|
|
12
|
+
const choices = [];
|
|
13
|
+
let nodejsChoice = { name: "Node.js (default)", value: exports.DEFAULT_RUNTIME };
|
|
12
14
|
try {
|
|
13
15
|
const supportedRuntimes = await apphosting.listSupportedRuntimes(projectId, location);
|
|
14
16
|
for (const r of supportedRuntimes) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
const abiuText = r.automaticBaseImageUpdatesSupported
|
|
18
|
+
? "Enables Automatic Base Image Updates"
|
|
19
|
+
: "No Automatic Base Image Updates";
|
|
20
|
+
const choiceName = `${r.runtimeId} - ${abiuText}`;
|
|
21
|
+
if (r.runtimeId === exports.DEFAULT_RUNTIME) {
|
|
22
|
+
nodejsChoice = { name: choiceName, value: r.runtimeId };
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
choices.push({ name: choiceName, value: r.runtimeId });
|
|
17
26
|
}
|
|
18
27
|
}
|
|
28
|
+
choices.unshift(nodejsChoice);
|
|
19
29
|
}
|
|
20
30
|
catch (err) {
|
|
21
31
|
(0, utils_1.logWarning)("Failed to list supported runtimes. Falling back to hardcoded list.");
|
|
22
|
-
choices.push({ name: "
|
|
32
|
+
choices.push({ name: "nodejs - No Automatic Base Image Updates", value: "nodejs" });
|
|
33
|
+
choices.push({ name: "nodejs22 - Enables Automatic Base Image Updates", value: "nodejs22" });
|
|
23
34
|
}
|
|
24
|
-
|
|
35
|
+
const selectedRuntime = await (0, prompt_1.select)({
|
|
25
36
|
message: "Which runtime do you want to use?",
|
|
26
37
|
choices: choices,
|
|
27
38
|
default: exports.DEFAULT_RUNTIME,
|
|
28
39
|
});
|
|
40
|
+
if (selectedRuntime === exports.DEFAULT_RUNTIME) {
|
|
41
|
+
(0, utils_1.logBullet)("ABIU will not be enabled for the unversioned 'nodejs' runtime.");
|
|
42
|
+
}
|
|
43
|
+
return selectedRuntime;
|
|
29
44
|
}
|
|
30
|
-
async function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
async function resolveRuntime(projectId, location, nonInteractive, runtimeOption) {
|
|
46
|
+
if (runtimeOption !== undefined) {
|
|
47
|
+
return runtimeOption;
|
|
48
|
+
}
|
|
49
|
+
if (!(0, experiments_1.isEnabled)("abiu")) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (nonInteractive) {
|
|
53
|
+
return exports.DEFAULT_RUNTIME;
|
|
54
|
+
}
|
|
55
|
+
return promptRuntime(projectId, location);
|
|
35
56
|
}
|
|
@@ -5,6 +5,7 @@ exports.serviceAccountsForBackend = serviceAccountsForBackend;
|
|
|
5
5
|
exports.grantSecretAccess = grantSecretAccess;
|
|
6
6
|
exports.grantEmailsSecretAccess = grantEmailsSecretAccess;
|
|
7
7
|
exports.upsertSecret = upsertSecret;
|
|
8
|
+
exports.loadSecret = loadSecret;
|
|
8
9
|
exports.fetchSecrets = fetchSecrets;
|
|
9
10
|
exports.getSecretNameParts = getSecretNameParts;
|
|
10
11
|
exports.apphostingSecretsSetAction = apphostingSecretsSetAction;
|
|
@@ -149,6 +150,48 @@ async function upsertSecret(project, secret, location) {
|
|
|
149
150
|
}
|
|
150
151
|
return false;
|
|
151
152
|
}
|
|
153
|
+
const secretResourceRegex = /^projects\/([^/]+)\/secrets\/([^/]+)(?:\/versions\/((?:latest)|\d+))?$/;
|
|
154
|
+
const secretShorthandRegex = /^([^/@]+)(?:@((?:latest)|\d+))?$/;
|
|
155
|
+
async function loadSecret(project, name) {
|
|
156
|
+
let projectId;
|
|
157
|
+
let secretId;
|
|
158
|
+
let version;
|
|
159
|
+
const match = secretResourceRegex.exec(name);
|
|
160
|
+
if (match) {
|
|
161
|
+
projectId = match[1];
|
|
162
|
+
secretId = match[2];
|
|
163
|
+
version = match[3] || "latest";
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const match = secretShorthandRegex.exec(name);
|
|
167
|
+
if (!match) {
|
|
168
|
+
throw new error_1.FirebaseError(`Invalid secret name: ${name}`);
|
|
169
|
+
}
|
|
170
|
+
if (!project) {
|
|
171
|
+
throw new error_1.FirebaseError(`Cannot load secret ${match[1]} without a project. ` +
|
|
172
|
+
`Please use ${clc.bold("firebase use")} or pass the --project flag.`);
|
|
173
|
+
}
|
|
174
|
+
projectId = project;
|
|
175
|
+
secretId = match[1];
|
|
176
|
+
version = match[2] || "latest";
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
return await gcsm.accessSecretVersion(projectId, secretId, version);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
if (err instanceof error_1.FirebaseError) {
|
|
183
|
+
const original = err.original;
|
|
184
|
+
if (typeof original === "object" && original !== null) {
|
|
185
|
+
if (original.code === 403 ||
|
|
186
|
+
original.context?.response?.statusCode === 403) {
|
|
187
|
+
utils.logLabeledError("apphosting", `Permission denied to access secret ${secretId}. Use ` +
|
|
188
|
+
`${clc.bold("firebase apphosting:secrets:grantaccess")} to get permissions.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
152
195
|
async function fetchSecrets(projectId, secrets) {
|
|
153
196
|
let secretsKeyValuePairs;
|
|
154
197
|
try {
|
package/lib/apphosting/yaml.js
CHANGED
|
@@ -58,10 +58,8 @@ class AppHostingYamlConfig {
|
|
|
58
58
|
exports.AppHostingYamlConfig = AppHostingYamlConfig;
|
|
59
59
|
function toEnvMap(envs) {
|
|
60
60
|
return Object.fromEntries(envs.map((env) => {
|
|
61
|
-
const variable = env
|
|
62
|
-
|
|
63
|
-
delete env.variable;
|
|
64
|
-
return [variable, tmp];
|
|
61
|
+
const { variable, ...rest } = env;
|
|
62
|
+
return [variable, rest];
|
|
65
63
|
}));
|
|
66
64
|
}
|
|
67
65
|
function toEnvList(envs) {
|
package/lib/bin/firebase.js
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const semver = require("semver");
|
|
5
5
|
const pkg = require("../../package.json");
|
|
6
|
+
const IGNORED_WARNINGS = [
|
|
7
|
+
"DEP0040",
|
|
8
|
+
];
|
|
9
|
+
process.on("warning", (warning) => {
|
|
10
|
+
const nodeWarning = warning;
|
|
11
|
+
if (nodeWarning.code && IGNORED_WARNINGS.includes(nodeWarning.code)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
console.warn(nodeWarning.stack || nodeWarning.message);
|
|
15
|
+
});
|
|
6
16
|
const nodeVersion = process.version;
|
|
7
17
|
if (!semver.satisfies(nodeVersion, pkg.engines.node)) {
|
|
8
18
|
console.error(`Firebase CLI v${pkg.version} is incompatible with Node.js ${nodeVersion} Please upgrade Node.js to version ${pkg.engines.node}`);
|
package/lib/bin/mcp.js
CHANGED
|
@@ -49,6 +49,8 @@ Options:
|
|
|
49
49
|
If specified, auto-detection is disabled for other features.
|
|
50
50
|
--tools <tools> Comma-separated list of specific tools to enable. Disables
|
|
51
51
|
auto-detection entirely.
|
|
52
|
+
--mode <mode> Server mode: stdio, sse (defaults to stdio).
|
|
53
|
+
--port <port> The port to listen on when running in SSE mode (defaults to 3000).
|
|
52
54
|
-h, --help Show this help message.
|
|
53
55
|
`;
|
|
54
56
|
async function mcp() {
|
|
@@ -57,6 +59,8 @@ async function mcp() {
|
|
|
57
59
|
only: { type: "string", default: "" },
|
|
58
60
|
tools: { type: "string", default: "" },
|
|
59
61
|
dir: { type: "string" },
|
|
62
|
+
mode: { type: "string", default: "stdio" },
|
|
63
|
+
port: { type: "string", default: "3000" },
|
|
60
64
|
"generate-tool-list": { type: "boolean", default: false },
|
|
61
65
|
"generate-prompt-list": { type: "boolean", default: false },
|
|
62
66
|
"generate-resource-list": { type: "boolean", default: false },
|
|
@@ -83,6 +87,10 @@ async function mcp() {
|
|
|
83
87
|
}
|
|
84
88
|
if (earlyExit)
|
|
85
89
|
return;
|
|
90
|
+
if (values.mode !== "stdio" && values.mode !== "sse") {
|
|
91
|
+
console.error("Error: --mode must be either 'stdio' or 'sse'");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
86
94
|
(0, env_1.setFirebaseMcp)(true);
|
|
87
95
|
const mcpLogDir = (0, path_1.join)((0, os_1.homedir)(), ".cache", "firebase");
|
|
88
96
|
await (0, promises_1.mkdir)(mcpLogDir, { recursive: true });
|
|
@@ -100,7 +108,10 @@ async function mcp() {
|
|
|
100
108
|
enabledTools,
|
|
101
109
|
projectRoot: values.dir ? (0, path_1.resolve)(values.dir) : undefined,
|
|
102
110
|
});
|
|
103
|
-
await server.start(
|
|
111
|
+
await server.start({
|
|
112
|
+
useSSE: values.mode === "sse",
|
|
113
|
+
port: values.port ? parseInt(values.port, 10) : undefined,
|
|
114
|
+
});
|
|
104
115
|
if (process.stdin.isTTY)
|
|
105
116
|
process.stderr.write(STARTUP_MESSAGE);
|
|
106
117
|
}
|
|
@@ -19,10 +19,7 @@ exports.command = new command_1.Command("apphosting:backends:create")
|
|
|
19
19
|
.option("--root-dir <rootDir>", "specify the root directory for the backend.");
|
|
20
20
|
const abiuEnabled = experiments.isEnabled("abiu");
|
|
21
21
|
if (abiuEnabled) {
|
|
22
|
-
exports.command
|
|
23
|
-
.option("--runtime [runtime]", "specify the runtime for the backend (e.g., nodejs, nodejs22)")
|
|
24
|
-
.option("--automatic-base-image-updates", "enable automatic base image updates")
|
|
25
|
-
.option("--no-automatic-base-image-updates", "disable automatic base image updates");
|
|
22
|
+
exports.command.option("--runtime [runtime]", "specify the runtime for the backend (e.g., nodejs, nodejs22)");
|
|
26
23
|
}
|
|
27
24
|
exports.command
|
|
28
25
|
.before(requireAuth_1.requireAuth)
|
|
@@ -34,12 +31,9 @@ exports.command
|
|
|
34
31
|
throw new error_1.FirebaseError(`--non-interactive option requires --backend and --primary-region`);
|
|
35
32
|
}
|
|
36
33
|
const abiuAllowed = experiments.isEnabled("abiu");
|
|
37
|
-
if (!abiuAllowed &&
|
|
38
|
-
throw new error_1.FirebaseError("The --runtime
|
|
34
|
+
if (!abiuAllowed && options.runtime) {
|
|
35
|
+
throw new error_1.FirebaseError("The --runtime flag is only available when the 'abiu' experiment is enabled. To enable it, run 'firebase experiments:enable abiu'.");
|
|
39
36
|
}
|
|
40
37
|
const runtime = abiuAllowed && typeof options.runtime === "string" ? options.runtime : undefined;
|
|
41
|
-
|
|
42
|
-
? options.automaticBaseImageUpdates === false
|
|
43
|
-
: undefined;
|
|
44
|
-
return (0, backend_1.doSetup)(projectId, options.nonInteractive, options.app, options.backend, options.serviceAccount, options.primaryRegion, options.rootDir, runtime, automaticBaseImageUpdatesDisabled);
|
|
38
|
+
return (0, backend_1.doSetup)(projectId, options.nonInteractive, options.app, options.backend, options.serviceAccount, options.primaryRegion, options.rootDir, runtime);
|
|
45
39
|
});
|
|
@@ -42,7 +42,7 @@ exports.command = new command_1.Command("dataconnect:sdk:generate")
|
|
|
42
42
|
},
|
|
43
43
|
instructions: [],
|
|
44
44
|
};
|
|
45
|
-
await dataconnectInit.askQuestions(setup);
|
|
45
|
+
await dataconnectInit.askQuestions(setup, config, options);
|
|
46
46
|
await dataconnectInit.actuate(setup, config, options);
|
|
47
47
|
await (0, init_1.postInitSaves)(setup, config);
|
|
48
48
|
justRanInit = true;
|
|
@@ -64,7 +64,7 @@ exports.command = new command_1.Command("dataconnect:sdk:generate")
|
|
|
64
64
|
},
|
|
65
65
|
instructions: [],
|
|
66
66
|
};
|
|
67
|
-
await dataconnectSdkInit.askQuestions(setup);
|
|
67
|
+
await dataconnectSdkInit.askQuestions(setup, config, options);
|
|
68
68
|
await dataconnectSdkInit.actuate(setup, config);
|
|
69
69
|
justRanInit = true;
|
|
70
70
|
serviceInfosWithSDKs = await loadAllWithSDKs(projectId, config, options);
|
package/lib/commands/deploy.js
CHANGED
|
@@ -16,6 +16,7 @@ const colorette_1 = require("colorette");
|
|
|
16
16
|
const interactive_1 = require("../hosting/interactive");
|
|
17
17
|
const utils_1 = require("../utils");
|
|
18
18
|
const api_1 = require("../hosting/api");
|
|
19
|
+
const experiments = require("../experiments");
|
|
19
20
|
exports.VALID_DEPLOY_TARGETS = [
|
|
20
21
|
"database",
|
|
21
22
|
"storage",
|
|
@@ -92,7 +93,11 @@ exports.command = new command_1.Command("deploy")
|
|
|
92
93
|
'(e.g. "--only dataconnect:serviceId,dataconnect:serviceId:connectorId,dataconnect:serviceId:schema")')
|
|
93
94
|
.option("--except <targets>", 'deploy to all targets except specified (e.g. "database")')
|
|
94
95
|
.option("--dry-run", "perform a dry run of your deployment. Validates your changes and builds your code without deploying any changes to your project. " +
|
|
95
|
-
"In order to provide better validation, this may still enable APIs on the target project")
|
|
96
|
+
"In order to provide better validation, this may still enable APIs on the target project");
|
|
97
|
+
if (experiments.isEnabled("apphostinglocalbuilds")) {
|
|
98
|
+
exports.command.option("--allow-local-build-secrets", "allow the use of build-available secrets in local builds for App Hosting without interactive confirmation");
|
|
99
|
+
}
|
|
100
|
+
exports.command
|
|
96
101
|
.before(requireConfig_1.requireConfig)
|
|
97
102
|
.before((options) => {
|
|
98
103
|
options.filteredTargets = (0, filterTargets_1.filterTargets)(options, exports.VALID_DEPLOY_TARGETS);
|
package/lib/config.js
CHANGED
|
@@ -197,6 +197,7 @@ class Config {
|
|
|
197
197
|
const shouldWrite = await (0, prompt_1.confirm)({
|
|
198
198
|
message: "File " + clc.underline(path) + " already exists. Overwrite?",
|
|
199
199
|
default: !!confirmByDefault,
|
|
200
|
+
nonInteractive: this.options.nonInteractive,
|
|
200
201
|
});
|
|
201
202
|
if (!shouldWrite) {
|
|
202
203
|
utils.logBullet("Skipping write of " + clc.bold(path));
|
package/lib/dataconnect/build.js
CHANGED
|
@@ -34,6 +34,12 @@ async function build(options, configDir, deployStats) {
|
|
|
34
34
|
return buildResult?.metadata ?? {};
|
|
35
35
|
}
|
|
36
36
|
async function handleBuildErrors(errors, nonInteractive, force, dryRun) {
|
|
37
|
+
const fatalDeploys = errors.filter((w) => w.extensions?.warningLevel === "ALWAYS_REQUIRED");
|
|
38
|
+
if (fatalDeploys.length) {
|
|
39
|
+
utils.logLabeledError("dataconnect", `There are deployment requirements that are always required and cannot be bypassed:\n` +
|
|
40
|
+
(0, graphqlError_1.prettifyTable)(fatalDeploys));
|
|
41
|
+
throw new error_1.FirebaseError("Deployment failed due to unbypassable requirements.");
|
|
42
|
+
}
|
|
37
43
|
if (errors.filter((w) => !w.extensions?.warningLevel).length) {
|
|
38
44
|
throw new error_1.FirebaseError(`There are errors in your schema and connector files:\n${errors.map(graphqlError_1.prettify).join("\n")}`);
|
|
39
45
|
}
|
package/lib/dataconnect/names.js
CHANGED
|
@@ -55,7 +55,7 @@ function parseCloudSQLInstanceName(cloudSQLInstanceName) {
|
|
|
55
55
|
throw new error_1.FirebaseError(`${cloudSQLInstanceName} is not a valid cloudSQL instance name`);
|
|
56
56
|
}
|
|
57
57
|
const toString = () => {
|
|
58
|
-
return `projects/${projectId}/locations/${location}/
|
|
58
|
+
return `projects/${projectId}/locations/${location}/instances/${instanceId}`;
|
|
59
59
|
};
|
|
60
60
|
return {
|
|
61
61
|
projectId,
|
|
@@ -110,7 +110,7 @@ async function default_1(context, options) {
|
|
|
110
110
|
const selectedBackends = selected.map((id) => notFoundBackends.find((backend) => backend.backendId === id));
|
|
111
111
|
for (const cfg of selectedBackends) {
|
|
112
112
|
(0, utils_1.logLabeledBullet)("apphosting", `Creating a new backend ${cfg.backendId}...`);
|
|
113
|
-
const { location } = await (0, backend_1.doSetupSourceDeploy)(projectId, cfg.backendId);
|
|
113
|
+
const { location } = await (0, backend_1.doSetupSourceDeploy)(projectId, cfg.backendId, options.nonInteractive, cfg.rootDir);
|
|
114
114
|
context.backendConfigs[cfg.backendId] = cfg;
|
|
115
115
|
context.backendLocations[cfg.backendId] = location;
|
|
116
116
|
}
|
|
@@ -133,7 +133,10 @@ async function default_1(context, options) {
|
|
|
133
133
|
await injectEnvVarsFromApphostingConfig(configs.filter((c) => c.backendId === cfg.backendId), options, buildEnv, runtimeEnv);
|
|
134
134
|
await injectAutoInitEnvVars(cfg, backends, buildEnv, runtimeEnv);
|
|
135
135
|
try {
|
|
136
|
-
const { outputFiles, annotations, buildConfig } = await (0, localbuilds_1.localBuild)(options.projectRoot || "./", "nextjs", buildEnv[cfg.backendId] || {}
|
|
136
|
+
const { outputFiles, annotations, buildConfig } = await (0, localbuilds_1.localBuild)(projectId, options.projectRoot || "./", "nextjs", buildEnv[cfg.backendId] || {}, {
|
|
137
|
+
nonInteractive: options.nonInteractive,
|
|
138
|
+
allowLocalBuildSecrets: !!options.allowLocalBuildSecrets,
|
|
139
|
+
});
|
|
137
140
|
if (outputFiles.length !== 1) {
|
|
138
141
|
throw new error_1.FirebaseError(`Local build for backend ${cfg.backendId} failed: No output files found.`);
|
|
139
142
|
}
|
|
@@ -13,7 +13,7 @@ const error_1 = require("../../error");
|
|
|
13
13
|
const types = require("../../firestore/api-types");
|
|
14
14
|
const api_2 = require("../../firestore/api");
|
|
15
15
|
function prepareRules(context, rulesDeploy, databaseId, rulesFile) {
|
|
16
|
-
rulesDeploy.addFile(rulesFile);
|
|
16
|
+
rulesDeploy.addFile(rulesFile, databaseId);
|
|
17
17
|
context.firestore.rules.push({
|
|
18
18
|
databaseId,
|
|
19
19
|
rulesFile,
|
|
@@ -210,20 +210,8 @@ async function loadExistingBackend(ctx) {
|
|
|
210
210
|
existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
|
|
211
211
|
}
|
|
212
212
|
unreachableRegions.gcfV1 = gcfV1Results.unreachable;
|
|
213
|
-
if (experiments.isEnabled("functionsrunapionly")
|
|
214
|
-
|
|
215
|
-
const runServices = await run.listServices(ctx.projectId);
|
|
216
|
-
for (const service of runServices) {
|
|
217
|
-
const endpoint = run.endpointFromService(service);
|
|
218
|
-
existingBackend.endpoints[endpoint.region] =
|
|
219
|
-
existingBackend.endpoints[endpoint.region] || {};
|
|
220
|
-
existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
catch (err) {
|
|
224
|
-
logger_1.logger.debug(err.message);
|
|
225
|
-
unreachableRegions.run = ["unknown"];
|
|
226
|
-
}
|
|
213
|
+
if (experiments.isEnabled("functionsrunapionly")) {
|
|
214
|
+
await loadCloudRunServices(ctx, existingBackend, unreachableRegions, false);
|
|
227
215
|
}
|
|
228
216
|
else {
|
|
229
217
|
const gcfV2Results = await gcfV2.listAllFunctions(ctx.projectId);
|
|
@@ -233,11 +221,31 @@ async function loadExistingBackend(ctx) {
|
|
|
233
221
|
existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
|
|
234
222
|
}
|
|
235
223
|
unreachableRegions.gcfV2 = gcfV2Results.unreachable;
|
|
224
|
+
if (experiments.isEnabled("dartfunctions")) {
|
|
225
|
+
await loadCloudRunServices(ctx, existingBackend, unreachableRegions, true);
|
|
226
|
+
}
|
|
236
227
|
}
|
|
237
228
|
ctx.existingBackend = existingBackend;
|
|
238
229
|
ctx.unreachableRegions = unreachableRegions;
|
|
239
230
|
return ctx.existingBackend;
|
|
240
231
|
}
|
|
232
|
+
async function loadCloudRunServices(ctx, existingBackend, unreachableRegions, onlyMissing) {
|
|
233
|
+
try {
|
|
234
|
+
const runServices = await run.listServices(ctx.projectId);
|
|
235
|
+
for (const service of runServices) {
|
|
236
|
+
const endpoint = run.endpointFromService(service);
|
|
237
|
+
if (!onlyMissing || !existingBackend.endpoints[endpoint.region]?.[endpoint.id]) {
|
|
238
|
+
existingBackend.endpoints[endpoint.region] =
|
|
239
|
+
existingBackend.endpoints[endpoint.region] || {};
|
|
240
|
+
existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
logger_1.logger.debug(`Error loading Cloud Run services: ${err.message}`);
|
|
246
|
+
unreachableRegions.run = ["unknown"];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
241
249
|
async function checkAvailability(context, want) {
|
|
242
250
|
await existingBackend(context);
|
|
243
251
|
const gcfV1Regions = new Set();
|
|
@@ -187,13 +187,19 @@ async function prepare(context, options, payload) {
|
|
|
187
187
|
context.sources[codebase] = source;
|
|
188
188
|
}
|
|
189
189
|
payload.functions = {};
|
|
190
|
-
const
|
|
190
|
+
const existingBackend = await backend.existingBackend(context);
|
|
191
|
+
for (const [codebase, wantBackend] of Object.entries(wantBackends)) {
|
|
192
|
+
const relevantEndpoints = backend
|
|
193
|
+
.allEndpoints(existingBackend)
|
|
194
|
+
.filter((e) => e.codebase === codebase || e.codebase === undefined);
|
|
195
|
+
await resolveDefaultRegions(wantBackend, backend.of(...relevantEndpoints));
|
|
196
|
+
}
|
|
197
|
+
const haveBackends = (0, functionsDeployHelper_1.groupEndpointsByCodebase)(wantBackends, backend.allEndpoints(existingBackend));
|
|
191
198
|
for (const [codebase, wantBackend] of Object.entries(wantBackends)) {
|
|
192
199
|
const haveBackend = haveBackends[codebase] || backend.empty();
|
|
193
200
|
payload.functions[codebase] = { wantBackend, haveBackend };
|
|
194
201
|
}
|
|
195
202
|
for (const [codebase, { wantBackend, haveBackend }] of Object.entries(payload.functions)) {
|
|
196
|
-
await resolveDefaultRegions(wantBackend, haveBackend);
|
|
197
203
|
inferDetailsFromExisting(wantBackend, haveBackend, codebaseUsesEnvs.includes(codebase));
|
|
198
204
|
await (0, triggerRegionHelper_1.ensureTriggerRegions)(wantBackend);
|
|
199
205
|
resolveCpuAndConcurrency(wantBackend);
|
|
@@ -69,9 +69,9 @@ class Fabricator {
|
|
|
69
69
|
results: [],
|
|
70
70
|
};
|
|
71
71
|
const changesets = Object.values(plan);
|
|
72
|
-
const scraperV1 = new sourceTokenScraper_1.SourceTokenScraper();
|
|
73
|
-
const scraperV2 = new sourceTokenScraper_1.SourceTokenScraper();
|
|
74
72
|
const createAndUpdatePromises = changesets.map((changes) => {
|
|
73
|
+
const scraperV1 = new sourceTokenScraper_1.SourceTokenScraper();
|
|
74
|
+
const scraperV2 = new sourceTokenScraper_1.SourceTokenScraper();
|
|
75
75
|
return this.applyUpserts(changes, scraperV1, scraperV2);
|
|
76
76
|
});
|
|
77
77
|
const createAndUpdateResultsArray = await Promise.allSettled(createAndUpdatePromises);
|
|
@@ -542,7 +542,20 @@ class Fabricator {
|
|
|
542
542
|
endpoint.runServiceId = endpoint.id;
|
|
543
543
|
})
|
|
544
544
|
.catch(rethrowAs(endpoint, "create"));
|
|
545
|
-
|
|
545
|
+
const serviceName = `projects/${endpoint.project}/locations/${endpoint.region}/services/${endpoint.runServiceId}`;
|
|
546
|
+
if (backend.isHttpsTriggered(endpoint)) {
|
|
547
|
+
const invoker = endpoint.httpsTrigger.invoker || ["public"];
|
|
548
|
+
if (!invoker.includes("private")) {
|
|
549
|
+
await this.executor
|
|
550
|
+
.run(() => run.setInvokerCreate(endpoint.project, serviceName, invoker))
|
|
551
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else if (backend.isCallableTriggered(endpoint)) {
|
|
555
|
+
await this.executor
|
|
556
|
+
.run(() => run.setInvokerCreate(endpoint.project, serviceName, ["public"]))
|
|
557
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
558
|
+
}
|
|
546
559
|
}
|
|
547
560
|
async updateRunFunction(update) {
|
|
548
561
|
const endpoint = update.endpoint;
|
|
@@ -567,7 +580,16 @@ class Fabricator {
|
|
|
567
580
|
endpoint.runServiceId = endpoint.id;
|
|
568
581
|
})
|
|
569
582
|
.catch(rethrowAs(endpoint, "update"));
|
|
570
|
-
|
|
583
|
+
const serviceName = `projects/${endpoint.project}/locations/${endpoint.region}/services/${endpoint.runServiceId}`;
|
|
584
|
+
let invoker;
|
|
585
|
+
if (backend.isHttpsTriggered(endpoint)) {
|
|
586
|
+
invoker = endpoint.httpsTrigger.invoker === null ? ["public"] : endpoint.httpsTrigger.invoker;
|
|
587
|
+
}
|
|
588
|
+
if (invoker) {
|
|
589
|
+
await this.executor
|
|
590
|
+
.run(() => run.setInvokerUpdate(endpoint.project, serviceName, invoker))
|
|
591
|
+
.catch(rethrowAs(endpoint, "set invoker"));
|
|
592
|
+
}
|
|
571
593
|
}
|
|
572
594
|
async deleteRunFunction(endpoint) {
|
|
573
595
|
await this.runFunctionExecutor
|
|
@@ -584,16 +606,6 @@ class Fabricator {
|
|
|
584
606
|
})
|
|
585
607
|
.catch(rethrowAs(endpoint, "delete"));
|
|
586
608
|
}
|
|
587
|
-
async setInvoker(endpoint) {
|
|
588
|
-
if (backend.isHttpsTriggered(endpoint)) {
|
|
589
|
-
const invoker = endpoint.httpsTrigger.invoker || ["public"];
|
|
590
|
-
if (!invoker.includes("private")) {
|
|
591
|
-
await this.executor
|
|
592
|
-
.run(() => run.setInvokerUpdate(endpoint.project, `projects/${endpoint.project}/locations/${endpoint.region}/services/${endpoint.runServiceId}`, invoker))
|
|
593
|
-
.catch(rethrowAs(endpoint, "set invoker"));
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
609
|
async setRunTraits(serviceName, endpoint) {
|
|
598
610
|
await this.functionExecutor
|
|
599
611
|
.run(async () => {
|
|
@@ -45,6 +45,9 @@ function getPythonBinary(runtime) {
|
|
|
45
45
|
else if (runtime === "python313") {
|
|
46
46
|
return "python3.13";
|
|
47
47
|
}
|
|
48
|
+
else if (runtime === "python314") {
|
|
49
|
+
return "python3.14";
|
|
50
|
+
}
|
|
48
51
|
(0, functional_1.assertExhaustive)(runtime, `Unhandled python runtime ${runtime}`);
|
|
49
52
|
}
|
|
50
53
|
class Delegate {
|
|
@@ -89,6 +89,12 @@ exports.RUNTIMES = runtimes({
|
|
|
89
89
|
deprecationDate: "2029-10-10",
|
|
90
90
|
decommissionDate: "2030-04-10",
|
|
91
91
|
},
|
|
92
|
+
python314: {
|
|
93
|
+
friendly: "Python 3.14",
|
|
94
|
+
status: "GA",
|
|
95
|
+
deprecationDate: "2030-10-10",
|
|
96
|
+
decommissionDate: "2031-04-10",
|
|
97
|
+
},
|
|
92
98
|
dart3: {
|
|
93
99
|
friendly: "Dart 3",
|
|
94
100
|
status: "experimental",
|