@nu-art/build-and-install 0.401.9 → 0.500.6

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 (67) hide show
  1. package/BuildAndInstall.d.ts +2 -2
  2. package/BuildAndInstall.js +10 -2
  3. package/config/consts.d.ts +2 -2
  4. package/config/consts.js +1 -1
  5. package/config/package/consts.d.ts +1 -1
  6. package/config/types/package/runtime-package.d.ts +1 -1
  7. package/config/types/project-config.d.ts +17 -2
  8. package/core/FilesCache.d.ts +5 -1
  9. package/core/FilesCache.js +5 -1
  10. package/core/Unit_HelpPrinter.d.ts +4 -7
  11. package/core/Unit_HelpPrinter.js +3 -11
  12. package/core/params.d.ts +2 -1
  13. package/core/params.js +12 -3
  14. package/core/types.d.ts +2 -2
  15. package/dependencies/UnitsDependencyMapper.d.ts +8 -0
  16. package/dependencies/UnitsDependencyMapper.js +58 -4
  17. package/exceptions/UnitPhaseException.d.ts +1 -1
  18. package/exports/IndicesMcpServer.js +1 -1
  19. package/package.json +5 -5
  20. package/phases/PhaseManager.d.ts +2 -2
  21. package/phases/PhaseManager.js +4 -4
  22. package/phases/definitions/consts.d.ts +3 -0
  23. package/phases/definitions/consts.js +7 -0
  24. package/phases/definitions/types.d.ts +1 -1
  25. package/templates/backend/proxy/proxy._ts +36 -31
  26. package/templates/firebase/config/database.rules.json +6 -0
  27. package/units/base/BaseUnit.d.ts +1 -1
  28. package/units/discovery/UnitsMapper.d.ts +3 -2
  29. package/units/discovery/resolvers/UnitMapper_Base.d.ts +2 -2
  30. package/units/discovery/resolvers/UnitMapper_FirebaseFunction.d.ts +12 -1
  31. package/units/discovery/resolvers/UnitMapper_FirebaseFunction.js +48 -18
  32. package/units/discovery/resolvers/UnitMapper_FirebaseHosting.d.ts +5 -9
  33. package/units/discovery/resolvers/UnitMapper_FirebaseHosting.js +3 -1
  34. package/units/discovery/resolvers/UnitMapper_Node.d.ts +1 -1
  35. package/units/discovery/resolvers/UnitMapper_Node.js +8 -15
  36. package/units/discovery/resolvers/UnitMapper_ViteHosting.d.ts +46 -0
  37. package/units/discovery/resolvers/UnitMapper_ViteHosting.js +59 -0
  38. package/units/implementations/Unit_NodeProject.d.ts +3 -2
  39. package/units/implementations/Unit_NodeProject.js +30 -6
  40. package/units/implementations/Unit_PackageJson.d.ts +8 -2
  41. package/units/implementations/Unit_PackageJson.js +46 -11
  42. package/units/implementations/Unit_TypescriptLib.d.ts +8 -2
  43. package/units/implementations/Unit_TypescriptLib.js +155 -4
  44. package/units/implementations/firebase/Unit_FirebaseFunctionsApp.d.ts +12 -2
  45. package/units/implementations/firebase/Unit_FirebaseFunctionsApp.js +73 -17
  46. package/units/implementations/firebase/Unit_FirebaseHostingApp.d.ts +19 -97
  47. package/units/implementations/firebase/Unit_FirebaseHostingApp.js +28 -290
  48. package/units/implementations/firebase/Unit_HostingApp.d.ts +59 -0
  49. package/units/implementations/firebase/Unit_HostingApp.js +225 -0
  50. package/units/implementations/firebase/Unit_ViteHostingApp.d.ts +10 -0
  51. package/units/implementations/firebase/Unit_ViteHostingApp.js +28 -0
  52. package/workspace/Workspace.d.ts +3 -1
  53. package/workspace/Workspace.js +2 -1
  54. package/config/types/configs/index.d.ts +0 -3
  55. package/config/types/configs/index.js +0 -3
  56. package/config/types/index.d.ts +0 -4
  57. package/config/types/index.js +0 -4
  58. package/config/types/package/index.d.ts +0 -2
  59. package/config/types/package/index.js +0 -2
  60. package/phases/definitions/index.d.ts +0 -2
  61. package/phases/definitions/index.js +0 -2
  62. package/phases/index.d.ts +0 -2
  63. package/phases/index.js +0 -2
  64. package/units/discovery/resolvers/index.d.ts +0 -4
  65. package/units/discovery/resolvers/index.js +0 -4
  66. package/units/index.d.ts +0 -6
  67. package/units/index.js +0 -6
@@ -1,110 +1,45 @@
1
- import { ImplementationMissingException, LogLevel } from '@nu-art/ts-common';
2
- import { CONST_DeployHostingDir, CONST_DeploymentMetadata, CONST_FirebaseJSON, CONST_FirebaseRC, CONST_HostingBuildTarball, CONST_StagingDir, CONST_TrashDir, CONST_VersionApp } from '../../../config/consts.js';
1
+ import { LogLevel } from '@nu-art/ts-common';
3
2
  import { Commando_Basic, Commando_NVM, CommandoException } from '@nu-art/commando';
3
+ import { Unit_HostingApp } from './Unit_HostingApp.js';
4
4
  import { resolve } from 'path';
5
- import { Unit_TypescriptLib } from '../Unit_TypescriptLib.js';
6
- import { deployLogFilter, ensureArtifactRegistryRepository } from './common.js';
7
5
  import { FileSystemUtils } from '@nu-art/ts-common/utils/FileSystemUtils';
8
6
  /**
9
- * Firebase Hosting application unit.
10
- *
11
- * **Key Features**:
12
- * - Extends Unit_TypescriptLib (compiles TypeScript)
13
- * - Manages Firebase hosting configuration (firebase.json, .firebaserc)
14
- * - Supports multiple environments (local, staging, production)
15
- * - Implements launch and deploy phases
16
- *
17
- * **Phases Implemented**:
18
- * - `prepare()`: Resolves Firebase hosting config files
19
- * - `compile()`: Compiles TypeScript (without declarations)
20
- * - `launch()`: Starts Firebase hosting emulator
21
- * - `deploy()`: Deploys to Firebase hosting
22
- *
23
- * **Configuration**:
24
- * - `servingPort`: Port for local hosting server
25
- * - `hostingConfig`: Firebase hosting configuration (public folder, rewrites)
26
- * - `envConfig`: Environment-specific config (projectId, isLocal)
7
+ * Firebase Hosting application unit (webpack bundler).
8
+ * Extends Unit_HostingApp; implements compile via npm run build, launch via npm run start.
27
9
  */
28
- export class Unit_FirebaseHostingApp extends Unit_TypescriptLib {
29
- hosting = {};
30
- injectedMetadata = {};
31
- static DefaultConfig_FirebaseHosting = {
32
- servingPort: 8100,
33
- output: 'dist',
34
- };
10
+ export class Unit_FirebaseHostingApp extends Unit_HostingApp {
11
+ static DefaultConfig_FirebaseHosting = Unit_HostingApp.DefaultConfig_Hosting;
35
12
  constructor(config) {
36
13
  super(config);
37
14
  this.addToClassStack(Unit_FirebaseHostingApp);
38
15
  }
39
- //######################### Phase Implementations #########################
40
16
  async prepare() {
41
17
  await super.prepare();
42
- await this.resolveHostingRC();
43
- await this.resolveHostingJSON();
44
- await this.resolveHostingRuntimeConfig();
18
+ await this.resolveWebpackEnvConfig();
45
19
  }
46
- async compile() {
47
- await this.resolveTSConfig(resolve(this.config.fullPath, './src'), 'main', { compilerOptions: { declaration: false } });
48
- await this.clearOutputDir();
49
- await this.createAppVersionFile();
50
- await this.compileImpl();
51
- }
52
- async launch() {
53
- this.setStatus('Launching');
54
- await this.releaseWebpackPorts();
55
- await this.runApp();
56
- }
57
- async releaseWebpackPorts() {
58
- return this.releasePorts([`${this.config.servingPort}`]);
59
- }
60
- async deploy() {
61
- const commando = this.allocateCommando(Commando_NVM).applyNVM()
62
- .cd(this.config.fullPath)
63
- .setLogLevelFilter(deployLogFilter)
64
- // example: Function URL (hello(us-central1)): https://hello-kv65k7yylq-uc.a.run.app
65
- .onLog(/.*Hosting URL.*(https:\/\/.*?)$/, match => {
66
- this.hosting[match[1]] = match[2];
67
- });
68
- const debug = this.runtimeContext.runtimeParams.verbose ? ' --debug' : '';
69
- await this.executeAsyncCommando(commando, `${this.npmCommand('firebase')}${debug} deploy --only hosting`);
70
- }
71
- //######################### ResolveConfig Logic #########################
72
- getEnvConfig() {
73
- const envConfig = this.config.envConfig;
74
- if (!envConfig)
75
- throw new ImplementationMissingException(`Missing EnvConfig in unit ${this.config.label}`);
76
- return envConfig;
77
- }
78
- async resolveHostingRC() {
20
+ async resolveWebpackEnvConfig() {
79
21
  const envConfig = this.getEnvConfig();
80
- const rcConfig = { projects: { default: envConfig.projectId } };
81
- const targetPath = `${this.config.fullPath}/${CONST_FirebaseRC}`;
82
- await FileSystemUtils.file.write.json(targetPath, rcConfig);
83
- }
84
- async resolveHostingJSON() {
85
- const envConfig = this.getEnvConfig();
86
- const targetPath = `${this.config.fullPath}/${CONST_FirebaseJSON}`;
87
- let fileContent;
88
- if (envConfig.isLocal)
89
- fileContent = {};
90
- else
91
- fileContent = {
92
- hosting: this.config.hostingConfig ?? {
93
- 'public': 'dist',
94
- 'rewrites': [
95
- { 'source': '**', 'destination': '/index.html' }
96
- ]
97
- }
98
- };
99
- await FileSystemUtils.file.write.json(targetPath, fileContent);
100
- }
101
- async resolveHostingRuntimeConfig() {
102
- const envConfig = this.getEnvConfig().config;
103
- const targetPath = resolve(this.config.fullPath, `./src/main/config.ts`);
104
- const fileContent = `export const config = ${JSON.stringify(envConfig, null, 2)};`;
22
+ const wp = envConfig.webpackConfig;
23
+ const minifyHtml = wp?.minifyHtml ?? false;
24
+ const devServerSSL = wp?.devServerSSL ?? false;
25
+ const devServerPort = wp?.devServerPort ?? this.config.servingPort;
26
+ const sslKey = wp?.sslKey ? `"${wp.sslKey}"` : 'undefined';
27
+ const sslCert = wp?.sslCert ? `"${wp.sslCert}"` : 'undefined';
28
+ const envName = this.runtimeContext.runtimeParams.environment ?? 'local';
29
+ const targetPath = resolve(this.config.fullPath, 'webpack-env.generated.ts');
30
+ const fileContent = [
31
+ `/** Generated by BAI during prepare - do not edit */`,
32
+ `export const webpackEnvConfig = {`,
33
+ ` envName: "${envName}",`,
34
+ ` minifyHtml: ${minifyHtml},`,
35
+ ` devServerSSL: ${devServerSSL},`,
36
+ ` devServerPort: ${devServerPort},`,
37
+ ` sslKey: ${sslKey},`,
38
+ ` sslCert: ${sslCert},`,
39
+ `};\n`,
40
+ ].join('\n');
105
41
  await FileSystemUtils.file.write(targetPath, fileContent);
106
42
  }
107
- //######################### Compile Logic #########################
108
43
  async compileImpl() {
109
44
  const commando = this.allocateCommando(Commando_NVM, Commando_Basic).applyNVM()
110
45
  .cd(this.config.fullPath);
@@ -113,212 +48,15 @@ export class Unit_FirebaseHostingApp extends Unit_TypescriptLib {
113
48
  throw new CommandoException(`Error compiling`, stdout, stderr, exitCode);
114
49
  });
115
50
  }
116
- async createAppVersionFile() {
117
- //Writing the file to the package source instead of the output is fine,
118
- //Webpack bundles files into the output automatically!
119
- const targetPath = `${this.config.fullPath}/src/main/${CONST_VersionApp}`;
120
- const appVersion = this.runtimeContext.version;
121
- await FileSystemUtils.file.write.json(targetPath, { version: appVersion });
122
- }
123
- //######################### Launch Logic #########################
124
51
  async runApp() {
125
52
  const commando = this.allocateCommando(Commando_NVM).applyNVM()
126
53
  .setUID(this.config.key)
127
54
  .cd(this.config.fullPath)
128
- .setLogLevelFilter((log, type) => {
55
+ .setLogLevelFilter((log) => {
129
56
  if (log.toLowerCase().includes('<i>'))
130
57
  return LogLevel.Info;
131
58
  });
132
59
  await this.executeAsyncCommando(commando, 'npm run start');
133
60
  this.logWarning('HOSTING TERMINATED');
134
61
  }
135
- //######################### Build Push Image Phase #########################
136
- /**
137
- * Builds hosting output and uploads it to Artifact Registry as a generic package.
138
- *
139
- * **Process**:
140
- * 1. Validates hostingDeployment config exists
141
- * 2. Creates deployment-metadata.json with build information
142
- * 3. Creates tarball of hosting output directory
143
- * 4. Uploads to Artifact Registry as generic package
144
- *
145
- * **Requirements**:
146
- * - `--build-push-image <tag>` CLI flag with tag value
147
- * - `hostingDeployment` config in unit config with `packageName` field
148
- * - `packageName` must comply with Artifact Registry naming restrictions
149
- * - gcloud CLI installed and authenticated
150
- * - Artifact Registry API enabled in GCP project
151
- */
152
- async buildPushImage() {
153
- const hostingDeployment = this.config.hostingDeployment;
154
- if (!hostingDeployment) {
155
- throw new ImplementationMissingException(`Missing hostingDeployment config in unit ${this.config.key}`);
156
- }
157
- const buildTag = this.runtimeContext.runtimeParams.buildPushImage;
158
- if (!buildTag) {
159
- throw new ImplementationMissingException(`Missing buildPushImage runtime param in unit ${this.config.key}`);
160
- }
161
- const artifactRegistry = hostingDeployment.artifactRegistry;
162
- const packageName = hostingDeployment.packageName;
163
- if (!packageName) {
164
- throw new ImplementationMissingException(`Missing packageName in hostingDeployment config for unit ${this.config.key}`);
165
- }
166
- this.logInfo(`Building and uploading hosting package to Artifact Registry:`);
167
- this.logInfo(` Package: ${packageName}`);
168
- this.logInfo(` Tag: ${buildTag}`);
169
- // Check for dry run mode
170
- if (this.runtimeContext.runtimeParams.dryRun) {
171
- this.logInfo(`[DRY RUN] Would build and upload hosting package: ${packageName}:${buildTag}`);
172
- return;
173
- }
174
- // Ensure Artifact Registry repository exists
175
- const commando = this.allocateCommando();
176
- await ensureArtifactRegistryRepository(commando, artifactRegistry, 'generic', this);
177
- // Create deployment-metadata.json in output directory
178
- const metadata = {
179
- ...this.injectedMetadata,
180
- 'build.timestamp': new Date().toISOString(),
181
- 'build.tag': buildTag,
182
- 'build.project': artifactRegistry.projectId,
183
- 'build.package-name': packageName,
184
- 'version': this.runtimeContext.version,
185
- 'git.commit': process.env.GIT_COMMIT || '',
186
- 'git.branch': process.env.GIT_BRANCH || '',
187
- 'build.user': process.env.USER || '',
188
- };
189
- this.logDebug(`Metadata: `, metadata);
190
- // Create staging directory for tarball contents
191
- const buildOutputDir = resolve(this.config.fullPath, CONST_TrashDir);
192
- const stagingDir = resolve(buildOutputDir, CONST_StagingDir);
193
- await FileSystemUtils.folder.delete(stagingDir);
194
- await FileSystemUtils.folder.create(stagingDir);
195
- const tarballPath = resolve(buildOutputDir, CONST_HostingBuildTarball);
196
- // Copy all files to staging directory
197
- const outputDirName = resolve(this.config.output).split('/').pop() || 'dist';
198
- // Copy firebase.json, .firebaserc, dist folder, and create deployment-metadata.json
199
- await FileSystemUtils.folder.copy(this.config.output, resolve(stagingDir, outputDirName));
200
- await FileSystemUtils.file.write.json(resolve(stagingDir, CONST_DeploymentMetadata), metadata);
201
- // Create tarball from staging directory contents
202
- // Note: Use explicit file list to include hidden files (.*) which * wildcard doesn't match
203
- commando.cd(stagingDir);
204
- await this.executeAsyncCommando(commando, `tar -czf ${tarballPath} ${CONST_DeploymentMetadata} ${outputDirName}`, (stdout, stderr, exitCode) => {
205
- if (exitCode !== 0)
206
- throw new CommandoException(`Failed to create tarball (exit code ${exitCode})`, stdout, stderr, exitCode);
207
- });
208
- this.logInfo(`Created tarball: ${tarballPath}`);
209
- // Upload to Artifact Registry
210
- const region = artifactRegistry.region;
211
- const repository = artifactRegistry.repository;
212
- const projectId = artifactRegistry.projectId;
213
- // Upload file - package and version are created automatically if they don't exist
214
- // Note: Generic packages don't support "latest" version like Docker images
215
- // The build tag should be used for deployment (defaults to 'latest' if not specified)
216
- await this.executeAsyncCommando(commando, `gcloud artifacts generic upload --package=${packageName} --version=${buildTag} --source=${tarballPath} --location=${region} --repository=${repository} --project=${projectId}`, (stdout, stderr, exitCode) => {
217
- if (exitCode !== 0)
218
- throw new CommandoException(`Failed to upload hosting package to Artifact Registry (exit code ${exitCode})`, stdout, stderr, exitCode);
219
- });
220
- this.logInfo(`Successfully uploaded hosting package: ${packageName}:${buildTag} and ${packageName}:latest`);
221
- }
222
- //######################### Deploy Image Phase #########################
223
- /**
224
- * Deploys hosting build from Artifact Registry to Firebase Hosting.
225
- *
226
- * **Process**:
227
- * 1. Validates hostingDeployment config exists
228
- * 2. Downloads tarball from Artifact Registry
229
- * 3. Extracts to temp directory
230
- * 4. Sets up Firebase tools (package.json + npm install)
231
- * 5. Deploys to Firebase Hosting
232
- * 6. Validates deployment by fetching deployment-metadata.json
233
- *
234
- * **Requirements**:
235
- * - `--deploy-image <tag>` CLI flag with tag value
236
- * - `hostingDeployment` config in unit config with `packageName` field
237
- * - Package must already exist in Artifact Registry (built via buildPushImage)
238
- * - gcloud CLI installed and authenticated
239
- * - Firebase CLI (installed via npm in temp directory)
240
- */
241
- async deployImage() {
242
- const hostingDeployment = this.config.hostingDeployment;
243
- if (!hostingDeployment) {
244
- throw new ImplementationMissingException(`Missing hostingDeployment config in unit ${this.config.key}`);
245
- }
246
- const deployTag = this.runtimeContext.runtimeParams.deployImage;
247
- if (!deployTag) {
248
- throw new ImplementationMissingException(`Missing deployImage runtime param in unit ${this.config.key}. Generic packages require a specific version tag (cannot use 'latest')`);
249
- }
250
- const artifactRegistry = hostingDeployment.artifactRegistry;
251
- const packageName = hostingDeployment.packageName;
252
- if (!packageName) {
253
- throw new ImplementationMissingException(`Missing packageName in hostingDeployment config for unit ${this.config.key}`);
254
- }
255
- const region = artifactRegistry.region;
256
- const repository = artifactRegistry.repository;
257
- const projectId = artifactRegistry.projectId;
258
- this.logInfo(`Deploying hosting package: ${packageName}:${deployTag}`);
259
- // Check for dry run mode
260
- if (this.runtimeContext.runtimeParams.dryRun) {
261
- this.logInfo(`[DRY RUN] Would deploy hosting package: ${packageName}:${deployTag}`);
262
- return;
263
- }
264
- // Setup temp directory for deployment
265
- const deployTempDir = resolve(this.config.fullPath, `${CONST_TrashDir}/${CONST_DeployHostingDir}`);
266
- this.logInfo(`Setting up deployment directory: ${deployTempDir}`);
267
- await FileSystemUtils.folder.delete(deployTempDir);
268
- await FileSystemUtils.folder.create(deployTempDir);
269
- const commando = this.allocateCommando(Commando_NVM)
270
- .applyNVM()
271
- .cd(deployTempDir)
272
- .mark();
273
- // Download tarball from Artifact Registry
274
- // Note: --destination must be a directory, not a file path
275
- this.logInfo(`Downloading hosting package from Artifact Registry from: ${projectId}/${repository}/${packageName}/${deployTag}`);
276
- await this.executeAsyncCommando(commando, `gcloud artifacts generic download --package=${packageName} --version=${deployTag} --destination=${deployTempDir} --location=${region} --repository=${repository} --project=${projectId}`, (stdout, stderr, exitCode) => {
277
- if (exitCode !== 0)
278
- throw new CommandoException(`Failed to download hosting package from Artifact Registry (exit code ${exitCode})`, stdout, stderr, exitCode);
279
- });
280
- // Find the downloaded file (gcloud downloads with auto-generated name based on package and version)
281
- // The file will be named: {packageName}-{version}.tar.gz
282
- this.logInfo(`Locating downloaded tarball...`);
283
- const downloadedFiles = await FileSystemUtils.folder.list(deployTempDir);
284
- const tarballFile = downloadedFiles.find(file => file.endsWith('.tar.gz'));
285
- if (!tarballFile)
286
- throw new ImplementationMissingException(`Downloaded tarball not found in ${deployTempDir}. Files found: ${downloadedFiles.join(', ')}`);
287
- const tarballPath = resolve(deployTempDir, tarballFile);
288
- this.logDebug(`Downloaded tarball: ${tarballPath}`);
289
- // Extract tarball directly to deployTempDir (contains firebase.json, .firebaserc, and dist/)
290
- this.logInfo(`Extracting hosting package...`);
291
- this.logInfo(`Extracting hosting package...`);
292
- await this.executeAsyncCommando(commando, `tar -xzf ${tarballPath} -C ${deployTempDir}`, (stdout, stderr, exitCode) => {
293
- if (exitCode !== 0) {
294
- throw new CommandoException(`Failed to extract tarball (exit code ${exitCode})`, stdout, stderr, exitCode);
295
- }
296
- });
297
- this.logInfo(`Extracted hosting package to: ${deployTempDir}`);
298
- this.logInfo(`Copying firebase configs..`);
299
- const firebaseJsonPath = resolve(this.config.fullPath, CONST_FirebaseJSON);
300
- const firebaseRcPath = resolve(this.config.fullPath, CONST_FirebaseRC);
301
- await FileSystemUtils.file.copy(firebaseJsonPath, resolve(deployTempDir, CONST_FirebaseJSON));
302
- await FileSystemUtils.file.copy(firebaseRcPath, resolve(deployTempDir, CONST_FirebaseRC));
303
- this.logDebug(`Copied firebase configs!`);
304
- // firebase.json and .firebaserc are already in deployTempDir from tarball extraction
305
- // Deploy using firebase CLI
306
- const envConfig = this.getEnvConfig();
307
- this.logInfo(`Deploying to Firebase Hosting: ${deployTempDir} => ${envConfig.projectId}`);
308
- const deployCommando = this.allocateCommando(Commando_NVM).applyNVM()
309
- .cd(deployTempDir)
310
- .setLogLevelFilter(deployLogFilter)
311
- .onLog(/.*Hosting URL.*(https:\/\/.*?)$/, match => {
312
- this.hosting[match[1]] = match[2];
313
- });
314
- const debug = this.runtimeContext.runtimeParams.verbose ? ' --debug' : '';
315
- await this.executeAsyncCommando(deployCommando, `npx firebase${debug} deploy --only hosting`, (stdout, stderr, exitCode) => {
316
- if (exitCode !== 0) {
317
- throw new CommandoException(`Failed to deploy hosting (exit code ${exitCode})`, stdout, stderr, exitCode);
318
- }
319
- });
320
- this.logInfo(`Hosting deployed: `, this.hosting);
321
- // Cleanup temp directory (optional - keep for debugging)
322
- // await FileSystemUtils.folder.delete(deployTempDir);
323
- }
324
62
  }
@@ -0,0 +1,59 @@
1
+ import { FirebasePackageConfig } from '../../../config/types/package/package.js';
2
+ import { UnitPhaseImplementor } from '../../../core/types.js';
3
+ import { StringMap, TS_Object } from '@nu-art/ts-common';
4
+ import { Phase_BuildPushImage, Phase_Deploy, Phase_DeployImage, Phase_Launch } from '../../../phases/definitions/consts.js';
5
+ import { Unit_TypescriptLib, Unit_TypescriptLib_Config } from '../Unit_TypescriptLib.js';
6
+ export type FirebaseHostingConfig = {
7
+ public: string;
8
+ rewrites: {
9
+ source: string;
10
+ destination: string;
11
+ }[];
12
+ };
13
+ export type FirebaseHosting_EnvConfig = {
14
+ config: TS_Object;
15
+ projectId: string;
16
+ isLocal?: boolean;
17
+ };
18
+ export type Unit_HostingApp_Config = Unit_TypescriptLib_Config & {
19
+ firebaseConfig?: FirebasePackageConfig;
20
+ servingPort: number;
21
+ hostingConfig?: FirebaseHostingConfig;
22
+ envConfig: FirebaseHosting_EnvConfig;
23
+ sources?: string[];
24
+ hostingDeployment?: {
25
+ artifactRegistry: {
26
+ region: string;
27
+ repository: string;
28
+ projectId: string;
29
+ };
30
+ packageName: string;
31
+ };
32
+ };
33
+ /**
34
+ * Abstract base for hosting application units (Firebase Hosting with webpack or Vite).
35
+ * Handles Firebase config resolution, deploy, buildPushImage, deployImage.
36
+ * Subclasses implement compileImpl() and runApp() for the specific bundler.
37
+ */
38
+ export declare abstract class Unit_HostingApp<C extends Unit_HostingApp_Config = Unit_HostingApp_Config> extends Unit_TypescriptLib<C> implements UnitPhaseImplementor<[Phase_Launch, Phase_Deploy, Phase_BuildPushImage, Phase_DeployImage]> {
39
+ hosting: StringMap;
40
+ injectedMetadata: StringMap;
41
+ static DefaultConfig_Hosting: {
42
+ servingPort: number;
43
+ output: string;
44
+ };
45
+ constructor(config: Unit_HostingApp<C>['config']);
46
+ prepare(): Promise<void>;
47
+ compile(): Promise<void>;
48
+ launch(): Promise<void>;
49
+ deploy(): Promise<void>;
50
+ protected getEnvConfig(): C["envConfig"];
51
+ private resolveHostingRC;
52
+ private resolveHostingJSON;
53
+ private resolveHostingRuntimeConfig;
54
+ protected abstract compileImpl(): Promise<void>;
55
+ protected abstract runApp(): Promise<void>;
56
+ protected createAppVersionFile(): Promise<void>;
57
+ buildPushImage(): Promise<void>;
58
+ deployImage(): Promise<void>;
59
+ }
@@ -0,0 +1,225 @@
1
+ import { ImplementationMissingException } from '@nu-art/ts-common';
2
+ import { CONST_DeployHostingDir, CONST_DeploymentMetadata, CONST_FirebaseJSON, CONST_FirebaseRC, CONST_HostingBuildTarball, CONST_StagingDir, CONST_TrashDir, CONST_VersionApp } from '../../../config/consts.js';
3
+ import { Commando_NVM, CommandoException } from '@nu-art/commando';
4
+ import { resolve } from 'path';
5
+ import { Unit_TypescriptLib } from '../Unit_TypescriptLib.js';
6
+ import { deployLogFilter, ensureArtifactRegistryRepository } from './common.js';
7
+ import { FileSystemUtils } from '@nu-art/ts-common/utils/FileSystemUtils';
8
+ /**
9
+ * Abstract base for hosting application units (Firebase Hosting with webpack or Vite).
10
+ * Handles Firebase config resolution, deploy, buildPushImage, deployImage.
11
+ * Subclasses implement compileImpl() and runApp() for the specific bundler.
12
+ */
13
+ export class Unit_HostingApp extends Unit_TypescriptLib {
14
+ hosting = {};
15
+ injectedMetadata = {};
16
+ static DefaultConfig_Hosting = {
17
+ servingPort: 8100,
18
+ output: 'dist',
19
+ };
20
+ constructor(config) {
21
+ super(config);
22
+ this.addToClassStack(Unit_HostingApp);
23
+ }
24
+ //######################### Phase Implementations #########################
25
+ async prepare() {
26
+ await super.prepare();
27
+ await this.resolveHostingRC();
28
+ await this.resolveHostingJSON();
29
+ await this.resolveHostingRuntimeConfig();
30
+ }
31
+ async compile() {
32
+ await this.resolveTSConfig(resolve(this.config.fullPath, './src'), 'main', { compilerOptions: { declaration: false } });
33
+ await this.clearOutputDir();
34
+ await this.createAppVersionFile();
35
+ await this.compileImpl();
36
+ }
37
+ async launch() {
38
+ this.setStatus('Launching');
39
+ await this.releasePorts([`${this.config.servingPort}`]);
40
+ await this.runApp();
41
+ }
42
+ async deploy() {
43
+ const commando = this.allocateCommando(Commando_NVM).applyNVM()
44
+ .cd(this.config.fullPath)
45
+ .setLogLevelFilter(deployLogFilter)
46
+ .onLog(/.*Hosting URL.*(https:\/\/.*?)$/, match => {
47
+ this.hosting[match[1]] = match[2];
48
+ });
49
+ const debug = this.runtimeContext.runtimeParams.verbose ? ' --debug' : '';
50
+ await this.executeAsyncCommando(commando, `${this.npmCommand('firebase')}${debug} deploy --only hosting`);
51
+ }
52
+ //######################### ResolveConfig Logic #########################
53
+ getEnvConfig() {
54
+ const envConfig = this.config.envConfig;
55
+ if (!envConfig)
56
+ throw new ImplementationMissingException(`Missing EnvConfig in unit ${this.config.label}`);
57
+ return envConfig;
58
+ }
59
+ async resolveHostingRC() {
60
+ const envConfig = this.getEnvConfig();
61
+ const rcConfig = { projects: { default: envConfig.projectId } };
62
+ const targetPath = `${this.config.fullPath}/${CONST_FirebaseRC}`;
63
+ await FileSystemUtils.file.write.json(targetPath, rcConfig);
64
+ }
65
+ async resolveHostingJSON() {
66
+ const envConfig = this.getEnvConfig();
67
+ const targetPath = `${this.config.fullPath}/${CONST_FirebaseJSON}`;
68
+ let fileContent;
69
+ if (envConfig.isLocal)
70
+ fileContent = {};
71
+ else
72
+ fileContent = {
73
+ hosting: this.config.hostingConfig ?? {
74
+ 'public': 'dist',
75
+ 'rewrites': [
76
+ { 'source': '**', 'destination': '/index.html' }
77
+ ]
78
+ }
79
+ };
80
+ await FileSystemUtils.file.write.json(targetPath, fileContent);
81
+ }
82
+ async resolveHostingRuntimeConfig() {
83
+ const envConfig = this.getEnvConfig().config;
84
+ const targetPath = resolve(this.config.fullPath, `./src/main/config.ts`);
85
+ const fileContent = `export const config = ${JSON.stringify(envConfig, null, 2)};`;
86
+ await FileSystemUtils.file.write(targetPath, fileContent);
87
+ }
88
+ //######################### Shared helpers #########################
89
+ async createAppVersionFile() {
90
+ const targetPath = `${this.config.fullPath}/src/main/${CONST_VersionApp}`;
91
+ const appVersion = this.runtimeContext.version;
92
+ await FileSystemUtils.file.write.json(targetPath, { version: appVersion });
93
+ }
94
+ //######################### Build Push Image Phase #########################
95
+ async buildPushImage() {
96
+ const hostingDeployment = this.config.hostingDeployment;
97
+ if (!hostingDeployment) {
98
+ throw new ImplementationMissingException(`Missing hostingDeployment config in unit ${this.config.key}`);
99
+ }
100
+ const buildTag = this.runtimeContext.runtimeParams.buildPushImage;
101
+ if (!buildTag) {
102
+ throw new ImplementationMissingException(`Missing buildPushImage runtime param in unit ${this.config.key}`);
103
+ }
104
+ const artifactRegistry = hostingDeployment.artifactRegistry;
105
+ const packageName = hostingDeployment.packageName;
106
+ if (!packageName) {
107
+ throw new ImplementationMissingException(`Missing packageName in hostingDeployment config for unit ${this.config.key}`);
108
+ }
109
+ this.logInfo(`Building and uploading hosting package to Artifact Registry:`);
110
+ this.logInfo(` Package: ${packageName}`);
111
+ this.logInfo(` Tag: ${buildTag}`);
112
+ if (this.runtimeContext.runtimeParams.dryRun) {
113
+ this.logInfo(`[DRY RUN] Would build and upload hosting package: ${packageName}:${buildTag}`);
114
+ return;
115
+ }
116
+ const commando = this.allocateCommando();
117
+ await ensureArtifactRegistryRepository(commando, artifactRegistry, 'generic', this);
118
+ const metadata = {
119
+ ...this.injectedMetadata,
120
+ 'build.timestamp': new Date().toISOString(),
121
+ 'build.tag': buildTag,
122
+ 'build.project': artifactRegistry.projectId,
123
+ 'build.package-name': packageName,
124
+ 'version': this.runtimeContext.version,
125
+ 'git.commit': process.env.GIT_COMMIT || '',
126
+ 'git.branch': process.env.GIT_BRANCH || '',
127
+ 'build.user': process.env.USER || '',
128
+ };
129
+ this.logDebug(`Metadata: `, metadata);
130
+ const buildOutputDir = resolve(this.config.fullPath, CONST_TrashDir);
131
+ const stagingDir = resolve(buildOutputDir, CONST_StagingDir);
132
+ await FileSystemUtils.folder.delete(stagingDir);
133
+ await FileSystemUtils.folder.create(stagingDir);
134
+ const tarballPath = resolve(buildOutputDir, CONST_HostingBuildTarball);
135
+ const outputDirName = resolve(this.config.output).split('/').pop() || 'dist';
136
+ await FileSystemUtils.folder.copy(this.config.output, resolve(stagingDir, outputDirName));
137
+ await FileSystemUtils.file.write.json(resolve(stagingDir, CONST_DeploymentMetadata), metadata);
138
+ commando.cd(stagingDir);
139
+ await this.executeAsyncCommando(commando, `tar -czf ${tarballPath} ${CONST_DeploymentMetadata} ${outputDirName}`, (stdout, stderr, exitCode) => {
140
+ if (exitCode !== 0)
141
+ throw new CommandoException(`Failed to create tarball (exit code ${exitCode})`, stdout, stderr, exitCode);
142
+ });
143
+ this.logInfo(`Created tarball: ${tarballPath}`);
144
+ const region = artifactRegistry.region;
145
+ const repository = artifactRegistry.repository;
146
+ const projectId = artifactRegistry.projectId;
147
+ await this.executeAsyncCommando(commando, `gcloud artifacts generic upload --package=${packageName} --version=${buildTag} --source=${tarballPath} --location=${region} --repository=${repository} --project=${projectId}`, (stdout, stderr, exitCode) => {
148
+ if (exitCode !== 0)
149
+ throw new CommandoException(`Failed to upload hosting package to Artifact Registry (exit code ${exitCode})`, stdout, stderr, exitCode);
150
+ });
151
+ this.logInfo(`Successfully uploaded hosting package: ${packageName}:${buildTag} and ${packageName}:latest`);
152
+ }
153
+ //######################### Deploy Image Phase #########################
154
+ async deployImage() {
155
+ const hostingDeployment = this.config.hostingDeployment;
156
+ if (!hostingDeployment) {
157
+ throw new ImplementationMissingException(`Missing hostingDeployment config in unit ${this.config.key}`);
158
+ }
159
+ const deployTag = this.runtimeContext.runtimeParams.deployImage;
160
+ if (!deployTag) {
161
+ throw new ImplementationMissingException(`Missing deployImage runtime param in unit ${this.config.key}. Generic packages require a specific version tag (cannot use 'latest')`);
162
+ }
163
+ const artifactRegistry = hostingDeployment.artifactRegistry;
164
+ const packageName = hostingDeployment.packageName;
165
+ if (!packageName) {
166
+ throw new ImplementationMissingException(`Missing packageName in hostingDeployment config for unit ${this.config.key}`);
167
+ }
168
+ const region = artifactRegistry.region;
169
+ const repository = artifactRegistry.repository;
170
+ const projectId = artifactRegistry.projectId;
171
+ this.logInfo(`Deploying hosting package: ${packageName}:${deployTag}`);
172
+ if (this.runtimeContext.runtimeParams.dryRun) {
173
+ this.logInfo(`[DRY RUN] Would deploy hosting package: ${packageName}:${deployTag}`);
174
+ return;
175
+ }
176
+ const deployTempDir = resolve(this.config.fullPath, `${CONST_TrashDir}/${CONST_DeployHostingDir}`);
177
+ this.logInfo(`Setting up deployment directory: ${deployTempDir}`);
178
+ await FileSystemUtils.folder.delete(deployTempDir);
179
+ await FileSystemUtils.folder.create(deployTempDir);
180
+ const commando = this.allocateCommando(Commando_NVM)
181
+ .applyNVM()
182
+ .cd(deployTempDir)
183
+ .mark();
184
+ this.logInfo(`Downloading hosting package from Artifact Registry from: ${projectId}/${repository}/${packageName}/${deployTag}`);
185
+ await this.executeAsyncCommando(commando, `gcloud artifacts generic download --package=${packageName} --version=${deployTag} --destination=${deployTempDir} --location=${region} --repository=${repository} --project=${projectId}`, (stdout, stderr, exitCode) => {
186
+ if (exitCode !== 0)
187
+ throw new CommandoException(`Failed to download hosting package from Artifact Registry (exit code ${exitCode})`, stdout, stderr, exitCode);
188
+ });
189
+ this.logInfo(`Locating downloaded tarball...`);
190
+ const downloadedFiles = await FileSystemUtils.folder.list(deployTempDir);
191
+ const tarballFile = downloadedFiles.find(file => file.endsWith('.tar.gz'));
192
+ if (!tarballFile)
193
+ throw new ImplementationMissingException(`Downloaded tarball not found in ${deployTempDir}. Files found: ${downloadedFiles.join(', ')}`);
194
+ const tarballPath = resolve(deployTempDir, tarballFile);
195
+ this.logDebug(`Downloaded tarball: ${tarballPath}`);
196
+ this.logInfo(`Extracting hosting package...`);
197
+ await this.executeAsyncCommando(commando, `tar -xzf ${tarballPath} -C ${deployTempDir}`, (stdout, stderr, exitCode) => {
198
+ if (exitCode !== 0) {
199
+ throw new CommandoException(`Failed to extract tarball (exit code ${exitCode})`, stdout, stderr, exitCode);
200
+ }
201
+ });
202
+ this.logInfo(`Extracted hosting package to: ${deployTempDir}`);
203
+ this.logInfo(`Copying firebase configs..`);
204
+ const firebaseJsonPath = resolve(this.config.fullPath, CONST_FirebaseJSON);
205
+ const firebaseRcPath = resolve(this.config.fullPath, CONST_FirebaseRC);
206
+ await FileSystemUtils.file.copy(firebaseJsonPath, resolve(deployTempDir, CONST_FirebaseJSON));
207
+ await FileSystemUtils.file.copy(firebaseRcPath, resolve(deployTempDir, CONST_FirebaseRC));
208
+ this.logDebug(`Copied firebase configs!`);
209
+ const envConfig = this.getEnvConfig();
210
+ this.logInfo(`Deploying to Firebase Hosting: ${deployTempDir} => ${envConfig.projectId}`);
211
+ const deployCommando = this.allocateCommando(Commando_NVM).applyNVM()
212
+ .cd(deployTempDir)
213
+ .setLogLevelFilter(deployLogFilter)
214
+ .onLog(/.*Hosting URL.*(https:\/\/.*?)$/, match => {
215
+ this.hosting[match[1]] = match[2];
216
+ });
217
+ const debug = this.runtimeContext.runtimeParams.verbose ? ' --debug' : '';
218
+ await this.executeAsyncCommando(deployCommando, `npx firebase${debug} deploy --only hosting`, (stdout, stderr, exitCode) => {
219
+ if (exitCode !== 0) {
220
+ throw new CommandoException(`Failed to deploy hosting (exit code ${exitCode})`, stdout, stderr, exitCode);
221
+ }
222
+ });
223
+ this.logInfo(`Hosting deployed: `, this.hosting);
224
+ }
225
+ }
@@ -0,0 +1,10 @@
1
+ import { Unit_HostingApp, Unit_HostingApp_Config } from './Unit_HostingApp.js';
2
+ /**
3
+ * Firebase Hosting application unit (Vite bundler).
4
+ * Extends Unit_HostingApp; implements compile via vite build, launch via vite (dev server).
5
+ */
6
+ export declare class Unit_ViteHostingApp<C extends Unit_HostingApp_Config = Unit_HostingApp_Config> extends Unit_HostingApp<C> {
7
+ constructor(config: Unit_ViteHostingApp<C>['config']);
8
+ protected compileImpl(): Promise<void>;
9
+ protected runApp(): Promise<void>;
10
+ }