diff --git a/packages/create-rstack/README.md b/packages/create-rstack/README.md index f94449e..e6c47e2 100644 --- a/packages/create-rstack/README.md +++ b/packages/create-rstack/README.md @@ -50,6 +50,7 @@ npx create-rstack --dir my-project --template app-vanilla-ts --no-git - `lib-solid-ts` - TypeScript Solid library - `doc` - Basic documentation site - `doc-i18n` - Multilingual documentation site +- `turborepo` - Turborepo ## Documentation diff --git a/packages/create-rstack/src/index.ts b/packages/create-rstack/src/index.ts index 34d951f..e211637 100644 --- a/packages/create-rstack/src/index.ts +++ b/packages/create-rstack/src/index.ts @@ -8,6 +8,7 @@ import { import { access, appendFile, + copyFile, mkdir, readFile, writeFile, @@ -43,6 +44,7 @@ const templateNames = [ 'lib-solid-ts', 'doc', 'doc-i18n', + 'turborepo', ]; const resolveTemplateName = (template: string): string => { @@ -55,7 +57,7 @@ const resolveTemplateName = (template: string): string => { const getTemplateName = async ({ template }: Argv): Promise => { if (typeof template === 'string') { - if (/^(?:app|lib|doc)(?:-|$)/u.test(template)) { + if (/^(?:app|lib|doc|turborepo)(?:-|$)/u.test(template)) { return resolveTemplateName(template); } @@ -69,10 +71,22 @@ const getTemplateName = async ({ template }: Argv): Promise => { { value: 'app', label: 'Web Application' }, { value: 'lib', label: 'Library' }, { value: 'doc', label: 'Documentation' }, + { value: 'monorepo', label: 'Monorepo' }, ], }), ); + if (projectType === 'monorepo') { + const monorepoType = checkCancel( + await select({ + message: 'Select monorepo tool', + options: [{ value: 'turborepo', label: 'Turborepo', hint: 'pnpm' }], + }), + ); + + return resolveTemplateName(monorepoType); + } + if (projectType === 'doc') { const documentationType = checkCancel( await select({ @@ -215,11 +229,29 @@ const injectStagedSetup = async ({ ]); }; +const configureTurborepo = async ({ + templateName, + distFolder, +}: GitResolvedContext): Promise => { + if (templateName !== 'turborepo') { + return; + } + + // The toolkit may skip this file based on the invoking package manager. + await copyFile( + path.join(packageRoot, 'template-turborepo/pnpm-workspace.yaml'), + path.join(distFolder, 'pnpm-workspace.yaml'), + ); +}; + await create({ root: packageRoot, name: 'rstack', templates: templateNames, builtinTools: [], getTemplateName, - onGitResolved: injectStagedSetup, + onGitResolved: async (context) => { + await configureTurborepo(context); + await injectStagedSetup(context); + }, }); diff --git a/packages/create-rstack/template-turborepo/README.md b/packages/create-rstack/template-turborepo/README.md new file mode 100644 index 0000000..d1b1706 --- /dev/null +++ b/packages/create-rstack/template-turborepo/README.md @@ -0,0 +1,31 @@ +# Rstack + Turborepo + +## Setup + +```bash +pnpm install +``` + +## Structure + +- `apps/web`: React application. +- `packages/ui`: Shared UI components. +- `packages/tsconfig`: Shared TypeScript configuration. + +## Scripts + +Run from the repository root: + +- `pnpm run build`: Build all packages. +- `pnpm run dev`: Start development. +- `pnpm run test`: Run tests. +- `pnpm run check`: Run lint, formatting, and TypeScript checks. +- `pnpm run lint`: Lint the workspace. +- `pnpm run format`: Format the workspace. + +Run `pnpm run build` before `pnpm run check`. + +## Learn more + +- [Rstack CLI monorepo guide](https://rstack.rs/guide/monorepo) +- [Turborepo documentation](https://turborepo.com/docs) diff --git a/packages/create-rstack/template-turborepo/apps/web/package.json b/packages/create-rstack/template-turborepo/apps/web/package.json new file mode 100644 index 0000000..2763997 --- /dev/null +++ b/packages/create-rstack/template-turborepo/apps/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs build", + "dev": "rs dev", + "preview": "rs preview", + "test": "rs test" + }, + "dependencies": { + "@repo/ui": "workspace:^", + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@repo/tsconfig": "workspace:^", + "@rsbuild/plugin-react": "^2.1.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", + "happy-dom": "^20.13.2" + } +} diff --git a/packages/create-rstack/template-turborepo/apps/web/rstack.config.ts b/packages/create-rstack/template-turborepo/apps/web/rstack.config.ts new file mode 100644 index 0000000..778e61c --- /dev/null +++ b/packages/create-rstack/template-turborepo/apps/web/rstack.config.ts @@ -0,0 +1,15 @@ +// Configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.app(async () => { + const { pluginReact } = await import('@rsbuild/plugin-react'); + return { + plugins: [pluginReact()], + html: { title: 'Web | Rstack' }, + server: { port: 3000 }, + }; +}); + +define.test({ + setupFiles: ['./tests/rstest.setup.ts'], +}); diff --git a/packages/create-rstack/template-turborepo/apps/web/src/App.tsx b/packages/create-rstack/template-turborepo/apps/web/src/App.tsx new file mode 100644 index 0000000..2f58c21 --- /dev/null +++ b/packages/create-rstack/template-turborepo/apps/web/src/App.tsx @@ -0,0 +1,17 @@ +import { Button } from '@repo/ui/button'; + +export function App() { + return ( +
+

Web

+

Turborepo + Rstack CLI

+

Edit apps/web/src/App.tsx to get started.

+ + +
+ ); +} diff --git a/packages/create-rstack/template-turborepo/apps/web/src/index.css b/packages/create-rstack/template-turborepo/apps/web/src/index.css new file mode 100644 index 0000000..6da5467 --- /dev/null +++ b/packages/create-rstack/template-turborepo/apps/web/src/index.css @@ -0,0 +1,27 @@ +body { + margin: 0; + font-family: system-ui, sans-serif; + color: #202124; + background: #f8f9fa; +} + +main { + max-width: 640px; + margin: 15vh auto; + padding: 24px; +} + +button { + padding: 10px 16px; + border: 0; + border-radius: 8px; + color: white; + background: #5755d9; + cursor: pointer; +} + +nav { + display: flex; + gap: 20px; + margin-top: 24px; +} diff --git a/packages/create-rstack/template-turborepo/apps/web/src/index.tsx b/packages/create-rstack/template-turborepo/apps/web/src/index.tsx new file mode 100644 index 0000000..8910946 --- /dev/null +++ b/packages/create-rstack/template-turborepo/apps/web/src/index.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import './index.css'; + +const root = document.getElementById('root'); +if (root) { + createRoot(root).render( + + + , + ); +} diff --git a/packages/create-rstack/template-turborepo/apps/web/tests/app.test.tsx b/packages/create-rstack/template-turborepo/apps/web/tests/app.test.tsx new file mode 100644 index 0000000..7268227 --- /dev/null +++ b/packages/create-rstack/template-turborepo/apps/web/tests/app.test.tsx @@ -0,0 +1,11 @@ +import { expect, test } from 'rstack/test'; +import { render, screen } from '@testing-library/react'; +import { App } from '../src/App'; + +test('renders the web app with the shared UI package', () => { + render(); + expect(screen.getByRole('heading', { name: 'Web' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Shared button' }), + ).toBeInTheDocument(); +}); diff --git a/packages/create-rstack/template-turborepo/apps/web/tests/rstest.setup.ts b/packages/create-rstack/template-turborepo/apps/web/tests/rstest.setup.ts new file mode 100644 index 0000000..93951b1 --- /dev/null +++ b/packages/create-rstack/template-turborepo/apps/web/tests/rstest.setup.ts @@ -0,0 +1,4 @@ +import { expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; + +expect.extend(jestDomMatchers); diff --git a/packages/create-rstack/template-turborepo/apps/web/tests/tsconfig.json b/packages/create-rstack/template-turborepo/apps/web/tests/tsconfig.json new file mode 100644 index 0000000..3eb5dc1 --- /dev/null +++ b/packages/create-rstack/template-turborepo/apps/web/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["rstack/types", "node", "@testing-library/jest-dom"] + }, + "include": ["./"] +} diff --git a/packages/create-rstack/template-turborepo/apps/web/tsconfig.json b/packages/create-rstack/template-turborepo/apps/web/tsconfig.json new file mode 100644 index 0000000..2ece2ca --- /dev/null +++ b/packages/create-rstack/template-turborepo/apps/web/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@repo/tsconfig/base.json", + "include": ["src", "rstack.config.ts"] +} diff --git a/packages/create-rstack/template-turborepo/apps/web/turbo.json b/packages/create-rstack/template-turborepo/apps/web/turbo.json new file mode 100644 index 0000000..1e9868c --- /dev/null +++ b/packages/create-rstack/template-turborepo/apps/web/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "dev": { + "persistent": true + } + } +} diff --git a/packages/create-rstack/template-turborepo/gitignore b/packages/create-rstack/template-turborepo/gitignore new file mode 100644 index 0000000..a2c517f --- /dev/null +++ b/packages/create-rstack/template-turborepo/gitignore @@ -0,0 +1,16 @@ +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea + +# Turborepo +.turbo/ diff --git a/packages/create-rstack/template-turborepo/package.json b/packages/create-rstack/template-turborepo/package.json new file mode 100644 index 0000000..c6505bc --- /dev/null +++ b/packages/create-rstack/template-turborepo/package.json @@ -0,0 +1,20 @@ +{ + "name": "rstack-turborepo", + "private": true, + "type": "module", + "scripts": { + "build": "turbo run build", + "check": "rs check --type-check", + "dev": "turbo watch dev", + "format": "rs fmt", + "lint": "rs lint", + "test": "turbo run test" + }, + "devDependencies": { + "@types/node": "^26.4.1", + "rstack": "^0.7.3", + "turbo": "^2.10.12", + "typescript": "^7.0.2" + }, + "packageManager": "pnpm@11.25.0" +} diff --git a/packages/create-rstack/template-turborepo/packages/tsconfig/base.json b/packages/create-rstack/template-turborepo/packages/tsconfig/base.json new file mode 100644 index 0000000..d57fde3 --- /dev/null +++ b/packages/create-rstack/template-turborepo/packages/tsconfig/base.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "types": ["rstack/types", "node"] + } +} diff --git a/packages/create-rstack/template-turborepo/packages/tsconfig/package.json b/packages/create-rstack/template-turborepo/packages/tsconfig/package.json new file mode 100644 index 0000000..032c7a9 --- /dev/null +++ b/packages/create-rstack/template-turborepo/packages/tsconfig/package.json @@ -0,0 +1,11 @@ +{ + "name": "@repo/tsconfig", + "version": "0.0.0", + "private": true, + "exports": { + "./base.json": "./base.json" + }, + "files": [ + "base.json" + ] +} diff --git a/packages/create-rstack/template-turborepo/packages/ui/package.json b/packages/create-rstack/template-turborepo/packages/ui/package.json new file mode 100644 index 0000000..6babb4a --- /dev/null +++ b/packages/create-rstack/template-turborepo/packages/ui/package.json @@ -0,0 +1,31 @@ +{ + "name": "@repo/ui", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + "./button": { + "types": "./dist/button.d.ts", + "default": "./dist/button.js" + } + }, + "scripts": { + "build": "rs lib", + "dev": "rs lib --no-clean", + "test": "rs test" + }, + "dependencies": { + "react": "^19.2.8" + }, + "devDependencies": { + "@repo/tsconfig": "workspace:^", + "@rsbuild/plugin-react": "^2.1.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", + "happy-dom": "^20.13.2", + "react-dom": "^19.2.8" + } +} diff --git a/packages/create-rstack/template-turborepo/packages/ui/rstack.config.ts b/packages/create-rstack/template-turborepo/packages/ui/rstack.config.ts new file mode 100644 index 0000000..7043804 --- /dev/null +++ b/packages/create-rstack/template-turborepo/packages/ui/rstack.config.ts @@ -0,0 +1,20 @@ +// Configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.lib(async () => { + const { pluginReact } = await import('@rsbuild/plugin-react'); + return { + dts: true, + source: { + entry: { button: './src/button.tsx' }, + }, + output: { + target: 'web', + }, + plugins: [pluginReact()], + }; +}); + +define.test({ + setupFiles: ['./tests/rstest.setup.ts'], +}); diff --git a/packages/create-rstack/template-turborepo/packages/ui/src/button.tsx b/packages/create-rstack/template-turborepo/packages/ui/src/button.tsx new file mode 100644 index 0000000..19db4f2 --- /dev/null +++ b/packages/create-rstack/template-turborepo/packages/ui/src/button.tsx @@ -0,0 +1,8 @@ +import type { ComponentProps } from 'react'; + +export function Button({ + type = 'button', + ...props +}: ComponentProps<'button'>) { + return ); + const button = screen.getByRole('button', { name: 'Save' }); + expect(button).toHaveAttribute('type', 'button'); + expect(button).toBeDisabled(); +}); + +test('allows a submit button', () => { + render(); + expect(screen.getByRole('button', { name: 'Submit' })).toHaveAttribute( + 'type', + 'submit', + ); +}); diff --git a/packages/create-rstack/template-turborepo/packages/ui/tests/rstest.setup.ts b/packages/create-rstack/template-turborepo/packages/ui/tests/rstest.setup.ts new file mode 100644 index 0000000..93951b1 --- /dev/null +++ b/packages/create-rstack/template-turborepo/packages/ui/tests/rstest.setup.ts @@ -0,0 +1,4 @@ +import { expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; + +expect.extend(jestDomMatchers); diff --git a/packages/create-rstack/template-turborepo/packages/ui/tests/tsconfig.json b/packages/create-rstack/template-turborepo/packages/ui/tests/tsconfig.json new file mode 100644 index 0000000..1f0e1fc --- /dev/null +++ b/packages/create-rstack/template-turborepo/packages/ui/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "types": ["rstack/types", "node", "@testing-library/jest-dom"] + }, + "include": ["./", "../rstack.config.ts"] +} diff --git a/packages/create-rstack/template-turborepo/packages/ui/tsconfig.json b/packages/create-rstack/template-turborepo/packages/ui/tsconfig.json new file mode 100644 index 0000000..7cac469 --- /dev/null +++ b/packages/create-rstack/template-turborepo/packages/ui/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@repo/tsconfig/base.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/create-rstack/template-turborepo/pnpm-workspace.yaml b/packages/create-rstack/template-turborepo/pnpm-workspace.yaml new file mode 100644 index 0000000..286cf7f --- /dev/null +++ b/packages/create-rstack/template-turborepo/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - apps/* + - packages/* diff --git a/packages/create-rstack/template-turborepo/rstack.config.ts b/packages/create-rstack/template-turborepo/rstack.config.ts new file mode 100644 index 0000000..b0f865d --- /dev/null +++ b/packages/create-rstack/template-turborepo/rstack.config.ts @@ -0,0 +1,30 @@ +// Configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.lint(({ js, ts, reactPlugin, reactHooksPlugin, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, + { + languageOptions: { + parserOptions: { + project: [ + './apps/*/tsconfig.json', + './apps/*/tests/tsconfig.json', + './packages/*/tsconfig.json', + './packages/*/tests/tsconfig.json', + ], + }, + }, + }, +]); + +define.fmt({ + singleQuote: true, + ignorePatterns: ['**/dist/**', '**/.turbo/**'], +}); diff --git a/packages/create-rstack/template-turborepo/turbo.json b/packages/create-rstack/template-turborepo/turbo.json new file mode 100644 index 0000000..e697288 --- /dev/null +++ b/packages/create-rstack/template-turborepo/turbo.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://turborepo.dev/schema.json", + "ui": "tui", + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "dev": { + "dependsOn": ["^dev"], + "cache": false + }, + "test": { + "dependsOn": ["^build"] + } + } +} diff --git a/packages/create-rstack/tests/create.test.ts b/packages/create-rstack/tests/create.test.ts index a6170b8..79685cf 100644 --- a/packages/create-rstack/tests/create.test.ts +++ b/packages/create-rstack/tests/create.test.ts @@ -21,6 +21,8 @@ const templatesWithoutTypeCheck = new Set([ type ProjectPackage = { name: string; scripts: Record; + packageManager?: string; + dependencies?: Record; }; type SourceTemplate = { @@ -293,3 +295,31 @@ test.each(docTemplates)( await expectFiles(projectDirectory, files); }, ); + +test('creates the Turborepo template with pnpm', async () => { + const projectDirectory = await createProject('turborepo'); + const packageJson = await readProjectPackage(projectDirectory); + const appPackage = await readProjectPackage( + path.join(projectDirectory, 'apps/web'), + ); + + await expectStagedSetup(projectDirectory, 'ts', packageJson.scripts); + expect(packageJson.packageManager).toMatch(/^pnpm@/u); + expect(appPackage.dependencies?.['@repo/ui']).toBe('workspace:^'); + await expectFiles(projectDirectory, [ + 'pnpm-workspace.yaml', + 'turbo.json', + 'apps/web/src/index.tsx', + 'packages/ui/src/button.tsx', + ]); +}); + +test('configures Turborepo when Git initialization is disabled', async () => { + const projectDirectory = await createProject('turborepo', { + args: ['--no-git'], + }); + const packageJson = await readProjectPackage(projectDirectory); + + expect(packageJson.packageManager).toMatch(/^pnpm@/u); + await expectNoStagedSetup(projectDirectory, 'ts', packageJson.scripts); +}); diff --git a/website/docs/en/guide/quick-start.mdx b/website/docs/en/guide/quick-start.mdx index 236fd55..891b802 100644 --- a/website/docs/en/guide/quick-start.mdx +++ b/website/docs/en/guide/quick-start.mdx @@ -26,7 +26,7 @@ Rstack CLI requires Node.js 22.18.0+ or 24.3.0+. ## Create a Rstack project -[create-rstack](https://www.npmjs.com/package/create-rstack) lets you quickly create an application, library, or documentation site with Rstack configured. We recommend using [pnpm](https://pnpm.io/) as your package manager. +[create-rstack](https://www.npmjs.com/package/create-rstack) lets you quickly create an application, library, documentation site, or monorepo with Rstack configured. We recommend using [pnpm](https://pnpm.io/) as your package manager. specify the package name --template-version specify the npm template version -Available templates: app-vanilla, app-vanilla-ts, app-react, app-react-ts, app-preact, app-preact-ts, app-vue, app-vue-ts, app-lit, app-lit-ts, app-svelte, app-svelte-ts, app-solid, app-solid-ts, lib-node, lib-node-ts, lib-react, lib-react-ts, lib-vue, lib-vue-ts, lib-svelte, lib-svelte-ts, lib-solid, lib-solid-ts, doc, doc-i18n +Available templates: app-vanilla, app-vanilla-ts, app-react, app-react-ts, app-preact, app-preact-ts, app-vue, app-vue-ts, app-lit, app-lit-ts, app-svelte, app-svelte-ts, app-solid, app-solid-ts, lib-node, lib-node-ts, lib-react, lib-react-ts, lib-vue, lib-vue-ts, lib-svelte, lib-svelte-ts, lib-solid, lib-solid-ts, doc, doc-i18n, turborepo ``` ## Install Rstack CLI \{#install-rstack} diff --git a/website/docs/zh/guide/quick-start.mdx b/website/docs/zh/guide/quick-start.mdx index 11fec3c..d10b0ff 100644 --- a/website/docs/zh/guide/quick-start.mdx +++ b/website/docs/zh/guide/quick-start.mdx @@ -26,7 +26,7 @@ Rstack CLI 要求 Node.js 22.18.0+ 或 24.3.0+。 ## 创建 Rstack 项目 \{#create-a-rstack-project} -使用 [create-rstack](https://www.npmjs.com/package/create-rstack) 可以快速创建已配置好 Rstack 的应用、库或文档站点。推荐使用 [pnpm](https://pnpm.io/) 作为包管理器。 +使用 [create-rstack](https://www.npmjs.com/package/create-rstack) 可以快速创建已配置好 Rstack 的应用、库、文档站点或 monorepo。推荐使用 [pnpm](https://pnpm.io/) 作为包管理器。 specify the package name --template-version specify the npm template version -Available templates: app-vanilla, app-vanilla-ts, app-react, app-react-ts, app-preact, app-preact-ts, app-vue, app-vue-ts, app-lit, app-lit-ts, app-svelte, app-svelte-ts, app-solid, app-solid-ts, lib-node, lib-node-ts, lib-react, lib-react-ts, lib-vue, lib-vue-ts, lib-svelte, lib-svelte-ts, lib-solid, lib-solid-ts, doc, doc-i18n +Available templates: app-vanilla, app-vanilla-ts, app-react, app-react-ts, app-preact, app-preact-ts, app-vue, app-vue-ts, app-lit, app-lit-ts, app-svelte, app-svelte-ts, app-solid, app-solid-ts, lib-node, lib-node-ts, lib-react, lib-react-ts, lib-vue, lib-vue-ts, lib-svelte, lib-svelte-ts, lib-solid, lib-solid-ts, doc, doc-i18n, turborepo ``` ## 安装 Rstack CLI \{#install-rstack}