backend-manager 5.2.17 β†’ 5.2.19

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/CHANGELOG.md CHANGED
@@ -14,6 +14,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
+ # [5.2.19] - 2026-05-29
18
+
19
+ ### Added
20
+
21
+ - **Shared CLI styling module (`src/cli/utils/ui.js`)** β€” the SSOT for BEM console output, matching the OMEGA Manager look: a `πŸš€` banner, 70-char `━` dividers, indented tree output, dimmed `Label:` fields, timestamps, a consistent set of status symbols (`β†’ βœ“ βœ— ⊘ ⚠ βœ… + ↻`), and a `Summary` block (green `βœ…` / yellow `⚠`). Exposed on every command as `this.ui` (wired in `base-command.js`) and adoptable incrementally by `serve`/`deploy`/`test`/`emulator`. See `docs/cli-output.md`.
22
+ - **Regression test for the `.env`/`.gitignore` merge** (`test/helpers/merge-line-files.js`). Covers key-based alignment when key order drifts (the original scramble), Custom→Default promotion when the template adopts a key, Default→Custom migration of unknown keys, value preservation, quote normalization, idempotency, and that the setup-test helper is the same function as the canonical impl (SSOT guard).
23
+
24
+ ### Changed
25
+
26
+ - **`npx mgr setup` now renders in the OMEGA style.** The old `---- RUNNING SETUP ----` / `[1] name: passed` / bare missing-keys dump is replaced with a banner, a divider-wrapped project header (brand name + Firebase console URL + `Project`/`API` fields), and `[DEFAULTS]` / `[CHECKS]` / `[STATS]` sections. Each check prints `[N] βœ“ name`, with `⚠ … β€” fixing…` β†’ `βœ“ fixed` on auto-fix and `βœ— Could not fix: …` on hard failure. The run ends with a `Summary` block (checks count, duration, passed/failed, and any failing checks with detail lines). The `bem-config` check lists the missing `backend-manager-config.json` keys as a bulleted list and surfaces a compact version in the summary.
27
+
28
+ ### Fixed
29
+
30
+ - **No more `UnhandledPromiseRejection` crash on setup failure.** Previously an unfixable check (e.g. missing config keys) rejected a promise with no reason, which bubbled out of the un-`catch`'d `bin/backend-manager` IIFE as Node's raw `UnhandledPromiseRejection: undefined` dump (exit non-zero, lost message). Setup now exits cleanly via `haltSetup()` β†’ `process.exit(1)` after printing the styled summary, and `bin/backend-manager` wraps the run in a `try/catch` that prints a one-line `βœ— <message>` backstop. The early-exit guards (missing `functions/package.json`, wrong directory) now print styled errors and exit `1` instead of `0`.
31
+ - **`.env` merge no longer scrambles keys under the wrong headers.** The `has correct .env file` / `has correct .gitignore` setup checks (`env-file.js`, `gitignore.js`) imported a SECOND, **positional** merge implementation in `src/cli/commands/setup-tests/helpers/merge-line-files.js` that zipped comment lines and value lines by index β€” so any drift between the consumer's key order and the template shifted every value down a slot (the last key of each group landed under the next group's header), dropped newly-added template keys, and duplicated keys. That duplicate is **deleted**; the helper is now a thin SSOT shim re-exporting the canonical key-based merge in `src/utils/merge-line-files.js`. The canonical merge also now **promotes** a key from the user's Custom section up into Default (with its value) when the framework template adopts that key, instead of emitting an empty Default line and leaving the value stranded in Custom.
32
+
17
33
  # [5.2.17] - 2026-05-29
18
34
 
19
35
  ### Added
package/CLAUDE.md CHANGED
@@ -106,6 +106,7 @@ Deep references live in `docs/`. **Whenever you make a behavioral change, update
106
106
  - [docs/file-naming.md](docs/file-naming.md) β€” naming table for routes, schemas, API commands, events, cron jobs, hooks
107
107
  - [docs/common-mistakes.md](docs/common-mistakes.md) β€” anti-pattern checklist (don't modify Manager internals, always await, increment-before-update, etc.)
108
108
  - [docs/key-files.md](docs/key-files.md) β€” quick lookup for the most-touched files (Manager, helpers, auth events, cron, payment processors, CLI commands)
109
+ - [docs/cli-output.md](docs/cli-output.md) β€” shared CLI styling module (`src/cli/utils/ui.js`): OMEGA-style banner/dividers/sections/status symbols + the `Summary` block; used by `setup`, adoptable by other commands
109
110
  - [docs/environment-detection.md](docs/environment-detection.md) β€” `assistant.isDevelopment/isProduction/isTesting()`
110
111
  - [docs/response-headers.md](docs/response-headers.md) β€” automatic `bm-properties` header
111
112
 
@@ -134,6 +135,6 @@ Deep references live in `docs/`. **Whenever you make a behavioral change, update
134
135
 
135
136
  ### Testing & CLI
136
137
 
137
- - [docs/testing.md](docs/testing.md) β€” running, filtering, log files, test types (standalone/suite/group), context object, assertions, auth levels. **All cleanup runs at the START of every run, never at the end.** If you add a test that writes Firestore data, register the collection/namespace in the runner's pre-test wipe list, don't add a trailing cleanup step. Marketing providers (SendGrid/Beehiiv) don't need a special exception β€” `_test.*` emails are blocked at the validation layer so test signups never reach providers. The `_test.allow_*` carve-out exists only for the live-provider lifecycle test (`test/marketing/consent-lifecycle.js`), which manages its own teardown.
138
+ - [docs/testing.md](docs/testing.md) β€” running, filtering, log files, test types (standalone/suite/group), context object, assertions, auth levels. **Each test file `module.exports` a `{ description, type, tests }` object β€” NOT raw Mocha (`describe`/`it`/`beforeEach`); those globals are not injected and the file fails to load. Split tests one-file-per-concern under `test/<area>/`, never one giant `test/test.js`.** **All cleanup runs at the START of every run, never at the end.** If you add a test that writes Firestore data, register the collection/namespace in the runner's pre-test wipe list, don't add a trailing cleanup step. Marketing providers (SendGrid/Beehiiv) don't need a special exception β€” `_test.*` emails are blocked at the validation layer so test signups never reach providers. The `_test.allow_*` carve-out exists only for the live-provider lifecycle test (`test/marketing/consent-lifecycle.js`), which manages its own teardown.
138
139
  - [docs/cli-firestore-auth.md](docs/cli-firestore-auth.md) β€” `npx mgr firestore:*` and `auth:*` commands, shared flags, examples
139
140
  - [docs/cli-logs.md](docs/cli-logs.md) β€” `npx mgr logs:read` / `logs:tail` with full flag reference and built-in Cloud Function names
@@ -2,5 +2,14 @@
2
2
  let Main = new (require('../src/cli/index.js'))(process.argv);
3
3
  (async function() {
4
4
  'use strict';
5
- await Main.process(process.argv);
5
+ try {
6
+ await Main.process(process.argv);
7
+ } catch (e) {
8
+ // Print a clean one-line error instead of Node's raw UnhandledPromiseRejection
9
+ // dump. Commands that intend a hard stop should `process.exit(1)` themselves
10
+ // (e.g. setup's haltSetup); this is the catch-all backstop.
11
+ const chalk = require('chalk').default;
12
+ console.error(chalk.red(`\nβœ— ${e && e.message ? e.message : e}`));
13
+ process.exit(1);
14
+ }
6
15
  }());
@@ -0,0 +1,122 @@
1
+ # CLI Output Styling (`src/cli/utils/ui.js`)
2
+
3
+ BEM's CLI shares a single styling module so every command renders with the same
4
+ look as the **OMEGA Manager** (`omega-manager`): `πŸš€` banner, 70-char `━`
5
+ dividers, indented tree output, dimmed labels, timestamps, and a consistent set
6
+ of status symbols. This is the **SSOT for console output** β€” commands should pull
7
+ helpers from here instead of hand-rolling `chalk` + `console.log`.
8
+
9
+ The module is exposed on every command as `this.ui` (wired in
10
+ [`src/cli/commands/base-command.js`](../src/cli/commands/base-command.js)) and can
11
+ also be `require('../utils/ui')`'d directly (e.g. from setup-test files).
12
+
13
+ ## Conventions
14
+
15
+ | Aspect | Value |
16
+ |---|---|
17
+ | Divider | `━` Γ— 70 (`ui.RULE_WIDTH`, `ui.RULE_CHAR`) |
18
+ | Indentation | 2 spaces per level (`ui.indent(level)`) |
19
+ | Section label | `[UPPERCASE]` in bold magenta, at indent level 1 |
20
+ | Section items | one level deeper than the label (level 2) |
21
+ | Timestamp | `new Date().toLocaleTimeString()` (e.g. `7:47:12 PM`) |
22
+ | Banner | bold cyan, prefixed with `πŸš€` |
23
+
24
+ ### Status symbols (`ui.SYMBOLS`)
25
+
26
+ | Key | Symbol | Color | Meaning |
27
+ |---|---|---|---|
28
+ | `running` | `β†’` | dim | step in progress / hint |
29
+ | `pass` | `βœ“` | green | success |
30
+ | `fail` | `βœ—` | red | failure |
31
+ | `skip` | `⊘` | dim | skipped / no-op |
32
+ | `warn` | `⚠` | yellow | warning |
33
+ | `done` | `βœ…` | green | final success (summary) |
34
+ | `add` | `+` | green | created a file/record |
35
+ | `change` | `↻` | yellow | modified a file/record |
36
+
37
+ ## API
38
+
39
+ ```js
40
+ const ui = require('../utils/ui'); // or this.ui inside a command
41
+
42
+ ui.banner('Backend Manager v5.2.18'); // πŸš€ bold-cyan banner + blank lines
43
+ ui.header('Somiibo', { subtitle: url }); // ━ divider / title @ time / ━ divider
44
+ ui.section('Checks'); // blank line + bold-magenta [CHECKS]
45
+ ui.field('Project', 'somiibo-91d13', { pad: 9 });// dimmed "Label:" + value (pad aligns columns)
46
+ ui.status('pass', 'Stats fetched', { level: 2 });// <symbol> <text> at an indent level
47
+ ui.note('All defaults up to date', 2); // dimmed line at a level
48
+ ui.blank(); // blank line
49
+ ui.rule(); // a bare 70-char rule string (cyan)
50
+ ```
51
+
52
+ `ui.header(title, opts)`:
53
+ - `opts.subtitle` β€” text after the title (default cyan), e.g. a URL.
54
+ - `opts.subtitleColor` β€” chalk fn for the subtitle.
55
+ - `opts.time` β€” append `@ <time>` (default `true`).
56
+ - `opts.color` β€” chalk fn for the rules (default cyan).
57
+
58
+ `ui.field(label, value, opts)`:
59
+ - `opts.level` β€” indent level (default 1).
60
+ - `opts.pad` β€” pad the label (incl. colon) to this width for column alignment.
61
+ - `opts.valueColor` β€” chalk fn for the value.
62
+
63
+ `ui.status(kind, text, opts)`:
64
+ - `kind` β€” one of `running|pass|fail|skip|warn|add|change` (picks symbol + color).
65
+ - `opts.level` β€” indent level (default 1).
66
+ - `opts.detail` β€” dimmed trailing detail string.
67
+
68
+ ### `ui.Summary`
69
+
70
+ Collects pass/fail outcomes and prints an OMEGA-style summary block (green `βœ…`
71
+ when all passed, yellow `⚠` otherwise).
72
+
73
+ ```js
74
+ const summary = new ui.Summary().start();
75
+ summary.pass(); // record a pass
76
+ summary.fail('check name', detailsArr);// record a fail with pre-formatted detail lines
77
+ summary.print({ hint: 'Fix the above, then run npx mgr setup again.' });
78
+ ```
79
+
80
+ `fail()`'s second arg is an array of already-styled lines shown indented under the
81
+ failing check in the summary block.
82
+
83
+ ## How `setup` uses it
84
+
85
+ [`src/cli/commands/setup.js`](../src/cli/commands/setup.js) builds the whole run
86
+ from these helpers:
87
+
88
+ 1. `ui.banner(...)` β†’ `ui.header(brand, { subtitle: consoleUrl })` β†’ `ui.field('Project'/'API', ...)`.
89
+ 2. `ui.section('Defaults')` then `ui.status('add'|'change', ...)` per scaffolded file.
90
+ 3. `ui.section('Checks')` then the per-check status lines (printed by the test
91
+ runner β€” see below), with `βœ“ fixed` / `βœ— Could not fix` sub-lines.
92
+ 4. `ui.section('Stats')` then a pass/skip/warn line.
93
+ 5. `self.setupSummary.print()` on success.
94
+
95
+ ### Test runner (`Main.prototype.test` in `src/cli/index.js`)
96
+
97
+ Each setup check prints ` [N] <symbol> <name>`. A check can:
98
+ - **pass** β†’ `βœ“` (recorded via `setupSummary.pass()`).
99
+ - **fail then auto-fix** β†’ `⚠ … β€” fixing…` then `βœ“ fixed`.
100
+ - **fail unfixably** β†’ `βœ— Could not fix: <message>`, recorded via
101
+ `setupSummary.fail(name, details)`, then `haltSetup()` prints the summary and
102
+ `process.exit(1)`.
103
+
104
+ A failing check's `fix()` may attach `error.summaryDetails` (an array of styled
105
+ lines) to surface a compact version in the summary block β€” see
106
+ [`setup-tests/bem-config.js`](../src/cli/commands/setup-tests/bem-config.js),
107
+ which lists the missing `backend-manager-config.json` keys.
108
+
109
+ `--continue` records the failure but keeps going instead of halting.
110
+
111
+ > **No more `UnhandledPromiseRejection`.** Hard failures exit cleanly via
112
+ > `haltSetup()` / `process.exit(1)`, and `bin/backend-manager` wraps the run in a
113
+ > `try/catch` that prints a one-line `βœ— <message>` instead of Node's raw rejection
114
+ > dump.
115
+
116
+ ## Adopting it in other commands
117
+
118
+ `serve`, `deploy`, `test`, `emulator`, etc. can migrate to the same look
119
+ incrementally: replace ad-hoc `console.log(chalk...)` with `this.ui.section(...)`,
120
+ `this.ui.status(...)`, and `this.ui.field(...)`. The legacy
121
+ `log/logError/logSuccess/logWarning` helpers on `BaseCommand` still work for
122
+ simple one-off lines.
package/docs/testing.md CHANGED
@@ -118,13 +118,15 @@ npx mgr test user/ admin/ # Multiple paths
118
118
 
119
119
  ## Test Locations
120
120
 
121
- - **BEM core tests:** `test/`
122
- - **Project tests:** `functions/test/bem/`
121
+ - **BEM core tests:** `test/` (in the framework repo)
122
+ - **Project tests:** the consumer project's repo-root `test/` directory (NOT inside `functions/`)
123
123
 
124
- Use `bem:` or `project:` prefix to filter by source.
124
+ Use `bem:` or `project:` prefix to filter by source. Mirror BEM's own per-area layout β€” **one file per concern** under `test/<area>/` (e.g. `test/article/allocation.js`, `test/article/markdown.js`, `test/article/generate.js`), never one giant `test/test.js`. The runner discovers files by directory, so the split is also what the `project:<path>` filter targets (`npx mgr test project:article`).
125
125
 
126
126
  ## Test Types
127
127
 
128
+ > **The runner reads each file's `module.exports` object β€” it does NOT inject Mocha/Jest globals.** A test file that calls `describe`/`it`/`before`/`beforeEach`/`after` at top level throws `ReferenceError: beforeEach is not defined` and shows as `Failed to load`. There is no global `assert` either β€” use the `assert` passed into `run({ assert })`. Every test file MUST export one of the shapes below.
129
+
128
130
  | Type | Use When | Behavior |
129
131
  |------|----------|----------|
130
132
  | Standalone | Single logical test | Runs once |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.2.17",
3
+ "version": "5.2.19",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -3,6 +3,7 @@ const { confirm } = require('@inquirer/prompts');
3
3
  const { execSync, spawn } = require('child_process');
4
4
  const path = require('path');
5
5
  const jetpack = require('fs-jetpack');
6
+ const ui = require('../utils/ui');
6
7
 
7
8
  class BaseCommand {
8
9
  constructor(main) {
@@ -10,6 +11,9 @@ class BaseCommand {
10
11
  this.firebaseProjectPath = main.firebaseProjectPath;
11
12
  this.argv = main.argv;
12
13
  this.options = main.options;
14
+ // Shared OMEGA-style CLI styling helpers (dividers, headers, status lines).
15
+ // See src/cli/utils/ui.js. Use `this.ui.*` in any command for consistent output.
16
+ this.ui = ui;
13
17
  }
14
18
 
15
19
  async execute() {
@@ -38,31 +38,44 @@ class BemConfigTest extends BaseTest {
38
38
  }
39
39
 
40
40
  async fix() {
41
- console.log(chalk.red(`There is no automatic fix for this check.`));
42
- console.log(chalk.red(`You need to open backend-manager-config.json and set each of these keys:`));
41
+ const ui = require('../../utils/ui');
43
42
 
44
43
  // Write if it doesn't exist
45
44
  if (!this.context.hasContent(this.self.bemConfigJSON)) {
46
45
  jetpack.write(`${this.self.firebaseProjectPath}/functions/backend-manager-config.json`, {});
47
46
  }
48
47
 
49
- // Log what keys are missing
48
+ // Collect the keys that are still missing (these are what the user must fill in).
49
+ const missing = [];
50
50
  powertools.getKeys(bemConfigTemplate).forEach((key) => {
51
51
  // Skip if an ancestor is explicitly set to a non-object value (e.g. stripe: false)
52
52
  if (this._isAncestorDisabled(key)) {
53
53
  return;
54
54
  }
55
-
56
55
  const userValue = _.get(this.self.bemConfigJSON, key, undefined);
57
-
58
56
  if (typeof userValue === 'undefined') {
59
- console.log(chalk.red.bold(`${key}`));
60
- } else {
61
- console.log(chalk.red(`${key} (${userValue})`));
57
+ missing.push(key);
62
58
  }
63
59
  });
64
60
 
65
- throw new Error('Missing required backend-manager-config.json keys');
61
+ ui.note(`Open ${chalk.bold('backend-manager-config.json')} and set the missing keys below:`, 3);
62
+ for (const key of missing) {
63
+ console.log(`${ui.indent(4)}${chalk.red('β€’')} ${key}`);
64
+ }
65
+
66
+ // Surface a compact version in the summary block (the full list printed above).
67
+ const preview = missing.slice(0, 8);
68
+ const summaryDetails = [
69
+ chalk.dim(`Set ${chalk.bold(missing.length)} missing key(s) in backend-manager-config.json:`),
70
+ ...preview.map((key) => `${chalk.red('β€’')} ${key}`),
71
+ ];
72
+ if (missing.length > preview.length) {
73
+ summaryDetails.push(chalk.dim(`…and ${missing.length - preview.length} more (see list above)`));
74
+ }
75
+
76
+ const error = new Error('Missing required backend-manager-config.json keys');
77
+ error.summaryDetails = summaryDetails;
78
+ throw error;
66
79
  }
67
80
  /**
68
81
  * Check if any ancestor of a dot-notation key is a non-object value (e.g. false)
@@ -1,181 +1,35 @@
1
1
  /**
2
- * Smart merge for line-based files (.gitignore, .env)
3
- * Preserves custom values while updating defaults
2
+ * SSOT shim for line-based file merging (.env / .gitignore / CLAUDE.md).
3
+ *
4
+ * The real merge logic lives in `src/utils/merge-line-files.js` β€” a key-based
5
+ * merge that keeps each KEY under its template header and promotes/migrates keys
6
+ * between the Default/Custom sections correctly. This file used to contain a
7
+ * SECOND, positional implementation that zipped comment lines and value lines by
8
+ * index; an off-by-one there is what historically scrambled consumers' `.env`
9
+ * files. That duplicate is gone β€” this module now just re-exports the canonical
10
+ * impl and adds the marker-name aliases + `hasSectionMarkers()` the setup tests
11
+ * (`env-file.js`, `gitignore.js`) consume.
4
12
  */
5
13
 
6
- const DEFAULT_SECTION_MARKER = '# ========== Default Values ==========';
7
- const CUSTOM_SECTION_MARKER = '# ========== Custom Values ==========';
8
-
9
- /**
10
- * Merge line-based files with smart Default/Custom section handling
11
- * @param {string} existingContent - Current file content
12
- * @param {string} newContent - New template content
13
- * @param {string} fileName - File name (.env or .gitignore)
14
- * @returns {string} Merged content
15
- */
16
- function mergeLineBasedFiles(existingContent, newContent, fileName) {
17
- const existingLines = existingContent.split('\n');
18
- const newLines = newContent.split('\n');
19
-
20
- const isEnvFile = fileName === '.env';
21
-
22
- // Parse existing file into default section and custom section
23
- let defaultSection = [];
24
- let customSection = [];
25
- let inCustomSection = false;
26
- let inDefaultSection = false;
27
-
28
- const existingDefaultKeys = new Set();
29
- const existingCustomKeys = new Set();
30
-
31
- for (const line of existingLines) {
32
- const trimmed = line.trim();
33
-
34
- if (trimmed === DEFAULT_SECTION_MARKER) {
35
- inDefaultSection = true;
36
- inCustomSection = false;
37
- continue;
38
- }
39
- if (trimmed === CUSTOM_SECTION_MARKER) {
40
- inCustomSection = true;
41
- inDefaultSection = false;
42
- continue;
43
- }
44
-
45
- if (inCustomSection) {
46
- customSection.push(line);
47
- if (isEnvFile && trimmed && !trimmed.startsWith('#')) {
48
- const key = trimmed.split('=')[0].trim();
49
- if (key) {
50
- existingCustomKeys.add(key);
51
- }
52
- }
53
- } else if (inDefaultSection) {
54
- defaultSection.push(line);
55
- if (isEnvFile && trimmed && !trimmed.startsWith('#')) {
56
- const key = trimmed.split('=')[0].trim();
57
- if (key) {
58
- existingDefaultKeys.add(key);
59
- }
60
- }
61
- }
62
- }
63
-
64
- // Parse new content to build complete default section
65
- const newDefaultSection = [];
66
- const newDefaultKeys = new Set();
67
-
68
- let inNewDefaultSection = false;
69
- let inNewCustomSection = false;
70
-
71
- for (const line of newLines) {
72
- const trimmed = line.trim();
73
-
74
- if (trimmed === DEFAULT_SECTION_MARKER) {
75
- inNewDefaultSection = true;
76
- inNewCustomSection = false;
77
- continue;
78
- }
79
- if (trimmed === CUSTOM_SECTION_MARKER) {
80
- inNewCustomSection = true;
81
- inNewDefaultSection = false;
82
- continue;
83
- }
84
-
85
- if (inNewDefaultSection) {
86
- if (isEnvFile && trimmed && !trimmed.startsWith('#')) {
87
- const key = trimmed.split('=')[0].trim();
88
- if (key) {
89
- newDefaultKeys.add(key);
90
- if (!existingDefaultKeys.has(key) && !existingCustomKeys.has(key)) {
91
- newDefaultSection.push(line);
92
- } else {
93
- newDefaultSection.push(null); // Placeholder
94
- }
95
- } else {
96
- newDefaultSection.push(line);
97
- }
98
- } else {
99
- newDefaultSection.push(line);
100
- }
101
- }
102
- }
103
-
104
- // Merge user's existing default values in the correct order
105
- const mergedDefaultSection = [];
106
- let defaultSectionIndex = 0;
107
-
108
- for (const line of newDefaultSection) {
109
- if (line === null) {
110
- while (defaultSectionIndex < defaultSection.length) {
111
- const userLine = defaultSection[defaultSectionIndex++];
112
- const trimmed = userLine.trim();
113
- if (trimmed && !trimmed.startsWith('#')) {
114
- mergedDefaultSection.push(userLine);
115
- break;
116
- }
117
- }
118
- } else {
119
- mergedDefaultSection.push(line);
120
- }
121
- }
122
-
123
- // Find user-added lines in default section that aren't in new defaults
124
- const userAddedToDefaults = [];
125
-
126
- for (const line of defaultSection) {
127
- const trimmed = line.trim();
128
-
129
- if (!trimmed || trimmed.startsWith('#')) {
130
- continue;
131
- }
132
-
133
- if (isEnvFile) {
134
- const key = trimmed.split('=')[0].trim();
135
- if (key && !newDefaultKeys.has(key) && !existingCustomKeys.has(key)) {
136
- userAddedToDefaults.push(line);
137
- }
138
- } else {
139
- const lineExistsInNewDefaults = newLines.some(newLine => {
140
- return newLine.trim() === trimmed;
141
- });
142
-
143
- if (!lineExistsInNewDefaults) {
144
- userAddedToDefaults.push(line);
145
- }
146
- }
147
- }
148
-
149
- // Build final result
150
- const result = [];
151
-
152
- result.push(DEFAULT_SECTION_MARKER);
153
- result.push(...mergedDefaultSection);
154
-
155
- result.push('');
156
- result.push(CUSTOM_SECTION_MARKER);
157
-
158
- if (userAddedToDefaults.length > 0) {
159
- result.push(...userAddedToDefaults);
160
- }
161
-
162
- result.push(...customSection);
163
-
164
- return result.join('\n');
165
- }
14
+ const {
15
+ mergeLineBasedFiles,
16
+ DEFAULT_MARKER,
17
+ CUSTOM_MARKER,
18
+ } = require('../../../../utils/merge-line-files.js');
166
19
 
167
20
  /**
168
- * Check if file has proper section markers
169
- * @param {string} content - File content
21
+ * Check if a file already has the Default/Custom section markers.
22
+ * @param {string} content
170
23
  * @returns {boolean}
171
24
  */
172
25
  function hasSectionMarkers(content) {
173
- return content.includes(DEFAULT_SECTION_MARKER) && content.includes(CUSTOM_SECTION_MARKER);
26
+ return content.includes(DEFAULT_MARKER) && content.includes(CUSTOM_MARKER);
174
27
  }
175
28
 
176
29
  module.exports = {
177
30
  mergeLineBasedFiles,
178
31
  hasSectionMarkers,
179
- DEFAULT_SECTION_MARKER,
180
- CUSTOM_SECTION_MARKER,
181
- };
32
+ // Names the setup tests import (aliases of the canonical markers).
33
+ DEFAULT_SECTION_MARKER: DEFAULT_MARKER,
34
+ CUSTOM_SECTION_MARKER: CUSTOM_MARKER,
35
+ };