mirror of
https://github.com/twentyhq/twenty.git
synced 2026-06-12 01:46:39 -04:00
## Summary - **Merge queue optimization**: Created a dedicated `ci-merge-queue.yaml` workflow that only runs Playwright E2E tests on `ubuntu-latest-8-cores`. Removed `merge_group` trigger from all 7 existing CI workflows (front, server, shared, website, sdk, zapier, docker-compose). The merge queue goes from ~30+ parallel jobs to a single focused E2E job. - **Label-based merge queue simulation**: Added `run-merge-queue` label support so developers can trigger the exact merge queue E2E pipeline on any open PR before it enters the queue. - **Prettier in lint**: Chained `prettier --check` into `lint` and `prettier --write` into `lint --configuration=fix` across `nx.json` defaults, `twenty-front`, and `twenty-server`. Prettier formatting errors are now caught by `lint` and fixed by `lint:fix` / `lint:diff-with-main --configuration=fix`. ## After merge (manual repo settings) Update GitHub branch protection required status checks: 1. Remove old per-workflow merge queue checks (`ci-front-status-check`, `ci-e2e-status-check`, `ci-server-status-check`, etc.) 2. Add `ci-merge-queue-status-check` as the required check for the merge queue
504 lines
13 KiB
TypeScript
504 lines
13 KiB
TypeScript
import prettier from '@prettier/sync';
|
|
import * as fs from 'fs';
|
|
import { globSync } from 'glob';
|
|
import path from 'path';
|
|
import { type Options } from 'prettier';
|
|
import slash from 'slash';
|
|
import ts from 'typescript';
|
|
|
|
// TODO prastoin refactor this file in several one into its dedicated package and make it a TypeScript CLI
|
|
|
|
const INDEX_FILENAME = 'index';
|
|
const PACKAGE_JSON_FILENAME = 'package.json';
|
|
const NX_PROJECT_CONFIGURATION_FILENAME = 'project.json';
|
|
const PACKAGE_PATH = path.resolve('packages/twenty-shared');
|
|
const SRC_PATH = path.resolve(`${PACKAGE_PATH}/src`);
|
|
const PACKAGE_JSON_PATH = path.join(PACKAGE_PATH, PACKAGE_JSON_FILENAME);
|
|
const NX_PROJECT_CONFIGURATION_PATH = path.join(
|
|
PACKAGE_PATH,
|
|
NX_PROJECT_CONFIGURATION_FILENAME,
|
|
);
|
|
|
|
const prettierConfigFile = prettier.resolveConfigFile();
|
|
if (prettierConfigFile == null) {
|
|
throw new Error('Prettier config file not found');
|
|
}
|
|
const prettierConfiguration = prettier.resolveConfig(prettierConfigFile);
|
|
const prettierFormat = (str: string, parser: Options['parser']) =>
|
|
prettier.format(str, {
|
|
...prettierConfiguration,
|
|
parser,
|
|
});
|
|
type createTypeScriptFileArgs = {
|
|
path: string;
|
|
content: string;
|
|
filename: string;
|
|
};
|
|
const createTypeScriptFile = ({
|
|
content,
|
|
path: filePath,
|
|
filename,
|
|
}: createTypeScriptFileArgs) => {
|
|
const header = `
|
|
/*
|
|
* _____ _
|
|
*|_ _|_ _____ _ __ | |_ _ _
|
|
* | | \\ \\ /\\ / / _ \\ '_ \\| __| | | | Auto-generated file
|
|
* | | \\ V V / __/ | | | |_| |_| | Any edits to this will be overridden
|
|
* |_| \\_/\\_/ \\___|_| |_|\\__|\\__, |
|
|
* |___/
|
|
*/
|
|
`;
|
|
const formattedContent = prettierFormat(
|
|
`${header}\n${content}\n`,
|
|
'typescript',
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(filePath, `${filename}.ts`),
|
|
formattedContent,
|
|
'utf-8',
|
|
);
|
|
};
|
|
|
|
const getLastPathFolder = (pathStr: string) => path.basename(pathStr);
|
|
|
|
const getSubDirectoryPaths = (directoryPath: string): string[] => {
|
|
const pattern = slash(path.join(directoryPath, '*/'));
|
|
return globSync(pattern, {
|
|
ignore: [...EXCLUDED_DIRECTORIES],
|
|
cwd: SRC_PATH,
|
|
nodir: false,
|
|
maxDepth: 1,
|
|
}).sort((a, b) => a.localeCompare(b));
|
|
};
|
|
|
|
const partitionFileExportsByType = (declarations: DeclarationOccurrence[]) => {
|
|
return declarations.reduce<{
|
|
typeAndInterfaceDeclarations: DeclarationOccurrence[];
|
|
otherDeclarations: DeclarationOccurrence[];
|
|
}>(
|
|
(acc, { kind, name }) => {
|
|
if (kind === 'type' || kind === 'interface') {
|
|
return {
|
|
...acc,
|
|
typeAndInterfaceDeclarations: [
|
|
...acc.typeAndInterfaceDeclarations,
|
|
{ kind, name },
|
|
],
|
|
};
|
|
}
|
|
|
|
return {
|
|
...acc,
|
|
otherDeclarations: [...acc.otherDeclarations, { kind, name }],
|
|
};
|
|
},
|
|
{
|
|
typeAndInterfaceDeclarations: [],
|
|
otherDeclarations: [],
|
|
},
|
|
);
|
|
};
|
|
|
|
const generateModuleIndexFiles = (exportByBarrel: ExportByBarrel[]) => {
|
|
return exportByBarrel.map<createTypeScriptFileArgs>(
|
|
({ barrel: { moduleDirectory }, allFileExports }) => {
|
|
const content = allFileExports
|
|
.sort((a, b) => a.file.localeCompare(b.file))
|
|
.map(({ exports, file }) => {
|
|
const { otherDeclarations, typeAndInterfaceDeclarations } =
|
|
partitionFileExportsByType(exports);
|
|
|
|
const fileWithoutExtension = path.parse(file).name;
|
|
const pathToImport = slash(
|
|
path.relative(
|
|
moduleDirectory,
|
|
path.join(path.dirname(file), fileWithoutExtension),
|
|
),
|
|
);
|
|
const mapDeclarationNameAndJoin = (
|
|
declarations: DeclarationOccurrence[],
|
|
) => declarations.map(({ name }) => name).join(', ');
|
|
|
|
const typeExport =
|
|
typeAndInterfaceDeclarations.length > 0
|
|
? `export type { ${mapDeclarationNameAndJoin(typeAndInterfaceDeclarations)} } from "./${pathToImport}"`
|
|
: '';
|
|
const othersExport =
|
|
otherDeclarations.length > 0
|
|
? `export { ${mapDeclarationNameAndJoin(otherDeclarations)} } from "./${pathToImport}"`
|
|
: '';
|
|
|
|
return [typeExport, othersExport]
|
|
.filter((el) => el !== '')
|
|
.join('\n');
|
|
})
|
|
.join('\n');
|
|
|
|
return {
|
|
content,
|
|
path: moduleDirectory,
|
|
filename: INDEX_FILENAME,
|
|
};
|
|
},
|
|
);
|
|
};
|
|
|
|
type JsonUpdate = Record<string, any>;
|
|
type WriteInJsonFileArgs = {
|
|
content: JsonUpdate;
|
|
file: string;
|
|
};
|
|
const updateJsonFile = ({ content, file }: WriteInJsonFileArgs) => {
|
|
const updatedJsonFile = JSON.stringify(content);
|
|
const formattedContent = prettier.format(updatedJsonFile, {
|
|
...prettierConfiguration,
|
|
filepath: file,
|
|
});
|
|
fs.writeFileSync(file, formattedContent, 'utf-8');
|
|
};
|
|
|
|
const writeInPackageJson = (update: JsonUpdate) => {
|
|
const rawJsonFile = fs.readFileSync(PACKAGE_JSON_PATH, 'utf-8');
|
|
const initialJsonFile = JSON.parse(rawJsonFile);
|
|
|
|
updateJsonFile({
|
|
file: PACKAGE_JSON_PATH,
|
|
content: {
|
|
...initialJsonFile,
|
|
...update,
|
|
},
|
|
});
|
|
};
|
|
|
|
const updateNxProjectConfigurationBuildOutputs = (outputs: JsonUpdate) => {
|
|
const rawJsonFile = fs.readFileSync(NX_PROJECT_CONFIGURATION_PATH, 'utf-8');
|
|
const initialJsonFile = JSON.parse(rawJsonFile);
|
|
|
|
updateJsonFile({
|
|
file: NX_PROJECT_CONFIGURATION_PATH,
|
|
content: {
|
|
...initialJsonFile,
|
|
targets: {
|
|
...initialJsonFile.targets,
|
|
build: {
|
|
...initialJsonFile.targets.build,
|
|
outputs,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
type ExportOccurrence = {
|
|
types: string;
|
|
import: string;
|
|
require: string;
|
|
};
|
|
type ExportsConfig = Record<string, ExportOccurrence | string>;
|
|
|
|
const generateModulePackageExports = (moduleDirectories: string[]) => {
|
|
return moduleDirectories.reduce<ExportsConfig>((acc, moduleDirectory) => {
|
|
const moduleName = getLastPathFolder(moduleDirectory);
|
|
if (moduleName === undefined) {
|
|
throw new Error(
|
|
`Should never occur, moduleName is undefined ${moduleDirectory}`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
...acc,
|
|
[`./${moduleName}`]: {
|
|
types: `./dist/${moduleName}/index.d.ts`,
|
|
import: `./dist/${moduleName}.mjs`,
|
|
require: `./dist/${moduleName}.cjs`,
|
|
},
|
|
};
|
|
}, {});
|
|
};
|
|
|
|
const computePackageJsonFilesAndExportsConfig = (
|
|
moduleDirectories: string[],
|
|
) => {
|
|
const entrypoints = moduleDirectories.map(getLastPathFolder);
|
|
const exports = {
|
|
'.': {
|
|
types: './dist/index.d.ts',
|
|
import: './dist/index.mjs',
|
|
require: './dist/index.cjs',
|
|
},
|
|
...generateModulePackageExports(moduleDirectories),
|
|
} satisfies ExportsConfig;
|
|
|
|
const typesVersionsEntries = entrypoints.reduce<Record<string, string[]>>(
|
|
(acc, moduleName) => ({
|
|
...acc,
|
|
[`${moduleName}`]: [`dist/${moduleName}/index.d.ts`],
|
|
}),
|
|
{},
|
|
);
|
|
|
|
return {
|
|
exports,
|
|
typesVersions: { '*': typesVersionsEntries },
|
|
files: ['dist', ...entrypoints],
|
|
};
|
|
};
|
|
|
|
const computeProjectNxBuildOutputsPath = (moduleDirectories: string[]) => {
|
|
const dynamicOutputsPath = moduleDirectories
|
|
.map(getLastPathFolder)
|
|
.flatMap((barrelName) =>
|
|
['package.json', 'dist'].map(
|
|
(subPath) => `{projectRoot}/${barrelName}/${subPath}`,
|
|
),
|
|
);
|
|
|
|
return ['{projectRoot}/dist', ...dynamicOutputsPath];
|
|
};
|
|
|
|
const EXCLUDED_EXTENSIONS = [
|
|
'**/*.test.ts',
|
|
'**/*.test.tsx',
|
|
'**/*.spec.ts',
|
|
'**/*.spec.tsx',
|
|
'**/*.stories.ts',
|
|
'**/*.stories.tsx',
|
|
] as const;
|
|
const EXCLUDED_DIRECTORIES = [
|
|
'**/__tests__/**',
|
|
'**/__mocks__/**',
|
|
'**/__stories__/**',
|
|
'**/internal/**',
|
|
] as const;
|
|
const EXCLUDED_FILES = ['**/get-function-input-schema.ts'] as const;
|
|
const getTypeScriptFiles = (
|
|
directoryPath: string,
|
|
includeIndex: boolean = false,
|
|
): string[] => {
|
|
const pattern = slash(path.join(directoryPath, '**', '*.{ts,tsx}'));
|
|
const files = globSync(pattern, {
|
|
cwd: SRC_PATH,
|
|
nodir: true,
|
|
ignore: [
|
|
...EXCLUDED_EXTENSIONS,
|
|
...EXCLUDED_DIRECTORIES,
|
|
...EXCLUDED_FILES,
|
|
],
|
|
});
|
|
|
|
return files.filter(
|
|
(file) =>
|
|
!file.endsWith('.d.ts') &&
|
|
(includeIndex ? true : !file.endsWith('index.ts')),
|
|
);
|
|
};
|
|
|
|
const getKind = (
|
|
node: ts.VariableStatement,
|
|
): Extract<ExportKind, 'const' | 'let' | 'var'> => {
|
|
const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
|
|
if (isConst) {
|
|
return 'const';
|
|
}
|
|
|
|
const isLet = (node.declarationList.flags & ts.NodeFlags.Let) !== 0;
|
|
if (isLet) {
|
|
return 'let';
|
|
}
|
|
|
|
return 'var';
|
|
};
|
|
|
|
const extractExportsFromSourceFile = (sourceFile: ts.SourceFile) => {
|
|
const exports: DeclarationOccurrence[] = [];
|
|
|
|
const visit = (node: ts.Node): void => {
|
|
if (!ts.canHaveModifiers(node)) {
|
|
return ts.forEachChild(node, visit);
|
|
}
|
|
const modifiers = ts.getModifiers(node);
|
|
const isExport = modifiers?.some(
|
|
(mod) => mod.kind === ts.SyntaxKind.ExportKeyword,
|
|
);
|
|
|
|
if (!isExport && !ts.isExportDeclaration(node)) {
|
|
return ts.forEachChild(node, visit);
|
|
}
|
|
|
|
switch (true) {
|
|
case ts.isTypeAliasDeclaration(node):
|
|
exports.push({
|
|
kind: 'type',
|
|
name: node.name.text,
|
|
});
|
|
break;
|
|
|
|
case ts.isInterfaceDeclaration(node):
|
|
exports.push({
|
|
kind: 'interface',
|
|
name: node.name.text,
|
|
});
|
|
break;
|
|
|
|
case ts.isEnumDeclaration(node):
|
|
exports.push({
|
|
kind: 'enum',
|
|
name: node.name.text,
|
|
});
|
|
break;
|
|
|
|
case ts.isFunctionDeclaration(node) && node.name !== undefined:
|
|
exports.push({
|
|
kind: 'function',
|
|
name: node.name.text,
|
|
});
|
|
break;
|
|
|
|
case ts.isVariableStatement(node):
|
|
node.declarationList.declarations.forEach((decl) => {
|
|
const kind = getKind(node);
|
|
|
|
if (ts.isIdentifier(decl.name)) {
|
|
exports.push({
|
|
kind,
|
|
name: decl.name.text,
|
|
});
|
|
} else if (ts.isObjectBindingPattern(decl.name)) {
|
|
decl.name.elements.forEach((element) => {
|
|
if (
|
|
!ts.isBindingElement(element) ||
|
|
!ts.isIdentifier(element.name)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
exports.push({
|
|
kind,
|
|
name: element.name.text,
|
|
});
|
|
});
|
|
}
|
|
});
|
|
break;
|
|
|
|
case ts.isClassDeclaration(node) && node.name !== undefined:
|
|
exports.push({
|
|
kind: 'class',
|
|
name: node.name.text,
|
|
});
|
|
break;
|
|
case ts.isExportDeclaration(node):
|
|
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
node.exportClause.elements.forEach((element) => {
|
|
const exportName = element.name.text;
|
|
|
|
// Check both the declaration and the individual specifier for type-only exports
|
|
const isTypeExport =
|
|
node.isTypeOnly || ts.isTypeOnlyExportDeclaration(node);
|
|
if (isTypeExport) {
|
|
// should handle kind
|
|
exports.push({
|
|
kind: 'type',
|
|
name: exportName,
|
|
});
|
|
return;
|
|
}
|
|
|
|
exports.push({
|
|
kind: 'const',
|
|
name: exportName,
|
|
});
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
return ts.forEachChild(node, visit);
|
|
};
|
|
|
|
visit(sourceFile);
|
|
return exports;
|
|
};
|
|
|
|
type ExportKind =
|
|
| 'type'
|
|
| 'interface'
|
|
| 'enum'
|
|
| 'function'
|
|
| 'const'
|
|
| 'let'
|
|
| 'var'
|
|
| 'class';
|
|
type DeclarationOccurrence = { kind: ExportKind; name: string };
|
|
type FileExports = Array<{
|
|
file: string;
|
|
exports: DeclarationOccurrence[];
|
|
}>;
|
|
|
|
const findAllExports = (directoryPath: string): FileExports => {
|
|
const results: FileExports = [];
|
|
|
|
const files = getTypeScriptFiles(directoryPath);
|
|
|
|
for (const file of files) {
|
|
const sourceFile = ts.createSourceFile(
|
|
file,
|
|
fs.readFileSync(file, 'utf8'),
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
);
|
|
|
|
const exports = extractExportsFromSourceFile(sourceFile);
|
|
if (exports.length > 0) {
|
|
results.push({
|
|
file,
|
|
exports,
|
|
});
|
|
}
|
|
}
|
|
|
|
return results;
|
|
};
|
|
|
|
type ExportByBarrel = {
|
|
barrel: {
|
|
moduleName: string;
|
|
moduleDirectory: string;
|
|
};
|
|
allFileExports: FileExports;
|
|
};
|
|
const retrieveExportsByBarrel = (barrelDirectories: string[]) => {
|
|
return barrelDirectories.map<ExportByBarrel>((moduleDirectory) => {
|
|
const moduleExportsPerFile = findAllExports(moduleDirectory);
|
|
const moduleName = getLastPathFolder(moduleDirectory);
|
|
if (!moduleName) {
|
|
throw new Error(
|
|
`Should never occur moduleName not found ${moduleDirectory}`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
barrel: {
|
|
moduleName,
|
|
moduleDirectory,
|
|
},
|
|
allFileExports: moduleExportsPerFile,
|
|
};
|
|
});
|
|
};
|
|
|
|
const main = () => {
|
|
const moduleDirectories = getSubDirectoryPaths(SRC_PATH);
|
|
const exportsByBarrel = retrieveExportsByBarrel(moduleDirectories);
|
|
const moduleIndexFiles = generateModuleIndexFiles(exportsByBarrel);
|
|
const packageJsonConfig =
|
|
computePackageJsonFilesAndExportsConfig(moduleDirectories);
|
|
const nxBuildOutputsPath =
|
|
computeProjectNxBuildOutputsPath(moduleDirectories);
|
|
|
|
updateNxProjectConfigurationBuildOutputs(nxBuildOutputsPath);
|
|
writeInPackageJson(packageJsonConfig);
|
|
moduleIndexFiles.forEach(createTypeScriptFile);
|
|
};
|
|
main();
|