diff --git a/openbas-front/eslint.config.js b/openbas-front/eslint.config.js index ebd8db5abc..65b3ad6626 100644 --- a/openbas-front/eslint.config.js +++ b/openbas-front/eslint.config.js @@ -5,6 +5,7 @@ import 'eslint-import-resolver-oxc'; import js from '@eslint/js'; import stylistic from '@stylistic/eslint-plugin'; +import vitest from '@vitest/eslint-plugin'; import i18next from 'eslint-plugin-i18next'; import importPlugin from 'eslint-plugin-import'; import playwright from 'eslint-plugin-playwright'; @@ -178,6 +179,27 @@ export default [ }, }, + // unit tests config + { + files: ['src/__tests__/**/*'], + // rules recommended by vitest + plugins: { + vitest, + }, + rules: { + ...vitest.configs.recommended.rules, + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: [ + '**/*.ts', + '**/*.tsx', + ], + }, + ], + }, + }, + // tests e2e config { files: ['tests_e2e/**/*'], @@ -190,6 +212,7 @@ export default [ { devDependencies: [ '**/*.ts', + '**/*.tsx', ], }, ], diff --git a/openbas-front/package.json b/openbas-front/package.json index 7982a45153..0949f3e01f 100644 --- a/openbas-front/package.json +++ b/openbas-front/package.json @@ -79,6 +79,7 @@ }, "devDependencies": { "@eslint/js": "9.15.0", + "@faker-js/faker": "9.3.0", "@playwright/test": "1.49.1", "@stylistic/eslint-plugin": "2.13.0", "@testing-library/dom": "10.4.0", @@ -95,7 +96,9 @@ "@types/seamless-immutable": "7.1.19", "@types/uuid": "10.0.0", "@typescript-eslint/parser": "8.18.1", + "@typescript-eslint/utils": "8.20.0", "@vitejs/plugin-react": "4.3.4", + "@vitest/eslint-plugin": "1.1.25", "chokidar": "4.0.3", "cross-env": "7.0.3", "esbuild": "0.24.0", diff --git a/openbas-front/src/__tests__/fixtures/api-types.fixtures.ts b/openbas-front/src/__tests__/fixtures/api-types.fixtures.ts new file mode 100644 index 0000000000..52ec42905d --- /dev/null +++ b/openbas-front/src/__tests__/fixtures/api-types.fixtures.ts @@ -0,0 +1,59 @@ +import { faker } from '@faker-js/faker'; + +import { Exercise, Organization, Scenario, Tag } from '../../utils/api-types'; + +export function createTagMap(numberTags: number): { [key: string]: Tag } { + const tagMap: { [key: string]: Tag } = {}; + for (let i = 0; i < numberTags; i++) { + const id = faker.string.uuid(); + tagMap[id] = { + tag_id: faker.string.uuid(), tag_name: faker.lorem.sentence(), + }; + } + return tagMap; +} + +export function createOrganisationsMap(numberTags: number): { [key: string]: Organization } { + const orgMap: { [key: string]: Organization } = {}; + for (let i = 0; i < numberTags; i++) { + const id = faker.string.uuid(); + orgMap[id] = { + organization_created_at: faker.date.recent().toISOString(), + organization_name: faker.hacker.noun(), + organization_updated_at: faker.date.soon().toISOString(), + organization_id: id, + }; + } + return orgMap; +} + +export function createExercisesMap(numberTags: number): { [key: string]: Exercise } { + const exerciseMap: { [key: string]: Exercise } = {}; + for (let i = 0; i < numberTags; i++) { + const id = faker.string.uuid(); + exerciseMap[id] = { + exercise_created_at: faker.date.recent().toISOString(), + exercise_id: id, + exercise_mail_from: faker.internet.email(), + exercise_name: faker.hacker.phrase(), + exercise_status: 'SCHEDULED', + exercise_updated_at: faker.date.soon().toISOString(), + }; + } + return exerciseMap; +} + +export function createScenarioMap(numberTags: number): { [key: string]: Scenario } { + const scenarioMap: { [key: string]: Scenario } = {}; + for (let i = 0; i < numberTags; i++) { + const id = faker.string.uuid(); + scenarioMap[id] = { + scenario_created_at: faker.date.recent().toISOString(), + scenario_id: id, + scenario_mail_from: faker.internet.email(), + scenario_name: faker.hacker.phrase(), + scenario_updated_at: faker.date.soon().toISOString(), + }; + } + return scenarioMap; +} diff --git a/openbas-front/src/__tests__/utils/Environment.test.tsx b/openbas-front/src/__tests__/utils/Environment.test.tsx new file mode 100644 index 0000000000..9196e0a22c --- /dev/null +++ b/openbas-front/src/__tests__/utils/Environment.test.tsx @@ -0,0 +1,442 @@ +import { faker } from '@faker-js/faker'; +import { describe, expect, it } from 'vitest'; + +import { exportData } from '../../utils/Environment'; +import { + createExercisesMap, + createOrganisationsMap, + createScenarioMap, + createTagMap, +} from '../fixtures/api-types.fixtures'; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +type testobj = { [key: string]: any }; +function createObjWithDefaultKeys(objtype: string): testobj { + const obj: testobj = {}; + ['name', 'extra_prop_1', 'extra_prop_2'].forEach((prop) => { + obj[`${objtype}_${prop}`] = faker.lorem.sentence(); + }); + return obj; +} + +describe('exportData tests', () => { + describe('when exporting a test object', () => { + const objtype = 'testobj'; + + describe('when only a single key from filter found in object', () => { + const obj = createObjWithDefaultKeys(objtype); + + const keys = [ + `${objtype}_name`, + ]; + const result = exportData( + objtype, + keys, + [obj], + ); + const line = result[0]; + + it('returns line with single column', async () => { + expect(line[`${objtype}_name`]).toBe(obj[`${objtype}_name`]); + }); + + it('returns line with no other keys than specified', () => { + Object.keys(obj).forEach(k => + keys.includes(k) + ? expect(Object.keys(line)).toContain(k) + : expect(Object.keys(line)).not.toContain(k), + ); + }); + }); + describe('when testobj_type is null', () => { + const obj = createObjWithDefaultKeys(objtype); + + obj[`${objtype}_type`] = null; + + const keys = [ + `${objtype}_name`, + `${objtype}_type`, + ]; + const result = exportData( + objtype, + keys, + [obj], + ); + const line = result[0]; + + it('sets testobj_type to deleted', () => { + expect(line[`${objtype}_type`]).toBe('deleted'); + }); + }); + + describe('when object does not have tags', () => { + const obj = createObjWithDefaultKeys(objtype); + + const keys = [ + `${objtype}_name`, + `${objtype}_tags`, + ]; + const result = exportData( + objtype, + keys, + [obj], + createTagMap(3), + ); + const line = result[0]; + it('does not incorporate tags in line', () => { + expect(Object.keys(line)).not.toContain(`${objtype}_tags`); + }); + }); + + describe('when object has tags', () => { + const obj = createObjWithDefaultKeys(objtype); + const tagMap = createTagMap(3); + obj[`${objtype}_tags`] = Object.keys(tagMap); + + // the goal is to concatenate tag names in the export + const expected_tag_names = Object.keys(tagMap) + .map(k => tagMap[k].tag_name) + .join(','); + + const keys = [ + `${objtype}_name`, + `${objtype}_tags`, + ]; + const result = exportData( + objtype, + keys, + [obj], + tagMap, + ); + + const line = result[0]; + + it('has key _tags in line', () => { + expect(Object.keys(line)).toContain(`${objtype}_tags`); + }); + + it('incorporates matching tags from map into line', () => { + expect(line[`${objtype}_tags`]).toBe(expected_tag_names); + }); + }); + + describe('when object has unknown tag', () => { + const obj = createObjWithDefaultKeys(objtype); + const tagMap = createTagMap(3); + obj[`${objtype}_tags`] = [faker.string.uuid(), faker.string.uuid()]; // not found in tag map + + // the goal is to concatenate tag names in the export + const expected_tag_names = ''; + + const keys = [ + `${objtype}_name`, + `${objtype}_tags`, + ]; + + const result = exportData( + objtype, + keys, + [obj], + tagMap, + ); + + const line = result[0]; + + it('has key _tags in line', () => { + expect(Object.keys(line)).toContain(`${objtype}_tags`); + }); + + it('incorporates matching tags from map into line', () => { + expect(line[`${objtype}_tags`]).toBe(expected_tag_names); + }); + }); + + describe('when object does not have organisation', () => { + const obj = createObjWithDefaultKeys(objtype); + + const keys = [ + `${objtype}_name`, + `${objtype}_organization`, + ]; + + const result = exportData( + objtype, + keys, + [obj], + ); + + const line = result[0]; + + it('does not incorporate orgs in line', () => { + expect(Object.keys(line)).not.toContain(`${objtype}_organization`); + }); + }); + + describe('when object has organizations', () => { + const obj = createObjWithDefaultKeys(objtype); + const orgMap = createOrganisationsMap(3); + obj[`${objtype}_organization`] = Object.keys(orgMap)[1]; + + // the goal is to concatenate org names in the export + const expected_org_name = orgMap[Object.keys(orgMap)[1]].organization_name; + + const keys = [ + `${objtype}_name`, + `${objtype}_organization`, + ]; + const result = exportData( + objtype, + keys, + [obj], + null, // tagMap + orgMap, + ); + + const line = result[0]; + + it('has key _organization in line', () => { + expect(Object.keys(line)).toContain(`${objtype}_organization`); + }); + + it('incorporates matching orgs from map into line', () => { + expect(line[`${objtype}_organization`]).toBe(expected_org_name); + }); + }); + + describe('when object has unknown organisation', () => { + const obj = createObjWithDefaultKeys(objtype); + const orgMap = createOrganisationsMap(3); + obj[`${objtype}_organization`] = faker.string.uuid(); // not found in org map + + // the goal is to concatenate tag names in the export + const expected_org_name = ''; + + const keys = [ + `${objtype}_name`, + `${objtype}_organization`, + ]; + const result = exportData( + objtype, + keys, + [obj], + null, // tagMap + orgMap, + ); + + const line = result[0]; + + it('has key _organization in line', () => { + expect(Object.keys(line)).toContain(`${objtype}_organization`); + }); + + it('incorporates matching org from map into line', () => { + expect(line[`${objtype}_organization`]).toBe(expected_org_name); + }); + }); + + describe('when object does not have exercises', () => { + const obj = createObjWithDefaultKeys(objtype); + + const keys = [ + `${objtype}_name`, + `${objtype}_exercises`, + ]; + const result = exportData( + objtype, + keys, + [obj], + ); + + const line = result[0]; + + it('does not incorporate exercises in line', () => { + expect(Object.keys(line)).not.toContain(`${objtype}_exercises`); + }); + }); + + describe('when object has exercises', () => { + const obj = createObjWithDefaultKeys(objtype); + const exerciseMap = createExercisesMap(3); + obj[`${objtype}_exercises`] = Object.keys(exerciseMap); + + // the goal is to concatenate tag names in the export + const expected_exercise_names = Object.keys(exerciseMap) + .map(k => exerciseMap[k].exercise_name) + .join(','); + + const keys = [ + `${objtype}_name`, + `${objtype}_exercises`, + ]; + const result = exportData( + objtype, + keys, + [obj], + null, // tagMap + null, // orgMap + exerciseMap, + ); + + const line = result[0]; + + it('has key _exercises in line', () => { + expect(Object.keys(line)).toContain(`${objtype}_exercises`); + }); + + it('incorporates matching tags from map into line', () => { + expect(line[`${objtype}_exercises`]).toBe(expected_exercise_names); + }); + }); + + describe('when object has unknown exercise', () => { + const obj = createObjWithDefaultKeys(objtype); + const exerciseMap = createExercisesMap(3); + obj[`${objtype}_exercises`] = [faker.string.uuid(), faker.string.uuid()]; // not found in tag map + + // the goal is to concatenate tag names in the export + const expected_exercise_names = ''; + + const keys = [ + `${objtype}_name`, + `${objtype}_exercises`, + ]; + const result = exportData( + objtype, + keys, + [obj], + null, // tagMap + null, // orgMap + exerciseMap, + ); + + const line = result[0]; + + it('has key _exercises in line', () => { + expect(Object.keys(line)).toContain(`${objtype}_exercises`); + }); + + it('incorporates matching exercises from map into line', () => { + expect(line[`${objtype}_exercises`]).toBe(expected_exercise_names); + }); + }); + + describe('when object does not have scenarios', () => { + const obj = createObjWithDefaultKeys(objtype); + + const keys = [ + `${objtype}_name`, + `${objtype}_scenarios`, + ]; + const result = exportData( + objtype, + keys, + [obj], + ); + + const line = result[0]; + + it('does not incorporate scenarios in line', () => { + expect(Object.keys(line)).not.toContain(`${objtype}_scenarios`); + }); + }); + + describe('when object has scenarios', () => { + const obj = createObjWithDefaultKeys(objtype); + const scenarioMap = createScenarioMap(3); + obj[`${objtype}_scenarios`] = Object.keys(scenarioMap); + + // the goal is to concatenate tag names in the export + const expected_scenario_names = Object.keys(scenarioMap) + .map(k => scenarioMap[k].scenario_name) + .join(','); + + const keys = [ + `${objtype}_name`, + `${objtype}_scenarios`, + ]; + + const result = exportData( + objtype, + keys, + [obj], + null, // tagMap + null, // orgMap + null, // exerciseMap + scenarioMap, + ); + + const line = result[0]; + + it('has key _scenarios in line', () => { + expect(Object.keys(line)).toContain(`${objtype}_scenarios`); + }); + + it('incorporates matching tags from map into line', () => { + expect(line[`${objtype}_scenarios`]).toBe(expected_scenario_names); + }); + }); + + describe('when object has unknown scenario', () => { + const obj = createObjWithDefaultKeys(objtype); + const scenarioMap = createScenarioMap(3); + obj[`${objtype}_scenarios`] = [faker.string.uuid(), faker.string.uuid()]; // not found in tag map + + // the goal is to concatenate tag names in the export + const expected_scenario_names = ''; + + const keys = [ + `${objtype}_name`, + `${objtype}_scenarios`, + ]; + + const result = exportData( + objtype, + keys, + [obj], + null, // tagMap + null, // orgMap + null, // exerciseMap + scenarioMap, + ); + + const line = result[0]; + + it('has key _scenarios in line', () => { + expect(Object.keys(line)).toContain(`${objtype}_scenarios`); + }); + + it('incorporates matching scenarios from map into line', () => { + expect(line[`${objtype}_scenarios`]).toBe(expected_scenario_names); + }); + }); + }); + + describe('when exporting an object of type inject', () => { + const objtype = 'inject'; + + describe('when inject has an object content', () => { + const obj = createObjWithDefaultKeys(objtype); + const object_content = { key1: 'content1', key2: 'content2' }; + obj[`${objtype}_content`] = object_content; + // mirror what's being done in the tested method + const expected_string_content = JSON.stringify(object_content).toString().replaceAll('"', '""'); + + const keys = [ + `${objtype}_name`, + `${objtype}_content`, + ]; + + const result = exportData( + objtype, + keys, + [obj], + ); + + const line = result[0]; + + it('transforms content into escaped string', async () => { + expect(line[`${objtype}_content`]).toBe(expected_string_content); + }); + }); + }); +}); diff --git a/openbas-front/src/utils/Environment.js b/openbas-front/src/utils/Environment.js index dcddfe8f51..ab00da4f03 100644 --- a/openbas-front/src/utils/Environment.js +++ b/openbas-front/src/utils/Environment.js @@ -33,7 +33,7 @@ const isEmptyPath = R.isNil(window.BASE_PATH) || R.isEmpty(window.BASE_PATH); const contextPath = isEmptyPath || window.BASE_PATH === '/' ? '' : window.BASE_PATH; export const APP_BASE_PATH = isEmptyPath || contextPath.startsWith('/') ? contextPath : `/${contextPath}`; -export const fileUri = fileImport => `${APP_BASE_PATH}${fileImport}`; // No slash here, will be replace by the builder +export const fileUri = fileImport => `${APP_BASE_PATH}${fileImport}`; // No slash here, will be replaced by the builder // Export const escape = value => value?.toString().replaceAll('"', '""'); @@ -58,28 +58,28 @@ export const exportData = ( if (entry[`${type}_tags`]) { entry = R.assoc( `${type}_tags`, - entry[`${type}_tags`].map(t => tagsMap[t]?.tag_name), + entry[`${type}_tags`].map(t => tagsMap[t]?.tag_name).filter(x => !!x), entry, ); } if (entry[`${type}_exercises`]) { entry = R.assoc( `${type}_exercises`, - entry[`${type}_exercises`].map(e => exercisesMap[e]?.exercise_name), + entry[`${type}_exercises`].map(e => exercisesMap[e]?.exercise_name).filter(x => !!x), entry, ); } if (entry[`${type}_scenarios`]) { entry = R.assoc( `${type}_scenarios`, - entry[`${type}_scenarios`].map(e => scenariosMap[e]?.scenario_name), + entry[`${type}_scenarios`].map(e => scenariosMap[e]?.scenario_name).filter(x => !!x), entry, ); } if (entry[`${type}_organization`]) { entry = R.assoc( `${type}_organization`, - organizationsMap[entry[`${type}_organization`]]?.organization_name, + organizationsMap[entry[`${type}_organization`]]?.organization_name || '', entry, ); } diff --git a/openbas-front/yarn.lock b/openbas-front/yarn.lock index 5fa3ac138a..0bf4c48e0c 100644 --- a/openbas-front/yarn.lock +++ b/openbas-front/yarn.lock @@ -1672,6 +1672,13 @@ __metadata: languageName: node linkType: hard +"@faker-js/faker@npm:9.3.0": + version: 9.3.0 + resolution: "@faker-js/faker@npm:9.3.0" + checksum: 10c0/6528e2f0bf0abc315780024534074a449e01e7f581f1a50a20ee7103d29d842e1c4d7dd6b27aa173f668308bf55a1d64c2547286c0c4e5a8e08d0b8269aaedc7 + languageName: node + linkType: hard + "@floating-ui/core@npm:^1.6.0": version: 1.6.8 resolution: "@floating-ui/core@npm:1.6.8" @@ -3310,6 +3317,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.20.0": + version: 8.20.0 + resolution: "@typescript-eslint/scope-manager@npm:8.20.0" + dependencies: + "@typescript-eslint/types": "npm:8.20.0" + "@typescript-eslint/visitor-keys": "npm:8.20.0" + checksum: 10c0/a8074768d06c863169294116624a45c19377ff0b8635ad5fa4ae673b43cf704d1b9b79384ceef0ff0abb78b107d345cd90fe5572354daf6ad773fe462ee71e6a + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.18.1": version: 8.18.1 resolution: "@typescript-eslint/type-utils@npm:8.18.1" @@ -3339,6 +3356,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.20.0": + version: 8.20.0 + resolution: "@typescript-eslint/types@npm:8.20.0" + checksum: 10c0/21292d4ca089897015d2bf5ab99909a7b362902f63f4ba10696676823b50d00c7b4cd093b4b43fba01d12bc3feca3852d2c28528c06d8e45446b7477887dbee7 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.15.0": version: 8.15.0 resolution: "@typescript-eslint/typescript-estree@npm:8.15.0" @@ -3376,6 +3400,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.20.0": + version: 8.20.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.20.0" + dependencies: + "@typescript-eslint/types": "npm:8.20.0" + "@typescript-eslint/visitor-keys": "npm:8.20.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^2.0.0" + peerDependencies: + typescript: ">=4.8.4 <5.8.0" + checksum: 10c0/54a2c1da7d1c5f7e865b941e8a3c98eb4b5f56ed8741664a84065173bde9602cdb8866b0984b26816d6af885c1528311c11e7286e869ed424483b74366514cbd + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:8.18.1": version: 8.18.1 resolution: "@typescript-eslint/utils@npm:8.18.1" @@ -3391,6 +3433,21 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:8.20.0": + version: 8.20.0 + resolution: "@typescript-eslint/utils@npm:8.20.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.20.0" + "@typescript-eslint/types": "npm:8.20.0" + "@typescript-eslint/typescript-estree": "npm:8.20.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.8.0" + checksum: 10c0/dd36c3b22a2adde1e1462aed0c8b4720f61859b4ebb0c3ef935a786a6b1cb0ec21eb0689f5a8debe8db26d97ebb979bab68d6f8fe7b0098e6200a485cfe2991b + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:^8.13.0": version: 8.15.0 resolution: "@typescript-eslint/utils@npm:8.15.0" @@ -3428,6 +3485,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.20.0": + version: 8.20.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.20.0" + dependencies: + "@typescript-eslint/types": "npm:8.20.0" + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10c0/e95d8b2685e8beb6637bf2e9d06e4177a400d3a2b142ba749944690f969ee3186b750082fd9bf34ada82acf1c5dd5970201dfd97619029c8ecca85fb4b50dbd8 + languageName: node + linkType: hard + "@uiw/copy-to-clipboard@npm:~1.0.12": version: 1.0.17 resolution: "@uiw/copy-to-clipboard@npm:1.0.17" @@ -3496,6 +3563,23 @@ __metadata: languageName: node linkType: hard +"@vitest/eslint-plugin@npm:1.1.25": + version: 1.1.25 + resolution: "@vitest/eslint-plugin@npm:1.1.25" + peerDependencies: + "@typescript-eslint/utils": ">= 8.0" + eslint: ">= 8.57.0" + typescript: ">= 5.0.0" + vitest: "*" + peerDependenciesMeta: + typescript: + optional: true + vitest: + optional: true + checksum: 10c0/9707dbad11d86136a36c6c820fea063cb96e5fa6f8111ad24d3a9894df5594154da9a28ab51858a158ccaff4f49fbd79baada57b49d4891f98d5251cd53c90cb + languageName: node + linkType: hard + "@vitest/expect@npm:2.1.1": version: 2.1.1 resolution: "@vitest/expect@npm:2.1.1" @@ -9051,6 +9135,7 @@ __metadata: "@emotion/react": "npm:11.14.0" "@emotion/styled": "npm:11.14.0" "@eslint/js": "npm:9.15.0" + "@faker-js/faker": "npm:9.3.0" "@fontsource/geologica": "npm:5.1.0" "@fontsource/ibm-plex-sans": "npm:5.1.0" "@hookform/resolvers": "npm:3.10.0" @@ -9079,8 +9164,10 @@ __metadata: "@types/seamless-immutable": "npm:7.1.19" "@types/uuid": "npm:10.0.0" "@typescript-eslint/parser": "npm:8.18.1" + "@typescript-eslint/utils": "npm:8.20.0" "@uiw/react-md-editor": "npm:4.0.5" "@vitejs/plugin-react": "npm:4.3.4" + "@vitest/eslint-plugin": "npm:1.1.25" "@xyflow/react": "npm:12.4.1" apexcharts: "npm:4.3.0" axios: "npm:1.7.9" @@ -11272,6 +11359,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^2.0.0": + version: 2.0.0 + resolution: "ts-api-utils@npm:2.0.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: 10c0/6165e29a5b75bd0218e3cb0f9ee31aa893dbd819c2e46dbb086c841121eb0436ed47c2c18a20cb3463d74fd1fb5af62e2604ba5971cc48e5b38ebbdc56746dfc + languageName: node + linkType: hard + "tsconfig-paths@npm:^3.15.0": version: 3.15.0 resolution: "tsconfig-paths@npm:3.15.0"