Skip to content

Commit abbd22b

Browse files
committed
feat(create-rstack): add Turborepo template
1 parent 0e95ea8 commit abbd22b

30 files changed

Lines changed: 451 additions & 6 deletions

packages/create-rstack/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ npx create-rstack --dir my-project --template app-vanilla-ts --no-git
5050
- `lib-solid-ts` - TypeScript Solid library
5151
- `doc` - Basic documentation site
5252
- `doc-i18n` - Multilingual documentation site
53+
- `turborepo` - Turborepo
5354

5455
## Documentation
5556

packages/create-rstack/src/index.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import {
99
access,
1010
appendFile,
11+
copyFile,
1112
mkdir,
1213
readFile,
1314
writeFile,
@@ -43,6 +44,7 @@ const templateNames = [
4344
'lib-solid-ts',
4445
'doc',
4546
'doc-i18n',
47+
'turborepo',
4648
];
4749

4850
const resolveTemplateName = (template: string): string => {
@@ -55,7 +57,7 @@ const resolveTemplateName = (template: string): string => {
5557

5658
const getTemplateName = async ({ template }: Argv): Promise<string> => {
5759
if (typeof template === 'string') {
58-
if (/^(?:app|lib|doc)(?:-|$)/u.test(template)) {
60+
if (/^(?:app|lib|doc|turborepo)(?:-|$)/u.test(template)) {
5961
return resolveTemplateName(template);
6062
}
6163

@@ -69,10 +71,22 @@ const getTemplateName = async ({ template }: Argv): Promise<string> => {
6971
{ value: 'app', label: 'Web Application' },
7072
{ value: 'lib', label: 'Library' },
7173
{ value: 'doc', label: 'Documentation' },
74+
{ value: 'monorepo', label: 'Monorepo' },
7275
],
7376
}),
7477
);
7578

79+
if (projectType === 'monorepo') {
80+
const monorepoType = checkCancel<string>(
81+
await select({
82+
message: 'Select monorepo tool',
83+
options: [{ value: 'turborepo', label: 'Turborepo', hint: 'pnpm' }],
84+
}),
85+
);
86+
87+
return resolveTemplateName(monorepoType);
88+
}
89+
7690
if (projectType === 'doc') {
7791
const documentationType = checkCancel<string>(
7892
await select({
@@ -215,11 +229,29 @@ const injectStagedSetup = async ({
215229
]);
216230
};
217231

232+
const configureTurborepo = async ({
233+
templateName,
234+
distFolder,
235+
}: GitResolvedContext): Promise<void> => {
236+
if (templateName !== 'turborepo') {
237+
return;
238+
}
239+
240+
// The toolkit may skip this file based on the invoking package manager.
241+
await copyFile(
242+
path.join(packageRoot, 'template-turborepo/pnpm-workspace.yaml'),
243+
path.join(distFolder, 'pnpm-workspace.yaml'),
244+
);
245+
};
246+
218247
await create({
219248
root: packageRoot,
220249
name: 'rstack',
221250
templates: templateNames,
222251
builtinTools: [],
223252
getTemplateName,
224-
onGitResolved: injectStagedSetup,
253+
onGitResolved: async (context) => {
254+
await configureTurborepo(context);
255+
await injectStagedSetup(context);
256+
},
225257
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Rstack + Turborepo
2+
3+
## Setup
4+
5+
```bash
6+
pnpm install
7+
```
8+
9+
## Structure
10+
11+
- `apps/web`: React application.
12+
- `packages/ui`: Shared UI components.
13+
- `packages/tsconfig`: Shared TypeScript configuration.
14+
15+
## Scripts
16+
17+
Run from the repository root:
18+
19+
- `pnpm run build`: Build all packages.
20+
- `pnpm run dev`: Start development.
21+
- `pnpm run test`: Run tests.
22+
- `pnpm run check`: Run lint, formatting, and TypeScript checks.
23+
- `pnpm run lint`: Lint the workspace.
24+
- `pnpm run format`: Format the workspace.
25+
26+
Run `pnpm run build` before `pnpm run check`.
27+
28+
## Learn more
29+
30+
- [Rstack CLI monorepo guide](https://rstack.rs/guide/monorepo)
31+
- [Turborepo documentation](https://turborepo.com/docs)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "web",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "rs build",
8+
"dev": "rs dev",
9+
"preview": "rs preview",
10+
"test": "rs test"
11+
},
12+
"dependencies": {
13+
"@repo/ui": "workspace:^",
14+
"react": "^19.2.8",
15+
"react-dom": "^19.2.8"
16+
},
17+
"devDependencies": {
18+
"@repo/tsconfig": "workspace:^",
19+
"@rsbuild/plugin-react": "^2.1.0",
20+
"@testing-library/dom": "^10.4.1",
21+
"@testing-library/jest-dom": "^7.0.1",
22+
"@testing-library/react": "^16.3.3",
23+
"@types/react": "^19.2.18",
24+
"@types/react-dom": "^19.2.5",
25+
"happy-dom": "^20.13.2"
26+
}
27+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Configuration guide: https://rstack.rs/config
2+
import { define } from 'rstack';
3+
4+
define.app(async () => {
5+
const { pluginReact } = await import('@rsbuild/plugin-react');
6+
return {
7+
plugins: [pluginReact()],
8+
html: { title: 'Web | Rstack' },
9+
server: { port: 3000 },
10+
};
11+
});
12+
13+
define.test({
14+
setupFiles: ['./tests/rstest.setup.ts'],
15+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Button } from '@repo/ui/button';
2+
3+
export function App() {
4+
return (
5+
<main>
6+
<h1>Web</h1>
7+
<p>Turborepo + Rstack CLI</p>
8+
<p>Edit apps/web/src/App.tsx to get started.</p>
9+
<Button onClick={() => window.alert('Hello from @repo/ui!')}>
10+
Shared button
11+
</Button>
12+
<nav>
13+
<a href="https://rstack.rs">Rstack documentation</a>
14+
</nav>
15+
</main>
16+
);
17+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
body {
2+
margin: 0;
3+
font-family: system-ui, sans-serif;
4+
color: #202124;
5+
background: #f8f9fa;
6+
}
7+
8+
main {
9+
max-width: 640px;
10+
margin: 15vh auto;
11+
padding: 24px;
12+
}
13+
14+
button {
15+
padding: 10px 16px;
16+
border: 0;
17+
border-radius: 8px;
18+
color: white;
19+
background: #5755d9;
20+
cursor: pointer;
21+
}
22+
23+
nav {
24+
display: flex;
25+
gap: 20px;
26+
margin-top: 24px;
27+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { StrictMode } from 'react';
2+
import { createRoot } from 'react-dom/client';
3+
import { App } from './App';
4+
import './index.css';
5+
6+
const root = document.getElementById('root');
7+
if (root) {
8+
createRoot(root).render(
9+
<StrictMode>
10+
<App />
11+
</StrictMode>,
12+
);
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { expect, test } from 'rstack/test';
2+
import { render, screen } from '@testing-library/react';
3+
import { App } from '../src/App';
4+
5+
test('renders the web app with the shared UI package', () => {
6+
render(<App />);
7+
expect(screen.getByRole('heading', { name: 'Web' })).toBeInTheDocument();
8+
expect(
9+
screen.getByRole('button', { name: 'Shared button' }),
10+
).toBeInTheDocument();
11+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { expect } from 'rstack/test';
2+
import * as jestDomMatchers from '@testing-library/jest-dom/matchers';
3+
4+
expect.extend(jestDomMatchers);

0 commit comments

Comments
 (0)