forked from firebase/firebase-tools
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathexperiments.ts
257 lines (236 loc) · 9.55 KB
/
experiments.ts
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
import { bold, italic } from "colorette";
import * as leven from "leven";
import { basename } from "path";
import { configstore } from "./configstore";
import { FirebaseError } from "./error";
import { isRunningInGithubAction } from "./init/features/hosting/github";
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 funtions to increase performance and reliaibility " +
"of deploys. This has been made an experiment due to backend bugs that are " +
"temporarily causing failures in some regions with this optimization enabled",
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,
},
// permanent experiment
automaticallydeletegcfartifacts: {
shortDescription: "Control whether functions cleans up images after deploys",
fullDescription:
"To control costs, Firebase defaults to automatically deleting containers " +
"created during the build process. This has the side-effect of preventing " +
"users from rolling back to previous revisions using the Run API. To change " +
`this behavior, call ${bold("experiments:disable deletegcfartifactsondeploy")} ` +
`consider also calling ${bold("experiments:enable deletegcfartifacts")} ` +
`to enable the new command ${bold("functions:deletegcfartifacts")} which` +
"lets you clean up images manually",
public: true,
default: true,
},
// 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.",
},
apphosting: {
shortDescription: "Allow CLI option for Frameworks",
default: true,
public: false,
},
// TODO(joehanley): Delete this once weve scrubbed all references to experiment from docs.
dataconnect: {
shortDescription: "Deprecated. Previosuly, enabled Data Connect related features.",
fullDescription: "Deprecated. Previously, enabled Data Connect related features.",
public: false,
},
genkit: {
shortDescription: "Enable Genkit related features.",
fullDescription: "Enable Genkit related features.",
default: true,
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) => leven(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());
}