Fix twenty sdk build (#17729)

- remove worksapce:* dependencies from published packages
- Use common tsconfig.ts in create-twenty-app
- Increase version to 0.4.4
This commit is contained in:
martmull
2026-02-05 14:25:49 +01:00
committed by GitHub
parent f899804db3
commit de5764ede0
14 changed files with 44 additions and 1193 deletions
+8 -2
View File
@@ -11,7 +11,9 @@ import unicornPlugin from 'eslint-plugin-unicorn';
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
import jsoncParser from 'jsonc-eslint-parser';
const twentyRules = await nxPlugin.loadWorkspaceRules('packages/twenty-eslint-rules');
const twentyRules = await nxPlugin.loadWorkspaceRules(
'packages/twenty-eslint-rules',
);
export default [
// Base JavaScript configuration
@@ -65,6 +67,10 @@ export default [
sourceTag: 'scope:sdk',
onlyDependOnLibsWithTags: ['scope:sdk', 'scope:shared'],
},
{
sourceTag: 'scope:create-app',
onlyDependOnLibsWithTags: ['scope:create-app', 'scope:shared'],
},
{
sourceTag: 'scope:shared',
onlyDependOnLibsWithTags: ['scope:shared'],
@@ -197,7 +203,7 @@ export default [
plugins: {
...mdxPlugin.flat.plugins,
'@nx': nxPlugin,
'twenty': { rules: twentyRules },
twenty: { rules: twentyRules },
},
},
mdxPlugin.flatCodeBlocks,
+8 -99
View File
@@ -1,111 +1,20 @@
import js from '@eslint/js';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import prettierPlugin from 'eslint-plugin-prettier';
import baseConfig from '../../eslint.config.mjs';
export default [
js.configs.recommended,
...baseConfig,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
globals: {
// Node.js globals
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
global: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
// Browser globals that Node.js also has
URL: 'readonly',
URLSearchParams: 'readonly',
// Node.js types
NodeJS: 'readonly',
},
},
plugins: {
'@typescript-eslint': typescriptEslint,
prettier: prettierPlugin,
},
ignores: ['**/dist/**'],
},
{
files: ['**/*.{js,jsx,ts,tsx}'],
rules: {
...typescriptEslint.configs.recommended.rules,
'prettier/prettier': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'no-useless-escape': 'off',
},
},
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
global: 'readonly',
},
},
},
{
files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**/*.ts'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
globals: {
// Node.js globals
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
global: 'readonly',
// Jest globals
describe: 'readonly',
it: 'readonly',
test: 'readonly',
expect: 'readonly',
jest: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
beforeAll: 'readonly',
afterAll: 'readonly',
},
},
plugins: {
'@typescript-eslint': typescriptEslint,
prettier: prettierPlugin,
},
rules: {
...typescriptEslint.configs.recommended.rules,
'prettier/prettier': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'no-useless-escape': 'off',
'no-console': 'off',
},
},
{
ignores: ['dist/**', 'node_modules/**'],
ignores: ['src/**/*.ts', '!src/cli/**/*.ts'],
},
];
+6 -2
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.4.3",
"version": "0.4.4",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -8,7 +8,9 @@
"dist/**/*"
],
"scripts": {
"build": "npx rimraf dist && npx vite build"
"build": "npx rimraf dist && npx vite build",
"prepack": "tsx ../twenty-utils/pack-scripts/prepack.ts",
"postpack": "tsx ../twenty-utils/pack-scripts/postpack.ts"
},
"keywords": [
"twenty",
@@ -34,6 +36,7 @@
"lodash.camelcase": "^4.3.0",
"lodash.kebabcase": "^4.1.1",
"lodash.startcase": "^4.4.0",
"twenty-shared": "workspace:*",
"uuid": "^13.0.0"
},
"devDependencies": {
@@ -43,6 +46,7 @@
"@types/lodash.kebabcase": "^4.1.7",
"@types/lodash.startcase": "^4",
"@types/node": "^20.0.0",
"typescript": "^5.9.2",
"vite": "^7.0.0",
"vite-plugin-dts": "^4.5.4",
"vite-tsconfig-paths": "^4.2.1"
@@ -70,7 +70,7 @@ describe('copyBaseApplicationProject', () => {
const packageJson = await fs.readJson(packageJsonPath);
expect(packageJson.name).toBe('my-test-app');
expect(packageJson.version).toBe('0.1.0');
expect(packageJson.dependencies['twenty-sdk']).toBe('0.4.3');
expect(packageJson.dependencies['twenty-sdk']).toBe('0.4.4');
expect(packageJson.scripts['app:dev']).toBe('twenty app:dev');
});
@@ -282,7 +282,7 @@ const createPackageJson = async ({
'lint:fix': 'eslint --fix',
},
dependencies: {
'twenty-sdk': '0.4.3',
'twenty-sdk': '0.4.4',
},
devDependencies: {
typescript: '^5.9.3',
+2 -1
View File
@@ -12,7 +12,8 @@
"types": ["jest", "node"],
"paths": {
"@/*": ["./src/*"]
}
},
"jsx": "react"
},
"include": [
"src/**/*.ts",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-sdk",
"version": "0.4.3",
"version": "0.4.4",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
@@ -14,8 +14,8 @@
],
"scripts": {
"build": "npx rimraf dist && npx vite build",
"prepack": "tsx ./scripts/prepack.ts",
"postpack": "tsx ./scripts/postpack.ts"
"prepack": "tsx ../twenty-utils/pack-scripts/prepack.ts",
"postpack": "tsx ../twenty-utils/pack-scripts/postpack.ts"
},
"keywords": [
"twenty",
+2 -50
View File
@@ -1,4 +1,3 @@
import * as fs from 'fs-extra';
import path from 'path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
@@ -12,8 +11,6 @@ const entries = [
'src/front-component/index.ts',
];
export const PACKAGES_TO_VENDOR = ['twenty-ui', 'twenty-shared'];
const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => {
if (!chunk.isEntry) {
throw new Error(
@@ -32,20 +29,6 @@ const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => {
return `${modulePath}/index.${extension}`;
};
const copyTwentyPackagesInVendor = (packages: string[]) => {
return packages.map((packageName) => ({
name: `copy-${packageName}-dist`,
closeBundle: async () => {
const sharedDist = path.resolve(__dirname, `../${packageName}/dist`);
const vendorDist = path.resolve(__dirname, `dist/vendor/${packageName}`);
await fs.remove(vendorDist);
await fs.ensureDir(path.dirname(vendorDist));
await fs.copy(sharedDist, vendorDist);
},
}));
};
export default defineConfig(() => {
const tsConfigPath = path.resolve(__dirname, './tsconfig.lib.json');
@@ -61,36 +44,7 @@ export default defineConfig(() => {
tsconfigPaths({
root: __dirname,
}),
...copyTwentyPackagesInVendor(PACKAGES_TO_VENDOR),
dts({
entryRoot: './src',
tsconfigPath: tsConfigPath,
exclude: ['vite.config.ts'],
beforeWriteFile: (filePath, content) => {
const fromDir = path.dirname(filePath);
const vendorDir = path.resolve(process.cwd(), 'dist/vendor');
let rel = path
.relative(fromDir, vendorDir)
.split(path.sep)
.join(path.posix.sep);
if (!rel.startsWith('.')) rel = `./${rel}`;
const formattedContent = PACKAGES_TO_VENDOR.reduce((acc, pkg) => {
const regex = new RegExp(
`(from\\s+["'])${pkg}(\\/[^"']*)?(["'])`,
'g',
);
return acc.replace(regex, `$1${rel}/${pkg}$2$3`);
}, content);
return {
filePath,
content: formattedContent,
};
},
}),
dts({ entryRoot: './src', tsconfigPath: tsConfigPath }),
],
worker: {
format: 'iife',
@@ -124,9 +78,7 @@ export default defineConfig(() => {
warn(warning);
},
external: [
...Object.keys((packageJson as any).dependencies || {}).filter(
(dep) => !PACKAGES_TO_VENDOR.includes(dep),
),
...Object.keys((packageJson as any).dependencies || {}),
'path',
'fs',
'fs/promises',
@@ -1,494 +0,0 @@
import prettier from '@prettier/sync';
import * as fs from 'fs';
import { globSync } from 'glob';
import * as path from 'path';
import ts from 'typescript';
const prettierConfigFile = prettier.resolveConfigFile();
if (prettierConfigFile == null) {
throw new Error('Prettier config file not found');
}
const prettierConfiguration = prettier.resolveConfig(prettierConfigFile);
type DeclarationOccurrence = { kind: string; name: string };
type ExtractedExports = Array<{
file: string;
exports: DeclarationOccurrence[];
}>;
type ExtractedImports = Array<{ file: string; imports: string[] }>;
type ExportPerModule = Array<{
moduleName: string;
exports: ExtractedExports[number]['exports'];
}>;
function findAllExports(directoryPath: string): ExtractedExports {
const results: ExtractedExports = [];
const files = getTypeScriptFiles(directoryPath);
for (const file of files) {
const sourceFile = ts.createSourceFile(
file,
fs.readFileSync(file, 'utf8'),
ts.ScriptTarget.Latest,
true,
);
const exports = extractExports(sourceFile);
if (exports.length > 0) {
results.push({
file,
exports,
});
}
}
return results;
}
function findAllImports(directoryPath: string): ExtractedImports {
const results: ExtractedImports = [];
const includeIndex = true;
const files = getTypeScriptFiles(directoryPath, includeIndex);
for (const file of files) {
try {
const sourceFile = ts.createSourceFile(
file,
fs.readFileSync(file, 'utf8'),
ts.ScriptTarget.Latest,
true,
);
const imports = extractImports(sourceFile);
if (imports.length > 0) {
results.push({
file,
imports,
});
}
} catch (e) {
console.log(e);
console.log('Because of file: ', file);
throw e;
}
}
return results;
}
function getTypeScriptFiles(
directoryPath: string,
includeIndex: boolean = false,
): string[] {
const pattern = path.join(directoryPath, '**/*.{ts,tsx}');
const files = globSync(pattern);
return files.filter(
(file) =>
!file.endsWith('.d.ts') &&
(includeIndex ? true : !file.endsWith('index.ts')),
);
}
const getKind = (node: ts.VariableStatement) => {
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';
};
function extractExports(sourceFile: ts.SourceFile) {
const exports: DeclarationOccurrence[] = [];
function visit(node: ts.Node) {
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) {
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) => {
if (ts.isIdentifier(decl.name)) {
const kind = getKind(node);
exports.push({
kind,
name: decl.name.text,
});
}
});
break;
case ts.isClassDeclaration(node) && node.name !== undefined:
exports.push({
kind: 'class',
name: node.name.text,
});
break;
}
return ts.forEachChild(node, visit);
}
visit(sourceFile);
return exports;
}
function extractImports(sourceFile: ts.SourceFile): string[] {
const imports: string[] = [];
function visit(node: ts.Node) {
if (!ts.isImportDeclaration(node)) {
return ts.forEachChild(node, visit);
}
const modulePath = node.moduleSpecifier.getText(sourceFile);
// Quite static
if (modulePath !== `'twenty-shared'`) {
return ts.forEachChild(node, visit);
}
if (!node.importClause) {
return ts.forEachChild(node, visit);
}
if (!node.importClause.namedBindings) {
return ts.forEachChild(node, visit);
}
if (ts.isNamedImports(node.importClause.namedBindings)) {
const namedImports = node.importClause.namedBindings.elements.map(
(element) => {
if (element.propertyName) {
return `${element.propertyName.text} as ${element.name.text}`;
}
return element.name.text;
},
);
// imports.push(`import { ${namedImports} } from ${modulePath}`);
namedImports.forEach((namedImport) => {
imports.push(namedImport);
});
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return imports;
}
const getSubDirectoryPaths = (directoryPath: string): string[] =>
fs
.readdirSync(directoryPath)
.filter((fileOrDirectoryName) => {
const isDirectory = fs
.statSync(path.join(directoryPath, fileOrDirectoryName))
.isDirectory();
return isDirectory;
})
.map((subDirectoryName) => path.join(directoryPath, subDirectoryName));
const retrievePackageExportsPerModule = (srcPath: string) => {
const subdirectories = getSubDirectoryPaths(srcPath);
return subdirectories.map<ExportPerModule[number]>((moduleDirectory) => {
const moduleExportsPerFile = findAllExports(moduleDirectory);
const moduleName = moduleDirectory.split('/').pop();
if (!moduleName) {
throw new Error(
`Should never occurs moduleName not found ${moduleDirectory}`,
);
}
const flattenExports = Object.values(moduleExportsPerFile).flatMap(
(arr) => arr.exports,
);
return {
moduleName,
exports: flattenExports,
};
});
};
type NewImport = { barrel: string; modules: string[] };
type MappedResolution = {
newImports: Record<string, NewImport>;
file: string;
};
type MapSourceImportToBarrelArgs = {
importsPerFile: ExtractedImports;
exportsPerModule: ExportPerModule;
};
const mapSourceImportToBarrel = ({
exportsPerModule,
importsPerFile,
}: MapSourceImportToBarrelArgs): MappedResolution[] => {
const mappedResolution: MappedResolution[] = [];
for (const fileImport of importsPerFile) {
const { file, imports } = fileImport;
let result: MappedResolution = {
file,
newImports: {},
};
for (const importedDeclaration of imports) {
const findResult = exportsPerModule.find(({ exports }) =>
exports.some((el) => el.name === importedDeclaration),
);
if (findResult === undefined) {
throw new Error(
`Should never occurs no barrel exports ${importedDeclaration}`,
);
}
const { moduleName } = findResult;
if (result.newImports[moduleName]) {
result.newImports[moduleName].modules.push(importedDeclaration);
} else {
result.newImports[moduleName] = {
barrel: moduleName,
modules: [importedDeclaration],
};
}
}
mappedResolution.push(result);
}
return mappedResolution;
};
const retrieveImportFromPackageInSource = (srcPath: string) => {
return findAllImports(srcPath);
};
/**
* Inserts a new import statement at the top of a TypeScript file
* @param filePath Path to the TypeScript file
* @param importSpecifier The module to import from (e.g., 'twenty-shared/utils')
* @param namedImports Array of named imports (e.g., ['useQuery', 'useMutation'])
*/
type InsertImportAtTopArgs = {
filePath: string;
importSpecifier: string;
namedImports: string[];
};
function insertImportAtTop({
filePath,
importSpecifier,
namedImports,
}: InsertImportAtTopArgs): void {
// Read the file content
const sourceText = fs.readFileSync(filePath, 'utf8');
// Create a source file
const sourceFile = ts.createSourceFile(
filePath,
sourceText,
ts.ScriptTarget.Latest,
true,
);
// Build the new import statement
let newImport = `import { ${namedImports.join(', ')} } from '${importSpecifier}';\n`;
// Find the position to insert the import
let insertPos = 0;
// Case 1: File has imports - insert after the last import
let lastImportEnd = 0;
ts.forEachChild(sourceFile, (node) => {
if (
ts.isImportDeclaration(node) ||
ts.isImportEqualsDeclaration(node) ||
(ts.isExpressionStatement(node) &&
ts.isCallExpression(node.expression) &&
node.expression.expression.kind === ts.SyntaxKind.ImportKeyword) // Overkill ?
) {
const end = node.getEnd();
if (end > lastImportEnd) {
lastImportEnd = end;
}
}
});
if (lastImportEnd > 0) {
// Insert after the last import with a newline
insertPos = lastImportEnd;
// Check if there's already a newline after the last import
if (sourceText[insertPos] !== '\n') {
newImport = '\n' + newImport;
}
}
// Insert the new import
const updatedSourceText =
sourceText.substring(0, insertPos) +
newImport +
sourceText.substring(insertPos);
// Write back to file
fs.writeFileSync(
filePath,
prettier.format(updatedSourceText, {
parser: 'typescript',
...prettierConfiguration,
}),
'utf8',
);
}
type RemoveSpecificImports = {
filePath: string;
moduleSpecifier: string;
};
function removeSpecificImports({
filePath,
moduleSpecifier,
}: RemoveSpecificImports) {
const sourceText = fs.readFileSync(filePath, 'utf8');
const sourceFile = ts.createSourceFile(
filePath,
sourceText,
ts.ScriptTarget.Latest,
true,
);
type Replacement = {
start: number;
end: number;
newText: string;
};
let replacement: Replacement | undefined;
function visit(node: ts.Node) {
if (ts.isImportDeclaration(node)) {
const importSource = node.moduleSpecifier
.getText(sourceFile)
.replace(/^['"]|['"]$/g, '');
if (importSource === moduleSpecifier && node.importClause) {
replacement = {
start: node.getFullStart(),
end: node.getEnd(),
newText: '',
};
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
let updatedSourceText = sourceText;
if (replacement) {
const { end, newText, start } = replacement;
updatedSourceText =
updatedSourceText.substring(0, start) +
newText +
updatedSourceText.substring(end);
fs.writeFileSync(
filePath,
prettier.format(updatedSourceText, {
parser: 'typescript',
...prettierConfiguration,
}),
'utf8',
);
}
}
const migrateImports = (mappedResolutions: MappedResolution[]) => {
for (const { file, newImports } of mappedResolutions) {
for (const { barrel, modules } of Object.values(newImports)) {
// TODO could refactor to avoid double source file and read
removeSpecificImports({
filePath: file,
moduleSpecifier: 'twenty-shared',
});
insertImportAtTop({
filePath: file,
importSpecifier: `twenty-shared/${barrel}`,
namedImports: modules,
});
}
}
};
const main = () => {
const packageSrcPath = 'packages/twenty-shared/src';
const exportsPerModule = retrievePackageExportsPerModule(packageSrcPath);
const packagesToMigrate = [
'twenty-front',
'twenty-ui',
'twenty-server',
'twenty-emails',
'twenty-zapier',
];
for (const currPackage of packagesToMigrate) {
console.log(`About to run over ${currPackage}`);
const importsPerFile = retrieveImportFromPackageInSource(
`packages/${currPackage}`,
);
const mappedResolutions = mapSourceImportToBarrel({
exportsPerModule,
importsPerFile,
});
migrateImports(mappedResolutions);
console.log(`${currPackage} migrated`);
}
console.log('SUCCESSFULLY COMPLETED');
};
main();
@@ -1,527 +0,0 @@
import prettier from '@prettier/sync';
import * as fs from 'fs';
import { globSync } from 'glob';
import * as path from 'path';
import ts from 'typescript';
const prettierConfigFile = prettier.resolveConfigFile();
if (prettierConfigFile == null) {
throw new Error('Prettier config file not found');
}
const prettierConfiguration = prettier.resolveConfig(prettierConfigFile);
type DeclarationOccurrence = { kind: string; name: string };
type ExtractedExports = Array<{
file: string;
exports: DeclarationOccurrence[];
}>;
type ExtractedImports = Array<{ file: string; imports: string[] }>;
type ExportPerModule = Array<{
moduleName: string;
exports: ExtractedExports[number]['exports'];
}>;
function findAllExports(directoryPath: string): ExtractedExports {
const results: ExtractedExports = [];
const files = getTypeScriptFiles(directoryPath);
for (const file of files) {
const sourceFile = ts.createSourceFile(
file,
fs.readFileSync(file, 'utf8'),
ts.ScriptTarget.Latest,
true,
);
const exports = extractExports(sourceFile);
if (exports.length > 0) {
results.push({
file,
exports,
});
}
}
return results;
}
function findAllImports(directoryPath: string): ExtractedImports {
const results: ExtractedImports = [];
const includeIndex = true;
const files = getTypeScriptFiles(directoryPath, includeIndex);
for (const file of files) {
try {
const sourceFile = ts.createSourceFile(
file,
fs.readFileSync(file, 'utf8'),
ts.ScriptTarget.Latest,
true,
);
const imports = extractImports(sourceFile);
if (imports.length > 0) {
results.push({
file,
imports,
});
}
} catch (e) {
console.log(e);
console.log('Because of file: ', file);
throw e;
}
}
return results;
}
function getTypeScriptFiles(
directoryPath: string,
includeIndex: boolean = false,
): string[] {
const pattern = path.join(directoryPath, '**/*.{ts,tsx,d.ts}');
const files = globSync(pattern);
return files.filter(
(file) =>
(includeIndex ? true : !file.endsWith('.d.ts')) &&
(includeIndex ? true : !file.endsWith('index.ts')),
);
}
const getKind = (node: ts.VariableStatement) => {
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';
};
function extractExports(sourceFile: ts.SourceFile) {
const exports: DeclarationOccurrence[] = [];
function visit(node: ts.Node) {
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;
}
function extractImports(sourceFile: ts.SourceFile): string[] {
const imports: string[] = [];
function visit(node: ts.Node) {
if (!ts.isImportDeclaration(node)) {
return ts.forEachChild(node, visit);
}
const modulePath = node.moduleSpecifier.getText(sourceFile);
// Quite static
if (modulePath !== `'twenty-ui'` && modulePath !== '"twenty-ui"') {
return ts.forEachChild(node, visit);
}
if (!node.importClause) {
return ts.forEachChild(node, visit);
}
if (!node.importClause.namedBindings) {
return ts.forEachChild(node, visit);
}
if (ts.isNamedImports(node.importClause.namedBindings)) {
const namedImports = node.importClause.namedBindings.elements.map(
(element) => {
if (element.propertyName) {
return `${element.propertyName.text} as ${element.name.text}`;
}
return element.name.text;
},
);
// imports.push(`import { ${namedImports} } from ${modulePath}`);
namedImports.forEach((namedImport) => {
imports.push(namedImport);
});
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return imports;
}
const getSubDirectoryPaths = (directoryPath: string): string[] =>
fs
.readdirSync(directoryPath)
.filter((fileOrDirectoryName) => {
const isDirectory = fs
.statSync(path.join(directoryPath, fileOrDirectoryName))
.isDirectory();
return isDirectory;
})
.map((subDirectoryName) => path.join(directoryPath, subDirectoryName));
const retrievePackageExportsPerModule = (srcPath: string) => {
const subdirectories = getSubDirectoryPaths(srcPath);
return subdirectories.map<ExportPerModule[number]>((moduleDirectory) => {
const moduleExportsPerFile = findAllExports(moduleDirectory);
const moduleName = moduleDirectory.split('/').pop();
if (!moduleName) {
throw new Error(
`Should never occurs moduleName not found ${moduleDirectory}`,
);
}
const flattenExports = Object.values(moduleExportsPerFile).flatMap(
(arr) => arr.exports,
);
return {
moduleName,
exports: flattenExports,
};
});
};
type NewImport = { barrel: string; modules: string[] };
type MappedResolution = {
newImports: Record<string, NewImport>;
file: string;
};
type MapSourceImportToBarrelArgs = {
importsPerFile: ExtractedImports;
exportsPerModule: ExportPerModule;
};
const mapSourceImportToBarrel = ({
exportsPerModule,
importsPerFile,
}: MapSourceImportToBarrelArgs): MappedResolution[] => {
const mappedResolution: MappedResolution[] = [];
for (const fileImport of importsPerFile) {
const { file, imports } = fileImport;
let result: MappedResolution = {
file,
newImports: {},
};
for (const importedDeclaration of imports) {
const findResult = exportsPerModule.find(({ exports }) =>
exports.some((el) => el.name === importedDeclaration),
);
if (findResult === undefined) {
throw new Error(
`Should never occurs no barrel exports ${importedDeclaration}`,
);
}
const { moduleName } = findResult;
if (result.newImports[moduleName]) {
result.newImports[moduleName].modules.push(importedDeclaration);
} else {
result.newImports[moduleName] = {
barrel: moduleName,
modules: [importedDeclaration],
};
}
}
mappedResolution.push(result);
}
return mappedResolution;
};
const retrieveImportFromPackageInSource = (srcPath: string) => {
return findAllImports(srcPath);
};
/**
* Inserts a new import statement at the top of a TypeScript file
* @param filePath Path to the TypeScript file
* @param importSpecifier The module to import from (e.g., 'twenty-ui/utils')
* @param namedImports Array of named imports (e.g., ['useQuery', 'useMutation'])
*/
type InsertImportAtTopArgs = {
filePath: string;
importSpecifier: string;
namedImports: string[];
};
function insertImportAtTop({
filePath,
importSpecifier,
namedImports,
}: InsertImportAtTopArgs): void {
// Read the file content
const sourceText = fs.readFileSync(filePath, 'utf8');
// Create a source file
const sourceFile = ts.createSourceFile(
filePath,
sourceText,
ts.ScriptTarget.Latest,
true,
);
// Build the new import statement
let newImport = `import { ${namedImports.join(', ')} } from '${importSpecifier}';\n`;
// Find the position to insert the import
let insertPos = 0;
// Case 1: File has imports - insert after the last import
let lastImportEnd = 0;
ts.forEachChild(sourceFile, (node) => {
if (
ts.isImportDeclaration(node) ||
ts.isImportEqualsDeclaration(node) ||
(ts.isExpressionStatement(node) &&
ts.isCallExpression(node.expression) &&
node.expression.expression.kind === ts.SyntaxKind.ImportKeyword) // Overkill ?
) {
const end = node.getEnd();
if (end > lastImportEnd) {
lastImportEnd = end;
}
}
});
if (lastImportEnd > 0) {
// Insert after the last import with a newline
insertPos = lastImportEnd;
// Check if there's already a newline after the last import
if (sourceText[insertPos] !== '\n') {
newImport = '\n' + newImport;
}
}
// Insert the new import
const updatedSourceText =
sourceText.substring(0, insertPos) +
newImport +
sourceText.substring(insertPos);
// Write back to file
fs.writeFileSync(
filePath,
prettier.format(updatedSourceText, {
parser: 'typescript',
...prettierConfiguration,
}),
'utf8',
);
}
type RemoveSpecificImports = {
filePath: string;
moduleSpecifier: string;
};
function removeSpecificImports({
filePath,
moduleSpecifier,
}: RemoveSpecificImports) {
const sourceText = fs.readFileSync(filePath, 'utf8');
const sourceFile = ts.createSourceFile(
filePath,
sourceText,
ts.ScriptTarget.Latest,
true,
);
type Replacement = {
start: number;
end: number;
newText: string;
};
let replacement: Replacement | undefined;
function visit(node: ts.Node) {
if (ts.isImportDeclaration(node)) {
const importSource = node.moduleSpecifier
.getText(sourceFile)
.replace(/^['"]|['"]$/g, '');
if (importSource === moduleSpecifier && node.importClause) {
replacement = {
start: node.getFullStart(),
end: node.getEnd(),
newText: '',
};
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
let updatedSourceText = sourceText;
if (replacement) {
const { end, newText, start } = replacement;
updatedSourceText =
updatedSourceText.substring(0, start) +
newText +
updatedSourceText.substring(end);
fs.writeFileSync(
filePath,
prettier.format(updatedSourceText, {
parser: 'typescript',
...prettierConfiguration,
}),
'utf8',
);
}
}
const migrateImports = (mappedResolutions: MappedResolution[]) => {
for (const { file, newImports } of mappedResolutions) {
for (const { barrel, modules } of Object.values(newImports)) {
// TODO could refactor to avoid double source file and read
removeSpecificImports({
filePath: file,
moduleSpecifier: 'twenty-ui',
});
insertImportAtTop({
filePath: file,
importSpecifier: `twenty-ui/${barrel}`,
namedImports: modules,
});
}
}
};
const main = () => {
const packageSrcPath = 'packages/twenty-ui/src';
const exportsPerModule = retrievePackageExportsPerModule(packageSrcPath);
const packagesToMigrate = ['twenty-front'];
for (const currPackage of packagesToMigrate) {
console.log(`About to run over ${currPackage}`);
const importsPerFile = retrieveImportFromPackageInSource(
`packages/${currPackage}`,
);
const mappedResolutions = mapSourceImportToBarrel({
exportsPerModule,
importsPerFile,
});
migrateImports(mappedResolutions);
console.log(`${currPackage} migrated`);
}
console.log('SUCCESSFULLY COMPLETED');
};
main();
@@ -0,0 +1,7 @@
import path from 'path';
export const PKG_PATH = path.resolve(process.cwd(), 'package.json');
export const BACKUP_PATH = path.resolve(
process.cwd(),
'package.__backup__.json',
);
@@ -1,10 +1,10 @@
import fs from 'fs-extra';
import { BACKUP_PATH, PKG_PATH } from './prepack';
import { BACKUP_PATH, PKG_PATH } from './constants';
const main = async () => {
if (await fs.pathExists(BACKUP_PATH)) {
const original = await fs.readJson(BACKUP_PATH);
await fs.rm(PKG_PATH);
await fs.remove(PKG_PATH);
await fs.writeJson(PKG_PATH, original, { spaces: 2 });
await fs.remove(BACKUP_PATH);
}
@@ -1,18 +1,9 @@
import fs from 'fs-extra';
import path from 'path';
import { PACKAGES_TO_VENDOR } from '../vite.config';
export const PKG_PATH = path.resolve(process.cwd(), 'package.json');
export const BACKUP_PATH = path.resolve(
process.cwd(),
'package.__backup__.json',
);
import { BACKUP_PATH, PKG_PATH } from './constants';
const stripWorkspace = (deps = {}) =>
Object.fromEntries(
Object.entries(deps).filter(
([key, _]) => !PACKAGES_TO_VENDOR.includes(key),
),
Object.entries(deps).filter(([_, value]) => !value.startsWith('workspace:')),
);
const main = async () => {
+2
View File
@@ -31097,6 +31097,8 @@ __metadata:
lodash.camelcase: "npm:^4.3.0"
lodash.kebabcase: "npm:^4.1.1"
lodash.startcase: "npm:^4.4.0"
twenty-shared: "workspace:*"
typescript: "npm:^5.9.2"
uuid: "npm:^13.0.0"
vite: "npm:^7.0.0"
vite-plugin-dts: "npm:^4.5.4"