@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 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 import_fs4 = require("fs");
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 { framework, confidence, indicators };
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 { framework, confidence, indicators };
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 { framework, confidence, indicators };
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 { framework, confidence, indicators };
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 { framework, confidence, indicators };
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
- return { framework, confidence, indicators };
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 import_glob = require("glob");
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, import_glob.glob)(jsPattern, {
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, import_glob.glob)(jsRootPattern, {
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, import_glob.glob)(cssPattern, {
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, import_glob.glob)(cssRootPattern, {
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, import_glob.glob)(pattern, {
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, import_glob.glob)(pattern, {
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, import_glob.glob)(`**/*{route,api,graphql}*.{js,ts,json}`, {
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 import_glob2 = require("glob");
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, import_glob2.glob)(pattern, {
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, import_glob2.glob)("**/*.{js,ts,tsx,jsx}", {
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
- if (routerFilesFound > 0) {
475
- indicators.push(`JS: router patterns found (${routerFilesFound} files)`);
476
- if (architecture === "static") {
477
- architecture = "spa";
478
- confidence = confidence === "low" ? "medium" : confidence;
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
- return { architecture, buildTool, confidence, indicators };
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 { projectPath, includeAssets = true, includeArchitecture = true } = options;
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 : { framework: null, confidence: "low", indicators: [] };
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
- return {
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, import_fs4.existsSync)(projectPath);
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
- return (v.framework === null || typeof v.framework === "string") && (v.confidence === "low" || v.confidence === "medium" || v.confidence === "high") && Array.isArray(v.indicators);
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 import_fs5 = require("fs");
552
- var import_path4 = require("path");
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.substring(0, 12);
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, import_path4.join)(outputDir, "manifest.json");
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, import_fs5.writeFileSync)(manifestPath, manifestJson, { encoding: "utf8" });
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 import_sharp = __toESM(require("sharp"), 1);
643
- var import_fs6 = require("fs");
644
- var import_path5 = require("path");
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, import_fs6.existsSync)(sourceImage)) {
1855
+ if (!(0, import_fs8.existsSync)(sourceImage)) {
681
1856
  throw new Error(`Source image not found: ${sourceImage}`);
682
1857
  }
683
- (0, import_fs6.mkdirSync)(outputDir, { recursive: true });
1858
+ (0, import_fs8.mkdirSync)(outputDir, { recursive: true });
684
1859
  const generatedFiles = [];
685
1860
  const icons = [];
686
1861
  const splashScreens = [];
687
- const image = (0, import_sharp.default)(sourceImage);
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, import_path5.join)(outputDir, size.name);
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, import_path5.join)(outputDir, size.name);
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, import_path5.join)(outputDir, "apple-touch-icon.png");
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, import_fs6.existsSync)(sourceImage)) {
1956
+ if (!(0, import_fs8.existsSync)(sourceImage)) {
782
1957
  throw new Error(`Source image not found: ${sourceImage}`);
783
1958
  }
784
- (0, import_fs6.mkdirSync)(outputDir, { recursive: true });
785
- const faviconPath = (0, import_path5.join)(outputDir, "favicon.ico");
1959
+ (0, import_fs8.mkdirSync)(outputDir, { recursive: true });
1960
+ const faviconPath = (0, import_path8.join)(outputDir, "favicon.ico");
786
1961
  try {
787
- await (0, import_sharp.default)(sourceImage).resize(32, 32, {
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, import_fs6.existsSync)(sourceImage)) {
1973
+ if (!(0, import_fs8.existsSync)(sourceImage)) {
799
1974
  throw new Error(`Source image not found: ${sourceImage}`);
800
1975
  }
801
- (0, import_fs6.mkdirSync)(outputDir, { recursive: true });
802
- const appleIconPath = (0, import_path5.join)(outputDir, "apple-touch-icon.png");
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, import_sharp.default)(sourceImage).resize(180, 180, {
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 import_fs7 = require("fs");
818
- var import_path6 = require("path");
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, import_fs7.mkdirSync)(outputDir, { recursive: true });
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, import_path6.join)(outputDir, "sw-src.js");
836
- (0, import_fs7.writeFileSync)(swSrcPath, template.content, "utf-8");
837
- const swDestPath = (0, import_path6.join)(outputDir, swDest);
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, import_fs7.existsSync)(swSrcPath)) {
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, import_fs7.mkdirSync)(outputDir, { recursive: true });
883
- const swDestPath = (0, import_path6.join)(outputDir, swDest);
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 import_fs8 = require("fs");
938
- var import_path7 = require("path");
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, import_path7.join)(projectPath, config.file);
994
- if ((0, import_fs8.existsSync)(filePath)) {
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, import_fs8.readFileSync)(filePath, "utf-8"));
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, import_fs8.readFileSync)(filePath, "utf-8");
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, import_fs8.readFileSync)(filePath, "utf-8");
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, import_fs8.readFileSync)(filePath, "utf-8");
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 import_fs9 = require("fs");
2230
+ var import_fs11 = require("fs");
1056
2231
  function parseHTMLFile(filePath, options = {}) {
1057
- const content = (0, import_fs9.readFileSync)(filePath, "utf-8");
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 import_fs10 = require("fs");
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 || window.navigator.standalone === true) {
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
- return isInstalled || window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;
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
- if (modifiedHtml.includes("</body>")) {
1364
- modifiedHtml = modifiedHtml.replace("</body>", `${swScript}
1365
- </body>`);
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 = `${modifiedHtml}${swScript}`;
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 || window.navigator.standalone === true) {
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
- return isInstalled || window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;
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
- if (modifiedHtml.includes("</body>")) {
1421
- modifiedHtml = modifiedHtml.replace("</body>", `${installScript}
1422
- </body>`);
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, import_fs10.writeFileSync)(filePath, html, "utf-8");
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
  });