@lenne.tech/cli 1.41.3 → 1.42.0
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/build/commands/deployment/create.js +18 -1
- package/build/commands/dev/vscode.js +173 -0
- package/build/commands/fullstack/update.js +17 -0
- package/build/lib/dev-test-session.js +119 -7
- package/build/lib/dev-ticket.js +17 -2
- package/build/lib/heal-vendor-migrate-store.js +285 -0
- package/build/lib/hoist-workspace-pnpm-config.js +59 -2
- package/build/lib/vscode-settings.js +351 -0
- package/docs/LT-ECOSYSTEM-GUIDE.md +1 -1
- package/docs/commands.md +131 -1
- package/package.json +31 -16
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.healVendorMigrateStore = healVendorMigrateStore;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const fs_1 = require("fs");
|
|
39
|
+
const path_1 = require("path");
|
|
40
|
+
const ts = __importStar(require("typescript"));
|
|
41
|
+
/**
|
|
42
|
+
* Idempotently repair a vendor-mode project's migration store
|
|
43
|
+
* (`migrations-utils/migrate.js`).
|
|
44
|
+
*
|
|
45
|
+
* ## The defect this heals
|
|
46
|
+
*
|
|
47
|
+
* The store is generated ONCE, during `convertCloneToVendored()`. Projects that
|
|
48
|
+
* were converted before the template learned its lesson keep the old file
|
|
49
|
+
* forever: `migrations-utils/` is project scaffolding, not `src/core/`, so
|
|
50
|
+
* neither the core updater nor any other update path ever touches it again.
|
|
51
|
+
*
|
|
52
|
+
* The old variant registers the ts-node bootstrap UNCONDITIONALLY:
|
|
53
|
+
*
|
|
54
|
+
* ```js
|
|
55
|
+
* require('./ts-compiler'); // -> require('ts-node')
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* `ts-node` is a devDependency that the production Dockerfile prunes with
|
|
59
|
+
* `pnpm install --prod`, while the image needs no transpiler at all (everything
|
|
60
|
+
* next to the store is already compiled). So every deployed container dies with
|
|
61
|
+
* `Cannot find module 'ts-node'` before applying a single migration.
|
|
62
|
+
*
|
|
63
|
+
* It stays invisible because `docker-entrypoint.sh` degrades a migration failure
|
|
64
|
+
* to a warning on purpose — a bad migration must not crash-loop the container and
|
|
65
|
+
* leave the orchestrator serving a stale build. The container reports healthy,
|
|
66
|
+
* nothing is migrated, and nobody notices.
|
|
67
|
+
*
|
|
68
|
+
* ## Why the detection is inverted — the expensive lesson
|
|
69
|
+
*
|
|
70
|
+
* This function replaces the file WHOLESALE, and the replacement is not
|
|
71
|
+
* behaviour-neutral: the bundled template hardcodes the collection name
|
|
72
|
+
* (`'migrations'`) and takes its URI from `./mongo-uri`. A project that used a
|
|
73
|
+
* different collection therefore gets an EMPTY migration ledger — and the next
|
|
74
|
+
* `migrate:up` re-runs every historical migration against the live database. A
|
|
75
|
+
* project that never had `./mongo-uri` crashes outright.
|
|
76
|
+
*
|
|
77
|
+
* The first implementation asked "can I SEE a guard?" and treated the answer
|
|
78
|
+
* "no" as proof that none exists. It recognised exactly two shapes — a
|
|
79
|
+
* `require.resolve` probe and a `try {` — so a perfectly production-safe
|
|
80
|
+
* `if (!fs.existsSync(compiled)) require('./ts-compiler')` read as broken and
|
|
81
|
+
* was destroyed, together with its collection name.
|
|
82
|
+
*
|
|
83
|
+
* So the question is inverted: heal ONLY when the hazard is positively proven,
|
|
84
|
+
* i.e. the require sits as a TOP-LEVEL, unconditional statement — the one shape
|
|
85
|
+
* that genuinely cannot survive a pruned image. Every other shape (inside `try`,
|
|
86
|
+
* `if`, a function, a ternary, a block) is by construction conditional, hence
|
|
87
|
+
* the project's own solution, and is left alone. "I did not recognise a guard"
|
|
88
|
+
* is no longer evidence that there is none.
|
|
89
|
+
*
|
|
90
|
+
* Detection runs on the TypeScript AST, not on regex-stripped text. A regex
|
|
91
|
+
* "lexer" has no string/template/regex-literal state, so a `/*` or `//` inside a
|
|
92
|
+
* literal earlier in the file silently erased the guard and triggered the very
|
|
93
|
+
* overwrite this function must avoid. `lib/strip-comments.ts` solves the comment
|
|
94
|
+
* half properly (TS scanner) and would have been the right reuse; the AST solves
|
|
95
|
+
* comments AND nesting in one step, and recognises a backtick require for free.
|
|
96
|
+
*
|
|
97
|
+
* ## Recoverability
|
|
98
|
+
*
|
|
99
|
+
* An overwrite is only acceptable when it can be undone. `git status --porcelain`
|
|
100
|
+
* returning nothing does NOT mean "committed" — it also means untracked-and-
|
|
101
|
+
* ignored, or not a git repo at all, i.e. exactly the cases where nothing can be
|
|
102
|
+
* recovered. The guard therefore establishes tracked-ness directly
|
|
103
|
+
* (`git ls-files --error-unmatch`) and writes a `.bak` whenever it cannot prove
|
|
104
|
+
* git has a copy. A file with UNCOMMITTED modifications is never overwritten —
|
|
105
|
+
* that would destroy work which exists nowhere else — and is reported as skipped.
|
|
106
|
+
*
|
|
107
|
+
* @param apiDir Absolute path to the api project (the directory holding `src/core`).
|
|
108
|
+
* @param assetPath Absolute path to the bundled `templates/vendor-scripts/migrate-store.js`.
|
|
109
|
+
* @returns Changed paths relative to `apiDir`; empty when nothing needed healing.
|
|
110
|
+
*/
|
|
111
|
+
function healVendorMigrateStore(apiDir, assetPath) {
|
|
112
|
+
const changed = [];
|
|
113
|
+
// Vendor mode only. In npm mode the store requires the compiled
|
|
114
|
+
// `@lenne.tech/nest-server` package and never needs a transpiler.
|
|
115
|
+
if (!(0, fs_1.existsSync)((0, path_1.join)(apiDir, 'src', 'core', 'VENDOR.md'))) {
|
|
116
|
+
return changed;
|
|
117
|
+
}
|
|
118
|
+
const rel = 'migrations-utils/migrate.js';
|
|
119
|
+
const storePath = (0, path_1.join)(apiDir, 'migrations-utils', 'migrate.js');
|
|
120
|
+
if (!(0, fs_1.existsSync)(storePath) || !(0, fs_1.existsSync)(assetPath)) {
|
|
121
|
+
return changed;
|
|
122
|
+
}
|
|
123
|
+
// Never write THROUGH a symlink: the target may live anywhere, and the caller
|
|
124
|
+
// asked us to repair a store, not to overwrite whatever it points at.
|
|
125
|
+
if (isSymbolicLink(storePath)) {
|
|
126
|
+
changed.push(`${rel} (skipped: is a symlink — repair the file it points at instead)`);
|
|
127
|
+
return changed;
|
|
128
|
+
}
|
|
129
|
+
let current;
|
|
130
|
+
try {
|
|
131
|
+
current = (0, fs_1.readFileSync)(storePath, 'utf8');
|
|
132
|
+
}
|
|
133
|
+
catch (_a) {
|
|
134
|
+
return changed;
|
|
135
|
+
}
|
|
136
|
+
if (!hasTopLevelTsCompilerRequire(current)) {
|
|
137
|
+
return changed;
|
|
138
|
+
}
|
|
139
|
+
const recoverability = gitRecoverability(apiDir, rel);
|
|
140
|
+
if (recoverability === 'dirty') {
|
|
141
|
+
changed.push(`${rel} (skipped: uncommitted changes — commit or discard them, then re-run)`);
|
|
142
|
+
return changed;
|
|
143
|
+
}
|
|
144
|
+
let template;
|
|
145
|
+
try {
|
|
146
|
+
template = (0, fs_1.readFileSync)(assetPath, 'utf8');
|
|
147
|
+
}
|
|
148
|
+
catch (_b) {
|
|
149
|
+
return changed;
|
|
150
|
+
}
|
|
151
|
+
// No git copy to fall back on (untracked, ignored, or not a repo at all), so
|
|
152
|
+
// leave one on disk before touching the file.
|
|
153
|
+
let note = '';
|
|
154
|
+
if (recoverability === 'unknown') {
|
|
155
|
+
const backupPath = `${storePath}.bak`;
|
|
156
|
+
try {
|
|
157
|
+
if (!(0, fs_1.existsSync)(backupPath)) {
|
|
158
|
+
(0, fs_1.copyFileSync)(storePath, backupPath);
|
|
159
|
+
}
|
|
160
|
+
note = ` — previous version saved to ${rel}.bak`;
|
|
161
|
+
}
|
|
162
|
+
catch (_c) {
|
|
163
|
+
changed.push(`${rel} (skipped: git has no copy and the .bak could not be written)`);
|
|
164
|
+
return changed;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Atomic: a crash between write and rename leaves the original intact rather
|
|
168
|
+
// than a truncated store the migrate CLI would then fail to parse.
|
|
169
|
+
if (!writeAtomic(storePath, template)) {
|
|
170
|
+
changed.push(`${rel} (skipped: write failed)`);
|
|
171
|
+
return changed;
|
|
172
|
+
}
|
|
173
|
+
changed.push(`${rel} (migrations never ran in deployed containers — see the file header)${note}`);
|
|
174
|
+
return changed;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Whether git holds a recoverable copy of `relPath`.
|
|
178
|
+
*
|
|
179
|
+
* - `recoverable` — tracked and unmodified: an overwrite is undoable via git.
|
|
180
|
+
* - `dirty` — tracked with uncommitted edits: must not be overwritten.
|
|
181
|
+
* - `unknown` — untracked, ignored, no repo, or no `git` on PATH. Git can
|
|
182
|
+
* recover nothing here, so the caller must back up itself.
|
|
183
|
+
*
|
|
184
|
+
* Deliberately does NOT infer "committed" from empty `status --porcelain`
|
|
185
|
+
* output: an ignored or untracked file is equally silent there, and treating
|
|
186
|
+
* that silence as safety is what made the overwrite unrecoverable.
|
|
187
|
+
*/
|
|
188
|
+
function gitRecoverability(projectRoot, relPath) {
|
|
189
|
+
try {
|
|
190
|
+
// Throws unless the path is TRACKED — the property we actually depend on.
|
|
191
|
+
(0, child_process_1.execFileSync)('git', ['-C', projectRoot, 'ls-files', '--error-unmatch', '--', relPath], {
|
|
192
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (_a) {
|
|
196
|
+
return 'unknown';
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const out = (0, child_process_1.execFileSync)('git', ['-C', projectRoot, 'status', '--porcelain', '--', relPath], {
|
|
200
|
+
encoding: 'utf8',
|
|
201
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
202
|
+
});
|
|
203
|
+
return out.trim().length > 0 ? 'dirty' : 'recoverable';
|
|
204
|
+
}
|
|
205
|
+
catch (_b) {
|
|
206
|
+
return 'unknown';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* True when the file requires the ts-node bootstrap as a TOP-LEVEL, unconditional
|
|
211
|
+
* statement — the only shape that provably breaks in a production image where
|
|
212
|
+
* ts-node has been pruned.
|
|
213
|
+
*
|
|
214
|
+
* Anything nested is conditional by construction and therefore the project's own
|
|
215
|
+
* (working) solution — both of these are left alone, as is any other guard shape
|
|
216
|
+
* someone invents:
|
|
217
|
+
*
|
|
218
|
+
* ```js
|
|
219
|
+
* try { require.resolve(`${HELPER}.js`) } catch { require('./ts-compiler') }
|
|
220
|
+
* if (!fs.existsSync(compiled)) { require('./ts-compiler') }
|
|
221
|
+
* ```
|
|
222
|
+
*
|
|
223
|
+
* Uses the AST rather than text matching, so comments, string literals, template
|
|
224
|
+
* literals and regex literals cannot fake — or hide — a match.
|
|
225
|
+
*/
|
|
226
|
+
function hasTopLevelTsCompilerRequire(source) {
|
|
227
|
+
let sourceFile;
|
|
228
|
+
try {
|
|
229
|
+
sourceFile = ts.createSourceFile('migrate.js', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
|
230
|
+
}
|
|
231
|
+
catch (_a) {
|
|
232
|
+
// Unparseable: we cannot prove the hazard, so we must not act.
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
return sourceFile.statements.some((statement) => {
|
|
236
|
+
if (!ts.isExpressionStatement(statement)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return isTsCompilerRequireCall(statement.expression);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/** True when `path` is a symlink (never follows it). */
|
|
243
|
+
function isSymbolicLink(path) {
|
|
244
|
+
try {
|
|
245
|
+
return (0, fs_1.lstatSync)(path).isSymbolicLink();
|
|
246
|
+
}
|
|
247
|
+
catch (_a) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** True for `require('./ts-compiler')` — single string or backtick argument. */
|
|
252
|
+
function isTsCompilerRequireCall(node) {
|
|
253
|
+
if (!ts.isCallExpression(node)) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
if (!ts.isIdentifier(node.expression) || node.expression.text !== 'require') {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
if (node.arguments.length !== 1) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const arg = node.arguments[0];
|
|
263
|
+
const isLiteral = ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg);
|
|
264
|
+
return isLiteral && arg.text === './ts-compiler';
|
|
265
|
+
}
|
|
266
|
+
/** Write via temp file + rename so a crash cannot leave a truncated store. */
|
|
267
|
+
function writeAtomic(target, content) {
|
|
268
|
+
const tmp = `${target}.lt-tmp`;
|
|
269
|
+
try {
|
|
270
|
+
(0, fs_1.writeFileSync)(tmp, content);
|
|
271
|
+
(0, fs_1.renameSync)(tmp, target);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
catch (_a) {
|
|
275
|
+
try {
|
|
276
|
+
if ((0, fs_1.existsSync)(tmp)) {
|
|
277
|
+
(0, fs_1.unlinkSync)(tmp);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (_b) {
|
|
281
|
+
/* best effort */
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -22,11 +22,26 @@ const fs_utils_1 = require("./fs-utils");
|
|
|
22
22
|
* is hoisted too so a sub-project's first-party exemption (e.g.
|
|
23
23
|
* `@lenne.tech/*`) keeps working in the monorepo — otherwise the
|
|
24
24
|
* minimum-release-age gate would block freshly published own packages.
|
|
25
|
+
*
|
|
26
|
+
* `auditConfig` is nested (`{ ignoreGhsas: [...], ignoreCves: [...] }`), so it
|
|
27
|
+
* needs a one-level-deeper merge than the flat object fields. It MUST be hoisted:
|
|
28
|
+
* the CI audit job is deploy-blocking, and a settings-only sub-workspace file is
|
|
29
|
+
* deleted after hoisting (see hoistFromSubWorkspaceYaml). Without this the
|
|
30
|
+
* starter's assessed-advisory allowlist is destroyed rather than merely ignored,
|
|
31
|
+
* and the generated project's very first pipeline goes red on an advisory that
|
|
32
|
+
* was already justified upstream.
|
|
25
33
|
*/
|
|
26
34
|
const OBJECT_FIELDS = ['overrides', 'allowBuilds'];
|
|
27
35
|
const ARRAY_FIELDS = ['onlyBuiltDependencies', 'ignoredOptionalDependencies', 'minimumReleaseAgeExclude'];
|
|
28
|
-
|
|
36
|
+
/** Objects whose values are arrays to be unioned, not replaced. */
|
|
37
|
+
const NESTED_ARRAY_FIELDS = ['auditConfig'];
|
|
38
|
+
/** Provenance note written above a hoisted `auditConfig` — see `annotateAuditConfig`. */
|
|
39
|
+
const AUDIT_CONFIG_NOTE = '# Hoisted from the sub-projects by the lt CLI. These advisory suppressions now\n' +
|
|
40
|
+
'# apply to EVERY package in this workspace, not just the one that justified\n' +
|
|
41
|
+
'# them — review before adding, and drop entries once the advisory is fixed.';
|
|
42
|
+
const WORKSPACE_SCOPED_PNPM_FIELDS = [...OBJECT_FIELDS, ...ARRAY_FIELDS, ...NESTED_ARRAY_FIELDS];
|
|
29
43
|
const isArrayField = (field) => ARRAY_FIELDS.includes(field);
|
|
44
|
+
const isNestedArrayField = (field) => NESTED_ARRAY_FIELDS.includes(field);
|
|
30
45
|
/**
|
|
31
46
|
* Hoist the Corepack `packageManager` pin from sub-projects into the monorepo
|
|
32
47
|
* root `package.json`, keeping the highest version and stripping the pin from
|
|
@@ -169,9 +184,31 @@ function hoistWorkspacePnpmConfig(options) {
|
|
|
169
184
|
// Keep allowBuilds (pnpm 11) and onlyBuiltDependencies (pnpm 10) in sync so
|
|
170
185
|
// the build-script allowlist survives regardless of which key pnpm reads.
|
|
171
186
|
syncBuildAllowlists(rootWs);
|
|
172
|
-
filesystem.write(rootWsPath, (0, js_yaml_1.dump)(rootWs, { lineWidth: -1, sortKeys: false }));
|
|
187
|
+
filesystem.write(rootWsPath, annotateAuditConfig((0, js_yaml_1.dump)(rootWs, { lineWidth: -1, sortKeys: false })));
|
|
173
188
|
}
|
|
174
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Mark a hoisted `auditConfig` as workspace-wide, in the file itself.
|
|
192
|
+
*
|
|
193
|
+
* `auditConfig.ignoreGhsas` / `.ignoreCves` are not ordinary settings — they
|
|
194
|
+
* SUPPRESS vulnerability findings, and the CI audit job is deploy-blocking.
|
|
195
|
+
* Hoisting changes their blast radius: an advisory a sub-project justified for
|
|
196
|
+
* one dev-only transitive dep now also silences that same advisory when it turns
|
|
197
|
+
* up in a sibling's RUNTIME tree, and pnpm's `auditConfig` has no expiry. That is
|
|
198
|
+
* the correct trade (the alternative — deleting the settings-only sub file
|
|
199
|
+
* unhoisted — destroys the allowlist and reddens the first pipeline), but it must
|
|
200
|
+
* not be invisible.
|
|
201
|
+
*
|
|
202
|
+
* A comment in the YAML is where a reviewer actually looks: it survives in the
|
|
203
|
+
* file, shows up in the `git diff` that introduces it, and needs no plumbing
|
|
204
|
+
* through the void-returning scaffolding call chain.
|
|
205
|
+
*/
|
|
206
|
+
function annotateAuditConfig(yaml) {
|
|
207
|
+
if (!/^auditConfig:/m.test(yaml) || yaml.includes(AUDIT_CONFIG_NOTE)) {
|
|
208
|
+
return yaml;
|
|
209
|
+
}
|
|
210
|
+
return yaml.replace(/^auditConfig:/m, `${AUDIT_CONFIG_NOTE}\nauditConfig:`);
|
|
211
|
+
}
|
|
175
212
|
/**
|
|
176
213
|
* Compare the versions of two `packageManager` pins (`pnpm@11.13.1+sha512.…`).
|
|
177
214
|
* Returns >0 if `a` is newer, <0 if older, 0 if equal. Numeric segment-wise
|
|
@@ -265,6 +302,26 @@ function mergePnpmFieldValue(field, rootValue, subValue) {
|
|
|
265
302
|
const subArr = Array.isArray(subValue) ? subValue : [];
|
|
266
303
|
return Array.from(new Set([...rootArr, ...subArr])).sort((a, b) => a.localeCompare(b));
|
|
267
304
|
}
|
|
305
|
+
// Nested (`auditConfig.ignoreGhsas` / `.ignoreCves`): union each inner array
|
|
306
|
+
// instead of letting the sub-project's object replace the root's. A plain
|
|
307
|
+
// key-by-key merge would drop every advisory the root had already justified.
|
|
308
|
+
if (isNestedArrayField(field)) {
|
|
309
|
+
const asObj = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
310
|
+
const rootObj = asObj(rootValue);
|
|
311
|
+
const subObj = asObj(subValue);
|
|
312
|
+
const merged = Object.assign({}, rootObj);
|
|
313
|
+
for (const [key, value] of Object.entries(subObj)) {
|
|
314
|
+
if (Array.isArray(value) || Array.isArray(merged[key])) {
|
|
315
|
+
const a = Array.isArray(merged[key]) ? merged[key] : [];
|
|
316
|
+
const b = Array.isArray(value) ? value : [];
|
|
317
|
+
merged[key] = Array.from(new Set([...a, ...b])).sort((x, y) => x.localeCompare(y));
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
merged[key] = value;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
|
|
324
|
+
}
|
|
268
325
|
const rootObj = rootValue && typeof rootValue === 'object' && !Array.isArray(rootValue)
|
|
269
326
|
? rootValue
|
|
270
327
|
: {};
|