extension 4.0.9 → 4.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,7 +94,7 @@ npx extension@latest dev --gecko-binary "/Applications/Firefox.app/Contents/MacO
94
94
 
95
95
  | <img alt="Chrome" src="https://media.extension.land/logos/browsers/chrome.svg" width="70"> | <img alt="Edge" src="https://media.extension.land/logos/browsers/edge.svg" width="70"> | <img alt="Firefox" src="https://media.extension.land/logos/browsers/firefox.svg" width="70"> | <img alt="Safari" src="https://media.extension.land/logos/browsers/safari.svg" width="70"> | <img alt="Chromium" src="https://media.extension.land/logos/browsers/chromium.svg" width="70"> | <img alt="Gecko" src="https://media.extension.land/logos/browsers/firefox.svg" width="70"> |
96
96
  | :-: | :-: | :-: | :-: | :-: | :-: |
97
- | Google Chrome<br>✅ Supported | Microsoft Edge<br>✅ Supported | Mozilla Firefox<br>✅ Supported | Apple Safari<br> 🚙 Next | Chromium-based<br>✅ Supported | Gecko-based<br>✅ Supported |
97
+ | Google Chrome<br>✅ Supported | Microsoft Edge<br>✅ Supported | Mozilla Firefox<br>✅ Supported | Apple Safari<br> 🧪 Alpha | Chromium-based<br>✅ Supported | Gecko-based<br>✅ Supported |
98
98
 
99
99
  </div>
100
100
 
package/dist/browsers.cjs CHANGED
@@ -463,6 +463,10 @@ var __webpack_modules__ = {
463
463
  function mv3BackgroundScriptsNotSupportedByChromium(extensionPath) {
464
464
  return `${getLoggingPrefix('warn')} ${pintor__rspack_import_4_default().brightYellow("This MV3 extension declares Firefox-style background.scripts with no service_worker — Chromium refuses to load it.")}\n${pintor__rspack_import_4_default().gray('PATH')} ${pintor__rspack_import_4_default().underline(extensionPath)}\nThe browser rejects it at launch with no console error, so the dev session cannot attach.\nRun it on the browser it targets (${pintor__rspack_import_4_default().blue('--browser firefox')}) or declare ${pintor__rspack_import_4_default().blue('background.service_worker')} for Chromium.`;
465
465
  }
466
+ function unsupportedManifestVersionOnChromium(extensionPath, declared) {
467
+ const value = void 0 === declared ? 'no manifest_version' : `manifest_version ${JSON.stringify(declared)}`;
468
+ return `${getLoggingPrefix('warn')} ${pintor__rspack_import_4_default().brightYellow(`This extension declares ${value}, which Chromium refuses as an unsupported manifest version.`)}\n${pintor__rspack_import_4_default().gray('PATH')} ${pintor__rspack_import_4_default().underline(extensionPath)}\nThe browser rejects it at launch (a native dialog, not a console error), so the dev session cannot attach.\nDeclare ${pintor__rspack_import_4_default().blue('"manifest_version": 3')} in the source manifest.`;
469
+ }
466
470
  function chromiumInvalidMatchPatterns(extensionPath, patterns) {
467
471
  const shown = patterns.slice(0, 6);
468
472
  const more = patterns.length - shown.length;
@@ -731,6 +735,7 @@ var __webpack_modules__ = {
731
735
  $w: ()=>bestEffortBannerPrintFailed,
732
736
  A9: ()=>firefoxLaunchCalled,
733
737
  AG: ()=>enhancedProcessManagementForceKill,
738
+ CN: ()=>unsupportedManifestVersionOnChromium,
734
739
  CY: ()=>devChromiumDebugPort,
735
740
  Cn: ()=>chromeProcessError,
736
741
  Dh: ()=>invalidGeckoBinaryPath,
@@ -1768,6 +1773,7 @@ var __webpack_modules__ = {
1768
1773
  const m = manifest;
1769
1774
  if (2 === Number(m?.manifest_version)) return 'mv2';
1770
1775
  if (3 === Number(m?.manifest_version) && Array.isArray(m?.background?.scripts) && m.background.scripts.length > 0 && !m?.background?.service_worker) return "mv3-background-scripts";
1776
+ if (m && 'object' == typeof m && !Array.isArray(m) && 3 !== Number(m.manifest_version)) return 'unsupported-manifest-version';
1771
1777
  return null;
1772
1778
  }
1773
1779
  function findInvalidMatchPatterns(manifest) {
@@ -1802,9 +1808,42 @@ var __webpack_modules__ = {
1802
1808
  if (!host.includes('*')) return false;
1803
1809
  return '*' !== host && !/^\*\.[^*]+$/.test(host);
1804
1810
  }
1805
- function findChromiumLoadBlockers(manifest) {
1811
+ function findChromiumLoadBlockers(manifest, browserVersion) {
1806
1812
  const m = manifest;
1807
1813
  const blockers = [];
1814
+ if (!m || 'object' != typeof m || Array.isArray(m)) return blockers;
1815
+ if ('string' != typeof m.name || '' === m.name) blockers.push("name: missing, empty, or not a string — Chrome requires a non-empty string name and refuses the extension.");
1816
+ if ('string' != typeof m.version || !isValidDottedVersion(m.version)) blockers.push("version: missing or invalid — Chrome requires 1-4 dot-separated integers (0-65535) and refuses the extension.");
1817
+ if (3 === Number(m.manifest_version) && Array.isArray(m.web_accessible_resources)) m.web_accessible_resources.forEach((entry, index)=>{
1818
+ if (!entry || 'object' != typeof entry || Array.isArray(entry)) return void blockers.push(`web_accessible_resources[${index}]: MV2-style entry — MV3 requires {resources, matches|extension_ids|use_dynamic_url} dictionaries.`);
1819
+ if (void 0 === entry.resources) blockers.push(`web_accessible_resources[${index}]: 'resources' is required — Chrome refuses the extension without it.`);
1820
+ if (void 0 === entry.matches && void 0 === entry.extension_ids && void 0 === entry.use_dynamic_url) blockers.push(`web_accessible_resources[${index}]: needs one of 'matches', 'extension_ids', or 'use_dynamic_url' beside resources — Chrome refuses the extension without it.`);
1821
+ });
1822
+ const CS_RUN_AT = [
1823
+ 'document_start',
1824
+ 'document_end',
1825
+ 'document_idle'
1826
+ ];
1827
+ const csGroups = Array.isArray(m.content_scripts) ? m.content_scripts : [];
1828
+ csGroups.forEach((group, index)=>{
1829
+ if (!group || 'object' != typeof group) return;
1830
+ if (void 0 === group.matches) blockers.push(`content_scripts[${index}]: 'matches' is required — Chrome refuses the extension without it.`);
1831
+ else if (Array.isArray(group.matches) && 0 === group.matches.length) blockers.push(`content_scripts[${index}].matches: there must be at least one match — Chrome refuses the extension over an empty list.`);
1832
+ for (const listKey of [
1833
+ 'js',
1834
+ 'css'
1835
+ ]){
1836
+ const list = group[listKey];
1837
+ if (Array.isArray(list)) list.forEach((entry, entryIndex)=>{
1838
+ if ('string' != typeof entry) blockers.push(`content_scripts[${index}].${listKey}[${entryIndex}]: expected a string, got ${typeof entry} — Chrome refuses the extension.`);
1839
+ });
1840
+ }
1841
+ if (void 0 !== group.run_at && !CS_RUN_AT.includes(group.run_at)) blockers.push(`content_scripts[${index}].run_at: expected "document_start", "document_end" or "document_idle", got ${JSON.stringify(group.run_at)} — Chrome refuses the extension.`);
1842
+ });
1843
+ const minVersion = m.minimum_chrome_version;
1844
+ if (void 0 !== minVersion) if ('string' == typeof minVersion && isValidDottedVersion(minVersion)) {
1845
+ if (browserVersion && isValidDottedVersion(browserVersion) && compareDottedVersions(minVersion, browserVersion) > 0) blockers.push(`minimum_chrome_version: requires ${minVersion} but the resolved browser is ${browserVersion} — the browser refuses the extension.`);
1846
+ } else blockers.push(`minimum_chrome_version: invalid value ${JSON.stringify(minVersion)} — Chrome refuses the extension.`);
1808
1847
  const commands = m?.commands;
1809
1848
  if (commands && 'object' == typeof commands) {
1810
1849
  const withKeys = Object.values(commands).filter((command)=>command?.suggested_key);
@@ -1851,6 +1890,74 @@ var __webpack_modules__ = {
1851
1890
  }
1852
1891
  return findings;
1853
1892
  }
1893
+ function findLocaleLoadBlockers(manifest, extensionDir) {
1894
+ const m = manifest;
1895
+ const blockers = [];
1896
+ if (!m || 'object' != typeof m || Array.isArray(m)) return blockers;
1897
+ const localesDir = external_path_.join(extensionDir, '_locales');
1898
+ const defaultLocale = m.default_locale;
1899
+ if ('string' == typeof defaultLocale && '' !== defaultLocale.trim()) {
1900
+ const catalogPath = external_path_.join(localesDir, defaultLocale, 'messages.json');
1901
+ let catalogKeys = null;
1902
+ try {
1903
+ if (external_fs_.existsSync(catalogPath)) try {
1904
+ const raw = external_fs_.readFileSync(catalogPath, 'utf8').replace(/^\uFEFF/, '');
1905
+ const catalog = JSON.parse(raw);
1906
+ catalogKeys = new Set(Object.keys(catalog || {}).map((key)=>key.toLowerCase()));
1907
+ } catch {
1908
+ blockers.push(`default_locale: _locales/${defaultLocale}/messages.json is not valid JSON — Chrome refuses the whole extension.`);
1909
+ }
1910
+ else blockers.push(`default_locale: "${defaultLocale}" is declared but _locales/${defaultLocale}/messages.json is missing — Chrome refuses the whole extension.`);
1911
+ } catch {
1912
+ return blockers;
1913
+ }
1914
+ if (catalogKeys) {
1915
+ const refs = new Set();
1916
+ collectMsgRefs(m, refs);
1917
+ for (const ref of refs)if (!catalogKeys.has(ref.toLowerCase())) blockers.push(`__MSG_${ref}__: used in the manifest but not defined in _locales/${defaultLocale}/messages.json — Chrome refuses the whole extension.`);
1918
+ }
1919
+ } else try {
1920
+ if (external_fs_.existsSync(localesDir)) {
1921
+ const hasCatalog = external_fs_.readdirSync(localesDir).some((entry)=>external_fs_.existsSync(external_path_.join(localesDir, entry, 'messages.json')));
1922
+ if (hasCatalog) blockers.push("_locales: a locales tree exists but the manifest declares no default_locale — Chrome refuses the whole extension.");
1923
+ }
1924
+ } catch {}
1925
+ return blockers;
1926
+ }
1927
+ function findMissingManagedSchema(manifest, extensionDir) {
1928
+ const m = manifest;
1929
+ const schema = m?.storage?.managed_schema;
1930
+ if ('string' != typeof schema || '' === schema.trim()) return [];
1931
+ try {
1932
+ const abs = external_path_.join(extensionDir, schema.replace(/^\//, ''));
1933
+ if (!external_fs_.existsSync(abs)) return [
1934
+ `storage.managed_schema: "${schema}" does not exist in the extension directory — Chrome refuses the whole extension.`
1935
+ ];
1936
+ } catch {}
1937
+ return [];
1938
+ }
1939
+ function collectMsgRefs(value, out) {
1940
+ if ('string' == typeof value) {
1941
+ const match = /^__MSG_(.+)__$/.exec(value.trim());
1942
+ if (match && !match[1].startsWith('@@')) out.add(match[1]);
1943
+ } else if (Array.isArray(value)) for (const item of value)collectMsgRefs(item, out);
1944
+ else if (value && 'object' == typeof value) for (const item of Object.values(value))collectMsgRefs(item, out);
1945
+ }
1946
+ function isValidDottedVersion(version) {
1947
+ if (!version) return false;
1948
+ const parts = version.split('.');
1949
+ if (parts.length > 4) return false;
1950
+ return parts.every((part)=>/^\d{1,5}$/.test(part) && Number(part) <= 65535);
1951
+ }
1952
+ function compareDottedVersions(a, b) {
1953
+ const pa = a.split('.').map(Number);
1954
+ const pb = b.split('.').map(Number);
1955
+ for(let i = 0; i < 4; i++){
1956
+ const diff = (pa[i] || 0) - (pb[i] || 0);
1957
+ if (0 !== diff) return diff;
1958
+ }
1959
+ return 0;
1960
+ }
1854
1961
  function isValidBase64(value) {
1855
1962
  if (0 === value.length || value.length % 4 !== 0) return false;
1856
1963
  if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return false;
@@ -2644,16 +2751,20 @@ var __webpack_modules__ = {
2644
2751
  console.log(messages.iL(browser, browserBinaryLocation, browserVersionLine));
2645
2752
  const extensionsToLoad = (0, runtime_options.fT)(this.options.extension);
2646
2753
  (0, runtime_options.sl)(extensionsToLoad, this.ctx.setExtensionRoot);
2754
+ const resolvedBrowserVersion = /(\d+(?:\.\d+){0,3})/.exec(browserVersionLine || '')?.[1];
2647
2755
  for (const extPath of extensionsToLoad)try {
2648
2756
  const m = JSON.parse(external_fs_.readFileSync(external_path_.join(String(extPath), 'manifest.json'), 'utf8'));
2649
2757
  const refusal = diagnoseChromiumManifestRefusal(m);
2650
2758
  if ('mv2' === refusal) console.warn(messages.sP(String(extPath)));
2651
2759
  else if ("mv3-background-scripts" === refusal) console.warn(messages.NO(String(extPath)));
2760
+ else if ('unsupported-manifest-version' === refusal) console.warn(messages.CN(String(extPath), m?.manifest_version));
2652
2761
  const invalidPatterns = findInvalidMatchPatterns(m);
2653
2762
  if (invalidPatterns.length) console.warn(messages.qR(String(extPath), invalidPatterns));
2654
2763
  const loadBlockers = [
2655
- ...findChromiumLoadBlockers(m),
2656
- ...findUnloadableIconFiles(m, String(extPath))
2764
+ ...findChromiumLoadBlockers(m, resolvedBrowserVersion),
2765
+ ...findUnloadableIconFiles(m, String(extPath)),
2766
+ ...findLocaleLoadBlockers(m, String(extPath)),
2767
+ ...findMissingManagedSchema(m, String(extPath))
2657
2768
  ];
2658
2769
  if (loadBlockers.length) console.warn(messages.L(String(extPath), loadBlockers));
2659
2770
  } catch {}
package/dist/cli.cjs CHANGED
@@ -464,6 +464,10 @@ var __webpack_modules__ = {
464
464
  function mv3BackgroundScriptsNotSupportedByChromium(extensionPath) {
465
465
  return `${getLoggingPrefix('warn')} ${pintor__rspack_import_4_default().brightYellow("This MV3 extension declares Firefox-style background.scripts with no service_worker — Chromium refuses to load it.")}\n${pintor__rspack_import_4_default().gray('PATH')} ${pintor__rspack_import_4_default().underline(extensionPath)}\nThe browser rejects it at launch with no console error, so the dev session cannot attach.\nRun it on the browser it targets (${pintor__rspack_import_4_default().blue('--browser firefox')}) or declare ${pintor__rspack_import_4_default().blue('background.service_worker')} for Chromium.`;
466
466
  }
467
+ function unsupportedManifestVersionOnChromium(extensionPath, declared) {
468
+ const value = void 0 === declared ? 'no manifest_version' : `manifest_version ${JSON.stringify(declared)}`;
469
+ return `${getLoggingPrefix('warn')} ${pintor__rspack_import_4_default().brightYellow(`This extension declares ${value}, which Chromium refuses as an unsupported manifest version.`)}\n${pintor__rspack_import_4_default().gray('PATH')} ${pintor__rspack_import_4_default().underline(extensionPath)}\nThe browser rejects it at launch (a native dialog, not a console error), so the dev session cannot attach.\nDeclare ${pintor__rspack_import_4_default().blue('"manifest_version": 3')} in the source manifest.`;
470
+ }
467
471
  function chromiumInvalidMatchPatterns(extensionPath, patterns) {
468
472
  const shown = patterns.slice(0, 6);
469
473
  const more = patterns.length - shown.length;
@@ -607,7 +611,10 @@ var __webpack_modules__ = {
607
611
  return platform;
608
612
  }
609
613
  function safariRequiresMacOS(platform) {
610
- return `${getLoggingPrefix('warn')} Safari extensions can only be built on macOS.\nDetected ${pintor__rspack_import_4_default().gray(prettyPlatform(platform))} — skipping Safari packaging. The web-extension build in ${pintor__rspack_import_4_default().yellow('dist/safari')} is still complete and can be packaged later on a Mac with Xcode.`;
614
+ return `${getLoggingPrefix('warn')} Safari extensions can only be built on macOS.\nDetected ${pintor__rspack_import_4_default().gray(prettyPlatform(platform))}. Target another browser via ${pintor__rspack_import_4_default().blue('--browser')} ${pintor__rspack_import_4_default().gray('<chrome|edge|firefox>')}, or run this command on a Mac with Xcode.`;
615
+ }
616
+ function safariPackagingSkippedNonMac(platform) {
617
+ return `${getLoggingPrefix('warn')} Safari packaging needs macOS with Xcode — detected ${pintor__rspack_import_4_default().gray(prettyPlatform(platform))}, so the Xcode packaging step is skipped.\nThe web-extension build in ${pintor__rspack_import_4_default().yellow('dist/safari')} is still complete and can be packaged later on a Mac with ${pintor__rspack_import_4_default().blue('extension build --browser=safari')}.`;
611
618
  }
612
619
  function safariXcodeRequired(developerDir) {
613
620
  const current = developerDir ? `${pintor__rspack_import_4_default().gray('Active toolchain:')} ${pintor__rspack_import_4_default().underline(developerDir)}` : `${pintor__rspack_import_4_default().gray('No active developer directory was found.')}`;
@@ -631,8 +638,19 @@ var __webpack_modules__ = {
631
638
  function safariOpening(target) {
632
639
  return `${getLoggingPrefix('info')} Opening ${pintor__rspack_import_4_default().underline(target)}`;
633
640
  }
634
- function safariFailed(error) {
635
- return `${getLoggingPrefix('error')} Safari build failed:\n${pintor__rspack_import_4_default().red(errorDetail(error))}`;
641
+ function safariToolFailed(tool, exitCode, outputTail) {
642
+ const code = null === exitCode ? 'no exit code' : `exit ${exitCode}`;
643
+ const tail = outputTail.trim().length ? `\n${pintor__rspack_import_4_default().gray('── last output ──')}\n${outputTail}` : `\n${pintor__rspack_import_4_default().gray('(no output captured)')}`;
644
+ return `${getLoggingPrefix('error')} Safari packaging tool ${pintor__rspack_import_4_default().underline(tool)} failed (${pintor__rspack_import_4_default().red(code)}).${tail}`;
645
+ }
646
+ function safariConverterWarnings(warnings) {
647
+ return `${getLoggingPrefix('warn')} safari-web-extension-converter reported ${pintor__rspack_import_4_default().yellow(String(warnings.length))} warning(s) — some manifest keys/APIs may not be supported by Safari:\n` + warnings.map((line)=>` ${pintor__rspack_import_4_default().gray('•')} ${line}`).join('\n');
648
+ }
649
+ function safariDefaultBundleIdNote(bundleId) {
650
+ return `${getLoggingPrefix('info')} Using the generated bundle id ${pintor__rspack_import_4_default().gray(bundleId)}. For an app you plan to distribute, set your own with ${pintor__rspack_import_4_default().blue('--bundle-id')} now — changing it later makes Safari treat the extension as a new identity.`;
651
+ }
652
+ function safariOpenHint(appPath, appName) {
653
+ return `${getLoggingPrefix('info')} Launch it once to register with Safari: ${pintor__rspack_import_4_default().blue('open')} ${pintor__rspack_import_4_default().underline(`"${appPath}"`)}\nThen enable ${pintor__rspack_import_4_default().brightBlue(appName)} via Safari ▸ Develop ▸ ${pintor__rspack_import_4_default().yellow('Allow Unsigned Extensions')} and Safari ▸ Settings ▸ Extensions.`;
636
654
  }
637
655
  function safariDryRunNotBuilding() {
638
656
  return `${getLoggingPrefix('info')} [browser] Dry run: not building Safari app`;
@@ -656,7 +674,13 @@ var __webpack_modules__ = {
656
674
  return `${getLoggingPrefix('success')} Rebuilt ${pintor__rspack_import_4_default().brightBlue(appName)} — reload the page (or toggle the extension) in Safari to see changes.`;
657
675
  }
658
676
  function safariProjectStale() {
659
- return `${getLoggingPrefix('info')} manifest.json changed since the Xcode project was generated — regenerating project.`;
677
+ return `${getLoggingPrefix('info')} manifest.json or identity options changed since the Xcode project was generated — regenerating project.`;
678
+ }
679
+ function safariForcedRegeneration() {
680
+ return `${getLoggingPrefix('info')} --force-regenerate set — regenerating the Xcode project.`;
681
+ }
682
+ function safariRegenerationDiscards(preservedKeys) {
683
+ return `${getLoggingPrefix('warn')} Regenerating replaces the Xcode project: customizations made in Xcode (entitlements, capabilities, added files/targets) are ${pintor__rspack_import_4_default().red('discarded')}.\nPreserved automatically: ${pintor__rspack_import_4_default().yellow(preservedKeys.join(', '))}. If you customized the project, back it up before continuing.`;
660
684
  }
661
685
  function safariSettingsPreserved(keys) {
662
686
  return `${getLoggingPrefix('info')} Preserved Xcode build settings across regeneration: ${pintor__rspack_import_4_default().yellow(keys.join(', '))}.\nIf any other project-level tweaks were lost, reconfigure them in Xcode.`;
@@ -795,10 +819,13 @@ var __webpack_modules__ = {
795
819
  return `${getLoggingPrefix('error')} Invalid RDP request payload`;
796
820
  }
797
821
  __webpack_require__.d(__webpack_exports__, {
822
+ $q: ()=>safariDefaultBundleIdNote,
798
823
  $w: ()=>bestEffortBannerPrintFailed,
824
+ A: ()=>safariToolFailed,
799
825
  A9: ()=>firefoxLaunchCalled,
800
826
  AG: ()=>enhancedProcessManagementForceKill,
801
827
  Aj: ()=>safariDryRunXcodebuild,
828
+ CN: ()=>unsupportedManifestVersionOnChromium,
802
829
  CY: ()=>devChromiumDebugPort,
803
830
  Cn: ()=>chromeProcessError,
804
831
  Cs: ()=>safariToolchainMissing,
@@ -811,6 +838,7 @@ var __webpack_modules__ = {
811
838
  Fm: ()=>connectionClosedError,
812
839
  G3: ()=>safariRequiresMacOS,
813
840
  Gq: ()=>locatingBrowser,
841
+ H1: ()=>safariRegenerationDiscards,
814
842
  HH: ()=>messageWithoutSenderError,
815
843
  Ih: ()=>browserNotInstalledError,
816
844
  JB: ()=>devChannelSnapshotInUse,
@@ -826,12 +854,13 @@ var __webpack_modules__ = {
826
854
  NO: ()=>mv3BackgroundScriptsNotSupportedByChromium,
827
855
  Nk: ()=>chromiumDryRunNotLaunching,
828
856
  Q0: ()=>chromiumDryRunBinary,
829
- Qr: ()=>safariFailed,
830
857
  Rl: ()=>safariBuilt,
831
858
  Sp: ()=>invalidChromiumBinaryPath,
832
859
  TF: ()=>enhancedProcessManagementCleanupError,
833
860
  Te: ()=>cdpClientTargetWebSocketUrlStored,
834
861
  Th: ()=>browserInstanceExited,
862
+ Tq: ()=>safariForcedRegeneration,
863
+ UW: ()=>safariPackagingSkippedNonMac,
835
864
  Ug: ()=>usingManagedChromiumFamilyFallback,
836
865
  Vk: ()=>enhancedProcessManagementCleanup,
837
866
  WV: ()=>addonInstallError,
@@ -840,6 +869,7 @@ var __webpack_modules__ = {
840
869
  Z5: ()=>cdpClientExtensionInfoFailed,
841
870
  _D: ()=>browserLaunchError,
842
871
  aI: ()=>devChromeProfilePath,
872
+ aO: ()=>safariOpenHint,
843
873
  bv: ()=>cdpUnifiedExtensionLog,
844
874
  cD: ()=>targetActorHasActiveRequestError,
845
875
  cF: ()=>safariRebuilt,
@@ -882,6 +912,7 @@ var __webpack_modules__ = {
882
912
  wk: ()=>chromeFailedToSpawn,
883
913
  xx: ()=>cdpFailedToHandleMessage,
884
914
  xy: ()=>chromeInitializingEnhancedReload,
915
+ y1: ()=>safariConverterWarnings,
885
916
  y7: ()=>skippingBrowserLaunchDueToCompileErrors,
886
917
  yc: ()=>safariDryRunNotBuilding
887
918
  });
@@ -1853,6 +1884,7 @@ var __webpack_modules__ = {
1853
1884
  const m = manifest;
1854
1885
  if (2 === Number(m?.manifest_version)) return 'mv2';
1855
1886
  if (3 === Number(m?.manifest_version) && Array.isArray(m?.background?.scripts) && m.background.scripts.length > 0 && !m?.background?.service_worker) return "mv3-background-scripts";
1887
+ if (m && 'object' == typeof m && !Array.isArray(m) && 3 !== Number(m.manifest_version)) return 'unsupported-manifest-version';
1856
1888
  return null;
1857
1889
  }
1858
1890
  function findInvalidMatchPatterns(manifest) {
@@ -1887,9 +1919,42 @@ var __webpack_modules__ = {
1887
1919
  if (!host.includes('*')) return false;
1888
1920
  return '*' !== host && !/^\*\.[^*]+$/.test(host);
1889
1921
  }
1890
- function findChromiumLoadBlockers(manifest) {
1922
+ function findChromiumLoadBlockers(manifest, browserVersion) {
1891
1923
  const m = manifest;
1892
1924
  const blockers = [];
1925
+ if (!m || 'object' != typeof m || Array.isArray(m)) return blockers;
1926
+ if ('string' != typeof m.name || '' === m.name) blockers.push("name: missing, empty, or not a string — Chrome requires a non-empty string name and refuses the extension.");
1927
+ if ('string' != typeof m.version || !isValidDottedVersion(m.version)) blockers.push("version: missing or invalid — Chrome requires 1-4 dot-separated integers (0-65535) and refuses the extension.");
1928
+ if (3 === Number(m.manifest_version) && Array.isArray(m.web_accessible_resources)) m.web_accessible_resources.forEach((entry, index)=>{
1929
+ if (!entry || 'object' != typeof entry || Array.isArray(entry)) return void blockers.push(`web_accessible_resources[${index}]: MV2-style entry — MV3 requires {resources, matches|extension_ids|use_dynamic_url} dictionaries.`);
1930
+ if (void 0 === entry.resources) blockers.push(`web_accessible_resources[${index}]: 'resources' is required — Chrome refuses the extension without it.`);
1931
+ if (void 0 === entry.matches && void 0 === entry.extension_ids && void 0 === entry.use_dynamic_url) blockers.push(`web_accessible_resources[${index}]: needs one of 'matches', 'extension_ids', or 'use_dynamic_url' beside resources — Chrome refuses the extension without it.`);
1932
+ });
1933
+ const CS_RUN_AT = [
1934
+ 'document_start',
1935
+ 'document_end',
1936
+ 'document_idle'
1937
+ ];
1938
+ const csGroups = Array.isArray(m.content_scripts) ? m.content_scripts : [];
1939
+ csGroups.forEach((group, index)=>{
1940
+ if (!group || 'object' != typeof group) return;
1941
+ if (void 0 === group.matches) blockers.push(`content_scripts[${index}]: 'matches' is required — Chrome refuses the extension without it.`);
1942
+ else if (Array.isArray(group.matches) && 0 === group.matches.length) blockers.push(`content_scripts[${index}].matches: there must be at least one match — Chrome refuses the extension over an empty list.`);
1943
+ for (const listKey of [
1944
+ 'js',
1945
+ 'css'
1946
+ ]){
1947
+ const list = group[listKey];
1948
+ if (Array.isArray(list)) list.forEach((entry, entryIndex)=>{
1949
+ if ('string' != typeof entry) blockers.push(`content_scripts[${index}].${listKey}[${entryIndex}]: expected a string, got ${typeof entry} — Chrome refuses the extension.`);
1950
+ });
1951
+ }
1952
+ if (void 0 !== group.run_at && !CS_RUN_AT.includes(group.run_at)) blockers.push(`content_scripts[${index}].run_at: expected "document_start", "document_end" or "document_idle", got ${JSON.stringify(group.run_at)} — Chrome refuses the extension.`);
1953
+ });
1954
+ const minVersion = m.minimum_chrome_version;
1955
+ if (void 0 !== minVersion) if ('string' == typeof minVersion && isValidDottedVersion(minVersion)) {
1956
+ if (browserVersion && isValidDottedVersion(browserVersion) && compareDottedVersions(minVersion, browserVersion) > 0) blockers.push(`minimum_chrome_version: requires ${minVersion} but the resolved browser is ${browserVersion} — the browser refuses the extension.`);
1957
+ } else blockers.push(`minimum_chrome_version: invalid value ${JSON.stringify(minVersion)} — Chrome refuses the extension.`);
1893
1958
  const commands = m?.commands;
1894
1959
  if (commands && 'object' == typeof commands) {
1895
1960
  const withKeys = Object.values(commands).filter((command)=>command?.suggested_key);
@@ -1936,6 +2001,74 @@ var __webpack_modules__ = {
1936
2001
  }
1937
2002
  return findings;
1938
2003
  }
2004
+ function findLocaleLoadBlockers(manifest, extensionDir) {
2005
+ const m = manifest;
2006
+ const blockers = [];
2007
+ if (!m || 'object' != typeof m || Array.isArray(m)) return blockers;
2008
+ const localesDir = external_path_.join(extensionDir, '_locales');
2009
+ const defaultLocale = m.default_locale;
2010
+ if ('string' == typeof defaultLocale && '' !== defaultLocale.trim()) {
2011
+ const catalogPath = external_path_.join(localesDir, defaultLocale, 'messages.json');
2012
+ let catalogKeys = null;
2013
+ try {
2014
+ if (external_fs_.existsSync(catalogPath)) try {
2015
+ const raw = external_fs_.readFileSync(catalogPath, 'utf8').replace(/^\uFEFF/, '');
2016
+ const catalog = JSON.parse(raw);
2017
+ catalogKeys = new Set(Object.keys(catalog || {}).map((key)=>key.toLowerCase()));
2018
+ } catch {
2019
+ blockers.push(`default_locale: _locales/${defaultLocale}/messages.json is not valid JSON — Chrome refuses the whole extension.`);
2020
+ }
2021
+ else blockers.push(`default_locale: "${defaultLocale}" is declared but _locales/${defaultLocale}/messages.json is missing — Chrome refuses the whole extension.`);
2022
+ } catch {
2023
+ return blockers;
2024
+ }
2025
+ if (catalogKeys) {
2026
+ const refs = new Set();
2027
+ collectMsgRefs(m, refs);
2028
+ for (const ref of refs)if (!catalogKeys.has(ref.toLowerCase())) blockers.push(`__MSG_${ref}__: used in the manifest but not defined in _locales/${defaultLocale}/messages.json — Chrome refuses the whole extension.`);
2029
+ }
2030
+ } else try {
2031
+ if (external_fs_.existsSync(localesDir)) {
2032
+ const hasCatalog = external_fs_.readdirSync(localesDir).some((entry)=>external_fs_.existsSync(external_path_.join(localesDir, entry, 'messages.json')));
2033
+ if (hasCatalog) blockers.push("_locales: a locales tree exists but the manifest declares no default_locale — Chrome refuses the whole extension.");
2034
+ }
2035
+ } catch {}
2036
+ return blockers;
2037
+ }
2038
+ function findMissingManagedSchema(manifest, extensionDir) {
2039
+ const m = manifest;
2040
+ const schema = m?.storage?.managed_schema;
2041
+ if ('string' != typeof schema || '' === schema.trim()) return [];
2042
+ try {
2043
+ const abs = external_path_.join(extensionDir, schema.replace(/^\//, ''));
2044
+ if (!external_fs_.existsSync(abs)) return [
2045
+ `storage.managed_schema: "${schema}" does not exist in the extension directory — Chrome refuses the whole extension.`
2046
+ ];
2047
+ } catch {}
2048
+ return [];
2049
+ }
2050
+ function collectMsgRefs(value, out) {
2051
+ if ('string' == typeof value) {
2052
+ const match = /^__MSG_(.+)__$/.exec(value.trim());
2053
+ if (match && !match[1].startsWith('@@')) out.add(match[1]);
2054
+ } else if (Array.isArray(value)) for (const item of value)collectMsgRefs(item, out);
2055
+ else if (value && 'object' == typeof value) for (const item of Object.values(value))collectMsgRefs(item, out);
2056
+ }
2057
+ function isValidDottedVersion(version) {
2058
+ if (!version) return false;
2059
+ const parts = version.split('.');
2060
+ if (parts.length > 4) return false;
2061
+ return parts.every((part)=>/^\d{1,5}$/.test(part) && Number(part) <= 65535);
2062
+ }
2063
+ function compareDottedVersions(a, b) {
2064
+ const pa = a.split('.').map(Number);
2065
+ const pb = b.split('.').map(Number);
2066
+ for(let i = 0; i < 4; i++){
2067
+ const diff = (pa[i] || 0) - (pb[i] || 0);
2068
+ if (0 !== diff) return diff;
2069
+ }
2070
+ return 0;
2071
+ }
1939
2072
  function isValidBase64(value) {
1940
2073
  if (0 === value.length || value.length % 4 !== 0) return false;
1941
2074
  if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return false;
@@ -2729,16 +2862,20 @@ var __webpack_modules__ = {
2729
2862
  console.log(messages.iL(browser, browserBinaryLocation, browserVersionLine));
2730
2863
  const extensionsToLoad = (0, runtime_options.fT)(this.options.extension);
2731
2864
  (0, runtime_options.sl)(extensionsToLoad, this.ctx.setExtensionRoot);
2865
+ const resolvedBrowserVersion = /(\d+(?:\.\d+){0,3})/.exec(browserVersionLine || '')?.[1];
2732
2866
  for (const extPath of extensionsToLoad)try {
2733
2867
  const m = JSON.parse(external_fs_.readFileSync(external_path_.join(String(extPath), 'manifest.json'), 'utf8'));
2734
2868
  const refusal = diagnoseChromiumManifestRefusal(m);
2735
2869
  if ('mv2' === refusal) console.warn(messages.sP(String(extPath)));
2736
2870
  else if ("mv3-background-scripts" === refusal) console.warn(messages.NO(String(extPath)));
2871
+ else if ('unsupported-manifest-version' === refusal) console.warn(messages.CN(String(extPath), m?.manifest_version));
2737
2872
  const invalidPatterns = findInvalidMatchPatterns(m);
2738
2873
  if (invalidPatterns.length) console.warn(messages.qR(String(extPath), invalidPatterns));
2739
2874
  const loadBlockers = [
2740
- ...findChromiumLoadBlockers(m),
2741
- ...findUnloadableIconFiles(m, String(extPath))
2875
+ ...findChromiumLoadBlockers(m, resolvedBrowserVersion),
2876
+ ...findUnloadableIconFiles(m, String(extPath)),
2877
+ ...findLocaleLoadBlockers(m, String(extPath)),
2878
+ ...findMissingManagedSchema(m, String(extPath))
2742
2879
  ];
2743
2880
  if (loadBlockers.length) console.warn(messages.L(String(extPath), loadBlockers));
2744
2881
  } catch {}
@@ -4973,6 +5110,12 @@ AI Assistants
4973
5110
  function unsupportedBrowserFlag(value, supported) {
4974
5111
  return `${getLoggingPrefix('error')} Unsupported --browser value: ${value}. Supported: ${supported.join(', ')}.`;
4975
5112
  }
5113
+ function safariOnlyOption(flags) {
5114
+ return `${getLoggingPrefix('error')} ${flags.map(messages_code).join(', ')} only appl${1 === flags.length ? 'ies' : 'y'} to Safari targets. Add ${messages_code('--browser safari')} (or ${messages_code('webkit-based')}).`;
5115
+ }
5116
+ function safariInvalidBundleId(bundleId) {
5117
+ return `${getLoggingPrefix('error')} Invalid bundle identifier: ${messages_code(bundleId)}\nUse reverse-DNS form — dot-separated segments of letters, digits and hyphens, each starting with a letter (e.g. ${messages_code('com.example.my-extension')}).`;
5118
+ }
4976
5119
  function safariCommandNotSupported(command) {
4977
5120
  return `${getLoggingPrefix('error')} ${messages_code(command)} can't load an extension into Safari automatically.\nSafari extensions ship inside a signed app and are enabled by hand, so there's no live browser session to load into — unlike Chromium and Firefox.\nBuild the Safari app instead: ${messages_code('extension build --browser safari')}\nThen open the generated app and enable it in Safari → Settings → Extensions.`;
4978
5121
  }
@@ -6156,6 +6299,9 @@ Cross-Browser Compatibility
6156
6299
  function deriveBundleId(appName) {
6157
6300
  return `dev.extensionjs.${bundleSegment(appName)}`;
6158
6301
  }
6302
+ function isValidBundleId(value) {
6303
+ return /^[A-Za-z][A-Za-z0-9-]*(\.[A-Za-z][A-Za-z0-9-]*)+$/.test(value);
6304
+ }
6159
6305
  function readManifest(extensionDir) {
6160
6306
  try {
6161
6307
  const manifestPath = external_path_.join(extensionDir, 'manifest.json');
@@ -6167,13 +6313,16 @@ Cross-Browser Compatibility
6167
6313
  const extensionDir = String(compilation?.options?.output?.path || '');
6168
6314
  const manifest = readManifest(extensionDir);
6169
6315
  const appName = sanitizeAppName(String(host.appName || manifest?.name || 'Extension'));
6170
- const bundleIdentifier = deriveBundleId(appName);
6316
+ const userBundleId = String(host.bundleId || '').trim();
6317
+ const bundleIdDerived = !userBundleId;
6318
+ const bundleIdentifier = bundleIdDerived ? deriveBundleId(appName) : userBundleId;
6171
6319
  const projectLocation = `${extensionDir.replace(/[\\/]+$/, '')}-xcode`;
6172
6320
  return {
6173
6321
  extensionDir,
6174
6322
  projectLocation,
6175
6323
  appName,
6176
6324
  bundleIdentifier,
6325
+ bundleIdDerived,
6177
6326
  macOsOnly: false !== host.macOsOnly,
6178
6327
  language: 'swift',
6179
6328
  open: !host.noOpen,
@@ -6250,19 +6399,31 @@ Cross-Browser Compatibility
6250
6399
  return raw;
6251
6400
  }
6252
6401
  }
6402
+ function composeProjectFingerprint(config) {
6403
+ return JSON.stringify({
6404
+ v: 2,
6405
+ identity: {
6406
+ appName: config.appName,
6407
+ bundleId: config.bundleIdentifier,
6408
+ macOsOnly: config.macOsOnly
6409
+ },
6410
+ manifest: normalizeManifest(readManifestRaw(config.extensionDir))
6411
+ });
6412
+ }
6253
6413
  function saveManifestFingerprint(config) {
6254
- const raw = readManifestRaw(config.extensionDir);
6255
6414
  external_fs_.mkdirSync(external_path_.dirname(manifestFingerprintPath(config)), {
6256
6415
  recursive: true
6257
6416
  });
6258
- external_fs_.writeFileSync(manifestFingerprintPath(config), normalizeManifest(raw), 'utf8');
6417
+ external_fs_.writeFileSync(manifestFingerprintPath(config), composeProjectFingerprint(config), 'utf8');
6259
6418
  }
6260
6419
  function isProjectStale(config) {
6261
6420
  const fpPath = manifestFingerprintPath(config);
6262
6421
  if (!external_fs_.existsSync(fpPath)) return true;
6263
6422
  const stored = external_fs_.readFileSync(fpPath, 'utf8');
6264
- const current = normalizeManifest(readManifestRaw(config.extensionDir));
6265
- return stored !== current;
6423
+ return stored !== composeProjectFingerprint(config);
6424
+ }
6425
+ function alignBundleIdentifiers(pbxprojContent, bundleId) {
6426
+ return pbxprojContent.replace(/PRODUCT_BUNDLE_IDENTIFIER = ("?)([^;"]+)\1;/g, (_match, _quote, value)=>/\.Extension$/.test(String(value)) ? `PRODUCT_BUNDLE_IDENTIFIER = "${bundleId}.Extension";` : `PRODUCT_BUNDLE_IDENTIFIER = "${bundleId}";`);
6266
6427
  }
6267
6428
  const PRESERVED_SETTINGS = [
6268
6429
  'DEVELOPMENT_TEAM',
@@ -6319,19 +6480,17 @@ Cross-Browser Compatibility
6319
6480
  function delay(ms) {
6320
6481
  return new Promise((resolve)=>setTimeout(resolve, ms));
6321
6482
  }
6322
- function runTool(bin, args) {
6323
- const inheritOutput = 'true' === process.env.EXTENSION_AUTHOR_MODE;
6324
- return new Promise((resolve)=>{
6325
- const child = (0, external_child_process_.spawn)(bin, args, {
6326
- stdio: inheritOutput ? 'inherit' : 'ignore'
6327
- });
6328
- child.on('error', ()=>resolve(false));
6329
- child.on('close', (code)=>resolve(0 === code));
6330
- });
6483
+ const TOOL_TAIL_LINES = 50;
6484
+ const TOOL_TAIL_BYTES = 8192;
6485
+ function toolOutputTail(output) {
6486
+ const lines = output.slice(4 * -TOOL_TAIL_BYTES).split(/\r?\n/).filter((line)=>line.trim().length > 0);
6487
+ const tail = lines.slice(-TOOL_TAIL_LINES).join('\n');
6488
+ return tail.length > TOOL_TAIL_BYTES ? tail.slice(-TOOL_TAIL_BYTES) : tail;
6331
6489
  }
6332
- function runToolCapture(bin, args) {
6490
+ function runTool(bin, args, opts) {
6491
+ const streamOutput = 'true' === process.env.EXTENSION_AUTHOR_MODE && !opts?.quiet;
6333
6492
  return new Promise((resolve)=>{
6334
- let stdout = '';
6493
+ let output = '';
6335
6494
  const child = (0, external_child_process_.spawn)(bin, args, {
6336
6495
  stdio: [
6337
6496
  'ignore',
@@ -6339,26 +6498,38 @@ Cross-Browser Compatibility
6339
6498
  'pipe'
6340
6499
  ]
6341
6500
  });
6342
- child.stdout?.on('data', (chunk)=>{
6343
- stdout += String(chunk);
6344
- });
6345
- child.on('error', ()=>resolve({
6501
+ const onChunk = (chunk)=>{
6502
+ const text = String(chunk);
6503
+ output += text;
6504
+ if (output.length > 8 * TOOL_TAIL_BYTES) output = output.slice(4 * -TOOL_TAIL_BYTES);
6505
+ if (streamOutput) process.stdout.write(text);
6506
+ };
6507
+ child.stdout?.on('data', onChunk);
6508
+ child.stderr?.on('data', onChunk);
6509
+ child.on('error', (error)=>resolve({
6346
6510
  ok: false,
6347
- stdout
6511
+ code: null,
6512
+ output: `${output}${String(error)}`
6348
6513
  }));
6349
6514
  child.on('close', (code)=>resolve({
6350
6515
  ok: 0 === code,
6351
- stdout
6516
+ code,
6517
+ output
6352
6518
  }));
6353
6519
  });
6354
6520
  }
6521
+ function converterWarnings(output) {
6522
+ return output.split(/\r?\n/).filter((line)=>/warning/i.test(line)).map((line)=>line.trim()).filter((line)=>line.length > 0);
6523
+ }
6355
6524
  async function confirmRegisteredWithSafari(bundleIdentifier) {
6356
6525
  const needle = `${bundleIdentifier}.Extension`;
6357
6526
  for(let attempt = 0; attempt < 6; attempt += 1){
6358
- const { ok, stdout } = await runToolCapture('pluginkit', [
6527
+ const { ok, output } = await runTool('pluginkit', [
6359
6528
  '-m'
6360
- ]);
6361
- if (ok && stdout.includes(needle)) return true;
6529
+ ], {
6530
+ quiet: true
6531
+ });
6532
+ if (ok && output.includes(needle)) return true;
6362
6533
  await delay(800);
6363
6534
  }
6364
6535
  return false;
@@ -6372,6 +6543,20 @@ Cross-Browser Compatibility
6372
6543
  }
6373
6544
  return null;
6374
6545
  }
6546
+ function safariBuildPreflight() {
6547
+ const tc = detectSafariToolchain();
6548
+ if (!tc.platformOk) return {
6549
+ severity: 'skip',
6550
+ message: messages.UW(process.platform)
6551
+ };
6552
+ if (!tc.ok) return {
6553
+ severity: 'fatal',
6554
+ message: tc.needsFullXcode ? messages.Xi(tc.developerDir) : messages.Cs(tc.converter ? 'xcodebuild' : 'safari-web-extension-converter')
6555
+ };
6556
+ return {
6557
+ severity: 'ok'
6558
+ };
6559
+ }
6375
6560
  async function runSafariPipeline(compilation, host, logger, mode) {
6376
6561
  const config = resolveSafariBuildConfig(compilation, host);
6377
6562
  const converterArgs = composeConverterArgs(config);
@@ -6390,10 +6575,24 @@ Cross-Browser Compatibility
6390
6575
  const projectExists = external_fs_.existsSync(xcodeProjectPath(config));
6391
6576
  const needsConversion = !projectExists || host.forceRegenerate || isProjectStale(config);
6392
6577
  if (needsConversion) {
6393
- if (projectExists) logger.info?.(messages.jc());
6578
+ if (projectExists) {
6579
+ logger.info?.(host.forceRegenerate ? messages.Tq() : messages.jc());
6580
+ logger.warn?.(messages.H1([
6581
+ ...PRESERVED_SETTINGS
6582
+ ]));
6583
+ }
6394
6584
  const { saved, restore } = backupAndRestoreXcodeSettings(config);
6395
6585
  logger.info?.(messages.uK(config.extensionDir));
6396
- if (!await runTool('xcrun', converterArgs)) return void logger.error?.(messages.Qr(new Error('safari-web-extension-converter failed')));
6586
+ const converted = await runTool('xcrun', converterArgs);
6587
+ if (!converted.ok) {
6588
+ const tail = toolOutputTail(converted.output);
6589
+ logger.error?.(messages.A('safari-web-extension-converter', converted.code, tail));
6590
+ throw new Error(`safari-web-extension-converter failed (exit ${converted.code})\n${tail}`);
6591
+ }
6592
+ const warnings = converterWarnings(converted.output);
6593
+ if (warnings.length > 0) logger.warn?.(messages.y1(warnings));
6594
+ const projFile = pbxprojPath(config);
6595
+ if (external_fs_.existsSync(projFile)) external_fs_.writeFileSync(projFile, alignBundleIdentifiers(external_fs_.readFileSync(projFile, 'utf8'), config.bundleIdentifier), 'utf8');
6397
6596
  restore();
6398
6597
  const preservedKeys = Object.keys(saved);
6399
6598
  if (preservedKeys.length > 0) logger.info?.(messages.oB(preservedKeys));
@@ -6401,21 +6600,26 @@ Cross-Browser Compatibility
6401
6600
  logger.info?.(messages.l_(config.projectLocation));
6402
6601
  } else logger.info?.(messages.oM());
6403
6602
  if ('full' === mode) logger.info?.(messages.jR(macOsSchemeName(config)));
6404
- if (!await runTool('xcodebuild', xcodebuildArgs)) return void logger.error?.(messages.Qr(new Error('xcodebuild failed')));
6603
+ const built = await runTool('xcodebuild', xcodebuildArgs);
6604
+ if (!built.ok) {
6605
+ const tail = toolOutputTail(built.output);
6606
+ logger.error?.(messages.A('xcodebuild', built.code, tail));
6607
+ throw new Error(`xcodebuild failed (exit ${built.code})\n${tail}`);
6608
+ }
6405
6609
  const appPath = builtAppPath(config);
6406
6610
  if ('resync' === mode) return void logger.info?.(messages.cF(config.appName));
6407
6611
  logger.info?.(messages.Rl(appPath));
6408
- if (config.open) {
6409
- const target = external_fs_.existsSync(appPath) ? appPath : xcodeProjectPath(config);
6410
- logger.info?.(messages.fw(target));
6411
- await runTool('open', [
6412
- target
6413
- ]);
6414
- if (config.safariBinary) await runTool('open', [
6415
- '-a',
6416
- config.safariBinary
6417
- ]);
6418
- }
6612
+ if (config.bundleIdDerived) logger.info?.(messages.$q(config.bundleIdentifier));
6613
+ if (!config.open) return void logger.info?.(messages.aO(appPath, config.appName));
6614
+ const target = external_fs_.existsSync(appPath) ? appPath : xcodeProjectPath(config);
6615
+ logger.info?.(messages.fw(target));
6616
+ await runTool('open', [
6617
+ target
6618
+ ]);
6619
+ if (config.safariBinary) await runTool('open', [
6620
+ '-a',
6621
+ config.safariBinary
6622
+ ]);
6419
6623
  logger.info?.(messages.fO(config.appName));
6420
6624
  if (await confirmRegisteredWithSafari(config.bundleIdentifier)) logger.info?.(messages.fd(config.appName));
6421
6625
  else logger.warn?.(messages.qY(config.appName));
@@ -6454,7 +6658,7 @@ Cross-Browser Compatibility
6454
6658
  return values.length > 0 ? values : void 0;
6455
6659
  }
6456
6660
  function registerDevCommand(program) {
6457
- program.command('dev').arguments('[project-path|remote-url]').usage('[project-path|remote-url] [options]').description(commandDescriptions.dev).addHelpText('after', "\nAdditional options:\n --no-browser do not launch the browser (dev server still starts)\n --no-reload emit a dev-mode dist without the content-script reload runtime; tabs need manual reload to see changes\n --wait wait for ready contract and exit\n --wait-format pretty|json output for wait mode\n").option('--profile <path-to-file | boolean>', 'what path to use for the browser profile. A boolean value of false sets the profile to the default user profile. Defaults to a fresh profile').option('-b, --browser <chrome | chromium | edge | firefox | chromium-based | gecko-based | firefox-based | safari | webkit-based>', 'specify a browser/engine to run. Defaults to `chromium`. `safari` builds and opens a Safari app via Xcode (macOS only; no live reload)').option('--chromium-binary <path-to-binary>', 'specify a path to the Chromium binary. This option overrides the --browser setting. Defaults to the system default').option('--gecko-binary, --firefox-binary <path-to-binary>', 'specify a path to the Gecko binary. This option overrides the --browser setting. Defaults to the system default').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `true`', parseOptionalBoolean).option('--no-open', 'do not open the browser automatically (default: open)').option('--starting-url <url>', 'specify the starting URL for the browser. Defaults to `undefined`').option('--port <port>', 'specify the port to use for the development server. Defaults to `8080`').option('--host <host>', 'specify the host to bind the dev server to. Use 0.0.0.0 for Docker/devcontainers. Defaults to `127.0.0.1`').option('--public-host <host>', 'connectable host the browser (HMR + reload bridge) dials when it differs from the bind host (e.g. a remote/devcontainer). Defaults to the bind host, or 127.0.0.1 when bound to 0.0.0.0').option('--log-context <list>', '[experimental] comma-separated contexts to include (background,content,page,sidebar,popup,options,devtools). Use `all` to include all contexts (default)').option('--logs <off|error|warn|info|debug|trace|all>', '[experimental] minimum centralized logger level to display in terminal (default: off)').option('--log-format <pretty|json|ndjson>', '[experimental] output format for logger events. Defaults to `pretty`').option('--no-log-timestamps', 'disable ISO timestamps in pretty output').option('--no-log-color', 'disable color in pretty output').option('--log-url <pattern>', '[experimental] only show logs where event.url matches this substring or regex (/re/i)').option('--log-tab <id>', 'only show logs for a specific tabId (number)').option('--extensions <list>', 'comma-separated list of companion extensions or store URLs to load').option('--install [boolean]', '[internal] install project dependencies when missing', parseOptionalBoolean).option('--wait [boolean]', 'wait for dist/extension-js/<browser>/ready.json and exit', parseOptionalBoolean).option('--wait-timeout <ms>', 'timeout in milliseconds when using --wait (default: 60000)').option('--wait-format <pretty|json>', 'output format for --wait results (default: pretty)').option('--author, --author-mode', '[internal] enable maintainer diagnostics (does not affect user runtime logs)').option('--allow-control', 'enable the agent-bridge control channel for bounded act (storage/reload/open): see `extension reload|storage|open`').option('--allow-eval', 'additionally enable `extension eval` (runs arbitrary code in a context; writes a 0600 session token)').action(async function(pathOrRemoteUrl, { browser = 'chromium', ...devOptions }) {
6661
+ program.command('dev').arguments('[project-path|remote-url]').usage('[project-path|remote-url] [options]').description(commandDescriptions.dev).addHelpText('after', "\nAdditional options:\n --no-browser do not launch the browser (dev server still starts)\n --no-reload emit a dev-mode dist without the content-script reload runtime; tabs need manual reload to see changes\n --wait wait for ready contract and exit\n --wait-format pretty|json output for wait mode\n").option('--profile <path-to-file | boolean>', 'what path to use for the browser profile. A boolean value of false sets the profile to the default user profile. Defaults to a fresh profile').option('-b, --browser <chrome | chromium | edge | firefox | chromium-based | gecko-based | firefox-based | safari | webkit-based>', 'specify a browser/engine to run. Defaults to `chromium`. `safari` builds and opens a Safari app via Xcode (macOS only; no live reload)').option('--chromium-binary <path-to-binary>', 'specify a path to the Chromium binary. This option overrides the --browser setting. Defaults to the system default').option('--gecko-binary, --firefox-binary <path-to-binary>', 'specify a path to the Gecko binary. This option overrides the --browser setting. Defaults to the system default').option('--safari-binary <path-to-binary>', 'specify the Safari binary to open after packaging (safari targets only)').option('--app-name <name>', 'override the Safari app name (safari targets only). Defaults to the manifest `name`').option('--bundle-id <reverse.dns>', 'set a user-owned Safari bundle identifier (safari targets only). Defaults to a generated dev.extensionjs.* id').option('--force-regenerate', 'regenerate the Safari Xcode project even when up to date (safari targets only)').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `true`', parseOptionalBoolean).option('--no-open', 'do not open the browser automatically (default: open)').option('--starting-url <url>', 'specify the starting URL for the browser. Defaults to `undefined`').option('--port <port>', 'specify the port to use for the development server. Defaults to `8080`').option('--host <host>', 'specify the host to bind the dev server to. Use 0.0.0.0 for Docker/devcontainers. Defaults to `127.0.0.1`').option('--public-host <host>', 'connectable host the browser (HMR + reload bridge) dials when it differs from the bind host (e.g. a remote/devcontainer). Defaults to the bind host, or 127.0.0.1 when bound to 0.0.0.0').option('--log-context <list>', '[experimental] comma-separated contexts to include (background,content,page,sidebar,popup,options,devtools). Use `all` to include all contexts (default)').option('--logs <off|error|warn|info|debug|trace|all>', '[experimental] minimum centralized logger level to display in terminal (default: off)').option('--log-format <pretty|json|ndjson>', '[experimental] output format for logger events. Defaults to `pretty`').option('--no-log-timestamps', 'disable ISO timestamps in pretty output').option('--no-log-color', 'disable color in pretty output').option('--log-url <pattern>', '[experimental] only show logs where event.url matches this substring or regex (/re/i)').option('--log-tab <id>', 'only show logs for a specific tabId (number)').option('--extensions <list>', 'comma-separated list of companion extensions or store URLs to load').option('--install [boolean]', '[internal] install project dependencies when missing', parseOptionalBoolean).option('--wait [boolean]', 'wait for dist/extension-js/<browser>/ready.json and exit', parseOptionalBoolean).option('--wait-timeout <ms>', 'timeout in milliseconds when using --wait (default: 60000)').option('--wait-format <pretty|json>', 'output format for --wait results (default: pretty)').option('--author, --author-mode', '[internal] enable maintainer diagnostics (does not affect user runtime logs)').option('--allow-control', 'enable the agent-bridge control channel for bounded act (storage/reload/open): see `extension reload|storage|open`').option('--allow-eval', 'additionally enable `extension eval` (runs arbitrary code in a context; writes a 0600 session token)').action(async function(pathOrRemoteUrl, { browser = 'chromium', ...devOptions }) {
6458
6662
  if (devOptions.author || devOptions['authorMode']) {
6459
6663
  process.env.EXTENSION_AUTHOR_MODE = 'true';
6460
6664
  if (!process.env.EXTENSION_VERBOSE) process.env.EXTENSION_VERBOSE = '1';
@@ -6463,6 +6667,33 @@ Cross-Browser Compatibility
6463
6667
  validateVendorsOrExit(list, (invalid, supported)=>{
6464
6668
  console.error(unsupportedBrowserFlag(invalid, supported));
6465
6669
  });
6670
+ const opts = devOptions;
6671
+ const safariOnlyFlags = [
6672
+ [
6673
+ '--safari-binary',
6674
+ opts.safariBinary
6675
+ ],
6676
+ [
6677
+ '--app-name',
6678
+ opts.appName
6679
+ ],
6680
+ [
6681
+ '--bundle-id',
6682
+ opts.bundleId
6683
+ ],
6684
+ [
6685
+ '--force-regenerate',
6686
+ opts.forceRegenerate
6687
+ ]
6688
+ ].filter(([, value])=>void 0 !== value && false !== value);
6689
+ if (safariOnlyFlags.length > 0 && !list.some(isSafariVendor)) {
6690
+ console.error(safariOnlyOption(safariOnlyFlags.map(([flag])=>flag)));
6691
+ process.exit(1);
6692
+ }
6693
+ if (opts.bundleId && !isValidBundleId(opts.bundleId)) {
6694
+ console.error(safariInvalidBundleId(opts.bundleId));
6695
+ process.exit(1);
6696
+ }
6466
6697
  if (list.some(isSafariVendor)) {
6467
6698
  const issue = safariPreflightError();
6468
6699
  if (issue) {
@@ -6514,14 +6745,15 @@ Cross-Browser Compatibility
6514
6745
  logUrl: devOptions.logUrl,
6515
6746
  logTab: devOptions.logTab,
6516
6747
  launcher: noBrowser ? void 0 : browsers.launchBrowser,
6517
- safariPackager: async (distPath, mode)=>{
6748
+ safariPackager: async (distPath, mode, overrides)=>{
6518
6749
  await packageSafariExtension({
6519
6750
  extension: [
6520
6751
  distPath
6521
6752
  ],
6522
6753
  browser: vendor,
6523
6754
  noOpen: false === devOptions.open,
6524
- dryRun: false
6755
+ dryRun: false,
6756
+ ...overrides
6525
6757
  }, distPath, void 0, mode);
6526
6758
  }
6527
6759
  };
@@ -6645,7 +6877,7 @@ Cross-Browser Compatibility
6645
6877
  });
6646
6878
  }
6647
6879
  function registerBuildCommand(program) {
6648
- program.command('build').arguments('[project-name]').usage('[path-to-remote-extension] [options]').description(commandDescriptions.build).option('--browser <chrome | chromium | edge | firefox | chromium-based | gecko-based | firefox-based | safari | webkit-based>', 'specify a browser/engine to run. Defaults to `chromium`. `safari` builds a Safari app via Xcode (macOS only)').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `false`', parseOptionalBoolean).option('--zip [boolean]', 'whether or not to compress the extension into a ZIP file. Defaults to `false`', parseOptionalBoolean).option('--zip-source [boolean]', 'whether or not to include the source files in the ZIP file. Defaults to `false`', parseOptionalBoolean).option('--zip-filename <string>', 'specify the name of the ZIP file. Defaults to the extension name and version').option('--silent [boolean]', 'whether or not to open the browser automatically. Defaults to `false`', parseOptionalBoolean).option('--install [boolean]', '[internal] install project dependencies when missing', parseOptionalBoolean).option('--extensions <list>', 'comma-separated list of companion extensions or store URLs to load').option('--mode <development|production|none>', 'bundler mode override (also sets NODE_ENV). Defaults to `production`').option('--author, --author-mode', '[internal] enable maintainer diagnostics (does not affect user runtime logs)').action(async function(pathOrRemoteUrl, { browser = 'chromium', ...buildOptions }) {
6880
+ program.command('build').arguments('[project-name]').usage('[path-to-remote-extension] [options]').description(commandDescriptions.build).option('--browser <chrome | chromium | edge | firefox | chromium-based | gecko-based | firefox-based | safari | webkit-based>', 'specify a browser/engine to run. Defaults to `chromium`. `safari` builds a Safari app via Xcode (macOS only)').option('--polyfill [boolean]', 'whether or not to apply the cross-browser polyfill. Defaults to `false`', parseOptionalBoolean).option('--zip [boolean]', 'whether or not to compress the extension into a ZIP file. Defaults to `false`', parseOptionalBoolean).option('--zip-source [boolean]', 'whether or not to include the source files in the ZIP file. Defaults to `false`', parseOptionalBoolean).option('--zip-filename <string>', 'specify the name of the ZIP file. Defaults to the extension name and version').option('--silent [boolean]', 'whether or not to open the browser automatically. Defaults to `false`', parseOptionalBoolean).option('--install [boolean]', '[internal] install project dependencies when missing', parseOptionalBoolean).option('--extensions <list>', 'comma-separated list of companion extensions or store URLs to load').option('--mode <development|production|none>', 'bundler mode override (also sets NODE_ENV). Defaults to `production`').option('--open [boolean]', 'open the built Safari app after packaging (safari targets only). Defaults to `false`', parseOptionalBoolean).option('--app-name <name>', 'override the Safari app name (safari targets only). Defaults to the manifest `name`').option('--bundle-id <reverse.dns>', 'set a user-owned Safari bundle identifier (safari targets only). Defaults to a generated dev.extensionjs.* id').option('--force-regenerate', 'regenerate the Safari Xcode project even when up to date (safari targets only)').option('--author, --author-mode', '[internal] enable maintainer diagnostics (does not affect user runtime logs)').action(async function(pathOrRemoteUrl, { browser = 'chromium', ...buildOptions }) {
6649
6881
  if (buildOptions.author || buildOptions['authorMode']) {
6650
6882
  process.env.EXTENSION_AUTHOR_MODE = 'true';
6651
6883
  if (!process.env.EXTENSION_VERBOSE) process.env.EXTENSION_VERBOSE = '1';
@@ -6663,12 +6895,43 @@ Cross-Browser Compatibility
6663
6895
  process.exit(1);
6664
6896
  }
6665
6897
  }
6898
+ const safariOnlyFlags = [
6899
+ [
6900
+ '--open',
6901
+ buildOptions.open
6902
+ ],
6903
+ [
6904
+ '--app-name',
6905
+ buildOptions.appName
6906
+ ],
6907
+ [
6908
+ '--bundle-id',
6909
+ buildOptions.bundleId
6910
+ ],
6911
+ [
6912
+ '--force-regenerate',
6913
+ buildOptions.forceRegenerate
6914
+ ]
6915
+ ].filter(([, value])=>void 0 !== value && false !== value);
6916
+ if (safariOnlyFlags.length > 0 && !list.some(isSafariVendor)) {
6917
+ console.error(safariOnlyOption(safariOnlyFlags.map(([flag])=>flag)));
6918
+ process.exit(1);
6919
+ }
6920
+ if (buildOptions.bundleId && !isValidBundleId(buildOptions.bundleId)) {
6921
+ console.error(safariInvalidBundleId(buildOptions.bundleId));
6922
+ process.exit(1);
6923
+ }
6924
+ let safariPackagingEnabled = true;
6666
6925
  if (list.some(isSafariVendor)) {
6667
- const issue = safariPreflightError();
6668
- if (issue) {
6669
- console.error(issue);
6926
+ const preflight = safariBuildPreflight();
6927
+ if ('fatal' === preflight.severity) {
6928
+ console.error(preflight.message);
6670
6929
  process.exit(1);
6671
6930
  }
6931
+ if ('skip' === preflight.severity) {
6932
+ safariPackagingEnabled = false;
6933
+ console.warn(preflight.message);
6934
+ }
6672
6935
  }
6673
6936
  const { extensionBuild } = await loadExtensionDevelopModule();
6674
6937
  for (const vendor of list)await extensionBuild(pathOrRemoteUrl, {
@@ -6681,16 +6944,20 @@ Cross-Browser Compatibility
6681
6944
  install: buildOptions.install,
6682
6945
  extensions: parseExtensionsList(buildOptions.extensions),
6683
6946
  mode,
6684
- safariPackager: async (distPath, packagerMode)=>{
6947
+ appName: buildOptions.appName,
6948
+ bundleId: buildOptions.bundleId,
6949
+ forceRegenerate: buildOptions.forceRegenerate,
6950
+ safariPackager: safariPackagingEnabled ? async (distPath, packagerMode, overrides)=>{
6685
6951
  await packageSafariExtension({
6686
6952
  extension: [
6687
6953
  distPath
6688
6954
  ],
6689
6955
  browser: vendor,
6690
- noOpen: !!buildOptions.silent,
6691
- dryRun: false
6956
+ noOpen: true !== buildOptions.open,
6957
+ dryRun: false,
6958
+ ...overrides
6692
6959
  }, distPath, void 0, packagerMode);
6693
- }
6960
+ } : void 0
6694
6961
  });
6695
6962
  });
6696
6963
  }
@@ -4,7 +4,7 @@
4
4
  * dev session just wedges with no CDP target. Diagnose them before spawn
5
5
  * and say why, like the resolved-binary line.
6
6
  */
7
- export type ChromiumManifestRefusal = 'mv2' | 'mv3-background-scripts' | null;
7
+ export type ChromiumManifestRefusal = 'mv2' | 'mv3-background-scripts' | 'unsupported-manifest-version' | null;
8
8
  export declare function diagnoseChromiumManifestRefusal(manifest: unknown): ChromiumManifestRefusal;
9
9
  /**
10
10
  * Match patterns Chrome's grammar refuses. ONE invalid pattern in
@@ -27,13 +27,37 @@ export declare function diagnoseChromiumManifestRefusal(manifest: unknown): Chro
27
27
  */
28
28
  export declare function findInvalidMatchPatterns(manifest: unknown): string[];
29
29
  /**
30
- * Other manifest shapes Chrome refuses outright, each proven against a wild
31
- * subject with CDP `Extensions.loadUnpacked` (which, unlike --load-extension,
32
- * reports the reason). All are extension-own loading the source unpacked in
33
- * real Chrome fails identically but the refusal is silent, so dev must name
34
- * it instead of printing an ID for an extension that never loads.
30
+ * Other manifest shapes Chrome refuses outright, each proven with CDP
31
+ * `Extensions.loadUnpacked` (which, unlike --load-extension, reports the
32
+ * reason) the wild-subject shapes on Chrome 150 (2026-07-11) and the
33
+ * fixture batch on Chrome 150 (2026-07-13). The refusal is silent under
34
+ * --load-extension, so dev must name it instead of printing an ID for an
35
+ * extension that never loads.
36
+ *
37
+ * NOT refusals — verified to LOAD on Chrome 150 (2026-07-13, CDP
38
+ * loadUnpacked); do NOT add these however fatal they look:
39
+ * - MV3 `background.page`; `background.persistent` true or false
40
+ * - icon files with undecodable bytes, and SVG icons — only a MISSING or
41
+ * 0-byte icon file refuses
42
+ * - unknown `permissions` entries; MV2 keys like `browser_action` under MV3
43
+ * - a WAR dictionary with only `use_dynamic_url` beside `resources`
44
+ * - `__MSG_@@predefined__` variables; catalog-key case differences (message
45
+ * lookup is case-insensitive)
46
+ * And on Firefox 147 (2026-07-13, RDP installTemporaryAddon): explicit AND
47
+ * wildcard ports in match patterns install fine — the old "host must not
48
+ * include a port" grammar is gone; do not resurrect it for gecko.
49
+ *
50
+ * Safari (2026-07-13, macOS 15.7.7): safari-web-extension-converter
51
+ * CONVERTS every one of these shapes with exit 0 — missing name, bad locale
52
+ * catalogs, all of it. The converter is not a refusal surface and must not
53
+ * be gated on manifest shapes; Safari's real refusals happen at runtime
54
+ * inside Safari, which has no scriptable install path to verify against.
55
+ * Converter/xcodebuild exit codes still matter (toolchain failures), but
56
+ * they say nothing about manifest validity.
57
+ *
58
+ * `browserVersion` (when known) enables the minimum_chrome_version compare.
35
59
  */
36
- export declare function findChromiumLoadBlockers(manifest: unknown): string[];
60
+ export declare function findChromiumLoadBlockers(manifest: unknown, browserVersion?: string): string[];
37
61
  /**
38
62
  * Icon files Chrome cannot load — missing from the extension directory or
39
63
  * present but empty (0 bytes, undecodable). Either one makes Chrome refuse
@@ -44,3 +68,17 @@ export declare function findChromiumLoadBlockers(manifest: unknown): string[];
44
68
  * validates at install: `icons` and `*_action.default_icon`.
45
69
  */
46
70
  export declare function findUnloadableIconFiles(manifest: unknown, extensionDir: string): string[];
71
+ /**
72
+ * The locale shapes Chrome refuses the whole extension over, all verified
73
+ * live on Chrome 150 (2026-07-13, CDP loadUnpacked; fixtures 03/04/05/20/
74
+ * 22/29). Common in converted/repacked extensions that lost their _locales
75
+ * tree. Verified tolerances: an EMPTY messages.json loads; message-key
76
+ * lookup is case-insensitive; `__MSG_@@predefined__` never needs a catalog.
77
+ */
78
+ export declare function findLocaleLoadBlockers(manifest: unknown, extensionDir: string): string[];
79
+ /**
80
+ * "File does not exist: <path>/schema.json" — a storage.managed_schema
81
+ * pointing at a file that is not in the extension directory refuses the
82
+ * whole extension (fixture 19, Chrome 150).
83
+ */
84
+ export declare function findMissingManagedSchema(manifest: unknown, extensionDir: string): string[];
@@ -17,6 +17,7 @@ export declare function resolvedBrowserBinary(browser: Browser, binaryPath: stri
17
17
  export declare function preferringSystemBrowserOverSnapshot(systemBinary: string, snapshotBinary: string): string;
18
18
  export declare function mv2NotSupportedByChromium(extensionPath: string): string;
19
19
  export declare function mv3BackgroundScriptsNotSupportedByChromium(extensionPath: string): string;
20
+ export declare function unsupportedManifestVersionOnChromium(extensionPath: string, declared: unknown): string;
20
21
  export declare function chromiumInvalidMatchPatterns(extensionPath: string, patterns: string[]): string;
21
22
  export declare function chromiumManifestLoadBlockers(extensionPath: string, blockers: string[]): string;
22
23
  export declare function devChannelSnapshotInUse(binaryPath: string): string;
@@ -58,6 +59,7 @@ export declare function firefoxDryRunBinary(path: string): string;
58
59
  export declare function firefoxDryRunConfig(cfg: string): string;
59
60
  export declare function safariBuildCalled(): string;
60
61
  export declare function safariRequiresMacOS(platform: string): string;
62
+ export declare function safariPackagingSkippedNonMac(platform: string): string;
61
63
  export declare function safariXcodeRequired(developerDir: string | null): string;
62
64
  export declare function safariToolchainMissing(tool: string): string;
63
65
  export declare function safariConverting(extensionDir: string): string;
@@ -65,7 +67,10 @@ export declare function safariConverted(projectDir: string): string;
65
67
  export declare function safariBuilding(scheme: string): string;
66
68
  export declare function safariBuilt(appPath: string): string;
67
69
  export declare function safariOpening(target: string): string;
68
- export declare function safariFailed(error: unknown): string;
70
+ export declare function safariToolFailed(tool: string, exitCode: number | null, outputTail: string): string;
71
+ export declare function safariConverterWarnings(warnings: string[]): string;
72
+ export declare function safariDefaultBundleIdNote(bundleId: string): string;
73
+ export declare function safariOpenHint(appPath: string, appName: string): string;
69
74
  export declare function safariDryRunNotBuilding(): string;
70
75
  export declare function safariDryRunConverter(cmd: string): string;
71
76
  export declare function safariDryRunXcodebuild(cmd: string): string;
@@ -74,6 +79,8 @@ export declare function safariRegistered(appName: string): string;
74
79
  export declare function safariNotYetRegistered(appName: string): string;
75
80
  export declare function safariRebuilt(appName: string): string;
76
81
  export declare function safariProjectStale(): string;
82
+ export declare function safariForcedRegeneration(): string;
83
+ export declare function safariRegenerationDiscards(preservedKeys: string[]): string;
77
84
  export declare function safariSettingsPreserved(keys: string[]): string;
78
85
  export declare function safariSkippingConversion(): string;
79
86
  export declare function cdpClientFoundTargets(count: number): string;
@@ -1,5 +1,18 @@
1
1
  import type { BrowserLogger } from '../../browsers-types';
2
2
  import type { SafariPluginLike } from '../safari-types';
3
+ export declare function toolOutputTail(output: string): string;
3
4
  export type SafariPipelineMode = 'full' | 'resync';
4
5
  export declare function safariPreflightError(): string | null;
6
+ export interface SafariBuildPreflight {
7
+ severity: 'ok' | 'skip' | 'fatal';
8
+ message?: string;
9
+ }
10
+ /**
11
+ * Preflight for `build`: a non-macOS host is not an error — the web-extension
12
+ * bundle is still produced and can be packaged later on a Mac — so packaging
13
+ * is skipped with a warning. A macOS host with a broken/missing Xcode stays
14
+ * fatal because the user can act on it locally. `dev` keeps the stricter
15
+ * safariPreflightError(): a Safari dev loop without packaging is pointless.
16
+ */
17
+ export declare function safariBuildPreflight(): SafariBuildPreflight;
5
18
  export declare function packageSafariExtension(host: SafariPluginLike, outputPath: string, logger?: BrowserLogger, mode?: SafariPipelineMode): Promise<void>;
@@ -1,5 +1,10 @@
1
1
  import type { CompilationLike } from '../../browsers-types';
2
2
  import type { SafariBuildConfig, SafariPluginLike } from '../safari-types';
3
+ /**
4
+ * Apple bundle identifiers: dot-separated segments of alphanumerics and
5
+ * hyphens, each starting with a letter, at least two segments (reverse-DNS).
6
+ */
7
+ export declare function isValidBundleId(value: string): boolean;
3
8
  export declare function resolveSafariBuildConfig(compilation: CompilationLike, host: SafariPluginLike): SafariBuildConfig;
4
9
  export declare function composeConverterArgs(config: SafariBuildConfig): string[];
5
10
  export declare function macOsSchemeName(config: SafariBuildConfig): string;
@@ -15,8 +20,27 @@ export declare function builtAppPath(config: SafariBuildConfig): string;
15
20
  */
16
21
  export declare function composeXcodebuildArgs(config: SafariBuildConfig): string[];
17
22
  export declare function manifestFingerprintPath(config: SafariBuildConfig): string;
23
+ /**
24
+ * v2 fingerprint: manifest content PLUS the identity inputs baked into the
25
+ * generated Xcode project (app name, bundle id, platform). An identity change
26
+ * must re-run the converter or the project keeps shipping the old identity.
27
+ * A v1 fingerprint (raw normalized manifest, no JSON envelope) never matches
28
+ * this shape, so old projects regenerate once and migrate automatically.
29
+ */
30
+ export declare function composeProjectFingerprint(config: SafariBuildConfig): string;
18
31
  export declare function saveManifestFingerprint(config: SafariBuildConfig): void;
19
32
  export declare function isProjectStale(config: SafariBuildConfig): boolean;
33
+ /**
34
+ * The converter does not honor --bundle-identifier verbatim: it writes the
35
+ * appex id as `<identifier>.Extension` but derives the PARENT app id from the
36
+ * identifier's namespace plus the app-name segment (observed on Xcode 16/26).
37
+ * With a user-provided id whose last segment differs from the app name, the
38
+ * prefixes mismatch and ValidateEmbeddedBinary fails the build. Rewrite both
39
+ * PRODUCT_BUNDLE_IDENTIFIERs so the project always carries exactly the
40
+ * configured identity: app = <bundleId>, appex = <bundleId>.Extension.
41
+ */
42
+ export declare function alignBundleIdentifiers(pbxprojContent: string, bundleId: string): string;
43
+ export declare const PRESERVED_SETTINGS: readonly ["DEVELOPMENT_TEAM", "CODE_SIGN_STYLE", "PROVISIONING_PROFILE_SPECIFIER"];
20
44
  export declare function pbxprojPath(config: SafariBuildConfig): string;
21
45
  export declare function extractXcodeUserSettings(pbxprojContent: string): Record<string, string>;
22
46
  export declare function applyXcodeUserSettings(pbxprojContent: string, settings: Record<string, string>): string;
@@ -3,6 +3,7 @@ export type SafariPluginLike = Pick<PluginInterface, 'extension' | 'noOpen' | 'i
3
3
  browser: PluginInterface['browser'];
4
4
  safariBinary?: string;
5
5
  appName?: string;
6
+ bundleId?: string;
6
7
  macOsOnly?: boolean;
7
8
  forceRegenerate?: boolean;
8
9
  };
@@ -11,6 +12,8 @@ export interface SafariBuildConfig {
11
12
  projectLocation: string;
12
13
  appName: string;
13
14
  bundleIdentifier: string;
15
+ /** True when the bundle id was derived (dev.extensionjs.*), not user-set. */
16
+ bundleIdDerived: boolean;
14
17
  macOsOnly: boolean;
15
18
  language: 'swift' | 'objc';
16
19
  open: boolean;
@@ -36,6 +36,8 @@ export declare function noURLWithoutStart(argument: string): string;
36
36
  export declare function notImplemented(argument: string): string;
37
37
  export declare function programUserHelp(): string;
38
38
  export declare function unsupportedBrowserFlag(value: string, supported: string[]): string;
39
+ export declare function safariOnlyOption(flags: string[]): string;
40
+ export declare function safariInvalidBundleId(bundleId: string): string;
39
41
  export declare function safariCommandNotSupported(command: 'dev' | 'preview' | 'start'): string;
40
42
  export declare function programAIHelp(): string;
41
43
  export type ProgramAIHelpJSON = {
package/package.json CHANGED
@@ -38,7 +38,7 @@
38
38
  "extension": "./bin/extension.cjs"
39
39
  },
40
40
  "name": "extension",
41
- "version": "4.0.9",
41
+ "version": "4.0.10",
42
42
  "description": "The cross-browser extension framework. Build Chrome, Edge, Firefox, and Safari extensions with no build configuration.",
43
43
  "homepage": "https://extension.js.org/",
44
44
  "bugs": {
@@ -109,9 +109,9 @@
109
109
  "vivaldi-location2": "2.1.0",
110
110
  "waterfox-location": "2.1.0",
111
111
  "yandex-location": "2.1.0",
112
- "extension-create": "4.0.9",
113
- "extension-develop": "4.0.9",
114
- "extension-install": "4.0.9",
112
+ "extension-create": "4.0.10",
113
+ "extension-develop": "4.0.10",
114
+ "extension-install": "4.0.10",
115
115
  "commander": "^15.0.0",
116
116
  "pintor": "0.3.0",
117
117
  "semver": "^7.7.3",