@nocobase/cli 2.1.0-beta.32 → 2.1.0-beta.34

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 (55) hide show
  1. package/dist/commands/app/down.js +10 -13
  2. package/dist/commands/app/logs.js +0 -1
  3. package/dist/commands/app/restart.js +63 -2
  4. package/dist/commands/app/start.js +41 -17
  5. package/dist/commands/app/stop.js +0 -1
  6. package/dist/commands/app/upgrade.js +9 -4
  7. package/dist/commands/env/add.js +3 -4
  8. package/dist/commands/env/auth.js +3 -2
  9. package/dist/commands/env/remove.js +38 -13
  10. package/dist/commands/env/update.js +9 -2
  11. package/dist/commands/examples/prompts-stages.js +4 -4
  12. package/dist/commands/examples/prompts-test.js +4 -4
  13. package/dist/commands/init.js +38 -31
  14. package/dist/commands/install.js +100 -63
  15. package/dist/commands/license/activate.js +66 -64
  16. package/dist/commands/license/id.js +0 -1
  17. package/dist/commands/license/plugins/clean.js +0 -1
  18. package/dist/commands/license/plugins/list.js +0 -1
  19. package/dist/commands/license/plugins/sync.js +0 -1
  20. package/dist/commands/license/shared.js +3 -3
  21. package/dist/commands/license/status.js +0 -1
  22. package/dist/commands/plugin/disable.js +0 -1
  23. package/dist/commands/plugin/enable.js +0 -1
  24. package/dist/commands/plugin/list.js +0 -1
  25. package/dist/commands/self/update.js +12 -3
  26. package/dist/commands/skills/install.js +12 -3
  27. package/dist/commands/skills/remove.js +12 -3
  28. package/dist/commands/skills/update.js +12 -3
  29. package/dist/commands/source/dev.js +0 -1
  30. package/dist/commands/source/download.js +29 -17
  31. package/dist/lib/app-managed-resources.js +8 -2
  32. package/dist/lib/bootstrap.js +11 -2
  33. package/dist/lib/db-connection-check.js +3 -23
  34. package/dist/lib/docker-env-file.js +52 -0
  35. package/dist/lib/env-auth.js +4 -3
  36. package/dist/lib/env-config.js +1 -0
  37. package/dist/lib/env-guard.js +8 -7
  38. package/dist/lib/generated-command.js +0 -1
  39. package/dist/lib/inquirer-theme.js +17 -0
  40. package/dist/lib/inquirer.js +244 -0
  41. package/dist/lib/object-utils.js +76 -0
  42. package/dist/lib/prompt-catalog-core.js +185 -0
  43. package/dist/lib/prompt-catalog-terminal.js +375 -0
  44. package/dist/lib/prompt-catalog.js +2 -573
  45. package/dist/lib/prompt-validators.js +56 -1
  46. package/dist/lib/resource-command.js +0 -1
  47. package/dist/lib/skills-manager.js +75 -11
  48. package/dist/lib/startup-update.js +12 -8
  49. package/dist/lib/ui.js +28 -51
  50. package/dist/locale/en-US.json +8 -3
  51. package/dist/locale/zh-CN.json +8 -3
  52. package/dist/post-processors/data-modeling.js +25 -7
  53. package/dist/post-processors/data-source-manager.js +24 -0
  54. package/nocobase-ctl.config.json +20 -1
  55. package/package.json +7 -5
@@ -6,12 +6,13 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
+ import { confirm } from "./inquirer.js";
9
10
  import { getCurrentEnvName, getEnv, setEnvRuntime, updateEnvConnection } from './auth-store.js';
10
11
  import { resolveAccessToken } from './env-auth.js';
11
12
  import { fetchWithPreservedAuthRedirect } from './http-request.js';
12
13
  import { generateRuntime } from './runtime-generator.js';
13
14
  import { hasRuntimeSync, saveRuntime } from './runtime-store.js';
14
- import { confirmAction, printInfo, printVerbose, printWarningBlock, setVerboseMode, stopTask, updateTask } from './ui.js';
15
+ import { printInfo, printVerbose, printWarningBlock, setVerboseMode, stopTask, updateTask } from './ui.js';
15
16
  const APP_RETRY_INTERVAL = 2000;
16
17
  const APP_RETRY_TIMEOUT = 120000;
17
18
  function readFlag(argv, name) {
@@ -186,7 +187,15 @@ async function waitForSwaggerSchema(baseUrl, token, role) {
186
187
  return await requestJson(swaggerUrl, { token, role });
187
188
  }
188
189
  async function confirmEnableApiDoc() {
189
- return confirmAction('Enable the API documentation plugin now?', { defaultValue: false });
190
+ try {
191
+ return await confirm({
192
+ message: 'Enable the API documentation plugin now?',
193
+ default: false,
194
+ });
195
+ }
196
+ catch {
197
+ return false;
198
+ }
190
199
  }
191
200
  async function fetchSwaggerSchema(baseUrl, token, role, context = {}, options = {}) {
192
201
  let response = options.retryAppAvailability === false
@@ -100,7 +100,7 @@ async function checkPostgresFamilyConnection(config) {
100
100
  await Promise.resolve(client.end()).catch(() => undefined);
101
101
  }
102
102
  }
103
- async function checkMysqlConnection(config) {
103
+ async function checkMysqlFamilyConnection(config) {
104
104
  const { default: mysql } = await import('mysql2/promise');
105
105
  const connection = await mysql.createConnection({
106
106
  host: config.host,
@@ -117,23 +117,6 @@ async function checkMysqlConnection(config) {
117
117
  await Promise.resolve(connection.end()).catch(() => undefined);
118
118
  }
119
119
  }
120
- async function checkMariaDbConnection(config) {
121
- const { default: mariadb } = await import('mariadb');
122
- const connection = await mariadb.createConnection({
123
- host: config.host,
124
- port: config.port,
125
- user: config.user,
126
- password: config.password,
127
- database: config.database,
128
- connectTimeout: DB_CONNECTION_TIMEOUT_MS,
129
- });
130
- try {
131
- await connection.query('SELECT 1');
132
- }
133
- finally {
134
- await Promise.resolve(connection.end()).catch(() => undefined);
135
- }
136
- }
137
120
  async function performExternalDbConnectionCheck(config) {
138
121
  try {
139
122
  switch (config.dialect) {
@@ -142,12 +125,9 @@ async function performExternalDbConnectionCheck(config) {
142
125
  await checkPostgresFamilyConnection(config);
143
126
  return undefined;
144
127
  }
145
- case 'mysql': {
146
- await checkMysqlConnection(config);
147
- return undefined;
148
- }
128
+ case 'mysql':
149
129
  case 'mariadb': {
150
- await checkMariaDbConnection(config);
130
+ await checkMysqlFamilyConnection(config);
151
131
  return undefined;
152
132
  }
153
133
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { access } from 'node:fs/promises';
10
+ import { resolveConfiguredEnvPath } from './cli-home.js';
11
+ function trimValue(value) {
12
+ const text = String(value ?? '').trim();
13
+ return text || undefined;
14
+ }
15
+ export function defaultDockerEnvFilePath(envName) {
16
+ return `${envName}/.env`;
17
+ }
18
+ export function resolveConfiguredDockerEnvFilePath(envName, config) {
19
+ return trimValue(config?.envFile) || defaultDockerEnvFilePath(envName);
20
+ }
21
+ export function hasExplicitDockerEnvFile(config) {
22
+ return Boolean(trimValue(config?.envFile));
23
+ }
24
+ export function resolveDockerEnvFilePath(envName, config) {
25
+ return resolveConfiguredEnvPath(resolveConfiguredDockerEnvFilePath(envName, config));
26
+ }
27
+ export async function dockerEnvFileExists(envName, config) {
28
+ const filePath = resolveDockerEnvFilePath(envName, config);
29
+ if (!filePath) {
30
+ return false;
31
+ }
32
+ try {
33
+ await access(filePath);
34
+ return true;
35
+ }
36
+ catch (_error) {
37
+ return false;
38
+ }
39
+ }
40
+ export async function resolveDockerEnvFileArg(envName, config) {
41
+ const filePath = resolveDockerEnvFilePath(envName, config);
42
+ if (!filePath) {
43
+ return undefined;
44
+ }
45
+ if (await dockerEnvFileExists(envName, config)) {
46
+ return filePath;
47
+ }
48
+ if (hasExplicitDockerEnvFile(config)) {
49
+ throw new Error(`The configured envFile for "${envName}" does not exist: ${resolveConfiguredDockerEnvFilePath(envName, config)}`);
50
+ }
51
+ return undefined;
52
+ }
@@ -14,7 +14,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
14
14
  import os from 'node:os';
15
15
  import path from 'node:path';
16
16
  import { getCurrentEnvName, getEnv, setEnvOauthSession, } from './auth-store.js';
17
- import { printInfo, printVerbose, printWarning, printWarningBlock, updateTask } from './ui.js';
17
+ import { printInfo, printVerbose, printWarning, printWarningBlock, stopTask, updateTask } from './ui.js';
18
18
  const ACCESS_TOKEN_REFRESH_WINDOW_MS = 60_000;
19
19
  const LOOPBACK_HOST = '127.0.0.1';
20
20
  const OAUTH_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
@@ -823,10 +823,11 @@ export async function authenticateEnvWithOauth(options) {
823
823
  const browser = await maybeOpenBrowser(authorizationUrl.toString());
824
824
  cleanupBrowserOpenTarget = browser.cleanup;
825
825
  if (!browser.opened) {
826
- printWarningBlock('We could not open your browser automatically. Open this URL to continue signing in:');
826
+ printWarningBlock('We could not open your browser automatically. Open the URL below to continue signing in:');
827
827
  }
828
828
  else {
829
- printInfo('Your browser should open shortly. Finish signing in there to continue.');
829
+ stopTask();
830
+ printInfo('Open this URL to sign in.');
830
831
  }
831
832
  printInfo(authorizationUrl.toString());
832
833
  const code = await new Promise((resolve, reject) => {
@@ -15,6 +15,7 @@ const STRING_ENV_CONFIG_KEYS = [
15
15
  'npmRegistry',
16
16
  'appRootPath',
17
17
  'storagePath',
18
+ 'envFile',
18
19
  'appPort',
19
20
  'appKey',
20
21
  'timezone',
@@ -6,9 +6,9 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
- import * as p from '@clack/prompts';
10
9
  import { stdin as input, stdout as output } from 'node:process';
11
10
  import { getCurrentEnvName } from './auth-store.js';
11
+ import { confirm } from "./inquirer.js";
12
12
  function normalizeEnvName(value) {
13
13
  const text = String(value ?? '').trim();
14
14
  return text || undefined;
@@ -50,12 +50,13 @@ export async function ensureCrossEnvConfirmed(options) {
50
50
  if (!interactiveTerminal) {
51
51
  options.command.error(formatCrossEnvRefusalMessage(currentEnv, requestedEnv));
52
52
  }
53
- const answer = await p.confirm({
54
- message: formatCrossEnvPromptMessage(currentEnv, requestedEnv),
55
- initialValue: false,
56
- });
57
- if (p.isCancel(answer)) {
53
+ try {
54
+ return Boolean(await confirm({
55
+ message: formatCrossEnvPromptMessage(currentEnv, requestedEnv),
56
+ default: false,
57
+ }));
58
+ }
59
+ catch {
58
60
  return false;
59
61
  }
60
- return Boolean(answer);
61
62
  }
@@ -152,7 +152,6 @@ export class GeneratedApiCommand extends Command {
152
152
  yes: flags.yes,
153
153
  });
154
154
  if (!confirmed) {
155
- this.log('Canceled.');
156
155
  return;
157
156
  }
158
157
  const response = await executeApiRequest({
@@ -0,0 +1,17 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ export function buildPlainMessageTheme(extra) {
10
+ return {
11
+ ...extra,
12
+ style: {
13
+ ...extra?.style,
14
+ message: (text) => text,
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { cursorTo } from '@inquirer/ansi';
10
+ import pc from 'picocolors';
11
+ import { createPrompt, isBackspaceKey, isEnterKey, makeTheme, useEffect, useKeypress, usePrefix, useState, } from '@inquirer/core';
12
+ let confirmModulePromise;
13
+ let selectModulePromise;
14
+ function loadConfirmModule() {
15
+ confirmModulePromise ??= import('@inquirer/confirm');
16
+ return confirmModulePromise;
17
+ }
18
+ function loadSelectModule() {
19
+ selectModulePromise ??= import('@inquirer/select');
20
+ return selectModulePromise;
21
+ }
22
+ export function buildPlainMessageTheme(extra) {
23
+ return {
24
+ ...extra,
25
+ style: {
26
+ ...extra?.style,
27
+ message: (text) => text,
28
+ },
29
+ };
30
+ }
31
+ function buildSelectAnswer(text) {
32
+ return `\n${pc.cyan('❯')} ${pc.cyan(text)}`;
33
+ }
34
+ function buildConfirmAnswer(text) {
35
+ return `\n${pc.cyan('❯')} ${pc.cyan(text)}`;
36
+ }
37
+ function isFullWidthCodePoint(codePoint) {
38
+ return (codePoint >= 0x1100 &&
39
+ (codePoint <= 0x115f ||
40
+ codePoint === 0x2329 ||
41
+ codePoint === 0x232a ||
42
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
43
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
44
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
45
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
46
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
47
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
48
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
49
+ (codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
50
+ (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
51
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)));
52
+ }
53
+ function stringWidth(value) {
54
+ let width = 0;
55
+ for (const char of value) {
56
+ const codePoint = char.codePointAt(0);
57
+ if (codePoint == null)
58
+ continue;
59
+ if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f))
60
+ continue;
61
+ width += isFullWidthCodePoint(codePoint) ? 2 : 1;
62
+ }
63
+ return width;
64
+ }
65
+ function stripAnsi(value) {
66
+ return value.replace(new RegExp(String.raw `\\u001B\\[[0-9;]*m`, 'g'), '');
67
+ }
68
+ function lastLineWidth(value) {
69
+ const lines = stripAnsi(value).split('\n');
70
+ return stringWidth(lines[lines.length - 1] ?? '');
71
+ }
72
+ function resolveRequiredError(required) {
73
+ if (typeof required === 'string') {
74
+ return required;
75
+ }
76
+ return '此项为必填';
77
+ }
78
+ function hasUsableDefault(value) {
79
+ return value != null && value !== '';
80
+ }
81
+ function buildIdleHint(config) {
82
+ const placeholder = config.placeholder?.trim();
83
+ if (placeholder) {
84
+ return placeholder;
85
+ }
86
+ return '';
87
+ }
88
+ function buildInputLine(value) {
89
+ return `${pc.cyan('❯')} ${value}`;
90
+ }
91
+ function buildErrorLine(error) {
92
+ return pc.red(`✖ ${error}`);
93
+ }
94
+ export const input = createPrompt((config, done) => {
95
+ const theme = makeTheme(config.theme);
96
+ const [status, setStatus] = useState('idle');
97
+ const [defaultValue, setDefaultValue] = useState(config.default ?? '');
98
+ const [value, setValue] = useState('');
99
+ const [error, setError] = useState();
100
+ const prefix = usePrefix({ status, theme });
101
+ useEffect((rl) => {
102
+ if (config.default) {
103
+ rl.write(config.default);
104
+ setValue(config.default);
105
+ }
106
+ }, []);
107
+ useKeypress(async (key, rl) => {
108
+ if (status !== 'idle')
109
+ return;
110
+ if (isBackspaceKey(key) && !value) {
111
+ setDefaultValue('');
112
+ return;
113
+ }
114
+ if (isBackspaceKey(key)) {
115
+ setValue(rl.line.slice(0, -1));
116
+ setError(undefined);
117
+ return;
118
+ }
119
+ if (!isEnterKey(key)) {
120
+ setValue(rl.line);
121
+ setError(undefined);
122
+ return;
123
+ }
124
+ const answer = value || defaultValue;
125
+ setStatus('loading');
126
+ if (!answer && config.required) {
127
+ rl.write(value);
128
+ setError(resolveRequiredError(config.required));
129
+ setStatus('idle');
130
+ return;
131
+ }
132
+ if (config.validate) {
133
+ const result = await config.validate(answer);
134
+ if (result !== true) {
135
+ rl.write(value);
136
+ setError(typeof result === 'string' ? result : 'Invalid input');
137
+ setStatus('idle');
138
+ return;
139
+ }
140
+ }
141
+ setValue(answer);
142
+ setStatus('done');
143
+ done(answer);
144
+ });
145
+ const message = theme.style.message(config.message, status);
146
+ const isTyping = value.length > 0;
147
+ const hasDefault = hasUsableDefault(defaultValue);
148
+ const idleHint = buildIdleHint(config);
149
+ const showIdleHint = !isTyping && status === 'idle' && idleHint !== '';
150
+ const showDefaultValue = !isTyping && status === 'idle' && hasDefault;
151
+ const displayValue = status === 'done'
152
+ ? theme.style.answer(value)
153
+ : isTyping
154
+ ? value
155
+ : showDefaultValue
156
+ ? defaultValue
157
+ : showIdleHint
158
+ ? pc.dim(idleHint)
159
+ : '';
160
+ const headerLine = [prefix, message].filter(Boolean).join(' ');
161
+ const inputLine = buildInputLine(displayValue);
162
+ const prompt = headerLine.endsWith('\n') ? `${headerLine}${inputLine}` : `${headerLine}\n${inputLine}`;
163
+ const promptWithCursorFix = showIdleHint
164
+ ? prompt + cursorTo(lastLineWidth(buildInputLine('')))
165
+ : showDefaultValue
166
+ ? prompt + cursorTo(lastLineWidth(buildInputLine(defaultValue)))
167
+ : prompt;
168
+ return [promptWithCursorFix, error ? buildErrorLine(error) : ''];
169
+ });
170
+ export async function confirm(options) {
171
+ const module = await loadConfirmModule();
172
+ return module.default({
173
+ ...options,
174
+ theme: buildPlainMessageTheme({
175
+ style: {
176
+ answer: buildConfirmAnswer,
177
+ },
178
+ ...options.theme,
179
+ }),
180
+ transformer: options.transformer ?? ((value) => (value ? 'Yes' : 'No')),
181
+ });
182
+ }
183
+ export async function select(options) {
184
+ const module = await loadSelectModule();
185
+ return module.default({
186
+ ...options,
187
+ theme: buildPlainMessageTheme({
188
+ style: {
189
+ answer: buildSelectAnswer,
190
+ },
191
+ ...options.theme,
192
+ }),
193
+ });
194
+ }
195
+ function resolveMaskChar(mask) {
196
+ if (typeof mask === 'string') {
197
+ return mask;
198
+ }
199
+ if (mask === false) {
200
+ return '';
201
+ }
202
+ return '•';
203
+ }
204
+ export const password = createPrompt((config, done) => {
205
+ const theme = makeTheme(config.theme);
206
+ const [status, setStatus] = useState('idle');
207
+ const [value, setValue] = useState('');
208
+ const [error, setError] = useState();
209
+ const prefix = usePrefix({ status, theme });
210
+ useKeypress(async (key, rl) => {
211
+ if (status !== 'idle')
212
+ return;
213
+ if (!isEnterKey(key)) {
214
+ setValue(rl.line);
215
+ setError(undefined);
216
+ return;
217
+ }
218
+ const answer = value;
219
+ setStatus('loading');
220
+ if (config.validate) {
221
+ const result = await config.validate(answer);
222
+ if (result !== true) {
223
+ rl.write(value);
224
+ setError(typeof result === 'string' ? result : 'Invalid input');
225
+ setStatus('idle');
226
+ return;
227
+ }
228
+ }
229
+ setValue(answer);
230
+ setStatus('done');
231
+ done(answer);
232
+ });
233
+ const message = theme.style.message(config.message, status);
234
+ const maskChar = resolveMaskChar(config.mask);
235
+ const displayValue = status === 'done'
236
+ ? theme.style.answer(maskChar.repeat(value.length))
237
+ : maskChar
238
+ ? maskChar.repeat(value.length)
239
+ : '';
240
+ const headerLine = [prefix, message].filter(Boolean).join(' ');
241
+ const inputLine = buildInputLine(displayValue);
242
+ const prompt = headerLine.endsWith('\n') ? `${headerLine}${inputLine}` : `${headerLine}\n${inputLine}`;
243
+ return [prompt, error ? buildErrorLine(error) : ''];
244
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ export function pickKeys(object, keys) {
10
+ const picked = {};
11
+ for (const key of keys) {
12
+ if (Object.prototype.hasOwnProperty.call(object, key)) {
13
+ const typedKey = key;
14
+ picked[typedKey] = object[typedKey];
15
+ }
16
+ }
17
+ return picked;
18
+ }
19
+ export function omitKeys(object, keys) {
20
+ const omittedKeys = new Set(keys);
21
+ const result = {};
22
+ for (const key of Object.keys(object)) {
23
+ if (!omittedKeys.has(key)) {
24
+ const typedKey = key;
25
+ result[typedKey] = object[typedKey];
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+ export function upperFirst(value) {
31
+ if (!value) {
32
+ return value;
33
+ }
34
+ return value[0].toUpperCase() + value.slice(1);
35
+ }
36
+ export function deepEqual(left, right) {
37
+ if (Object.is(left, right)) {
38
+ return true;
39
+ }
40
+ if (left == null || right == null) {
41
+ return false;
42
+ }
43
+ if (typeof left !== typeof right) {
44
+ return false;
45
+ }
46
+ if (typeof left !== 'object') {
47
+ return false;
48
+ }
49
+ if (Array.isArray(left) || Array.isArray(right)) {
50
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
51
+ return false;
52
+ }
53
+ for (let index = 0; index < left.length; index += 1) {
54
+ if (!deepEqual(left[index], right[index])) {
55
+ return false;
56
+ }
57
+ }
58
+ return true;
59
+ }
60
+ const leftObject = left;
61
+ const rightObject = right;
62
+ const leftKeys = Object.keys(leftObject);
63
+ const rightKeys = Object.keys(rightObject);
64
+ if (leftKeys.length !== rightKeys.length) {
65
+ return false;
66
+ }
67
+ for (const key of leftKeys) {
68
+ if (!Object.prototype.hasOwnProperty.call(rightObject, key)) {
69
+ return false;
70
+ }
71
+ if (!deepEqual(leftObject[key], rightObject[key])) {
72
+ return false;
73
+ }
74
+ }
75
+ return true;
76
+ }