Imap perf improvement (#14629)

- Removed buffer to string conversion on message and we instead pass the
direct Buffer
- Removed redundant flattening of results
- Cached `JSDOM` and `DOMPurify` Instance
- MailParser added `skipTextToHtml` along with some other options, which
reduced parsing time from 50ms to 35ms

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
neo773
2025-09-23 12:51:30 +05:30
committed by GitHub
parent 216d72b5d7
commit 6f49481095
9 changed files with 187 additions and 293 deletions
@@ -18,6 +18,7 @@ import { ImapHandleErrorService } from 'src/modules/messaging/message-import-man
import { ImapIncrementalSyncService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-incremental-sync.service';
import { ImapMessageFetcherService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-fetcher.service';
import { ImapMessageProcessorService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-processor.service';
import { ImapMessageTextExtractorService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-text-extractor.service';
import { MessageParticipantManagerModule } from 'src/modules/messaging/message-participant-manager/message-participant-manager.module';
@Module({
@@ -41,6 +42,7 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p
ImapMessageFetcherService,
ImapMessageProcessorService,
ImapFindSentFolderService,
ImapMessageTextExtractorService,
],
exports: [
ImapGetMessagesService,
@@ -13,8 +13,7 @@ type ConnectedAccount = Pick<
>;
type FetchAllResult = {
uidsByBatch: number[][];
batchResults: MessageFetchResult[][];
results: MessageFetchResult[];
};
@Injectable()
@@ -32,8 +31,7 @@ export class ImapFetchByBatchService {
folder: string,
): Promise<FetchAllResult> {
const batchLimit = 20;
const batchResults: MessageFetchResult[][] = [];
const uidsByBatch: number[][] = [];
const results: MessageFetchResult[] = [];
this.logger.log(
`Starting optimized batch fetch for ${uids.length} messages from folder ${folder}`,
@@ -45,8 +43,6 @@ export class ImapFetchByBatchService {
for (let i = 0; i < uids.length; i += batchLimit) {
const batchUids = uids.slice(i, i + batchLimit);
uidsByBatch.push(batchUids);
try {
const batchResult =
await this.imapMessageProcessorService.processMessagesByUidsInFolder(
@@ -55,7 +51,7 @@ export class ImapFetchByBatchService {
client,
);
batchResults.push(batchResult);
results.push(...batchResult);
this.logger.log(
`Fetched batch ${Math.floor(i / batchLimit) + 1}/${Math.ceil(uids.length / batchLimit)} (${batchUids.length} messages)`,
@@ -72,14 +68,11 @@ export class ImapFetchByBatchService {
error as Error,
);
batchResults.push(errorResults);
results.push(...errorResults);
}
}
return {
uidsByBatch,
batchResults,
};
return { results };
} finally {
if (client) {
await this.imapClientProvider.closeClient(client);
@@ -7,7 +7,7 @@ import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-acco
import { computeMessageDirection } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util';
import { ImapFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-fetch-by-batch.service';
import { type MessageFetchResult } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-processor.service';
import { extractTextWithoutReplyQuotations } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/extract-message-text.util';
import { ImapMessageTextExtractorService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-text-extractor.service';
import { parseMessageId } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/parse-message-id.util';
import { type EmailAddress } from 'src/modules/messaging/message-import-manager/types/email-address';
import { type MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
@@ -25,7 +25,10 @@ type ConnectedAccountType = Pick<
export class ImapGetMessagesService {
private readonly logger = new Logger(ImapGetMessagesService.name);
constructor(private readonly fetchByBatchService: ImapFetchByBatchService) {}
constructor(
private readonly fetchByBatchService: ImapFetchByBatchService,
private readonly messageTextExtractor: ImapMessageTextExtractorService,
) {}
async getMessages(
messageIds: string[],
@@ -44,7 +47,7 @@ export class ImapGetMessagesService {
continue;
}
const { batchResults } = await this.fetchByBatchService.fetchAllByBatches(
const { results } = await this.fetchByBatchService.fetchAllByBatches(
uids,
connectedAccount,
folder,
@@ -52,8 +55,8 @@ export class ImapGetMessagesService {
this.logger.log(`IMAP fetch completed for folder: ${folder}`);
const messages = this.formatBatchResponsesAsMessages(
batchResults,
const messages = this.formatBatchResponseAsMessages(
results,
connectedAccount,
folder,
);
@@ -86,23 +89,6 @@ export class ImapGetMessagesService {
return folderToUidsMap;
}
public formatBatchResponsesAsMessages(
batchResults: MessageFetchResult[][],
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'handle' | 'handleAliases'
>,
folder: string,
): MessageWithParticipants[] {
return batchResults.flatMap((batchResult) => {
return this.formatBatchResponseAsMessages(
batchResult,
connectedAccount,
folder,
);
});
}
private formatBatchResponseAsMessages(
batchResults: MessageFetchResult[],
connectedAccount: Pick<
@@ -158,7 +144,7 @@ export class ImapGetMessagesService {
const fromHandle = fromAddresses.length > 0 ? fromAddresses[0].address : '';
const textWithoutReplyQuotations =
extractTextWithoutReplyQuotations(parsed);
this.messageTextExtractor.extractTextWithoutReplyQuotations(parsed);
const direction = computeMessageDirection(fromHandle, connectedAccount);
const text = sanitizeString(textWithoutReplyQuotations);
@@ -66,23 +66,23 @@ export class ImapMessageProcessorService {
{ uid: true },
);
const messagesData = new Map<number, FetchMessageObject>();
const processedMessages = new Map<number, MessageFetchResult>();
for await (const message of fetchResults) {
messagesData.set(message.uid, message);
const result = await this.processMessageData(
message.uid,
message,
startTime,
);
processedMessages.set(message.uid, result);
}
for (const uid of uids) {
const messageData = messagesData.get(uid);
const processedMessage = processedMessages.get(uid);
if (messageData) {
const result = await this.processMessageData(
uid,
messageData,
startTime,
);
results.push(result);
if (processedMessage) {
results.push(processedMessage);
} else {
results.push({
uid,
@@ -108,9 +108,7 @@ export class ImapMessageProcessorService {
startTime: number,
): Promise<MessageFetchResult> {
try {
const rawContent = messageData.source?.toString() || '';
if (!rawContent) {
if (!messageData.source) {
this.logger.debug(`No source content for message UID ${uid}`);
return {
@@ -120,7 +118,7 @@ export class ImapMessageProcessorService {
};
}
const parsed = await this.parseMessage(rawContent, uid);
const parsed = await this.parseMessageFromBuffer(messageData.source, uid);
const processingTime = Date.now() - startTime;
this.logger.debug(`Processed message UID ${uid} in ${processingTime}ms`);
@@ -135,12 +133,17 @@ export class ImapMessageProcessorService {
}
}
private async parseMessage(
rawContent: string,
private async parseMessageFromBuffer(
source: Buffer,
uid: number,
): Promise<ParsedMail> {
try {
return await simpleParser(rawContent);
return await simpleParser(source, {
skipTextToHtml: true,
skipImageLinks: true,
skipTextLinks: true,
keepCidLinks: false,
});
} catch (error) {
this.logger.error(`Failed to parse message UID ${uid}: ${error.message}`);
throw error;
@@ -0,0 +1,50 @@
import { Injectable } from '@nestjs/common';
import DOMPurify from 'dompurify';
import { convert } from 'html-to-text';
import { JSDOM } from 'jsdom';
import { type ParsedMail } from 'mailparser';
import * as planer from 'planer';
import { safeDecodeURIComponent } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/safe-decode-uri-component.util';
@Injectable()
export class ImapMessageTextExtractorService {
private readonly jsdomInstance: JSDOM;
private readonly purify: DOMPurify.DOMPurify;
constructor() {
this.jsdomInstance = new JSDOM('');
this.purify = DOMPurify(this.jsdomInstance.window);
}
extractTextWithoutReplyQuotations(parsed: ParsedMail): string {
if (parsed.text) {
const extractedText = planer.extractFrom(parsed.text, 'text/plain');
return safeDecodeURIComponent(extractedText);
}
if (parsed.html) {
const sanitizedHtml = this.purify.sanitize(parsed.html);
this.jsdomInstance.window.document.documentElement.innerHTML = `<html><body>${sanitizedHtml}</body></html>`;
const cleanedHtml = planer.extractFromHtml(
sanitizedHtml,
this.jsdomInstance.window.document,
);
const text = convert(cleanedHtml, {
wordwrap: false,
preserveNewlines: true,
}).trim();
const processedText = text.replace(/\u00A0/g, ' ');
return safeDecodeURIComponent(processedText);
}
return '';
}
}
@@ -1,8 +1,14 @@
import { type ParsedMail } from 'mailparser';
import { extractTextWithoutReplyQuotations } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/extract-message-text.util';
import { ImapMessageTextExtractorService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-text-extractor.service';
describe('ImapMessageTextExtractorService', () => {
let service: ImapMessageTextExtractorService;
beforeEach(() => {
service = new ImapMessageTextExtractorService();
});
describe('extractTextWithoutReplyQuotations', () => {
it('should extract text from plain text emails with lot of reply quotations', () => {
const parsed: ParsedMail = {
text: `Hi John,
@@ -96,7 +102,7 @@ Developer Support
html: false,
};
const result = extractTextWithoutReplyQuotations(parsed);
const result = service.extractTextWithoutReplyQuotations(parsed);
expect(result).toBe(`Hi John,
@@ -132,7 +138,7 @@ Developer Support`);
html: false,
};
const result = extractTextWithoutReplyQuotations(parsed);
const result = service.extractTextWithoutReplyQuotations(parsed);
expect(result).toBe('just a follow up');
});
@@ -154,7 +160,7 @@ Developer Support`);
headerLines: [],
};
const result = extractTextWithoutReplyQuotations(parsed);
const result = service.extractTextWithoutReplyQuotations(parsed);
expect(result).toBe('just a follow up');
});
@@ -167,7 +173,7 @@ Developer Support`);
html: false,
};
const result = extractTextWithoutReplyQuotations(parsed);
const result = service.extractTextWithoutReplyQuotations(parsed);
expect(result).toBe('');
});
@@ -295,7 +301,7 @@ Developer Support`);
</style></head><body><div id="inbox-html-wrapper"><div id="isPasted" fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;">Hi Sarah,</div><div fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;"><br fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;"></div><div fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;">I wanted to quickly follow up regarding the Q3 marketing campaign results. &nbsp;</div><div fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;">Weve seen a 14% increase in engagement compared to last quarter, but conversions are still slightly below target. &nbsp;</div><div fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;"><br fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;"></div><div fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;">Lets schedule a short call early next week to discuss adjustments before the Q4 push. &nbsp;</div><div fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;">Would Monday 10 AM work for you?</div><div fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;"><br fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;"></div><div fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;">Best regards, &nbsp;</div><div fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;">John</div><div fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;"><br fr-original-style="" style="user-select: inherit; scrollbar-color: var(--scrollbar-active-color) #0000; box-sizing: border-box;"></div><img class="flm-open" width="0" height="0" style="border: 0px; width: 0px; height: 0px; max-width: 100vw;" data-open-tracking-src="{{track-read-receipt}}"></div></body></html>`,
};
const result = extractTextWithoutReplyQuotations(parsed);
const result = service.extractTextWithoutReplyQuotations(parsed);
expect(result).toEqual(`Hi Sarah,
@@ -321,7 +327,7 @@ John`);
headerLines: [],
};
const result = extractTextWithoutReplyQuotations(parsed);
const result = service.extractTextWithoutReplyQuotations(parsed);
expect(result).toBe('Plain text content');
});
@@ -1,34 +0,0 @@
import DOMPurify from 'dompurify';
import { convert } from 'html-to-text';
import { JSDOM } from 'jsdom';
import { type ParsedMail } from 'mailparser';
import * as planer from 'planer';
export const extractTextWithoutReplyQuotations = (
parsed: ParsedMail,
): string => {
if (parsed.text) {
return planer.extractFrom(parsed.text, 'text/plain');
}
if (parsed.html) {
const window = new JSDOM('').window;
const purify = DOMPurify(window);
const sanitizedHtml = purify.sanitize(parsed.html);
const dom = new JSDOM(sanitizedHtml, { runScripts: 'outside-only' });
const cleanedHtml = planer.extractFromHtml(
sanitizedHtml,
dom.window.document,
);
const text = convert(cleanedHtml, {
wordwrap: false,
preserveNewlines: true,
}).trim();
return text.replace(/\u00A0/g, ' ');
}
return '';
};
@@ -0,0 +1,7 @@
export const safeDecodeURIComponent = (text: string): string => {
try {
return decodeURIComponent(text);
} catch {
return text;
}
};
+79 -198
View File
@@ -7133,47 +7133,6 @@ __metadata:
languageName: node
linkType: hard
"@jest/core@npm:30.1.3":
version: 30.1.3
resolution: "@jest/core@npm:30.1.3"
dependencies:
"@jest/console": "npm:30.1.2"
"@jest/pattern": "npm:30.0.1"
"@jest/reporters": "npm:30.1.3"
"@jest/test-result": "npm:30.1.3"
"@jest/transform": "npm:30.1.2"
"@jest/types": "npm:30.0.5"
"@types/node": "npm:*"
ansi-escapes: "npm:^4.3.2"
chalk: "npm:^4.1.2"
ci-info: "npm:^4.2.0"
exit-x: "npm:^0.2.2"
graceful-fs: "npm:^4.2.11"
jest-changed-files: "npm:30.0.5"
jest-config: "npm:30.1.3"
jest-haste-map: "npm:30.1.0"
jest-message-util: "npm:30.1.0"
jest-regex-util: "npm:30.0.1"
jest-resolve: "npm:30.1.3"
jest-resolve-dependencies: "npm:30.1.3"
jest-runner: "npm:30.1.3"
jest-runtime: "npm:30.1.3"
jest-snapshot: "npm:30.1.2"
jest-util: "npm:30.0.5"
jest-validate: "npm:30.1.0"
jest-watcher: "npm:30.1.3"
micromatch: "npm:^4.0.8"
pretty-format: "npm:30.0.5"
slash: "npm:^3.0.0"
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
checksum: 10c0/0f934a027b969daebe8e44ba3127a80c2ac1a65becc16b1f9d5a072718d950ce3c1910ce0675a98d5ef1777014e842b5bf6dd736fb9c6442a22ebfd326ae50c5
languageName: node
linkType: hard
"@jest/core@npm:^29.7.0":
version: 29.7.0
resolution: "@jest/core@npm:29.7.0"
@@ -7419,42 +7378,6 @@ __metadata:
languageName: node
linkType: hard
"@jest/reporters@npm:30.1.3, @jest/reporters@npm:^30.0.2":
version: 30.1.3
resolution: "@jest/reporters@npm:30.1.3"
dependencies:
"@bcoe/v8-coverage": "npm:^0.2.3"
"@jest/console": "npm:30.1.2"
"@jest/test-result": "npm:30.1.3"
"@jest/transform": "npm:30.1.2"
"@jest/types": "npm:30.0.5"
"@jridgewell/trace-mapping": "npm:^0.3.25"
"@types/node": "npm:*"
chalk: "npm:^4.1.2"
collect-v8-coverage: "npm:^1.0.2"
exit-x: "npm:^0.2.2"
glob: "npm:^10.3.10"
graceful-fs: "npm:^4.2.11"
istanbul-lib-coverage: "npm:^3.0.0"
istanbul-lib-instrument: "npm:^6.0.0"
istanbul-lib-report: "npm:^3.0.0"
istanbul-lib-source-maps: "npm:^5.0.0"
istanbul-reports: "npm:^3.1.3"
jest-message-util: "npm:30.1.0"
jest-util: "npm:30.0.5"
jest-worker: "npm:30.1.0"
slash: "npm:^3.0.0"
string-length: "npm:^4.0.2"
v8-to-istanbul: "npm:^9.0.1"
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
checksum: 10c0/2b027e775216804b4f3766105bd4eb3e8e31480f78cc9a409ae7d335cc21883f5bd063c66215c0bc18cbcdda98824d1b02e74593f6464caf8920f0804ce706bb
languageName: node
linkType: hard
"@jest/reporters@npm:^29.7.0":
version: 29.7.0
resolution: "@jest/reporters@npm:29.7.0"
@@ -7492,6 +7415,42 @@ __metadata:
languageName: node
linkType: hard
"@jest/reporters@npm:^30.0.2":
version: 30.1.3
resolution: "@jest/reporters@npm:30.1.3"
dependencies:
"@bcoe/v8-coverage": "npm:^0.2.3"
"@jest/console": "npm:30.1.2"
"@jest/test-result": "npm:30.1.3"
"@jest/transform": "npm:30.1.2"
"@jest/types": "npm:30.0.5"
"@jridgewell/trace-mapping": "npm:^0.3.25"
"@types/node": "npm:*"
chalk: "npm:^4.1.2"
collect-v8-coverage: "npm:^1.0.2"
exit-x: "npm:^0.2.2"
glob: "npm:^10.3.10"
graceful-fs: "npm:^4.2.11"
istanbul-lib-coverage: "npm:^3.0.0"
istanbul-lib-instrument: "npm:^6.0.0"
istanbul-lib-report: "npm:^3.0.0"
istanbul-lib-source-maps: "npm:^5.0.0"
istanbul-reports: "npm:^3.1.3"
jest-message-util: "npm:30.1.0"
jest-util: "npm:30.0.5"
jest-worker: "npm:30.1.0"
slash: "npm:^3.0.0"
string-length: "npm:^4.0.2"
v8-to-istanbul: "npm:^9.0.1"
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
checksum: 10c0/2b027e775216804b4f3766105bd4eb3e8e31480f78cc9a409ae7d335cc21883f5bd063c66215c0bc18cbcdda98824d1b02e74593f6464caf8920f0804ce706bb
languageName: node
linkType: hard
"@jest/schemas@npm:30.0.0-beta.3":
version: 30.0.0-beta.3
resolution: "@jest/schemas@npm:30.0.0-beta.3"
@@ -18873,7 +18832,7 @@ __metadata:
languageName: node
linkType: hard
"@swc/core@npm:^1.12.11, @swc/core@npm:^1.5.22, @swc/core@npm:^1.9.3":
"@swc/core@npm:^1.12.11, @swc/core@npm:^1.5.22":
version: 1.13.5
resolution: "@swc/core@npm:1.13.5"
dependencies:
@@ -18954,7 +18913,7 @@ __metadata:
languageName: node
linkType: hard
"@swc/jest@npm:^0.2.23, @swc/jest@npm:^0.2.36, @swc/jest@npm:^0.2.39":
"@swc/jest@npm:^0.2.23, @swc/jest@npm:^0.2.39":
version: 0.2.39
resolution: "@swc/jest@npm:0.2.39"
dependencies:
@@ -20815,7 +20774,7 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=8.1.0, @types/node@npm:^24.0.0, @types/node@npm:^24.3.1":
"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=8.1.0, @types/node@npm:^24.0.0":
version: 24.3.1
resolution: "@types/node@npm:24.3.1"
dependencies:
@@ -35354,7 +35313,7 @@ __metadata:
languageName: node
linkType: hard
"import-local@npm:^3.0.2, import-local@npm:^3.2.0":
"import-local@npm:^3.0.2":
version: 3.2.0
resolution: "import-local@npm:3.2.0"
dependencies:
@@ -36823,17 +36782,6 @@ __metadata:
languageName: node
linkType: hard
"jest-changed-files@npm:30.0.5":
version: 30.0.5
resolution: "jest-changed-files@npm:30.0.5"
dependencies:
execa: "npm:^5.1.1"
jest-util: "npm:30.0.5"
p-limit: "npm:^3.1.0"
checksum: 10c0/41ce090f324e8450443327f19f772a9c3f225b4b1374ba9704358f0c8b8cd91fd134fa41df7db4d278428ab974c432abc3eca9484e67c8f18528974378fddef6
languageName: node
linkType: hard
"jest-changed-files@npm:^29.7.0":
version: 29.7.0
resolution: "jest-changed-files@npm:29.7.0"
@@ -36901,31 +36849,6 @@ __metadata:
languageName: node
linkType: hard
"jest-cli@npm:30.1.3":
version: 30.1.3
resolution: "jest-cli@npm:30.1.3"
dependencies:
"@jest/core": "npm:30.1.3"
"@jest/test-result": "npm:30.1.3"
"@jest/types": "npm:30.0.5"
chalk: "npm:^4.1.2"
exit-x: "npm:^0.2.2"
import-local: "npm:^3.2.0"
jest-config: "npm:30.1.3"
jest-util: "npm:30.0.5"
jest-validate: "npm:30.1.0"
yargs: "npm:^17.7.2"
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
bin:
jest: ./bin/jest.js
checksum: 10c0/e15e811b0a14625ce70b1b3948955e9a9c7a523b2c7970effa8f924006dc4d1839b5636fa5c7c5c772d7959ed331a45b0e9d85d3b71f9a065aa6440ae3c1aa64
languageName: node
linkType: hard
"jest-cli@npm:^29.7.0":
version: 29.7.0
resolution: "jest-cli@npm:29.7.0"
@@ -36952,7 +36875,45 @@ __metadata:
languageName: node
linkType: hard
"jest-config@npm:30.1.3, jest-config@npm:^30.0.2":
"jest-config@npm:^29.7.0":
version: 29.7.0
resolution: "jest-config@npm:29.7.0"
dependencies:
"@babel/core": "npm:^7.11.6"
"@jest/test-sequencer": "npm:^29.7.0"
"@jest/types": "npm:^29.6.3"
babel-jest: "npm:^29.7.0"
chalk: "npm:^4.0.0"
ci-info: "npm:^3.2.0"
deepmerge: "npm:^4.2.2"
glob: "npm:^7.1.3"
graceful-fs: "npm:^4.2.9"
jest-circus: "npm:^29.7.0"
jest-environment-node: "npm:^29.7.0"
jest-get-type: "npm:^29.6.3"
jest-regex-util: "npm:^29.6.3"
jest-resolve: "npm:^29.7.0"
jest-runner: "npm:^29.7.0"
jest-util: "npm:^29.7.0"
jest-validate: "npm:^29.7.0"
micromatch: "npm:^4.0.4"
parse-json: "npm:^5.2.0"
pretty-format: "npm:^29.7.0"
slash: "npm:^3.0.0"
strip-json-comments: "npm:^3.1.1"
peerDependencies:
"@types/node": "*"
ts-node: ">=9.0.0"
peerDependenciesMeta:
"@types/node":
optional: true
ts-node:
optional: true
checksum: 10c0/bab23c2eda1fff06e0d104b00d6adfb1d1aabb7128441899c9bff2247bd26710b050a5364281ce8d52b46b499153bf7e3ee88b19831a8f3451f1477a0246a0f1
languageName: node
linkType: hard
"jest-config@npm:^30.0.2":
version: 30.1.3
resolution: "jest-config@npm:30.1.3"
dependencies:
@@ -36995,44 +36956,6 @@ __metadata:
languageName: node
linkType: hard
"jest-config@npm:^29.7.0":
version: 29.7.0
resolution: "jest-config@npm:29.7.0"
dependencies:
"@babel/core": "npm:^7.11.6"
"@jest/test-sequencer": "npm:^29.7.0"
"@jest/types": "npm:^29.6.3"
babel-jest: "npm:^29.7.0"
chalk: "npm:^4.0.0"
ci-info: "npm:^3.2.0"
deepmerge: "npm:^4.2.2"
glob: "npm:^7.1.3"
graceful-fs: "npm:^4.2.9"
jest-circus: "npm:^29.7.0"
jest-environment-node: "npm:^29.7.0"
jest-get-type: "npm:^29.6.3"
jest-regex-util: "npm:^29.6.3"
jest-resolve: "npm:^29.7.0"
jest-runner: "npm:^29.7.0"
jest-util: "npm:^29.7.0"
jest-validate: "npm:^29.7.0"
micromatch: "npm:^4.0.4"
parse-json: "npm:^5.2.0"
pretty-format: "npm:^29.7.0"
slash: "npm:^3.0.0"
strip-json-comments: "npm:^3.1.1"
peerDependencies:
"@types/node": "*"
ts-node: ">=9.0.0"
peerDependenciesMeta:
"@types/node":
optional: true
ts-node:
optional: true
checksum: 10c0/bab23c2eda1fff06e0d104b00d6adfb1d1aabb7128441899c9bff2247bd26710b050a5364281ce8d52b46b499153bf7e3ee88b19831a8f3451f1477a0246a0f1
languageName: node
linkType: hard
"jest-diff@npm:30.1.2, jest-diff@npm:^30.0.2":
version: 30.1.2
resolution: "jest-diff@npm:30.1.2"
@@ -37420,16 +37343,6 @@ __metadata:
languageName: node
linkType: hard
"jest-resolve-dependencies@npm:30.1.3":
version: 30.1.3
resolution: "jest-resolve-dependencies@npm:30.1.3"
dependencies:
jest-regex-util: "npm:30.0.1"
jest-snapshot: "npm:30.1.2"
checksum: 10c0/1fc144c3107ad7faae455ac13b32de0cabb8e2718a55f431246a222da86c6da539f80230814b1cbcaf22c06df704c3c00ab761d065b9a9241659757e3fd28f66
languageName: node
linkType: hard
"jest-resolve-dependencies@npm:^29.7.0":
version: 29.7.0
resolution: "jest-resolve-dependencies@npm:29.7.0"
@@ -37832,25 +37745,6 @@ __metadata:
languageName: node
linkType: hard
"jest@npm:^30.1.3":
version: 30.1.3
resolution: "jest@npm:30.1.3"
dependencies:
"@jest/core": "npm:30.1.3"
"@jest/types": "npm:30.0.5"
import-local: "npm:^3.2.0"
jest-cli: "npm:30.1.3"
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
bin:
jest: ./bin/jest.js
checksum: 10c0/6f0c18d8c94cfb3158cae90d048f38985b8aab9634f2fd8481f09ac0839697bf4a14fc3ed46d9d16e5bf653cad3af12547af62fbc46f64b3f06b32f8f8ee7d55
languageName: node
linkType: hard
"jiti@npm:1.17.1":
version: 1.17.1
resolution: "jiti@npm:1.17.1"
@@ -51500,19 +51394,6 @@ __metadata:
languageName: node
linkType: hard
"twenty-apps@workspace:packages/twenty-apps":
version: 0.0.0-use.local
resolution: "twenty-apps@workspace:packages/twenty-apps"
dependencies:
"@swc/core": "npm:^1.9.3"
"@swc/jest": "npm:^0.2.36"
"@types/jest": "npm:^30.0.0"
"@types/node": "npm:^24.3.1"
jest: "npm:^30.1.3"
typescript: "npm:^5.9.2"
languageName: unknown
linkType: soft
"twenty-cli@workspace:packages/twenty-cli":
version: 0.0.0-use.local
resolution: "twenty-cli@workspace:packages/twenty-cli"