fix, feat, doc: massive build system refactorion, bug fixes and apex documentation (#19)

This commit is contained in:
deytenit authored and GitHub committed 2026-07-12 01:57:40 +03:00
1 parent e8b42c60c2
commit 91f5245823
165 files changed
+8598 -1205

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
import { defineCli, env, s3SyncCommand } from '@ermnvldmr/cli';
export default defineCli({
commands: [
s3SyncCommand({
name: 'sync:static',
description: 'Sync static assets to S3',
targetPrefix: 'www',
bucket: env('COM_ERMNVLDMR_STATIC_S3_BUCKET'),
region: env('COM_ERMNVLDMR_STATIC_S3_REGION'),
endpoint: env('COM_ERMNVLDMR_STATIC_S3_ENDPOINT_URL'),
accessKeyId: env('COM_ERMNVLDMR_STATIC_AWS_ACCESS_KEY_ID'),
secretAccessKey: env('COM_ERMNVLDMR_STATIC_AWS_SECRET_ACCESS_KEY'),
hooks: {
beforeSync: 'bash ../../ci/scripts/optimize-images.sh',
},
}),
],
});
+4
View File
@@ -4,6 +4,8 @@
"description": "www.ermnvldmr.com website",
"type": "module",
"scripts": {
"cli": "ermnvldmr-cli",
"sync:static": "pnpm cli sync:static",
"dev": "rsbuild dev",
"build": "rsbuild build",
"build:en": "LOCALE=en rsbuild build",
@@ -27,6 +29,7 @@
},
"dependencies": {
"@ermnvldmr/i18n": "workspace:*",
"@ermnvldmr/ssg": "workspace:*",
"@ermnvldmr/stl": "workspace:*",
"@ermnvldmr/ui": "workspace:*",
"lucide-react": "^0.503.0",
@@ -35,6 +38,7 @@
"tailwindcss": "^4.1.4"
},
"devDependencies": {
"@ermnvldmr/cli": "workspace:*",
"@ermnvldmr/eslint-config": "workspace:*",
"@ermnvldmr/rsbuild-config": "workspace:*",
"@ermnvldmr/storybook-config": "workspace:*",
+50 -2
View File
@@ -1,4 +1,6 @@
import { defineServiceConfig, discoverEntries, mergeConfig } from '@ermnvldmr/rsbuild-config/dev';
import { createReadStream, existsSync } from 'node:fs';
import { extname, join, sep } from 'node:path';
import { defineSSGServiceConfig, discoverEntries, mergeConfig } from '@ermnvldmr/ssg/dev';
import { THEME_INIT_SCRIPT, THEME_INIT_STYLES } from '@ermnvldmr/ui/dev';
import { localeRsbuildConfig } from '@ermnvldmr/i18n/dev';
import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';
@@ -6,11 +8,56 @@ import { pluginMdx } from '@rsbuild/plugin-mdx';
import remarkFrontmatter from 'remark-frontmatter';
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
const MIME: Record<string, string> = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
'.avif': 'image/avif',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.txt': 'text/plain',
};
const locale = process.env.LOCALE ?? 'en';
const fontBase = `/${locale}/static/font`;
export default defineServiceConfig(
export default defineSSGServiceConfig(
mergeConfig(localeRsbuildConfig(), {
dev: {
// Serve public/static/* at /static/* directly, bypassing server.base.
// Without this, server.base (e.g. /en) would push the public dir to
// /en/static/*, making /static/* unreachable in dev.
setupMiddlewares: [
(middlewares) => {
middlewares.unshift((req, res, next) => {
const url = (req as { url?: string }).url ?? '';
if (!url.startsWith('/static/')) return next();
// Strip the leading slash so the '/static/...' segment is treated as
// relative to public/ (an absolute segment would otherwise let a
// resolved path escape it), then confine the result to public/ to
// block '..' path traversal.
const publicDir = join(import.meta.dirname, 'public');
const requestedPath = (url.split('?')[0] ?? url).replace(/^\/+/, '');
const filePath = join(publicDir, requestedPath);
if (!filePath.startsWith(publicDir + sep) || !existsSync(filePath)) return next();
const mime = MIME[extname(filePath)] ?? 'application/octet-stream';
res.setHeader('Content-Type', mime);
createReadStream(filePath).pipe(res as import('node:stream').Writable);
});
return middlewares;
},
],
},
tools: {
rspack: {
plugins: [
@@ -31,6 +78,7 @@ export default defineServiceConfig(
],
source: {
entry: discoverEntries(import.meta.dirname, './src/app/**/*.tsx'),
preEntry: ['./src/static/global.css'],
},
html: {
tags: [
@@ -1,10 +1,10 @@
import { localePath } from '@ermnvldmr/i18n';
import { createPage } from '@ermnvldmr/ssg';
import { MARKDOWN_COMPONENTS } from '@ermnvldmr/ui';
import React from 'react';
import { articles } from '../../../../content/registry';
import { ArticleLayout } from '../../../components/Layout/ArticleLayout';
import { createPage } from '../../../lib/core/createPage';
const article = articles['joplin-web-deployment-guide'];
@@ -1,10 +1,10 @@
import { localePath } from '@ermnvldmr/i18n';
import { createPage } from '@ermnvldmr/ssg';
import { MARKDOWN_COMPONENTS } from '@ermnvldmr/ui';
import React from 'react';
import { articles } from '../../../../content/registry';
import { ArticleLayout } from '../../../components/Layout/ArticleLayout';
import { createPage } from '../../../lib/core/createPage';
const article = articles['matrix-server-deployment-guide'];
@@ -1,10 +1,10 @@
import { localePath } from '@ermnvldmr/i18n';
import { createPage } from '@ermnvldmr/ssg';
import { MARKDOWN_COMPONENTS } from '@ermnvldmr/ui';
import React from 'react';
import { articles } from '../../../../content/registry';
import { ArticleLayout } from '../../../components/Layout/ArticleLayout';
import { createPage } from '../../../lib/core/createPage';
const article = articles['test-article'];
@@ -1,10 +1,10 @@
import { localePath } from '@ermnvldmr/i18n';
import { createPage } from '@ermnvldmr/ssg';
import { MARKDOWN_COMPONENTS } from '@ermnvldmr/ui';
import React from 'react';
import { articles } from '../../../../content/registry';
import { ArticleLayout } from '../../../components/Layout/ArticleLayout';
import { createPage } from '../../../lib/core/createPage';
const article = articles['xray-core-subnet-mimicry'];
+32 -23
View File
@@ -1,11 +1,11 @@
import { LOCALE, localePath } from '@ermnvldmr/i18n';
import { Article, Breadcrumbs } from '@ermnvldmr/ui';
import { createPage } from '@ermnvldmr/ssg';
import { BentoGrid, Breadcrumbs, HStack, Separator, Text, Time, VStack } from '@ermnvldmr/ui';
import React from 'react';
import { t } from './index.i18n';
import { sortedArticles } from '../../../content/registry';
import { DefaultLayout } from '../../components/Layout/DefaultLayout';
import { createPage } from '../../lib/core/createPage';
import { SITE_TITLE } from '../../lib/shared/constants';
const DATE_LOCALE_MAP: Record<string, string> = { en: 'en-US', ru: 'ru-RU' };
@@ -35,28 +35,37 @@ function ArticlesList(): React.JSX.Element {
paddingY="medium"
title={t('Articles')}
>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
{sortedArticles.map((article) => (
<React.Fragment key={article.slug}>
<Article
additionalText={article.tags?.join(' • ')}
className="h-full"
headline={article.title}
href={localePath(`/articles/${article.slug}`)}
subHeadline={article.createdDate.toLocaleDateString(
DATE_LOCALE_MAP[LOCALE] ?? 'en-US',
{
year: 'numeric',
month: 'long',
day: 'numeric',
}
)}
>
{article.description}
</Article>
</React.Fragment>
<BentoGrid>
{sortedArticles.map((article, index) => (
<BentoGrid.InfoCard
key={article.slug}
colSpan={index === 0 ? 2 : 1}
description={article.description}
footer={
<VStack align="stretch" gap={2}>
<Separator thinned="thinned-end" type="single" />
<HStack align="center" gap={4} justify="between" wrap="wrap">
<Time
color="muted"
date={article.createdDate}
locale={DATE_LOCALE_MAP[LOCALE] ?? 'en-US'}
size="s"
type="label"
/>
{article.tags?.length ? (
<Text italic color="muted" size="s" type="label">
{article.tags.join(' • ')}
</Text>
) : null}
</HStack>
</VStack>
}
href={localePath(`/articles/${article.slug}`)}
rowSpan={index === 0 ? 2 : 1}
title={article.title}
/>
))}
</div>
</BentoGrid>
</DefaultLayout>
);
}
+1 -1
View File
@@ -1,10 +1,10 @@
import { localePath } from '@ermnvldmr/i18n';
import { createPage } from '@ermnvldmr/ssg';
import { Paragraph, Link, VStack, Header } from '@ermnvldmr/ui';
import React from 'react';
import { t, tRaw } from './index.i18n';
import { DefaultLayout } from '../components/Layout/DefaultLayout';
import { createPage } from '../lib/core/createPage';
import { SITE_TITLE } from '../lib/shared/constants';
/**
+46 -38
View File
@@ -5,38 +5,43 @@ export const { t } = createKeyset({
en: 'Wanted',
ru: 'Разыскивается',
},
Home: {
en: 'Home',
ru: 'Главная'
},
'A quiet log of things I am planning to pick up eventually. It serves mostly as a personal reference, but also as a zero-pressure guide for friends.': {
en: 'A quiet log of things I am planning to pick up eventually. It serves mostly as a personal reference, but also as a zero-pressure guide for friends.',
ru: 'Список вещей, которые я планирую со временем приобрести. В основном это личная заметка, но также и ненавязчивый вишлист.',
Home: {
en: 'Home',
ru: 'Главная',
},
'A quiet log of things I am planning to pick up eventually. It serves mostly as a personal reference, but also as a zero-pressure guide for friends.':
{
en: 'A quiet log of things I am planning to pick up eventually. It serves mostly as a personal reference, but also as a zero-pressure guide for friends.',
ru: 'Список вещей, которые я планирую со временем приобрести. В основном это личная заметка, но также и ненавязчивый вишлист.',
},
'How this works': {
en: 'How this works',
ru: 'Как это работает',
},
'Think of this as a living list of things I plan to acquire over time. If you ever feel inspired to contribute, I would be genuinely thrilled, but please know it is never expected.': {
en: 'Think of this as a living list of things I plan to acquire over time. If you ever feel inspired to contribute, I would be genuinely thrilled, but please know it is never expected.',
ru: 'Считайте это живым списком вещей, которые я планирую приобрести. Если у вас когда-нибудь появится желание поучаствовать — я буду искренне рад, но, пожалуйста, знайте, что это абсолютно необязательно.',
},
'When it comes to the items themselves, authenticity and quality matter a lot to me. I would always prefer to have nothing at all rather than a replica or a knock-off.': {
en: 'When it comes to the items themselves, authenticity and quality matter a lot to me. I would always prefer to have nothing at all rather than a replica or a knock-off.',
ru: 'Когда дело доходит до самих вещей, подлинность и качество значат для меня очень много. Я всегда предпочту не иметь ничего, чем реплику или подделку.',
},
'And of course, if you are ever unsure about a specific edition or detail, we can always just talk about it.': {
en: 'And of course, if you are ever unsure about a specific edition or detail, we can always just talk about it.',
ru: 'И, конечно, если вы не уверены в каком-то конкретном издании или деталях, можно всегда это обсудить.',
},
'Figurines': {
'Think of this as a living list of things I plan to acquire over time. If you ever feel inspired to contribute, I would be genuinely thrilled, but please know it is never expected.':
{
en: 'Think of this as a living list of things I plan to acquire over time. If you ever feel inspired to contribute, I would be genuinely thrilled, but please know it is never expected.',
ru: 'Считайте это живым списком вещей, которые я планирую приобрести. Если у вас когда-нибудь появится желание поучаствовать — я буду искренне рад, но, пожалуйста, знайте, что это абсолютно необязательно.',
},
'When it comes to the items themselves, authenticity and quality matter a lot to me. I would always prefer to have nothing at all rather than a replica or a knock-off.':
{
en: 'When it comes to the items themselves, authenticity and quality matter a lot to me. I would always prefer to have nothing at all rather than a replica or a knock-off.',
ru: 'Когда дело доходит до самих вещей, подлинность и качество значат для меня очень много. Я всегда предпочту не иметь ничего, чем реплику или подделку.',
},
'And of course, if you are ever unsure about a specific edition or detail, we can always just talk about it.':
{
en: 'And of course, if you are ever unsure about a specific edition or detail, we can always just talk about it.',
ru: 'И, конечно, если вы не уверены в каком-то конкретном издании или деталях, можно всегда это обсудить.',
},
Figurines: {
en: 'Figurines',
ru: 'Фигурки',
},
'The specifics of the edition and its authenticity are quite important here. It is usually best to know the exact details to get it right.': {
en: 'The specifics of the edition and its authenticity are quite important here. It is usually best to know the exact details to get it right.',
ru: 'Специфика издания и его подлинность здесь довольно важны. Обычно лучше знать точные детали, чтобы не ошибиться.',
},
'The specifics of the edition and its authenticity are quite important here. It is usually best to know the exact details to get it right.':
{
en: 'The specifics of the edition and its authenticity are quite important here. It is usually best to know the exact details to get it right.',
ru: 'Специфика издания и его подлинность здесь довольно важны. Обычно лучше знать точные детали, чтобы не ошибиться.',
},
'Looking for': {
en: 'Looking for',
ru: 'Ищу',
@@ -61,10 +66,11 @@ export const { t } = createKeyset({
en: 'LEGO Sets',
ru: 'Наборы LEGO',
},
'I appreciate sets that focus on engineering and make for good display pieces, like Architecture, Technic, or the Botanical collection. I generally lean away from traditional play-sets.': {
en: 'I appreciate sets that focus on engineering and make for good display pieces, like Architecture, Technic, or the Botanical collection. I generally lean away from traditional play-sets.',
ru: 'Я ценю наборы, ориентированные на инженерию, которые хорошо смотрятся на полке — например, Architecture, Technic или Botanical. Обычно я избегаю традиционных игровых наборов (friends, city, и другие подобные).',
},
'I appreciate sets that focus on engineering and make for good display pieces, like Architecture, Technic, or the Botanical collection. I generally lean away from traditional play-sets.':
{
en: 'I appreciate sets that focus on engineering and make for good display pieces, like Architecture, Technic, or the Botanical collection. I generally lean away from traditional play-sets.',
ru: 'Я ценю наборы, ориентированные на инженерию, которые хорошо смотрятся на полке — например, Architecture, Technic или Botanical. Обычно я избегаю традиционных игровых наборов (friends, city, и другие подобные).',
},
'Architectural landmarks or skylines': {
en: 'Architectural landmarks or skylines',
ru: 'Архитектурные наборы',
@@ -93,10 +99,11 @@ export const { t } = createKeyset({
en: 'Die-cast Models',
ru: 'Дайкаст модели',
},
'I enjoy high-quality pre-assembled die-cast models. I am not much into plastic kits that require glue or paint to assemble.': {
en: 'I enjoy high-quality pre-assembled die-cast models. I am not much into plastic kits that require glue or paint to assemble.',
ru: 'Мне нравятся качественные предварительно собранные дайкаст модели. Я не особо увлекаюсь пластиковыми наборами, которые нужно собирать с помощью клея и красок.',
},
'I enjoy high-quality pre-assembled die-cast models. I am not much into plastic kits that require glue or paint to assemble.':
{
en: 'I enjoy high-quality pre-assembled die-cast models. I am not much into plastic kits that require glue or paint to assemble.',
ru: 'Мне нравятся качественные предварительно собранные дайкаст модели. Я не особо увлекаюсь пластиковыми наборами, которые нужно собирать с помощью клея и красок.',
},
'Spacecraft scale replicas (SpaceX, NASA)': {
en: 'Spacecraft scale replicas (SpaceX, NASA)',
ru: 'Масштабные модели космических аппаратов (SpaceX, NASA)',
@@ -117,11 +124,12 @@ export const { t } = createKeyset({
en: 'Coffee gear',
ru: 'Всё для кофе',
},
'My current espresso setup uses a 51mm portafilter, so any accessories for it need to match that size perfectly.': {
en: 'My current espresso setup uses a 51mm portafilter, so any accessories for it need to match that size perfectly.',
ru: 'Мой текущий сетап для эспрессо использует 51-миллиметровый портафильтр, поэтому любые аксессуары для него должны идеально подходить по размеру.',
},
'AeroPress': {
'My current espresso setup uses a 51mm portafilter, so any accessories for it need to match that size perfectly.':
{
en: 'My current espresso setup uses a 51mm portafilter, so any accessories for it need to match that size perfectly.',
ru: 'Мой текущий сетап для эспрессо использует 51-миллиметровый портафильтр, поэтому любые аксессуары для него должны идеально подходить по размеру.',
},
AeroPress: {
en: 'AeroPress',
ru: 'AeroPress',
},
@@ -153,7 +161,7 @@ export const { t } = createKeyset({
en: 'French Press',
ru: 'Френч-пресс',
},
'Hobby': {
Hobby: {
en: 'Hobby',
ru: 'Хобби',
},
+2 -4
View File
@@ -1,10 +1,10 @@
import { localePath } from '@ermnvldmr/i18n';
import { createPage } from '@ermnvldmr/ssg';
import { BentoGrid, Header, Paragraph, VStack, Breadcrumbs } from '@ermnvldmr/ui';
import { Ghost, Coffee, Plane, Check, Gift, Cuboid, BoxSelect, Cpu } from 'lucide-react';
import { t } from './index.i18n';
import { DefaultLayout } from '../../components/Layout/DefaultLayout';
import { createPage } from '../../lib/core/createPage';
/**
* Wanted page component.
@@ -189,9 +189,7 @@ function WantedPage(): React.JSX.Element {
value: 'wanted',
label: t('Looking for'),
labelIcon: <Gift size={18} />,
items: [
t('Alientek T90 Soldering Iron (or a similar compact, smart iron)'),
],
items: [t('Alientek T90 Soldering Iron (or a similar compact, smart iron)')],
},
]}
title={t('Hobby')}
-56
View File
@@ -1,56 +0,0 @@
import React from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
import '../../static/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 {
if (typeof window === 'undefined') return; // SSG build context: skip DOM mounting
const rootElement = document.getElementById('root');
if (options.title) {
document.title = options.title;
}
if (options.description) {
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement('meta');
metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription);
}
metaDescription.setAttribute('content', options.description);
}
if (rootElement) {
const node = (
<React.StrictMode>
<Component />
</React.StrictMode>
);
if (rootElement.hasChildNodes()) {
hydrateRoot(rootElement, node);
} else {
createRoot(rootElement).render(node);
}
}
}
+2
View File
@@ -4,6 +4,8 @@
"baseUrl": "./src",
"paths": {
"#*": ["./*"],
"@ermnvldmr/ssg": ["../../../packages/ssg/src"],
"@ermnvldmr/ssg/*": ["../../../packages/ssg/src/*"],
"@ermnvldmr/stl": ["../../../packages/stl/src"],
"@ermnvldmr/ui": ["../../../packages/ui/src"],
"@ermnvldmr/ui/*": ["../../../packages/ui/src/*"]