Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ log/
.cursor/
e2e/test-results/
.rsdoctor/

.worktrees/
13 changes: 8 additions & 5 deletions examples/rsbuild-minimal/rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ export default defineConfig({
chain.plugin('Rsdoctor').use(RsdoctorRspackPlugin, [
{
disableClientServer: !process.env.ENABLE_CLIENT_SERVER,
features: ['resolver', 'bundle', 'plugins', 'loader'],
features: ['resolver', 'bundle', 'plugins', 'loader', 'runtime'],
output: {
mode: 'brief',
options: {
type: ['json', 'html'],
},
// mode: 'brief',
// options: {
// type: ['json', 'html'],
// },
reportCodeType: {
noCode: true,
},
Expand Down Expand Up @@ -50,6 +50,9 @@ export default defineConfig({
]);
},
},
server: {
port: 1111,
},
output: {
minify: false,
filenameHash: false,
Expand Down
132 changes: 120 additions & 12 deletions examples/rsbuild-minimal/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,130 @@ body {
background-image: linear-gradient(to bottom, #020917, #101725);
}

.content {
display: flex;
.shell {
min-height: 100vh;
line-height: 1.1;
text-align: center;
padding: 24px;
box-sizing: border-box;
}

.top-nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
}

.top-nav h1 {
margin: 0;
font-size: 22px;
}

.top-nav nav {
display: flex;
gap: 12px;
}

.top-nav a,
.actions a {
color: #8ac5ff;
text-decoration: none;
}

.top-nav a:hover,
.actions a:hover {
text-decoration: underline;
}

.page {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 20px;
}

.page h2 {
margin-top: 0;
margin-bottom: 8px;
}

.hint {
margin-top: 0;
color: rgba(255, 255, 255, 0.75);
}

.grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}

.card,
.detail,
.order-item {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
padding: 14px;
}

.card h3,
.detail h3 {
margin-top: 0;
}

.meta,
.actions,
.order-right {
display: flex;
justify-content: space-between;
gap: 10px;
align-items: center;
}

.actions {
margin-top: 12px;
}

.list {
display: flex;
flex-direction: column;
justify-content: center;
gap: 10px;
}

.content h1 {
font-size: 3.6rem;
font-weight: 700;
.order-item p {
margin: 6px 0 0;
color: rgba(255, 255, 255, 0.75);
}

.status {
font-size: 12px;
border-radius: 999px;
padding: 2px 8px;
font-weight: 600;
}

.status-pending {
color: #ffe08a;
background: rgba(255, 224, 138, 0.18);
}

.content p {
font-size: 1.2rem;
font-weight: 400;
opacity: 0.5;
.status-paid {
color: #9af0b8;
background: rgba(154, 240, 184, 0.18);
}

.status-shipped {
color: #8ac5ff;
background: rgba(138, 197, 255, 0.18);
}

.primary-btn {
border: 0;
color: #041229;
font-weight: 700;
cursor: pointer;
border-radius: 8px;
padding: 10px 14px;
background: #8ac5ff;
}
197 changes: 190 additions & 7 deletions examples/rsbuild-minimal/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
import './App.css';
import './semver';
import './semver7';
import { useEffect, useMemo, useState } from 'react';
import { mockOrders, mockProducts, Order } from './mock/shop';

type RouteState =
| { name: 'products' }
| { name: 'product'; id: string }
| { name: 'order' }
| { name: 'checkout' };

function parseHashRoute(hash: string): RouteState {
const normalized = hash.replace(/^#/, '') || '/products';
const segments = normalized.split('/').filter(Boolean);

if (segments[0] === 'product' && segments[1]) {
return { name: 'product', id: segments[1] };
}
if (segments[0] === 'order') {
return { name: 'order' };
}
if (segments[0] === 'checkout') {
return { name: 'checkout' };
}
return { name: 'products' };
}

function StatusTag({ status }: { status: Order['status'] }) {
return (
<span className={`status status-${status}`}>{status.toUpperCase()}</span>
);
}

const App = () => {
// Dynamically import shared.ts to make it an async chunk
Expand All @@ -9,14 +39,167 @@ const App = () => {
console.log('Shared module loaded as async chunk');
});

const [route, setRoute] = useState<RouteState>(
parseHashRoute(window.location.hash),
);
const [checkoutProductId, setCheckoutProductId] = useState<string>(
mockProducts[0].id,
);

useEffect(() => {
if (!window.location.hash) {
window.location.hash = '/products';
}
const onHashChange = () => {
setRoute(parseHashRoute(window.location.hash));
};
window.addEventListener('hashchange', onHashChange);
return () => {
window.removeEventListener('hashchange', onHashChange);
};
}, []);

const checkoutProduct = useMemo(
() =>
mockProducts.find((item) => item.id === checkoutProductId) ??
mockProducts[0],
[checkoutProductId],
);

const renderPage = () => {
if (route.name === 'products') {
return (
<section className="page">
<h2>商品页</h2>
<p className="hint">Demo mock 商品列表(点击查看详情或直接下单)</p>
<div className="grid">
{mockProducts.map((item) => (
<article key={item.id} className="card">
<h3>{item.name}</h3>
<p>{item.desc}</p>
<div className="meta">
<span>¥{item.price}</span>
<span>库存: {item.stock}</span>
</div>
<div className="actions">
<a href={`#/product/${item.id}`}>详情页</a>
<a
href="#/checkout"
onClick={() => {
setCheckoutProductId(item.id);
}}
>
下单页
</a>
</div>
</article>
))}
</div>
</section>
);
}

if (route.name === 'product') {
const product = mockProducts.find((item) => item.id === route.id);
if (!product) {
return (
<section className="page">
<h2>详情页</h2>
<p className="hint">未找到商品:{route.id}</p>
<a href="#/products">返回商品页</a>
</section>
);
}
return (
<section className="page">
<h2>详情页</h2>
<article className="detail">
<h3>{product.name}</h3>
<p>{product.desc}</p>
<ul>
<li>商品 ID: {product.id}</li>
<li>分类: {product.category}</li>
<li>价格: ¥{product.price}</li>
<li>库存: {product.stock}</li>
</ul>
<div className="actions">
<a href="#/products">返回商品页</a>
<a
href="#/checkout"
onClick={() => {
setCheckoutProductId(product.id);
}}
>
立即下单
</a>
</div>
</article>
</section>
);
}

if (route.name === 'order') {
return (
<section className="page">
<h2>订单页</h2>
<p className="hint">Demo mock 订单列表</p>
<div className="list">
{mockOrders.map((order) => (
<article key={order.id} className="order-item">
<div>
<strong>{order.id}</strong>
<p>
{order.itemCount} 件商品 | 创建时间: {order.createdAt}
</p>
</div>
<div className="order-right">
<StatusTag status={order.status} />
<span>¥{order.total}</span>
</div>
</article>
))}
</div>
</section>
);
}

return (
<section className="page">
<h2>下单页</h2>
<p className="hint">Demo mock 结算信息</p>
<article className="detail">
<h3>{checkoutProduct.name}</h3>
<ul>
<li>商品 ID: {checkoutProduct.id}</li>
<li>单价: ¥{checkoutProduct.price}</li>
<li>数量: 1</li>
<li>合计: ¥{checkoutProduct.price}</li>
</ul>
<button
className="primary-btn"
onClick={() => {
window.alert(`下单成功(mock):${checkoutProduct.name}`);
}}
>
提交订单
</button>
</article>
</section>
);
};

return (
<>
<div className="content">
<h1>Rsbuild with React</h1>
<p>Start building amazing things with Rsbuild.</p>
</div>
<button className="button">Button</button>
</>
<div className="shell">
<header className="top-nav">
<h1>Rsbuild Minimal Mall Demo</h1>
<nav>
<a href="#/products">商品页</a>
<a href="#/order">订单页</a>
<a href="#/checkout">下单页</a>
</nav>
</header>
{renderPage()}
</div>
);
};

Expand Down
Loading
Loading