diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1552bc1..1d92241 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,6 +35,9 @@ jobs: - name: Build cosmoscopes run: sh ./e2e/exec-modelize.sh + - name: Lint + run: npm run lint + - name: Run unit tests run: npm run test:unit -- --verbose diff --git a/controllers/autorecord.js b/controllers/autorecord.js index 9fd6e56..58ce1a3 100644 --- a/controllers/autorecord.js +++ b/controllers/autorecord.js @@ -27,13 +27,14 @@ function autorecord(title = '', type = 'undefined', tags = '', saveIdOnYmlFrontM return; } + let shouldSaveId; if (config.opts['generate_id'] === 'never') { - saveIdOnYmlFrontMatter = false; + shouldSaveId = false; } else { - saveIdOnYmlFrontMatter = config.opts['generate_id'] === 'always' || !!saveIdOnYmlFrontMatter; + shouldSaveId = config.opts['generate_id'] === 'always' || Boolean(saveIdOnYmlFrontMatter); } - createRecord(title, type, tags, config, saveIdOnYmlFrontMatter); + createRecord(title, type, tags, config, shouldSaveId); } export default autorecord; diff --git a/controllers/batch.js b/controllers/batch.js index c6caf97..edc489e 100644 --- a/controllers/batch.js +++ b/controllers/batch.js @@ -19,14 +19,16 @@ async function batch(filePath, saveIdOnYmlFrontMatter) { const config = Config.get(Config.configFilePath); console.log(config.getConfigConsolMessage()); + let shouldSaveId; if (config.opts['generate_id'] === 'never') { - saveIdOnYmlFrontMatter = false; + shouldSaveId = false; } else { - saveIdOnYmlFrontMatter = config.opts['generate_id'] === 'always' || !!saveIdOnYmlFrontMatter; + shouldSaveId = config.opts['generate_id'] === 'always' || Boolean(saveIdOnYmlFrontMatter); } if (fs.existsSync(filePath) === false) { - return console.error(['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'Data file does not exist.'); + console.error(['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'Data file does not exist.'); + return; } const files = await findMarkdownFilesRecursively(config.opts['files_origin']); @@ -35,8 +37,8 @@ async function batch(filePath, saveIdOnYmlFrontMatter) { const timestamps = [todayMaxTimestamp]; await Promise.all( - files.map(async (filePath) => { - const content = await fsPromises.readFile(filePath, 'utf8'); + files.map(async (mdFile) => { + const content = await fsPromises.readFile(mdFile, 'utf8'); const record = Record.recordFromFile(content, config); if (isTimestampIncrement(record.id)) { timestamps.push(record.id); @@ -51,62 +53,62 @@ async function batch(filePath, saveIdOnYmlFrontMatter) { /** @type {Record[]} */ let records = []; - fs.readFile(filePath, 'utf-8', async (err, data) => { + fs.readFile(filePath, 'utf-8', async (err, rawData) => { if (err) { - return console.error(['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'Cannot read data file.'); + console.error(['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'Cannot read data file.'); + return; } + let parsedData; + switch (path.extname(filePath)) { case '.json': try { - data = JSON.parse(data); - } catch (error) { - return console.error( - ['\x1b[31m', 'Err.', '\x1b[0m'].join(''), - 'JSON data file is invalid.', - ); + parsedData = JSON.parse(rawData); + } catch (_error) { + console.error(['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'JSON data file is invalid.'); + return; } break; case '.csv': try { - data = parse(data, { + parsedData = parse(rawData, { columns: true, skip_empty_lines: true, cast: (value) => (value === '' ? undefined : value), }); - } catch (error) { - return console.error( - ['\x1b[31m', 'Err.', '\x1b[0m'].join(''), - 'CSV data file is invalid.', - ); + } catch (_error) { + console.error(['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'CSV data file is invalid.'); + return; } break; default: - return console.error( + console.error( ['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'Data file format unrecognized. Supported file extensions: .json, .csv.', ); + return; } - if (!Array.isArray(data)) { + if (!Array.isArray(parsedData)) { throw new Error('Batch data should be array'); } - records = data.map((e, i) => { - e = formatAsRecord(e, config); - return Record.recordWithIncrementedTimestamp(e, config, increment + i); + records = parsedData.map((entry, i) => { + const formatted = formatAsRecord(entry, config); + return Record.recordWithIncrementedTimestamp(formatted, config, increment + i); }); await Promise.all( records.map(async (record) => { - const filePath = path.join(config.opts['files_origin'], record.getFileName()); - if (fs.existsSync(filePath)) { - throw new Error(`File ${filePath} already exist`); + const recordPath = path.join(config.opts['files_origin'], record.getFileName()); + if (fs.existsSync(recordPath)) { + throw new Error(`File ${recordPath} already exist`); } - await fsPromises.writeFile(filePath, record.getFileContent(saveIdOnYmlFrontMatter)); + await fsPromises.writeFile(recordPath, record.getFileContent(shouldSaveId)); }), ); diff --git a/controllers/config.js b/controllers/config.js index 30ba71b..99603aa 100644 --- a/controllers/config.js +++ b/controllers/config.js @@ -12,32 +12,34 @@ import slugify from '../core/utils/slugify.js'; */ function makeConfigFile(title, { global: isGlobal }) { - isGlobal = !!isGlobal; + const globalFlag = Boolean(isGlobal); // Early validations - if (isGlobal && fs.existsSync(Config.configDirPath) === false) { - return console.log( + if (globalFlag && fs.existsSync(Config.configDirPath) === false) { + console.log( ['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'To create global configuration files, first create a user data directory by running', ['\x1b[1m', 'cosma --create-user-data-dir', '\x1b[0m'].join(''), '.', ); + return; } - if (process.cwd() === Config.configDirPath && isGlobal === false) { - return console.log( + if (process.cwd() === Config.configDirPath && globalFlag === false) { + console.log( ['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'Cannot create a local config file in the global config directory.', 'To create a global config file, use "cosma config --global".', ); + return; } const defaultConfigExists = Config.defaultConfigExists(); - const hasTitle = !!title; + const hasTitle = Boolean(title); let opts; let configSource; - if (isGlobal && !hasTitle) { + if (globalFlag && !hasTitle) { opts = Config.base; configSource = 'base'; } else if (defaultConfigExists) { @@ -52,10 +54,10 @@ function makeConfigFile(title, { global: isGlobal }) { let configFilePath; let configScope; - if (isGlobal && hasTitle) { + if (globalFlag && hasTitle) { configFilePath = path.join(Config.configDirPath, slugify(title) + '.yml'); configScope = 'global'; - } else if (isGlobal && !hasTitle) { + } else if (globalFlag && !hasTitle) { configFilePath = Config.defaultConfigPath; configScope = 'global default'; } else { diff --git a/controllers/config.spec.js b/controllers/config.spec.js index 6dfa25c..530389c 100644 --- a/controllers/config.spec.js +++ b/controllers/config.spec.js @@ -18,8 +18,8 @@ const mockConfigGet = jest.fn(() => ({ opts, getYaml: mockGetYaml })); const mockDefaultConfigExists = jest.fn(() => false); jest.mock('../core/models/config.js', () => ({ - get: (path) => mockConfigGet(path), - getFrom: (opts) => mockConfigGet(opts), + get: (_path) => mockConfigGet(_path), + getFrom: (_opts) => mockConfigGet(_opts), defaultConfigExists: () => mockDefaultConfigExists(), configDirPath: 'configDirPath', executionConfigPath: 'executionConfigPath.yml', @@ -51,7 +51,9 @@ describe('makeConfigFile', () => { it('should not make config file if global and config directory does not exist', () => { mockFileExists.mockImplementation((path) => { - if (path === 'configDirPath') return false; + if (path === 'configDirPath') { + return false; + } return true; }); @@ -71,7 +73,9 @@ describe('makeConfigFile', () => { beforeEach(() => { mockDefaultConfigExists.mockReturnValue(false); mockFileExists.mockImplementation((path) => { - if (path === 'configDirPath') return true; + if (path === 'configDirPath') { + return true; + } return false; }); }); @@ -145,8 +149,12 @@ describe('makeConfigFile', () => { beforeEach(() => { mockDefaultConfigExists.mockReturnValue(true); mockFileExists.mockImplementation((path) => { - if (path === 'configDirPath') return true; - if (path === 'defaultConfigPath.yml') return true; + if (path === 'configDirPath') { + return true; + } + if (path === 'defaultConfigPath.yml') { + return true; + } return false; }); }); @@ -207,7 +215,7 @@ describe('makeConfigFile', () => { describe('when config file already exists', () => { beforeEach(() => { mockDefaultConfigExists.mockReturnValue(false); - mockFileExists.mockImplementation((path) => { + mockFileExists.mockImplementation((_path) => { return true; }); }); diff --git a/controllers/create-record.js b/controllers/create-record.js index 8429ebc..d0adc3c 100644 --- a/controllers/create-record.js +++ b/controllers/create-record.js @@ -30,20 +30,20 @@ function createRecord( throw new Error('Need instance of Config to create record'); } - typeString = typeString.trim(); - tagsString = tagsString.trim(); + const trimmedType = typeString.trim(); + const trimmedTags = tagsString.trim(); let types = []; let tags = []; - if (typeString !== '') { - types = typeString + if (trimmedType !== '') { + types = trimmedType .split(',') .map((s) => s.trim()) .filter((s) => s !== ''); } - if (tagsString !== '') { - tags = tagsString + if (trimmedTags !== '') { + tags = trimmedTags .split(',') .map((s) => s.trim()) .filter((s) => s !== ''); @@ -75,7 +75,7 @@ function createRecord( const filePath = path.join(config.opts['files_origin'], fileName); const save = () => - fs.writeFile(filePath, record.getFileContent(saveIdOnYmlFrontMatter), (err) => { + fs.writeFile(filePath, record.getFileContent(saveIdOnYmlFrontMatter), (_err) => { logRecordIsSaved(); }); @@ -97,10 +97,10 @@ function createRecord( save(); function logRecordIsSaved() { - const { dir: fileDir, base: fileName } = path.parse(filePath); + const { dir: fileDir, base: recordFileName } = path.parse(filePath); console.log( ['\x1b[32m', 'Record created', '\x1b[0m'].join(''), - `: ${['\x1b[2m', fileDir, '/', '\x1b[0m', fileName].join('')}`, + `: ${['\x1b[2m', fileDir, '/', '\x1b[0m', recordFileName].join('')}`, ); } } diff --git a/controllers/history.js b/controllers/history.js index 057e480..fc5e6b3 100644 --- a/controllers/history.js +++ b/controllers/history.js @@ -32,14 +32,16 @@ async function getHistorySavePath(projectName, projectScope) { } const pathFile = path.join(pathDir, `${getTimestampTuple().join('')}.html`); - return new Promise(async (resolve, reject) => { + return new Promise((resolve, reject) => { if (fs.existsSync(pathDir) === false) { fs.mkdir(pathDir, { recursive: true }, (err) => { if (err) { reject(err.message); + return; } resolve(pathFile); }); + return; } resolve(pathFile); }); diff --git a/controllers/modelize.js b/controllers/modelize.js index ed53ade..b52e925 100644 --- a/controllers/modelize.js +++ b/controllers/modelize.js @@ -1,7 +1,6 @@ import fs from 'node:fs'; import fsPromise from 'node:fs/promises'; import path from 'node:path'; -import Record from '../core/models/record.js'; import Bibliography from '../core/models/bibliography.js'; import Config from '../core/models/config.js'; import Template from '../core/models/template.js'; @@ -23,18 +22,19 @@ const { log: envPathLogDir } = envPaths('cosma-cli', { suffix: '' }); const reportDir = path.join(envPathLogDir, 'logs'); async function modelize(options) { - let config = Config.get(Config.configFilePath); + const config = Config.get(Config.configFilePath); - options['citeproc'] = !!options['citeproc'] && config.canCiteproc(); - options['css_custom'] = !!options['customCss'] && config.canCssCustom(); + const processedOptions = { ...options }; + processedOptions['citeproc'] = Boolean(processedOptions['citeproc']) && config.canCiteproc(); + processedOptions['css_custom'] = Boolean(processedOptions['customCss']) && config.canCssCustom(); - options = Object.entries(options) + const optionsList = Object.entries(processedOptions) .map(([name, value]) => { return { name, value }; }) .filter(({ value }) => value === true); - const optionsTemplate = options + const optionsTemplate = optionsList .filter(({ name }) => Template.validParams.has(name)) .map(({ name }) => name); @@ -43,26 +43,29 @@ async function modelize(options) { switch (config.opts.select_origin) { case 'directory': if (config.canModelizeFromDirectory() === false) { - return console.error( + console.error( ['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'Cannot modelize from directory with this config.', ); + return; } break; case 'csv': if (config.canModelizeFromCsvFiles() === false) { - return console.error( + console.error( ['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'Cannot modelize from csv files with this config.', ); + return; } break; case 'online': if (config.canModelizeFromOnline() === false) { - return console.error( + console.error( ['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'Cannot modelize from online csv files with this config.', ); + return; } break; default: @@ -193,10 +196,8 @@ async function modelize(options) { fs.writeFile(path.join(config.opts.export_target, 'cosmoscope.html'), html, (err) => { // Cosmoscope file for export folder if (err) { - return console.error( - ['\x1b[31m', 'Err.', '\x1b[0m'].join(''), - 'write Cosmoscope file: ' + err, - ); + console.error(['\x1b[31m', 'Err.', '\x1b[0m'].join(''), 'write Cosmoscope file: ' + err); + return; } console.log( ['\x1b[34m', 'Cosmoscope generated', '\x1b[0m'].join(''), diff --git a/controllers/modelize.test.js b/controllers/modelize.test.js index f2c0d29..7eb7a92 100644 --- a/controllers/modelize.test.js +++ b/controllers/modelize.test.js @@ -80,8 +80,8 @@ const options = { }; describe('modelize', () => { - it('should throw an error for unknown data origin', () => { - const config = { + it('should throw an error for unknown data origin', async () => { + const unknownConfig = { opts: { select_origin: 'unknown', }, @@ -91,9 +91,9 @@ describe('modelize', () => { canModelizeFromDirectory: () => true, }; - Config.get.mockReturnValue(config); + Config.get.mockReturnValue(unknownConfig); - expect(modelize(options)).rejects.toThrow('Unknown data origin.'); + await expect(modelize(options)).rejects.toThrow('Unknown data origin.'); }); it('should record files without bibliography if no citeproc option', async () => { diff --git a/controllers/record.js b/controllers/record.js index 9a993cd..351fb01 100644 --- a/controllers/record.js +++ b/controllers/record.js @@ -42,32 +42,33 @@ async function makeRecord() { }); } - metas.title = await new Promise((resolve, reject) => { + metas.title = await new Promise((resolve, _reject) => { rl.question(`${['\x1b[1m', 'title', '\x1b[0m'].join('')} (required): `, (answer) => { if (answer.trim() === '') { - reject('Title is required'); + _reject('Title is required'); } resolve(answer); }); }); - metas.type = await new Promise((resolve, reject) => { + metas.type = await new Promise((resolve, _reject) => { rl.question( `${['\x1b[1m', 'type', '\x1b[0m'].join( '', )} (optional; enter as comma-separated values; if left blank, will be set as "undefined"): `, (answer) => { + let resolved = answer; if (answer.trim() === '') { - answer = 'undefined'; + resolved = 'undefined'; } - resolve(answer); + resolve(resolved); }, ); }); - metas.tags = await new Promise((resolve, reject) => { + metas.tags = await new Promise((resolve, _reject) => { rl.question( `${['\x1b[1m', 'tags', '\x1b[0m'].join('')} (optional; enter as comma-separated values): `, (answer) => { diff --git a/core/frontend/filter.js b/core/frontend/filter.js index 49f6d86..2477011 100644 --- a/core/frontend/filter.js +++ b/core/frontend/filter.js @@ -5,6 +5,7 @@ */ import { setNodesDisplaying, setLinksDisplaying, graph } from './graph.js'; +import hotkeys from 'hotkeys-js'; /** * Display all hidden elements by checking all unchecked inputs @@ -71,7 +72,7 @@ window.addEventListener('DOMContentLoaded', () => { const nodeIdsToDisplay = new Set(); types - .filter(([name]) => !!formState[name]) + .filter(([name]) => Boolean(formState[name])) .forEach(([, nodes]) => { nodes.forEach((id) => nodeIdsToDisplay.add(id)); }); diff --git a/core/frontend/graph.js b/core/frontend/graph.js index 89f4c20..370fb55 100644 --- a/core/frontend/graph.js +++ b/core/frontend/graph.js @@ -8,9 +8,7 @@ import * as d3 from 'd3'; import GraphEngine from 'graphology'; -import View from './view.js'; import { getRecordIdFromHash } from './records.js'; -import { setCounters } from './counter.js'; import hotkeys from 'hotkeys-js'; /** Data serialization @@ -77,14 +75,17 @@ window.updateForces = function () { simulation.alpha(1).restart(); }; -updateForces(); +window.updateForces(); hotkeys('space', (e) => { e.preventDefault(); - updateForces(); + window.updateForces(); }); -simulation.on('tick', function () { +const elts = {}; +const imageFileValidExtnames = new Set(['jpg', 'jpeg', 'png']); + +simulation.on('tick', () => { elts.links .attr('x1', (d) => d.source.x) .attr('y1', (d) => d.source.y) @@ -101,9 +102,6 @@ simulation.on('tick', function () { /** Elements ------------------------------------------------------------*/ -const elts = {}; -const imageFileValidExtnames = new Set(['jpg', 'jpeg', 'png']); - /** @type {d3.Selection} */ elts.links = svgSub .append('g') @@ -116,13 +114,13 @@ elts.links = svgSub .attr('data-link', (d) => d.key) .attr('data-source', (d) => d.source.key) .attr('data-target', (d) => d.target.key) - .attr('stroke-dasharray', function (d) { + .attr('stroke-dasharray', (d) => { if (d.attributes.shape.stroke === 'dash' || d.attributes.shape.stroke === 'dotted') { return d.attributes.shape.dashInterval; } return false; }) - .attr('filter', function (d) { + .attr('filter', (d) => { if (d.attributes.shape.stroke === 'double') { return 'url(#double)'; } @@ -146,17 +144,21 @@ elts.nodes = svgSub .call( d3 .drag() - .on('start', function (e, d) { - if (!e.active) simulation.alphaTarget(0.3).restart(); + .on('start', (e, d) => { + if (!e.active) { + simulation.alphaTarget(0.3).restart(); + } d.fx = d.x; d.fy = d.y; }) - .on('drag', function (e, d) { + .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; }) - .on('end', function (e, d) { - if (!e.active) simulation.alphaTarget(0.0001); + .on('end', (e, d) => { + if (!e.active) { + simulation.alphaTarget(0.0001); + } d.fx = null; d.fy = null; }), @@ -180,7 +182,7 @@ elts.nodes = svgSub nodesTransparent.nodes().forEach((elt) => elt.classList.add('translucent')); linksTransparent.nodes().forEach((elt) => elt.classList.add('translucent')); }) - .on('mouseout', (e, { key: nodeId }) => { + .on('mouseout', (_e, { key: _nodeId }) => { if (!graphProperties.graph_highlight_on_hover) { return; } @@ -213,7 +215,7 @@ elts.nodes = svgSub elts.nodes.each(function (d) { const node = d3.select(this); - const link = node.append('a').attr('href', (d) => '#' + d.key); + const link = node.append('a').attr('href', (_d) => '#' + _d.key); const getFill = (fill) => { if (imageFileValidExtnames.has(fill.split('.').at(-1))) { @@ -403,7 +405,7 @@ function setNodesDisplaying(nodeIds) { hideNodes(toHide); } -graph.on('nodeAttributesUpdated', function ({ key, attributes }) { +graph.on('nodeAttributesUpdated', ({ key, attributes }) => { const { links, node } = getNodeNetwork(key); if (attributes.hidden) { @@ -415,13 +417,13 @@ graph.on('nodeAttributesUpdated', function ({ key, attributes }) { } }); -graph.on('edgeAttributesUpdated', function ({ key, attributes }) { - const link = elts.links.filter((link) => link.key === key); +graph.on('edgeAttributesUpdated', ({ key, attributes }) => { + const edgeLink = elts.links.filter((l) => l.key === key); if (attributes.hidden) { - link.node().classList.add('hide'); + edgeLink.node().classList.add('hide'); } else { - link.node().classList.remove('hide'); + edgeLink.node().classList.remove('hide'); } }); @@ -544,6 +546,8 @@ window.updateFontsize = function () { elts.labels.attr('font-size', graphProperties.text_size); }; +const position = { x: 0, y: 0, zoom: 1 }; + function translate() { const minX = d3.min(data.nodes, (d) => d.x); const maxX = d3.max(data.nodes, (d) => d.x); @@ -558,9 +562,13 @@ function translate() { const screenMin = d3.min([screenHeight, screenWidth]); let viewBoxWidth = maxX - minX; - if (viewBoxWidth < screenMin) viewBoxWidth = screenMin; + if (viewBoxWidth < screenMin) { + viewBoxWidth = screenMin; + } let viewBoxHeight = maxY - minY; - if (viewBoxHeight < screenMin) viewBoxHeight = screenMin; + if (viewBoxHeight < screenMin) { + viewBoxHeight = screenMin; + } if (0 > minX || 0 > minY) { const viewBox = [ @@ -594,8 +602,6 @@ window.addEventListener('resize', () => { zoomInterval = Math.log2(density); }); -const position = { x: 0, y: 0, zoom: 1 }; - const zoom = d3 .zoom() .scaleExtent([zoomMin, zoomMax]) @@ -644,7 +650,9 @@ function zoomToNode(nodeId) { const node = nodes.find(({ key }) => key === nodeId); - if (!node) return; + if (!node) { + return; + } const { x, y } = node; const meanX = d3.mean(nodes, (d) => d.x); diff --git a/core/frontend/index.js b/core/frontend/index.js index 6dc58d3..a58d396 100644 --- a/core/frontend/index.js +++ b/core/frontend/index.js @@ -1,6 +1,7 @@ import './records.js'; import './search.js'; import './graph.js'; +import './view.js'; import './bibliography.js'; import './timeline.js'; import './filter.js'; diff --git a/core/frontend/records.js b/core/frontend/records.js index 1bc9076..ce1e31c 100644 --- a/core/frontend/records.js +++ b/core/frontend/records.js @@ -6,7 +6,9 @@ window.addEventListener('DOMContentLoaded', () => { const closeRightSideButton = document.getElementById('close-right-side'); const recordId = getRecordIdFromHash(); - if (recordId) openRecord(recordId); + if (recordId) { + openRecord(recordId); + } let hasRightSideClosedByClick = false; closeRightSideButton.addEventListener('click', () => { @@ -27,10 +29,10 @@ window.addEventListener('DOMContentLoaded', () => { }); window.addEventListener('hashchange', () => { - const recordId = getRecordIdFromHash(); - if (recordId) { - openRecord(recordId); - zoomToNode(recordId); + const changedRecordId = getRecordIdFromHash(); + if (changedRecordId) { + openRecord(changedRecordId); + zoomToNode(changedRecordId); } else { recordContainer.classList.remove('active'); unlightNodes(); @@ -110,7 +112,7 @@ window.addEventListener('DOMContentLoaded', () => { } }); -graph.on('nodeAttributesUpdated', function ({ key, attributes }) { +graph.on('nodeAttributesUpdated', ({ key, attributes }) => { const elt = indexContainer.querySelector(`[data-index="${key}"]`); elt.style.display = attributes.hidden ? 'none' : null; }); diff --git a/core/frontend/search.js b/core/frontend/search.js index b2078da..db890b0 100644 --- a/core/frontend/search.js +++ b/core/frontend/search.js @@ -8,8 +8,8 @@ const fuse = new Fuse([], { }); window.addEventListener('DOMContentLoaded', () => { - let maxResultNb = 5, - resultList = [], + const maxResultNb = 5; + let resultList = [], selectedResult = 0; const input = document.getElementById('search'); @@ -33,14 +33,18 @@ window.addEventListener('DOMContentLoaded', () => { selectedResult = 0; resultList = []; - if (input.value === '') return; + if (input.value === '') { + return; + } resultList = fuse.search(input.value); - if (resultList.length === 0) return; + if (resultList.length === 0) { + return; + } for (let i = 0; i < Math.min(maxResultNb, resultList.length); i++) { - let { + const { item: { key, attributes: { label, types }, diff --git a/core/frontend/tags.js b/core/frontend/tags.js index 2b66741..d346a37 100644 --- a/core/frontend/tags.js +++ b/core/frontend/tags.js @@ -6,7 +6,9 @@ window.addEventListener('DOMContentLoaded', () => { /** @type {HTMLFormElement} */ const form = document.getElementById('tags-form'); - if (!form) return; + if (!form) { + return; + } /** @type {HTMLSelectElement} */ const sortSelect = document.querySelector('.menu-tags .sorting-select'); @@ -15,7 +17,6 @@ window.addEventListener('DOMContentLoaded', () => { const tags = Object.entries(tagList); const tagsSorting = sorting.tags; - let tagsState; /** * Default state @@ -69,8 +70,8 @@ window.addEventListener('DOMContentLoaded', () => { const nodeIdsToDisplay = new Set(); - tagsState = tags - .filter(([name]) => !!formState[name]) + tags + .filter(([name]) => Boolean(formState[name])) .forEach(([, nodes]) => { nodes.forEach((id) => nodeIdsToDisplay.add(id)); }); diff --git a/core/frontend/timeline.js b/core/frontend/timeline.js index 776bbdb..f26dffa 100644 --- a/core/frontend/timeline.js +++ b/core/frontend/timeline.js @@ -11,7 +11,9 @@ window.addEventListener('DOMContentLoaded', () => { /** @type {HTMLDataListElement} */ const ticks = document.getElementById('timeline-ticks'); - if (!form) return; + if (!form) { + return; + } /** @type {HTMLOutputElement} */ const output = form.querySelector('output'); @@ -43,20 +45,21 @@ window.addEventListener('DOMContentLoaded', () => { const toDisplay = []; - for (let { + for (const { attributes: { begin: nodeBegin, end: nodeEnd }, key, } of data.nodes) { - console.log({ nodeBegin, nodeEnd }); + let effectiveEnd = nodeEnd; + let effectiveBegin = nodeBegin; if (nodeEnd === undefined) { - nodeEnd = end; + effectiveEnd = end; } if (nodeBegin === undefined) { - nodeBegin = begin; + effectiveBegin = begin; } - if (timestamp >= nodeBegin && timestamp <= nodeEnd) { + if (timestamp >= effectiveBegin && timestamp <= effectiveEnd) { toDisplay.push(key); } } diff --git a/core/models/bibliography.js b/core/models/bibliography.js index f9c72b3..e1f7891 100644 --- a/core/models/bibliography.js +++ b/core/models/bibliography.js @@ -36,7 +36,7 @@ class Bibliography { static getBibliographicLinksFromText(recordContent) { /** @type {BibliographicRecord[]} */ - let quotes = []; + const quotes = []; extractParaphs(recordContent).forEach((paraph) => { extractCitations(paraph).forEach((result) => { @@ -60,7 +60,7 @@ class Bibliography { */ static getBibliographicLinksFromList(quotesId = []) { - return quotesId.map((quoteId, index) => { + return quotesId.map((quoteId, _index) => { return { contexts: [], target: quoteId, @@ -193,7 +193,7 @@ class Bibliography { */ existsOnLibrary(item) { - return !!this.library[item.id]; + return Boolean(this.library[item.id]); } /** @@ -223,7 +223,7 @@ class Bibliography { } this.citeproc.updateItems(ids); - let record = this.citeproc + const record = this.citeproc .makeBibliography()[1] .map((t) => Bibliography.getFormatedHtmlBibliographicRecord(t)); diff --git a/core/models/bibliography.test.js b/core/models/bibliography.test.js index 3499e18..f33bfe7 100644 --- a/core/models/bibliography.test.js +++ b/core/models/bibliography.test.js @@ -1,6 +1,4 @@ import Bibliography from './bibliography'; -import fs from 'node:fs'; -import CSL from 'citeproc'; jest.mock('node:fs'); jest.mock('citeproc'); diff --git a/core/models/config.js b/core/models/config.js index c1cd0b6..f513387 100644 --- a/core/models/config.js +++ b/core/models/config.js @@ -108,12 +108,12 @@ const minValues = { attraction_horizontal: 0, }; -function pathExists(path, helpers) { - if (fs.existsSync(path)) { - return path; +function pathExists(filePath, helpers) { + if (fs.existsSync(filePath)) { + return filePath; } return helpers.error('any.invalid', { - message: `File ${path} does not exists.`, + message: `File ${filePath} does not exists.`, }); } @@ -329,7 +329,7 @@ class Config { try { files = fs.readdirSync(Config.configDirPath, 'utf-8'); - } catch (error) { + } catch (_error) { throw new ReadUserDataDirError('try to get config files', Config.configDirPath); } return files @@ -346,12 +346,12 @@ class Config { */ static getFrom(opts) { - opts = { + const mergedOpts = { ...Config.base, ...opts, }; - const { error, value } = optionsSchema.validate(opts, { stripUnknown: true }); + const { error, value } = optionsSchema.validate(mergedOpts, { stripUnknown: true }); if (error) { const details = (error?.details || []) .flatMap((detail) => [detail.message, detail.context.message]) @@ -421,27 +421,29 @@ class Config { } canModelizeFromDirectory() { - return !!this.opts.files_origin; + return Boolean(this.opts.files_origin); } canModelizeFromCsvFiles() { - return !!this.opts.nodes_origin && !!this.opts.links_origin; + return Boolean(this.opts.nodes_origin) && Boolean(this.opts.links_origin); } canModelizeFromOnline() { - return !!this.opts.nodes_online && !!this.opts.links_online; + return Boolean(this.opts.nodes_online) && Boolean(this.opts.links_online); } canCiteproc() { - return !!this.opts.csl && !!this.opts.bibliography && !!this.opts.csl_locale; + return ( + Boolean(this.opts.csl) && Boolean(this.opts.bibliography) && Boolean(this.opts.csl_locale) + ); } canCssCustom() { - return !!this.opts.css_custom; + return Boolean(this.opts.css_custom); } canSaveRecords() { - return !!this.opts.files_origin; + return Boolean(this.opts.files_origin); } getTypesRecords() { diff --git a/core/models/errors.js b/core/models/errors.js index 901a7d5..e061db1 100644 --- a/core/models/errors.js +++ b/core/models/errors.js @@ -56,7 +56,7 @@ export class DowloadOnlineCsvFilesError extends CoreError { } export class FindUserDataDirError extends CoreError { - constructor(cause) { + constructor(_cause) { super( 'Cosma user data directory does not exist. Use "cosma --create-user-data-dir"', undefined, diff --git a/core/models/record.js b/core/models/record.js index 9d3ea1e..2934016 100644 --- a/core/models/record.js +++ b/core/models/record.js @@ -86,15 +86,15 @@ export default class Record { */ static recordFromFile(body, props, config) { - props = configContraints(props, config); + let p = configContraints(props, config); - props = { - ...props, + p = { + ...p, links: parseWikilinks(body, config), content: body, }; - const { error, value: validProps } = schema.validate(props, { stripUnknown: true }); + const { error, value: validProps } = schema.validate(p, { stripUnknown: true }); if (error) { throw new Error(`Record contains error: ${error.message}`); } @@ -123,10 +123,10 @@ export default class Record { */ static recordFromCsv(props, config) { - props = formatAsRecord(props, config); - props = configContraints(props, config); + let p = formatAsRecord(props, config); + p = configContraints(p, config); - const { error, value: validProps } = schema.validate(props, { stripUnknown: true }); + const { error, value: validProps } = schema.validate(p, { stripUnknown: true }); if (error) { throw new Error(`Record contains error: ${error.message}`); } @@ -172,17 +172,17 @@ export default class Record { */ static recordWithTimestamp(props, config) { - props = { + const p = { ...props, id: getTimestampTuple().join(''), }; - const { error } = schema.validate(props); + const { error } = schema.validate(p); if (error) { throw new Error(`Record contains error: ${error.message}`); } - return new Record(props, config); + return new Record(p, config); } /** @@ -192,19 +192,19 @@ export default class Record { */ static recordWithIncrementedTimestamp(props, config, increment) { - props = configContraints(props, config); + let p = configContraints(props, config); - props = { - ...props, + p = { + ...p, id: timestampIncrement(increment), }; - const { error } = schema.validate(props, { stripUnknown: true }); + const { error } = schema.validate(p, { stripUnknown: true }); if (error) { throw new Error(`Record contains error: ${error.message}`); } - return new Record(props, config); + return new Record(p, config); } /** @@ -306,15 +306,15 @@ function configContraints(props, config) { } } - props = { + const result = { ...props, metas, }; - props.id = slugify(props.id); + result.id = slugify(result.id); - if (props.types) { - props.types = props.types.reduce((acc, curr) => { + if (result.types) { + result.types = result.types.reduce((acc, curr) => { if (!config.hasRecordType(curr)) { if (!acc.includes('undefined')) { acc.push('undefined'); @@ -326,5 +326,5 @@ function configContraints(props, config) { }, []); } - return props; + return result; } diff --git a/core/models/record.spec.js b/core/models/record.spec.js index 2b4b155..f78dbf3 100644 --- a/core/models/record.spec.js +++ b/core/models/record.spec.js @@ -30,27 +30,27 @@ describe('Record model', () => { it('should correctly initialize the Record instance with provided properties', () => { const record = new Record(props, config); - expect(record.id).toEqual('20200501150208'); - expect(record.title).toEqual('Test Record'); - expect(record.content).toEqual('This is a test content'); + expect(record.id).toBe('20200501150208'); + expect(record.title).toBe('Test Record'); + expect(record.content).toBe('This is a test content'); expect(record.links).toEqual([]); expect(record.types).toEqual(['type1', 'type2']); expect(record.tags).toEqual(['tag1', 'tag2']); expect(record.metas).toEqual({ author: 'John Doe' }); - expect(record.begin).toEqual(1609459200); - expect(record.end).toEqual(1609545600); - expect(record.thumbnail).toEqual('img.jpg'); - expect(record.template).toEqual(0); + expect(record.begin).toBe(1609459200); + expect(record.end).toBe(1609545600); + expect(record.thumbnail).toBe('img.jpg'); + expect(record.template).toBe(0); }); it('should correctly initialize default values if some properties are not provided', () => { - const props = { + const defaultProps = { id: '20200501150208', title: 'Default Record', content: 'Content with defaults', }; - const record = new Record(props, config); + const record = new Record(defaultProps, config); expect(record.id).toBe('20200501150208'); expect(record.title).toBe('Default Record'); @@ -69,7 +69,7 @@ describe('Record model', () => { const file = record.getFileContent(true); - expect(file).toEqual(`--- + expect(file).toBe(`--- id: "20200501150208" title: Test Record types: @@ -90,7 +90,7 @@ This is a test content`); const file = record.getFileContent(false); - expect(file).toEqual(`--- + expect(file).toBe(`--- title: Test Record thumbnail: img.jpg author: John Doe @@ -136,7 +136,7 @@ File linked to [[20210901132906]]`; end: undefined, thumbnail: undefined, template: 0, - config: config, + config, }); }); @@ -176,7 +176,7 @@ File linked to [[20210901132906]]`; end: undefined, thumbnail: undefined, template: 1, - config: config, + config, }); }); @@ -185,8 +185,8 @@ File linked to [[20210901132906]]`; expect(Joi.isError(result)).toBe(true); - expect(result.message).toEqual('"id" is required'); - expect(result.name).toEqual('ValidationError'); + expect(result.message).toBe('"id" is required'); + expect(result.name).toBe('ValidationError'); expect(result.details).toEqual([ { context: { key: 'id', label: 'id' }, diff --git a/core/models/template.js b/core/models/template.js index bf5723d..cfbe3c6 100644 --- a/core/models/template.js +++ b/core/models/template.js @@ -18,7 +18,6 @@ import favicon from '../../static/icons/cosmafavicon.png'; import logo from '../../static/icons/cosmalogo.svg'; import frontendScript from 'front'; import katekCss from 'katekCss'; -import GraphEngine from 'graphology'; import { extent } from 'd3'; import extractCitations from '../utils/citeExtractor.js'; import cssPrint from '../frontend/print.css'; @@ -54,7 +53,7 @@ class Template { * ``` */ - constructor(records, graph, params = [], opts = {}) { + constructor(records, graph, params = [], _opts = {}) { this.params = new Set(params.filter((param) => Template.validParams.has(param))); this.config = Config.get(Config.configFilePath); @@ -62,14 +61,12 @@ class Template { images_origin: imagesPath, css_custom: cssCustomPath, lang, - link_symbol: linkSymbol, views, title, author, description, keywords, focus_max: focusMax, - record_types: recordTypes, link_types: linkTypes, hide_id_from_record_header: hideIdFromRecordHeader, } = this.config.opts; @@ -137,22 +134,30 @@ class Template { .sort((a, b) => a.localeCompare(b)); const tagsListIncreasing = tagsDictAsArrays .sort(([, aNodes], [, bNodes]) => { - if (aNodes.length < bNodes.length) return -1; - if (aNodes.length > bNodes.length) return 1; + if (aNodes.length < bNodes.length) { + return -1; + } + if (aNodes.length > bNodes.length) { + return 1; + } return 0; }) .map(([name]) => name); const recordsListAlphabetical = [...records.values()] .sort((a, b) => a.title.localeCompare(b.title)) - .map(({ title }) => title); + .map(({ title: recordTitle }) => recordTitle); const recordsListChronological = [...records.values()] .sort((a, b) => { - if (a.begin < b.begin) return -1; - if (a.begin > b.begin) return 1; + if (a.begin < b.begin) { + return -1; + } + if (a.begin > b.begin) { + return 1; + } return 0; }) - .map(({ title }) => title); + .map(({ title: recordTitle }) => recordTitle); if (this.params.has('citeproc') && this.config.canCiteproc()) { const { bib, cslStyle, xmlLocal } = Bibliography.getBibliographicFilesFromConfig(this.config); @@ -187,7 +192,7 @@ class Template { } }); - Object.entries(this.config.opts.record_types).forEach(([type, { fill }]) => { + Object.entries(this.config.opts.record_types).forEach(([_type, { fill }]) => { if ( validExtnames.has(path.extname(fill)) && fs.existsSync(path.join(this.config.opts.images_origin, fill)) @@ -205,18 +210,18 @@ class Template { templateEngine.addFilter('slugify', (input) => { return slugify(input); }); - templateEngine.addFilter('convertLinks', (input, opts, idToHighlight) => { - input = convertWikilinks(input, records, opts, idToHighlight); + templateEngine.addFilter('convertLinks', (input, filterOpts, idToHighlight) => { + let result = convertWikilinks(input, records, filterOpts, idToHighlight); if (bibliography) { - const citeItems = quotesFromText(input); + const citeItems = quotesFromText(result); if (citeItems.every((item) => bibliography.existsOnLibrary(item))) { - input = convertQuotes(input, bibliography, records, idToHighlight); + result = convertQuotes(result, bibliography, records, idToHighlight); } } - return input; + return result; }); templateEngine.addFilter('markdown', (input) => { return markdownParser(input, this.config); @@ -237,7 +242,7 @@ class Template { hideIdFromRecordHeader, records: [...records.values()] .sort((a, b) => a.title.localeCompare(b.title)) - .map(({ thumbnail, links, bibliographicLinks, content, ...rest }) => { + .map(({ thumbnail, links, content: recordContent, ...rest }) => { const backNodes = graph.inNeighbors(rest.id); const recordLinks = links @@ -282,7 +287,7 @@ class Template { let citeNotes = []; if (bibliography) { - const citeItems = quotesFromText(content); + const citeItems = quotesFromText(recordContent); if (citeItems.every((item) => bibliography.existsOnLibrary(item))) { citeNotes = new Set(bibliography.getNotes(citeItems)); @@ -295,7 +300,7 @@ class Template { backlinks: recordBacklinks, links: recordLinks, bibliography: citeNotes, - content, + content: recordContent, thumbnail: thumbnailsMap.has(thumbnail) ? thumbnailsMap.get(thumbnail).path : undefined, }; }), @@ -307,7 +312,7 @@ class Template { }, timeline: (() => { - let dates = []; + const dates = []; for (const { begin, end } of [...records.values()]) { dates.push(begin, end); } @@ -315,7 +320,7 @@ class Template { return { begin, // Add margin of one second to display oldest node at end of timeline - end: end, + end, }; })(), @@ -354,9 +359,9 @@ class Template { }), sorting: { - records: [...records.values()].map(({ title }) => ({ - alphabetical: recordsListAlphabetical.indexOf(title), - chronological: recordsListChronological.indexOf(title), + records: [...records.values()].map(({ title: recordTitle }) => ({ + alphabetical: recordsListAlphabetical.indexOf(recordTitle), + chronological: recordsListChronological.indexOf(recordTitle), })), tags: tagsDictAsArrays.map(([name]) => ({ alphabetical: tagsListAlphabetical.indexOf(name), @@ -364,7 +369,7 @@ class Template { })), }, - app: app, // app version, description, license… + app, // app version, description, license… script: frontendScript, favicon, logo, @@ -372,8 +377,4 @@ class Template { } } -function escapeQuotes(text) { - return text.replace(/'/g, ''').replace(/"/g, '"'); -} - export default Template; diff --git a/core/utils/citeExtractor.js b/core/utils/citeExtractor.js index c3b7439..37cc6ce 100644 --- a/core/utils/citeExtractor.js +++ b/core/utils/citeExtractor.js @@ -99,16 +99,16 @@ function parseSuffix(suffix, containsLocator) { return retValue; } - suffix = suffix.trim(); + const trimmedSuffix = suffix.trim(); for (const label in locatorLabels) { for (const natural of locatorLabels[label]) { - if (suffix.toLowerCase().startsWith(natural.toLowerCase())) { + if (trimmedSuffix.toLowerCase().startsWith(natural.toLowerCase())) { retValue.label = label; if (containsLocator) { - retValue.locator = suffix.substr(natural.length).trim(); + retValue.locator = trimmedSuffix.substr(natural.length).trim(); } else { - retValue.suffix = suffix.substr(natural.length).trim(); + retValue.suffix = trimmedSuffix.substr(natural.length).trim(); const match = locatorRE.exec(retValue.suffix); if (match !== null) { retValue.locator = match[0]; @@ -121,14 +121,14 @@ function parseSuffix(suffix, containsLocator) { } if (containsLocator) { - retValue.locator = suffix; + retValue.locator = trimmedSuffix; } else { - const match = locatorRE.exec(suffix); + const match = locatorRE.exec(trimmedSuffix); if (match !== null) { retValue.locator = match[0]; - retValue.suffix = suffix.substr(match[0].length).trim(); + retValue.suffix = trimmedSuffix.substr(match[0].length).trim(); } else { - retValue.suffix = suffix; + retValue.suffix = trimmedSuffix; } } @@ -145,7 +145,7 @@ export default function extractCitations(markdown) { for (const match of markdown.matchAll(citationRE)) { let from = match.index; - let to = from + match[0].length; + const to = from + match[0].length; /** @type {CiteToto[]} */ const citations = []; let composite = false; @@ -161,13 +161,13 @@ export default function extractCitations(markdown) { if (fullCitation !== undefined) { for (const citationPart of fullCitation.split(';')) { - const match = fullCitationRE.exec(citationPart.trim()); - if (match === null) { + const citationMatch = fullCitationRE.exec(citationPart.trim()); + if (citationMatch === null) { continue; } const thisCitation = { - id: match.groups.citekey.replace(/{(.+)}/, '$1'), + id: citationMatch.groups.citekey.replace(/{(.+)}/, '$1'), prefix: undefined, locator: undefined, label: 'page', @@ -175,7 +175,7 @@ export default function extractCitations(markdown) { suffix: undefined, }; - const rawPrefix = match.groups.prefix; + const rawPrefix = citationMatch.groups.prefix; if (rawPrefix !== undefined) { thisCitation['suppress-author'] = rawPrefix.trim().endsWith('-'); if (thisCitation['suppress-author']) { @@ -187,9 +187,9 @@ export default function extractCitations(markdown) { } } - const explicitLocator = match.groups.explicitLocator; - const explicitLocatorInSuffix = match.groups.explicitLocatorInSuffix; - const rawSuffix = match.groups.suffix; + const explicitLocator = citationMatch.groups.explicitLocator; + const explicitLocatorInSuffix = citationMatch.groups.explicitLocatorInSuffix; + const rawSuffix = citationMatch.groups.suffix; let suffixToParse; let containsLocator = true; diff --git a/core/utils/citeExtractor.spec.js b/core/utils/citeExtractor.spec.js index 4f9304a..853b349 100644 --- a/core/utils/citeExtractor.spec.js +++ b/core/utils/citeExtractor.spec.js @@ -8,7 +8,7 @@ const defaults = { suffix: undefined, }; -describe('extractCitations', function () { +describe('extractCitations', () => { it('extracts a regular, full citation containing three IDs', () => { const input = 'Blah blah [@doe99; @smith2000; @smith2004].'; const expected = [ @@ -118,7 +118,7 @@ describe('extractCitations', function () { expect(extractCitations(input)).toEqual(expected); }); - it('extracts citation with composite page locator and suffix', () => { + it('extracts citation with page locator and prefix', () => { const input = 'Another one [see @engelbart1962 p. 34-35].'; const expected = [ { diff --git a/core/utils/convertQuotes.js b/core/utils/convertQuotes.js index ecbeb98..0d2e0bd 100644 --- a/core/utils/convertQuotes.js +++ b/core/utils/convertQuotes.js @@ -8,13 +8,14 @@ import extractCitations from './citeExtractor'; */ function convertQuotes(markdown, bibliography, records, idToHighlight) { - extractCitations(markdown).forEach((quote, index) => { + let result = markdown; + extractCitations(result).forEach((quote, index) => { const originalText = quote.source; // => "[@engelbart1962; quoted by @matuschak2019]" const idsDictionnary = new Map(); - if (!quote.citations.every(({ id }) => !!bibliography.library[id])) { - return markdown; + if (!quote.citations.every(({ id }) => Boolean(bibliography.library[id]))) { + return; } for (const item of quote.citations) { @@ -48,17 +49,19 @@ function convertQuotes(markdown, bibliography, records, idToHighlight) { quoteText = quoteText.replace(key, () => { const record = records.get(recordId); - if (!record) return key; + if (!record) { + return key; + } return `${key}`; }); }); // "[@engelbart1962; quoted by @matuschak2019]" => // (Engelbart, 1962 ; quoted by Matuschak, Nielsen, 2019) - markdown = markdown.replace(originalText, quoteText); + result = result.replace(originalText, quoteText); }); - return markdown; + return result; } export default convertQuotes; diff --git a/core/utils/convertQuotes.spec.js b/core/utils/convertQuotes.spec.js index cf01b76..75bd57b 100644 --- a/core/utils/convertQuotes.spec.js +++ b/core/utils/convertQuotes.spec.js @@ -80,7 +80,7 @@ describe('convertQuotes', () => { [], ); - expect(result).toEqual( + expect(result).toBe( 'Lorem (Matuschak, 2019) ipsum dolor est.', ); }); @@ -136,7 +136,7 @@ describe('convertQuotes', () => { [], ); - expect(result).toEqual( + expect(result).toBe( 'Lorem (Engelbart, 1962 ; quoted by Matuschak, Nielsen, 2019) ipsum dolor est.', ); }); @@ -146,13 +146,13 @@ describe('convertQuotes', () => { const result = convertQuotes(text, bibliography, records, 'matuschak2019'); - expect(result).toEqual('Lorem ipsum dolor est.'); + expect(result).toBe('Lorem ipsum dolor est.'); }); it('should not add "highlight" class if unknown record', () => { const text = 'Lorem @matuschak2019 ipsum dolor est.'; - expect(convertQuotes(text, bibliography, records, 'unknown')).toEqual( + expect(convertQuotes(text, bibliography, records, 'unknown')).toBe( 'Lorem (Matuschak, 2019) ipsum dolor est.', ); }); @@ -160,7 +160,7 @@ describe('convertQuotes', () => { it('should not get anchor if no record', () => { const text = 'Lorem @matuschak2019 ipsum dolor est.'; - expect(convertQuotes(text, bibliography, new Map(), 'matuschak2019')).toEqual( + expect(convertQuotes(text, bibliography, new Map(), 'matuschak2019')).toBe( 'Lorem (Matuschak, 2019) ipsum dolor est.', ); }); @@ -168,7 +168,7 @@ describe('convertQuotes', () => { it('should return original if unknown quote id from library', () => { const text = 'Lorem [@matuschak2019; @unknown] ipsum dolor est.'; - expect(convertQuotes(text, bibliography, records, 'matuschak2019')).toEqual( + expect(convertQuotes(text, bibliography, records, 'matuschak2019')).toBe( 'Lorem [@matuschak2019; @unknown] ipsum dolor est.', ); }); diff --git a/core/utils/convertWikilinks.js b/core/utils/convertWikilinks.js index 7b3b1c6..ff52a87 100644 --- a/core/utils/convertWikilinks.js +++ b/core/utils/convertWikilinks.js @@ -16,7 +16,9 @@ function convertWikilinks(markdown, records, opts, idToHighlight) { return markdown.replace(wikilinkRE, (match, _, type, targetId, __, text) => { const record = records.get(slugify(targetId)); - if (!record) return match; + if (!record) { + return match; + } let linkLibelle; if (text) { diff --git a/core/utils/convertWikilinks.spec.js b/core/utils/convertWikilinks.spec.js index 6dd82ac..b3bccf0 100644 --- a/core/utils/convertWikilinks.spec.js +++ b/core/utils/convertWikilinks.spec.js @@ -31,7 +31,7 @@ describe('convertWikilinks', () => { it('should replace one link on text, with symbol', () => { const markdown = 'Lorem ipsum [[20220403222345]] dolor est.'; - expect(convertWikilinks(markdown, records, opts, '20220403222345')).toEqual( + expect(convertWikilinks(markdown, records, opts, '20220403222345')).toBe( `Lorem ipsum dolor est.`, ); }); @@ -39,7 +39,7 @@ describe('convertWikilinks', () => { it('should replace one link on text, with link text', () => { const markdown = 'Lorem ipsum [[20220403222345|Paul Otlet]] dolor est.'; - expect(convertWikilinks(markdown, records, opts, '20220403222345')).toEqual( + expect(convertWikilinks(markdown, records, opts, '20220403222345')).toBe( `Lorem ipsum Paul Otlet dolor est.`, ); }); @@ -47,7 +47,7 @@ describe('convertWikilinks', () => { it('should replace one link on text, with link content', () => { const markdown = 'Lorem ipsum [[20220403222345]] dolor est.'; - expect(convertWikilinks(markdown, records, {}, '20220403222345')).toEqual( + expect(convertWikilinks(markdown, records, {}, '20220403222345')).toBe( `Lorem ipsum [[20220403222345]] dolor est.`, ); }); @@ -55,7 +55,7 @@ describe('convertWikilinks', () => { it('should replace several link with capitalized text as id', () => { const markdown = 'Lorem ipsum [[Dewey]] dolor est [[CDU]].'; - expect(convertWikilinks(markdown, records, {}, '20220403222345')).toEqual( + expect(convertWikilinks(markdown, records, {}, '20220403222345')).toBe( `Lorem ipsum Dewey dolor est CDU.`, ); }); @@ -63,7 +63,7 @@ describe('convertWikilinks', () => { it('should not replace link for unknown record id', () => { const markdown = 'Lorem ipsum [[unknown]] dolor est.'; - expect(convertWikilinks(markdown, records, {}, '20220403222345')).toEqual( + expect(convertWikilinks(markdown, records, {}, '20220403222345')).toBe( `Lorem ipsum [[unknown]] dolor est.`, ); }); @@ -71,7 +71,7 @@ describe('convertWikilinks', () => { it('should not add class "highlight" if not current record', () => { const markdown = 'Lorem ipsum [[20220403222345]] dolor est.'; - expect(convertWikilinks(markdown, records, {}, '20190403222543')).toEqual( + expect(convertWikilinks(markdown, records, {}, '20190403222543')).toBe( `Lorem ipsum [[20220403222345]] dolor est.`, ); }); diff --git a/core/utils/csvToNodes.js b/core/utils/csvToNodes.js index a93ab0b..ef771b4 100644 --- a/core/utils/csvToNodes.js +++ b/core/utils/csvToNodes.js @@ -1,7 +1,7 @@ -import { Readable } from 'stream'; +import { Readable } from 'node:stream'; import { parse } from 'csv-parse'; -import { finished } from 'stream/promises'; -import fs from 'fs'; +import { finished } from 'node:stream/promises'; +import fs from 'node:fs'; import Record from '../models/record.js'; import formatAsRecord from './formatAsRecord.js'; import unknownTypesMessage from './unknownTypesMessage.js'; @@ -25,7 +25,7 @@ export async function processNodes(filePath, config) { cast: (value) => (value === '' ? undefined : value), }), ); - parser.on('readable', function () { + parser.on('readable', () => { let line; let i = 1; @@ -93,7 +93,7 @@ export async function processNodesOnline(url, config) { }), ); - parser.on('readable', function () { + parser.on('readable', () => { let line; let i = 1; @@ -158,13 +158,10 @@ export async function processLinksOnline(url, records, config) { }), ); - parser.on('readable', function () { + parser.on('readable', () => { let line; - let i = 1; while ((line = parser.read()) !== null) { - i++; - let linkType = 'undefined'; if (linkTypes.has(line['type'])) { @@ -199,13 +196,10 @@ export async function processLinks(filePath, records, config) { cast: (value) => (value === '' ? undefined : value), }), ); - parser.on('readable', function () { + parser.on('readable', () => { let line; - let i = 1; while ((line = parser.read()) !== null) { - i++; - let linkType = 'undefined'; if (linkTypes.has(line['type'])) { diff --git a/core/utils/csvToNodes.spec.js b/core/utils/csvToNodes.spec.js index fae7e99..5594449 100644 --- a/core/utils/csvToNodes.spec.js +++ b/core/utils/csvToNodes.spec.js @@ -1,7 +1,6 @@ -import { PassThrough } from 'stream'; -import fs from 'fs'; -import { processNodes, processNodesOnline } from './csvToNodes'; -const { Readable } = require('stream'); +import { PassThrough } from 'node:stream'; +import fs from 'node:fs'; +import { processNodes } from './csvToNodes'; jest.mock('fs'); diff --git a/core/utils/formatAsRecord.js b/core/utils/formatAsRecord.js index a609573..be44fab 100644 --- a/core/utils/formatAsRecord.js +++ b/core/utils/formatAsRecord.js @@ -14,26 +14,26 @@ const aliasTable = { */ export default function formatAsRecord(props) { - props = normalizeWithAliases(aliasTable, props); + const result = normalizeWithAliases(aliasTable, props); - if (!props.id && props.title) { - props.id = props.title; + if (!result.id && result.title) { + result.id = result.title; } - if (!props.title && props.id) { - props.title = props.id; + if (!result.title && result.id) { + result.title = result.id; } - if (props.types && typeof props.types === 'string') { - props.types = [props.types]; + if (result.types && typeof result.types === 'string') { + result.types = [result.types]; } - if (props.tags && typeof props.tags === 'string') { - props.tags = [props.tags]; + if (result.tags && typeof result.tags === 'string') { + result.tags = [result.tags]; } - if (props.begin && typeof props.begin === 'string') { - props.begin = new Date(props.begin).getTime() / 1000; + if (result.begin && typeof result.begin === 'string') { + result.begin = new Date(result.begin).getTime() / 1000; } - if (props.end && typeof props.end === 'string') { - props.end = new Date(props.end).getTime() / 1000; + if (result.end && typeof result.end === 'string') { + result.end = new Date(result.end).getTime() / 1000; } - return props; + return result; } diff --git a/core/utils/formatAsRecord.spec.js b/core/utils/formatAsRecord.spec.js index 9bbbf36..282cdfe 100644 --- a/core/utils/formatAsRecord.spec.js +++ b/core/utils/formatAsRecord.spec.js @@ -20,17 +20,17 @@ describe('formatAsRecord', () => { it('should convert date strings to timestamps', () => { const result = formatAsRecord(data); - expect(result.begin).toEqual(-3198528000); - expect(result.end).toEqual(-790819200); + expect(result.begin).toBe(-3198528000); + expect(result.end).toBe(-790819200); }); it('should use id as title if undefined', () => { const result = formatAsRecord({ ...data, id: undefined }); - expect(result.id).toEqual('Paul Otlet'); + expect(result.id).toBe('Paul Otlet'); }); it('should use title as id if undefined', () => { const result = formatAsRecord({ ...data, title: undefined }); - expect(result.title).toEqual('otlet'); + expect(result.title).toBe('otlet'); }); }); diff --git a/core/utils/getGraph.js b/core/utils/getGraph.js index f54e1f3..a9e3327 100644 --- a/core/utils/getGraph.js +++ b/core/utils/getGraph.js @@ -1,6 +1,5 @@ import GraphEngine from 'graphology'; import { scaleLinear } from 'd3'; -import Config from '../models/config'; import slugify from './slugify'; /** @@ -46,7 +45,7 @@ function getNodeSize(degree, minDegree, maxDegree, config) { switch (config.opts['node_size_method']) { case 'unique': return config.opts['node_size']; - case 'degree': + case 'degree': { const compute = scaleLinear() .domain([minDegree, maxDegree]) .range([config.opts['node_size_min'], config.opts['node_size_max']]); @@ -54,6 +53,9 @@ function getNodeSize(degree, minDegree, maxDegree, config) { const size = compute(degree); // round at most two decimals return Math.round(size * 100) / 100; + } + default: + return config.opts['node_size']; } } @@ -69,13 +71,13 @@ function getLinkShape(linkType, config) { switch (stroke) { case 'simple': - return { stroke: stroke, dashInterval: null }; + return { stroke, dashInterval: null }; case 'double': - return { stroke: stroke, dashInterval: null }; + return { stroke, dashInterval: null }; case 'dash': - return { stroke: stroke, dashInterval: '4, 5' }; + return { stroke, dashInterval: '4, 5' }; case 'dotted': - return { stroke: stroke, dashInterval: '1, 3' }; + return { stroke, dashInterval: '1, 3' }; } return { stroke: 'simple', dashInterval: null }; } diff --git a/core/utils/imagePathToBase64.js b/core/utils/imagePathToBase64.js index 66553ff..0dc57fa 100644 --- a/core/utils/imagePathToBase64.js +++ b/core/utils/imagePathToBase64.js @@ -14,7 +14,9 @@ export default function imagePathToBase64(imgPath) { const imgFileContent = fs.readFileSync(imgPath); const imgType = getImageType(imgFileContent); - if (!imgType) return ''; + if (!imgType) { + return ''; + } const imgBase64 = imgFileContent.toString('base64'); return `data:image/${imgType};base64,${imgBase64}`; diff --git a/core/utils/imagePathToBase64.test.js b/core/utils/imagePathToBase64.test.js index 9a6403c..66aef5a 100644 --- a/core/utils/imagePathToBase64.test.js +++ b/core/utils/imagePathToBase64.test.js @@ -1,6 +1,5 @@ import imagePathToBase64 from './imagePathToBase64'; import fs from 'node:fs'; -import path from 'path'; jest.mock('node:fs'); @@ -21,11 +20,11 @@ describe('imagePathToBase64', () => { }); it('should return empty string for invalid image path', () => { - const imgPath = '/path/to/invalid.txt'; + const invalidImgPath = '/path/to/invalid.txt'; fs.existsSync.mockReturnValue(false); - const result = imagePathToBase64(imgPath); + const result = imagePathToBase64(invalidImgPath); expect(result).toBe(''); }); diff --git a/core/utils/makeRecord.js b/core/utils/makeRecord.js index e775e7f..3f5a536 100644 --- a/core/utils/makeRecord.js +++ b/core/utils/makeRecord.js @@ -1,3 +1,3 @@ -export default function makeRecord(props, config) { +export default function makeRecord(props, _config) { props.begin; } diff --git a/core/utils/normalizeWithAliases.js b/core/utils/normalizeWithAliases.js index 7356eaf..edae242 100644 --- a/core/utils/normalizeWithAliases.js +++ b/core/utils/normalizeWithAliases.js @@ -1,7 +1,7 @@ export default function normalizeWithAliases(aliasTable, props) { - let renamedProps = { ...props }; + const renamedProps = { ...props }; - for (let key of Object.keys(props)) { + for (const key of Object.keys(props)) { if (aliasTable[key]) { renamedProps[aliasTable[key]] = renamedProps[key]; delete renamedProps[key]; diff --git a/core/utils/paraphExtractor.spec.js b/core/utils/paraphExtractor.spec.js index b1b4125..8cbfe8d 100644 --- a/core/utils/paraphExtractor.spec.js +++ b/core/utils/paraphExtractor.spec.js @@ -63,10 +63,8 @@ This is a paragraph`, }, ]; -describe('paraphExtractor', function () { - for (const test of tests) { - it(test.description, () => { - expect(extractParaphs(test.input)).toEqual(test.expected); - }); - } +describe('paraphExtractor', () => { + it.each(tests)('$description', ({ input, expected }) => { + expect(extractParaphs(input)).toEqual(expected); + }); }); diff --git a/core/utils/parseWikilinks.js b/core/utils/parseWikilinks.js index c88b7d6..dbff24e 100644 --- a/core/utils/parseWikilinks.js +++ b/core/utils/parseWikilinks.js @@ -20,7 +20,9 @@ const wikilinkRE = new RegExp(/\[\[((?[^:|\]]+?):)?(?.+?)(\|(?.+ */ export default function parseWikilinks(markdown, config) { - if (!markdown) return []; + if (!markdown) { + return []; + } const linkTypes = config.getTypesLinks(); @@ -30,7 +32,7 @@ export default function parseWikilinks(markdown, config) { const linksDict = new Map(); for (const match of markdown.matchAll(wikilinkRE) || []) { - const [full, _, type, id, __, placeholder] = match; + const [, , type, id, , placeholder] = match; const target = slugify(id); let linkType = 'undefined'; @@ -52,7 +54,7 @@ export default function parseWikilinks(markdown, config) { extractParaphs(markdown).forEach((paraph) => { for (const match of paraph.matchAll(wikilinkRE)) { - const [full, _, type, id] = match; + const [, , , id] = match; const target = slugify(id); linksDict.get(target).contexts.add(paraph); diff --git a/core/utils/quoteIdsWithContexts.js b/core/utils/quoteIdsWithContexts.js index 63720ca..e84d3cf 100644 --- a/core/utils/quoteIdsWithContexts.js +++ b/core/utils/quoteIdsWithContexts.js @@ -8,7 +8,7 @@ import extractParaphs from './paraphExtractor'; */ export default function quoteIdsWithContexts(markdown) { - let quotes = {}; + const quotes = {}; extractParaphs(markdown).forEach((paraph) => { extractCitations(paraph).forEach((result) => { diff --git a/core/utils/readRecordFile.js b/core/utils/readRecordFile.js index ee66af7..f54f6a0 100644 --- a/core/utils/readRecordFile.js +++ b/core/utils/readRecordFile.js @@ -102,6 +102,7 @@ export default async function readRecordFile(filePath, config, bibliography) { locator: { file: filePath }, message: `Quote "${citeItem.id}" has no reference from library.`, }); + return false; }) .forEach((citeItem) => { const recordCite = Record.recordFromCiteItem(citeItem, config, bibliography); diff --git a/core/utils/readRecordFile.spec.js b/core/utils/readRecordFile.spec.js index a5fd95e..675fbdf 100644 --- a/core/utils/readRecordFile.spec.js +++ b/core/utils/readRecordFile.spec.js @@ -41,9 +41,9 @@ title: Test Title Test @smith04`; fsPromise.readFile.mockResolvedValue(fileContent); - const bibliography = undefined; + const noBib = undefined; - const result = await readRecordFile(filePath, config, bibliography); + const result = await readRecordFile(filePath, config, noBib); expect(result).toEqual({ records: [expect.objectContaining({ id: 'test-1' })], @@ -64,9 +64,7 @@ type: personne Test`; fsPromise.readFile.mockResolvedValue(fileContent); - const bibliography = undefined; - - const result = await readRecordFile(filePath, config, bibliography); + const result = await readRecordFile(filePath, config, undefined); expect(result).toEqual({ records: [expect.objectContaining({ types: ['undefined'] })], @@ -92,9 +90,7 @@ title: Test Title Test`; fsPromise.readFile.mockResolvedValue(fileContent); - const bibliography = undefined; - - const result = await readRecordFile(filePath, config, bibliography); + const result = await readRecordFile(filePath, config, undefined); expect(result).toEqual({ records: [expect.objectContaining({ types: ['undefined'] })], @@ -112,9 +108,7 @@ id: test-1 Test @smith04`; fsPromise.readFile.mockResolvedValue(fileContent); - const bibliography = undefined; - - const result = await readRecordFile(filePath, config, bibliography); + const result = await readRecordFile(filePath, config, undefined); expect(result).toEqual({ records: [], @@ -133,9 +127,7 @@ Test @smith04`; const fileContent = 'Test'; fsPromise.readFile.mockResolvedValue(fileContent); - const bibliography = undefined; - - const result = await readRecordFile(filePath, config, bibliography); + const result = await readRecordFile(filePath, config, undefined); expect(result).toEqual({ records: [], diff --git a/core/utils/slugify.spec.js b/core/utils/slugify.spec.js index c203a6a..8b5a8c2 100644 --- a/core/utils/slugify.spec.js +++ b/core/utils/slugify.spec.js @@ -2,34 +2,34 @@ import slugify from './slugify'; describe('slugify', () => { it('replaces spaces with hyphens and lowercase', () => { - expect(slugify('Hello world')).toEqual('hello-world'); + expect(slugify('Hello world')).toBe('hello-world'); }); it('removes accents', () => { - expect(slugify('À bientôt, François!')).toEqual('a-bientot-francois'); + expect(slugify('À bientôt, François!')).toBe('a-bientot-francois'); }); it('removes special characters', () => { - expect(slugify('Hello @world#123!')).toEqual('hello-world123'); + expect(slugify('Hello @world#123!')).toBe('hello-world123'); }); it('handles empty strings', () => { - expect(slugify('')).toEqual(''); + expect(slugify('')).toBe(''); }); it('handles strings with only spaces', () => { - expect(slugify(' ')).toEqual(''); + expect(slugify(' ')).toBe(''); }); it('keeps only letters, numbers, and hyphens', () => { - expect(slugify('abc-!@#$%^&*()123')).toEqual('abc-123'); + expect(slugify('abc-!@#$%^&*()123')).toBe('abc-123'); }); it('handles multiple consecutive spaces', () => { - expect(slugify('Hello world')).toEqual('hello-world'); + expect(slugify('Hello world')).toBe('hello-world'); }); it('handles strings with slashes', () => { - expect(slugify('OIB / IIB / FID')).toEqual('oib-iib-fid'); + expect(slugify('OIB / IIB / FID')).toBe('oib-iib-fid'); }); }); diff --git a/core/utils/writeReportFile.js b/core/utils/writeReportFile.js index 6174786..d85df49 100644 --- a/core/utils/writeReportFile.js +++ b/core/utils/writeReportFile.js @@ -33,8 +33,12 @@ export default function writeReportFile(items, config) { }); const sortItems = (a, b) => { - if (a.locator.line === undefined) return 1; - if (b.locator.line === undefined) return -1; + if (a.locator.line === undefined) { + return 1; + } + if (b.locator.line === undefined) { + return -1; + } return a.locator.line - b.locator.line; }; diff --git a/core/utils/writeReportFile.spec.js b/core/utils/writeReportFile.spec.js index c322202..851f5d4 100644 --- a/core/utils/writeReportFile.spec.js +++ b/core/utils/writeReportFile.spec.js @@ -1,4 +1,3 @@ -import { warn } from 'yaml/util'; import writeReportFile from './writeReportFile'; import nunjucks from 'nunjucks'; diff --git a/core/utils/yamlfrontmatter.js b/core/utils/yamlfrontmatter.js index b3867b6..f07bb6f 100644 --- a/core/utils/yamlfrontmatter.js +++ b/core/utils/yamlfrontmatter.js @@ -1,7 +1,7 @@ import yml from 'yaml'; // Thanks to https://github.com/dworthen/js-yaml-front-matter/blob/master/src/index.js -const regex = /^(-{3}(?:\n|\r)([\w\W]+?)(?:\n|\r)[-|\.]{3})?([\w\W]*)*/; +const regex = /^(-{3}(?:\n|\r)([\w\W]+?)(?:\n|\r)[-|.]{3})?([\w\W]*)*/; /** * Read head of markdown files as YAML content @@ -21,13 +21,13 @@ export default function readYamlFrontmatter(fileContent, options = {}) { } const windowsCariageReturn = new RegExp(/\r\n/g); - fileContent = fileContent.replace(windowsCariageReturn, '\n'); + const normalizedContent = fileContent.replace(windowsCariageReturn, '\n'); - const [, withDash, withoutDash, body] = regex.exec(fileContent); + const [, , withoutDash, body] = regex.exec(normalizedContent); if (withoutDash === undefined) { return { - body: fileContent, + body: normalizedContent, head: null, }; } diff --git a/core/utils/yamlfrontmatter.spec.js b/core/utils/yamlfrontmatter.spec.js index 4c2ecea..5e172a0 100644 --- a/core/utils/yamlfrontmatter.spec.js +++ b/core/utils/yamlfrontmatter.spec.js @@ -1,7 +1,7 @@ import { YAMLParseError } from 'yaml'; import readYamlFrontmatter from './yamlfrontmatter'; -describe('YAML Front Matter parser', function () { +describe('YAML Front Matter parser', () => { it('With "---" separator', () => { const input = `--- id: 20210901132906 diff --git a/e2e/cypress.config.js b/e2e/cypress.config.js index 049f9f0..a88e42a 100644 --- a/e2e/cypress.config.js +++ b/e2e/cypress.config.js @@ -10,7 +10,7 @@ module.exports = defineConfig({ e2e: { specPattern: './**/*.cy.js', supportFile: path.join(__dirname, './e2e-support.js'), - setupNodeEvents(on, config) { + setupNodeEvents(_on, _config) { // implement node event listeners here }, }, diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..fbdee1b --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,135 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import pluginNode from 'eslint-plugin-n'; +import pluginJest from 'eslint-plugin-jest'; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + // ── Ignore generated & downloaded directories ────────────────────────────── + { + ignores: ['dist/**', 'temp/**', 'node_modules/**'], + }, + + // ── Règles exigeantes partagées par tous les fichiers JS/MJS ────────────── + { + files: ['**/*.{js,mjs}'], + rules: { + ...js.configs.recommended.rules, + + // Variables & portée + 'no-unused-vars': ['error', { argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], + 'no-shadow': 'error', + 'prefer-const': 'error', + 'no-var': 'error', + 'no-use-before-define': ['error', { functions: false, classes: true, variables: true }], + + // Qualité du code + 'eqeqeq': ['error', 'always'], + 'curly': ['error', 'all'], + 'no-implicit-coercion': 'error', + 'no-duplicate-imports': 'error', + 'consistent-return': 'error', + 'no-param-reassign': ['error', { props: false }], + 'object-shorthand': ['error', 'always'], + 'prefer-arrow-callback': 'error', + + // Logs : géré par contexte (error frontend) + }, + }, + + // ── Backend Node.js (tout sauf frontend & e2e) ───────────────────────────── + { + files: ['**/*.{js,mjs}'], + ignores: ['core/frontend/**', 'e2e/**'], + plugins: { n: pluginNode }, + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { ...globals.node }, + }, + rules: { + 'n/no-deprecated-api': 'error', + 'n/prefer-node-protocol': 'error', + }, + }, + + // ── Fichiers de config CJS (babel, jest, cypress) ───────────────────────── + { + files: ['babel.config.js', 'jest.config.js', 'e2e/cypress.config.js'], + languageOptions: { + sourceType: 'commonjs', + globals: { ...globals.node }, + }, + rules: { + // require() est légal dans les fichiers CJS + 'no-var': 'error', + }, + }, + + // ── Frontend navigateur ──────────────────────────────────────────────────── + { + files: ['core/frontend/**/*.js'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + ...globals.browser, + data: 'readonly', + graphProperties: 'readonly', + typeList: 'readonly', + tagList: 'readonly', + sorting: 'readonly', + focusIsActive: 'readonly', + timeline: 'readonly', + }, + }, + rules: { + // Aucun console.* dans le code de production du navigateur + 'no-console': 'error', + }, + }, + + // ── Tests unitaires (Jest) ───────────────────────────────────────────────── + { + files: ['**/*.spec.js', '**/*.test.js'], + plugins: { jest: pluginJest }, + languageOptions: { + globals: { ...globals.jest }, + }, + rules: { + ...pluginJest.configs['flat/recommended'].rules, + // ...existing code... + 'jest/prefer-to-be': 'error', + 'jest/prefer-to-have-length': 'error', + 'jest/no-disabled-tests': 'error', + 'jest/no-focused-tests': 'error', + }, + }, + + // ── Tests e2e (Cypress) ──────────────────────────────────────────────────── + { + files: ['e2e/**/*.cy.js', 'e2e/e2e-support.js'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'script', + globals: { + ...globals.browser, + // Globals Cypress/Mocha + cy: 'readonly', + Cypress: 'readonly', + describe: 'readonly', + context: 'readonly', + it: 'readonly', + before: 'readonly', + beforeEach: 'readonly', + after: 'readonly', + afterEach: 'readonly', + expect: 'readonly', + require: 'readonly', + }, + }, + rules: { + // ...existing code... + }, + }, +]; diff --git a/package-lock.json b/package-lock.json index 1954efa..8e81bd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,9 +31,14 @@ "devDependencies": { "@babel/core": "^7.29.0", "@babel/preset-env": "^7.29.0", + "@eslint/js": "^10.0.1", "babel-jest": "^30.3.0", "babel-loader": "^10.1.1", "cypress": "^15.12.0", + "eslint": "^10.0.3", + "eslint-plugin-jest": "^29.15.0", + "eslint-plugin-n": "^17.24.0", + "globals": "^17.4.0", "jest": "^30.3.0", "prettier": "3.8.1", "webpack": "^5.105.4", @@ -1840,6 +1845,173 @@ "tslib": "^2.4.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@hapi/address": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", @@ -1888,6 +2060,58 @@ "@hapi/hoek": "^11.0.2" } }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2616,6 +2840,13 @@ "@types/estree": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2714,6 +2945,199 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "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/@typescript-eslint/typescript-estree": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "eslint-visitor-keys": "^5.0.0" + }, + "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", @@ -3230,6 +3654,16 @@ "acorn": "^8.14.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -4711,6 +5145,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -4948,37 +5389,376 @@ "node": ">= 0.4" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", + "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-compat-utils/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "29.15.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.15.0.tgz", + "integrity": "sha512-ZCGr7vTH2WSo2hrK5oM2RULFmMruQ7W3cX7YfwoTiPfzTGTFBMmrVIz45jZHd++cGKj/kWf02li/RhTGcANJSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.0.0" + }, + "engines": { + "node": "^20.12.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "jest": "*", + "typescript": ">=4.8.4 <6.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-n": { + "version": "17.24.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.24.0.tgz", + "integrity": "sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.5.0", + "enhanced-resolve": "^5.17.1", + "eslint-plugin-es-x": "^7.8.0", + "get-tsconfig": "^4.8.1", + "globals": "^15.11.0", + "globrex": "^0.1.2", + "ignore": "^5.3.2", + "semver": "^7.6.3", + "ts-declaration-location": "^1.0.6" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": ">=8.23.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-n/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, "engines": { - "node": ">=6" + "node": ">=10.13.0" } }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": ">=8.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { @@ -4995,6 +5775,29 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -5162,6 +5965,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -5231,6 +6041,19 @@ "node": ">=0.8.0" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5269,6 +6092,27 @@ "flat": "cli.js" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -5461,6 +6305,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -5528,6 +6385,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5731,6 +6608,16 @@ } ] }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -5917,8 +6804,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -5946,8 +6832,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -6915,6 +7800,13 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -6935,6 +7827,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -7006,6 +7905,16 @@ "node": ">= 12" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -7025,6 +7934,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7482,6 +8405,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ospath": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", @@ -7676,6 +8617,16 @@ "node": ">= 6" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", @@ -7757,6 +8708,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -7953,6 +8914,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -8599,6 +9570,54 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -8670,6 +9689,55 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-declaration-location": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz", + "integrity": "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==", + "dev": true, + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/ts-declaration-location" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": ">=4.0.0" + } + }, + "node_modules/ts-declaration-location/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -8696,6 +9764,19 @@ "dev": true, "license": "Unlicense" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -8718,6 +9799,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", @@ -8847,6 +9943,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -9047,6 +10153,16 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index bbd8702..656862e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "test:unit": "./node_modules/.bin/jest", "test:e2e": "./node_modules/.bin/cypress run --config-file ./e2e/cypress.config.js", "test:e2e:gui": "./node_modules/.bin/cypress open --config-file ./e2e/cypress.config.js", + "lint": "./node_modules/.bin/eslint .", + "lint:fix": "./node_modules/.bin/eslint . --fix", "format": "./node_modules/.bin/prettier --write --list-different './**/**+(.js|.css)'", "man": "mkdir -p man && pandoc docs/user-manual.md -f markdown -t man -s --lua-filter man/manfilter.lua --include-before-body man/cosma.1.before --include-after-body man/cosma.1.after --metadata author=\"\" -o man/cosma.1", "watch:front": "./node_modules/.bin/webpack build --config ./webpack-front.config.mjs --mode development --watch", @@ -54,9 +56,14 @@ "devDependencies": { "@babel/core": "^7.29.0", "@babel/preset-env": "^7.29.0", + "@eslint/js": "^10.0.1", "babel-jest": "^30.3.0", "babel-loader": "^10.1.1", "cypress": "^15.12.0", + "eslint": "^10.0.3", + "eslint-plugin-jest": "^29.15.0", + "eslint-plugin-n": "^17.24.0", + "globals": "^17.4.0", "jest": "^30.3.0", "prettier": "3.8.1", "webpack": "^5.105.4", diff --git a/webpack-back.config.mjs b/webpack-back.config.mjs index 29f30dc..6a876c0 100644 --- a/webpack-back.config.mjs +++ b/webpack-back.config.mjs @@ -1,6 +1,6 @@ import webpack from 'webpack'; import path from 'node:path'; -import { fileURLToPath } from 'url'; +import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/webpack-front.config.mjs b/webpack-front.config.mjs index 218b1d9..8cfbea5 100644 --- a/webpack-front.config.mjs +++ b/webpack-front.config.mjs @@ -1,5 +1,5 @@ import path from 'node:path'; -import { fileURLToPath } from 'url'; +import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url));