@universal-pwa/core 0.1.0

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.js ADDED
@@ -0,0 +1,1301 @@
1
+ // src/scanner/index.ts
2
+ import { existsSync as existsSync3 } from "fs";
3
+
4
+ // src/scanner/framework-detector.ts
5
+ import { existsSync, readFileSync } from "fs";
6
+ import { join } from "path";
7
+ function detectFramework(projectPath) {
8
+ const indicators = [];
9
+ let framework = null;
10
+ let confidence = "low";
11
+ if (existsSync(join(projectPath, "wp-config.php"))) {
12
+ indicators.push("wp-config.php");
13
+ if (existsSync(join(projectPath, "wp-content"))) {
14
+ indicators.push("wp-content/");
15
+ framework = "wordpress";
16
+ confidence = "high";
17
+ return { framework, confidence, indicators };
18
+ }
19
+ }
20
+ const composerPath = join(projectPath, "composer.json");
21
+ if (existsSync(composerPath)) {
22
+ try {
23
+ const composerContent = JSON.parse(readFileSync(composerPath, "utf-8"));
24
+ const dependencies = {
25
+ ...composerContent.require ?? {},
26
+ ...composerContent["require-dev"] ?? {}
27
+ };
28
+ if (dependencies["symfony/symfony"] || dependencies["symfony/framework-bundle"]) {
29
+ indicators.push("composer.json: symfony/*");
30
+ if (existsSync(join(projectPath, "public"))) {
31
+ indicators.push("public/");
32
+ framework = "symfony";
33
+ confidence = "high";
34
+ return { framework, confidence, indicators };
35
+ }
36
+ }
37
+ if (dependencies["laravel/framework"]) {
38
+ indicators.push("composer.json: laravel/framework");
39
+ if (existsSync(join(projectPath, "public"))) {
40
+ indicators.push("public/");
41
+ framework = "laravel";
42
+ confidence = "high";
43
+ return { framework, confidence, indicators };
44
+ }
45
+ }
46
+ } catch {
47
+ }
48
+ }
49
+ const packageJsonPath = join(projectPath, "package.json");
50
+ if (existsSync(packageJsonPath)) {
51
+ try {
52
+ const packageContent = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
53
+ const dependencies = {
54
+ ...packageContent.dependencies ?? {},
55
+ ...packageContent.devDependencies ?? {}
56
+ };
57
+ if (dependencies.next) {
58
+ indicators.push("package.json: next");
59
+ if (existsSync(join(projectPath, ".next"))) {
60
+ indicators.push(".next/");
61
+ framework = "nextjs";
62
+ confidence = "high";
63
+ return { framework, confidence, indicators };
64
+ }
65
+ }
66
+ if (dependencies.nuxt) {
67
+ indicators.push("package.json: nuxt");
68
+ if (existsSync(join(projectPath, ".nuxt"))) {
69
+ indicators.push(".nuxt/");
70
+ framework = "nuxt";
71
+ confidence = "high";
72
+ return { framework, confidence, indicators };
73
+ }
74
+ }
75
+ if (dependencies.react) {
76
+ indicators.push("package.json: react");
77
+ framework = "react";
78
+ confidence = framework ? "high" : "medium";
79
+ }
80
+ if (dependencies.vue) {
81
+ indicators.push("package.json: vue");
82
+ if (!framework) {
83
+ framework = "vue";
84
+ confidence = "high";
85
+ }
86
+ }
87
+ if (dependencies["@angular/core"]) {
88
+ indicators.push("package.json: @angular/core");
89
+ if (!framework) {
90
+ framework = "angular";
91
+ confidence = "high";
92
+ }
93
+ }
94
+ } catch {
95
+ }
96
+ }
97
+ if (!framework) {
98
+ const htmlFiles = ["index.html", "index.htm"];
99
+ const hasHtml = htmlFiles.some((file) => existsSync(join(projectPath, file)));
100
+ if (hasHtml) {
101
+ indicators.push("HTML files present");
102
+ framework = "static";
103
+ confidence = "medium";
104
+ }
105
+ }
106
+ return { framework, confidence, indicators };
107
+ }
108
+
109
+ // src/scanner/asset-detector.ts
110
+ import { glob } from "glob";
111
+ import { join as join2 } from "path";
112
+ import { statSync } from "fs";
113
+ var IGNORED_PATTERNS = [
114
+ "**/node_modules/**",
115
+ "**/.git/**",
116
+ "**/dist/**",
117
+ "**/.next/**",
118
+ "**/.nuxt/**",
119
+ "**/build/**",
120
+ "**/coverage/**"
121
+ ];
122
+ var JS_EXTENSIONS = [".js", ".mjs", ".ts", ".tsx", ".jsx"];
123
+ var CSS_EXTENSIONS = [".css", ".scss", ".sass", ".less"];
124
+ var IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif", ".ico"];
125
+ var FONT_EXTENSIONS = [".woff", ".woff2", ".ttf", ".otf", ".eot"];
126
+ var API_PATTERNS = [
127
+ "/api/**",
128
+ "/graphql",
129
+ "/rest/**",
130
+ "/v1/**",
131
+ "/v2/**"
132
+ ];
133
+ async function detectAssets(projectPath) {
134
+ const result = {
135
+ javascript: [],
136
+ css: [],
137
+ images: [],
138
+ fonts: [],
139
+ apiRoutes: []
140
+ };
141
+ const jsExtensionsStr = JS_EXTENSIONS.map((ext) => ext.slice(1)).join(",");
142
+ const jsPattern = `**/*.{${jsExtensionsStr}}`;
143
+ const jsRootPattern = `*.{${jsExtensionsStr}}`;
144
+ const [jsFiles, jsRootFiles] = await Promise.all([
145
+ glob(jsPattern, {
146
+ cwd: projectPath,
147
+ ignore: IGNORED_PATTERNS,
148
+ absolute: true,
149
+ nodir: true
150
+ }),
151
+ glob(jsRootPattern, {
152
+ cwd: projectPath,
153
+ ignore: IGNORED_PATTERNS,
154
+ absolute: true,
155
+ nodir: true
156
+ })
157
+ ]);
158
+ const allJsFiles = /* @__PURE__ */ new Set([...jsFiles, ...jsRootFiles]);
159
+ result.javascript.push(...allJsFiles);
160
+ const cssExtensionsStr = CSS_EXTENSIONS.map((ext) => ext.slice(1)).join(",");
161
+ const cssPattern = `**/*.{${cssExtensionsStr}}`;
162
+ const cssFiles = await glob(cssPattern, {
163
+ cwd: projectPath,
164
+ ignore: IGNORED_PATTERNS,
165
+ absolute: false,
166
+ nodir: true
167
+ });
168
+ const cssRootPattern = `*.{${cssExtensionsStr}}`;
169
+ const cssRootFiles = await glob(cssRootPattern, {
170
+ cwd: projectPath,
171
+ ignore: IGNORED_PATTERNS,
172
+ absolute: false,
173
+ nodir: true
174
+ });
175
+ const allCssFiles = [.../* @__PURE__ */ new Set([...cssFiles, ...cssRootFiles])];
176
+ result.css.push(...allCssFiles.map((f) => join2(projectPath, f)));
177
+ const imagePatterns = IMAGE_EXTENSIONS.flatMap((ext) => [`**/*${ext}`, `*${ext}`]);
178
+ const allImageFiles = /* @__PURE__ */ new Set();
179
+ for (const pattern of imagePatterns) {
180
+ const files = await glob(pattern, {
181
+ cwd: projectPath,
182
+ ignore: IGNORED_PATTERNS,
183
+ absolute: false,
184
+ nodir: true
185
+ });
186
+ files.forEach((f) => allImageFiles.add(f));
187
+ }
188
+ result.images.push(...Array.from(allImageFiles).map((f) => join2(projectPath, f)));
189
+ const fontPatterns = FONT_EXTENSIONS.flatMap((ext) => [`**/*${ext}`, `*${ext}`]);
190
+ const allFontFiles = /* @__PURE__ */ new Set();
191
+ for (const pattern of fontPatterns) {
192
+ const files = await glob(pattern, {
193
+ cwd: projectPath,
194
+ ignore: IGNORED_PATTERNS,
195
+ absolute: false,
196
+ nodir: true
197
+ });
198
+ files.forEach((f) => allFontFiles.add(f));
199
+ }
200
+ result.fonts.push(...Array.from(allFontFiles).map((f) => join2(projectPath, f)));
201
+ const routeFiles = await glob(`**/*{route,api,graphql}*.{js,ts,json}`, {
202
+ cwd: projectPath,
203
+ ignore: IGNORED_PATTERNS,
204
+ absolute: false,
205
+ nodir: true
206
+ });
207
+ if (routeFiles.length > 0) {
208
+ result.apiRoutes.push(...API_PATTERNS);
209
+ }
210
+ result.javascript = result.javascript.filter((f) => {
211
+ try {
212
+ return statSync(f).isFile();
213
+ } catch {
214
+ return false;
215
+ }
216
+ });
217
+ result.css = result.css.filter((f) => {
218
+ try {
219
+ return statSync(f).isFile();
220
+ } catch {
221
+ return false;
222
+ }
223
+ });
224
+ result.images = result.images.filter((f) => {
225
+ try {
226
+ return statSync(f).isFile();
227
+ } catch {
228
+ return false;
229
+ }
230
+ });
231
+ result.fonts = result.fonts.filter((f) => {
232
+ try {
233
+ return statSync(f).isFile();
234
+ } catch {
235
+ return false;
236
+ }
237
+ });
238
+ return result;
239
+ }
240
+
241
+ // src/scanner/architecture-detector.ts
242
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
243
+ import { join as join3 } from "path";
244
+ import { glob as glob2 } from "glob";
245
+ async function detectArchitecture(projectPath) {
246
+ const indicators = [];
247
+ let architecture = "static";
248
+ let buildTool = null;
249
+ let confidence = "low";
250
+ const packageJsonPath = join3(projectPath, "package.json");
251
+ if (existsSync2(packageJsonPath)) {
252
+ try {
253
+ const packageContent = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
254
+ const dependencies = {
255
+ ...packageContent.dependencies ?? {},
256
+ ...packageContent.devDependencies ?? {}
257
+ };
258
+ if (dependencies.vite || packageContent.devDependencies?.["vite"]) {
259
+ indicators.push("package.json: vite");
260
+ buildTool = "vite";
261
+ confidence = "high";
262
+ } else if (dependencies.webpack || packageContent.devDependencies?.["webpack"]) {
263
+ indicators.push("package.json: webpack");
264
+ buildTool = "webpack";
265
+ confidence = "high";
266
+ } else if (dependencies.rollup || packageContent.devDependencies?.["rollup"]) {
267
+ indicators.push("package.json: rollup");
268
+ buildTool = "rollup";
269
+ confidence = "high";
270
+ } else if (dependencies.esbuild || packageContent.devDependencies?.["esbuild"]) {
271
+ indicators.push("package.json: esbuild");
272
+ buildTool = "esbuild";
273
+ confidence = "high";
274
+ } else if (dependencies.parcel || packageContent.devDependencies?.["parcel"]) {
275
+ indicators.push("package.json: parcel");
276
+ buildTool = "parcel";
277
+ confidence = "high";
278
+ } else if (dependencies["@turbo/gen"] || packageContent.devDependencies?.["@turbo/gen"]) {
279
+ indicators.push("package.json: turbopack");
280
+ buildTool = "turbopack";
281
+ confidence = "high";
282
+ }
283
+ if (dependencies.next || packageContent.dependencies?.["next"] || packageContent.devDependencies?.["next"]) {
284
+ architecture = "ssr";
285
+ confidence = "high";
286
+ indicators.push("Next.js detected \u2192 SSR");
287
+ } else if (dependencies.nuxt || packageContent.dependencies?.["nuxt"] || packageContent.devDependencies?.["nuxt"]) {
288
+ architecture = "ssr";
289
+ confidence = "high";
290
+ indicators.push("Nuxt detected \u2192 SSR");
291
+ }
292
+ } catch {
293
+ }
294
+ }
295
+ const htmlPatterns = ["**/*.html", "*.html", "index.html"];
296
+ const allHtmlFiles = /* @__PURE__ */ new Set();
297
+ for (const pattern of htmlPatterns) {
298
+ const files = await glob2(pattern, {
299
+ cwd: projectPath,
300
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/.nuxt/**"],
301
+ absolute: false,
302
+ nodir: true
303
+ });
304
+ files.forEach((f) => allHtmlFiles.add(f));
305
+ }
306
+ if (allHtmlFiles.size > 0 && architecture !== "ssr") {
307
+ const htmlArray = Array.from(allHtmlFiles);
308
+ const firstHtml = htmlArray.find((f) => f === "index.html" || f.endsWith("/index.html")) || htmlArray[0];
309
+ const htmlPath = join3(projectPath, firstHtml);
310
+ try {
311
+ const htmlContent = readFileSync2(htmlPath, "utf-8").toLowerCase();
312
+ const spaPatterns = [
313
+ /<div[^>]*id=["']root["']/i,
314
+ /<div[^>]*id=["']app["']/i,
315
+ /<div[^>]*id=["']main["']/i,
316
+ /react-dom/i,
317
+ /mount\(/i,
318
+ /createRoot\(/i
319
+ ];
320
+ const ssrPatterns = [
321
+ /<body[^>]*>[\s\S]{100,}/i,
322
+ // Body avec beaucoup de contenu
323
+ /<article/i,
324
+ /<main[^>]*>[\s\S]{50,}/i,
325
+ /hydrat/i,
326
+ /__next/i,
327
+ /__next_data__/i,
328
+ /nuxt/i
329
+ ];
330
+ const hasSpaPattern = spaPatterns.some((pattern) => pattern.test(htmlContent));
331
+ const hasSsrPattern = ssrPatterns.some((pattern) => pattern.test(htmlContent));
332
+ const isLargeContent = htmlContent.length > 2e3;
333
+ const hasRealContent = htmlContent.replace(/<[^>]+>/g, "").trim().length > 100;
334
+ if (isLargeContent && hasRealContent && !hasSsrPattern) {
335
+ indicators.push("HTML: very large content with real text \u2192 SSR");
336
+ if (hasSpaPattern) {
337
+ indicators.push("HTML: SPA pattern but large content \u2192 SSR");
338
+ }
339
+ architecture = "ssr";
340
+ confidence = "high";
341
+ } else if (hasSsrPattern && htmlContent.length > 1e3) {
342
+ indicators.push("HTML: SSR patterns detected (large content)");
343
+ if (isLargeContent && hasRealContent) {
344
+ indicators.push("HTML: very large content with real text \u2192 SSR");
345
+ }
346
+ if (hasSpaPattern) {
347
+ indicators.push("HTML: SPA pattern but large content \u2192 SSR");
348
+ }
349
+ architecture = "ssr";
350
+ confidence = "high";
351
+ } else if (hasSsrPattern && !hasSpaPattern) {
352
+ indicators.push("HTML: SSR patterns detected");
353
+ if (htmlContent.length > 500) {
354
+ indicators.push("HTML: large content");
355
+ }
356
+ architecture = "ssr";
357
+ confidence = "high";
358
+ } else if (hasSpaPattern) {
359
+ if (isLargeContent && hasRealContent) {
360
+ indicators.push("HTML: SPA pattern but large content \u2192 SSR");
361
+ architecture = "ssr";
362
+ confidence = "medium";
363
+ } else {
364
+ indicators.push("HTML: SPA patterns detected");
365
+ architecture = "spa";
366
+ confidence = "high";
367
+ }
368
+ } else if (htmlContent.length > 500) {
369
+ indicators.push("HTML: large content");
370
+ architecture = "ssr";
371
+ confidence = "medium";
372
+ } else {
373
+ indicators.push("HTML: minimal content");
374
+ architecture = "static";
375
+ confidence = "medium";
376
+ }
377
+ } catch {
378
+ }
379
+ }
380
+ const jsFiles = await glob2("**/*.{js,ts,tsx,jsx}", {
381
+ cwd: projectPath,
382
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/.nuxt/**", "**/*.test.*", "**/*.spec.*"],
383
+ absolute: false,
384
+ nodir: true
385
+ });
386
+ if (jsFiles.length > 0 && architecture === "static") {
387
+ let routerFilesFound = 0;
388
+ for (const jsFile of jsFiles.slice(0, 10)) {
389
+ try {
390
+ const jsPath = join3(projectPath, jsFile);
391
+ const jsContent = readFileSync2(jsPath, "utf-8").toLowerCase();
392
+ const routerPatterns = [
393
+ /react-router/i,
394
+ /vue-router/i,
395
+ /@angular\/router/i,
396
+ /next\/router/i,
397
+ /nuxt/i,
398
+ /createBrowserRouter/i,
399
+ /BrowserRouter/i,
400
+ /Router/i
401
+ ];
402
+ if (routerPatterns.some((pattern) => pattern.test(jsContent))) {
403
+ routerFilesFound++;
404
+ }
405
+ } catch {
406
+ }
407
+ }
408
+ if (routerFilesFound > 0) {
409
+ indicators.push(`JS: router patterns found (${routerFilesFound} files)`);
410
+ if (architecture === "static") {
411
+ architecture = "spa";
412
+ confidence = confidence === "low" ? "medium" : confidence;
413
+ }
414
+ }
415
+ }
416
+ return { architecture, buildTool, confidence, indicators };
417
+ }
418
+
419
+ // src/scanner/index.ts
420
+ async function scanProject(options) {
421
+ const { projectPath, includeAssets = true, includeArchitecture = true } = options;
422
+ const frameworkCandidate = detectFramework(projectPath);
423
+ const framework = isFrameworkDetectionResult(frameworkCandidate) ? frameworkCandidate : { framework: null, confidence: "low", indicators: [] };
424
+ const assetsCandidate = includeAssets ? await detectAssets(projectPath) : getEmptyAssets();
425
+ const assets = isAssetDetectionResult(assetsCandidate) ? assetsCandidate : getEmptyAssets();
426
+ const architectureCandidate = includeArchitecture ? await detectArchitecture(projectPath) : getEmptyArchitecture();
427
+ const architecture = isArchitectureDetectionResult(architectureCandidate) ? architectureCandidate : getEmptyArchitecture();
428
+ return {
429
+ framework,
430
+ assets,
431
+ architecture,
432
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
433
+ projectPath
434
+ };
435
+ }
436
+ function generateReport(result) {
437
+ return JSON.stringify(result, null, 2);
438
+ }
439
+ function validateProjectPath(projectPath) {
440
+ try {
441
+ return existsSync3(projectPath);
442
+ } catch {
443
+ return false;
444
+ }
445
+ }
446
+ function isFrameworkDetectionResult(value) {
447
+ if (!value || typeof value !== "object") return false;
448
+ const v = value;
449
+ return (v.framework === null || typeof v.framework === "string") && (v.confidence === "low" || v.confidence === "medium" || v.confidence === "high") && Array.isArray(v.indicators);
450
+ }
451
+ function isAssetDetectionResult(value) {
452
+ if (!value || typeof value !== "object") return false;
453
+ const v = value;
454
+ const isStringArray = (x) => Array.isArray(x) && x.every((i) => typeof i === "string");
455
+ return isStringArray(v.javascript) && isStringArray(v.css) && isStringArray(v.images) && isStringArray(v.fonts) && isStringArray(v.apiRoutes);
456
+ }
457
+ function isArchitectureDetectionResult(value) {
458
+ if (!value || typeof value !== "object") return false;
459
+ const v = value;
460
+ const isArch = v.architecture === "spa" || v.architecture === "ssr" || v.architecture === "static";
461
+ const isBuildTool = v.buildTool === null || v.buildTool === "vite" || v.buildTool === "webpack" || v.buildTool === "rollup";
462
+ const isConfidence = v.confidence === "low" || v.confidence === "medium" || v.confidence === "high";
463
+ return isArch && isBuildTool && isConfidence && Array.isArray(v.indicators);
464
+ }
465
+ function getEmptyAssets() {
466
+ return {
467
+ javascript: [],
468
+ css: [],
469
+ images: [],
470
+ fonts: [],
471
+ apiRoutes: []
472
+ };
473
+ }
474
+ function getEmptyArchitecture() {
475
+ return {
476
+ architecture: "static",
477
+ buildTool: null,
478
+ confidence: "low",
479
+ indicators: []
480
+ };
481
+ }
482
+
483
+ // src/generator/manifest-generator.ts
484
+ import { z } from "zod";
485
+ import { writeFileSync } from "fs";
486
+ import { join as join4 } from "path";
487
+ var ManifestIconSchema = z.object({
488
+ src: z.string(),
489
+ sizes: z.string(),
490
+ type: z.string().optional(),
491
+ purpose: z.string().optional()
492
+ });
493
+ var ManifestSplashScreenSchema = z.object({
494
+ src: z.string(),
495
+ sizes: z.string(),
496
+ type: z.string().optional()
497
+ });
498
+ var ManifestSchema = z.object({
499
+ name: z.string().min(1),
500
+ short_name: z.string().min(1).max(12),
501
+ description: z.string().optional(),
502
+ start_url: z.string().default("/"),
503
+ scope: z.string().default("/"),
504
+ display: z.enum(["standalone", "fullscreen", "minimal-ui", "browser"]).default("standalone"),
505
+ orientation: z.enum(["any", "portrait", "landscape"]).optional(),
506
+ theme_color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
507
+ background_color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
508
+ icons: z.array(ManifestIconSchema).min(1),
509
+ splash_screens: z.array(ManifestSplashScreenSchema).optional(),
510
+ categories: z.array(z.string()).optional(),
511
+ lang: z.string().optional(),
512
+ dir: z.enum(["ltr", "rtl", "auto"]).optional(),
513
+ prefer_related_applications: z.boolean().optional(),
514
+ related_applications: z.array(z.unknown()).optional()
515
+ });
516
+ function generateManifest(options) {
517
+ const manifest = {
518
+ name: options.name,
519
+ short_name: options.shortName,
520
+ start_url: options.startUrl ?? "/",
521
+ scope: options.scope ?? "/",
522
+ display: options.display ?? "standalone",
523
+ icons: options.icons
524
+ };
525
+ if (options.description) {
526
+ manifest.description = options.description;
527
+ }
528
+ if (options.orientation) {
529
+ manifest.orientation = options.orientation;
530
+ }
531
+ if (options.themeColor) {
532
+ manifest.theme_color = options.themeColor;
533
+ }
534
+ if (options.backgroundColor) {
535
+ manifest.background_color = options.backgroundColor;
536
+ }
537
+ if (options.splashScreens && options.splashScreens.length > 0) {
538
+ manifest.splash_screens = options.splashScreens;
539
+ }
540
+ if (options.categories && options.categories.length > 0) {
541
+ manifest.categories = options.categories;
542
+ }
543
+ if (options.lang) {
544
+ manifest.lang = options.lang;
545
+ }
546
+ if (options.dir) {
547
+ manifest.dir = options.dir;
548
+ }
549
+ if (options.preferRelatedApplications !== void 0) {
550
+ manifest.prefer_related_applications = options.preferRelatedApplications;
551
+ }
552
+ if (options.relatedApplications && options.relatedApplications.length > 0) {
553
+ manifest.related_applications = options.relatedApplications;
554
+ }
555
+ return ManifestSchema.parse(manifest);
556
+ }
557
+ function writeManifest(manifest, outputDir) {
558
+ const manifestPath = join4(outputDir, "manifest.json");
559
+ const manifestJson = JSON.stringify(manifest, null, 2);
560
+ writeFileSync(manifestPath, manifestJson, "utf-8");
561
+ return manifestPath;
562
+ }
563
+ function generateAndWriteManifest(options, outputDir) {
564
+ const manifest = generateManifest(options);
565
+ return writeManifest(manifest, outputDir);
566
+ }
567
+
568
+ // src/generator/icon-generator.ts
569
+ import sharp from "sharp";
570
+ import { existsSync as existsSync4, mkdirSync } from "fs";
571
+ import { join as join5 } from "path";
572
+ var STANDARD_ICON_SIZES = [
573
+ { width: 72, height: 72, name: "icon-72x72.png" },
574
+ { width: 96, height: 96, name: "icon-96x96.png" },
575
+ { width: 128, height: 128, name: "icon-128x128.png" },
576
+ { width: 144, height: 144, name: "icon-144x144.png" },
577
+ { width: 152, height: 152, name: "icon-152x152.png" },
578
+ { width: 192, height: 192, name: "icon-192x192.png" },
579
+ { width: 384, height: 384, name: "icon-384x384.png" },
580
+ { width: 512, height: 512, name: "icon-512x512.png" }
581
+ ];
582
+ var STANDARD_SPLASH_SIZES = [
583
+ { width: 640, height: 1136, name: "splash-640x1136.png" },
584
+ // iPhone 5
585
+ { width: 750, height: 1334, name: "splash-750x1334.png" },
586
+ // iPhone 6/7/8
587
+ { width: 828, height: 1792, name: "splash-828x1792.png" },
588
+ // iPhone XR
589
+ { width: 1125, height: 2436, name: "splash-1125x2436.png" },
590
+ // iPhone X/XS
591
+ { width: 1242, height: 2688, name: "splash-1242x2688.png" },
592
+ // iPhone XS Max
593
+ { width: 1536, height: 2048, name: "splash-1536x2048.png" },
594
+ // iPad
595
+ { width: 2048, height: 2732, name: "splash-2048x2732.png" }
596
+ // iPad Pro
597
+ ];
598
+ async function generateIcons(options) {
599
+ const {
600
+ sourceImage,
601
+ outputDir,
602
+ iconSizes = STANDARD_ICON_SIZES,
603
+ splashSizes = STANDARD_SPLASH_SIZES,
604
+ format = "png",
605
+ quality = 90
606
+ } = options;
607
+ if (!existsSync4(sourceImage)) {
608
+ throw new Error(`Source image not found: ${sourceImage}`);
609
+ }
610
+ mkdirSync(outputDir, { recursive: true });
611
+ const generatedFiles = [];
612
+ const icons = [];
613
+ const splashScreens = [];
614
+ const image = sharp(sourceImage);
615
+ const metadata = await image.metadata();
616
+ if (!metadata.width || !metadata.height) {
617
+ throw new Error("Unable to read image dimensions");
618
+ }
619
+ for (const size of iconSizes) {
620
+ const outputPath = join5(outputDir, size.name);
621
+ try {
622
+ let pipeline = image.clone().resize(size.width, size.height, {
623
+ fit: "cover",
624
+ position: "center"
625
+ });
626
+ if (format === "png") {
627
+ pipeline = pipeline.png({ quality, compressionLevel: 9 });
628
+ } else {
629
+ pipeline = pipeline.webp({ quality });
630
+ }
631
+ await pipeline.toFile(outputPath);
632
+ generatedFiles.push(outputPath);
633
+ icons.push({
634
+ src: `/${size.name}`,
635
+ sizes: `${size.width}x${size.height}`,
636
+ type: format === "png" ? "image/png" : "image/webp",
637
+ purpose: size.width >= 192 && size.width <= 512 ? "any" : void 0
638
+ });
639
+ } catch (err) {
640
+ const message = err instanceof Error ? err.message : String(err);
641
+ throw new Error(`Failed to generate icon ${size.name}: ${message}`);
642
+ }
643
+ }
644
+ for (const size of splashSizes) {
645
+ const outputPath = join5(outputDir, size.name);
646
+ try {
647
+ let pipeline = image.clone().resize(size.width, size.height, {
648
+ fit: "cover",
649
+ position: "center"
650
+ });
651
+ if (format === "png") {
652
+ pipeline = pipeline.png({ quality, compressionLevel: 9 });
653
+ } else {
654
+ pipeline = pipeline.webp({ quality });
655
+ }
656
+ await pipeline.toFile(outputPath);
657
+ generatedFiles.push(outputPath);
658
+ splashScreens.push({
659
+ src: `/${size.name}`,
660
+ sizes: `${size.width}x${size.height}`,
661
+ type: format === "png" ? "image/png" : "image/webp"
662
+ });
663
+ } catch (err) {
664
+ const message = err instanceof Error ? err.message : String(err);
665
+ throw new Error(`Failed to generate splash screen ${size.name}: ${message}`);
666
+ }
667
+ }
668
+ return {
669
+ icons,
670
+ splashScreens,
671
+ generatedFiles
672
+ };
673
+ }
674
+ async function generateIconsOnly(options) {
675
+ const result = await generateIcons({
676
+ ...options,
677
+ splashSizes: []
678
+ });
679
+ return {
680
+ icons: result.icons,
681
+ generatedFiles: result.generatedFiles
682
+ };
683
+ }
684
+ async function generateSplashScreensOnly(options) {
685
+ const result = await generateIcons({
686
+ ...options,
687
+ iconSizes: []
688
+ });
689
+ return {
690
+ splashScreens: result.splashScreens,
691
+ generatedFiles: result.generatedFiles
692
+ };
693
+ }
694
+ async function generateFavicon(sourceImage, outputDir) {
695
+ if (!existsSync4(sourceImage)) {
696
+ throw new Error(`Source image not found: ${sourceImage}`);
697
+ }
698
+ mkdirSync(outputDir, { recursive: true });
699
+ const faviconPath = join5(outputDir, "favicon.ico");
700
+ try {
701
+ await sharp(sourceImage).resize(32, 32, {
702
+ fit: "cover",
703
+ position: "center"
704
+ }).png().toFile(faviconPath);
705
+ return faviconPath;
706
+ } catch (err) {
707
+ const message = err instanceof Error ? err.message : String(err);
708
+ throw new Error(`Failed to generate favicon: ${message}`);
709
+ }
710
+ }
711
+ async function generateAppleTouchIcon(sourceImage, outputDir) {
712
+ if (!existsSync4(sourceImage)) {
713
+ throw new Error(`Source image not found: ${sourceImage}`);
714
+ }
715
+ mkdirSync(outputDir, { recursive: true });
716
+ const appleIconPath = join5(outputDir, "apple-touch-icon.png");
717
+ try {
718
+ await sharp(sourceImage).resize(180, 180, {
719
+ fit: "cover",
720
+ position: "center"
721
+ }).png({ quality: 90, compressionLevel: 9 }).toFile(appleIconPath);
722
+ return appleIconPath;
723
+ } catch (err) {
724
+ const message = err instanceof Error ? err.message : String(err);
725
+ throw new Error(`Failed to generate apple-touch-icon: ${message}`);
726
+ }
727
+ }
728
+
729
+ // src/generator/service-worker-generator.ts
730
+ import { injectManifest, generateSW } from "workbox-build";
731
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
732
+ import { join as join6 } from "path";
733
+ import { getServiceWorkerTemplate, determineTemplateType } from "@universal-pwa/templates";
734
+ async function generateServiceWorker(options) {
735
+ const {
736
+ projectPath,
737
+ outputDir,
738
+ architecture,
739
+ framework,
740
+ templateType,
741
+ globDirectory,
742
+ globPatterns = ["**/*.{js,css,html,png,jpg,jpeg,svg,webp,woff,woff2,ttf,otf}"],
743
+ swDest = "sw.js",
744
+ offlinePage
745
+ } = options;
746
+ mkdirSync2(outputDir, { recursive: true });
747
+ const finalTemplateType = templateType ?? determineTemplateType(architecture, framework ?? null);
748
+ const template = getServiceWorkerTemplate(finalTemplateType);
749
+ const swSrcPath = join6(outputDir, "sw-src.js");
750
+ writeFileSync2(swSrcPath, template.content, "utf-8");
751
+ const swDestPath = join6(outputDir, swDest);
752
+ const workboxConfig = {
753
+ globDirectory: globDirectory ?? projectPath,
754
+ globPatterns,
755
+ swDest: swDestPath,
756
+ swSrc: swSrcPath,
757
+ // Injection du manifest dans le template
758
+ injectionPoint: "self.__WB_MANIFEST"
759
+ };
760
+ if (offlinePage) {
761
+ workboxConfig.globPatterns = [...workboxConfig.globPatterns ?? [], offlinePage];
762
+ }
763
+ try {
764
+ const result = await injectManifest(workboxConfig);
765
+ try {
766
+ if (existsSync5(swSrcPath)) {
767
+ }
768
+ } catch {
769
+ }
770
+ const resultFilePaths = result.filePaths ?? [];
771
+ const normalizedOffline = offlinePage ? offlinePage.replace(/^\.\//, "") : void 0;
772
+ const finalFilePaths = normalizedOffline && !resultFilePaths.some((p) => p.includes(normalizedOffline)) ? [...resultFilePaths, normalizedOffline] : resultFilePaths;
773
+ return {
774
+ swPath: swDestPath,
775
+ count: result.count,
776
+ size: result.size,
777
+ warnings: result.warnings ?? [],
778
+ filePaths: finalFilePaths
779
+ };
780
+ } catch (err) {
781
+ const message = err instanceof Error ? err.message : String(err);
782
+ throw new Error(`Failed to generate service worker: ${message}`);
783
+ }
784
+ }
785
+ async function generateSimpleServiceWorker(options) {
786
+ const {
787
+ projectPath,
788
+ outputDir,
789
+ globDirectory,
790
+ globPatterns = ["**/*.{js,css,html,png,jpg,jpeg,svg,webp,woff,woff2,ttf,otf}"],
791
+ swDest = "sw.js",
792
+ skipWaiting = true,
793
+ clientsClaim = true,
794
+ runtimeCaching
795
+ } = options;
796
+ mkdirSync2(outputDir, { recursive: true });
797
+ const swDestPath = join6(outputDir, swDest);
798
+ const workboxConfig = {
799
+ globDirectory: globDirectory ?? projectPath,
800
+ globPatterns,
801
+ swDest: swDestPath,
802
+ skipWaiting,
803
+ clientsClaim,
804
+ mode: "production",
805
+ sourcemap: false
806
+ };
807
+ if (runtimeCaching && runtimeCaching.length > 0) {
808
+ workboxConfig.runtimeCaching = runtimeCaching.map((cache) => {
809
+ const handlerMap = {
810
+ NetworkFirst: "NetworkFirst",
811
+ CacheFirst: "CacheFirst",
812
+ StaleWhileRevalidate: "StaleWhileRevalidate",
813
+ NetworkOnly: "NetworkOnly",
814
+ CacheOnly: "CacheOnly"
815
+ };
816
+ const handler = handlerMap[cache.handler] ?? cache.handler;
817
+ return {
818
+ urlPattern: typeof cache.urlPattern === "string" ? new RegExp(cache.urlPattern) : cache.urlPattern,
819
+ handler,
820
+ options: cache.options ? {
821
+ cacheName: cache.options.cacheName,
822
+ expiration: cache.options.expiration ? {
823
+ maxEntries: cache.options.expiration.maxEntries,
824
+ maxAgeSeconds: cache.options.expiration.maxAgeSeconds
825
+ } : void 0
826
+ } : void 0
827
+ };
828
+ });
829
+ }
830
+ try {
831
+ const result = await generateSW(workboxConfig);
832
+ return {
833
+ swPath: swDestPath,
834
+ count: result.count,
835
+ size: result.size,
836
+ warnings: result.warnings ?? [],
837
+ filePaths: (result.manifestEntries ?? []).map(
838
+ (entry) => typeof entry === "string" ? entry : entry.url
839
+ )
840
+ };
841
+ } catch (err) {
842
+ const message = err instanceof Error ? err.message : String(err);
843
+ throw new Error(`Failed to generate service worker: ${message}`);
844
+ }
845
+ }
846
+ async function generateAndWriteServiceWorker(options) {
847
+ return generateServiceWorker(options);
848
+ }
849
+
850
+ // src/generator/https-checker.ts
851
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
852
+ import { join as join7 } from "path";
853
+ function checkHttps(url, allowHttpLocalhost = true) {
854
+ let parsedUrl;
855
+ try {
856
+ parsedUrl = new URL(url);
857
+ } catch {
858
+ return {
859
+ isSecure: false,
860
+ isLocalhost: false,
861
+ isProduction: false,
862
+ protocol: "unknown",
863
+ hostname: null,
864
+ warning: "Invalid URL format",
865
+ recommendation: "Provide a valid URL (e.g., https://example.com or http://localhost:3000)"
866
+ };
867
+ }
868
+ const protocol = parsedUrl.protocol.replace(":", "");
869
+ const hostname = parsedUrl.hostname;
870
+ const isHttps = protocol === "https";
871
+ const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.startsWith("172.16.") || hostname.endsWith(".local");
872
+ const isSecure = isHttps || isLocalhost && allowHttpLocalhost;
873
+ const isProduction = isHttps && !isLocalhost;
874
+ let warning;
875
+ let recommendation;
876
+ if (!isSecure) {
877
+ if (isLocalhost && !allowHttpLocalhost) {
878
+ warning = "Service Workers require HTTPS in production. HTTP is only allowed on localhost for development.";
879
+ recommendation = "Use HTTPS in production or enable allowHttpLocalhost option for development.";
880
+ } else if (!isLocalhost) {
881
+ warning = "Service Workers require HTTPS in production. HTTP is not secure and will not work.";
882
+ recommendation = "Deploy your PWA with HTTPS. Consider using services like Vercel, Netlify, or Cloudflare that provide HTTPS by default.";
883
+ }
884
+ }
885
+ return {
886
+ isSecure,
887
+ isLocalhost,
888
+ isProduction,
889
+ protocol,
890
+ hostname,
891
+ warning,
892
+ recommendation
893
+ };
894
+ }
895
+ function detectProjectUrl(projectPath) {
896
+ const configFiles = [
897
+ { file: "package.json", key: "homepage" },
898
+ { file: "package.json", key: "url" },
899
+ { file: ".env", pattern: /^.*URL.*=(.+)$/i },
900
+ { file: ".env.local", pattern: /^.*URL.*=(.+)$/i },
901
+ { file: "vercel.json", key: "url" },
902
+ { file: "netlify.toml", pattern: /^.*url.*=.*["'](.+)["']/i },
903
+ { file: "next.config.js", pattern: /baseUrl.*["'](.+)["']/ },
904
+ { file: "next.config.ts", pattern: /baseUrl.*["'](.+)["']/ }
905
+ ];
906
+ for (const config of configFiles) {
907
+ const filePath = join7(projectPath, config.file);
908
+ if (existsSync6(filePath)) {
909
+ try {
910
+ if (config.file.endsWith(".json") && config.key) {
911
+ const parsed = JSON.parse(readFileSync3(filePath, "utf-8"));
912
+ const content = parsed;
913
+ const value = content[config.key];
914
+ if (value && typeof value === "string") {
915
+ return value;
916
+ }
917
+ } else if (config.file.endsWith(".toml") && config.pattern) {
918
+ const content = readFileSync3(filePath, "utf-8");
919
+ const match = config.pattern ? content.match(config.pattern) : null;
920
+ if (match && match[1]) {
921
+ return match[1];
922
+ }
923
+ } else if ((config.file.endsWith(".js") || config.file.endsWith(".ts")) && config.pattern) {
924
+ const content = readFileSync3(filePath, "utf-8");
925
+ const match = config.pattern ? content.match(config.pattern) : null;
926
+ if (match && match[1]) {
927
+ return match[1];
928
+ }
929
+ } else if (config.file.startsWith(".env") && config.pattern) {
930
+ const content = readFileSync3(filePath, "utf-8");
931
+ const lines = content.split("\n");
932
+ for (const line of lines) {
933
+ const match = config.pattern ? line.match(config.pattern) : null;
934
+ if (match && match[1]) {
935
+ return match[1].trim();
936
+ }
937
+ }
938
+ }
939
+ } catch {
940
+ }
941
+ }
942
+ }
943
+ return null;
944
+ }
945
+ function checkProjectHttps(options = {}) {
946
+ const { url, projectPath, allowHttpLocalhost = true } = options;
947
+ if (url) {
948
+ return checkHttps(url, allowHttpLocalhost);
949
+ }
950
+ if (projectPath) {
951
+ const detectedUrl = detectProjectUrl(projectPath);
952
+ if (detectedUrl) {
953
+ return checkHttps(detectedUrl, allowHttpLocalhost);
954
+ }
955
+ }
956
+ return {
957
+ isSecure: false,
958
+ isLocalhost: false,
959
+ isProduction: false,
960
+ protocol: "unknown",
961
+ hostname: null,
962
+ warning: "Unable to determine project URL. HTTPS check cannot be performed.",
963
+ recommendation: "Provide a URL explicitly or ensure your project has a valid configuration file (package.json, .env, etc.)."
964
+ };
965
+ }
966
+
967
+ // src/injector/html-parser.ts
968
+ import { parseDocument } from "htmlparser2";
969
+ import { readFileSync as readFileSync4 } from "fs";
970
+ function parseHTMLFile(filePath, options = {}) {
971
+ const content = readFileSync4(filePath, "utf-8");
972
+ return parseHTML(content, options);
973
+ }
974
+ function parseHTML(htmlContent, options = {}) {
975
+ const {
976
+ decodeEntities = true,
977
+ lowerCaseAttributeNames = true
978
+ } = options;
979
+ const document = parseDocument(htmlContent, {
980
+ decodeEntities,
981
+ lowerCaseAttributeNames,
982
+ lowerCaseTags: true
983
+ });
984
+ let head = null;
985
+ let body = null;
986
+ let html = null;
987
+ const findElements = (node) => {
988
+ if (node.type === "tag") {
989
+ const element = node;
990
+ const tagName = element.tagName.toLowerCase();
991
+ if (tagName === "head" && !head) {
992
+ head = element;
993
+ } else if (tagName === "body" && !body) {
994
+ body = element;
995
+ } else if (tagName === "html" && !html) {
996
+ html = element;
997
+ }
998
+ if (element.children) {
999
+ for (const child of element.children) {
1000
+ findElements(child);
1001
+ }
1002
+ }
1003
+ }
1004
+ };
1005
+ if (document.children) {
1006
+ for (const child of document.children) {
1007
+ findElements(child);
1008
+ }
1009
+ }
1010
+ return {
1011
+ document,
1012
+ head,
1013
+ body,
1014
+ html,
1015
+ originalContent: htmlContent
1016
+ };
1017
+ }
1018
+ function findElement(parsed, tagName, attribute) {
1019
+ const searchIn = parsed.head || parsed.document;
1020
+ const search = (node) => {
1021
+ if (node.type === "tag") {
1022
+ const element = node;
1023
+ if (element.tagName.toLowerCase() === tagName.toLowerCase()) {
1024
+ if (!attribute) {
1025
+ return element;
1026
+ }
1027
+ const attrValue = element.attributes?.find((attr) => attr.name.toLowerCase() === attribute.name.toLowerCase())?.value;
1028
+ if (attrValue === attribute.value) {
1029
+ return element;
1030
+ }
1031
+ }
1032
+ if (element.children) {
1033
+ for (const child of element.children) {
1034
+ const found = search(child);
1035
+ if (found) {
1036
+ return found;
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+ return null;
1042
+ };
1043
+ if (searchIn) {
1044
+ if (searchIn.type === "tag") {
1045
+ return search(searchIn);
1046
+ }
1047
+ if ("children" in searchIn && searchIn.children) {
1048
+ for (const child of searchIn.children) {
1049
+ const found = search(child);
1050
+ if (found) {
1051
+ return found;
1052
+ }
1053
+ }
1054
+ }
1055
+ }
1056
+ return null;
1057
+ }
1058
+ function findAllElements(parsed, tagName, attribute) {
1059
+ const results = [];
1060
+ const targetTagName = tagName.toLowerCase();
1061
+ const search = (node, isRoot = false) => {
1062
+ if (node.type === "tag") {
1063
+ const element = node;
1064
+ const nodeTagName = element.tagName.toLowerCase();
1065
+ if (nodeTagName === targetTagName && (!isRoot || targetTagName === "html" || targetTagName === "head" || targetTagName === "body")) {
1066
+ if (!attribute) {
1067
+ results.push(element);
1068
+ } else {
1069
+ const attrValue = element.attributes?.find((attr) => attr.name.toLowerCase() === attribute.name.toLowerCase())?.value;
1070
+ if (attribute.value === void 0 || attrValue === attribute.value) {
1071
+ results.push(element);
1072
+ }
1073
+ }
1074
+ }
1075
+ if (element.children) {
1076
+ for (const child of element.children) {
1077
+ const childElement = child;
1078
+ const childIsRoot = isRoot && childElement.type === "tag" && (childElement.tagName.toLowerCase() === "html" || childElement.tagName.toLowerCase() === "head" || childElement.tagName.toLowerCase() === "body");
1079
+ search(child, childIsRoot);
1080
+ }
1081
+ }
1082
+ }
1083
+ };
1084
+ if (parsed.head) {
1085
+ search(parsed.head, true);
1086
+ } else {
1087
+ if (parsed.document.children) {
1088
+ for (const child of parsed.document.children) {
1089
+ const childElement = child;
1090
+ const childIsRoot = childElement.type === "tag" && (childElement.tagName.toLowerCase() === "html" || childElement.tagName.toLowerCase() === "head" || childElement.tagName.toLowerCase() === "body");
1091
+ search(child, childIsRoot);
1092
+ }
1093
+ }
1094
+ }
1095
+ return results;
1096
+ }
1097
+ function elementExists(parsed, tagName, attribute) {
1098
+ return findElement(parsed, tagName, attribute) !== null;
1099
+ }
1100
+ function serializeHTML(parsed) {
1101
+ return parsed.originalContent;
1102
+ }
1103
+
1104
+ // src/injector/meta-injector.ts
1105
+ import { writeFileSync as writeFileSync3 } from "fs";
1106
+ import { render } from "dom-serializer";
1107
+ function injectMetaTags(htmlContent, options = {}) {
1108
+ const parsed = parseHTML(htmlContent);
1109
+ const result = {
1110
+ injected: [],
1111
+ skipped: [],
1112
+ warnings: []
1113
+ };
1114
+ let head = parsed.head;
1115
+ if (!head) {
1116
+ if (parsed.html) {
1117
+ const headElement = {
1118
+ type: "tag",
1119
+ name: "head",
1120
+ tagName: "head",
1121
+ attribs: {},
1122
+ children: [],
1123
+ parent: parsed.html,
1124
+ next: null,
1125
+ prev: null
1126
+ };
1127
+ if (parsed.html.children) {
1128
+ parsed.html.children.unshift(headElement);
1129
+ } else {
1130
+ parsed.html.children = [headElement];
1131
+ }
1132
+ head = headElement;
1133
+ result.warnings.push("Created <head> tag (was missing)");
1134
+ } else {
1135
+ result.warnings.push("No <html> or <head> tag found, meta tags may not be injected correctly");
1136
+ return { html: htmlContent, result };
1137
+ }
1138
+ }
1139
+ if (options.manifestPath) {
1140
+ const manifestHref = options.manifestPath.startsWith("/") ? options.manifestPath : `/${options.manifestPath}`;
1141
+ if (!elementExists(parsed, "link", { name: "rel", value: "manifest" })) {
1142
+ injectLinkTag(head, "manifest", manifestHref);
1143
+ result.injected.push(`<link rel="manifest" href="${manifestHref}">`);
1144
+ } else {
1145
+ result.skipped.push("manifest link (already exists)");
1146
+ }
1147
+ }
1148
+ if (options.themeColor) {
1149
+ const existingThemeColor = findElement(parsed, "meta", { name: "name", value: "theme-color" });
1150
+ if (existingThemeColor) {
1151
+ updateMetaContent(existingThemeColor, options.themeColor);
1152
+ result.injected.push(`<meta name="theme-color" content="${options.themeColor}"> (updated)`);
1153
+ } else if (!elementExists(parsed, "meta", { name: "theme-color", value: options.themeColor })) {
1154
+ injectMetaTag(head, "theme-color", options.themeColor);
1155
+ result.injected.push(`<meta name="theme-color" content="${options.themeColor}">`);
1156
+ } else {
1157
+ result.skipped.push("theme-color (already exists)");
1158
+ }
1159
+ }
1160
+ if (options.appleTouchIcon) {
1161
+ const iconHref = options.appleTouchIcon.startsWith("/") ? options.appleTouchIcon : `/${options.appleTouchIcon}`;
1162
+ if (!elementExists(parsed, "link", { name: "rel", value: "apple-touch-icon" })) {
1163
+ injectLinkTag(head, "apple-touch-icon", iconHref);
1164
+ result.injected.push(`<link rel="apple-touch-icon" href="${iconHref}">`);
1165
+ } else {
1166
+ result.skipped.push("apple-touch-icon (already exists)");
1167
+ }
1168
+ }
1169
+ if (options.appleMobileWebAppCapable !== void 0) {
1170
+ const content = options.appleMobileWebAppCapable ? "yes" : "no";
1171
+ if (!elementExists(parsed, "meta", { name: "apple-mobile-web-app-capable", value: content })) {
1172
+ injectMetaTag(head, "apple-mobile-web-app-capable", content);
1173
+ result.injected.push(`<meta name="apple-mobile-web-app-capable" content="${content}">`);
1174
+ } else {
1175
+ result.skipped.push("apple-mobile-web-app-capable (already exists)");
1176
+ }
1177
+ }
1178
+ if (options.appleMobileWebAppStatusBarStyle) {
1179
+ if (!elementExists(parsed, "meta", { name: "apple-mobile-web-app-status-bar-style", value: options.appleMobileWebAppStatusBarStyle })) {
1180
+ injectMetaTag(head, "apple-mobile-web-app-status-bar-style", options.appleMobileWebAppStatusBarStyle);
1181
+ result.injected.push(`<meta name="apple-mobile-web-app-status-bar-style" content="${options.appleMobileWebAppStatusBarStyle}">`);
1182
+ } else {
1183
+ result.skipped.push("apple-mobile-web-app-status-bar-style (already exists)");
1184
+ }
1185
+ }
1186
+ if (options.appleMobileWebAppTitle) {
1187
+ if (!elementExists(parsed, "meta", { name: "apple-mobile-web-app-title", value: options.appleMobileWebAppTitle })) {
1188
+ injectMetaTag(head, "apple-mobile-web-app-title", options.appleMobileWebAppTitle);
1189
+ result.injected.push(`<meta name="apple-mobile-web-app-title" content="${options.appleMobileWebAppTitle}">`);
1190
+ } else {
1191
+ result.skipped.push("apple-mobile-web-app-title (already exists)");
1192
+ }
1193
+ }
1194
+ const modifiedHtml = render(parsed.document, { decodeEntities: false });
1195
+ if (options.serviceWorkerPath) {
1196
+ const swPath = options.serviceWorkerPath.startsWith("/") ? options.serviceWorkerPath : `/${options.serviceWorkerPath}`;
1197
+ if (!htmlContent.includes("navigator.serviceWorker")) {
1198
+ const swScript = `
1199
+ <script>
1200
+ if ('serviceWorker' in navigator) {
1201
+ window.addEventListener('load', () => {
1202
+ navigator.serviceWorker.register('${swPath}')
1203
+ .then((registration) => console.log('SW registered:', registration))
1204
+ .catch((error) => console.error('SW registration failed:', error));
1205
+ });
1206
+ }
1207
+ </script>`;
1208
+ const finalHtml = modifiedHtml.replace("</body>", `${swScript}
1209
+ </body>`);
1210
+ result.injected.push("Service Worker registration script");
1211
+ return { html: finalHtml, result };
1212
+ } else {
1213
+ result.skipped.push("Service Worker registration (already exists)");
1214
+ }
1215
+ }
1216
+ return { html: modifiedHtml, result };
1217
+ }
1218
+ function injectLinkTag(head, rel, href) {
1219
+ const linkElement = {
1220
+ type: "tag",
1221
+ name: "link",
1222
+ tagName: "link",
1223
+ attribs: {
1224
+ rel,
1225
+ href
1226
+ },
1227
+ children: [],
1228
+ parent: head,
1229
+ next: null,
1230
+ prev: null
1231
+ };
1232
+ if (!head.children) {
1233
+ head.children = [];
1234
+ }
1235
+ head.children.push(linkElement);
1236
+ }
1237
+ function injectMetaTag(head, name, content) {
1238
+ const metaElement = {
1239
+ type: "tag",
1240
+ name: "meta",
1241
+ tagName: "meta",
1242
+ attribs: {
1243
+ name,
1244
+ content
1245
+ },
1246
+ children: [],
1247
+ parent: head,
1248
+ next: null,
1249
+ prev: null
1250
+ };
1251
+ if (!head.children) {
1252
+ head.children = [];
1253
+ }
1254
+ head.children.push(metaElement);
1255
+ }
1256
+ function updateMetaContent(metaElement, newContent) {
1257
+ if (metaElement.attribs) {
1258
+ metaElement.attribs.content = newContent;
1259
+ } else {
1260
+ metaElement.attribs = { content: newContent };
1261
+ }
1262
+ }
1263
+ function injectMetaTagsInFile(filePath, options = {}) {
1264
+ const parsed = parseHTMLFile(filePath);
1265
+ const { html, result } = injectMetaTags(parsed.originalContent, options);
1266
+ writeFileSync3(filePath, html, "utf-8");
1267
+ return result;
1268
+ }
1269
+ export {
1270
+ ManifestSchema,
1271
+ STANDARD_ICON_SIZES,
1272
+ STANDARD_SPLASH_SIZES,
1273
+ checkHttps,
1274
+ checkProjectHttps,
1275
+ detectArchitecture,
1276
+ detectAssets,
1277
+ detectFramework,
1278
+ detectProjectUrl,
1279
+ elementExists,
1280
+ findAllElements,
1281
+ findElement,
1282
+ generateAndWriteManifest,
1283
+ generateAndWriteServiceWorker,
1284
+ generateAppleTouchIcon,
1285
+ generateFavicon,
1286
+ generateIcons,
1287
+ generateIconsOnly,
1288
+ generateManifest,
1289
+ generateReport,
1290
+ generateServiceWorker,
1291
+ generateSimpleServiceWorker,
1292
+ generateSplashScreensOnly,
1293
+ injectMetaTags,
1294
+ injectMetaTagsInFile,
1295
+ parseHTML,
1296
+ parseHTMLFile,
1297
+ scanProject,
1298
+ serializeHTML,
1299
+ validateProjectPath,
1300
+ writeManifest
1301
+ };