@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.
- package/BuildAndInstall.d.ts +40 -0
- package/BuildAndInstall.js +155 -0
- package/build-and-install-v3.d.ts +1 -44
- package/build-and-install-v3.js +1 -157
- package/build-and-install.js +11 -11
- package/config/consts.d.ts +43 -0
- package/config/consts.js +42 -0
- package/{core → config}/package/consts.d.ts +1 -1
- package/{core → config}/types/project-config.d.ts +3 -0
- package/core/FilesCache.d.ts +50 -0
- package/core/FilesCache.js +76 -0
- package/core/Unit_HelpPrinter.d.ts +16 -0
- package/core/Unit_HelpPrinter.js +47 -0
- package/core/params/params.d.ts +1 -41
- package/core/params/params.js +1 -332
- package/core/params.d.ts +50 -0
- package/core/params.js +441 -0
- package/{v3/core → core}/types.d.ts +1 -1
- package/{v3/UnitsDependencyMapper → dependencies}/UnitsDependencyMapper.d.ts +21 -1
- package/{v3/UnitsDependencyMapper → dependencies}/UnitsDependencyMapper.js +26 -3
- package/dependencies/types.d.ts +1 -0
- package/dependencies/types.js +1 -0
- package/exceptions/PhaseAggregatedException.d.ts +34 -0
- package/{core/exceptions → exceptions}/PhaseAggregatedException.js +26 -0
- package/exceptions/UnitPhaseException.d.ts +20 -0
- package/exceptions/UnitPhaseException.js +21 -0
- package/exports/ExportIndexCache.d.ts +25 -0
- package/exports/ExportIndexCache.js +115 -0
- package/exports/ExportMapper.d.ts +43 -0
- package/exports/ExportMapper.js +519 -0
- package/exports/IndicesMcpServer.d.ts +22 -0
- package/exports/IndicesMcpServer.js +220 -0
- package/exports/types.js +3 -0
- package/package.json +20 -9
- package/phases/PhaseManager.d.ts +130 -0
- package/{v3 → phases}/PhaseManager.js +99 -2
- package/{v3/phase → phases/definitions}/consts.d.ts +36 -0
- package/{v3/phase → phases/definitions}/consts.js +44 -2
- package/phases/definitions/types.d.ts +40 -0
- package/phases/index.d.ts +2 -0
- package/phases/index.js +2 -0
- package/run.js +10 -0
- package/runtime/RunningStatusHandler.d.ts +104 -0
- package/runtime/RunningStatusHandler.js +153 -0
- package/runtime/types.d.ts +1 -0
- package/runtime/types.js +2 -0
- package/{defaults → templates}/consts.d.ts +9 -0
- package/{defaults → templates}/consts.js +12 -2
- package/templates/firebase/functions/cloudbuild.yaml +17 -0
- package/templates/firebase/functions/dockerfile +19 -0
- package/templates/firebase/functions/service.yaml +49 -0
- package/{v3/units → units/base}/BaseUnit.d.ts +34 -4
- package/{v3/units → units/base}/BaseUnit.js +22 -2
- package/units/base/ProjectUnit.d.ts +32 -0
- package/units/base/ProjectUnit.js +25 -0
- package/units/base/types.js +1 -0
- package/units/discovery/UnitsMapper.d.ts +69 -0
- package/{v3/UnitsMapper → units/discovery}/UnitsMapper.js +50 -2
- package/units/discovery/resolvers/UnitMapper_Base.d.ts +65 -0
- package/units/discovery/resolvers/UnitMapper_Base.js +46 -0
- package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_FirebaseFunction.d.ts +5 -3
- package/units/discovery/resolvers/UnitMapper_FirebaseFunction.js +105 -0
- package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_FirebaseHosting.d.ts +3 -2
- package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_FirebaseHosting.js +14 -10
- package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_Node.d.ts +1 -1
- package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_Node.js +2 -2
- package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_NodeLib.d.ts +24 -1
- package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_NodeLib.js +24 -1
- package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_NodeProject.d.ts +22 -1
- package/{v3/UnitsMapper → units/discovery}/resolvers/UnitMapper_NodeProject.js +22 -1
- package/units/discovery/types.js +1 -0
- package/units/implementations/Unit_NodeProject.d.ts +59 -0
- package/{v3/units → units/implementations}/Unit_NodeProject.js +65 -5
- package/units/implementations/Unit_PackageJson.d.ts +56 -0
- package/{v3/units → units/implementations}/Unit_PackageJson.js +39 -3
- package/{v3/units → units/implementations}/Unit_TypescriptLib.d.ts +40 -4
- package/{v3/units → units/implementations}/Unit_TypescriptLib.js +167 -17
- package/units/implementations/firebase/Unit_FirebaseFunctionsApp.d.ts +233 -0
- package/units/implementations/firebase/Unit_FirebaseFunctionsApp.js +804 -0
- package/units/implementations/firebase/Unit_FirebaseHostingApp.d.ts +113 -0
- package/units/implementations/firebase/Unit_FirebaseHostingApp.js +320 -0
- package/units/implementations/firebase/common.d.ts +26 -0
- package/units/implementations/firebase/common.js +65 -0
- package/units/index.d.ts +6 -0
- package/units/index.js +6 -0
- package/v3/core/Unit_HelpPrinter.d.ts +1 -16
- package/v3/core/Unit_HelpPrinter.js +1 -47
- package/{v3 → workspace}/Workspace.d.ts +30 -15
- package/{v3 → workspace}/Workspace.js +48 -35
- package/core/consts.d.ts +0 -13
- package/core/consts.js +0 -12
- package/core/exceptions/PhaseAggregatedException.d.ts +0 -8
- package/core/exceptions/UnitPhaseException.d.ts +0 -5
- package/core/exceptions/UnitPhaseException.js +0 -6
- package/old/PhaseRunnerDispatcher.d.ts +0 -24
- package/old/PhaseRunnerDispatcher.js +0 -32
- package/old/runner-dispatchers.d.ts +0 -10
- package/old/runner-dispatchers.js +0 -3
- package/v3/PhaseManager.d.ts +0 -27
- package/v3/RunningStatusHandler.d.ts +0 -18
- package/v3/RunningStatusHandler.js +0 -67
- package/v3/UnitsMapper/UnitsMapper.d.ts +0 -21
- package/v3/UnitsMapper/resolvers/UnitMapper_Base.d.ts +0 -23
- package/v3/UnitsMapper/resolvers/UnitMapper_Base.js +0 -16
- package/v3/UnitsMapper/resolvers/UnitMapper_FirebaseFunction.js +0 -66
- package/v3/core/FilesCache.d.ts +0 -7
- package/v3/core/FilesCache.js +0 -33
- package/v3/phase/types.d.ts +0 -10
- package/v3/units/ProjectUnit.d.ts +0 -18
- package/v3/units/ProjectUnit.js +0 -11
- package/v3/units/Unit_NodeProject.d.ts +0 -30
- package/v3/units/Unit_PackageJson.d.ts +0 -17
- package/v3/units/firebase/Unit_FirebaseFunctionsApp.d.ts +0 -64
- package/v3/units/firebase/Unit_FirebaseFunctionsApp.js +0 -306
- package/v3/units/firebase/Unit_FirebaseHostingApp.d.ts +0 -49
- package/v3/units/firebase/Unit_FirebaseHostingApp.js +0 -118
- package/v3/units/firebase/common.d.ts +0 -3
- package/v3/units/firebase/common.js +0 -13
- package/v3/units/index.d.ts +0 -6
- package/v3/units/index.js +0 -6
- /package/{core → config}/package/consts.js +0 -0
- /package/{core → config}/types/configs/firebasejson.d.ts +0 -0
- /package/{core → config}/types/configs/firebasejson.js +0 -0
- /package/{core → config}/types/configs/firebaserc.d.ts +0 -0
- /package/{core → config}/types/configs/firebaserc.js +0 -0
- /package/{core → config}/types/configs/index.d.ts +0 -0
- /package/{core → config}/types/configs/index.js +0 -0
- /package/{core → config}/types/configs/package-json.d.ts +0 -0
- /package/{core → config}/types/configs/package-json.js +0 -0
- /package/{core → config}/types/core.d.ts +0 -0
- /package/{core → config}/types/core.js +0 -0
- /package/{core → config}/types/index.d.ts +0 -0
- /package/{core → config}/types/index.js +0 -0
- /package/{core → config}/types/package/index.d.ts +0 -0
- /package/{core → config}/types/package/index.js +0 -0
- /package/{core → config}/types/package/package.d.ts +0 -0
- /package/{core → config}/types/package/package.js +0 -0
- /package/{core → config}/types/package/runtime-package.d.ts +0 -0
- /package/{core → config}/types/package/runtime-package.js +0 -0
- /package/{core → config}/types/project-config.js +0 -0
- /package/{v3/core → core}/types.js +0 -0
- /package/{v3/UnitsMapper/types.js → exports/types.d.ts} +0 -0
- /package/{v3/phase → phases/definitions}/index.d.ts +0 -0
- /package/{v3/phase → phases/definitions}/index.js +0 -0
- /package/{v3/phase → phases/definitions}/types.js +0 -0
- /package/{v3/units/types.js → run.d.ts} +0 -0
- /package/{defaults/backend-proxy → templates/backend/proxy}/proxy._ts +0 -0
- /package/{defaults/.firebase_config → templates/firebase/config}/database.rules.json +0 -0
- /package/{defaults/.firebase_config → templates/firebase/config}/firestore.indexes.json +0 -0
- /package/{defaults/.firebase_config → templates/firebase/config}/firestore.rules +0 -0
- /package/{defaults/.firebase_config → templates/firebase/config}/storage.rules +0 -0
- /package/{v3/units → units/base}/types.d.ts +0 -0
- /package/{v3/UnitsMapper → units/discovery}/resolvers/index.d.ts +0 -0
- /package/{v3/UnitsMapper → units/discovery}/resolvers/index.js +0 -0
- /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
|
+
}
|