Sync built files (#17379)

as title, upload built files to local storage

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
martmull
2026-01-23 13:43:53 +01:00
committed by GitHub
parent 30620c79fd
commit 0091ef5f6c
134 changed files with 1051 additions and 416 deletions
@@ -1,11 +1,11 @@
import { runAppBuild } from '@/cli/__tests__/integration/utils/run-app-build.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { join } from 'path';
import { runAppBuild } from '@/cli/__tests__/integration/utils/run-app-build.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineManifestTests } from '../app-dev/tests/manifest.tests';
import { defineFrontComponentsTests } from '../app-dev/tests/front-components.tests';
import { defineFunctionsTests } from '../app-dev/tests/functions.tests';
import { defineManifestTests } from '../app-dev/tests/manifest.tests';
import { defineConsoleOutputTests } from './tests/console-output.tests';
const APP_PATH = join(__dirname, '../..');
@@ -33,7 +33,10 @@ export const defineConsoleOutputTests = (
});
it('should contain front-components-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'front-components-watch');
const output = getOutputByPrefix(
getResult().output,
'front-components-watch',
);
expect(output).toContain('[front-components-watch] 🎨 Building...');
expect(output).toContain('[front-components-watch] ✓ Built');
@@ -3,9 +3,9 @@ import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-
import { join } from 'path';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineFrontComponentsTests } from './tests/front-components.tests';
import { defineFunctionsTests } from './tests/functions.tests';
import { defineManifestTests } from './tests/manifest.tests';
import { defineManifestTests } from '../app-dev/tests/manifest.tests';
import { defineFrontComponentsTests } from '../app-dev/tests/front-components.tests';
import { defineFunctionsTests } from '../app-dev/tests/functions.tests';
const APP_PATH = join(__dirname, '../..');
@@ -0,0 +1,376 @@
import { FieldType } from '@/application';
import type { ApplicationManifest } from 'twenty-shared/application';
import { PermissionFlagType } from 'twenty-shared/constants';
export const EXPECTED_MANIFEST: ApplicationManifest = {
sources: {},
application: {
applicationVariables: {
DEFAULT_RECIPIENT_NAME: {
description: 'Default recipient name for postcards',
isSecret: false,
universalIdentifier: '19e94e59-d4fe-4251-8981-b96d0a9f74de',
value: 'Alex Karp',
},
},
description: 'A simple hello world app',
displayName: 'Hello World',
functionRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
icon: 'IconWorld',
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
},
frontComponents: [
{
builtComponentPath: 'front-components/src/root.front-component.mjs',
builtComponentChecksum: '[checksum]',
componentName: 'RootComponent',
description: 'A root-level front component',
name: 'root-component',
sourceComponentPath: 'src/root.front-component.tsx',
universalIdentifier: 'a0a1a2a3-a4a5-4000-8000-000000000001',
},
{
builtComponentPath:
'front-components/src/components/card.front-component.mjs',
builtComponentChecksum: '[checksum]',
componentName: 'CardDisplay',
description: 'A component using an external component file',
name: 'card-component',
sourceComponentPath: 'src/components/card.front-component.tsx',
universalIdentifier: 'i0i1i2i3-i4i5-4000-8000-000000000001',
},
{
builtComponentPath:
'front-components/src/components/greeting.front-component.mjs',
builtComponentChecksum: '[checksum]',
componentName: 'GreetingComponent',
description: 'A component that uses greeting utility',
name: 'greeting-component',
sourceComponentPath: 'src/components/greeting.front-component.tsx',
universalIdentifier: 'h0h1h2h3-h4h5-4000-8000-000000000001',
},
{
builtComponentPath:
'front-components/src/components/test.front-component.mjs',
builtComponentChecksum: '[checksum]',
componentName: 'TestComponent',
description: 'A test front component',
name: 'test-component',
sourceComponentPath: 'src/components/test.front-component.tsx',
universalIdentifier: 'f1234567-abcd-4000-8000-000000000001',
},
],
objectExtensions: [
{
fields: [
{
description: 'Priority level for the post card (1-10)',
label: 'Priority',
name: 'priority',
type: FieldType.NUMBER,
universalIdentifier: '7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d',
},
{
description: 'Post card category',
label: 'Category',
name: 'category',
options: [
{
color: 'blue',
label: 'Personal',
position: 0,
value: 'PERSONAL',
},
{
color: 'green',
label: 'Business',
position: 1,
value: 'BUSINESS',
},
{
color: 'orange',
label: 'Promotional',
position: 2,
value: 'PROMOTIONAL',
},
],
type: FieldType.SELECT,
universalIdentifier: '8b9c0d1e-2f3a-4b5c-6d7e-8f9a0b1c2d3e',
},
],
targetObject: {
nameSingular: 'postCard',
},
},
],
objects: [
{
description: 'A simple root-level object',
fields: [
{
label: 'Title',
name: 'title',
type: FieldType.TEXT,
universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000002',
},
{
label: 'Body',
name: 'body',
type: FieldType.TEXT,
universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000003',
},
],
icon: 'IconNote',
labelPlural: 'Root notes',
labelSingular: 'Root note',
namePlural: 'rootNotes',
nameSingular: 'rootNote',
universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000001',
},
{
description: 'A post card object',
fields: [
{
description: "Postcard's content",
icon: 'IconAbc',
label: 'Content',
name: 'content',
type: FieldType.TEXT,
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
},
{
icon: 'IconUser',
label: 'Recipient name',
name: 'recipientName',
type: FieldType.FULL_NAME,
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
},
{
icon: 'IconHome',
label: 'Recipient address',
name: 'recipientAddress',
type: FieldType.ADDRESS,
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
},
{
defaultValue: "'DRAFT'",
icon: 'IconSend',
label: 'Status',
name: 'status',
options: [
{
color: 'gray',
label: 'Draft',
position: 0,
value: 'DRAFT',
},
{
color: 'orange',
label: 'Sent',
position: 1,
value: 'SENT',
},
{
color: 'green',
label: 'Delivered',
position: 2,
value: 'DELIVERED',
},
{
color: 'orange',
label: 'Returned',
position: 3,
value: 'RETURNED',
},
],
type: FieldType.SELECT,
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
},
{
defaultValue: null,
icon: 'IconCheck',
isNullable: true,
label: 'Delivered at',
name: 'deliveredAt',
type: FieldType.DATE_TIME,
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
},
],
icon: 'IconMail',
labelPlural: 'Post cards',
labelSingular: 'Post card',
namePlural: 'postCards',
nameSingular: 'postCard',
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
},
],
packageJson: {
name: 'rich-app',
version: '0.1.0',
license: 'MIT',
engines: {
node: '^24.5.0',
npm: 'please-use-yarn',
yarn: '>=4.0.2',
},
packageManager: 'yarn@4.9.2',
scripts: {
'auth:login': 'twenty auth:login',
'auth:logout': 'twenty auth:logout',
'auth:status': 'twenty auth:status',
'auth:switch': 'twenty auth:switch',
'auth:list': 'twenty auth:list',
'app:dev': 'twenty app:dev',
'app:build': 'twenty app:build',
'app:sync': 'twenty app:sync',
'entity:add': 'twenty entity:add',
'app:generate': 'twenty app:generate',
'function:logs': 'twenty function:logs',
'function:execute': 'twenty function:execute',
'app:uninstall': 'twenty app:uninstall',
help: 'twenty help',
lint: 'eslint',
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': 'latest',
},
devDependencies: {
typescript: '^5.9.3',
'@types/node': '^24.7.2',
'@types/react': '^19.0.2',
react: '^19.0.2',
eslint: '^9.32.0',
'typescript-eslint': '^8.50.0',
},
},
roles: [
{
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
canBeAssignedToUsers: true,
canDestroyAllObjectRecords: false,
canReadAllObjectRecords: true,
canSoftDeleteAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canUpdateAllSettings: false,
description: 'A simple root-level role',
label: 'Root role',
universalIdentifier: 'c0c1c2c3-c4c5-4000-8000-000000000001',
},
{
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
canBeAssignedToUsers: false,
canDestroyAllObjectRecords: false,
canReadAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canUpdateAllSettings: false,
description: 'Default role for function Twenty client',
fieldPermissions: [
{
canReadFieldValue: false,
canUpdateFieldValue: false,
fieldName: 'content',
objectNameSingular: 'postCard',
},
],
label: 'Default function role',
objectPermissions: [
{
canDestroyObjectRecords: false,
canReadObjectRecords: true,
canSoftDeleteObjectRecords: false,
canUpdateObjectRecords: true,
objectNameSingular: 'postCard',
},
],
permissionFlags: [PermissionFlagType.APPLICATIONS],
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
},
],
functions: [
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'functions/src/root.function.mjs',
handlerName: 'rootHandler',
name: 'root-function',
sourceHandlerPath: 'src/root.function.ts',
timeoutSeconds: 5,
triggers: [
{
httpMethod: 'GET',
isAuthRequired: false,
path: '/root',
type: 'route',
universalIdentifier: 'f0f1f2f3-f4f5-4000-8000-000000000002',
},
],
universalIdentifier: 'f0f1f2f3-f4f5-4000-8000-000000000001',
},
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'functions/src/functions/greeting.function.mjs',
handlerName: 'greetingHandler',
name: 'greeting-function',
sourceHandlerPath: 'src/functions/greeting.function.ts',
timeoutSeconds: 5,
triggers: [
{
httpMethod: 'GET',
isAuthRequired: false,
path: '/greet',
type: 'route',
universalIdentifier: 'g0g1g2g3-g4g5-4000-8000-000000000002',
},
],
universalIdentifier: 'g0g1g2g3-g4g5-4000-8000-000000000001',
},
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'functions/src/functions/test-function-2.function.mjs',
handlerName: 'testFunction2',
name: 'test-function-2',
sourceHandlerPath: 'src/utils/test-function-2.util.ts',
timeoutSeconds: 2,
triggers: [
{
pattern: '0 0 1 1 *',
type: 'cron',
universalIdentifier: '9fd0dda9-4664-4fbc-9656-509f4477b9ff',
},
],
universalIdentifier: 'eb3ffc98-88ec-45d4-9b4a-56833b219ccb',
},
{
builtHandlerChecksum: '[checksum]',
builtHandlerPath: 'functions/src/functions/test-function.function.mjs',
handlerName: 'handler',
name: 'test-function',
sourceHandlerPath: 'src/functions/test-function.function.ts',
timeoutSeconds: 2,
triggers: [
{
forwardedRequestHeaders: ['signature'],
httpMethod: 'GET',
isAuthRequired: false,
path: '/post-card/create',
type: 'route',
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
},
{
pattern: '0 0 1 1 *',
type: 'cron',
universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
},
{
eventName: 'person.created',
type: 'databaseEvent',
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
},
],
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
},
],
};
@@ -8,7 +8,9 @@ export const defineConsoleOutputTests = (
it('should contain init messages', () => {
const output = getOutputByPrefix(getResult().output, 'init');
expect(output).toContain('[init] 🚀 Starting Twenty Application Development Mode');
expect(output).toContain(
'[init] 🚀 Starting Twenty Application Development Mode',
);
expect(output).toContain('[init] 📁 App Path:');
});
@@ -33,7 +35,10 @@ export const defineConsoleOutputTests = (
});
it('should contain front-components-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'front-components-watch');
const output = getOutputByPrefix(
getResult().output,
'front-components-watch',
);
expect(output).toContain('[front-components-watch] 🎨 Building...');
expect(output).toContain('[front-components-watch] ✓ Built');
@@ -4,7 +4,10 @@ import { join } from 'path';
export const defineFrontComponentsTests = (appPath: string): void => {
describe('front-components', () => {
it('should have built front components preserving source path structure', async () => {
const frontComponentsDir = join(appPath, '.twenty/output/front-components');
const frontComponentsDir = join(
appPath,
'.twenty/output/front-components',
);
const files = await fs.readdir(frontComponentsDir, { recursive: true });
const sortedFiles = files.map((f) => f.toString()).sort();
@@ -2,7 +2,7 @@ import * as fs from 'fs-extra';
import { join } from 'path';
import { normalizeManifestForComparison } from '@/cli/__tests__/integration/utils/normalize-manifest.util';
import expectedManifest from '../manifest.expected.json';
import { EXPECTED_MANIFEST } from '../expected-manifest';
export const defineManifestTests = (appPath: string): void => {
const manifestOutputPath = join(appPath, '.twenty/output/manifest.json');
@@ -16,7 +16,7 @@ export const defineManifestTests = (appPath: string): void => {
const { sources: _sources, ...sanitizedManifest } = manifest;
expect(normalizeManifestForComparison(sanitizedManifest)).toEqual(
normalizeManifestForComparison(expectedManifest),
normalizeManifestForComparison(EXPECTED_MANIFEST),
);
for (const fn of manifest.functions) {
@@ -36,7 +36,9 @@ export const defineManifestTests = (appPath: string): void => {
const manifest = await fs.readJson(manifestOutputPath);
expect(manifest?.application.displayName).toBe('Hello World');
expect(manifest?.application.description).toBe('A simple hello world app');
expect(manifest?.application.description).toBe(
'A simple hello world app',
);
});
it('should load all entity types', async () => {
@@ -1,7 +1,7 @@
import { join } from 'path';
import { runAppBuild } from '../../../../integration/utils/run-app-build.util';
import { type RunCliCommandResult } from '../../../../integration/utils/run-cli-command.util';
import { runAppBuild } from '@/cli/__tests__/integration/utils/run-app-build.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineFrontComponentsTests } from '../app-dev/tests/front-components.tests';
import { defineFunctionsTests } from '../app-dev/tests/functions.tests';
@@ -33,7 +33,10 @@ export const defineConsoleOutputTests = (
});
it('should contain front-components-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'front-components-watch');
const output = getOutputByPrefix(
getResult().output,
'front-components-watch',
);
expect(output).toContain('[front-components-watch] 🎨 Building...');
expect(output).toContain('[front-components-watch] ✓ Built');
@@ -1,7 +1,7 @@
import { join } from 'path';
import { runAppDev } from '../../../../integration/utils/run-app-dev.util';
import { type RunCliCommandResult } from '../../../../integration/utils/run-cli-command.util';
import { runAppDev } from '@/cli/__tests__/integration/utils/run-app-dev.util';
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
import { defineConsoleOutputTests } from './tests/console-output.tests';
import { defineFrontComponentsTests } from './tests/front-components.tests';
import { defineFunctionsTests } from './tests/functions.tests';
@@ -8,7 +8,9 @@ export const defineConsoleOutputTests = (
it('should contain init messages', () => {
const output = getOutputByPrefix(getResult().output, 'init');
expect(output).toContain('[init] 🚀 Starting Twenty Application Development Mode');
expect(output).toContain(
'[init] 🚀 Starting Twenty Application Development Mode',
);
expect(output).toContain('[init] 📁 App Path:');
});
@@ -33,7 +35,10 @@ export const defineConsoleOutputTests = (
});
it('should contain front-components-watch messages', () => {
const output = getOutputByPrefix(getResult().output, 'front-components-watch');
const output = getOutputByPrefix(
getResult().output,
'front-components-watch',
);
expect(output).toContain('[front-components-watch] 🎨 Building...');
expect(output).toContain('[front-components-watch] ✓ Built');
@@ -4,7 +4,10 @@ import { join } from 'path';
export const defineFrontComponentsTests = (appPath: string): void => {
describe('front-components', () => {
it('should have built front components at root level', async () => {
const frontComponentsDir = join(appPath, '.twenty/output/front-components');
const frontComponentsDir = join(
appPath,
'.twenty/output/front-components',
);
const files = await fs.readdir(frontComponentsDir, { recursive: true });
const sortedFiles = files.map((f) => f.toString()).sort();
@@ -8,10 +8,7 @@ export const defineFunctionsTests = (appPath: string): void => {
const files = await fs.readdir(functionsDir, { recursive: true });
const sortedFiles = files.map((f) => f.toString()).sort();
expect(sortedFiles).toEqual([
'my.function.mjs',
'my.function.mjs.map',
]);
expect(sortedFiles).toEqual(['my.function.mjs', 'my.function.mjs.map']);
});
});
};
@@ -16,14 +16,21 @@ export const defineManifestTests = (appPath: string): void => {
it('should have correct manifest content', async () => {
const manifestPath = join(appPath, '.twenty/output/manifest.json');
const manifest: ApplicationManifest = await fs.readJSON(manifestPath);
const expectedPath = join(appPath, '__integration__/app-dev/manifest.expected.json');
const expectedPath = join(
appPath,
'__integration__/app-dev/manifest.expected.json',
);
const expected: ApplicationManifest = await fs.readJSON(expectedPath);
expect(manifest.application).toEqual(expected.application);
expect(manifest.objects).toEqual(expected.objects);
expect(normalizeManifestForComparison({ functions: manifest.functions }).functions).toEqual(
normalizeManifestForComparison({ functions: expected.functions }).functions,
expect(
normalizeManifestForComparison({ functions: manifest.functions })
.functions,
).toEqual(
normalizeManifestForComparison({ functions: expected.functions })
.functions,
);
for (const fn of manifest.functions) {
@@ -33,9 +40,13 @@ export const defineManifestTests = (appPath: string): void => {
}
expect(
normalizeManifestForComparison({ frontComponents: manifest.frontComponents }).frontComponents,
normalizeManifestForComparison({
frontComponents: manifest.frontComponents,
}).frontComponents,
).toEqual(
normalizeManifestForComparison({ frontComponents: expected.frontComponents }).frontComponents,
normalizeManifestForComparison({
frontComponents: expected.frontComponents,
}).frontComponents,
);
for (const component of manifest.frontComponents ?? []) {
@@ -1,4 +1,4 @@
import { type TwentyConfig } from '@/cli/utilities/config/services/config.service';
import { type TwentyConfig } from '@/cli/utilities/config/config-service';
export const testConfig: TwentyConfig = {
apiUrl: 'http://localhost:3000',
@@ -1,4 +1,4 @@
import { ConfigService } from '@/cli/utilities/config/services/config.service';
import { ConfigService } from '@/cli/utilities/config/config-service';
import { testConfig } from '@/cli/__tests__/e2e/constants/testConfig';
import { vi, beforeAll, afterAll } from 'vitest';
@@ -1,6 +1,13 @@
export type LogPrefix = 'init' | 'manifest-watch' | 'functions-watch' | 'front-components-watch';
export type LogPrefix =
| 'init'
| 'manifest-watch'
| 'functions-watch'
| 'front-components-watch';
export const getOutputByPrefix = (output: string, prefix: LogPrefix): string => {
export const getOutputByPrefix = (
output: string,
prefix: LogPrefix,
): string => {
const prefixPattern = `[${prefix}]`;
const lines = output.split('\n');
@@ -1,7 +1,13 @@
// Loose type for JSON manifest imports where enum values are inferred as strings
type JsonManifestInput = {
functions?: Array<{ builtHandlerChecksum?: string | null; [key: string]: unknown }>;
frontComponents?: Array<{ builtComponentChecksum?: string | null; [key: string]: unknown }>;
functions?: Array<{
builtHandlerChecksum?: string | null;
[key: string]: unknown;
}>;
frontComponents?: Array<{
builtComponentChecksum?: string | null;
[key: string]: unknown;
}>;
[key: string]: unknown;
};
@@ -20,4 +26,5 @@ export const normalizeManifestForComparison = <T extends JsonManifestInput>(
? '[checksum]'
: null,
})),
sources: {}, // removing sources for now, waiting compressed file implementation
});
@@ -1,11 +1,16 @@
import { runCliCommand, type RunCliCommandResult } from './run-cli-command.util';
import {
runCliCommand,
type RunCliCommandResult,
} from './run-cli-command.util';
export type RunAppBuildOptions = {
appPath: string;
timeout?: number;
};
export const runAppBuild = (options: RunAppBuildOptions): Promise<RunCliCommandResult> => {
export const runAppBuild = (
options: RunAppBuildOptions,
): Promise<RunCliCommandResult> => {
const { appPath, timeout = 30000 } = options;
// app:build runs once and exits, so we don't wait for specific output
@@ -1,14 +1,18 @@
import { runCliCommand, type RunCliCommandResult } from './run-cli-command.util';
import {
runCliCommand,
type RunCliCommandResult,
} from './run-cli-command.util';
export type RunAppDevOptions = {
appPath: string;
timeout?: number;
};
export const runAppDev = (options: RunAppDevOptions): Promise<RunCliCommandResult> => {
export const runAppDev = (
options: RunAppDevOptions,
): Promise<RunCliCommandResult> => {
const { appPath, timeout = 30000 } = options;
return runCliCommand({
command: 'app:dev',
args: [appPath],
@@ -23,12 +23,7 @@ export type RunCliCommandResult = {
export const runCliCommand = (
options: RunCliCommandOptions,
): Promise<RunCliCommandResult> => {
const {
command,
args = [],
waitForOutput,
timeout = 30000,
} = options;
const { command, args = [], waitForOutput, timeout = 30000 } = options;
return new Promise((resolve) => {
// Run from CLI directory to use twenty-sdk's tsconfig paths
@@ -68,7 +63,7 @@ export const runCliCommand = (
setTimeout(() => {
child.kill();
resolve({ success: true, output });
}, 500);
}, 1500);
}
});
@@ -1,16 +1,18 @@
export const sanitizeOutput = (output: string): string => {
return output
// Remove ANSI color codes
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
// Normalize file paths (replace any absolute path to a test app)
.replace(/\/[^\s]+\/__tests__\/apps\/[^/]+/g, '<APP_PATH>')
// Normalize timestamps
.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/g, '<TIMESTAMP>')
// Normalize durations
.replace(/\d+ms/g, '<DURATION>')
// Trim trailing whitespace from each line
.split('\n')
.map((line) => line.trimEnd())
.join('\n')
.trim();
return (
output
// Remove ANSI color codes
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
// Normalize file paths (replace any absolute path to a test app)
.replace(/\/[^\s]+\/__tests__\/apps\/[^/]+/g, '<APP_PATH>')
// Normalize timestamps
.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/g, '<TIMESTAMP>')
// Normalize durations
.replace(/\d+ms/g, '<DURATION>')
// Trim trailing whitespace from each line
.split('\n')
.map((line) => line.trimEnd())
.join('\n')
.trim()
);
};