wv-cli 0.1.0__tar.gz
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.
- wv_cli-0.1.0/PKG-INFO +163 -0
- wv_cli-0.1.0/README.md +154 -0
- wv_cli-0.1.0/pyproject.toml +24 -0
- wv_cli-0.1.0/setup.cfg +4 -0
- wv_cli-0.1.0/wv_cli/__init__.py +0 -0
- wv_cli-0.1.0/wv_cli/commands/__init__.py +0 -0
- wv_cli-0.1.0/wv_cli/commands/build.py +113 -0
- wv_cli-0.1.0/wv_cli/commands/create.py +178 -0
- wv_cli-0.1.0/wv_cli/commands/run.py +39 -0
- wv_cli-0.1.0/wv_cli/icon/favicon.ico +0 -0
- wv_cli-0.1.0/wv_cli/icon/logo.png +0 -0
- wv_cli-0.1.0/wv_cli/main.py +17 -0
- wv_cli-0.1.0/wv_cli/templates.py +212 -0
- wv_cli-0.1.0/wv_cli/utils.py +181 -0
- wv_cli-0.1.0/wv_cli.egg-info/PKG-INFO +163 -0
- wv_cli-0.1.0/wv_cli.egg-info/SOURCES.txt +18 -0
- wv_cli-0.1.0/wv_cli.egg-info/dependency_links.txt +1 -0
- wv_cli-0.1.0/wv_cli.egg-info/entry_points.txt +2 -0
- wv_cli-0.1.0/wv_cli.egg-info/requires.txt +2 -0
- wv_cli-0.1.0/wv_cli.egg-info/top_level.txt +1 -0
wv_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wv-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI scaffold tool for pywebview + Vue3 desktop apps
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: click>=8.0
|
|
8
|
+
Requires-Dist: toml>=0.10
|
|
9
|
+
|
|
10
|
+
# wv-cli
|
|
11
|
+
|
|
12
|
+
A command-line scaffold tool for building **pywebview (Python backend) + Vue 3 (frontend)** desktop apps.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Python ≥ 3.9
|
|
17
|
+
- [uv](https://docs.astral.sh/uv/) — Python package manager
|
|
18
|
+
- [Node.js / npm](https://nodejs.org) — for the Vue 3 frontend
|
|
19
|
+
- [Inno Setup 6](https://jrsoftware.org/isdl.php) *(Windows only, required for `--publish`)*
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install wv-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install from source with `uv`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/yourname/wv-cli
|
|
31
|
+
cd wv-cli
|
|
32
|
+
uv pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### Create a new project
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Interactive — creates ./my-app/
|
|
41
|
+
wv create
|
|
42
|
+
|
|
43
|
+
# In the current directory
|
|
44
|
+
wv create .
|
|
45
|
+
|
|
46
|
+
# Explicit directory
|
|
47
|
+
wv create path/to/my-app
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
You will be prompted for:
|
|
51
|
+
| Prompt | Default |
|
|
52
|
+
|---|---|
|
|
53
|
+
| 项目名称 (project name) | directory name |
|
|
54
|
+
| 窗口标题 (window title) | project name |
|
|
55
|
+
| 版本号 (version) | `1.0.0` |
|
|
56
|
+
| 作者 (author) | *(empty)* |
|
|
57
|
+
|
|
58
|
+
After answering, the CLI will:
|
|
59
|
+
1. Scaffold the full directory structure
|
|
60
|
+
2. Run `npm create vue@latest` for the frontend (you drive the Vue prompts)
|
|
61
|
+
3. Run `uv init / venv / add pywebview pyinstaller` for the backend
|
|
62
|
+
|
|
63
|
+
### Run in development mode
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd my-app
|
|
67
|
+
wv run
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Builds the Vue frontend, then launches the pywebview window loading `frontend/dist`.
|
|
71
|
+
|
|
72
|
+
### Production build
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
wv build
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Builds the frontend and runs PyInstaller to produce `build/dist/<project-name>/`.
|
|
79
|
+
|
|
80
|
+
### Build + Windows installer
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
wv build --publish
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Runs the full build, then calls Inno Setup to produce
|
|
87
|
+
`build/publish/<project-name>-<version>-setup.exe`.
|
|
88
|
+
|
|
89
|
+
Configure the Inno Setup path in `wv.toml` if needed:
|
|
90
|
+
|
|
91
|
+
```toml
|
|
92
|
+
[build]
|
|
93
|
+
inno_setup_path = "C:/Program Files (x86)/Inno Setup 6/ISCC.exe"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Generated Project Structure
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
my-app/
|
|
100
|
+
├── icon/
|
|
101
|
+
│ ├── favicon.ico
|
|
102
|
+
│ └── logo.png
|
|
103
|
+
├── frontend/ ← Vue 3 (npm create vue@latest)
|
|
104
|
+
│ └── dist/ ← built by wv run / wv build
|
|
105
|
+
├── backend/
|
|
106
|
+
│ ├── .venv/ ← uv virtual environment
|
|
107
|
+
│ └── src/
|
|
108
|
+
│ ├── main.py
|
|
109
|
+
│ ├── config.py
|
|
110
|
+
│ └── bridge/
|
|
111
|
+
│ ├── __init__.py
|
|
112
|
+
│ └── api.py
|
|
113
|
+
├── build/
|
|
114
|
+
│ ├── my-app.spec ← PyInstaller config
|
|
115
|
+
│ ├── my-app.iss ← Inno Setup config
|
|
116
|
+
│ └── publish/ ← installer output
|
|
117
|
+
└── wv.toml
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Frontend Router Auto-Fix
|
|
121
|
+
|
|
122
|
+
`wv run` and `wv build` automatically replace `createWebHistory` with
|
|
123
|
+
`createWebHashHistory` in `frontend/src/router/index.{ts,js}` before building.
|
|
124
|
+
This ensures the app works correctly when loaded via the `file://` protocol
|
|
125
|
+
after packaging. The replacement is **idempotent** — running it multiple times
|
|
126
|
+
has no side effects.
|
|
127
|
+
|
|
128
|
+
## `wv.toml` Reference
|
|
129
|
+
|
|
130
|
+
```toml
|
|
131
|
+
[project]
|
|
132
|
+
name = "my-app"
|
|
133
|
+
version = "1.0.0"
|
|
134
|
+
window_title = "My App"
|
|
135
|
+
author = ""
|
|
136
|
+
|
|
137
|
+
[build]
|
|
138
|
+
inno_setup_path = "C:/Program Files (x86)/Inno Setup 6/ISCC.exe"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Extending the JS Bridge
|
|
142
|
+
|
|
143
|
+
Edit `backend/src/bridge/api.py`:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
class Api:
|
|
147
|
+
def greet(self, name: str) -> str:
|
|
148
|
+
return f"Hello, {name}!"
|
|
149
|
+
|
|
150
|
+
def read_file(self, path: str) -> str:
|
|
151
|
+
with open(path) as f:
|
|
152
|
+
return f.read()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Call from Vue:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
const result = await window.pywebview.api.greet('World')
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|
wv_cli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# wv-cli
|
|
2
|
+
|
|
3
|
+
A command-line scaffold tool for building **pywebview (Python backend) + Vue 3 (frontend)** desktop apps.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Python ≥ 3.9
|
|
8
|
+
- [uv](https://docs.astral.sh/uv/) — Python package manager
|
|
9
|
+
- [Node.js / npm](https://nodejs.org) — for the Vue 3 frontend
|
|
10
|
+
- [Inno Setup 6](https://jrsoftware.org/isdl.php) *(Windows only, required for `--publish`)*
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install wv-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or install from source with `uv`:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git clone https://github.com/yourname/wv-cli
|
|
22
|
+
cd wv-cli
|
|
23
|
+
uv pip install -e .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### Create a new project
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Interactive — creates ./my-app/
|
|
32
|
+
wv create
|
|
33
|
+
|
|
34
|
+
# In the current directory
|
|
35
|
+
wv create .
|
|
36
|
+
|
|
37
|
+
# Explicit directory
|
|
38
|
+
wv create path/to/my-app
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
You will be prompted for:
|
|
42
|
+
| Prompt | Default |
|
|
43
|
+
|---|---|
|
|
44
|
+
| 项目名称 (project name) | directory name |
|
|
45
|
+
| 窗口标题 (window title) | project name |
|
|
46
|
+
| 版本号 (version) | `1.0.0` |
|
|
47
|
+
| 作者 (author) | *(empty)* |
|
|
48
|
+
|
|
49
|
+
After answering, the CLI will:
|
|
50
|
+
1. Scaffold the full directory structure
|
|
51
|
+
2. Run `npm create vue@latest` for the frontend (you drive the Vue prompts)
|
|
52
|
+
3. Run `uv init / venv / add pywebview pyinstaller` for the backend
|
|
53
|
+
|
|
54
|
+
### Run in development mode
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cd my-app
|
|
58
|
+
wv run
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Builds the Vue frontend, then launches the pywebview window loading `frontend/dist`.
|
|
62
|
+
|
|
63
|
+
### Production build
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
wv build
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Builds the frontend and runs PyInstaller to produce `build/dist/<project-name>/`.
|
|
70
|
+
|
|
71
|
+
### Build + Windows installer
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
wv build --publish
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Runs the full build, then calls Inno Setup to produce
|
|
78
|
+
`build/publish/<project-name>-<version>-setup.exe`.
|
|
79
|
+
|
|
80
|
+
Configure the Inno Setup path in `wv.toml` if needed:
|
|
81
|
+
|
|
82
|
+
```toml
|
|
83
|
+
[build]
|
|
84
|
+
inno_setup_path = "C:/Program Files (x86)/Inno Setup 6/ISCC.exe"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Generated Project Structure
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
my-app/
|
|
91
|
+
├── icon/
|
|
92
|
+
│ ├── favicon.ico
|
|
93
|
+
│ └── logo.png
|
|
94
|
+
├── frontend/ ← Vue 3 (npm create vue@latest)
|
|
95
|
+
│ └── dist/ ← built by wv run / wv build
|
|
96
|
+
├── backend/
|
|
97
|
+
│ ├── .venv/ ← uv virtual environment
|
|
98
|
+
│ └── src/
|
|
99
|
+
│ ├── main.py
|
|
100
|
+
│ ├── config.py
|
|
101
|
+
│ └── bridge/
|
|
102
|
+
│ ├── __init__.py
|
|
103
|
+
│ └── api.py
|
|
104
|
+
├── build/
|
|
105
|
+
│ ├── my-app.spec ← PyInstaller config
|
|
106
|
+
│ ├── my-app.iss ← Inno Setup config
|
|
107
|
+
│ └── publish/ ← installer output
|
|
108
|
+
└── wv.toml
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Frontend Router Auto-Fix
|
|
112
|
+
|
|
113
|
+
`wv run` and `wv build` automatically replace `createWebHistory` with
|
|
114
|
+
`createWebHashHistory` in `frontend/src/router/index.{ts,js}` before building.
|
|
115
|
+
This ensures the app works correctly when loaded via the `file://` protocol
|
|
116
|
+
after packaging. The replacement is **idempotent** — running it multiple times
|
|
117
|
+
has no side effects.
|
|
118
|
+
|
|
119
|
+
## `wv.toml` Reference
|
|
120
|
+
|
|
121
|
+
```toml
|
|
122
|
+
[project]
|
|
123
|
+
name = "my-app"
|
|
124
|
+
version = "1.0.0"
|
|
125
|
+
window_title = "My App"
|
|
126
|
+
author = ""
|
|
127
|
+
|
|
128
|
+
[build]
|
|
129
|
+
inno_setup_path = "C:/Program Files (x86)/Inno Setup 6/ISCC.exe"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Extending the JS Bridge
|
|
133
|
+
|
|
134
|
+
Edit `backend/src/bridge/api.py`:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
class Api:
|
|
138
|
+
def greet(self, name: str) -> str:
|
|
139
|
+
return f"Hello, {name}!"
|
|
140
|
+
|
|
141
|
+
def read_file(self, path: str) -> str:
|
|
142
|
+
with open(path) as f:
|
|
143
|
+
return f.read()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Call from Vue:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
const result = await window.pywebview.api.greet('World')
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wv-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CLI scaffold tool for pywebview + Vue3 desktop apps"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"click>=8.0",
|
|
13
|
+
"toml>=0.10",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
wv = "wv_cli.main:cli"
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["."]
|
|
21
|
+
include = ["wv_cli*"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.package-data]
|
|
24
|
+
wv_cli = ["icon/*.ico", "icon/*.png"]
|
wv_cli-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""wv build — production build with optional Windows installer packaging."""
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from ..utils import find_project_root, load_config, run_cmd, fix_router_history, ensure_npm_deps, inject_favicon
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command('build')
|
|
12
|
+
@click.option(
|
|
13
|
+
'--publish',
|
|
14
|
+
is_flag=True,
|
|
15
|
+
default=False,
|
|
16
|
+
help='Also create a Windows installer with Inno Setup after building.',
|
|
17
|
+
)
|
|
18
|
+
def build(publish: bool):
|
|
19
|
+
"""Build the project for production.
|
|
20
|
+
|
|
21
|
+
Use --publish to additionally generate a Windows installer via Inno Setup.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
project_root = find_project_root()
|
|
25
|
+
config = load_config(project_root)
|
|
26
|
+
|
|
27
|
+
project_name = config['project']['name']
|
|
28
|
+
version = config['project']['version']
|
|
29
|
+
|
|
30
|
+
click.echo(f'🏗 生产构建:{project_name} v{version}')
|
|
31
|
+
|
|
32
|
+
# 1. Fix Vue Router history mode
|
|
33
|
+
fix_router_history(project_root)
|
|
34
|
+
|
|
35
|
+
# 2. Build frontend
|
|
36
|
+
frontend_dir = os.path.join(project_root, 'frontend')
|
|
37
|
+
click.echo('\n📦 构建前端…')
|
|
38
|
+
ensure_npm_deps(frontend_dir)
|
|
39
|
+
run_cmd(['npm', 'run', 'build'], cwd=frontend_dir)
|
|
40
|
+
inject_favicon(project_root)
|
|
41
|
+
|
|
42
|
+
# Verify frontend/dist
|
|
43
|
+
dist_dir = os.path.join(frontend_dir, 'dist')
|
|
44
|
+
if not os.path.isdir(dist_dir):
|
|
45
|
+
raise click.ClickException(
|
|
46
|
+
"frontend/dist 不存在。\n"
|
|
47
|
+
"请检查 npm run build 是否成功执行,然后重试。"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# 3. Run PyInstaller
|
|
51
|
+
build_dir = os.path.join(project_root, 'build')
|
|
52
|
+
spec_file = os.path.join(build_dir, f'{project_name}.spec')
|
|
53
|
+
|
|
54
|
+
if not os.path.isfile(spec_file):
|
|
55
|
+
raise click.ClickException(
|
|
56
|
+
f"未找到 spec 文件:{spec_file}\n"
|
|
57
|
+
"请确认当前目录是 wv 项目根目录,且 build/ 目录完整。"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
click.echo('\n📦 PyInstaller 打包…')
|
|
61
|
+
backend_dir = os.path.join(project_root, 'backend')
|
|
62
|
+
run_cmd(
|
|
63
|
+
['uv', 'run', 'pyinstaller', spec_file, '--distpath', os.path.join(build_dir, 'dist')],
|
|
64
|
+
cwd=backend_dir,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
click.echo(f'\n✔ 构建完成:build/dist/{project_name}/')
|
|
68
|
+
|
|
69
|
+
# 4. Optional: Inno Setup packaging (Windows only)
|
|
70
|
+
if publish:
|
|
71
|
+
_publish_installer(project_root, config, project_name, version, build_dir)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _publish_installer(
|
|
75
|
+
project_root: str,
|
|
76
|
+
config: dict,
|
|
77
|
+
project_name: str,
|
|
78
|
+
version: str,
|
|
79
|
+
build_dir: str,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Generate a Windows installer using Inno Setup."""
|
|
82
|
+
|
|
83
|
+
if platform.system() != 'Windows':
|
|
84
|
+
click.echo(
|
|
85
|
+
'\n⚠ --publish 仅支持 Windows 平台(需要 Inno Setup),已跳过安装包生成。'
|
|
86
|
+
)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
inno_path = config.get('build', {}).get(
|
|
90
|
+
'inno_setup_path',
|
|
91
|
+
'C:/Program Files (x86)/Inno Setup 6/ISCC.exe',
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if not os.path.isfile(inno_path):
|
|
95
|
+
raise click.ClickException(
|
|
96
|
+
f"未找到 Inno Setup:{inno_path}\n"
|
|
97
|
+
"请安装 Inno Setup 后在 wv.toml 中配置正确的 inno_setup_path。\n"
|
|
98
|
+
"下载:https://jrsoftware.org/isdl.php"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
iss_file = os.path.join(build_dir, f'{project_name}.iss')
|
|
102
|
+
if not os.path.isfile(iss_file):
|
|
103
|
+
raise click.ClickException(
|
|
104
|
+
f"未找到 iss 文件:{iss_file}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
click.echo('\n📦 Inno Setup 打包安装程序…')
|
|
108
|
+
run_cmd([inno_path, iss_file])
|
|
109
|
+
|
|
110
|
+
installer = os.path.join(
|
|
111
|
+
build_dir, 'publish', f'{project_name}-{version}-setup.exe'
|
|
112
|
+
)
|
|
113
|
+
click.echo(f'\n✔ 安装包已生成:{installer}')
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""wv create — scaffold a new pywebview + Vue3 project."""
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import toml
|
|
7
|
+
|
|
8
|
+
from ..utils import require_node, require_uv, run_cmd
|
|
9
|
+
from ..templates import (
|
|
10
|
+
WV_TOML,
|
|
11
|
+
CONFIG_PY,
|
|
12
|
+
MAIN_PY,
|
|
13
|
+
BRIDGE_INIT_PY,
|
|
14
|
+
BRIDGE_API_PY,
|
|
15
|
+
SPEC_FILE,
|
|
16
|
+
ISS_FILE,
|
|
17
|
+
ROOT_GITIGNORE,
|
|
18
|
+
BACKEND_GITIGNORE,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# 包内默认图标目录:wv_cli/icon/
|
|
22
|
+
_PKG_ICON_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'icon')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Directory / file creation helpers
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def _makedirs(path: str) -> None:
|
|
30
|
+
os.makedirs(path, exist_ok=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _write_text(path: str, content: str, overwrite: bool = False) -> None:
|
|
34
|
+
"""Write text to path; skip if file already exists and overwrite is False."""
|
|
35
|
+
if os.path.exists(path) and not overwrite:
|
|
36
|
+
return
|
|
37
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
38
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
39
|
+
f.write(content)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _copy_default_icons(project_dir: str) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Copy default favicon.ico and logo.png from the wv-cli package's icon/
|
|
45
|
+
directory into the project's icon/ directory.
|
|
46
|
+
Skips files that already exist (safe for `wv create .`).
|
|
47
|
+
"""
|
|
48
|
+
for filename in ('favicon.ico', 'logo.png'):
|
|
49
|
+
src = os.path.join(_PKG_ICON_DIR, filename)
|
|
50
|
+
dst = os.path.join(project_dir, 'icon', filename)
|
|
51
|
+
if os.path.exists(dst):
|
|
52
|
+
continue
|
|
53
|
+
if os.path.isfile(src):
|
|
54
|
+
shutil.copy2(src, dst)
|
|
55
|
+
else:
|
|
56
|
+
click.echo(f' ⚠ 未找到默认图标:{src},跳过复制')
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _scaffold_directories(project_dir: str) -> None:
|
|
60
|
+
dirs = [
|
|
61
|
+
'icon',
|
|
62
|
+
'frontend',
|
|
63
|
+
'backend/src/bridge',
|
|
64
|
+
'backend/tests',
|
|
65
|
+
'build/publish',
|
|
66
|
+
]
|
|
67
|
+
for d in dirs:
|
|
68
|
+
_makedirs(os.path.join(project_dir, d))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _scaffold_files(
|
|
72
|
+
project_dir: str,
|
|
73
|
+
project_name: str,
|
|
74
|
+
version: str,
|
|
75
|
+
window_title: str,
|
|
76
|
+
author: str,
|
|
77
|
+
) -> None:
|
|
78
|
+
ctx = dict(
|
|
79
|
+
project_name=project_name,
|
|
80
|
+
version=version,
|
|
81
|
+
window_title=window_title,
|
|
82
|
+
author=author,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
_write_text(os.path.join(project_dir, 'wv.toml'), WV_TOML.format(**ctx))
|
|
86
|
+
|
|
87
|
+
_write_text(
|
|
88
|
+
os.path.join(project_dir, 'backend', 'src', 'config.py'),
|
|
89
|
+
CONFIG_PY.format(**ctx),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
_write_text(os.path.join(project_dir, 'backend', 'src', 'main.py'), MAIN_PY)
|
|
93
|
+
|
|
94
|
+
_write_text(
|
|
95
|
+
os.path.join(project_dir, 'backend', 'src', 'bridge', '__init__.py'),
|
|
96
|
+
BRIDGE_INIT_PY,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
_write_text(
|
|
100
|
+
os.path.join(project_dir, 'backend', 'src', 'bridge', 'api.py'),
|
|
101
|
+
BRIDGE_API_PY,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
_write_text(
|
|
105
|
+
os.path.join(project_dir, 'build', f'{project_name}.spec'),
|
|
106
|
+
SPEC_FILE.format(**ctx),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
_write_text(
|
|
110
|
+
os.path.join(project_dir, 'build', f'{project_name}.iss'),
|
|
111
|
+
ISS_FILE.format(**ctx),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# 项目根目录 .gitignore
|
|
115
|
+
_write_text(os.path.join(project_dir, '.gitignore'), ROOT_GITIGNORE)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# wv create command
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
@click.command('create')
|
|
123
|
+
@click.argument('directory', required=False, default=None)
|
|
124
|
+
def create(directory):
|
|
125
|
+
"""Create a new pywebview + Vue3 desktop app project."""
|
|
126
|
+
|
|
127
|
+
cwd = os.path.abspath(os.getcwd())
|
|
128
|
+
|
|
129
|
+
if directory is None:
|
|
130
|
+
project_name = click.prompt('项目名称', default='my-app')
|
|
131
|
+
project_dir = os.path.join(cwd, project_name)
|
|
132
|
+
elif directory == '.':
|
|
133
|
+
default_name = os.path.basename(cwd)
|
|
134
|
+
project_name = click.prompt('项目名称', default=default_name)
|
|
135
|
+
project_dir = cwd
|
|
136
|
+
else:
|
|
137
|
+
project_dir = os.path.abspath(directory)
|
|
138
|
+
project_name = click.prompt('项目名称', default=os.path.basename(project_dir))
|
|
139
|
+
|
|
140
|
+
window_title = click.prompt('窗口标题', default=project_name)
|
|
141
|
+
version = click.prompt('版本号', default='1.0.0')
|
|
142
|
+
author = click.prompt('作者', default='')
|
|
143
|
+
|
|
144
|
+
click.echo('\n🔍 检查运行环境…')
|
|
145
|
+
require_node()
|
|
146
|
+
click.echo(' ✔ Node.js / npm')
|
|
147
|
+
require_uv()
|
|
148
|
+
click.echo(' ✔ uv')
|
|
149
|
+
|
|
150
|
+
click.echo('\n📁 创建项目结构…')
|
|
151
|
+
_scaffold_directories(project_dir)
|
|
152
|
+
_scaffold_files(project_dir, project_name, version, window_title, author)
|
|
153
|
+
_copy_default_icons(project_dir)
|
|
154
|
+
click.echo(' ✔ 目录与文件已生成')
|
|
155
|
+
|
|
156
|
+
click.echo('\n🖼 初始化前端(npm create vue@latest)…')
|
|
157
|
+
frontend_dir = os.path.join(project_dir, 'frontend')
|
|
158
|
+
_makedirs(frontend_dir)
|
|
159
|
+
run_cmd(['npm', 'create', 'vue@latest', '.'], cwd=frontend_dir)
|
|
160
|
+
|
|
161
|
+
click.echo('\n🐍 初始化后端(uv)…')
|
|
162
|
+
backend_dir = os.path.join(project_dir, 'backend')
|
|
163
|
+
run_cmd(['uv', 'init', '--no-workspace', '--vcs', 'none'], cwd=backend_dir)
|
|
164
|
+
run_cmd(['uv', 'venv'], cwd=backend_dir)
|
|
165
|
+
run_cmd(['uv', 'add', 'pywebview', 'pyinstaller'], cwd=backend_dir)
|
|
166
|
+
|
|
167
|
+
_write_text(os.path.join(backend_dir, '.gitignore'), BACKEND_GITIGNORE)
|
|
168
|
+
|
|
169
|
+
rel = os.path.relpath(project_dir, cwd)
|
|
170
|
+
cd_hint = f'\n cd {rel}' if rel != '.' else ''
|
|
171
|
+
|
|
172
|
+
click.echo(f"""
|
|
173
|
+
✔ 项目创建完成!{cd_hint}
|
|
174
|
+
|
|
175
|
+
下一步:
|
|
176
|
+
wv run # 开发模式运行(构建前端 + 启动 pywebview)
|
|
177
|
+
wv build # 生产构建(PyInstaller 打包)
|
|
178
|
+
""")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""wv run — development mode: build frontend then launch pywebview."""
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from ..utils import find_project_root, load_config, run_cmd, fix_router_history, ensure_npm_deps, inject_favicon
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command('run')
|
|
10
|
+
def run():
|
|
11
|
+
"""Run the app in development mode (builds frontend, then starts pywebview)."""
|
|
12
|
+
|
|
13
|
+
project_root = find_project_root()
|
|
14
|
+
config = load_config(project_root)
|
|
15
|
+
|
|
16
|
+
click.echo('🔧 开发模式启动…')
|
|
17
|
+
|
|
18
|
+
# 1. Fix Vue Router history mode for file:// compatibility
|
|
19
|
+
fix_router_history(project_root)
|
|
20
|
+
|
|
21
|
+
# 2. Build frontend
|
|
22
|
+
frontend_dir = os.path.join(project_root, 'frontend')
|
|
23
|
+
click.echo('\n📦 构建前端…')
|
|
24
|
+
ensure_npm_deps(frontend_dir)
|
|
25
|
+
run_cmd(['npm', 'run', 'build'], cwd=frontend_dir)
|
|
26
|
+
inject_favicon(project_root)
|
|
27
|
+
|
|
28
|
+
# 3. Verify frontend/dist exists
|
|
29
|
+
dist_dir = os.path.join(frontend_dir, 'dist')
|
|
30
|
+
if not os.path.isdir(dist_dir):
|
|
31
|
+
raise click.ClickException(
|
|
32
|
+
"frontend/dist 不存在。\n"
|
|
33
|
+
"请检查 npm run build 是否成功执行。"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# 4. Launch pywebview via uv
|
|
37
|
+
backend_dir = os.path.join(project_root, 'backend')
|
|
38
|
+
click.echo('\n🚀 启动 pywebview…')
|
|
39
|
+
run_cmd(['uv', 'run', 'src/main.py'], cwd=backend_dir)
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""wv-cli: Scaffold tool for pywebview + Vue3 desktop apps."""
|
|
2
|
+
import click
|
|
3
|
+
from .commands.create import create
|
|
4
|
+
from .commands.run import run
|
|
5
|
+
from .commands.build import build
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.group()
|
|
9
|
+
@click.version_option(version="0.1.0", prog_name="wv")
|
|
10
|
+
def cli():
|
|
11
|
+
"""wv-cli — pywebview + Vue3 desktop app scaffold tool."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
cli.add_command(create)
|
|
16
|
+
cli.add_command(run)
|
|
17
|
+
cli.add_command(build)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template content for files generated by `wv create`.
|
|
3
|
+
All templates are plain strings; variable substitution is done in commands/create.py.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
WV_TOML = """\
|
|
7
|
+
[project]
|
|
8
|
+
name = "{project_name}"
|
|
9
|
+
version = "{version}"
|
|
10
|
+
window_title = "{window_title}"
|
|
11
|
+
author = "{author}"
|
|
12
|
+
|
|
13
|
+
[build]
|
|
14
|
+
# Windows default path; ignored on non-Windows platforms
|
|
15
|
+
inno_setup_path = "C:/Program Files (x86)/Inno Setup 6/ISCC.exe"
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
CONFIG_PY = """\
|
|
19
|
+
# Development mode: load index.html directly from frontend/dist
|
|
20
|
+
HTML_PATH_DEV = '../../frontend/dist/index.html'
|
|
21
|
+
|
|
22
|
+
# Packaged mode: PyInstaller bundles frontend/dist contents into _f_dist
|
|
23
|
+
HTML_PATH_APP = '_f_dist/index.html'
|
|
24
|
+
|
|
25
|
+
# Window title (injected from wv.toml at project creation time)
|
|
26
|
+
WINDOW_TITLE = "{window_title}"
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
MAIN_PY = """\
|
|
30
|
+
import sys
|
|
31
|
+
import os
|
|
32
|
+
import webview
|
|
33
|
+
from config import WINDOW_TITLE, HTML_PATH_DEV, HTML_PATH_APP
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_html_path() -> str:
|
|
37
|
+
\"\"\"Resolve the correct HTML path depending on the runtime environment.\"\"\"
|
|
38
|
+
if getattr(sys, 'frozen', False):
|
|
39
|
+
# PyInstaller packaged environment: use sys._MEIPASS to locate resources
|
|
40
|
+
base = sys._MEIPASS
|
|
41
|
+
return os.path.join(base, HTML_PATH_APP)
|
|
42
|
+
else:
|
|
43
|
+
# Development / testing environment: use relative path to frontend/dist
|
|
44
|
+
base = os.path.dirname(os.path.abspath(__file__))
|
|
45
|
+
return os.path.join(base, HTML_PATH_DEV)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
from bridge.api import Api
|
|
50
|
+
api = Api()
|
|
51
|
+
window = webview.create_window(
|
|
52
|
+
WINDOW_TITLE,
|
|
53
|
+
url=get_html_path(),
|
|
54
|
+
js_api=api,
|
|
55
|
+
)
|
|
56
|
+
webview.start()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == '__main__':
|
|
60
|
+
main()
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
BRIDGE_INIT_PY = """\
|
|
64
|
+
# bridge package: pywebview JS API bridge classes live here
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
BRIDGE_API_PY = """\
|
|
68
|
+
class Api:
|
|
69
|
+
\"\"\"
|
|
70
|
+
pywebview JS API example.
|
|
71
|
+
The frontend calls methods of this class via window.pywebview.api.<method>().
|
|
72
|
+
Add your own methods below and expose them to the Vue frontend.
|
|
73
|
+
\"\"\"
|
|
74
|
+
|
|
75
|
+
def greet(self, name: str) -> str:
|
|
76
|
+
return f"Hello, {name}!"
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
SPEC_FILE = """\
|
|
80
|
+
# -*- mode: python ; coding: utf-8 -*-
|
|
81
|
+
block_cipher = None
|
|
82
|
+
|
|
83
|
+
a = Analysis(
|
|
84
|
+
['../backend/src/main.py'],
|
|
85
|
+
pathex=['../backend/src'],
|
|
86
|
+
binaries=[],
|
|
87
|
+
datas=[
|
|
88
|
+
('../frontend/dist', '_f_dist'), # Bundle frontend build output
|
|
89
|
+
('../icon', 'icon'), # Icon resources
|
|
90
|
+
],
|
|
91
|
+
hiddenimports=['webview'],
|
|
92
|
+
hookspath=[],
|
|
93
|
+
runtime_hooks=[],
|
|
94
|
+
excludes=[],
|
|
95
|
+
win_no_prefer_redirects=False,
|
|
96
|
+
win_private_assemblies=False,
|
|
97
|
+
cipher=block_cipher,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
|
101
|
+
|
|
102
|
+
exe = EXE(
|
|
103
|
+
pyz,
|
|
104
|
+
a.scripts,
|
|
105
|
+
[],
|
|
106
|
+
exclude_binaries=True,
|
|
107
|
+
name='{project_name}',
|
|
108
|
+
debug=False,
|
|
109
|
+
bootloader_ignore_signals=False,
|
|
110
|
+
strip=False,
|
|
111
|
+
upx=True,
|
|
112
|
+
console=False,
|
|
113
|
+
icon='../icon/favicon.ico',
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
coll = COLLECT(
|
|
117
|
+
exe,
|
|
118
|
+
a.binaries,
|
|
119
|
+
a.zipfiles,
|
|
120
|
+
a.datas,
|
|
121
|
+
strip=False,
|
|
122
|
+
upx=True,
|
|
123
|
+
upx_exclude=[],
|
|
124
|
+
name='{project_name}',
|
|
125
|
+
)
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
ISS_FILE = """\
|
|
129
|
+
[Setup]
|
|
130
|
+
AppName={project_name}
|
|
131
|
+
AppVersion={version}
|
|
132
|
+
DefaultDirName={{autopf}}\\{project_name}
|
|
133
|
+
DefaultGroupName={project_name}
|
|
134
|
+
OutputDir=publish
|
|
135
|
+
OutputBaseFilename={project_name}-{version}-setup
|
|
136
|
+
SetupIconFile=../icon/favicon.ico
|
|
137
|
+
Compression=lzma
|
|
138
|
+
SolidCompression=yes
|
|
139
|
+
|
|
140
|
+
[Files]
|
|
141
|
+
Source: "../build/dist/{project_name}/*"; DestDir: "{{app}}"; Flags: recursesubdirs createallsubdirs
|
|
142
|
+
|
|
143
|
+
[Icons]
|
|
144
|
+
Name: "{{group}}\\{project_name}"; Filename: "{{app}}\\{project_name}.exe"
|
|
145
|
+
Name: "{{commondesktop}}\\{project_name}"; Filename: "{{app}}\\{project_name}.exe"
|
|
146
|
+
|
|
147
|
+
[Run]
|
|
148
|
+
Filename: "{{app}}\\{project_name}.exe"; Description: "立即启动"; Flags: nowait postinstall skipifsilent
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
ROOT_GITIGNORE = """\
|
|
152
|
+
# ── Build output ─────────────────────────────────────────────────────────────
|
|
153
|
+
build/dist/
|
|
154
|
+
build/publish/
|
|
155
|
+
|
|
156
|
+
# ── Frontend ──────────────────────────────────────────────────────────────────
|
|
157
|
+
frontend/dist/
|
|
158
|
+
frontend/node_modules/
|
|
159
|
+
|
|
160
|
+
# ── Backend ───────────────────────────────────────────────────────────────────
|
|
161
|
+
backend/.venv/
|
|
162
|
+
backend/__pycache__/
|
|
163
|
+
backend/**/__pycache__/
|
|
164
|
+
|
|
165
|
+
# ── OS / IDE ──────────────────────────────────────────────────────────────────
|
|
166
|
+
.DS_Store
|
|
167
|
+
Thumbs.db
|
|
168
|
+
.vscode/
|
|
169
|
+
.idea/
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
BACKEND_GITIGNORE = """\
|
|
173
|
+
# Python
|
|
174
|
+
__pycache__/
|
|
175
|
+
*.py[cod]
|
|
176
|
+
*.pyo
|
|
177
|
+
*.pyd
|
|
178
|
+
*.egg
|
|
179
|
+
*.egg-info/
|
|
180
|
+
|
|
181
|
+
# Virtual environment (uv)
|
|
182
|
+
.venv/
|
|
183
|
+
|
|
184
|
+
# uv
|
|
185
|
+
.uv/
|
|
186
|
+
uv.lock
|
|
187
|
+
|
|
188
|
+
# Distribution
|
|
189
|
+
dist/
|
|
190
|
+
build/
|
|
191
|
+
|
|
192
|
+
# PyInstaller
|
|
193
|
+
*.spec.bak
|
|
194
|
+
|
|
195
|
+
# Testing
|
|
196
|
+
.pytest_cache/
|
|
197
|
+
.coverage
|
|
198
|
+
htmlcov/
|
|
199
|
+
|
|
200
|
+
# IDE
|
|
201
|
+
.vscode/
|
|
202
|
+
.idea/
|
|
203
|
+
*.swp
|
|
204
|
+
*.swo
|
|
205
|
+
|
|
206
|
+
# OS
|
|
207
|
+
.DS_Store
|
|
208
|
+
Thumbs.db
|
|
209
|
+
|
|
210
|
+
# Logs
|
|
211
|
+
*.log
|
|
212
|
+
"""
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Shared utility functions for wv-cli."""
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import toml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# wv.toml helpers
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
def load_config(project_root: str) -> dict:
|
|
18
|
+
"""Load wv.toml from the project root. Raises click.ClickException on failure."""
|
|
19
|
+
config_path = os.path.join(project_root, "wv.toml")
|
|
20
|
+
if not os.path.isfile(config_path):
|
|
21
|
+
raise click.ClickException(
|
|
22
|
+
f"wv.toml not found in {project_root}. "
|
|
23
|
+
"Are you inside a wv project directory?"
|
|
24
|
+
)
|
|
25
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
26
|
+
return toml.load(f)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def find_project_root() -> str:
|
|
30
|
+
"""Walk up from cwd until wv.toml is found. Returns the directory path."""
|
|
31
|
+
cwd = os.path.abspath(os.getcwd())
|
|
32
|
+
candidate = cwd
|
|
33
|
+
while True:
|
|
34
|
+
if os.path.isfile(os.path.join(candidate, "wv.toml")):
|
|
35
|
+
return candidate
|
|
36
|
+
parent = os.path.dirname(candidate)
|
|
37
|
+
if parent == candidate:
|
|
38
|
+
raise click.ClickException(
|
|
39
|
+
"wv.toml not found. Run this command from inside a wv project."
|
|
40
|
+
)
|
|
41
|
+
candidate = parent
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Environment checks
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def check_command(cmd: str) -> bool:
|
|
49
|
+
"""Return True if the shell command is available on PATH."""
|
|
50
|
+
return shutil.which(cmd) is not None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def require_node():
|
|
54
|
+
"""Abort with a helpful message if node/npm is missing."""
|
|
55
|
+
if not check_command("node") or not check_command("npm"):
|
|
56
|
+
raise click.ClickException(
|
|
57
|
+
"Node.js / npm not found.\n"
|
|
58
|
+
"Please install Node.js from: https://nodejs.org"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def require_uv():
|
|
63
|
+
"""Abort with a helpful message if uv is missing."""
|
|
64
|
+
if not check_command("uv"):
|
|
65
|
+
raise click.ClickException(
|
|
66
|
+
"uv not found.\n"
|
|
67
|
+
"Install it with: pip install uv\n"
|
|
68
|
+
" or (Windows): winget install astral-sh.uv"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Subprocess helpers
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def _resolve_cmd(cmd: str) -> str:
|
|
77
|
+
"""On Windows, resolve e.g. 'npm' → 'npm.cmd' so subprocess can find it."""
|
|
78
|
+
if platform.system() == "Windows":
|
|
79
|
+
resolved = shutil.which(cmd)
|
|
80
|
+
if resolved:
|
|
81
|
+
return resolved
|
|
82
|
+
return cmd
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def ensure_npm_deps(frontend_dir: str) -> None:
|
|
86
|
+
"""Run `npm install` if node_modules is missing or package.json has changed."""
|
|
87
|
+
node_modules = os.path.join(frontend_dir, 'node_modules')
|
|
88
|
+
if not os.path.isdir(node_modules):
|
|
89
|
+
click.echo(' 📥 安装前端依赖(npm install)…')
|
|
90
|
+
run_cmd(['npm', 'install'], cwd=frontend_dir)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def run_cmd(args: list, cwd: str = None, shell: bool = False):
|
|
94
|
+
"""Run a command, streaming output to the terminal. Raise on non-zero exit."""
|
|
95
|
+
args = [_resolve_cmd(args[0])] + args[1:]
|
|
96
|
+
click.echo(f" $ {' '.join(args)}")
|
|
97
|
+
result = subprocess.run(args, cwd=cwd, shell=shell)
|
|
98
|
+
if result.returncode != 0:
|
|
99
|
+
raise click.ClickException(
|
|
100
|
+
f"Command failed (exit {result.returncode}): {' '.join(args)}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Favicon injection
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def inject_favicon(project_root: str) -> None:
|
|
110
|
+
"""
|
|
111
|
+
After `npm run build`, overwrite every favicon.ico found under
|
|
112
|
+
frontend/dist/ with the project's own icon/favicon.ico.
|
|
113
|
+
Idempotent and skipped gracefully when source or dist is missing.
|
|
114
|
+
"""
|
|
115
|
+
src_ico = os.path.join(project_root, 'icon', 'favicon.ico')
|
|
116
|
+
|
|
117
|
+
if not os.path.isfile(src_ico):
|
|
118
|
+
click.echo('跳过 favicon 注入:icon/favicon.ico 不存在')
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
dist_dir = os.path.join(project_root, 'frontend', 'dist')
|
|
122
|
+
if not os.path.isdir(dist_dir):
|
|
123
|
+
click.echo('跳过 favicon 注入:frontend/dist 不存在')
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
replaced = 0
|
|
127
|
+
for dirpath, _, filenames in os.walk(dist_dir):
|
|
128
|
+
for filename in filenames:
|
|
129
|
+
if filename.lower() == 'favicon.ico':
|
|
130
|
+
dst = os.path.join(dirpath, filename)
|
|
131
|
+
shutil.copy2(src_ico, dst)
|
|
132
|
+
rel = os.path.relpath(dst, project_root)
|
|
133
|
+
click.echo(f'✔ 已注入 favicon:{rel}')
|
|
134
|
+
replaced += 1
|
|
135
|
+
|
|
136
|
+
if replaced == 0:
|
|
137
|
+
click.echo('跳过 favicon 注入:dist 中未找到 favicon.ico')
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Frontend router fix
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def fix_router_history(project_root: str) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Replace createWebHistory with createWebHashHistory in the Vue Router
|
|
146
|
+
entry file so that file:// protocol works correctly after packaging.
|
|
147
|
+
This function is idempotent.
|
|
148
|
+
"""
|
|
149
|
+
router_dir = os.path.join(project_root, "frontend", "src", "router")
|
|
150
|
+
|
|
151
|
+
if not os.path.isdir(router_dir):
|
|
152
|
+
click.echo("跳过路由修复:未检测到 router 目录")
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
target_file = None
|
|
156
|
+
for filename in ("index.ts", "index.js"):
|
|
157
|
+
candidate = os.path.join(router_dir, filename)
|
|
158
|
+
if os.path.isfile(candidate):
|
|
159
|
+
target_file = candidate
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
if target_file is None:
|
|
163
|
+
click.echo("跳过路由修复:未找到 router/index.ts 或 router/index.js")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
with open(target_file, "r", encoding="utf-8") as f:
|
|
167
|
+
content = f.read()
|
|
168
|
+
|
|
169
|
+
if "createWebHistory" not in content:
|
|
170
|
+
click.echo("跳过路由修复:未检测到 createWebHistory,无需修改")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# \b boundary: createWebHashHistory does NOT contain createWebHistory as a
|
|
174
|
+
# substring, so this replacement is idempotent.
|
|
175
|
+
new_content = re.sub(r"\bcreateWebHistory\b", "createWebHashHistory", content)
|
|
176
|
+
|
|
177
|
+
with open(target_file, "w", encoding="utf-8") as f:
|
|
178
|
+
f.write(new_content)
|
|
179
|
+
|
|
180
|
+
rel_path = os.path.relpath(target_file, project_root)
|
|
181
|
+
click.echo(f"✔ 已修复:{rel_path}(createWebHistory → createWebHashHistory)")
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wv-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI scaffold tool for pywebview + Vue3 desktop apps
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: click>=8.0
|
|
8
|
+
Requires-Dist: toml>=0.10
|
|
9
|
+
|
|
10
|
+
# wv-cli
|
|
11
|
+
|
|
12
|
+
A command-line scaffold tool for building **pywebview (Python backend) + Vue 3 (frontend)** desktop apps.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Python ≥ 3.9
|
|
17
|
+
- [uv](https://docs.astral.sh/uv/) — Python package manager
|
|
18
|
+
- [Node.js / npm](https://nodejs.org) — for the Vue 3 frontend
|
|
19
|
+
- [Inno Setup 6](https://jrsoftware.org/isdl.php) *(Windows only, required for `--publish`)*
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install wv-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install from source with `uv`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/yourname/wv-cli
|
|
31
|
+
cd wv-cli
|
|
32
|
+
uv pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### Create a new project
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Interactive — creates ./my-app/
|
|
41
|
+
wv create
|
|
42
|
+
|
|
43
|
+
# In the current directory
|
|
44
|
+
wv create .
|
|
45
|
+
|
|
46
|
+
# Explicit directory
|
|
47
|
+
wv create path/to/my-app
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
You will be prompted for:
|
|
51
|
+
| Prompt | Default |
|
|
52
|
+
|---|---|
|
|
53
|
+
| 项目名称 (project name) | directory name |
|
|
54
|
+
| 窗口标题 (window title) | project name |
|
|
55
|
+
| 版本号 (version) | `1.0.0` |
|
|
56
|
+
| 作者 (author) | *(empty)* |
|
|
57
|
+
|
|
58
|
+
After answering, the CLI will:
|
|
59
|
+
1. Scaffold the full directory structure
|
|
60
|
+
2. Run `npm create vue@latest` for the frontend (you drive the Vue prompts)
|
|
61
|
+
3. Run `uv init / venv / add pywebview pyinstaller` for the backend
|
|
62
|
+
|
|
63
|
+
### Run in development mode
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd my-app
|
|
67
|
+
wv run
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Builds the Vue frontend, then launches the pywebview window loading `frontend/dist`.
|
|
71
|
+
|
|
72
|
+
### Production build
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
wv build
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Builds the frontend and runs PyInstaller to produce `build/dist/<project-name>/`.
|
|
79
|
+
|
|
80
|
+
### Build + Windows installer
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
wv build --publish
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Runs the full build, then calls Inno Setup to produce
|
|
87
|
+
`build/publish/<project-name>-<version>-setup.exe`.
|
|
88
|
+
|
|
89
|
+
Configure the Inno Setup path in `wv.toml` if needed:
|
|
90
|
+
|
|
91
|
+
```toml
|
|
92
|
+
[build]
|
|
93
|
+
inno_setup_path = "C:/Program Files (x86)/Inno Setup 6/ISCC.exe"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Generated Project Structure
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
my-app/
|
|
100
|
+
├── icon/
|
|
101
|
+
│ ├── favicon.ico
|
|
102
|
+
│ └── logo.png
|
|
103
|
+
├── frontend/ ← Vue 3 (npm create vue@latest)
|
|
104
|
+
│ └── dist/ ← built by wv run / wv build
|
|
105
|
+
├── backend/
|
|
106
|
+
│ ├── .venv/ ← uv virtual environment
|
|
107
|
+
│ └── src/
|
|
108
|
+
│ ├── main.py
|
|
109
|
+
│ ├── config.py
|
|
110
|
+
│ └── bridge/
|
|
111
|
+
│ ├── __init__.py
|
|
112
|
+
│ └── api.py
|
|
113
|
+
├── build/
|
|
114
|
+
│ ├── my-app.spec ← PyInstaller config
|
|
115
|
+
│ ├── my-app.iss ← Inno Setup config
|
|
116
|
+
│ └── publish/ ← installer output
|
|
117
|
+
└── wv.toml
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Frontend Router Auto-Fix
|
|
121
|
+
|
|
122
|
+
`wv run` and `wv build` automatically replace `createWebHistory` with
|
|
123
|
+
`createWebHashHistory` in `frontend/src/router/index.{ts,js}` before building.
|
|
124
|
+
This ensures the app works correctly when loaded via the `file://` protocol
|
|
125
|
+
after packaging. The replacement is **idempotent** — running it multiple times
|
|
126
|
+
has no side effects.
|
|
127
|
+
|
|
128
|
+
## `wv.toml` Reference
|
|
129
|
+
|
|
130
|
+
```toml
|
|
131
|
+
[project]
|
|
132
|
+
name = "my-app"
|
|
133
|
+
version = "1.0.0"
|
|
134
|
+
window_title = "My App"
|
|
135
|
+
author = ""
|
|
136
|
+
|
|
137
|
+
[build]
|
|
138
|
+
inno_setup_path = "C:/Program Files (x86)/Inno Setup 6/ISCC.exe"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Extending the JS Bridge
|
|
142
|
+
|
|
143
|
+
Edit `backend/src/bridge/api.py`:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
class Api:
|
|
147
|
+
def greet(self, name: str) -> str:
|
|
148
|
+
return f"Hello, {name}!"
|
|
149
|
+
|
|
150
|
+
def read_file(self, path: str) -> str:
|
|
151
|
+
with open(path) as f:
|
|
152
|
+
return f.read()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Call from Vue:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
const result = await window.pywebview.api.greet('World')
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
wv_cli/__init__.py
|
|
4
|
+
wv_cli/main.py
|
|
5
|
+
wv_cli/templates.py
|
|
6
|
+
wv_cli/utils.py
|
|
7
|
+
wv_cli.egg-info/PKG-INFO
|
|
8
|
+
wv_cli.egg-info/SOURCES.txt
|
|
9
|
+
wv_cli.egg-info/dependency_links.txt
|
|
10
|
+
wv_cli.egg-info/entry_points.txt
|
|
11
|
+
wv_cli.egg-info/requires.txt
|
|
12
|
+
wv_cli.egg-info/top_level.txt
|
|
13
|
+
wv_cli/commands/__init__.py
|
|
14
|
+
wv_cli/commands/build.py
|
|
15
|
+
wv_cli/commands/create.py
|
|
16
|
+
wv_cli/commands/run.py
|
|
17
|
+
wv_cli/icon/favicon.ico
|
|
18
|
+
wv_cli/icon/logo.png
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wv_cli
|