-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathexperiments.ts
More file actions
375 lines (351 loc) · 13.1 KB
/
Copy pathexperiments.ts
File metadata and controls
375 lines (351 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
import { bold, italic } from "colorette";
import { basename } from "path";
import { configstore } from "./configstore";
import { FirebaseError } from "./error";
import { isRunningInGithubAction, stringDistance } from "./utils";
export interface Experiment {
shortDescription: string;
fullDescription?: string;
public?: boolean;
docsUri?: string;
default?: boolean;
}
// Utility method to ensure there are no typos in defining ALL_EXPERIMENTS
function experiments<Keys extends string>(exp: Record<Keys, Experiment>): Record<Keys, Experiment> {
return Object.freeze(exp);
}
export const ALL_EXPERIMENTS = experiments({
// meta:
experiments: {
shortDescription: "enables the experiments family of commands",
},
// Realtime Database experiments
rtdbrules: {
shortDescription: "Advanced security rules management",
},
rtdbmanagement: {
shortDescription: "Use new endpoint to administer realtime database instances",
},
// Cloud Functions for Firebase experiments
functionsv2deployoptimizations: {
shortDescription: "Optimize deployments of v2 firebase functions",
fullDescription:
"Reuse build images across functions to increase performance and reliability " +
"of deploys.",
public: true,
default: true,
},
deletegcfartifacts: {
shortDescription: `Add the ${bold(
"functions:deletegcfartifacts",
)} command to purge docker build images`,
fullDescription:
`Add the ${bold("functions:deletegcfartifacts")}` +
"command. Google Cloud Functions creates Docker images when building your " +
"functions. Cloud Functions for Firebase automatically cleans up these " +
"images for you on deploy. Customers who predated this cleanup, or customers " +
"who also deploy Google Cloud Functions with non-Firebase tooling may have " +
"old Docker images stored in either Google Container Repository or Artifact " +
`Registry. The ${bold("functions:deletegcfartifacts")} command ` +
"will delete all Docker images created by Google Cloud Functions irrespective " +
"of how that image was created.",
public: true,
},
legacyRuntimeConfigCommands: {
shortDescription: "Expose legacy functions.config() CLI commands",
fullDescription:
"The Cloud Runtime Config API is deprecated. Enable this experiment to continue using the " +
"`functions:config:*` commands while you migrate to the Firebase Functions params APIs.",
default: false,
public: true,
},
runfunctions: {
shortDescription:
"Functions created using the V2 API target Cloud Run Functions (not production ready)",
public: false,
},
functionsiac: {
shortDescription: "Exports functions IaC code",
public: false,
},
functionsrunapionly: {
shortDescription: "Use Cloud Run API to list v2 functions",
public: false,
},
bypassfunctionsdeprecationcheck: {
shortDescription: "Bypass Functions check for old runtimes",
fullDescription:
"Bypasses the local check for whether a functions runtime is " +
"decommissioned. This does not, by itself, allow you to deploy a function with a " +
"decommissioned runtime, as there are server-side checks as well.",
public: false,
default: false,
},
dartfunctions: {
shortDescription: "Enable Dart Functions.",
public: true,
default: false,
},
kits: {
shortDescription: "Enable Functions Kits.",
fullDescription: "Adds support for Function Kits.",
public: false,
default: false,
},
// Emulator experiments
emulatoruisnapshot: {
shortDescription: "Load pre-release versions of the emulator UI",
},
emulatorapphosting: {
shortDescription: "App Hosting emulator",
public: false,
},
// Hosting experiments
webframeworks: {
shortDescription: "Native support for popular web frameworks",
fullDescription:
"Adds support for popular web frameworks such as Next.js " +
"Angular, React, Svelte, and Vite-compatible frameworks. A manual migration " +
"may be required when the non-experimental support for these frameworks " +
"is released",
docsUri: "https://firebase.google.com/docs/hosting/frameworks-overview",
public: true,
},
pintags: {
shortDescription: "Adds the pinTag option to Run and Functions rewrites",
fullDescription:
"Adds support for the 'pinTag' boolean on Runction and Run rewrites for " +
"Firebase Hosting. With this option, newly released hosting sites will be " +
"bound to the current latest version of their referenced functions or services. " +
"This option depends on Run pinned traffic targets, of which only 2000 can " +
"exist per region. firebase-tools aggressively garbage collects tags it creates " +
"if any service exceeds 500 tags, but it is theoretically possible that a project " +
"exceeds the region-wide limit of tags and an old site version fails",
public: true,
default: true,
},
// Access experiments
crossservicerules: {
shortDescription: "Allow Firebase Rules to reference resources in other services",
},
internaltesting: {
shortDescription: "Exposes Firebase CLI commands intended for internal testing purposes.",
fullDescription:
"Exposes Firebase CLI commands intended for internal testing purposes. " +
"These commands are not meant for public consumption and may break or disappear " +
"without a notice.",
},
appcheckadmin: {
shortDescription: "Manage App Check enforcement and attestation providers from the CLI.",
fullDescription:
"Enables the `firebase appcheck:services`, `firebase appcheck:providers` and " +
"`firebase appcheck:apps` commands for reading and changing App Check enforcement per " +
"service and the attestation providers of each app. These commands are in preview and " +
"may change until the surface is API council approved. The `firebase appcheck:debugtokens` " +
"commands are generally available and are not affected by this experiment.",
},
ailogic: {
shortDescription: "Manage Firebase AI Logic from the CLI.",
fullDescription:
"Enables the `firebase ailogic` command surface for managing Firebase AI Logic, " +
"starting with the Gemini API providers. These commands are in preview and may " +
"change until the underlying API is finalized.",
},
apphosting: {
shortDescription: "Allow CLI option for Frameworks",
default: true,
public: false,
},
apphostinglocalbuilds: {
shortDescription: "Enable App Hosting local builds",
default: false,
public: false,
},
abiu: {
shortDescription:
"Enable Automatic Base Image Updates (ABIU) and runtime selection for App Hosting",
default: true,
public: true,
},
// TODO(joehanley): Delete this once weve scrubbed all references to experiment from docs.
dataconnect: {
shortDescription: "Deprecated. Previosuly, enabled SQL Connect related features.",
fullDescription: "Deprecated. Previously, enabled SQL Connect related features.",
public: false,
},
genkit: {
shortDescription: "Enable Genkit related features.",
fullDescription: "Enable Genkit related features.",
default: true,
public: false,
},
appsinit: {
shortDescription: "Adds experimental `apps:init` command.",
fullDescription:
"Adds experimental `apps:init` command. When run from an app directory, this command detects the app's platform and configures required files.",
default: false,
public: true,
},
mcp: {
shortDescription: "Adds experimental `firebase mcp` command for running a Firebase MCP server.",
default: true,
public: false,
},
mcpalpha: {
shortDescription: "Opt-in to early MCP features before they're widely released.",
default: false,
public: true,
},
mcpapps: {
shortDescription: "Enables MCP Apps features",
fullDescription: "Enables MCP Apps features, including returning UI resource URIs.",
public: true,
},
fdcift: {
shortDescription: "Enable instrumentless trial for SQL Connect",
default: true,
public: false,
},
apptesting: {
shortDescription: "Adds experimental App Testing feature",
public: true,
},
fdcwebhooks: {
shortDescription: "Enable Firebase SQL Connect webhooks feature.",
default: true,
public: false,
},
fdcrealtime: {
shortDescription: "Enable Firebase SQL Connect realtime feature.",
default: true,
public: false,
},
crashlyticsWeb: {
shortDescription: "Enable the ability to upload source maps for web apps to Crashlytics.",
default: false,
public: true,
},
secretEnvParams: {
shortDescription:
"Enable writing the backing resource binding for a Functions secret param to .env",
default: false,
public: false,
},
extdeprecationwarnings: {
shortDescription: "Show deprecation warnings for Firebase Extensions CLI commands.",
default: true,
public: true,
},
extMigrationFeatures: {
shortDescription:
"Enable features intended to assist with the migration of Extension instances to Kits.",
default: false,
public: false,
},
});
export type ExperimentName = keyof typeof ALL_EXPERIMENTS;
/** Determines whether a name is a valid experiment name. */
export function isValidExperiment(name: string): name is ExperimentName {
return Object.keys(ALL_EXPERIMENTS).includes(name);
}
/**
* Detects experiment names that were potentially what a customer intended to
* type when they provided malformed.
* Returns null if the malformed name is actually an experiment. Returns all
* possible typos.
*/
export function experimentNameAutocorrect(malformed: string): string[] {
if (isValidExperiment(malformed)) {
throw new FirebaseError(
"Assertion failed: experimentNameAutocorrect given actual experiment name",
{ exit: 2 },
);
}
// N.B. I personally would use < (name.length + malformed.length) * 0.2
// but this logic matches src/index.ts. I neither want to change something
// with such potential impact nor to create divergent behavior.
return Object.keys(ALL_EXPERIMENTS).filter(
(name) => stringDistance(name, malformed) < malformed.length * 0.4,
);
}
let localPreferencesCache: Record<ExperimentName, boolean> | undefined = undefined;
function localPreferences(): Record<ExperimentName, boolean> {
if (!localPreferencesCache) {
localPreferencesCache = (configstore.get("previews") || {}) as Record<ExperimentName, boolean>;
for (const key of Object.keys(localPreferencesCache)) {
if (!isValidExperiment(key)) {
delete localPreferencesCache[key as ExperimentName];
}
}
}
return localPreferencesCache;
}
/** Returns whether an experiment is enabled. */
export function isEnabled(name: ExperimentName): boolean {
return localPreferences()[name] ?? ALL_EXPERIMENTS[name]?.default ?? false;
}
/**
* Sets whether an experiment is enabled.
* Set to a boolean value to explicitly opt in or out of an experiment.
* Set to null to go on the default track for this experiment.
*/
export function setEnabled(name: ExperimentName, to: boolean | null): void {
if (to === null) {
delete localPreferences()[name];
} else {
localPreferences()[name] = to;
}
}
/**
* Enables multiple experiments given a comma-delimited environment variable:
* `FIREBASE_CLI_EXPERIMENTS`.
*
* Example:
* FIREBASE_CLI_PREVIEWS=experiment1,experiment2,turtle
*
* Would silently enable `experiment1` and `experiment2`, but would not enable `turtle`.
*/
export function enableExperimentsFromCliEnvVariable(): void {
const experiments = process.env.FIREBASE_CLI_EXPERIMENTS || "";
for (const experiment of experiments.split(",")) {
if (isValidExperiment(experiment)) {
setEnabled(experiment, true);
}
}
}
/**
* Assert that an experiment is enabled before following a code path.
* This code is unnecessary in code paths guarded by ifEnabled. When
* a customer's project was clearly written against an experiment that
* was not enabled, assertEnabled will throw a standard error. The "task"
* param is part of this error. It will be presented as "Cannot ${task}".
*/
export function assertEnabled(name: ExperimentName, task: string): void {
if (!isEnabled(name)) {
const prefix = `Cannot ${task} because the experiment ${bold(name)} is not enabled.`;
if (isRunningInGithubAction()) {
const path = process.env.GITHUB_WORKFLOW_REF?.split("@")[0];
const filename = path ? `.github/workflows/${basename(path)}` : "your action's yml";
const newValue = [process.env.FIREBASE_CLI_EXPERIMENTS, name].filter((it) => !!it).join(",");
throw new FirebaseError(
`${prefix} To enable add a ${bold(
"FIREBASE_CLI_EXPERIMENTS",
)} environment variable to ${filename}, like so: ${italic(`
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
...
env:
FIREBASE_CLI_EXPERIMENTS: ${newValue}
`)}`,
);
} else {
throw new FirebaseError(
`${prefix} To enable ${bold(name)} run ${bold(`firebase experiments:enable ${name}`)}`,
);
}
}
}
/** Saves the current set of enabled experiments to disk. */
export function flushToDisk(): void {
configstore.set("previews", localPreferences());
}