Compare commits

..
Author SHA1 Message Date
copilot-swe-agent[bot]anddeytenit a20e34a37c fix (ui): update PageHead test to match current component behavior
Agent-Logs-Url: https://github.com/deytenit/ermnvldmr.com/sessions/6131fb85-0d48-4782-8495-a5c48f64a016

Co-authored-by: deytenit <60275123+deytenit@users.noreply.github.com>
2026-05-04 15:05:03 +00:00
deytenit 208f6406e6 fix: Fix PR issues 2026-05-04 17:55:51 +03:00
deytenit 82236be7d6 feat: Add wanted page 2026-05-04 15:03:19 +03:00
deytenit 7ac7c1b405 feat: Add BentoGrid component 2026-05-04 14:42:54 +03:00
deytenit 77fa576ac6 feat: Add Accordion component 2026-05-04 13:52:57 +03:00
dependabot[bot] 07b16d404c build(deps): bump picomatch from 2.3.1 to 2.3.2 in the npm_and_yarn group across 0 directory (#14)
Updates `picomatch` from 2.3.1 to 2.3.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/micromatch/picomatch/releases">picomatch's
releases</a>.</em></p>
<blockquote>
<h2>2.3.2</h2>
<p>This is a security release fixing several security relevant
issues.</p>
<h2>What's Changed</h2>
<ul>
<li>fix: exception when glob pattern contains constructor by <a
href="https://github.com/Jason3S"><code>@​Jason3S</code></a> in <a
href="https://redirect.github.com/micromatch/picomatch/pull/144">micromatch/picomatch#144</a></li>
<li>Fix for <a
href="https://github.com/micromatch/picomatch/security/advisories/GHSA-c2c7-rcm5-vvqj">CVE-2026-33671</a></li>
<li>Fix for <a
href="https://github.com/micromatch/picomatch/security/advisories/GHSA-3v7f-55p6-f55p">CVE-2026-33672</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2">https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md">picomatch's
changelog</a>.</em></p>
<blockquote>
<h1>Release history</h1>
<p><strong>All notable changes to this project will be documented in
this file.</strong></p>
<p>The format is based on <a
href="http://keepachangelog.com/en/1.0.0/">Keep a Changelog</a>
and this project adheres to <a
href="http://semver.org/spec/v2.0.0.html">Semantic Versioning</a>.</p>
<!-- raw HTML omitted -->
<ul>
<li>Changelogs are for humans, not machines.</li>
<li>There should be an entry for every single version.</li>
<li>The same types of changes should be grouped.</li>
<li>Versions and sections should be linkable.</li>
<li>The latest version comes first.</li>
<li>The release date of each versions is displayed.</li>
<li>Mention whether you follow Semantic Versioning.</li>
</ul>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<p>Changelog entries are classified using the following labels <em>(from
<a href="http://keepachangelog.com/">keep-a-changelog</a></em>):</p>
<ul>
<li><code>Added</code> for new features.</li>
<li><code>Changed</code> for changes in existing functionality.</li>
<li><code>Deprecated</code> for soon-to-be removed features.</li>
<li><code>Removed</code> for now removed features.</li>
<li><code>Fixed</code> for any bug fixes.</li>
<li><code>Security</code> in case of vulnerabilities.</li>
</ul>
<!-- raw HTML omitted -->
<h2>4.0.0 (2024-02-07)</h2>
<h3>Fixes</h3>
<ul>
<li>Fix bad text values in parse <a
href="https://redirect.github.com/micromatch/picomatch/issues/126">#126</a>,
thanks to <a
href="https://github.com/connor4312"><code>@​connor4312</code></a></li>
</ul>
<h3>Changed</h3>
<ul>
<li>Remove process global to work outside of node <a
href="https://redirect.github.com/micromatch/picomatch/issues/129">#129</a>,
thanks to <a
href="https://github.com/styfle"><code>@​styfle</code></a></li>
<li>Add sideEffects to package.json <a
href="https://redirect.github.com/micromatch/picomatch/issues/128">#128</a>,
thanks to <a
href="https://github.com/frandiox"><code>@​frandiox</code></a></li>
<li>Removed <code>os</code>, make compatible browser environment. See <a
href="https://redirect.github.com/micromatch/picomatch/issues/124">#124</a>,
thanks to <a
href="https://github.com/gwsbhqt"><code>@​gwsbhqt</code></a></li>
</ul>
<h2>3.0.1</h2>
<h3>Fixes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/micromatch/picomatch/commit/81cba8d4b767cab3cb29d26eb4f691eed75b73b2"><code>81cba8d</code></a>
Publish 2.3.2</li>
<li><a
href="https://github.com/micromatch/picomatch/commit/fc1f6b69006e9435caf8fb40d8aff378bc0b7bce"><code>fc1f6b6</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/micromatch/picomatch/commit/eec17aee5428a7249e9ca5adbb8a0d28fa29619b"><code>eec17ae</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/micromatch/picomatch/commit/78f8ca4362d9e66cadea97b93e292f10096452ed"><code>78f8ca4</code></a>
Merge pull request <a
href="https://redirect.github.com/micromatch/picomatch/issues/156">#156</a>
from micromatch/backport-144</li>
<li><a
href="https://github.com/micromatch/picomatch/commit/3f4f10eaa65bf3a52e8f2999674cd27e11fa3c9b"><code>3f4f10e</code></a>
Merge pull request <a
href="https://redirect.github.com/micromatch/picomatch/issues/144">#144</a>
from Jason3S/jdent-object-properties</li>
<li>See full diff in <a
href="https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=picomatch&package-manager=npm_and_yarn&previous-version=2.3.1&new-version=2.3.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/deytenit/ermnvldmr.com/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 00:59:13 +03:00
dependabot[bot] a103313480 build(deps-dev): bump vite from 7.3.1 to 7.3.2 in the npm_and_yarn group across 1 directory (#12)
Bumps the npm_and_yarn group with 1 update in the / directory:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).

Updates `vite` from 7.3.1 to 7.3.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v7.3.2</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v7.3.1...v7.3.2">7.3.2</a>
(2026-04-06)<!-- raw HTML omitted --></h2>
<h3>Bug Fixes</h3>
<ul>
<li>avoid path traversal with optimize deps sourcemap handler (<a
href="https://redirect.github.com/vitejs/vite/issues/22161">#22161</a>)
(<a
href="https://github.com/vitejs/vite/commit/09d8c903bde12fee2710314d3b42bc789c686df7">09d8c90</a>)</li>
<li>backport <a
href="https://redirect.github.com/vitejs/vite/issues/22159">#22159</a>,
apply server.fs check to env transport (<a
href="https://redirect.github.com/vitejs/vite/issues/22162">#22162</a>)
(<a
href="https://github.com/vitejs/vite/commit/19db0f29c3a3ac4e64cc95c270716c77fd223ad1">19db0f2</a>)</li>
<li>check <code>server.fs</code> after stripping query as well (<a
href="https://redirect.github.com/vitejs/vite/issues/22160">#22160</a>)
(<a
href="https://github.com/vitejs/vite/commit/f8103cc946f137a54e395fe3f5d08e8209231ed6">f8103cc</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vitejs/vite/commit/cc383e07b66d4c5a9768fcb570e0af812cb8d999"><code>cc383e0</code></a>
release: v7.3.2</li>
<li><a
href="https://github.com/vitejs/vite/commit/09d8c903bde12fee2710314d3b42bc789c686df7"><code>09d8c90</code></a>
fix: avoid path traversal with optimize deps sourcemap handler (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22161">#22161</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/f8103cc946f137a54e395fe3f5d08e8209231ed6"><code>f8103cc</code></a>
fix: check <code>server.fs</code> after stripping query as well (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22160">#22160</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/19db0f29c3a3ac4e64cc95c270716c77fd223ad1"><code>19db0f2</code></a>
fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22159">#22159</a>,
apply server.fs check to env transport (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22162">#22162</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v7.3.2/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=7.3.1&new-version=7.3.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/deytenit/ermnvldmr.com/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-15 00:49:32 +03:00
deytenit 51a783f93a docs: Update docs for the correctness (#13) 2026-04-15 00:48:06 +03:00
deytenit 8dba55272c docs: update references to renamed commons repository (#11) 2026-04-06 22:18:55 +03:00
deytenit 6b0a893c86 Update package.json to remove broken postinstall (#10) 2026-03-20 01:41:14 +03:00
deytenit 575959ffa2 Add joplin web article (#9) 2026-03-20 01:32:26 +03:00
deytenit 90bf68c4c8 fix(nginx): add explicit root and fix regex location priority for sta… (#8)
…tic assets

Regex locations (~* .js|css|woff2 etc) have higher priority than prefix
locations (/en/, /ru/) in nginx. Without an explicit root in the server
block, the caching location had no root to serve files from, causing
404s for all /en/static/... and /ru/static/... assets.

- Add root /usr/share/nginx/html to server block so all locations
inherit it
- Remove alias from /en/ and /ru/ locations (redundant with explicit
root)
- Reorder: caching regex locations before locale prefix locations
(documents intent)

Made-with: Cursor
2026-03-16 01:33:29 +03:00
deytenit cd40139eea fix(www): resolve SSG build issues with font preloads, dark theme and… (#7)
… RU locale

- Fix depth-dependent duplicate font preload prefixes by using
locale-absolute fontBase path and publicPath:false on html.tags to
prevent Rsbuild from re-applying assetPrefix (path.posix.join behavior
with MPA nested entries)
- Fix .dark CSS rule stripped by critters: set reduceInlineStyles:false
so critters only processes external stylesheets and leaves inline theme
init styles (THEME_INIT_STYLES) intact
- Fix NormalModuleReplacementPlugin lost in mergeConfig: refactored
locale MDX replacement from tools.rspack function into a proper Rsbuild
plugin so mergeConfig concatenates plugin arrays correctly (enables RU
locale articles)
- Extend Critters type declaration to include reduceInlineStyles option

Made-with: Cursor
2026-03-16 01:27:42 +03:00
deytenit f5e901c131 Add i18n build step to releases workflows (#6)
Build @ermnvldmr/i18n and include its dist in the config-artifacts
bundle for services-www-releases-required and
services-www-releases-dispatch-release workflows, matching the fix
already applied to pr/next workflows.

Made-with: Cursor
2026-03-16 00:12:02 +03:00
deytenit b84ef775b8 Various build shenenigans (#5)
This PR extends the build pipeline of the www service to support
locale-prefixed static builds (EN/RU) with build-time SSG output
injection, while also refactoring UI typography to a tokenized Tailwind
v4 @theme-driven approach.

Changes:

- Add build-time SSG plugin to @ermnvldmr/rsbuild-config and wire
multi-locale builds (/en/, /ru/) into services/www.
- Introduce new @ermnvldmr/i18n package (locale constants, path helpers,
keysets, pluralization) and apply it to www navigation/text.
- Refactor ui typography to a shared getTypographyClassNames utility +
Tailwind token definitions, with stl/cn updated for correct
Tailwind-merge behavior.
2026-03-16 00:00:20 +03:00
dependabot[bot] ac9c455e5a build(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#3)
Bumps the npm_and_yarn group with 1 update in the / directory:
[storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core).

Updates `storybook` from 8.6.15 to 8.6.17
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/storybookjs/storybook/releases">storybook's
releases</a>.</em></p>
<blockquote>
<h2>v8.6.17</h2>
<h2>8.6.17</h2>
<ul>
<li>Harden websocket connection</li>
</ul>
<h2>v8.6.16</h2>
<h2>8.6.16</h2>
<ul>
<li>No-op release. No changes.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/storybookjs/storybook/blob/v8.6.17/CHANGELOG.md">storybook's
changelog</a>.</em></p>
<blockquote>
<h2>8.6.17</h2>
<ul>
<li>Harden websocket connection</li>
</ul>
<h2>8.6.16</h2>
<ul>
<li>No-op release. No changes.</li>
</ul>
<h2>8.6.14</h2>
<ul>
<li>CLI: Add skip onboarding, recommended/minimal config - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30930">#30930</a>,
thanks <a
href="https://github.com/shilman"><code>@​shilman</code></a>!</li>
<li>Core: Fix using dates in expect statements - <a
href="https://redirect.github.com/storybookjs/storybook/pull/28413">#28413</a>,
thanks <a
href="https://github.com/yann-combarnous"><code>@​yann-combarnous</code></a>!</li>
<li>React Native Web: Fix expo router by setting JSX to automatic - <a
href="https://redirect.github.com/storybookjs/storybook/pull/31484">#31484</a>,
thanks <a
href="https://github.com/dannyhw"><code>@​dannyhw</code></a>!</li>
<li>Test: Make sure that lit arrays are not cloned - <a
href="https://redirect.github.com/storybookjs/storybook/pull/31435">#31435</a>,
thanks <a
href="https://github.com/kasperpeulen"><code>@​kasperpeulen</code></a>!</li>
</ul>
<h2>8.6.13</h2>
<ul>
<li>Controls: Fix boxShadow on empty controls - <a
href="https://redirect.github.com/storybookjs/storybook/pull/27193">#27193</a>,
thanks <a
href="https://github.com/H0onnn"><code>@​H0onnn</code></a>!</li>
<li>React Native Web: Update <code>react-native-web</code> - <a
href="https://redirect.github.com/storybookjs/storybook/pull/31324">#31324</a>,
thanks <a
href="https://github.com/ndelangen"><code>@​ndelangen</code></a>!</li>
</ul>
<h2>8.6.12</h2>
<ul>
<li>CLI: Only install Visual Test Addon if test feature is selected - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30966">#30966</a>,
thanks <a
href="https://github.com/ghengeveld"><code>@​ghengeveld</code></a>!</li>
<li>Core: Fix telemetry error on Storybook UI - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30953">#30953</a>,
thanks <a
href="https://github.com/yannbf"><code>@​yannbf</code></a>!</li>
<li>Ember: Fix <code>ember-template-compiler</code> import for ember 6+
- <a
href="https://redirect.github.com/storybookjs/storybook/pull/30682">#30682</a>,
thanks <a
href="https://github.com/leoeuclids"><code>@​leoeuclids</code></a>!</li>
<li>Next: Update vite-plugin-storybook-nextjs to
2.0.0--canary.33.17a2310.0 - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30997">#30997</a>,
thanks <a
href="https://github.com/kasperpeulen"><code>@​kasperpeulen</code></a>!</li>
<li>Svelte: Exclude <code>node_modules</code> from docgen - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30981">#30981</a>,
thanks <a
href="https://github.com/JReinhold"><code>@​JReinhold</code></a>!</li>
</ul>
<h2>8.6.11</h2>
<ul>
<li>Angular: Fix zone.js support for Angular libraries - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30941">#30941</a>,
thanks <a
href="https://github.com/valentinpalkovic"><code>@​valentinpalkovic</code></a>!</li>
</ul>
<h2>8.6.10</h2>
<ul>
<li>Addon-docs: Fix non-string handling in Stories block - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30913">#30913</a>,
thanks <a
href="https://github.com/JamesIves"><code>@​JamesIves</code></a>!</li>
<li>Nextjs: Fix styled-jsx optimize vite warnings - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30932">#30932</a>,
thanks <a
href="https://github.com/kasperpeulen"><code>@​kasperpeulen</code></a>!</li>
<li>React: Fix actImplementation is not a function - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30929">#30929</a>,
thanks <a
href="https://github.com/kasperpeulen"><code>@​kasperpeulen</code></a>!</li>
</ul>
<h2>8.6.9</h2>
<ul>
<li>Next: Fix react aliases in next vite plugin - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30914">#30914</a>,
thanks <a
href="https://github.com/kasperpeulen"><code>@​kasperpeulen</code></a>!</li>
</ul>
<h2>8.6.8</h2>
<ul>
<li>Angular: Export all files in Angular package.json - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30849">#30849</a>,
thanks <a
href="https://github.com/kasperpeulen"><code>@​kasperpeulen</code></a>!</li>
<li>CLI: Don't add packageManager entry to package.json automatically -
<a
href="https://redirect.github.com/storybookjs/storybook/pull/30855">#30855</a>,
thanks <a
href="https://github.com/kasperpeulen"><code>@​kasperpeulen</code></a>!</li>
<li>React: Allow portable stories to be used in SSR - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30847">#30847</a>,
thanks <a
href="https://github.com/kasperpeulen"><code>@​kasperpeulen</code></a>!</li>
<li>Svelte: Adjust Svelte typings to include Svelte 5 function
components - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30852">#30852</a>,
thanks <a
href="https://github.com/dummdidumm"><code>@​dummdidumm</code></a>!</li>
<li>Telemetry: Make sure that telemetry doesn't fail on init - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30857">#30857</a>,
thanks <a
href="https://github.com/kasperpeulen"><code>@​kasperpeulen</code></a>!</li>
<li>Vite: Update HMR filter to target specific story file types - <a
href="https://redirect.github.com/storybookjs/storybook/pull/30845">#30845</a>,
thanks <a
href="https://github.com/kasperpeulen"><code>@​kasperpeulen</code></a>!</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/storybookjs/storybook/commit/c6e550a58e69723187157165d3610e876518d935"><code>c6e550a</code></a>
Bump version from &quot;8.6.16&quot; to &quot;8.6.17&quot; [skip
ci]</li>
<li><a
href="https://github.com/storybookjs/storybook/commit/9cf9d89dbd357aeb3f872cc35ae301bc1c0fc173"><code>9cf9d89</code></a>
Core: Require token for websocket connections</li>
<li><a
href="https://github.com/storybookjs/storybook/commit/7e5151586ba2475d40e19c592c2f10ce81965ae8"><code>7e51515</code></a>
Bump version from &quot;8.6.15&quot; to &quot;8.6.16&quot; [skip
ci]</li>
<li>See full diff in <a
href="https://github.com/storybookjs/storybook/commits/v8.6.17/code/core">compare
view</a></li>
</ul>
</details>
<br />

Updates `@isaacs/brace-expansion` from 5.0.0 to 5.0.1


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/deytenit/ermnvldmr.com/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 02:30:20 +03:00
deytenit ea35a3d9f5 TRIVIAL: Add xray-core-subnet-mimicry article (#4) 2026-03-08 02:29:09 +03:00
deytenit 49b545f1b4 E1: Refactor layouts and fix styles for visible elements in www 2026-03-07 21:02:22 +03:00
deytenit f3a3a52aa8 feat: Refactor layouts in www 2026-03-07 20:59:03 +03:00
deytenit 6aa3a03319 fix: Fix PR issues 2026-03-07 10:51:54 +03:00
deytenit d2903a3715 feat: Update styles for Article 2026-03-06 23:27:01 +03:00
deytenit 7b574a1a66 ci: Add workflows for prs 2026-03-06 23:14:13 +03:00
deytenit 56d0d65971 feat: Add codehighlight for the Code' 2026-03-06 23:06:30 +03:00
deytenit 6784a28701 feat: Variate Separator thinkness 2026-03-06 22:41:44 +03:00
deytenit a95723c4f1 feat: Update Code styles 2026-03-06 22:16:34 +03:00
deytenit 4e36e800d4 feat: Add more thinned variants to Separator 2026-03-06 21:19:41 +03:00
deytenit 8d57d6ba1b feat: Add PageHead component 2026-03-06 09:50:38 +03:00
deytenit 30fd52b8e9 chore: Bump hextra submodule 2026-02-26 22:25:25 +03:00
deytenit e9f27f2437 ci: Fix docs build via author 2026-02-26 00:37:31 +03:00
deytenit 52ada4eba6 docs: Add missing docs for initial host setup 2026-02-26 00:32:15 +03:00
deytenit 9c8e57a7b1 feat: Add local fontfaces 2026-02-15 19:26:23 +03:00
deytenit a571b343c8 fix: Fix several lighthouse issues 2026-02-15 19:14:12 +03:00
deytenit 90e8fad496 chore: Fix lint errors in UI 2026-02-15 18:57:01 +03:00
deytenit 0784e0b305 feat: Introduce SSG 2026-02-15 18:48:51 +03:00
deytenit 7fd11d2220 ci: Update release-dispatch build workflow 2026-02-15 17:55:21 +03:00
deytenit 1b0fa16adc chore: Fix lint issues in static 2026-02-15 17:30:16 +03:00
deytenit 3546594d41 feat: Add matrix deployment article 2026-02-15 17:26:42 +03:00
deytenit 45c7189b6b ci: Fix www storybook build 2026-02-15 16:30:49 +03:00
deytenit 3aa169ae29 build: Fix build issues 2026-02-15 16:19:50 +03:00
deytenit 2d1bc9d119 feat: Add theme switch 2026-02-15 15:36:31 +03:00
deytenit 891027cab0 refactor: Continue refactoring after migrating to rsbuild 2026-02-15 12:23:04 +03:00
deytenit baba22ad6f docs: Update readme and gemini mds 2026-02-13 01:50:49 +03:00
deytenit a12b03b3b5 ci: Fix lint in www && storybook build errors 2026-02-13 01:41:33 +03:00
deytenit 0534b784c6 ci: Fix vitest module resolution in CI 2026-02-13 01:21:20 +03:00
deytenit e193e75cc4 ci: Fix config artifacts download/upload 2026-02-13 00:59:34 +03:00
deytenit 54b0bf0314 ci: Fix storybook CI issues 2026-02-13 00:46:50 +03:00
deytenit be56500148 chore: Delete ignored dir 2026-02-13 00:35:06 +03:00
deytenit 9ed7409dc8 ci: Fix CI issues after rspack migration 2026-02-13 00:20:45 +03:00
deytenit 4628c043ce feat(infra): migrate to rsbuild 2026-02-12 23:39:20 +03:00
deytenit 97c6347f2e feat: Add markdown components 2026-02-10 08:48:12 +03:00
deytenit 25044c75ff feat: Migrate www to the ui components 2026-02-10 08:01:18 +03:00
deytenit cb956be216 fix: Make Code more like pre/code elements 2026-02-10 07:26:53 +03:00
deytenit 42a0b71a8f feat(ui): implement comprehensive Table component system with compound API 2026-02-09 23:15:39 +03:00
deytenit df2abc06f2 feat: Add more base component to UI 2026-02-01 16:24:45 +03:00
deytenit ca20310460 refactor: Refactor UI structure 2026-02-01 01:49:02 +03:00
deytenit bbb9503172 ci: Add astro type generation in CI 2026-01-31 21:08:50 +03:00
deytenit ef9d521e86 chore: Delete AI generated plan 2026-01-31 17:53:25 +03:00
deytenit 054cf4f5e3 feat: Add generic components for UI 2026-01-31 17:52:45 +03:00
deytenit 08a7f44588 feat: Add articles in www 2026-01-31 13:16:38 +03:00
775 changed files with 28666 additions and 24613 deletions

No files matched your search

+138 -169
View File
@@ -1,6 +1,6 @@
---
name: write-docs
description: "Use this to write uniform comprehensive documentation in ermnvldmr.com context, using nextra"
description: 'Use this to write uniform comprehensive documentation in ermnvldmr.com context, using nextra'
---
# HEXTRA DOCUMENTATION ARCHITECT
@@ -22,68 +22,59 @@ You must adhere to the following strict stylistic rules. There are no exceptions
### 1.1 Language & Localization
* **MUST** write in grammatically perfect **English** (or **Russian**, if explicitly requested by the prompt).
* **MUST** verify that all technical terminology matches the standard glossary of the specific domain (e.g., Hugo terms, Cloud infrastructure terms).
* **MUST** translate the entire document structure (headers, admonitions, link descriptions) when writing in a specific language. Do not mix languages unless referring to specific code entities that cannot be translated.
- **MUST** write in grammatically perfect **English** (or **Russian**, if explicitly requested by the prompt).
- **MUST** verify that all technical terminology matches the standard glossary of the specific domain (e.g., Hugo terms, Cloud infrastructure terms).
- **MUST** translate the entire document structure (headers, admonitions, link descriptions) when writing in a specific language. Do not mix languages unless referring to specific code entities that cannot be translated.
### 1.2 Typography & Symbols
* **MUST** use standard Unicode typography to enhance readability.
* *Allowed:* French quotes (`«...»`), em-dashes (`—`), en-dashes (`–`), ellipses (`…`), arrows (`→`, `←`).
- **MUST** use standard Unicode typography to enhance readability.
- _Allowed:_ French quotes (`«...»`), em-dashes (`—`), en-dashes (`–`), ellipses (`…`), arrows (`→`, `←`).
- **MUST NOT** use Emojis (`🚀`, `⚠️`, `✅`), Kaomojis (`(o_O)`), or non-standard decorative symbols.
- _Reasoning:_ These symbols break search functionality, interfere with copy-pasting, and degrade the professional tone of the documentation.
* **MUST NOT** use Emojis (`🚀`, `⚠️`, `✅`), Kaomojis (`(o_O)`), or non-standard decorative symbols.
* *Reasoning:* These symbols break search functionality, interfere with copy-pasting, and degrade the professional tone of the documentation.
* **MUST NOT** use "Title Case" for every word in headers. Capitalize the first word and proper nouns only, or follow standard sentence case rules (e.g., "Architecture of module A", not "Architecture Of Module A").
- **MUST NOT** use "Title Case" for every word in headers. Capitalize the first word and proper nouns only, or follow standard sentence case rules (e.g., "Architecture of module A", not "Architecture Of Module A").
### 1.3 Document Titles & Headers
* **MUST** ensure the document `title` (in Front Matter) is descriptive and summarizes the document's purpose.
* *Bad:* "Settings"
* *Good:* "Module A Configuration Settings"
- **MUST** ensure the document `title` (in Front Matter) is descriptive and summarizes the document's purpose.
- _Bad:_ "Settings"
- _Good:_ "Module A Configuration Settings"
- **MUST NOT** write a top-level H1 header (e.g., `# Title`) in the Markdown body, as Hextra automatically generates this from the Front Matter `title`.
* **MUST NOT** write a top-level H1 header (e.g., `# Title`) in the Markdown body, as Hextra automatically generates this from the Front Matter `title`.
* **MUST** use lower-case conjunctions in headers (and, or, the, of) unless they start the sentence.
- **MUST** use lower-case conjunctions in headers (and, or, the, of) unless they start the sentence.
### 1.4 Tone & Audience
* **MUST** adopt an **authoritative and concrete** tone.
* *Bad:* "You should probably try to click the button..."
* *Good:* "Click the **Submit** button."
- **MUST** adopt an **authoritative and concrete** tone.
- _Bad:_ "You should probably try to click the button..."
- _Good:_ "Click the **Submit** button."
- **MUST** accommodate different skill levels within the same documentation ecosystem:
- Provide **Quick Start** sections for beginners (minimal theory, fast results).
- Provide **How-to Guides** and **Concepts** for experienced users (complex scenarios, architectural deep-dives).
* **MUST** accommodate different skill levels within the same documentation ecosystem:
* Provide **Quick Start** sections for beginners (minimal theory, fast results).
* Provide **How-to Guides** and **Concepts** for experienced users (complex scenarios, architectural deep-dives).
- **SHOULD** maximize "Signal-to-Noise" ratio.
- Eliminate obvious statements.
- _Bad:_ "To save the file, click save. This will save your changes to the disk."
- _Good:_ "Save the file."
* **SHOULD** maximize "Signal-to-Noise" ratio.
* Eliminate obvious statements.
* *Bad:* "To save the file, click save. This will save your changes to the disk."
* *Good:* "Save the file."
* **MUST NOT** be chatty or conversational. Avoid phrases like "Let's dive in," "In this tutorial, we will learn," or "Happy coding!"
- **MUST NOT** be chatty or conversational. Avoid phrases like "Let's dive in," "In this tutorial, we will learn," or "Happy coding!"
### 1.5 Content Precision
* **MUST** define clear steps, expected results, and precise formulations.
* **SHOULD** only comment on code examples if the logic is non-obvious. Do not explain standard syntax (e.g., do not explain what a `for` loop does in Python, unless the loop logic is unique to the business case).
* **MUST** actively prompt the user for missing materials if the input is insufficient.
* *Example:* "I need the specific configuration parameters for Service X to complete this section. Please provide a link to the wiki or a code snippet."
- **MUST** define clear steps, expected results, and precise formulations.
- **SHOULD** only comment on code examples if the logic is non-obvious. Do not explain standard syntax (e.g., do not explain what a `for` loop does in Python, unless the loop logic is unique to the business case).
- **MUST** actively prompt the user for missing materials if the input is insufficient.
- _Example:_ "I need the specific configuration parameters for Service X to complete this section. Please provide a link to the wiki or a code snippet."
### 1.6 Source Fidelity (Tone Aspect)
* **MUST** write *only* what is confirmed by the provided sources.
* **MUST NOT** speculate, guess, or fill in "gray areas" with "typical" or "default" values unless explicitly instructed.
* **MUST NOT** invent "Best Practices" or "Common Mistakes" sections unless the source material explicitly lists them. Your job is to document the *actual* system, not a *hypothetical* ideal system.
- **MUST** write _only_ what is confirmed by the provided sources.
- **MUST NOT** speculate, guess, or fill in "gray areas" with "typical" or "default" values unless explicitly instructed.
- **MUST NOT** invent "Best Practices" or "Common Mistakes" sections unless the source material explicitly lists them. Your job is to document the _actual_ system, not a _hypothetical_ ideal system.
---
@@ -93,24 +84,23 @@ This is the most critical directive. You are a documentation engine, not a creat
### 2.1 The "Zero Hallucination" Policy (CRITICAL / MUST NOT)
* **MUST NOT** add "logical" assumptions, behavioral descriptions, or implementation details if they are not explicitly present in the source text or code.
* **MUST NOT** invent "Best Practices" or "Common Pitfalls" unless the source explicitly lists them.
* **MUST NOT** fabricate command-line arguments, file paths, service names, configuration parameters, or variable names. If a parameter name is missing, use a placeholder like `<variable_name>` and flag it.
- **MUST NOT** add "logical" assumptions, behavioral descriptions, or implementation details if they are not explicitly present in the source text or code.
- **MUST NOT** invent "Best Practices" or "Common Pitfalls" unless the source explicitly lists them.
- **MUST NOT** fabricate command-line arguments, file paths, service names, configuration parameters, or variable names. If a parameter name is missing, use a placeholder like `<variable_name>` and flag it.
### 2.2 Evidence-Based Writing (MUST)
* **MUST** ensure every technical assertion is backed by one of the provided sources (documents, user-provided files, chat logs).
* **MUST** explicitly identify missing information. If the source material is insufficient to complete a section (e.g., a missing step in a procedure):
- **MUST** ensure every technical assertion is backed by one of the provided sources (documents, user-provided files, chat logs).
- **MUST** explicitly identify missing information. If the source material is insufficient to complete a section (e.g., a missing step in a procedure):
1. Stop writing that specific section.
2. Ask the user clarifying questions.
3. List exactly what data is missing (e.g., "I need the default port number for Service X to complete the Configuration section.").
### 2.3 Handling Conflicts
* **MUST** integrate materials sequentially if multiple sources are provided. Process Source A -> Normalize headers/terms -> Process Source B.
* **MUST NOT** resolve contradictions yourself. If Source A says "Port 80" and Source B says "Port 8080", you must document the discrepancy and ask the user for the "Source of Truth."
- **MUST** integrate materials sequentially if multiple sources are provided. Process Source A -> Normalize headers/terms -> Process Source B.
- **MUST NOT** resolve contradictions yourself. If Source A says "Port 80" and Source B says "Port 8080", you must document the discrepancy and ask the user for the "Source of Truth."
---
@@ -120,27 +110,25 @@ You must structure the output to fit a standard localized Hugo project architect
### 3.1 File Locations (MUST)
* **MUST** assume documentation lives in the following localized directories:
* `content/en/` (English)
* `content/ru/` (Russian)
- **MUST** assume documentation lives in the following localized directories:
- `content/en/` (English)
- `content/ru/` (Russian)
* **MUST** provide output that is ready to be placed in these directories.
- **MUST** provide output that is ready to be placed in these directories.
### 3.2 Asset Management (MUST)
* **MUST** assume all static assets (images, videos, PDFs) are stored in the `static/` directory at the project root.
* **MUST** format links relative to the `static` root.
* *Input:* An image named `diagram.png` stored in `static/images/`.
* *Markdown Output:* `![Description](/images/diagram.png)` (Note the leading slash).
- **MUST** assume all static assets (images, videos, PDFs) are stored in the `static/` directory at the project root.
- **MUST** format links relative to the `static` root.
- _Input:_ An image named `diagram.png` stored in `static/images/`.
- _Markdown Output:_ `![Description](/images/diagram.png)` (Note the leading slash).
* **MUST NOT** include `static/` in the markdown link path itself (e.g., `![...](static/images/...)` is **WRONG**).
- **MUST NOT** include `static/` in the markdown link path itself (e.g., `![...](static/images/...)` is **WRONG**).
### 3.3 Translation Requirements (MUST)
* **MUST** be capable of generating the document in both English and Russian upon request.
* **MUST** ensure the glossary terms (see Part 3) are consistent across both languages.
- **MUST** be capable of generating the document in both English and Russian upon request.
- **MUST** ensure the glossary terms (see Part 3) are consistent across both languages.
---
@@ -152,84 +140,74 @@ A consistent glossary and linking strategy is mandatory for maintainable documen
The glossary is the "Source of Truth" for terminology. It resides in `(en/ru)/{project}/glossary.md`.
* **MUST** maintain the glossary in **Alphabetical Order**.
* **MUST** assign a Markdown anchor to every term in the glossary definition.
* *Format:* `## Term Name {#term-key}`
* *Definition Format:*
- **MUST** maintain the glossary in **Alphabetical Order**.
- **MUST** assign a Markdown anchor to every term in the glossary definition.
- _Format:_ `## Term Name {#term-key}`
- _Definition Format:_
```markdown
## Term Name {#term-key}
**Term Name** - A brief definition in 1-3 sentences.
```
- **MUST** link to these anchors whenever the term is first used in other documents.
- _Usage Syntax:_ `... configure the [Term Name](/glossary#term-key) settings...`
* **MUST** link to these anchors whenever the term is first used in other documents.
* *Usage Syntax:* `... configure the [Term Name](/glossary#term-key) settings...`
* **MUST** maintain the glossary in both English and Russian if the documentation is bilingual.
- **MUST** maintain the glossary in both English and Russian if the documentation is bilingual.
### 4.2 Hyperlink Formatting (MUST / SHOULD)
* **MUST NOT** use "naked" URLs in the text.
* *Bad:* `Check this link: https://example.com/api`
* *Bad:* `[https://example.com/api](https://example.com/api)`
- **MUST NOT** use "naked" URLs in the text.
- _Bad:_ `Check this link: https://example.com/api`
- _Bad:_ `[https://example.com/api](https://example.com/api)`
- **MUST** format links with the **Source Type** and **Content Description**.
- _Good:_ `[Wiki: API Deployment Guide](https://example.com/api)`
- _Good:_ `[RFC: HTTP/2 Protocol](https://tools.ietf.org/html/rfc7540)`
* **MUST** format links with the **Source Type** and **Content Description**.
* *Good:* `[Wiki: API Deployment Guide](https://example.com/api)`
* *Good:* `[RFC: HTTP/2 Protocol](https://tools.ietf.org/html/rfc7540)`
* **SHOULD** use relative paths for internal links using Hugo `ref` or standard relative markdown paths where possible.
- **SHOULD** use relative paths for internal links using Hugo `ref` or standard relative markdown paths where possible.
### 4.3 The "See Also" Footer (MUST)
Every document MUST end with a "See Also" block containing external references and last update metadata.
* **Location:** Always at the very bottom of the file.
* **Format:** Use a horizontal rule (`---`) followed by bold text.
* **Content:**
* **MUST** contain only a bulleted list of Markdown links.
* **MUST NOT** contain paths to files as inline code.
* **MUST NOT** link to the current document (self-referencing) or the Glossary (redundant).
- **Location:** Always at the very bottom of the file.
- **Format:** Use a horizontal rule (`---`) followed by bold text.
- **Content:**
- **MUST** contain only a bulleted list of Markdown links.
- **MUST NOT** contain paths to files as inline code.
- **MUST NOT** link to the current document (self-referencing) or the Glossary (redundant).
- **Template:**
* **Template:**
```markdown
---
**See also:**
- [Doc: Related Component A](https://docs.example.com/comp-a)
- [Wiki: Legacy Architecture](https://wiki.example.com/arch)
```
### 4.4 Code References (MUST)
* **Product Code:**
* **SHOULD NOT** paste large blocks of product source code, as it becomes stale.
* **MUST** link to a specific GitHub/GitLab commit (permalink) that shows the implementation.
* *Format:* `[GitHub: sha - Description of change](https://github.com/org/repo/blob/sha/file.go)`
- **Product Code:**
- **SHOULD NOT** paste large blocks of product source code, as it becomes stale.
- **MUST** link to a specific GitHub/GitLab commit (permalink) that shows the implementation.
- _Format:_ `[GitHub: sha - Description of change](https://github.com/org/repo/blob/sha/file.go)`
- **Commands & Scripts:**
- **MUST** use Fenced Code Blocks (````bash`) for instructions, scripts, or conceptual examples that the user needs to copy.
* **Commands & Scripts:**
* **MUST** use Fenced Code Blocks (````bash`) for instructions, scripts, or conceptual examples that the user needs to copy.
* **Inline Code Entities:**
* **MUST** use inline code backticks for:
* Variable names (`retryCount`)
* Function names (`init()`)
* Component names (`UserProfile`)
* Literal values (`true`, `404`)
* File extensions (`.md`, `.json`)
* Filenames (`hugo.yaml`)
- **Inline Code Entities:**
- **MUST** use inline code backticks for:
- Variable names (`retryCount`)
- Function names (`init()`)
- Component names (`UserProfile`)
- Literal values (`true`, `404`)
- File extensions (`.md`, `.json`)
- Filenames (`hugo.yaml`)
---
@@ -239,34 +217,26 @@ You must use Hextra's advanced features to create structured, interactive docume
### 5.1 Images & Media (MUST)
* **Alt Text & Titles:** Every image **MUST** have alt-text and a hover title.
* *Syntax:* `![Alt Text](./path/image.png "Hover Title" =700x)`
- **Alt Text & Titles:** Every image **MUST** have alt-text and a hover title.
- _Syntax:_ `![Alt Text](./path/image.png "Hover Title" =700x)`
- **Sizing:**
- **Small/Medium Images:** **SHOULD** be resized using the width attribute (e.g., `=700x`) to prevent layout shift.
- **Large Images/Diagrams:** **MUST NOT** be resized if detail loss is a risk. Instead, wrap them in a **Details** shortcode (see below).
* **Sizing:**
* **Small/Medium Images:** **SHOULD** be resized using the width attribute (e.g., `=700x`) to prevent layout shift.
* **Large Images/Diagrams:** **MUST NOT** be resized if detail loss is a risk. Instead, wrap them in a **Details** shortcode (see below).
* **YouTube:** Use the shortcode for videos.
* *Syntax:* `{{< youtube id="VIDEO_ID" loading="lazy" >}}`
* **PDFs:** Use the PDF shortcode for embeddings.
* *Syntax:* `{{< pdf "/path/to/doc.pdf" >}}`
- **YouTube:** Use the shortcode for videos.
- _Syntax:_ `{{< youtube id="VIDEO_ID" loading="lazy" >}}`
- **PDFs:** Use the PDF shortcode for embeddings.
- _Syntax:_ `{{< pdf "/path/to/doc.pdf" >}}`
### 5.2 UI & Text Formatting (MUST)
* **"Points of Attraction":** Use **Bold** for goals, key list items, important preambles, and action names.
* *Example:* **Goal:** Set up the environment.
* **UI References:** Use *Italics* for buttons, labels, window names, and screen titles.
* *Example:* Click the *Submit* button on the *Settings* screen.
- **"Points of Attraction":** Use **Bold** for goals, key list items, important preambles, and action names.
- _Example:_ **Goal:** Set up the environment.
- **UI References:** Use _Italics_ for buttons, labels, window names, and screen titles.
- _Example:_ Click the _Submit_ button on the _Settings_ screen.
### 5.3 Interactive Shortcodes (MUST / SHOULD)
@@ -276,22 +246,21 @@ Use for large screenshots, detailed logs, or deep-dive explanations that disrupt
```markdown
{{% details title="Click to reveal configuration details" closed="true" %}}
1. Detailed step one...
![Large Screenshot](/images/screen.png "Settings Config")
{{% /details %}}
1. Detailed step one...
![Large Screenshot](/images/screen.png 'Settings Config')
{{% /details %}}
```
#### B. Tabs (Mutually Exclusive Content)
Use when the user must choose *one* path (e.g., OS specific, Language specific).
Use when the user must choose _one_ path (e.g., OS specific, Language specific).
```markdown
{{< tabs items="JSON,YAML" >}}
{{< tab >}} **JSON** content here... {{< /tab >}}
{{< tab >}} **YAML** content here... {{< /tab >}}
{{< tabs >}}
{{< tab name="JSON" >}} **JSON** content here... {{< /tab >}}
{{< tab name="YAML" >}} **YAML** content here... {{< /tab >}}
{{< /tabs >}}
```
#### C. Steps (Procedures)
@@ -300,25 +269,26 @@ Use for sequential instructions. **MUST** use H3 (`###`) headers inside.
```markdown
{{% steps %}}
### Step 1: Install
Run `npm install`
### Step 2: Configure
Edit `config.js`
{{% /steps %}}
```
#### D. Badges (Status/Versioning)
Use to denote version availability or feature status.
* *Options:* `color` (green, orange, red, blue), `icon` (sparkles, github), `style` (outline/filled).
- _Options:_ `color` (green, orange, red, blue), `icon` (sparkles, github), `style` (outline/filled).
```markdown
{{< badge content="v2.0+" color="green" >}}
{{< badge content="Beta" color="orange" icon="sparkles" >}}
```
#### E. File Trees (Directory Structure)
@@ -327,11 +297,10 @@ Use to visualize folder layouts.
```markdown
{{< filetree/container >}}
{{< filetree/folder name="src" state="open" >}}
{{< filetree/file name="index.js" >}}
{{< /filetree/folder >}}
{{< filetree/folder name="src" state="open" >}}
{{< filetree/file name="index.js" >}}
{{< /filetree/folder >}}
{{< /filetree/container >}}
```
#### F. Alerts/Callouts
@@ -344,7 +313,6 @@ Use GitHub-flavored alerts for emphasis.
> [!WARNING]
> This action is irreversible.
```
---
@@ -352,7 +320,7 @@ Use GitHub-flavored alerts for emphasis.
## 6. DOCUMENT TYPES & TEMPLATES
Do not output full-page boilerplate unless requested. Instead, apply these structural rules ("Skeletons") dynamically based on the content type.
This is also serves as the fixed categories which should populate every project's docs: Getting Started, Guides, Recipes, Concepts, Troubleshooting.
This is also serves as the fixed categories which should populate every project's docs: Getting Started, Guides, Concepts, Troubleshooting.
### 6.1 General Skeleton (All Docs)
@@ -364,46 +332,47 @@ This is also serves as the fixed categories which should populate every project'
### 6.2 Quick Start (Beginner)
* **Goal:** Minimal steps to "Hello World".
* **Structure:**
- **Goal:** Minimal steps to "Hello World".
- **Structure:**
1. **Prerequisites:** (Brief list).
2. **Steps:** Use `{{% steps %}}`. Keep theory to zero.
3. **Validation:** How to check it worked.
### 6.3 Procedure / Guide (Strict)
* **Goal:** Reproducible, audit-ready process.
* **Structure:**
- **Goal:** Reproducible, audit-ready process.
- **Structure:**
1. **Objective:** What is being done.
2. **Launch Conditions:** When to start (Triggers).
3. **Procedure:** Strict numbered steps (or `{{% steps %}}`).
4. **Result Control:** How to verify success.
### 6.4 How-to / Guide (Task-Oriented)
### 6.4 How-to / Recipe (Task-Oriented)
- **Goal:** Solve a specific problem.
- **Structure:**
* **Goal:** Solve a specific problem.
* **Structure:**
1. **Context:** Why do this?
2. **Steps:** Detailed execution.
3. **Notes:** Edge cases or warnings.
### 6.5 Concept / Architecture (Theory)
* **Goal:** Explain how it works.
* **Structure:**
- **Goal:** Explain how it works.
- **Structure:**
1. **Overview:** High-level summary.
2. **Architecture:** Mermaid diagrams or Images.
3. **Components:** Description of parts.
4. **Terminology:** Definitions.
### 6.6 Troubleshooting (Diagnostic)
* **Goal:** Fix a broken state.
* **Structure:**
- **Goal:** Fix a broken state.
- **Structure:**
1. **Symptoms:** Error logs, behaviors.
2. **Diagnosis:** How to confirm the issue.
3. **Solution:** Steps to resolve.
@@ -417,18 +386,18 @@ You are now active. Your operating loop is as follows:
1. **Wait for Input:** Receive raw text, code, or a topic request.
2. **Classify:** Determine the Document Type (Quick Start, How-to, etc.).
3. **Validate Sources:** Check if you have enough info.
* *If info is missing:* STOP and ask the user specific questions.
* *If info is contradictory:* STOP and ask for the "Source of Truth".
- _If info is missing:_ STOP and ask the user specific questions.
- _If info is contradictory:_ STOP and ask for the "Source of Truth".
4. **Draft:** Generate the Hextra Markdown.
5. **Audit:**
* Did I use `filename=""` in code blocks?
* Did I use Hextra shortcodes (`steps`, `tabs`) effectively?
* Are links descriptive (No naked URLs)?
* Is the "See Also" footer present?
* Did I avoid hallucinations?
- Did I use `filename=""` in code blocks?
- Did I use Hextra shortcodes (`steps`, `tabs`) effectively?
- Are links descriptive (No naked URLs)?
- Is the "See Also" footer present?
- Did I avoid hallucinations?
6. **Output:** Present the final Markdown code block.
@@ -3,8 +3,9 @@ name: packages/stl - next/required
on:
push:
branches: [next]
paths:
paths:
- 'packages/stl/**'
- '.github/workflows/packages-stl-next-required.yml'
- 'pnpm-lock.yaml'
concurrency:
@@ -58,17 +59,13 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Jest config
run: pnpm --filter @ermnvldmr/jest-config build
- name: Build Vite config
run: pnpm --filter @ermnvldmr/vite-config build
- name: Build Rsbuild config
run: pnpm --filter @ermnvldmr/rsbuild-config build
- name: Upload Config Artifacts
uses: actions/upload-artifact@v4
with:
name: config-artifacts
path: |
packages/jest-config/dist
packages/vite-config/dist
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
retention-days: 1
lint-stl:
@@ -90,8 +87,8 @@ jobs:
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Lint STL package
run: pnpm --filter @ermnvldmr/stl lint
@@ -114,8 +111,8 @@ jobs:
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Typecheck STL package
run: pnpm --filter @ermnvldmr/stl typecheck
@@ -138,8 +135,8 @@ jobs:
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Test STL package
run: pnpm --filter @ermnvldmr/stl test:coverage
- name: Upload coverage to Codecov
@@ -169,7 +166,7 @@ jobs:
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Build STL package
run: pnpm --filter @ermnvldmr/stl build
@@ -0,0 +1,172 @@
name: packages/stl - pr/required
on:
pull_request:
branches: [next]
paths:
- 'packages/stl/**'
- '.github/workflows/packages-stl-pr-required.yml'
- 'pnpm-lock.yaml'
concurrency:
group: ci-pr-stl-${{ github.head_ref }}
cancel-in-progress: true
jobs:
setup-stage:
name: Setup - Stage
runs-on: ubuntu-latest
outputs:
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Cache pnpm store
id: cache-pnpm-store
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
build-configs:
name: Build - Configs
needs: setup-stage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Rsbuild config
run: pnpm --filter @ermnvldmr/rsbuild-config build
- name: Upload Config Artifacts
uses: actions/upload-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
retention-days: 1
lint-stl:
name: Lint - STL
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Lint STL package
run: pnpm --filter @ermnvldmr/stl lint
typecheck-stl:
name: Typecheck - STL
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Typecheck STL package
run: pnpm --filter @ermnvldmr/stl typecheck
test-stl:
name: Test - STL
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Test STL package
run: pnpm --filter @ermnvldmr/stl test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./packages/stl/coverage/lcov.info
flags: stl
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
build-stl:
name: Build - STL
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Build STL package
run: pnpm --filter @ermnvldmr/stl build
+100 -35
View File
@@ -3,9 +3,11 @@ name: packages/ui - next/required
on:
push:
branches: [next]
paths:
paths:
- 'packages/ui/**'
- 'packages/stl/**'
- 'packages/i18n/**'
- '.github/workflows/packages-ui-next-required.yml'
- 'pnpm-lock.yaml'
concurrency:
@@ -59,20 +61,29 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Jest config
run: pnpm --filter @ermnvldmr/jest-config build
- name: Build Vite config
run: pnpm --filter @ermnvldmr/vite-config build
- name: Build Rsbuild config
run: pnpm --filter @ermnvldmr/rsbuild-config build
- name: Build Storybook config
run: pnpm --filter @ermnvldmr/storybook-config build
- name: Upload Config Artifacts
- name: Build i18n
run: pnpm --filter @ermnvldmr/i18n build
- name: Upload Rsbuild Config
uses: actions/upload-artifact@v4
with:
name: config-artifacts
path: |
packages/jest-config/dist
packages/vite-config/dist
packages/storybook-config/dist
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
retention-days: 1
- name: Upload Storybook Config
uses: actions/upload-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
retention-days: 1
- name: Upload i18n dist
uses: actions/upload-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
retention-days: 1
lint-ui:
@@ -91,13 +102,21 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
- name: Lint STL package
run: pnpm --filter @ermnvldmr/stl lint
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Lint UI package
run: pnpm --filter @ermnvldmr/ui lint
@@ -117,13 +136,21 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
- name: Typecheck STL package
run: pnpm --filter @ermnvldmr/stl typecheck
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Typecheck UI package
run: pnpm --filter @ermnvldmr/ui typecheck
@@ -143,11 +170,21 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Test UI package
run: pnpm --filter @ermnvldmr/ui test:coverage
- name: Upload coverage to Codecov
@@ -174,13 +211,21 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
- name: Build UI package
run: pnpm --filter @ermnvldmr/ui build
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Build UI Storybook
run: pnpm --filter @ermnvldmr/ui build-storybook
@@ -200,11 +245,21 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Install Playwright browsers
run: pnpm --filter @ermnvldmr/ui exec playwright install --with-deps chromium
- name: Build UI Storybook
@@ -235,10 +290,20 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Build UI package
run: pnpm --filter @ermnvldmr/ui build
@@ -0,0 +1,318 @@
name: packages/ui - pr/required
on:
pull_request:
branches: [next]
paths:
- 'packages/ui/**'
- 'packages/stl/**'
- 'packages/i18n/**'
- '.github/workflows/packages-ui-pr-required.yml'
- 'pnpm-lock.yaml'
concurrency:
group: ci-pr-ui-${{ github.head_ref }}
cancel-in-progress: true
jobs:
setup-stage:
name: Setup - Stage
runs-on: ubuntu-latest
outputs:
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Cache pnpm store
id: cache-pnpm-store
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
build-configs:
name: Build - Configs
needs: setup-stage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Rsbuild config
run: pnpm --filter @ermnvldmr/rsbuild-config build
- name: Build Storybook config
run: pnpm --filter @ermnvldmr/storybook-config build
- name: Build i18n
run: pnpm --filter @ermnvldmr/i18n build
- name: Upload Rsbuild Config
uses: actions/upload-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
retention-days: 1
- name: Upload Storybook Config
uses: actions/upload-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
retention-days: 1
- name: Upload i18n dist
uses: actions/upload-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
retention-days: 1
lint-ui:
name: Lint - UI
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Lint UI package
run: pnpm --filter @ermnvldmr/ui lint
typecheck-ui:
name: Typecheck - UI
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Typecheck UI package
run: pnpm --filter @ermnvldmr/ui typecheck
test-ui:
name: Test - Unit
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Test UI package
run: pnpm --filter @ermnvldmr/ui test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./packages/ui/coverage/lcov.info
flags: ui
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
build-storybook:
name: Build - Storybook
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Build UI Storybook
run: pnpm --filter @ermnvldmr/ui build-storybook
- name: Upload Storybook build
uses: actions/upload-artifact@v4
with:
name: ui-storybook-static-${{ github.run_id }}
path: packages/ui/storybook-static/
retention-days: 1
test-storybook:
name: Test - Storybook
needs: build-storybook
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Install Playwright browsers
run: pnpm --filter @ermnvldmr/ui exec playwright install --with-deps chromium
- name: Download Storybook build
uses: actions/download-artifact@v4
with:
name: ui-storybook-static-${{ github.run_id }}
path: packages/ui/storybook-static/
- name: Test Storybook
run: pnpm --filter @ermnvldmr/ui test:storybook:ci
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./packages/ui/coverage/storybook/lcov.info
flags: ui-storybook
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
build-ui:
name: Build - UI
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Build UI package
run: pnpm --filter @ermnvldmr/ui build
@@ -3,7 +3,7 @@ name: services/docs - next/required
on:
push:
branches: [next]
paths:
paths:
- 'services/docs/**'
concurrency:
@@ -106,4 +106,3 @@ jobs:
# Use the short SHA to match the tag from release-docker
export DOCS_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)"
./ci/scripts/deploy-new-tags.sh
@@ -0,0 +1,50 @@
name: services/docs - pr/required
on:
pull_request:
branches: [next]
paths:
- 'services/docs/**'
- '.github/workflows/services-docs-pr-required.yml'
concurrency:
group: ci-pr-docs-${{ github.head_ref }}
cancel-in-progress: true
jobs:
setup-hugo:
name: Setup - Hugo
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: 'latest'
extended: true
build-hugo:
name: Build - Hugo Site
needs: setup-hugo
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: 'latest'
extended: true
- name: Build Hugo site
working-directory: services/docs
run: hugo --gc --minify
@@ -6,6 +6,9 @@ on:
paths:
- 'services/static/**'
- 'packages/ui/**'
- 'packages/stl/**'
- 'packages/i18n/**'
- '.github/workflows/services-static-next-required.yml'
- 'pnpm-lock.yaml'
concurrency:
@@ -43,8 +46,8 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
lint-static:
name: Lint - Static
build-configs:
name: Build - Configs
needs: setup-stage
runs-on: ubuntu-latest
steps:
@@ -59,12 +62,55 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Rsbuild config
run: pnpm --filter @ermnvldmr/rsbuild-config build
- name: Build i18n
run: pnpm --filter @ermnvldmr/i18n build
- name: Upload Config Artifacts
uses: actions/upload-artifact@v4
with:
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
retention-days: 1
- name: Upload i18n dist
uses: actions/upload-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
retention-days: 1
lint-static:
name: Lint - Static
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Lint static service
run: pnpm --filter @ermnvldmr/static lint
typecheck-static:
name: Typecheck - Static
needs: setup-stage
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -78,12 +124,22 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Typecheck static service
run: pnpm --filter @ermnvldmr/static typecheck
build-static:
name: Build - Static Site
needs: setup-stage
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -97,6 +153,16 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Build static service
run: pnpm --filter @ermnvldmr/static build
- name: Upload build artifacts
@@ -0,0 +1,173 @@
name: services/static - pr/required
on:
pull_request:
branches: [next]
paths:
- 'services/static/**'
- 'packages/ui/**'
- 'packages/stl/**'
- 'packages/i18n/**'
- '.github/workflows/services-static-pr-required.yml'
- 'pnpm-lock.yaml'
concurrency:
group: ci-pr-static-${{ github.head_ref }}
cancel-in-progress: true
jobs:
setup-stage:
name: Setup - Stage
runs-on: ubuntu-latest
outputs:
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Cache pnpm store
id: cache-pnpm-store
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
build-configs:
name: Build - Configs
needs: setup-stage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Rsbuild config
run: pnpm --filter @ermnvldmr/rsbuild-config build
- name: Build i18n
run: pnpm --filter @ermnvldmr/i18n build
- name: Upload Config Artifacts
uses: actions/upload-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
retention-days: 1
- name: Upload i18n dist
uses: actions/upload-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
retention-days: 1
lint-static:
name: Lint - Static
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Lint static service
run: pnpm --filter @ermnvldmr/static lint
typecheck-static:
name: Typecheck - Static
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Typecheck static service
run: pnpm --filter @ermnvldmr/static typecheck
build-static:
name: Build - Static Site
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Build static service
run: pnpm --filter @ermnvldmr/static build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: static-dist-${{ github.run_id }}
path: services/static/dist/
retention-days: 1
+113 -46
View File
@@ -3,10 +3,12 @@ name: services/www - next/required
on:
push:
branches: [next]
paths:
paths:
- 'services/www/**'
- 'packages/ui/**'
- 'packages/stl/**'
- 'packages/i18n/**'
- '.github/workflows/services-www-next-required.yml'
- 'pnpm-lock.yaml'
concurrency:
@@ -60,24 +62,33 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Jest config
run: pnpm --filter @ermnvldmr/jest-config build
- name: Build Vite config
run: pnpm --filter @ermnvldmr/vite-config build
- name: Build Rsbuild config
run: pnpm --filter @ermnvldmr/rsbuild-config build
- name: Build Storybook config
run: pnpm --filter @ermnvldmr/storybook-config build
- name: Upload Config Artifacts
- name: Upload Rsbuild Config
uses: actions/upload-artifact@v4
with:
name: config-artifacts
path: |
packages/jest-config/dist
packages/vite-config/dist
packages/storybook-config/dist
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
retention-days: 1
- name: Upload Storybook Config
uses: actions/upload-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
retention-days: 1
- name: Build i18n package
run: pnpm --filter @ermnvldmr/i18n build
- name: Upload i18n dist
uses: actions/upload-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
retention-days: 1
lint-www:
name: Lint - WWW & Deps
name: Lint - WWW
needs: build-configs
runs-on: ubuntu-latest
steps:
@@ -92,20 +103,26 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
- name: Lint STL package
run: pnpm --filter @ermnvldmr/stl lint
- name: Lint UI package
run: pnpm --filter @ermnvldmr/ui lint
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
- name: Lint WWW service
run: pnpm --filter @ermnvldmr/www lint
typecheck-www:
name: Typecheck - WWW & Deps
name: Typecheck - WWW
needs: build-configs
runs-on: ubuntu-latest
steps:
@@ -120,15 +137,21 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
- name: Typecheck STL package
run: pnpm --filter @ermnvldmr/stl typecheck
- name: Typecheck UI package
run: pnpm --filter @ermnvldmr/ui typecheck
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
- name: Typecheck WWW service
run: pnpm --filter @ermnvldmr/www typecheck
@@ -148,11 +171,21 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
- name: Build WWW Storybook
run: pnpm --filter @ermnvldmr/www build-storybook
- name: Upload Storybook build
@@ -178,15 +211,21 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
- name: Test STL package
run: pnpm --filter @ermnvldmr/stl test:coverage
- name: Test UI package
run: pnpm --filter @ermnvldmr/ui test:coverage
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
- name: Test WWW service
run: pnpm --filter @ermnvldmr/www test:coverage
- name: Upload Unit coverage to Codecov
@@ -213,11 +252,21 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
- name: Install Playwright browsers
run: pnpm --filter @ermnvldmr/www exec playwright install --with-deps chromium
- name: Download Storybook build
@@ -251,13 +300,31 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Config Artifacts
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: config-artifacts
path: packages
- name: Build WWW service
run: pnpm --filter @ermnvldmr/www build
name: rsbuild-config-dist
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist
path: packages/i18n/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist
path: packages/storybook-config/dist
- name: Build WWW service (all locales)
run: pnpm --filter @ermnvldmr/www build:all
env:
RSDOCTOR: 'true'
- name: Upload Rsdoctor report
uses: actions/upload-artifact@v4
with:
name: rsdoctor-report
path: services/www/rsdoctor/
retention-days: 30
release-docker:
name: Release - Docker Image
@@ -293,4 +360,4 @@ jobs:
file: ci/docker/www/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }}
@@ -0,0 +1,327 @@
name: services/www - pr/required
on:
pull_request:
branches: [next]
paths:
- 'services/www/**'
- 'packages/ui/**'
- 'packages/stl/**'
- 'packages/i18n/**'
- '.github/workflows/services-www-pr-required.yml'
- 'pnpm-lock.yaml'
concurrency:
group: ci-pr-www-${{ github.head_ref }}
cancel-in-progress: true
jobs:
setup-stage:
name: Setup - Stage
runs-on: ubuntu-latest
outputs:
cache-hit: ${{ steps.cache-pnpm-store.outputs.cache-hit }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Cache pnpm store
id: cache-pnpm-store
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
build-configs:
name: Build - Configs
needs: setup-stage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Rsbuild config
run: pnpm --filter @ermnvldmr/rsbuild-config build
- name: Build Storybook config
run: pnpm --filter @ermnvldmr/storybook-config build
- name: Upload Rsbuild Config
uses: actions/upload-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
retention-days: 1
- name: Upload Storybook Config
uses: actions/upload-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
retention-days: 1
- name: Build i18n package
run: pnpm --filter @ermnvldmr/i18n build
- name: Upload i18n dist
uses: actions/upload-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
retention-days: 1
lint-www:
name: Lint - WWW
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
- name: Lint WWW service
run: pnpm --filter @ermnvldmr/www lint
typecheck-www:
name: Typecheck - WWW
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
- name: Typecheck WWW service
run: pnpm --filter @ermnvldmr/www typecheck
build-storybook:
name: Build - Storybook
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
- name: Build WWW Storybook
run: pnpm --filter @ermnvldmr/www build-storybook
- name: Upload Storybook build
uses: actions/upload-artifact@v4
with:
name: www-storybook-static-${{ github.run_id }}
path: services/www/storybook-static/
retention-days: 1
test-www:
name: Test - Unit
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
- name: Test WWW service
run: pnpm --filter @ermnvldmr/www test:coverage
- name: Upload Unit coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./services/www/coverage/lcov.info
flags: www
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test-storybook:
name: Test - Storybook
needs: build-storybook
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
- name: Install Playwright browsers
run: pnpm --filter @ermnvldmr/www exec playwright install --with-deps chromium
- name: Download Storybook build
uses: actions/download-artifact@v4
with:
name: www-storybook-static-${{ github.run_id }}
path: services/www/storybook-static/
- name: Serve Storybook and run tests with coverage
run: pnpm --filter @ermnvldmr/www test:storybook:ci
- name: Upload Storybook coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./services/www/coverage/storybook/lcov.info
flags: www-storybook
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
build-www:
name: Build - WWW Service
needs: build-configs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download Rsbuild Config
uses: actions/download-artifact@v4
with:
name: rsbuild-config-dist-${{ github.run_id }}
path: packages/rsbuild-config/dist
- name: Download i18n dist
uses: actions/download-artifact@v4
with:
name: i18n-dist-${{ github.run_id }}
path: packages/i18n/dist
- name: Download Storybook Config
uses: actions/download-artifact@v4
with:
name: storybook-config-dist-${{ github.run_id }}
path: packages/storybook-config/dist
- name: Build WWW service (all locales)
run: pnpm --filter @ermnvldmr/www build:all
env:
RSDOCTOR: 'true'
- name: Upload Rsdoctor report
uses: actions/upload-artifact@v4
with:
name: rsdoctor-report-${{ github.run_id }}
path: services/www/rsdoctor/
retention-days: 7
@@ -50,24 +50,24 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Jest config
run: pnpm --filter @ermnvldmr/jest-config build
- name: Build Vite config
run: pnpm --filter @ermnvldmr/vite-config build
- name: Build Rsbuild config
run: pnpm --filter @ermnvldmr/rsbuild-config build
- name: Build Storybook config
run: pnpm --filter @ermnvldmr/storybook-config build
- name: Build i18n
run: pnpm --filter @ermnvldmr/i18n build
- name: Upload Config Artifacts
uses: actions/upload-artifact@v4
with:
name: config-artifacts
path: |
packages/jest-config/dist
packages/vite-config/dist
packages/rsbuild-config/dist
packages/storybook-config/dist
packages/i18n/dist
retention-days: 1
lint-www:
name: Lint - WWW & Deps
name: Lint - WWW
needs: build-configs
runs-on: ubuntu-latest
steps:
@@ -87,15 +87,11 @@ jobs:
with:
name: config-artifacts
path: packages
- name: Lint STL package
run: pnpm --filter @ermnvldmr/stl lint
- name: Lint UI package
run: pnpm --filter @ermnvldmr/ui lint
- name: Lint WWW service
run: pnpm --filter @ermnvldmr/www lint
typecheck-www:
name: Typecheck - WWW & Deps
name: Typecheck - WWW
needs: build-configs
runs-on: ubuntu-latest
steps:
@@ -115,10 +111,6 @@ jobs:
with:
name: config-artifacts
path: packages
- name: Typecheck STL package
run: pnpm --filter @ermnvldmr/stl typecheck
- name: Typecheck UI package
run: pnpm --filter @ermnvldmr/ui typecheck
- name: Typecheck WWW service
run: pnpm --filter @ermnvldmr/www typecheck
@@ -173,10 +165,6 @@ jobs:
with:
name: config-artifacts
path: packages
- name: Test STL package
run: pnpm --filter @ermnvldmr/stl test:coverage
- name: Test UI package
run: pnpm --filter @ermnvldmr/ui test:coverage
- name: Test WWW service
run: pnpm --filter @ermnvldmr/www test:coverage
@@ -298,7 +286,7 @@ jobs:
name: Deploy - VPS
needs: release-tag-docker
runs-on: ubuntu-latest
environment:
environment:
name: services-www-releases-deploy
url: https://ermnvldmr.com
steps:
@@ -315,4 +303,4 @@ jobs:
chmod +x ci/scripts/deploy-new-tags.sh
# Use the short SHA to match the tag from release-tag-docker
export WWW_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)"
./ci/scripts/deploy-new-tags.sh
./ci/scripts/deploy-new-tags.sh
@@ -56,24 +56,24 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Jest config
run: pnpm --filter @ermnvldmr/jest-config build
- name: Build Vite config
run: pnpm --filter @ermnvldmr/vite-config build
- name: Build Rsbuild config
run: pnpm --filter @ermnvldmr/rsbuild-config build
- name: Build Storybook config
run: pnpm --filter @ermnvldmr/storybook-config build
- name: Build i18n
run: pnpm --filter @ermnvldmr/i18n build
- name: Upload Config Artifacts
uses: actions/upload-artifact@v4
with:
name: config-artifacts
path: |
packages/jest-config/dist
packages/vite-config/dist
packages/rsbuild-config/dist
packages/storybook-config/dist
packages/i18n/dist
retention-days: 1
lint-www:
name: Lint - WWW & Deps
name: Lint - WWW
needs: build-configs
runs-on: ubuntu-latest
steps:
@@ -93,15 +93,11 @@ jobs:
with:
name: config-artifacts
path: packages
- name: Lint STL package
run: pnpm --filter @ermnvldmr/stl lint
- name: Lint UI package
run: pnpm --filter @ermnvldmr/ui lint
- name: Lint WWW service
run: pnpm --filter @ermnvldmr/www lint
typecheck-www:
name: Typecheck - WWW & Deps
name: Typecheck - WWW
needs: build-configs
runs-on: ubuntu-latest
steps:
@@ -121,10 +117,6 @@ jobs:
with:
name: config-artifacts
path: packages
- name: Typecheck STL package
run: pnpm --filter @ermnvldmr/stl typecheck
- name: Typecheck UI package
run: pnpm --filter @ermnvldmr/ui typecheck
- name: Typecheck WWW service
run: pnpm --filter @ermnvldmr/www typecheck
@@ -179,10 +171,6 @@ jobs:
with:
name: config-artifacts
path: packages
- name: Test STL package
run: pnpm --filter @ermnvldmr/stl test:coverage
- name: Test UI package
run: pnpm --filter @ermnvldmr/ui test:coverage
- name: Test WWW service
run: pnpm --filter @ermnvldmr/www test:coverage
@@ -283,4 +271,4 @@ jobs:
file: ci/docker/www/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }}
+5 -2
View File
@@ -1,7 +1,6 @@
# build output
dist/
# generated types
.astro/
*.tsbuildinfo
# dependencies
node_modules/
@@ -28,3 +27,7 @@ playwright-report/
# jetbrains setting folder
.idea/
# agents plans and designs
docs/plans/
docs/superpowers/
+3
View File
@@ -0,0 +1,3 @@
[submodule "services/docs/themes/hextra"]
path = services/docs/themes/hextra
url = https://github.com/imfing/hextra
+13
View File
@@ -0,0 +1,13 @@
node_modules
dist
build
coverage
storybook-static
.next
.astro
public
**/public/**
**/dist/**
**/node_modules/**
pnpm-lock.yaml
services/docs/themes/hextra
+1 -2
View File
@@ -4,6 +4,5 @@
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"plugins": ["prettier-plugin-astro", "prettier-plugin-react", "prettier-plugin-tailwindcss"]
"useTabs": false
}
+146 -49
View File
@@ -5,6 +5,7 @@
**ermnvldmr.com** is Vladimir Eremin's personal ecosystem of services delivered through a modern monorepo architecture. The project encompasses personal websites, documentation sites, and shared infrastructure for web-based content and services.
### Monorepo Architecture
- **Repository Structure**: pnpm workspace monorepo
- **Package Manager**: pnpm v9.12.3+
- **Core Technology**: TypeScript (Strict mode)
@@ -14,6 +15,7 @@
- **CI/CD**: GitHub Actions with path-based triggers
### Design Philosophy
- **Component-First**: All UI elements developed as isolated, reusable components
- **Configuration Sharing**: Centralized tooling configurations across services
- **TypeScript-First**: Strict typing for all public APIs and components
@@ -26,13 +28,12 @@
```
├── packages/ # Shared packages and configurations
│ ├── eslint-config/ # Shared ESLint configurations
│ ├── jest-config/ # Shared Jest configurations
│ ├── rsbuild-config/ # Shared Rsbuild configurations
│ ├── storybook-config/ # Shared Storybook configurations
│ ├── vite-config/ # Shared Vite configurations
│ ├── stl/ # Standard Type Library (utilities)
│ └── ui/ # Shared React component library
├── services/ # Independent web services
│ ├── www/ # Main website (Astro + React)
│ ├── www/ # Main website (Rsbuild + React)
│ └── docs/ # Documentation site (Hugo)
├── .github/workflows/ # CI/CD automation
├── pnpm-workspace.yaml # Workspace configuration
@@ -42,22 +43,27 @@
### Package Organization
#### Configuration Packages (`packages/`)
- **eslint-config**: Shared ESLint rules for TypeScript, React, and Astro
- **jest-config**: Shared Jest configurations for different project types
- **eslint-config**: Shared ESLint rules for TypeScript and React
- **rsbuild-config**: Shared Rsbuild and Rspack configurations for library and service builds
- **storybook-config**: Shared Storybook setup and configurations
- **vite-config**: Shared Vite configurations for library builds
#### Library Packages (`packages/`)
- **stl** (Standard Type Library): Common utilities, types, and helper functions
- **ui**: React component library with design system and Storybook stories
- **i18n**: Internationalization utilities
#### Service Packages (`services/`)
- **www**: Main website built with Astro.js, React, and Tailwind CSS
- **www**: Main website built with Rsbuild, React, and Tailwind CSS
- **static**: Static landing pages service built with Rsbuild and React
- **docs**: Documentation site built with Hugo and Hextra theme
### Key Directory Rules
#### `/packages/`
- **Purpose**: Shared configurations and reusable libraries
- **Dependencies**: Can depend on each other following build order (configs → stl → ui)
- **Exports**: Must provide proper TypeScript declarations
@@ -65,6 +71,7 @@
- **Testing**: Individual test suites for each package
#### `/services/`
- **Purpose**: Independent web applications and sites
- **Dependencies**: Can consume packages but not other services
- **Deployment**: Independent deployment pipelines
@@ -74,59 +81,93 @@
## 🛠️ Development Workflow
### Monorepo Development Principles
- **Root-Level Scripts**: Check `package.json` at repository root for monorepo-wide operations
- **Service-Specific Scripts**: Each service in `services/` has its own development commands
- **Package Filtering**: Use pnpm workspace filtering to target specific packages/services
- **Dependency-Aware Building**: UI components must be built before dependent services
### Package Development Workflow
The monorepo follows a strict dependency hierarchy that must be respected:
#### 1. Configuration Packages First
Configuration packages provide shared tooling setups and must be built before anything else. Look for packages ending in `-config` in the `packages/` directory.
#### 2. Library Packages
Library packages (`stl`, `ui`) provide reusable code and components. They depend on configuration packages and must be built in dependency order (check their `package.json` dependencies).
#### 3. Services
Services consume packages but are independent of each other. They can be developed and deployed separately. Check `services/` directory for available services and their individual `package.json` files for available scripts.
### Code Organization Principles
- **Dependency Order**: configs → stl → ui → services
- **Single Responsibility**: Each package serves one clear purpose
- **Explicit Dependencies**: All inter-package dependencies declared in package.json
- **Build Isolation**: Each package can be built independently
- **Configuration Inheritance**: Services inherit from shared configurations
- **Package Entry Points**:
- `.` (Main): Runtime code used in production (e.g., `import { ... } from '@ermnvldmr/stl'`).
- `/testing`: Test-related utilities and mocks (e.g., `import { ... } from '@ermnvldmr/stl/testing'`).
- `/dev`: Build-time dependencies (e.g., `import { ... } from '@ermnvldmr/stl/dev'`).
- All other entry points are forbidden. Strictly build-time packages must still expose via `/dev`.
- **Directory Structure (Packages & Services)**:
- `src/lib/`: Helpers, utilities, and types.
- `{category}/`: Grouped logic.
- `index.ts`: Barrel export for all of `lib/`.
- `testing.ts`: Testing utilities for the library.
- `dev.ts`: Build-time utilities for the library.
- `src/components/`: UI components.
- `{ComponentName}/`: Isolated component directory.
- `index.ts`: Barrel export for all components.
- `testing.ts`: Component-specific testing utilities.
- `src/static/`: Static assets (`.html`, `.css`, `.js`) directly in this directory (no subdirectories).
- `src/app/` (Services only): Page definitions.
- `{page}.tsx`: Single file per page, inlining the component in `createPage`.
- `{path}/{page}.tsx`: Nested routing structure.
- Top-level `src/index.ts`, `src/testing.ts`, and `src/dev.ts` should only re-export from `lib/` or `components/`.
## 📝 Coding Conventions
### Package Naming Convention
All packages use the `@ermnvldmr/` namespace:
- `@ermnvldmr/eslint-config` - Shared ESLint configurations
- `@ermnvldmr/jest-config` - Shared Jest configurations
- `@ermnvldmr/rsbuild-config` - Shared Rsbuild configurations
- `@ermnvldmr/storybook-config` - Shared Storybook configurations
- `@ermnvldmr/vite-config` - Shared Vite configurations
- `@ermnvldmr/stl` - Standard Type Library
- `@ermnvldmr/ui` - Component library
- `@ermnvldmr/i18n` - Internationalization utilities
- `@ermnvldmr/www` - Main website service
- `@ermnvldmr/static` - Static landing pages service
- `@ermnvldmr/docs` - Documentation service
### TypeScript Guidelines
- **Strict Mode**: All TypeScript strict checks must pass
- **Explicit Types**: Avoid `any` and prefer explicit type definitions
- **Interface First**: Use interfaces for component props and public APIs
- **Cross-Package Types**: Export types from packages for service consumption
- **No 'any'**: The use of `any` is strictly prohibited. Use `unknown` if the type is truly unknown, or define proper generic constraints.
- **No 'ts-ignore' or 'ts-expect-error'**: Never suppress compiler errors. If a third-party library has missing types, provide a local declaration file (.d.ts) instead.
- **No Unsafe Assertions**: Avoid `as any` or `as T` assertions. Prefer type guards, `instanceof` checks, or proper schema validation (e.g., Zod) to narrow types.
- **Explicit Types**: Avoid type inference for public APIs; prefer explicit type definitions for exports.
- **Interface First**: Use interfaces for component props and public APIs.
- **Cross-Package Types**: Export types from packages for service consumption.
### TSDoc Documentation Requirements
**MANDATORY for all exported elements**: types, functions, constants, components
```typescript
/**
* Combines class names using clsx and tailwind-merge for consistent styling.
*
*
* @param inputs - Class name inputs to combine
* @returns Combined and deduplicated class string
*
*
* @example
* \`\`\`typescript
* cn('px-4 py-2', 'bg-blue-500', 'px-6') // 'py-2 bg-blue-500 px-6'
@@ -138,8 +179,19 @@ export function cn(...inputs: ClassValue[]): string {
}
```
### Internationalization (i18n) Guidelines
- **Package**: Use the `@ermnvldmr/i18n` package for all translations across services.
- **Keyset Pattern**: Create `.i18n.ts` files alongside components that require translation.
- **Russian Translation Style**: Embrace the Russian language fully. Prefer phonetic transliteration/transcription for global brand names and descriptive terms over keeping them in English.
- **Brands**: 'GitHub' → 'ГитХаб', 'Instagram' → 'Инстаграм', 'Telegram' → 'Телеграм'.
- **Descriptive**: 'Email' → 'Электропочта'.
- **Source of Truth**: The English locale (`en`) serves as the primary reference for keys and structure.
### Component Development Patterns
#### UI Package Components
```typescript
/**
* Props for the Button component.
@@ -170,59 +222,83 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
```
### File Naming Conventions
- **Components**: PascalCase (e.g., `Button.tsx`, `NavigationMenu.tsx`)
- **Utilities**: camelCase (e.g., `cn.ts`, `formatters.ts`)
- **Constants**: UPPER_SNAKE_CASE for exports (e.g., `API_ENDPOINTS`)
- **Types**: PascalCase with descriptive suffixes (e.g., `ButtonProps`, `ApiResponse`)
- **Config Files**: kebab-case (e.g., `jest-config`, `eslint-config`)
- **Config Files**: kebab-case (e.g., `rsbuild-config`, `eslint-config`)
### Module Structure Patterns
#### Package Structure
```
packages/ui/
├── .config/
│ ├── vitest/
│ │ └── setup.ts
│ └── storybook/
│ ├── main.ts
│ └── preview.tsx
├── src/
│ ├── components/ # Component categories
│ │ ├── generic/ # Layout and generic components
│ │ └── paper/ # Themed component variants
│ ├── styles/ # Global styles and CSS
│ ├── test-utils.tsx # Testing utilities
│ └── index.ts # Main package exports
│ │ ├── index.ts # Production barrel
│ │ ├── testing.ts # Testing barrel
│ │ └── Button/ # Isolated component
│ ├── lib/ # Core logic
│ │ ├── index.ts
│ │ ├── testing.ts
│ │ └── dev.ts
│ ├── static/ # Flat static assets (CSS, etc.)
│ ├── index.ts # Main entry point (runtime)
│ ├── testing.ts # Testing entry point
│ └── dev.ts # Dev entry point
├── package.json
├── vite.config.ts
├── tsconfig.json
└── .storybook/ # Storybook configuration
├── rsbuild.config.ts
└── tsconfig.json
```
#### Service Structure (www example)
```
services/www/
├── .config/
│ ├── vitest/
│ │ └── setup.ts
│ └── storybook/
├── src/
│ ├── layouts/ # Astro layout components
│ ├── pages/ # Astro pages (file-based routing)
│ ├── shared/ # Shared utilities and constants
│ └── global.css # Service-specific styles
├── public/ # Static assets
├── astro.config.mjs # Astro configuration
├── package.json
└── tailwind.config.mjs # Tailwind configuration
│ ├── app/ # Single file per page
│ │ ├── index.tsx # Inlined createPage
│ │ └── articles/
│ ├── components/ # layouts, widgets
│ ├── lib/ # core, shared
│ ├── static/ # global.css, index.html
│ ├── index.ts
│ └── ...
├── public/ # Direct static assets (images)
├── rsbuild.config.ts
└── package.json
```
## 🎨 Design System (UI Package)
### Component Organization
- **Generic Components**: Layout utilities (Stack, VStack, HStack)
- **Themed Components**: Paper-themed variants with consistent styling
- **Radix Integration**: Accessibility-first components using Radix UI
- **Storybook Stories**: Every component has comprehensive documentation
### Styling Approach
- **Tailwind CSS v4+**: Utility-first CSS framework
- **Class Variance Authority**: Type-safe variant generation
- **Custom Design Tokens**: Consistent spacing, colors, typography
- **Responsive Design**: Mobile-first responsive patterns
### Component Categories
```typescript
// Generic Layout Components
export { Stack } from './components/generic/Stack/Stack';
@@ -237,49 +313,56 @@ export { Separator } from './components/paper/PaperSeparator/Separator';
## ⚙️ Configuration Management
### Shared Configurations
Each configuration package provides standardized setups:
#### ESLint Configuration
```javascript
// Provides: base, react, astro configurations
import { base } from '@ermnvldmr/eslint-config';
import { react } from '@ermnvldmr/eslint-config/react';
import { astro } from '@ermnvldmr/eslint-config/astro';
```
#### Jest Configuration
```typescript
// Provides: base, react configurations
import { base } from '@ermnvldmr/jest-config/base';
import { react } from '@ermnvldmr/jest-config/react';
import { baseConfig } from '@ermnvldmr/eslint-config/dev';
```
#### Vite Configuration
#### Vitest Configuration
```typescript
// Provides: library build configurations
import { defineLibConfig } from '@ermnvldmr/vite-config/lib';
// Provides: base, react configurations from rsbuild-config
import { baseVitestConfig, reactVitestConfig } from '@ermnvldmr/rsbuild-config/dev';
import { mergeConfig } from 'vitest/config';
```
#### Rsbuild Configuration
```typescript
// Provides: definePackageConfig, defineServiceConfig
import { definePackageConfig } from '@ermnvldmr/rsbuild-config/dev';
```
### Service Configuration Inheritance
Services inherit from shared configs but can override:
```typescript
// In service package.json devDependencies
"@ermnvldmr/eslint-config": "workspace:*"
"@ermnvldmr/jest-config": "workspace:*"
"@ermnvldmr/rsbuild-config": "workspace:*"
```
## 🚀 Build & Deployment
### Build Dependencies
The monorepo must respect a strict build order due to package dependencies:
1. **Configuration packages** (eslint-config, jest-config, etc.) - Foundation tooling
1. **Configuration packages** (eslint-config, rsbuild-config, etc.) - Foundation tooling
2. **STL package** - Shared utilities and types
3. **UI package** - Component library (depends on STL)
4. **Services** - Applications that consume packages (www, docs)
### CI/CD Pipeline Structure
The repository uses path-based CI/CD triggers - each package and service has dedicated workflows in `.github/workflows/`. Examine the workflow files to understand:
- **Trigger Patterns**: Which file changes activate each pipeline
- **Build Dependencies**: How packages are built in sequence
- **Quality Gates**: Linting, type-checking, and testing requirements
@@ -288,12 +371,14 @@ The repository uses path-based CI/CD triggers - each package and service has ded
## 🔧 Development Environment
### Prerequisites
- **Node.js**: v20+
- **pnpm**: v9.12.3+
- **TypeScript**: v5.8+
- **Hugo**: v0.110.0+ (for docs service)
### Initial Setup
To start development, follow the dependency hierarchy:
1. **Install dependencies** using the root `package.json` scripts
@@ -302,6 +387,7 @@ To start development, follow the dependency hierarchy:
4. **Start services** after dependencies are built
### Development Commands
The monorepo provides scripts at multiple levels:
- **Root Level**: Check root `package.json` for monorepo-wide operations
@@ -313,13 +399,15 @@ Use pnpm workspace filtering (`--filter`) to target specific packages or service
## 🧪 Testing Strategy
### Testing Framework Distribution
- **Shared Configuration**: Jest configs in `@ermnvldmr/jest-config`
- **Shared Configuration**: Vitest configs in `@ermnvldmr/rsbuild-config`
- **Package Testing**: Each package has its own test suite
- **Service Testing**: Services test their specific functionality
- **Component Testing**: UI package uses React Testing Library
- **Component Testing**: UI package uses React Testing Library with Vitest
- **Storybook Testing**: Visual regression and interaction testing
### Test Organization
```typescript
// Package-level testing (packages/stl/src/functions/cn/cn.test.ts)
describe('cn function', () => {
@@ -338,10 +426,19 @@ describe('components/paper/Button/Button', () => {
```
### Coverage Strategy
- **Package Coverage**: Individual coverage reports per package
- **Service Coverage**: Service-specific coverage including Storybook tests
- **Aggregated Coverage**: Combined reporting for the entire monorepo
---
*This guide serves as the architectural foundation for the monorepo. Focus on these stable patterns and structures rather than specific implementation details that may change over time.*
_This guide serves as the architectural foundation for the monorepo. Focus on these stable patterns and structures rather than specific implementation details that may change over time._
## 🤖 AI Agent Best Practices
The following are explicit best practices and preferences intended for AI tools and agents operating within this repository:
- **Build System Preference**: Rsbuild and Rspack are the standard and strongly preferred build tools for all projects in this monorepo due to their Rust-based architecture and performance. Do not introduce Vite, Astro, or Webpack unless strictly necessary.
- **Framework Preference**: The monorepo has migrated away from Astro in favor of React + Rsbuild (with MDX support).
- **Content Sites**: Rspress is the preferred infrastructure for any new content-heavy or documentation sites.
+10 -9
View File
@@ -23,16 +23,17 @@ Composite (see [Copyright & License](#copyright--license))
#### Services
- **[www](./services/www)**: Main website, built with Astro and React.
- **[www](./services/www)**: Main website, built with Rsbuild, React, and MDX.
- **[static](./services/static)**: Static landing pages, built with Rsbuild and React.
- **[docs](./services/docs)**: Documentation site, built with Hugo and the Hextra theme.
#### Packages
- **[@ermnvldmr/ui](./packages/ui)**: Shared React component library and design system.
- **[@ermnvldmr/stl](./packages/stl)**: Standard Template Library – common utilities and reusable logic.
- **[@ermnvldmr/i18n](./packages/i18n)**: Internationalization utilities.
- **[@ermnvldmr/eslint-config](./packages/eslint-config)**: Shared linting configurations.
- **[@ermnvldmr/jest-config](./packages/jest-config)**: Shared testing configurations.
- **[@ermnvldmr/vite-config](./packages/vite-config)**: Shared build configurations.
- **[@ermnvldmr/rsbuild-config](./packages/rsbuild-config)**: Shared build configurations (Rsbuild & Rspack).
- **[@ermnvldmr/storybook-config](./packages/storybook-config)**: Shared Storybook configurations.
## Who Are You?
@@ -49,9 +50,9 @@ Want to contribute or understand the codebase.
- **Setup**: `pnpm install`
- **Development**:
- Website: `pnpm www:dev`
- Documentation: `pnpm docs:dev`
- UI Library: `pnpm ui:dev`
- Website: `pnpm www:dev`
- Documentation: `pnpm docs:dev`
- UI Library: `pnpm ui:dev`
- **Development Guide**: [AGENTS.md](./AGENTS.md)
- **Component Development**: `pnpm ui:storybook`
- **Testing**: `pnpm test` or `pnpm test:coverage` (View results on [Codecov](https://app.codecov.io/github/deytenit/ermnvldmr.com))
@@ -63,7 +64,7 @@ Interested in the design system and components.
- **Components**: Run `pnpm ui:storybook` to explore components
- **Design System**: See [packages/ui/src/styles/index.css](./packages/ui/src/styles/index.css)
for the color palette and theme
for the color palette and theme
- **Theme Structure**: CSS variables with light/dark mode support
### Contributor
@@ -87,12 +88,12 @@ Want to submit improvements.
**Documentation & Informational Content**: Technical documentation, guides, and informational articles within `services/docs/content/` and `services/www/content/` are licensed under [Creative Commons Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/).
**Creative Literature & Art**: Poems, personal essays, artworks, photography, and other purely creative or literary works are **individually copyrighted by their respective owners**. These works are *not* open-source and are *not* covered by the GPL or CC BY 4.0 licenses. They remain under full copyright protection unless explicitly stated otherwise within their specific file metadata or directory.
**Creative Literature & Art**: Poems, personal essays, artworks, photography, and other purely creative or literary works are **individually copyrighted by their respective owners**. These works are _not_ open-source and are _not_ covered by the GPL or CC BY 4.0 licenses. They remain under full copyright protection unless explicitly stated otherwise within their specific file metadata or directory.
---
**AI Assistance**:
Significant portions of this project's *source code* were developed with
Significant portions of this project's _source code_ were developed with
the assistance of AI code generation tools.
Copyright © 2026 Vladimir Eremin
+27 -9
View File
@@ -12,20 +12,38 @@ server {
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
# Clean URLs (remove .html from URI)
location / {
try_files $uri $uri/ $uri.html /index.html;
# Root redirect: detect browser language, fallback to /en/
location = / {
set $locale "en";
if ($http_accept_language ~* "(^|,\s*)ru") {
set $locale "ru";
}
return 302 /$locale/;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
# Cache hashed static assets (JS, CSS, fonts) — matched before locale prefix blocks
# root is inherited from server block, so /en/static/js/file.js resolves correctly
location ~* \.(js|css|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Cache images
location ~* \.(png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, no-transform";
}
# Error pages
error_page 404 /404.html;
location = /404.html {
internal;
# English locale — SPA fallback for non-asset routes
location /en/ {
try_files $uri $uri/ $uri.html /en/index.html;
}
# Russian locale — SPA fallback for non-asset routes
location /ru/ {
try_files $uri $uri/ $uri.html /ru/index.html;
}
# Error pages
error_page 404 /en/index.html;
}
+2 -2
View File
@@ -6,8 +6,8 @@ services:
container_name: com-ermnvldmr-nginx
restart: always
ports:
- "80:80"
- "443:443"
- '80:80'
- '443:443'
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
+12 -5
View File
@@ -9,13 +9,14 @@ WORKDIR /app
# Copy monorepo configuration
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
# Copy all package.json files to allow pnpm to fetch dependencies
COPY packages/eslint-config/package.json ./packages/eslint-config/
COPY packages/jest-config/package.json ./packages/jest-config/
COPY packages/rsbuild-config/package.json ./packages/rsbuild-config/
COPY packages/stl/package.json ./packages/stl/
COPY packages/storybook-config/package.json ./packages/storybook-config/
COPY packages/ui/package.json ./packages/ui/
COPY packages/vite-config/package.json ./packages/vite-config/
COPY packages/i18n/package.json ./packages/i18n/
COPY services/www/package.json ./services/www/
# Install dependencies
@@ -24,8 +25,14 @@ RUN pnpm install --frozen-lockfile
# Copy source code
COPY . .
# Build the website
RUN pnpm --filter @ermnvldmr/www build
# Build dependencies
RUN pnpm --filter @ermnvldmr/rsbuild-config build
RUN pnpm --filter @ermnvldmr/stl build
RUN pnpm --filter @ermnvldmr/i18n build
RUN pnpm --filter @ermnvldmr/ui build
# Build the website (all locales)
RUN pnpm --filter @ermnvldmr/www build:all
# Stage 2: Runtime
FROM nginx:alpine
@@ -34,4 +41,4 @@ COPY --from=builder /app/services/www/dist /usr/share/nginx/html
COPY ci/config/nginx/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
CMD ["nginx", "-g", "daemon off;"]
+12 -6
View File
@@ -2,30 +2,36 @@ codecov:
notify:
wait_for_ci: true
ignore:
- 'packages/eslint-config'
- 'packages/rsbuild-config'
- 'packages/storybook-config'
coverage:
status:
project:
default:
target: auto
target: 80%
threshold: 1%
range: 10%
stl:
target: auto
target: 80%
flags:
- stl
ui:
target: auto
target: 80%
flags:
- ui
ui-storybook:
target: auto
target: 80%
flags:
- ui-storybook
www:
target: auto
target: 80%
flags:
- www
www-storybook:
target: auto
target: 80%
flags:
- www-storybook
+12 -15
View File
@@ -1,6 +1,5 @@
// @ts-check
import { baseConfig } from '@ermnvldmr/eslint-config';
import eslintPluginAstro from 'eslint-plugin-astro';
import { baseConfig } from '@ermnvldmr/eslint-config/dev';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
@@ -9,25 +8,23 @@ import jsxA11y from 'eslint-plugin-jsx-a11y';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
export default [
...baseConfig,
// Additional ignores for monorepo
{
ignores: [
'services/docs/**/*', // Hugo content, no linting needed
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/coverage/**',
'**/storybook-static/**',
'**/.next/**',
'**/*.d.ts',
],
},
// Astro
...eslintPluginAstro.configs.recommended,
{
files: ['services/www/**/*.astro', 'services/static/**/*.astro'],
rules: {
// Astro specific rules can go here
},
},
// Storybook - recommended configuration for flat config
...storybook.configs['flat/recommended'],
@@ -97,7 +94,7 @@ export default tseslint.config(
// React Refresh
{
files: ['**/*.{jsx,tsx}'],
ignores: ['.config/jest/**/*.{jsx,tsx}'],
ignores: ['.config/jest/**/*.{jsx,tsx}', '**/test-utils.tsx'],
plugins: { 'react-refresh': reactRefresh },
rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
@@ -136,5 +133,5 @@ export default tseslint.config(
languageOptions: {
globals: globals.node,
},
}
);
},
];
+2 -1
View File
@@ -43,7 +43,8 @@
"typecheck": "pnpm -r typecheck",
"test": "pnpm -r test",
"test:coverage": "pnpm -r test:coverage",
"clean": "pnpm -r clean && rm -rf node_modules"
"clean": "pnpm -r clean && rm -rf node_modules",
"build:deps": "pnpm -r --filter='./packages/*-config' build && pnpm --filter='./packages/i18n' build"
},
"devDependencies": {
"globals": "^16.0.0",
-169
View File
@@ -1,169 +0,0 @@
// @ts-check
import eslint from '@eslint/js';
import prettier from 'eslint-config-prettier/flat';
import importPlugin from 'eslint-plugin-import';
import jsdoc from 'eslint-plugin-jsdoc';
import globals from 'globals';
import tseslint from 'typescript-eslint';
/**
* Base ESLint configuration
*/
export const baseConfig = tseslint.config(
{
ignores: [
'**/dist',
'**/node_modules',
'**/.github',
'**/.changeset',
'**/coverage',
'.config/**/*',
'*.config.{js,ts,mjs,cjs}',
'!.storybook', // Allow linting of .storybook directory
],
},
// JavaScript
eslint.configs.recommended,
// TypeScript - Strict Configuration (only for .ts/.tsx files)
{
files: ['**/*.{ts,tsx,mts,cts}'],
extends: [
...tseslint.configs.recommendedTypeChecked,
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// TypeScript strict rules
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/no-unnecessary-condition': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/prefer-as-const': 'error',
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', disallowTypeAnnotations: false },
],
'@typescript-eslint/explicit-function-return-type': [
'error',
{ allowExpressions: true, allowTypedFunctionExpressions: true },
],
},
},
// Allow triple-slash references in `*.d.ts` files.
{
files: ['**/*.d.ts'],
rules: {
'@typescript-eslint/triple-slash-reference': 'off',
},
},
// Import/Export Rules
{
plugins: { import: importPlugin },
rules: {
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
['parent', 'sibling'],
'index',
'type',
],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
'import/no-duplicates': 'error',
'import/no-unused-modules': 'error',
'import/no-cycle': 'error',
'import/no-self-import': 'error',
},
},
// JSDoc Requirements - Only for main source code
{
files: ['src/**/*.{ts,tsx}', 'packages/**/*.{ts,tsx}', 'services/**/*.{ts,tsx}'],
plugins: { jsdoc },
rules: {
'jsdoc/require-jsdoc': [
'error',
{
contexts: [
'ExportNamedDeclaration > FunctionDeclaration',
'ExportDefaultDeclaration > FunctionDeclaration',
'TSInterfaceDeclaration',
'TSTypeAliasDeclaration',
],
},
],
'jsdoc/require-description': 'error',
'jsdoc/require-param': 'error',
'jsdoc/require-returns': 'error',
'jsdoc/require-example': 'warn',
'jsdoc/check-alignment': 'error',
'jsdoc/check-indentation': 'error',
'jsdoc/check-syntax': 'error',
'jsdoc/check-tag-names': 'error',
'jsdoc/check-types': 'error',
'jsdoc/valid-types': 'error',
},
},
// Test files
{
files: ['**/*.{test,spec}.{js,jsx,ts,tsx}', '.config/jest/**/*.{js,jsx,ts,tsx}'],
languageOptions: {
globals: {
...globals.jest,
},
},
rules: {
// Relax rules for test files and jest configuration
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'jsdoc/require-jsdoc': 'off',
},
},
// CommonJS files - disable all TypeScript rules
{
files: ['**/*.cjs'],
...tseslint.configs.disableTypeChecked,
},
// Config files and tooling
{
files: ['*.config.{js,ts,mjs,cjs}', '.storybook/**/*.{js,ts,cjs,mjs}'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'jsdoc/require-jsdoc': 'off',
'no-undef': 'off', // For CommonJS files
},
},
// Prettier
prettier
);
+9 -7
View File
@@ -3,16 +3,16 @@
"version": "1.0.0",
"description": "Shared ESLint configuration",
"type": "module",
"main": "./index.js",
"exports": {
"./dev": "./src/dev.js",
"./testing": "./src/testing.js"
},
"files": [
"index.js",
"react.js",
"astro.js"
"src"
],
"dependencies": {
"@eslint/js": "^9.39.2",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-astro": "^1.3.1",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsdoc": "^61.5.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
@@ -20,7 +20,9 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-storybook": "^0.10.2",
"globals": "^16.0.0",
"typescript-eslint": "^8.31.1"
"globals": "^16.0.0"
},
"devDependencies": {
"typescript-eslint": "^8.53.1"
}
}
+1
View File
@@ -0,0 +1 @@
export * from './lib/dev.js';
+1
View File
@@ -0,0 +1 @@
export * from './lib/index.js';
@@ -0,0 +1,25 @@
// @ts-check
import importPlugin from 'eslint-plugin-import';
/**
* Import and Export rules.
*/
export const importsConfig = [
{
plugins: { import: importPlugin },
rules: {
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', ['parent', 'sibling'], 'index', 'type'],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
'import/no-duplicates': 'error',
'import/no-unused-modules': 'error',
'import/no-cycle': 'error',
'import/no-self-import': 'error',
},
},
];
@@ -0,0 +1,43 @@
// @ts-check
import { javascriptConfig } from './javascript.js';
import { getTypescriptConfig } from './typescript.js';
import { importsConfig } from './imports.js';
import { jsdocConfig } from './jsdoc.js';
import { testingConfig } from './testing.js';
import { toolingConfig } from './tooling.js';
import { defineMonorepoRestrictedImports } from './monorepo.js';
/**
* Common ignores for the entire monorepo.
*/
const ignoresConfig = {
ignores: [
'**/dist',
'**/node_modules',
'**/.github',
'**/.changeset',
'**/coverage',
'.config/**/*',
'*.config.{js,ts,mjs,cjs}',
'!.storybook', // Allow linting of .storybook directory
],
};
/**
* Assemble the base ESLint configuration.
*
* @param {string} rootDir - The root directory of the project using this config.
* @returns {import('eslint').Linter.Config[]} The assembled configuration.
*/
export function defineBaseConfig(rootDir) {
return [
ignoresConfig,
...javascriptConfig,
...getTypescriptConfig(rootDir),
...importsConfig,
...jsdocConfig,
...testingConfig,
...toolingConfig,
...defineMonorepoRestrictedImports(rootDir),
];
}
@@ -0,0 +1,25 @@
// @ts-check
import eslint from '@eslint/js';
import prettier from 'eslint-config-prettier/flat';
import globals from 'globals';
/**
* Core JavaScript and Prettier configuration.
*/
export const javascriptConfig = [
// JavaScript recommended rules
eslint.configs.recommended,
// Browser and Service Worker globals
{
languageOptions: {
globals: {
...globals.serviceworker,
...globals.browser,
},
},
},
// Prettier integration (must be last in the assembler)
prettier,
];
@@ -0,0 +1,35 @@
// @ts-check
import jsdoc from 'eslint-plugin-jsdoc';
/**
* JSDoc documentation requirements.
*/
export const jsdocConfig = [
{
files: ['src/**/*.{ts,tsx}', 'packages/**/*.{ts,tsx}', 'services/**/*.{ts,tsx}'],
plugins: { jsdoc },
rules: {
'jsdoc/require-jsdoc': [
'error',
{
contexts: [
'ExportNamedDeclaration > FunctionDeclaration',
'ExportDefaultDeclaration > FunctionDeclaration',
'TSInterfaceDeclaration',
'TSTypeAliasDeclaration',
],
},
],
'jsdoc/require-description': 'error',
'jsdoc/require-param': 'off',
'jsdoc/require-returns': 'off',
'jsdoc/require-example': 'warn',
'jsdoc/check-alignment': 'error',
'jsdoc/check-indentation': 'error',
'jsdoc/check-syntax': 'error',
'jsdoc/check-tag-names': 'error',
'jsdoc/check-types': 'error',
'jsdoc/valid-types': 'error',
},
},
];
@@ -0,0 +1,70 @@
// @ts-check
import fs from 'node:fs';
import path from 'node:path';
/** @type {import('eslint').Linter.Config[] | null} */
let cachedRules = null;
/**
* Dynamically generates restricted import rules for monorepo packages
* to ensure they use relative imports for internal code.
*
* @param {string} rootDir - The root directory of the monorepo.
* @returns {import('eslint').Linter.Config[]} An array of ESLint configurations.
*/
export function defineMonorepoRestrictedImports(rootDir) {
if (cachedRules) return cachedRules;
const workspaces = ['packages', 'services'];
const configs = [];
for (const workspace of workspaces) {
const workspacePath = path.join(rootDir, workspace);
if (!fs.existsSync(workspacePath)) continue;
const entries = fs.readdirSync(workspacePath, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const pkgPath = path.join(workspacePath, entry.name);
const pkgJsonPath = path.join(pkgPath, 'package.json');
if (fs.existsSync(pkgJsonPath)) {
try {
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
const pkgName = pkgJson.name;
// Use relative path from root to package for 'files' pattern
const relativePkgPath = path.relative(rootDir, pkgPath);
configs.push({
files: [`${relativePkgPath}/**/*`],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: pkgName,
message: `Please use relative imports within the package instead of "${pkgName}".`,
},
],
patterns: [
{
group: [`${pkgName}/*`],
message: `Please use relative imports within the package instead of "${pkgName}/*".`,
},
],
},
],
},
});
} catch (e) {
// Skip packages that can't be parsed
}
}
}
}
cachedRules = configs;
return configs;
}
@@ -0,0 +1,43 @@
// @ts-check
import globals from 'globals';
/**
* Relaxation of rules for test files and stories.
*/
export const testingConfig = [
{
files: [
'**/*.{test,spec,stories}.{js,jsx,ts,tsx}',
'.config/jest/**/*.{js,jsx,ts,tsx}',
'**/test-utils.tsx',
],
languageOptions: {
globals: {
...globals.jest,
},
},
rules: {
// Relax rules for test files, stories and jest configuration
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/consistent-type-assertions': 'off',
// Disable all JSDoc rules
'jsdoc/require-jsdoc': 'off',
'jsdoc/require-description': 'off',
'jsdoc/require-param': 'off',
'jsdoc/require-returns': 'off',
'jsdoc/require-example': 'off',
'jsdoc/check-alignment': 'off',
'jsdoc/check-indentation': 'off',
'jsdoc/check-syntax': 'off',
'jsdoc/check-tag-names': 'off',
'jsdoc/check-types': 'off',
'jsdoc/valid-types': 'off',
},
},
];
@@ -0,0 +1,29 @@
// @ts-check
import tseslint from 'typescript-eslint';
/**
* Rules for tooling, config files, and CommonJS files.
*/
export const toolingConfig = [
// CommonJS files - disable all TypeScript rules
{
files: ['**/*.cjs'],
...tseslint.configs.disableTypeChecked,
},
// Config files and tooling
{
files: ['*.config.{js,ts,mjs,cjs}', '.storybook/**/*.{js,ts,cjs,mjs}'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/consistent-type-assertions': 'off',
'jsdoc/require-jsdoc': 'off',
'no-undef': 'off', // For CommonJS files
},
},
];
@@ -0,0 +1,58 @@
// @ts-check
import tseslint from 'typescript-eslint';
/**
* TypeScript configuration.
*
* @param {string} rootDir - The root directory for the TypeScript parser.
* @returns {import('eslint').Linter.Config[]} ESLint configurations.
*/
export function getTypescriptConfig(rootDir) {
return [
// TypeScript - Strict Configuration (only for .ts/.tsx files)
...[...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.stylisticTypeChecked].map(
(config) => ({
...config,
files: ['**/*.{ts,tsx,mts,cts}'],
})
),
{
files: ['**/*.{ts,tsx,mts,cts}'],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: rootDir,
},
},
rules: {
// TypeScript strict rules
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/no-unnecessary-condition': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/prefer-as-const': 'error',
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', disallowTypeAnnotations: false },
],
'@typescript-eslint/consistent-type-assertions': [
'error',
{
assertionStyle: 'never',
},
],
'@typescript-eslint/explicit-function-return-type': 'off',
},
},
// Allow triple-slash references in `*.d.ts` files.
{
files: ['**/*.d.ts'],
rules: {
'@typescript-eslint/triple-slash-reference': 'off',
},
},
];
}
+13
View File
@@ -0,0 +1,13 @@
// @ts-check
import path from 'node:path';
import { defineBaseConfig } from './config/index.js';
// Resolve the monorepo root relative to this library's location
const rootDir = path.resolve(import.meta.dirname, '../../../../');
/**
* Default base configuration for the monorepo.
*/
export const baseConfig = defineBaseConfig(rootDir);
export { defineBaseConfig };
+2
View File
@@ -0,0 +1,2 @@
// Runtime entry point (empty)
export {};
@@ -0,0 +1,2 @@
// Testing helpers (empty)
export {};
+1
View File
@@ -0,0 +1 @@
export * from './lib/testing.js';
+49
View File
@@ -0,0 +1,49 @@
{
"name": "@ermnvldmr/i18n",
"version": "1.0.0",
"description": "Internationalization utilities for ermnvldmr projects",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts",
"default": "./src/index.ts"
},
"./dev": {
"import": "./dist/dev.js",
"types": "./dist/dev.d.ts",
"default": "./src/dev.ts"
},
"./testing": {
"import": "./dist/testing.js",
"types": "./dist/testing.d.ts",
"default": "./src/testing.ts"
}
},
"files": [
"dist"
],
"scripts": {
"build": "rsbuild build",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint src --max-warnings 0",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist coverage"
},
"peerDependencies": {
"react": "^19.1.0"
},
"devDependencies": {
"@ermnvldmr/eslint-config": "workspace:*",
"@ermnvldmr/rsbuild-config": "workspace:*",
"@rsbuild/core": "^1.2.11",
"@types/react": "^19.1.2",
"@vitest/coverage-v8": "^4.0.18",
"react": "^19.1.0",
"typescript": "^5.8.3",
"vitest": "^4.0.18"
}
}
+27
View File
@@ -0,0 +1,27 @@
import { definePackageConfig } from '@ermnvldmr/rsbuild-config/dev';
export default definePackageConfig({
source: {
entry: {
index: './src/index.ts',
dev: './src/dev.ts',
testing: './src/testing.ts',
},
alias: { '@': './src' },
},
output: {
target: 'web',
distPath: { root: 'dist', js: '' },
filename: { js: '[name].js' },
cleanDistPath: true,
},
tools: {
htmlPlugin: false,
rspack: {
output: { library: { type: 'module' } },
experiments: { outputModule: true },
externals: ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
externalsType: 'module',
},
},
});
+73
View File
@@ -0,0 +1,73 @@
import { DEFAULT_LOCALE, isLocale, type Locale } from './lib/locale.js';
import type { RsbuildConfig, RsbuildPlugin } from '@rsbuild/core';
/**
* Returns a partial Rsbuild config that enables locale-aware builds.
*
* The locale MDX replacement is provided as an Rsbuild plugin (rather than a
* `tools.rspack` function) so that `mergeConfig` can safely concatenate it
* with the service's own `plugins` array. A `tools.rspack` function would be
* silently dropped by `mergeConfig` when the service config also defines
* `tools.rspack` as a plain object.
*
* Designed to be merged with defineServiceConfig() in each service's rsbuild.config.ts:
*
* @example
* ```ts
* import { localeRsbuildConfig } from '@ermnvldmr/i18n/dev';
* import { defineServiceConfig, mergeConfig } from '@ermnvldmr/rsbuild-config/dev';
*
* export default defineServiceConfig(mergeConfig(localeRsbuildConfig(), { ... }));
* ```
*/
export function localeRsbuildConfig(): RsbuildConfig {
const rawLocale = process.env.LOCALE ?? '';
const locale: Locale = isLocale(rawLocale) ? rawLocale : DEFAULT_LOCALE;
const localePlugins: RsbuildPlugin[] = [];
if (locale !== DEFAULT_LOCALE) {
localePlugins.push({
name: 'ermnvldmr:locale-mdx-replacement',
setup(api) {
// Resolve .en.mdx imports to the locale-specific file when LOCALE != 'en'.
// The canonical import is always `.en.mdx`; other locales are resolved here.
api.modifyRspackConfig((config, { rspack }) => {
config.plugins = [
...config.plugins,
new rspack.NormalModuleReplacementPlugin(
/\.en\.mdx$/,
(resource: { request: string }) => {
resource.request = resource.request.replace(/\.en\.mdx$/, `.${locale}.mdx`);
}
),
];
return config;
});
},
});
}
return {
source: {
define: {
'process.env.LOCALE': JSON.stringify(locale),
},
},
server: {
// Dev server serves pages under the locale base so localePath() links
// resolve correctly in dev without any NODE_ENV branching.
base: `/${locale}`,
},
output: {
distPath: {
root: `dist/${locale}`,
},
assetPrefix: `/${locale}/`,
},
plugins: localePlugins,
};
}
export { LOCALES, DEFAULT_LOCALE, type Locale } from './lib/locale.js';
+4
View File
@@ -0,0 +1,4 @@
export * from './lib/locale.js';
export * from './lib/pluralize.js';
export * from './lib/keyset.js';
export * from './lib/path.js';
+34
View File
@@ -0,0 +1,34 @@
import React from 'react';
import { describe, it, expect } from 'vitest';
import { createKeyset } from './keyset.js';
const { t, tRaw } = createKeyset({
greeting: { en: 'Hello', ru: 'Привет' },
'count {value}': { en: 'count {value}', ru: 'счёт {value}' },
items: {
en: { none: 'no items', one: 'one item', some: '{count} items', many: '{count} items' },
ru: {
none: 'нет элементов',
one: 'один элемент',
some: '{count} элемента',
many: '{count} элементов',
},
},
});
describe('t', () => {
it('returns plain translation', () => expect(t('greeting')).toBe('Hello'));
it('interpolates {value}', () => expect(t('count {value}', { value: '42' })).toBe('count 42'));
it('pluralizes none', () => expect(t('items', { count: 0 })).toBe('no items'));
it('pluralizes one', () => expect(t('items', { count: 1 })).toBe('one item'));
it('pluralizes many with interpolation', () => expect(t('items', { count: 5 })).toBe('5 items'));
});
describe('tRaw', () => {
it('returns string node for plain key', () => expect(tRaw('greeting')).toBe('Hello'));
it('interpolates ReactNode', () => {
const node = tRaw('count {value}', { value: React.createElement('strong', null, '42') });
expect(Array.isArray(node)).toBe(true);
});
});
+138
View File
@@ -0,0 +1,138 @@
import { LOCALE } from './locale.js';
import { pluralize, type PluralForms } from './pluralize.js';
import type { ReactNode } from 'react';
/** A translation entry with a plain string per locale. */
interface PlainTranslation {
en: string;
ru: string;
}
/** A translation entry with plural forms per locale. */
interface PluralTranslation {
en: PluralForms;
ru: PluralForms;
}
/** Union of all supported translation shapes. */
type Translation = PlainTranslation | PluralTranslation;
/** A map of translation keys to their translation entries. */
type Keyset = Record<string, Translation>;
/**
* Returns `true` when the translation entry uses plural forms.
*
* @param t - The translation entry to inspect.
* @returns Type predicate narrowing `t` to {@link PluralTranslation}.
* @example
* isPluralTranslation({ en: 'hello', ru: 'привет' }) // → false
*/
function isPluralTranslation(t: Translation): t is PluralTranslation {
return typeof t.en === 'object';
}
/**
* Replaces `{key}` placeholders in `template` with values from `params`.
*
* @param template - String containing `{key}` placeholders.
* @param params - Map of placeholder names to string/number values.
* @returns The interpolated string.
* @example
* interpolate('Hello {name}!', { name: 'World' }) // → 'Hello World!'
*/
function interpolate(template: string, params: Record<string, string | number>): string {
return template.replace(/\{(\w+)\}/g, (_, key: string) =>
key in params ? String(params[key]) : `{${key}}`
);
}
/**
* Splits `template` on `{key}` placeholders and substitutes ReactNode values.
*
* @param template - String containing `{key}` placeholders.
* @param params - Map of placeholder names to ReactNode values.
* @returns A single string when all parts are strings, otherwise a ReactNode array.
* @example
* interpolateRaw('Click {link}', { link: <a href="/">here</a> })
*/
function interpolateRaw(template: string, params: Record<string, ReactNode>): ReactNode {
const parts = template.split(/(\{[^}]+\})/g);
const result = parts.map((part) => {
const match = /^\{(\w+)\}$/.exec(part);
if (match) {
const key = match[1];
return key in params ? params[key] : part;
}
return part;
});
if (result.every((p): p is string => typeof p === 'string')) {
return result.join('');
}
return result;
}
/**
* Creates a typed `t` and `tRaw` pair bound to `keyset`.
*
* @param keyset - An object mapping translation keys to their locale entries.
* @returns `{ t, tRaw }` translation helpers.
* @example
* const { t } = createKeyset({ greeting: { en: 'Hello', ru: 'Привет' } });
* t('greeting') // → 'Hello' (in EN build)
*/
export function createKeyset<const K extends Keyset>(keyset: K) {
/** String union of all keys defined in `keyset`. */
type Keys = keyof K & string;
/**
* Returns the translated string for `key`, with optional interpolation and pluralisation.
*
* @param key - A key defined in the keyset.
* @param params - Optional interpolation/pluralisation params.
* @returns The translated string for the current build locale.
* @example
* t('greeting') // → 'Hello'
* t('items {count}', { count: 3 }) // → '3 items'
*/
function t(key: Keys, params?: Record<string, string | number>): string {
const entry = keyset[key];
if (isPluralTranslation(entry)) {
const forms = entry[LOCALE];
const count = typeof params?.count === 'number' ? params.count : 0;
const template = pluralize(count, forms);
return params ? interpolate(template, params) : template;
}
const template = entry[LOCALE];
return params ? interpolate(template, params) : template;
}
/**
* Returns the translated ReactNode for `key`, allowing ReactNode interpolation values.
*
* @param key - A key defined in the keyset.
* @param params - Optional interpolation params; values may be any ReactNode.
* @returns The translated string or ReactNode array for the current build locale.
* @example
* tRaw('click {link}', { link: <a href="/">here</a> })
*/
function tRaw(key: Keys, params?: Record<string, ReactNode>): ReactNode {
const entry = keyset[key];
if (isPluralTranslation(entry)) {
const forms = entry[LOCALE];
const countParam = params?.count;
const count = typeof countParam === 'number' ? countParam : 0;
const template = pluralize(count, forms);
return params ? interpolateRaw(template, params) : template;
}
const template = entry[LOCALE];
return params ? interpolateRaw(template, params) : template;
}
return { t, tRaw };
}
+50
View File
@@ -0,0 +1,50 @@
/** The supported locale identifiers. */
export type Locale = 'en' | 'ru';
export const LOCALES: readonly Locale[] = ['en', 'ru'] as const;
export const DEFAULT_LOCALE: Locale = 'en';
// Widened once so inclusion checks need no type assertions.
const LOCALES_STRINGS: readonly string[] = LOCALES;
/**
* Returns `true` when `value` is a known {@link Locale} identifier.
*
* @param value - The string to test.
* @returns Type predicate narrowing `value` to `Locale`.
* @example
* isLocale('en') // → true
* isLocale('fr') // → false
*/
export function isLocale(value: string): value is Locale {
return LOCALES_STRINGS.includes(value);
}
/**
* The active locale for the current build.
* Injected at compile time via source.define → process.env.LOCALE.
* Falls back to DEFAULT_LOCALE in non-locale-aware builds (tests, Storybook).
*/
const rawLocale = process.env.LOCALE ?? '';
export const LOCALE: Locale = isLocale(rawLocale) ? rawLocale : DEFAULT_LOCALE;
/**
* The other (non-active) locale for the current build.
* Constant-folded at build time.
*/
export const OTHER_LOCALE: Locale = LOCALES.find((l) => l !== LOCALE) ?? DEFAULT_LOCALE;
/**
* Human-readable label for switching to the other locale,
* expressed in the CURRENT locale's language.
* - EN build → "In Russian"
* - RU build → "На Английском"
*/
const SWITCH_LABELS: Record<Locale, Partial<Record<Locale, string>>> = {
en: { ru: 'In Russian' },
ru: { en: 'На Английском' },
};
export const OTHER_LOCALE_LABEL: string =
SWITCH_LABELS[LOCALE][OTHER_LOCALE] ?? OTHER_LOCALE.toUpperCase();
+19
View File
@@ -0,0 +1,19 @@
import { describe, it, expect } from 'vitest';
import { localePath, localeHref } from './path.js';
describe('localePath', () => {
it('prepends LOCALE to absolute path', () =>
expect(localePath('/articles')).toBe('/en/articles'));
it('handles root path', () => expect(localePath('/')).toBe('/en/'));
it('handles nested path', () =>
expect(localePath('/articles/2026/test')).toBe('/en/articles/2026/test'));
});
describe('localeHref', () => {
it('replaces locale prefix in URL', () =>
expect(localeHref('/en/articles', 'ru')).toBe('/ru/articles'));
it('replaces locale at root', () => expect(localeHref('/en/', 'ru')).toBe('/ru/'));
it('falls back to path if no prefix found', () =>
expect(localeHref('/articles', 'ru')).toBe('/ru/articles'));
});
+57
View File
@@ -0,0 +1,57 @@
import { LOCALE, LOCALES, type Locale } from './locale.js';
/**
* Resolves any href value to its locale-aware form.
*
* Only absolute internal paths (starting with `/`) are rewritten; everything else
* (external URLs, mailto/tel, relative paths, hash anchors) passes through unchanged.
* The function is idempotent: paths already carrying any known locale prefix are returned as-is.
*
* Works identically in dev and production: the dev server is configured with
* `server.base = /${LOCALE}` via `localeRsbuildConfig`.
*
* @param href - The href value to resolve.
* @returns The locale-prefixed href, or the original value when no rewrite is needed.
* @example
* // LOCALE = 'en'
* localePath('/articles') // → '/en/articles'
* localePath('/') // → '/en/'
* localePath('/en/articles') // → '/en/articles' (idempotent)
* localePath('https://github.com') // → 'https://github.com' (external)
* localePath('mailto:a@b.com') // → 'mailto:a@b.com'
* localePath('#section') // → '#section'
*/
export function localePath(href: string): string;
export function localePath(href: null): null;
export function localePath(href: undefined): undefined;
export function localePath(href: string | undefined): string | undefined;
export function localePath(href: string | null): string | null;
export function localePath(href: string | null | undefined): string | null | undefined;
export function localePath(href: string | null | undefined): string | null | undefined {
if (href == null) return href;
if (!href.startsWith('/')) return href;
if (LOCALES.some((l) => href === `/${l}` || href === `/${l}/` || href.startsWith(`/${l}/`))) {
return href;
}
if (href === '/') return `/${LOCALE}/`;
return `/${LOCALE}${href}`;
}
/**
* Builds a URL for a specific locale given the current locale-prefixed URL.
*
* @param currentPath - The current page path, e.g. `/en/articles`.
* @param locale - The target locale.
* @returns The equivalent path for `locale`, e.g. `/ru/articles`.
* @example
* localeHref('/en/articles', 'ru') // → '/ru/articles'
* localeHref('/en/', 'ru') // → '/ru/'
*/
export function localeHref(currentPath: string, locale: Locale): string {
const localePrefix = new RegExp(`^\\/(${LOCALES.join('|')})(.*)`);
const match = localePrefix.exec(currentPath);
if (match) {
return `/${locale}${match[2] || '/'}`;
}
return `/${locale}${currentPath}`;
}
+19
View File
@@ -0,0 +1,19 @@
import { describe, it, expect } from 'vitest';
import { pluralize } from './pluralize.js';
const forms = { none: 'none', one: 'one', some: 'some', many: 'many' };
describe('pluralize', () => {
it('returns none for 0', () => expect(pluralize(0, forms)).toBe('none'));
it('returns one for 1', () => expect(pluralize(1, forms)).toBe('one'));
it('returns some for 2', () => expect(pluralize(2, forms)).toBe('some'));
it('returns some for 4', () => expect(pluralize(4, forms)).toBe('some'));
it('returns many for 5', () => expect(pluralize(5, forms)).toBe('many'));
it('returns many for 11', () => expect(pluralize(11, forms)).toBe('many'));
it('returns many for 12', () => expect(pluralize(12, forms)).toBe('many'));
it('returns many for 100', () => expect(pluralize(100, forms)).toBe('many'));
it('returns one for 21', () => expect(pluralize(21, forms)).toBe('one'));
it('returns some for 22', () => expect(pluralize(22, forms)).toBe('some'));
it('returns many for 111', () => expect(pluralize(111, forms)).toBe('many'));
});
+30
View File
@@ -0,0 +1,30 @@
/** The four plural categories used across EN and RU grammar rules. */
export interface PluralForms {
none: string;
one: string;
some: string;
many: string;
}
/**
* Selects the correct plural form for the given count.
* Covers both English (none/one/many) and Russian (none/one/some/many) rules.
*
* @param count - The numeric value to pluralise.
* @param forms - The set of plural forms to select from.
* @returns The appropriate plural form string.
* @example
* const forms = { none: 'no items', one: 'one item', some: '{n} items', many: '{n} items' };
* pluralize(0, forms) // → 'no items'
* pluralize(1, forms) // → 'one item'
* pluralize(3, forms) // → '{n} items' (some)
* pluralize(21, forms) // → 'one item' (Russian rule: 21 ends in 1)
*/
export function pluralize(count: number, forms: PluralForms): string {
if (count === 0) return forms.none;
const mod10 = count % 10;
const mod100 = count % 100;
if (mod10 === 1 && mod100 !== 11) return forms.one;
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return forms.some;
return forms.many;
}
+2
View File
@@ -0,0 +1,2 @@
export { LOCALE, DEFAULT_LOCALE, isLocale } from './lib/locale.js';
export { localePath, localeHref } from './lib/path.js';
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2020", "DOM"],
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["src"]
}
-39
View File
@@ -1,39 +0,0 @@
{
"name": "@ermnvldmr/jest-config",
"version": "1.0.0",
"description": "Shared Jest configuration",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./base": "./dist/base.js",
"./react": "./dist/react.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist"
},
"dependencies": {
"@jest/globals": "^30.2.0",
"jest": "^30.2.0",
"ts-jest": "^29.4.6",
"@types/jest": "^30.0.0"
},
"peerDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
"@testing-library/user-event": "^14.6.1",
"jest-environment-jsdom": "^30.2.0"
},
"devDependencies": {
"typescript": "^5.8.3"
}
}
-43
View File
@@ -1,43 +0,0 @@
import type { Config } from 'jest';
/**
* Base Jest configuration for TypeScript projects.
*
* @example
* ```typescript
* import { baseJestConfig } from '@ermnvldmr/jest-config/base';
*
* export default {
* ...baseJestConfig,
* // package-specific overrides
* } satisfies Config;
* ```
*/
export const baseJestConfig: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.test.{ts,tsx}',
'!src/**/*.stories.{ts,tsx}',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
testMatch: [
'**/__tests__/**/*.{ts,tsx}',
'**/*.{test,spec}.{ts,tsx}',
],
};
-6
View File
@@ -1,6 +0,0 @@
/**
* Jest configuration exports for the monorepo.
*/
export { baseJestConfig } from './base.js';
export { reactJestConfig } from './react.js';
-31
View File
@@ -1,31 +0,0 @@
import type { Config } from 'jest';
import { baseJestConfig } from './base.js';
/**
* Jest configuration for React projects with jsdom environment.
*
* @example
* ```typescript
* import { reactJestConfig } from '@ermnvldmr/jest-config/react';
*
* export default reactJestConfig;
* ```
*/
export const reactJestConfig: Config = {
...baseJestConfig,
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
moduleNameMapper: {
...baseJestConfig.moduleNameMapper,
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
transform: {
...baseJestConfig.transform,
'^.+\\.(js|jsx|ts|tsx)$': [
'ts-jest',
{
useESM: true,
},
],
},
};
-11
View File
@@ -1,11 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"types": []
},
"include": ["src/**/*"],
"exclude": ["dist"]
}
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@ermnvldmr/rsbuild-config",
"version": "0.0.1",
"type": "module",
"exports": {
"./dev": "./dist/dev.js",
"./testing": "./dist/testing.js"
},
"scripts": {
"build": "tsc"
},
"dependencies": {
"@rsbuild/plugin-react": "^1.1.1",
"@rsbuild/plugin-type-check": "^1.1.1",
"critters": "^0.0.25",
"fast-glob": "^3.3.3",
"jsdom": "^28.0.0",
"react": "^19.1.0",
"rsbuild-plugin-dts": "^0.19.5"
},
"devDependencies": {
"@rsbuild/core": "^1.7.3",
"@types/jsdom": "^27.0.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"react-dom": "^19.1.0",
"typescript": "^5.8.2",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.18"
}
}
+1
View File
@@ -0,0 +1 @@
export * from './lib/dev.js';
+1
View File
@@ -0,0 +1 @@
export * from './lib/index.js';
@@ -0,0 +1,31 @@
import { join } from 'node:path';
import fg from 'fast-glob';
export function discoverEntries(root: string, pattern: string): Record<string, string> {
const files = fg.sync(pattern, { cwd: root });
const entries: Record<string, string> = {};
for (const file of files) {
// We expect the user to provide a pattern like './src/app/*.tsx'
// We'll strip the directory parts to get the name
const parts = file.split('/');
// Remove extension from the last part
const filename = parts[parts.length - 1].replace(/\.tsx$/, '');
// For nested routes, we might want to keep the path but relative to the app root
// But since you want 'index', 'articles/index', etc.
// Let's just use the file path relative to the common root 'src/app' if it exists in the path
const appMatch = file.match(/src\/app\/(.*)\.tsx$/);
let name = appMatch ? appMatch[1] : filename;
// If it's src/app/articles/index.tsx -> name is 'articles/index'
// We want it to be 'articles/index' to generate 'dist/articles/index.html'
// Rsbuild automatically maps 'articles/index' entry to 'dist/articles/index.html'
// The previous replace(/\/index$/, '') was stripping the 'index' making it 'articles'
// which generates 'dist/articles.html' instead of 'dist/articles/index.html'
entries[name] = join(root, file);
}
return entries;
}
@@ -0,0 +1,3 @@
export * from './rsbuild.js';
export * from './vitest.js';
export * from './entries.js';
@@ -0,0 +1,75 @@
import { defineConfig, type RsbuildConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginTypeCheck } from '@rsbuild/plugin-type-check';
import { pluginDts } from 'rsbuild-plugin-dts';
import { mergeConfig } from '../utils/merge.js';
import { ssgPlugin } from './ssg.js';
/**
* Defines a standard Rsbuild configuration for services (apps).
*
* @param config - Optional Rsbuild configuration overrides
* @returns A complete Rsbuild configuration object
*
* @example
* ```typescript
* import { defineServiceConfig } from '@ermnvldmr/rsbuild-config/dev';
* export default defineServiceConfig({
* source: { entry: { index: './src/main.ts' } }
* });
* ```
*/
export function defineServiceConfig(config: RsbuildConfig = {}): RsbuildConfig {
const defaultConfig: RsbuildConfig = {
plugins: [pluginReact(), pluginTypeCheck(), ssgPlugin()],
output: {
distPath: {
font: 'static/font',
},
filename: {
font: '[name][ext]',
},
},
html: {
template: './src/static/index.html',
meta: {
description: 'Thoughts and creations of mine',
},
},
};
return defineConfig(mergeConfig(defaultConfig, config));
}
/**
* Defines a standard Rsbuild configuration for packages (libraries).
*
* @param config - Optional Rsbuild configuration overrides
* @returns A complete Rsbuild configuration object
*
* @example
* ```typescript
* import { definePackageConfig } from '@ermnvldmr/rsbuild-config/dev';
* export default definePackageConfig({
* output: { target: 'node' }
* });
* ```
*/
export function definePackageConfig(config: RsbuildConfig = {}): RsbuildConfig {
const defaultConfig: RsbuildConfig = {
plugins: [pluginReact(), pluginTypeCheck(), pluginDts()],
output: {
distPath: {
root: './dist',
js: '.',
},
target: 'web',
},
tools: {
htmlPlugin: false,
},
};
return defineConfig(mergeConfig(defaultConfig, config));
}
@@ -0,0 +1,185 @@
import fs from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import React from 'react';
import type { RsbuildPlugin } from '@rsbuild/core';
/**
* Rsbuild plugin for build-time Static Site Generation.
*
* After the web build completes:
* 1. Builds each entry as a Node.js CJS bundle (dist-ssr/)
* 2. Renders each entry with react-dom/server renderToString
* 3. Injects the rendered HTML into the web build's HTML files
* 4. Runs critters to inline critical CSS and make the main stylesheet async
* 5. Cleans up dist-ssr/
*/
export function ssgPlugin(): RsbuildPlugin {
return {
name: 'ermnvldmr:ssg-plugin',
setup(api) {
api.onAfterBuild(async ({ stats }) => {
if (!stats) {
console.warn('[ssg-plugin] No stats available, skipping SSG.');
return;
}
const config = api.getRsbuildConfig();
const rawEntries = config.source?.entry;
if (!rawEntries || typeof rawEntries !== 'object' || Array.isArray(rawEntries)) {
console.warn('[ssg-plugin] source.entry is not an object map, skipping SSG.');
return;
}
// Normalise entries: value may be a string path or an EntryDescription object
const entries: Record<string, string> = {};
for (const [name, value] of Object.entries(rawEntries)) {
if (typeof value === 'string') {
entries[name] = value;
} else if (value && typeof value === 'object' && 'import' in value) {
const imp = (value as { import: string | string[] }).import;
entries[name] = Array.isArray(imp) ? imp[0] : imp;
}
}
const entryNames = Object.keys(entries);
if (entryNames.length === 0) {
console.warn('[ssg-plugin] No entries to process, skipping SSG.');
return;
}
// api.context.distPath is the absolute path to the web build output directory.
// Fallback: derive from rootPath + output.distPath.root if the property is absent.
const webDistPath: string =
(api.context as { distPath?: string }).distPath ??
path.join(
api.context.rootPath,
(config.output?.distPath as { root?: string } | undefined)?.root ?? 'dist'
);
const ssrDistPath = path.join(path.dirname(webDistPath), 'dist-ssr');
// ── Step 1: Build node-target CJS bundles ─────────────────────────────
console.log('[ssg-plugin] Building SSR bundles...');
const { createRsbuild } = await import('@rsbuild/core');
// Reuse web build plugins minus ssgPlugin (avoids recursion) and pluginTypeCheck
// (type-checking is not needed for the SSR build). This automatically includes
// pluginReact, pluginMdx, and any other service-level plugins.
const SSG_PLUGIN_NAME = 'ermnvldmr:ssg-plugin';
const TYPE_CHECK_PLUGIN_NAME = 'rsbuild:type-check';
const ssrPlugins = (config.plugins ?? []).filter((p) => {
if (!p || typeof p !== 'object') return true;
const name = (p as { name?: string }).name;
return name !== SSG_PLUGIN_NAME && name !== TYPE_CHECK_PLUGIN_NAME;
});
const ssrBuild = await createRsbuild({
rsbuildConfig: {
plugins: ssrPlugins,
source: { entry: entries },
tools: config.tools,
output: {
target: 'node',
distPath: { root: ssrDistPath, js: '.' },
filename: { js: '[name].cjs' },
// Externalize React so the SSR bundle shares the same React instance
// as react-dom/server, preventing "null dispatcher" hook errors.
externals: {
react: 'commonjs react',
'react-dom': 'commonjs react-dom',
'react/jsx-runtime': 'commonjs react/jsx-runtime',
'react/jsx-dev-runtime': 'commonjs react/jsx-dev-runtime',
},
},
},
});
await ssrBuild.build();
console.log('[ssg-plugin] SSR bundles ready.');
// ── Step 2: Render, inject, inline critical CSS ───────────────────────
const { renderToString } = await import('react-dom/server');
// createRequire from ESM context — needed to load CJS modules (critters, SSR bundles)
const _require = createRequire(import.meta.url);
const Critters = _require('critters') as new (opts?: {
path?: string;
publicPath?: string;
preload?: string;
inlineFonts?: boolean;
pruneSource?: boolean;
reduceInlineStyles?: boolean;
}) => { process(html: string): Promise<string> };
// critters `path` is the filesystem root that corresponds to publicPath.
// It resolves <link href="..."> to webDistPath. Use configured assetPrefix for locale builds.
const configuredAssetPrefix =
(config.output as { assetPrefix?: string } | undefined)?.assetPrefix ?? '/';
const critters = new Critters({
path: webDistPath,
publicPath: configuredAssetPrefix,
preload: 'swap', // <link rel="preload" as="style" onload="this.rel='stylesheet'">
inlineFonts: false, // fonts are handled via separate <link rel="preload"> tags
pruneSource: false, // keep full CSS file for post-hydration correctness
reduceInlineStyles: false, // preserve theme init styles (prevents .dark rule from being stripped)
});
for (const entryName of entryNames) {
const ssrBundlePath = path.join(ssrDistPath, `${entryName}.cjs`);
const webHtmlPath = path.join(webDistPath, `${entryName}.html`);
// Clear module cache so each entry loads fresh
const resolved = _require.resolve(ssrBundlePath);
delete _require.cache[resolved];
const mod = _require(ssrBundlePath) as { default?: React.ComponentType };
const Component = mod.default;
if (!Component) {
console.warn(`[ssg-plugin] Entry "${entryName}" has no default export — skipping.`);
continue;
}
// Render to HTML string
const renderedHtml = renderToString(
React.createElement(React.StrictMode, null, React.createElement(Component))
);
// Read web HTML, inject rendered content
const source = await fs.readFile(webHtmlPath, 'utf8');
const injected = source.replace(
/<div id="root"[^>]*>\s*<\/div>/,
`<div id="root">${renderedHtml}</div>`
);
if (injected === source) {
console.warn(
`[ssg-plugin] Could not find <div id="root"> in ${webHtmlPath} — injection skipped.`
);
}
// Run critters: inline critical CSS, make main stylesheet async
const processed = await critters.process(injected);
// Set <html lang> to the current build locale
const locale = process.env.LOCALE ?? 'en';
const localized = processed.replace(
/<html([^>]*)\slang="[^"]*"/,
`<html$1 lang="${locale}"`
);
await fs.writeFile(webHtmlPath, localized, 'utf8');
console.log(`[ssg-plugin] ✓ ${entryName}`);
}
// ── Step 3: Cleanup ───────────────────────────────────────────────────
await fs.rm(ssrDistPath, { recursive: true, force: true });
console.log('[ssg-plugin] SSG complete. dist-ssr cleaned up.');
});
},
};
}
@@ -0,0 +1,35 @@
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';
/**
* Base Vitest configuration for the monorepo.
*/
export const baseVitestConfig = defineConfig({
plugins: [tsconfigPaths()],
test: {
globals: true,
environment: 'node',
passWithNoTests: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: ['node_modules/**', 'dist/**', '**/*.d.ts', '**/*.config.*', '**/*.stories.tsx'],
},
},
});
/**
* React-specific Vitest configuration.
*/
export const reactVitestConfig = defineConfig({
test: {
...baseVitestConfig.test,
environment: 'jsdom',
setupFiles: ['./.config/vitest/setup.ts'],
alias: {
// Handle asset mocks if needed
'\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
},
plugins: [tsconfigPaths()],
});
+2
View File
@@ -0,0 +1,2 @@
export * from './config/index.js';
export { mergeConfig } from './utils/merge.js';
+2
View File
@@ -0,0 +1,2 @@
// Runtime entry point (empty)
export {};
@@ -0,0 +1 @@
export * from './config/vitest.js';
@@ -0,0 +1,35 @@
/**
* Minimal deep merge utility for Rsbuild configurations.
* Concatenates arrays and recursively merges objects.
*
* @param source - The source object
* @param target - The target object to merge into source
* @returns The merged object
*/
export function mergeConfig<T>(source: any, target: any): T {
if (target === undefined) return source;
if (source === undefined) return target;
if (Array.isArray(source) && Array.isArray(target)) {
return [...source, ...target] as any;
}
if (
source !== null &&
typeof source === 'object' &&
!Array.isArray(source) &&
target !== null &&
typeof target === 'object' &&
!Array.isArray(target)
) {
const result = { ...source };
for (const key in target) {
if (Object.prototype.hasOwnProperty.call(target, key)) {
result[key] = mergeConfig(source[key], target[key]);
}
}
return result as any;
}
return target;
}
+1
View File
@@ -0,0 +1 @@
export * from './lib/testing.js';
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"outDir": "./dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"composite": true,
"declarationMap": true
},
"include": ["src"]
}
-3
View File
@@ -1,3 +0,0 @@
import { baseJestConfig } from '@ermnvldmr/jest-config/base';
export default baseJestConfig;
+31 -11
View File
@@ -1,24 +1,35 @@
{
"name": "@ermnvldmr/stl",
"version": "1.0.0",
"description": "Common reusable dependencies",
"description": "Common utilities for ermnvldmr projects",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
"types": "./dist/index.d.ts",
"default": "./src/index.ts"
},
"./testing": {
"import": "./dist/testing.js",
"types": "./dist/testing.d.ts",
"default": "./src/testing.ts"
},
"./dev": {
"import": "./dist/dev.js",
"types": "./dist/dev.d.ts",
"default": "./src/dev.ts"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && vite build",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"build": "rsbuild build",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "eslint src --max-warnings 0",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist coverage"
@@ -29,14 +40,23 @@
},
"devDependencies": {
"@ermnvldmr/eslint-config": "workspace:*",
"@ermnvldmr/jest-config": "workspace:*",
"@ermnvldmr/vite-config": "workspace:*",
"jest": "^30.2.0",
"ts-jest": "^29.4.6",
"@ermnvldmr/rsbuild-config": "workspace:*",
"@rsbuild/core": "^1.2.11",
"@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",
"@vitest/coverage-v8": "^4.0.18",
"jsdom": "^28.0.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"typescript": "^5.8.3",
"vite": "^7.2.2"
"vitest": "^4.0.18"
},
"peerDependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.4"
}
}
+48
View File
@@ -0,0 +1,48 @@
import { definePackageConfig } from '@ermnvldmr/rsbuild-config/dev';
export default definePackageConfig({
source: {
entry: {
index: './src/index.ts',
testing: './src/testing.ts',
dev: './src/dev.ts',
},
alias: {
'@': './src',
},
},
output: {
target: 'web',
distPath: {
root: 'dist',
js: '',
},
filename: {
js: '[name].js',
},
cleanDistPath: true,
},
tools: {
htmlPlugin: false,
rspack: {
output: {
library: {
type: 'module',
},
},
experiments: {
outputModule: true,
},
externals: [
'react',
'react-dom',
'react/jsx-runtime',
'react/jsx-dev-runtime',
'tailwindcss',
'clsx',
'tailwind-merge',
],
externalsType: 'module',
},
},
});
+1
View File
@@ -0,0 +1 @@
export * from './lib/dev.js';
+1 -2
View File
@@ -1,2 +1 @@
export * from './types';
export * from './functions';
export * from './lib/index.js';
+2
View File
@@ -0,0 +1,2 @@
// No library-specific development utilities currently
export {};
@@ -19,10 +19,10 @@ describe('shared/helpers/cn/cn', () => {
// Test with actual conditional values (simulating real usage)
const activeCondition = Math.random() > -1; // Always true but not constant
const inactiveCondition = Math.random() < -1; // Always false but not constant
expect(cn('p-4', activeCondition && 'bg-red-500')).toBe('p-4 bg-red-500');
expect(cn('p-4', inactiveCondition && 'bg-red-500')).toBe('p-4');
// Test null and undefined conditions
const nullCondition: string | null = null;
const undefinedCondition: string | undefined = undefined;
@@ -36,7 +36,9 @@ describe('shared/helpers/cn/cn', () => {
});
it('handles array inputs', () => {
expect(cn(['flex', 'items-center'], 'justify-between')).toBe('flex items-center justify-between');
expect(cn(['flex', 'items-center'], 'justify-between')).toBe(
'flex items-center justify-between'
);
expect(cn(['p-4', null, 'bg-red-500'])).toBe('p-4 bg-red-500');
});
@@ -50,11 +52,11 @@ describe('shared/helpers/cn/cn', () => {
const getIsActive = (): boolean => true;
const getPrimaryVariant = (): 'primary' | 'secondary' => 'primary';
const getSecondaryVariant = (): 'primary' | 'secondary' => 'secondary';
const isActive = getIsActive();
const primaryVariant = getPrimaryVariant();
const secondaryVariant = getSecondaryVariant();
// Test primary variant
expect(
cn(
@@ -84,12 +86,8 @@ describe('shared/helpers/cn/cn', () => {
});
it('properly handles spacing and responsive classes', () => {
expect(
cn('p-2 p-4', 'sm:p-6', 'md:p-8')
).toBe('p-4 sm:p-6 md:p-8');
expect(
cn('m-2', 'mx-4', 'ml-6')
).toBe('m-2 mx-4 ml-6');
expect(cn('p-2 p-4', 'sm:p-6', 'md:p-8')).toBe('p-4 sm:p-6 md:p-8');
expect(cn('m-2', 'mx-4', 'ml-6')).toBe('m-2 mx-4 ml-6');
});
});
@@ -1,15 +1,32 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { extendTailwindMerge } from 'tailwind-merge';
const RB_TYPES = ['display', 'headline', 'title', 'body', 'label'] as const;
const RB_SIZES = ['l', 'm', 's'] as const;
const rbFontSizeClasses = RB_TYPES.flatMap((type) => RB_SIZES.map((size) => `${type}-${size}`));
const twMerge = extendTailwindMerge({
extend: {
classGroups: {
'font-size': [{ 'text-rb': rbFontSizeClasses }],
},
},
});
/**
* Combines and merges CSS class names using clsx and tailwind-merge.
*
*
* This utility function combines the power of clsx for conditional class handling
* with tailwind-merge for proper Tailwind CSS class deduplication and conflict resolution.
*
*
* Tailwind-merge is configured to recognise `text-rb-{type}-{size}` classes
* (e.g. `text-rb-body-m`) as font-size utilities so they do not conflict with
* text-color classes such as `text-[var(--rb-text)]`.
*
* @param inputs - Class values to combine (strings, objects, arrays, etc.)
* @returns Merged class string with conflicts resolved
*
*
* @example
* ```typescript
* cn('px-4 py-2', 'bg-blue-500') // 'px-4 py-2 bg-blue-500'
File renamed without changes.
+5
View File
@@ -0,0 +1,5 @@
export * from './types/index.js';
export * from './functions/index.js';
export * from './object/index.js';
export * from './window/localStorage.js';
export * from './react/index.js';
+6
View File
@@ -0,0 +1,6 @@
/**
* Object utilities for common operations on objects.
*/
export { merge } from './merge/merge';
export type { MergeOptions } from './merge/merge';
@@ -0,0 +1,108 @@
import { merge } from './merge';
/**
* Test suite for the merge utility function.
*/
describe('shared/helpers/object/merge', () => {
it('deeply merges two objects, preserving source values by default', () => {
const source = { a: 1, b: { c: 2 } };
const target = { a: 3, b: { c: 4, d: 5 }, e: 6 };
const result = merge(source, target);
expect(result).toEqual({
a: 1, // force is false, so 1 is preserved
b: {
c: 2, // force is false, so 2 is preserved
d: 5, // added because it was not in source
},
e: 6, // added because it was not in source
});
});
it('deeply merges two objects and overrides values when force is true', () => {
const source = { a: 1, b: { c: 2 } };
const target = { a: 3, b: { c: 4, d: 5 }, e: 6 };
const result = merge(source, target, { force: true });
expect(result).toEqual({
a: 3,
b: {
c: 4,
d: 5,
},
e: 6,
});
});
it('concatenates arrays when both are present at the same key', () => {
const source = { a: [1, 2] };
const target = { a: [3, 4] };
const result = merge(source, target);
expect(result).toEqual({ a: [1, 2, 3, 4] });
});
it('handles primitive values at the root', () => {
expect(merge(1, 2)).toBe(1);
expect(merge(1, 2, { force: true })).toBe(2);
});
it('handles mixed types between source and target', () => {
const source = { a: { b: 1 } };
const target = { a: [1, 2] };
// Source wins because force is false
expect(merge(source, target)).toEqual({ a: { b: 1 } });
// Target wins because force is true
expect(merge(source, target, { force: true })).toEqual({ a: [1, 2] });
});
it('handles deep recursion with arrays and objects', () => {
const source = {
users: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
],
config: {
retries: 3,
enabled: true,
},
};
const target = {
users: [{ id: 3, name: 'Doe' }],
config: {
retries: 5,
timeout: 1000,
},
};
const result = merge(source, target);
expect(result).toEqual({
users: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
{ id: 3, name: 'Doe' },
],
config: {
retries: 3,
enabled: true,
timeout: 1000,
},
});
});
it('preserves the source when target has undefined values', () => {
const source = { a: 1 };
const target = { a: undefined };
expect(merge(source, target)).toEqual({ a: 1 });
});
it('replaces undefined source with target', () => {
const source = { a: undefined };
const target = { a: 1 };
expect(merge(source, target)).toEqual({ a: 1 });
});
});
@@ -0,0 +1,76 @@
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unnecessary-type-assertion */
/**
* Options for the merge utility.
*/
export interface MergeOptions {
/**
* Whether to override existing primitive values in the source with values from the target.
* If false, existing primitive values are preserved.
* @default false
*/
force?: boolean;
}
/**
* Deeply merges two values (objects, arrays, or primitives).
*
* Behavior:
* - If both values are arrays, they are concatenated.
* - If both values are objects, they are merged recursively.
* - If encounter a primitive value, it is overridden only if `force` is set to true.
* - If a key exists in target but not in source, it is added to the result.
*
* @param source - The source value to merge from
* @param target - The target value to merge with
* @param options - Merge configuration options
* @returns The merged result
*
* @example
* ```typescript
* merge({ a: 1 }, { a: 2 }) // { a: 1 }
* merge({ a: 1 }, { a: 2 }, { force: true }) // { a: 2 }
* merge({ a: [1] }, { a: [2] }) // { a: [1, 2] }
* merge({ a: { b: 1 } }, { a: { c: 2 } }) // { a: { b: 1, c: 2 } }
* ```
*/
export function merge<T = any>(source: any, target: any, options: MergeOptions = {}): T {
const { force = false } = options;
// If target is undefined, source wins
if (target === undefined) {
return source;
}
// If source is undefined, target wins
if (source === undefined) {
return target;
}
const sourceIsArray = Array.isArray(source);
const targetIsArray = Array.isArray(target);
// If both are arrays, concatenate
if (sourceIsArray && targetIsArray) {
return [...source, ...target] as any;
}
const sourceIsObject = source !== null && typeof source === 'object' && !sourceIsArray;
const targetIsObject = target !== null && typeof target === 'object' && !targetIsArray;
// If both are objects, deep merge
if (sourceIsObject && targetIsObject) {
const result = { ...source };
for (const key in target) {
if (Object.prototype.hasOwnProperty.call(target, key)) {
if (key in source) {
result[key] = merge(source[key], target[key], options);
} else {
result[key] = target[key];
}
}
}
return result as any;
}
// If we encounter primitive values or mixed types, override only if force is true
return force ? target : source;
}
@@ -0,0 +1,20 @@
import type React from 'react';
/**
* Safely casts a React ref to a mutable ref object.
* This is a controlled escape hatch for situations where TypeScript's
* built-in Ref types are too restrictive (e.g., when syncing refs manually).
*
* @param ref - The ref to cast
* @returns The casted mutable ref
* @example
* ```tsx
* const mutableRef = castMutableRef<HTMLDivElement>(ref);
* mutableRef.current = someElement;
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function castMutableRef<T>(ref: any): React.MutableRefObject<T | null> {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return ref as React.MutableRefObject<T | null>;
}
@@ -0,0 +1,11 @@
import { castRef } from './castRef';
import type React from 'react';
describe('react/castRef', () => {
it('should cast a ref to a different type', () => {
const ref = { current: null } as unknown as React.ForwardedRef<HTMLDivElement>;
const casted = castRef<HTMLSpanElement>(ref);
expect(casted).toBe(ref);
});
});
@@ -0,0 +1,19 @@
import type React from 'react';
/**
* Safely casts a React ref to a specific type.
* This is a controlled escape hatch for situations where TypeScript's
* built-in Ref types are too restrictive (e.g., in generic forwardRef components).
*
* @param ref - The ref to cast
* @returns The casted ref
* @example
* ```tsx
* <Component ref={castRef<HTMLDivElement>(ref)} />
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function castRef<T>(ref: any): React.RefObject<T> {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return ref as unknown as React.RefObject<T>;
}
@@ -0,0 +1,14 @@
import React from 'react';
import { genericMemo } from './genericMemo';
describe('react/genericMemo', () => {
it('should memoize a component', () => {
const Component = ({ name }: { name: string }) => <div>{name}</div>;
const Memoized = genericMemo(Component);
expect(Memoized).toBeDefined();
// In React, memo returns a special object with type: component
expect((Memoized as any).type).toBe(Component);
});
});
@@ -0,0 +1,16 @@
import React from 'react';
/**
* A type-safe version of React.memo that preserves generic types.
*
* @param component - The component to memoize
* @returns The memoized component
* @example
* ```tsx
* export const MyComponent = genericMemo(MyGenericComponent);
* ```
*/
export function genericMemo<T>(component: T): T {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
return React.memo(component as any) as any as T;
}
+9
View File
@@ -0,0 +1,9 @@
export { castRef } from './castRef/castRef';
export { castMutableRef } from './castRef/castMutableRef';
export { genericMemo } from './genericMemo/genericMemo';
export { useResizeObserver } from './useResizeObserver/useResizeObserver';
export { useIntersectionObserver } from './useIntersectionObserver/useIntersectionObserver';
export { useScroll } from './useScroll/useScroll';
export { useLocalStorage } from './useLocalStorage/useLocalStorage';
export type { ReactElementProps } from './types/ReactElementProps';
@@ -0,0 +1,8 @@
import type React from 'react';
/**
* Common props for any React element.
* Useful for forwarding props to dynamic components.
*/
export type ReactElementProps = React.HTMLAttributes<HTMLElement> &
React.AllHTMLAttributes<HTMLElement>;
@@ -0,0 +1,139 @@
import { renderHook, act } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { useIntersectionObserver } from './useIntersectionObserver';
describe('useIntersectionObserver', () => {
let observerCallback: IntersectionObserverCallback;
let observeMock = vi.fn();
let unobserveMock = vi.fn();
let disconnectMock = vi.fn();
beforeEach(() => {
observeMock = vi.fn();
unobserveMock = vi.fn();
disconnectMock = vi.fn();
class MockIntersectionObserver {
constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
observerCallback = callback;
this.root = options?.root ?? null;
this.rootMargin = options?.rootMargin ?? '';
this.thresholds = options?.threshold
? Array.isArray(options.threshold)
? options.threshold
: [options.threshold]
: [0];
}
root: Element | Document | null;
rootMargin: string;
thresholds: readonly number[];
takeRecords = vi.fn();
observe = observeMock;
unobserve = unobserveMock;
disconnect = disconnectMock;
}
vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
});
afterEach(() => {
vi.unstubAllGlobals();
vi.clearAllMocks();
});
it('should return initial state as not intersecting', () => {
const ref = { current: document.createElement('div') };
const { result } = renderHook(() => useIntersectionObserver(ref));
expect(result.current).toBe(false);
});
it('should call observe on mount', () => {
const element = document.createElement('div');
const ref = { current: element };
renderHook(() => useIntersectionObserver(ref));
expect(observeMock).toHaveBeenCalledWith(element);
});
it('should update isIntersecting when observer triggers', () => {
const element = document.createElement('div');
const ref = { current: element };
const { result } = renderHook(() => useIntersectionObserver(ref));
const entries = [
{
isIntersecting: true,
target: element,
},
] as unknown as IntersectionObserverEntry[];
act(() => {
observerCallback(entries, {} as IntersectionObserver);
});
expect(result.current).toBe(true);
});
it('should disconnect after first intersection if once is true', () => {
const element = document.createElement('div');
const ref = { current: element };
renderHook(() => useIntersectionObserver(ref, { once: true }));
const entries = [
{
isIntersecting: true,
target: element,
},
] as unknown as IntersectionObserverEntry[];
act(() => {
observerCallback(entries, {} as IntersectionObserver);
});
expect(disconnectMock).toHaveBeenCalled();
});
it('should not disconnect after first intersection if once is false', () => {
const element = document.createElement('div');
const ref = { current: element };
renderHook(() => useIntersectionObserver(ref, { once: false }));
const entries = [
{
isIntersecting: true,
target: element,
},
] as unknown as IntersectionObserverEntry[];
act(() => {
observerCallback(entries, {} as IntersectionObserver);
});
expect(disconnectMock).not.toHaveBeenCalled();
});
it('should call disconnect on unmount', () => {
const element = document.createElement('div');
const ref = { current: element };
const { unmount } = renderHook(() => useIntersectionObserver(ref));
unmount();
expect(disconnectMock).toHaveBeenCalled();
});
it('should not observe if ref is null', () => {
const ref = { current: null };
renderHook(() => useIntersectionObserver(ref));
expect(observeMock).not.toHaveBeenCalled();
});
it('should return true immediately if IntersectionObserver is not available', () => {
vi.stubGlobal('IntersectionObserver', undefined);
const ref = { current: document.createElement('div') };
const { result } = renderHook(() => useIntersectionObserver(ref));
expect(result.current).toBe(true);
});
});
@@ -0,0 +1,62 @@
import { useEffect, useState } from 'react';
/** Options for useIntersectionObserver */
export interface UseIntersectionObserverOptions extends IntersectionObserverInit {
/** Whether to disconnect after the first intersection */
once?: boolean;
}
/**
* A hook that observes if an element is intersecting with its root.
*
* @param ref - A React ref to the element to observe.
* @param options - IntersectionObserver configuration.
* @returns Boolean indicating if the element is currently intersecting.
*
* @example
* ```tsx
* const MyComponent = () => {
* const ref = useRef<HTMLDivElement>(null);
* const isVisible = useIntersectionObserver(ref);
*
* return <div ref={ref}>{isVisible ? 'I am visible!' : 'Scroll down...'}</div>;
* };
* ```
*/
export const useIntersectionObserver = <T extends HTMLElement>(
ref: React.RefObject<T | null>,
{ once = true, ...options }: UseIntersectionObserverOptions = {}
): boolean => {
const [isIntersecting, setIntersecting] = useState(false);
const { root, rootMargin, threshold } = options;
useEffect(() => {
if (typeof IntersectionObserver === 'undefined') {
setIntersecting(true);
return;
}
const observeTarget = ref.current;
if (!observeTarget) {
return;
}
const observer = new IntersectionObserver(
([entry]) => {
setIntersecting(entry.isIntersecting);
if (entry.isIntersecting && once) {
observer.disconnect();
}
},
{ root, rootMargin, threshold }
);
observer.observe(observeTarget);
return () => {
observer.disconnect();
};
}, [ref, once, root, rootMargin, threshold]);
return isIntersecting;
};
@@ -0,0 +1 @@
export * from './useLocalStorage';
@@ -0,0 +1,65 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { useLocalStorage } from './useLocalStorage';
import { createLocalStorage } from '../../window/localStorage';
describe('lib/react/useLocalStorage', () => {
const KEY = 'ermnvldmr/stl/lib/react/useLocalStorage/test';
const DEFAULT_VALUE = 'default';
beforeEach(() => {
window.localStorage.clear();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should return default value initially', () => {
const manager = createLocalStorage(KEY, DEFAULT_VALUE);
const { result } = renderHook(() => useLocalStorage(manager));
expect(result.current[0]).toBe(DEFAULT_VALUE);
});
it('should update state when setter is called', () => {
const manager = createLocalStorage(KEY, DEFAULT_VALUE);
const { result } = renderHook(() => useLocalStorage(manager));
act(() => {
result.current[1]('new value');
});
expect(result.current[0]).toBe('new value');
expect(window.localStorage.getItem(KEY)).toBe(JSON.stringify('new value'));
});
it('should sync across tabs via storage event', () => {
const manager = createLocalStorage(KEY, DEFAULT_VALUE);
const { result } = renderHook(() => useLocalStorage(manager));
act(() => {
// Simulate external localStorage update
window.localStorage.setItem(KEY, JSON.stringify('external update'));
window.dispatchEvent(
new StorageEvent('storage', {
key: KEY,
newValue: JSON.stringify('external update'),
})
);
});
expect(result.current[0]).toBe('external update');
});
it('should support functional updates', () => {
const manager = createLocalStorage(KEY, 0);
const { result } = renderHook(() => useLocalStorage(manager));
act(() => {
result.current[1]((prev) => prev + 1);
});
expect(result.current[0]).toBe(1);
});
});
@@ -0,0 +1,55 @@
import { useCallback, useSyncExternalStore } from 'react';
import type { StorageManager } from '../../window/localStorage';
/**
* A React hook that synchronizes state with a StorageManager.
*
* It provides reactivity across the component lifecycle and synchronizes
* values across multiple browser tabs using the 'storage' event.
*
* @template T - The type of the stored value.
* @param manager - The StorageManager instance returned by createLocalStorage.
* @returns A tuple containing the current value and a setter function.
*
* @example
* ```tsx
* const [theme, setTheme] = useLocalStorage(themeStorage);
* ```
*/
export function useLocalStorage<T>(
manager: StorageManager<T>
): [T, (value: T | ((prev: T) => T)) => void] {
const subscribe = useCallback(
(callback: () => void) => {
const handleStorageChange = (event: StorageEvent): void => {
if (event.key === manager.key) {
callback();
}
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
},
[manager.key]
);
const getSnapshot = useCallback(() => manager.get(), [manager]);
const value = useSyncExternalStore(subscribe, getSnapshot, () => {
// SSR snapshot: createLocalStorage returns a no-op manager in Node context,
// so manager.get() safely returns defaultValue without accessing localStorage.
return manager.get();
});
const setValue = useCallback(
(nextValue: T | ((prev: T) => T)) => {
manager.set(nextValue);
},
[manager]
);
return [value, setValue];
}
@@ -0,0 +1,79 @@
import { renderHook, act } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { useResizeObserver } from './useResizeObserver';
describe('useResizeObserver', () => {
let observerCallback: ResizeObserverCallback;
const observeMock = vi.fn();
const unobserveMock = vi.fn();
const disconnectMock = vi.fn();
beforeEach(() => {
class MockResizeObserver {
constructor(callback: ResizeObserverCallback) {
observerCallback = callback;
}
observe = observeMock;
unobserve = unobserveMock;
disconnect = disconnectMock;
}
vi.stubGlobal('ResizeObserver', MockResizeObserver);
});
afterEach(() => {
vi.unstubAllGlobals();
vi.clearAllMocks();
});
it('should return initial dimensions', () => {
const ref = { current: document.createElement('div') };
const { result } = renderHook(() => useResizeObserver(ref));
expect(result.current).toEqual({ width: 0, height: 0 });
});
it('should call observe on mount', () => {
const element = document.createElement('div');
const ref = { current: element };
renderHook(() => useResizeObserver(ref));
expect(observeMock).toHaveBeenCalledWith(element);
});
it('should update dimensions when ResizeObserver triggers', () => {
const element = document.createElement('div');
const ref = { current: element };
const { result } = renderHook(() => useResizeObserver(ref));
const entries = [
{
contentRect: { width: 100, height: 200 },
target: element,
},
] as unknown as ResizeObserverEntry[];
act(() => {
observerCallback(entries, {} as ResizeObserver);
});
expect(result.current).toEqual({ width: 100, height: 200 });
});
it('should call disconnect on unmount', () => {
const element = document.createElement('div');
const ref = { current: element };
const { unmount } = renderHook(() => useResizeObserver(ref));
unmount();
expect(disconnectMock).toHaveBeenCalled();
});
it('should not observe if ref is null', () => {
const ref = { current: null };
renderHook(() => useResizeObserver(ref));
expect(observeMock).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,57 @@
import { useEffect, useState } from 'react';
/** Result of useResizeObserver */
export interface UseResizeObserverResult {
/** Observed width */
width: number;
/** Observed height */
height: number;
}
/**
* A hook that observes the size of an element and returns its dimensions.
*
* @param ref - A React ref to the element to observe.
* @returns The dimensions of the observed element.
*
* @example
* ```tsx
* const MyComponent = () => {
* const ref = useRef<HTMLDivElement>(null);
* const { width, height } = useResizeObserver(ref);
*
* return <div ref={ref}>Width: {width}, Height: {height}</div>;
* };
* ```
*/
export const useResizeObserver = <T extends HTMLElement>(
ref: React.RefObject<T | null>
): UseResizeObserverResult => {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const observeTarget = ref.current;
if (!observeTarget) {
return;
}
const resizeObserver = new ResizeObserver((entries) => {
if (!Array.isArray(entries) || !entries.length) {
return;
}
const entry = entries[0];
setDimensions({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
});
resizeObserver.observe(observeTarget);
return () => {
resizeObserver.disconnect();
};
}, [ref]);
return dimensions;
};
Loaded 100 of 775 files, more files were not shown because too many files have changed in this diff. Show more