Files
twenty/packages/twenty-shared/scripts/generateBarrels.ts
Paul Rastoin 9ad8287dbc [REFACTOR] twenty-shared multi barrel and CJS/ESM build with preconstruct (#11083)
# Introduction

In this PR we've migrated `twenty-shared` from a `vite` app
[libary-mode](https://vite.dev/guide/build#library-mode) to a
[preconstruct](https://preconstruct.tools/) "atomic" application ( in
the future would like to introduce preconstruct to handle of all our
atomic dependencies such as `twenty-emails` `twenty-ui` etc it will be
integrated at the monorepo's root directly, would be to invasive in the
first, starting incremental via `twenty-shared`)

For more information regarding the motivations please refer to nor:
- https://github.com/twentyhq/core-team-issues/issues/587
-
https://github.com/twentyhq/core-team-issues/issues/281#issuecomment-2630949682

close https://github.com/twentyhq/core-team-issues/issues/589
close https://github.com/twentyhq/core-team-issues/issues/590

## How to test
In order to ease the review this PR will ship all the codegen at the
very end, the actual meaning full diff is `+2,411 −114`
In order to migrate existing dependent packages to `twenty-shared` multi
barrel new arch you need to run in local:
```sh
yarn tsx packages/twenty-shared/scripts/migrateFromSingleToMultiBarrelImport.ts && \
npx nx run-many -t lint --fix -p twenty-front twenty-ui twenty-server twenty-emails twenty-shared twenty-zapier
```
Note that `migrateFromSingleToMultiBarrelImport` is idempotent, it's atm
included in the PR but should not be merged. ( such as codegen will be
added before merging this script will be removed )

## Misc
- related opened issue preconstruct
https://github.com/preconstruct/preconstruct/issues/617

## Closed related PR
- https://github.com/twentyhq/twenty/pull/11028
- https://github.com/twentyhq/twenty/pull/10993
- https://github.com/twentyhq/twenty/pull/10960

## Upcoming enhancement: ( in others dedicated PRs )
- 1/ refactor generate barrel to export atomic module instead of `*`
- 2/ generate barrel own package with several files and tests
- 3/ Migration twenty-ui the same way
- 4/ Use `preconstruct` at monorepo global level

## Conclusion
As always any suggestions are welcomed !
2025-03-22 19:16:06 +01:00

254 lines
7.2 KiB
TypeScript

import prettier from '@prettier/sync';
import * as fs from 'fs';
import path from 'path';
import { Options } from 'prettier';
import slash from 'slash';
// TODO prastoin refactor this file in several one into its dedicated package and make it a TypeScript CLI
const INCLUDED_EXTENSIONS = ['.ts', '.tsx'];
const EXCLUDED_EXTENSIONS = [
'.test.ts',
'.test.tsx',
'.spec.ts',
'.spec.tsx',
'.stories.ts',
'.stories.tsx',
];
const EXCLUDED_DIRECTORIES = [
'__tests__',
'__mocks__',
'__stories__',
'internal',
];
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 = (path: string) => path.split('/').pop();
const getSubDirectoryPaths = (directoryPath: string): string[] =>
fs
.readdirSync(directoryPath)
.filter((fileOrDirectoryName) => {
const isDirectory = fs
.statSync(path.join(directoryPath, fileOrDirectoryName))
.isDirectory();
if (!isDirectory) {
return false;
}
const isExcludedDirectory =
EXCLUDED_DIRECTORIES.includes(fileOrDirectoryName);
return !isExcludedDirectory;
})
.map((subDirectoryName) => path.join(directoryPath, subDirectoryName));
const getDirectoryPathsRecursive = (directoryPath: string): string[] => [
directoryPath,
...getSubDirectoryPaths(directoryPath).flatMap(getDirectoryPathsRecursive),
];
const getFilesPaths = (directoryPath: string): string[] =>
fs.readdirSync(directoryPath).filter((filePath) => {
const isFile = fs.statSync(path.join(directoryPath, filePath)).isFile();
if (!isFile) {
return false;
}
const isIndexFile = filePath.startsWith(INDEX_FILENAME);
if (isIndexFile) {
return false;
}
const isWhiteListedExtension = INCLUDED_EXTENSIONS.some((extension) =>
filePath.endsWith(extension),
);
const isExcludedExtension = EXCLUDED_EXTENSIONS.every(
(excludedExtension) => !filePath.endsWith(excludedExtension),
);
return isWhiteListedExtension && isExcludedExtension;
});
type ComputeExportLineForGivenFileArgs = {
filePath: string;
moduleDirectory: string; // Rename
directoryPath: string; // Rename
};
const computeExportLineForGivenFile = ({
filePath,
moduleDirectory,
directoryPath,
}: ComputeExportLineForGivenFileArgs) => {
const fileNameWithoutExtension = filePath.split('.').slice(0, -1).join('.');
const pathToImport = slash(
path.relative(
moduleDirectory,
path.join(directoryPath, fileNameWithoutExtension),
),
);
// TODO refactor should extract all exports atomically please refer to https://github.com/twentyhq/core-team-issues/issues/644
return `export * from './${pathToImport}';`;
};
const generateModuleIndexFiles = (moduleDirectories: string[]) => {
return moduleDirectories.map<createTypeScriptFileArgs>((moduleDirectory) => {
const directoryPaths = getDirectoryPathsRecursive(moduleDirectory);
const content = directoryPaths
.flatMap((directoryPath) => {
const directFilesPaths = getFilesPaths(directoryPath);
return directFilesPaths.map((filePath) =>
computeExportLineForGivenFile({
directoryPath,
filePath,
moduleDirectory: moduleDirectory,
}),
);
})
.sort((a, b) => a.localeCompare(b)) // Could be removed as using prettier afterwards anw ?
.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 = prettierFormat(updatedJsonFile, 'json-stringify');
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,
},
},
},
});
};
const computePackageJsonFilesAndPreconstructConfig = (
moduleDirectories: string[],
) => {
const entrypoints = [...moduleDirectories.map(getLastPathFolder)];
return {
preconstruct: {
entrypoints: [
'./index.ts',
...entrypoints.map((module) => `./${module}/index.ts`),
],
},
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 main = () => {
const moduleDirectories = getSubDirectoryPaths(SRC_PATH);
const moduleIndexFiles = generateModuleIndexFiles(moduleDirectories);
const packageJsonPreconstructConfigAndFiles =
computePackageJsonFilesAndPreconstructConfig(moduleDirectories);
const nxBuildOutputsPath =
computeProjectNxBuildOutputsPath(moduleDirectories);
updateNxProjectConfigurationBuildOutputs(
nxBuildOutputsPath
);
writeInPackageJson(packageJsonPreconstructConfigAndFiles);
moduleIndexFiles.forEach(createTypeScriptFile);
};
main();