From 59d38d400b0d916ee1e628046289a7461fcec92f Mon Sep 17 00:00:00 2001 From: neverland Date: Fri, 7 Aug 2026 16:40:40 +0800 Subject: [PATCH] feat: add create-rstack package --- packages/create-rstack/LICENSE | 21 +++++ packages/create-rstack/README.md | 33 ++++++++ packages/create-rstack/bin.js | 2 + packages/create-rstack/package.json | 52 +++++++++++++ packages/create-rstack/rstack.config.ts | 12 +++ packages/create-rstack/src/index.ts | 43 +++++++++++ .../template-app-js/package.json | 14 ++++ .../template-app-js/rstack.config.js | 5 ++ .../template-app-js/src/index.css | 26 +++++++ .../template-app-js/src/index.js | 12 +++ .../template-app-ts/package.json | 16 ++++ .../template-app-ts/rstack.config.ts | 4 + .../template-app-ts/src/index.css | 26 +++++++ .../template-app-ts/src/index.ts | 12 +++ .../template-app-ts/tsconfig.json | 22 ++++++ .../create-rstack/template-common/AGENTS.md | 15 ++++ .../create-rstack/template-common/README.md | 34 +++++++++ .../create-rstack/template-common/gitignore | 13 ++++ packages/create-rstack/tests/create.test.ts | 76 +++++++++++++++++++ packages/create-rstack/tests/tsconfig.json | 10 +++ packages/create-rstack/tsconfig.json | 16 ++++ pnpm-lock.yaml | 25 ++++++ pnpm-workspace.yaml | 1 + 23 files changed, 490 insertions(+) create mode 100644 packages/create-rstack/LICENSE create mode 100644 packages/create-rstack/README.md create mode 100755 packages/create-rstack/bin.js create mode 100644 packages/create-rstack/package.json create mode 100644 packages/create-rstack/rstack.config.ts create mode 100644 packages/create-rstack/src/index.ts create mode 100644 packages/create-rstack/template-app-js/package.json create mode 100644 packages/create-rstack/template-app-js/rstack.config.js create mode 100644 packages/create-rstack/template-app-js/src/index.css create mode 100644 packages/create-rstack/template-app-js/src/index.js create mode 100644 packages/create-rstack/template-app-ts/package.json create mode 100644 packages/create-rstack/template-app-ts/rstack.config.ts create mode 100644 packages/create-rstack/template-app-ts/src/index.css create mode 100644 packages/create-rstack/template-app-ts/src/index.ts create mode 100644 packages/create-rstack/template-app-ts/tsconfig.json create mode 100644 packages/create-rstack/template-common/AGENTS.md create mode 100644 packages/create-rstack/template-common/README.md create mode 100644 packages/create-rstack/template-common/gitignore create mode 100644 packages/create-rstack/tests/create.test.ts create mode 100644 packages/create-rstack/tests/tsconfig.json create mode 100644 packages/create-rstack/tsconfig.json diff --git a/packages/create-rstack/LICENSE b/packages/create-rstack/LICENSE new file mode 100644 index 00000000..235ea7b4 --- /dev/null +++ b/packages/create-rstack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Rstack contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/create-rstack/README.md b/packages/create-rstack/README.md new file mode 100644 index 00000000..34d9bbc7 --- /dev/null +++ b/packages/create-rstack/README.md @@ -0,0 +1,33 @@ +# create-rstack + +Create a new Rstack project. + +## Usage + +Using `npm create`: + +```bash +npm create rstack@latest +``` + +Using CLI flags: + +```bash +npx create-rstack --dir my-project --template app-ts + +# Using abbreviations +npx create-rstack -d my-project -t app-ts +``` + +## Templates + +- `app-js` - JavaScript application +- `app-ts` - TypeScript application + +## Documentation + +See the [Rstack documentation](https://rstack.rs). + +## License + +[MIT](https://github.com/rstackjs/rstack-cli/blob/main/LICENSE). diff --git a/packages/create-rstack/bin.js b/packages/create-rstack/bin.js new file mode 100755 index 00000000..1ae21c85 --- /dev/null +++ b/packages/create-rstack/bin.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import './dist/index.js'; diff --git a/packages/create-rstack/package.json b/packages/create-rstack/package.json new file mode 100644 index 00000000..7fbea590 --- /dev/null +++ b/packages/create-rstack/package.json @@ -0,0 +1,52 @@ +{ + "name": "create-rstack", + "version": "3.0.0", + "description": "Create a new Rstack project", + "homepage": "https://rstack.rs", + "bugs": { + "url": "https://github.com/rstackjs/rstack-cli/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rstackjs/rstack-cli.git", + "directory": "packages/create-rstack" + }, + "license": "MIT", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "types": "./dist/index.d.ts", + "bin": { + "create-rstack": "./bin.js" + }, + "files": [ + "template-*/**", + "dist", + "bin.js" + ], + "scripts": { + "build": "rs lib", + "dev": "rs lib --watch", + "start": "node ./dist/index.js", + "test": "rs test" + }, + "dependencies": { + "@rstackjs/create-toolkit": "catalog:" + }, + "devDependencies": { + "@types/node": "catalog:", + "rstack": "workspace:*", + "typescript": "catalog:" + }, + "engines": { + "node": ">=22.12.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/create-rstack/rstack.config.ts b/packages/create-rstack/rstack.config.ts new file mode 100644 index 00000000..61659acf --- /dev/null +++ b/packages/create-rstack/rstack.config.ts @@ -0,0 +1,12 @@ +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.lib({ + syntax: 'es2023', +}); + +define.test({ + source: { + tsconfigPath: './tests/tsconfig.json', + }, +}); diff --git a/packages/create-rstack/src/index.ts b/packages/create-rstack/src/index.ts new file mode 100644 index 00000000..17e9833b --- /dev/null +++ b/packages/create-rstack/src/index.ts @@ -0,0 +1,43 @@ +import { + type Argv, + checkCancel, + create, + type ESLintTemplateName, + type RslintTemplateName, + select, +} from '@rstackjs/create-toolkit'; +import path from 'node:path'; + +const getTemplateName = async ({ template }: Argv): Promise => { + if (typeof template === 'string') { + const [type, language = 'js'] = template.split('-'); + return `${type}-${language}`; + } + + const language = checkCancel( + await select({ + message: 'Select language', + options: [ + { value: 'ts', label: 'TypeScript' }, + { value: 'js', label: 'JavaScript' }, + ], + }), + ); + + return `app-${language}`; +}; + +const mapESLintTemplate = (templateName: string): ESLintTemplateName => + templateName.endsWith('-ts') ? 'vanilla-ts' : 'vanilla-js'; + +const mapRslintTemplate = (templateName: string): RslintTemplateName => + templateName.endsWith('-ts') ? 'vanilla-ts' : 'vanilla-js'; + +await create({ + root: path.join(import.meta.dirname, '..'), + name: 'rstack', + templates: ['app-js', 'app-ts'], + getTemplateName, + mapESLintTemplate, + mapRslintTemplate, +}); diff --git a/packages/create-rstack/template-app-js/package.json b/packages/create-rstack/template-app-js/package.json new file mode 100644 index 00000000..0247d794 --- /dev/null +++ b/packages/create-rstack/template-app-js/package.json @@ -0,0 +1,14 @@ +{ + "name": "rstack-app-js", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs build", + "dev": "rs dev --open", + "preview": "rs preview" + }, + "devDependencies": { + "rstack": "^0.3.5" + } +} diff --git a/packages/create-rstack/template-app-js/rstack.config.js b/packages/create-rstack/template-app-js/rstack.config.js new file mode 100644 index 00000000..82c1574b --- /dev/null +++ b/packages/create-rstack/template-app-js/rstack.config.js @@ -0,0 +1,5 @@ +// @ts-check +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.app({}); diff --git a/packages/create-rstack/template-app-js/src/index.css b/packages/create-rstack/template-app-js/src/index.css new file mode 100644 index 00000000..164c0a6a --- /dev/null +++ b/packages/create-rstack/template-app-js/src/index.css @@ -0,0 +1,26 @@ +body { + margin: 0; + color: #fff; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + background-image: linear-gradient(to bottom, #020917, #101725); +} + +.content { + display: flex; + min-height: 100vh; + line-height: 1.1; + text-align: center; + flex-direction: column; + justify-content: center; +} + +.content h1 { + font-size: 3.6rem; + font-weight: 700; +} + +.content p { + font-size: 1.2rem; + font-weight: 400; + opacity: 0.5; +} diff --git a/packages/create-rstack/template-app-js/src/index.js b/packages/create-rstack/template-app-js/src/index.js new file mode 100644 index 00000000..a1546df9 --- /dev/null +++ b/packages/create-rstack/template-app-js/src/index.js @@ -0,0 +1,12 @@ +import './index.css'; + +const rootElement = document.querySelector('#root'); + +if (rootElement) { + rootElement.innerHTML = ` +
+

Rstack

+

Start building amazing things with Rstack.

+
+ `; +} diff --git a/packages/create-rstack/template-app-ts/package.json b/packages/create-rstack/template-app-ts/package.json new file mode 100644 index 00000000..bd79bc97 --- /dev/null +++ b/packages/create-rstack/template-app-ts/package.json @@ -0,0 +1,16 @@ +{ + "name": "rstack-app-ts", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs build", + "dev": "rs dev --open", + "preview": "rs preview" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "rstack": "^0.3.5", + "typescript": "^7.0.2" + } +} diff --git a/packages/create-rstack/template-app-ts/rstack.config.ts b/packages/create-rstack/template-app-ts/rstack.config.ts new file mode 100644 index 00000000..6dd79648 --- /dev/null +++ b/packages/create-rstack/template-app-ts/rstack.config.ts @@ -0,0 +1,4 @@ +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.app({}); diff --git a/packages/create-rstack/template-app-ts/src/index.css b/packages/create-rstack/template-app-ts/src/index.css new file mode 100644 index 00000000..164c0a6a --- /dev/null +++ b/packages/create-rstack/template-app-ts/src/index.css @@ -0,0 +1,26 @@ +body { + margin: 0; + color: #fff; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + background-image: linear-gradient(to bottom, #020917, #101725); +} + +.content { + display: flex; + min-height: 100vh; + line-height: 1.1; + text-align: center; + flex-direction: column; + justify-content: center; +} + +.content h1 { + font-size: 3.6rem; + font-weight: 700; +} + +.content p { + font-size: 1.2rem; + font-weight: 400; + opacity: 0.5; +} diff --git a/packages/create-rstack/template-app-ts/src/index.ts b/packages/create-rstack/template-app-ts/src/index.ts new file mode 100644 index 00000000..a1546df9 --- /dev/null +++ b/packages/create-rstack/template-app-ts/src/index.ts @@ -0,0 +1,12 @@ +import './index.css'; + +const rootElement = document.querySelector('#root'); + +if (rootElement) { + rootElement.innerHTML = ` +
+

Rstack

+

Start building amazing things with Rstack.

+
+ `; +} diff --git a/packages/create-rstack/template-app-ts/tsconfig.json b/packages/create-rstack/template-app-ts/tsconfig.json new file mode 100644 index 00000000..3fae4ab2 --- /dev/null +++ b/packages/create-rstack/template-app-ts/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["DOM", "ES2020"], + "target": "ES2020", + "noEmit": true, + "skipLibCheck": true, + "types": ["rstack/types", "node"], + "useDefineForClassFields": true, + + /* modules */ + "moduleDetection": "force", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + + /* type checking */ + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} diff --git a/packages/create-rstack/template-common/AGENTS.md b/packages/create-rstack/template-common/AGENTS.md new file mode 100644 index 00000000..6c887e17 --- /dev/null +++ b/packages/create-rstack/template-common/AGENTS.md @@ -0,0 +1,15 @@ +# AGENTS.md + +You are an expert in JavaScript, Rstack, and web application development. You write maintainable, performant, and accessible code. + +## Commands + +- `{{ packageManager }} run dev` - Start the development server +- `{{ packageManager }} run build` - Build the app for production +- `{{ packageManager }} run preview` - Preview the production build locally + +## Docs + +- Rstack: https://rstack.rs/llms.txt +- Rsbuild: https://rsbuild.rs/llms.txt +- Rspack: https://rspack.rs/llms.txt diff --git a/packages/create-rstack/template-common/README.md b/packages/create-rstack/template-common/README.md new file mode 100644 index 00000000..d62a167e --- /dev/null +++ b/packages/create-rstack/template-common/README.md @@ -0,0 +1,34 @@ +# Rstack project + +## Setup + +Install the dependencies: + +```bash +{{ packageManager }} install +``` + +## Get started + +Start the development server: + +```bash +{{ packageManager }} run dev +``` + +Build the app for production: + +```bash +{{ packageManager }} run build +``` + +Preview the production build locally: + +```bash +{{ packageManager }} run preview +``` + +## Learn more + +- [Rstack documentation](https://rstack.rs) +- [Rstack GitHub repository](https://github.com/rstackjs/rstack-cli) diff --git a/packages/create-rstack/template-common/gitignore b/packages/create-rstack/template-common/gitignore new file mode 100644 index 00000000..38d7344c --- /dev/null +++ b/packages/create-rstack/template-common/gitignore @@ -0,0 +1,13 @@ +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea diff --git a/packages/create-rstack/tests/create.test.ts b/packages/create-rstack/tests/create.test.ts new file mode 100644 index 00000000..ad12c492 --- /dev/null +++ b/packages/create-rstack/tests/create.test.ts @@ -0,0 +1,76 @@ +import { execFile } from 'node:child_process'; +import { access, mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { promisify } from 'node:util'; +import { afterEach, expect, test } from 'rstack/test'; + +const execFileAsync = promisify(execFile); +const packageRoot = path.resolve(import.meta.dirname, '..'); +const binPath = path.join(packageRoot, 'bin.js'); +const tempDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +const createProject = async (template: string) => { + const tempDirectory = await mkdtemp(path.join(tmpdir(), 'create-rstack-')); + const projectDirectory = path.join(tempDirectory, 'my-app'); + tempDirectories.push(tempDirectory); + + await execFileAsync(process.execPath, [binPath, projectDirectory, '--template', template], { + cwd: tempDirectory, + env: { + ...process.env, + npm_config_user_agent: 'pnpm/11.20.0', + }, + }); + + return projectDirectory; +}; + +test.each([ + { template: 'app-js', extension: 'js', hasTypeScript: false }, + { template: 'app-ts', extension: 'ts', hasTypeScript: true }, +])('creates the $template template', async ({ template, extension, hasTypeScript }) => { + const projectDirectory = await createProject(template); + const packageJson = JSON.parse( + await readFile(path.join(projectDirectory, 'package.json'), 'utf8'), + ); + + expect(packageJson).toMatchObject({ + name: 'my-app', + private: true, + scripts: { + build: 'rs build', + dev: 'rs dev --open', + preview: 'rs preview', + }, + devDependencies: { + rstack: '^0.3.5', + }, + }); + + await expect(access(path.join(projectDirectory, 'README.md'))).resolves.toBeUndefined(); + await expect(access(path.join(projectDirectory, '.gitignore'))).resolves.toBeUndefined(); + await expect( + access(path.join(projectDirectory, `rstack.config.${extension}`)), + ).resolves.toBeUndefined(); + await expect( + access(path.join(projectDirectory, `src/index.${extension}`)), + ).resolves.toBeUndefined(); + + const tsconfigPath = path.join(projectDirectory, 'tsconfig.json'); + if (hasTypeScript) { + await expect(access(tsconfigPath)).resolves.toBeUndefined(); + expect(packageJson.devDependencies).toMatchObject({ + '@types/node': '^24.13.3', + typescript: '^7.0.2', + }); + } else { + await expect(access(tsconfigPath)).rejects.toThrow(); + } +}); diff --git a/packages/create-rstack/tests/tsconfig.json b/packages/create-rstack/tests/tsconfig.json new file mode 100644 index 00000000..9696beec --- /dev/null +++ b/packages/create-rstack/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "noEmit": true, + "rootDir": "..", + "types": ["node"] + }, + "include": ["./**/*.ts", "../src"] +} diff --git a/packages/create-rstack/tsconfig.json b/packages/create-rstack/tsconfig.json new file mode 100644 index 00000000..227d39da --- /dev/null +++ b/packages/create-rstack/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "target": "ES2023", + "types": ["node"], + "lib": ["ESNext"], + "declaration": true, + "isolatedDeclarations": true, + "skipLibCheck": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "rewriteRelativeImportExtensions": true + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be9bdd9c..55c5b963 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,9 @@ catalogs: '@rstack-dev/doc-ui': specifier: 1.14.7 version: 1.14.7 + '@rstackjs/create-toolkit': + specifier: 2.1.4 + version: 2.1.4 '@rstackjs/load-config': specifier: ^0.1.2 version: 0.1.2 @@ -325,6 +328,22 @@ importers: specifier: 'catalog:' version: 7.0.2 + packages/create-rstack: + dependencies: + '@rstackjs/create-toolkit': + specifier: 'catalog:' + version: 2.1.4 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 24.13.3 + rstack: + specifier: workspace:* + version: link:../rstack + typescript: + specifier: 'catalog:' + version: 7.0.2 + packages/rstack: dependencies: '@rsbuild/core': @@ -852,6 +871,10 @@ packages: '@rspress/core': optional: true + '@rstackjs/create-toolkit@2.1.4': + resolution: {integrity: sha512-kwPhfMdgWN7z/Op5UKMszhmZy7+hzqXeiQPli4uycYYTLxCVUGLlEW3etoGoZlrM2t5zLqXZQAgL60PTUR/ocw==} + engines: {node: ^20.19.0 || >=22.12.0} + '@rstackjs/load-config@0.1.2': resolution: {integrity: sha512-6hChPVosmh2rzEt1M1CvGpsaE/+gGlt51ci5pbyd4Bd1FXyH+Owlg99ECvdcWtD7zdDwDM3jGkQL05xn9oWSIA==} peerDependencies: @@ -2700,6 +2723,8 @@ snapshots: optionalDependencies: '@rspress/core': 2.0.19(micromark-util-types@2.0.2)(micromark@4.0.2)(supports-color@8.1.1) + '@rstackjs/create-toolkit@2.1.4': {} + '@rstackjs/load-config@0.1.2': {} '@rstackjs/test-utils@0.2.0': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a12d531e..b22118e4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -21,6 +21,7 @@ catalog: '@rspress/plugin-client-redirects': '^2.0.19' '@rspress/plugin-sitemap': '^2.0.19' '@rstack-dev/doc-ui': '1.14.7' + '@rstackjs/create-toolkit': '2.1.4' '@rstackjs/load-config': ^0.1.2 '@rstackjs/test-utils': ^0.2.0 '@rstest/adapter-rsbuild': '~0.11.5'