@julien-lin/universal-pwa-core 1.3.1 → 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/dist/index.cjs +1775 -108
- package/dist/index.d.cts +166 -2
- package/dist/index.d.ts +166 -2
- package/dist/index.js +1763 -106
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -35,13 +35,16 @@ __export(index_exports, {
|
|
|
35
35
|
STANDARD_SPLASH_SIZES: () => STANDARD_SPLASH_SIZES,
|
|
36
36
|
checkHttps: () => checkHttps,
|
|
37
37
|
checkProjectHttps: () => checkProjectHttps,
|
|
38
|
+
detectApiType: () => detectApiType,
|
|
38
39
|
detectArchitecture: () => detectArchitecture,
|
|
39
40
|
detectAssets: () => detectAssets,
|
|
40
41
|
detectFramework: () => detectFramework,
|
|
41
42
|
detectProjectUrl: () => detectProjectUrl,
|
|
43
|
+
detectUnoptimizedImages: () => detectUnoptimizedImages,
|
|
42
44
|
elementExists: () => elementExists,
|
|
43
45
|
findAllElements: () => findAllElements,
|
|
44
46
|
findElement: () => findElement,
|
|
47
|
+
generateAdaptiveCacheStrategies: () => generateAdaptiveCacheStrategies,
|
|
45
48
|
generateAndWriteManifest: () => generateAndWriteManifest,
|
|
46
49
|
generateAndWriteServiceWorker: () => generateAndWriteServiceWorker,
|
|
47
50
|
generateAppleTouchIcon: () => generateAppleTouchIcon,
|
|
@@ -49,40 +52,259 @@ __export(index_exports, {
|
|
|
49
52
|
generateIcons: () => generateIcons,
|
|
50
53
|
generateIconsOnly: () => generateIconsOnly,
|
|
51
54
|
generateManifest: () => generateManifest,
|
|
55
|
+
generateOptimalShortName: () => generateOptimalShortName,
|
|
52
56
|
generateReport: () => generateReport,
|
|
57
|
+
generateResponsiveImageSizes: () => generateResponsiveImageSizes,
|
|
53
58
|
generateServiceWorker: () => generateServiceWorker,
|
|
54
59
|
generateSimpleServiceWorker: () => generateSimpleServiceWorker,
|
|
55
60
|
generateSplashScreensOnly: () => generateSplashScreensOnly,
|
|
56
61
|
injectMetaTags: () => injectMetaTags,
|
|
57
62
|
injectMetaTagsInFile: () => injectMetaTagsInFile,
|
|
63
|
+
optimizeImage: () => optimizeImage,
|
|
64
|
+
optimizeProject: () => optimizeProject,
|
|
65
|
+
optimizeProjectImages: () => optimizeProjectImages,
|
|
58
66
|
parseHTML: () => parseHTML,
|
|
59
67
|
parseHTMLFile: () => parseHTMLFile,
|
|
60
68
|
scanProject: () => scanProject,
|
|
61
69
|
serializeHTML: () => serializeHTML,
|
|
70
|
+
suggestManifestColors: () => suggestManifestColors,
|
|
71
|
+
validatePWA: () => validatePWA,
|
|
62
72
|
validateProjectPath: () => validateProjectPath,
|
|
63
73
|
writeManifest: () => writeManifest
|
|
64
74
|
});
|
|
65
75
|
module.exports = __toCommonJS(index_exports);
|
|
66
76
|
|
|
67
77
|
// src/scanner/index.ts
|
|
68
|
-
var
|
|
78
|
+
var import_fs6 = require("fs");
|
|
79
|
+
var import_path6 = require("path");
|
|
69
80
|
|
|
70
81
|
// src/scanner/framework-detector.ts
|
|
71
82
|
var import_fs = require("fs");
|
|
72
83
|
var import_path = require("path");
|
|
84
|
+
var import_glob = require("glob");
|
|
85
|
+
function calculateConfidenceScore(framework, confidence, indicators) {
|
|
86
|
+
if (!framework) {
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
let baseScore = 0;
|
|
90
|
+
switch (confidence) {
|
|
91
|
+
case "high":
|
|
92
|
+
baseScore = 80;
|
|
93
|
+
break;
|
|
94
|
+
case "medium":
|
|
95
|
+
baseScore = 50;
|
|
96
|
+
break;
|
|
97
|
+
case "low":
|
|
98
|
+
baseScore = 20;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
const indicatorBonus = Math.min(indicators.length * 2, 20);
|
|
102
|
+
const specificFrameworkBonus = indicators.some(
|
|
103
|
+
(ind) => ind.includes("composer.json:") || ind.includes("package.json:") || ind.includes("Gemfile")
|
|
104
|
+
) ? 10 : 0;
|
|
105
|
+
const totalScore = baseScore + indicatorBonus + specificFrameworkBonus;
|
|
106
|
+
return Math.min(totalScore, 100);
|
|
107
|
+
}
|
|
108
|
+
function scoreToConfidence(score) {
|
|
109
|
+
if (score >= 70) return "high";
|
|
110
|
+
if (score >= 40) return "medium";
|
|
111
|
+
return "low";
|
|
112
|
+
}
|
|
113
|
+
function parseVersion(versionString) {
|
|
114
|
+
if (!versionString) return null;
|
|
115
|
+
const cleanVersion = versionString.replace(/^[\^~>=<]+\s*/, "").trim();
|
|
116
|
+
const parts = cleanVersion.split(".");
|
|
117
|
+
if (parts.length === 0) return null;
|
|
118
|
+
const major = parseInt(parts[0], 10);
|
|
119
|
+
if (isNaN(major)) return null;
|
|
120
|
+
const minor = parts[1] ? parseInt(parts[1], 10) : null;
|
|
121
|
+
const patch = parts[2] ? parseInt(parts[2], 10) : null;
|
|
122
|
+
return {
|
|
123
|
+
major,
|
|
124
|
+
minor: isNaN(minor ?? 0) ? null : minor,
|
|
125
|
+
patch: isNaN(patch ?? 0) ? null : patch,
|
|
126
|
+
raw: versionString
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function detectFrameworkVersion(framework, dependencies) {
|
|
130
|
+
switch (framework) {
|
|
131
|
+
case "react":
|
|
132
|
+
if (dependencies.react) {
|
|
133
|
+
return parseVersion(dependencies.react);
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
case "vue":
|
|
137
|
+
if (dependencies.vue) {
|
|
138
|
+
return parseVersion(dependencies.vue);
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
case "angular":
|
|
142
|
+
if (dependencies["@angular/core"]) {
|
|
143
|
+
return parseVersion(dependencies["@angular/core"]);
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
default:
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function detectProjectConfiguration(projectPath) {
|
|
152
|
+
const config = {
|
|
153
|
+
language: null,
|
|
154
|
+
cssInJs: [],
|
|
155
|
+
stateManagement: [],
|
|
156
|
+
buildTool: null
|
|
157
|
+
};
|
|
158
|
+
const tsConfigPath = (0, import_path.join)(projectPath, "tsconfig.json");
|
|
159
|
+
if ((0, import_fs.existsSync)(tsConfigPath)) {
|
|
160
|
+
config.language = "typescript";
|
|
161
|
+
} else {
|
|
162
|
+
try {
|
|
163
|
+
const tsFiles = (0, import_glob.globSync)("**/*.{ts,tsx}", {
|
|
164
|
+
cwd: projectPath,
|
|
165
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"],
|
|
166
|
+
absolute: false,
|
|
167
|
+
maxDepth: 3
|
|
168
|
+
});
|
|
169
|
+
if (tsFiles.length > 0) {
|
|
170
|
+
config.language = "typescript";
|
|
171
|
+
} else {
|
|
172
|
+
config.language = "javascript";
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
config.language = "javascript";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const packageJsonPath = (0, import_path.join)(projectPath, "package.json");
|
|
179
|
+
if ((0, import_fs.existsSync)(packageJsonPath)) {
|
|
180
|
+
try {
|
|
181
|
+
const packageContent = JSON.parse((0, import_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
182
|
+
const dependencies = {
|
|
183
|
+
...packageContent.dependencies ?? {},
|
|
184
|
+
...packageContent.devDependencies ?? {}
|
|
185
|
+
};
|
|
186
|
+
if (dependencies["styled-components"]) {
|
|
187
|
+
config.cssInJs.push("styled-components");
|
|
188
|
+
}
|
|
189
|
+
if (dependencies["@emotion/react"] || dependencies["@emotion/styled"]) {
|
|
190
|
+
config.cssInJs.push("emotion");
|
|
191
|
+
}
|
|
192
|
+
if (dependencies["@stitches/react"] || dependencies["@stitches/core"]) {
|
|
193
|
+
config.cssInJs.push("stitches");
|
|
194
|
+
}
|
|
195
|
+
if (dependencies.linaria) {
|
|
196
|
+
config.cssInJs.push("linaria");
|
|
197
|
+
}
|
|
198
|
+
if (dependencies.goober) {
|
|
199
|
+
config.cssInJs.push("goober");
|
|
200
|
+
}
|
|
201
|
+
if (dependencies.aphrodite) {
|
|
202
|
+
config.cssInJs.push("aphrodite");
|
|
203
|
+
}
|
|
204
|
+
if (dependencies.redux || dependencies["@reduxjs/toolkit"]) {
|
|
205
|
+
config.stateManagement.push("redux");
|
|
206
|
+
}
|
|
207
|
+
if (dependencies.zustand) {
|
|
208
|
+
config.stateManagement.push("zustand");
|
|
209
|
+
}
|
|
210
|
+
if (dependencies.pinia || dependencies["@pinia/core"]) {
|
|
211
|
+
config.stateManagement.push("pinia");
|
|
212
|
+
}
|
|
213
|
+
if (dependencies.mobx || dependencies["mobx-react"]) {
|
|
214
|
+
config.stateManagement.push("mobx");
|
|
215
|
+
}
|
|
216
|
+
if (dependencies.recoil) {
|
|
217
|
+
config.stateManagement.push("recoil");
|
|
218
|
+
}
|
|
219
|
+
if (dependencies.jotai) {
|
|
220
|
+
config.stateManagement.push("jotai");
|
|
221
|
+
}
|
|
222
|
+
if (dependencies.valtio) {
|
|
223
|
+
config.stateManagement.push("valtio");
|
|
224
|
+
}
|
|
225
|
+
if (dependencies["@tanstack/react-query"] || dependencies["react-query"]) {
|
|
226
|
+
config.stateManagement.push("react-query");
|
|
227
|
+
}
|
|
228
|
+
if (dependencies.vite || Object.keys(dependencies).some((key) => key.startsWith("vite-plugin-"))) {
|
|
229
|
+
config.buildTool = "vite";
|
|
230
|
+
} else if (dependencies.webpack || Object.keys(dependencies).some((key) => key.startsWith("webpack-"))) {
|
|
231
|
+
config.buildTool = "webpack";
|
|
232
|
+
} else if (dependencies.rollup || Object.keys(dependencies).some((key) => key.startsWith("rollup-plugin-"))) {
|
|
233
|
+
config.buildTool = "rollup";
|
|
234
|
+
} else if (dependencies.esbuild) {
|
|
235
|
+
config.buildTool = "esbuild";
|
|
236
|
+
} else if (dependencies.parcel || dependencies["parcel-bundler"]) {
|
|
237
|
+
config.buildTool = "parcel";
|
|
238
|
+
} else if (dependencies["@turbo/*"] || dependencies.turbo) {
|
|
239
|
+
config.buildTool = "turbopack";
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return config;
|
|
245
|
+
}
|
|
246
|
+
function createResult(framework, confidence, indicators, version = null, configuration = null) {
|
|
247
|
+
const confidenceScore = calculateConfidenceScore(framework, confidence, indicators);
|
|
248
|
+
const finalConfidence = scoreToConfidence(confidenceScore);
|
|
249
|
+
const finalConfig = configuration ?? {
|
|
250
|
+
language: null,
|
|
251
|
+
cssInJs: [],
|
|
252
|
+
stateManagement: [],
|
|
253
|
+
buildTool: null
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
framework,
|
|
257
|
+
confidence: finalConfidence,
|
|
258
|
+
confidenceScore,
|
|
259
|
+
indicators,
|
|
260
|
+
version,
|
|
261
|
+
configuration: finalConfig
|
|
262
|
+
};
|
|
263
|
+
}
|
|
73
264
|
function detectFramework(projectPath) {
|
|
74
265
|
const indicators = [];
|
|
75
266
|
let framework = null;
|
|
76
267
|
let confidence = "low";
|
|
268
|
+
const projectConfig = detectProjectConfiguration(projectPath);
|
|
77
269
|
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "wp-config.php"))) {
|
|
78
270
|
indicators.push("wp-config.php");
|
|
79
271
|
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "wp-content"))) {
|
|
80
272
|
indicators.push("wp-content/");
|
|
273
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "wp-content", "plugins", "woocommerce"))) {
|
|
274
|
+
indicators.push("wp-content/plugins/woocommerce/");
|
|
275
|
+
framework = "woocommerce";
|
|
276
|
+
confidence = "high";
|
|
277
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
278
|
+
}
|
|
81
279
|
framework = "wordpress";
|
|
82
280
|
confidence = "high";
|
|
83
|
-
return
|
|
281
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "sites")) && (0, import_fs.existsSync)((0, import_path.join)(projectPath, "modules"))) {
|
|
285
|
+
indicators.push("sites/ and modules/ (Drupal)");
|
|
286
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "themes"))) {
|
|
287
|
+
indicators.push("themes/");
|
|
288
|
+
framework = "drupal";
|
|
289
|
+
confidence = "high";
|
|
290
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "configuration.php"))) {
|
|
294
|
+
indicators.push("configuration.php (Joomla)");
|
|
295
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "administrator"))) {
|
|
296
|
+
indicators.push("administrator/");
|
|
297
|
+
framework = "joomla";
|
|
298
|
+
confidence = "high";
|
|
299
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
84
300
|
}
|
|
85
301
|
}
|
|
302
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "theme.liquid")) || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "config", "settings_schema.json"))) {
|
|
303
|
+
indicators.push("theme.liquid or config/settings_schema.json (Shopify)");
|
|
304
|
+
framework = "shopify";
|
|
305
|
+
confidence = "high";
|
|
306
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
307
|
+
}
|
|
86
308
|
const composerPath = (0, import_path.join)(projectPath, "composer.json");
|
|
87
309
|
if ((0, import_fs.existsSync)(composerPath)) {
|
|
88
310
|
try {
|
|
@@ -91,13 +313,31 @@ function detectFramework(projectPath) {
|
|
|
91
313
|
...composerContent.require ?? {},
|
|
92
314
|
...composerContent["require-dev"] ?? {}
|
|
93
315
|
};
|
|
316
|
+
if (dependencies["magento/product-community-edition"] || dependencies["magento/product-enterprise-edition"]) {
|
|
317
|
+
indicators.push("composer.json: magento/*");
|
|
318
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "app")) && (0, import_fs.existsSync)((0, import_path.join)(projectPath, "pub"))) {
|
|
319
|
+
indicators.push("app/ and pub/");
|
|
320
|
+
framework = "magento";
|
|
321
|
+
confidence = "high";
|
|
322
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (dependencies["prestashop/prestashop"]) {
|
|
326
|
+
indicators.push("composer.json: prestashop/prestashop");
|
|
327
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "config")) && (0, import_fs.existsSync)((0, import_path.join)(projectPath, "themes"))) {
|
|
328
|
+
indicators.push("config/ and themes/");
|
|
329
|
+
framework = "prestashop";
|
|
330
|
+
confidence = "high";
|
|
331
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
94
334
|
if (dependencies["symfony/symfony"] || dependencies["symfony/framework-bundle"]) {
|
|
95
335
|
indicators.push("composer.json: symfony/*");
|
|
96
336
|
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "public"))) {
|
|
97
337
|
indicators.push("public/");
|
|
98
338
|
framework = "symfony";
|
|
99
339
|
confidence = "high";
|
|
100
|
-
return
|
|
340
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
101
341
|
}
|
|
102
342
|
}
|
|
103
343
|
if (dependencies["laravel/framework"]) {
|
|
@@ -106,7 +346,43 @@ function detectFramework(projectPath) {
|
|
|
106
346
|
indicators.push("public/");
|
|
107
347
|
framework = "laravel";
|
|
108
348
|
confidence = "high";
|
|
109
|
-
return
|
|
349
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (dependencies["codeigniter4/framework"]) {
|
|
353
|
+
indicators.push("composer.json: codeigniter4/framework");
|
|
354
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "public"))) {
|
|
355
|
+
indicators.push("public/");
|
|
356
|
+
framework = "codeigniter";
|
|
357
|
+
confidence = "high";
|
|
358
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (dependencies["cakephp/cakephp"]) {
|
|
362
|
+
indicators.push("composer.json: cakephp/cakephp");
|
|
363
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "webroot")) || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "public"))) {
|
|
364
|
+
indicators.push("webroot/ or public/");
|
|
365
|
+
framework = "cakephp";
|
|
366
|
+
confidence = "high";
|
|
367
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (dependencies["yiisoft/yii2"] || dependencies["yiisoft/yii"]) {
|
|
371
|
+
indicators.push("composer.json: yiisoft/*");
|
|
372
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "web")) || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "public"))) {
|
|
373
|
+
indicators.push("web/ or public/");
|
|
374
|
+
framework = "yii";
|
|
375
|
+
confidence = "high";
|
|
376
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (dependencies["laminas/laminas-mvc"] || dependencies["laminas/laminas-component-installer"]) {
|
|
380
|
+
indicators.push("composer.json: laminas/*");
|
|
381
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "public"))) {
|
|
382
|
+
indicators.push("public/");
|
|
383
|
+
framework = "laminas";
|
|
384
|
+
confidence = "high";
|
|
385
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
110
386
|
}
|
|
111
387
|
}
|
|
112
388
|
} catch {
|
|
@@ -126,7 +402,7 @@ function detectFramework(projectPath) {
|
|
|
126
402
|
indicators.push(".next/");
|
|
127
403
|
framework = "nextjs";
|
|
128
404
|
confidence = "high";
|
|
129
|
-
return
|
|
405
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
130
406
|
}
|
|
131
407
|
}
|
|
132
408
|
if (dependencies.nuxt) {
|
|
@@ -135,19 +411,27 @@ function detectFramework(projectPath) {
|
|
|
135
411
|
indicators.push(".nuxt/");
|
|
136
412
|
framework = "nuxt";
|
|
137
413
|
confidence = "high";
|
|
138
|
-
return
|
|
414
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
139
415
|
}
|
|
140
416
|
}
|
|
141
417
|
if (dependencies.react) {
|
|
142
418
|
indicators.push("package.json: react");
|
|
143
419
|
framework = "react";
|
|
144
420
|
confidence = framework ? "high" : "medium";
|
|
421
|
+
const version = detectFrameworkVersion("react", dependencies);
|
|
422
|
+
if (version) {
|
|
423
|
+
indicators.push(`React version: ${version.major}.${version.minor ?? "x"}.${version.patch ?? "x"}`);
|
|
424
|
+
}
|
|
145
425
|
}
|
|
146
426
|
if (dependencies.vue) {
|
|
147
427
|
indicators.push("package.json: vue");
|
|
148
428
|
if (!framework) {
|
|
149
429
|
framework = "vue";
|
|
150
430
|
confidence = "high";
|
|
431
|
+
const version = detectFrameworkVersion("vue", dependencies);
|
|
432
|
+
if (version) {
|
|
433
|
+
indicators.push(`Vue version: ${version.major}.${version.minor ?? "x"}.${version.patch ?? "x"}`);
|
|
434
|
+
}
|
|
151
435
|
}
|
|
152
436
|
}
|
|
153
437
|
if (dependencies["@angular/core"]) {
|
|
@@ -155,11 +439,274 @@ function detectFramework(projectPath) {
|
|
|
155
439
|
if (!framework) {
|
|
156
440
|
framework = "angular";
|
|
157
441
|
confidence = "high";
|
|
442
|
+
const version = detectFrameworkVersion("angular", dependencies);
|
|
443
|
+
if (version) {
|
|
444
|
+
indicators.push(`Angular version: ${version.major}.${version.minor ?? "x"}.${version.patch ?? "x"}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (dependencies["@sveltejs/kit"]) {
|
|
449
|
+
indicators.push("package.json: @sveltejs/kit");
|
|
450
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, ".svelte-kit"))) {
|
|
451
|
+
indicators.push(".svelte-kit/");
|
|
452
|
+
framework = "sveltekit";
|
|
453
|
+
confidence = "high";
|
|
454
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (dependencies.svelte && !dependencies["@sveltejs/kit"]) {
|
|
458
|
+
indicators.push("package.json: svelte");
|
|
459
|
+
if (!framework) {
|
|
460
|
+
framework = "svelte";
|
|
461
|
+
confidence = "high";
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (dependencies["@remix-run/node"] || dependencies["@remix-run/react"]) {
|
|
465
|
+
indicators.push("package.json: @remix-run/*");
|
|
466
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "app"))) {
|
|
467
|
+
indicators.push("app/");
|
|
468
|
+
framework = "remix";
|
|
469
|
+
confidence = "high";
|
|
470
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (dependencies.astro) {
|
|
474
|
+
indicators.push("package.json: astro");
|
|
475
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, ".astro"))) {
|
|
476
|
+
indicators.push(".astro/");
|
|
477
|
+
framework = "astro";
|
|
478
|
+
confidence = "high";
|
|
479
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (dependencies["solid-js"]) {
|
|
483
|
+
indicators.push("package.json: solid-js");
|
|
484
|
+
if (!framework) {
|
|
485
|
+
framework = "solidjs";
|
|
486
|
+
confidence = "high";
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} catch {
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (!framework) {
|
|
493
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "manage.py")) && (0, import_fs.existsSync)((0, import_path.join)(projectPath, "settings.py"))) {
|
|
494
|
+
indicators.push("manage.py and settings.py (Django)");
|
|
495
|
+
framework = "django";
|
|
496
|
+
confidence = "high";
|
|
497
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
498
|
+
}
|
|
499
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "manage.py")) && (0, import_fs.existsSync)((0, import_path.join)(projectPath, "django"))) {
|
|
500
|
+
indicators.push("manage.py and django/ (Django)");
|
|
501
|
+
framework = "django";
|
|
502
|
+
confidence = "high";
|
|
503
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
504
|
+
}
|
|
505
|
+
const requirementsPath = (0, import_path.join)(projectPath, "requirements.txt");
|
|
506
|
+
if ((0, import_fs.existsSync)(requirementsPath)) {
|
|
507
|
+
try {
|
|
508
|
+
const requirementsContent = (0, import_fs.readFileSync)(requirementsPath, "utf-8");
|
|
509
|
+
if (requirementsContent.includes("Flask") || requirementsContent.includes("flask")) {
|
|
510
|
+
indicators.push("requirements.txt: Flask");
|
|
511
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "app.py")) || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "application.py"))) {
|
|
512
|
+
indicators.push("app.py or application.py");
|
|
513
|
+
framework = "flask";
|
|
514
|
+
confidence = "high";
|
|
515
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (requirementsContent.includes("fastapi") || requirementsContent.includes("FastAPI")) {
|
|
519
|
+
indicators.push("requirements.txt: FastAPI");
|
|
520
|
+
framework = "fastapi";
|
|
521
|
+
confidence = "high";
|
|
522
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
523
|
+
}
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (!framework) {
|
|
529
|
+
const gemfilePath = (0, import_path.join)(projectPath, "Gemfile");
|
|
530
|
+
if ((0, import_fs.existsSync)(gemfilePath)) {
|
|
531
|
+
try {
|
|
532
|
+
const gemfileContent = (0, import_fs.readFileSync)(gemfilePath, "utf-8");
|
|
533
|
+
if (gemfileContent.includes("gem 'rails'") || gemfileContent.includes('gem "rails"')) {
|
|
534
|
+
indicators.push("Gemfile: rails");
|
|
535
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "config", "application.rb")) || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "config", "routes.rb"))) {
|
|
536
|
+
indicators.push("config/application.rb or config/routes.rb");
|
|
537
|
+
framework = "rails";
|
|
538
|
+
confidence = "high";
|
|
539
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (gemfileContent.includes("gem 'sinatra'") || gemfileContent.includes('gem "sinatra"')) {
|
|
543
|
+
indicators.push("Gemfile: sinatra");
|
|
544
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "app.rb")) || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "main.rb"))) {
|
|
545
|
+
indicators.push("app.rb or main.rb");
|
|
546
|
+
framework = "sinatra";
|
|
547
|
+
confidence = "high";
|
|
548
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (!framework) {
|
|
556
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "go.mod"))) {
|
|
557
|
+
try {
|
|
558
|
+
const goModContent = (0, import_fs.readFileSync)((0, import_path.join)(projectPath, "go.mod"), "utf-8");
|
|
559
|
+
if (goModContent.includes("net/http") || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "main.go")) || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "server.go"))) {
|
|
560
|
+
const mainGoPath = (0, import_path.join)(projectPath, "main.go");
|
|
561
|
+
if ((0, import_fs.existsSync)(mainGoPath)) {
|
|
562
|
+
try {
|
|
563
|
+
const mainGoContent = (0, import_fs.readFileSync)(mainGoPath, "utf-8");
|
|
564
|
+
if (mainGoContent.includes("http.ListenAndServe") || mainGoContent.includes("http.Server")) {
|
|
565
|
+
indicators.push("go.mod and main.go with HTTP server");
|
|
566
|
+
framework = "go";
|
|
567
|
+
confidence = "high";
|
|
568
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
569
|
+
}
|
|
570
|
+
} catch {
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const serverGoPath = (0, import_path.join)(projectPath, "server.go");
|
|
574
|
+
if ((0, import_fs.existsSync)(serverGoPath)) {
|
|
575
|
+
try {
|
|
576
|
+
const serverGoContent = (0, import_fs.readFileSync)(serverGoPath, "utf-8");
|
|
577
|
+
if (serverGoContent.includes("http.ListenAndServe") || serverGoContent.includes("http.Server")) {
|
|
578
|
+
indicators.push("go.mod and server.go with HTTP server");
|
|
579
|
+
framework = "go";
|
|
580
|
+
confidence = "high";
|
|
581
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
582
|
+
}
|
|
583
|
+
} catch {
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (!framework) {
|
|
592
|
+
const pomXmlPath = (0, import_path.join)(projectPath, "pom.xml");
|
|
593
|
+
const buildGradlePath = (0, import_path.join)(projectPath, "build.gradle");
|
|
594
|
+
if ((0, import_fs.existsSync)(pomXmlPath)) {
|
|
595
|
+
try {
|
|
596
|
+
const pomContent = (0, import_fs.readFileSync)(pomXmlPath, "utf-8");
|
|
597
|
+
if (pomContent.includes("spring-boot-starter") || pomContent.includes("org.springframework.boot")) {
|
|
598
|
+
indicators.push("pom.xml: spring-boot");
|
|
599
|
+
framework = "spring";
|
|
600
|
+
confidence = "high";
|
|
601
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
602
|
+
}
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if ((0, import_fs.existsSync)(buildGradlePath)) {
|
|
607
|
+
try {
|
|
608
|
+
const gradleContent = (0, import_fs.readFileSync)(buildGradlePath, "utf-8");
|
|
609
|
+
if (gradleContent.includes("spring-boot") || gradleContent.includes("org.springframework.boot")) {
|
|
610
|
+
indicators.push("build.gradle: spring-boot");
|
|
611
|
+
framework = "spring";
|
|
612
|
+
confidence = "high";
|
|
613
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
614
|
+
}
|
|
615
|
+
} catch {
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (!framework) {
|
|
620
|
+
try {
|
|
621
|
+
const csprojMatches = (0, import_glob.globSync)("*.csproj", { cwd: projectPath, absolute: false });
|
|
622
|
+
if (csprojMatches.length > 0) {
|
|
623
|
+
const firstCsproj = (0, import_path.join)(projectPath, csprojMatches[0]);
|
|
624
|
+
try {
|
|
625
|
+
const csprojContent = (0, import_fs.readFileSync)(firstCsproj, "utf-8");
|
|
626
|
+
if (csprojContent.includes("Microsoft.AspNetCore") || csprojContent.includes("Microsoft.NET.Sdk.Web")) {
|
|
627
|
+
indicators.push(`${csprojMatches[0]}: ASP.NET Core`);
|
|
628
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "Program.cs")) || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "Startup.cs"))) {
|
|
629
|
+
indicators.push("Program.cs or Startup.cs");
|
|
630
|
+
framework = "aspnet";
|
|
631
|
+
confidence = "high";
|
|
632
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
158
636
|
}
|
|
159
637
|
}
|
|
160
638
|
} catch {
|
|
161
639
|
}
|
|
162
640
|
}
|
|
641
|
+
if (!framework) {
|
|
642
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "_config.yml"))) {
|
|
643
|
+
indicators.push("_config.yml (Jekyll)");
|
|
644
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "_posts"))) {
|
|
645
|
+
indicators.push("_posts/");
|
|
646
|
+
framework = "jekyll";
|
|
647
|
+
confidence = "high";
|
|
648
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
649
|
+
}
|
|
650
|
+
framework = "jekyll";
|
|
651
|
+
confidence = "medium";
|
|
652
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
653
|
+
}
|
|
654
|
+
const hugoConfigFiles = ["config.toml", "config.yaml", "config.yml", "hugo.toml", "hugo.yaml", "hugo.yml"];
|
|
655
|
+
const hasHugoConfig = hugoConfigFiles.some((file) => (0, import_fs.existsSync)((0, import_path.join)(projectPath, file)));
|
|
656
|
+
if (hasHugoConfig) {
|
|
657
|
+
indicators.push("Hugo config file found");
|
|
658
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "content")) && (0, import_fs.existsSync)((0, import_path.join)(projectPath, "layouts"))) {
|
|
659
|
+
indicators.push("content/ and layouts/");
|
|
660
|
+
framework = "hugo";
|
|
661
|
+
confidence = "high";
|
|
662
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
663
|
+
}
|
|
664
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "content"))) {
|
|
665
|
+
indicators.push("content/");
|
|
666
|
+
framework = "hugo";
|
|
667
|
+
confidence = "high";
|
|
668
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "gatsby-config.js")) || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "gatsby-config.ts"))) {
|
|
672
|
+
indicators.push("gatsby-config.js/ts (Gatsby)");
|
|
673
|
+
framework = "gatsby";
|
|
674
|
+
confidence = "high";
|
|
675
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
676
|
+
}
|
|
677
|
+
const eleventyFiles = [".eleventy.js", ".eleventy.cjs", "eleventy.config.js", "eleventy.config.cjs"];
|
|
678
|
+
const hasEleventyConfig = eleventyFiles.some((file) => (0, import_fs.existsSync)((0, import_path.join)(projectPath, file)));
|
|
679
|
+
if (hasEleventyConfig) {
|
|
680
|
+
indicators.push("Eleventy config file found");
|
|
681
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "_data"))) {
|
|
682
|
+
indicators.push("_data/");
|
|
683
|
+
framework = "eleventy";
|
|
684
|
+
confidence = "high";
|
|
685
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
686
|
+
}
|
|
687
|
+
framework = "eleventy";
|
|
688
|
+
confidence = "high";
|
|
689
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
690
|
+
}
|
|
691
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "vitepress.config.js")) || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "vitepress.config.ts"))) {
|
|
692
|
+
indicators.push("vitepress.config.js/ts (VitePress)");
|
|
693
|
+
framework = "vitepress";
|
|
694
|
+
confidence = "high";
|
|
695
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
696
|
+
}
|
|
697
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "docs", ".vitepress", "config.js")) || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "docs", ".vitepress", "config.ts"))) {
|
|
698
|
+
indicators.push("docs/.vitepress/config.js/ts (VitePress)");
|
|
699
|
+
framework = "vitepress";
|
|
700
|
+
confidence = "high";
|
|
701
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
702
|
+
}
|
|
703
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(projectPath, "docusaurus.config.js")) || (0, import_fs.existsSync)((0, import_path.join)(projectPath, "docusaurus.config.ts"))) {
|
|
704
|
+
indicators.push("docusaurus.config.js/ts (Docusaurus)");
|
|
705
|
+
framework = "docusaurus";
|
|
706
|
+
confidence = "high";
|
|
707
|
+
return createResult(framework, confidence, indicators, null, projectConfig);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
163
710
|
if (!framework) {
|
|
164
711
|
const htmlFiles = ["index.html", "index.htm"];
|
|
165
712
|
const hasHtml = htmlFiles.some((file) => (0, import_fs.existsSync)((0, import_path.join)(projectPath, file)));
|
|
@@ -169,11 +716,38 @@ function detectFramework(projectPath) {
|
|
|
169
716
|
confidence = "medium";
|
|
170
717
|
}
|
|
171
718
|
}
|
|
172
|
-
|
|
719
|
+
let detectedVersion = null;
|
|
720
|
+
if (framework && (framework === "react" || framework === "vue" || framework === "angular")) {
|
|
721
|
+
const packageJsonPath2 = (0, import_path.join)(projectPath, "package.json");
|
|
722
|
+
if ((0, import_fs.existsSync)(packageJsonPath2)) {
|
|
723
|
+
try {
|
|
724
|
+
const packageContent = JSON.parse((0, import_fs.readFileSync)(packageJsonPath2, "utf-8"));
|
|
725
|
+
const dependencies = {
|
|
726
|
+
...packageContent.dependencies ?? {},
|
|
727
|
+
...packageContent.devDependencies ?? {}
|
|
728
|
+
};
|
|
729
|
+
detectedVersion = detectFrameworkVersion(framework, dependencies);
|
|
730
|
+
} catch {
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (projectConfig.language === "typescript") {
|
|
735
|
+
indicators.push("TypeScript detected");
|
|
736
|
+
}
|
|
737
|
+
if (projectConfig.cssInJs.length > 0) {
|
|
738
|
+
indicators.push(`CSS-in-JS: ${projectConfig.cssInJs.join(", ")}`);
|
|
739
|
+
}
|
|
740
|
+
if (projectConfig.stateManagement.length > 0) {
|
|
741
|
+
indicators.push(`State Management: ${projectConfig.stateManagement.join(", ")}`);
|
|
742
|
+
}
|
|
743
|
+
if (projectConfig.buildTool) {
|
|
744
|
+
indicators.push(`Build Tool: ${projectConfig.buildTool}`);
|
|
745
|
+
}
|
|
746
|
+
return createResult(framework, confidence, indicators, detectedVersion, projectConfig);
|
|
173
747
|
}
|
|
174
748
|
|
|
175
749
|
// src/scanner/asset-detector.ts
|
|
176
|
-
var
|
|
750
|
+
var import_glob2 = require("glob");
|
|
177
751
|
var import_path2 = require("path");
|
|
178
752
|
var import_fs2 = require("fs");
|
|
179
753
|
var IGNORED_PATTERNS = [
|
|
@@ -208,13 +782,13 @@ async function detectAssets(projectPath) {
|
|
|
208
782
|
const jsPattern = `**/*.{${jsExtensionsStr}}`;
|
|
209
783
|
const jsRootPattern = `*.{${jsExtensionsStr}}`;
|
|
210
784
|
const [jsFiles, jsRootFiles] = await Promise.all([
|
|
211
|
-
(0,
|
|
785
|
+
(0, import_glob2.glob)(jsPattern, {
|
|
212
786
|
cwd: projectPath,
|
|
213
787
|
ignore: IGNORED_PATTERNS,
|
|
214
788
|
absolute: true,
|
|
215
789
|
nodir: true
|
|
216
790
|
}),
|
|
217
|
-
(0,
|
|
791
|
+
(0, import_glob2.glob)(jsRootPattern, {
|
|
218
792
|
cwd: projectPath,
|
|
219
793
|
ignore: IGNORED_PATTERNS,
|
|
220
794
|
absolute: true,
|
|
@@ -225,14 +799,14 @@ async function detectAssets(projectPath) {
|
|
|
225
799
|
result.javascript.push(...allJsFiles);
|
|
226
800
|
const cssExtensionsStr = CSS_EXTENSIONS.map((ext) => ext.slice(1)).join(",");
|
|
227
801
|
const cssPattern = `**/*.{${cssExtensionsStr}}`;
|
|
228
|
-
const cssFiles = await (0,
|
|
802
|
+
const cssFiles = await (0, import_glob2.glob)(cssPattern, {
|
|
229
803
|
cwd: projectPath,
|
|
230
804
|
ignore: IGNORED_PATTERNS,
|
|
231
805
|
absolute: false,
|
|
232
806
|
nodir: true
|
|
233
807
|
});
|
|
234
808
|
const cssRootPattern = `*.{${cssExtensionsStr}}`;
|
|
235
|
-
const cssRootFiles = await (0,
|
|
809
|
+
const cssRootFiles = await (0, import_glob2.glob)(cssRootPattern, {
|
|
236
810
|
cwd: projectPath,
|
|
237
811
|
ignore: IGNORED_PATTERNS,
|
|
238
812
|
absolute: false,
|
|
@@ -243,7 +817,7 @@ async function detectAssets(projectPath) {
|
|
|
243
817
|
const imagePatterns = IMAGE_EXTENSIONS.flatMap((ext) => [`**/*${ext}`, `*${ext}`]);
|
|
244
818
|
const allImageFiles = /* @__PURE__ */ new Set();
|
|
245
819
|
for (const pattern of imagePatterns) {
|
|
246
|
-
const files = await (0,
|
|
820
|
+
const files = await (0, import_glob2.glob)(pattern, {
|
|
247
821
|
cwd: projectPath,
|
|
248
822
|
ignore: IGNORED_PATTERNS,
|
|
249
823
|
absolute: false,
|
|
@@ -255,7 +829,7 @@ async function detectAssets(projectPath) {
|
|
|
255
829
|
const fontPatterns = FONT_EXTENSIONS.flatMap((ext) => [`**/*${ext}`, `*${ext}`]);
|
|
256
830
|
const allFontFiles = /* @__PURE__ */ new Set();
|
|
257
831
|
for (const pattern of fontPatterns) {
|
|
258
|
-
const files = await (0,
|
|
832
|
+
const files = await (0, import_glob2.glob)(pattern, {
|
|
259
833
|
cwd: projectPath,
|
|
260
834
|
ignore: IGNORED_PATTERNS,
|
|
261
835
|
absolute: false,
|
|
@@ -264,7 +838,7 @@ async function detectAssets(projectPath) {
|
|
|
264
838
|
files.forEach((f) => allFontFiles.add(f));
|
|
265
839
|
}
|
|
266
840
|
result.fonts.push(...Array.from(allFontFiles).map((f) => (0, import_path2.join)(projectPath, f)));
|
|
267
|
-
const routeFiles = await (0,
|
|
841
|
+
const routeFiles = await (0, import_glob2.glob)(`**/*{route,api,graphql}*.{js,ts,json}`, {
|
|
268
842
|
cwd: projectPath,
|
|
269
843
|
ignore: IGNORED_PATTERNS,
|
|
270
844
|
absolute: false,
|
|
@@ -307,7 +881,7 @@ async function detectAssets(projectPath) {
|
|
|
307
881
|
// src/scanner/architecture-detector.ts
|
|
308
882
|
var import_fs3 = require("fs");
|
|
309
883
|
var import_path3 = require("path");
|
|
310
|
-
var
|
|
884
|
+
var import_glob3 = require("glob");
|
|
311
885
|
async function detectArchitecture(projectPath) {
|
|
312
886
|
const indicators = [];
|
|
313
887
|
let architecture = "static";
|
|
@@ -354,6 +928,26 @@ async function detectArchitecture(projectPath) {
|
|
|
354
928
|
architecture = "ssr";
|
|
355
929
|
confidence = "high";
|
|
356
930
|
indicators.push("Nuxt detected \u2192 SSR");
|
|
931
|
+
} else if (dependencies["@sveltejs/kit"] || packageContent.dependencies?.["@sveltejs/kit"] || packageContent.devDependencies?.["@sveltejs/kit"]) {
|
|
932
|
+
architecture = "ssr";
|
|
933
|
+
confidence = "high";
|
|
934
|
+
indicators.push("SvelteKit detected \u2192 SSR");
|
|
935
|
+
} else if (dependencies["@remix-run/node"] || dependencies["@remix-run/react"] || packageContent.dependencies?.["@remix-run/node"] || packageContent.devDependencies?.["@remix-run/node"]) {
|
|
936
|
+
architecture = "ssr";
|
|
937
|
+
confidence = "high";
|
|
938
|
+
indicators.push("Remix detected \u2192 SSR");
|
|
939
|
+
} else if (dependencies.astro || packageContent.dependencies?.["astro"] || packageContent.devDependencies?.["astro"]) {
|
|
940
|
+
architecture = "ssr";
|
|
941
|
+
confidence = "high";
|
|
942
|
+
indicators.push("Astro detected \u2192 SSR");
|
|
943
|
+
} else if (dependencies.svelte && !dependencies["@sveltejs/kit"]) {
|
|
944
|
+
architecture = "spa";
|
|
945
|
+
confidence = "high";
|
|
946
|
+
indicators.push("Svelte detected \u2192 SPA");
|
|
947
|
+
} else if (dependencies["solid-js"] || packageContent.dependencies?.["solid-js"] || packageContent.devDependencies?.["solid-js"]) {
|
|
948
|
+
architecture = "spa";
|
|
949
|
+
confidence = "high";
|
|
950
|
+
indicators.push("SolidJS detected \u2192 SPA");
|
|
357
951
|
}
|
|
358
952
|
} catch {
|
|
359
953
|
}
|
|
@@ -361,7 +955,7 @@ async function detectArchitecture(projectPath) {
|
|
|
361
955
|
const htmlPatterns = ["**/*.html", "*.html", "index.html"];
|
|
362
956
|
const allHtmlFiles = /* @__PURE__ */ new Set();
|
|
363
957
|
for (const pattern of htmlPatterns) {
|
|
364
|
-
const files = await (0,
|
|
958
|
+
const files = await (0, import_glob3.glob)(pattern, {
|
|
365
959
|
cwd: projectPath,
|
|
366
960
|
ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/.nuxt/**"],
|
|
367
961
|
absolute: false,
|
|
@@ -443,68 +1037,638 @@ async function detectArchitecture(projectPath) {
|
|
|
443
1037
|
} catch {
|
|
444
1038
|
}
|
|
445
1039
|
}
|
|
446
|
-
const jsFiles = await (0,
|
|
447
|
-
cwd: projectPath,
|
|
448
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/.nuxt/**", "**/*.test.*", "**/*.spec.*"],
|
|
449
|
-
absolute: false,
|
|
450
|
-
nodir: true
|
|
451
|
-
});
|
|
452
|
-
if (jsFiles.length > 0 && architecture === "static") {
|
|
453
|
-
let routerFilesFound = 0;
|
|
454
|
-
for (const jsFile of jsFiles.slice(0, 10)) {
|
|
455
|
-
try {
|
|
456
|
-
const jsPath = (0, import_path3.join)(projectPath, jsFile);
|
|
457
|
-
const jsContent = (0, import_fs3.readFileSync)(jsPath, "utf-8").toLowerCase();
|
|
458
|
-
const routerPatterns = [
|
|
459
|
-
/react-router/i,
|
|
460
|
-
/vue-router/i,
|
|
461
|
-
/@angular\/router/i,
|
|
462
|
-
/next\/router/i,
|
|
463
|
-
/nuxt/i,
|
|
464
|
-
/createBrowserRouter/i,
|
|
465
|
-
/BrowserRouter/i,
|
|
466
|
-
/Router/i
|
|
467
|
-
];
|
|
468
|
-
if (routerPatterns.some((pattern) => pattern.test(jsContent))) {
|
|
469
|
-
routerFilesFound++;
|
|
470
|
-
}
|
|
471
|
-
} catch {
|
|
1040
|
+
const jsFiles = await (0, import_glob3.glob)("**/*.{js,ts,tsx,jsx}", {
|
|
1041
|
+
cwd: projectPath,
|
|
1042
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/.nuxt/**", "**/*.test.*", "**/*.spec.*"],
|
|
1043
|
+
absolute: false,
|
|
1044
|
+
nodir: true
|
|
1045
|
+
});
|
|
1046
|
+
if (jsFiles.length > 0 && architecture === "static") {
|
|
1047
|
+
let routerFilesFound = 0;
|
|
1048
|
+
for (const jsFile of jsFiles.slice(0, 10)) {
|
|
1049
|
+
try {
|
|
1050
|
+
const jsPath = (0, import_path3.join)(projectPath, jsFile);
|
|
1051
|
+
const jsContent = (0, import_fs3.readFileSync)(jsPath, "utf-8").toLowerCase();
|
|
1052
|
+
const routerPatterns = [
|
|
1053
|
+
/react-router/i,
|
|
1054
|
+
/vue-router/i,
|
|
1055
|
+
/@angular\/router/i,
|
|
1056
|
+
/next\/router/i,
|
|
1057
|
+
/nuxt/i,
|
|
1058
|
+
/createBrowserRouter/i,
|
|
1059
|
+
/BrowserRouter/i,
|
|
1060
|
+
/Router/i
|
|
1061
|
+
];
|
|
1062
|
+
if (routerPatterns.some((pattern) => pattern.test(jsContent))) {
|
|
1063
|
+
routerFilesFound++;
|
|
1064
|
+
}
|
|
1065
|
+
} catch {
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (routerFilesFound > 0) {
|
|
1069
|
+
indicators.push(`JS: router patterns found (${routerFilesFound} files)`);
|
|
1070
|
+
if (architecture === "static") {
|
|
1071
|
+
architecture = "spa";
|
|
1072
|
+
confidence = confidence === "low" ? "medium" : confidence;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return { architecture, buildTool, confidence, indicators };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/scanner/cache.ts
|
|
1080
|
+
var import_fs4 = require("fs");
|
|
1081
|
+
var import_path4 = require("path");
|
|
1082
|
+
var import_crypto = require("crypto");
|
|
1083
|
+
var DEFAULT_TTL = 24 * 60 * 60;
|
|
1084
|
+
var CACHE_VERSION = "1.0.0";
|
|
1085
|
+
var DEFAULT_CACHE_FILE = ".universal-pwa-cache.json";
|
|
1086
|
+
function hashFile(filePath) {
|
|
1087
|
+
try {
|
|
1088
|
+
if (!(0, import_fs4.existsSync)(filePath)) {
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
const content = (0, import_fs4.readFileSync)(filePath, "utf-8");
|
|
1092
|
+
return (0, import_crypto.createHash)("md5").update(content).digest("hex");
|
|
1093
|
+
} catch {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
function hashDirectory(projectPath) {
|
|
1098
|
+
const hashes = {};
|
|
1099
|
+
const keyFiles = [
|
|
1100
|
+
"package.json",
|
|
1101
|
+
"composer.json",
|
|
1102
|
+
"tsconfig.json",
|
|
1103
|
+
"vite.config.js",
|
|
1104
|
+
"vite.config.ts",
|
|
1105
|
+
"webpack.config.js",
|
|
1106
|
+
"rollup.config.js",
|
|
1107
|
+
"next.config.js",
|
|
1108
|
+
"nuxt.config.js",
|
|
1109
|
+
"angular.json",
|
|
1110
|
+
"wp-config.php",
|
|
1111
|
+
"composer.lock",
|
|
1112
|
+
"package-lock.json",
|
|
1113
|
+
"yarn.lock",
|
|
1114
|
+
"pnpm-lock.yaml"
|
|
1115
|
+
];
|
|
1116
|
+
for (const file of keyFiles) {
|
|
1117
|
+
const filePath = (0, import_path4.join)(projectPath, file);
|
|
1118
|
+
const hash = hashFile(filePath);
|
|
1119
|
+
if (hash) {
|
|
1120
|
+
hashes[file] = hash;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return hashes;
|
|
1124
|
+
}
|
|
1125
|
+
function loadCache(cacheFile = DEFAULT_CACHE_FILE) {
|
|
1126
|
+
try {
|
|
1127
|
+
if (!(0, import_fs4.existsSync)(cacheFile)) {
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
const content = (0, import_fs4.readFileSync)(cacheFile, "utf-8");
|
|
1131
|
+
const parsed = JSON.parse(content);
|
|
1132
|
+
const cache = parsed;
|
|
1133
|
+
if (cache.version !== CACHE_VERSION) {
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
return cache;
|
|
1137
|
+
} catch {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
function saveCache(cache, cacheFile = DEFAULT_CACHE_FILE) {
|
|
1142
|
+
try {
|
|
1143
|
+
(0, import_fs4.writeFileSync)(cacheFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1146
|
+
console.warn(`Failed to save cache: ${message}`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function generateCacheKey(projectPath) {
|
|
1150
|
+
const normalizedPath = projectPath.replace(/\/$/, "");
|
|
1151
|
+
return (0, import_crypto.createHash)("md5").update(normalizedPath).digest("hex");
|
|
1152
|
+
}
|
|
1153
|
+
function isCacheValid(projectPath, cache, options = {}) {
|
|
1154
|
+
if (!cache || options.force) {
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
const cacheKey = generateCacheKey(projectPath);
|
|
1158
|
+
const entry = cache.entries[cacheKey];
|
|
1159
|
+
if (!entry) {
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
const entryTimestamp = new Date(entry.timestamp).getTime();
|
|
1163
|
+
const now = Date.now();
|
|
1164
|
+
const age = (now - entryTimestamp) / 1e3;
|
|
1165
|
+
if (age > (options.ttl ?? entry.ttl ?? DEFAULT_TTL)) {
|
|
1166
|
+
return false;
|
|
1167
|
+
}
|
|
1168
|
+
const currentHashes = hashDirectory(projectPath);
|
|
1169
|
+
const cachedHashes = entry.fileHashes;
|
|
1170
|
+
for (const [file, hash] of Object.entries(currentHashes)) {
|
|
1171
|
+
if (cachedHashes[file] !== hash) {
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
for (const file of Object.keys(cachedHashes)) {
|
|
1176
|
+
if (!(file in currentHashes) && (0, import_fs4.existsSync)((0, import_path4.join)(projectPath, file))) {
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
function getCachedResult(projectPath, cache) {
|
|
1183
|
+
if (!cache) {
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
const cacheKey = generateCacheKey(projectPath);
|
|
1187
|
+
const entry = cache.entries[cacheKey];
|
|
1188
|
+
if (!entry) {
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
return entry.result;
|
|
1192
|
+
}
|
|
1193
|
+
function updateCache(projectPath, result, cache, options = {}) {
|
|
1194
|
+
const cacheKey = generateCacheKey(projectPath);
|
|
1195
|
+
const fileHashes = hashDirectory(projectPath);
|
|
1196
|
+
const ttl = options.ttl ?? DEFAULT_TTL;
|
|
1197
|
+
const newEntry = {
|
|
1198
|
+
result,
|
|
1199
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1200
|
+
fileHashes,
|
|
1201
|
+
ttl
|
|
1202
|
+
};
|
|
1203
|
+
const updatedCache = cache || {
|
|
1204
|
+
version: CACHE_VERSION,
|
|
1205
|
+
entries: {}
|
|
1206
|
+
};
|
|
1207
|
+
updatedCache.entries[cacheKey] = newEntry;
|
|
1208
|
+
return updatedCache;
|
|
1209
|
+
}
|
|
1210
|
+
function cleanCache(cache, ttl = DEFAULT_TTL) {
|
|
1211
|
+
if (!cache) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
const now = Date.now();
|
|
1215
|
+
const cleanedEntries = {};
|
|
1216
|
+
for (const [key, entry] of Object.entries(cache.entries)) {
|
|
1217
|
+
const entryTimestamp = new Date(entry.timestamp).getTime();
|
|
1218
|
+
const age = (now - entryTimestamp) / 1e3;
|
|
1219
|
+
if (age <= (entry.ttl ?? ttl)) {
|
|
1220
|
+
cleanedEntries[key] = entry;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (Object.keys(cleanedEntries).length === 0) {
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
return {
|
|
1227
|
+
version: cache.version,
|
|
1228
|
+
entries: cleanedEntries
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// src/scanner/optimizer.ts
|
|
1233
|
+
var import_fs5 = require("fs");
|
|
1234
|
+
var import_path5 = require("path");
|
|
1235
|
+
var import_sharp = __toESM(require("sharp"), 1);
|
|
1236
|
+
function detectApiType(projectPath, assets) {
|
|
1237
|
+
const indicators = [];
|
|
1238
|
+
let hasRest = false;
|
|
1239
|
+
let hasGraphQL = false;
|
|
1240
|
+
const graphqlIndicators = [
|
|
1241
|
+
"graphql",
|
|
1242
|
+
"gql",
|
|
1243
|
+
"apollo",
|
|
1244
|
+
"relay",
|
|
1245
|
+
"urql"
|
|
1246
|
+
];
|
|
1247
|
+
const packageJsonPath = (0, import_path5.join)(projectPath, "package.json");
|
|
1248
|
+
if ((0, import_fs5.existsSync)(packageJsonPath)) {
|
|
1249
|
+
try {
|
|
1250
|
+
const packageContent = JSON.parse((0, import_fs5.readFileSync)(packageJsonPath, "utf-8"));
|
|
1251
|
+
const dependencies = {
|
|
1252
|
+
...packageContent.dependencies ?? {},
|
|
1253
|
+
...packageContent.devDependencies ?? {}
|
|
1254
|
+
};
|
|
1255
|
+
if (dependencies["graphql"] || dependencies["apollo-client"] || dependencies["@apollo/client"] || dependencies["relay-runtime"] || dependencies["urql"]) {
|
|
1256
|
+
hasGraphQL = true;
|
|
1257
|
+
indicators.push("GraphQL library detected");
|
|
1258
|
+
}
|
|
1259
|
+
} catch {
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (assets.apiRoutes.length > 0) {
|
|
1263
|
+
hasRest = assets.apiRoutes.some((route) => route.includes("/api/") || route.includes("/rest/"));
|
|
1264
|
+
if (assets.apiRoutes.some((route) => route.includes("/graphql"))) {
|
|
1265
|
+
hasGraphQL = true;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
if (assets.javascript.length > 0) {
|
|
1269
|
+
const jsContent = assets.javascript.slice(0, 10).map((file) => {
|
|
1270
|
+
try {
|
|
1271
|
+
return (0, import_fs5.readFileSync)(file, "utf-8").toLowerCase();
|
|
1272
|
+
} catch {
|
|
1273
|
+
return "";
|
|
1274
|
+
}
|
|
1275
|
+
}).join(" ");
|
|
1276
|
+
if (graphqlIndicators.some((ind) => jsContent.includes(ind))) {
|
|
1277
|
+
hasGraphQL = true;
|
|
1278
|
+
indicators.push("GraphQL usage in code");
|
|
1279
|
+
}
|
|
1280
|
+
if (jsContent.includes("/api/") || jsContent.includes("fetch(") || jsContent.includes("axios")) {
|
|
1281
|
+
hasRest = true;
|
|
1282
|
+
indicators.push("REST API usage in code");
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if (hasGraphQL && hasRest) {
|
|
1286
|
+
return "Mixed";
|
|
1287
|
+
}
|
|
1288
|
+
if (hasGraphQL) {
|
|
1289
|
+
return "GraphQL";
|
|
1290
|
+
}
|
|
1291
|
+
if (hasRest) {
|
|
1292
|
+
return "REST";
|
|
1293
|
+
}
|
|
1294
|
+
return "None";
|
|
1295
|
+
}
|
|
1296
|
+
function generateAdaptiveCacheStrategies(apiType, assets, configuration) {
|
|
1297
|
+
const strategies = [];
|
|
1298
|
+
if (apiType === "REST" || apiType === "Mixed") {
|
|
1299
|
+
strategies.push({
|
|
1300
|
+
urlPattern: "/api/.*",
|
|
1301
|
+
handler: "NetworkFirst",
|
|
1302
|
+
options: {
|
|
1303
|
+
cacheName: "api-rest-cache",
|
|
1304
|
+
expiration: {
|
|
1305
|
+
maxEntries: 50,
|
|
1306
|
+
maxAgeSeconds: 5 * 60
|
|
1307
|
+
// 5 minutes pour REST
|
|
1308
|
+
},
|
|
1309
|
+
networkTimeoutSeconds: 3
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
if (apiType === "GraphQL" || apiType === "Mixed") {
|
|
1314
|
+
strategies.push({
|
|
1315
|
+
urlPattern: "/graphql",
|
|
1316
|
+
handler: "NetworkFirst",
|
|
1317
|
+
options: {
|
|
1318
|
+
cacheName: "api-graphql-cache",
|
|
1319
|
+
expiration: {
|
|
1320
|
+
maxEntries: 30,
|
|
1321
|
+
maxAgeSeconds: 2 * 60
|
|
1322
|
+
// 2 minutes pour GraphQL (plus court car souvent mutations)
|
|
1323
|
+
},
|
|
1324
|
+
networkTimeoutSeconds: 5
|
|
1325
|
+
// Plus long pour GraphQL
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
if (configuration.buildTool === "vite") {
|
|
1330
|
+
strategies.push({
|
|
1331
|
+
urlPattern: "/assets/.*",
|
|
1332
|
+
handler: "CacheFirst",
|
|
1333
|
+
options: {
|
|
1334
|
+
cacheName: "vite-assets-cache",
|
|
1335
|
+
expiration: {
|
|
1336
|
+
maxEntries: 100,
|
|
1337
|
+
maxAgeSeconds: 30 * 24 * 60 * 60
|
|
1338
|
+
// 30 jours
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
} else if (configuration.buildTool === "webpack") {
|
|
1343
|
+
strategies.push({
|
|
1344
|
+
urlPattern: "/(static|_next|assets)/.*",
|
|
1345
|
+
handler: "StaleWhileRevalidate",
|
|
1346
|
+
options: {
|
|
1347
|
+
cacheName: "webpack-assets-cache",
|
|
1348
|
+
expiration: {
|
|
1349
|
+
maxEntries: 100,
|
|
1350
|
+
maxAgeSeconds: 7 * 24 * 60 * 60
|
|
1351
|
+
// 7 jours
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
if (configuration.cssInJs.length > 0) {
|
|
1357
|
+
strategies.push({
|
|
1358
|
+
urlPattern: "/.*\\.css",
|
|
1359
|
+
handler: "StaleWhileRevalidate",
|
|
1360
|
+
options: {
|
|
1361
|
+
cacheName: "css-cache",
|
|
1362
|
+
expiration: {
|
|
1363
|
+
maxEntries: 50,
|
|
1364
|
+
maxAgeSeconds: 7 * 24 * 60 * 60
|
|
1365
|
+
// 7 jours
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
return strategies;
|
|
1371
|
+
}
|
|
1372
|
+
function detectUnoptimizedImages(assets) {
|
|
1373
|
+
const suggestions = [];
|
|
1374
|
+
for (const imagePath of assets.images) {
|
|
1375
|
+
try {
|
|
1376
|
+
const stats = (0, import_fs5.statSync)(imagePath);
|
|
1377
|
+
const sizeInMB = stats.size / (1024 * 1024);
|
|
1378
|
+
const ext = imagePath.toLowerCase().split(".").pop();
|
|
1379
|
+
if (sizeInMB > 1) {
|
|
1380
|
+
suggestions.push({
|
|
1381
|
+
file: imagePath,
|
|
1382
|
+
suggestion: `Image > 1MB (${sizeInMB.toFixed(2)}MB). Consid\xE9rer compression ou conversion WebP.`,
|
|
1383
|
+
priority: "high"
|
|
1384
|
+
});
|
|
1385
|
+
} else if (sizeInMB > 0.5) {
|
|
1386
|
+
suggestions.push({
|
|
1387
|
+
file: imagePath,
|
|
1388
|
+
suggestion: `Image > 500KB (${sizeInMB.toFixed(2)}MB). Consid\xE9rer optimisation.`,
|
|
1389
|
+
priority: "medium"
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
if (ext === "png" && sizeInMB > 0.1) {
|
|
1393
|
+
suggestions.push({
|
|
1394
|
+
file: imagePath,
|
|
1395
|
+
suggestion: "PNG volumineux. Consid\xE9rer conversion WebP ou JPEG si pas de transparence.",
|
|
1396
|
+
priority: "medium"
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
if (ext === "jpg" || ext === "jpeg") {
|
|
1400
|
+
suggestions.push({
|
|
1401
|
+
file: imagePath,
|
|
1402
|
+
suggestion: "JPEG d\xE9tect\xE9. Consid\xE9rer conversion WebP pour meilleure compression.",
|
|
1403
|
+
priority: "low"
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
} catch {
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return suggestions;
|
|
1410
|
+
}
|
|
1411
|
+
async function generateResponsiveImageSizes(imagePath, outputDir, sizes = [320, 640, 768, 1024, 1280, 1920]) {
|
|
1412
|
+
if (!(0, import_fs5.existsSync)(imagePath)) {
|
|
1413
|
+
throw new Error(`Image not found: ${imagePath}`);
|
|
1414
|
+
}
|
|
1415
|
+
(0, import_fs5.mkdirSync)(outputDir, { recursive: true });
|
|
1416
|
+
const generatedFiles = [];
|
|
1417
|
+
const ext = (0, import_path5.extname)(imagePath);
|
|
1418
|
+
const baseName = (0, import_path5.basename)(imagePath, ext);
|
|
1419
|
+
const image = (0, import_sharp.default)(imagePath);
|
|
1420
|
+
const metadata = await image.metadata();
|
|
1421
|
+
if (!metadata.width || !metadata.height) {
|
|
1422
|
+
throw new Error("Unable to read image dimensions");
|
|
1423
|
+
}
|
|
1424
|
+
const aspectRatio = metadata.width / metadata.height;
|
|
1425
|
+
for (const width of sizes) {
|
|
1426
|
+
if (width > metadata.width) {
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
const height = Math.round(width / aspectRatio);
|
|
1430
|
+
const outputPath = (0, import_path5.join)(outputDir, `${baseName}-${width}w${ext}`);
|
|
1431
|
+
try {
|
|
1432
|
+
await image.clone().resize(width, height, {
|
|
1433
|
+
fit: "cover",
|
|
1434
|
+
position: "center"
|
|
1435
|
+
}).toFile(outputPath);
|
|
1436
|
+
generatedFiles.push(outputPath);
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1439
|
+
console.warn(`Failed to generate responsive size ${width}w for ${imagePath}: ${message}`);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return generatedFiles;
|
|
1443
|
+
}
|
|
1444
|
+
async function optimizeImage(imagePath, options = {}) {
|
|
1445
|
+
if (!(0, import_fs5.existsSync)(imagePath)) {
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
const {
|
|
1449
|
+
convertToWebP = false,
|
|
1450
|
+
quality = 85,
|
|
1451
|
+
maxWidth,
|
|
1452
|
+
outputDir = (0, import_path5.dirname)(imagePath)
|
|
1453
|
+
} = options;
|
|
1454
|
+
try {
|
|
1455
|
+
const originalStats = (0, import_fs5.statSync)(imagePath);
|
|
1456
|
+
const originalSize = originalStats.size;
|
|
1457
|
+
const ext = (0, import_path5.extname)(imagePath).toLowerCase();
|
|
1458
|
+
const baseName = (0, import_path5.basename)(imagePath, ext);
|
|
1459
|
+
const image = (0, import_sharp.default)(imagePath);
|
|
1460
|
+
const metadata = await image.metadata();
|
|
1461
|
+
if (!metadata.width || !metadata.height) {
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
const optimizedFiles = [];
|
|
1465
|
+
let totalOptimizedSize = 0;
|
|
1466
|
+
const outputFormat = convertToWebP && ext !== ".webp" ? "webp" : ext.slice(1);
|
|
1467
|
+
const outputExt = outputFormat === "webp" ? ".webp" : ext;
|
|
1468
|
+
const outputPath = (0, import_path5.join)(outputDir, `${baseName}${outputExt}`);
|
|
1469
|
+
(0, import_fs5.mkdirSync)(outputDir, { recursive: true });
|
|
1470
|
+
let pipeline = image.clone();
|
|
1471
|
+
if (maxWidth && metadata.width > maxWidth) {
|
|
1472
|
+
const aspectRatio = metadata.width / metadata.height;
|
|
1473
|
+
const newHeight = Math.round(maxWidth / aspectRatio);
|
|
1474
|
+
pipeline = pipeline.resize(maxWidth, newHeight, {
|
|
1475
|
+
fit: "cover",
|
|
1476
|
+
position: "center"
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
if (outputFormat === "webp") {
|
|
1480
|
+
pipeline = pipeline.webp({ quality });
|
|
1481
|
+
} else if (outputFormat === "png") {
|
|
1482
|
+
pipeline = pipeline.png({ quality, compressionLevel: 9 });
|
|
1483
|
+
} else if (outputFormat === "jpg" || outputFormat === "jpeg") {
|
|
1484
|
+
pipeline = pipeline.jpeg({ quality, mozjpeg: true });
|
|
1485
|
+
}
|
|
1486
|
+
await pipeline.toFile(outputPath);
|
|
1487
|
+
optimizedFiles.push(outputPath);
|
|
1488
|
+
const optimizedStats = (0, import_fs5.statSync)(outputPath);
|
|
1489
|
+
totalOptimizedSize = optimizedStats.size;
|
|
1490
|
+
const savings = (originalSize - totalOptimizedSize) / originalSize * 100;
|
|
1491
|
+
return {
|
|
1492
|
+
original: imagePath,
|
|
1493
|
+
optimized: optimizedFiles,
|
|
1494
|
+
format: outputFormat,
|
|
1495
|
+
originalSize,
|
|
1496
|
+
optimizedSize: totalOptimizedSize,
|
|
1497
|
+
savings: Math.max(0, savings)
|
|
1498
|
+
};
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1501
|
+
console.warn(`Failed to optimize image ${imagePath}: ${message}`);
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
async function optimizeProjectImages(assets, options = {}) {
|
|
1506
|
+
const results = [];
|
|
1507
|
+
const highPriorityImages = detectUnoptimizedImages(assets).filter((s) => s.priority === "high");
|
|
1508
|
+
const imagesToOptimize = highPriorityImages.length > 0 ? highPriorityImages.map((s) => s.file) : assets.images.slice(0, 10);
|
|
1509
|
+
for (const imagePath of imagesToOptimize) {
|
|
1510
|
+
try {
|
|
1511
|
+
const result = await optimizeImage(imagePath, {
|
|
1512
|
+
...options,
|
|
1513
|
+
convertToWebP: true,
|
|
1514
|
+
// Par défaut, convertir en WebP
|
|
1515
|
+
quality: 85
|
|
1516
|
+
});
|
|
1517
|
+
if (result && result.savings > 0) {
|
|
1518
|
+
results.push(result);
|
|
472
1519
|
}
|
|
1520
|
+
} catch (err) {
|
|
1521
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1522
|
+
console.warn(`Skipped optimization for ${imagePath}: ${message}`);
|
|
473
1523
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
1524
|
+
}
|
|
1525
|
+
return results;
|
|
1526
|
+
}
|
|
1527
|
+
function generateOptimalShortName(name) {
|
|
1528
|
+
if (!name || name.trim().length === 0) {
|
|
1529
|
+
return "PWA";
|
|
1530
|
+
}
|
|
1531
|
+
const cleanName = name.trim();
|
|
1532
|
+
if (cleanName.length <= 12) {
|
|
1533
|
+
return cleanName;
|
|
1534
|
+
}
|
|
1535
|
+
const words = cleanName.split(/[\s\-_]+/).filter((w) => w.length > 0);
|
|
1536
|
+
if (words.length > 1) {
|
|
1537
|
+
const initials = words.map((w) => w[0].toUpperCase()).join("");
|
|
1538
|
+
if (initials.length <= 12) {
|
|
1539
|
+
return initials;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
const stopWords = ["the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "by"];
|
|
1543
|
+
const significantWords = words.filter((w) => !stopWords.includes(w.toLowerCase()));
|
|
1544
|
+
if (significantWords.length > 0) {
|
|
1545
|
+
const firstWord = significantWords[0];
|
|
1546
|
+
if (firstWord.length <= 12) {
|
|
1547
|
+
return firstWord.substring(0, 12);
|
|
1548
|
+
}
|
|
1549
|
+
return firstWord.substring(0, 12);
|
|
1550
|
+
}
|
|
1551
|
+
return cleanName.substring(0, 12);
|
|
1552
|
+
}
|
|
1553
|
+
function detectDominantColors(_imagePath) {
|
|
1554
|
+
return null;
|
|
1555
|
+
}
|
|
1556
|
+
function suggestManifestColors(projectPath, framework, iconSource) {
|
|
1557
|
+
if (iconSource) {
|
|
1558
|
+
const iconPath = (0, import_fs5.existsSync)(iconSource) ? iconSource : (0, import_path5.join)(projectPath, iconSource);
|
|
1559
|
+
if ((0, import_fs5.existsSync)(iconPath)) {
|
|
1560
|
+
const colors = detectDominantColors(iconPath);
|
|
1561
|
+
if (colors) {
|
|
1562
|
+
return colors;
|
|
479
1563
|
}
|
|
480
1564
|
}
|
|
481
1565
|
}
|
|
482
|
-
|
|
1566
|
+
const frameworkColors = {
|
|
1567
|
+
react: { themeColor: "#61dafb", backgroundColor: "#282c34" },
|
|
1568
|
+
vue: { themeColor: "#42b983", backgroundColor: "#ffffff" },
|
|
1569
|
+
angular: { themeColor: "#dd0031", backgroundColor: "#ffffff" },
|
|
1570
|
+
nextjs: { themeColor: "#000000", backgroundColor: "#ffffff" },
|
|
1571
|
+
nuxt: { themeColor: "#00dc82", backgroundColor: "#ffffff" },
|
|
1572
|
+
svelte: { themeColor: "#ff3e00", backgroundColor: "#ffffff" },
|
|
1573
|
+
wordpress: { themeColor: "#21759b", backgroundColor: "#ffffff" },
|
|
1574
|
+
symfony: { themeColor: "#000000", backgroundColor: "#ffffff" },
|
|
1575
|
+
laravel: { themeColor: "#ff2d20", backgroundColor: "#ffffff" }
|
|
1576
|
+
};
|
|
1577
|
+
if (framework && frameworkColors[framework.toLowerCase()]) {
|
|
1578
|
+
return frameworkColors[framework.toLowerCase()];
|
|
1579
|
+
}
|
|
1580
|
+
return { themeColor: "#ffffff", backgroundColor: "#000000" };
|
|
1581
|
+
}
|
|
1582
|
+
async function optimizeProject(projectPath, assets, configuration, framework, iconSource, autoOptimizeImages = false) {
|
|
1583
|
+
const apiType = detectApiType(projectPath, assets);
|
|
1584
|
+
const cacheStrategies = generateAdaptiveCacheStrategies(apiType, assets, configuration);
|
|
1585
|
+
const assetSuggestions = detectUnoptimizedImages(assets);
|
|
1586
|
+
let optimizedImages;
|
|
1587
|
+
if (autoOptimizeImages && assets.images.length > 0) {
|
|
1588
|
+
optimizedImages = await optimizeProjectImages(assets, {
|
|
1589
|
+
convertToWebP: true,
|
|
1590
|
+
quality: 85
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
const manifestColors = suggestManifestColors(projectPath, framework, iconSource);
|
|
1594
|
+
return {
|
|
1595
|
+
cacheStrategies,
|
|
1596
|
+
manifestConfig: {
|
|
1597
|
+
themeColor: manifestColors.themeColor,
|
|
1598
|
+
backgroundColor: manifestColors.backgroundColor
|
|
1599
|
+
},
|
|
1600
|
+
assetSuggestions,
|
|
1601
|
+
apiType,
|
|
1602
|
+
optimizedImages
|
|
1603
|
+
};
|
|
483
1604
|
}
|
|
484
1605
|
|
|
485
1606
|
// src/scanner/index.ts
|
|
486
1607
|
async function scanProject(options) {
|
|
487
|
-
const {
|
|
1608
|
+
const {
|
|
1609
|
+
projectPath,
|
|
1610
|
+
includeAssets = true,
|
|
1611
|
+
includeArchitecture = true,
|
|
1612
|
+
useCache = true,
|
|
1613
|
+
cacheFile,
|
|
1614
|
+
forceScan = false
|
|
1615
|
+
} = options;
|
|
1616
|
+
const cacheFilePath = cacheFile ?? (0, import_path6.join)(projectPath, ".universal-pwa-cache.json");
|
|
1617
|
+
let cache = useCache ? loadCache(cacheFilePath) : null;
|
|
1618
|
+
if (cache) {
|
|
1619
|
+
cache = cleanCache(cache) ?? null;
|
|
1620
|
+
if (cache) {
|
|
1621
|
+
saveCache(cache, cacheFilePath);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
if (useCache && !forceScan && cache) {
|
|
1625
|
+
const cacheOptions = {
|
|
1626
|
+
force: forceScan
|
|
1627
|
+
};
|
|
1628
|
+
if (isCacheValid(projectPath, cache, cacheOptions)) {
|
|
1629
|
+
const cachedResult = getCachedResult(projectPath, cache);
|
|
1630
|
+
if (cachedResult) {
|
|
1631
|
+
return cachedResult;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
488
1635
|
const frameworkCandidate = detectFramework(projectPath);
|
|
489
|
-
const framework = isFrameworkDetectionResult(frameworkCandidate) ? frameworkCandidate : {
|
|
1636
|
+
const framework = isFrameworkDetectionResult(frameworkCandidate) ? frameworkCandidate : {
|
|
1637
|
+
framework: null,
|
|
1638
|
+
confidence: "low",
|
|
1639
|
+
confidenceScore: 0,
|
|
1640
|
+
indicators: [],
|
|
1641
|
+
version: null,
|
|
1642
|
+
configuration: {
|
|
1643
|
+
language: null,
|
|
1644
|
+
cssInJs: [],
|
|
1645
|
+
stateManagement: [],
|
|
1646
|
+
buildTool: null
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
490
1649
|
const assetsCandidate = includeAssets ? await detectAssets(projectPath) : getEmptyAssets();
|
|
491
1650
|
const assets = isAssetDetectionResult(assetsCandidate) ? assetsCandidate : getEmptyAssets();
|
|
492
1651
|
const architectureCandidate = includeArchitecture ? await detectArchitecture(projectPath) : getEmptyArchitecture();
|
|
493
1652
|
const architecture = isArchitectureDetectionResult(architectureCandidate) ? architectureCandidate : getEmptyArchitecture();
|
|
494
|
-
|
|
1653
|
+
const result = {
|
|
495
1654
|
framework,
|
|
496
1655
|
assets,
|
|
497
1656
|
architecture,
|
|
498
1657
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
499
1658
|
projectPath
|
|
500
1659
|
};
|
|
1660
|
+
if (useCache) {
|
|
1661
|
+
const updatedCache = updateCache(projectPath, result, cache);
|
|
1662
|
+
saveCache(updatedCache, cacheFilePath);
|
|
1663
|
+
}
|
|
1664
|
+
return result;
|
|
501
1665
|
}
|
|
502
1666
|
function generateReport(result) {
|
|
503
1667
|
return JSON.stringify(result, null, 2);
|
|
504
1668
|
}
|
|
505
1669
|
function validateProjectPath(projectPath) {
|
|
506
1670
|
try {
|
|
507
|
-
return (0,
|
|
1671
|
+
return (0, import_fs6.existsSync)(projectPath);
|
|
508
1672
|
} catch {
|
|
509
1673
|
return false;
|
|
510
1674
|
}
|
|
@@ -512,7 +1676,18 @@ function validateProjectPath(projectPath) {
|
|
|
512
1676
|
function isFrameworkDetectionResult(value) {
|
|
513
1677
|
if (!value || typeof value !== "object") return false;
|
|
514
1678
|
const v = value;
|
|
515
|
-
|
|
1679
|
+
const isValidVersion = (ver) => {
|
|
1680
|
+
if (ver === null) return true;
|
|
1681
|
+
if (typeof ver !== "object") return false;
|
|
1682
|
+
const vv = ver;
|
|
1683
|
+
return typeof vv.major === "number" && (vv.minor === null || typeof vv.minor === "number") && (vv.patch === null || typeof vv.patch === "number") && typeof vv.raw === "string";
|
|
1684
|
+
};
|
|
1685
|
+
const isValidConfiguration = (config) => {
|
|
1686
|
+
if (!config || typeof config !== "object") return false;
|
|
1687
|
+
const cfg = config;
|
|
1688
|
+
return (cfg.language === null || cfg.language === "typescript" || cfg.language === "javascript") && Array.isArray(cfg.cssInJs) && Array.isArray(cfg.stateManagement) && (cfg.buildTool === null || typeof cfg.buildTool === "string");
|
|
1689
|
+
};
|
|
1690
|
+
return (v.framework === null || typeof v.framework === "string") && (v.confidence === "low" || v.confidence === "medium" || v.confidence === "high") && (typeof v.confidenceScore === "number" && v.confidenceScore >= 0 && v.confidenceScore <= 100) && Array.isArray(v.indicators) && (v.version === null || isValidVersion(v.version)) && (v.configuration !== void 0 && isValidConfiguration(v.configuration));
|
|
516
1691
|
}
|
|
517
1692
|
function isAssetDetectionResult(value) {
|
|
518
1693
|
if (!value || typeof value !== "object") return false;
|
|
@@ -548,8 +1723,8 @@ function getEmptyArchitecture() {
|
|
|
548
1723
|
|
|
549
1724
|
// src/generator/manifest-generator.ts
|
|
550
1725
|
var import_zod = require("zod");
|
|
551
|
-
var
|
|
552
|
-
var
|
|
1726
|
+
var import_fs7 = require("fs");
|
|
1727
|
+
var import_path7 = require("path");
|
|
553
1728
|
var ManifestIconSchema = import_zod.z.object({
|
|
554
1729
|
src: import_zod.z.string(),
|
|
555
1730
|
sizes: import_zod.z.string(),
|
|
@@ -584,7 +1759,7 @@ function generateManifest(options) {
|
|
|
584
1759
|
if (options.shortName && typeof options.shortName === "string" && options.shortName.trim().length > 0) {
|
|
585
1760
|
shortName = options.shortName.trim().substring(0, 12);
|
|
586
1761
|
} else if (options.name && typeof options.name === "string" && options.name.length > 0) {
|
|
587
|
-
shortName = options.name
|
|
1762
|
+
shortName = generateOptimalShortName(options.name);
|
|
588
1763
|
}
|
|
589
1764
|
if (!shortName || shortName.trim().length === 0) {
|
|
590
1765
|
shortName = "PWA";
|
|
@@ -627,10 +1802,10 @@ function generateManifest(options) {
|
|
|
627
1802
|
return ManifestSchema.parse(manifest);
|
|
628
1803
|
}
|
|
629
1804
|
function writeManifest(manifest, outputDir) {
|
|
630
|
-
const manifestPath = (0,
|
|
1805
|
+
const manifestPath = (0, import_path7.join)(outputDir, "manifest.json");
|
|
631
1806
|
const validatedManifest = ManifestSchema.parse(manifest);
|
|
632
1807
|
const manifestJson = JSON.stringify(validatedManifest, null, 2);
|
|
633
|
-
(0,
|
|
1808
|
+
(0, import_fs7.writeFileSync)(manifestPath, manifestJson, { encoding: "utf8" });
|
|
634
1809
|
return manifestPath;
|
|
635
1810
|
}
|
|
636
1811
|
function generateAndWriteManifest(options, outputDir) {
|
|
@@ -639,9 +1814,9 @@ function generateAndWriteManifest(options, outputDir) {
|
|
|
639
1814
|
}
|
|
640
1815
|
|
|
641
1816
|
// src/generator/icon-generator.ts
|
|
642
|
-
var
|
|
643
|
-
var
|
|
644
|
-
var
|
|
1817
|
+
var import_sharp2 = __toESM(require("sharp"), 1);
|
|
1818
|
+
var import_fs8 = require("fs");
|
|
1819
|
+
var import_path8 = require("path");
|
|
645
1820
|
var STANDARD_ICON_SIZES = [
|
|
646
1821
|
{ width: 72, height: 72, name: "icon-72x72.png" },
|
|
647
1822
|
{ width: 96, height: 96, name: "icon-96x96.png" },
|
|
@@ -677,20 +1852,20 @@ async function generateIcons(options) {
|
|
|
677
1852
|
format = "png",
|
|
678
1853
|
quality = 90
|
|
679
1854
|
} = options;
|
|
680
|
-
if (!(0,
|
|
1855
|
+
if (!(0, import_fs8.existsSync)(sourceImage)) {
|
|
681
1856
|
throw new Error(`Source image not found: ${sourceImage}`);
|
|
682
1857
|
}
|
|
683
|
-
(0,
|
|
1858
|
+
(0, import_fs8.mkdirSync)(outputDir, { recursive: true });
|
|
684
1859
|
const generatedFiles = [];
|
|
685
1860
|
const icons = [];
|
|
686
1861
|
const splashScreens = [];
|
|
687
|
-
const image = (0,
|
|
1862
|
+
const image = (0, import_sharp2.default)(sourceImage);
|
|
688
1863
|
const metadata = await image.metadata();
|
|
689
1864
|
if (!metadata.width || !metadata.height) {
|
|
690
1865
|
throw new Error("Unable to read image dimensions");
|
|
691
1866
|
}
|
|
692
1867
|
for (const size of iconSizes) {
|
|
693
|
-
const outputPath = (0,
|
|
1868
|
+
const outputPath = (0, import_path8.join)(outputDir, size.name);
|
|
694
1869
|
try {
|
|
695
1870
|
let pipeline = image.clone().resize(size.width, size.height, {
|
|
696
1871
|
fit: "cover",
|
|
@@ -715,7 +1890,7 @@ async function generateIcons(options) {
|
|
|
715
1890
|
}
|
|
716
1891
|
}
|
|
717
1892
|
for (const size of splashSizes) {
|
|
718
|
-
const outputPath = (0,
|
|
1893
|
+
const outputPath = (0, import_path8.join)(outputDir, size.name);
|
|
719
1894
|
try {
|
|
720
1895
|
let pipeline = image.clone().resize(size.width, size.height, {
|
|
721
1896
|
fit: "cover",
|
|
@@ -740,7 +1915,7 @@ async function generateIcons(options) {
|
|
|
740
1915
|
}
|
|
741
1916
|
if (format === "png") {
|
|
742
1917
|
try {
|
|
743
|
-
const appleIconPath = (0,
|
|
1918
|
+
const appleIconPath = (0, import_path8.join)(outputDir, "apple-touch-icon.png");
|
|
744
1919
|
await image.clone().resize(180, 180, {
|
|
745
1920
|
fit: "cover",
|
|
746
1921
|
position: "center"
|
|
@@ -778,13 +1953,13 @@ async function generateSplashScreensOnly(options) {
|
|
|
778
1953
|
};
|
|
779
1954
|
}
|
|
780
1955
|
async function generateFavicon(sourceImage, outputDir) {
|
|
781
|
-
if (!(0,
|
|
1956
|
+
if (!(0, import_fs8.existsSync)(sourceImage)) {
|
|
782
1957
|
throw new Error(`Source image not found: ${sourceImage}`);
|
|
783
1958
|
}
|
|
784
|
-
(0,
|
|
785
|
-
const faviconPath = (0,
|
|
1959
|
+
(0, import_fs8.mkdirSync)(outputDir, { recursive: true });
|
|
1960
|
+
const faviconPath = (0, import_path8.join)(outputDir, "favicon.ico");
|
|
786
1961
|
try {
|
|
787
|
-
await (0,
|
|
1962
|
+
await (0, import_sharp2.default)(sourceImage).resize(32, 32, {
|
|
788
1963
|
fit: "cover",
|
|
789
1964
|
position: "center"
|
|
790
1965
|
}).png().toFile(faviconPath);
|
|
@@ -795,13 +1970,13 @@ async function generateFavicon(sourceImage, outputDir) {
|
|
|
795
1970
|
}
|
|
796
1971
|
}
|
|
797
1972
|
async function generateAppleTouchIcon(sourceImage, outputDir) {
|
|
798
|
-
if (!(0,
|
|
1973
|
+
if (!(0, import_fs8.existsSync)(sourceImage)) {
|
|
799
1974
|
throw new Error(`Source image not found: ${sourceImage}`);
|
|
800
1975
|
}
|
|
801
|
-
(0,
|
|
802
|
-
const appleIconPath = (0,
|
|
1976
|
+
(0, import_fs8.mkdirSync)(outputDir, { recursive: true });
|
|
1977
|
+
const appleIconPath = (0, import_path8.join)(outputDir, "apple-touch-icon.png");
|
|
803
1978
|
try {
|
|
804
|
-
await (0,
|
|
1979
|
+
await (0, import_sharp2.default)(sourceImage).resize(180, 180, {
|
|
805
1980
|
fit: "cover",
|
|
806
1981
|
position: "center"
|
|
807
1982
|
}).png({ quality: 90, compressionLevel: 9 }).toFile(appleIconPath);
|
|
@@ -814,8 +1989,8 @@ async function generateAppleTouchIcon(sourceImage, outputDir) {
|
|
|
814
1989
|
|
|
815
1990
|
// src/generator/service-worker-generator.ts
|
|
816
1991
|
var import_workbox_build = require("workbox-build");
|
|
817
|
-
var
|
|
818
|
-
var
|
|
1992
|
+
var import_fs9 = require("fs");
|
|
1993
|
+
var import_path9 = require("path");
|
|
819
1994
|
var import_universal_pwa_templates = require("@julien-lin/universal-pwa-templates");
|
|
820
1995
|
async function generateServiceWorker(options) {
|
|
821
1996
|
const {
|
|
@@ -829,12 +2004,12 @@ async function generateServiceWorker(options) {
|
|
|
829
2004
|
swDest = "sw.js",
|
|
830
2005
|
offlinePage
|
|
831
2006
|
} = options;
|
|
832
|
-
(0,
|
|
2007
|
+
(0, import_fs9.mkdirSync)(outputDir, { recursive: true });
|
|
833
2008
|
const finalTemplateType = templateType ?? (0, import_universal_pwa_templates.determineTemplateType)(architecture, framework ?? null);
|
|
834
2009
|
const template = (0, import_universal_pwa_templates.getServiceWorkerTemplate)(finalTemplateType);
|
|
835
|
-
const swSrcPath = (0,
|
|
836
|
-
(0,
|
|
837
|
-
const swDestPath = (0,
|
|
2010
|
+
const swSrcPath = (0, import_path9.join)(outputDir, "sw-src.js");
|
|
2011
|
+
(0, import_fs9.writeFileSync)(swSrcPath, template.content, "utf-8");
|
|
2012
|
+
const swDestPath = (0, import_path9.join)(outputDir, swDest);
|
|
838
2013
|
const workboxConfig = {
|
|
839
2014
|
globDirectory: globDirectory ?? projectPath,
|
|
840
2015
|
globPatterns,
|
|
@@ -849,7 +2024,7 @@ async function generateServiceWorker(options) {
|
|
|
849
2024
|
try {
|
|
850
2025
|
const result = await (0, import_workbox_build.injectManifest)(workboxConfig);
|
|
851
2026
|
try {
|
|
852
|
-
if ((0,
|
|
2027
|
+
if ((0, import_fs9.existsSync)(swSrcPath)) {
|
|
853
2028
|
}
|
|
854
2029
|
} catch {
|
|
855
2030
|
}
|
|
@@ -879,8 +2054,8 @@ async function generateSimpleServiceWorker(options) {
|
|
|
879
2054
|
clientsClaim = true,
|
|
880
2055
|
runtimeCaching
|
|
881
2056
|
} = options;
|
|
882
|
-
(0,
|
|
883
|
-
const swDestPath = (0,
|
|
2057
|
+
(0, import_fs9.mkdirSync)(outputDir, { recursive: true });
|
|
2058
|
+
const swDestPath = (0, import_path9.join)(outputDir, swDest);
|
|
884
2059
|
const workboxConfig = {
|
|
885
2060
|
globDirectory: globDirectory ?? projectPath,
|
|
886
2061
|
globPatterns,
|
|
@@ -934,8 +2109,8 @@ async function generateAndWriteServiceWorker(options) {
|
|
|
934
2109
|
}
|
|
935
2110
|
|
|
936
2111
|
// src/generator/https-checker.ts
|
|
937
|
-
var
|
|
938
|
-
var
|
|
2112
|
+
var import_fs10 = require("fs");
|
|
2113
|
+
var import_path10 = require("path");
|
|
939
2114
|
function checkHttps(url, allowHttpLocalhost = true) {
|
|
940
2115
|
let parsedUrl;
|
|
941
2116
|
try {
|
|
@@ -990,30 +2165,30 @@ function detectProjectUrl(projectPath) {
|
|
|
990
2165
|
{ file: "next.config.ts", pattern: /baseUrl.*["'](.+)["']/ }
|
|
991
2166
|
];
|
|
992
2167
|
for (const config of configFiles) {
|
|
993
|
-
const filePath = (0,
|
|
994
|
-
if ((0,
|
|
2168
|
+
const filePath = (0, import_path10.join)(projectPath, config.file);
|
|
2169
|
+
if ((0, import_fs10.existsSync)(filePath)) {
|
|
995
2170
|
try {
|
|
996
2171
|
if (config.file.endsWith(".json") && config.key) {
|
|
997
|
-
const parsed = JSON.parse((0,
|
|
2172
|
+
const parsed = JSON.parse((0, import_fs10.readFileSync)(filePath, "utf-8"));
|
|
998
2173
|
const content = parsed;
|
|
999
2174
|
const value = content[config.key];
|
|
1000
2175
|
if (value && typeof value === "string") {
|
|
1001
2176
|
return value;
|
|
1002
2177
|
}
|
|
1003
2178
|
} else if (config.file.endsWith(".toml") && config.pattern) {
|
|
1004
|
-
const content = (0,
|
|
2179
|
+
const content = (0, import_fs10.readFileSync)(filePath, "utf-8");
|
|
1005
2180
|
const match = config.pattern ? content.match(config.pattern) : null;
|
|
1006
2181
|
if (match && match[1]) {
|
|
1007
2182
|
return match[1];
|
|
1008
2183
|
}
|
|
1009
2184
|
} else if ((config.file.endsWith(".js") || config.file.endsWith(".ts")) && config.pattern) {
|
|
1010
|
-
const content = (0,
|
|
2185
|
+
const content = (0, import_fs10.readFileSync)(filePath, "utf-8");
|
|
1011
2186
|
const match = config.pattern ? content.match(config.pattern) : null;
|
|
1012
2187
|
if (match && match[1]) {
|
|
1013
2188
|
return match[1];
|
|
1014
2189
|
}
|
|
1015
2190
|
} else if (config.file.startsWith(".env") && config.pattern) {
|
|
1016
|
-
const content = (0,
|
|
2191
|
+
const content = (0, import_fs10.readFileSync)(filePath, "utf-8");
|
|
1017
2192
|
const lines = content.split("\n");
|
|
1018
2193
|
for (const line of lines) {
|
|
1019
2194
|
const match = config.pattern ? line.match(config.pattern) : null;
|
|
@@ -1052,9 +2227,9 @@ function checkProjectHttps(options = {}) {
|
|
|
1052
2227
|
|
|
1053
2228
|
// src/injector/html-parser.ts
|
|
1054
2229
|
var import_htmlparser2 = require("htmlparser2");
|
|
1055
|
-
var
|
|
2230
|
+
var import_fs11 = require("fs");
|
|
1056
2231
|
function parseHTMLFile(filePath, options = {}) {
|
|
1057
|
-
const content = (0,
|
|
2232
|
+
const content = (0, import_fs11.readFileSync)(filePath, "utf-8");
|
|
1058
2233
|
return parseHTML(content, options);
|
|
1059
2234
|
}
|
|
1060
2235
|
function parseHTML(htmlContent, options = {}) {
|
|
@@ -1188,7 +2363,7 @@ function serializeHTML(parsed) {
|
|
|
1188
2363
|
}
|
|
1189
2364
|
|
|
1190
2365
|
// src/injector/meta-injector.ts
|
|
1191
|
-
var
|
|
2366
|
+
var import_fs12 = require("fs");
|
|
1192
2367
|
var import_dom_serializer = require("dom-serializer");
|
|
1193
2368
|
function injectMetaTags(htmlContent, options = {}) {
|
|
1194
2369
|
const parsed = parseHTML(htmlContent);
|
|
@@ -1283,6 +2458,31 @@ function injectMetaTags(htmlContent, options = {}) {
|
|
|
1283
2458
|
result.skipped.push("apple-mobile-web-app-title (already exists)");
|
|
1284
2459
|
}
|
|
1285
2460
|
}
|
|
2461
|
+
let body = parsed.body;
|
|
2462
|
+
const originalBodyExists = !!body;
|
|
2463
|
+
if (!body) {
|
|
2464
|
+
if (parsed.html) {
|
|
2465
|
+
const bodyElement = {
|
|
2466
|
+
type: "tag",
|
|
2467
|
+
name: "body",
|
|
2468
|
+
tagName: "body",
|
|
2469
|
+
attribs: {},
|
|
2470
|
+
children: [],
|
|
2471
|
+
parent: parsed.html,
|
|
2472
|
+
next: null,
|
|
2473
|
+
prev: null
|
|
2474
|
+
};
|
|
2475
|
+
if (parsed.html.children) {
|
|
2476
|
+
parsed.html.children.push(bodyElement);
|
|
2477
|
+
} else {
|
|
2478
|
+
parsed.html.children = [bodyElement];
|
|
2479
|
+
}
|
|
2480
|
+
body = bodyElement;
|
|
2481
|
+
result.warnings.push("Created <body> tag (was missing)");
|
|
2482
|
+
} else {
|
|
2483
|
+
result.warnings.push("No <html> or <body> tag found, scripts may not be injected correctly");
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
1286
2486
|
let modifiedHtml = (0, import_dom_serializer.render)(parsed.document, { decodeEntities: false });
|
|
1287
2487
|
if (options.serviceWorkerPath) {
|
|
1288
2488
|
const swPath = options.serviceWorkerPath.startsWith("/") ? options.serviceWorkerPath : `/${options.serviceWorkerPath}`;
|
|
@@ -1304,7 +2504,9 @@ let deferredPrompt = null;
|
|
|
1304
2504
|
let isInstalled = false;
|
|
1305
2505
|
|
|
1306
2506
|
// Check if app is already installed
|
|
1307
|
-
if (window.matchMedia('(display-mode: standalone)').matches
|
|
2507
|
+
if (typeof window.matchMedia === 'function' && window.matchMedia('(display-mode: standalone)').matches) {
|
|
2508
|
+
isInstalled = true;
|
|
2509
|
+
} else if (window.navigator.standalone === true) {
|
|
1308
2510
|
isInstalled = true;
|
|
1309
2511
|
}
|
|
1310
2512
|
|
|
@@ -1352,7 +2554,14 @@ window.installPWA = function() {
|
|
|
1352
2554
|
|
|
1353
2555
|
// Expose global check function
|
|
1354
2556
|
window.isPWAInstalled = function() {
|
|
1355
|
-
|
|
2557
|
+
if (isInstalled) return true;
|
|
2558
|
+
if (typeof window.matchMedia === 'function' && window.matchMedia('(display-mode: standalone)').matches) {
|
|
2559
|
+
return true;
|
|
2560
|
+
}
|
|
2561
|
+
if (window.navigator.standalone === true) {
|
|
2562
|
+
return true;
|
|
2563
|
+
}
|
|
2564
|
+
return false;
|
|
1356
2565
|
};
|
|
1357
2566
|
|
|
1358
2567
|
// Expose global check if installable
|
|
@@ -1360,11 +2569,16 @@ window.isPWAInstallable = function() {
|
|
|
1360
2569
|
return deferredPrompt !== null;
|
|
1361
2570
|
};
|
|
1362
2571
|
</script>`;
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
2572
|
+
const lastBodyIndex = modifiedHtml.lastIndexOf("</body>");
|
|
2573
|
+
if (lastBodyIndex !== -1 && originalBodyExists) {
|
|
2574
|
+
modifiedHtml = modifiedHtml.slice(0, lastBodyIndex) + swScript + "\n" + modifiedHtml.slice(lastBodyIndex);
|
|
2575
|
+
} else if (modifiedHtml.includes("</html>")) {
|
|
2576
|
+
const htmlIndex = modifiedHtml.lastIndexOf("</html>");
|
|
2577
|
+
modifiedHtml = modifiedHtml.slice(0, htmlIndex) + swScript + "\n</body>\n" + modifiedHtml.slice(htmlIndex);
|
|
2578
|
+
result.warnings.push("Injected script before </html> (no </body> found)");
|
|
1366
2579
|
} else {
|
|
1367
|
-
modifiedHtml =
|
|
2580
|
+
modifiedHtml = modifiedHtml + swScript;
|
|
2581
|
+
result.warnings.push("Injected script at end of file (no </body> or </html> found)");
|
|
1368
2582
|
}
|
|
1369
2583
|
result.injected.push("Service Worker registration script");
|
|
1370
2584
|
result.injected.push("PWA install handler script");
|
|
@@ -1377,7 +2591,9 @@ let deferredPrompt = null;
|
|
|
1377
2591
|
let isInstalled = false;
|
|
1378
2592
|
|
|
1379
2593
|
// Check if app is already installed
|
|
1380
|
-
if (window.matchMedia('(display-mode: standalone)').matches
|
|
2594
|
+
if (typeof window.matchMedia === 'function' && window.matchMedia('(display-mode: standalone)').matches) {
|
|
2595
|
+
isInstalled = true;
|
|
2596
|
+
} else if (window.navigator.standalone === true) {
|
|
1381
2597
|
isInstalled = true;
|
|
1382
2598
|
}
|
|
1383
2599
|
|
|
@@ -1410,18 +2626,30 @@ window.installPWA = function() {
|
|
|
1410
2626
|
};
|
|
1411
2627
|
|
|
1412
2628
|
window.isPWAInstalled = function() {
|
|
1413
|
-
|
|
2629
|
+
if (isInstalled) return true;
|
|
2630
|
+
if (typeof window.matchMedia === 'function' && window.matchMedia('(display-mode: standalone)').matches) {
|
|
2631
|
+
return true;
|
|
2632
|
+
}
|
|
2633
|
+
if (window.navigator.standalone === true) {
|
|
2634
|
+
return true;
|
|
2635
|
+
}
|
|
2636
|
+
return false;
|
|
1414
2637
|
};
|
|
1415
2638
|
|
|
1416
2639
|
window.isPWAInstallable = function() {
|
|
1417
2640
|
return deferredPrompt !== null;
|
|
1418
2641
|
};
|
|
1419
2642
|
</script>`;
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
2643
|
+
const lastBodyIndex = modifiedHtml.lastIndexOf("</body>");
|
|
2644
|
+
if (lastBodyIndex !== -1 && originalBodyExists) {
|
|
2645
|
+
modifiedHtml = modifiedHtml.slice(0, lastBodyIndex) + installScript + "\n" + modifiedHtml.slice(lastBodyIndex);
|
|
2646
|
+
} else if (modifiedHtml.includes("</html>")) {
|
|
2647
|
+
const htmlIndex = modifiedHtml.lastIndexOf("</html>");
|
|
2648
|
+
modifiedHtml = modifiedHtml.slice(0, htmlIndex) + installScript + "\n</body>\n" + modifiedHtml.slice(htmlIndex);
|
|
2649
|
+
result.warnings.push("Injected install script before </html> (no </body> found)");
|
|
1423
2650
|
} else {
|
|
1424
2651
|
modifiedHtml = `${modifiedHtml}${installScript}`;
|
|
2652
|
+
result.warnings.push("Injected install script at end of file (no </body> or </html> found)");
|
|
1425
2653
|
}
|
|
1426
2654
|
result.injected.push("PWA install handler script");
|
|
1427
2655
|
} else {
|
|
@@ -1483,7 +2711,436 @@ function escapeJavaScriptString(str) {
|
|
|
1483
2711
|
function injectMetaTagsInFile(filePath, options = {}) {
|
|
1484
2712
|
const parsed = parseHTMLFile(filePath);
|
|
1485
2713
|
const { html, result } = injectMetaTags(parsed.originalContent, options);
|
|
1486
|
-
(0,
|
|
2714
|
+
(0, import_fs12.writeFileSync)(filePath, html, "utf-8");
|
|
2715
|
+
return result;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// src/validator/pwa-validator.ts
|
|
2719
|
+
var import_fs13 = require("fs");
|
|
2720
|
+
var import_path11 = require("path");
|
|
2721
|
+
var import_promises = require("fs/promises");
|
|
2722
|
+
function validateManifest(projectPath, outputDir) {
|
|
2723
|
+
const errors = [];
|
|
2724
|
+
const manifestPath = (0, import_path11.join)(outputDir, "manifest.json");
|
|
2725
|
+
if (!(0, import_fs13.existsSync)(manifestPath)) {
|
|
2726
|
+
return {
|
|
2727
|
+
exists: false,
|
|
2728
|
+
valid: false,
|
|
2729
|
+
errors: [
|
|
2730
|
+
{
|
|
2731
|
+
code: "MANIFEST_MISSING",
|
|
2732
|
+
message: "manifest.json is missing",
|
|
2733
|
+
severity: "error",
|
|
2734
|
+
file: manifestPath,
|
|
2735
|
+
suggestion: "Generate manifest.json using universal-pwa init"
|
|
2736
|
+
}
|
|
2737
|
+
]
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
try {
|
|
2741
|
+
const manifestContent = (0, import_fs13.readFileSync)(manifestPath, "utf-8");
|
|
2742
|
+
const manifest = JSON.parse(manifestContent);
|
|
2743
|
+
if (!manifest.name || manifest.name.trim().length === 0) {
|
|
2744
|
+
errors.push({
|
|
2745
|
+
code: "MANIFEST_NAME_MISSING",
|
|
2746
|
+
message: 'manifest.json: "name" field is required',
|
|
2747
|
+
severity: "error",
|
|
2748
|
+
file: manifestPath
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
if (!manifest.short_name || manifest.short_name.trim().length === 0) {
|
|
2752
|
+
errors.push({
|
|
2753
|
+
code: "MANIFEST_SHORT_NAME_MISSING",
|
|
2754
|
+
message: 'manifest.json: "short_name" field is required',
|
|
2755
|
+
severity: "error",
|
|
2756
|
+
file: manifestPath
|
|
2757
|
+
});
|
|
2758
|
+
} else if (manifest.short_name.length > 12) {
|
|
2759
|
+
errors.push({
|
|
2760
|
+
code: "MANIFEST_SHORT_NAME_TOO_LONG",
|
|
2761
|
+
message: `manifest.json: "short_name" must be \u2264 12 characters (current: ${manifest.short_name.length})`,
|
|
2762
|
+
severity: "error",
|
|
2763
|
+
file: manifestPath,
|
|
2764
|
+
suggestion: "Shorten the short_name to 12 characters or less"
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
if (!manifest.icons || manifest.icons.length === 0) {
|
|
2768
|
+
errors.push({
|
|
2769
|
+
code: "MANIFEST_ICONS_MISSING",
|
|
2770
|
+
message: 'manifest.json: "icons" array is required and must contain at least one icon',
|
|
2771
|
+
severity: "error",
|
|
2772
|
+
file: manifestPath,
|
|
2773
|
+
suggestion: "Generate icons using universal-pwa init --icon-source <path>"
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
if (!manifest.start_url) {
|
|
2777
|
+
errors.push({
|
|
2778
|
+
code: "MANIFEST_START_URL_MISSING",
|
|
2779
|
+
message: 'manifest.json: "start_url" field is required',
|
|
2780
|
+
severity: "error",
|
|
2781
|
+
file: manifestPath
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
if (!manifest.display) {
|
|
2785
|
+
errors.push({
|
|
2786
|
+
code: "MANIFEST_DISPLAY_MISSING",
|
|
2787
|
+
message: 'manifest.json: "display" field is required',
|
|
2788
|
+
severity: "error",
|
|
2789
|
+
file: manifestPath
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
if (!manifest.theme_color) {
|
|
2793
|
+
errors.push({
|
|
2794
|
+
code: "MANIFEST_THEME_COLOR_MISSING",
|
|
2795
|
+
message: 'manifest.json: "theme_color" is recommended for PWA installability',
|
|
2796
|
+
severity: "warning",
|
|
2797
|
+
file: manifestPath,
|
|
2798
|
+
suggestion: "Add theme_color to manifest.json"
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
if (!manifest.background_color) {
|
|
2802
|
+
errors.push({
|
|
2803
|
+
code: "MANIFEST_BACKGROUND_COLOR_MISSING",
|
|
2804
|
+
message: 'manifest.json: "background_color" is recommended for PWA installability',
|
|
2805
|
+
severity: "warning",
|
|
2806
|
+
file: manifestPath,
|
|
2807
|
+
suggestion: "Add background_color to manifest.json"
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
return {
|
|
2811
|
+
exists: true,
|
|
2812
|
+
valid: errors.filter((e) => e.severity === "error").length === 0,
|
|
2813
|
+
manifest,
|
|
2814
|
+
errors
|
|
2815
|
+
};
|
|
2816
|
+
} catch (error) {
|
|
2817
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2818
|
+
return {
|
|
2819
|
+
exists: true,
|
|
2820
|
+
valid: false,
|
|
2821
|
+
errors: [
|
|
2822
|
+
{
|
|
2823
|
+
code: "MANIFEST_INVALID_JSON",
|
|
2824
|
+
message: `manifest.json is invalid JSON: ${errorMessage}`,
|
|
2825
|
+
severity: "error",
|
|
2826
|
+
file: manifestPath,
|
|
2827
|
+
suggestion: "Fix JSON syntax errors in manifest.json"
|
|
2828
|
+
}
|
|
2829
|
+
]
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
function validateIcons(projectPath, outputDir, manifest) {
|
|
2834
|
+
const errors = [];
|
|
2835
|
+
let has192x192 = false;
|
|
2836
|
+
let has512x512 = false;
|
|
2837
|
+
if (!manifest || !manifest.icons || manifest.icons.length === 0) {
|
|
2838
|
+
return {
|
|
2839
|
+
exists: false,
|
|
2840
|
+
valid: false,
|
|
2841
|
+
has192x192: false,
|
|
2842
|
+
has512x512: false,
|
|
2843
|
+
errors: [
|
|
2844
|
+
{
|
|
2845
|
+
code: "ICONS_MANIFEST_MISSING",
|
|
2846
|
+
message: "Icons cannot be validated: manifest.json icons array is missing",
|
|
2847
|
+
severity: "error"
|
|
2848
|
+
}
|
|
2849
|
+
]
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
for (const icon of manifest.icons) {
|
|
2853
|
+
const iconPath = icon.src.startsWith("/") ? icon.src.substring(1) : icon.src;
|
|
2854
|
+
const fullIconPath = (0, import_path11.join)(outputDir, iconPath);
|
|
2855
|
+
if (!(0, import_fs13.existsSync)(fullIconPath)) {
|
|
2856
|
+
errors.push({
|
|
2857
|
+
code: "ICON_FILE_MISSING",
|
|
2858
|
+
message: `Icon file not found: ${iconPath}`,
|
|
2859
|
+
severity: "error",
|
|
2860
|
+
file: fullIconPath,
|
|
2861
|
+
suggestion: `Ensure ${iconPath} exists in ${outputDir}`
|
|
2862
|
+
});
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
if (icon.sizes.includes("192x192") || icon.sizes === "192x192") {
|
|
2866
|
+
has192x192 = true;
|
|
2867
|
+
}
|
|
2868
|
+
if (icon.sizes.includes("512x512") || icon.sizes === "512x512") {
|
|
2869
|
+
has512x512 = true;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
if (!has192x192) {
|
|
2873
|
+
errors.push({
|
|
2874
|
+
code: "ICON_192X192_MISSING",
|
|
2875
|
+
message: "Icon 192x192 is required for PWA installability",
|
|
2876
|
+
severity: "error",
|
|
2877
|
+
suggestion: "Generate a 192x192 icon using universal-pwa init --icon-source <path>"
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
if (!has512x512) {
|
|
2881
|
+
errors.push({
|
|
2882
|
+
code: "ICON_512X512_MISSING",
|
|
2883
|
+
message: "Icon 512x512 is required for PWA installability",
|
|
2884
|
+
severity: "error",
|
|
2885
|
+
suggestion: "Generate a 512x512 icon using universal-pwa init --icon-source <path>"
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
return {
|
|
2889
|
+
exists: manifest.icons.length > 0,
|
|
2890
|
+
valid: errors.filter((e) => e.severity === "error").length === 0 && has192x192 && has512x512,
|
|
2891
|
+
has192x192,
|
|
2892
|
+
has512x512,
|
|
2893
|
+
errors
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
function validateServiceWorker(projectPath, outputDir) {
|
|
2897
|
+
const errors = [];
|
|
2898
|
+
const swPath = (0, import_path11.join)(outputDir, "sw.js");
|
|
2899
|
+
if (!(0, import_fs13.existsSync)(swPath)) {
|
|
2900
|
+
return {
|
|
2901
|
+
exists: false,
|
|
2902
|
+
valid: false,
|
|
2903
|
+
errors: [
|
|
2904
|
+
{
|
|
2905
|
+
code: "SERVICE_WORKER_MISSING",
|
|
2906
|
+
message: "Service worker (sw.js) is missing",
|
|
2907
|
+
severity: "error",
|
|
2908
|
+
file: swPath,
|
|
2909
|
+
suggestion: "Generate service worker using universal-pwa init"
|
|
2910
|
+
}
|
|
2911
|
+
]
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
try {
|
|
2915
|
+
const swContent = (0, import_fs13.readFileSync)(swPath, "utf-8");
|
|
2916
|
+
if (!swContent.includes("workbox") && !swContent.includes("serviceWorker")) {
|
|
2917
|
+
errors.push({
|
|
2918
|
+
code: "SERVICE_WORKER_INVALID",
|
|
2919
|
+
message: "Service worker does not appear to be a valid Workbox service worker",
|
|
2920
|
+
severity: "warning",
|
|
2921
|
+
file: swPath,
|
|
2922
|
+
suggestion: "Ensure service worker is generated using Workbox"
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
if (!swContent.includes("precache") && !swContent.includes("precacheAndRoute")) {
|
|
2926
|
+
errors.push({
|
|
2927
|
+
code: "SERVICE_WORKER_NO_PRECACHE",
|
|
2928
|
+
message: "Service worker does not appear to have precaching configured",
|
|
2929
|
+
severity: "warning",
|
|
2930
|
+
file: swPath,
|
|
2931
|
+
suggestion: "Ensure service worker includes precaching for offline support"
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
return {
|
|
2935
|
+
exists: true,
|
|
2936
|
+
valid: errors.filter((e) => e.severity === "error").length === 0,
|
|
2937
|
+
errors
|
|
2938
|
+
};
|
|
2939
|
+
} catch (error) {
|
|
2940
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2941
|
+
return {
|
|
2942
|
+
exists: true,
|
|
2943
|
+
valid: false,
|
|
2944
|
+
errors: [
|
|
2945
|
+
{
|
|
2946
|
+
code: "SERVICE_WORKER_READ_ERROR",
|
|
2947
|
+
message: `Failed to read service worker: ${errorMessage}`,
|
|
2948
|
+
severity: "error",
|
|
2949
|
+
file: swPath
|
|
2950
|
+
}
|
|
2951
|
+
]
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
async function validateMetaTags(htmlFiles, manifestPath, serviceWorkerPath, maxHtmlFiles) {
|
|
2956
|
+
const errors = [];
|
|
2957
|
+
if (htmlFiles.length === 0) {
|
|
2958
|
+
errors.push({
|
|
2959
|
+
code: "HTML_FILES_MISSING",
|
|
2960
|
+
message: "No HTML files found to validate",
|
|
2961
|
+
severity: "warning",
|
|
2962
|
+
suggestion: "Ensure HTML files exist in the project"
|
|
2963
|
+
});
|
|
2964
|
+
return { valid: false, errors };
|
|
2965
|
+
}
|
|
2966
|
+
const maxHtmlFilesLimit = typeof maxHtmlFiles === "number" && maxHtmlFiles > 0 ? maxHtmlFiles : void 0;
|
|
2967
|
+
const htmlFilesToProcess = maxHtmlFilesLimit ? htmlFiles.slice(0, maxHtmlFilesLimit) : htmlFiles;
|
|
2968
|
+
for (const htmlFile of htmlFilesToProcess) {
|
|
2969
|
+
try {
|
|
2970
|
+
const htmlContent = await (0, import_promises.readFile)(htmlFile, "utf-8");
|
|
2971
|
+
const hasManifestLink = htmlContent.includes("manifest.json") || htmlContent.includes('rel="manifest"');
|
|
2972
|
+
if (!hasManifestLink) {
|
|
2973
|
+
errors.push({
|
|
2974
|
+
code: "META_MANIFEST_MISSING",
|
|
2975
|
+
message: `HTML file missing manifest link: ${htmlFile}`,
|
|
2976
|
+
severity: "error",
|
|
2977
|
+
file: htmlFile,
|
|
2978
|
+
suggestion: 'Add <link rel="manifest" href="/manifest.json"> to <head>'
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
if (!htmlContent.includes("theme-color") && !htmlContent.includes("theme_color")) {
|
|
2982
|
+
errors.push({
|
|
2983
|
+
code: "META_THEME_COLOR_MISSING",
|
|
2984
|
+
message: `HTML file missing theme-color meta tag: ${htmlFile}`,
|
|
2985
|
+
severity: "warning",
|
|
2986
|
+
file: htmlFile,
|
|
2987
|
+
suggestion: 'Add <meta name="theme-color" content="#..."> to <head>'
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
if (!htmlContent.includes("apple-mobile-web-app-capable")) {
|
|
2991
|
+
errors.push({
|
|
2992
|
+
code: "META_APPLE_MOBILE_MISSING",
|
|
2993
|
+
message: `HTML file missing apple-mobile-web-app-capable meta tag: ${htmlFile}`,
|
|
2994
|
+
severity: "warning",
|
|
2995
|
+
file: htmlFile,
|
|
2996
|
+
suggestion: 'Add <meta name="apple-mobile-web-app-capable" content="yes"> to <head>'
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
if (serviceWorkerPath && !htmlContent.includes("serviceWorker") && !htmlContent.includes("navigator.serviceWorker")) {
|
|
3000
|
+
errors.push({
|
|
3001
|
+
code: "META_SERVICE_WORKER_REGISTRATION_MISSING",
|
|
3002
|
+
message: `HTML file missing service worker registration: ${htmlFile}`,
|
|
3003
|
+
severity: "warning",
|
|
3004
|
+
file: htmlFile,
|
|
3005
|
+
suggestion: "Add service worker registration script to HTML"
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
} catch (error) {
|
|
3009
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3010
|
+
errors.push({
|
|
3011
|
+
code: "HTML_READ_ERROR",
|
|
3012
|
+
message: `Failed to read HTML file ${htmlFile}: ${errorMessage}`,
|
|
3013
|
+
severity: "error",
|
|
3014
|
+
file: htmlFile
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
return {
|
|
3019
|
+
// Considérer la section invalide dès qu'au moins une anomalie est détectée
|
|
3020
|
+
valid: errors.length === 0,
|
|
3021
|
+
errors
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
function validateHttps(_projectPath) {
|
|
3025
|
+
return {
|
|
3026
|
+
isSecure: false,
|
|
3027
|
+
// Par défaut, on assume que ce n'est pas HTTPS (sera vérifié en production)
|
|
3028
|
+
isLocalhost: false,
|
|
3029
|
+
errors: [
|
|
3030
|
+
{
|
|
3031
|
+
code: "HTTPS_NOT_VERIFIED",
|
|
3032
|
+
message: "HTTPS cannot be verified in local environment. Ensure HTTPS is enabled in production.",
|
|
3033
|
+
severity: "warning",
|
|
3034
|
+
suggestion: "PWA requires HTTPS in production. Use a service like Let's Encrypt or a hosting provider with HTTPS."
|
|
3035
|
+
}
|
|
3036
|
+
]
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
3039
|
+
function calculatePWAScore(result) {
|
|
3040
|
+
let score = 100;
|
|
3041
|
+
for (const error of result.errors) {
|
|
3042
|
+
if (error.severity === "error") {
|
|
3043
|
+
score -= 10;
|
|
3044
|
+
} else if (error.severity === "warning") {
|
|
3045
|
+
score -= 5;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
score -= result.warnings.length * 3;
|
|
3049
|
+
if (!result.details.manifest.exists) {
|
|
3050
|
+
score -= 20;
|
|
3051
|
+
} else if (!result.details.manifest.valid) {
|
|
3052
|
+
score -= 15;
|
|
3053
|
+
}
|
|
3054
|
+
if (!result.details.icons.exists) {
|
|
3055
|
+
score -= 20;
|
|
3056
|
+
} else if (!result.details.icons.has192x192 || !result.details.icons.has512x512) {
|
|
3057
|
+
score -= 15;
|
|
3058
|
+
}
|
|
3059
|
+
if (!result.details.serviceWorker.exists) {
|
|
3060
|
+
score -= 20;
|
|
3061
|
+
} else if (!result.details.serviceWorker.valid) {
|
|
3062
|
+
score -= 10;
|
|
3063
|
+
}
|
|
3064
|
+
if (!result.details.metaTags.valid) {
|
|
3065
|
+
score -= 10;
|
|
3066
|
+
}
|
|
3067
|
+
return Math.max(0, Math.min(100, score));
|
|
3068
|
+
}
|
|
3069
|
+
async function validatePWA(options) {
|
|
3070
|
+
const { projectPath, outputDir = "public", htmlFiles = [], strict = false } = options;
|
|
3071
|
+
const finalOutputDir = outputDir.startsWith("/") ? outputDir : (0, import_path11.join)(projectPath, outputDir);
|
|
3072
|
+
const manifestValidation = validateManifest(projectPath, finalOutputDir);
|
|
3073
|
+
const iconsValidation = validateIcons(projectPath, finalOutputDir, manifestValidation.manifest);
|
|
3074
|
+
const serviceWorkerValidation = validateServiceWorker(projectPath, finalOutputDir);
|
|
3075
|
+
const manifestPath = manifestValidation.exists ? (0, import_path11.join)(finalOutputDir, "manifest.json") : void 0;
|
|
3076
|
+
const serviceWorkerPath = serviceWorkerValidation.exists ? (0, import_path11.join)(finalOutputDir, "sw.js") : void 0;
|
|
3077
|
+
const metaTagsValidation = await validateMetaTags(htmlFiles, manifestPath, serviceWorkerPath, options.maxHtmlFiles);
|
|
3078
|
+
const httpsValidation = validateHttps(projectPath);
|
|
3079
|
+
const allErrors = [
|
|
3080
|
+
...manifestValidation.errors,
|
|
3081
|
+
...iconsValidation.errors,
|
|
3082
|
+
...serviceWorkerValidation.errors,
|
|
3083
|
+
...metaTagsValidation.errors,
|
|
3084
|
+
...httpsValidation.errors
|
|
3085
|
+
];
|
|
3086
|
+
const errors = allErrors.filter((e) => e.severity === "error" || e.code.startsWith("META_"));
|
|
3087
|
+
const warnings = allErrors.filter((e) => e.severity === "warning" || e.severity === "info").map((error) => ({
|
|
3088
|
+
code: error.code,
|
|
3089
|
+
message: error.message,
|
|
3090
|
+
file: error.file,
|
|
3091
|
+
suggestion: error.suggestion
|
|
3092
|
+
}));
|
|
3093
|
+
const suggestions = [];
|
|
3094
|
+
if (!manifestValidation.exists) {
|
|
3095
|
+
suggestions.push("Generate manifest.json using: universal-pwa init");
|
|
3096
|
+
}
|
|
3097
|
+
if (!iconsValidation.has192x192 || !iconsValidation.has512x512) {
|
|
3098
|
+
suggestions.push("Generate icons using: universal-pwa init --icon-source <path>");
|
|
3099
|
+
}
|
|
3100
|
+
if (!serviceWorkerValidation.exists) {
|
|
3101
|
+
suggestions.push("Generate service worker using: universal-pwa init");
|
|
3102
|
+
}
|
|
3103
|
+
if (errors.length > 0) {
|
|
3104
|
+
suggestions.push("Fix all errors to achieve PWA compliance");
|
|
3105
|
+
}
|
|
3106
|
+
const isValid = errors.length === 0 && (strict ? warnings.length === 0 : true);
|
|
3107
|
+
const result = {
|
|
3108
|
+
isValid,
|
|
3109
|
+
score: 0,
|
|
3110
|
+
// Sera calculé après
|
|
3111
|
+
errors,
|
|
3112
|
+
warnings,
|
|
3113
|
+
suggestions,
|
|
3114
|
+
details: {
|
|
3115
|
+
manifest: {
|
|
3116
|
+
exists: manifestValidation.exists,
|
|
3117
|
+
valid: manifestValidation.valid,
|
|
3118
|
+
errors: manifestValidation.errors
|
|
3119
|
+
},
|
|
3120
|
+
icons: {
|
|
3121
|
+
exists: iconsValidation.exists,
|
|
3122
|
+
valid: iconsValidation.valid,
|
|
3123
|
+
has192x192: iconsValidation.has192x192,
|
|
3124
|
+
has512x512: iconsValidation.has512x512,
|
|
3125
|
+
errors: iconsValidation.errors
|
|
3126
|
+
},
|
|
3127
|
+
serviceWorker: {
|
|
3128
|
+
exists: serviceWorkerValidation.exists,
|
|
3129
|
+
valid: serviceWorkerValidation.valid,
|
|
3130
|
+
errors: serviceWorkerValidation.errors
|
|
3131
|
+
},
|
|
3132
|
+
metaTags: {
|
|
3133
|
+
valid: metaTagsValidation.valid,
|
|
3134
|
+
errors: metaTagsValidation.errors
|
|
3135
|
+
},
|
|
3136
|
+
https: {
|
|
3137
|
+
isSecure: httpsValidation.isSecure,
|
|
3138
|
+
isLocalhost: httpsValidation.isLocalhost,
|
|
3139
|
+
errors: httpsValidation.errors
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
};
|
|
3143
|
+
result.score = calculatePWAScore(result);
|
|
1487
3144
|
return result;
|
|
1488
3145
|
}
|
|
1489
3146
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1493,13 +3150,16 @@ function injectMetaTagsInFile(filePath, options = {}) {
|
|
|
1493
3150
|
STANDARD_SPLASH_SIZES,
|
|
1494
3151
|
checkHttps,
|
|
1495
3152
|
checkProjectHttps,
|
|
3153
|
+
detectApiType,
|
|
1496
3154
|
detectArchitecture,
|
|
1497
3155
|
detectAssets,
|
|
1498
3156
|
detectFramework,
|
|
1499
3157
|
detectProjectUrl,
|
|
3158
|
+
detectUnoptimizedImages,
|
|
1500
3159
|
elementExists,
|
|
1501
3160
|
findAllElements,
|
|
1502
3161
|
findElement,
|
|
3162
|
+
generateAdaptiveCacheStrategies,
|
|
1503
3163
|
generateAndWriteManifest,
|
|
1504
3164
|
generateAndWriteServiceWorker,
|
|
1505
3165
|
generateAppleTouchIcon,
|
|
@@ -1507,16 +3167,23 @@ function injectMetaTagsInFile(filePath, options = {}) {
|
|
|
1507
3167
|
generateIcons,
|
|
1508
3168
|
generateIconsOnly,
|
|
1509
3169
|
generateManifest,
|
|
3170
|
+
generateOptimalShortName,
|
|
1510
3171
|
generateReport,
|
|
3172
|
+
generateResponsiveImageSizes,
|
|
1511
3173
|
generateServiceWorker,
|
|
1512
3174
|
generateSimpleServiceWorker,
|
|
1513
3175
|
generateSplashScreensOnly,
|
|
1514
3176
|
injectMetaTags,
|
|
1515
3177
|
injectMetaTagsInFile,
|
|
3178
|
+
optimizeImage,
|
|
3179
|
+
optimizeProject,
|
|
3180
|
+
optimizeProjectImages,
|
|
1516
3181
|
parseHTML,
|
|
1517
3182
|
parseHTMLFile,
|
|
1518
3183
|
scanProject,
|
|
1519
3184
|
serializeHTML,
|
|
3185
|
+
suggestManifestColors,
|
|
3186
|
+
validatePWA,
|
|
1520
3187
|
validateProjectPath,
|
|
1521
3188
|
writeManifest
|
|
1522
3189
|
});
|