firebase-tools 15.10.0 → 15.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/appdistribution/client.js +17 -0
- package/lib/apphosting/utils.js +14 -0
- package/lib/apptesting/parseTestFiles.js +25 -13
- package/lib/commands/apptesting.js +35 -16
- package/lib/commands/index.js +5 -5
- package/lib/commands/studio-export.js +2 -2
- package/lib/deploy/apphosting/util.js +1 -1
- package/lib/deploy/firestore/prepare.js +17 -0
- package/lib/deploy/functions/prepareFunctionsUpload.js +1 -1
- package/lib/deploy/functions/runtimes/dart/index.js +282 -0
- package/lib/deploy/functions/runtimes/index.js +1 -1
- package/lib/deploy/functions/runtimes/supported/index.js +4 -0
- package/lib/emulator/apphosting/serve.js +13 -15
- package/lib/emulator/downloadableEmulatorInfo.json +37 -37
- package/lib/emulator/functionsEmulator.js +103 -24
- package/lib/emulator/functionsRuntimeWorker.js +21 -18
- package/lib/firebase_studio/migrate.js +274 -95
- package/lib/init/features/functions/dart.js +31 -0
- package/lib/init/features/functions/index.js +14 -0
- package/lib/mcp/index.js +18 -6
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/schema/firebase-config.json +7 -0
- package/templates/firebase-studio-export/readme_template.md +13 -7
- package/templates/firebase-studio-export/system_instructions_template.md +14 -0
- package/templates/firebase-studio-export/workflows/cleanup.md +20 -0
- package/templates/init/apphosting/apphosting.yaml +1 -0
- package/templates/init/functions/dart/_gitignore +11 -0
- package/templates/init/functions/dart/pubspec.yaml +14 -0
- package/templates/init/functions/dart/server.dart +15 -0
- package/lib/deploy/functions/runtimes/dart.js +0 -42
- package/templates/firebase-studio-export/workflows/startup_workflow.md +0 -12
|
@@ -298,5 +298,22 @@ class AppDistributionClient {
|
|
|
298
298
|
throw new error_1.FirebaseError(`Failed to upsert test cases ${(0, error_1.getErrMsg)(err)}`);
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
|
+
async getLatestRelease(appName) {
|
|
302
|
+
try {
|
|
303
|
+
const response = await this.appDistroV1Client.get(`/${appName}/releases`, {
|
|
304
|
+
queryParams: {
|
|
305
|
+
pageSize: "1",
|
|
306
|
+
orderBy: "createTime desc",
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
if (!response.body.releases?.length) {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
return response.body.releases[0];
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
throw new error_1.FirebaseError(`Failed to get latest release for app ${appName}: ${(0, error_1.getErrMsg)(err)}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
301
318
|
}
|
|
302
319
|
exports.AppDistributionClient = AppDistributionClient;
|
package/lib/apphosting/utils.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getEnvironmentName = getEnvironmentName;
|
|
4
4
|
exports.promptForAppHostingYaml = promptForAppHostingYaml;
|
|
5
|
+
exports.getAutoinitEnvVars = getAutoinitEnvVars;
|
|
5
6
|
const error_1 = require("../error");
|
|
6
7
|
const config_1 = require("./config");
|
|
7
8
|
const prompt = require("../prompt");
|
|
@@ -36,3 +37,16 @@ async function promptForAppHostingYaml(apphostingFileNameToPathMap, promptMessag
|
|
|
36
37
|
});
|
|
37
38
|
return fileToExportPath;
|
|
38
39
|
}
|
|
40
|
+
function getAutoinitEnvVars(webappConfig) {
|
|
41
|
+
if (!webappConfig) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
FIREBASE_WEBAPP_CONFIG: JSON.stringify(webappConfig),
|
|
46
|
+
FIREBASE_CONFIG: JSON.stringify({
|
|
47
|
+
databaseURL: webappConfig.databaseURL,
|
|
48
|
+
storageBucket: webappConfig.storageBucket,
|
|
49
|
+
projectId: webappConfig.projectId,
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseTestFiles = parseTestFiles;
|
|
4
|
+
exports.pluralizeTests = pluralizeTests;
|
|
4
5
|
const fsutils_1 = require("../fsutils");
|
|
5
6
|
const path_1 = require("path");
|
|
6
7
|
const logger_1 = require("../logger");
|
|
@@ -27,8 +28,8 @@ async function parseTestFiles(dir, targetUri, filePattern, namePattern) {
|
|
|
27
28
|
}
|
|
28
29
|
return accumulator;
|
|
29
30
|
}, {});
|
|
30
|
-
const fileFilterFn = createFilter(filePattern);
|
|
31
|
-
const nameFilterFn = createFilter(namePattern);
|
|
31
|
+
const fileFilterFn = createFilter(filePattern, "file pattern");
|
|
32
|
+
const nameFilterFn = createFilter(namePattern, "test name pattern");
|
|
32
33
|
const filteredInvocations = files
|
|
33
34
|
.filter((file) => fileFilterFn(file.path))
|
|
34
35
|
.flatMap((file) => file.invocations)
|
|
@@ -61,33 +62,41 @@ async function parseTestFiles(dir, targetUri, filePattern, namePattern) {
|
|
|
61
62
|
};
|
|
62
63
|
});
|
|
63
64
|
}
|
|
64
|
-
function createFilter(pattern) {
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
function createFilter(pattern, context) {
|
|
66
|
+
try {
|
|
67
|
+
const regex = pattern ? new RegExp(pattern) : undefined;
|
|
68
|
+
return (s) => !regex || regex.test(s);
|
|
69
|
+
}
|
|
70
|
+
catch (ex) {
|
|
71
|
+
if (ex instanceof SyntaxError) {
|
|
72
|
+
const errMsg = context ? `Invalid ${context} regex: ${pattern}` : `Invalid regex: ${pattern}`;
|
|
73
|
+
throw new error_1.FirebaseError(errMsg, { original: (0, error_1.getError)(ex) });
|
|
74
|
+
}
|
|
75
|
+
throw ex;
|
|
76
|
+
}
|
|
67
77
|
}
|
|
68
78
|
async function parseTestFilesRecursive(params) {
|
|
69
79
|
const testDir = params.testDir;
|
|
70
80
|
const targetUri = params.targetUri;
|
|
71
|
-
const
|
|
81
|
+
const filenames = (0, fsutils_1.listFiles)(testDir);
|
|
72
82
|
const results = [];
|
|
73
|
-
for (const
|
|
74
|
-
const path = (0, path_1.join)(testDir,
|
|
83
|
+
for (const filename of filenames) {
|
|
84
|
+
const path = (0, path_1.join)(testDir, filename);
|
|
75
85
|
if ((0, fsutils_1.dirExistsSync)(path)) {
|
|
76
86
|
results.push(...(await parseTestFilesRecursive({ testDir: path, targetUri })));
|
|
77
87
|
}
|
|
78
|
-
else if ((0, fsutils_1.fileExistsSync)(path)) {
|
|
88
|
+
else if ((0, fsutils_1.fileExistsSync)(path) && (path.endsWith(".yaml") || path.endsWith(".yml"))) {
|
|
79
89
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
logger_1.logger.debug(`Reading ${path}.`);
|
|
91
|
+
const file = await (0, utils_1.readFileFromDirectory)(testDir, filename);
|
|
82
92
|
const parsedFile = (0, utils_1.wrappedSafeLoad)(file.source);
|
|
83
|
-
logger_1.logger.debug(`Parsed the file.`);
|
|
84
93
|
const tests = parsedFile.tests;
|
|
85
|
-
logger_1.logger.debug(`There are ${tests.length} tests.`);
|
|
86
94
|
const defaultConfig = parsedFile.defaultConfig;
|
|
87
95
|
if (!tests || !tests.length) {
|
|
88
96
|
logger_1.logger.debug(`No tests found in ${path}. Ignoring.`);
|
|
89
97
|
continue;
|
|
90
98
|
}
|
|
99
|
+
logger_1.logger.debug(`File contains ${pluralizeTests(tests.length)}.`);
|
|
91
100
|
const invocations = [];
|
|
92
101
|
for (const rawTestDef of tests) {
|
|
93
102
|
const invocation = toTestCaseInvocation(rawTestDef, targetUri, defaultConfig);
|
|
@@ -105,6 +114,9 @@ async function parseTestFilesRecursive(params) {
|
|
|
105
114
|
}
|
|
106
115
|
return results;
|
|
107
116
|
}
|
|
117
|
+
function pluralizeTests(numTests) {
|
|
118
|
+
return `${numTests} test${numTests === 1 ? "" : "s"}`;
|
|
119
|
+
}
|
|
108
120
|
function toTestCaseInvocation(testDef, targetUri, defaultConfig) {
|
|
109
121
|
const steps = testDef.steps ?? [];
|
|
110
122
|
const route = testDef.testConfig?.route ?? defaultConfig?.route ?? "";
|
|
@@ -3,14 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.command = void 0;
|
|
4
4
|
const requireAuth_1 = require("../requireAuth");
|
|
5
5
|
const command_1 = require("../command");
|
|
6
|
-
const logger_1 = require("../logger");
|
|
7
|
-
const clc = require("colorette");
|
|
8
6
|
const parseTestFiles_1 = require("../apptesting/parseTestFiles");
|
|
9
7
|
const ora = require("ora");
|
|
10
8
|
const error_1 = require("../error");
|
|
11
9
|
const client_1 = require("../appdistribution/client");
|
|
12
10
|
const distribution_1 = require("../appdistribution/distribution");
|
|
13
11
|
const options_parser_util_1 = require("../appdistribution/options-parser-util");
|
|
12
|
+
const utils = require("../utils");
|
|
13
|
+
const fsutils_1 = require("../fsutils");
|
|
14
|
+
const path = require("path");
|
|
14
15
|
const defaultDevices = [
|
|
15
16
|
{
|
|
16
17
|
model: "MediumPhone.arm",
|
|
@@ -19,44 +20,62 @@ const defaultDevices = [
|
|
|
19
20
|
orientation: "portrait",
|
|
20
21
|
},
|
|
21
22
|
];
|
|
22
|
-
exports.command = new command_1.Command("apptesting:execute
|
|
23
|
+
exports.command = new command_1.Command("apptesting:execute [release-binary-file]")
|
|
23
24
|
.description("Run mobile automated tests written in natural language driven by AI")
|
|
24
25
|
.option("--app <app_id>", "The app id of your Firebase web app. Optional if the project contains exactly one web app.")
|
|
25
26
|
.option("--test-file-pattern <pattern>", "Test file pattern. Only tests contained in files that match this pattern will be executed.")
|
|
26
27
|
.option("--test-name-pattern <pattern>", "Test name pattern. Only tests with names that match this pattern will be executed.")
|
|
27
|
-
.option("--test-dir <test_dir>", "Directory where tests can be found.")
|
|
28
|
-
.option("--test-devices <string>", "
|
|
29
|
-
.option("--test-devices-file <string>", "
|
|
28
|
+
.option("--test-dir <test_dir>", "Directory where tests can be found. Defaults to './tests'.")
|
|
29
|
+
.option("--test-devices <string>", "Semicolon-separated list of devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.")
|
|
30
|
+
.option("--test-devices-file <string>", "Path to file containing a list of semicolon- or newline-separated devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.")
|
|
31
|
+
.option("--test-non-blocking", "Run automated tests without waiting for them to complete. Visit the Firebase console for the test results.")
|
|
30
32
|
.before(requireAuth_1.requireAuth)
|
|
31
33
|
.action(async (target, options) => {
|
|
32
34
|
const appName = (0, options_parser_util_1.getAppName)(options);
|
|
33
|
-
const testDir = options.testDir || "tests";
|
|
35
|
+
const testDir = path.resolve(options.testDir || "tests");
|
|
36
|
+
if (!(0, fsutils_1.dirExistsSync)(testDir)) {
|
|
37
|
+
throw new error_1.FirebaseError(`Tests directory not found: ${testDir}. Use the --test-dir flag to choose a different directory.`);
|
|
38
|
+
}
|
|
34
39
|
const tests = await (0, parseTestFiles_1.parseTestFiles)(testDir, undefined, options.testFilePattern, options.testNamePattern);
|
|
35
40
|
const testDevices = (0, options_parser_util_1.parseTestDevices)(options.testDevices, options.testDevicesFile);
|
|
36
41
|
if (!tests.length) {
|
|
37
|
-
throw new error_1.FirebaseError(
|
|
42
|
+
throw new error_1.FirebaseError(`No tests found under test directory ${testDir}`);
|
|
38
43
|
}
|
|
44
|
+
utils.logBullet(`Found ${(0, parseTestFiles_1.pluralizeTests)(tests.length)} to run under test directory ${testDir}`);
|
|
39
45
|
const invokeSpinner = ora("Requesting test execution");
|
|
46
|
+
const client = new client_1.AppDistributionClient();
|
|
40
47
|
let releaseTests;
|
|
41
48
|
let release;
|
|
42
49
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
if (target) {
|
|
51
|
+
release = await (0, distribution_1.upload)(client, appName, new distribution_1.Distribution(target));
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
utils.logBullet("release-binary-file not provided, using the latest App Distribution release.");
|
|
55
|
+
const latestRelease = await client.getLatestRelease(appName);
|
|
56
|
+
if (!latestRelease) {
|
|
57
|
+
throw new error_1.FirebaseError(`No app binary found for ${appName}. Call apptesting:execute with a local app binary file, or upload a release to App Distribution.`);
|
|
58
|
+
}
|
|
59
|
+
release = latestRelease;
|
|
60
|
+
utils.logBullet(`Using release ${release.displayVersion} created at ${release.createTime}`);
|
|
61
|
+
}
|
|
45
62
|
invokeSpinner.start();
|
|
46
63
|
releaseTests = await invokeTests(client, release.name, tests, !testDevices.length ? defaultDevices : testDevices);
|
|
47
|
-
invokeSpinner.text =
|
|
64
|
+
invokeSpinner.text = `${(0, parseTestFiles_1.pluralizeTests)(releaseTests.length)} started successfully!`;
|
|
48
65
|
invokeSpinner.succeed();
|
|
49
66
|
}
|
|
50
67
|
catch (ex) {
|
|
51
68
|
invokeSpinner.fail("Failed to request test execution");
|
|
52
69
|
throw ex;
|
|
53
70
|
}
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
if (options.testNonBlocking) {
|
|
72
|
+
utils.logBullet(`View progress and results in the Firebase Console:\n${release.firebaseConsoleUri}`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
await (0, distribution_1.awaitTestResults)(releaseTests, client);
|
|
76
|
+
utils.logBullet(`View detailed results in the Firebase Console:\n${release.firebaseConsoleUri}`);
|
|
77
|
+
}
|
|
56
78
|
});
|
|
57
|
-
function pluralizeTests(numTests) {
|
|
58
|
-
return `${numTests} test${numTests === 1 ? "" : "s"}`;
|
|
59
|
-
}
|
|
60
79
|
async function invokeTests(client, releaseName, testDefs, devices) {
|
|
61
80
|
try {
|
|
62
81
|
const releaseTests = [];
|
package/lib/commands/index.js
CHANGED
|
@@ -33,6 +33,11 @@ function load(client) {
|
|
|
33
33
|
client.appdistribution.testCases = {};
|
|
34
34
|
client.appdistribution.testCases.export = loadCommand("appdistribution-testcases-export");
|
|
35
35
|
client.appdistribution.testCases.import = loadCommand("appdistribution-testcases-import");
|
|
36
|
+
client.apptesting = {};
|
|
37
|
+
client.apptesting.execute = loadCommand("apptesting");
|
|
38
|
+
if (experiments.isEnabled("apptesting")) {
|
|
39
|
+
client.apptesting.wata = loadCommand("apptesting-wata");
|
|
40
|
+
}
|
|
36
41
|
client.apps = {};
|
|
37
42
|
client.apps.create = loadCommand("apps-create");
|
|
38
43
|
client.apps.list = loadCommand("apps-list");
|
|
@@ -254,11 +259,6 @@ function load(client) {
|
|
|
254
259
|
client.target.clear = loadCommand("target-clear");
|
|
255
260
|
client.target.remove = loadCommand("target-remove");
|
|
256
261
|
client.use = loadCommand("use");
|
|
257
|
-
client.apptesting = {};
|
|
258
|
-
client.apptesting.execute = loadCommand("apptesting");
|
|
259
|
-
if (experiments.isEnabled("apptesting")) {
|
|
260
|
-
client.apptesting.wata = loadCommand("apptesting-wata");
|
|
261
|
-
}
|
|
262
262
|
const t1 = process.hrtime.bigint();
|
|
263
263
|
const diffMS = (t1 - t0) / BigInt(1e6);
|
|
264
264
|
if (diffMS > 100) {
|
|
@@ -8,12 +8,12 @@ const path = require("path");
|
|
|
8
8
|
const error_1 = require("../error");
|
|
9
9
|
const unzip_1 = require("../unzip");
|
|
10
10
|
const fs = require("fs");
|
|
11
|
-
exports.command = new command_1.Command("studio:export
|
|
11
|
+
exports.command = new command_1.Command("studio:export [path]")
|
|
12
12
|
.description("Bootstrap Firebase Studio apps for migration to Antigravity. Run on the unzipped folder from the Firebase Studio download, or directly on the downloaded zip file.")
|
|
13
13
|
.option("--no-start-antigravity", "skip starting the Antigravity IDE after migration")
|
|
14
14
|
.action(async (exportPath, options) => {
|
|
15
15
|
if (!exportPath) {
|
|
16
|
-
throw new error_1.FirebaseError("Must specify
|
|
16
|
+
throw new error_1.FirebaseError("Must specify the path to the Firebase Studio downloaded zip file or the unzipped folder path.", { exit: 1 });
|
|
17
17
|
}
|
|
18
18
|
let rootPath = path.resolve(exportPath);
|
|
19
19
|
if (fs.existsSync(rootPath) && fs.statSync(rootPath).isFile() && rootPath.endsWith(".zip")) {
|
|
@@ -35,7 +35,7 @@ async function createArchive(config, rootDir, targetSubDir) {
|
|
|
35
35
|
await pipeAsync(archive, fileStream);
|
|
36
36
|
}
|
|
37
37
|
catch (err) {
|
|
38
|
-
throw new error_1.FirebaseError(
|
|
38
|
+
throw new error_1.FirebaseError(`Could not read source directory. Remove links and shortcuts and try again. Original: ${err}`, { original: err, exit: 1 });
|
|
39
39
|
}
|
|
40
40
|
return tmpFile;
|
|
41
41
|
}
|
|
@@ -52,6 +52,21 @@ async function createDatabase(context, options) {
|
|
|
52
52
|
}
|
|
53
53
|
edition = upperEdition;
|
|
54
54
|
}
|
|
55
|
+
let firestoreDataAccessMode;
|
|
56
|
+
let mongodbCompatibleDataAccessMode;
|
|
57
|
+
if (firestoreCfg.dataAccessMode) {
|
|
58
|
+
if (edition !== types.DatabaseEdition.ENTERPRISE) {
|
|
59
|
+
throw new error_1.FirebaseError("dataAccessMode can only be specified for enterprise edition databases.");
|
|
60
|
+
}
|
|
61
|
+
if (firestoreCfg.dataAccessMode === "FIRESTORE_NATIVE") {
|
|
62
|
+
firestoreDataAccessMode = types.DataAccessMode.ENABLED;
|
|
63
|
+
mongodbCompatibleDataAccessMode = types.DataAccessMode.DISABLED;
|
|
64
|
+
}
|
|
65
|
+
else if (firestoreCfg.dataAccessMode === "MONGODB_COMPATIBLE") {
|
|
66
|
+
firestoreDataAccessMode = types.DataAccessMode.DISABLED;
|
|
67
|
+
mongodbCompatibleDataAccessMode = types.DataAccessMode.ENABLED;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
55
70
|
const api = new api_2.FirestoreApi();
|
|
56
71
|
try {
|
|
57
72
|
await api.getDatabase(options.projectId, firestoreCfg.database);
|
|
@@ -67,6 +82,8 @@ async function createDatabase(context, options) {
|
|
|
67
82
|
databaseEdition: edition,
|
|
68
83
|
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
|
|
69
84
|
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
|
|
85
|
+
firestoreDataAccessMode,
|
|
86
|
+
mongodbCompatibleDataAccessMode,
|
|
70
87
|
};
|
|
71
88
|
await api.createDatabase(createDatabaseReq);
|
|
72
89
|
}
|
|
@@ -98,7 +98,7 @@ async function packageSource(projectDir, sourceDir, config, additionalSources, r
|
|
|
98
98
|
if (err instanceof error_1.FirebaseError) {
|
|
99
99
|
throw err;
|
|
100
100
|
}
|
|
101
|
-
throw new error_1.FirebaseError(
|
|
101
|
+
throw new error_1.FirebaseError(`Could not read source directory. Remove links and shortcuts and try again. Original: ${err}`, {
|
|
102
102
|
original: err,
|
|
103
103
|
exit: 1,
|
|
104
104
|
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Delegate = exports.DART_ENTRY_POINT = void 0;
|
|
4
|
+
exports.tryCreateDelegate = tryCreateDelegate;
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const util_1 = require("util");
|
|
8
|
+
const spawn = require("cross-spawn");
|
|
9
|
+
const discovery = require("../discovery");
|
|
10
|
+
const supported = require("../supported");
|
|
11
|
+
const logger_1 = require("../../../../logger");
|
|
12
|
+
const error_1 = require("../../../../error");
|
|
13
|
+
const utils_1 = require("../../../../utils");
|
|
14
|
+
const registry_1 = require("../../../../emulator/registry");
|
|
15
|
+
const types_1 = require("../../../../emulator/types");
|
|
16
|
+
async function tryCreateDelegate(context) {
|
|
17
|
+
const pubspecYamlPath = path.join(context.sourceDir, "pubspec.yaml");
|
|
18
|
+
if (!(await (0, util_1.promisify)(fs.exists)(pubspecYamlPath))) {
|
|
19
|
+
logger_1.logger.debug("Customer code is not Dart code.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const runtime = context.runtime ?? supported.latest("dart");
|
|
23
|
+
if (!supported.isRuntime(runtime)) {
|
|
24
|
+
throw new error_1.FirebaseError(`Runtime ${runtime} is not a valid Dart runtime`);
|
|
25
|
+
}
|
|
26
|
+
if (!supported.runtimeIsLanguage(runtime, "dart")) {
|
|
27
|
+
throw new error_1.FirebaseError(`Internal error. Trying to construct a dart runtime delegate for runtime ${runtime}`, { exit: 1 });
|
|
28
|
+
}
|
|
29
|
+
return Promise.resolve(new Delegate(context.projectId, context.sourceDir, runtime));
|
|
30
|
+
}
|
|
31
|
+
const MIN_DART_SDK_VERSION = "3.8.0";
|
|
32
|
+
exports.DART_ENTRY_POINT = "bin/server.dart";
|
|
33
|
+
class Delegate {
|
|
34
|
+
constructor(projectId, sourceDir, runtime) {
|
|
35
|
+
this.projectId = projectId;
|
|
36
|
+
this.sourceDir = sourceDir;
|
|
37
|
+
this.runtime = runtime;
|
|
38
|
+
this.language = "dart";
|
|
39
|
+
this.bin = "dart";
|
|
40
|
+
this.entryPoint = exports.DART_ENTRY_POINT;
|
|
41
|
+
this.buildRunnerProcess = null;
|
|
42
|
+
}
|
|
43
|
+
async validate() {
|
|
44
|
+
const result = spawn.sync(this.bin, ["--version"], {
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
timeout: 10000,
|
|
47
|
+
});
|
|
48
|
+
if (result.error) {
|
|
49
|
+
throw new error_1.FirebaseError(`Could not find a Dart SDK. Make sure the '${this.bin}' command is available on your PATH.`);
|
|
50
|
+
}
|
|
51
|
+
const versionOutput = (result.stdout || result.stderr || "").toString();
|
|
52
|
+
const match = /Dart SDK version:\s*(\d+\.\d+\.\d+)/.exec(versionOutput);
|
|
53
|
+
if (match) {
|
|
54
|
+
const installedVersion = match[1];
|
|
55
|
+
if (installedVersion.localeCompare(MIN_DART_SDK_VERSION, undefined, { numeric: true }) < 0) {
|
|
56
|
+
throw new error_1.FirebaseError(`Dart SDK version ${installedVersion} is not supported. ` +
|
|
57
|
+
`Firebase Functions for Dart requires Dart ${MIN_DART_SDK_VERSION} or later. ` +
|
|
58
|
+
`Please upgrade your Dart SDK.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
logger_1.logger.debug(`Could not parse Dart SDK version from: ${versionOutput}`);
|
|
63
|
+
}
|
|
64
|
+
const pubspecYamlPath = path.join(this.sourceDir, "pubspec.yaml");
|
|
65
|
+
try {
|
|
66
|
+
await fs.promises.access(pubspecYamlPath, fs.constants.R_OK);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
throw new error_1.FirebaseError(`Failed to read pubspec.yaml at ${pubspecYamlPath}: ${err.message}`);
|
|
70
|
+
}
|
|
71
|
+
const entryPointPath = path.join(this.sourceDir, this.entryPoint);
|
|
72
|
+
try {
|
|
73
|
+
await fs.promises.access(entryPointPath, fs.constants.R_OK);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
throw new error_1.FirebaseError(`Could not find entry point at ${entryPointPath}. ` +
|
|
77
|
+
`Firebase Functions for Dart expects your main function in ${this.entryPoint}.`);
|
|
78
|
+
}
|
|
79
|
+
const packageConfigPath = path.join(this.sourceDir, ".dart_tool", "package_config.json");
|
|
80
|
+
try {
|
|
81
|
+
await fs.promises.access(packageConfigPath, fs.constants.R_OK);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
(0, utils_1.logLabeledBullet)("functions", "running dart pub get...");
|
|
85
|
+
const pubGetProcess = spawn(this.bin, ["pub", "get"], {
|
|
86
|
+
cwd: this.sourceDir,
|
|
87
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
88
|
+
});
|
|
89
|
+
pubGetProcess.stdout?.on("data", (chunk) => {
|
|
90
|
+
logger_1.logger.debug(`[dart pub get] ${chunk.toString("utf8").trim()}`);
|
|
91
|
+
});
|
|
92
|
+
pubGetProcess.stderr?.on("data", (chunk) => {
|
|
93
|
+
logger_1.logger.debug(`[dart pub get] ${chunk.toString("utf8").trim()}`);
|
|
94
|
+
});
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
pubGetProcess.on("exit", (code) => {
|
|
97
|
+
if (code === 0 || code === null) {
|
|
98
|
+
resolve();
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
reject(new error_1.FirebaseError(`dart pub get failed with exit code ${code}. ` +
|
|
102
|
+
`Make sure your pubspec.yaml is valid and dependencies are available.`));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
pubGetProcess.on("error", reject);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async build() {
|
|
110
|
+
if (Delegate.watchModeActive) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
(0, utils_1.logLabeledBullet)("functions", "running build_runner...");
|
|
114
|
+
const buildRunnerProcess = spawn(this.bin, ["run", "build_runner", "build", "--delete-conflicting-outputs"], {
|
|
115
|
+
cwd: this.sourceDir,
|
|
116
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
117
|
+
});
|
|
118
|
+
buildRunnerProcess.stdout?.on("data", (chunk) => {
|
|
119
|
+
logger_1.logger.debug(`[build_runner] ${chunk.toString("utf8").trim()}`);
|
|
120
|
+
});
|
|
121
|
+
buildRunnerProcess.stderr?.on("data", (chunk) => {
|
|
122
|
+
logger_1.logger.debug(`[build_runner] ${chunk.toString("utf8").trim()}`);
|
|
123
|
+
});
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
buildRunnerProcess.on("exit", (code) => {
|
|
126
|
+
if (code === 0 || code === null) {
|
|
127
|
+
resolve();
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
reject(new error_1.FirebaseError(`build_runner failed with exit code ${code}. ` +
|
|
131
|
+
`Make sure your Dart project is properly configured.`));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
buildRunnerProcess.on("error", reject);
|
|
135
|
+
});
|
|
136
|
+
if (registry_1.EmulatorRegistry.isRunning(types_1.Emulators.FUNCTIONS)) {
|
|
137
|
+
logger_1.logger.debug("Skipping Dart compilation in emulator mode.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const binDir = path.join(this.sourceDir, "bin");
|
|
141
|
+
await fs.promises.mkdir(binDir, { recursive: true });
|
|
142
|
+
(0, utils_1.logLabeledBullet)("functions", "compiling Dart to linux-x64 executable...");
|
|
143
|
+
const compileProcess = spawn(this.bin, [
|
|
144
|
+
"compile",
|
|
145
|
+
"exe",
|
|
146
|
+
this.entryPoint,
|
|
147
|
+
"-o",
|
|
148
|
+
"bin/server",
|
|
149
|
+
"--target-os=linux",
|
|
150
|
+
"--target-arch=x64",
|
|
151
|
+
], {
|
|
152
|
+
cwd: this.sourceDir,
|
|
153
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
154
|
+
});
|
|
155
|
+
compileProcess.stdout?.on("data", (chunk) => {
|
|
156
|
+
logger_1.logger.debug(`[dart compile] ${chunk.toString("utf8").trim()}`);
|
|
157
|
+
});
|
|
158
|
+
compileProcess.stderr?.on("data", (chunk) => {
|
|
159
|
+
logger_1.logger.debug(`[dart compile] ${chunk.toString("utf8").trim()}`);
|
|
160
|
+
});
|
|
161
|
+
await new Promise((resolve, reject) => {
|
|
162
|
+
compileProcess.on("exit", (code) => {
|
|
163
|
+
if (code === 0 || code === null) {
|
|
164
|
+
resolve();
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
reject(new error_1.FirebaseError(`Dart compilation failed with exit code ${code}. ` +
|
|
168
|
+
`Make sure your Dart project compiles successfully with: ` +
|
|
169
|
+
`dart compile exe ${this.entryPoint} --target-os=linux --target-arch=x64`));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
compileProcess.on("error", reject);
|
|
173
|
+
});
|
|
174
|
+
(0, utils_1.logLabeledBullet)("functions", "Dart compilation complete.");
|
|
175
|
+
}
|
|
176
|
+
async watch(onRebuild) {
|
|
177
|
+
Delegate.watchModeActive = true;
|
|
178
|
+
logger_1.logger.debug("Starting build_runner watch for Dart functions...");
|
|
179
|
+
const buildRunnerProcess = spawn(this.bin, ["run", "build_runner", "watch", "--delete-conflicting-outputs"], {
|
|
180
|
+
cwd: this.sourceDir,
|
|
181
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
182
|
+
});
|
|
183
|
+
this.buildRunnerProcess = buildRunnerProcess;
|
|
184
|
+
let initialBuildComplete = false;
|
|
185
|
+
let resolveInitialBuild;
|
|
186
|
+
let rejectInitialBuild;
|
|
187
|
+
const initialBuildPromise = new Promise((resolve, reject) => {
|
|
188
|
+
resolveInitialBuild = resolve;
|
|
189
|
+
rejectInitialBuild = reject;
|
|
190
|
+
});
|
|
191
|
+
const buildCompletePattern = /Succeeded after|Built with build_runner/;
|
|
192
|
+
buildRunnerProcess.stdout?.on("data", (chunk) => {
|
|
193
|
+
const output = chunk.toString("utf8").trim();
|
|
194
|
+
if (output) {
|
|
195
|
+
logger_1.logger.debug(`[build_runner] ${output}`);
|
|
196
|
+
if (buildCompletePattern.test(output)) {
|
|
197
|
+
if (!initialBuildComplete) {
|
|
198
|
+
initialBuildComplete = true;
|
|
199
|
+
logger_1.logger.debug("build_runner initial build completed");
|
|
200
|
+
resolveInitialBuild();
|
|
201
|
+
}
|
|
202
|
+
else if (onRebuild) {
|
|
203
|
+
onRebuild();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
buildRunnerProcess.stderr?.on("data", (chunk) => {
|
|
209
|
+
const output = chunk.toString("utf8").trim();
|
|
210
|
+
if (output) {
|
|
211
|
+
logger_1.logger.debug(`[build_runner] ${output}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
buildRunnerProcess.on("exit", (code) => {
|
|
215
|
+
if (code !== 0 && code !== null) {
|
|
216
|
+
logger_1.logger.debug(`build_runner exited with code ${code}. Initial build failed.`);
|
|
217
|
+
if (!initialBuildComplete) {
|
|
218
|
+
rejectInitialBuild(new error_1.FirebaseError(`build_runner exited with code ${code}. Your Dart functions may not be deployed or emulated correctly.`));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
this.buildRunnerProcess = null;
|
|
222
|
+
});
|
|
223
|
+
buildRunnerProcess.on("error", (err) => {
|
|
224
|
+
logger_1.logger.debug(`Failed to start build_runner: ${err.message}. Your Dart functions may not be deployed or emulated correctly.`);
|
|
225
|
+
if (!initialBuildComplete) {
|
|
226
|
+
rejectInitialBuild(err);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
await initialBuildPromise;
|
|
230
|
+
return async () => {
|
|
231
|
+
if (this.buildRunnerProcess && !this.buildRunnerProcess.killed) {
|
|
232
|
+
this.buildRunnerProcess.kill("SIGTERM");
|
|
233
|
+
this.buildRunnerProcess = null;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async discoverBuild(_configValues, envs) {
|
|
238
|
+
const yamlDir = this.sourceDir;
|
|
239
|
+
const yamlPath = path.join(yamlDir, "functions.yaml");
|
|
240
|
+
let discovered = await discovery.detectFromYaml(yamlDir, this.projectId, this.runtime);
|
|
241
|
+
if (!discovered) {
|
|
242
|
+
logger_1.logger.debug("functions.yaml not found, running build_runner to generate it...");
|
|
243
|
+
const buildRunnerProcess = spawn(this.bin, ["run", "build_runner", "build"], {
|
|
244
|
+
cwd: this.sourceDir,
|
|
245
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
246
|
+
});
|
|
247
|
+
buildRunnerProcess.stdout?.on("data", (chunk) => {
|
|
248
|
+
logger_1.logger.debug(`[build_runner] ${chunk.toString("utf8")}`);
|
|
249
|
+
});
|
|
250
|
+
buildRunnerProcess.stderr?.on("data", (chunk) => {
|
|
251
|
+
logger_1.logger.debug(`[build_runner] ${chunk.toString("utf8")}`);
|
|
252
|
+
});
|
|
253
|
+
await new Promise((resolve, reject) => {
|
|
254
|
+
buildRunnerProcess.on("exit", (code) => {
|
|
255
|
+
if (code === 0 || code === null) {
|
|
256
|
+
resolve();
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
reject(new error_1.FirebaseError(`build_runner failed with exit code ${code}. Make sure your Dart project is properly configured.`));
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
buildRunnerProcess.on("error", reject);
|
|
263
|
+
});
|
|
264
|
+
discovered = await discovery.detectFromYaml(yamlDir, this.projectId, this.runtime);
|
|
265
|
+
if (!discovered) {
|
|
266
|
+
throw new error_1.FirebaseError(`Could not find functions.yaml at ${yamlPath} after running build_runner. ` +
|
|
267
|
+
`Make sure your Dart project is properly configured with firebase_functions.`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const isEmulator = envs.FUNCTIONS_EMULATOR === "true";
|
|
271
|
+
if (!isEmulator) {
|
|
272
|
+
for (const ep of Object.values(discovered.endpoints)) {
|
|
273
|
+
if (ep.platform === "gcfv2") {
|
|
274
|
+
ep.platform = "run";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return discovered;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
exports.Delegate = Delegate;
|
|
282
|
+
Delegate.watchModeActive = false;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getRuntimeDelegate = getRuntimeDelegate;
|
|
4
|
+
const dart = require("./dart");
|
|
4
5
|
const node = require("./node");
|
|
5
6
|
const python = require("./python");
|
|
6
7
|
const validate = require("../validate");
|
|
7
8
|
const error_1 = require("../../../error");
|
|
8
9
|
const supported = require("./supported");
|
|
9
|
-
const dart = require("./dart");
|
|
10
10
|
const experiments = require("../../../experiments");
|
|
11
11
|
const factories = [
|
|
12
12
|
node.tryCreateDelegate,
|
|
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.isRuntime = isRuntime;
|
|
18
18
|
exports.runtimeIsLanguage = runtimeIsLanguage;
|
|
19
|
+
exports.isLanguageRuntime = isLanguageRuntime;
|
|
19
20
|
exports.latest = latest;
|
|
20
21
|
exports.isDecommissioned = isDecommissioned;
|
|
21
22
|
exports.guardVersionSupport = guardVersionSupport;
|
|
@@ -29,6 +30,9 @@ function isRuntime(maybe) {
|
|
|
29
30
|
function runtimeIsLanguage(runtime, language) {
|
|
30
31
|
return runtime.startsWith(language);
|
|
31
32
|
}
|
|
33
|
+
function isLanguageRuntime(runtime, language) {
|
|
34
|
+
return !!runtime && runtime.startsWith(language);
|
|
35
|
+
}
|
|
32
36
|
function latest(language, runtimes = Object.keys(types_1.RUNTIMES)) {
|
|
33
37
|
const sorted = runtimes
|
|
34
38
|
.filter((s) => runtimeIsLanguage(s, language))
|