Необходимо реализовать конструктор страниц. Сейчас компоненты собираются с помощью esbuild в esm модули. И в конструкторе страниц собранные component.js отображаются с помощью загрузки через import(component.js).
Но в этом есть проблемы. Во-первых в сборку надо помещать все библиотеки, что увеличивает бандл. Иначе при загрузке компонента выдаются ошибки по нахождению в брaузере react:
Ошибка загрузки компонента MyApp: TypeError: Failed to resolve module specifier "react". Relative references must start with either "/", "./", or "../".
Во-вторых происходит дубляж React и каждая зависимость использует номерной React.

Здесь React28.useMemo выдает ошибку
react-dom.development.js:26923 Uncaught TypeError: Cannot read properties of null (reading 'useMemo')
at Object.useMemo4 (MyApp.js:1120:29)
at Provider (MyApp.js:46709:32)
Код сборщика esbuild:
import fs from "fs";
import path from "path";
import esbuild from "esbuild";
import { fileURLToPath } from "url";
import aliasPlugin from "esbuild-plugin-alias";
import { svgrPlugin } from "esbuild-svgr-plugin";
import { sassPlugin, postcssModules } from "esbuild-sass-plugin";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const componentsDir = path.resolve(__dirname, "components");
const args = process.argv.slice(2);
const specificComponentDir = args[0];
const getEntryPoints = () => {
if (specificComponentDir) {
const entryPoint = path.join(specificComponentDir, "index.tsx");
if (!fs.existsSync(entryPoint)) {
console.error(`Указанный компонент не найден: ${specificComponentDir}`);
process.exit(1);
}
return [entryPoint];
} else {
return fs
.readdirSync(componentsDir)
.filter((folder) =>
fs.statSync(path.join(componentsDir, folder)).isDirectory(),
)
.map((folder) => path.join(componentsDir, folder, "index.tsx"));
}
};
const entryPoints = getEntryPoints();
const customProcessingPlugin = {
name: "custom-processing",
setup(build) {
build.onLoad(
{ filter: /\.(ts|tsx)$/ },
async (args) => {
let source = await fs.promises.readFile(args.path, "utf-8");
// Добавляем React. перед хуками (useState, useEffect и т. д.)
source = source.replace(
/\b(useState|useEffect|useRef|useMemo|useCallback|useContext)\s*(<[^>]*>)?\s*\(/g,
"React.$1$2(",
);
// Заменяем process.env.NEXT_PUBLIC_IS_PRODUCTION на false
source = source.replace(/process\.env\.NEXT_PUBLIC_IS_PRODUCTION/g, "false");
return { contents: source, loader: "tsx" };
}
);
},
};
entryPoints.forEach((entryPoint) => {
const componentName = path.basename(path.dirname(entryPoint));
const outFile = path.resolve(__dirname, `dist`, `${componentName}.js`);
esbuild
.build({
entryPoints: [entryPoint],
outfile: outFile,
format: "esm",
bundle: true,
loader: { ".tsx": "tsx", ".ts": "ts", ".jpg": "file", ".png": "file" },
platform: "browser",
sourcemap: false,
minify: false,
treeShaking: true,
plugins: [
aliasPlugin({
"@lib": path.resolve(__dirname, "./ui-kit/lib"),
"@ui-kit/types": path.resolve(
__dirname,
"./ui-kit/models/types/index.ts",
),
"@icons": path.resolve(__dirname, "./ui-kit/assets/icons"),
"@images": path.resolve(__dirname, "./ui-kit/assets/images"),
"@ui-kit": path.resolve(__dirname, "./ui-kit"),
"@components": path.resolve(__dirname, "./components"),
}),
svgrPlugin(),
customProcessingPlugin,
sassPlugin({
filter: /\.module\.scss$/,
transform: postcssModules({
localsConvention: "camelCaseOnly",
}),
type: "style",
}),
sassPlugin({
type: "style",
}),
],
})
.then(() => {
console.log("Сборка завершена");
})
.catch((error) => {
console.error("Ошибка сборки:", error);
});
});