@adaptivestone/framework 5.0.0-alpha.2 → 5.0.0-alpha.21

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 (54) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/commands/CreateUser.js +3 -1
  3. package/commands/GenerateRandomBytes.js +15 -0
  4. package/commands/migration/Migrate.js +1 -1
  5. package/config/auth.js +5 -1
  6. package/config/ipDetector.js +14 -0
  7. package/controllers/Auth.js +2 -2
  8. package/controllers/Home.js +1 -1
  9. package/folderConfig.js +0 -1
  10. package/helpers/files.js +8 -8
  11. package/jsconfig.json +9 -0
  12. package/models/User.js +7 -1
  13. package/modules/AbstractCommand.js +2 -1
  14. package/modules/AbstractController.js +22 -18
  15. package/modules/AbstractModel.d.ts +48 -0
  16. package/modules/AbstractModel.js +20 -2
  17. package/modules/Base.d.ts +5 -4
  18. package/modules/Base.js +11 -1
  19. package/package.json +13 -16
  20. package/server.d.ts +7 -5
  21. package/server.js +11 -7
  22. package/services/cache/Cache.d.ts +2 -2
  23. package/services/cache/Cache.js +10 -6
  24. package/services/http/HttpServer.js +12 -20
  25. package/services/http/middleware/GetUserByToken.js +3 -2
  26. package/services/http/middleware/I18n.js +20 -21
  27. package/services/http/middleware/IpDetector.js +59 -0
  28. package/services/http/middleware/Pagination.js +3 -2
  29. package/services/http/middleware/RateLimiter.js +8 -2
  30. package/services/http/middleware/RequestLogger.js +3 -3
  31. package/services/http/middleware/RequestParser.js +5 -2
  32. package/services/messaging/email/index.js +7 -15
  33. package/services/validate/ValidateService.js +1 -1
  34. package/tests/setup.js +3 -1
  35. package/tests/setupVitest.js +1 -1
  36. package/types/TFoldersConfig.d.ts +0 -2
  37. package/.eslintrc.cjs +0 -41
  38. package/controllers/Auth.test.js +0 -451
  39. package/controllers/Home.test.js +0 -12
  40. package/models/Migration.test.js +0 -20
  41. package/models/Sequence.test.js +0 -43
  42. package/models/User.test.js +0 -143
  43. package/modules/Modules.test.js +0 -18
  44. package/services/cache/Cache.test.js +0 -81
  45. package/services/http/middleware/Auth.test.js +0 -57
  46. package/services/http/middleware/Cors.test.js +0 -147
  47. package/services/http/middleware/GetUserByToken.test.js +0 -108
  48. package/services/http/middleware/I18n.test.js +0 -96
  49. package/services/http/middleware/PrepareAppInfo.test.js +0 -26
  50. package/services/http/middleware/RateLimiter.test.js +0 -233
  51. package/services/http/middleware/RequestParser.test.js +0 -112
  52. package/services/http/middleware/Role.test.js +0 -93
  53. package/services/http/middleware/StaticFiles.js +0 -59
  54. package/services/validate/ValidateService.test.js +0 -107
package/server.js CHANGED
@@ -1,16 +1,21 @@
1
1
  /* eslint-disable no-console */
2
2
  import EventEmitter from 'node:events';
3
- import { hrtime } from 'node:process';
3
+ import { hrtime, loadEnvFile } from 'node:process';
4
4
  import * as url from 'node:url';
5
5
  import path from 'node:path';
6
6
 
7
- import 'dotenv/config';
8
7
  import merge from 'deepmerge';
9
8
  import winston from 'winston';
10
9
  import { getFilesPathWithInheritance } from './helpers/files.js';
11
10
  import { consoleLogger } from './helpers/logger.js';
12
11
  import Cache from './services/cache/Cache.js';
13
12
 
13
+ try {
14
+ loadEnvFile();
15
+ } catch (e) {
16
+ console.warn('No env file found. This is ok. But please check youself.');
17
+ }
18
+
14
19
  /**
15
20
  * Main framework class.
16
21
  */
@@ -19,6 +24,8 @@ class Server {
19
24
 
20
25
  #isInited = false;
21
26
 
27
+ cli = null;
28
+
22
29
  /**
23
30
  * Construct new server
24
31
  * @param {Object} config main config object
@@ -27,7 +34,6 @@ class Server {
27
34
  * @param {String} config.folders.models path to folder with moidels files
28
35
  * @param {String} config.folders.controllers path to folder with controllers files
29
36
  * @param {String} config.folders.views path to folder with view files
30
- * @param {String} config.folders.public path to folder with public files
31
37
  * @param {String} config.folders.locales path to folder with locales files
32
38
  * @param {String} config.folders.emails path to folder with emails files
33
39
  */
@@ -56,13 +62,11 @@ class Server {
56
62
  models: new Map(),
57
63
  modelConstructors: new Map(),
58
64
  };
59
-
60
- this.cli = false;
61
65
  }
62
66
 
63
67
  /**
64
68
  * Start server (http + init all http ralated functions)
65
- * @param <Promise>callbackBefore404 code that should be executed before adding page 404
69
+ * @param {Function} callbackBefore404 code that should be executed before adding page 404
66
70
  * @returns {Promise}
67
71
  */
68
72
  async startServer(callbackBefore404 = async () => Promise.resolve()) {
@@ -350,7 +354,7 @@ class Server {
350
354
  * Return model from {modelName} (file name) on model folder.
351
355
  * Support cache
352
356
  * @param {String} modelName name on config file to load
353
- * @returns {import('mongoose').Model}
357
+ * @returns {import('mongoose').Model | false| {}}
354
358
  */
355
359
  getModel(modelName) {
356
360
  if (modelName.endsWith('s')) {
@@ -1,5 +1,5 @@
1
1
  import Base from '../../modules/Base';
2
- import Server from '../../server';
2
+ import Server from '../../server.js';
3
3
 
4
4
  declare class Cache extends Base {
5
5
  app: Server['app'];
@@ -32,4 +32,4 @@ declare class Cache extends Base {
32
32
  removeKey(key: string): Promise<number>;
33
33
  }
34
34
 
35
- export = Cache;
35
+ export default Cache;
@@ -43,8 +43,10 @@ class Cache extends Base {
43
43
  }
44
44
  const key = this.getKeyWithNameSpace(keyValue);
45
45
  // 5 mins default
46
- let resolve = null;
47
- let reject = null;
46
+ // eslint-disable-next-line no-unused-vars
47
+ let resolve = (value) => {};
48
+ // eslint-disable-next-line no-unused-vars
49
+ let reject = (value) => {};
48
50
  if (this.promiseMapping.has(key)) {
49
51
  return this.promiseMapping.get(key);
50
52
  }
@@ -69,12 +71,14 @@ class Cache extends Base {
69
71
  return Promise.reject(e);
70
72
  }
71
73
 
72
- this.redisClient.setEx(
74
+ this.redisClient.set(
73
75
  key,
74
- storeTime,
75
- JSON.stringify(result, (jsonkey, value) =>
76
+ JSON.stringify(result, (_jsonkey, value) =>
76
77
  typeof value === 'bigint' ? `${value}n` : value,
77
78
  ),
79
+ {
80
+ EX: storeTime,
81
+ },
78
82
  );
79
83
  } else {
80
84
  this.logger.verbose(
@@ -84,7 +88,7 @@ class Cache extends Base {
84
88
  )}`,
85
89
  );
86
90
  try {
87
- result = JSON.parse(result, (jsonkey, value) => {
91
+ result = JSON.parse(result, (_jsonkey, value) => {
88
92
  if (typeof value === 'string' && /^\d+n$/.test(value)) {
89
93
  return BigInt(value.slice(0, value.length - 1));
90
94
  }
@@ -1,12 +1,12 @@
1
1
  import http from 'node:http';
2
- import path from 'node:path';
3
- import * as url from 'node:url';
2
+ // import path from 'node:path';
3
+ // import * as url from 'node:url';
4
4
  import express from 'express';
5
5
  import RequestLoggerMiddleware from './middleware/RequestLogger.js';
6
6
  import I18nMiddleware from './middleware/I18n.js';
7
7
  import PrepareAppInfoMiddleware from './middleware/PrepareAppInfo.js';
8
8
  import RequestParserMiddleware from './middleware/RequestParser.js';
9
- import StaticFilesMiddleware from './middleware/StaticFiles.js';
9
+ import IpDetector from './middleware/IpDetector.js';
10
10
  import Cors from './middleware/Cors.js';
11
11
  import Base from '../../modules/Base.js';
12
12
 
@@ -18,15 +18,16 @@ class HttpServer extends Base {
18
18
  super(app);
19
19
  this.express = express();
20
20
  this.express.disable('x-powered-by');
21
- const dirname = url.fileURLToPath(new URL('.', import.meta.url));
22
- this.express.set('views', [
23
- this.app.foldersConfig.views,
24
- path.join(dirname, '../../views'),
25
- ]);
26
- this.express.set('view engine', 'pug');
21
+ // const dirname = url.fileURLToPath(new URL('.', import.meta.url));
22
+ // this.express.set('views', [
23
+ // this.app.foldersConfig.views,
24
+ // path.join(dirname, '../../views'),
25
+ // ]);
26
+ // this.express.set('view engine', 'pug');
27
27
 
28
- this.express.use(new PrepareAppInfoMiddleware(this.app).getMiddleware());
29
28
  this.express.use(new RequestLoggerMiddleware(this.app).getMiddleware());
29
+ this.express.use(new PrepareAppInfoMiddleware(this.app).getMiddleware());
30
+ this.express.use(new IpDetector(this.app).getMiddleware());
30
31
  this.express.use(new I18nMiddleware(this.app).getMiddleware());
31
32
 
32
33
  const httpConfig = this.app.getConfig('http');
@@ -35,15 +36,6 @@ class HttpServer extends Base {
35
36
  origins: httpConfig.corsDomains,
36
37
  }).getMiddleware(),
37
38
  );
38
- // todo whitelist
39
- this.express.use(
40
- new StaticFilesMiddleware(this.app, {
41
- folders: [
42
- this.app.foldersConfig.public,
43
- path.join(dirname, '../../public/files'),
44
- ],
45
- }).getMiddleware(),
46
- );
47
39
 
48
40
  this.express.use(new RequestParserMiddleware(this.app).getMiddleware());
49
41
 
@@ -57,7 +49,7 @@ class HttpServer extends Base {
57
49
  res.status(500).json({ message: 'Something broke!' });
58
50
  });
59
51
 
60
- this.httpServer = http.Server(this.express);
52
+ this.httpServer = http.createServer(this.express);
61
53
 
62
54
  const listener = this.httpServer.listen(
63
55
  httpConfig.port,
@@ -5,13 +5,14 @@ class GetUserByToken extends AbstractMiddleware {
5
5
  return 'Grab a token and try to parse the user from it. It user exist will add req.appInfo.user variable';
6
6
  }
7
7
 
8
+ // eslint-disable-next-line class-methods-use-this
8
9
  get usedAuthParameters() {
9
10
  return [
10
11
  {
11
12
  name: 'Authorization',
12
13
  type: 'apiKey',
13
14
  in: 'header',
14
- description: this?.description,
15
+ description: GetUserByToken.description,
15
16
  },
16
17
  ];
17
18
  }
@@ -21,7 +22,7 @@ class GetUserByToken extends AbstractMiddleware {
21
22
  this.logger.warn('You call GetUserByToken more then once');
22
23
  return next();
23
24
  }
24
- let { token } = req.body;
25
+ let { token } = req.body || {};
25
26
  this.logger.verbose(
26
27
  `GetUserByToken token in BODY ${token}. Token in Authorization header ${req.get(
27
28
  'Authorization',
@@ -1,37 +1,36 @@
1
1
  import i18next from 'i18next';
2
2
  import BackendFS from 'i18next-fs-backend';
3
- import Backend from 'i18next-chained-backend';
4
3
  import AbstractMiddleware from './AbstractMiddleware.js';
5
4
 
6
5
  class I18n extends AbstractMiddleware {
6
+ cache = {};
7
+
8
+ enabled = true;
9
+
10
+ lookupQuerystring = '';
11
+
12
+ supportedLngs = [];
13
+
14
+ fallbackLng = 'en';
15
+
16
+ /** @type {i18next} */
17
+ i18n = {
18
+ // @ts-ignore
19
+ t: (text) => text,
20
+ language: 'en',
21
+ };
22
+
7
23
  constructor(app, params) {
8
24
  super(app, params);
9
25
  const I18NConfig = this.app.getConfig('i18n');
10
- this.i18n = {
11
- t: (text) => text,
12
- language: I18NConfig.fallbackLng,
13
- };
14
- this.cache = {};
15
26
 
16
27
  if (I18NConfig.enabled) {
17
28
  this.logger.info('Enabling i18n support');
18
29
  this.i18n = i18next;
19
- i18next.use(Backend).init({
30
+ i18next.use(BackendFS).init({
20
31
  backend: {
21
- backends: [
22
- BackendFS,
23
- // BackendFS,
24
- ],
25
- backendOptions: [
26
- // {
27
- // loadPath: __dirname + '/../../locales/{{lng}}/{{ns}}.json',
28
- // addPath: __dirname + '/../../locales/{{lng}}/{{ns}}.missing.json'
29
- // },
30
- {
31
- loadPath: `${this.app.foldersConfig.locales}/{{lng}}/{{ns}}.json`,
32
- addPath: `${this.app.foldersConfig.locales}/{{lng}}/{{ns}}.missing.json`,
33
- },
34
- ],
32
+ loadPath: `${this.app.foldersConfig.locales}/{{lng}}/{{ns}}.json`,
33
+ addPath: `${this.app.foldersConfig.locales}/{{lng}}/{{ns}}.missing.json`,
35
34
  },
36
35
  fallbackLng: I18NConfig.fallbackLng,
37
36
  preload: I18NConfig.preload,
@@ -0,0 +1,59 @@
1
+ import { BlockList } from 'node:net';
2
+
3
+ import AbstractMiddleware from './AbstractMiddleware.js';
4
+
5
+ class IpDetector extends AbstractMiddleware {
6
+ static get description() {
7
+ return 'Detect real user IP address. Support proxy and load balancer';
8
+ }
9
+
10
+ constructor(app, params) {
11
+ super(app, params);
12
+ const { trustedProxy } = this.app.getConfig('ipDetector');
13
+
14
+ this.blockList = new BlockList();
15
+
16
+ for (const subnet of trustedProxy) {
17
+ const addressType = subnet.includes(':') ? 'ipv6' : 'ipv4';
18
+ if (subnet.includes('/')) {
19
+ // CIDR
20
+ const [realSubnet, prefixLength] = subnet.split('/');
21
+ this.blockList.addSubnet(
22
+ realSubnet,
23
+ parseInt(prefixLength, 10),
24
+ addressType,
25
+ );
26
+ } else if (subnet.includes('-')) {
27
+ // RANGE
28
+ const [start, end] = subnet.split('-');
29
+ this.blockList.addRange(start, end, addressType);
30
+ } else {
31
+ // just an address
32
+ this.blockList.addAddress(subnet, addressType);
33
+ }
34
+ }
35
+ }
36
+
37
+ async middleware(req, res, next) {
38
+ const { headers } = this.app.getConfig('ipDetector');
39
+ const initialIp = req.socket.remoteAddress;
40
+ req.appInfo.ip = initialIp;
41
+ const addressType = initialIp.includes(':') ? 'ipv6' : 'ipv4';
42
+
43
+ if (this.blockList.check(initialIp, addressType)) {
44
+ // we can trust this source
45
+ for (const header of headers) {
46
+ // in a range
47
+ const ipHeader = req.headers[header.toLowerCase()];
48
+ if (ipHeader) {
49
+ const [firstIp] = ipHeader.split(',').map((ip) => ip.trim());
50
+ req.appInfo.ip = firstIp;
51
+ break;
52
+ }
53
+ }
54
+ }
55
+ next();
56
+ }
57
+ }
58
+
59
+ export default IpDetector;
@@ -19,8 +19,9 @@ class Pagination extends AbstractMiddleware {
19
19
  async middleware(req, res, next) {
20
20
  let { limit, maxLimit } = this.params;
21
21
 
22
- limit = typeof limit === 'number' ? parseInt(limit, 10) : 10;
23
- maxLimit = typeof maxLimit === 'number' ? parseInt(maxLimit, 10) : 100;
22
+ limit = (typeof limit !== 'number' ? parseInt(limit, 10) : limit) || 10;
23
+ maxLimit =
24
+ (typeof maxLimit !== 'number' ? parseInt(maxLimit, 10) : maxLimit) || 100;
24
25
 
25
26
  req.appInfo.pagination = {};
26
27
  req.appInfo.pagination.page =
@@ -78,10 +78,16 @@ class RateLimiter extends AbstractMiddleware {
78
78
 
79
79
  const key = [];
80
80
  if (ip) {
81
- key.push(req.ip);
81
+ if (!req.appInfo.ip) {
82
+ this.logger.error(
83
+ `RateLimiter: Can't get remote address from request. Please check that you used IpDetecor middleware before RateLimiter`,
84
+ );
85
+ } else {
86
+ key.push(req.appInfo.ip);
87
+ }
82
88
  }
83
89
  if (route) {
84
- key.push(req.originalUrl);
90
+ key.push(`${req.baseUrl ?? ''}${req.path ?? ''}`); // to avoid quesry params
85
91
  }
86
92
  if (user && req.appInfo?.user) {
87
93
  key.push(req.appInfo?.user.id);
@@ -6,13 +6,13 @@ class RequestLogger extends AbstractMiddleware {
6
6
  }
7
7
 
8
8
  async middleware(req, res, next) {
9
- const startTime = Date.now();
9
+ const startTime = performance.now();
10
10
  const text = `Request is [${req.method}] ${req.url}`;
11
11
  this.logger.info(text);
12
12
  res.on('finish', () => {
13
- const duration = Date.now() - startTime;
13
+ const end = performance.now();
14
14
  this.logger.info(
15
- `Finished ${text}. Status: ${res.statusCode}. Duration ${duration} ms`,
15
+ `Finished ${text}. Status: ${res.statusCode}. [${(end - startTime).toFixed(2)} ms]`,
16
16
  );
17
17
  });
18
18
  next();
@@ -18,7 +18,10 @@ class RequestParser extends AbstractMiddleware {
18
18
  [fields, files] = await form.parse(req);
19
19
  } catch (err) {
20
20
  this.logger.error(`Parsing failed ${err}`);
21
- return next(err);
21
+ return res.status(400).json({
22
+ message: `Error to parse your request. You provided invalid content type or content-length. Please check your request headers and content type.`,
23
+ });
24
+ // return next(err);
22
25
  }
23
26
  this.logger.verbose(
24
27
  `Parsing multipart/formdata request DONE ${Date.now() - time}ms`,
@@ -26,7 +29,7 @@ class RequestParser extends AbstractMiddleware {
26
29
 
27
30
  req.body = {
28
31
  // todo avoid body in next versions
29
- ...req.body,
32
+ ...(req.body || {}),
30
33
  ...fields,
31
34
  ...files,
32
35
  };
@@ -66,7 +66,8 @@ class Mail extends Base {
66
66
  * @param {object} templateData
67
67
  * @returns string
68
68
  */
69
- static async #renderTemplateFile({ type, fullPath } = {}, templateData = {}) {
69
+ // eslint-disable-next-line class-methods-use-this
70
+ async #renderTemplateFile({ type, fullPath } = {}, templateData = {}) {
70
71
  if (!type) {
71
72
  return null;
72
73
  }
@@ -116,19 +117,10 @@ class Mail extends Base {
116
117
 
117
118
  const [htmlRendered, subjectRendered, textRendered, extraCss] =
118
119
  await Promise.all([
119
- this.constructor.#renderTemplateFile(
120
- templates.html,
121
- templateDataToRender,
122
- ),
123
- this.constructor.#renderTemplateFile(
124
- templates.subject,
125
- templateDataToRender,
126
- ),
127
- this.constructor.#renderTemplateFile(
128
- templates.text,
129
- templateDataToRender,
130
- ),
131
- this.constructor.#renderTemplateFile(templates.style),
120
+ this.#renderTemplateFile(templates.html, templateDataToRender),
121
+ this.#renderTemplateFile(templates.subject, templateDataToRender),
122
+ this.#renderTemplateFile(templates.text, templateDataToRender),
123
+ this.#renderTemplateFile(templates.style),
132
124
  ]);
133
125
 
134
126
  juice.tableElements = ['TABLE'];
@@ -171,7 +163,7 @@ class Mail extends Base {
171
163
 
172
164
  /**
173
165
  * Send provided text (html) to email. Low level function. All data should be prepared before sending (like inline styles)
174
- * @param {objetc} app application
166
+ * @param {import('../../../server.js').default['app']} app application
175
167
  * @param {string} to send to
176
168
  * @param {string} subject email topic
177
169
  * @param {string} html hmlt body of emain
@@ -26,7 +26,7 @@ class ValidateService extends Base {
26
26
  );
27
27
  }
28
28
 
29
- static getDriverByValidatorBody(app, body) {
29
+ static getDriverByValidatorBody(app, body = {}) {
30
30
  if (this.isValidatorExists(body)) {
31
31
  return body;
32
32
  }
package/tests/setup.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable no-undef */
2
2
  import path from 'node:path';
3
+ import { randomBytes } from 'node:crypto';
3
4
  import { MongoMemoryReplSet } from 'mongodb-memory-server';
4
5
  import mongoose from 'mongoose';
5
6
  import redis from 'redis';
@@ -19,6 +20,8 @@ beforeAll(async () => {
19
20
  });
20
21
  await mongoMemoryServerInstance.waitUntilRunning();
21
22
  process.env.LOGGER_CONSOLE_LEVEL = 'error';
23
+ process.env.AUTH_SALT = randomBytes(16).toString('hex');
24
+
22
25
  const connectionStringMongo = await mongoMemoryServerInstance.getUri();
23
26
  // console.info('MONGO_URI: ', connectionStringMongo);
24
27
  global.server = new Server({
@@ -27,7 +30,6 @@ beforeAll(async () => {
27
30
  controllers:
28
31
  process.env.TEST_FOLDER_CONTROLLERS || path.resolve('./controllers'),
29
32
  views: process.env.TEST_FOLDER_VIEWS || path.resolve('./views'),
30
- public: process.env.TEST_FOLDER_PUBLIC || path.resolve('./public'),
31
33
  models: process.env.TEST_FOLDER_MODELS || path.resolve('./models'),
32
34
  emails:
33
35
  process.env.TEST_FOLDER_EMAIL ||
@@ -12,13 +12,13 @@ mongoose.set('autoIndex', false);
12
12
 
13
13
  beforeAll(async () => {
14
14
  process.env.LOGGER_CONSOLE_LEVEL = 'error';
15
+ process.env.AUTH_SALT = crypto.randomBytes(16).toString('hex');
15
16
  global.server = new Server({
16
17
  folders: {
17
18
  config: process.env.TEST_FOLDER_CONFIG || path.resolve('./config'),
18
19
  controllers:
19
20
  process.env.TEST_FOLDER_CONTROLLERS || path.resolve('./controllers'),
20
21
  views: process.env.TEST_FOLDER_VIEWS || path.resolve('./views'),
21
- public: process.env.TEST_FOLDER_PUBLIC || path.resolve('./public'),
22
22
  models: process.env.TEST_FOLDER_MODELS || path.resolve('./models'),
23
23
  emails:
24
24
  process.env.TEST_FOLDER_EMAIL ||
@@ -3,7 +3,6 @@
3
3
  * @param models path to folder with moidels files
4
4
  * @param controllers path to folder with controllers files
5
5
  * @param views path to folder with view files
6
- * @param public path to folder with public files
7
6
  * @param locales path to folder with locales files
8
7
  * @param emails path to folder with emails files
9
8
  */
@@ -12,7 +11,6 @@ type FolderConfig = {
12
11
  models: string;
13
12
  controllers: string;
14
13
  views: string;
15
- public: string;
16
14
  emails: string;
17
15
  };
18
16
 
package/.eslintrc.cjs DELETED
@@ -1,41 +0,0 @@
1
- module.exports = {
2
- env: {
3
- es2023: true,
4
- node: true,
5
- },
6
- extends: ['airbnb-base', 'prettier'],
7
- plugins: ['prettier'],
8
- parserOptions: {
9
- ecmaVersion: 2023,
10
- },
11
- rules: {
12
- 'no-restricted-syntax': [
13
- 'error',
14
- {
15
- selector: 'ForInStatement',
16
- message:
17
- 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.',
18
- },
19
- {
20
- selector: 'LabeledStatement',
21
- message:
22
- 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.',
23
- },
24
- {
25
- selector: 'WithStatement',
26
- message:
27
- '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
28
- },
29
- ],
30
- 'prettier/prettier': 'error',
31
- 'import/extensions': 'off', // it have a problem with dynamic imports
32
- 'import/prefer-default-export': 'off', // we want to have signgke default export too
33
- },
34
- overrides: [
35
- {
36
- files: ['**/*.test.js'],
37
- extends: ['plugin:vitest/all', 'plugin:vitest/recommended'],
38
- plugins: ['vitest'],
39
- },
40
- ],
41
- };