firebase-tools 14.10.0 → 14.11.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/api.js +3 -1
- package/lib/apphosting/backend.js +5 -2
- package/lib/apptesting/invokeTests.js +40 -0
- package/lib/apptesting/parseTestFiles.js +75 -0
- package/lib/apptesting/types.js +18 -0
- package/lib/commands/apptesting-execute.js +102 -0
- package/lib/commands/index.js +4 -0
- package/lib/commands/init.js +7 -0
- package/lib/deploy/apphosting/prepare.js +1 -9
- package/lib/deploy/functions/runtimes/discovery/index.js +53 -4
- package/lib/deploy/functions/runtimes/node/index.js +74 -44
- package/lib/emulator/dataconnect/pgliteServer.js +4 -4
- package/lib/emulator/downloadableEmulatorInfo.json +18 -18
- package/lib/emulator/env.js +2 -1
- package/lib/emulator/hub.js +1 -2
- package/lib/emulator/tasksEmulator.js +1 -1
- package/lib/emulator/ui.js +3 -1
- package/lib/experiments.js +4 -0
- package/lib/firestore/api.js +1 -11
- package/lib/init/features/aitools/claude.js +44 -0
- package/lib/init/features/aitools/cursor.js +62 -0
- package/lib/init/features/aitools/gemini.js +58 -0
- package/lib/init/features/aitools/index.js +28 -0
- package/lib/init/features/aitools/promptUpdater.js +109 -0
- package/lib/init/features/aitools/studio.js +17 -0
- package/lib/init/features/aitools/types.js +2 -0
- package/lib/init/features/aitools.js +83 -0
- package/lib/init/features/apptesting/index.js +29 -0
- package/lib/init/features/index.js +4 -1
- package/lib/init/index.js +5 -0
- package/lib/mcp/index.js +32 -1
- package/lib/utils.js +21 -1
- package/package.json +2 -1
- package/prompts/FIREBASE.md +122 -0
- package/prompts/FIREBASE_FUNCTIONS.md +221 -0
- package/schema/apptesting-yaml.json +64 -0
- package/templates/init/aitools/cursor-rules-header.txt +8 -0
- package/templates/init/aitools/gemini-extension.json +11 -0
- package/templates/init/apptesting/smoke_test.yaml +6 -0
package/lib/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.storageOrigin = exports.runtimeconfigOrigin = exports.rulesOrigin = exports.resourceManagerOrigin = exports.crashlyticsApiOrigin = exports.messagingApiOrigin = exports.remoteConfigApiOrigin = exports.rtdbMetadataOrigin = exports.rtdbManagementOrigin = exports.realtimeOrigin = exports.extensionsTOSOrigin = exports.extensionsPublisherOrigin = exports.extensionsOrigin = exports.iamOrigin = exports.identityOrigin = exports.hostingOrigin = exports.googleOrigin = exports.pubsubOrigin = exports.cloudTasksOrigin = exports.cloudschedulerOrigin = exports.cloudbuildOrigin = exports.functionsDefaultRegion = exports.runOrigin = exports.functionsV2Origin = exports.functionsOrigin = exports.firestoreOrigin = exports.firestoreOriginOrEmulator = exports.firedataOrigin = exports.firebaseExtensionsRegistryOrigin = exports.firebaseApiOrigin = exports.eventarcOrigin = exports.dynamicLinksKey = exports.dynamicLinksOrigin = exports.consoleOrigin = exports.authManagementOrigin = exports.authOrigin = exports.apphostingGitHubAppInstallationURL = exports.apphostingP4SADomain = exports.apphostingOrigin = exports.appDistributionOrigin = exports.artifactRegistryDomain = exports.developerConnectP4SADomain = exports.developerConnectOrigin = exports.containerRegistryDomain = exports.cloudMonitoringOrigin = exports.cloudloggingOrigin = exports.cloudbillingOrigin = exports.clientSecret = exports.clientId = exports.authProxyOrigin = void 0;
|
|
4
|
-
exports.setScopes = exports.getScopes = exports.cloudAiCompanionOrigin = exports.vertexAIOrigin = exports.cloudSQLAdminOrigin = exports.dataConnectLocalConnString = exports.dataconnectP4SADomain = exports.dataconnectOrigin = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = void 0;
|
|
4
|
+
exports.setScopes = exports.getScopes = exports.appTestingOrigin = exports.cloudAiCompanionOrigin = exports.vertexAIOrigin = exports.cloudSQLAdminOrigin = exports.dataConnectLocalConnString = exports.dataconnectP4SADomain = exports.dataconnectOrigin = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = void 0;
|
|
5
5
|
const constants_1 = require("./emulator/constants");
|
|
6
6
|
const logger_1 = require("./logger");
|
|
7
7
|
const scopes = require("./scopes");
|
|
@@ -144,6 +144,8 @@ const vertexAIOrigin = () => utils.envOverride("VERTEX_AI_URL", "https://aiplatf
|
|
|
144
144
|
exports.vertexAIOrigin = vertexAIOrigin;
|
|
145
145
|
const cloudAiCompanionOrigin = () => utils.envOverride("CLOUD_AI_COMPANION_URL", "https://cloudaicompanion.googleapis.com");
|
|
146
146
|
exports.cloudAiCompanionOrigin = cloudAiCompanionOrigin;
|
|
147
|
+
const appTestingOrigin = () => utils.envOverride("FIREBASE_APP_TESTING_URL", "https://firebaseapptesting.googleapis.com");
|
|
148
|
+
exports.appTestingOrigin = appTestingOrigin;
|
|
147
149
|
function getScopes() {
|
|
148
150
|
return Array.from(commandScopes);
|
|
149
151
|
}
|
|
@@ -162,9 +162,12 @@ async function ensureAppHostingComputeServiceAccount(projectId, serviceAccount)
|
|
|
162
162
|
if (!(err instanceof error_1.FirebaseError)) {
|
|
163
163
|
throw err;
|
|
164
164
|
}
|
|
165
|
-
|
|
165
|
+
if (err.status === 403) {
|
|
166
166
|
throw new error_1.FirebaseError(`Failed to create backend due to missing delegation permissions for ${sa}. Make sure you have the iam.serviceAccounts.actAs permission.`, { original: err });
|
|
167
167
|
}
|
|
168
|
+
else if (err.status !== 404) {
|
|
169
|
+
throw new error_1.FirebaseError("Unexpected error occurred while testing for IAM service account permissions", { original: err });
|
|
170
|
+
}
|
|
168
171
|
}
|
|
169
172
|
await provisionDefaultComputeServiceAccount(projectId);
|
|
170
173
|
}
|
|
@@ -232,7 +235,7 @@ async function provisionDefaultComputeServiceAccount(projectId) {
|
|
|
232
235
|
}
|
|
233
236
|
catch (err) {
|
|
234
237
|
if ((0, error_1.getErrStatus)(err) === 400) {
|
|
235
|
-
(0, utils_1.logWarning)("Your App Hosting compute service account is still being provisioned in the background
|
|
238
|
+
(0, utils_1.logWarning)("Your App Hosting compute service account is still being provisioned in the background. If you encounter an error, please try again after a few moments.");
|
|
236
239
|
}
|
|
237
240
|
else {
|
|
238
241
|
throw err;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pollInvocationStatus = exports.invokeTests = void 0;
|
|
4
|
+
const apiv2_1 = require("../apiv2");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
const operationPoller = require("../operation-poller");
|
|
7
|
+
const error_1 = require("../error");
|
|
8
|
+
const apiClient = new apiv2_1.Client({ urlPrefix: (0, api_1.appTestingOrigin)(), apiVersion: "v1alpha" });
|
|
9
|
+
async function invokeTests(appId, startUri, testDefs) {
|
|
10
|
+
const appResource = `projects/${appId.split(":")[1]}/apps/${appId}`;
|
|
11
|
+
try {
|
|
12
|
+
const invocationResponse = await apiClient.post(`${appResource}/testInvocations:invokeTestCases`, buildInvokeTestCasesRequest(testDefs));
|
|
13
|
+
return invocationResponse.body;
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
throw new error_1.FirebaseError("Test invocation failed", { original: (0, error_1.getError)(err) });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.invokeTests = invokeTests;
|
|
20
|
+
function buildInvokeTestCasesRequest(testCaseInvocations) {
|
|
21
|
+
return {
|
|
22
|
+
resource: {
|
|
23
|
+
testInvocation: {},
|
|
24
|
+
testCaseInvocations,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function pollInvocationStatus(operationName, onPoll, backoff = 30 * 1000) {
|
|
29
|
+
return operationPoller.pollOperation({
|
|
30
|
+
pollerName: "App Testing Invocation Poller",
|
|
31
|
+
apiOrigin: (0, api_1.appTestingOrigin)(),
|
|
32
|
+
apiVersion: "v1alpha",
|
|
33
|
+
operationResourceName: operationName,
|
|
34
|
+
masterTimeout: 30 * 60 * 1000,
|
|
35
|
+
backoff,
|
|
36
|
+
maxBackoff: 15 * 1000,
|
|
37
|
+
onPoll,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
exports.pollInvocationStatus = pollInvocationStatus;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseTestFiles = void 0;
|
|
4
|
+
const fsutils_1 = require("../fsutils");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const logger_1 = require("../logger");
|
|
7
|
+
const types_1 = require("./types");
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
const error_1 = require("../error");
|
|
10
|
+
function createFilter(pattern) {
|
|
11
|
+
const regex = pattern ? new RegExp(pattern) : undefined;
|
|
12
|
+
return (s) => !regex || regex.test(s);
|
|
13
|
+
}
|
|
14
|
+
async function parseTestFiles(dir, targetUri, filePattern, namePattern) {
|
|
15
|
+
try {
|
|
16
|
+
new URL(targetUri);
|
|
17
|
+
}
|
|
18
|
+
catch (ex) {
|
|
19
|
+
const errMsg = "Invalid URL" + (targetUri.startsWith("http") ? "" : " (must include protocol)");
|
|
20
|
+
throw new error_1.FirebaseError(errMsg, { original: (0, error_1.getError)(ex) });
|
|
21
|
+
}
|
|
22
|
+
const fileFilterFn = createFilter(filePattern);
|
|
23
|
+
const nameFilterFn = createFilter(namePattern);
|
|
24
|
+
async function parseTestFilesRecursive(testDir) {
|
|
25
|
+
const items = (0, fsutils_1.listFiles)(testDir);
|
|
26
|
+
const results = [];
|
|
27
|
+
for (const item of items) {
|
|
28
|
+
const path = (0, path_1.join)(testDir, item);
|
|
29
|
+
if ((0, fsutils_1.dirExistsSync)(path)) {
|
|
30
|
+
results.push(...(await parseTestFilesRecursive(path)));
|
|
31
|
+
}
|
|
32
|
+
else if (fileFilterFn(path) && (0, fsutils_1.fileExistsSync)(path)) {
|
|
33
|
+
try {
|
|
34
|
+
const file = await (0, utils_1.readFileFromDirectory)(testDir, item);
|
|
35
|
+
const parsedFile = (0, utils_1.wrappedSafeLoad)(file.source);
|
|
36
|
+
const tests = parsedFile.tests;
|
|
37
|
+
const defaultConfig = parsedFile.defaultConfig;
|
|
38
|
+
if (!tests || !tests.length) {
|
|
39
|
+
logger_1.logger.info(`No tests found in ${path}. Ignoring.`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
for (const rawTestDef of parsedFile.tests) {
|
|
43
|
+
if (!nameFilterFn(rawTestDef.testName))
|
|
44
|
+
continue;
|
|
45
|
+
const testDef = toTestDef(rawTestDef, targetUri, defaultConfig);
|
|
46
|
+
results.push(testDef);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (ex) {
|
|
50
|
+
const errMsg = (0, error_1.getErrMsg)(ex);
|
|
51
|
+
const errDetails = errMsg ? `Error details: \n${errMsg}` : "";
|
|
52
|
+
logger_1.logger.info(`Unable to parse test file ${path}. Ignoring.${errDetails}`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
return parseTestFilesRecursive(dir);
|
|
60
|
+
}
|
|
61
|
+
exports.parseTestFiles = parseTestFiles;
|
|
62
|
+
function toTestDef(testDef, targetUri, defaultConfig) {
|
|
63
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
64
|
+
const steps = (_a = testDef.steps) !== null && _a !== void 0 ? _a : [];
|
|
65
|
+
const route = (_d = (_c = (_b = testDef.testConfig) === null || _b === void 0 ? void 0 : _b.route) !== null && _c !== void 0 ? _c : defaultConfig === null || defaultConfig === void 0 ? void 0 : defaultConfig.route) !== null && _d !== void 0 ? _d : "";
|
|
66
|
+
const browsers = (_g = (_f = (_e = testDef.testConfig) === null || _e === void 0 ? void 0 : _e.browsers) !== null && _f !== void 0 ? _f : defaultConfig === null || defaultConfig === void 0 ? void 0 : defaultConfig.browsers) !== null && _g !== void 0 ? _g : [types_1.Browser.CHROME];
|
|
67
|
+
return {
|
|
68
|
+
testCase: {
|
|
69
|
+
startUri: targetUri + route,
|
|
70
|
+
displayName: testDef.testName,
|
|
71
|
+
instructions: { steps },
|
|
72
|
+
},
|
|
73
|
+
testExecution: browsers.map((browser) => ({ config: { browser } })),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CompletionReason = exports.Browser = void 0;
|
|
4
|
+
var Browser;
|
|
5
|
+
(function (Browser) {
|
|
6
|
+
Browser["BROWSER_UNSPECIFIED"] = "BROWSER_UNSPECIFIED";
|
|
7
|
+
Browser["CHROME"] = "CHROME";
|
|
8
|
+
})(Browser = exports.Browser || (exports.Browser = {}));
|
|
9
|
+
var CompletionReason;
|
|
10
|
+
(function (CompletionReason) {
|
|
11
|
+
CompletionReason[CompletionReason["COMPLETION_REASON_UNSPECIFIED"] = 0] = "COMPLETION_REASON_UNSPECIFIED";
|
|
12
|
+
CompletionReason[CompletionReason["MAX_STEPS_REACHED"] = 1] = "MAX_STEPS_REACHED";
|
|
13
|
+
CompletionReason[CompletionReason["GOAL_FAILED"] = 2] = "GOAL_FAILED";
|
|
14
|
+
CompletionReason[CompletionReason["NO_ACTIONS_REQUIRED"] = 3] = "NO_ACTIONS_REQUIRED";
|
|
15
|
+
CompletionReason[CompletionReason["GOAL_INCONCLUSIVE"] = 4] = "GOAL_INCONCLUSIVE";
|
|
16
|
+
CompletionReason[CompletionReason["TEST_CANCELLED"] = 5] = "TEST_CANCELLED";
|
|
17
|
+
CompletionReason[CompletionReason["GOAL_SUCCEEDED"] = 6] = "GOAL_SUCCEEDED";
|
|
18
|
+
})(CompletionReason = exports.CompletionReason || (exports.CompletionReason = {}));
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
const requireAuth_1 = require("../requireAuth");
|
|
5
|
+
const command_1 = require("../command");
|
|
6
|
+
const requireConfig_1 = require("../requireConfig");
|
|
7
|
+
const logger_1 = require("../logger");
|
|
8
|
+
const clc = require("colorette");
|
|
9
|
+
const parseTestFiles_1 = require("../apptesting/parseTestFiles");
|
|
10
|
+
const ora = require("ora");
|
|
11
|
+
const invokeTests_1 = require("../apptesting/invokeTests");
|
|
12
|
+
const error_1 = require("../error");
|
|
13
|
+
const marked_1 = require("marked");
|
|
14
|
+
const projectUtils_1 = require("../projectUtils");
|
|
15
|
+
const utils_1 = require("../utils");
|
|
16
|
+
const apps_1 = require("../management/apps");
|
|
17
|
+
exports.command = new command_1.Command("apptesting:execute <target>")
|
|
18
|
+
.description("Run automated tests written in natural language driven by AI")
|
|
19
|
+
.option("--app <app_id>", "The app id of your Firebase web app. Optional if the project contains exactly one web app.")
|
|
20
|
+
.option("--test-file-pattern <pattern>", "Test file pattern. Only tests contained in files that match this pattern will be executed.")
|
|
21
|
+
.option("--test-name-pattern <pattern>", "Test name pattern. Only tests with names that match this pattern will be executed.")
|
|
22
|
+
.option("--tests-non-blocking", "Request test execution without waiting for them to complete.")
|
|
23
|
+
.before(requireAuth_1.requireAuth)
|
|
24
|
+
.before(requireConfig_1.requireConfig)
|
|
25
|
+
.action(async (target, options) => {
|
|
26
|
+
var _a, _b;
|
|
27
|
+
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
28
|
+
const appList = await (0, apps_1.listFirebaseApps)(projectId, apps_1.AppPlatform.WEB);
|
|
29
|
+
let app = appList.find((a) => a.appId === options.app);
|
|
30
|
+
if (!app && appList.length === 1) {
|
|
31
|
+
app = appList[0];
|
|
32
|
+
logger_1.logger.info(`No app specified, defaulting to ${app.appId}`);
|
|
33
|
+
}
|
|
34
|
+
else if (!app) {
|
|
35
|
+
throw new error_1.FirebaseError("Invalid app id");
|
|
36
|
+
}
|
|
37
|
+
const testDir = ((_a = options.config.src.apptesting) === null || _a === void 0 ? void 0 : _a.testDir) || "tests";
|
|
38
|
+
const tests = await (0, parseTestFiles_1.parseTestFiles)(testDir, target, options.testFilePattern, options.testNamePattern);
|
|
39
|
+
if (!tests.length) {
|
|
40
|
+
throw new error_1.FirebaseError("No tests found");
|
|
41
|
+
}
|
|
42
|
+
const invokeSpinner = ora("Requesting test execution");
|
|
43
|
+
invokeSpinner.start();
|
|
44
|
+
let invocationOperation;
|
|
45
|
+
try {
|
|
46
|
+
invocationOperation = await (0, invokeTests_1.invokeTests)(app.appId, target, tests);
|
|
47
|
+
invokeSpinner.text = "Test execution requested";
|
|
48
|
+
invokeSpinner.succeed();
|
|
49
|
+
}
|
|
50
|
+
catch (ex) {
|
|
51
|
+
invokeSpinner.fail("Failed to request test execution");
|
|
52
|
+
throw ex;
|
|
53
|
+
}
|
|
54
|
+
logger_1.logger.info(clc.bold(`\n${clc.white("===")} Running ${pluralizeTests(tests.length)}`));
|
|
55
|
+
const invocationId = (_b = invocationOperation.name) === null || _b === void 0 ? void 0 : _b.split("/").pop();
|
|
56
|
+
const appWebId = app.webId;
|
|
57
|
+
const url = (0, utils_1.consoleUrl)(projectId, `/apptesting/app/web:${appWebId}/invocations/${invocationId}`);
|
|
58
|
+
logger_1.logger.info(await (0, marked_1.marked)(`**Invocation ID:** ${invocationId}`));
|
|
59
|
+
logger_1.logger.info(await (0, marked_1.marked)(`View progress and results in the [Firebase Console](${url})`));
|
|
60
|
+
if (options.testsNonBlocking) {
|
|
61
|
+
logger_1.logger.info("Not waiting for results");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!invocationOperation.metadata) {
|
|
65
|
+
throw new error_1.FirebaseError("Invocation details unavailable");
|
|
66
|
+
}
|
|
67
|
+
const executionSpinner = ora(getOutput(invocationOperation.metadata));
|
|
68
|
+
executionSpinner.start();
|
|
69
|
+
const invocationOp = await (0, invokeTests_1.pollInvocationStatus)(invocationOperation.name, (operation) => {
|
|
70
|
+
if (!operation.done) {
|
|
71
|
+
executionSpinner.text = getOutput(operation.metadata);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const response = invocationOp.resource.testInvocation;
|
|
75
|
+
executionSpinner.text = `Testing complete\n${getOutput(response)}`;
|
|
76
|
+
if (response.failedExecutions || response.cancelledExecutions) {
|
|
77
|
+
executionSpinner.fail();
|
|
78
|
+
throw new error_1.FirebaseError("Testing complete with errors");
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
executionSpinner.succeed();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
function pluralizeTests(numTests) {
|
|
85
|
+
return `${numTests} test${numTests === 1 ? "" : "s"}`;
|
|
86
|
+
}
|
|
87
|
+
function getOutput(invocation) {
|
|
88
|
+
const output = [];
|
|
89
|
+
if (invocation.runningExecutions) {
|
|
90
|
+
output.push(`${pluralizeTests(invocation.runningExecutions)} running (this may take a while)...`);
|
|
91
|
+
}
|
|
92
|
+
if (invocation.succeededExecutions) {
|
|
93
|
+
output.push(`✔ ${pluralizeTests(invocation.succeededExecutions)} passed`);
|
|
94
|
+
}
|
|
95
|
+
if (invocation.failedExecutions) {
|
|
96
|
+
output.push(`✖ ${pluralizeTests(invocation.failedExecutions)} failed`);
|
|
97
|
+
}
|
|
98
|
+
if (invocation.cancelledExecutions) {
|
|
99
|
+
output.push(`⊝ ${pluralizeTests(invocation.cancelledExecutions)} cancelled`);
|
|
100
|
+
}
|
|
101
|
+
return output.length ? output.join("\n") : "Tests are starting";
|
|
102
|
+
}
|
package/lib/commands/index.js
CHANGED
|
@@ -228,6 +228,10 @@ function load(client) {
|
|
|
228
228
|
client.target.clear = loadCommand("target-clear");
|
|
229
229
|
client.target.remove = loadCommand("target-remove");
|
|
230
230
|
client.use = loadCommand("use");
|
|
231
|
+
if (experiments.isEnabled("apptesting")) {
|
|
232
|
+
client.apptesting = {};
|
|
233
|
+
client.apptesting.execute = loadCommand("apptesting-execute");
|
|
234
|
+
}
|
|
231
235
|
const t1 = process.hrtime.bigint();
|
|
232
236
|
const diffMS = (t1 - t0) / BigInt(1e6);
|
|
233
237
|
if (diffMS > 100) {
|
package/lib/commands/init.js
CHANGED
|
@@ -99,6 +99,13 @@ if ((0, experiments_1.isEnabled)("genkit")) {
|
|
|
99
99
|
...choices.slice(2),
|
|
100
100
|
];
|
|
101
101
|
}
|
|
102
|
+
if ((0, experiments_1.isEnabled)("apptesting")) {
|
|
103
|
+
choices.push({
|
|
104
|
+
value: "apptesting",
|
|
105
|
+
name: "App Testing: create a smoke test",
|
|
106
|
+
checked: false,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
102
109
|
const featureNames = choices.map((choice) => choice.value);
|
|
103
110
|
const HELP = `Interactively configure the current directory as a Firebase project or initialize new features in an already configured Firebase project directory.
|
|
104
111
|
|
|
@@ -13,15 +13,7 @@ async function default_1(context, options) {
|
|
|
13
13
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
14
14
|
await (0, apphosting_1.ensureApiEnabled)(options);
|
|
15
15
|
await (0, backend_1.ensureRequiredApisEnabled)(projectId);
|
|
16
|
-
|
|
17
|
-
await (0, backend_1.ensureAppHostingComputeServiceAccount)(projectId, "");
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
if (err.status === 400) {
|
|
21
|
-
(0, utils_1.logLabeledWarning)("apphosting", "Your App Hosting compute service account is still being provisioned. Please try again in a few moments.");
|
|
22
|
-
}
|
|
23
|
-
throw err;
|
|
24
|
-
}
|
|
16
|
+
await (0, backend_1.ensureAppHostingComputeServiceAccount)(projectId, "");
|
|
25
17
|
context.backendConfigs = new Map();
|
|
26
18
|
context.backendLocations = new Map();
|
|
27
19
|
context.backendStorageUris = new Map();
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.detectFromOutputPath = exports.detectFromPort = exports.detectFromYaml = exports.yamlToBuild = exports.getFunctionDiscoveryTimeout = void 0;
|
|
4
4
|
const node_fetch_1 = require("node-fetch");
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const yaml = require("yaml");
|
|
8
|
-
const util_1 = require("util");
|
|
9
8
|
const logger_1 = require("../../../../logger");
|
|
10
9
|
const api = require("../../.../../../../api");
|
|
11
10
|
const v1alpha1 = require("./v1alpha1");
|
|
12
11
|
const error_1 = require("../../../../error");
|
|
13
|
-
exports.readFileAsync = (0, util_1.promisify)(fs.readFile);
|
|
14
12
|
const TIMEOUT_OVERRIDE_ENV_VAR = "FUNCTIONS_DISCOVERY_TIMEOUT";
|
|
15
13
|
function getFunctionDiscoveryTimeout() {
|
|
16
14
|
return +(process.env[TIMEOUT_OVERRIDE_ENV_VAR] || 0) * 1000;
|
|
@@ -34,7 +32,7 @@ exports.yamlToBuild = yamlToBuild;
|
|
|
34
32
|
async function detectFromYaml(directory, project, runtime) {
|
|
35
33
|
let text;
|
|
36
34
|
try {
|
|
37
|
-
text = await
|
|
35
|
+
text = await fs.promises.readFile(path.join(directory, "functions.yaml"), "utf8");
|
|
38
36
|
}
|
|
39
37
|
catch (err) {
|
|
40
38
|
if (err.code === "ENOENT") {
|
|
@@ -96,3 +94,54 @@ async function detectFromPort(port, project, runtime, initialDelay = 0, timeout
|
|
|
96
94
|
return yamlToBuild(parsed, project, api.functionsDefaultRegion(), runtime);
|
|
97
95
|
}
|
|
98
96
|
exports.detectFromPort = detectFromPort;
|
|
97
|
+
async function detectFromOutputPath(childProcess, manifestPath, project, runtime, timeout = 10000) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
var _a;
|
|
100
|
+
let stderrBuffer = "";
|
|
101
|
+
let resolved = false;
|
|
102
|
+
const discoveryTimeout = getFunctionDiscoveryTimeout() || timeout;
|
|
103
|
+
const timer = setTimeout(() => {
|
|
104
|
+
if (!resolved) {
|
|
105
|
+
resolved = true;
|
|
106
|
+
reject(new error_1.FirebaseError(`User code failed to load. Cannot determine backend specification. Timeout after ${discoveryTimeout}ms`));
|
|
107
|
+
}
|
|
108
|
+
}, discoveryTimeout);
|
|
109
|
+
(_a = childProcess.stderr) === null || _a === void 0 ? void 0 : _a.on("data", (chunk) => {
|
|
110
|
+
stderrBuffer += chunk.toString();
|
|
111
|
+
});
|
|
112
|
+
childProcess.on("exit", async (code) => {
|
|
113
|
+
var _a;
|
|
114
|
+
if (!resolved) {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
resolved = true;
|
|
117
|
+
if (code !== 0 && code !== null) {
|
|
118
|
+
const errorMessage = (_a = stderrBuffer.trim()) !== null && _a !== void 0 ? _a : `Discovery process exited with code ${code}`;
|
|
119
|
+
reject(new error_1.FirebaseError(`User code failed to load. Cannot determine backend specification.\n${errorMessage}`));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
try {
|
|
123
|
+
const manifestContent = await fs.promises.readFile(manifestPath, "utf8");
|
|
124
|
+
const parsed = yaml.parse(manifestContent);
|
|
125
|
+
resolve(yamlToBuild(parsed, project, api.functionsDefaultRegion(), runtime));
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err.code === "ENOENT") {
|
|
129
|
+
reject(new error_1.FirebaseError(`Discovery process completed but no function manifest was found at ${manifestPath}`));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
reject(new error_1.FirebaseError(`Failed to read or parse manifest file: ${err.message}`));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
childProcess.on("error", (err) => {
|
|
139
|
+
if (!resolved) {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
resolved = true;
|
|
142
|
+
reject(new error_1.FirebaseError(`Discovery process failed: ${err.message}`));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
exports.detectFromOutputPath = detectFromOutputPath;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Delegate = exports.tryCreateDelegate = void 0;
|
|
4
|
-
const
|
|
4
|
+
const os = require("os");
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const portfinder = require("portfinder");
|
|
@@ -22,7 +22,10 @@ const MIN_FUNCTIONS_SDK_VERSION = "3.20.0";
|
|
|
22
22
|
const MIN_FUNCTIONS_SDK_VERSION_FOR_EXTENSIONS_FEATURES = "5.1.0";
|
|
23
23
|
async function tryCreateDelegate(context) {
|
|
24
24
|
const packageJsonPath = path.join(context.sourceDir, "package.json");
|
|
25
|
-
|
|
25
|
+
try {
|
|
26
|
+
await fs.promises.access(packageJsonPath);
|
|
27
|
+
}
|
|
28
|
+
catch (_a) {
|
|
26
29
|
logger_1.logger.debug("Customer code is not Node");
|
|
27
30
|
return undefined;
|
|
28
31
|
}
|
|
@@ -93,12 +96,7 @@ class Delegate {
|
|
|
93
96
|
watch() {
|
|
94
97
|
return Promise.resolve(() => Promise.resolve());
|
|
95
98
|
}
|
|
96
|
-
|
|
97
|
-
var _a, _b;
|
|
98
|
-
const env = Object.assign(Object.assign({}, envs), { PORT: port, FUNCTIONS_CONTROL_API: "true", HOME: process.env.HOME, PATH: process.env.PATH, NODE_ENV: process.env.NODE_ENV, __FIREBASE_FRAMEWORKS_ENTRY__: process.env.__FIREBASE_FRAMEWORKS_ENTRY__ });
|
|
99
|
-
if (Object.keys(config || {}).length) {
|
|
100
|
-
env.CLOUD_RUNTIME_CONFIG = JSON.stringify(config);
|
|
101
|
-
}
|
|
99
|
+
findFunctionsBinary() {
|
|
102
100
|
const sourceNodeModulesPath = path.join(this.sourceDir, "node_modules");
|
|
103
101
|
const projectNodeModulesPath = path.join(this.projectDir, "node_modules");
|
|
104
102
|
const sdkPath = require.resolve("firebase-functions", { paths: [this.sourceDir] });
|
|
@@ -113,40 +111,56 @@ class Delegate {
|
|
|
113
111
|
const binPath = path.join(nodeModulesPath, ".bin", "firebase-functions");
|
|
114
112
|
if ((0, fsutils_1.fileExistsSync)(binPath)) {
|
|
115
113
|
logger_1.logger.debug(`Found firebase-functions binary at '${binPath}'`);
|
|
116
|
-
|
|
117
|
-
env,
|
|
118
|
-
cwd: this.sourceDir,
|
|
119
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
120
|
-
});
|
|
121
|
-
(_a = childProcess.stdout) === null || _a === void 0 ? void 0 : _a.on("data", (chunk) => {
|
|
122
|
-
logger_1.logger.info(chunk.toString("utf8"));
|
|
123
|
-
});
|
|
124
|
-
(_b = childProcess.stderr) === null || _b === void 0 ? void 0 : _b.on("data", (chunk) => {
|
|
125
|
-
logger_1.logger.error(chunk.toString("utf8"));
|
|
126
|
-
});
|
|
127
|
-
return Promise.resolve(async () => {
|
|
128
|
-
const p = new Promise((resolve, reject) => {
|
|
129
|
-
childProcess.once("exit", resolve);
|
|
130
|
-
childProcess.once("error", reject);
|
|
131
|
-
});
|
|
132
|
-
try {
|
|
133
|
-
await (0, node_fetch_1.default)(`http://localhost:${port}/__/quitquitquit`);
|
|
134
|
-
}
|
|
135
|
-
catch (e) {
|
|
136
|
-
logger_1.logger.debug("Failed to call quitquitquit. This often means the server failed to start", e);
|
|
137
|
-
}
|
|
138
|
-
setTimeout(() => {
|
|
139
|
-
if (!childProcess.killed) {
|
|
140
|
-
childProcess.kill("SIGKILL");
|
|
141
|
-
}
|
|
142
|
-
}, 10000);
|
|
143
|
-
return p;
|
|
144
|
-
});
|
|
114
|
+
return binPath;
|
|
145
115
|
}
|
|
146
116
|
}
|
|
147
117
|
throw new error_1.FirebaseError("Failed to find location of Firebase Functions SDK. " +
|
|
148
118
|
"Please file a bug on Github (https://github.com/firebase/firebase-tools/).");
|
|
149
119
|
}
|
|
120
|
+
spawnFunctionsProcess(config, envs) {
|
|
121
|
+
var _a, _b;
|
|
122
|
+
const env = Object.assign(Object.assign({}, envs), { FUNCTIONS_CONTROL_API: "true", HOME: process.env.HOME, PATH: process.env.PATH, NODE_ENV: process.env.NODE_ENV, __FIREBASE_FRAMEWORKS_ENTRY__: process.env.__FIREBASE_FRAMEWORKS_ENTRY__ });
|
|
123
|
+
if (Object.keys(config || {}).length) {
|
|
124
|
+
env.CLOUD_RUNTIME_CONFIG = JSON.stringify(config);
|
|
125
|
+
}
|
|
126
|
+
const binPath = this.findFunctionsBinary();
|
|
127
|
+
const childProcess = spawn(binPath, [this.sourceDir], {
|
|
128
|
+
env,
|
|
129
|
+
cwd: this.sourceDir,
|
|
130
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
131
|
+
});
|
|
132
|
+
(_a = childProcess.stdout) === null || _a === void 0 ? void 0 : _a.on("data", (chunk) => {
|
|
133
|
+
logger_1.logger.info(chunk.toString("utf8"));
|
|
134
|
+
});
|
|
135
|
+
(_b = childProcess.stderr) === null || _b === void 0 ? void 0 : _b.on("data", (chunk) => {
|
|
136
|
+
logger_1.logger.error(chunk.toString("utf8"));
|
|
137
|
+
});
|
|
138
|
+
return childProcess;
|
|
139
|
+
}
|
|
140
|
+
execAdmin(config, envs, manifestPath) {
|
|
141
|
+
return this.spawnFunctionsProcess(config, Object.assign(Object.assign({}, envs), { FUNCTIONS_MANIFEST_OUTPUT_PATH: manifestPath }));
|
|
142
|
+
}
|
|
143
|
+
serveAdmin(config, envs, port) {
|
|
144
|
+
const childProcess = this.spawnFunctionsProcess(config, Object.assign(Object.assign({}, envs), { PORT: port }));
|
|
145
|
+
return Promise.resolve(async () => {
|
|
146
|
+
const p = new Promise((resolve, reject) => {
|
|
147
|
+
childProcess.once("exit", resolve);
|
|
148
|
+
childProcess.once("error", reject);
|
|
149
|
+
});
|
|
150
|
+
try {
|
|
151
|
+
await (0, node_fetch_1.default)(`http://localhost:${port}/__/quitquitquit`);
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
logger_1.logger.debug("Failed to call quitquitquit. This often means the server failed to start", e);
|
|
155
|
+
}
|
|
156
|
+
setTimeout(() => {
|
|
157
|
+
if (!childProcess.killed) {
|
|
158
|
+
childProcess.kill("SIGKILL");
|
|
159
|
+
}
|
|
160
|
+
}, 10000);
|
|
161
|
+
return p;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
150
164
|
async discoverBuild(config, env) {
|
|
151
165
|
if (!semver.valid(this.sdkVersion)) {
|
|
152
166
|
logger_1.logger.debug(`Could not parse firebase-functions version '${this.sdkVersion}' into semver. Falling back to parseTriggers.`);
|
|
@@ -163,14 +177,30 @@ class Delegate {
|
|
|
163
177
|
}
|
|
164
178
|
let discovered = await discovery.detectFromYaml(this.sourceDir, this.projectId, this.runtime);
|
|
165
179
|
if (!discovered) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
180
|
+
const discoveryPath = process.env.FIREBASE_FUNCTIONS_DISCOVERY_OUTPUT_PATH;
|
|
181
|
+
if (!discoveryPath) {
|
|
182
|
+
const basePort = 8000 + (0, utils_1.randomInt)(0, 1000);
|
|
183
|
+
const port = await portfinder.getPortPromise({ port: basePort });
|
|
184
|
+
const kill = await this.serveAdmin(config, env, port.toString());
|
|
185
|
+
try {
|
|
186
|
+
discovered = await discovery.detectFromPort(port, this.projectId, this.runtime);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
await kill();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (discoveryPath === "true") {
|
|
193
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "firebase-discovery-"));
|
|
194
|
+
const manifestPath = path.join(tempDir, "functions.yaml");
|
|
195
|
+
logger_1.logger.debug(`Writing functions discovery manifest to temporary file ${manifestPath}`);
|
|
196
|
+
const childProcess = this.execAdmin(config, env, manifestPath);
|
|
197
|
+
discovered = await discovery.detectFromOutputPath(childProcess, manifestPath, this.projectId, this.runtime);
|
|
171
198
|
}
|
|
172
|
-
|
|
173
|
-
|
|
199
|
+
else {
|
|
200
|
+
const manifestPath = path.join(discoveryPath, "functions.yaml");
|
|
201
|
+
logger_1.logger.debug(`Writing functions discovery manifest to ${manifestPath}`);
|
|
202
|
+
const childProcess = this.execAdmin(config, env, manifestPath);
|
|
203
|
+
discovered = await discovery.detectFromOutputPath(childProcess, manifestPath, this.projectId, this.runtime);
|
|
174
204
|
}
|
|
175
205
|
}
|
|
176
206
|
return discovered;
|
|
@@ -44,7 +44,6 @@ BEGIN
|
|
|
44
44
|
END
|
|
45
45
|
$do$;`;
|
|
46
46
|
const decoder = new node_string_decoder_1.StringDecoder();
|
|
47
|
-
const pgliteDebugLog = fs.createWriteStream("pglite-debug.log");
|
|
48
47
|
class PostgresServer {
|
|
49
48
|
async createPGServer(host = "127.0.0.1", port) {
|
|
50
49
|
const getDb = this.getDb.bind(this);
|
|
@@ -244,6 +243,7 @@ class PGliteExtendedQueryPatch {
|
|
|
244
243
|
this.connection = connection;
|
|
245
244
|
this.isExtendedQuery = false;
|
|
246
245
|
this.eqpErrored = false;
|
|
246
|
+
this.pgliteDebugLog = fs.createWriteStream("pglite-debug.log");
|
|
247
247
|
}
|
|
248
248
|
filterResponse(message, response) {
|
|
249
249
|
return __asyncGenerator(this, arguments, function* filterResponse_1() {
|
|
@@ -254,7 +254,7 @@ class PGliteExtendedQueryPatch {
|
|
|
254
254
|
pg_gateway_1.FrontendMessageCode.Close,
|
|
255
255
|
];
|
|
256
256
|
const decoded = decoder.write(message);
|
|
257
|
-
pgliteDebugLog.write("Front: " + decoded);
|
|
257
|
+
this.pgliteDebugLog.write("Front: " + decoded);
|
|
258
258
|
if (pipelineStartMessages.includes(message[0])) {
|
|
259
259
|
this.isExtendedQuery = true;
|
|
260
260
|
}
|
|
@@ -275,10 +275,10 @@ class PGliteExtendedQueryPatch {
|
|
|
275
275
|
this.eqpErrored = true;
|
|
276
276
|
}
|
|
277
277
|
if (this.isExtendedQuery && bm[0] === pg_gateway_1.BackendMessageCode.ReadyForQuery) {
|
|
278
|
-
pgliteDebugLog.write("Filtered: " + decoder.write(bm));
|
|
278
|
+
this.pgliteDebugLog.write("Filtered: " + decoder.write(bm));
|
|
279
279
|
continue;
|
|
280
280
|
}
|
|
281
|
-
pgliteDebugLog.write("Sent: " + decoder.write(bm));
|
|
281
|
+
this.pgliteDebugLog.write("Sent: " + decoder.write(bm));
|
|
282
282
|
yield yield __await(bm);
|
|
283
283
|
}
|
|
284
284
|
finally {
|