e22m4u 3 дней назад
Родитель
Сommit
524ac0a744
15 измененных файлов с 239 добавлено и 52 удалено
  1. 4 0
      .mocharc.cjs
  2. 0 4
      .mocharc.json
  3. 2 2
      README.md
  4. 4 1
      build-cjs.js
  5. 7 9
      dist/cjs/index.cjs
  6. 1 12
      eslint.config.js
  7. 13 11
      package.json
  8. 56 0
      src/create-spies-group.d.ts
  9. 3 6
      src/create-spies-group.js
  10. 110 0
      src/create-spy.d.ts
  11. 5 5
      src/create-spy.js
  12. 0 2
      src/create-spy.spec.js
  13. 2 0
      src/index.d.ts
  14. 18 0
      src/types.d.ts
  15. 14 0
      tsconfig.json

+ 4 - 0
.mocharc.cjs

@@ -0,0 +1,4 @@
+module.exports = {
+  extension: ['js'],
+  spec: 'src/**/*.spec.js',
+}

+ 0 - 4
.mocharc.json

@@ -1,4 +0,0 @@
-{
-  "extension": ["js"],
-  "spec": "src/**/*.spec.js"
-}

+ 2 - 2
README.md

@@ -36,12 +36,12 @@ npm install @e22m4u/js-spy
 
 
 *ESM*
 *ESM*
 ```js
 ```js
-import {createSpy} from '@e22m4u/js-spy';
+import {createSpy, createSpiesGroup} from '@e22m4u/js-spy';
 ```
 ```
 
 
 *CommonJS*
 *CommonJS*
 ```js
 ```js
-const {createSpy} = require('@e22m4u/js-spy');
+const {createSpy, createSpiesGroup} = require('@e22m4u/js-spy');
 ```
 ```
 
 
 ## Использование
 ## Использование

+ 4 - 1
build-cjs.js

@@ -1,12 +1,15 @@
+import {rmSync} from 'node:fs';
 import * as esbuild from 'esbuild';
 import * as esbuild from 'esbuild';
 import packageJson from './package.json' with {type: 'json'};
 import packageJson from './package.json' with {type: 'json'};
 
 
+rmSync('./dist/cjs', {recursive: true, force: true});
+
 await esbuild.build({
 await esbuild.build({
   entryPoints: ['src/index.js'],
   entryPoints: ['src/index.js'],
   outfile: 'dist/cjs/index.cjs',
   outfile: 'dist/cjs/index.cjs',
   format: 'cjs',
   format: 'cjs',
   platform: 'node',
   platform: 'node',
-  target: ['node12'],
+  target: ['node18'],
   bundle: true,
   bundle: true,
   keepNames: true,
   keepNames: true,
   external: [
   external: [

+ 7 - 9
dist/cjs/index.cjs

@@ -171,7 +171,10 @@ function createSpy(target = void 0, methodNameOrImpl = void 0, customImplForMeth
 __name(createSpy, "createSpy");
 __name(createSpy, "createSpy");
 
 
 // src/create-spies-group.js
 // src/create-spies-group.js
-var _SpiesGroup = class _SpiesGroup {
+var SpiesGroup = class {
+  static {
+    __name(this, "SpiesGroup");
+  }
   /**
   /**
    * Constructor.
    * Constructor.
    */
    */
@@ -182,10 +185,9 @@ var _SpiesGroup = class _SpiesGroup {
    * Создает шпиона для отдельной функции
    * Создает шпиона для отдельной функции
    * или метода объекта и добавляет его в группу.
    * или метода объекта и добавляет его в группу.
    *
    *
-   * @param {Function|object} target
-   * @param {Function|string} [methodNameOrImpl]
-   * @param {Function} [customImplForMethod]
-   * @returns {Function}
+   * @param target
+   * @param methodNameOrImpl
+   * @param customImplForMethod
    */
    */
   on(target, methodNameOrImpl, customImplForMethod) {
   on(target, methodNameOrImpl, customImplForMethod) {
     const spy = createSpy(target, methodNameOrImpl, customImplForMethod);
     const spy = createSpy(target, methodNameOrImpl, customImplForMethod);
@@ -197,8 +199,6 @@ var _SpiesGroup = class _SpiesGroup {
    * для которых были созданы шпионы в этой группе,
    * для которых были созданы шпионы в этой группе,
    * и сброс истории вызовов для всех шпионов в группе.
    * и сброс истории вызовов для всех шпионов в группе.
    * Очищает внутренний список шпионов.
    * Очищает внутренний список шпионов.
-   *
-   * @returns {SpiesGroup}
    */
    */
   restore() {
   restore() {
     this.spies.forEach((spy) => spy.restore());
     this.spies.forEach((spy) => spy.restore());
@@ -206,8 +206,6 @@ var _SpiesGroup = class _SpiesGroup {
     return this;
     return this;
   }
   }
 };
 };
-__name(_SpiesGroup, "SpiesGroup");
-var SpiesGroup = _SpiesGroup;
 function createSpiesGroup() {
 function createSpiesGroup() {
   return new SpiesGroup();
   return new SpiesGroup();
 }
 }

+ 1 - 12
eslint.config.js

@@ -1,8 +1,6 @@
 import globals from 'globals';
 import globals from 'globals';
 import eslintJs from '@eslint/js';
 import eslintJs from '@eslint/js';
-import eslintJsdocPlugin from 'eslint-plugin-jsdoc';
 import eslintMochaPlugin from 'eslint-plugin-mocha';
 import eslintMochaPlugin from 'eslint-plugin-mocha';
-import eslintImportPlugin from 'eslint-plugin-import';
 import eslintPrettierConfig from 'eslint-config-prettier';
 import eslintPrettierConfig from 'eslint-config-prettier';
 import eslintChaiExpectPlugin from 'eslint-plugin-chai-expect';
 import eslintChaiExpectPlugin from 'eslint-plugin-chai-expect';
 
 
@@ -16,25 +14,16 @@ export default [{
     },
     },
   },
   },
   plugins: {
   plugins: {
-    'jsdoc': eslintJsdocPlugin,
     'mocha': eslintMochaPlugin,
     'mocha': eslintMochaPlugin,
-    'import': eslintImportPlugin,
     'chai-expect': eslintChaiExpectPlugin,
     'chai-expect': eslintChaiExpectPlugin,
   },
   },
   rules: {
   rules: {
     ...eslintJs.configs.recommended.rules,
     ...eslintJs.configs.recommended.rules,
     ...eslintPrettierConfig.rules,
     ...eslintPrettierConfig.rules,
-    ...eslintImportPlugin.flatConfigs.recommended.rules,
     ...eslintMochaPlugin.configs.recommended.rules,
     ...eslintMochaPlugin.configs.recommended.rules,
     ...eslintChaiExpectPlugin.configs['recommended-flat'].rules,
     ...eslintChaiExpectPlugin.configs['recommended-flat'].rules,
-    ...eslintJsdocPlugin.configs['flat/recommended-error'].rules,
+    "no-unused-vars": ["error", {caughtErrors: "none"}],
     'no-duplicate-imports': 'error',
     'no-duplicate-imports': 'error',
-    'jsdoc/reject-any-type': 0,
-    'jsdoc/reject-function-type': 0,
-    'jsdoc/require-param-description': 0,
-    'jsdoc/require-returns-description': 0,
-    'jsdoc/require-property-description': 0,
-    'jsdoc/tag-lines': ['error', 'any', {startLines: 1}],
   },
   },
   files: ['src/**/*.js'],
   files: ['src/**/*.js'],
 }];
 }];

+ 13 - 11
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@e22m4u/js-spy",
   "name": "@e22m4u/js-spy",
-  "version": "0.3.0",
+  "version": "0.2.0",
   "description": "Утилита слежения за вызовом функций и методов для JavaScript",
   "description": "Утилита слежения за вызовом функций и методов для JavaScript",
   "author": "Mikhail Evstropov <e22m4u@yandex.ru>",
   "author": "Mikhail Evstropov <e22m4u@yandex.ru>",
   "license": "MIT",
   "license": "MIT",
@@ -10,47 +10,49 @@
     "test",
     "test",
     "unit"
     "unit"
   ],
   ],
-  "homepage": "https://gitrepos.ru/e22m4u/js-spy",
+  "homepage": "https://github.com/e22m4u/js-spy",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
-    "url": "git+https://gitrepos.ru/e22m4u/js-spy.git"
+    "url": "git+https://github.com/e22m4u/js-spy.git"
   },
   },
   "type": "module",
   "type": "module",
+  "types": "./src/index.d.ts",
   "module": "./src/index.js",
   "module": "./src/index.js",
   "main": "./dist/cjs/index.cjs",
   "main": "./dist/cjs/index.cjs",
   "exports": {
   "exports": {
+    "types": "./src/index.d.ts",
     "import": "./src/index.js",
     "import": "./src/index.js",
     "require": "./dist/cjs/index.cjs"
     "require": "./dist/cjs/index.cjs"
   },
   },
   "engines": {
   "engines": {
-    "node": ">=12"
+    "node": ">=18"
   },
   },
   "scripts": {
   "scripts": {
-    "lint": "eslint ./src",
-    "lint:fix": "eslint ./src --fix",
+    "lint": "tsc && eslint ./src",
+    "lint:fix": "tsc && eslint ./src --fix",
     "format": "prettier --write \"./src/**/*.js\"",
     "format": "prettier --write \"./src/**/*.js\"",
     "test": "npm run lint && c8 --reporter=text-summary mocha",
     "test": "npm run lint && c8 --reporter=text-summary mocha",
     "test:coverage": "npm run lint && c8 --reporter=text mocha",
     "test:coverage": "npm run lint && c8 --reporter=text mocha",
-    "build:cjs": "rimraf ./dist/cjs && node build-cjs.js",
+    "build:cjs": "node --no-warnings=ExperimentalWarning build-cjs.js",
     "prepare": "husky"
     "prepare": "husky"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@commitlint/cli": "~20.1.0",
     "@commitlint/cli": "~20.1.0",
     "@commitlint/config-conventional": "~20.0.0",
     "@commitlint/config-conventional": "~20.0.0",
     "@eslint/js": "~9.39.1",
     "@eslint/js": "~9.39.1",
+    "@types/chai": "~5.2.3",
+    "@types/mocha": "~10.0.10",
     "c8": "~10.1.3",
     "c8": "~10.1.3",
     "chai": "~6.2.1",
     "chai": "~6.2.1",
     "esbuild": "~0.27.0",
     "esbuild": "~0.27.0",
     "eslint": "~9.39.1",
     "eslint": "~9.39.1",
     "eslint-config-prettier": "~10.1.8",
     "eslint-config-prettier": "~10.1.8",
     "eslint-plugin-chai-expect": "~3.1.0",
     "eslint-plugin-chai-expect": "~3.1.0",
-    "eslint-plugin-import": "^2.32.0",
-    "eslint-plugin-jsdoc": "^61.4.1",
     "eslint-plugin-mocha": "~11.2.0",
     "eslint-plugin-mocha": "~11.2.0",
     "globals": "~16.5.0",
     "globals": "~16.5.0",
     "husky": "~9.1.7",
     "husky": "~9.1.7",
     "mocha": "~11.7.5",
     "mocha": "~11.7.5",
-    "prettier": "~3.7.3",
-    "rimraf": "~6.1.2"
+    "prettier": "~3.6.2",
+    "typescript": "~5.9.3"
   }
   }
 }
 }

+ 56 - 0
src/create-spies-group.d.ts

@@ -0,0 +1,56 @@
+import {Spy} from './create-spy.js';
+import {MethodKey, AnyCallable} from './types.js';
+
+/**
+ * Представляет группу шпионов, позволяющую
+ * управлять ими коллективно.
+ */
+export declare class SpiesGroup {
+  /**
+   * Внутренний массив, хранящий все шпионы,
+   * созданные в этой группе. Не предназначен
+   * для прямого доступа.
+   */
+  spies: Spy<any>[];
+
+  /**
+   * Создает шпиона для отдельной функции
+   * и добавляет его в группу.
+   *
+   * @param target
+   * @param customImpl
+   */
+  on<TFunc extends AnyCallable>(
+    target: TFunc,
+    customImpl?: TFunc,
+  ): Spy<TFunc>;
+
+  /**
+   * Создает шпиона для метода объекта, добавляет
+   * его в группу и заменяет оригинальный метод
+   * объекта шпионом.
+   *
+   * @param target
+   * @param methodName
+   * @param customImpl
+   */
+  on<TObj extends object, K extends MethodKey<TObj>>(
+    target: TObj,
+    methodName: K,
+    customImpl?: TObj[K],
+  ): Spy<Extract<TObj[K], AnyCallable>>;
+
+  /**
+   * Восстановление всех оригинальных методов объектов,
+   * для которых были созданы шпионы в этой группе,
+   * и сброс истории вызовов для всех шпионов в группе.
+   * Очищает внутренний список шпионов.
+   */
+  restore(): this;
+}
+
+/**
+ * Фабричная функция для создания
+ * нового экземпляра `SpiesGroup`.
+ */
+export function createSpiesGroup(): SpiesGroup;

+ 3 - 6
src/create-spies-group.js

@@ -16,10 +16,9 @@ export class SpiesGroup {
    * Создает шпиона для отдельной функции
    * Создает шпиона для отдельной функции
    * или метода объекта и добавляет его в группу.
    * или метода объекта и добавляет его в группу.
    *
    *
-   * @param {Function|object} target
-   * @param {Function|string} [methodNameOrImpl]
-   * @param {Function} [customImplForMethod]
-   * @returns {Function}
+   * @param target
+   * @param methodNameOrImpl
+   * @param customImplForMethod
    */
    */
   on(target, methodNameOrImpl, customImplForMethod) {
   on(target, methodNameOrImpl, customImplForMethod) {
     const spy = createSpy(target, methodNameOrImpl, customImplForMethod);
     const spy = createSpy(target, methodNameOrImpl, customImplForMethod);
@@ -32,8 +31,6 @@ export class SpiesGroup {
    * для которых были созданы шпионы в этой группе,
    * для которых были созданы шпионы в этой группе,
    * и сброс истории вызовов для всех шпионов в группе.
    * и сброс истории вызовов для всех шпионов в группе.
    * Очищает внутренний список шпионов.
    * Очищает внутренний список шпионов.
-   *
-   * @returns {SpiesGroup}
    */
    */
   restore() {
   restore() {
     this.spies.forEach(spy => spy.restore());
     this.spies.forEach(spy => spy.restore());

+ 110 - 0
src/create-spy.d.ts

@@ -0,0 +1,110 @@
+import {AnyCallable, MethodKey} from './types.js';
+
+/**
+ * Информация о единичном вызове
+ * отслеживаемой функции.
+ *
+ * @template Args
+ * @template Return
+ */
+export interface CallInfo<Args extends any[] = any[], Return = any> {
+  /**
+   * Аргументы, с которыми был вызван шпион.
+   */
+  readonly args: Args;
+
+  /**
+   * Контекст this, с которым был вызван шпион.
+   */
+  readonly thisArg: any;
+
+  /**
+   * Значение, возвращенное шпионом.
+   * (undefined, если шпион выбросил ошибку)
+   */
+  readonly returnValue: Return | undefined;
+
+  /**
+   * Ошибка, выброшенная шпионом.
+   * (undefined, если шпион не выбросил ошибку)
+   */
+  readonly error: unknown | undefined;
+}
+
+/**
+ * Представляет функцию-шпиона, созданную `createSpy`.
+ * Это вызываемая функция, которая также имеет свойства
+ * и методы для инспекции вызовов.
+ *
+ * @template TFunc
+ */
+export interface Spy<TFunc extends AnyCallable = AnyCallable> {
+  /**
+   * Сама функция-шпион.
+   */
+  (...args: Parameters<TFunc>): ReturnType<TFunc>;
+
+  /**
+   * Вызовы шпиона.
+   *
+   * @readonly
+   */
+  readonly calls: CallInfo[];
+
+  /**
+   * Количество вызовов шпиона.
+   *
+   * @readonly
+   */
+  readonly callCount: number;
+
+  /**
+   * Булево значение, указывающее,
+   * был ли шпион вызван хотя бы один раз.
+   *
+   * @readonly
+   */
+  readonly isCalled: boolean;
+
+  /**
+   * Восстанавливает оригинальный метод,
+   * если шпион был создан для метода объекта.
+   * Ничего не делает, если шпион был создан
+   * для отдельной функции.
+   */
+  restore(): void;
+}
+
+/**
+ * Создает шпиона.
+ */
+export declare function createSpy(): Spy<(...args: any[]) => void>;
+
+/**
+ * Создает шпиона для отдельной функции.
+ *
+ * @param targetFn
+ * @param customImpl
+ */
+export declare function createSpy<TFunc extends AnyCallable>(
+  targetFn: TFunc,
+  customImpl?: TFunc,
+): Spy<TFunc>;
+
+/**
+ * Создание шпиона для метода объекта. Оригинальный метод
+ * объекта будет заменен шпионом. Используйте `spy.restore()`
+ * для восстановления оригинального метода.
+ *
+ * @param targetObject
+ * @param methodName
+ * @param customImpl
+ */
+export declare function createSpy<
+  TObj extends object,
+  K extends MethodKey<TObj>,
+>(
+  targetObject: TObj,
+  methodName: K,
+  customImpl?: TObj[K],
+): Spy<Extract<TObj[K], AnyCallable>>;

+ 5 - 5
src/create-spy.js

@@ -2,8 +2,8 @@
  * Вспомогательная функция для разбора аргументов createSpy.
  * Вспомогательная функция для разбора аргументов createSpy.
  *
  *
  * @param {Function|object} target
  * @param {Function|object} target
- * @param {Function|string} [methodNameOrImpl]
- * @param {Function} [customImplForMethod]
+ * @param {Function|string|undefined} methodNameOrImpl
+ * @param {Function|undefined} customImplForMethod
  * @returns {object}
  * @returns {object}
  */
  */
 function _parseSpyArgs(target, methodNameOrImpl, customImplForMethod) {
 function _parseSpyArgs(target, methodNameOrImpl, customImplForMethod) {
@@ -142,9 +142,9 @@ function _parseSpyArgs(target, methodNameOrImpl, customImplForMethod) {
  * Шпионить за методом объекта:
  * Шпионить за методом объекта:
  * createSpy(targetObject, methodName, [customImplementation])
  * createSpy(targetObject, methodName, [customImplementation])
  *
  *
- * @param {Function|object} [target]
- * @param {Function|string} [methodNameOrImpl]
- * @param {Function} [customImplForMethod]
+ * @param {Function|object|undefined} target
+ * @param {Function|string|undefined} methodNameOrImpl
+ * @param {Function|undefined} customImplForMethod
  * @returns {Function}
  * @returns {Function}
  */
  */
 export function createSpy(
 export function createSpy(

+ 0 - 2
src/create-spy.spec.js

@@ -157,7 +157,6 @@ describe('createSpy', function () {
       // определение объекта с методом, который
       // определение объекта с методом, который
       // будет использоваться как target функция
       // будет использоваться как target функция
       const contextObj = {val: 10};
       const contextObj = {val: 10};
-      // eslint-disable-next-line jsdoc/require-jsdoc
       function originalFn() {
       function originalFn() {
         return this.val;
         return this.val;
       }
       }
@@ -178,7 +177,6 @@ describe('createSpy', function () {
       // использующей контекст this
       // использующей контекст this
       const contextObj = {val: 20};
       const contextObj = {val: 20};
       const originalFn = () => {};
       const originalFn = () => {};
-      // eslint-disable-next-line jsdoc/require-jsdoc
       function customImpl() {
       function customImpl() {
         return this.val;
         return this.val;
       }
       }

+ 2 - 0
src/index.d.ts

@@ -0,0 +1,2 @@
+export * from './create-spy.js';
+export * from './create-spies-group.js';

+ 18 - 0
src/types.d.ts

@@ -0,0 +1,18 @@
+/**
+ * Тип любой функции.
+ */
+export type AnyCallable = (...args: any[]) => any;
+
+/**
+ * Конструктор любой ошибки.
+ */
+export type AnyErrorCtor = (new (...args: any[]) => Error);
+
+/**
+ * Ключ любого метода в объекте.
+ * Должен быть ключом `TObj`, значение
+ * которого является функцией.
+ */
+export type MethodKey<TObj extends object> = {
+  [P in keyof TObj]: TObj[P] extends AnyCallable ? P : never
+}[keyof TObj];

+ 14 - 0
tsconfig.json

@@ -0,0 +1,14 @@
+{
+  "compilerOptions": {
+    "noEmit": true,
+    "target": "es2022",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",
+    "allowJs": true,
+    "checkJs": true
+  },
+  "include": [
+    "./src/**/*.ts",
+    "./src/**/*.js"
+  ]
+}