Repository: web-push-libs/web-push (JavaScript)

Issue:
Cannot read property 'generateRequestDetails' of undefined on TypeScript version greater than 4.0.
Got problem with web-push package using with typescript version greater than 4.0 but it works fine on typescript versions lesser than 4.0.
Environment info:
-   NodeJs Version: 14.17.6
-   Yarn Version: 1.22.11
-   web-push package version: 3.4.5

tsconfig.json:

> {
    "compilerOptions": {
        "outDir": "../out",
        "module": "commonjs",
        "target": "esnext",
        "moduleResolution": "node",
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "sourceMap": true,
        "skipLibCheck": true,
        "declaration": false,
        "resolveJsonModule": true,
        "lib": [
            "dom",
            "esnext",
            "esnext.asynciterable"
        ]
    },
    "include": [
        "./**/*"
    ]
}


Error Message:
`TypeError: Cannot read property 'generateRequestDetails' of undefined`


Discussion:
I have the same issue.
I also encounter this error with Typescript 4.3.5
This library is exposing a singleton (instance object of a class), therefore it is not compatible with named import.

This doesn't work:
```typescript 
import { sendNotification } from 'web-push'

sendNotification(…)
```

But this works:
```typescript
import * as webPush from 'web-push'

webPush.sendNotification(…)
```

We may include this importing guideline in the readme to help future users.
> We may include this importing guideline in the readme to help future users.

Sounds good to me, PRs welcome ;)
I'd also be OK with a PR to somehow make named import feasible.



Patch:
diff --git a/src/index.js b/src/index.js
index d3cd06b7..f7b36741 100644
--- a/src/index.js
+++ b/src/index.js
@@ -17,5 +17,5 @@ module.exports = {
   setGCMAPIKey: webPush.setGCMAPIKey,
   setVapidDetails: webPush.setVapidDetails,
   generateRequestDetails: webPush.generateRequestDetails,
-  sendNotification: webPush.sendNotification
+  sendNotification: webPush.sendNotification.bind(webPush)
 };


Test patch:
diff --git a/test/test-generate-request-details.js b/test/test-generate-request-details.js
index d4501e25..007de665 100644
--- a/test/test-generate-request-details.js
+++ b/test/test-generate-request-details.js
@@ -1,7 +1,7 @@
 'use strict';
 
 const assert = require('assert');
-const webPush = require('../src/index');
+const { generateRequestDetails } = require('../src/index');
 const crypto = require('crypto');
 const jws = require('jws');
 const urlParse = require('url').parse;
@@ -9,7 +9,7 @@ const https = require('https');
 
 suite('Test Generate Request Details', function() {
   test('is defined', function() {
-    assert(webPush.generateRequestDetails);
+    assert(generateRequestDetails);
   });
 
   const userCurve = crypto.createECDH('prime256v1');
@@ -241,7 +241,7 @@ suite('Test Generate Request Details', function() {
       }
 
       assert.throws(function() {
-        return webPush.generateRequestDetails(
+        return generateRequestDetails(
           invalidRequest.requestOptions.subscription,
           invalidRequest.requestOptions.message,
           invalidRequest.requestOptions.extraOptions
@@ -260,7 +260,7 @@ suite('Test Generate Request Details', function() {
         'Urgency': 'normal'
       }
     };
-    let details = webPush.generateRequestDetails(
+    let details = generateRequestDetails(
       subscription,
       message,
       extraOptions
@@ -283,7 +283,7 @@ suite('Test Generate Request Details', function() {
       }
     };
 
-    const requestDetails = webPush.generateRequestDetails(subscription, null, extraOptions);
+    const requestDetails = generateRequestDetails(subscription, null, extraOptions);
     const authHeader = requestDetails.headers.Authorization;
 
     // Get the Encoded JWT Token from the Authorization Header
@@ -311,7 +311,7 @@ suite('Test Generate Request Details', function() {
       contentEncoding: 'aesgcm'
     };
 
-    const requestDetails = webPush.generateRequestDetails(subscription, null, extraOptions);
+    const requestDetails = generateRequestDetails(subscription, null, extraOptions);
     const authHeader = requestDetails.headers.Authorization;
 
     // Get the Encoded JWT Token from the Authorization Header
@@ -331,7 +331,7 @@ suite('Test Generate Request Details', function() {
     let extraOptions = {
       'proxy': 'proxy'
     };
-    let details = webPush.generateRequestDetails(
+    let details = generateRequestDetails(
       subscription,
       message,
       extraOptions
@@ -347,7 +347,7 @@ suite('Test Generate Request Details', function() {
     let extraOptions = {
       proxy: proxyOption
     };
-    let details = webPush.generateRequestDetails(
+    let details = generateRequestDetails(
       subscription,
       null,
       extraOptions
@@ -362,7 +362,7 @@ suite('Test Generate Request Details', function() {
     let extraOptions = {
       agent: new https.Agent({ keepAlive: true })
     };
-    let details = webPush.generateRequestDetails(
+    let details = generateRequestDetails(
       subscription,
       null,
       extraOptions
diff --git a/test/test-set-vapid-details.js b/test/test-set-vapid-details.js
index fe9b3ac2..657017d8 100644
--- a/test/test-set-vapid-details.js
+++ b/test/test-set-vapid-details.js
@@ -1,7 +1,7 @@
 'use strict';
 
 const assert = require('assert');
-const webPush = require('../src/index');
+const { setVapidDetails } = require('../src/index');
 
 const VALID_SUBJECT_MAILTO = 'mailto: example@example.com';
 const VALID_SUBJECT_URL = 'https://exampe.com/contact';
@@ -10,24 +10,24 @@ const VALID_PRIVATE_KEY = Buffer.alloc(32).toString('base64url');
 
 suite('setVapidDetails()', function() {
   test('is defined', function() {
-    assert(webPush.setVapidDetails);
+    assert(setVapidDetails);
   });
 
   test('Valid URL input', function() {
     assert.doesNotThrow(function() {
-      webPush.setVapidDetails(VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY);
+      setVapidDetails(VALID_SUBJECT_URL, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY);
     });
   });
 
   test('Valid mailto: input', function() {
     assert.doesNotThrow(function() {
-      webPush.setVapidDetails(VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY);
+      setVapidDetails(VALID_SUBJECT_MAILTO, VALID_PUBLIC_KEY, VALID_PRIVATE_KEY);
     });
   });
 
   test('reset Vapid Details with null', function() {
     assert.doesNotThrow(function() {
-      webPush.setVapidDetails(null);
+      setVapidDetails(null);
     });
   });
 
@@ -107,7 +107,7 @@ suite('setVapidDetails()', function() {
   test('Invalid input should throw an error', function() {
     invalidInputs.forEach(function(invalidInput, index) {
       assert.throws(function() {
-        webPush.setVapidDetails(
+        setVapidDetails(
           invalidInput.subject,
           invalidInput.publicKey,
           invalidInput.privateKey
diff --git a/test/testSendNotification.js b/test/testSendNotification.js
index 484c0aa5..429235c2 100644
--- a/test/testSendNotification.js
+++ b/test/testSendNotification.js
@@ -12,9 +12,16 @@ const mocha = require('mocha');
 const WebPushConstants = require('../src/web-push-constants.js');
 
 suite('sendNotification', function() {
+  let sendNotification;
+  let setGCMAPIKey;
+  let setVapidDetails;
+
+  mocha.beforeEach(function () {
+    ({ sendNotification, setGCMAPIKey, setVapidDetails } = require('../src/index'));
+  });
+
   test('is defined', function() {
-    const webPush = require('../src/index');
-    assert(webPush.sendNotification);
+    assert(sendNotification);
   });
 
   let server;
@@ -364,8 +371,7 @@ suite('sendNotification', function() {
       validRequest.requestOptions.extraOptions = validRequest.requestOptions.extraOptions || {};
       validRequest.requestOptions.extraOptions.contentEncoding = WebPushConstants.supportedContentEncodings.AES_GCM;
 
-      const webPush = require('../src/index');
-      return webPush.sendNotification(
+      return sendNotification(
         validRequest.requestOptions.subscription,
         validRequest.requestOptions.message,
         validRequest.requestOptions.extraOptions
@@ -392,8 +398,7 @@ suite('sendNotification', function() {
       validRequest.requestOptions.extraOptions = validRequest.requestOptions.extraOptions || {};
       validRequest.requestOptions.extraOptions.contentEncoding = WebPushConstants.supportedContentEncodings.AES_128_GCM;
 
-      const webPush = require('../src/index');
-      return webPush.sendNotification(
+      return sendNotification(
         validRequest.requestOptions.subscription,
         validRequest.requestOptions.message,
         validRequest.requestOptions.extraOptions
@@ -582,19 +587,18 @@ suite('sendNotification', function() {
         validGCMRequest.globalOptions.gcmAPIKey = 'my_gcm_key';
       }
 
-      const webPush = require('../src/index');
       if (validGCMRequest.globalOptions.gcmAPIKey) {
-        webPush.setGCMAPIKey(validGCMRequest.globalOptions.gcmAPIKey);
+        setGCMAPIKey(validGCMRequest.globalOptions.gcmAPIKey);
       }
       if (validGCMRequest.globalOptions.vapidDetails) {
-        webPush.setVapidDetails(
+        setVapidDetails(
           validGCMRequest.globalOptions.vapidDetails.subject,
           validGCMRequest.globalOptions.vapidDetails.publicKey,
           validGCMRequest.globalOptions.vapidDetails.privateKey
         );
       }
 
-      return webPush.sendNotification(
+      return sendNotification(
         validGCMRequest.requestOptions.subscription,
         validGCMRequest.requestOptions.message,
         validGCMRequest.requestOptions.extraOptions
@@ -799,8 +803,7 @@ suite('sendNotification', function() {
       invalidRequest.requestOptions.extraOptions = invalidRequest.requestOptions.extraOptions || {};
       invalidRequest.requestOptions.extraOptions.contentEncoding = WebPushConstants.supportedContentEncodings.AES_GCM;
 
-      const webPush = require('../src/index');
-      return webPush.sendNotification(
+      return sendNotification(
         invalidRequest.requestOptions.subscription,
         invalidRequest.requestOptions.message,
         invalidRequest.requestOptions.extraOptions
@@ -825,8 +828,7 @@ suite('sendNotification', function() {
       invalidRequest.requestOptions.extraOptions = invalidRequest.requestOptions.extraOptions || {};
       invalidRequest.requestOptions.extraOptions.contentEncoding = WebPushConstants.supportedContentEncodings.AES_128_GCM;
 
-      const webPush = require('../src/index');
-      return webPush.sendNotification(
+      return sendNotification(
         invalidRequest.requestOptions.subscription,
         invalidRequest.requestOptions.message,
         invalidRequest.requestOptions.extraOptions
@@ -843,8 +845,7 @@ suite('sendNotification', function() {
     const currentServerPort = serverPort;
     return closeServer()
     .then(function() {
-      const webPush = require('../src/index');
-      return webPush.sendNotification({
+      return sendNotification({
         endpoint: 'https://127.0.0.1:' + currentServerPort
       })
       .then(function() {
diff --git a/test/testSetGCMAPIKey.js b/test/testSetGCMAPIKey.js
index f5d5e260..30fb1e52 100644
--- a/test/testSetGCMAPIKey.js
+++ b/test/testSetGCMAPIKey.js
@@ -1,40 +1,40 @@
 'use strict';
 
 const assert = require('assert');
-const webPush = require('../src/index');
+const { setGCMAPIKey } = require('../src/index');
 
 suite('setGCMAPIKey', function() {
   test('is defined', function() {
-    assert(webPush.setGCMAPIKey);
+    assert(setGCMAPIKey);
   });
 
   test('non-empty string', function() {
     assert.doesNotThrow(function() {
-      webPush.setGCMAPIKey('AIzaSyAwmdX6KKd4hPfIcGU2SOfj9vuRDW6u-wo');
+      setGCMAPIKey('AIzaSyAwmdX6KKd4hPfIcGU2SOfj9vuRDW6u-wo');
     });
   });
 
   test('reset GCM API Key with null', function() {
     assert.doesNotThrow(function() {
-      webPush.setGCMAPIKey(null);
+      setGCMAPIKey(null);
     });
   });
 
   test('empty string', function() {
     assert.throws(function() {
-      webPush.setGCMAPIKey('');
+      setGCMAPIKey('');
     }, Error);
   });
 
   test('non string', function() {
     assert.throws(function() {
-      webPush.setGCMAPIKey(42);
+      setGCMAPIKey(42);
     }, Error);
   });
 
   test('undefined value', function() {
     assert.throws(function() {
-      webPush.setGCMAPIKey();
+      setGCMAPIKey();
     }, Error);
   });
 });