From dd9ad876a4baab774311ef4dc880124861ae68a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:53:14 +0200 Subject: [PATCH] Reduce published twenty-ui npm package size (#22087) The published `twenty-ui@1.0.0-alpha.0` tarball was ~181 MB unpacked (27 MB compressed, 2,701 files). This was a build-config issue, so a clean CI build would reproduce the same size. Main fix: externalize `@tabler/icons-react` instead of bundling it. It was forced into the bundle and aliased to the full icon barrel, inlining the entire icon set into every entry point in both ESM and CJS (~81% of the package). It stays a `dependency`, so consumers still get it; the dynamic `` registry still resolves icons at runtime. Also: - Stop emitting/shipping declaration maps (`declarationMap: false`). - Exclude the internal `dist/individual` build and `*.map` from the tarball via `files` (it still builds locally for `twenty-front-component-renderer`). - Clean up the stale `files` / `project.json` build outputs at their source, `scripts/generateBarrels.ts`. - Add a `pack-size` CI guard (30 MB unpacked budget) and wire `size` + `pack-size` into `ci-ui.yaml`. Result: ~181 MB to ~2.3 MB unpacked (0.40 MB tarball, 400 files). All export subpaths, types, and icon rendering verified intact in both module formats. Review in cubic --- .github/workflows/ci-ui.yaml | 2 +- .../constants/NumberDataModelSelectOptions.ts | 6 +-- packages/twenty-ui/.size-limit.json | 14 ++--- packages/twenty-ui/package.json | 21 ++------ packages/twenty-ui/project.json | 53 ++++++------------- packages/twenty-ui/scripts/checkPackSize.ts | 47 ++++++++++++++++ packages/twenty-ui/scripts/generateBarrels.ts | 22 ++++---- packages/twenty-ui/src/accessibility/index.ts | 1 + packages/twenty-ui/tsconfig.lib.json | 2 +- packages/twenty-ui/vite.config.ts | 3 +- 10 files changed, 87 insertions(+), 84 deletions(-) create mode 100644 packages/twenty-ui/scripts/checkPackSize.ts diff --git a/.github/workflows/ci-ui.yaml b/.github/workflows/ci-ui.yaml index 391baa35e2..27df639e0a 100644 --- a/.github/workflows/ci-ui.yaml +++ b/.github/workflows/ci-ui.yaml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - task: [lint, typecheck, test] + task: [lint, typecheck, test, size, pack-size] steps: - name: Fetch custom Github Actions and base branch history uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/constants/NumberDataModelSelectOptions.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/constants/NumberDataModelSelectOptions.ts index ecca1ed9c4..0cfb2b57da 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/constants/NumberDataModelSelectOptions.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/constants/NumberDataModelSelectOptions.ts @@ -1,19 +1,15 @@ import { type FieldNumberVariant } from '@/object-record/record-field/ui/types/FieldMetadata'; import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; -import { type ForwardRefExoticComponent, type RefAttributes } from 'react'; import { IconLetterK, IconNumber9, IconPercentage, type IconComponent, - type IconComponentProps, } from 'twenty-ui/icon'; type NumberDataModelSelectOptions = { - Icon: ForwardRefExoticComponent< - IconComponentProps & RefAttributes - >; + Icon: IconComponent; label: MessageDescriptor; value: FieldNumberVariant; }; diff --git a/packages/twenty-ui/.size-limit.json b/packages/twenty-ui/.size-limit.json index e5be2efeef..40c37d2625 100644 --- a/packages/twenty-ui/.size-limit.json +++ b/packages/twenty-ui/.size-limit.json @@ -18,7 +18,7 @@ { "name": "testing", "path": "dist/testing.mjs", - "limit": "188 kB" + "limit": "50 kB" }, { "name": "data-display", @@ -38,7 +38,7 @@ { "name": "surfaces", "path": "dist/surfaces.mjs", - "limit": "100 kB" + "limit": "70 kB" }, { "name": "feedback", @@ -53,7 +53,7 @@ { "name": "layout", "path": "dist/layout.mjs", - "limit": "60 kB" + "limit": "25 kB" }, { "name": "navigation", @@ -63,17 +63,17 @@ { "name": "json-visualizer", "path": "dist/json-visualizer.mjs", - "limit": "173 kB" + "limit": "30 kB" }, { "name": "theme", "path": "dist/theme.mjs", - "limit": "55 kB" + "limit": "25 kB" }, { "name": "theme-constants", "path": "dist/theme-constants.mjs", - "limit": "20 kB" + "limit": "12 kB" }, { "name": "utilities", @@ -83,6 +83,6 @@ { "name": "style.css", "path": "dist/style.css", - "limit": "40 kB" + "limit": "20 kB" } ] diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index 1fc8b1777d..a2dfc1dce4 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -1,6 +1,6 @@ { "name": "twenty-ui", - "version": "1.0.0-alpha.0", + "version": "1.0.0-alpha.1", "description": "Twenty's shared React UI component library.", "license": "AGPL-3.0", "repository": { @@ -92,22 +92,9 @@ }, "files": [ "dist", - "accessibility", - "assets", - "data-display", - "feedback", - "icon", - "input", - "json-visualizer", - "layout", - "navigation", - "styles", - "surfaces", - "testing", - "theme", - "theme-constants", - "typography", - "utilities" + "!dist/individual", + "!dist/individual/**", + "!dist/**/*.map" ], "exports": { ".": { diff --git a/packages/twenty-ui/project.json b/packages/twenty-ui/project.json index 6ed5f3a7c9..eeee3375c5 100644 --- a/packages/twenty-ui/project.json +++ b/packages/twenty-ui/project.json @@ -5,44 +5,7 @@ "projectType": "library", "tags": ["scope:shared"], "targets": { - "build": { - "dependsOn": ["^build"], - "outputs": [ - "{projectRoot}/dist", - "{projectRoot}/accessibility/package.json", - "{projectRoot}/accessibility/dist", - "{projectRoot}/assets/package.json", - "{projectRoot}/assets/dist", - "{projectRoot}/data-display/package.json", - "{projectRoot}/data-display/dist", - "{projectRoot}/feedback/package.json", - "{projectRoot}/feedback/dist", - "{projectRoot}/icon/package.json", - "{projectRoot}/icon/dist", - "{projectRoot}/input/package.json", - "{projectRoot}/input/dist", - "{projectRoot}/json-visualizer/package.json", - "{projectRoot}/json-visualizer/dist", - "{projectRoot}/layout/package.json", - "{projectRoot}/layout/dist", - "{projectRoot}/navigation/package.json", - "{projectRoot}/navigation/dist", - "{projectRoot}/styles/package.json", - "{projectRoot}/styles/dist", - "{projectRoot}/surfaces/package.json", - "{projectRoot}/surfaces/dist", - "{projectRoot}/testing/package.json", - "{projectRoot}/testing/dist", - "{projectRoot}/theme/package.json", - "{projectRoot}/theme/dist", - "{projectRoot}/theme-constants/package.json", - "{projectRoot}/theme-constants/dist", - "{projectRoot}/typography/package.json", - "{projectRoot}/typography/dist", - "{projectRoot}/utilities/package.json", - "{projectRoot}/utilities/dist" - ] - }, + "build": { "dependsOn": ["^build"], "outputs": ["{projectRoot}/dist"] }, "generateBarrels": { "executor": "nx:run-commands", "cache": true, @@ -74,6 +37,20 @@ "options": { "cwd": "{projectRoot}", "command": "npx size-limit" }, "configurations": { "why": { "command": "npx size-limit --why" } } }, + "pack-size": { + "executor": "nx:run-commands", + "cache": true, + "dependsOn": ["build"], + "inputs": [ + "{projectRoot}/dist", + "{projectRoot}/package.json", + "{projectRoot}/scripts/checkPackSize.ts" + ], + "options": { + "cwd": "{projectRoot}", + "command": "tsx scripts/checkPackSize.ts" + } + }, "clean": { "executor": "nx:run-commands", "options": { "command": "rimraf {projectRoot}/dist" } diff --git a/packages/twenty-ui/scripts/checkPackSize.ts b/packages/twenty-ui/scripts/checkPackSize.ts new file mode 100644 index 0000000000..0d00bcd6bf --- /dev/null +++ b/packages/twenty-ui/scripts/checkPackSize.ts @@ -0,0 +1,47 @@ +import { execSync } from 'child_process'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const MAX_UNPACKED_BYTES = 30 * 1024 * 1024; +const PACKAGE_PATH = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +); + +const formatMegabytes = (bytes: number) => + `${(bytes / (1024 * 1024)).toFixed(2)} MB`; + +type NpmPackResult = { + filename: string; + size: number; + unpackedSize: number; + entryCount: number; +}; + +const output = execSync('npm pack --dry-run --json', { + cwd: PACKAGE_PATH, + encoding: 'utf-8', + maxBuffer: 64 * 1024 * 1024, +}); + +const [packResult] = JSON.parse(output) as NpmPackResult[]; +if (packResult === undefined) { + throw new Error('npm pack did not return any package information'); +} + +const { size, unpackedSize, entryCount } = packResult; + +if (unpackedSize > MAX_UNPACKED_BYTES) { + process.stderr.write( + `twenty-ui unpacked size ${formatMegabytes(unpackedSize)} exceeds budget ${formatMegabytes( + MAX_UNPACKED_BYTES, + )} (tarball ${formatMegabytes(size)}, ${entryCount} files)\n`, + ); + process.exit(1); +} + +process.stdout.write( + `twenty-ui pack OK: tarball ${formatMegabytes(size)}, unpacked ${formatMegabytes( + unpackedSize, + )}, ${entryCount} files\n`, +); diff --git a/packages/twenty-ui/scripts/generateBarrels.ts b/packages/twenty-ui/scripts/generateBarrels.ts index 814f94fc4c..4458263b30 100644 --- a/packages/twenty-ui/scripts/generateBarrels.ts +++ b/packages/twenty-ui/scripts/generateBarrels.ts @@ -250,20 +250,17 @@ const computePackageJsonFilesAndExportsConfig = ( return { exports, typesVersions: { '*': typesVersionsEntries }, - files: ['dist', ...entrypoints], + files: [ + 'dist', + '!dist/individual', + '!dist/individual/**', + '!dist/**/*.map', + ], }; }; -const computeProjectNxBuildOutputsPath = (moduleDirectories: string[]) => { - const dynamicOutputsPath = moduleDirectories - .map(getLastPathFolder) - .flatMap((barrelName) => - ['package.json', 'dist'].map( - (subPath) => `{projectRoot}/${barrelName}/${subPath}`, - ), - ); - - return ['{projectRoot}/dist', ...dynamicOutputsPath]; +const computeProjectNxBuildOutputsPath = () => { + return ['{projectRoot}/dist']; }; const EXCLUDED_EXTENSIONS = [ @@ -531,8 +528,7 @@ const main = () => { const moduleIndexFiles = generateModuleIndexFiles(exportsByBarrel); const packageJsonConfig = computePackageJsonFilesAndExportsConfig(moduleDirectories); - const nxBuildOutputsPath = - computeProjectNxBuildOutputsPath(moduleDirectories); + const nxBuildOutputsPath = computeProjectNxBuildOutputsPath(); updateNxProjectConfigurationBuildOutputs(nxBuildOutputsPath); writeInPackageJson(packageJsonConfig); diff --git a/packages/twenty-ui/src/accessibility/index.ts b/packages/twenty-ui/src/accessibility/index.ts index 0c3e751b47..767ee9d3d9 100644 --- a/packages/twenty-ui/src/accessibility/index.ts +++ b/packages/twenty-ui/src/accessibility/index.ts @@ -9,4 +9,5 @@ export { VisibilityHidden } from './components/VisibilityHidden'; export { VisibilityHiddenInput } from './components/VisibilityHiddenInput'; +export { handleClickableElementKeyDown } from './utils/handleClickableElementKeyDown'; export { VISIBILITY_HIDDEN } from './utils/visibility-hidden'; diff --git a/packages/twenty-ui/tsconfig.lib.json b/packages/twenty-ui/tsconfig.lib.json index ead3d78666..3e2cf990d3 100644 --- a/packages/twenty-ui/tsconfig.lib.json +++ b/packages/twenty-ui/tsconfig.lib.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "declaration": true, - "declarationMap": true, + "declarationMap": false, "noEmit": false }, "include": [ diff --git a/packages/twenty-ui/vite.config.ts b/packages/twenty-ui/vite.config.ts index a7f58d46b4..b382469510 100644 --- a/packages/twenty-ui/vite.config.ts +++ b/packages/twenty-ui/vite.config.ts @@ -55,7 +55,7 @@ export default defineConfig(({ command }) => { tsconfigPath: tsConfigPath, }; - const BUNDLED_DEPS = ['@tabler/icons-react']; + const BUNDLED_DEPS: string[] = []; const externalDeps = Object.keys({ ...(packageJson.dependencies || {}), @@ -68,7 +68,6 @@ export default defineConfig(({ command }) => { '@ui/': path.resolve(__dirname, 'src') + '/', '@assets/': path.resolve(__dirname, 'src/assets') + '/', '@styles/': path.resolve(__dirname, 'src/styles') + '/', - '@tabler/icons-react': '@tabler/icons-react/dist/esm/icons/index.mjs', }, }, css: {