backend-manager 5.2.18 β 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 +16 -0
- package/CLAUDE.md +2 -1
- package/bin/backend-manager +10 -1
- package/docs/cli-output.md +122 -0
- package/docs/testing.md +5 -3
- package/package.json +1 -1
- package/src/cli/commands/base-command.js +4 -0
- package/src/cli/commands/setup-tests/bem-config.js +22 -9
- package/src/cli/commands/setup-tests/helpers/merge-line-files.js +22 -168
- package/src/cli/commands/setup.js +53 -29
- package/src/cli/index.js +68 -39
- package/src/cli/utils/ui.js +270 -0
- package/src/manager/libraries/email/data/disposable-domains.json +1 -0
- package/src/utils/merge-line-files.js +16 -5
- package/templates/_.env +1 -0
- package/test/helpers/merge-line-files.js +271 -0
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
|
package/bin/backend-manager
CHANGED
|
@@ -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
|
-
|
|
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:** `
|
|
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
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
60
|
-
} else {
|
|
61
|
-
console.log(chalk.red(`${key} (${userValue})`));
|
|
57
|
+
missing.push(key);
|
|
62
58
|
}
|
|
63
59
|
});
|
|
64
60
|
|
|
65
|
-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
169
|
-
* @param {string} 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(
|
|
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
|
-
|
|
180
|
-
|
|
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
|
+
};
|