@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,7 +1,7 @@
1
1
  import { RecursivePartial, TypedMap } from '@nu-art/ts-common';
2
2
  import { UnitPhaseImplementor } from '../../core/types.js';
3
3
  import { Unit_PackageJson, Unit_PackageJson_Config } from './Unit_PackageJson.js';
4
- import { Phase_CheckCyclicImports, Phase_Compile, Phase_ExtractDynamicDeps, Phase_Lint, Phase_MapExports, Phase_PreCompile, Phase_PrintDependencyTree, Phase_Publish, Phase_Test, Phase_ToESM } from '../../phases/definitions/index.js';
4
+ import { Phase_CheckCyclicImports, Phase_Compile, Phase_ExtractDynamicDeps, Phase_Lint, Phase_MapExports, Phase_PreCompile, Phase_PrintDependencyTree, Phase_Publish, Phase_Test, Phase_ToESM } from '../../phases/definitions/consts.js';
5
5
  import { TsConfig } from '../base/types.js';
6
6
  export type Unit_TypescriptLib_Config = Unit_PackageJson_Config & {
7
7
  customESLintConfig: boolean;
@@ -14,7 +14,7 @@ export type Unit_TypescriptLib_Config = Unit_PackageJson_Config & {
14
14
  *
15
15
  * **Key Responsibilities**:
16
16
  * - Compiles TypeScript to JavaScript
17
- * - Runs tests (pure, firebase, ui, mobile)
17
+ * - Runs tests (pure, firebase, ui, mobile, playwright)
18
18
  * - Lints code
19
19
  * - Publishes packages
20
20
  * - Manages assets (JSON, SCSS, SVG, images)
@@ -36,6 +36,11 @@ export type Unit_TypescriptLib_Config = Unit_PackageJson_Config & {
36
36
  * - **firebase**: Tests with Firebase emulators
37
37
  * - **ui**: UI tests (not implemented)
38
38
  * - **mobile**: Mobile tests (not implemented)
39
+ * - **playwright**: Playwright test runner (@playwright/test)
40
+ * - Automatic browser instance management (one per worker)
41
+ * - Better performance with shared browser instances across test files
42
+ * - Built-in fixtures and test isolation
43
+ * - Pattern: **\/*.test.playwright.ts
39
44
  *
40
45
  * **Asset Management**: Automatically copies non-TypeScript files (JSON, SCSS, SVG, images)
41
46
  * to output directory during compilation.
@@ -45,6 +50,7 @@ export type Unit_TypescriptLib_Config = Unit_PackageJson_Config & {
45
50
  export declare class Unit_TypescriptLib<C extends Unit_TypescriptLib_Config = Unit_TypescriptLib_Config> extends Unit_PackageJson<C> implements UnitPhaseImplementor<[Phase_PreCompile, Phase_Compile, Phase_PrintDependencyTree, Phase_CheckCyclicImports, Phase_Lint, Phase_Test, Phase_Publish, Phase_ToESM, Phase_ExtractDynamicDeps, Phase_MapExports]> {
46
51
  private TestTypeWorkspaceSetup;
47
52
  runTests(): Promise<void>;
53
+ private generatePlaywrightConfig;
48
54
  protected dependencyUnits: Unit_TypescriptLib[];
49
55
  constructor(config: Unit_TypescriptLib<C>['config']);
50
56
  protected clearOutputDir(): Promise<void>;
@@ -7,7 +7,7 @@ import { resolve, resolve as pathResolve } from 'path';
7
7
  import { Unit_PackageJson } from './Unit_PackageJson.js';
8
8
  import { glob } from 'node:fs/promises';
9
9
  import { TestTypes } from '../../core/params.js';
10
- import { DEFAULT_OLD_TEMPLATE_PATTERN, DEFAULT_TEMPLATE_PATTERN, FileSystemUtils } from '@nu-art/ts-common/utils/FileSystemUtils';
10
+ import { DEFAULT_TEMPLATE_PATTERN, FileSystemUtils } from '@nu-art/ts-common/utils/FileSystemUtils';
11
11
  import path from 'node:path';
12
12
  import { ExportMapper } from '../../exports/ExportMapper.js';
13
13
  const assets = [
@@ -28,7 +28,8 @@ const defaultTestPatterns = {
28
28
  pure: '**/*.test.ts',
29
29
  firebase: '**/*.test.firebase.ts',
30
30
  ui: '**/*.test.ui.ts',
31
- mobile: '**/*.test.mobile.ts'
31
+ mobile: '**/*.test.mobile.ts',
32
+ playwright: '**/*.test.playwright.ts'
32
33
  };
33
34
  const CONST_ESM_PREFIX = 'export NODE_OPTIONS=\'--import data:text/javascript,import%20%7B%20register%20%7D%20from%20%22node%3Amodule%22%3B%20import%20%7B%20pathToFileURL%20%7D%20from%20%22node%3Aurl%22%3B%20register%28%22ts-node%2Fesm%22%2C%20pathToFileURL%28%22.%2F%22%29%29%3B\'';
34
35
  const TestsCommandComposer = {
@@ -62,13 +63,35 @@ const TestsCommandComposer = {
62
63
  mobile: async () => {
63
64
  throw new NotImplementedYetException('Mobile tests not implemented yet');
64
65
  },
66
+ playwright: async (config, runtimeContext) => {
67
+ const command = resolve(runtimeContext.parentUnit.config.fullPath, 'node_modules/.bin/playwright');
68
+ // Playwright accepts file paths as arguments (not glob patterns)
69
+ // If testFiles not explicitly provided, discover them via glob and convert to relative paths
70
+ let testFiles = runtimeContext.runtimeParams.testFiles;
71
+ if (!testFiles) {
72
+ const pattern = resolve(config.fullPath, 'src/test', defaultTestPatterns.playwright);
73
+ const fileIterator = glob(pattern, {});
74
+ const absoluteFiles = [];
75
+ for await (const file of fileIterator)
76
+ absoluteFiles.push(file);
77
+ testFiles = absoluteFiles.map(file => path.relative(config.fullPath, file));
78
+ }
79
+ const cli_testFiles = testFiles.length > 0 ? ` ${testFiles.join(' ')}` : '';
80
+ // Map test cases to Playwright's --grep option
81
+ const testCases = runtimeContext.runtimeParams.testCases;
82
+ const cli_testCases = testCases ? ` --grep '${testCases.join('|')}'` : '';
83
+ // Map debug port to Playwright's --debug option
84
+ const debugPort = runtimeContext.runtimeParams.testDebugPort;
85
+ const cli_debug = debugPort ? ` --debug=${debugPort}` : '';
86
+ return `${command} test${cli_testFiles}${cli_testCases}${cli_debug}`;
87
+ },
65
88
  };
66
89
  /**
67
90
  * TypeScript library unit for building TypeScript packages.
68
91
  *
69
92
  * **Key Responsibilities**:
70
93
  * - Compiles TypeScript to JavaScript
71
- * - Runs tests (pure, firebase, ui, mobile)
94
+ * - Runs tests (pure, firebase, ui, mobile, playwright)
72
95
  * - Lints code
73
96
  * - Publishes packages
74
97
  * - Manages assets (JSON, SCSS, SVG, images)
@@ -90,6 +113,11 @@ const TestsCommandComposer = {
90
113
  * - **firebase**: Tests with Firebase emulators
91
114
  * - **ui**: UI tests (not implemented)
92
115
  * - **mobile**: Mobile tests (not implemented)
116
+ * - **playwright**: Playwright test runner (@playwright/test)
117
+ * - Automatic browser instance management (one per worker)
118
+ * - Better performance with shared browser instances across test files
119
+ * - Built-in fixtures and test isolation
120
+ * - Pattern: **\/*.test.playwright.ts
93
121
  *
94
122
  * **Asset Management**: Automatically copies non-TypeScript files (JSON, SCSS, SVG, images)
95
123
  * to output directory during compilation.
@@ -128,6 +156,33 @@ export class Unit_TypescriptLib extends Unit_PackageJson {
128
156
  FIREBASE_FIRESTORE_INDICES: resolve(pathToProjectRoot, firebaseConfigFiles.firestoreIndexesRules),
129
157
  }, DEFAULT_TEMPLATE_PATTERN);
130
158
  await this.releasePorts(ports);
159
+ const mongoPort = runtimeContext.baiConfig.files?.tests?.firebase?.mongoPort;
160
+ if (mongoPort) {
161
+ const containerName = `mongo-test-${config.key.replace(/[^a-z0-9-]/gi, '-')}`;
162
+ this.logInfo(`Starting MongoDB test container on port ${mongoPort} (container: ${containerName})`);
163
+ await this.releasePorts([`${mongoPort}`]);
164
+ const commando = this.allocateCommando();
165
+ await this.executeAsyncCommando(commando, `docker rm -f ${containerName} 2>/dev/null; docker run -d --name ${containerName} -p ${mongoPort}:${mongoPort} mongo:7 --replSet rs0 --port ${mongoPort}`, (stdout, stderr, exitCode) => {
166
+ if (exitCode !== 0)
167
+ throw new CommandoException('Failed to start MongoDB test container', stdout, stderr, exitCode);
168
+ });
169
+ const initCommando = this.allocateCommando();
170
+ await this.executeAsyncCommando(initCommando, `sleep 3 && docker exec ${containerName} mongosh --port ${mongoPort} --quiet --eval "rs.initiate({_id:'rs0',members:[{_id:0,host:'localhost:${mongoPort}'}]}); while(!rs.status().members.some(m=>m.stateStr==='PRIMARY')){sleep(200)} print('PRIMARY ready')"`, (stdout, stderr, exitCode) => {
171
+ if (exitCode !== 0)
172
+ throw new CommandoException('Failed to initiate MongoDB test replica set', stdout, stderr, exitCode);
173
+ });
174
+ this.logInfo(`MongoDB test container started as replica set on port ${mongoPort}`);
175
+ this.registerTerminatable(async () => {
176
+ try {
177
+ const stopCommando = this.allocateCommando();
178
+ await this.executeAsyncCommando(stopCommando, `docker rm -f ${containerName}`, () => {
179
+ });
180
+ }
181
+ catch (e) {
182
+ this.logWarning(`Failed to stop MongoDB test container: ${e.message}`);
183
+ }
184
+ });
185
+ }
131
186
  },
132
187
  ui: async () => {
133
188
  throw new NotImplementedYetException('UI tests not implemented yet');
@@ -135,6 +190,48 @@ export class Unit_TypescriptLib extends Unit_PackageJson {
135
190
  mobile: async () => {
136
191
  throw new NotImplementedYetException('Mobile tests not implemented yet');
137
192
  },
193
+ playwright: async (config, runtimeContext) => {
194
+ const playwrightConfigPath = resolve(config.fullPath, 'playwright.config.ts');
195
+ // Generate playwright.config.ts if missing
196
+ if (!await FileSystemUtils.file.exists(playwrightConfigPath)) {
197
+ const playwrightConfig = runtimeContext.baiConfig.files?.tests?.playwright;
198
+ const browsers = playwrightConfig?.browsers ?? ['chromium'];
199
+ const headless = playwrightConfig?.headless ?? true;
200
+ const baseURL = playwrightConfig?.baseURL;
201
+ const viewport = playwrightConfig?.viewport ?? { width: 1280, height: 720 };
202
+ // Vite config: from bai-config, or default to shared _thunderstorm/vite.config.ts when package is under _thunderstorm
203
+ const projectRoot = runtimeContext.parentUnit.config.fullPath;
204
+ const viteConfigPathFromConfig = playwrightConfig?.vite?.configPath;
205
+ const defaultThunderstormVitePath = '_thunderstorm/vite.config.ts';
206
+ const viteConfigPath = viteConfigPathFromConfig ?? (config.fullPath.includes('_thunderstorm') ? defaultThunderstormVitePath : undefined);
207
+ const vitePort = playwrightConfig?.vite?.port ?? 5173;
208
+ const viteOptions = viteConfigPath ? {
209
+ port: vitePort,
210
+ configPath: path.relative(config.fullPath, resolve(projectRoot, viteConfigPath))
211
+ } : undefined;
212
+ const configContent = this.generatePlaywrightConfig({
213
+ browsers,
214
+ headless,
215
+ baseURL,
216
+ viewport,
217
+ vite: viteOptions
218
+ });
219
+ await FileSystemUtils.file.write(playwrightConfigPath, configContent);
220
+ this.logInfo(`Generated playwright.config.ts for ${config.key}`);
221
+ }
222
+ // Ensure browser binaries are installed
223
+ const commando = this.allocateCommando(Commando_NVM)
224
+ .cd(config.fullPath);
225
+ const playwrightConfig = runtimeContext.baiConfig.files?.tests?.playwright;
226
+ const browsersToInstall = playwrightConfig?.browsers ?? ['chromium'];
227
+ for (const browser of browsersToInstall) {
228
+ await this.executeAsyncCommando(commando, `npx playwright install ${browser}`, (stdout, stderr, exitCode) => {
229
+ if (exitCode !== 0) {
230
+ this.logWarning(`Failed to install ${browser} browser, tests may fail`);
231
+ }
232
+ });
233
+ }
234
+ },
138
235
  };
139
236
  async runTests() {
140
237
  const testsFolder = resolve(this.config.fullPath, 'src/test');
@@ -165,6 +262,60 @@ export class Unit_TypescriptLib extends Unit_PackageJson {
165
262
  });
166
263
  }
167
264
  }
265
+ generatePlaywrightConfig(options) {
266
+ const browserNames = options.browsers.map(browser => {
267
+ const deviceName = browser === 'webkit' ? 'Safari' : browser.charAt(0).toUpperCase() + browser.slice(1);
268
+ return ` {
269
+ name: '${browser}',
270
+ use: { ...devices['Desktop ${deviceName}'] },
271
+ }`;
272
+ }).join(',\n');
273
+ // Build webServer block if vite config is provided
274
+ const viteConfig = options.vite;
275
+ const webServerBlock = viteConfig ? `
276
+ webServer: {
277
+ command: \`npx vite --config \${resolve(__dirname, '${viteConfig.configPath}')} --port \${vitePort} --host 127.0.0.1\`,
278
+ url: \`http://127.0.0.1:\${vitePort}/src/test/index.html\`,
279
+ reuseExistingServer: true,
280
+ timeout: 60000,
281
+ stdout: 'pipe',
282
+ stderr: 'pipe',
283
+ },
284
+ ` : '';
285
+ // Use vite URL as baseURL when vite is configured, otherwise use provided baseURL
286
+ const baseURL = viteConfig
287
+ ? '`http://127.0.0.1:${vitePort}`'
288
+ : options.baseURL ? `'${options.baseURL}'` : undefined;
289
+ const baseURLConfig = baseURL ? ` baseURL: ${baseURL},` : '';
290
+ // Add imports based on whether vite is used
291
+ const imports = viteConfig
292
+ ? `import {defineConfig, devices} from '@playwright/test';
293
+ import {resolve} from 'path';
294
+ import {fileURLToPath} from 'url';
295
+
296
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
297
+ const vitePort = process.env.VITE_PORT || '${viteConfig.port}';
298
+ `
299
+ : `import {defineConfig, devices} from '@playwright/test';
300
+ `;
301
+ return `${imports}
302
+ export default defineConfig({
303
+ testDir: './src/test',
304
+ testMatch: '**/*.test.playwright.ts',
305
+ ${webServerBlock}
306
+ use: {
307
+ headless: ${options.headless},
308
+ ${baseURLConfig ? baseURLConfig + '\n' : ''} viewport: { width: ${options.viewport.width}, height: ${options.viewport.height} },
309
+ },
310
+
311
+ projects: [
312
+ ${browserNames}
313
+ ],
314
+
315
+ timeout: 30000,
316
+ });
317
+ `;
318
+ }
168
319
  dependencyUnits;
169
320
  constructor(config) {
170
321
  super(config);
@@ -401,7 +552,7 @@ export class Unit_TypescriptLib extends Unit_PackageJson {
401
552
  const packageJson = FileSystemUtils.file.template.transform(__stringify(this.config.packageJson, true), params);
402
553
  this.logVerbose('Compiling params: ', params);
403
554
  this.logVerbose('Compiling from package.json: ', packageJson);
404
- await FileSystemUtils.file.template.write(targetPath, packageJson, params, DEFAULT_OLD_TEMPLATE_PATTERN);
555
+ await FileSystemUtils.file.template.write(targetPath, packageJson, params, DEFAULT_TEMPLATE_PATTERN);
405
556
  }
406
557
  async purge() {
407
558
  await FileSystemUtils.folder.delete(this.config.output);
@@ -1,7 +1,7 @@
1
1
  import { UnitPhaseImplementor } from '../../../core/types.js';
2
- import { FirebasePackageConfig } from '../../../config/types/index.js';
2
+ import { FirebasePackageConfig } from '../../../config/types/package/package.js';
3
3
  import { StringMap } from '@nu-art/ts-common';
4
- import { Phase_BuildPushImage, Phase_Deploy, Phase_DeployImage, Phase_Launch } from '../../../phases/definitions/index.js';
4
+ import { Phase_BuildPushImage, Phase_Deploy, Phase_DeployImage, Phase_Launch } from '../../../phases/definitions/consts.js';
5
5
  import { Unit_TypescriptLib, Unit_TypescriptLib_Config } from '../Unit_TypescriptLib.js';
6
6
  export declare const firebaseFunctionEmulator_ErrorStrings: string[];
7
7
  export declare const firebaseFunctionEmulator_WarningStrings: string[];
@@ -27,6 +27,10 @@ export type FunctionConfig = {
27
27
  serviceAccountName?: string;
28
28
  resources?: FunctionResourceConfig;
29
29
  };
30
+ export type MongoEmulatorConfig = {
31
+ port?: number;
32
+ dbName?: string;
33
+ };
30
34
  export type Unit_FirebaseFunctionsApp_Config = Unit_TypescriptLib_Config & {
31
35
  firebaseConfig?: FirebasePackageConfig;
32
36
  pathToFirebaseConfig: string;
@@ -39,6 +43,7 @@ export type Unit_FirebaseFunctionsApp_Config = Unit_TypescriptLib_Config & {
39
43
  pathToEmulatorData: string;
40
44
  sources?: string[];
41
45
  functions: string[] | FunctionConfig[];
46
+ mongo?: MongoEmulatorConfig;
42
47
  containerDeployment?: {
43
48
  artifactRegistry: {
44
49
  region: string;
@@ -98,6 +103,11 @@ export declare class Unit_FirebaseFunctionsApp<C extends Unit_FirebaseFunctionsA
98
103
  postCompile(): Promise<void>;
99
104
  launch(): Promise<void>;
100
105
  releaseEmulatorPorts(): Promise<void>;
106
+ private resolveMongoPort;
107
+ private resolveMongoContainerName;
108
+ private resolveMongoDataPath;
109
+ private startMongoEmulator;
110
+ private stopMongoEmulator;
101
111
  deploy(): Promise<void>;
102
112
  /**
103
113
  * Builds Docker container image using Google Cloud Build and pushes it to Artifact Registry.
@@ -1,11 +1,10 @@
1
1
  import { CONST_BuildImageDir, CONST_FirebaseJSON, CONST_FirebaseRC, CONST_LatestTag, CONST_PackageJSON, CONST_TrashDir, CONST_VersionApp } from '../../../config/consts.js';
2
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';
3
+ import { Const_FirebaseConfigKeys, Const_FirebaseDefaultsKeyToFile, Default_Files, FunctionBuildTemplateFiles } from '../../../templates/consts.js';
4
+ import { Commando_NVM, CommandoException } from '@nu-art/commando';
5
5
  import { resolve } from 'path';
6
- import { DEFAULT_OLD_TEMPLATE_PATTERN, FileSystemUtils } from '@nu-art/ts-common/utils/FileSystemUtils';
6
+ import { DEFAULT_TEMPLATE_PATTERN, FileSystemUtils } from '@nu-art/ts-common/utils/FileSystemUtils';
7
7
  import { Unit_TypescriptLib } from '../Unit_TypescriptLib.js';
8
- import { CommandoException } from '@nu-art/commando';
9
8
  import { deployLogFilter, ensureArtifactRegistryRepository } from './common.js';
10
9
  export const firebaseFunctionEmulator_ErrorStrings = [
11
10
  'functions: Failed',
@@ -86,7 +85,7 @@ export class Unit_FirebaseFunctionsApp extends Unit_TypescriptLib {
86
85
  return dependencies;
87
86
  }, dependencies);
88
87
  packageJson.dependencies = dependencies;
89
- await FileSystemUtils.file.template.write(targetPath, __stringify(packageJson, true), this.deriveDistDependencies(), DEFAULT_OLD_TEMPLATE_PATTERN);
88
+ await FileSystemUtils.file.template.write(targetPath, __stringify(packageJson, true), this.deriveDistDependencies(), DEFAULT_TEMPLATE_PATTERN);
90
89
  }
91
90
  async prepare() {
92
91
  await super.prepare();
@@ -114,15 +113,63 @@ export class Unit_FirebaseFunctionsApp extends Unit_TypescriptLib {
114
113
  async launch() {
115
114
  await sleep(2 * Second * Unit_FirebaseFunctionsApp.staggerCount++);
116
115
  await this.releaseEmulatorPorts();
117
- await Promise.all([
118
- this.runProxy(),
119
- this.runEmulator(),
120
- ]);
116
+ if (this.config.mongo)
117
+ await this.startMongoEmulator();
118
+ let emulatorPromise;
119
+ const proxyPromise = this.runProxy(() => {
120
+ emulatorPromise = this.runEmulator();
121
+ });
122
+ await proxyPromise;
123
+ if (emulatorPromise)
124
+ await emulatorPromise;
121
125
  }
122
126
  async releaseEmulatorPorts() {
123
- const allPorts = Array.from({ length: 10 }, (_, i) => `${this.config.basePort + i}`);
127
+ const allPorts = Array.from({ length: 11 }, (_, i) => `${this.config.basePort + i}`);
128
+ if (this.config.mongo)
129
+ allPorts.push(`${this.resolveMongoPort()}`);
124
130
  return this.releasePorts(allPorts);
125
131
  }
132
+ //######################### MongoDB Emulator #########################
133
+ resolveMongoPort() {
134
+ return this.config.mongo?.port ?? this.config.basePort + 11;
135
+ }
136
+ resolveMongoContainerName() {
137
+ return `mongo-emu-${this.config.key.replace(/[^a-z0-9-]/gi, '-')}`;
138
+ }
139
+ resolveMongoDataPath() {
140
+ return resolve(this.config.fullPath, CONST_TrashDir, 'mongo-data');
141
+ }
142
+ async startMongoEmulator() {
143
+ const port = this.resolveMongoPort();
144
+ const containerName = this.resolveMongoContainerName();
145
+ const mongoDataPath = this.resolveMongoDataPath();
146
+ await FileSystemUtils.folder.create(mongoDataPath);
147
+ this.logInfo(`Starting MongoDB emulator on port ${port} (container: ${containerName}, data: ${mongoDataPath})`);
148
+ const stopAction = async () => this.stopMongoEmulator();
149
+ this.registerTerminatable(stopAction);
150
+ const commando = this.allocateCommando();
151
+ await this.executeAsyncCommando(commando, `docker rm -f ${containerName} 2>/dev/null; docker run -d --name ${containerName} -p ${port}:${port} -v ${mongoDataPath}:/data/db mongo:7 --replSet rs0 --port ${port}`, (stdout, stderr, exitCode) => {
152
+ if (exitCode !== 0)
153
+ throw new CommandoException(`Failed to start MongoDB emulator container`, stdout, stderr, exitCode);
154
+ });
155
+ const initCommando = this.allocateCommando();
156
+ await this.executeAsyncCommando(initCommando, `sleep 3 && docker exec ${containerName} mongosh --port ${port} --quiet --eval "try{rs.status()}catch(e){rs.initiate({_id:'rs0',members:[{_id:0,host:'localhost:${port}'}]})} while(!rs.status().members.some(m=>m.stateStr==='PRIMARY')){sleep(200)} print('PRIMARY ready')"`, (stdout, stderr, exitCode) => {
157
+ if (exitCode !== 0)
158
+ throw new CommandoException(`Failed to initiate MongoDB replica set`, stdout, stderr, exitCode);
159
+ });
160
+ this.logInfo(`MongoDB emulator started as replica set on port ${port}`);
161
+ }
162
+ async stopMongoEmulator() {
163
+ const containerName = this.resolveMongoContainerName();
164
+ this.logInfo(`Stopping MongoDB emulator (container: ${containerName})`);
165
+ try {
166
+ const commando = this.allocateCommando();
167
+ await this.executeAsyncCommando(commando, `docker rm -f ${containerName}`, () => { });
168
+ }
169
+ catch (e) {
170
+ this.logWarning(`Failed to stop MongoDB emulator container: ${e.message}`);
171
+ }
172
+ }
126
173
  async deploy() {
127
174
  const commando = this.allocateCommando(Commando_NVM).applyNVM()
128
175
  .cd(this.config.output)
@@ -635,9 +682,8 @@ export class Unit_FirebaseFunctionsApp extends Unit_TypescriptLib {
635
682
  async resolveProxyFile() {
636
683
  const envConfig = this.getEnvConfig();
637
684
  const targetPath = this.pathToProxy();
638
- const path = this.runtimeContext.baiConfig.files?.backend?.proxy;
639
- if (!path)
640
- return;
685
+ const templatePath = this.runtimeContext.baiConfig.files?.backend?.proxy ?? Default_Files.backend.proxy;
686
+ await FileSystemUtils.folder.create(resolve(this.config.fullPath, '.trash/proxy'));
641
687
  const params = {
642
688
  PROJECT_ID: `${envConfig.projectId}`,
643
689
  PROXY_PORT: `${this.config.basePort}`,
@@ -645,10 +691,10 @@ export class Unit_FirebaseFunctionsApp extends Unit_TypescriptLib {
645
691
  PATH_TO_SSL_KEY: `${this.config.sslKey}`,
646
692
  PATH_TO_SSL_CERTIFICATE: `${this.config.sslCert}`,
647
693
  };
648
- await FileSystemUtils.file.template.copy(path, targetPath, params);
694
+ await FileSystemUtils.file.template.copy(templatePath, targetPath, params);
649
695
  }
650
696
  pathToProxy() {
651
- return resolve(this.config.fullPath, 'src/main/proxy.ts');
697
+ return resolve(this.config.fullPath, '.trash/proxy/proxy.ts');
652
698
  }
653
699
  async resolveConfigDir() {
654
700
  //Create the dir if it doesn't exist
@@ -779,11 +825,19 @@ export class Unit_FirebaseFunctionsApp extends Unit_TypescriptLib {
779
825
  }));
780
826
  }
781
827
  //######################### Launch Logic #########################
782
- async runProxy() {
828
+ async runProxy(onReady) {
783
829
  await this.resolveProxyFile();
784
830
  const commando = this.allocateCommando(Commando_NVM).applyNVM()
785
831
  .cd(this.config.fullPath);
786
- await this.executeAsyncCommando(commando, `${this.npmCommand('tsx')} src/main/proxy.ts`);
832
+ let readyFired = false;
833
+ commando.addLogProcessor((log) => {
834
+ if (!readyFired && log.includes('SSL proxy started at port')) {
835
+ readyFired = true;
836
+ onReady?.();
837
+ }
838
+ return true;
839
+ });
840
+ await this.executeAsyncCommando(commando, `${this.npmCommand('tsx')} .trash/proxy/proxy.ts`);
787
841
  this.logWarning('PROXY TERMINATED');
788
842
  }
789
843
  async runEmulator() {
@@ -797,6 +851,8 @@ export class Unit_FirebaseFunctionsApp extends Unit_TypescriptLib {
797
851
  return LogLevel.Warning;
798
852
  })
799
853
  .onLog(/.*Emulator Hub running.*/, () => this.setStatus('Launch Complete'));
854
+ if (this.config.mongo)
855
+ commando.custom(`export MONGODB_EMULATOR_HOST=localhost:${this.resolveMongoPort()}`);
800
856
  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
857
  ? `--inspect-functions ${this.config.debugPort}` : ''}`);
802
858
  this.logWarning('EMULATORS TERMINATED');
@@ -1,115 +1,37 @@
1
- import { FirebasePackageConfig } from '../../../config/types/index.js';
2
- import { UnitPhaseImplementor } from '../../../core/types.js';
3
- import { StringMap, TS_Object, TypedMap } from '@nu-art/ts-common';
1
+ import { TypedMap } from '@nu-art/ts-common';
4
2
  import { UnitConfigJSON_Node } from '../../discovery/resolvers/UnitMapper_Node.js';
5
- import { Phase_BuildPushImage, Phase_Deploy, Phase_DeployImage, Phase_Launch } from '../../../phases/definitions/index.js';
6
- import { Unit_TypescriptLib, Unit_TypescriptLib_Config } from '../Unit_TypescriptLib.js';
7
- export type FirebaseHostingConfig = {
8
- public: string;
9
- rewrites: {
10
- source: string;
11
- destination: string;
12
- }[];
3
+ import { FirebaseHosting_EnvConfig, FirebaseHostingConfig, Unit_HostingApp, Unit_HostingApp_Config } from './Unit_HostingApp.js';
4
+ export type { FirebaseHostingConfig, FirebaseHosting_EnvConfig };
5
+ export type WebpackEnvConfig = {
6
+ minifyHtml?: boolean;
7
+ devServerSSL?: boolean;
8
+ devServerPort?: number;
9
+ sslKey?: string;
10
+ sslCert?: string;
13
11
  };
14
- export type FirebaseHosting_EnvConfig = {
15
- config: TS_Object;
16
- projectId: string;
17
- isLocal?: boolean;
12
+ export type WebpackHosting_EnvConfig = FirebaseHosting_EnvConfig & {
13
+ webpackConfig?: WebpackEnvConfig;
18
14
  };
19
15
  export type UnitConfigJSON_FirebaseHosting = UnitConfigJSON_Node & {
20
16
  servingPort?: number;
21
17
  hostingConfig?: FirebaseHostingConfig;
22
- envs: TypedMap<FirebaseHosting_EnvConfig>;
18
+ envs: TypedMap<WebpackHosting_EnvConfig>;
23
19
  };
24
- export type Unit_FirebaseHostingApp_Config = Unit_TypescriptLib_Config & {
25
- firebaseConfig?: FirebasePackageConfig;
26
- servingPort: number;
27
- hostingConfig?: FirebaseHostingConfig;
28
- envConfig: FirebaseHosting_EnvConfig;
29
- sources?: string[];
30
- hostingDeployment?: {
31
- artifactRegistry: {
32
- region: string;
33
- repository: string;
34
- projectId: string;
35
- };
36
- packageName: string;
37
- };
20
+ export type Unit_FirebaseHostingApp_Config = Unit_HostingApp_Config & {
21
+ envConfig: WebpackHosting_EnvConfig;
38
22
  };
39
23
  /**
40
- * Firebase Hosting application unit.
41
- *
42
- * **Key Features**:
43
- * - Extends Unit_TypescriptLib (compiles TypeScript)
44
- * - Manages Firebase hosting configuration (firebase.json, .firebaserc)
45
- * - Supports multiple environments (local, staging, production)
46
- * - Implements launch and deploy phases
47
- *
48
- * **Phases Implemented**:
49
- * - `prepare()`: Resolves Firebase hosting config files
50
- * - `compile()`: Compiles TypeScript (without declarations)
51
- * - `launch()`: Starts Firebase hosting emulator
52
- * - `deploy()`: Deploys to Firebase hosting
53
- *
54
- * **Configuration**:
55
- * - `servingPort`: Port for local hosting server
56
- * - `hostingConfig`: Firebase hosting configuration (public folder, rewrites)
57
- * - `envConfig`: Environment-specific config (projectId, isLocal)
24
+ * Firebase Hosting application unit (webpack bundler).
25
+ * Extends Unit_HostingApp; implements compile via npm run build, launch via npm run start.
58
26
  */
59
- export declare class Unit_FirebaseHostingApp<C extends Unit_FirebaseHostingApp_Config = Unit_FirebaseHostingApp_Config> extends Unit_TypescriptLib<C> implements UnitPhaseImplementor<[Phase_Launch, Phase_Deploy, Phase_BuildPushImage, Phase_DeployImage]> {
60
- hosting: StringMap;
61
- injectedMetadata: StringMap;
27
+ export declare class Unit_FirebaseHostingApp<C extends Unit_FirebaseHostingApp_Config = Unit_FirebaseHostingApp_Config> extends Unit_HostingApp<C> {
62
28
  static DefaultConfig_FirebaseHosting: {
63
29
  servingPort: number;
64
30
  output: string;
65
31
  };
66
32
  constructor(config: Unit_FirebaseHostingApp<C>['config']);
67
33
  prepare(): Promise<void>;
68
- compile(): Promise<void>;
69
- launch(): Promise<void>;
70
- releaseWebpackPorts(): Promise<void>;
71
- deploy(): Promise<void>;
72
- private getEnvConfig;
73
- private resolveHostingRC;
74
- private resolveHostingJSON;
75
- private resolveHostingRuntimeConfig;
34
+ private resolveWebpackEnvConfig;
76
35
  protected compileImpl(): Promise<void>;
77
- private createAppVersionFile;
78
- private runApp;
79
- /**
80
- * Builds hosting output and uploads it to Artifact Registry as a generic package.
81
- *
82
- * **Process**:
83
- * 1. Validates hostingDeployment config exists
84
- * 2. Creates deployment-metadata.json with build information
85
- * 3. Creates tarball of hosting output directory
86
- * 4. Uploads to Artifact Registry as generic package
87
- *
88
- * **Requirements**:
89
- * - `--build-push-image <tag>` CLI flag with tag value
90
- * - `hostingDeployment` config in unit config with `packageName` field
91
- * - `packageName` must comply with Artifact Registry naming restrictions
92
- * - gcloud CLI installed and authenticated
93
- * - Artifact Registry API enabled in GCP project
94
- */
95
- buildPushImage(): Promise<void>;
96
- /**
97
- * Deploys hosting build from Artifact Registry to Firebase Hosting.
98
- *
99
- * **Process**:
100
- * 1. Validates hostingDeployment config exists
101
- * 2. Downloads tarball from Artifact Registry
102
- * 3. Extracts to temp directory
103
- * 4. Sets up Firebase tools (package.json + npm install)
104
- * 5. Deploys to Firebase Hosting
105
- * 6. Validates deployment by fetching deployment-metadata.json
106
- *
107
- * **Requirements**:
108
- * - `--deploy-image <tag>` CLI flag with tag value
109
- * - `hostingDeployment` config in unit config with `packageName` field
110
- * - Package must already exist in Artifact Registry (built via buildPushImage)
111
- * - gcloud CLI installed and authenticated
112
- * - Firebase CLI (installed via npm in temp directory)
113
- */
114
- deployImage(): Promise<void>;
36
+ protected runApp(): Promise<void>;
115
37
  }