diff --git a/.github/workflows/trigger-docs-update.yml b/.github/workflows/trigger-docs-update.yml new file mode 100644 index 0000000000..4923a4a39d --- /dev/null +++ b/.github/workflows/trigger-docs-update.yml @@ -0,0 +1,35 @@ +name: Trigger Documentation Update + +on: + push: + branches: + - main + paths: + # Trigger when extensions change + - 'packages/super-editor/src/extensions/**/*.js' + # - 'packages/super-editor/src/core/**/*.js' + # - 'packages/super-editor/src/components/**/*.js' + workflow_dispatch: # Manual trigger + +jobs: + trigger-docs: + runs-on: ubuntu-latest + steps: + - name: Trigger Documentation Repository Update + run: | + # Set the required variables + repo_owner="Harbour-Enterprises" + repo_name="SuperDoc-Docs" + event_type="docs-update" + + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.SUPERDOC_PAT }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/$repo_owner/$repo_name/dispatches \ + -d "{\"event_type\": \"$event_type\"}" + + - name: Notify Success + if: success() + run: echo "✅ Documentation update triggered successfully" \ No newline at end of file diff --git a/.gitignore b/.gitignore index fb58e3cbb3..0e7977df61 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ dist-ssr .env .npmrc .eslintcache +types/ npm-debug.log* yarn-debug.log* diff --git a/eslint.config.docs.mjs b/eslint.config.docs.mjs new file mode 100644 index 0000000000..e3e1837d7c --- /dev/null +++ b/eslint.config.docs.mjs @@ -0,0 +1,84 @@ +import jsdoc from 'eslint-plugin-jsdoc'; + +export default [ + { + files: ['packages/super-editor/src/extensions/**/*.js'], + plugins: { + jsdoc + }, + rules: { + // Require minimal JSDoc for extensions + 'jsdoc/require-jsdoc': ['warn', { + publicOnly: true, + require: { + FunctionDeclaration: false, + MethodDefinition: false, + ClassDeclaration: false, + ArrowFunctionExpression: false, + FunctionExpression: false + }, + contexts: [ + // Document the extension module + 'ExportNamedDeclaration > VariableDeclaration > VariableDeclarator > CallExpression[callee.property.name="create"]' + + // Note: We document commands/helpers with @param/@returns + // inside addCommands/addHelpers, not with require-jsdoc + ] + }], + + // When JSDoc exists, validate it's correct + 'jsdoc/require-param': 'error', // All params must be documented + 'jsdoc/require-param-type': 'error', // @param must have {Type} + 'jsdoc/check-param-names': 'error', // @param names must match + 'jsdoc/check-types': 'error', // Valid type syntax (string not String) + + // Optional - we use @returns {Function} or skip it + 'jsdoc/require-returns': 'off', + 'jsdoc/require-returns-type': 'off', + + // Don't require descriptions if obvious + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-returns-description': 'off', + 'jsdoc/require-description': 'off', + + // Don't require examples - nice to have but not essential + 'jsdoc/require-example': 'off', + + // Simple formatting + 'jsdoc/require-hyphen-before-param-description': 'off', // Optional: @param {Type} name - Description + + // Allow types from our .d.ts files and common types + 'jsdoc/no-undefined-types': ['warn', { + definedTypes: [ + // Allow any type that starts with capital letter (likely imported) + '/^[A-Z]/', + + // Common utility types + 'Partial', 'Required', 'Readonly', 'Pick', 'Omit', + + // Built-in types + 'Function', 'Object', 'Array', 'Promise', + + // DOM + 'HTMLElement', 'Element', 'Event' + ] + }], + + // Don't enforce these + 'jsdoc/valid-types': 'off', // We use TypeScript syntax + 'jsdoc/check-tag-names': 'off', // Allow @module, @typedef, etc. + 'jsdoc/check-alignment': 'off', // Don't worry about alignment + 'jsdoc/multiline-blocks': 'off' // Allow single or multi-line + }, + settings: { + jsdoc: { + mode: 'typescript', // Understand TypeScript syntax in JSDoc + preferredTypes: { + object: 'Object', // Use Object not object + array: 'Array', // Use Array not array + 'Array.<>': 'Array<>', // Use Array not Array. + } + } + } + } +]; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 222ad3a6bd..f66c0fa15d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@vuepress/theme-default": "^2.0.0-rc.60", "eslint": "^9.31.0", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jsdoc": "^54.1.0", "husky": "^9.1.7", "lint-staged": "^16.1.4", "prettier": "3.3.3", @@ -503,6 +504,23 @@ "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", "license": "MIT" }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.53.0.tgz", + "integrity": "sha512-Wyed8Wfn3vMNVwrZrgLMxmqwmlcCE1/RfUAOHFzMJb3QLH03mi9Yv1iOCZjif0yx5EZUeJ+17VD1MHPka9IQjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@typescript-eslint/types": "^8.39.1", + "comment-parser": "1.4.1", + "esquery": "^1.6.0", + "jsdoc-type-pratt-parser": "~4.8.0" + }, + "engines": { + "node": ">=20.11.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -2962,6 +2980,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/types": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", + "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -3956,6 +3988,16 @@ "license": "ISC", "optional": true }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -5102,6 +5144,16 @@ "node": ">=20" } }, + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -6703,6 +6755,42 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-jsdoc": { + "version": "54.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-54.1.0.tgz", + "integrity": "sha512-tZJuW6s3gtveVsg08IbJgmfgAA1SpSkEz7KjxPEVmyAO4fPlz7zsMHdxjyn+Zku1l+wejr2JUdTFTNirRgHOrQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.53.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.4.1", + "escape-string-regexp": "^4.0.0", + "espree": "^10.4.0", + "esquery": "^1.6.0", + "parse-imports-exports": "^0.2.4", + "semver": "^7.7.2", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": ">=20.11.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -7712,24 +7800,6 @@ } } }, - "node_modules/git-semver-tags/node_modules/conventional-commits-parser": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.2.0.tgz", - "integrity": "sha512-uLnoLeIW4XaoFtH37qEcg/SXMJmKF4vi7V0H2rnPueg+VEtFGA/asSCNTcq4M/GQ6QmlzchAEtOoDTtKqWeHag==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "meow": "^13.0.0" - }, - "bin": { - "conventional-commits-parser": "dist/cli/index.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/git-up": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/git-up/-/git-up-8.1.1.tgz", @@ -9069,6 +9139,16 @@ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "license": "MIT" }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.8.0.tgz", + "integrity": "sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jsdom": { "version": "25.0.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", @@ -10997,6 +11077,16 @@ "node": ">= 0.10" } }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-statements": "1.0.11" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -11025,6 +11115,13 @@ "protocols": "^2.0.0" } }, + "node_modules/parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true, + "license": "MIT" + }, "node_modules/parse-url": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-9.2.0.tgz", @@ -15864,7 +15961,7 @@ }, "packages/superdoc": { "name": "@harbour-enterprises/superdoc", - "version": "0.15.10-next.4", + "version": "0.15.15-next.3", "license": "AGPL-3.0", "dependencies": { "buffer-crc32": "^1.0.0", diff --git a/package.json b/package.json index 1c4749f016..e290a3e3e1 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "format:check": "prettier --check \"**/*.{js,jsx,vue,css,scss,json,md}\"", "lint": "eslint", "lint:fix": "eslint --fix", + "lint:jsdoc": "eslint packages/super-editor/src/extensions/field-annotation/field-annotation.js --config eslint.config.docs.mjs", + "lint:jsdoc:fix": "eslint packages/super-editor/src/extensions/field-annotation/field-annotation.js --config eslint.config.docs.mjs --fix", "docs:dev": "vuepress dev docs", "docs:build": "vuepress build docs", "pack": "npm run build:super-editor && npm --prefix ./packages/superdoc run pack", @@ -54,6 +56,7 @@ "@vuepress/theme-default": "^2.0.0-rc.60", "eslint": "^9.31.0", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jsdoc": "^54.1.0", "husky": "^9.1.7", "lint-staged": "^16.1.4", "prettier": "3.3.3", diff --git a/packages/super-editor/package.json b/packages/super-editor/package.json index 0f72efc18b..97c1933efd 100644 --- a/packages/super-editor/package.json +++ b/packages/super-editor/package.json @@ -38,11 +38,13 @@ "module": "./dist/super-editor.es.js", "scripts": { "dev": "vite", - "preview": "vite preview", - "build": "vite build && tsc", - "clean": "rm -rf dist", + "build": "vite build && npm run types:build", + "build:watch": "vite build --watch", + "types:check": "tsc --noEmit", + "types:build": "tsc -p tsconfig.build.json", "test": "vitest", - "build:watch": "vite build --watch --logLevel warn", + "preview": "vite preview", + "clean": "rm -rf dist types", "pack": "rm *.tgz 2>/dev/null || true && npm run build && npm pack && mv harbour-enterprises-super-editor-0.0.1-alpha.0.tgz ./super-editor.tgz" }, "dependencies": { diff --git a/packages/super-editor/src/core/Editor.js b/packages/super-editor/src/core/Editor.js index 38725d3647..ea4870b994 100644 --- a/packages/super-editor/src/core/Editor.js +++ b/packages/super-editor/src/core/Editor.js @@ -365,7 +365,7 @@ export class Editor extends EventEmitter { // it will be in itialized via this.#onCollaborationReady if (!this.options.ydoc) { if (!this.options.isChildEditor) { - this.initPagination(); + this.#initPagination(); this.#initComments(); this.#validateDocumentInit(); @@ -679,7 +679,7 @@ export class Editor extends EventEmitter { this.view.dispatch(tr); setTimeout(() => { - this.initPagination(); + this.#initPagination(); this.#initComments(); }, 50); } @@ -1257,7 +1257,7 @@ export class Editor extends EventEmitter { this.view.dispatch(tr); if (!this.options.isNewFile) { - this.initPagination(); + this.#initPagination(); this.#initComments(); updateYdocDocxData(this); } @@ -1285,11 +1285,10 @@ export class Editor extends EventEmitter { /** * Initialize pagination, if the pagination extension is enabled. - * @private * @async * @returns {Promise} */ - async initPagination() { + async #initPagination() { if (this.options.isHeadless || !this.extensionService || this.options.isHeaderOrFooter) { return; } @@ -1771,7 +1770,7 @@ export class Editor extends EventEmitter { } if (!this.options.ydoc) { - this.initPagination(); + this.#initPagination(); this.#initComments(); } } diff --git a/packages/super-editor/src/core/InputRule.js b/packages/super-editor/src/core/InputRule.js index 124832b7d6..93f1cf6cf8 100644 --- a/packages/super-editor/src/core/InputRule.js +++ b/packages/super-editor/src/core/InputRule.js @@ -293,7 +293,7 @@ export function handleHtmlPaste(html, editor) { * converting em units to pt and removing unnecessary tags. * @param {String} html The HTML string to be processed. * @param {Editor} editor The editor instance. - * @returns {String} The processed HTML string. + * @returns {DocumentFragment} The processed HTML string. */ export function htmlHandler(html, editor) { const flatHtml = flattenListsInHtml(html, editor); diff --git a/packages/super-editor/src/extensions/structured-content/document-section.js b/packages/super-editor/src/extensions/structured-content/document-section.js index 7122dc6551..adc602d80c 100644 --- a/packages/super-editor/src/extensions/structured-content/document-section.js +++ b/packages/super-editor/src/extensions/structured-content/document-section.js @@ -1,3 +1,36 @@ +// @ts-check + +/** + * Base section attributes + * @typedef {Object} SectionAttributes + * @property {number} [id] - Section identifier + * @property {string} [title] - Section title + * @property {string} [description] - Section description + * @property {string} [sectionType] - Type of section + * @property {boolean} [isLocked] - Whether section is locked + */ + +/** + * Options for creating a section + * @typedef {Object} SectionCreate + * @property {number} [id] - Section identifier (auto-generated if not provided) + * @property {string} [title] - Section title (defaults to "Document section") + * @property {string} [description] - Section description + * @property {string} [sectionType] - Type of section + * @property {boolean} [isLocked] - Whether section is locked + * @property {string} [html] - HTML content to parse + * @property {Object} [json] - ProseMirror JSON content (takes precedence over html) + */ + +/** + * Options for updating a section + * @typedef {Object} SectionUpdate + * @property {number} id - Section ID to update (required) + * @property {string} [html] - HTML content to parse + * @property {Object} [json] - ProseMirror JSON content (takes precedence over html) + * @property {Partial} [attrs] - Attributes to update + */ + import { Node, Attribute } from '@core/index.js'; import { DocumentSectionView } from './document-section/DocumentSectionView.js'; import { htmlHandler } from '@core/InputRule.js'; @@ -5,6 +38,10 @@ import { Selection } from 'prosemirror-state'; import { DOMParser as PMDOMParser } from 'prosemirror-model'; import { findParentNode, SectionHelpers } from '@helpers/index.js'; +/** + * Document Section - Structured content blocks + * @module DocumentSection + */ export const DocumentSection = Node.create({ name: 'documentSection', group: 'block', @@ -53,12 +90,10 @@ export const DocumentSection = Node.create({ addCommands() { return { /** - * Create a new structured content block - * You can pass in options like title, description, html, or json. - * If html is provided, it will be parsed and converted to ProseMirror nodes. - * If json is provided, it will be used to create the content of the block. - * @param {Object} params - The command parameters - * @returns {boolean} Returns true if the command was executed successfully + * Create a new document section + * @category Command + * @param {SectionCreate} [options={}] + * @returns {Function} Command function - returns true if section was created successfully */ createDocumentSection: (options = {}) => @@ -171,11 +206,9 @@ export const DocumentSection = Node.create({ }, /** - * Remove the structured content block at the current selection, retaining contents. - * This will remove the block node but keep its content in the document. - * If the selection is not within a structured content block, it does nothing. - * @param {Object} params - The command parameters - * @returns {boolean} Returns true if the command was executed successfully + * Remove section at current selection, keeping content + * @category Command + * @returns {Function} Command function - returns true if section was removed, false if none found */ removeSectionAtSelection: () => @@ -213,9 +246,10 @@ export const DocumentSection = Node.create({ }, /** - * Remove a document section by its ID. - * @param {string} id - The ID of the section to remove - * @returns {Function} A command function that takes the editor state and dispatch function + * Remove section by ID + * @category Command + * @param {number} id - Section ID to remove + * @returns {Function} Command function - returns true if section was removed, false if not found */ removeSectionById: (id) => @@ -240,10 +274,11 @@ export const DocumentSection = Node.create({ }, /** - * Lock a document section by its ID. - * This command is a placeholder and does not perform any action yet. - * @param {string} id - The ID of the section to lock - * @returns {Function} A command function that takes the editor state and dispatch function + * Lock section by ID + * @category Command + * @param {number} id - Section ID to lock + * @returns {Function} Command function - returns true if section was locked, false if not found + * @private */ lockSectionById: (id) => @@ -263,20 +298,13 @@ export const DocumentSection = Node.create({ }, /** - * Update a document section by its ID. - * You can pass in options like id, html, json, or attrs. - * The attrs include json, title, description, etc. - * If html is provided, it will be parsed and converted to ProseMirror nodes - * If json is provided, it will be used to create the content of the block. - * @param {Object} params - The command parameters - * @param {string} params.id - The ID of the section to update - * @param {string} [params.html] - The HTML content to set for the section - * @param {Object} [params.json] - The JSON content to set for the section - * @param {Object} [params.attrs] - Additional attributes to update (e.g., title, description) - * @returns {Function} A command function that takes the editor state and dispatch function + * Update section by ID + * @category Command + * @param {SectionUpdate} options + * @returns {Function} Command function - returns true if section was updated, false if not found */ updateSectionById: - ({ id, html, json, attrs } = {}) => + ({ id, html, json, attrs }) => ({ tr, dispatch, editor }) => { const sections = SectionHelpers.getAllSections(editor || this.editor); const sectionToUpdate = sections.find(({ node }) => node.attrs.id === id); diff --git a/packages/super-editor/tsconfig.build.json b/packages/super-editor/tsconfig.build.json new file mode 100644 index 0000000000..b0f8b21b73 --- /dev/null +++ b/packages/super-editor/tsconfig.build.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "checkJs": false + } +} diff --git a/packages/super-editor/tsconfig.json b/packages/super-editor/tsconfig.json index fc72b654a6..1bf61cc21f 100644 --- a/packages/super-editor/tsconfig.json +++ b/packages/super-editor/tsconfig.json @@ -1,14 +1,26 @@ { - "include": ["./src/**/*"], "compilerOptions": { + "checkJs": true, "allowJs": true, + "moduleResolution": "node", + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "skipLibCheck": true, "declaration": true, "emitDeclarationOnly": true, "outDir": "dist", - "declarationMap": true, - "skipLibCheck": true, - "module": "NodeNext", - "moduleResolution": "NodeNext", - "typeRoots": ["./node_modules/@types/*"] - } + "baseUrl": ".", + "paths": { + "@": ["./src/*"], + "@core/*": ["./src/core/*"], + "@extensions/*": ["./src/extensions/*"], + "@features/*": ["./src/features/*"], + "@components/*": ["./src/components/*"], + "@helpers/*": ["./src/core/helpers/*"], + "@packages/*": ["../*"], + "@converter/*": ["./src/core/super-converter/*"], + "@tests/*": ["./src/tests/*"] + } + }, + "exclude": ["node_modules", "dist", "**/*.test.js"] }