@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 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 (CLI will auto-detect dist/ directory)
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
- The CLI automatically detects `dist/` directory for React/Vite projects if it exists. You can also explicitly specify it with `--output-dir dist`.
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
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ detectEnvironment
3
+ } from "./chunk-Y2M6FMPR.js";
4
+ export {
5
+ detectEnvironment
6
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ detectEnvironment
3
+ } from "./chunk-35T5DDFX.js";
4
+ export {
5
+ detectEnvironment
6
+ };