@julien-lin/universal-pwa-cli 1.3.0 → 1.3.2
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.fr.md +78 -0
- package/README.md +102 -5
- package/dist/chunk-35T5DDFX.js +88 -0
- package/dist/chunk-Y2M6FMPR.js +87 -0
- package/dist/environment-detector-7AC3BRDB.js +6 -0
- package/dist/environment-detector-SV7DMPWT.js +6 -0
- package/dist/index.cjs +1300 -244
- package/dist/index.js +1195 -237
- package/package.json +2 -2
package/README.fr.md
CHANGED
|
@@ -92,6 +92,84 @@ universal-pwa init \
|
|
|
92
92
|
--theme-color "#2c3e50"
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
### Bouton d'Installation PWA
|
|
96
|
+
|
|
97
|
+
Le CLI injecte automatiquement un gestionnaire d'installation PWA dans votre HTML. Pour afficher un bouton d'installation dans votre application, utilisez les fonctions globales exposées :
|
|
98
|
+
|
|
99
|
+
#### Fonctions Globales Disponibles
|
|
100
|
+
|
|
101
|
+
- `window.installPWA()` : Déclenche la prompt d'installation
|
|
102
|
+
- `window.isPWAInstalled()` : Vérifie si l'app est déjà installée
|
|
103
|
+
- `window.isPWAInstallable()` : Vérifie si l'app est installable
|
|
104
|
+
|
|
105
|
+
#### Exemple Vanilla JavaScript
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
// Vérifier si installable et afficher un bouton
|
|
109
|
+
if (window.isPWAInstallable && window.isPWAInstallable()) {
|
|
110
|
+
const installButton = document.createElement('button')
|
|
111
|
+
installButton.textContent = 'Installer l\'app'
|
|
112
|
+
installButton.onclick = () => {
|
|
113
|
+
window.installPWA().catch(console.error)
|
|
114
|
+
}
|
|
115
|
+
document.body.appendChild(installButton)
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### Exemple React
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import { useState, useEffect } from 'react'
|
|
123
|
+
|
|
124
|
+
function InstallButton() {
|
|
125
|
+
const [isInstallable, setIsInstallable] = useState(false)
|
|
126
|
+
const [isInstalled, setIsInstalled] = useState(false)
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
// Vérifier l'état initial
|
|
130
|
+
if (window.isPWAInstalled) {
|
|
131
|
+
setIsInstalled(window.isPWAInstalled())
|
|
132
|
+
}
|
|
133
|
+
if (window.isPWAInstallable) {
|
|
134
|
+
setIsInstallable(window.isPWAInstallable())
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Écouter les événements personnalisés
|
|
138
|
+
const handleInstallable = () => setIsInstallable(true)
|
|
139
|
+
const handleInstalled = () => {
|
|
140
|
+
setIsInstalled(true)
|
|
141
|
+
setIsInstallable(false)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
window.addEventListener('pwa-installable', handleInstallable)
|
|
145
|
+
window.addEventListener('pwa-installed', handleInstalled)
|
|
146
|
+
|
|
147
|
+
return () => {
|
|
148
|
+
window.removeEventListener('pwa-installable', handleInstallable)
|
|
149
|
+
window.removeEventListener('pwa-installed', handleInstalled)
|
|
150
|
+
}
|
|
151
|
+
}, [])
|
|
152
|
+
|
|
153
|
+
if (isInstalled || !isInstallable) {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<button onClick={() => window.installPWA?.()}>
|
|
159
|
+
Installer l'app
|
|
160
|
+
</button>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Événements Personnalisés
|
|
166
|
+
|
|
167
|
+
Le script injecté émet des événements personnalisés que vous pouvez écouter :
|
|
168
|
+
|
|
169
|
+
- `pwa-installable` : Émis quand l'app devient installable
|
|
170
|
+
- `pwa-installed` : Émis après l'installation réussie
|
|
171
|
+
- `pwa-install-choice` : Émis avec le choix de l'utilisateur (`{ detail: { outcome: 'accepted' | 'dismissed' } }`)
|
|
172
|
+
|
|
95
173
|
### Commande `scan`
|
|
96
174
|
|
|
97
175
|
Scanne un projet et détecte le framework, l'architecture et les assets.
|
package/README.md
CHANGED
|
@@ -37,13 +37,23 @@ pnpm build
|
|
|
37
37
|
# or
|
|
38
38
|
yarn build
|
|
39
39
|
|
|
40
|
-
# 2. Then initialize PWA
|
|
40
|
+
# 2. Then initialize PWA
|
|
41
|
+
# In interactive mode, select "Production" when prompted
|
|
42
|
+
# The CLI will auto-detect dist/ directory and suggest it
|
|
43
|
+
universal-pwa init
|
|
44
|
+
|
|
45
|
+
# Or explicitly specify output directory
|
|
41
46
|
universal-pwa init --output-dir dist
|
|
42
47
|
```
|
|
43
48
|
|
|
44
49
|
**Why?** The service worker needs to precache all your built assets (JS/CSS with hashes). If you initialize before building, the service worker won't know about the hashed filenames.
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
**Environment Detection:**
|
|
52
|
+
- The CLI automatically detects your environment:
|
|
53
|
+
- **Production**: If `dist/` or `build/` exists with recent files (< 24h)
|
|
54
|
+
- **Local**: Otherwise, defaults to `public/`
|
|
55
|
+
- Detection indicators are displayed during interactive prompts
|
|
56
|
+
- You can override the detection by explicitly choosing Local or Production
|
|
47
57
|
|
|
48
58
|
#### Interactive Mode (Recommended)
|
|
49
59
|
|
|
@@ -53,13 +63,22 @@ Simply run without arguments to launch interactive prompts:
|
|
|
53
63
|
universal-pwa init
|
|
54
64
|
```
|
|
55
65
|
|
|
56
|
-
The CLI will guide you through:
|
|
66
|
+
The CLI will guide you through a 2-phase workflow:
|
|
67
|
+
|
|
68
|
+
**Phase 1: Environment Selection**
|
|
69
|
+
- Choose between **Local** (development) or **Production** (build)
|
|
70
|
+
- The CLI automatically detects your environment based on the presence of `dist/` or `build/` directories
|
|
71
|
+
- Displays detection indicators (e.g., "dist/ directory exists with 15 built files")
|
|
72
|
+
|
|
73
|
+
**Phase 2: Application Configuration**
|
|
57
74
|
- App name (auto-detected from `package.json`)
|
|
58
|
-
- Short name (max 12 characters)
|
|
75
|
+
- Short name (max 12 characters, auto-generated from app name)
|
|
59
76
|
- Icon source path (auto-detected from common locations)
|
|
60
|
-
- Theme and background colors
|
|
77
|
+
- Theme and background colors (suggested based on detected framework)
|
|
61
78
|
- Icon generation options
|
|
62
79
|
|
|
80
|
+
All prompts include smart defaults, validation, and contextual suggestions!
|
|
81
|
+
|
|
63
82
|
#### Command Line Mode
|
|
64
83
|
|
|
65
84
|
```bash
|
|
@@ -94,6 +113,84 @@ universal-pwa init \
|
|
|
94
113
|
--theme-color "#2c3e50"
|
|
95
114
|
```
|
|
96
115
|
|
|
116
|
+
### PWA Install Button
|
|
117
|
+
|
|
118
|
+
The CLI automatically injects a PWA install handler into your HTML. To display an install button in your application, use the exposed global functions:
|
|
119
|
+
|
|
120
|
+
#### Available Global Functions
|
|
121
|
+
|
|
122
|
+
- `window.installPWA()` : Triggers the install prompt
|
|
123
|
+
- `window.isPWAInstalled()` : Checks if the app is already installed
|
|
124
|
+
- `window.isPWAInstallable()` : Checks if the app is installable
|
|
125
|
+
|
|
126
|
+
#### Vanilla JavaScript Example
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
// Check if installable and show a button
|
|
130
|
+
if (window.isPWAInstallable && window.isPWAInstallable()) {
|
|
131
|
+
const installButton = document.createElement('button')
|
|
132
|
+
installButton.textContent = 'Install App'
|
|
133
|
+
installButton.onclick = () => {
|
|
134
|
+
window.installPWA().catch(console.error)
|
|
135
|
+
}
|
|
136
|
+
document.body.appendChild(installButton)
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### React Example
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { useState, useEffect } from 'react'
|
|
144
|
+
|
|
145
|
+
function InstallButton() {
|
|
146
|
+
const [isInstallable, setIsInstallable] = useState(false)
|
|
147
|
+
const [isInstalled, setIsInstalled] = useState(false)
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
// Check initial state
|
|
151
|
+
if (window.isPWAInstalled) {
|
|
152
|
+
setIsInstalled(window.isPWAInstalled())
|
|
153
|
+
}
|
|
154
|
+
if (window.isPWAInstallable) {
|
|
155
|
+
setIsInstallable(window.isPWAInstallable())
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Listen to custom events
|
|
159
|
+
const handleInstallable = () => setIsInstallable(true)
|
|
160
|
+
const handleInstalled = () => {
|
|
161
|
+
setIsInstalled(true)
|
|
162
|
+
setIsInstallable(false)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
window.addEventListener('pwa-installable', handleInstallable)
|
|
166
|
+
window.addEventListener('pwa-installed', handleInstalled)
|
|
167
|
+
|
|
168
|
+
return () => {
|
|
169
|
+
window.removeEventListener('pwa-installable', handleInstallable)
|
|
170
|
+
window.removeEventListener('pwa-installed', handleInstalled)
|
|
171
|
+
}
|
|
172
|
+
}, [])
|
|
173
|
+
|
|
174
|
+
if (isInstalled || !isInstallable) {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<button onClick={() => window.installPWA?.()}>
|
|
180
|
+
Install App
|
|
181
|
+
</button>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### Custom Events
|
|
187
|
+
|
|
188
|
+
The injected script emits custom events you can listen to:
|
|
189
|
+
|
|
190
|
+
- `pwa-installable` : Emitted when the app becomes installable
|
|
191
|
+
- `pwa-installed` : Emitted after successful installation
|
|
192
|
+
- `pwa-install-choice` : Emitted with user's choice (`{ detail: { outcome: 'accepted' | 'dismissed' } }`)
|
|
193
|
+
|
|
97
194
|
### `scan` Command
|
|
98
195
|
|
|
99
196
|
Scan a project and detect framework, architecture, and assets.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/utils/environment-detector.ts
|
|
2
|
+
import { existsSync, statSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { glob } from "glob";
|
|
5
|
+
function detectEnvironment(projectPath, framework) {
|
|
6
|
+
const indicators = [];
|
|
7
|
+
let environment = "local";
|
|
8
|
+
let confidence = "low";
|
|
9
|
+
let suggestedOutputDir = "public";
|
|
10
|
+
const distDir = join(projectPath, "dist");
|
|
11
|
+
const publicDir = join(projectPath, "public");
|
|
12
|
+
const buildDir = join(projectPath, "build");
|
|
13
|
+
if (existsSync(distDir)) {
|
|
14
|
+
try {
|
|
15
|
+
const distFiles = glob.sync("**/*.{js,css,html}", {
|
|
16
|
+
cwd: distDir,
|
|
17
|
+
absolute: false,
|
|
18
|
+
maxDepth: 2
|
|
19
|
+
});
|
|
20
|
+
if (distFiles.length > 0) {
|
|
21
|
+
indicators.push(`dist/ directory exists with ${distFiles.length} built files`);
|
|
22
|
+
const recentFiles = distFiles.filter((file) => {
|
|
23
|
+
try {
|
|
24
|
+
const filePath = join(distDir, file);
|
|
25
|
+
const stats = statSync(filePath);
|
|
26
|
+
const ageInHours = (Date.now() - stats.mtimeMs) / (1e3 * 60 * 60);
|
|
27
|
+
return ageInHours < 24;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
if (recentFiles.length > 0) {
|
|
33
|
+
indicators.push(`${recentFiles.length} recent files in dist/ (last 24h)`);
|
|
34
|
+
environment = "production";
|
|
35
|
+
confidence = "high";
|
|
36
|
+
suggestedOutputDir = "dist";
|
|
37
|
+
} else {
|
|
38
|
+
indicators.push("dist/ exists but files are old (may be stale)");
|
|
39
|
+
environment = "production";
|
|
40
|
+
confidence = "medium";
|
|
41
|
+
suggestedOutputDir = "dist";
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
indicators.push("dist/ directory exists but is empty");
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (existsSync(buildDir) && environment === "local") {
|
|
50
|
+
try {
|
|
51
|
+
const buildFiles = glob.sync("**/*.{js,css,html}", {
|
|
52
|
+
cwd: buildDir,
|
|
53
|
+
absolute: false,
|
|
54
|
+
maxDepth: 2
|
|
55
|
+
});
|
|
56
|
+
if (buildFiles.length > 0) {
|
|
57
|
+
indicators.push(`build/ directory exists with ${buildFiles.length} built files`);
|
|
58
|
+
environment = "production";
|
|
59
|
+
confidence = "high";
|
|
60
|
+
suggestedOutputDir = "build";
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if ((framework === "React" || framework === "Vite") && existsSync(distDir)) {
|
|
66
|
+
if (environment === "local") {
|
|
67
|
+
environment = "production";
|
|
68
|
+
confidence = "medium";
|
|
69
|
+
suggestedOutputDir = "dist";
|
|
70
|
+
indicators.push("React/Vite project with dist/ directory (likely production)");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (environment === "local" && indicators.length === 0) {
|
|
74
|
+
indicators.push("No production build detected, using local mode");
|
|
75
|
+
confidence = "medium";
|
|
76
|
+
suggestedOutputDir = "public";
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
environment,
|
|
80
|
+
confidence,
|
|
81
|
+
indicators,
|
|
82
|
+
suggestedOutputDir
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
detectEnvironment
|
|
88
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/utils/environment-detector.ts
|
|
2
|
+
import { existsSync, statSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { glob } from "glob";
|
|
5
|
+
function detectEnvironment(projectPath, framework) {
|
|
6
|
+
const indicators = [];
|
|
7
|
+
let environment = "local";
|
|
8
|
+
let confidence = "low";
|
|
9
|
+
let suggestedOutputDir = "public";
|
|
10
|
+
const distDir = join(projectPath, "dist");
|
|
11
|
+
const buildDir = join(projectPath, "build");
|
|
12
|
+
if (existsSync(distDir)) {
|
|
13
|
+
try {
|
|
14
|
+
const distFiles = glob.sync("**/*.{js,css,html}", {
|
|
15
|
+
cwd: distDir,
|
|
16
|
+
absolute: false,
|
|
17
|
+
maxDepth: 2
|
|
18
|
+
});
|
|
19
|
+
if (distFiles.length > 0) {
|
|
20
|
+
indicators.push(`dist/ directory exists with ${distFiles.length} built files`);
|
|
21
|
+
const recentFiles = distFiles.filter((file) => {
|
|
22
|
+
try {
|
|
23
|
+
const filePath = join(distDir, file);
|
|
24
|
+
const stats = statSync(filePath);
|
|
25
|
+
const ageInHours = (Date.now() - stats.mtimeMs) / (1e3 * 60 * 60);
|
|
26
|
+
return ageInHours < 24;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
if (recentFiles.length > 0) {
|
|
32
|
+
indicators.push(`${recentFiles.length} recent files in dist/ (last 24h)`);
|
|
33
|
+
environment = "production";
|
|
34
|
+
confidence = "high";
|
|
35
|
+
suggestedOutputDir = "dist";
|
|
36
|
+
} else {
|
|
37
|
+
indicators.push("dist/ exists but files are old (may be stale)");
|
|
38
|
+
environment = "production";
|
|
39
|
+
confidence = "medium";
|
|
40
|
+
suggestedOutputDir = "dist";
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
indicators.push("dist/ directory exists but is empty");
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (existsSync(buildDir) && environment === "local") {
|
|
49
|
+
try {
|
|
50
|
+
const buildFiles = glob.sync("**/*.{js,css,html}", {
|
|
51
|
+
cwd: buildDir,
|
|
52
|
+
absolute: false,
|
|
53
|
+
maxDepth: 2
|
|
54
|
+
});
|
|
55
|
+
if (buildFiles.length > 0) {
|
|
56
|
+
indicators.push(`build/ directory exists with ${buildFiles.length} built files`);
|
|
57
|
+
environment = "production";
|
|
58
|
+
confidence = "high";
|
|
59
|
+
suggestedOutputDir = "build";
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if ((framework === "React" || framework === "Vite") && existsSync(distDir)) {
|
|
65
|
+
if (environment === "local") {
|
|
66
|
+
environment = "production";
|
|
67
|
+
confidence = "medium";
|
|
68
|
+
suggestedOutputDir = "dist";
|
|
69
|
+
indicators.push("React/Vite project with dist/ directory (likely production)");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (environment === "local" && indicators.length === 0) {
|
|
73
|
+
indicators.push("No production build detected, using local mode");
|
|
74
|
+
confidence = "medium";
|
|
75
|
+
suggestedOutputDir = "public";
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
environment,
|
|
79
|
+
confidence,
|
|
80
|
+
indicators,
|
|
81
|
+
suggestedOutputDir
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
detectEnvironment
|
|
87
|
+
};
|