Pull to refresh
81.74
SimbirSoft
Лидер в разработке современных ИТ-решений на заказ

Настройка ESLint для чистого кода в проектах на Vue

Reading time14 min
Views23K

В процессе работы над проектами разработчики придерживаются определенного кодстайла. Как правило, за это отвечает ESLint. ESLint — это линтер для языка программирования JavaScript. Он статически анализирует код на наличие проблем, многие из которых можно исправить автоматически.

Как показывает практика, команды в проектах часто пренебрегают кастомной настройкой ESLint, оставляя дефолтную. В этом случае большая часть кодстайла остается на совести разработчика. Кодстайл, как правило, в таких проектах нигде не описан или существует в формате устной договоренности. При таком подходе большую часть правил приходится держать в уме, не говоря уже о том, что многие из них основаны на субъективных предпочтениях. Нередки случаи, когда разные части приложения отформатированы под разные правила. Например, если разработчики пишут код в разных операционных системах, то переносы строк у них отличаются. Правил так много, а настройки столь обширны, что использование разных редакторов кода в командной разработке может усложнить взаимодействие.

В этой статье рассмотрим пример настройки ESLint для разработки приложений на Vue. В итоге мы получим настройки ESLint, которые будут проверять наш код на соответствие большинству правил официального стайлгайда Vue. Материал полезен начинающим разработчикам, которые хотят улучшить свой стиль кода, и более опытным на старте нового проекта в незнакомой или большой распределенной команде. Эти настройки помогут придерживаться кодстайла и отслеживать некоторые ошибки (синтаксические, логические, ошибки, связанные с динамической типизацией) еще на этапе написания кода, повысят его читаемость и упростят код-ревью. В конечном итоге это приведет к сокращению сроков разработки.

Все форматирование кода будет осуществляться с помощью ESLint, поэтому если у вас установлен prettier или Vetur, желательно их отключить. ESLint необходимо установить в extensions вашей IDE, чтобы пользоваться его функционалом.

В конце статьи приведем пример файла .eslintrc.js с настройками.  

Начало работы

Создадим новое приложение. В настройках при установке выберем  ESLint + Airbnb config.

Файл .eslintrc.js должен выглядеть так:

В качестве базовых настроек используем конфигурацию @vue/airbnb, plugin:vue/essential заменим на plugin:vue/strongly-recommended (или plugin:vue/vue3-strongly-recommended, plugin:vue/vue3-essential, plugin:vue/vue3-recommended для vue 3) и дополним кастомными настройками. Если вы еще не знакомы с отличиями настроек essential от strongly-recommended, то рекомендуем изучить их здесь

Далее в объект rules будем добавлять правила, которые хотим использовать.

Правила для секции <template>

#vue/attributes-order Проверка порядка атрибутов:

<template>
  <!-- ✓ GOOD -->
  <div
    is="header"
    v-for="item in items"
    v-if="!visible"
    v-once
    id="uniqueID"
    ref="header"
    v-model="headerData"
    my-prop="prop"
    @click="functionCall"
    v-text="textContent">
  </div>
 
  <!-- ✗ BAD -->
  <div
    ref="header"
    my-prop="prop"
    v-for="item in items"
    v-once
    @click="functionCall"
    id="uniqueID"
    v-model="headerData"
    v-if="!visible"
    is="header"
    v-text="textContent">
  </div>
</template>

Пример настройки:

"vue/attributes-order": ["error", {
 "order": [
   "DEFINITION",
   "LIST_RENDERING",
   "CONDITIONALS",
   "RENDER_MODIFIERS",
   "GLOBAL",
   ["UNIQUE", "SLOT"],
   "TWO_WAY_BINDING",
   "OTHER_DIRECTIVES",
   "OTHER_ATTR",
   "EVENTS",
   "CONTENT"
 ],
 "alphabetical": false
}],

Это правило упорядочивания атрибутов компонентов. Порядок по умолчанию указан в руководстве по стилю Vue.js. и определен:

  • DEFINITION e.g. 'is', 'v-is'

  • LIST_RENDERING e.g. 'v-for item in items'

  • CONDITIONALS e.g. 'v-if', 'v-else-if', 'v-else', 'v-show', 'v-cloak'

  • RENDER_MODIFIERS e.g. 'v-once', 'v-pre'

  • GLOBAL e.g. 'id'

  • UNIQUE e.g. 'ref', 'key'

  • SLOT e.g. 'v-slot', 'slot'

  • TWO_WAY_BINDING e.g. 'v-model'

  • OTHER_DIRECTIVES e.g. 'v-custom-directive'

  • OTHER_ATTR e.g. 'custom-prop="foo"', 'v-bind:prop="foo"', ':prop="foo"'

  • EVENTS e.g. '@click="functionCall"', 'v-on="event"'

  • CONTENT e.g. 'v-text', 'v-html'

#vue/max-attributes-per-line Проверка на максимальное количество атрибутов в строке:

<template>
  <!-- ✓ GOOD -->
  <MyComponent lorem="1"/>
  <MyComponent
    lorem="1"
    ipsum="2"
  />
 
  <!-- ✗ BAD -->
  <MyComponent lorem="1" ipsum="2"/>
  <MyComponent
    lorem="1" ipsum="2"
  />
  <MyComponent
    lorem="1" ipsum="2"
    dolor="3"
  />
</template>

Пример настройки:

 "vue/max-attributes-per-line": ["error", {
    "singleline": {
      "max": 1
    },      
    "multiline": {
      "max": 1
    }
  }]

Значения singleline.max (number) и multiline.max (number) установим в значение 1, чтобы каждый атрибут начинался с новой строчки.

#vue/html-self-closing Проверка на самозакрывающийся тег или компонент:

<template>
  <!-- ✓ GOOD -->
  <div/>
  <img>
  <MyComponent/>
  <svg><path d=""/></svg>
 
  <!-- ✗ BAD -->
  <div></div>
  <img/>
  <MyComponent></MyComponent>
  <svg><path d=""></path></svg>
</template>
 "vue/html-self-closing": ["error", {
    "html": {
      "void": "never",
      "normal": "always",
      "component": "always"
    },
    "svg": "always",
    "math": "always"
  }]
  • html.void ("never" по умолчанию) — стиль хорошо известных пустых элементов HTML.

  • html.normal ("always" по умолчанию) — стиль известных элементов HTML за исключением пустых элементов.

  • html.component ("always" по умолчанию) — стиль пользовательских компонентов Vue.js.

  • svg ("always" по умолчанию) — стиль известных элементов SVG.

  • math ("always" по умолчанию) — стиль известных элементов MathML.

Каждый параметр может быть установлен в одно из следующих значений:

  • "always" — требовать самозакрытия элементов, у которых нет своего содержимого.

  • "never" — запретить самозакрытие.

  • "any" — не применять самозакрывающийся стиль.

#vue/html-indent Проверка последовательного отступа в шаблоне <template>:

<template>
  <!-- ✓ GOOD -->
  <div class="foo">
    Hello.
  </div>
  <div class="foo">
    Hello.
  </div>
  <div class="foo"
       :foo="bar"
  >
    World.
  </div>
  <div
    id="a"
    class="b"
    :other-attr="{
      aaa: 1,
      bbb: 2
    }"
    @other-attr2="
      foo();
      bar();
    "
  >
    {{
      displayMessage
    }}
  </div>
 
  <!-- ✗ BAD -->
 <div class="foo">
   Hello.
    </div>
</template>

Пример настройки:

'vue/html-indent': [
 'error',
 4,
 {
   attribute: 1,
   baseIndent: 1,
   closeBracket: 0,
   alignAttributesVertically: true,
   ignores: []
 }
],
  • type (number | "tab") — тип отступа. Значение по умолчанию 2. Если это число, то это количество пробелов для одного отступа. Если это "tab", он использует одну вкладку для одного отступа.

  • attribute (integer) — множитель отступа для атрибутов. Значение по умолчанию 1.

  • baseIndent (integer) — множитель отступа для операторов верхнего уровня. Значение по умолчанию 1.

  • closeBracket (integer | object) — множитель отступа для правых скобок. Значение по умолчанию 0.

    Вы можете применить все нижеперечисленное, установив числовое значение.

    • closeBracket.startTag (integer) — множитель отступа для правых скобок открывающих тегов (<div>). Значение по умолчанию 0.

    • closeBracket.endTag (integer) — множитель отступа для правых скобок закрывающих тегов (</div>). Значение по умолчанию 0.

    • closeBracket.selfClosingTag (integer) — множитель отступа для правых скобок открывающих тегов (<div/>). Значение по умолчанию 0.

  • alignAttributesVertically (boolean) — условие того, должны ли атрибуты выравниваться по вертикали с первым атрибутом в многострочном случае или нет. По умолчанию true.

  • ignores (string[]) — селектор для игнорирования узлов. Со спецификацией AST можно ознакомиться здесь.

#vue/component-name-in-template-casing Проверка регистра для стиля именования компонентов в шаблоне:

<template>
  <!-- ✓ GOOD -->
  <cool-component />
 
  <!-- ✗ BAD -->
  <CoolComponent />
  <coolComponent />
  <Cool-component />
 
  <!-- ignore -->
  <unregistered-component />
  <UnregisteredComponent />
</template>
<script>
export default {
  components: {
    CoolComponent
  }
}
</script>

Пример настройки:

"vue/component-name-in-template-casing": ["error", "kebab-case", {
 "registeredComponentsOnly": true,
}],
  • "PascalCase" (по умолчанию) — требует написание имен тегов в регистре паскаля. Например, <CoolComponent>. Это соответствует практике JSX.

  • "kebab-case" — требует написание имен тегов в регистре кебаба: например, <cool-component>. Это согласуется с практикой HTML, которая изначально нечувствительна к регистру.

  • registeredComponentsOnly — если true, проверяются только зарегистрированные компоненты (в PascalCase). Если false, проверяются все. По умолчанию true.

  • ignores (string[]) — имена элементов, которые следует игнорировать. Устанавливает разрешающее имя элемента. Например, пользовательские элементы или компоненты Vue со специальным именем. Вы можете установить регулярное выражение, написав его как "/^name/".

#vue/no-irregular-whitespace Проверка нерегулярных пробелов:

<template>
  <!-- ✓ GOOD -->
  <div class="foo bar" />
  <!-- ✗ BAD -->
  <div class="foo
bar" />
  <!--           ^ LINE TABULATION (U+000B) -->
</template>
<script>
/* ✓ GOOD */
var foo = bar;
/* ✗ BAD */
var foo =
bar;
//       ^ LINE TABULATION (U+000B)
</script>

Пример настройки:

"vue/no-irregular-whitespace": ["error", {
 "skipStrings": true,
 "skipComments": false,
 "skipRegExps": false,
 "skipTemplates": false,
 "skipHTMLAttributeValues": false,
 "skipHTMLTextContents": false
}],
  • skipStrings: true — разрешает любые пробельные символы в строковых литералах. По умолчанию true.

  • skipComments: true — разрешает любые пробельные символы в комментариях. По умолчанию false.

  • skipRegExps: true — разрешает любые пробельные символы в литералах регулярных выражений. По умолчанию false.

  • skipTemplates: true — разрешает любые пробельные символы в литералах шаблона. По умолчанию false.

  • skipHTMLAttributeValues: true — разрешает любые пробельные символы в значениях атрибутов HTML. По умолчанию false.

  • skipHTMLTextContents: true — разрешает любые пробельные символы в текстовом содержимом HTML. По умолчанию false.

Правила для секции <script>

#vue/component-definition-name-casing Проверка на определенный регистр для имени компонента:

<script>
export default {
  /* ✓ GOOD */
  name: 'MyComponent'
  /* ✗ BAD */
  name: 'my-component'
}
</script>

Пример настройки:

"vue/component-definition-name-casing": ["error", "PascalCase"],
  • "PascalCase" (по умолчанию) — требует преобразования имен компонентов к регистру паскаля.

  • "kebab-case" — требует преобразования имен компонентов к регистру kebab.

#vue/match-component-file-name Проверка имени компонента — оно должно соответствовать имени файла, в котором он находится:

// file name: src/MyComponent.vue
export default {
  /* ✓ GOOD */
  name: 'MyComponent',
  render() {
    return <h1>Hello world</h1>
  }
}
// file name: src/MyComponent.vue
export default {
  /* ✓ GOOD */
  name: 'my-component',
  render() { return <div /> }
}
// file name: src/MyComponent.vue
export default {
  /* ✗ BAD */
  name: 'MComponent', // пропущена буква y
  render() {
    return <h1>Hello world</h1>
  }
}

Пример настройки:

"vue/match-component-file-name": ["error", {
 "extensions": ["vue"],
 "shouldMatchCase": false
}],
  • "extensions": [] — массив расширений файлов для проверки. По умолчанию установлено значение ["jsx"].

  • "shouldMatchCase": false — логическое значение, указывающее, должно ли имя компонента также соответствовать регистру имени файла. По умолчанию установлено значение false.

#vue/no-dupe-keys Запретить дублирование имен полей:

<script>
/* ✗ BAD */
export default {
  props: {
    foo: String 
  },
  computed: {
    foo: {  // дубликат свойства
      get () {} 
    }
  },
  data: {
    foo: null // дубликат свойства
  },
  methods: {
    foo () {} // дубликат свойства
  }
}
</script>

Пример настройки:

 "vue/no-dupe-keys": ["error", {
    "groups": []
  }]
  • "groups" (string[]) — массив дополнительных групп для поиска дубликатов. По умолчанию пусто.

#vue/order-in-components Порядок свойств в компонентах:

<script>
/* ✗ BAD */
export default {
  name: 'app',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  props: {// неправильный порядок свойств props перед data 
    propA: Number   
  },
  methods: { // неправильный порядок свойств после computed
   add() {}
  },
  computed: { // неправильный порядок свойств перед methods
    foo () {}
  }
}
</script>
'vue/order-in-components': ['error', {
 order: [
   'el',
   'name',
   'key',
   'parent',
   'functional',
   ['delimiters', 'comments'],
   ['components', 'directives', 'filters'],
   'extends',
   'mixins',
   ['provide', 'inject'],
   'ROUTER_GUARDS',
   'layout',
   'middleware',
   'validate',
   'scrollToTop',
   'transition',
   'loading',
   'inheritAttrs',
   'model',
   ['props', 'propsData'],
   'emits',
   'setup',
   'asyncData',
   'data',
   'fetch',
   'head',
   'computed',
   'watch',
   'watchQuery',
   'LIFECYCLE_HOOKS',
   'methods',
   ['template', 'render'],
   'renderError'
 ]
}],

#comma-dangle Проверка  запятых:

  • arrays для литералов массива и шаблонов деструктуризации массива.
    например, let [a,] = [1,];

  • objects для объектных литералов и объектных шаблонов деструктуризации.
    например, let {a,} = {a: 1};

  • imports предназначен для деклараций импорта модулей ES.
    например, import {a,} from "foo";

  • exports для экспортных деклараций модулей ES.
    например, export {a,};

  • functions предназначен для объявлений функций и вызовов функций.
    например, (function(a,){ })(b,);

  • functions следует включать только при анализе ECMAScript 2017 или более поздней версии.

/* ✗ BAD */
var foo = {
    bar: "baz",
    qux: "quux",
};

var arr = [1,2,];

foo({
  bar: "baz",
  qux: "quux",
});
 /* ✓ GOOD */
var foo = {
    bar: "baz",
    qux: "quux"
};

var arr = [1,2];

foo({
  bar: "baz",
  qux: "quux"
});

Пример настройки:

   "comma-dangle": ["error", {
        "arrays": "never",
        "objects": "never",
        "imports": "never",
        "exports": "never",
        "functions": "never"
    }]
  • "never" (по умолчанию) запрещает запятые в конце.

  • "always" требует наличие запятых в конце.

  • "always-multiline" требует замыкающих запятых, когда последний элемент или свойство находятся в другой строке, а закрывающий “]” или  “}” на следующей и запрещает замыкающие запятые, когда последний элемент или свойство находится в той же строке, что и закрывающий “]” или “}”.

  • "only-multiline" разрешает (но не требует) замыкающие запятые, когда последний элемент или свойство находятся в иной строке, чем закрывающий “]” или  “}”, и запрещает замыкающие запятые, когда последний элемент или свойство находятся на той же строке, что и закрывающий “]” или “}”.

Вы также можете использовать параметр объекта, чтобы настроить это правило для каждого типа синтаксиса. Для каждого из следующих параметров можно установить значения "never", "always", "always-multiline", "only-multiline" или "ignore". Значение по умолчанию для каждого параметра равно "never", если не указано иное.

Общие настройки ESLint

'linebreak-style': ["error", "unix"], //стиль разрыва строки linebreak-style: ["error", "unix || windows"]
'no-console': 'error', // без console.log
'no-debugger': 'error',// без debugger
'arrow-parens': ['error', 'as-needed'], // скобки в стрелочной функции
'no-plusplus': 'off', //запрещает унарные операторы ++и --
'constructor-super': 'off', // конструкторы производных классов должны вызывать super(). Конструкторы не производных классов не должны вызывать super().
"no-mixed-operators": [ //Заключение сложных выражений в круглые скобки проясняет замысел разработчика
 "error",
 {
   "groups": [
     ["+", "-", "*", "/", "%", "**"],
     ["&", "|", "^", "~", "<<", ">>", ">>>"],
     ["==", "!=", "===", "!==", ">", ">=", "<", "<="],
     ["&&", "||"],
     ["in", "instanceof"]
   ],
   "allowSamePrecedence": true
 }
],
'import/extensions': 'off', // обеспечить согласованное использование расширения файла в пути импорта
'import/prefer-default-export': 'off', // ESLint предпочитает экспорт по умолчанию импорт/предпочитает экспорт по умолчанию
'no-unused-expressions': 'error', //нет неиспользуемых выражений
'no-param-reassign': 'off', //без переназначения параметров
'prefer-destructuring': ["error", { // требуется деструктуризация массивов и/или объектов.
     "array": true,
     "object": true
   }, {
     "enforceForRenamedProperties": false
   }
 ],
'no-bitwise': ['error', { allow: ['~'] }], // запрещает побитовые операторы.
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // запрещает неиспользуемые переменные.
'max-len': ['error', { code: 120 }], // обеспечивает максимальную длину строки.
'object-curly-newline': ['error', {
 ObjectExpression: { multiline: true, consistent: true },
 ObjectPattern: { multiline: true, consistent: true }
}], // применяет согласованные разрывы строк после открытия и перед закрытием фигурных скобок.
'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }] // требует или запрещает пустую строку между членами класса.
module.exports = {
 root: true,
 env: {
   node: true,//Указание среды Глобальные переменные Node.js и область видимости Node.js. browser- глобальные переменные браузера.
 },
 globals: { 
   var1: "writable",
   var2: "readonly",
   Promise: "off"
 }, // настройка глобальных переменных
 extends: [
   'plugin:vue/strongly-recommended', //Использование общей конфигурации
   '@vue/airbnb',
 ],
 parserOptions: {
   parser: 'babel-eslint',
 },
 rules: {} //Настройка правил
}
Окончательно файл .eslintrc.js выглядит так:
module.exports = {
 root: true,
 env: {
   node: true,
 },
 extends: [
   'plugin:vue/strongly-recommended',
   '@vue/airbnb',
 ],
 parserOptions: {
   parser: 'babel-eslint',
 },
 rules: {
   "vue/html-self-closing": ["error", {
     "html": {
       "void": "never",
       "normal": "always",
       "component": "always"
     },
     "svg": "always",
     "math": "always"
   }],
   'vue/html-indent': [
     'error',
     4,
     {
       attribute: 1,
       baseIndent: 1,
       closeBracket: 0,
       alignAttributesVertically: true,
       ignores: []
     }
   ],
   "vue/max-attributes-per-line": ["error", {
     "singleline": {
       "max": 1
     },
     "multiline": {
       "max": 1
     }
   }],
   'vue/order-in-components': ['error', {
     order: [
       'el',
       'name',
       'key',
       'parent',
       'functional',
       ['delimiters', 'comments'],
       ['components', 'directives', 'filters'],
       'extends',
       'mixins',
       ['provide', 'inject'],
       'ROUTER_GUARDS',
       'layout',
       'middleware',
       'validate',
       'scrollToTop',
       'transition',
       'loading',
       'inheritAttrs',
       'model',
       ['props', 'propsData'],
       'emits',
       'setup',
       'asyncData',
       'data',
       'fetch',
       'head',
       'computed',
       'watch',
       'watchQuery',
       'LIFECYCLE_HOOKS',
       'methods',
       ['template', 'render'],
       'renderError'
     ]
   }],
   "vue/no-irregular-whitespace": ["error", {
     "skipStrings": true,
     "skipComments": false,
     "skipRegExps": false,
     "skipTemplates": false,
     "skipHTMLAttributeValues": false,
     "skipHTMLTextContents": false
   }],
   "vue/component-definition-name-casing": ["error", "PascalCase"],
   "vue/match-component-file-name": ["error", {
     "extensions": ["vue"],
     "shouldMatchCase": false
   }],
   "vue/no-dupe-keys": ["error", {
     "groups": []
   }],
   "vue/component-name-in-template-casing": ["error", "kebab-case", {
     "registeredComponentsOnly": true,
   }],
   'comma-dangle': ['error', {
     arrays: 'never',
     objects: 'never',
     imports: 'never',
     exports: 'never',
     functions: 'never'
   }],
   'linebreak-style': ["error", "windows"], 
   'no-console': 'error',
   'no-debugger': 'error',
   'arrow-parens': ['error', 'as-needed'],
   'no-plusplus': 'off',
   'constructor-super': 'off',
   "no-mixed-operators": [ 
     "error",
     {
       "groups": [
         ["+", "-", "*", "/", "%", "**"],
         ["&", "|", "^", "~", "<<", ">>", ">>>"],
         ["==", "!=", "===", "!==", ">", ">=", "<", "<="],
         ["&&", "||"],
         ["in", "instanceof"]
       ],
       "allowSamePrecedence": true
     }
   ],
   'import/extensions': 'off',
   'import/prefer-default-export': 'off',
   'no-unused-expressions': 'error',
   'no-param-reassign': 'off',
   'prefer-destructuring': ["error", { 
         "array": true,
         "object": true
       }, {
         "enforceForRenamedProperties": false
       }
     ],
   'no-bitwise': ['error', { allow: ['~'] }],
   'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
   'max-len': ['error', { code: 120 }],
   'object-curly-newline': ['error', {
     ObjectExpression: { multiline: true, consistent: true },
     ObjectPattern: { multiline: true, consistent: true }
   }],
   'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }]
 },
};

Рассмотрим на примере одного компонента, что у нас получилось. Для этого возьмем следующий компонент:

<template>
   <div>
       <HelloWorld v-model="headerData" is="header" v-once id="uniqueID" @click="functionCall"
                   v-text="textContent" ref="header" my-prop="prop" v-for="item in items"
       />
   </div>
</template>

<script>
import HelloWorld from './HelloWorld.vue';
export default {
 name: 'MyHeadergа',
 components: {
   HelloWorld
 },
 data() {
   return {
     array: [],
     firstName: "Alex",
     lastName: "Ivanov"
   };
 },
 props: {
   foo: String
 },
 methods: {
   foo() {}
 },
 computed: {
   fullName() {
     return `${this.firstName} ${this.lastName}`;
   },
   reversedArray() {
     return this.array.reverse(); // <- side effect - orginal array is being mutated
   }
 },

};
</script>

После выполнения команды eslint --fix получаем следующее:

Автоматически отформатировалось название компонента template в стиле kebab-case, атрибуты в компонентах выстроены по порядку и каждый с новой строки. Название myProps заменено на my-props в соответствии с кодстайлом vue. Появилась пустая строка 19 между секцией template и script. На 21 строке подсвечивается название компонента которое не совпадает с названием файла. Двойные кавычки при определении строковых переменных заменены на одинарные. Свойства в секции script выстроены по порядку name, components, props, data, computed, method. На строке 40 подсвечивается ошибка, так как мы создаем сайд-эффекты в вычисляемом свойстве.

Заключение

Итак, мы разобрали большое количество правил настройки ESLint, которые можно без труда скорректировать по собственному желанию. Результат в большей степени соответствует рекомендациям по стилю написания кода Vue style guide, а если настроить Auto Fix On Save, то код будет автоматически формироваться согласно этим правилам. Все это в целом приведет к написанию более качественного кода, упрощению и ускорению разработки и код ревью. 

Полезные ссылки:

Спасибо за внимание! Полезные материалы для разработчиков мы также публикуем в наших соцсетях – ВК и Telegram.

Tags:
Hubs:
+10
Comments7

Articles

Information

Website
www.simbirsoft.com
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия