@julien-lin/universal-pwa-core 1.3.4 → 1.3.6

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
@@ -60,6 +60,7 @@ __export(index_exports, {
60
60
  generateSplashScreensOnly: () => generateSplashScreensOnly,
61
61
  injectMetaTags: () => injectMetaTags,
62
62
  injectMetaTagsInFile: () => injectMetaTagsInFile,
63
+ injectMetaTagsInFilesBatch: () => injectMetaTagsInFilesBatch,
63
64
  optimizeImage: () => optimizeImage,
64
65
  optimizeProject: () => optimizeProject,
65
66
  optimizeProjectImages: () => optimizeProjectImages,
@@ -1646,9 +1647,11 @@ async function scanProject(options) {
1646
1647
  buildTool: null
1647
1648
  }
1648
1649
  };
1649
- const assetsCandidate = includeAssets ? await detectAssets(projectPath) : getEmptyAssets();
1650
+ const [assetsCandidate, architectureCandidate] = await Promise.all([
1651
+ includeAssets ? detectAssets(projectPath) : Promise.resolve(getEmptyAssets()),
1652
+ includeArchitecture ? detectArchitecture(projectPath) : Promise.resolve(getEmptyArchitecture())
1653
+ ]);
1650
1654
  const assets = isAssetDetectionResult(assetsCandidate) ? assetsCandidate : getEmptyAssets();
1651
- const architectureCandidate = includeArchitecture ? await detectArchitecture(projectPath) : getEmptyArchitecture();
1652
1655
  const architecture = isArchitectureDetectionResult(architectureCandidate) ? architectureCandidate : getEmptyArchitecture();
1653
1656
  const result = {
1654
1657
  framework,
@@ -1814,9 +1817,117 @@ function generateAndWriteManifest(options, outputDir) {
1814
1817
  }
1815
1818
 
1816
1819
  // src/generator/icon-generator.ts
1820
+ var import_sharp3 = __toESM(require("sharp"), 1);
1821
+ var import_fs9 = require("fs");
1822
+ var import_path8 = require("path");
1823
+
1824
+ // src/validator/icon-validator.ts
1817
1825
  var import_sharp2 = __toESM(require("sharp"), 1);
1818
1826
  var import_fs8 = require("fs");
1819
- var import_path8 = require("path");
1827
+ async function validateIconSource(options) {
1828
+ const {
1829
+ sourceImage,
1830
+ strict = false,
1831
+ minRecommendedSize = 192,
1832
+ optimalSize = 512
1833
+ } = options;
1834
+ const result = {
1835
+ valid: true,
1836
+ errors: [],
1837
+ warnings: [],
1838
+ suggestions: []
1839
+ };
1840
+ if (!(0, import_fs8.existsSync)(sourceImage)) {
1841
+ result.valid = false;
1842
+ result.errors.push(`Source image not found: ${sourceImage}`);
1843
+ return result;
1844
+ }
1845
+ try {
1846
+ const image = (0, import_sharp2.default)(sourceImage);
1847
+ const metadata = await image.metadata();
1848
+ if (!metadata.width || !metadata.height) {
1849
+ result.valid = false;
1850
+ result.errors.push("Unable to read image dimensions");
1851
+ return result;
1852
+ }
1853
+ const { width, height, format, size } = metadata;
1854
+ const minDimension = Math.min(width, height);
1855
+ const maxDimension = Math.max(width, height);
1856
+ const isSquare = width === height;
1857
+ result.metadata = {
1858
+ width,
1859
+ height,
1860
+ format: format || "unknown",
1861
+ size: size || 0
1862
+ };
1863
+ const supportedFormats = ["png", "jpeg", "jpg", "webp", "svg"];
1864
+ if (!format || !supportedFormats.includes(format.toLowerCase())) {
1865
+ result.valid = false;
1866
+ result.errors.push(
1867
+ `Unsupported image format: ${format || "unknown"}. Supported formats: ${supportedFormats.join(", ")}`
1868
+ );
1869
+ }
1870
+ if (minDimension < minRecommendedSize) {
1871
+ const message = `Icon dimensions too small: ${width}x${height}. Minimum recommended: ${minRecommendedSize}x${minRecommendedSize}`;
1872
+ if (strict) {
1873
+ result.valid = false;
1874
+ result.errors.push(message);
1875
+ } else {
1876
+ result.warnings.push(message);
1877
+ result.suggestions.push(
1878
+ `Use an image at least ${minRecommendedSize}x${minRecommendedSize} pixels for best PWA compatibility`
1879
+ );
1880
+ }
1881
+ }
1882
+ if (minDimension < optimalSize) {
1883
+ result.warnings.push(
1884
+ `Icon dimensions below optimal size: ${width}x${height}. Optimal size: ${optimalSize}x${optimalSize} for best quality on all devices`
1885
+ );
1886
+ result.suggestions.push(
1887
+ `Consider using a ${optimalSize}x${optimalSize} source image for optimal icon quality`
1888
+ );
1889
+ }
1890
+ if (!isSquare) {
1891
+ result.warnings.push(
1892
+ `Icon is not square (${width}x${height}). Square images work best for PWA icons`
1893
+ );
1894
+ result.suggestions.push(
1895
+ "Consider using a square source image. The icon will be cropped to fit during generation"
1896
+ );
1897
+ }
1898
+ if (format?.toLowerCase() === "jpg" || format?.toLowerCase() === "jpeg") {
1899
+ result.suggestions.push(
1900
+ "PNG format is recommended for icons with transparency. Consider converting JPG to PNG"
1901
+ );
1902
+ }
1903
+ if (size && size > 1024 * 1024) {
1904
+ const sizeMB = (size / (1024 * 1024)).toFixed(2);
1905
+ result.warnings.push(`Icon file size is large: ${sizeMB}MB. This may slow down generation`);
1906
+ result.suggestions.push(
1907
+ "Consider optimizing the source image before generation to reduce file size"
1908
+ );
1909
+ } else if (size && size > 500 * 1024) {
1910
+ const sizeKB = (size / 1024).toFixed(2);
1911
+ result.suggestions.push(
1912
+ `Icon file size is ${sizeKB}KB. Consider optimizing to reduce generation time`
1913
+ );
1914
+ }
1915
+ const aspectRatio = maxDimension / minDimension;
1916
+ if (aspectRatio > 2) {
1917
+ result.warnings.push(
1918
+ `Icon has extreme aspect ratio (${aspectRatio.toFixed(2)}:1). Square images are recommended`
1919
+ );
1920
+ }
1921
+ return result;
1922
+ } catch (error) {
1923
+ const message = error instanceof Error ? error.message : String(error);
1924
+ result.valid = false;
1925
+ result.errors.push(`Failed to validate icon: ${message}`);
1926
+ return result;
1927
+ }
1928
+ }
1929
+
1930
+ // src/generator/icon-generator.ts
1820
1931
  var STANDARD_ICON_SIZES = [
1821
1932
  { width: 72, height: 72, name: "icon-72x72.png" },
1822
1933
  { width: 96, height: 96, name: "icon-96x96.png" },
@@ -1850,67 +1961,114 @@ async function generateIcons(options) {
1850
1961
  iconSizes = STANDARD_ICON_SIZES,
1851
1962
  splashSizes = STANDARD_SPLASH_SIZES,
1852
1963
  format = "png",
1853
- quality = 90
1964
+ quality = 90,
1965
+ validate = false,
1966
+ strictValidation = false
1854
1967
  } = options;
1855
- if (!(0, import_fs8.existsSync)(sourceImage)) {
1968
+ if (!(0, import_fs9.existsSync)(sourceImage)) {
1856
1969
  throw new Error(`Source image not found: ${sourceImage}`);
1857
1970
  }
1858
- (0, import_fs8.mkdirSync)(outputDir, { recursive: true });
1971
+ let validation;
1972
+ if (validate) {
1973
+ validation = await validateIconSource({
1974
+ sourceImage,
1975
+ strict: strictValidation
1976
+ });
1977
+ if (strictValidation && !validation.valid) {
1978
+ const errorMessages = validation.errors.join("; ");
1979
+ throw new Error(`Icon validation failed: ${errorMessages}`);
1980
+ }
1981
+ }
1982
+ (0, import_fs9.mkdirSync)(outputDir, { recursive: true });
1859
1983
  const generatedFiles = [];
1860
1984
  const icons = [];
1861
1985
  const splashScreens = [];
1862
- const image = (0, import_sharp2.default)(sourceImage);
1986
+ const image = (0, import_sharp3.default)(sourceImage);
1863
1987
  const metadata = await image.metadata();
1864
1988
  if (!metadata.width || !metadata.height) {
1865
1989
  throw new Error("Unable to read image dimensions");
1866
1990
  }
1867
- for (const size of iconSizes) {
1868
- const outputPath = (0, import_path8.join)(outputDir, size.name);
1869
- try {
1870
- let pipeline = image.clone().resize(size.width, size.height, {
1871
- fit: "cover",
1872
- position: "center"
1873
- });
1874
- if (format === "png") {
1875
- pipeline = pipeline.png({ quality, compressionLevel: 9 });
1876
- } else {
1877
- pipeline = pipeline.webp({ quality });
1991
+ const iconResults = await Promise.all(
1992
+ iconSizes.map(async (size) => {
1993
+ const outputPath = (0, import_path8.join)(outputDir, size.name);
1994
+ try {
1995
+ let pipeline = image.clone().resize(size.width, size.height, {
1996
+ fit: "cover",
1997
+ position: "center"
1998
+ });
1999
+ if (format === "png") {
2000
+ pipeline = pipeline.png({ quality, compressionLevel: 9 });
2001
+ } else {
2002
+ pipeline = pipeline.webp({ quality });
2003
+ }
2004
+ await pipeline.toFile(outputPath);
2005
+ return {
2006
+ success: true,
2007
+ file: outputPath,
2008
+ icon: {
2009
+ src: `/${size.name}`,
2010
+ sizes: `${size.width}x${size.height}`,
2011
+ type: format === "png" ? "image/png" : "image/webp",
2012
+ purpose: size.width >= 192 && size.width <= 512 ? "any" : void 0
2013
+ }
2014
+ };
2015
+ } catch (err) {
2016
+ const message = err instanceof Error ? err.message : String(err);
2017
+ return {
2018
+ success: false,
2019
+ error: `Failed to generate icon ${size.name}: ${message}`,
2020
+ size: size.name
2021
+ };
1878
2022
  }
1879
- await pipeline.toFile(outputPath);
1880
- generatedFiles.push(outputPath);
1881
- icons.push({
1882
- src: `/${size.name}`,
1883
- sizes: `${size.width}x${size.height}`,
1884
- type: format === "png" ? "image/png" : "image/webp",
1885
- purpose: size.width >= 192 && size.width <= 512 ? "any" : void 0
1886
- });
1887
- } catch (err) {
1888
- const message = err instanceof Error ? err.message : String(err);
1889
- throw new Error(`Failed to generate icon ${size.name}: ${message}`);
2023
+ })
2024
+ );
2025
+ for (const result of iconResults) {
2026
+ if (result.success) {
2027
+ generatedFiles.push(result.file);
2028
+ icons.push(result.icon);
2029
+ } else {
2030
+ throw new Error(result.error);
1890
2031
  }
1891
2032
  }
1892
- for (const size of splashSizes) {
1893
- const outputPath = (0, import_path8.join)(outputDir, size.name);
1894
- try {
1895
- let pipeline = image.clone().resize(size.width, size.height, {
1896
- fit: "cover",
1897
- position: "center"
1898
- });
1899
- if (format === "png") {
1900
- pipeline = pipeline.png({ quality, compressionLevel: 9 });
1901
- } else {
1902
- pipeline = pipeline.webp({ quality });
2033
+ const splashResults = await Promise.all(
2034
+ splashSizes.map(async (size) => {
2035
+ const outputPath = (0, import_path8.join)(outputDir, size.name);
2036
+ try {
2037
+ let pipeline = image.clone().resize(size.width, size.height, {
2038
+ fit: "cover",
2039
+ position: "center"
2040
+ });
2041
+ if (format === "png") {
2042
+ pipeline = pipeline.png({ quality, compressionLevel: 9 });
2043
+ } else {
2044
+ pipeline = pipeline.webp({ quality });
2045
+ }
2046
+ await pipeline.toFile(outputPath);
2047
+ return {
2048
+ success: true,
2049
+ file: outputPath,
2050
+ splash: {
2051
+ src: `/${size.name}`,
2052
+ sizes: `${size.width}x${size.height}`,
2053
+ type: format === "png" ? "image/png" : "image/webp"
2054
+ }
2055
+ };
2056
+ } catch (err) {
2057
+ const message = err instanceof Error ? err.message : String(err);
2058
+ return {
2059
+ success: false,
2060
+ error: `Failed to generate splash screen ${size.name}: ${message}`,
2061
+ size: size.name
2062
+ };
1903
2063
  }
1904
- await pipeline.toFile(outputPath);
1905
- generatedFiles.push(outputPath);
1906
- splashScreens.push({
1907
- src: `/${size.name}`,
1908
- sizes: `${size.width}x${size.height}`,
1909
- type: format === "png" ? "image/png" : "image/webp"
1910
- });
1911
- } catch (err) {
1912
- const message = err instanceof Error ? err.message : String(err);
1913
- throw new Error(`Failed to generate splash screen ${size.name}: ${message}`);
2064
+ })
2065
+ );
2066
+ for (const result of splashResults) {
2067
+ if (result.success) {
2068
+ generatedFiles.push(result.file);
2069
+ splashScreens.push(result.splash);
2070
+ } else {
2071
+ throw new Error(result.error);
1914
2072
  }
1915
2073
  }
1916
2074
  if (format === "png") {
@@ -1929,7 +2087,8 @@ async function generateIcons(options) {
1929
2087
  return {
1930
2088
  icons,
1931
2089
  splashScreens,
1932
- generatedFiles
2090
+ generatedFiles,
2091
+ validation
1933
2092
  };
1934
2093
  }
1935
2094
  async function generateIconsOnly(options) {
@@ -1953,13 +2112,13 @@ async function generateSplashScreensOnly(options) {
1953
2112
  };
1954
2113
  }
1955
2114
  async function generateFavicon(sourceImage, outputDir) {
1956
- if (!(0, import_fs8.existsSync)(sourceImage)) {
2115
+ if (!(0, import_fs9.existsSync)(sourceImage)) {
1957
2116
  throw new Error(`Source image not found: ${sourceImage}`);
1958
2117
  }
1959
- (0, import_fs8.mkdirSync)(outputDir, { recursive: true });
2118
+ (0, import_fs9.mkdirSync)(outputDir, { recursive: true });
1960
2119
  const faviconPath = (0, import_path8.join)(outputDir, "favicon.ico");
1961
2120
  try {
1962
- await (0, import_sharp2.default)(sourceImage).resize(32, 32, {
2121
+ await (0, import_sharp3.default)(sourceImage).resize(32, 32, {
1963
2122
  fit: "cover",
1964
2123
  position: "center"
1965
2124
  }).png().toFile(faviconPath);
@@ -1970,13 +2129,13 @@ async function generateFavicon(sourceImage, outputDir) {
1970
2129
  }
1971
2130
  }
1972
2131
  async function generateAppleTouchIcon(sourceImage, outputDir) {
1973
- if (!(0, import_fs8.existsSync)(sourceImage)) {
2132
+ if (!(0, import_fs9.existsSync)(sourceImage)) {
1974
2133
  throw new Error(`Source image not found: ${sourceImage}`);
1975
2134
  }
1976
- (0, import_fs8.mkdirSync)(outputDir, { recursive: true });
2135
+ (0, import_fs9.mkdirSync)(outputDir, { recursive: true });
1977
2136
  const appleIconPath = (0, import_path8.join)(outputDir, "apple-touch-icon.png");
1978
2137
  try {
1979
- await (0, import_sharp2.default)(sourceImage).resize(180, 180, {
2138
+ await (0, import_sharp3.default)(sourceImage).resize(180, 180, {
1980
2139
  fit: "cover",
1981
2140
  position: "center"
1982
2141
  }).png({ quality: 90, compressionLevel: 9 }).toFile(appleIconPath);
@@ -1989,7 +2148,7 @@ async function generateAppleTouchIcon(sourceImage, outputDir) {
1989
2148
 
1990
2149
  // src/generator/service-worker-generator.ts
1991
2150
  var import_workbox_build = require("workbox-build");
1992
- var import_fs9 = require("fs");
2151
+ var import_fs10 = require("fs");
1993
2152
  var import_path9 = require("path");
1994
2153
  var import_universal_pwa_templates = require("@julien-lin/universal-pwa-templates");
1995
2154
  async function generateServiceWorker(options) {
@@ -2004,11 +2163,11 @@ async function generateServiceWorker(options) {
2004
2163
  swDest = "sw.js",
2005
2164
  offlinePage
2006
2165
  } = options;
2007
- (0, import_fs9.mkdirSync)(outputDir, { recursive: true });
2166
+ (0, import_fs10.mkdirSync)(outputDir, { recursive: true });
2008
2167
  const finalTemplateType = templateType ?? (0, import_universal_pwa_templates.determineTemplateType)(architecture, framework ?? null);
2009
2168
  const template = (0, import_universal_pwa_templates.getServiceWorkerTemplate)(finalTemplateType);
2010
2169
  const swSrcPath = (0, import_path9.join)(outputDir, "sw-src.js");
2011
- (0, import_fs9.writeFileSync)(swSrcPath, template.content, "utf-8");
2170
+ (0, import_fs10.writeFileSync)(swSrcPath, template.content, "utf-8");
2012
2171
  const swDestPath = (0, import_path9.join)(outputDir, swDest);
2013
2172
  const workboxConfig = {
2014
2173
  globDirectory: globDirectory ?? projectPath,
@@ -2024,7 +2183,7 @@ async function generateServiceWorker(options) {
2024
2183
  try {
2025
2184
  const result = await (0, import_workbox_build.injectManifest)(workboxConfig);
2026
2185
  try {
2027
- if ((0, import_fs9.existsSync)(swSrcPath)) {
2186
+ if ((0, import_fs10.existsSync)(swSrcPath)) {
2028
2187
  }
2029
2188
  } catch {
2030
2189
  }
@@ -2054,7 +2213,7 @@ async function generateSimpleServiceWorker(options) {
2054
2213
  clientsClaim = true,
2055
2214
  runtimeCaching
2056
2215
  } = options;
2057
- (0, import_fs9.mkdirSync)(outputDir, { recursive: true });
2216
+ (0, import_fs10.mkdirSync)(outputDir, { recursive: true });
2058
2217
  const swDestPath = (0, import_path9.join)(outputDir, swDest);
2059
2218
  const workboxConfig = {
2060
2219
  globDirectory: globDirectory ?? projectPath,
@@ -2109,7 +2268,7 @@ async function generateAndWriteServiceWorker(options) {
2109
2268
  }
2110
2269
 
2111
2270
  // src/generator/https-checker.ts
2112
- var import_fs10 = require("fs");
2271
+ var import_fs11 = require("fs");
2113
2272
  var import_path10 = require("path");
2114
2273
  function checkHttps(url, allowHttpLocalhost = true) {
2115
2274
  let parsedUrl;
@@ -2166,29 +2325,29 @@ function detectProjectUrl(projectPath) {
2166
2325
  ];
2167
2326
  for (const config of configFiles) {
2168
2327
  const filePath = (0, import_path10.join)(projectPath, config.file);
2169
- if ((0, import_fs10.existsSync)(filePath)) {
2328
+ if ((0, import_fs11.existsSync)(filePath)) {
2170
2329
  try {
2171
2330
  if (config.file.endsWith(".json") && config.key) {
2172
- const parsed = JSON.parse((0, import_fs10.readFileSync)(filePath, "utf-8"));
2331
+ const parsed = JSON.parse((0, import_fs11.readFileSync)(filePath, "utf-8"));
2173
2332
  const content = parsed;
2174
2333
  const value = content[config.key];
2175
2334
  if (value && typeof value === "string") {
2176
2335
  return value;
2177
2336
  }
2178
2337
  } else if (config.file.endsWith(".toml") && config.pattern) {
2179
- const content = (0, import_fs10.readFileSync)(filePath, "utf-8");
2338
+ const content = (0, import_fs11.readFileSync)(filePath, "utf-8");
2180
2339
  const match = config.pattern ? content.match(config.pattern) : null;
2181
2340
  if (match && match[1]) {
2182
2341
  return match[1];
2183
2342
  }
2184
2343
  } else if ((config.file.endsWith(".js") || config.file.endsWith(".ts")) && config.pattern) {
2185
- const content = (0, import_fs10.readFileSync)(filePath, "utf-8");
2344
+ const content = (0, import_fs11.readFileSync)(filePath, "utf-8");
2186
2345
  const match = config.pattern ? content.match(config.pattern) : null;
2187
2346
  if (match && match[1]) {
2188
2347
  return match[1];
2189
2348
  }
2190
2349
  } else if (config.file.startsWith(".env") && config.pattern) {
2191
- const content = (0, import_fs10.readFileSync)(filePath, "utf-8");
2350
+ const content = (0, import_fs11.readFileSync)(filePath, "utf-8");
2192
2351
  const lines = content.split("\n");
2193
2352
  for (const line of lines) {
2194
2353
  const match = config.pattern ? line.match(config.pattern) : null;
@@ -2227,9 +2386,9 @@ function checkProjectHttps(options = {}) {
2227
2386
 
2228
2387
  // src/injector/html-parser.ts
2229
2388
  var import_htmlparser2 = require("htmlparser2");
2230
- var import_fs11 = require("fs");
2389
+ var import_fs12 = require("fs");
2231
2390
  function parseHTMLFile(filePath, options = {}) {
2232
- const content = (0, import_fs11.readFileSync)(filePath, "utf-8");
2391
+ const content = (0, import_fs12.readFileSync)(filePath, "utf-8");
2233
2392
  return parseHTML(content, options);
2234
2393
  }
2235
2394
  function parseHTML(htmlContent, options = {}) {
@@ -2363,7 +2522,7 @@ function serializeHTML(parsed) {
2363
2522
  }
2364
2523
 
2365
2524
  // src/injector/meta-injector.ts
2366
- var import_fs12 = require("fs");
2525
+ var import_fs13 = require("fs");
2367
2526
  var import_dom_serializer = require("dom-serializer");
2368
2527
  function injectMetaTags(htmlContent, options = {}) {
2369
2528
  const parsed = parseHTML(htmlContent);
@@ -2732,18 +2891,61 @@ function escapeJavaScriptString(str) {
2732
2891
  function injectMetaTagsInFile(filePath, options = {}) {
2733
2892
  const parsed = parseHTMLFile(filePath);
2734
2893
  const { html, result } = injectMetaTags(parsed.originalContent, options);
2735
- (0, import_fs12.writeFileSync)(filePath, html, "utf-8");
2894
+ (0, import_fs13.writeFileSync)(filePath, html, "utf-8");
2736
2895
  return result;
2737
2896
  }
2897
+ async function injectMetaTagsInFilesBatch(batchOptions) {
2898
+ const { files, options, concurrency = 5, continueOnError = true } = batchOptions;
2899
+ const successful = [];
2900
+ const failed = [];
2901
+ const processBatch = async (batch) => {
2902
+ const results = await Promise.allSettled(
2903
+ batch.map((file) => {
2904
+ try {
2905
+ const result = injectMetaTagsInFile(file, options);
2906
+ return Promise.resolve({ file, result, success: true });
2907
+ } catch (err) {
2908
+ const message = err instanceof Error ? err.message : String(err);
2909
+ if (!continueOnError) {
2910
+ return Promise.reject(new Error(`Failed to inject meta tags in ${file}: ${message}`));
2911
+ }
2912
+ return Promise.resolve({ file, error: message, success: false });
2913
+ }
2914
+ })
2915
+ );
2916
+ for (const result of results) {
2917
+ if (result.status === "fulfilled") {
2918
+ if (result.value.success) {
2919
+ successful.push({ file: result.value.file, result: result.value.result });
2920
+ } else {
2921
+ failed.push({ file: result.value.file, error: result.value.error });
2922
+ }
2923
+ } else {
2924
+ const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
2925
+ failed.push({ file: "unknown", error: errorMessage });
2926
+ }
2927
+ }
2928
+ };
2929
+ for (let i = 0; i < files.length; i += concurrency) {
2930
+ const batch = files.slice(i, i + concurrency);
2931
+ await processBatch(batch);
2932
+ }
2933
+ return {
2934
+ successful,
2935
+ failed,
2936
+ totalProcessed: successful.length,
2937
+ totalFailed: failed.length
2938
+ };
2939
+ }
2738
2940
 
2739
2941
  // src/validator/pwa-validator.ts
2740
- var import_fs13 = require("fs");
2942
+ var import_fs14 = require("fs");
2741
2943
  var import_path11 = require("path");
2742
2944
  var import_promises = require("fs/promises");
2743
2945
  function validateManifest(projectPath, outputDir) {
2744
2946
  const errors = [];
2745
2947
  const manifestPath = (0, import_path11.join)(outputDir, "manifest.json");
2746
- if (!(0, import_fs13.existsSync)(manifestPath)) {
2948
+ if (!(0, import_fs14.existsSync)(manifestPath)) {
2747
2949
  return {
2748
2950
  exists: false,
2749
2951
  valid: false,
@@ -2759,7 +2961,7 @@ function validateManifest(projectPath, outputDir) {
2759
2961
  };
2760
2962
  }
2761
2963
  try {
2762
- const manifestContent = (0, import_fs13.readFileSync)(manifestPath, "utf-8");
2964
+ const manifestContent = (0, import_fs14.readFileSync)(manifestPath, "utf-8");
2763
2965
  const manifest = JSON.parse(manifestContent);
2764
2966
  if (!manifest.name || manifest.name.trim().length === 0) {
2765
2967
  errors.push({
@@ -2873,7 +3075,7 @@ function validateIcons(projectPath, outputDir, manifest) {
2873
3075
  for (const icon of manifest.icons) {
2874
3076
  const iconPath = icon.src.startsWith("/") ? icon.src.substring(1) : icon.src;
2875
3077
  const fullIconPath = (0, import_path11.join)(outputDir, iconPath);
2876
- if (!(0, import_fs13.existsSync)(fullIconPath)) {
3078
+ if (!(0, import_fs14.existsSync)(fullIconPath)) {
2877
3079
  errors.push({
2878
3080
  code: "ICON_FILE_MISSING",
2879
3081
  message: `Icon file not found: ${iconPath}`,
@@ -2917,7 +3119,7 @@ function validateIcons(projectPath, outputDir, manifest) {
2917
3119
  function validateServiceWorker(projectPath, outputDir) {
2918
3120
  const errors = [];
2919
3121
  const swPath = (0, import_path11.join)(outputDir, "sw.js");
2920
- if (!(0, import_fs13.existsSync)(swPath)) {
3122
+ if (!(0, import_fs14.existsSync)(swPath)) {
2921
3123
  return {
2922
3124
  exists: false,
2923
3125
  valid: false,
@@ -2933,7 +3135,7 @@ function validateServiceWorker(projectPath, outputDir) {
2933
3135
  };
2934
3136
  }
2935
3137
  try {
2936
- const swContent = (0, import_fs13.readFileSync)(swPath, "utf-8");
3138
+ const swContent = (0, import_fs14.readFileSync)(swPath, "utf-8");
2937
3139
  if (!swContent.includes("workbox") && !swContent.includes("serviceWorker")) {
2938
3140
  errors.push({
2939
3141
  code: "SERVICE_WORKER_INVALID",
@@ -3196,6 +3398,7 @@ async function validatePWA(options) {
3196
3398
  generateSplashScreensOnly,
3197
3399
  injectMetaTags,
3198
3400
  injectMetaTagsInFile,
3401
+ injectMetaTagsInFilesBatch,
3199
3402
  optimizeImage,
3200
3403
  optimizeProject,
3201
3404
  optimizeProjectImages,
package/dist/index.d.cts CHANGED
@@ -253,6 +253,19 @@ declare function writeManifest(manifest: Manifest, outputDir: string): string;
253
253
  */
254
254
  declare function generateAndWriteManifest(options: ManifestGeneratorOptions, outputDir: string): string;
255
255
 
256
+ interface IconValidationResult {
257
+ valid: boolean;
258
+ errors: string[];
259
+ warnings: string[];
260
+ suggestions: string[];
261
+ metadata?: {
262
+ width: number;
263
+ height: number;
264
+ format: string;
265
+ size: number;
266
+ };
267
+ }
268
+
256
269
  interface IconSize {
257
270
  width: number;
258
271
  height: number;
@@ -272,11 +285,14 @@ interface IconGeneratorOptions {
272
285
  splashSizes?: SplashScreenSize[];
273
286
  format?: 'png' | 'webp';
274
287
  quality?: number;
288
+ validate?: boolean;
289
+ strictValidation?: boolean;
275
290
  }
276
291
  interface IconGenerationResult {
277
292
  icons: ManifestIcon[];
278
293
  splashScreens: ManifestSplashScreen[];
279
294
  generatedFiles: string[];
295
+ validation?: IconValidationResult;
280
296
  }
281
297
  /**
282
298
  * Generates all PWA icons from a source image
@@ -448,6 +464,35 @@ declare function injectMetaTags(htmlContent: string, options?: MetaInjectorOptio
448
464
  * Injects meta-tags into an HTML file
449
465
  */
450
466
  declare function injectMetaTagsInFile(filePath: string, options?: MetaInjectorOptions): InjectionResult;
467
+ /**
468
+ * Batch process options for parallel HTML injection
469
+ */
470
+ interface BatchInjectOptions {
471
+ files: string[];
472
+ options: MetaInjectorOptions;
473
+ concurrency?: number;
474
+ continueOnError?: boolean;
475
+ }
476
+ /**
477
+ * Result of batch injection
478
+ */
479
+ interface BatchInjectResult {
480
+ successful: Array<{
481
+ file: string;
482
+ result: InjectionResult;
483
+ }>;
484
+ failed: Array<{
485
+ file: string;
486
+ error: string;
487
+ }>;
488
+ totalProcessed: number;
489
+ totalFailed: number;
490
+ }
491
+ /**
492
+ * Injects PWA meta-tags into multiple HTML files in parallel with concurrency limit
493
+ * This significantly improves performance when processing many files
494
+ */
495
+ declare function injectMetaTagsInFilesBatch(batchOptions: BatchInjectOptions): Promise<BatchInjectResult>;
451
496
 
452
497
  interface ValidationError {
453
498
  code: string;
@@ -509,4 +554,4 @@ interface PWAValidatorOptions {
509
554
  */
510
555
  declare function validatePWA(options: PWAValidatorOptions): Promise<ValidationResult>;
511
556
 
512
- export { type AdaptiveCacheStrategy, type ApiType, type Architecture, type ArchitectureDetectionResult, type AssetDetectionResult, type AssetOptimizationSuggestion, type BuildTool, type CacheStrategy, type Framework, type FrameworkDetectionResult, type FrameworkVersion, type HTMLParserOptions, type HttpsCheckResult, type HttpsCheckerOptions, type IconGenerationResult, type IconGeneratorOptions, type IconSize, type ImageOptimizationOptions, type InjectionResult, type Manifest, type ManifestGeneratorOptions, type ManifestIcon, ManifestSchema, type ManifestSplashScreen, type MetaInjectorOptions, type OptimizationResult, type OptimizedImageResult, type OptimizedManifestConfig, type PWAValidatorOptions, type ParsedHTML, type ProjectConfiguration, STANDARD_ICON_SIZES, STANDARD_SPLASH_SIZES, type ScannerOptions, type ScannerResult, type ServiceWorkerGenerationResult, type ServiceWorkerGeneratorOptions, type SplashScreenSize, type ValidationError, type ValidationResult, type ValidationWarning, checkHttps, checkProjectHttps, detectApiType, detectArchitecture, detectAssets, detectFramework, detectProjectUrl, detectUnoptimizedImages, elementExists, findAllElements, findElement, generateAdaptiveCacheStrategies, generateAndWriteManifest, generateAndWriteServiceWorker, generateAppleTouchIcon, generateFavicon, generateIcons, generateIconsOnly, generateManifest, generateOptimalShortName, generateReport, generateResponsiveImageSizes, generateServiceWorker, generateSimpleServiceWorker, generateSplashScreensOnly, injectMetaTags, injectMetaTagsInFile, optimizeImage, optimizeProject, optimizeProjectImages, parseHTML, parseHTMLFile, scanProject, serializeHTML, suggestManifestColors, validatePWA, validateProjectPath, writeManifest };
557
+ export { type AdaptiveCacheStrategy, type ApiType, type Architecture, type ArchitectureDetectionResult, type AssetDetectionResult, type AssetOptimizationSuggestion, type BatchInjectOptions, type BatchInjectResult, type BuildTool, type CacheStrategy, type Framework, type FrameworkDetectionResult, type FrameworkVersion, type HTMLParserOptions, type HttpsCheckResult, type HttpsCheckerOptions, type IconGenerationResult, type IconGeneratorOptions, type IconSize, type ImageOptimizationOptions, type InjectionResult, type Manifest, type ManifestGeneratorOptions, type ManifestIcon, ManifestSchema, type ManifestSplashScreen, type MetaInjectorOptions, type OptimizationResult, type OptimizedImageResult, type OptimizedManifestConfig, type PWAValidatorOptions, type ParsedHTML, type ProjectConfiguration, STANDARD_ICON_SIZES, STANDARD_SPLASH_SIZES, type ScannerOptions, type ScannerResult, type ServiceWorkerGenerationResult, type ServiceWorkerGeneratorOptions, type SplashScreenSize, type ValidationError, type ValidationResult, type ValidationWarning, checkHttps, checkProjectHttps, detectApiType, detectArchitecture, detectAssets, detectFramework, detectProjectUrl, detectUnoptimizedImages, elementExists, findAllElements, findElement, generateAdaptiveCacheStrategies, generateAndWriteManifest, generateAndWriteServiceWorker, generateAppleTouchIcon, generateFavicon, generateIcons, generateIconsOnly, generateManifest, generateOptimalShortName, generateReport, generateResponsiveImageSizes, generateServiceWorker, generateSimpleServiceWorker, generateSplashScreensOnly, injectMetaTags, injectMetaTagsInFile, injectMetaTagsInFilesBatch, optimizeImage, optimizeProject, optimizeProjectImages, parseHTML, parseHTMLFile, scanProject, serializeHTML, suggestManifestColors, validatePWA, validateProjectPath, writeManifest };
package/dist/index.d.ts CHANGED
@@ -253,6 +253,19 @@ declare function writeManifest(manifest: Manifest, outputDir: string): string;
253
253
  */
254
254
  declare function generateAndWriteManifest(options: ManifestGeneratorOptions, outputDir: string): string;
255
255
 
256
+ interface IconValidationResult {
257
+ valid: boolean;
258
+ errors: string[];
259
+ warnings: string[];
260
+ suggestions: string[];
261
+ metadata?: {
262
+ width: number;
263
+ height: number;
264
+ format: string;
265
+ size: number;
266
+ };
267
+ }
268
+
256
269
  interface IconSize {
257
270
  width: number;
258
271
  height: number;
@@ -272,11 +285,14 @@ interface IconGeneratorOptions {
272
285
  splashSizes?: SplashScreenSize[];
273
286
  format?: 'png' | 'webp';
274
287
  quality?: number;
288
+ validate?: boolean;
289
+ strictValidation?: boolean;
275
290
  }
276
291
  interface IconGenerationResult {
277
292
  icons: ManifestIcon[];
278
293
  splashScreens: ManifestSplashScreen[];
279
294
  generatedFiles: string[];
295
+ validation?: IconValidationResult;
280
296
  }
281
297
  /**
282
298
  * Generates all PWA icons from a source image
@@ -448,6 +464,35 @@ declare function injectMetaTags(htmlContent: string, options?: MetaInjectorOptio
448
464
  * Injects meta-tags into an HTML file
449
465
  */
450
466
  declare function injectMetaTagsInFile(filePath: string, options?: MetaInjectorOptions): InjectionResult;
467
+ /**
468
+ * Batch process options for parallel HTML injection
469
+ */
470
+ interface BatchInjectOptions {
471
+ files: string[];
472
+ options: MetaInjectorOptions;
473
+ concurrency?: number;
474
+ continueOnError?: boolean;
475
+ }
476
+ /**
477
+ * Result of batch injection
478
+ */
479
+ interface BatchInjectResult {
480
+ successful: Array<{
481
+ file: string;
482
+ result: InjectionResult;
483
+ }>;
484
+ failed: Array<{
485
+ file: string;
486
+ error: string;
487
+ }>;
488
+ totalProcessed: number;
489
+ totalFailed: number;
490
+ }
491
+ /**
492
+ * Injects PWA meta-tags into multiple HTML files in parallel with concurrency limit
493
+ * This significantly improves performance when processing many files
494
+ */
495
+ declare function injectMetaTagsInFilesBatch(batchOptions: BatchInjectOptions): Promise<BatchInjectResult>;
451
496
 
452
497
  interface ValidationError {
453
498
  code: string;
@@ -509,4 +554,4 @@ interface PWAValidatorOptions {
509
554
  */
510
555
  declare function validatePWA(options: PWAValidatorOptions): Promise<ValidationResult>;
511
556
 
512
- export { type AdaptiveCacheStrategy, type ApiType, type Architecture, type ArchitectureDetectionResult, type AssetDetectionResult, type AssetOptimizationSuggestion, type BuildTool, type CacheStrategy, type Framework, type FrameworkDetectionResult, type FrameworkVersion, type HTMLParserOptions, type HttpsCheckResult, type HttpsCheckerOptions, type IconGenerationResult, type IconGeneratorOptions, type IconSize, type ImageOptimizationOptions, type InjectionResult, type Manifest, type ManifestGeneratorOptions, type ManifestIcon, ManifestSchema, type ManifestSplashScreen, type MetaInjectorOptions, type OptimizationResult, type OptimizedImageResult, type OptimizedManifestConfig, type PWAValidatorOptions, type ParsedHTML, type ProjectConfiguration, STANDARD_ICON_SIZES, STANDARD_SPLASH_SIZES, type ScannerOptions, type ScannerResult, type ServiceWorkerGenerationResult, type ServiceWorkerGeneratorOptions, type SplashScreenSize, type ValidationError, type ValidationResult, type ValidationWarning, checkHttps, checkProjectHttps, detectApiType, detectArchitecture, detectAssets, detectFramework, detectProjectUrl, detectUnoptimizedImages, elementExists, findAllElements, findElement, generateAdaptiveCacheStrategies, generateAndWriteManifest, generateAndWriteServiceWorker, generateAppleTouchIcon, generateFavicon, generateIcons, generateIconsOnly, generateManifest, generateOptimalShortName, generateReport, generateResponsiveImageSizes, generateServiceWorker, generateSimpleServiceWorker, generateSplashScreensOnly, injectMetaTags, injectMetaTagsInFile, optimizeImage, optimizeProject, optimizeProjectImages, parseHTML, parseHTMLFile, scanProject, serializeHTML, suggestManifestColors, validatePWA, validateProjectPath, writeManifest };
557
+ export { type AdaptiveCacheStrategy, type ApiType, type Architecture, type ArchitectureDetectionResult, type AssetDetectionResult, type AssetOptimizationSuggestion, type BatchInjectOptions, type BatchInjectResult, type BuildTool, type CacheStrategy, type Framework, type FrameworkDetectionResult, type FrameworkVersion, type HTMLParserOptions, type HttpsCheckResult, type HttpsCheckerOptions, type IconGenerationResult, type IconGeneratorOptions, type IconSize, type ImageOptimizationOptions, type InjectionResult, type Manifest, type ManifestGeneratorOptions, type ManifestIcon, ManifestSchema, type ManifestSplashScreen, type MetaInjectorOptions, type OptimizationResult, type OptimizedImageResult, type OptimizedManifestConfig, type PWAValidatorOptions, type ParsedHTML, type ProjectConfiguration, STANDARD_ICON_SIZES, STANDARD_SPLASH_SIZES, type ScannerOptions, type ScannerResult, type ServiceWorkerGenerationResult, type ServiceWorkerGeneratorOptions, type SplashScreenSize, type ValidationError, type ValidationResult, type ValidationWarning, checkHttps, checkProjectHttps, detectApiType, detectArchitecture, detectAssets, detectFramework, detectProjectUrl, detectUnoptimizedImages, elementExists, findAllElements, findElement, generateAdaptiveCacheStrategies, generateAndWriteManifest, generateAndWriteServiceWorker, generateAppleTouchIcon, generateFavicon, generateIcons, generateIconsOnly, generateManifest, generateOptimalShortName, generateReport, generateResponsiveImageSizes, generateServiceWorker, generateSimpleServiceWorker, generateSplashScreensOnly, injectMetaTags, injectMetaTagsInFile, injectMetaTagsInFilesBatch, optimizeImage, optimizeProject, optimizeProjectImages, parseHTML, parseHTMLFile, scanProject, serializeHTML, suggestManifestColors, validatePWA, validateProjectPath, writeManifest };
package/dist/index.js CHANGED
@@ -1570,9 +1570,11 @@ async function scanProject(options) {
1570
1570
  buildTool: null
1571
1571
  }
1572
1572
  };
1573
- const assetsCandidate = includeAssets ? await detectAssets(projectPath) : getEmptyAssets();
1573
+ const [assetsCandidate, architectureCandidate] = await Promise.all([
1574
+ includeAssets ? detectAssets(projectPath) : Promise.resolve(getEmptyAssets()),
1575
+ includeArchitecture ? detectArchitecture(projectPath) : Promise.resolve(getEmptyArchitecture())
1576
+ ]);
1574
1577
  const assets = isAssetDetectionResult(assetsCandidate) ? assetsCandidate : getEmptyAssets();
1575
- const architectureCandidate = includeArchitecture ? await detectArchitecture(projectPath) : getEmptyArchitecture();
1576
1578
  const architecture = isArchitectureDetectionResult(architectureCandidate) ? architectureCandidate : getEmptyArchitecture();
1577
1579
  const result = {
1578
1580
  framework,
@@ -1738,9 +1740,117 @@ function generateAndWriteManifest(options, outputDir) {
1738
1740
  }
1739
1741
 
1740
1742
  // src/generator/icon-generator.ts
1741
- import sharp2 from "sharp";
1742
- import { existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
1743
+ import sharp3 from "sharp";
1744
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
1743
1745
  import { join as join8 } from "path";
1746
+
1747
+ // src/validator/icon-validator.ts
1748
+ import sharp2 from "sharp";
1749
+ import { existsSync as existsSync6 } from "fs";
1750
+ async function validateIconSource(options) {
1751
+ const {
1752
+ sourceImage,
1753
+ strict = false,
1754
+ minRecommendedSize = 192,
1755
+ optimalSize = 512
1756
+ } = options;
1757
+ const result = {
1758
+ valid: true,
1759
+ errors: [],
1760
+ warnings: [],
1761
+ suggestions: []
1762
+ };
1763
+ if (!existsSync6(sourceImage)) {
1764
+ result.valid = false;
1765
+ result.errors.push(`Source image not found: ${sourceImage}`);
1766
+ return result;
1767
+ }
1768
+ try {
1769
+ const image = sharp2(sourceImage);
1770
+ const metadata = await image.metadata();
1771
+ if (!metadata.width || !metadata.height) {
1772
+ result.valid = false;
1773
+ result.errors.push("Unable to read image dimensions");
1774
+ return result;
1775
+ }
1776
+ const { width, height, format, size } = metadata;
1777
+ const minDimension = Math.min(width, height);
1778
+ const maxDimension = Math.max(width, height);
1779
+ const isSquare = width === height;
1780
+ result.metadata = {
1781
+ width,
1782
+ height,
1783
+ format: format || "unknown",
1784
+ size: size || 0
1785
+ };
1786
+ const supportedFormats = ["png", "jpeg", "jpg", "webp", "svg"];
1787
+ if (!format || !supportedFormats.includes(format.toLowerCase())) {
1788
+ result.valid = false;
1789
+ result.errors.push(
1790
+ `Unsupported image format: ${format || "unknown"}. Supported formats: ${supportedFormats.join(", ")}`
1791
+ );
1792
+ }
1793
+ if (minDimension < minRecommendedSize) {
1794
+ const message = `Icon dimensions too small: ${width}x${height}. Minimum recommended: ${minRecommendedSize}x${minRecommendedSize}`;
1795
+ if (strict) {
1796
+ result.valid = false;
1797
+ result.errors.push(message);
1798
+ } else {
1799
+ result.warnings.push(message);
1800
+ result.suggestions.push(
1801
+ `Use an image at least ${minRecommendedSize}x${minRecommendedSize} pixels for best PWA compatibility`
1802
+ );
1803
+ }
1804
+ }
1805
+ if (minDimension < optimalSize) {
1806
+ result.warnings.push(
1807
+ `Icon dimensions below optimal size: ${width}x${height}. Optimal size: ${optimalSize}x${optimalSize} for best quality on all devices`
1808
+ );
1809
+ result.suggestions.push(
1810
+ `Consider using a ${optimalSize}x${optimalSize} source image for optimal icon quality`
1811
+ );
1812
+ }
1813
+ if (!isSquare) {
1814
+ result.warnings.push(
1815
+ `Icon is not square (${width}x${height}). Square images work best for PWA icons`
1816
+ );
1817
+ result.suggestions.push(
1818
+ "Consider using a square source image. The icon will be cropped to fit during generation"
1819
+ );
1820
+ }
1821
+ if (format?.toLowerCase() === "jpg" || format?.toLowerCase() === "jpeg") {
1822
+ result.suggestions.push(
1823
+ "PNG format is recommended for icons with transparency. Consider converting JPG to PNG"
1824
+ );
1825
+ }
1826
+ if (size && size > 1024 * 1024) {
1827
+ const sizeMB = (size / (1024 * 1024)).toFixed(2);
1828
+ result.warnings.push(`Icon file size is large: ${sizeMB}MB. This may slow down generation`);
1829
+ result.suggestions.push(
1830
+ "Consider optimizing the source image before generation to reduce file size"
1831
+ );
1832
+ } else if (size && size > 500 * 1024) {
1833
+ const sizeKB = (size / 1024).toFixed(2);
1834
+ result.suggestions.push(
1835
+ `Icon file size is ${sizeKB}KB. Consider optimizing to reduce generation time`
1836
+ );
1837
+ }
1838
+ const aspectRatio = maxDimension / minDimension;
1839
+ if (aspectRatio > 2) {
1840
+ result.warnings.push(
1841
+ `Icon has extreme aspect ratio (${aspectRatio.toFixed(2)}:1). Square images are recommended`
1842
+ );
1843
+ }
1844
+ return result;
1845
+ } catch (error) {
1846
+ const message = error instanceof Error ? error.message : String(error);
1847
+ result.valid = false;
1848
+ result.errors.push(`Failed to validate icon: ${message}`);
1849
+ return result;
1850
+ }
1851
+ }
1852
+
1853
+ // src/generator/icon-generator.ts
1744
1854
  var STANDARD_ICON_SIZES = [
1745
1855
  { width: 72, height: 72, name: "icon-72x72.png" },
1746
1856
  { width: 96, height: 96, name: "icon-96x96.png" },
@@ -1774,67 +1884,114 @@ async function generateIcons(options) {
1774
1884
  iconSizes = STANDARD_ICON_SIZES,
1775
1885
  splashSizes = STANDARD_SPLASH_SIZES,
1776
1886
  format = "png",
1777
- quality = 90
1887
+ quality = 90,
1888
+ validate = false,
1889
+ strictValidation = false
1778
1890
  } = options;
1779
- if (!existsSync6(sourceImage)) {
1891
+ if (!existsSync7(sourceImage)) {
1780
1892
  throw new Error(`Source image not found: ${sourceImage}`);
1781
1893
  }
1894
+ let validation;
1895
+ if (validate) {
1896
+ validation = await validateIconSource({
1897
+ sourceImage,
1898
+ strict: strictValidation
1899
+ });
1900
+ if (strictValidation && !validation.valid) {
1901
+ const errorMessages = validation.errors.join("; ");
1902
+ throw new Error(`Icon validation failed: ${errorMessages}`);
1903
+ }
1904
+ }
1782
1905
  mkdirSync2(outputDir, { recursive: true });
1783
1906
  const generatedFiles = [];
1784
1907
  const icons = [];
1785
1908
  const splashScreens = [];
1786
- const image = sharp2(sourceImage);
1909
+ const image = sharp3(sourceImage);
1787
1910
  const metadata = await image.metadata();
1788
1911
  if (!metadata.width || !metadata.height) {
1789
1912
  throw new Error("Unable to read image dimensions");
1790
1913
  }
1791
- for (const size of iconSizes) {
1792
- const outputPath = join8(outputDir, size.name);
1793
- try {
1794
- let pipeline = image.clone().resize(size.width, size.height, {
1795
- fit: "cover",
1796
- position: "center"
1797
- });
1798
- if (format === "png") {
1799
- pipeline = pipeline.png({ quality, compressionLevel: 9 });
1800
- } else {
1801
- pipeline = pipeline.webp({ quality });
1914
+ const iconResults = await Promise.all(
1915
+ iconSizes.map(async (size) => {
1916
+ const outputPath = join8(outputDir, size.name);
1917
+ try {
1918
+ let pipeline = image.clone().resize(size.width, size.height, {
1919
+ fit: "cover",
1920
+ position: "center"
1921
+ });
1922
+ if (format === "png") {
1923
+ pipeline = pipeline.png({ quality, compressionLevel: 9 });
1924
+ } else {
1925
+ pipeline = pipeline.webp({ quality });
1926
+ }
1927
+ await pipeline.toFile(outputPath);
1928
+ return {
1929
+ success: true,
1930
+ file: outputPath,
1931
+ icon: {
1932
+ src: `/${size.name}`,
1933
+ sizes: `${size.width}x${size.height}`,
1934
+ type: format === "png" ? "image/png" : "image/webp",
1935
+ purpose: size.width >= 192 && size.width <= 512 ? "any" : void 0
1936
+ }
1937
+ };
1938
+ } catch (err) {
1939
+ const message = err instanceof Error ? err.message : String(err);
1940
+ return {
1941
+ success: false,
1942
+ error: `Failed to generate icon ${size.name}: ${message}`,
1943
+ size: size.name
1944
+ };
1802
1945
  }
1803
- await pipeline.toFile(outputPath);
1804
- generatedFiles.push(outputPath);
1805
- icons.push({
1806
- src: `/${size.name}`,
1807
- sizes: `${size.width}x${size.height}`,
1808
- type: format === "png" ? "image/png" : "image/webp",
1809
- purpose: size.width >= 192 && size.width <= 512 ? "any" : void 0
1810
- });
1811
- } catch (err) {
1812
- const message = err instanceof Error ? err.message : String(err);
1813
- throw new Error(`Failed to generate icon ${size.name}: ${message}`);
1946
+ })
1947
+ );
1948
+ for (const result of iconResults) {
1949
+ if (result.success) {
1950
+ generatedFiles.push(result.file);
1951
+ icons.push(result.icon);
1952
+ } else {
1953
+ throw new Error(result.error);
1814
1954
  }
1815
1955
  }
1816
- for (const size of splashSizes) {
1817
- const outputPath = join8(outputDir, size.name);
1818
- try {
1819
- let pipeline = image.clone().resize(size.width, size.height, {
1820
- fit: "cover",
1821
- position: "center"
1822
- });
1823
- if (format === "png") {
1824
- pipeline = pipeline.png({ quality, compressionLevel: 9 });
1825
- } else {
1826
- pipeline = pipeline.webp({ quality });
1956
+ const splashResults = await Promise.all(
1957
+ splashSizes.map(async (size) => {
1958
+ const outputPath = join8(outputDir, size.name);
1959
+ try {
1960
+ let pipeline = image.clone().resize(size.width, size.height, {
1961
+ fit: "cover",
1962
+ position: "center"
1963
+ });
1964
+ if (format === "png") {
1965
+ pipeline = pipeline.png({ quality, compressionLevel: 9 });
1966
+ } else {
1967
+ pipeline = pipeline.webp({ quality });
1968
+ }
1969
+ await pipeline.toFile(outputPath);
1970
+ return {
1971
+ success: true,
1972
+ file: outputPath,
1973
+ splash: {
1974
+ src: `/${size.name}`,
1975
+ sizes: `${size.width}x${size.height}`,
1976
+ type: format === "png" ? "image/png" : "image/webp"
1977
+ }
1978
+ };
1979
+ } catch (err) {
1980
+ const message = err instanceof Error ? err.message : String(err);
1981
+ return {
1982
+ success: false,
1983
+ error: `Failed to generate splash screen ${size.name}: ${message}`,
1984
+ size: size.name
1985
+ };
1827
1986
  }
1828
- await pipeline.toFile(outputPath);
1829
- generatedFiles.push(outputPath);
1830
- splashScreens.push({
1831
- src: `/${size.name}`,
1832
- sizes: `${size.width}x${size.height}`,
1833
- type: format === "png" ? "image/png" : "image/webp"
1834
- });
1835
- } catch (err) {
1836
- const message = err instanceof Error ? err.message : String(err);
1837
- throw new Error(`Failed to generate splash screen ${size.name}: ${message}`);
1987
+ })
1988
+ );
1989
+ for (const result of splashResults) {
1990
+ if (result.success) {
1991
+ generatedFiles.push(result.file);
1992
+ splashScreens.push(result.splash);
1993
+ } else {
1994
+ throw new Error(result.error);
1838
1995
  }
1839
1996
  }
1840
1997
  if (format === "png") {
@@ -1853,7 +2010,8 @@ async function generateIcons(options) {
1853
2010
  return {
1854
2011
  icons,
1855
2012
  splashScreens,
1856
- generatedFiles
2013
+ generatedFiles,
2014
+ validation
1857
2015
  };
1858
2016
  }
1859
2017
  async function generateIconsOnly(options) {
@@ -1877,13 +2035,13 @@ async function generateSplashScreensOnly(options) {
1877
2035
  };
1878
2036
  }
1879
2037
  async function generateFavicon(sourceImage, outputDir) {
1880
- if (!existsSync6(sourceImage)) {
2038
+ if (!existsSync7(sourceImage)) {
1881
2039
  throw new Error(`Source image not found: ${sourceImage}`);
1882
2040
  }
1883
2041
  mkdirSync2(outputDir, { recursive: true });
1884
2042
  const faviconPath = join8(outputDir, "favicon.ico");
1885
2043
  try {
1886
- await sharp2(sourceImage).resize(32, 32, {
2044
+ await sharp3(sourceImage).resize(32, 32, {
1887
2045
  fit: "cover",
1888
2046
  position: "center"
1889
2047
  }).png().toFile(faviconPath);
@@ -1894,13 +2052,13 @@ async function generateFavicon(sourceImage, outputDir) {
1894
2052
  }
1895
2053
  }
1896
2054
  async function generateAppleTouchIcon(sourceImage, outputDir) {
1897
- if (!existsSync6(sourceImage)) {
2055
+ if (!existsSync7(sourceImage)) {
1898
2056
  throw new Error(`Source image not found: ${sourceImage}`);
1899
2057
  }
1900
2058
  mkdirSync2(outputDir, { recursive: true });
1901
2059
  const appleIconPath = join8(outputDir, "apple-touch-icon.png");
1902
2060
  try {
1903
- await sharp2(sourceImage).resize(180, 180, {
2061
+ await sharp3(sourceImage).resize(180, 180, {
1904
2062
  fit: "cover",
1905
2063
  position: "center"
1906
2064
  }).png({ quality: 90, compressionLevel: 9 }).toFile(appleIconPath);
@@ -1913,7 +2071,7 @@ async function generateAppleTouchIcon(sourceImage, outputDir) {
1913
2071
 
1914
2072
  // src/generator/service-worker-generator.ts
1915
2073
  import { injectManifest, generateSW } from "workbox-build";
1916
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "fs";
2074
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync8 } from "fs";
1917
2075
  import { join as join9 } from "path";
1918
2076
  import { getServiceWorkerTemplate, determineTemplateType } from "@julien-lin/universal-pwa-templates";
1919
2077
  async function generateServiceWorker(options) {
@@ -1948,7 +2106,7 @@ async function generateServiceWorker(options) {
1948
2106
  try {
1949
2107
  const result = await injectManifest(workboxConfig);
1950
2108
  try {
1951
- if (existsSync7(swSrcPath)) {
2109
+ if (existsSync8(swSrcPath)) {
1952
2110
  }
1953
2111
  } catch {
1954
2112
  }
@@ -2033,7 +2191,7 @@ async function generateAndWriteServiceWorker(options) {
2033
2191
  }
2034
2192
 
2035
2193
  // src/generator/https-checker.ts
2036
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
2194
+ import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
2037
2195
  import { join as join10 } from "path";
2038
2196
  function checkHttps(url, allowHttpLocalhost = true) {
2039
2197
  let parsedUrl;
@@ -2090,7 +2248,7 @@ function detectProjectUrl(projectPath) {
2090
2248
  ];
2091
2249
  for (const config of configFiles) {
2092
2250
  const filePath = join10(projectPath, config.file);
2093
- if (existsSync8(filePath)) {
2251
+ if (existsSync9(filePath)) {
2094
2252
  try {
2095
2253
  if (config.file.endsWith(".json") && config.key) {
2096
2254
  const parsed = JSON.parse(readFileSync5(filePath, "utf-8"));
@@ -2659,15 +2817,58 @@ function injectMetaTagsInFile(filePath, options = {}) {
2659
2817
  writeFileSync4(filePath, html, "utf-8");
2660
2818
  return result;
2661
2819
  }
2820
+ async function injectMetaTagsInFilesBatch(batchOptions) {
2821
+ const { files, options, concurrency = 5, continueOnError = true } = batchOptions;
2822
+ const successful = [];
2823
+ const failed = [];
2824
+ const processBatch = async (batch) => {
2825
+ const results = await Promise.allSettled(
2826
+ batch.map((file) => {
2827
+ try {
2828
+ const result = injectMetaTagsInFile(file, options);
2829
+ return Promise.resolve({ file, result, success: true });
2830
+ } catch (err) {
2831
+ const message = err instanceof Error ? err.message : String(err);
2832
+ if (!continueOnError) {
2833
+ return Promise.reject(new Error(`Failed to inject meta tags in ${file}: ${message}`));
2834
+ }
2835
+ return Promise.resolve({ file, error: message, success: false });
2836
+ }
2837
+ })
2838
+ );
2839
+ for (const result of results) {
2840
+ if (result.status === "fulfilled") {
2841
+ if (result.value.success) {
2842
+ successful.push({ file: result.value.file, result: result.value.result });
2843
+ } else {
2844
+ failed.push({ file: result.value.file, error: result.value.error });
2845
+ }
2846
+ } else {
2847
+ const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
2848
+ failed.push({ file: "unknown", error: errorMessage });
2849
+ }
2850
+ }
2851
+ };
2852
+ for (let i = 0; i < files.length; i += concurrency) {
2853
+ const batch = files.slice(i, i + concurrency);
2854
+ await processBatch(batch);
2855
+ }
2856
+ return {
2857
+ successful,
2858
+ failed,
2859
+ totalProcessed: successful.length,
2860
+ totalFailed: failed.length
2861
+ };
2862
+ }
2662
2863
 
2663
2864
  // src/validator/pwa-validator.ts
2664
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
2865
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
2665
2866
  import { join as join11 } from "path";
2666
2867
  import { readFile } from "fs/promises";
2667
2868
  function validateManifest(projectPath, outputDir) {
2668
2869
  const errors = [];
2669
2870
  const manifestPath = join11(outputDir, "manifest.json");
2670
- if (!existsSync9(manifestPath)) {
2871
+ if (!existsSync10(manifestPath)) {
2671
2872
  return {
2672
2873
  exists: false,
2673
2874
  valid: false,
@@ -2797,7 +2998,7 @@ function validateIcons(projectPath, outputDir, manifest) {
2797
2998
  for (const icon of manifest.icons) {
2798
2999
  const iconPath = icon.src.startsWith("/") ? icon.src.substring(1) : icon.src;
2799
3000
  const fullIconPath = join11(outputDir, iconPath);
2800
- if (!existsSync9(fullIconPath)) {
3001
+ if (!existsSync10(fullIconPath)) {
2801
3002
  errors.push({
2802
3003
  code: "ICON_FILE_MISSING",
2803
3004
  message: `Icon file not found: ${iconPath}`,
@@ -2841,7 +3042,7 @@ function validateIcons(projectPath, outputDir, manifest) {
2841
3042
  function validateServiceWorker(projectPath, outputDir) {
2842
3043
  const errors = [];
2843
3044
  const swPath = join11(outputDir, "sw.js");
2844
- if (!existsSync9(swPath)) {
3045
+ if (!existsSync10(swPath)) {
2845
3046
  return {
2846
3047
  exists: false,
2847
3048
  valid: false,
@@ -3119,6 +3320,7 @@ export {
3119
3320
  generateSplashScreensOnly,
3120
3321
  injectMetaTags,
3121
3322
  injectMetaTagsInFile,
3323
+ injectMetaTagsInFilesBatch,
3122
3324
  optimizeImage,
3123
3325
  optimizeProject,
3124
3326
  optimizeProjectImages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@julien-lin/universal-pwa-core",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "Core engine for scanning, generation, and injection for UniversalPWA",
5
5
  "keywords": [
6
6
  "pwa",
@@ -61,7 +61,7 @@
61
61
  "workbox-routing": "^7.4.0",
62
62
  "workbox-strategies": "^7.4.0",
63
63
  "zod": "^4.2.1",
64
- "@julien-lin/universal-pwa-templates": "^1.3.4"
64
+ "@julien-lin/universal-pwa-templates": "^1.3.6"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@vitest/coverage-v8": "^2.1.4",