Files
twenty/packages/twenty-front-component-renderer/scripts/front-component-stories/build-source-examples.ts
T
Raphaël Bosi 9c9c34fccf Remove twenty-ui-deprecated and migrate frontend to twenty-ui (#21596)
Migrates `twenty-front`, `twenty-sdk`, and
`twenty-front-component-renderer` from `twenty-ui-deprecated` to
`twenty-ui` (mechanical import swap — the packages have API parity) and
deletes the deprecated package along with its workspace/CI/config
wiring.

Also adds `@linaria/react`/`@linaria/core` as direct deps of
`twenty-front` (it used them transitively via the deprecated package).

Note: move the required status check from `ci-ui-status-check` to
`ci-new-ui-status-check`.

Argos: the Storybook box-model/button-reset baseline shift (the bulk of
the visual diffs) is isolated in #21665 — Storybook now loads
twenty-ui's global `reset.scss`, which the production app already ships.
Once #21665 merges and this branch is rebased, the remaining Argos diffs
are component-level visual-parity items only.
2026-06-17 09:41:11 +00:00

208 lines
5.3 KiB
TypeScript

import * as esbuild from 'esbuild';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getFrontComponentBuildPlugins } from 'twenty-sdk/front-component-renderer/build';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const storiesDir = path.resolve(dirname, '../../src/__stories__');
const exampleSourcesBuiltDir = path.resolve(
dirname,
'../../src/__stories__/example-sources-built',
);
const exampleSourcesBuiltPreactDir = path.resolve(
dirname,
'../../src/__stories__/example-sources-built-preact',
);
const SOURCE_SCAN_ROOTS = ['html-tag', 'host-api', 'showcase'];
const rootNodeModules = path.resolve(dirname, '../../../../node_modules');
const twentyUiIndividualIndex = path.resolve(
dirname,
'../../../twenty-ui/dist/individual/individual-entry.js',
);
const sdkDefineIndex = path.resolve(
dirname,
'../../../twenty-sdk/dist/define/index.mjs',
);
const sdkFrontComponentIndex = path.resolve(
dirname,
'../../../twenty-sdk/dist/front-component/index.mjs',
);
const twentySharedIndividualDir = path.resolve(
dirname,
'../../../twenty-shared/dist/individual',
);
const TWENTY_SHARED_SUBMODULES = [
'ai',
'application',
'constants',
'database-events',
'metadata',
'testing',
'translations',
'types',
'utils',
'workflow',
'workspace',
];
const twentySharedAliases = Object.fromEntries(
TWENTY_SHARED_SUBMODULES.map((submodule) => [
`twenty-shared/${submodule}`,
path.join(twentySharedIndividualDir, submodule, 'index.js'),
]),
);
const storyAlias = {
react: path.join(rootNodeModules, 'react'),
'react-dom': path.join(rootNodeModules, 'react-dom'),
'twenty-sdk/define': sdkDefineIndex,
'twenty-sdk/front-component': sdkFrontComponentIndex,
'twenty-sdk/ui': twentyUiIndividualIndex,
...twentySharedAliases,
};
const ENTRY_POINT_PATTERN = /\.front-component\.tsx$/;
const findEntryPointFiles = (directory: string): string[] => {
const result: string[] = [];
if (!fs.existsSync(directory)) {
return result;
}
for (const dirent of fs.readdirSync(directory, { withFileTypes: true })) {
const absolutePath = path.join(directory, dirent.name);
if (dirent.isDirectory()) {
if (dirent.name === 'shared') {
continue;
}
result.push(...findEntryPointFiles(absolutePath));
continue;
}
if (!dirent.isFile()) {
continue;
}
if (ENTRY_POINT_PATTERN.test(dirent.name)) {
result.push(absolutePath);
}
}
return result;
};
const resolveEntryPoints = (): Record<string, string> => {
const files = SOURCE_SCAN_ROOTS.flatMap((root) =>
findEntryPointFiles(path.join(storiesDir, root)),
);
const entryPoints: Record<string, string> = {};
for (const filePath of files) {
const basename = path.basename(filePath).replace(/\.tsx$/, '');
if (entryPoints[basename] !== undefined) {
throw new Error(
`Duplicate front-component basename "${basename}" found at ${filePath} and ${entryPoints[basename]}`,
);
}
entryPoints[basename] = filePath;
}
if (Object.keys(entryPoints).length === 0) {
throw new Error(
`No front-component source files found under ${storiesDir} (scanned: ${SOURCE_SCAN_ROOTS.join(', ')})`,
);
}
return entryPoints;
};
const STORY_COMPONENTS = Object.keys(resolveEntryPoints());
type BundleSizeEntry = {
name: string;
reactBytes: number;
preactBytes: number;
};
const collectBundleSizes = (): BundleSizeEntry[] =>
STORY_COMPONENTS.map((name) => {
const reactFile = path.join(exampleSourcesBuiltDir, `${name}.mjs`);
const preactFile = path.join(exampleSourcesBuiltPreactDir, `${name}.mjs`);
return {
name,
reactBytes: fs.existsSync(reactFile) ? fs.statSync(reactFile).size : 0,
preactBytes: fs.existsSync(preactFile) ? fs.statSync(preactFile).size : 0,
};
});
const buildSourceExamples = async (): Promise<void> => {
const entryPoints = resolveEntryPoints();
const tsconfigPath = path.join(dirname, '../../tsconfig.json');
const commonOptions: esbuild.BuildOptions = {
entryPoints,
bundle: true,
splitting: false,
format: 'esm',
outExtension: { '.js': '.mjs' },
tsconfig: tsconfigPath,
jsx: 'automatic',
sourcemap: true,
metafile: true,
logLevel: 'silent',
minify: true,
alias: storyAlias,
};
fs.mkdirSync(exampleSourcesBuiltDir, { recursive: true });
await esbuild.build({
...commonOptions,
outdir: exampleSourcesBuiltDir,
plugins: getFrontComponentBuildPlugins(),
});
console.log(
`Built ${STORY_COMPONENTS.length} React story components to ${exampleSourcesBuiltDir}`,
);
fs.mkdirSync(exampleSourcesBuiltPreactDir, { recursive: true });
await esbuild.build({
...commonOptions,
outdir: exampleSourcesBuiltPreactDir,
plugins: getFrontComponentBuildPlugins({ usePreact: true }),
});
console.log(
`Built ${STORY_COMPONENTS.length} Preact story components to ${exampleSourcesBuiltPreactDir}`,
);
const sizes = collectBundleSizes();
const manifestPath = path.join(exampleSourcesBuiltDir, 'bundle-sizes.json');
fs.writeFileSync(manifestPath, JSON.stringify(sizes, null, 2));
console.log(`Wrote bundle size manifest to ${manifestPath}`);
};
buildSourceExamples().catch((error) => {
console.error('Failed to build mock components:', error);
process.exit(1);
});