quapp 1.1.2 → 1.1.3
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 +62 -128
- package/bin/cli.js +1 -1
- package/commands/serve.js +111 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,193 +1,127 @@
|
|
|
1
1
|
# quapp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Dev server and build tool for Quapp projects. Serves your app over LAN with a QR code for mobile testing, and packages production builds as `.qpp` files.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
If you scaffolded with `create-quapp`, run the pre-configured scripts:
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
npm
|
|
10
|
+
npm run dev # Start dev server
|
|
11
|
+
npm run qbuild # Build .qpp package
|
|
9
12
|
```
|
|
10
13
|
|
|
11
|
-
##
|
|
14
|
+
## Installation
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
For existing Vite projects:
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
```bash
|
|
19
|
+
npm install -D quapp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Initialize configuration and scripts:
|
|
16
23
|
|
|
17
24
|
```bash
|
|
18
|
-
quapp init
|
|
25
|
+
npx quapp init
|
|
19
26
|
```
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
| Flag | Short | Description |
|
|
23
|
-
|------|-------|-------------|
|
|
24
|
-
| `--yes` | `-y` | Skip confirmation prompt |
|
|
25
|
-
| `--force` | `-f` | Overwrite existing config/scripts |
|
|
26
|
-
| `--dry-run` | | Preview changes without applying |
|
|
28
|
+
## Commands
|
|
27
29
|
|
|
28
30
|
### `quapp serve`
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
Starts Vite dev server with LAN network access and QR code.
|
|
31
33
|
|
|
32
34
|
```bash
|
|
33
|
-
quapp serve
|
|
35
|
+
npx quapp serve
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
| `--
|
|
40
|
-
| `--
|
|
41
|
-
| `--
|
|
42
|
-
| `--
|
|
43
|
-
| `--https` | | Enable HTTPS |
|
|
38
|
+
| Flag | Description |
|
|
39
|
+
|------|-------------|
|
|
40
|
+
| `-p, --port <port>` | Server port (default: 5173) |
|
|
41
|
+
| `--host <host>` | Host to bind |
|
|
42
|
+
| `--open` | Open in browser |
|
|
43
|
+
| `--no-qr` | Disable QR code |
|
|
44
|
+
| `--https` | Enable HTTPS |
|
|
44
45
|
|
|
45
46
|
### `quapp build`
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
Builds for production and creates `.qpp` package.
|
|
48
49
|
|
|
49
50
|
```bash
|
|
50
|
-
quapp build
|
|
51
|
+
npx quapp build
|
|
51
52
|
```
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
| `--
|
|
57
|
-
| `--no-clean` |
|
|
58
|
-
| `--skip-prompts` | | Skip interactive prompts |
|
|
54
|
+
| Flag | Description |
|
|
55
|
+
|------|-------------|
|
|
56
|
+
| `-o, --output <file>` | Output filename (default: dist.qpp) |
|
|
57
|
+
| `--skip-prompts` | Non-interactive mode |
|
|
58
|
+
| `--no-clean` | Keep dist folder |
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
### `quapp init`
|
|
61
|
+
|
|
62
|
+
Initialize Quapp in an existing Vite project.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx quapp init
|
|
66
|
+
```
|
|
61
67
|
|
|
62
|
-
| Flag |
|
|
63
|
-
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
| `--
|
|
67
|
-
| `--version` | `-v` | Show version |
|
|
68
|
-
| `--help` | `-h` | Show help |
|
|
68
|
+
| Flag | Description |
|
|
69
|
+
|------|-------------|
|
|
70
|
+
| `-y, --yes` | Skip prompts |
|
|
71
|
+
| `-f, --force` | Overwrite existing config |
|
|
72
|
+
| `--dry-run` | Preview changes |
|
|
69
73
|
|
|
70
74
|
## Configuration
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
`quapp.config.json`:
|
|
73
77
|
|
|
74
78
|
```json
|
|
75
79
|
{
|
|
76
80
|
"server": {
|
|
77
81
|
"port": 5173,
|
|
78
82
|
"qr": true,
|
|
79
|
-
"
|
|
80
|
-
"openBrowser": false,
|
|
81
|
-
"https": false,
|
|
82
|
-
"fallbackPort": true,
|
|
83
|
-
"autoRetry": true,
|
|
84
|
-
"strictPort": false
|
|
83
|
+
"openBrowser": false
|
|
85
84
|
},
|
|
86
85
|
"build": {
|
|
87
|
-
"outDir": "dist",
|
|
88
86
|
"outputFile": "dist.qpp"
|
|
89
87
|
}
|
|
90
88
|
}
|
|
91
89
|
```
|
|
92
90
|
|
|
93
|
-
##
|
|
94
|
-
|
|
95
|
-
```bash
|
|
96
|
-
# Initialize Quapp in existing project
|
|
97
|
-
quapp init
|
|
98
|
-
|
|
99
|
-
# Initialize without prompts (AI-friendly)
|
|
100
|
-
quapp init --yes --json
|
|
101
|
-
|
|
102
|
-
# Start dev server
|
|
103
|
-
quapp serve
|
|
104
|
-
|
|
105
|
-
# Start on specific port
|
|
106
|
-
quapp serve -p 3000
|
|
107
|
-
|
|
108
|
-
# Start and open browser
|
|
109
|
-
quapp serve --open
|
|
110
|
-
|
|
111
|
-
# Build for production
|
|
112
|
-
quapp build
|
|
113
|
-
|
|
114
|
-
# Build with custom output name
|
|
115
|
-
quapp build -o my-app.qpp
|
|
91
|
+
## Global Options
|
|
116
92
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
93
|
+
| Flag | Description |
|
|
94
|
+
|------|-------------|
|
|
95
|
+
| `--json` | JSON output for automation |
|
|
96
|
+
| `--verbose` | Detailed logging |
|
|
97
|
+
| `-h, --help` | Show help |
|
|
98
|
+
| `-v, --version` | Show version |
|
|
120
99
|
|
|
121
|
-
##
|
|
100
|
+
## Automation
|
|
122
101
|
|
|
123
|
-
For
|
|
102
|
+
For CI/CD or programmatic usage:
|
|
124
103
|
|
|
125
104
|
```bash
|
|
126
|
-
|
|
127
|
-
quapp init --yes --json
|
|
128
|
-
|
|
129
|
-
# Preview init changes
|
|
130
|
-
quapp init --dry-run --json
|
|
131
|
-
|
|
132
|
-
# Build without prompts
|
|
133
|
-
quapp build --skip-prompts --json
|
|
134
|
-
|
|
135
|
-
# Build with custom output
|
|
136
|
-
quapp build -o myapp.qpp --skip-prompts --json
|
|
105
|
+
npx quapp build --skip-prompts --json
|
|
137
106
|
```
|
|
138
107
|
|
|
139
|
-
### JSON Output Examples
|
|
140
|
-
|
|
141
|
-
Init success:
|
|
142
|
-
```json
|
|
143
|
-
{
|
|
144
|
-
"success": true,
|
|
145
|
-
"changes": ["quapp.config.json", "script:dev", "script:qbuild", "devDependency:quapp"],
|
|
146
|
-
"nextSteps": ["npm install", "npm run dev"]
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Build success:
|
|
151
108
|
```json
|
|
152
109
|
{
|
|
153
110
|
"success": true,
|
|
154
111
|
"outputFile": "dist.qpp",
|
|
155
|
-
"outputPath": "/path/to/
|
|
156
|
-
"
|
|
157
|
-
"package_name": "com.author.myapp",
|
|
158
|
-
"version": "1.0.0",
|
|
159
|
-
"version_code": 10000
|
|
160
|
-
},
|
|
161
|
-
"duration": 5230
|
|
162
|
-
}
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
Error (with suggestion for AI):
|
|
166
|
-
```json
|
|
167
|
-
{
|
|
168
|
-
"success": false,
|
|
169
|
-
"errorCode": "NO_BUILD_SCRIPT",
|
|
170
|
-
"error": "No build script",
|
|
171
|
-
"suggestion": "Add to package.json: \"scripts\": { \"build\": \"vite build\" }"
|
|
112
|
+
"outputPath": "/path/to/dist.qpp",
|
|
113
|
+
"duration": 1234
|
|
172
114
|
}
|
|
173
115
|
```
|
|
174
116
|
|
|
175
|
-
##
|
|
117
|
+
## Requirements
|
|
176
118
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
| 0 | Success |
|
|
180
|
-
| 1 | General error |
|
|
181
|
-
| 2 | Invalid arguments |
|
|
182
|
-
| 3 | Build failed |
|
|
183
|
-
| 4 | Configuration error |
|
|
184
|
-
| 5 | Missing dependency |
|
|
185
|
-
| 130 | User cancelled |
|
|
119
|
+
- Node.js 18+
|
|
120
|
+
- Vite project
|
|
186
121
|
|
|
187
|
-
##
|
|
122
|
+
## Related
|
|
188
123
|
|
|
189
|
-
-
|
|
190
|
-
- Vite (in your project's dependencies)
|
|
124
|
+
- [create-quapp](https://www.npmjs.com/package/create-quapp) — Project scaffolding
|
|
191
125
|
|
|
192
126
|
## License
|
|
193
127
|
|
package/bin/cli.js
CHANGED
package/commands/serve.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Serve command - Start development server with LAN access
|
|
3
|
+
*
|
|
4
|
+
* Design: We use --strictPort so Vite exits on port conflicts, allowing us to:
|
|
5
|
+
* 1. Control ALL user-facing messages (no duplicate/conflicting output)
|
|
6
|
+
* 2. Show accurate port information
|
|
7
|
+
* 3. Provide a clean, quapp-branded DX
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
10
|
import { spawn } from 'child_process';
|
|
6
11
|
import os from 'os';
|
|
12
|
+
import net from 'net';
|
|
7
13
|
import * as logger from '../lib/logger.js';
|
|
8
14
|
import { loadConfig, checkViteAvailable } from '../lib/config.js';
|
|
9
15
|
import { EXIT_CODES } from '../lib/constants.js';
|
|
@@ -27,6 +33,50 @@ function getIP(networkType = 'private') {
|
|
|
27
33
|
return 'localhost';
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Check if a port is available
|
|
38
|
+
* @param {number} port - Port to check
|
|
39
|
+
* @param {string} host - Host to check on
|
|
40
|
+
* @returns {Promise<boolean>}
|
|
41
|
+
*/
|
|
42
|
+
function isPortAvailable(port, host = 'localhost') {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const server = net.createServer();
|
|
45
|
+
server.once('error', () => {
|
|
46
|
+
server.close(); // Clean up even on error to prevent fd leak
|
|
47
|
+
resolve(false);
|
|
48
|
+
});
|
|
49
|
+
server.once('listening', () => {
|
|
50
|
+
server.close();
|
|
51
|
+
resolve(true);
|
|
52
|
+
});
|
|
53
|
+
server.listen(port, host);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Find an available port starting from the given port
|
|
59
|
+
* @param {number} startPort - Port to start searching from
|
|
60
|
+
* @param {string} host - Host to check on
|
|
61
|
+
* @param {number} maxAttempts - Maximum number of ports to try
|
|
62
|
+
* @returns {Promise<{port: number, retries: number}>}
|
|
63
|
+
*/
|
|
64
|
+
async function findAvailablePort(startPort, host = 'localhost', maxAttempts = 10) {
|
|
65
|
+
let port = startPort;
|
|
66
|
+
let retries = 0;
|
|
67
|
+
|
|
68
|
+
while (retries < maxAttempts) {
|
|
69
|
+
if (await isPortAvailable(port, host)) {
|
|
70
|
+
return { port, retries };
|
|
71
|
+
}
|
|
72
|
+
logger.info(`Port ${port} is in use, trying ${port + 1}...`);
|
|
73
|
+
port++;
|
|
74
|
+
retries++;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new Error(`Could not find an available port after ${maxAttempts} attempts`);
|
|
78
|
+
}
|
|
79
|
+
|
|
30
80
|
/**
|
|
31
81
|
* Run the serve command
|
|
32
82
|
* @param {Object} options - Command options
|
|
@@ -61,19 +111,29 @@ export async function runServe(options = {}) {
|
|
|
61
111
|
return { success: false, error: 'Vite not found', exitCode: EXIT_CODES.MISSING_DEPENDENCY };
|
|
62
112
|
}
|
|
63
113
|
|
|
64
|
-
|
|
114
|
+
// Find available port BEFORE starting Vite (if autoRetry is enabled)
|
|
115
|
+
let actualPort = serverConfig.port;
|
|
116
|
+
|
|
117
|
+
if (config.server.autoRetry && !serverConfig.strictPort) {
|
|
118
|
+
try {
|
|
119
|
+
const { port, retries } = await findAvailablePort(serverConfig.port, serverConfig.host);
|
|
120
|
+
actualPort = port;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
logger.error(err.message);
|
|
123
|
+
return { success: false, error: err.message, exitCode: EXIT_CODES.GENERAL_ERROR };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
65
126
|
|
|
66
|
-
|
|
127
|
+
logger.debug(`Starting dev server on ${serverConfig.host}:${actualPort}`);
|
|
128
|
+
|
|
129
|
+
// Build Vite arguments - always use strictPort since we handle port finding ourselves
|
|
67
130
|
const viteArgs = [
|
|
68
131
|
'vite',
|
|
69
132
|
'--host', serverConfig.host,
|
|
70
|
-
'--port', String(
|
|
133
|
+
'--port', String(actualPort),
|
|
134
|
+
'--strictPort', // Always strict since we pre-check port availability
|
|
71
135
|
];
|
|
72
136
|
|
|
73
|
-
if (serverConfig.strictPort || !config.server.autoRetry) {
|
|
74
|
-
viteArgs.push('--strictPort');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
137
|
if (serverConfig.https) {
|
|
78
138
|
viteArgs.push('--https');
|
|
79
139
|
}
|
|
@@ -90,24 +150,22 @@ export async function runServe(options = {}) {
|
|
|
90
150
|
shell: true,
|
|
91
151
|
});
|
|
92
152
|
|
|
93
|
-
let
|
|
94
|
-
|
|
95
|
-
// Build URLs - LAN URL for QR code, localhost for browser
|
|
153
|
+
let serverReady = false;
|
|
154
|
+
let bannerShown = false;
|
|
96
155
|
const protocol = serverConfig.https ? 'https' : 'http';
|
|
97
|
-
const lanUrl = `${protocol}://${serverConfig.host}:${
|
|
98
|
-
const localUrl = `${protocol}://localhost:${
|
|
156
|
+
const lanUrl = `${protocol}://${serverConfig.host}:${actualPort}`;
|
|
157
|
+
const localUrl = `${protocol}://localhost:${actualPort}`;
|
|
158
|
+
|
|
159
|
+
// Function to show our custom banner with QR code
|
|
160
|
+
const showBanner = async () => {
|
|
161
|
+
if (bannerShown) return;
|
|
162
|
+
bannerShown = true;
|
|
99
163
|
|
|
100
|
-
// Function to show QR code and open browser
|
|
101
|
-
const showQRAndOpenBrowser = async () => {
|
|
102
|
-
if (qrShown) return;
|
|
103
|
-
qrShown = true;
|
|
104
|
-
|
|
105
|
-
// Show access URLs
|
|
106
164
|
logger.newline();
|
|
107
|
-
console.log(' \x1b[1m\x1b[32m✓\x1b[0m \x1b[
|
|
165
|
+
console.log(' \x1b[1m\x1b[32m✓\x1b[0m \x1b[1mDev server running!\x1b[0m');
|
|
108
166
|
logger.newline();
|
|
109
167
|
// console.log(` \x1b[36m➜\x1b[0m \x1b[1mLocal:\x1b[0m ${localUrl}`);
|
|
110
|
-
console.log(` \x1b[36m➜\x1b[0m \x1b[1mNetwork:\x1b[0m ${lanUrl}`);
|
|
168
|
+
// console.log(` \x1b[36m➜\x1b[0m \x1b[1mNetwork:\x1b[0m ${lanUrl}`);
|
|
111
169
|
|
|
112
170
|
// Show QR code for mobile access
|
|
113
171
|
if (serverConfig.qr) {
|
|
@@ -118,33 +176,55 @@ export async function runServe(options = {}) {
|
|
|
118
176
|
logger.newline();
|
|
119
177
|
qrcode.default.generate(lanUrl, { small: true });
|
|
120
178
|
} catch (err) {
|
|
121
|
-
// qrcode-terminal not available, skip
|
|
122
179
|
logger.debug(`QR code generation failed: ${err.message}`);
|
|
123
180
|
}
|
|
124
181
|
}
|
|
125
182
|
|
|
183
|
+
logger.newline();
|
|
184
|
+
console.log(' \x1b[90mpress h + enter to show help\x1b[0m');
|
|
126
185
|
logger.newline();
|
|
127
186
|
|
|
128
|
-
// Open browser if requested
|
|
187
|
+
// Open browser if requested
|
|
129
188
|
if (serverConfig.openBrowser) {
|
|
130
189
|
try {
|
|
131
190
|
const open = await import('open');
|
|
132
|
-
await open.default(
|
|
191
|
+
await open.default(localUrl);
|
|
133
192
|
} catch (err) {
|
|
134
193
|
logger.debug(`Failed to open browser: ${err.message}`);
|
|
135
194
|
}
|
|
136
195
|
}
|
|
137
196
|
};
|
|
138
197
|
|
|
139
|
-
//
|
|
140
|
-
setTimeout(showQRAndOpenBrowser, 1500);
|
|
141
|
-
|
|
142
|
-
// Handle stdout - just pass through to console
|
|
198
|
+
// Handle stdout - filter Vite's startup banner, pass through HMR/other messages
|
|
143
199
|
viteProcess.stdout.on('data', (data) => {
|
|
200
|
+
const output = data.toString();
|
|
201
|
+
|
|
202
|
+
// Detect when Vite is ready
|
|
203
|
+
if (output.includes('ready in')) {
|
|
204
|
+
serverReady = true;
|
|
205
|
+
// Show our banner instead of Vite's
|
|
206
|
+
showBanner();
|
|
207
|
+
return; // Don't print Vite's ready line
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Filter out Vite's startup banner lines (we show our own)
|
|
211
|
+
// These patterns match Vite's default startup output
|
|
212
|
+
const isStartupBanner =
|
|
213
|
+
output.includes('VITE v') ||
|
|
214
|
+
output.includes('➜ Local:') ||
|
|
215
|
+
output.includes('➜ Network:') ||
|
|
216
|
+
output.includes('press h + enter') ||
|
|
217
|
+
(output.trim() === '' && !serverReady); // Empty lines before ready
|
|
218
|
+
|
|
219
|
+
if (isStartupBanner) {
|
|
220
|
+
return; // Suppress Vite's banner
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Pass through all other output (HMR updates, warnings, etc.)
|
|
144
224
|
process.stdout.write(data);
|
|
145
225
|
});
|
|
146
226
|
|
|
147
|
-
// Handle stderr
|
|
227
|
+
// Handle stderr - pass through (errors, warnings)
|
|
148
228
|
viteProcess.stderr.on('data', (data) => {
|
|
149
229
|
process.stderr.write(data);
|
|
150
230
|
});
|
|
@@ -152,20 +232,10 @@ export async function runServe(options = {}) {
|
|
|
152
232
|
// Handle process exit
|
|
153
233
|
return new Promise((resolve) => {
|
|
154
234
|
viteProcess.on('close', (code) => {
|
|
155
|
-
if (code !== 0
|
|
156
|
-
const nextPort = serverConfig.port + 1;
|
|
157
|
-
logger.warn(`Port ${serverConfig.port} in use. Trying port ${nextPort}...`);
|
|
158
|
-
|
|
159
|
-
// Retry with next port
|
|
160
|
-
resolve(runServe({
|
|
161
|
-
...options,
|
|
162
|
-
port: nextPort,
|
|
163
|
-
_attempt: (options._attempt || 0) + 1,
|
|
164
|
-
}));
|
|
165
|
-
} else if (code !== 0) {
|
|
235
|
+
if (code !== 0) {
|
|
166
236
|
resolve({
|
|
167
237
|
success: false,
|
|
168
|
-
error: `
|
|
238
|
+
error: `Dev server exited with code ${code}`,
|
|
169
239
|
exitCode: EXIT_CODES.GENERAL_ERROR,
|
|
170
240
|
});
|
|
171
241
|
} else {
|
|
@@ -178,7 +248,7 @@ export async function runServe(options = {}) {
|
|
|
178
248
|
});
|
|
179
249
|
|
|
180
250
|
viteProcess.on('error', (err) => {
|
|
181
|
-
logger.error(`Failed to start
|
|
251
|
+
logger.error(`Failed to start dev server: ${err.message}`);
|
|
182
252
|
resolve({
|
|
183
253
|
success: false,
|
|
184
254
|
error: err.message,
|
package/package.json
CHANGED