feat(infra): migrate to rsbuild

This commit is contained in:
deytenit committed 2026-02-12 23:39:20 +03:00
1 parent 97c6347f2e
commit 4628c043ce
95 files changed
+5582 -11282

No files matched your search

+11 -23
View File
@@ -1,7 +1,5 @@
import path from 'path';
import type { StorybookConfig } from '@storybook/react-vite';
import { mergeConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
import type { StorybookConfig } from 'storybook-react-rsbuild';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
@@ -13,28 +11,18 @@ const config: StorybookConfig = {
'@storybook/addon-coverage',
],
framework: {
name: '@storybook/react-vite',
name: 'storybook-react-rsbuild',
options: {},
},
typescript: {
check: false,
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
async viteFinal(config) {
return mergeConfig(config, {
plugins: [tailwindcss()],
resolve: {
alias: {
'#': path.resolve(__dirname, '../src'),
'@ermnvldmr/stl': path.resolve(__dirname, '../../../packages/stl/src'),
'@ermnvldmr/ui': path.resolve(__dirname, '../../../packages/ui/src'),
},
},
});
rsbuildFinal: (config) => {
config.resolve ||= {};
config.resolve.alias = {
...config.resolve.alias,
'#': path.resolve(__dirname, '../src'),
'@ermnvldmr/stl': path.resolve(__dirname, '../../../packages/stl/src'),
'@ermnvldmr/ui': path.resolve(__dirname, '../../../packages/ui/src'),
};
return config;
},
};
-25
View File
@@ -1,25 +0,0 @@
// @ts-check
import mdx from '@astrojs/mdx';
import react from '@astrojs/react';
import sitemap from '@astrojs/sitemap';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({
site: 'https://www.ermnvldmr.com',
output: 'static',
srcDir: './src',
integrations: [mdx(), sitemap(), react()],
vite: {
plugins: [tailwindcss()],
resolve: {
alias: {
'@ermnvldmr/stl': new URL('../../packages/stl/src', import.meta.url).pathname,
'@ermnvldmr/ui': new URL('../../packages/ui/src', import.meta.url).pathname,
'#': new URL('./src/', import.meta.url).pathname,
},
},
},
});
+35
View File
@@ -0,0 +1,35 @@
import React from 'react';
import * as TestArticle from './articles/2026/test-article.mdx';
export interface Article {
slug: string;
title: string;
description?: string;
createdDate: Date;
updatedDate?: Date;
tags?: string[];
image?: string;
Component: React.ComponentType<{ components?: Record<string, React.ComponentType<any>> }>;
}
function register(module: any, slug: string): Article {
const { frontmatter, default: Component } = module;
return {
slug,
title: frontmatter.title,
description: frontmatter.description,
createdDate: new Date(frontmatter.createdDate),
updatedDate: frontmatter.updatedDate ? new Date(frontmatter.updatedDate) : undefined,
tags: frontmatter.tags,
image: frontmatter.image,
Component,
};
}
export const articles: Record<string, Article> = {
'test-article': register(TestArticle, '2026/test-article'),
};
export const sortedArticles = Object.values(articles).sort(
(a, b) => b.createdDate.getTime() - a.createdDate.getTime()
);
-18
View File
@@ -1,18 +0,0 @@
import { reactJestConfig } from '@ermnvldmr/jest-config/react';
export default {
...reactJestConfig,
moduleNameMapper: {
...reactJestConfig.moduleNameMapper,
'^#(.*)$': '<rootDir>/src/$1',
'^@ermnvldmr/ui/(.*)$': '<rootDir>/../../packages/ui/src/$1',
'^@ermnvldmr/ui$': '<rootDir>/../../packages/ui/src',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{ts,tsx}',
'!src/**/*.test.{ts,tsx}',
'!src/**/*.astro',
],
};
+17 -20
View File
@@ -4,16 +4,15 @@
"description": "www.ermnvldmr.com website",
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"dev": "rsbuild dev",
"build": "rsbuild build",
"preview": "rsbuild preview",
"lint": "eslint src --max-warnings 0",
"format": "prettier --write .",
"typecheck": "tsc --noEmit",
"test": "jest --passWithNoTests",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage --passWithNoTests",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:storybook": "test-storybook --coverage",
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"http-server storybook-static --port 6006 --silent\" \"wait-on tcp:127.0.0.1:6006 && pnpm run test:storybook\"",
"check": "pnpm run lint && pnpm run typecheck && pnpm run test",
@@ -23,13 +22,8 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@astrojs/mdx": "^4.2.5",
"@astrojs/react": "^4.2.5",
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.3.1",
"@ermnvldmr/stl": "workspace:*",
"@ermnvldmr/ui": "workspace:*",
"astro": "^5.7.5",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"simplex-noise": "^4.0.3",
@@ -38,8 +32,10 @@
},
"devDependencies": {
"@ermnvldmr/eslint-config": "workspace:*",
"@ermnvldmr/jest-config": "workspace:*",
"@ermnvldmr/rsbuild-config": "workspace:*",
"@ermnvldmr/storybook-config": "workspace:*",
"@rsbuild/core": "^1.2.11",
"@rsbuild/plugin-mdx": "^1.1.1",
"@storybook/addon-a11y": "^8.6.15",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-essentials": "^8.6.15",
@@ -49,28 +45,29 @@
"@storybook/react-vite": "^8.6.15",
"@storybook/test": "^8.6.15",
"@storybook/test-runner": "^0.19.1",
"@tailwindcss/vite": "^4.1.4",
"@tailwindcss/postcss": "^4.1.18",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/suncalc": "^1.9.2",
"@vitest/coverage-v8": "^4.0.18",
"concurrently": "^9.1.2",
"eslint": "^9.25.1",
"http-server": "^14.1.1",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"jsdom": "^28.0.0",
"playwright": "^1.57.0",
"postcss": "^8.5.6",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.11",
"remark-frontmatter": "^5.0.0",
"remark-mdx-frontmatter": "^5.2.0",
"storybook": "^8.6.15",
"ts-jest": "^29.4.6",
"storybook-react-rsbuild": "^0.1.10",
"ts-node": "^10.9.2",
"typescript": "^5.8.3",
"vite": "^7.2.2",
"vite-plugin-istanbul": "^7.2.1",
"vitest": "^4.0.18",
"wait-on": "^8.0.3"
}
}
+5
View File
@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};
+21
View File
@@ -0,0 +1,21 @@
import { defineServiceConfig } from '@ermnvldmr/rsbuild-config';
import { pluginMdx } from '@rsbuild/plugin-mdx';
import remarkFrontmatter from 'remark-frontmatter';
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
export default defineServiceConfig({
plugins: [
pluginMdx({
mdxLoaderOptions: {
remarkPlugins: [remarkFrontmatter, [remarkMdxFrontmatter, { name: 'frontmatter' }]],
},
}),
],
source: {
entry: {
index: './src/app/index.tsx',
'articles/index': './src/app/articles/index.tsx',
'articles/2026/test-article': './src/app/articles/2026/test-article.tsx',
},
},
});
@@ -1,12 +1,7 @@
---
import { Container, Header, Paragraph, Link, VStack } from '@ermnvldmr/ui';
import React from 'react';
import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '#shared/constants';
import '#global.css';
---
<IndexLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
export const HomePage = (): React.JSX.Element => (
<Container as="main" className="flex h-full w-full justify-center p-4">
<VStack gap={4}>
<Header level={2}>Welcome to my page!</Header>
@@ -16,12 +11,11 @@ import '#global.css';
It will be further configured.
</Paragraph>
<Paragraph>
For the source code and support please refer to <Link href="https://github.com/deytenit/ermnvldmr.com/"
>ermnvldmr.com monorepo</Link
>.
For the source code and support please refer to{' '}
<Link href="https://github.com/deytenit/ermnvldmr.com/">ermnvldmr.com monorepo</Link>.
<br />
Pictures of mine are available at <Link href="https://ermnvldmr.com/instagram">@deytenit</Link
>.
Pictures of mine are available at{' '}
<Link href="https://ermnvldmr.com/instagram">@deytenit</Link>.
</Paragraph>
<Paragraph>
Check out my <Link href="/articles">thoughts and articles on various matters</Link>.
@@ -29,4 +23,4 @@ import '#global.css';
<Paragraph italic>Thank you for visiting!</Paragraph>
</VStack>
</Container>
</IndexLayout>
);
@@ -0,0 +1,26 @@
import { MARKDOWN_COMPONENTS } from '@ermnvldmr/ui';
import React from 'react';
import { articles } from '../../../../content/registry';
import { ArticleLayout } from '../../../layouts/ArticleLayout/ArticleLayout';
export const ArticlePage = (): React.JSX.Element => {
const article = articles['test-article'];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!article) {
throw new Error('Article not found');
}
return (
<ArticleLayout
createdDate={article.createdDate}
description={article.description}
tags={article.tags}
title={article.title}
updatedDate={article.updatedDate}
>
<article.Component components={MARKDOWN_COMPONENTS} />
</ArticleLayout>
);
};
@@ -0,0 +1,15 @@
import { ArticlePage } from './ArticlePage';
import { articles } from '../../../../content/registry';
import { createPage } from '../../../lib/createPage';
const article = articles['test-article'];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!article) {
throw new Error('Article not found');
}
createPage(ArticlePage, {
title: `${article.title} - Vladimir Eremin`,
description: article.description,
});
@@ -0,0 +1,36 @@
import { Container, Header, Link, Separator, Article } from '@ermnvldmr/ui';
import React from 'react';
import { sortedArticles } from '../../../content/registry';
export const ArticlesList = (): React.JSX.Element => (
<Container as="main" className="mx-auto max-w-5xl px-6 py-20">
<Header className="mb-12 text-center" level={1}>
Articles
</Header>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
{sortedArticles.map((article, index) => (
<React.Fragment key={article.slug}>
{index > 0 && index % 2 === 0 && (
<div className="col-span-1 py-8 md:col-span-2">
<Separator className="w-full" type="double" />
</div>
)}
<Link className="group block h-full no-underline!" href={`/articles/${article.slug}`}>
<Article
additionalText={article.tags?.join(' • ')}
headline={article.title}
subHeadline={article.createdDate.toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
>
{article.description}
</Article>
</Link>
</React.Fragment>
))}
</div>
</Container>
);
+8
View File
@@ -0,0 +1,8 @@
import { ArticlesList } from './ArticlesList';
import { createPage } from '../../lib/createPage';
import { SITE_TITLE } from '../../shared/constants';
createPage(ArticlesList, {
title: `Articles | ${SITE_TITLE}`,
description: 'Browse all articles and projects.',
});
+5
View File
@@ -0,0 +1,5 @@
import { HomePage } from './HomePage';
import { createPage } from '../lib/createPage';
import { SITE_DESCRIPTION, SITE_TITLE } from '../shared/constants';
createPage(HomePage, { title: SITE_TITLE, description: SITE_DESCRIPTION });
-18
View File
@@ -1,18 +0,0 @@
import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';
const articles = defineCollection({
// Load all markdown and MDX files from the content/articles directory
loader: glob({ pattern: '**/*.{md,mdx}', base: './content/articles' }),
schema: z.object({
title: z.string(),
description: z.string().optional(),
// Astro handles date strings in frontmatter, but we use coerce to be safe
createdDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
tags: z.array(z.string()).optional(),
image: z.string().optional(),
}),
});
export const collections = { articles };
+5
View File
@@ -0,0 +1,5 @@
declare module '*.mdx' {
import type { ComponentType } from 'react';
const Component: ComponentType<{ components?: Record<string, React.ComponentType> }>;
export default Component;
}
@@ -1,19 +1,7 @@
---
/**
*
*/
export interface Props {
title: string;
description: string;
}
const { title, description } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<script is:inline>
<script>
const getThemePreference = () => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
@@ -37,13 +25,9 @@ const { title, description } = Astro.props;
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="robots" content="index,follow" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href="https://www.ermnvldmr.com/" />
<title>Vladimir Eremin</title>
</head>
<body>
<slot />
<div id="root"></div>
</body>
</html>
@@ -1,4 +1,13 @@
import { Header, Paragraph, List, Blockquote, Code, CodeBlock, Container, Text } from '@ermnvldmr/ui';
import {
Header,
Paragraph,
List,
Blockquote,
Code,
CodeBlock,
Container,
Text,
} from '@ermnvldmr/ui';
import { ArticleLayout } from './ArticleLayout';
@@ -23,7 +32,7 @@ const meta: Meta<typeof ArticleLayout> = {
export default meta;
/**
*
*
*/
type Story = StoryObj<typeof ArticleLayout>;
@@ -94,7 +103,12 @@ export const collections = { articles };`}
We can also include images and videos. The layout ensures they are responsive and fit the
grid.
</Paragraph>
<Container border bg="muted" className="flex aspect-video items-center justify-center" rounded="lg">
<Container
border
bg="muted"
className="flex aspect-video items-center justify-center"
rounded="lg"
>
<Text color="muted">Placeholder for Image/Video</Text>
</Container>
@@ -102,4 +116,3 @@ export const collections = { articles };`}
</ArticleLayout>
),
};
+39
View File
@@ -0,0 +1,39 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import '../global.css'; // Ensure global styles are available
/**
*
*/
export interface PageOptions {
title?: string;
description?: string;
}
/**
* Creates a React page component with hydration support.
*
* @param Component - The React component to render.
* @param options - Page options like title.
* @returns The page component.
*
* @example
* ```tsx
* createPage(<App />);
* ```
*/
export function createPage(Component: React.ComponentType, options: PageOptions = {}): void {
const rootElement = document.getElementById('root');
if (options.title) {
document.title = options.title;
}
if (rootElement) {
createRoot(rootElement).render(
<React.StrictMode>
<Component />
</React.StrictMode>
);
}
}
@@ -1,36 +0,0 @@
---
import { MARKDOWN_COMPONENTS } from '@ermnvldmr/ui';
import { getCollection, render } from 'astro:content';
import { ArticleLayout } from '#layouts/ArticleLayout/ArticleLayout';
import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro';
import '#global.css';
/**
* Generate paths for all articles in the collection.
* The slug will include the year directory (e.g., "2026/test-article").
*/
export async function getStaticPaths() {
const articles = await getCollection('articles');
return articles.map((entry) => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
const { title, description, createdDate, updatedDate, tags } = entry.data;
---
<IndexLayout title={title} description={description ?? ''}>
<ArticleLayout
title={title}
description={description}
createdDate={createdDate}
updatedDate={updatedDate}
tags={tags}
>
<Content components={MARKDOWN_COMPONENTS} />
</ArticleLayout>
</IndexLayout>
@@ -1,56 +0,0 @@
---
import { Container, Header, Paragraph, Link, VStack, List, Time, Text } from '@ermnvldmr/ui';
import { getCollection } from 'astro:content';
import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro';
import { SITE_TITLE } from '#shared/constants';
import '#global.css';
const articles = (await getCollection('articles')).sort(
(a, b) => b.data.createdDate.valueOf() - a.data.createdDate.valueOf()
);
---
<IndexLayout title={`Articles | ${SITE_TITLE}`} description="Browse all articles and projects.">
<Container as="main" className="mx-auto max-w-3xl px-6 py-20">
<Header level={1} className="mb-12">Articles</Header>
<List variant="plain" spacing="l">
{
articles.map((article) => (
<List.Item>
<Link href={`/articles/${article.id}`} className="group block no-underline!">
<VStack gap={3} as="article">
<Time
date={article.data.createdDate}
color="muted"
size="s"
type="label"
/>
<Header
level={2}
className="group-hover:text-primary mt-0 border-none pb-0 text-3xl transition-colors"
>
{article.data.title}
</Header>
{article.data.description && (
<Paragraph color="muted" size="l" maxLines={3}>
{article.data.description}
</Paragraph>
)}
{article.data.tags && (
<div class="flex gap-2">
{article.data.tags.map((tag) => (
<Text color="muted" size="s" className="before:content-['#']">
{tag}
</Text>
))}
</div>
)}
</VStack>
</Link>
</List.Item>
))
}
</List>
</Container>
</IndexLayout>
+9
View File
@@ -0,0 +1,9 @@
import { reactVitestConfig } from '@ermnvldmr/rsbuild-config/vitest';
import { mergeConfig } from 'vitest/config';
export default mergeConfig(reactVitestConfig, {
test: {
include: ['src/**/*.{test,spec}.{ts,tsx}'],
setupFiles: ['./src/setupTests.ts'],
},
});