xi-create 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -0
- package/bin/xi-create.js +189 -0
- package/package.json +40 -0
- package/templates/template-react-ts/README.md +50 -0
- package/templates/template-react-ts/_gitignore +41 -0
- package/templates/template-react-ts/eslint.config.js +28 -0
- package/templates/template-react-ts/index.html +13 -0
- package/templates/template-react-ts/package.json +67 -0
- package/templates/template-react-ts/public/vite.svg +1 -0
- package/templates/template-react-ts/src/App.css +0 -0
- package/templates/template-react-ts/src/App.tsx +29 -0
- package/templates/template-react-ts/src/index.css +1 -0
- package/templates/template-react-ts/src/main.tsx +11 -0
- package/templates/template-react-ts/src/pages/home/Home.tsx +24 -0
- package/templates/template-react-ts/src/pages/login/Login.tsx +24 -0
- package/templates/template-react-ts/src/vite-env.d.ts +1 -0
- package/templates/template-react-ts/tsconfig.app.json +32 -0
- package/templates/template-react-ts/tsconfig.json +14 -0
- package/templates/template-react-ts/tsconfig.node.json +28 -0
- package/templates/template-react-ts/vite.config.ts +99 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# xi-create
|
|
2
|
+
|
|
3
|
+
一个简单易用的命令行工具,用于基于模板快速创建新项目,支持交互式与「项目名 + 模板」一行创建。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 特性
|
|
8
|
+
|
|
9
|
+
- 支持多模板选择
|
|
10
|
+
- 支持交互式与一行命令创建
|
|
11
|
+
- 支持 `--template` / `-t` 指定模板
|
|
12
|
+
- 自动处理 package.json、.gitignore 等
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
### 全局安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g xi-create
|
|
22
|
+
# 或
|
|
23
|
+
pnpm add -g xi-create
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 临时使用(推荐)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx xi-create@latest
|
|
30
|
+
# 或
|
|
31
|
+
pnpm dlx xi-create
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 使用方法
|
|
37
|
+
|
|
38
|
+
### 1. 交互式创建
|
|
39
|
+
|
|
40
|
+
不加参数时进入交互(选择模板、输入项目名):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
xi-create
|
|
44
|
+
# 或
|
|
45
|
+
npx xi-create@latest
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. 一行命令快速创建
|
|
49
|
+
|
|
50
|
+
默认子命令为 `create`,可直接写项目名与模板(模板短名为 `template-` 后的部分,如 `react-ts`):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
xi-create my-app -t react-ts
|
|
54
|
+
# 或
|
|
55
|
+
xi-create my-app --template react-ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
显式写出子命令时等价:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
xi-create create my-app --template react-ts
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. 只指定部分参数
|
|
65
|
+
|
|
66
|
+
只给项目名时会提示选择模板;只给模板时会提示输入项目名(通过 `-t` / `--template`):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
xi-create my-app
|
|
70
|
+
xi-create -t react-ts
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 4. 查看所有可用模板
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
xi-create list
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 关于 `npm create`
|
|
82
|
+
|
|
83
|
+
npm 的 `npm create <name>` 会执行 **`create-<name>`** 这个包(例如 `npm create vite` 对应 `create-vite`)。若要通过 `npm create xi-create` 使用本工具,需要单独发布 **`create-xi-create`** 包并在其中转发到 `xi-create`;当前仓库仅提供 **`xi-create`**,请优先使用 **`npx xi-create`** / **`pnpm dlx xi-create`** 或全局安装后的 **`xi-create`**。
|
|
84
|
+
|
|
85
|
+
使用 `npx` / `pnpm dlx` 时,模板参数直接跟在项目名后面即可,**不要**在项目名与 `--template` 之间插入单独的 `--`。中间的 `--` 会原样传给本工具,Commander 会报 `too many arguments`(与本地用 `node` 执行时相同):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx xi-create@latest my-app --template react-ts
|
|
89
|
+
# 或
|
|
90
|
+
pnpm dlx xi-create my-app -t react-ts
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
node ./bin/xi-create.js my-app --template react-ts
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 扩展模板
|
|
100
|
+
|
|
101
|
+
1. 在 `templates` 目录下添加新的模板文件夹,命名格式为 `template-xxx`,如:
|
|
102
|
+
- `template-react-ts`
|
|
103
|
+
- `template-vue`
|
|
104
|
+
2. 每个模板文件夹下建议包含自己的 `package.json`、`_gitignore` 等。
|
|
105
|
+
3. 工具会自动识别并展示所有模板。
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 开发调试
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pnpm install
|
|
113
|
+
pnpm start
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
ISC
|
package/bin/xi-create.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
const pkgJsonPath = path.join(__dirname, '..', 'package.json');
|
|
16
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
|
17
|
+
|
|
18
|
+
function getTemplates() {
|
|
19
|
+
const templatesDir = path.join(__dirname, '..', 'templates');
|
|
20
|
+
if (!fs.existsSync(templatesDir)) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const templates = fs
|
|
25
|
+
.readdirSync(templatesDir, { withFileTypes: true })
|
|
26
|
+
.filter((dirent) => dirent.isDirectory())
|
|
27
|
+
.map((dirent) => dirent.name)
|
|
28
|
+
.filter((name) => name.startsWith('template-'));
|
|
29
|
+
|
|
30
|
+
return templates.map((name) => ({
|
|
31
|
+
name: name.replace('template-', ''),
|
|
32
|
+
value: name,
|
|
33
|
+
path: path.join(templatesDir, name),
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function copyTemplate(templatePath, targetPath, projectName) {
|
|
38
|
+
const spinner = ora('正在创建项目...').start();
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await fs.copy(templatePath, targetPath);
|
|
42
|
+
|
|
43
|
+
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
44
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
45
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
46
|
+
packageJson.name = projectName;
|
|
47
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const gitignorePath = path.join(targetPath, '_gitignore');
|
|
51
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
52
|
+
await fs.move(gitignorePath, path.join(targetPath, '.gitignore'));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const nodeModulesPath = path.join(targetPath, 'node_modules');
|
|
56
|
+
if (await fs.pathExists(nodeModulesPath)) {
|
|
57
|
+
await fs.remove(nodeModulesPath);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
spinner.succeed('项目创建成功!');
|
|
61
|
+
|
|
62
|
+
console.log(chalk.green('\n🎉 项目已创建完成!'));
|
|
63
|
+
console.log(chalk.cyan(`\n📁 项目路径: ${targetPath}`));
|
|
64
|
+
console.log(chalk.yellow('\n📋 下一步操作:'));
|
|
65
|
+
console.log(chalk.white(` cd ${projectName}`));
|
|
66
|
+
console.log(chalk.white(' pnpm install'));
|
|
67
|
+
console.log(chalk.white(' pnpm dev'));
|
|
68
|
+
} catch (error) {
|
|
69
|
+
spinner.fail('项目创建失败!');
|
|
70
|
+
console.error(chalk.red('错误信息:'), error.message);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function runCreate({ projectName, template } = {}) {
|
|
76
|
+
const templates = getTemplates();
|
|
77
|
+
if (templates.length === 0) {
|
|
78
|
+
console.log(chalk.red('❌ 没有找到可用的模板!'));
|
|
79
|
+
console.log(chalk.yellow('请确保 templates 目录下有模板文件夹。'));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const finalName = projectName?.trim() || undefined;
|
|
84
|
+
const finalTemplate = template?.trim() || undefined;
|
|
85
|
+
|
|
86
|
+
if (finalName && finalTemplate) {
|
|
87
|
+
const tpl = templates.find(
|
|
88
|
+
(t) => t.name === finalTemplate || t.value === finalTemplate
|
|
89
|
+
);
|
|
90
|
+
if (!tpl) {
|
|
91
|
+
console.log(chalk.red(`❌ 未找到模板: ${finalTemplate}`));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
if (fs.existsSync(finalName)) {
|
|
95
|
+
console.log(chalk.red('❌ 项目目录已存在,请选择其他名称!'));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
await copyTemplate(
|
|
99
|
+
tpl.path,
|
|
100
|
+
path.join(process.cwd(), finalName),
|
|
101
|
+
finalName
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const questions = [];
|
|
107
|
+
if (!finalTemplate) {
|
|
108
|
+
questions.push({
|
|
109
|
+
type: 'list',
|
|
110
|
+
name: 'template',
|
|
111
|
+
message: '请选择项目模板:',
|
|
112
|
+
choices: templates.map((t) => ({
|
|
113
|
+
name: `${t.name} (${t.value})`,
|
|
114
|
+
value: t.value,
|
|
115
|
+
})),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (!finalName) {
|
|
119
|
+
questions.push({
|
|
120
|
+
type: 'input',
|
|
121
|
+
name: 'projectName',
|
|
122
|
+
message: '请输入项目名称:',
|
|
123
|
+
validate: (input) => {
|
|
124
|
+
if (!input.trim()) {
|
|
125
|
+
return '项目名称不能为空!';
|
|
126
|
+
}
|
|
127
|
+
if (fs.existsSync(input)) {
|
|
128
|
+
return '项目目录已存在,请选择其他名称!';
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
|
|
137
|
+
const resolvedName = finalName || answers.projectName;
|
|
138
|
+
const resolvedTemplate = finalTemplate || answers.template;
|
|
139
|
+
const tpl = templates.find(
|
|
140
|
+
(t) => t.name === resolvedTemplate || t.value === resolvedTemplate
|
|
141
|
+
);
|
|
142
|
+
if (!tpl) {
|
|
143
|
+
console.log(chalk.red(`❌ 未找到模板: ${resolvedTemplate}`));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
await copyTemplate(
|
|
147
|
+
tpl.path,
|
|
148
|
+
path.join(process.cwd(), resolvedName),
|
|
149
|
+
resolvedName
|
|
150
|
+
);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(chalk.red('创建项目时发生错误:'), error.message);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
program.name('xi-create').description('一个用于创建项目的命令行工具').version(pkg.version);
|
|
158
|
+
|
|
159
|
+
program
|
|
160
|
+
.command('list')
|
|
161
|
+
.description('列出所有可用模板')
|
|
162
|
+
.action(() => {
|
|
163
|
+
const templates = getTemplates();
|
|
164
|
+
if (templates.length === 0) {
|
|
165
|
+
console.log(chalk.yellow('没有找到可用的模板。'));
|
|
166
|
+
} else {
|
|
167
|
+
console.log(chalk.green('可用的模板:'));
|
|
168
|
+
templates.forEach((t) => {
|
|
169
|
+
console.log(chalk.cyan(` - ${t.name}`));
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
program
|
|
175
|
+
.command('create [projectName]', { isDefault: true })
|
|
176
|
+
.description('创建新项目')
|
|
177
|
+
.option('-t, --template <name>', '模板短名(如 react-ts)或完整目录名(如 template-react-ts)')
|
|
178
|
+
.action(async (projectName, options) => {
|
|
179
|
+
await runCreate({ projectName, template: options.template });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (process.argv.length === 2) {
|
|
183
|
+
runCreate({}).catch((err) => {
|
|
184
|
+
console.error(err);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
program.parse();
|
|
189
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xi-create",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "一个用于创建项目的命令行工具",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./bin/xi-create.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"xi-create": "bin/xi-create.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
|
+
"start": "node ./bin/xi-create.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"cli",
|
|
16
|
+
"scaffold",
|
|
17
|
+
"template",
|
|
18
|
+
"create"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/The-End-Hero/xi-create.git"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"registry": "https://registry.npmjs.org/"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"chalk": "^5.6.2",
|
|
32
|
+
"commander": "^14.0.3",
|
|
33
|
+
"fs-extra": "^11.3.4",
|
|
34
|
+
"inquirer": "^13.3.2",
|
|
35
|
+
"ora": "^9.3.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^24.12.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# React + TypeScript + Vite
|
|
2
|
+
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
+
|
|
5
|
+
Currently, two official plugins are available:
|
|
6
|
+
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
9
|
+
|
|
10
|
+
## Expanding the ESLint configuration
|
|
11
|
+
|
|
12
|
+
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
|
13
|
+
|
|
14
|
+
- Configure the top-level `parserOptions` property like this:
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
export default tseslint.config({
|
|
18
|
+
languageOptions: {
|
|
19
|
+
// other options...
|
|
20
|
+
parserOptions: {
|
|
21
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
22
|
+
tsconfigRootDir: import.meta.dirname,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
|
29
|
+
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
|
30
|
+
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
// eslint.config.js
|
|
34
|
+
import react from 'eslint-plugin-react'
|
|
35
|
+
|
|
36
|
+
export default tseslint.config({
|
|
37
|
+
// Set the react version
|
|
38
|
+
settings: { react: { version: '18.3' } },
|
|
39
|
+
plugins: {
|
|
40
|
+
// Add the react plugin
|
|
41
|
+
react,
|
|
42
|
+
},
|
|
43
|
+
rules: {
|
|
44
|
+
// other rules...
|
|
45
|
+
// Enable its recommended rules
|
|
46
|
+
...react.configs.recommended.rules,
|
|
47
|
+
...react.configs['jsx-runtime'].rules,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
dist-ssr
|
|
13
|
+
dist-zip
|
|
14
|
+
*.local
|
|
15
|
+
|
|
16
|
+
# Editor directories and files
|
|
17
|
+
.vscode/*
|
|
18
|
+
!.vscode/extensions.json
|
|
19
|
+
.idea
|
|
20
|
+
.DS_Store
|
|
21
|
+
*.suo
|
|
22
|
+
*.ntvs*
|
|
23
|
+
*.njsproj
|
|
24
|
+
*.sln
|
|
25
|
+
*.sw?
|
|
26
|
+
|
|
27
|
+
.cursor
|
|
28
|
+
.claude
|
|
29
|
+
.gpt_engineer
|
|
30
|
+
.aider
|
|
31
|
+
.codeium
|
|
32
|
+
.tabnine
|
|
33
|
+
.continue
|
|
34
|
+
.v0
|
|
35
|
+
.bolt
|
|
36
|
+
.lovable
|
|
37
|
+
.grain
|
|
38
|
+
.coderabbit
|
|
39
|
+
.replit
|
|
40
|
+
.stackblitz
|
|
41
|
+
.qodeer
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
|
|
7
|
+
export default tseslint.config(
|
|
8
|
+
{ ignores: ['dist'] },
|
|
9
|
+
{
|
|
10
|
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
languageOptions: {
|
|
13
|
+
ecmaVersion: 2020,
|
|
14
|
+
globals: globals.browser,
|
|
15
|
+
},
|
|
16
|
+
plugins: {
|
|
17
|
+
'react-hooks': reactHooks,
|
|
18
|
+
'react-refresh': reactRefresh,
|
|
19
|
+
},
|
|
20
|
+
rules: {
|
|
21
|
+
...reactHooks.configs.recommended.rules,
|
|
22
|
+
'react-refresh/only-export-components': [
|
|
23
|
+
'warn',
|
|
24
|
+
{ allowConstantExport: true },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Vite + React + TS</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-react-typescript-starter",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "eslint ",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"preinstall": "npx only-allow pnpm"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
15
|
+
"@xiping/react-components": "^1.0.70",
|
|
16
|
+
"@zumer/snapdom": "^2.7.0",
|
|
17
|
+
"ahooks": "^3.9.7",
|
|
18
|
+
"axios": "^1.14.0",
|
|
19
|
+
"clsx": "^2.1.1",
|
|
20
|
+
"dayjs": "^1.11.20",
|
|
21
|
+
"disable-devtool": "^0.3.9",
|
|
22
|
+
"file-saver": "^2.0.5",
|
|
23
|
+
"lodash-es": "^4.18.1",
|
|
24
|
+
"lucide-react": "^1.7.0",
|
|
25
|
+
"motion": "^12.38.0",
|
|
26
|
+
"react": "^19.2.4",
|
|
27
|
+
"react-device-detect": "^2.2.3",
|
|
28
|
+
"react-dom": "^19.2.4",
|
|
29
|
+
"react-dropzone": "^15.0.0",
|
|
30
|
+
"react-hot-toast": "^2.6.0",
|
|
31
|
+
"react-router-dom": "^7.13.2",
|
|
32
|
+
"zustand": "^5.0.12"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@eslint/js": "^9.39.4",
|
|
36
|
+
"@types/file-saver": "^2.0.7",
|
|
37
|
+
"@types/lodash-es": "^4.17.12",
|
|
38
|
+
"@types/node": "^24.12.0",
|
|
39
|
+
"@types/react": "^19.2.14",
|
|
40
|
+
"@types/react-dom": "^19.2.3",
|
|
41
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
42
|
+
"@xiping/vite-version": "^1.0.71",
|
|
43
|
+
"autoprefixer": "^10.4.27",
|
|
44
|
+
"eslint": "^9.39.4",
|
|
45
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
46
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
47
|
+
"globals": "^17.4.0",
|
|
48
|
+
"less": "^4.6.4",
|
|
49
|
+
"only-allow": "^1.2.2",
|
|
50
|
+
"postcss": "^8.5.8",
|
|
51
|
+
"prettier": "^3.8.1",
|
|
52
|
+
"sharp": "^0.34.5",
|
|
53
|
+
"svgo": "^4.0.1",
|
|
54
|
+
"tailwind-scrollbar": "^4.0.2",
|
|
55
|
+
"tailwindcss": "^4.2.2",
|
|
56
|
+
"tailwindcss-safe-area": "^1.3.0",
|
|
57
|
+
"typescript": "~5.9.3",
|
|
58
|
+
"typescript-eslint": "^8.58.0",
|
|
59
|
+
"vite": "^8.0.3",
|
|
60
|
+
"vite-plugin-image-optimizer": "^2.0.3",
|
|
61
|
+
"vite-plugin-pwa": "^1.2.0",
|
|
62
|
+
"vite-plugin-zip-pack": "^1.2.4"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=22"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { HashRouter, Route, Routes, useLocation } from "react-router-dom";
|
|
2
|
+
import Home from "@/pages/home/Home.tsx";
|
|
3
|
+
import "./App.css";
|
|
4
|
+
import Login from "@/pages/login/Login.tsx";
|
|
5
|
+
import { AnimatePresence } from "motion/react";
|
|
6
|
+
|
|
7
|
+
function App() {
|
|
8
|
+
return (
|
|
9
|
+
<HashRouter>
|
|
10
|
+
<AppContent />
|
|
11
|
+
</HashRouter>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Separate component to use hooks inside HashRouter
|
|
16
|
+
function AppContent() {
|
|
17
|
+
const location = useLocation();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<AnimatePresence mode="wait">
|
|
21
|
+
<Routes location={location} key={location.pathname}>
|
|
22
|
+
<Route path="/" element={<Home />} />
|
|
23
|
+
<Route path="/login" element={<Login />} />
|
|
24
|
+
</Routes>
|
|
25
|
+
</AnimatePresence>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default App;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import "./index.css";
|
|
4
|
+
import App from "./App.tsx";
|
|
5
|
+
|
|
6
|
+
const root = createRoot(document.getElementById("root")!);
|
|
7
|
+
root.render(
|
|
8
|
+
// <StrictMode>
|
|
9
|
+
<App />,
|
|
10
|
+
// </StrictMode>,
|
|
11
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Link } from "react-router-dom";
|
|
2
|
+
import { motion } from "motion/react";
|
|
3
|
+
|
|
4
|
+
const Home = () => {
|
|
5
|
+
return (
|
|
6
|
+
<motion.div
|
|
7
|
+
className="w-screen h-screen bg-blue-600 flex flex-col items-center justify-center text-white"
|
|
8
|
+
initial={{ opacity: 0, filter: "blur(8px)" }}
|
|
9
|
+
animate={{ opacity: 1, filter: "blur(0px)" }}
|
|
10
|
+
exit={{ opacity: 0, filter: "blur(8px)" }}
|
|
11
|
+
transition={{ duration: 0.3 }}
|
|
12
|
+
>
|
|
13
|
+
<h1 className="text-4xl font-bold mb-4">Home Page</h1>
|
|
14
|
+
<Link
|
|
15
|
+
to="/login"
|
|
16
|
+
className="px-4 py-2 bg-white text-blue-600 rounded-md hover:bg-gray-100 transition-colors"
|
|
17
|
+
>
|
|
18
|
+
Go to Login
|
|
19
|
+
</Link>
|
|
20
|
+
</motion.div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default Home;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Link } from "react-router-dom";
|
|
2
|
+
import { motion } from "motion/react";
|
|
3
|
+
|
|
4
|
+
const Login = () => {
|
|
5
|
+
return (
|
|
6
|
+
<motion.div
|
|
7
|
+
className="w-screen h-screen bg-amber-400 flex flex-col items-center justify-center text-white"
|
|
8
|
+
initial={{ opacity: 0, filter: "blur(8px)" }}
|
|
9
|
+
animate={{ opacity: 1, filter: "blur(0px)" }}
|
|
10
|
+
exit={{ opacity: 0, filter: "blur(8px)" }}
|
|
11
|
+
transition={{ duration: 0.3 }}
|
|
12
|
+
>
|
|
13
|
+
<h1 className="text-4xl font-bold mb-4">Login Page</h1>
|
|
14
|
+
<Link
|
|
15
|
+
to="/"
|
|
16
|
+
className="px-4 py-2 bg-white text-amber-700 rounded-md hover:bg-gray-100 transition-colors"
|
|
17
|
+
>
|
|
18
|
+
Back to Home
|
|
19
|
+
</Link>
|
|
20
|
+
</motion.div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default Login;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "Bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"jsx": "react-jsx",
|
|
17
|
+
|
|
18
|
+
/* Linting */
|
|
19
|
+
"strict": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true,
|
|
24
|
+
|
|
25
|
+
// alias
|
|
26
|
+
"baseUrl": "./",
|
|
27
|
+
"paths": {
|
|
28
|
+
"@/*": ["./src/*"]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"include": ["src"]
|
|
32
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2023",
|
|
7
|
+
],
|
|
8
|
+
"module": "ESNext",
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "Bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
|
|
18
|
+
/* Linting */
|
|
19
|
+
"strict": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"vite.config.ts"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import zipPack from "vite-plugin-zip-pack";
|
|
4
|
+
import { ViteImageOptimizer } from "vite-plugin-image-optimizer";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { viteVersionPlugin } from "@xiping/vite-version";
|
|
7
|
+
import packageJSON from "./package.json";
|
|
8
|
+
import { VitePWA } from "vite-plugin-pwa";
|
|
9
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
10
|
+
|
|
11
|
+
// https://vite.dev/config/
|
|
12
|
+
export default defineConfig(({ mode }) => {
|
|
13
|
+
console.log(mode);
|
|
14
|
+
return {
|
|
15
|
+
plugins: [
|
|
16
|
+
tailwindcss(),
|
|
17
|
+
react(),
|
|
18
|
+
VitePWA({
|
|
19
|
+
registerType: "autoUpdate",
|
|
20
|
+
devOptions: { enabled: false },
|
|
21
|
+
workbox: {
|
|
22
|
+
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 10MB
|
|
23
|
+
clientsClaim: true,
|
|
24
|
+
skipWaiting: true,
|
|
25
|
+
runtimeCaching: [
|
|
26
|
+
{
|
|
27
|
+
urlPattern: /^https:\/\/.*\/.*/i,
|
|
28
|
+
handler: "NetworkFirst",
|
|
29
|
+
options: {
|
|
30
|
+
cacheName: "api-cache",
|
|
31
|
+
networkTimeoutSeconds: 10,
|
|
32
|
+
expiration: {
|
|
33
|
+
maxEntries: 100,
|
|
34
|
+
maxAgeSeconds: 24 * 60 * 60, // 24 hours
|
|
35
|
+
},
|
|
36
|
+
cacheableResponse: {
|
|
37
|
+
statuses: [0, 200],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
|
|
43
|
+
handler: "CacheFirst",
|
|
44
|
+
options: {
|
|
45
|
+
cacheName: "images",
|
|
46
|
+
expiration: {
|
|
47
|
+
maxEntries: 60,
|
|
48
|
+
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
manifest: {
|
|
55
|
+
name: packageJSON.name,
|
|
56
|
+
short_name: packageJSON.name,
|
|
57
|
+
theme_color: "#ffffff",
|
|
58
|
+
icons: [
|
|
59
|
+
{
|
|
60
|
+
src: "pwa-192x192.png",
|
|
61
|
+
sizes: "192x192",
|
|
62
|
+
type: "image/png",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
src: "pwa-512x512.png",
|
|
66
|
+
sizes: "512x512",
|
|
67
|
+
type: "image/png",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
ViteImageOptimizer({
|
|
73
|
+
/* pass your config */
|
|
74
|
+
png: {
|
|
75
|
+
quality: 80,
|
|
76
|
+
},
|
|
77
|
+
jpg: {
|
|
78
|
+
quality: 80,
|
|
79
|
+
},
|
|
80
|
+
jpeg: {
|
|
81
|
+
quality: 80,
|
|
82
|
+
},
|
|
83
|
+
webp: {
|
|
84
|
+
quality: 80,
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
viteVersionPlugin({ filename: "version.txt" }),
|
|
88
|
+
zipPack({
|
|
89
|
+
outDir: "dist-zip",
|
|
90
|
+
outFileName: `${packageJSON.name}-${mode}.zip`,
|
|
91
|
+
}),
|
|
92
|
+
],
|
|
93
|
+
resolve: {
|
|
94
|
+
alias: {
|
|
95
|
+
"@": path.resolve(__dirname, "src"),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
});
|