web-ext 7.12.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +24 -16
  2. package/bin/web-ext.js +1 -1
  3. package/lib/cmd/build.js +1 -3
  4. package/lib/cmd/build.js.map +1 -1
  5. package/lib/cmd/docs.js +1 -0
  6. package/lib/cmd/docs.js.map +1 -1
  7. package/lib/cmd/dump-config.js +6 -0
  8. package/lib/cmd/dump-config.js.map +1 -0
  9. package/lib/cmd/index.js +16 -8
  10. package/lib/cmd/index.js.map +1 -1
  11. package/lib/cmd/lint.js +0 -8
  12. package/lib/cmd/lint.js.map +1 -1
  13. package/lib/cmd/run.js +0 -7
  14. package/lib/cmd/run.js.map +1 -1
  15. package/lib/cmd/sign.js +29 -85
  16. package/lib/cmd/sign.js.map +1 -1
  17. package/lib/config.js +53 -7
  18. package/lib/config.js.map +1 -1
  19. package/lib/errors.js.map +1 -1
  20. package/lib/extension-runners/chromium.js +11 -3
  21. package/lib/extension-runners/chromium.js.map +1 -1
  22. package/lib/extension-runners/firefox-android.js +15 -3
  23. package/lib/extension-runners/firefox-android.js.map +1 -1
  24. package/lib/extension-runners/firefox-desktop.js +7 -8
  25. package/lib/extension-runners/firefox-desktop.js.map +1 -1
  26. package/lib/extension-runners/index.js +2 -2
  27. package/lib/extension-runners/index.js.map +1 -1
  28. package/lib/firefox/index.js +1 -8
  29. package/lib/firefox/index.js.map +1 -1
  30. package/lib/firefox/package-identifiers.js +3 -1
  31. package/lib/firefox/package-identifiers.js.map +1 -1
  32. package/lib/firefox/preferences.js +0 -2
  33. package/lib/firefox/preferences.js.map +1 -1
  34. package/lib/firefox/rdp-client.js +10 -2
  35. package/lib/firefox/rdp-client.js.map +1 -1
  36. package/lib/firefox/remote.js +3 -2
  37. package/lib/firefox/remote.js.map +1 -1
  38. package/lib/main.js.map +1 -1
  39. package/lib/program.js +25 -48
  40. package/lib/program.js.map +1 -1
  41. package/lib/util/adb.js +15 -9
  42. package/lib/util/adb.js.map +1 -1
  43. package/lib/util/artifacts.js.map +1 -1
  44. package/lib/util/desktop-notifier.js.map +1 -1
  45. package/lib/util/file-exists.js +1 -0
  46. package/lib/util/file-exists.js.map +1 -1
  47. package/lib/util/file-filter.js +2 -0
  48. package/lib/util/file-filter.js.map +1 -1
  49. package/lib/util/is-directory.js.map +1 -1
  50. package/lib/util/logger.js +5 -5
  51. package/lib/util/logger.js.map +1 -1
  52. package/lib/util/manifest.js +1 -1
  53. package/lib/util/manifest.js.map +1 -1
  54. package/lib/util/promisify.js +0 -7
  55. package/lib/util/promisify.js.map +1 -1
  56. package/lib/util/stdin.js +0 -2
  57. package/lib/util/stdin.js.map +1 -1
  58. package/lib/util/submit-addon.js +214 -50
  59. package/lib/util/submit-addon.js.map +1 -1
  60. package/lib/util/temp-dir.js +2 -0
  61. package/lib/util/temp-dir.js.map +1 -1
  62. package/lib/util/updates.js.map +1 -1
  63. package/lib/watcher.js.map +1 -1
  64. package/package.json +38 -40
  65. package/lib/extension-runners/base.js +0 -2
  66. package/lib/extension-runners/base.js.map +0 -1
@@ -51,7 +51,7 @@ export default async function getValidatedManifest(sourceDir) {
51
51
  export function getManifestId(manifestData) {
52
52
  const manifestApps = [manifestData.browser_specific_settings, manifestData.applications];
53
53
  for (const apps of manifestApps) {
54
- // If both bss and applicants contains a defined gecko property,
54
+ // If both bss and applications contain a defined gecko property,
55
55
  // we prefer bss even if the id property isn't available.
56
56
  // This match what Firefox does in this particular scenario, see
57
57
  // https://searchfox.org/mozilla-central/rev/828f2319c0195d7f561ed35533aef6fe183e68e3/toolkit/mozapps/extensions/internal/XPIInstall.jsm#470-474,488
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.js","names":["path","fs","parseJSON","stripBom","stripJsonComments","InvalidManifest","createLogger","log","import","meta","url","getValidatedManifest","sourceDir","manifestFile","join","debug","manifestContents","readFile","encoding","error","manifestData","errors","name","push","version","applications","gecko","length","getManifestId","manifestApps","browser_specific_settings","apps","id","undefined"],"sources":["../../src/util/manifest.js"],"sourcesContent":["/* @flow */\nimport path from 'path';\n\nimport { fs } from 'mz';\nimport parseJSON from 'parse-json';\nimport stripBom from 'strip-bom';\nimport stripJsonComments from 'strip-json-comments';\n\nimport { InvalidManifest } from '../errors.js';\nimport { createLogger } from './logger.js';\n\nconst log = createLogger(import.meta.url);\n\n// getValidatedManifest helper types and implementation\n\nexport type ExtensionManifestApplications = {|\n gecko?: {|\n id?: string,\n strict_min_version?: string,\n strict_max_version?: string,\n update_url?: string,\n |},\n|};\n\nexport type ExtensionManifest = {|\n name: string,\n version: string,\n default_locale?: string,\n applications?: ExtensionManifestApplications,\n browser_specific_settings?: ExtensionManifestApplications,\n permissions?: Array<string>,\n|};\n\nexport default async function getValidatedManifest(\n sourceDir: string\n): Promise<ExtensionManifest> {\n const manifestFile = path.join(sourceDir, 'manifest.json');\n log.debug(`Validating manifest at ${manifestFile}`);\n\n let manifestContents;\n\n try {\n manifestContents = await fs.readFile(manifestFile, { encoding: 'utf-8' });\n } catch (error) {\n throw new InvalidManifest(\n `Could not read manifest.json file at ${manifestFile}: ${error}`\n );\n }\n\n manifestContents = stripBom(manifestContents);\n\n let manifestData;\n\n try {\n manifestData = parseJSON(stripJsonComments(manifestContents));\n } catch (error) {\n throw new InvalidManifest(\n `Error parsing manifest.json file at ${manifestFile}: ${error}`\n );\n }\n\n const errors = [];\n // This is just some basic validation of what web-ext needs, not\n // what Firefox will need to run the extension.\n // TODO: integrate with the addons-linter for actual validation.\n if (!manifestData.name) {\n errors.push('missing \"name\" property');\n }\n if (!manifestData.version) {\n errors.push('missing \"version\" property');\n }\n\n if (manifestData.applications && !manifestData.applications.gecko) {\n // Since the applications property only applies to gecko, make\n // sure 'gecko' exists when 'applications' is defined. This should\n // make introspection of gecko properties easier.\n errors.push('missing \"applications.gecko\" property');\n }\n\n if (errors.length) {\n throw new InvalidManifest(\n `Manifest at ${manifestFile} is invalid: ${errors.join('; ')}`\n );\n }\n\n return manifestData;\n}\n\nexport function getManifestId(manifestData: ExtensionManifest): string | void {\n const manifestApps = [\n manifestData.browser_specific_settings,\n manifestData.applications,\n ];\n for (const apps of manifestApps) {\n // If both bss and applicants contains a defined gecko property,\n // we prefer bss even if the id property isn't available.\n // This match what Firefox does in this particular scenario, see\n // https://searchfox.org/mozilla-central/rev/828f2319c0195d7f561ed35533aef6fe183e68e3/toolkit/mozapps/extensions/internal/XPIInstall.jsm#470-474,488\n if (apps?.gecko) {\n return apps.gecko.id;\n }\n }\n\n return undefined;\n}\n"],"mappings":"AACA,OAAOA,IAAI,MAAM,MAAM;AAEvB,SAASC,EAAE,QAAQ,IAAI;AACvB,OAAOC,SAAS,MAAM,YAAY;AAClC,OAAOC,QAAQ,MAAM,WAAW;AAChC,OAAOC,iBAAiB,MAAM,qBAAqB;AAEnD,SAASC,eAAe,QAAQ,cAAc;AAC9C,SAASC,YAAY,QAAQ,aAAa;AAE1C,MAAMC,GAAG,GAAGD,YAAY,CAACE,MAAM,CAACC,IAAI,CAACC,GAAG,CAAC;;AAEzC;;AAoBA,eAAe,eAAeC,oBAAoBA,CAChDC,SAAiB,EACW;EAC5B,MAAMC,YAAY,GAAGb,IAAI,CAACc,IAAI,CAACF,SAAS,EAAE,eAAe,CAAC;EAC1DL,GAAG,CAACQ,KAAK,CAAE,0BAAyBF,YAAa,EAAC,CAAC;EAEnD,IAAIG,gBAAgB;EAEpB,IAAI;IACFA,gBAAgB,GAAG,MAAMf,EAAE,CAACgB,QAAQ,CAACJ,YAAY,EAAE;MAAEK,QAAQ,EAAE;IAAQ,CAAC,CAAC;EAC3E,CAAC,CAAC,OAAOC,KAAK,EAAE;IACd,MAAM,IAAId,eAAe,CACtB,wCAAuCQ,YAAa,KAAIM,KAAM,EACjE,CAAC;EACH;EAEAH,gBAAgB,GAAGb,QAAQ,CAACa,gBAAgB,CAAC;EAE7C,IAAII,YAAY;EAEhB,IAAI;IACFA,YAAY,GAAGlB,SAAS,CAACE,iBAAiB,CAACY,gBAAgB,CAAC,CAAC;EAC/D,CAAC,CAAC,OAAOG,KAAK,EAAE;IACd,MAAM,IAAId,eAAe,CACtB,uCAAsCQ,YAAa,KAAIM,KAAM,EAChE,CAAC;EACH;EAEA,MAAME,MAAM,GAAG,EAAE;EACjB;EACA;EACA;EACA,IAAI,CAACD,YAAY,CAACE,IAAI,EAAE;IACtBD,MAAM,CAACE,IAAI,CAAC,yBAAyB,CAAC;EACxC;EACA,IAAI,CAACH,YAAY,CAACI,OAAO,EAAE;IACzBH,MAAM,CAACE,IAAI,CAAC,4BAA4B,CAAC;EAC3C;EAEA,IAAIH,YAAY,CAACK,YAAY,IAAI,CAACL,YAAY,CAACK,YAAY,CAACC,KAAK,EAAE;IACjE;IACA;IACA;IACAL,MAAM,CAACE,IAAI,CAAC,uCAAuC,CAAC;EACtD;EAEA,IAAIF,MAAM,CAACM,MAAM,EAAE;IACjB,MAAM,IAAItB,eAAe,CACtB,eAAcQ,YAAa,gBAAeQ,MAAM,CAACP,IAAI,CAAC,IAAI,CAAE,EAC/D,CAAC;EACH;EAEA,OAAOM,YAAY;AACrB;AAEA,OAAO,SAASQ,aAAaA,CAACR,YAA+B,EAAiB;EAC5E,MAAMS,YAAY,GAAG,CACnBT,YAAY,CAACU,yBAAyB,EACtCV,YAAY,CAACK,YAAY,CAC1B;EACD,KAAK,MAAMM,IAAI,IAAIF,YAAY,EAAE;IAC/B;IACA;IACA;IACA;IACA,IAAIE,IAAI,aAAJA,IAAI,eAAJA,IAAI,CAAEL,KAAK,EAAE;MACf,OAAOK,IAAI,CAACL,KAAK,CAACM,EAAE;IACtB;EACF;EAEA,OAAOC,SAAS;AAClB"}
1
+ {"version":3,"file":"manifest.js","names":["path","fs","parseJSON","stripBom","stripJsonComments","InvalidManifest","createLogger","log","import","meta","url","getValidatedManifest","sourceDir","manifestFile","join","debug","manifestContents","readFile","encoding","error","manifestData","errors","name","push","version","applications","gecko","length","getManifestId","manifestApps","browser_specific_settings","apps","id","undefined"],"sources":["../../src/util/manifest.js"],"sourcesContent":["import path from 'path';\n\nimport { fs } from 'mz';\nimport parseJSON from 'parse-json';\nimport stripBom from 'strip-bom';\nimport stripJsonComments from 'strip-json-comments';\n\nimport { InvalidManifest } from '../errors.js';\nimport { createLogger } from './logger.js';\n\nconst log = createLogger(import.meta.url);\n\n// getValidatedManifest helper types and implementation\n\nexport default async function getValidatedManifest(sourceDir) {\n const manifestFile = path.join(sourceDir, 'manifest.json');\n log.debug(`Validating manifest at ${manifestFile}`);\n\n let manifestContents;\n\n try {\n manifestContents = await fs.readFile(manifestFile, { encoding: 'utf-8' });\n } catch (error) {\n throw new InvalidManifest(\n `Could not read manifest.json file at ${manifestFile}: ${error}`,\n );\n }\n\n manifestContents = stripBom(manifestContents);\n\n let manifestData;\n\n try {\n manifestData = parseJSON(stripJsonComments(manifestContents));\n } catch (error) {\n throw new InvalidManifest(\n `Error parsing manifest.json file at ${manifestFile}: ${error}`,\n );\n }\n\n const errors = [];\n // This is just some basic validation of what web-ext needs, not\n // what Firefox will need to run the extension.\n // TODO: integrate with the addons-linter for actual validation.\n if (!manifestData.name) {\n errors.push('missing \"name\" property');\n }\n if (!manifestData.version) {\n errors.push('missing \"version\" property');\n }\n\n if (manifestData.applications && !manifestData.applications.gecko) {\n // Since the applications property only applies to gecko, make\n // sure 'gecko' exists when 'applications' is defined. This should\n // make introspection of gecko properties easier.\n errors.push('missing \"applications.gecko\" property');\n }\n\n if (errors.length) {\n throw new InvalidManifest(\n `Manifest at ${manifestFile} is invalid: ${errors.join('; ')}`,\n );\n }\n\n return manifestData;\n}\n\nexport function getManifestId(manifestData) {\n const manifestApps = [\n manifestData.browser_specific_settings,\n manifestData.applications,\n ];\n for (const apps of manifestApps) {\n // If both bss and applications contain a defined gecko property,\n // we prefer bss even if the id property isn't available.\n // This match what Firefox does in this particular scenario, see\n // https://searchfox.org/mozilla-central/rev/828f2319c0195d7f561ed35533aef6fe183e68e3/toolkit/mozapps/extensions/internal/XPIInstall.jsm#470-474,488\n if (apps?.gecko) {\n return apps.gecko.id;\n }\n }\n\n return undefined;\n}\n"],"mappings":"AAAA,OAAOA,IAAI,MAAM,MAAM;AAEvB,SAASC,EAAE,QAAQ,IAAI;AACvB,OAAOC,SAAS,MAAM,YAAY;AAClC,OAAOC,QAAQ,MAAM,WAAW;AAChC,OAAOC,iBAAiB,MAAM,qBAAqB;AAEnD,SAASC,eAAe,QAAQ,cAAc;AAC9C,SAASC,YAAY,QAAQ,aAAa;AAE1C,MAAMC,GAAG,GAAGD,YAAY,CAACE,MAAM,CAACC,IAAI,CAACC,GAAG,CAAC;;AAEzC;;AAEA,eAAe,eAAeC,oBAAoBA,CAACC,SAAS,EAAE;EAC5D,MAAMC,YAAY,GAAGb,IAAI,CAACc,IAAI,CAACF,SAAS,EAAE,eAAe,CAAC;EAC1DL,GAAG,CAACQ,KAAK,CAAC,0BAA0BF,YAAY,EAAE,CAAC;EAEnD,IAAIG,gBAAgB;EAEpB,IAAI;IACFA,gBAAgB,GAAG,MAAMf,EAAE,CAACgB,QAAQ,CAACJ,YAAY,EAAE;MAAEK,QAAQ,EAAE;IAAQ,CAAC,CAAC;EAC3E,CAAC,CAAC,OAAOC,KAAK,EAAE;IACd,MAAM,IAAId,eAAe,CACvB,wCAAwCQ,YAAY,KAAKM,KAAK,EAChE,CAAC;EACH;EAEAH,gBAAgB,GAAGb,QAAQ,CAACa,gBAAgB,CAAC;EAE7C,IAAII,YAAY;EAEhB,IAAI;IACFA,YAAY,GAAGlB,SAAS,CAACE,iBAAiB,CAACY,gBAAgB,CAAC,CAAC;EAC/D,CAAC,CAAC,OAAOG,KAAK,EAAE;IACd,MAAM,IAAId,eAAe,CACvB,uCAAuCQ,YAAY,KAAKM,KAAK,EAC/D,CAAC;EACH;EAEA,MAAME,MAAM,GAAG,EAAE;EACjB;EACA;EACA;EACA,IAAI,CAACD,YAAY,CAACE,IAAI,EAAE;IACtBD,MAAM,CAACE,IAAI,CAAC,yBAAyB,CAAC;EACxC;EACA,IAAI,CAACH,YAAY,CAACI,OAAO,EAAE;IACzBH,MAAM,CAACE,IAAI,CAAC,4BAA4B,CAAC;EAC3C;EAEA,IAAIH,YAAY,CAACK,YAAY,IAAI,CAACL,YAAY,CAACK,YAAY,CAACC,KAAK,EAAE;IACjE;IACA;IACA;IACAL,MAAM,CAACE,IAAI,CAAC,uCAAuC,CAAC;EACtD;EAEA,IAAIF,MAAM,CAACM,MAAM,EAAE;IACjB,MAAM,IAAItB,eAAe,CACvB,eAAeQ,YAAY,gBAAgBQ,MAAM,CAACP,IAAI,CAAC,IAAI,CAAC,EAC9D,CAAC;EACH;EAEA,OAAOM,YAAY;AACrB;AAEA,OAAO,SAASQ,aAAaA,CAACR,YAAY,EAAE;EAC1C,MAAMS,YAAY,GAAG,CACnBT,YAAY,CAACU,yBAAyB,EACtCV,YAAY,CAACK,YAAY,CAC1B;EACD,KAAK,MAAMM,IAAI,IAAIF,YAAY,EAAE;IAC/B;IACA;IACA;IACA;IACA,IAAIE,IAAI,aAAJA,IAAI,eAAJA,IAAI,CAAEL,KAAK,EAAE;MACf,OAAOK,IAAI,CAACL,KAAK,CAACM,EAAE;IACtB;EACF;EAEA,OAAOC,SAAS;AAClB","ignoreList":[]}
@@ -1,11 +1,4 @@
1
1
  import { promisify } from 'util';
2
-
3
- // promisify.custom is missing from the node types know to flow,
4
- // and it triggers flow-check errors if used directly.
5
- // By using the value exported here, flow-check passes successfully
6
- // using a single FLOW_IGNORE suppress comment.
7
-
8
- // $FlowIgnore: promisify.custom is missing in flow type signatures.
9
2
  export const promisifyCustom = promisify.custom;
10
3
 
11
4
  /*
@@ -1 +1 @@
1
- {"version":3,"file":"promisify.js","names":["promisify","promisifyCustom","custom","multiArgsPromisedFn","fn","callerArgs","Promise","resolve","reject","err","rest"],"sources":["../../src/util/promisify.js"],"sourcesContent":["/* @flow */\n\nimport { promisify } from 'util';\n\n// promisify.custom is missing from the node types know to flow,\n// and it triggers flow-check errors if used directly.\n// By using the value exported here, flow-check passes successfully\n// using a single FLOW_IGNORE suppress comment.\n\n// $FlowIgnore: promisify.custom is missing in flow type signatures.\nexport const promisifyCustom = promisify.custom;\n\n/*\n * A small promisify helper to make it easier to customize a\n * function promisified (using the 'util' module available in\n * nodejs >= 8) to resolve to an array of results:\n *\n * import {promisify} from 'util';\n * import {multiArgsPromisedFn} from '../util/promisify';\n *\n * aCallbackBasedFn[promisify.custom] = multiArgsPromisedFn(tmp.dir);\n * ...\n */\nexport function multiArgsPromisedFn(fn: Function): Function {\n return (...callerArgs: Array<any>): Promise<any> => {\n return new Promise((resolve, reject) => {\n fn(...callerArgs, (err, ...rest) => {\n if (err) {\n reject(err);\n } else {\n resolve(rest);\n }\n });\n });\n };\n}\n"],"mappings":"AAEA,SAASA,SAAS,QAAQ,MAAM;;AAEhC;AACA;AACA;AACA;;AAEA;AACA,OAAO,MAAMC,eAAe,GAAGD,SAAS,CAACE,MAAM;;AAE/C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,mBAAmBA,CAACC,EAAY,EAAY;EAC1D,OAAO,CAAC,GAAGC,UAAsB,KAAmB;IAClD,OAAO,IAAIC,OAAO,CAAC,CAACC,OAAO,EAAEC,MAAM,KAAK;MACtCJ,EAAE,CAAC,GAAGC,UAAU,EAAE,CAACI,GAAG,EAAE,GAAGC,IAAI,KAAK;QAClC,IAAID,GAAG,EAAE;UACPD,MAAM,CAACC,GAAG,CAAC;QACb,CAAC,MAAM;UACLF,OAAO,CAACG,IAAI,CAAC;QACf;MACF,CAAC,CAAC;IACJ,CAAC,CAAC;EACJ,CAAC;AACH"}
1
+ {"version":3,"file":"promisify.js","names":["promisify","promisifyCustom","custom","multiArgsPromisedFn","fn","callerArgs","Promise","resolve","reject","err","rest"],"sources":["../../src/util/promisify.js"],"sourcesContent":["import { promisify } from 'util';\n\nexport const promisifyCustom = promisify.custom;\n\n/*\n * A small promisify helper to make it easier to customize a\n * function promisified (using the 'util' module available in\n * nodejs >= 8) to resolve to an array of results:\n *\n * import {promisify} from 'util';\n * import {multiArgsPromisedFn} from '../util/promisify';\n *\n * aCallbackBasedFn[promisify.custom] = multiArgsPromisedFn(tmp.dir);\n * ...\n */\nexport function multiArgsPromisedFn(fn) {\n return (...callerArgs) => {\n return new Promise((resolve, reject) => {\n fn(...callerArgs, (err, ...rest) => {\n if (err) {\n reject(err);\n } else {\n resolve(rest);\n }\n });\n });\n };\n}\n"],"mappings":"AAAA,SAASA,SAAS,QAAQ,MAAM;AAEhC,OAAO,MAAMC,eAAe,GAAGD,SAAS,CAACE,MAAM;;AAE/C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,mBAAmBA,CAACC,EAAE,EAAE;EACtC,OAAO,CAAC,GAAGC,UAAU,KAAK;IACxB,OAAO,IAAIC,OAAO,CAAC,CAACC,OAAO,EAAEC,MAAM,KAAK;MACtCJ,EAAE,CAAC,GAAGC,UAAU,EAAE,CAACI,GAAG,EAAE,GAAGC,IAAI,KAAK;QAClC,IAAID,GAAG,EAAE;UACPD,MAAM,CAACC,GAAG,CAAC;QACb,CAAC,MAAM;UACLF,OAAO,CAACG,IAAI,CAAC;QACf;MACF,CAAC,CAAC;IACJ,CAAC,CAAC;EACJ,CAAC;AACH","ignoreList":[]}
package/lib/util/stdin.js CHANGED
@@ -1,9 +1,7 @@
1
1
  export function isTTY(stream) {
2
- // $FlowFixMe: flow complains that stream may not provide isTTY as a property.
3
2
  return stream.isTTY;
4
3
  }
5
4
  export function setRawMode(stream, rawMode) {
6
- // $FlowFixMe: flow complains that stdin may not provide setRawMode.
7
5
  stream.setRawMode(rawMode);
8
6
  }
9
7
  //# sourceMappingURL=stdin.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"stdin.js","names":["isTTY","stream","setRawMode","rawMode"],"sources":["../../src/util/stdin.js"],"sourcesContent":["/* @flow */\n\nimport type { Readable } from 'stream';\n\nexport function isTTY(stream: Readable): boolean {\n // $FlowFixMe: flow complains that stream may not provide isTTY as a property.\n return stream.isTTY;\n}\n\nexport function setRawMode(stream: Readable, rawMode: boolean) {\n // $FlowFixMe: flow complains that stdin may not provide setRawMode.\n stream.setRawMode(rawMode);\n}\n"],"mappings":"AAIA,OAAO,SAASA,KAAKA,CAACC,MAAgB,EAAW;EAC/C;EACA,OAAOA,MAAM,CAACD,KAAK;AACrB;AAEA,OAAO,SAASE,UAAUA,CAACD,MAAgB,EAAEE,OAAgB,EAAE;EAC7D;EACAF,MAAM,CAACC,UAAU,CAACC,OAAO,CAAC;AAC5B"}
1
+ {"version":3,"file":"stdin.js","names":["isTTY","stream","setRawMode","rawMode"],"sources":["../../src/util/stdin.js"],"sourcesContent":["export function isTTY(stream) {\n return stream.isTTY;\n}\n\nexport function setRawMode(stream, rawMode) {\n stream.setRawMode(rawMode);\n}\n"],"mappings":"AAAA,OAAO,SAASA,KAAKA,CAACC,MAAM,EAAE;EAC5B,OAAOA,MAAM,CAACD,KAAK;AACrB;AAEA,OAAO,SAASE,UAAUA,CAACD,MAAM,EAAEE,OAAO,EAAE;EAC1CF,MAAM,CAACC,UAAU,CAACC,OAAO,CAAC;AAC5B","ignoreList":[]}
@@ -1,12 +1,16 @@
1
+ import { createHash } from 'crypto';
1
2
  import { createWriteStream, promises as fsPromises } from 'fs';
2
- import { pipeline } from 'stream';
3
- import { promisify } from 'util';
3
+ import { promises as streamPromises } from 'stream';
4
4
 
5
5
  // eslint-disable-next-line no-shadow
6
- import fetch, { FormData, fileFromSync, Response } from 'node-fetch';
6
+ import fetch, { FormData, fileFromSync } from 'node-fetch';
7
7
  import { SignJWT } from 'jose';
8
+ import JSZip from 'jszip';
9
+ import { HttpsProxyAgent } from 'https-proxy-agent';
10
+ import { isErrorWithCode } from '../errors.js';
8
11
  import { createLogger } from './../util/logger.js';
9
12
  const log = createLogger(import.meta.url);
13
+ export const defaultAsyncFsReadFile = fsPromises.readFile;
10
14
  export class JwtApiAuth {
11
15
  #apiKey;
12
16
  #apiSecret;
@@ -37,8 +41,18 @@ export class JwtApiAuth {
37
41
  }
38
42
  }
39
43
  export default class Client {
44
+ apiAuth;
45
+ apiProxy;
46
+ apiUr;
47
+ validationCheckInterval;
48
+ validationCheckTimeout;
49
+ approvalCheckInterval;
50
+ approvalCheckTimeout;
51
+ downloadDir;
52
+ userAgentString;
40
53
  constructor({
41
54
  apiAuth,
55
+ apiProxy,
42
56
  baseUrl,
43
57
  validationCheckInterval = 1000,
44
58
  validationCheckTimeout = 300000,
@@ -50,6 +64,9 @@ export default class Client {
50
64
  userAgentString
51
65
  }) {
52
66
  this.apiAuth = apiAuth;
67
+ if (apiProxy) {
68
+ this.apiProxy = apiProxy;
69
+ }
53
70
  if (!baseUrl.pathname.endsWith('/')) {
54
71
  baseUrl = new URL(baseUrl.href);
55
72
  baseUrl.pathname += '/';
@@ -68,12 +85,14 @@ export default class Client {
68
85
  nodeFetch(url, {
69
86
  method,
70
87
  headers,
71
- body
88
+ body,
89
+ agent
72
90
  }) {
73
91
  return fetch(url, {
74
92
  method,
75
93
  headers,
76
- body
94
+ body,
95
+ agent
77
96
  });
78
97
  }
79
98
  async doUploadSubmit(xpiPath, channel) {
@@ -83,19 +102,23 @@ export default class Client {
83
102
  formData.set('upload', this.fileFromSync(xpiPath));
84
103
  const {
85
104
  uuid
86
- } = await this.fetchJson(url, 'POST', formData);
105
+ } = await this.fetchJson(url, 'POST', formData, 'Upload failed');
87
106
  return this.waitForValidation(uuid);
88
107
  }
89
- waitRetry(successFunc, checkUrl, checkInterval, abortInterval, context) {
108
+ waitRetry(successFunc, checkUrl, checkInterval, abortInterval, context, editUrl = null) {
90
109
  let checkTimeout;
91
110
  return new Promise((resolve, reject) => {
92
111
  const abortTimeout = setTimeout(() => {
93
112
  clearTimeout(checkTimeout);
94
- reject(new Error(`${context}: timeout.`));
113
+ let errorMessage = `${context}: timeout exceeded.`;
114
+ if (editUrl) {
115
+ errorMessage += ` When approved the signed XPI file can be downloaded from ${editUrl}`;
116
+ }
117
+ reject(new Error(errorMessage));
95
118
  }, abortInterval);
96
119
  const pollStatus = async () => {
97
120
  try {
98
- const responseData = await this.fetchJson(checkUrl, 'GET', undefined, 'Getting details failed.');
121
+ const responseData = await this.fetchJson(checkUrl, 'GET', undefined, 'Getting details failed');
99
122
  const success = successFunc(responseData);
100
123
  if (success) {
101
124
  clearTimeout(abortTimeout);
@@ -113,17 +136,16 @@ export default class Client {
113
136
  });
114
137
  }
115
138
  waitForValidation(uuid) {
116
- log.info('Waiting for Validation...');
139
+ log.info('Waiting for validation...');
117
140
  return this.waitRetry(detailResponseData => {
118
141
  if (!detailResponseData.processed) {
119
142
  return null;
120
143
  }
121
- log.info('Validation results:', detailResponseData.validation);
144
+ log.debug('Validation results:', detailResponseData.validation);
122
145
  if (detailResponseData.valid) {
123
146
  return detailResponseData.uuid;
124
147
  }
125
- log.info('Validation failed.');
126
- throw new Error('Validation failed, open the following URL for more information: ' + `${detailResponseData.url}`);
148
+ throw new Error(['Validation failed:\n', JSON.stringify(detailResponseData.validation, null, 2)].join(''));
127
149
  }, new URL(`upload/${uuid}/`, this.apiUrl), this.validationCheckInterval, this.validationCheckTimeout, 'Validation');
128
150
  }
129
151
  async doNewAddonSubmit(uuid, metaDataJson) {
@@ -135,7 +157,7 @@ export default class Client {
135
157
  ...metaDataJson.version
136
158
  }
137
159
  };
138
- return this.fetchJson(url, 'POST', JSON.stringify(jsonData));
160
+ return this.fetchJson(url, 'POST', JSON.stringify(jsonData), 'Submission failed (1)');
139
161
  }
140
162
  doNewAddonOrVersionSubmit(addonId, uuid, metaDataJson) {
141
163
  const url = new URL(`addon/${addonId}/`, this.apiUrl);
@@ -146,10 +168,38 @@ export default class Client {
146
168
  ...metaDataJson.version
147
169
  }
148
170
  };
149
- return this.fetch(url, 'PUT', JSON.stringify(jsonData));
171
+ return this.fetchJson(url, 'PUT', JSON.stringify(jsonData), 'Submission failed (2)');
150
172
  }
151
- waitForApproval(addonId, versionId) {
152
- log.info('Waiting for Approval...');
173
+ async doFormDataPatch(data, addonId, versionId) {
174
+ const patchUrl = new URL(`addon/${addonId}/versions/${versionId}/`, this.apiUrl);
175
+ try {
176
+ const formData = new FormData();
177
+ for (const field in data) {
178
+ formData.set(field, data[field]);
179
+ }
180
+ const response = await this.fetch(patchUrl, 'PATCH', formData);
181
+ if (!response.ok) {
182
+ throw new Error(`response status was ${response.status}`);
183
+ }
184
+ } catch (error) {
185
+ log.warn(`Upload of ${Object.keys(data)} failed: ${error}.`);
186
+ throw new Error(`Uploading ${Object.keys(data)} failed`);
187
+ }
188
+ }
189
+ async doAfterSubmit(addonId, newVersionId, editUrl, patchData) {
190
+ if (patchData && patchData.version) {
191
+ log.info(`Submitting ${Object.keys(patchData.version)} to version`);
192
+ await this.doFormDataPatch(patchData.version, addonId, newVersionId);
193
+ }
194
+ if (this.approvalCheckTimeout === 0) {
195
+ log.info(['Waiting for approval and download of signed XPI skipped.', `When approved the signed XPI file can be downloaded from ${editUrl}`].join(' '));
196
+ return this.returnResult(addonId);
197
+ }
198
+ const fileUrl = new URL(await this.waitForApproval(addonId, newVersionId, editUrl));
199
+ return this.downloadSignedFile(fileUrl, addonId);
200
+ }
201
+ waitForApproval(addonId, versionId, editUrl) {
202
+ log.info('Waiting for approval...');
153
203
  return this.waitRetry(detailResponseData => {
154
204
  const {
155
205
  file
@@ -158,7 +208,7 @@ export default class Client {
158
208
  return file.url;
159
209
  }
160
210
  return null;
161
- }, new URL(`addon/${addonId}/versions/${versionId}/`, this.apiUrl), this.approvalCheckInterval, this.approvalCheckTimeout, 'Approval');
211
+ }, new URL(`addon/${addonId}/versions/${versionId}/`, this.apiUrl), this.approvalCheckInterval, this.approvalCheckTimeout, 'Approval', editUrl);
162
212
  }
163
213
  async fetchJson(url, method = 'GET', body, errorMsg = 'Bad Request') {
164
214
  const response = await this.fetch(url, method, body);
@@ -167,13 +217,12 @@ export default class Client {
167
217
  }
168
218
  const data = await response.json();
169
219
  if (!response.ok) {
170
- log.info('Server Response:', data);
171
- throw new Error(`${errorMsg}: ${response.statusText || response.status}.`);
220
+ throw new Error([`${errorMsg}: ${response.statusText || response.status}`, JSON.stringify(data, null, 2)].join('\n'));
172
221
  }
173
222
  return data;
174
223
  }
175
224
  async fetch(url, method = 'GET', body) {
176
- log.info(`Fetching URL: ${url.href}`);
225
+ log.debug(`${method}ing URL: ${url.href}`);
177
226
  let headers = {
178
227
  Authorization: await this.apiAuth.getAuthHeader(),
179
228
  Accept: 'application/json',
@@ -185,12 +234,23 @@ export default class Client {
185
234
  'Content-Type': 'application/json'
186
235
  };
187
236
  }
237
+ let agent;
238
+ if (this.apiProxy) {
239
+ agent = new HttpsProxyAgent(this.apiProxy);
240
+ }
188
241
  return this.nodeFetch(url, {
189
242
  method,
190
243
  body,
191
- headers
244
+ headers,
245
+ agent
192
246
  });
193
247
  }
248
+ returnResult(addonId, downloadedFiles) {
249
+ return {
250
+ id: addonId,
251
+ downloadedFiles
252
+ };
253
+ }
194
254
  async downloadSignedFile(fileUrl, addonId) {
195
255
  const filename = fileUrl.pathname.split('/').pop(); // get the name from fileUrl
196
256
  const dest = `${this.downloadDir}/${filename}`;
@@ -204,54 +264,102 @@ export default class Client {
204
264
  log.info(`Download of signed xpi failed: ${error}.`);
205
265
  throw new Error(`Downloading ${filename} failed`);
206
266
  }
207
- return {
208
- id: addonId,
209
- downloadedFiles: [filename]
210
- };
267
+ log.info(`Signed xpi downloaded: ${dest}`);
268
+ return this.returnResult(addonId, [filename]);
211
269
  }
212
270
  async saveToFile(contents, destPath) {
213
- return promisify(pipeline)(contents, createWriteStream(destPath));
271
+ return streamPromises.pipeline(contents, createWriteStream(destPath));
272
+ }
273
+
274
+ /*
275
+ This function aims to quickly hash the contents of the zip file that's being uploaded,
276
+ to compare it to the previous zip file that was uploaded, so we can skip the upload for
277
+ efficiency.
278
+ CRCs are used from the zip to avoid having to extract and hash all the files.
279
+ Two zips that have different byte contents in their files must have a different hash;
280
+ but returning a different hash when the contents are the same in some cases is acceptable
281
+ - a false mismatch does not result in lost data.
282
+ */
283
+ async hashXpiCrcs(filePath, asyncFsReadFile = defaultAsyncFsReadFile) {
284
+ const zip = await JSZip.loadAsync(asyncFsReadFile(filePath, {
285
+ createFolders: true
286
+ }));
287
+ const hash = createHash('sha256');
288
+ const entries = [];
289
+ zip.forEach((relativePath, entry) => {
290
+ var _entry$_data;
291
+ let path = relativePath.replace(/\/+$/, '');
292
+ if (entry.dir) {
293
+ path += '/';
294
+ }
295
+ // if the file is 0 bytes or a dir `_data` is missing so assume crc is 0
296
+ entries.push({
297
+ path,
298
+ crc32: ((_entry$_data = entry._data) === null || _entry$_data === void 0 ? void 0 : _entry$_data.crc32) || 0
299
+ });
300
+ });
301
+ entries.sort((a, b) => a.path === b.path ? 0 : a.path > b.path ? 1 : -1);
302
+ hash.update(JSON.stringify(entries));
303
+ return hash.digest('hex');
214
304
  }
215
- async postNewAddon(xpiPath, channel, savedIdPath, metaDataJson, saveIdToFileFunc = saveIdToFile) {
216
- const uploadUuid = await this.doUploadSubmit(xpiPath, channel);
217
- const versionObject = channel === 'listed' ? 'current_version' : 'latest_unlisted_version';
305
+ async getPreviousUuidOrUploadXpi(xpiPath, channel, savedUploadUuidPath, saveUploadUuidToFileFunc = saveUploadUuidToFile, getUploadUuidFromFileFunc = getUploadUuidFromFile) {
306
+ const [{
307
+ uploadUuid: previousUuid,
308
+ channel: previousChannel,
309
+ xpiCrcHash: previousHash
310
+ }, xpiCrcHash] = await Promise.all([getUploadUuidFromFileFunc(savedUploadUuidPath), this.hashXpiCrcs(xpiPath)]);
311
+ let uploadUuid;
312
+ if (previousChannel !== channel || xpiCrcHash !== previousHash) {
313
+ uploadUuid = await this.doUploadSubmit(xpiPath, channel);
314
+ await saveUploadUuidToFileFunc(savedUploadUuidPath, {
315
+ uploadUuid,
316
+ channel,
317
+ xpiCrcHash
318
+ });
319
+ } else {
320
+ uploadUuid = previousUuid;
321
+ }
322
+ return uploadUuid;
323
+ }
324
+ async postNewAddon(uploadUuid, savedIdPath, metaDataJson, patchData, saveIdToFileFunc = saveIdToFile) {
218
325
  const {
219
326
  guid: addonId,
220
- [versionObject]: {
221
- id: newVersionId
327
+ version: {
328
+ id: newVersionId,
329
+ edit_url: editUrl
222
330
  }
223
331
  } = await this.doNewAddonSubmit(uploadUuid, metaDataJson);
224
332
  await saveIdToFileFunc(savedIdPath, addonId);
225
333
  log.info(`Generated extension ID: ${addonId}.`);
226
334
  log.info('You must add the following to your manifest:');
227
335
  log.info(`"browser_specific_settings": {"gecko": {"id": "${addonId}"}}`);
228
- const fileUrl = new URL(await this.waitForApproval(addonId, newVersionId));
229
- return this.downloadSignedFile(fileUrl, addonId);
336
+ return this.doAfterSubmit(addonId, newVersionId, editUrl, patchData);
230
337
  }
231
- async putVersion(xpiPath, channel, addonId, metaDataJson) {
232
- const uploadUuid = await this.doUploadSubmit(xpiPath, channel);
233
- await this.doNewAddonOrVersionSubmit(addonId, uploadUuid, metaDataJson);
234
- const url = new URL(`addon/${addonId}/versions/?filter=all_with_unlisted`, this.apiUrl);
338
+ async putVersion(uploadUuid, addonId, metaDataJson, patchData) {
235
339
  const {
236
- results: [{
237
- id: newVersionId
238
- }]
239
- } = await this.fetchJson(url);
240
- const fileUrl = new URL(await this.waitForApproval(addonId, newVersionId));
241
- return this.downloadSignedFile(fileUrl, addonId);
340
+ version: {
341
+ id: newVersionId,
342
+ edit_url: editUrl
343
+ }
344
+ } = await this.doNewAddonOrVersionSubmit(addonId, uploadUuid, metaDataJson);
345
+ return this.doAfterSubmit(addonId, newVersionId, editUrl, patchData);
242
346
  }
243
347
  }
244
348
  export async function signAddon({
245
349
  apiKey,
246
350
  apiSecret,
351
+ apiProxy,
247
352
  amoBaseUrl,
248
- timeout,
353
+ validationCheckTimeout,
354
+ approvalCheckTimeout,
249
355
  id,
250
356
  xpiPath,
251
357
  downloadDir,
252
358
  channel,
253
359
  savedIdPath,
360
+ savedUploadUuidPath,
254
361
  metaDataJson = {},
362
+ submissionSource,
255
363
  userAgentString,
256
364
  SubmitClient = Client,
257
365
  ApiAuthClass = JwtApiAuth
@@ -259,7 +367,7 @@ export async function signAddon({
259
367
  try {
260
368
  const stats = await fsPromises.stat(xpiPath);
261
369
  if (!stats.isFile()) {
262
- throw new Error(`not a file: ${xpiPath}`);
370
+ throw new Error('not a file');
263
371
  }
264
372
  } catch (statError) {
265
373
  throw new Error(`error with ${xpiPath}: ${statError}`);
@@ -275,22 +383,78 @@ export async function signAddon({
275
383
  apiKey,
276
384
  apiSecret
277
385
  }),
386
+ apiProxy,
278
387
  baseUrl,
279
- validationCheckTimeout: timeout,
280
- approvalCheckTimeout: timeout,
388
+ validationCheckTimeout,
389
+ approvalCheckTimeout,
281
390
  downloadDir,
282
391
  userAgentString
283
392
  });
393
+ const uploadUuid = await client.getPreviousUuidOrUploadXpi(xpiPath, channel, savedUploadUuidPath);
394
+ const patchData = {};
395
+ // if we have a source file we need to upload we patch after the create
396
+ if (submissionSource) {
397
+ try {
398
+ const stats2 = await fsPromises.stat(submissionSource);
399
+ if (!stats2.isFile()) {
400
+ throw new Error('not a file');
401
+ }
402
+ } catch (statError) {
403
+ throw new Error(`error with ${submissionSource}: ${statError}`);
404
+ }
405
+ patchData.version = {
406
+ source: client.fileFromSync(submissionSource)
407
+ };
408
+ }
284
409
 
285
410
  // We specifically need to know if `id` has not been passed as a parameter because
286
411
  // it's the indication that a new add-on should be created, rather than a new version.
287
412
  if (id === undefined) {
288
- return client.postNewAddon(xpiPath, channel, savedIdPath, metaDataJson);
413
+ return client.postNewAddon(uploadUuid, savedIdPath, metaDataJson, patchData);
289
414
  }
290
- return client.putVersion(xpiPath, channel, id, metaDataJson);
415
+ return client.putVersion(uploadUuid, id, metaDataJson, patchData);
291
416
  }
292
417
  export async function saveIdToFile(filePath, id) {
293
418
  await fsPromises.writeFile(filePath, ['# This file was created by https://github.com/mozilla/web-ext', '# Your auto-generated extension ID for addons.mozilla.org is:', id.toString()].join('\n'));
294
419
  log.debug(`Saved auto-generated ID ${id} to ${filePath}`);
295
420
  }
421
+ export async function saveUploadUuidToFile(filePath, {
422
+ uploadUuid,
423
+ channel,
424
+ xpiCrcHash
425
+ }) {
426
+ await fsPromises.writeFile(filePath, JSON.stringify({
427
+ uploadUuid,
428
+ channel,
429
+ xpiCrcHash
430
+ }));
431
+ log.debug(`Saved upload UUID ${uploadUuid}, xpi crc hash ${xpiCrcHash}, and channel ${channel} to ${filePath}`);
432
+ }
433
+ export async function getUploadUuidFromFile(filePath, asyncFsReadFile = defaultAsyncFsReadFile) {
434
+ try {
435
+ const content = await asyncFsReadFile(filePath, 'utf-8');
436
+ const {
437
+ uploadUuid,
438
+ channel,
439
+ xpiCrcHash
440
+ } = JSON.parse(content);
441
+ log.debug(`Found upload uuid:${uploadUuid}, channel:${channel}, hash:${xpiCrcHash} in ${filePath}`);
442
+ return {
443
+ uploadUuid,
444
+ channel,
445
+ xpiCrcHash
446
+ };
447
+ } catch (error) {
448
+ if (isErrorWithCode('ENOENT', error)) {
449
+ log.debug(`No upload uuid file found at: ${filePath}`);
450
+ } else {
451
+ log.debug(`Invalid upload uuid file contents in ${filePath}: ${error}`);
452
+ }
453
+ }
454
+ return {
455
+ uploadUuid: '',
456
+ channel: '',
457
+ xpiCrcHash: ''
458
+ };
459
+ }
296
460
  //# sourceMappingURL=submit-addon.js.map