@nu-art/build-and-install 0.401.0 → 0.401.1

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.
Files changed (155) hide show
  1. package/BuildAndInstall.d.ts +40 -0
  2. package/BuildAndInstall.js +155 -0
  3. package/build-and-install-v3.d.ts +1 -44
  4. package/build-and-install-v3.js +1 -157
  5. package/build-and-install.js +11 -11
  6. package/config/consts.d.ts +43 -0
  7. package/config/consts.js +42 -0
  8. package/{core → config}/package/consts.d.ts +1 -1
  9. package/{core → config}/types/project-config.d.ts +3 -0
  10. package/core/FilesCache.d.ts +50 -0
  11. package/core/FilesCache.js +76 -0
  12. package/core/Unit_HelpPrinter.d.ts +16 -0
  13. package/core/Unit_HelpPrinter.js +47 -0
  14. package/core/params/params.d.ts +1 -41
  15. package/core/params/params.js +1 -332
  16. package/core/params.d.ts +50 -0
  17. package/core/params.js +441 -0
  18. package/{v3/core → core}/types.d.ts +1 -1
  19. package/{v3/UnitsDependencyMapper → dependencies}/UnitsDependencyMapper.d.ts +21 -1
  20. package/{v3/UnitsDependencyMapper → dependencies}/UnitsDependencyMapper.js +26 -3
  21. package/dependencies/types.d.ts +1 -0
  22. package/dependencies/types.js +1 -0
  23. package/exceptions/PhaseAggregatedException.d.ts +34 -0
  24. package/{core/exceptions → exceptions}/PhaseAggregatedException.js +26 -0
  25. package/exceptions/UnitPhaseException.d.ts +20 -0
  26. package/exceptions/UnitPhaseException.js +21 -0
  27. package/exports/ExportIndexCache.d.ts +25 -0
  28. package/exports/ExportIndexCache.js +115 -0
  29. package/exports/ExportMapper.d.ts +43 -0
  30. package/exports/ExportMapper.js +519 -0
  31. package/exports/IndicesMcpServer.d.ts +22 -0
  32. package/exports/IndicesMcpServer.js +220 -0
  33. package/exports/types.js +3 -0
  34. package/package.json +20 -9
  35. package/phases/PhaseManager.d.ts +130 -0
  36. package/{v3 → phases}/PhaseManager.js +99 -2
  37. package/{v3/phase → phases/definitions}/consts.d.ts +36 -0
  38. package/{v3/phase → phases/definitions}/consts.js +44 -2
  39. package/phases/definitions/types.d.ts +40 -0
  40. package/phases/index.d.ts +2 -0
  41. package/phases/index.js +2 -0
  42. package/run.js +10 -0
  43. package/runtime/RunningStatusHandler.d.ts +104 -0
  44. package/runtime/RunningStatusHandler.js +153 -0
  45. package/runtime/types.d.ts +1 -0
  46. package/runtime/types.js +2 -0
  47. package/{defaults → templates}/consts.d.ts +9 -0
  48. package/{defaults → templates}/consts.js +12 -2
  49. package/templates/firebase/functions/cloudbuild.yaml +17 -0
  50. package/templates/firebase/functions/dockerfile +19 -0
  51. package/templates/firebase/functions/service.yaml +49 -0
  52. package/{v3/units → units/base}/BaseUnit.d.ts +34 -4
  53. package/{v3/units → units/base}/BaseUnit.js +22 -2
  54. package/units/base/ProjectUnit.d.ts +32 -0
  55. package/units/base/ProjectUnit.js +25 -0
  56. package/units/base/types.js +1 -0
  57. package/units/discovery/UnitsMapper.d.ts +69 -0
  58. package/{v3/UnitsMapper → units/discovery}/UnitsMapper.js +50 -2
  59. package/units/discovery/resolvers/UnitMapper_Base.d.ts +65 -0
  60. package/units/discovery/resolvers/UnitMapper_Base.js +46 -0
  61. package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_FirebaseFunction.d.ts +5 -3
  62. package/units/discovery/resolvers/UnitMapper_FirebaseFunction.js +105 -0
  63. package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_FirebaseHosting.d.ts +3 -2
  64. package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_FirebaseHosting.js +14 -10
  65. package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_Node.d.ts +1 -1
  66. package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_Node.js +2 -2
  67. package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_NodeLib.d.ts +24 -1
  68. package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_NodeLib.js +24 -1
  69. package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_NodeProject.d.ts +22 -1
  70. package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_NodeProject.js +22 -1
  71. package/units/discovery/types.js +1 -0
  72. package/units/implementations/Unit_NodeProject.d.ts +59 -0
  73. package/{v3/units → units/implementations}/Unit_NodeProject.js +65 -5
  74. package/units/implementations/Unit_PackageJson.d.ts +56 -0
  75. package/{v3/units → units/implementations}/Unit_PackageJson.js +39 -3
  76. package/{v3/units → units/implementations}/Unit_TypescriptLib.d.ts +40 -4
  77. package/{v3/units → units/implementations}/Unit_TypescriptLib.js +167 -17
  78. package/units/implementations/firebase/Unit_FirebaseFunctionsApp.d.ts +233 -0
  79. package/units/implementations/firebase/Unit_FirebaseFunctionsApp.js +804 -0
  80. package/units/implementations/firebase/Unit_FirebaseHostingApp.d.ts +113 -0
  81. package/units/implementations/firebase/Unit_FirebaseHostingApp.js +320 -0
  82. package/units/implementations/firebase/common.d.ts +26 -0
  83. package/units/implementations/firebase/common.js +65 -0
  84. package/units/index.d.ts +6 -0
  85. package/units/index.js +6 -0
  86. package/v3/core/Unit_HelpPrinter.d.ts +1 -16
  87. package/v3/core/Unit_HelpPrinter.js +1 -47
  88. package/{v3 → workspace}/Workspace.d.ts +30 -15
  89. package/{v3 → workspace}/Workspace.js +48 -35
  90. package/core/consts.d.ts +0 -13
  91. package/core/consts.js +0 -12
  92. package/core/exceptions/PhaseAggregatedException.d.ts +0 -8
  93. package/core/exceptions/UnitPhaseException.d.ts +0 -5
  94. package/core/exceptions/UnitPhaseException.js +0 -6
  95. package/old/PhaseRunnerDispatcher.d.ts +0 -24
  96. package/old/PhaseRunnerDispatcher.js +0 -32
  97. package/old/runner-dispatchers.d.ts +0 -10
  98. package/old/runner-dispatchers.js +0 -3
  99. package/v3/PhaseManager.d.ts +0 -27
  100. package/v3/RunningStatusHandler.d.ts +0 -18
  101. package/v3/RunningStatusHandler.js +0 -67
  102. package/v3/UnitsMapper/UnitsMapper.d.ts +0 -21
  103. package/v3/UnitsMapper/resolvers/UnitMapper_Base.d.ts +0 -23
  104. package/v3/UnitsMapper/resolvers/UnitMapper_Base.js +0 -16
  105. package/v3/UnitsMapper/resolvers/UnitMapper_FirebaseFunction.js +0 -66
  106. package/v3/core/FilesCache.d.ts +0 -7
  107. package/v3/core/FilesCache.js +0 -33
  108. package/v3/phase/types.d.ts +0 -10
  109. package/v3/units/ProjectUnit.d.ts +0 -18
  110. package/v3/units/ProjectUnit.js +0 -11
  111. package/v3/units/Unit_NodeProject.d.ts +0 -30
  112. package/v3/units/Unit_PackageJson.d.ts +0 -17
  113. package/v3/units/firebase/Unit_FirebaseFunctionsApp.d.ts +0 -64
  114. package/v3/units/firebase/Unit_FirebaseFunctionsApp.js +0 -306
  115. package/v3/units/firebase/Unit_FirebaseHostingApp.d.ts +0 -49
  116. package/v3/units/firebase/Unit_FirebaseHostingApp.js +0 -118
  117. package/v3/units/firebase/common.d.ts +0 -3
  118. package/v3/units/firebase/common.js +0 -13
  119. package/v3/units/index.d.ts +0 -6
  120. package/v3/units/index.js +0 -6
  121. /package/{core → config}/package/consts.js +0 -0
  122. /package/{core → config}/types/configs/firebasejson.d.ts +0 -0
  123. /package/{core → config}/types/configs/firebasejson.js +0 -0
  124. /package/{core → config}/types/configs/firebaserc.d.ts +0 -0
  125. /package/{core → config}/types/configs/firebaserc.js +0 -0
  126. /package/{core → config}/types/configs/index.d.ts +0 -0
  127. /package/{core → config}/types/configs/index.js +0 -0
  128. /package/{core → config}/types/configs/package-json.d.ts +0 -0
  129. /package/{core → config}/types/configs/package-json.js +0 -0
  130. /package/{core → config}/types/core.d.ts +0 -0
  131. /package/{core → config}/types/core.js +0 -0
  132. /package/{core → config}/types/index.d.ts +0 -0
  133. /package/{core → config}/types/index.js +0 -0
  134. /package/{core → config}/types/package/index.d.ts +0 -0
  135. /package/{core → config}/types/package/index.js +0 -0
  136. /package/{core → config}/types/package/package.d.ts +0 -0
  137. /package/{core → config}/types/package/package.js +0 -0
  138. /package/{core → config}/types/package/runtime-package.d.ts +0 -0
  139. /package/{core → config}/types/package/runtime-package.js +0 -0
  140. /package/{core → config}/types/project-config.js +0 -0
  141. /package/{v3/core → core}/types.js +0 -0
  142. /package/{v3/UnitsMapper/types.js → exports/types.d.ts} +0 -0
  143. /package/{v3/phase → phases/definitions}/index.d.ts +0 -0
  144. /package/{v3/phase → phases/definitions}/index.js +0 -0
  145. /package/{v3/phase → phases/definitions}/types.js +0 -0
  146. /package/{v3/units/types.js → run.d.ts} +0 -0
  147. /package/{defaults/backend-proxy → templates/backend/proxy}/proxy._ts +0 -0
  148. /package/{defaults/.firebase_config → templates/firebase/config}/database.rules.json +0 -0
  149. /package/{defaults/.firebase_config → templates/firebase/config}/firestore.indexes.json +0 -0
  150. /package/{defaults/.firebase_config → templates/firebase/config}/firestore.rules +0 -0
  151. /package/{defaults/.firebase_config → templates/firebase/config}/storage.rules +0 -0
  152. /package/{v3/units → units/base}/types.d.ts +0 -0
  153. /package/{v3/UnitsMapper → units/discovery}/resolvers/index.d.ts +0 -0
  154. /package/{v3/UnitsMapper → units/discovery}/resolvers/index.js +0 -0
  155. /package/{v3/UnitsMapper → units/discovery}/types.d.ts +0 -0
@@ -0,0 +1,804 @@
1
+ import { CONST_BuildImageDir, CONST_FirebaseJSON, CONST_FirebaseRC, CONST_LatestTag, CONST_PackageJSON, CONST_TrashDir, CONST_VersionApp } from '../../../config/consts.js';
2
+ import { __stringify, _keys, _logger_logPrefixes, deepClone, ImplementationMissingException, LogLevel, Second, sleep } from '@nu-art/ts-common';
3
+ import { Const_FirebaseConfigKeys, Const_FirebaseDefaultsKeyToFile, FunctionBuildTemplateFiles } from '../../../templates/consts.js';
4
+ import { Commando_NVM } from '@nu-art/commando/shell/plugins/nvm';
5
+ import { resolve } from 'path';
6
+ import { DEFAULT_OLD_TEMPLATE_PATTERN, FileSystemUtils } from '@nu-art/ts-common/utils/FileSystemUtils';
7
+ import { Unit_TypescriptLib } from '../Unit_TypescriptLib.js';
8
+ import { CommandoException } from '@nu-art/commando/shell/core/CliError';
9
+ import { deployLogFilter, ensureArtifactRegistryRepository } from './common.js';
10
+ export const firebaseFunctionEmulator_ErrorStrings = [
11
+ 'functions: Failed',
12
+ ];
13
+ export const firebaseFunctionEmulator_WarningStrings = [
14
+ '⚠',
15
+ ];
16
+ // const CONST_VersionApp = 'version-app.json';
17
+ /**
18
+ * Firebase Functions application unit.
19
+ *
20
+ * **Key Features**:
21
+ * - Extends Unit_TypescriptLib (compiles TypeScript)
22
+ * - Manages Firebase Functions configuration
23
+ * - Supports emulator with SSL and debug ports
24
+ * - Handles function deployment
25
+ *
26
+ * **Phases Implemented**:
27
+ * - `prepare()`: Sets up Firebase Functions config
28
+ * - `compile()`: Compiles TypeScript for functions
29
+ * - `launch()`: Starts Firebase Functions emulator
30
+ * - `deploy()`: Deploys functions to Firebase
31
+ *
32
+ * **Configuration**:
33
+ * - `debugPort`: Port for Node.js debugger
34
+ * - `basePort`: Base port for emulator
35
+ * - `sslKey`/`sslCert`: SSL certificates for emulator
36
+ * - `pathToEmulatorData`: Path for emulator data persistence
37
+ * - `envConfig`: Environment config (projectId, identityAccount)
38
+ *
39
+ * **Emulator**: Runs Firebase Functions emulator with log filtering and error detection.
40
+ */
41
+ export class Unit_FirebaseFunctionsApp extends Unit_TypescriptLib {
42
+ functions = {};
43
+ injectedMetadata = {};
44
+ static staggerCount = 0;
45
+ static DefaultConfig_FirebaseFunction = {
46
+ pathToFirebaseConfig: '.firebase_config',
47
+ debugPort: 8100,
48
+ basePort: 8102,
49
+ sslKey: '.ssl/key.pem',
50
+ sslCert: '.ssl/cert.pem',
51
+ output: 'dist',
52
+ pathToEmulatorData: `${CONST_TrashDir}/data`,
53
+ };
54
+ emulatorLogStrings = {
55
+ error: firebaseFunctionEmulator_ErrorStrings,
56
+ warning: firebaseFunctionEmulator_WarningStrings,
57
+ };
58
+ constructor(config) {
59
+ super(config);
60
+ this.addToClassStack(Unit_FirebaseFunctionsApp);
61
+ this.logger.setLogTransformer(log => {
62
+ const prefix = _logger_logPrefixes.find(prefix => log.includes(prefix));
63
+ if (!prefix)
64
+ return log;
65
+ return log.substring(log.indexOf(prefix) + prefix.length);
66
+ });
67
+ }
68
+ //######################### Phase Implementations #########################
69
+ async copyPackageJSONToOutput() {
70
+ const targetPath = resolve(this.config.output, CONST_PackageJSON);
71
+ const packageJson = deepClone(this.config.packageJson);
72
+ const distDependencies = this.deriveDistDependencies();
73
+ packageJson.main = 'index.js';
74
+ packageJson.types = 'index.d.ts';
75
+ const dependencies = packageJson.dependencies ?? {};
76
+ // First, update existing dependencies (replace workspace:* with file: paths where applicable)
77
+ _keys(dependencies).reduce((dependencies, packageName) => {
78
+ if (distDependencies[packageName])
79
+ dependencies[packageName] = distDependencies[packageName];
80
+ return dependencies;
81
+ }, dependencies);
82
+ // Then, add ALL dependencyUnits to the dependencies (this includes transitive dependencies)
83
+ // This ensures the entire dependency tree is referenced in the main package.json
84
+ this.dependencyUnits.reduce((dependencies, unit) => {
85
+ dependencies[unit.config.key] = distDependencies[unit.config.key];
86
+ return dependencies;
87
+ }, dependencies);
88
+ packageJson.dependencies = dependencies;
89
+ await FileSystemUtils.file.template.write(targetPath, __stringify(packageJson, true), this.deriveDistDependencies(), DEFAULT_OLD_TEMPLATE_PATTERN);
90
+ }
91
+ async prepare() {
92
+ await super.prepare();
93
+ await FileSystemUtils.folder.list.forEach.folder(this.config.fullPath, async (path) => {
94
+ if (path.replace(`${this.config.fullPath}/`, '').startsWith('firebase-export-'))
95
+ return await FileSystemUtils.folder.delete(path);
96
+ });
97
+ await FileSystemUtils.folder.create(resolve(this.config.fullPath, this.config.pathToEmulatorData));
98
+ await FileSystemUtils.file.delete(this.pathToProxy());
99
+ await this.resolveConfigs();
100
+ }
101
+ async resolveConfigs() {
102
+ await this.resolveFunctionsRC();
103
+ await this.resolveConfigDir();
104
+ await this.resolveFunctionsRuntimeConfig();
105
+ await this.resolveFunctionsJSON();
106
+ }
107
+ async compile() {
108
+ await this.createAppVersionFile();
109
+ await super.compile();
110
+ }
111
+ async postCompile() {
112
+ await this.createDependenciesDir();
113
+ }
114
+ async launch() {
115
+ await sleep(2 * Second * Unit_FirebaseFunctionsApp.staggerCount++);
116
+ await this.releaseEmulatorPorts();
117
+ await Promise.all([
118
+ this.runProxy(),
119
+ this.runEmulator(),
120
+ ]);
121
+ }
122
+ async releaseEmulatorPorts() {
123
+ const allPorts = Array.from({ length: 10 }, (_, i) => `${this.config.basePort + i}`);
124
+ return this.releasePorts(allPorts);
125
+ }
126
+ async deploy() {
127
+ const commando = this.allocateCommando(Commando_NVM).applyNVM()
128
+ .cd(this.config.output)
129
+ .ls()
130
+ .cat('package.json')
131
+ .cat('index.js')
132
+ .cd(this.config.fullPath)
133
+ .setLogLevelFilter(deployLogFilter)
134
+ // example: Function URL (hello(us-central1)): https://hello-kv65k7yylq-uc.a.run.app
135
+ .onLog(/.*Function URL.*?\((.*?)\(.*(https:\/\/.*?)$/, match => {
136
+ this.functions[match[1]] = match[2];
137
+ });
138
+ const debug = this.runtimeContext.runtimeParams.verbose ? ' --debug' : '';
139
+ await this.executeAsyncCommando(commando, `${this.npmCommand('firebase')}${debug} deploy --only functions --force`, (stdout, stderr, exitCode) => {
140
+ if (exitCode === 0)
141
+ return;
142
+ throw new CommandoException(`Failed to deploy function with exit code ${exitCode}`, stdout, stderr, exitCode);
143
+ });
144
+ this.logInfo(`Functions: `, this.functions);
145
+ }
146
+ /**
147
+ * Builds Docker container image using Google Cloud Build and pushes it to Artifact Registry.
148
+ *
149
+ * **Process**:
150
+ * 1. Validates image tag is provided via CLI
151
+ * 2. Validates containerDeployment config exists
152
+ * 3. Constructs Artifact Registry image reference
153
+ * 4. Creates isolated staging directory with only required files (dist/, Dockerfile, .cloudbuild.yaml)
154
+ * 5. Builds and pushes image using Google Cloud Build from staging directory (no local Docker required)
155
+ *
156
+ * **Staging Directory Structure**:
157
+ * - `dist/` - Contains compiled code and package.json (copied from output)
158
+ * - `Dockerfile` - Container build instructions
159
+ * - `.cloudbuild.yaml` - Cloud Build configuration
160
+ *
161
+ * **Requirements**:
162
+ * - `--build-push-image <tag>` CLI flag with tag value
163
+ * - `containerDeployment` config in unit config
164
+ * - gcloud CLI installed and authenticated
165
+ * - Cloud Build API enabled in GCP project
166
+ * - No local Docker daemon required - Cloud Build handles everything
167
+ */
168
+ async buildPushImage() {
169
+ const containerDeployment = this.config.containerDeployment;
170
+ if (!containerDeployment)
171
+ throw new ImplementationMissingException(`Missing containerDeployment config in unit ${this.config.key}`);
172
+ const imageTag = this.runtimeContext.runtimeParams.buildPushImage;
173
+ // Tag is validated by CLI param processor
174
+ const artifactRegistry = containerDeployment.artifactRegistry;
175
+ const imageName = containerDeployment.imageName;
176
+ const artifactRegistryPath = `${artifactRegistry.region}-docker.pkg.dev/${artifactRegistry.projectId}/${artifactRegistry.repository}`;
177
+ const imageReference = `${artifactRegistryPath}/${imageName}:${imageTag}`;
178
+ const imageReferenceLatest = `${artifactRegistryPath}/${imageName}:latest`;
179
+ this.logInfo(`Building and pushing container image using Cloud Build:`);
180
+ this.logInfo(` Tagged: ${imageReference}`);
181
+ this.logInfo(` Latest: ${imageReferenceLatest}`);
182
+ // Check for dry run mode
183
+ if (this.runtimeContext.runtimeParams.dryRun) {
184
+ this.logInfo(`[DRY RUN] Would build and push image: ${imageReference} and ${imageReferenceLatest}`);
185
+ return;
186
+ }
187
+ // Ensure Artifact Registry repository exists
188
+ const commando = this.allocateCommando();
189
+ await ensureArtifactRegistryRepository(commando, artifactRegistry, 'docker', this);
190
+ // Create isolated staging directory for container build
191
+ // This ensures we have full control over what goes into the image
192
+ const buildOutputDir = resolve(this.config.fullPath, CONST_TrashDir);
193
+ const stagingDir = resolve(buildOutputDir, CONST_BuildImageDir);
194
+ await FileSystemUtils.folder.delete(stagingDir);
195
+ await FileSystemUtils.folder.create(stagingDir);
196
+ // Copy only what's needed: the entire dist folder (which contains package.json)
197
+ const distTargetPath = resolve(stagingDir, 'dist');
198
+ await FileSystemUtils.folder.copy(this.config.output, distTargetPath);
199
+ this.logInfo(`Created staging directory at ${stagingDir}`);
200
+ this.logInfo(` - Copied dist/ from ${this.config.output} (includes package.json)`);
201
+ // Generate Dockerfile in staging directory
202
+ // Build context will be staging directory
203
+ const dockerfileName = containerDeployment.dockerfile || 'Dockerfile';
204
+ const dockerfilePath = resolve(stagingDir, dockerfileName);
205
+ await FileSystemUtils.file.template.copy(FunctionBuildTemplateFiles.dockerfile, dockerfilePath, {});
206
+ this.logInfo(`Created Dockerfile at ${dockerfilePath}`);
207
+ const metadata = {
208
+ ...this.injectedMetadata,
209
+ 'build.timestamp': new Date().toISOString(),
210
+ 'build.tag': imageTag,
211
+ 'build.project': artifactRegistry.projectId,
212
+ 'build.image-name': imageName,
213
+ 'version': this.runtimeContext.version,
214
+ 'git.commit': process.env.GIT_COMMIT || '',
215
+ 'git.branch': process.env.GIT_BRANCH || '',
216
+ 'build.user': process.env.USER || '',
217
+ };
218
+ this.logDebug(`Metadata: `, metadata);
219
+ const labels = Object.entries(metadata)
220
+ .map(([key, value]) => ` - '--label=${key}=${value}'`)
221
+ .join('\n');
222
+ const params = {
223
+ IMAGE_REFERENCE: imageReference,
224
+ IMAGE_REFERENCE_LATEST: imageReferenceLatest,
225
+ DOCKERFILE_PATH: dockerfileName, // Simple path since build context is staging dir
226
+ LABELS: labels
227
+ };
228
+ // Generate cloudbuild.yaml in staging directory
229
+ const cloudbuildYamlPath = resolve(stagingDir, '.cloudbuild.yaml');
230
+ await FileSystemUtils.file.template.copy(FunctionBuildTemplateFiles.cloudbuildYaml, cloudbuildYamlPath, params);
231
+ // Build from staging directory - this ensures only staging contents are uploaded
232
+ commando.cd(stagingDir);
233
+ await this.executeAsyncCommando(commando, `gcloud builds submit --config .cloudbuild.yaml --project ${artifactRegistry.projectId} .`, (stdout, stderr, exitCode) => {
234
+ if (exitCode === 0)
235
+ return;
236
+ throw new CommandoException(`Failed to build and push Docker image with Cloud Build (exit code ${exitCode})`, stdout, stderr, exitCode);
237
+ });
238
+ this.logInfo(`Successfully built and pushed images: ${imageReference} and ${imageReferenceLatest}`);
239
+ }
240
+ /**
241
+ * Discovers exported functions from the compiled dist/index.js file.
242
+ * Parses export statements to extract function names.
243
+ *
244
+ * @returns Array of function names found in exports
245
+ */
246
+ async discoverExportedFunctions() {
247
+ const indexPath = resolve(this.config.output, 'index.js');
248
+ const content = await FileSystemUtils.file.read(indexPath);
249
+ const functionNames = [];
250
+ // Match patterns like: export const hello = ... or export const helloWorld = ...
251
+ const exportConstRegex = /export\s+const\s+(\w+)\s*=/g;
252
+ let match;
253
+ while ((match = exportConstRegex.exec(content)) !== null) {
254
+ functionNames.push(match[1]);
255
+ }
256
+ return functionNames;
257
+ }
258
+ /**
259
+ * Normalizes function configuration to FunctionConfig[] format.
260
+ * Handles both legacy format (string[]) and new format (FunctionConfig[]).
261
+ */
262
+ normalizeFunctionConfigs() {
263
+ if (this.config.functions.length === 0)
264
+ return [];
265
+ // Check if it's already in FunctionConfig format
266
+ if (typeof this.config.functions[0] === 'object') {
267
+ return this.config.functions;
268
+ }
269
+ // Legacy format: convert string[] to FunctionConfig[]
270
+ return this.config.functions.map(name => ({
271
+ name,
272
+ trigger: 'http'
273
+ }));
274
+ }
275
+ /**
276
+ * Gets function names from configuration (for backward compatibility).
277
+ */
278
+ getFunctionNames() {
279
+ return this.normalizeFunctionConfigs().map(f => f.name);
280
+ }
281
+ /**
282
+ * Gets function config by name.
283
+ */
284
+ getFunctionConfig(functionName) {
285
+ return this.normalizeFunctionConfigs().find(f => f.name === functionName);
286
+ }
287
+ /**
288
+ * Validates that all configured functions exist in the compiled dist/index.js file.
289
+ * Throws ImplementationMissingException if any configured function is missing.
290
+ */
291
+ async validateFunctionsExist() {
292
+ const configuredFunctions = this.normalizeFunctionConfigs();
293
+ const functionNames = configuredFunctions.map(f => f.name);
294
+ const exportedFunctions = await this.discoverExportedFunctions();
295
+ const exportedSet = new Set(exportedFunctions);
296
+ const missingFunctions = functionNames.filter(func => !exportedSet.has(func));
297
+ if (missingFunctions.length > 0) {
298
+ throw new ImplementationMissingException(`Configured functions not found in dist/index.js: ${missingFunctions.join(', ')}. ` +
299
+ `Available exports: ${exportedFunctions.length > 0 ? exportedFunctions.join(', ') : 'none'}`);
300
+ }
301
+ }
302
+ /**
303
+ * Deletes a single function using gcloud run services delete.
304
+ *
305
+ * @param functionName Name of the function to delete (original function name with underscores)
306
+ */
307
+ async deleteFunction(functionName) {
308
+ const containerDeployment = this.config.containerDeployment;
309
+ if (!containerDeployment) {
310
+ throw new ImplementationMissingException(`Missing containerDeployment config in unit ${this.config.key}`);
311
+ }
312
+ const artifactRegistry = containerDeployment.artifactRegistry;
313
+ const region = artifactRegistry.region;
314
+ // Use runtime project ID (where function is deployed), not Artifact Registry project ID
315
+ const envConfig = this.getEnvConfig();
316
+ const runtimeProjectId = envConfig.projectId;
317
+ // Cloud Run service names cannot contain underscores, convert to dashes
318
+ const serviceName = functionName.replace(/_/g, '-');
319
+ const commando = this.allocateCommando(Commando_NVM).applyNVM()
320
+ .cd(this.config.fullPath)
321
+ .setLogLevelFilter(deployLogFilter);
322
+ // Use gcloud run services delete (Gen2 functions run on Cloud Run)
323
+ const deleteCommand = `gcloud run services delete ${serviceName} --region=${region} --project=${runtimeProjectId} --quiet`;
324
+ // Check for dry run mode
325
+ if (this.runtimeContext.runtimeParams.dryRun) {
326
+ this.logInfo(`[DRY RUN] Would execute: ${deleteCommand}`);
327
+ return;
328
+ }
329
+ this.logInfo(`Deleting function: ${functionName} (service: ${serviceName})`);
330
+ await this.executeAsyncCommando(commando, deleteCommand, (stdout, stderr, exitCode) => {
331
+ // Ignore errors for non-existent functions (function might already be deleted)
332
+ if (exitCode === 0)
333
+ return;
334
+ // Check if error is about function/service not found
335
+ // gcloud returns various messages like "could not be found", "not found", "does not exist"
336
+ const errorText = (stderr || stdout || '').toLowerCase();
337
+ const notFoundPatterns = [
338
+ 'not found',
339
+ 'could not be found',
340
+ 'does not exist',
341
+ 'cannot be found',
342
+ 'no such service'
343
+ ];
344
+ if (notFoundPatterns.some(pattern => errorText.includes(pattern))) {
345
+ this.logWarning(`Function ${functionName} not found (may already be deleted)`);
346
+ return;
347
+ }
348
+ // Re-throw other errors
349
+ throw new CommandoException(`Failed to delete function ${functionName} with exit code ${exitCode}`, stdout, stderr, exitCode);
350
+ });
351
+ }
352
+ /**
353
+ * Deletes multiple functions.
354
+ * Determines which functions to delete based on CLI parameters.
355
+ *
356
+ * @returns Array of function names that were deleted (or would be deleted in dry run)
357
+ */
358
+ async deleteFunctions() {
359
+ const deleteFunctionParam = this.runtimeContext.runtimeParams.deleteFunction;
360
+ const deleteFunctionsFlag = this.runtimeContext.runtimeParams.deleteFunctions;
361
+ const deployFunctionParam = this.runtimeContext.runtimeParams.deployFunction;
362
+ const functionNames = this.getFunctionNames();
363
+ let functionsToDelete = [];
364
+ // Determine which functions to delete
365
+ if (deleteFunctionParam) {
366
+ // Delete specific function
367
+ functionsToDelete = [deleteFunctionParam];
368
+ }
369
+ else if (deleteFunctionsFlag) {
370
+ // Delete based on context
371
+ if (deployFunctionParam) {
372
+ // Delete only the function being deployed
373
+ functionsToDelete = [deployFunctionParam];
374
+ }
375
+ else {
376
+ // Delete all functions from config
377
+ functionsToDelete = functionNames;
378
+ }
379
+ }
380
+ if (functionsToDelete.length === 0) {
381
+ return [];
382
+ }
383
+ this.logInfo(`Deleting ${functionsToDelete.length} function(s): ${functionsToDelete.join(', ')}`);
384
+ // Delete each function
385
+ for (const functionName of functionsToDelete) {
386
+ await this.deleteFunction(functionName);
387
+ }
388
+ if (functionsToDelete.length > 0) {
389
+ this.logInfo(`Deleted ${functionsToDelete.length} function(s) before deployment`);
390
+ }
391
+ }
392
+ /**
393
+ * Deploys container image from Artifact Registry to Firebase Functions using gcloud.
394
+ *
395
+ * **Process**:
396
+ * 1. Validates image tag is provided via CLI
397
+ * 2. Validates containerDeployment config exists
398
+ * 3. Validates configured functions exist in dist/index.js
399
+ * 4. Deletes functions if requested via CLI flags
400
+ * 5. Determines which functions to deploy (single or all)
401
+ * 6. Generates Cloud Run service YAML definitions for each function
402
+ * 7. Sets required environment variables (FIREBASE_CONFIG, GCLOUD_PROJECT, FUNCTION_TARGET, etc.)
403
+ * 8. Deploys each function using `gcloud run services replace` with YAML definition
404
+ * 9. Retrieves function URLs after successful deployment
405
+ *
406
+ * **Environment Variables Set**:
407
+ * - `FUNCTION_TARGET`: Function name to invoke
408
+ * - `GCLOUD_PROJECT`: Runtime project ID
409
+ * - `GOOGLE_CLOUD_PROJECT`: Runtime project ID (alternative)
410
+ * - `FIREBASE_CONFIG`: JSON string with projectId, databaseURL, storageBucket, locationId
411
+ * - `EVENTARC_CLOUD_EVENT_SOURCE`: Eventarc source path
412
+ * - `LOG_EXECUTION_ID`: Set to 'true' for execution ID logging
413
+ *
414
+ * **Function Configuration**:
415
+ * Functions can be configured in two formats:
416
+ * 1. **Legacy format** (string[]): Simple array of function names (all default to HTTP trigger)
417
+ * 2. **New format** (FunctionConfig[]): Array of function config objects with:
418
+ * - `name`: Function name (must match exported function name)
419
+ * - `trigger`: Trigger type ('http', 'schedule', or 'eventarc')
420
+ * - `schedule`: Schedule expression (required for 'schedule' trigger, e.g., 'every 24 hours', '0 2 * * *')
421
+ * - `resources`: Per-function resource configuration:
422
+ * - `cpu`: CPU allocation (e.g., '1', '2', '4')
423
+ * - `memory`: Memory allocation (e.g., '512Mi', '1Gi', '2Gi', '4Gi', '8Gi')
424
+ * - `timeout`: Timeout in seconds (default: 300, max: 3600)
425
+ * - `concurrency`: Container concurrency (default: 80, max: 1000)
426
+ * - `minInstances`: Minimum number of instances (default: 0)
427
+ * - `maxInstances`: Maximum number of instances (default: 100)
428
+ *
429
+ * **Trigger Types**:
430
+ * - `http`: HTTP-triggered function (deployed as Cloud Run service)
431
+ * - `schedule`: Scheduled function (requires `schedule` property, deployed as Cloud Run service with Cloud Scheduler)
432
+ * - `eventarc`: Event-triggered function (deployed as Cloud Run service with Eventarc)
433
+ *
434
+ * **Requirements**:
435
+ * - `--deploy-image <tag>` CLI flag with tag value
436
+ * - `containerDeployment` config in unit config
437
+ * - `functions` array in unit config (legacy or new format)
438
+ * - Image must already exist in Artifact Registry (built via buildPushImage)
439
+ * - gcloud CLI installed and authenticated
440
+ * - Cloud Functions API enabled in GCP project
441
+ * - Cloud Scheduler API enabled (for scheduled functions)
442
+ *
443
+ * **Note on Request Size Limits**:
444
+ * The `PayloadTooLargeError` is typically caused by Express body-parser limits, not Cloud Run limits.
445
+ * Configure `bodyParserLimit` in your HttpServer module config to increase the limit (default: 200kb).
446
+ * Cloud Run supports request bodies up to 32MB, but Express must be configured to accept them.
447
+ */
448
+ async deployImage() {
449
+ const imageTag = this.runtimeContext.runtimeParams.deployImage;
450
+ // Tag is validated by CLI param processor
451
+ const containerDeployment = this.config.containerDeployment;
452
+ if (!containerDeployment) {
453
+ throw new ImplementationMissingException(`Missing containerDeployment config in unit ${this.config.key}`);
454
+ }
455
+ const artifactRegistry = containerDeployment.artifactRegistry;
456
+ const imageName = containerDeployment.imageName;
457
+ const artifactRegistryPath = `${artifactRegistry.region}-docker.pkg.dev/${artifactRegistry.projectId}/${artifactRegistry.repository}`;
458
+ // Use 'latest' tag if no specific tag provided, otherwise use the provided tag
459
+ const imageTagToUse = imageTag || CONST_LatestTag;
460
+ const imageReference = `${artifactRegistryPath}/${imageName}:${imageTagToUse}`;
461
+ this.logInfo(`Deploying container image: ${imageReference}`);
462
+ // Validate that configured functions exist in compiled code
463
+ await this.validateFunctionsExist();
464
+ // Delete functions if requested
465
+ await this.deleteFunctions();
466
+ // Determine which functions to deploy
467
+ const deployFunctionParam = this.runtimeContext.runtimeParams.deployFunction;
468
+ const allFunctionConfigs = this.normalizeFunctionConfigs();
469
+ let functionsToDeploy;
470
+ if (deployFunctionParam) {
471
+ // Deploy single function
472
+ const functionConfig = this.getFunctionConfig(deployFunctionParam);
473
+ if (!functionConfig) {
474
+ const functionNames = this.getFunctionNames();
475
+ throw new ImplementationMissingException(`Function '${deployFunctionParam}' not found in configured functions: ${functionNames.join(', ')}`);
476
+ }
477
+ functionsToDeploy = [functionConfig];
478
+ }
479
+ else {
480
+ // Deploy all functions from config
481
+ functionsToDeploy = allFunctionConfigs;
482
+ }
483
+ const region = artifactRegistry.region;
484
+ // Use runtime project ID (where function is deployed), not Artifact Registry project ID
485
+ const envConfig = this.getEnvConfig();
486
+ const runtimeProjectId = envConfig.projectId;
487
+ // Deploy each function separately with the same image but different entry points
488
+ const commando = this.allocateCommando(Commando_NVM).applyNVM()
489
+ .cd(this.config.fullPath)
490
+ .setLogLevelFilter(deployLogFilter);
491
+ this.logInfo(`Deploying ${functionsToDeploy.length} function(s): ${functionsToDeploy.map(f => f.name).join(', ')}`);
492
+ // Deploy each function
493
+ for (const functionConfig of functionsToDeploy) {
494
+ const functionName = functionConfig.name;
495
+ const trigger = functionConfig.trigger;
496
+ // Cloud Run service names cannot contain underscores, convert to dashes
497
+ // But FUNCTION_TARGET must use the original function name (with underscore) as it's exported in code
498
+ const serviceName = functionName.replace(/_/g, '-');
499
+ this.logInfo(`Deploying function: ${functionName}`);
500
+ this.logInfo(` Service name: ${serviceName} (Cloud Run requires dashes, not underscores)`);
501
+ this.logInfo(` Function target: ${functionName} (original function name for FUNCTION_TARGET)`);
502
+ this.logInfo(` Trigger type: ${trigger}`);
503
+ // Validate trigger-specific requirements
504
+ if (trigger === 'schedule' && !functionConfig.schedule) {
505
+ throw new ImplementationMissingException(`Function '${functionName}' has trigger type 'schedule' but no schedule expression is configured. Add 'schedule' property to function config.`);
506
+ }
507
+ // Construct Firebase configuration JSON (matches Firebase Functions deployment format)
508
+ // Convert region format (e.g., "us-central1" -> "us-central")
509
+ const locationId = region.replace(/\d+$/, ''); // Remove trailing digits
510
+ const firebaseConfig = {
511
+ projectId: runtimeProjectId,
512
+ databaseURL: `https://${runtimeProjectId}-default-rtdb.firebaseio.com`,
513
+ storageBucket: `${runtimeProjectId}.appspot.com`,
514
+ locationId: locationId
515
+ };
516
+ // Set required environment variables for Firebase Admin SDK
517
+ // FIREBASE_CONFIG is required by Firebase Admin SDK to determine database URL and other services
518
+ // Use env-vars-file to avoid shell escaping issues with JSON values containing special characters
519
+ const firebaseConfigJson = JSON.stringify(firebaseConfig);
520
+ // Create temporary file for environment variables
521
+ // This avoids shell escaping issues with JSON values containing braces, commas, and quotes
522
+ const buildOutputDir = resolve(this.config.fullPath, CONST_TrashDir);
523
+ const envVarsFile = resolve(buildOutputDir, `env-vars-${functionName}.json`);
524
+ const envVars = {
525
+ FUNCTION_TARGET: functionName,
526
+ GCLOUD_PROJECT: runtimeProjectId,
527
+ GOOGLE_CLOUD_PROJECT: runtimeProjectId,
528
+ FIREBASE_CONFIG: firebaseConfigJson,
529
+ EVENTARC_CLOUD_EVENT_SOURCE: `projects/${runtimeProjectId}/locations/${region}/services/${serviceName}`,
530
+ LOG_EXECUTION_ID: 'true'
531
+ };
532
+ // Add schedule-specific environment variable if needed
533
+ if (trigger === 'schedule' && functionConfig.schedule) {
534
+ envVars.SCHEDULE = functionConfig.schedule;
535
+ }
536
+ await FileSystemUtils.file.write.json(envVarsFile, envVars);
537
+ // Build Cloud Run service YAML definition using template
538
+ const resources = functionConfig.resources;
539
+ // Generate environment variables as YAML array
540
+ // Indentation: 8 spaces for list item, 10 spaces for value (under env: which is at 8 spaces)
541
+ const envVarsYaml = Object.entries(envVars)
542
+ .map(([name, value]) => {
543
+ // Always quote string values to prevent YAML from interpreting them as booleans/numbers
544
+ // This is critical for values like 'true', 'false', 'yes', 'no', etc.
545
+ const escapedValue = typeof value === 'string'
546
+ ? JSON.stringify(value) // JSON.stringify adds quotes and escapes special characters
547
+ : value;
548
+ return ` - name: ${name}\n value: ${escapedValue}`;
549
+ })
550
+ .join('\n');
551
+ // Generate service account line conditionally
552
+ const serviceAccountYaml = functionConfig.serviceAccountName
553
+ ? ` serviceAccountName: ${functionConfig.serviceAccountName}`
554
+ : '';
555
+ // Build template parameters
556
+ // Convert cpu to string if it's a number
557
+ const cpuValue = resources?.cpu !== undefined
558
+ ? (typeof resources.cpu === 'number' ? resources.cpu.toString() : resources.cpu)
559
+ : '1';
560
+ const serviceYamlParams = {
561
+ SERVICE_NAME: serviceName,
562
+ FUNCTION_NAME: functionName,
563
+ REGION: region,
564
+ RUNTIME_PROJECT_ID: runtimeProjectId,
565
+ IMAGE_REFERENCE: imageReference,
566
+ TRIGGER_TYPE: trigger === 'http' ? 'HTTP_TRIGGER' : 'EVENT_TRIGGER',
567
+ MAX_INSTANCES: (resources?.maxInstances ?? 100).toString(),
568
+ MIN_INSTANCES: (resources?.minInstances ?? 0).toString(),
569
+ CONCURRENCY: (resources?.concurrency ?? 100).toString(),
570
+ TIMEOUT: (resources?.timeout ?? 540).toString(),
571
+ CPU: cpuValue,
572
+ MEMORY: resources?.memory || '2Gi',
573
+ ENV_VARS: envVarsYaml,
574
+ SERVICE_ACCOUNT: serviceAccountYaml
575
+ };
576
+ // Generate service YAML from template
577
+ const serviceYamlFile = resolve(buildOutputDir, `service-${functionName}.yaml`);
578
+ await FileSystemUtils.file.template.copy(FunctionBuildTemplateFiles.serviceYaml, serviceYamlFile, serviceYamlParams);
579
+ this.logInfo(`Created service YAML at ${serviceYamlFile}`);
580
+ // Deploy using gcloud run services replace
581
+ const serviceYamlFileRelative = serviceYamlFile.replace(`${this.config.fullPath}/`, '');
582
+ const gcloudDeployCommand = `gcloud run services replace ${serviceYamlFileRelative} --region=${region} --project=${runtimeProjectId}`;
583
+ if (this.runtimeContext.runtimeParams.dryRun) {
584
+ this.logInfo(`[DRY RUN] Would execute: ${gcloudDeployCommand}`);
585
+ continue;
586
+ }
587
+ await this.executeAsyncCommando(commando, gcloudDeployCommand, (stdout, stderr, exitCode) => {
588
+ if (exitCode === 0) {
589
+ // Get service URL after successful deployment
590
+ return;
591
+ }
592
+ throw new CommandoException(`Failed to deploy function ${functionName} with exit code ${exitCode}`, stdout, stderr, exitCode);
593
+ });
594
+ // Get service URL after deployment
595
+ const getUrlCommand = `gcloud run services describe ${serviceName} --region=${region} --project=${runtimeProjectId} --format="value(status.url)"`;
596
+ await this.executeAsyncCommando(commando, getUrlCommand, (stdout, stderr, exitCode) => {
597
+ if (exitCode === 0) {
598
+ const url = stdout.trim();
599
+ if (url) {
600
+ this.functions[functionName] = url;
601
+ this.logInfo(`Function ${functionName} deployed at: ${url}`);
602
+ }
603
+ return;
604
+ }
605
+ // URL retrieval failure is not critical, log warning but don't fail
606
+ this.logWarning(`Failed to retrieve URL for function ${functionName}: ${stderr || stdout}`);
607
+ });
608
+ }
609
+ this.logInfo(`Functions deployed: `, this.functions);
610
+ }
611
+ //######################### ResolveConfig Logic #########################
612
+ getEnvConfig() {
613
+ const envConfig = this.config.envConfig;
614
+ if (!envConfig)
615
+ throw new ImplementationMissingException(`Missing EnvConfig in unit ${this.config.key}`);
616
+ return envConfig;
617
+ }
618
+ async resolveFunctionsRC() {
619
+ const envConfig = this.getEnvConfig();
620
+ const rcConfig = {
621
+ projects: {
622
+ default: envConfig.projectId
623
+ },
624
+ targets: {
625
+ [envConfig.projectId]: {
626
+ database: {
627
+ [envConfig.projectId]: [envConfig.projectId]
628
+ }
629
+ }
630
+ }
631
+ };
632
+ const targetPath = resolve(this.config.fullPath, CONST_FirebaseRC);
633
+ await FileSystemUtils.file.write.json(targetPath, rcConfig);
634
+ }
635
+ async resolveProxyFile() {
636
+ const envConfig = this.getEnvConfig();
637
+ const targetPath = this.pathToProxy();
638
+ const path = this.runtimeContext.baiConfig.files?.backend?.proxy;
639
+ if (!path)
640
+ return;
641
+ const params = {
642
+ PROJECT_ID: `${envConfig.projectId}`,
643
+ PROXY_PORT: `${this.config.basePort}`,
644
+ SERVER_PORT: `${this.config.basePort + 1}`,
645
+ PATH_TO_SSL_KEY: `${this.config.sslKey}`,
646
+ PATH_TO_SSL_CERTIFICATE: `${this.config.sslCert}`,
647
+ };
648
+ await FileSystemUtils.file.template.copy(path, targetPath, params);
649
+ }
650
+ pathToProxy() {
651
+ return resolve(this.config.fullPath, 'src/main/proxy.ts');
652
+ }
653
+ async resolveConfigDir() {
654
+ //Create the dir if it doesn't exist
655
+ const pathToFirebaseConfigFolder = `${this.config.fullPath}/${this.config.pathToFirebaseConfig}`;
656
+ await FileSystemUtils.folder.create(pathToFirebaseConfigFolder);
657
+ //Fill config dir with relevant files for each file that doesn't exist
658
+ const defaultFiles = this.runtimeContext.baiConfig.files?.firebase;
659
+ if (!defaultFiles) {
660
+ this.logError('No defaultFileRoutes in project config');
661
+ return;
662
+ }
663
+ await Promise.all(Const_FirebaseConfigKeys.map(async (firebaseConfigKey) => {
664
+ const pathToConfigFile = `${pathToFirebaseConfigFolder}/${Const_FirebaseDefaultsKeyToFile[firebaseConfigKey]}`;
665
+ if (!defaultFiles[firebaseConfigKey])
666
+ return;
667
+ const path = resolve(this.runtimeContext.parentUnit.config.fullPath, defaultFiles[firebaseConfigKey]);
668
+ await FileSystemUtils.file.copy(path, pathToConfigFile);
669
+ }));
670
+ }
671
+ async resolveFunctionsJSON() {
672
+ const envConfig = this.getEnvConfig();
673
+ const targetPath = `${this.config.fullPath}/${CONST_FirebaseJSON}`;
674
+ let fileContent;
675
+ // Check if container deployment is active
676
+ const deployImageTag = this.runtimeContext.runtimeParams.deployImage;
677
+ const isContainerDeployment = !!deployImageTag && !!this.config.containerDeployment;
678
+ if (envConfig.isLocal) {
679
+ const port = this.config.basePort;
680
+ fileContent = {
681
+ database: [{
682
+ target: this.config.envConfig.projectId,
683
+ rules: `${this.config.pathToFirebaseConfig}/database.rules.json`
684
+ }],
685
+ firestore: {
686
+ rules: `${this.config.pathToFirebaseConfig}/firestore.rules`,
687
+ indexes: `${this.config.pathToFirebaseConfig}/firestore.indexes.json`
688
+ },
689
+ storage: {
690
+ rules: `${this.config.pathToFirebaseConfig}/storage.rules`
691
+ },
692
+ remoteconfig: {
693
+ template: `${this.config.pathToFirebaseConfig}/remoteconfig.template.json`
694
+ },
695
+ functions: {
696
+ ignore: this.config.ignore,
697
+ source: '.',
698
+ predeploy: [
699
+ 'echo "Thunderstorm - Local environment is not deployable... Aborting..." && exit 2'
700
+ ]
701
+ },
702
+ emulators: {
703
+ singleProjectMode: true,
704
+ functions: { port: port + 1 },
705
+ database: { port: port + 2 },
706
+ firestore: {
707
+ port: port + 3,
708
+ websocketPort: port + 4
709
+ },
710
+ pubsub: { port: port + 5 },
711
+ storage: { port: port + 6 },
712
+ auth: { port: port + 7 },
713
+ ui: { port: port + 8, enabled: true },
714
+ hub: { port: port + 9 },
715
+ logging: { port: port + 10 }
716
+ }
717
+ };
718
+ }
719
+ else if (isContainerDeployment) {
720
+ // Container-based deployment
721
+ // For container deployment, source must still point to a directory (for compatibility)
722
+ // The container image is specified via --docker-image flag in gcloud functions deploy
723
+ fileContent = {
724
+ functions: {
725
+ source: this.config.output.replace(`${this.config.fullPath}/`, ''),
726
+ ignore: this.config.ignore,
727
+ }
728
+ };
729
+ }
730
+ else {
731
+ // Source-based deployment (existing behavior)
732
+ fileContent = {
733
+ functions: {
734
+ source: this.config.output.replace(`${this.config.fullPath}/`, ''),
735
+ ignore: this.config.ignore,
736
+ runtime: 'nodejs22',
737
+ }
738
+ };
739
+ }
740
+ await FileSystemUtils.file.write.json(targetPath, fileContent);
741
+ }
742
+ async resolveFunctionsRuntimeConfig() {
743
+ const envConfig = this.getEnvConfig();
744
+ const targetPath = `${this.config.fullPath}/src/main/config.ts`;
745
+ const envKey = this.runtimeContext.runtimeParams.environment;
746
+ const beConfig = {
747
+ envKey,
748
+ pathToDefaultConfig: envConfig.defaultConfig ?? `/_config/default`,
749
+ pathToEnvOverrideConfig: envConfig.defaultConfig ?? `/_config/${envKey}`,
750
+ };
751
+ const inLocalIgnoreTLS = `${envConfig.isLocal ? '// @ts-ignore\nprocess.env[\'NODE_TLS_REJECT_UNAUTHORIZED\'] = 0;\n\n' : ''}`;
752
+ const fileContent = `${inLocalIgnoreTLS}export const Environment = ${JSON.stringify(beConfig)};`;
753
+ await FileSystemUtils.file.write(targetPath, fileContent);
754
+ }
755
+ //######################### Compile Logic #########################
756
+ async createAppVersionFile() {
757
+ //Writing the file to the package source instead of the output is fine,
758
+ //copyAssetsToOutput will move the file to output
759
+ const targetPath = `${this.config.fullPath}/src/main/${CONST_VersionApp}`;
760
+ const appVersion = this.runtimeContext.version;
761
+ await FileSystemUtils.file.write.json(targetPath, { version: appVersion });
762
+ }
763
+ deriveDistDependencies() {
764
+ return this.dependencyUnits.reduce((dependencies, unit) => {
765
+ dependencies[unit.config.key] = `file:.dependencies/${unit.config.key}`;
766
+ return dependencies;
767
+ }, super.deriveDistDependencies());
768
+ }
769
+ async createDependenciesDir() {
770
+ //Gather units that are dependencies of this unit
771
+ await Promise.all(this.dependencyUnits.map(async (unit) => {
772
+ //Copy dependency unit output into this units output/.dependency dir
773
+ const dependencyOutputPath = `${unit.config.output}/`;
774
+ const targetPath = `${this.config.output}/.dependencies/${unit.config.key}/`;
775
+ await FileSystemUtils.folder.create(targetPath);
776
+ await this.allocateCommando()
777
+ .append(`rsync -a --delete ${dependencyOutputPath} ${targetPath}`)
778
+ .execute();
779
+ }));
780
+ }
781
+ //######################### Launch Logic #########################
782
+ async runProxy() {
783
+ await this.resolveProxyFile();
784
+ const commando = this.allocateCommando(Commando_NVM).applyNVM()
785
+ .cd(this.config.fullPath);
786
+ await this.executeAsyncCommando(commando, `${this.npmCommand('tsx')} src/main/proxy.ts`);
787
+ this.logWarning('PROXY TERMINATED');
788
+ }
789
+ async runEmulator() {
790
+ const commando = this.allocateCommando(Commando_NVM).applyNVM()
791
+ .setUID(this.config.key)
792
+ .cd(this.config.fullPath)
793
+ .setLogLevelFilter((log, type) => {
794
+ if (this.emulatorLogStrings.error.some(errStr => log.includes(errStr)))
795
+ return LogLevel.Error;
796
+ if (this.emulatorLogStrings.warning.some(warnStr => log.includes(warnStr)))
797
+ return LogLevel.Warning;
798
+ })
799
+ .onLog(/.*Emulator Hub running.*/, () => this.setStatus('Launch Complete'));
800
+ await this.executeAsyncCommando(commando, `${this.npmCommand('firebase')} emulators:start --project ${this.config.envConfig.projectId} --export-on-exit --import=${this.config.pathToEmulatorData} ${this.runtimeContext.runtimeParams.debugBackend
801
+ ? `--inspect-functions ${this.config.debugPort}` : ''}`);
802
+ this.logWarning('EMULATORS TERMINATED');
803
+ }
804
+ }